From 754af4134ad424139e1ee1fbf24b35f7f13bb14b Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Tue, 23 Jun 2026 20:08:54 -0400 Subject: [PATCH] v1.3.0: Add 14 keyfob protocols (8 ProtoPirate + 6 Flipper-ARF), fix KeeLoq overflow Brings KAT to 32 protocol decoders. Ported from the ProtoPirate reference and the D4C1-Labs/Flipper-ARF firmware: ProtoPirate (8): Kia V7, Ford V1, Ford V2, Ford V3, Chrysler V0, Honda Static, Honda V1, Land Rover V0. Flipper-ARF (6): Toyota, Land Rover RKE, Mazda Siemens, BMW CAS4, Porsche Cayenne, PSA2. Each decoder is gated (CRC / checksum / fixed markers / frame structure) so it cannot false-match existing protocols; every previously-decoding IMPORTS capture decodes unchanged. Real-capture decodes added for Ford V3 (LDV T80), Honda Static (Honda), Toyota (Camry NRZ variant), and PSA2 (Groupe PSA, serial 0x99EB25); the rest are validated by encode/decode round-trip and synthetic-frame tests. Also fixes a debug-only underflow panic in keeloq_common::keeloq_decrypt (15 - r underflowed; now wrapping_sub to match the reference's unsigned wrap; release behavior unchanged). Adds per-protocol docs, updates the README protocol table, capture metadata (encoding / RF / encryption), make-suggestion mapping, and CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JEfaKqzB3T4qsrybwfEi73 --- CHANGELOG.md | 18 + Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 16 +- docs/README.md | 14 + docs/bmw_cas4.md | 64 +++ docs/chrysler_v0.md | 66 +++ docs/ford_v1.md | 68 +++ docs/ford_v2.md | 62 +++ docs/ford_v3.md | 55 ++ docs/honda_static.md | 65 +++ docs/honda_v1.md | 63 +++ docs/kia_v7.md | 55 ++ docs/land_rover_rke.md | 71 +++ docs/land_rover_v0.md | 69 +++ docs/mazda_siemens.md | 70 +++ docs/porsche_cayenne.md | 67 +++ docs/psa2.md | 76 +++ docs/toyota.md | 69 +++ src/app.rs | 12 +- src/capture.rs | 41 +- src/protocols/bmw_cas4.rs | 461 ++++++++++++++++ src/protocols/chrysler_v0.rs | 604 ++++++++++++++++++++ src/protocols/ford_v1.rs | 778 ++++++++++++++++++++++++++ src/protocols/ford_v2.rs | 413 ++++++++++++++ src/protocols/ford_v3.rs | 258 +++++++++ src/protocols/honda_static.rs | 558 +++++++++++++++++++ src/protocols/honda_v1.rs | 605 ++++++++++++++++++++ src/protocols/keeloq_common.rs | 4 +- src/protocols/kia_v7.rs | 314 +++++++++++ src/protocols/land_rover_rke.rs | 531 ++++++++++++++++++ src/protocols/land_rover_v0.rs | 696 +++++++++++++++++++++++ src/protocols/mazda_siemens.rs | 525 ++++++++++++++++++ src/protocols/mod.rs | 281 ++++++++++ src/protocols/porsche_cayenne.rs | 613 +++++++++++++++++++++ src/protocols/psa2.rs | 919 +++++++++++++++++++++++++++++++ src/protocols/toyota.rs | 579 +++++++++++++++++++ 37 files changed, 9157 insertions(+), 7 deletions(-) create mode 100644 docs/bmw_cas4.md create mode 100644 docs/chrysler_v0.md create mode 100644 docs/ford_v1.md create mode 100644 docs/ford_v2.md create mode 100644 docs/ford_v3.md create mode 100644 docs/honda_static.md create mode 100644 docs/honda_v1.md create mode 100644 docs/kia_v7.md create mode 100644 docs/land_rover_rke.md create mode 100644 docs/land_rover_v0.md create mode 100644 docs/mazda_siemens.md create mode 100644 docs/porsche_cayenne.md create mode 100644 docs/psa2.md create mode 100644 docs/toyota.md create mode 100644 src/protocols/bmw_cas4.rs create mode 100644 src/protocols/chrysler_v0.rs create mode 100644 src/protocols/ford_v1.rs create mode 100644 src/protocols/ford_v2.rs create mode 100644 src/protocols/ford_v3.rs create mode 100644 src/protocols/honda_static.rs create mode 100644 src/protocols/honda_v1.rs create mode 100644 src/protocols/kia_v7.rs create mode 100644 src/protocols/land_rover_rke.rs create mode 100644 src/protocols/land_rover_v0.rs create mode 100644 src/protocols/mazda_siemens.rs create mode 100644 src/protocols/porsche_cayenne.rs create mode 100644 src/protocols/psa2.rs create mode 100644 src/protocols/toyota.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c58e8d6..85127ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to KAT are documented here. +## [1.3.0] - 2026-06-23 + +### Added + +- **14 new protocol decoders** (total now 32), ported from the [ProtoPirate](https://protopirate.net/ProtoPirate/ProtoPirate) reference and the [Flipper-ARF](https://github.com/D4C1-Labs/Flipper-ARF) firmware: + - **ProtoPirate (8):** **Kia V7** (Manchester 250/500, header 0x4C + CRC8), **Ford V1** (Manchester 65/130, descramble + CRC16/CCITT), **Ford V2** (Manchester 200/400, 0x7FA7 sync), **Ford V3** (Manchester 240/480, plaintext, decode-only), **Chrysler V0** (PWM, seed-XOR transform; Dodge/Jeep), **Honda Static** (FM, XOR checksum), **Honda V1** (PWM/PPM, CRC-fold), **Land Rover V0** (differential Manchester, 3-bit check polynomial). + - **Flipper-ARF (6):** **Toyota** (dual PWM + NRZ variants, KeeLoq, decode-only; Lexus), **Land Rover RKE** (PWM, KeeLoq), **Mazda Siemens** (Siemens XOR/interleave), **BMW CAS4** (Manchester, 0x30/0xC5 markers, decode-only), **Porsche Cayenne** (PWM, VAG rolling-register cipher; adds an encoder to the shared Touareg air protocol), **PSA2 / "PSA OLD"** (Manchester, TEA with bounded brute-force gated behind a structural/checksum check). +- **Real-capture validation** — decoders verified against `IMPORTS/` samples where they exist: **Ford V3** (LDV T80), **Honda Static** (Honda), **Toyota** (Camry NRZ variant), **PSA2** (Groupe PSA, recovered serial 0x99EB25 + rolling counter). The rest are validated by in-module encode→decode round-trip and synthetic-frame unit tests. +- **Metadata, make-suggestion, and docs** — each new protocol is tagged with Encoding / RF (AM/FM) / Encryption in the signal detail panel, mapped to a Make suggestion (Honda/Acura, Chrysler/Dodge/Jeep, Toyota/Lexus, Land Rover, BMW, Porsche, Mazda, Peugeot/Citroën), and documented under `docs/`. + +### Fixed + +- **KeeLoq decrypt debug-panic** — `keeloq_common::keeloq_decrypt` computed `15 - r` (with `r` up to 527), underflowing and panicking in debug builds / `cargo test`; now uses `wrapping_sub` to match the reference's unsigned wraparound (release behavior, which silently wrapped, is unchanged). + +### Notes + +- All new decoders are gated (CRC / checksum / fixed markers / frame structure) so they do not false-match existing protocols — every previously-decoding `IMPORTS/*.sub` capture decodes unchanged. Where two decoders share a wire protocol (Porsche Cayenne ↔ Touareg; Mazda Siemens ↔ Mazda V0; Toyota variant-A ↔ Kia V3/V4 KeeLoq), the pre-existing decoder keeps first-match priority. + ## [1.1.3] - 2026-02-20 ### Added diff --git a/Cargo.lock b/Cargo.lock index 215bd82..0f0bf3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,7 +444,7 @@ dependencies = [ [[package]] name = "kat" -version = "1.2.0" +version = "1.3.0" dependencies = [ "anyhow", "atty", diff --git a/Cargo.toml b/Cargo.toml index 68590d5..e677e2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kat" -version = "1.2.0" +version = "1.3.0" edition = "2021" description = "Keyfob Analysis Toolkit - HackRF/RTL-SDR signal capture, decode, and transmit (HackRF only)" authors = ["KAT Team"] diff --git a/README.md b/README.md index a4efea3..225c7b3 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A terminal-based RF signal analysis tool for capturing, decoding, and retransmit ## Features - **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies (HackRF uses AM envelope detection; FM protocols are tagged for display and may decode when signal is strong) -- **Multi-protocol decoding** — 18 protocol decoders: Kia V0–V6, Ford V0, Fiat V0/V1, Mazda V0, Mitsubishi V0, Porsche Touareg, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions +- **Multi-protocol decoding** — 32 protocol decoders: Kia V0–V7, Ford V0–V3, Chrysler V0 (Dodge/Jeep), Honda Static, Honda V1, Toyota (Lexus), Land Rover V0, Land Rover RKE, Fiat V0/V1, Mazda V0, Mazda Siemens, Mitsubishi V0, BMW CAS4, Porsche Touareg, Porsche Cayenne, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, PSA2, Scher-Khan, Star Line; adaptive demodulation for real-world conditions - **KeeLoq generic fallback** — when a signal doesn’t match any known protocol, KAT tries decoding it as KeeLoq using every manufacturer key in the embedded keystore (Kia V3/V4 and Star Line air formats); successful decodes appear as **Keeloq (keystore name)** in the capture list - **RF modulation metadata** — each protocol tagged as AM, FM, or both (from ProtoPirate); shown in signal detail and exported in .fob - **Rich signal detail** — encoding (PWM/Manchester), RF (AM/FM), encryption, serial, counter, key data, CRC, frequency, and raw level/duration pairs @@ -234,18 +234,32 @@ Protocol behavior and RF modulation (AM/FM) follow the ProtoPirate reference. KA | Kia V3/V4 | PWM | AM/FM | KeeLoq | 315 / 433.92 MHz | | Kia V5 | Manchester | FM | Fixed Code | 433.92 MHz | | Kia V6 | Manchester | FM | AES-128 | 433.92 MHz | +| Kia V7 | Manchester | FM | Fixed Code (CRC8) | 315 / 433.92 MHz | | Ford V0 | Manchester | FM | Rolling Code | 315 / 433.92 MHz | +| Ford V1 | Manchester | FM | Rolling Code (descramble + CRC16) | 315 / 433.92 MHz | +| Ford V2 | Manchester | FM | Fixed Code (0x7FA7 sync) | 315 / 433.92 MHz | +| Ford V3 | Manchester | FM | Fixed Code (decode-only) | 315 / 433.92 MHz | +| Chrysler V0 (Dodge/Jeep) | PWM | AM | Rolling Code (seed-XOR) | 315 / 433.92 MHz | +| Honda Static | Manchester | FM | Fixed Code | 315 / 433.92 MHz | +| Honda V1 | PWM | AM | Fixed Code | 315 / 433.92 MHz | +| Toyota (Lexus) | PWM / NRZ | AM | KeeLoq (decode-only) | 315 / 433.92 MHz | +| Land Rover V0 | Diff. Manchester | FM | Rolling Code (3-bit check) | 315 / 433.92 MHz | +| Land Rover RKE | PWM | AM | KeeLoq | 315 / 433.92 MHz | | Fiat V0 | Manchester | FM | Fixed Code | 433.92 MHz | | Fiat V1 (Magneti Marelli) | Manchester | FM | Rolling Code | 433.92 MHz | | Mazda V0 | Pair-based | FM | XOR Deobfuscation | 433.92 MHz | +| Mazda Siemens | Manchester | FM | Siemens XOR/interleave | 433.92 MHz | | Mitsubishi V0 | PWM | FM | Bit Negation + XOR | 868.35 MHz | +| BMW CAS4 | Manchester | AM | CAS4 rolling (decode-only) | 433.92 MHz | | Porsche Touareg | PWM | AM | Rotation Cipher | 433.92 / 868.35 MHz | +| Porsche Cayenne | PWM | AM | VAG rolling register | 433.92 / 868.35 MHz | | Subaru | PWM | AM | Rolling Code | 433.92 MHz | | Suzuki | PWM | AM | Rolling Code | 433.92 MHz | | VAG (VW/Audi/Seat/Skoda) | Manchester | AM | AUT64/XTEA | 433.92 / 434.42 MHz | | Scher-Khan | PWM | FM | Magic Code | 433.92 MHz | | Star Line | PWM | AM | KeeLoq | 433.92 MHz | | PSA (Peugeot/Citroën) | Manchester | FM | Modified TEA/XOR | 433.92 MHz | +| PSA2 (PSA OLD) | Manchester | AM | TEA | 433.92 MHz | **KeeLoq generic fallback:** If no protocol decodes a capture, KAT tries KeeLoq with every keystore manufacturer key (Kia V3/V4 and Star Line bit layouts). On success the protocol is shown as **Keeloq (*keystore name*)** (e.g. Keeloq (Alligator), Keeloq (Pandora_PRO)). See [docs/keeloq_generic.md](docs/keeloq_generic.md). diff --git a/docs/README.md b/docs/README.md index b5f35bd..5a26ff6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,18 +12,32 @@ This folder describes how each keyfob protocol supported by KAT works. Each docu | Kia V3/V4 | `kia_v3_v4.rs` | [kia_v3_v4.md](kia_v3_v4.md) | | Kia V5 | `kia_v5.rs` | [kia_v5.md](kia_v5.md) | | Kia V6 | `kia_v6.rs` | [kia_v6.md](kia_v6.md) | +| Kia V7 | `kia_v7.rs` | [kia_v7.md](kia_v7.md) | | Ford V0 | `ford_v0.rs` | [ford_v0.md](ford_v0.md) | +| Ford V1 | `ford_v1.rs` | [ford_v1.md](ford_v1.md) | +| Ford V2 | `ford_v2.rs` | [ford_v2.md](ford_v2.md) | +| Ford V3 | `ford_v3.rs` | [ford_v3.md](ford_v3.md) | +| Honda Static | `honda_static.rs` | [honda_static.md](honda_static.md) | +| Honda V1 | `honda_v1.rs` | [honda_v1.md](honda_v1.md) | +| Chrysler V0 | `chrysler_v0.rs` | [chrysler_v0.md](chrysler_v0.md) | | Subaru | `subaru.rs` | [subaru.md](subaru.md) | +| Toyota | `toyota.rs` | [toyota.md](toyota.md) | | VAG | `vag.rs` | [vag.md](vag.md) | | Fiat V0 | `fiat_v0.rs` | [fiat_v0.md](fiat_v0.md) | | Fiat V1 | `fiat_v1.rs` | [fiat_v1.md](fiat_v1.md) | | Mazda V0 | `mazda_v0.rs` | [mazda_v0.md](mazda_v0.md) | +| Mazda Siemens | `mazda_siemens.rs` | [mazda_siemens.md](mazda_siemens.md) | | Mitsubishi V0 | `mitsubishi_v0.rs` | [mitsubishi_v0.md](mitsubishi_v0.md) | +| Land Rover V0 | `land_rover_v0.rs` | [land_rover_v0.md](land_rover_v0.md) | +| Land Rover RKE | `land_rover_rke.rs` | [land_rover_rke.md](land_rover_rke.md) | +| BMW CAS4 | `bmw_cas4.rs` | [bmw_cas4.md](bmw_cas4.md) | | Porsche Touareg | `porsche_touareg.rs` | [porsche_touareg.md](porsche_touareg.md) | +| Porsche Cayenne | `porsche_cayenne.rs` | [porsche_cayenne.md](porsche_cayenne.md) | | Suzuki | `suzuki.rs` | [suzuki.md](suzuki.md) | | Scher-Khan | `scher_khan.rs` | [scher_khan.md](scher_khan.md) | | Star Line | `star_line.rs` | [star_line.md](star_line.md) | | PSA | `psa.rs` | [psa.md](psa.md) | +| PSA2 (PSA OLD) | `psa2.rs` | [psa2.md](psa2.md) | | KeeLoq generic (fallback) | `keeloq_generic.rs` | [keeloq_generic.md](keeloq_generic.md) | **KeeLoq generic** is not a registered decoder; it runs when no protocol matches and tries KeeLoq with every keystore manufacturer key (using `keeloq_common`). Successful decodes appear as **Keeloq (*keystore name*)**. diff --git a/docs/bmw_cas4.md b/docs/bmw_cas4.md new file mode 100644 index 0000000..9584629 --- /dev/null +++ b/docs/bmw_cas4.md @@ -0,0 +1,64 @@ +--- +layout: default +--- + +# BMW CAS4 Protocol + +**Rust module:** `src/protocols/bmw_cas4.rs` +**Reference:** `Flipper-ARF lib/subghz/protocols/bmw_cas4.c` + +## Overview + +BMW CAS4 uses Manchester encoding at 500/1000 µs. 64 bits (8 bytes), AM/OOK. The CAS4 rolling cipher's +manufacturer key is not available, so the encrypted portion is left as-is — the frame is only framed and +validated, not decrypted. Emission is gated on two fixed marker bytes (`byte[0] == 0x30` and +`byte[6] == 0xC5`), which makes the protocol specific and prevents false matches. + +The Manchester decoder uses Flipper's `manchester_decoder.h` transition table, with polarity +`level ? Low : High` (the same mapping as Ford V0 / common). + +## Timing + +| Parameter | Value | Notes | +|----------------|---------|-----------------------------| +| Short | 500 µs | ±150 µs (te_delta) | +| Long | 1000 µs | ±150 µs | +| Preamble pulse | 300–700 µs | | +| Gap | ≥1800 µs | separates preamble from data | +| Min bits | 64 | 8 bytes | +| Preamble | ≥10 pulses | | + +## Frame Layout (64 bits / 8 bytes) + +- **byte 0:** fixed marker `0x30` +- **bytes 1–3:** serial (24-bit) +- **byte 5:** counter +- **byte 6:** fixed marker `0xC5` +- **byte 7:** button + +The CAS4 rolling cipher is left undecrypted; the two markers serve as the integrity gate. + +## RF + +- **Encoding:** Manchester (Flipper table, polarity `level ? Low : High`) +- **RF modulation:** AM/OOK +- **Encryption:** CAS4 rolling cipher left undecrypted (no manufacturer key); gated on fixed markers `0x30`/`0xC5` +- **Frequencies:** 433.92 MHz only + +## Decoder Steps + +1. **Reset** — begin on a HIGH preamble pulse within the 300–700 µs window. +2. **Preamble** — count preamble pulses; a long LOW gap (≥1800 µs) with ≥10 pulses enters Data. +3. **Data** — Manchester-decode 64 bits MSB-first; at the 64th bit, require `byte[0]==0x30 && byte[6]==0xC5`, build the signal on success, and reset. An out-of-range pulse aborts. + +## Encoder + +Not supported (the reference encoder is a non-functional stub: `yield` returns reset, `deserialize` +returns error). Decode-only. + +## Validation + +Verified by a synthetic-frame check (no local capture available): a unit test Manchester-encodes a frame +by driving the decoder's own transition table (the Flipper table is differential, so there is no +fixed per-bit pattern), confirms it decodes with the right serial/counter/button, and confirms a frame +with wrong marker bytes is rejected. diff --git a/docs/chrysler_v0.md b/docs/chrysler_v0.md new file mode 100644 index 0000000..346ff99 --- /dev/null +++ b/docs/chrysler_v0.md @@ -0,0 +1,66 @@ +--- +layout: default +--- + +# Chrysler V0 Protocol + +**Rust module:** `src/protocols/chrysler_v0.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/chrysler_v0.c` + +## Overview + +Chrysler/Dodge/Jeep keyfobs. PWM with a short HIGH pulse and two long-LOW symbols: a "1" payload bit is +HIGH ≈600 µs + LOW ≈3400 µs; a "0" payload bit is HIGH ≈300 µs + LOW ≈3700 µs. ~24 preamble pairs +(short HIGH + long_b LOW) precede each frame. 80-bit frame: the first 64 bits are `decode_data` (payload +bytes 0–7), the last 16 bits are `data_2` (payload bytes 8,9). The 80-bit frame exceeds u64, so `data` +reports the most-significant 64 bits and `data_count_bit = 80`. + +Crypto is a proprietary seed-XOR (ported exactly from the reference): `seed = reverse6(key[0] >> 2)` (a +6-bit reversed counter); `transform_block` XORs all 9 transformed bytes with `xor_table[seed & 0x0F]`, +with an extra nibble flip when the Lock button is set. Dual payload: A (seed even, carries serial + +counter) / B (seed odd, carries serial). The frame is gated on a structural `check_ok`, so it does not +false-match; `crc_valid` reflects that check. + +## Timing + +| Parameter | Value | Notes | +|-------------|---------|------------------------| +| Short HIGH | 300 µs | ±150 µs (te_delta) | +| One-short HIGH | 600 µs | "1" bit HIGH width | +| Long LOW A | 3400 µs | ±400 µs (long_delta) | +| Long LOW B | 3700 µs | ±400 µs | +| Gap | 8000 µs | te_gap | +| Frame gap | 15600 µs| | +| Min bits | 80 | | +| Preamble | 24 pairs| | + +## Frame Layout (80 bits / 10 payload bytes) + +- **byte 0:** `(reverse6(counter) << 2) | header_low2` — the seed/counter byte +- **bytes 1–9:** transformed (XOR-masked) payload; after `transform_block`: + - Payload A (even seed): serial = decoded[0..3], rolling counter + - Payload B (odd seed): serial = decoded[0..2] + decoded[7] +- Button: Lock=1, Unlock=2 (derived from structural relationships between key bytes) + +## RF + +- **Encoding:** PWM (short HIGH + dual long LOW) +- **RF modulation:** AM +- **Encryption:** proprietary seed-XOR (`transform_block` + `xor_table`); gated on structural `check_ok` +- **Frequencies:** 315 MHz, 433.92 MHz + +## Decoder Steps + +1. **Reset** — a short HIGH pulse begins the seek. +2. **Seek** — count preamble pairs (short HIGH + long LOW); after >15 pairs, a non-short HIGH transitions to Data. +3. **Data** — classify each HIGH/LOW pair into a payload bit (`bit ^ 1`); collect 64 bits into `decode_data` then 16 into `data_2`. At 80 bits (or on a terminating gap with >0x4F bits) run `decode_packet`, check `check_ok`, and emit. + +## Encoder + +Supported (ENABLE_EMULATE_FEATURE). Rebuilds the 10-byte payload, re-derives seed/plaintext, applies the +requested button, then emits preamble + dual 80-bit PWM frames (A and B) separated by frame gaps. + +## Validation + +Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests +cover `reverse6` involution, `transform_block` invertibility, and a full payload-A round trip. diff --git a/docs/ford_v1.md b/docs/ford_v1.md new file mode 100644 index 0000000..f08ba30 --- /dev/null +++ b/docs/ford_v1.md @@ -0,0 +1,68 @@ +--- +layout: default +--- + +# Ford V1 Protocol + +**Rust module:** `src/protocols/ford_v1.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v1.c` + +## Overview + +Ford V1 uses Manchester encoding at 65/130 µs. 136 bits / 17 bytes: key1 (bytes 0–6, 56 bits) + key2 +(bytes 7–14, 64 bits) + CRC16 (bytes 15–16). Preamble ≥50 long pulses, then a short-pulse sync window +(`sync_event_count > 2`) replays buffered Manchester events and enters the 17-byte data collection. This +is a ROLLING-code protocol. + +Crypto: a proprietary parity-based descrambling cipher operating on the 9-byte air block `raw[6..15]`, +plus CRC16/CCITT (poly `0x1021`, init `0x0000`) over `raw[3..15]`. Emission is gated on CRC16 validity +(with a 17-byte bit-inverted fallback) so it never false-matches. A strict branch +(`decoded[3]==raw[5] && decoded[4]==raw[6]`) yields plaintext serial/button/counter; otherwise it is +classified as encrypted/rolling (device id only). The Manchester transition table is the Flipper +differential-Manchester table (same as Ford V0/V2). + +## Timing + +| Parameter | Value | Notes | +|-------------|--------|------------------------------| +| Short | 65 µs | ±39 µs (te_delta) | +| Long | 130 µs | ±39 µs (preamble uses ±40) | +| Min bits | 136 | 17 bytes | +| Preamble | ≥50 long pulses | | + +## Frame Layout (136 bits / 17 bytes) + +- **bytes 0–6:** key1 (56 bits, big-endian) → `DecodedSignal.data` +- **bytes 6–14:** air block (9 bytes) — descrambled to plaintext +- **bytes 15–16:** CRC16/CCITT over bytes 3–14 + +Plaintext fields (when the strict branch matches): serial = `plain[1..3] + plain[0]`, button = `plain[5]>>4` +(Sync=0, Lock=1, Unlock=2, Trunk=4, Panic=8), counter = `((plain[5] & 0x0F) << 16) | plain[6..7]`. The +`extra` word stashes the CRC16, the strict flag, and `plain[4]` so the encoder can rebuild the full frame. + +## RF + +- **Encoding:** Manchester (Flipper differential table) +- **RF modulation:** FM +- **Encryption:** proprietary parity descramble cipher + CRC16/CCITT gate; rolling code +- **Frequencies:** 315 MHz, 433.92 MHz + +## Decoder Steps + +1. **Reset** — a long LOW pulse begins the preamble (count = 1). +2. **Preamble** — count long pulses; after ≥50, a short pulse enters Sync. +3. **Sync** — buffer short/long events; once `sync_event_count > 2`, replay the buffered events into Manchester and enter Data. +4. **Data** — Manchester-decode and pack 17 bytes; at the 17th byte run `process_data` (CRC16 gate + descramble + field extraction) and emit on a valid CRC. + +## Encoder + +Supported (6 bursts). Faithful re-encode is only possible when the original frame's plaintext was +recovered (strict branch); the encoder rebuilds the plaintext from fields + the stashed `plain[4]`, +re-derives the air block + CRC16, and emits 6 bursts of a 400-pair long preamble + sync + 136 Manchester +bits, with a per-burst `pkt[4]` override. + +## Validation + +Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests +cover a CRC16/XMODEM known vector, the descramble round trip, the strict-branch decode, and a full +encode→decode at the on-air-frame level. diff --git a/docs/ford_v2.md b/docs/ford_v2.md new file mode 100644 index 0000000..45e4a53 --- /dev/null +++ b/docs/ford_v2.md @@ -0,0 +1,62 @@ +--- +layout: default +--- + +# Ford V2 Protocol + +**Rust module:** `src/protocols/ford_v2.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v2.c` + +## Overview + +Ford V2 uses Manchester encoding at 200/400 µs. 104 bits total (13 bytes). The frame begins with a +16-bit Manchester sync that equals `0x7FA7` (the decoder matches the *inverted* shift register against +`!0x7FA7`); those two sync bytes `0x7F 0xA7` head the 13-byte buffer. Decoded data bits are inverted +before packing (`data_bit = !bit`). There is no CRC — structure is validated by the two sync bytes plus +a known button code, so Ford V2 will not false-match. + +The Manchester decoder uses Flipper's transition table (same table as Ford V0), with Ford V2 polarity +`level ? High : Low`. + +## Timing + +| Parameter | Value | Notes | +|-----------|--------|-----------------------------| +| Short | 200 µs | ±260 µs (te_delta) | +| Long | 400 µs | ±260 µs | +| Min bits | 104 | 13 bytes | +| Preamble | ≥64 short pulses | before the sync | + +## Frame Layout (104 bits / 13 bytes) + +- **bytes 0–1:** sync `0x7F 0xA7` +- **bytes 2–5:** serial (32-bit, big-endian) +- **byte 6:** button — valid codes `0x10` (Lock), `0x11` (Unlock), `0x13` (Trunk), `0x14` (Panic), `0x15` +- **bytes 7–8 + byte 9 MSB:** counter = `((b7 & 0x7F) << 9) | (b8 << 1) | (b9 >> 7)`; byte 7 MSB carries a button parity bit (refreshed by the encoder) +- **bytes 9–12:** rolling/hop tail (stashed in `extra` so the encoder can rebuild the full frame) + +The exported `data` (Key) word is the top 8 bytes; bytes 8–12 (40 bits) are carried in `extra`. + +## RF + +- **Encoding:** Manchester (Ford V2 polarity `level ? High : Low`; decoded bits inverted) +- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`) +- **Encryption:** none — validated by sync bytes + valid button code +- **Frequencies:** 315 MHz, 433.92 MHz + +## Decoder Steps + +1. **Reset** → a short pulse (~200 µs) begins the preamble. +2. **Preamble** — count short pulses; after ≥64 shorts a long LOW pulse enters Sync. +3. **Sync** — Manchester-decode bits into a 16-bit shift register; when it matches `!0x7FA7`, prime the two sync bytes and enter Data. +4. **Data** — Manchester events feed the state machine; decoded bits are inverted and packed MSB-first into 13 bytes. At 104 bits the sync bytes and button are validated and the frame committed. A non-short/long pulse (gap) ends the attempt. + +## Encoder + +Supported (matches `subghz_protocol_encoder_ford_v2`). Rebuilds the 13-byte frame, applies the requested +button, refreshes byte-7 parity, then emits 6 bursts of a 70-pair preamble + sync + 104 Manchester bits +(encoder short 240 µs), separated by ~16 ms inter-burst gaps. + +## Validation + +Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). diff --git a/docs/ford_v3.md b/docs/ford_v3.md new file mode 100644 index 0000000..2e48c57 --- /dev/null +++ b/docs/ford_v3.md @@ -0,0 +1,55 @@ +--- +layout: default +--- + +# Ford V3 Protocol + +**Rust module:** `src/protocols/ford_v3.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v3.c` + +## Overview + +Ford V3 uses Manchester encoding at 240/480 µs. 104 bits total (13 bytes), transmitted as plaintext — +there is no CRC or encryption. Decode-only: the ProtoPirate reference ships a NULL encoder, so KAT +does not transmit Ford V3 (Replay still works on the raw capture). + +The Manchester decoder uses Flipper's `manchester_decoder.h` transition table (same table as Ford V0), +but Ford V3 maps the level the opposite way from Ford V0: `level ? High : Low`. + +## Timing + +| Parameter | Value | Notes | +|-----------|--------|--------------------| +| Short | 240 µs | ±60 µs (te_delta) | +| Long | 480 µs | ±60 µs | +| Min bits | 104 | 13 bytes | +| Preamble | ≥30 short pulses | before first long | + +## Frame Layout (104 bits / 13 bytes) + +- **byte 0:** header +- **bytes 1–4:** serial (32-bit, big-endian) +- **byte 5:** hop/reserved +- **byte 6:** button — bit 0 set → Unlock, else Lock +- **bytes 7–8:** counter, stored bitwise-inverted (`~b[7]`, `~b[8]`) +- **bytes 9–12:** rolling/hop tail (not parsed) + +## Decoder Steps + +1. **Reset** — a short pulse (~240 µs) starts the preamble (count = 1). +2. **Preamble** — count short pulses; once ≥30 shorts are seen, a long pulse begins the data + (Manchester state seeded to Mid1, first bit decoded from the long pulse). +3. **Data** — Manchester events (short/long × level) feed the state machine; bits pack MSB-first into + 13 bytes. At 104 bits the fields are parsed and the signal is emitted. A non-short/non-long pulse + (gap) ends the frame. + +## RF + +- **Encoding:** Manchester +- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`) +- **Encryption:** none (plaintext) +- **Frequencies:** 315 MHz, 433.92 MHz + +## Encoder + +Not supported (reference encoder is NULL). Decode-only. diff --git a/docs/honda_static.md b/docs/honda_static.md new file mode 100644 index 0000000..015e8e6 --- /dev/null +++ b/docs/honda_static.md @@ -0,0 +1,65 @@ +--- +layout: default +--- + +# Honda Static Protocol + +**Rust module:** `src/protocols/honda_static.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/honda_static.c` + +## Overview + +Honda/Acura fixed-code keyfobs. 64-bit frame. Unlike most KAT decoders, Honda Static does NOT use the +Flipper `manchester_decoder.h` transition table: it buffers a per-element *symbol stream* (one bit per +~63 µs element) and then performs a custom Manchester unpack over symbol pairs. A short pulse contributes +one symbol equal to the pulse level; a long pulse contributes two symbols of the same level. Anything +outside both ranges (e.g. the 700 µs sync or a trailing gap) terminates the buffer and triggers a parse. + +The checksum is an XOR of the first 7 packet bytes. Emission is gated on the checksum, so Honda Static +will not false-match. The parser tries the inverted-Manchester interpretation first (what the encoder +emits), then non-inverted forward, then a bit-reversed-bytes pass. + +## Timing + +| Parameter | Value | Notes | +|----------------|--------|--------------------------------| +| Element | 63 µs | one symbol per element | +| Short pulse | 28–98 µs | base 28 µs, span 70 µs | +| Long pulse | 61–191 µs | base 61 µs, span 130 µs (two symbols) | +| Sync | 700 µs | terminates the symbol buffer | +| Min symbols | 36 | before a parse is attempted | +| Min bits | 64 | | + +(Reported timing profile: te_short 63 µs, te_long 700 µs, te_delta 120 µs.) + +## Frame Layout (64 bits, MSB-first into 8 bytes) + +- **bits 0–3:** button (4-bit) — Lock=1, Unlock=2, Trunk=4, Remote Start=5, Panic=8, Lock×2=9 +- **bits 4–31:** serial (28-bit) +- **bits 32–55:** counter (24-bit) +- **bits 56–63:** checksum (XOR of bytes 0–6) + +The exported `data` (Key) word is the C `generic.data`: a compact nibble-packed layout, NOT the raw +decoded packet bytes. The KAT counter field is the low 16 bits of the 24-bit counter. + +## RF + +- **Encoding:** custom symbol-stream Manchester over ~63 µs elements +- **RF modulation:** FM +- **Encryption:** none — gated on the XOR checksum + valid button + valid serial (serial ≠ 0 and ≠ 0x0FFFFFFF) +- **Frequencies:** 315 MHz, 433.92 MHz + +## Decoder Steps + +1. **feed** — classify each pulse: short → 1 symbol, long → 2 symbols; buffer them. +2. On an out-of-range pulse (700 µs sync or gap), if ≥36 symbols are buffered, walk past the alternating preamble + sync run, Manchester-unpack 64 bits over symbol pairs (inverted first, then non-inverted), validate, and emit. Then clear the buffer. + +## Encoder + +Supported (matches `honda_static_build_upload`). Emits a 700 µs HIGH sync, a 160-element alternating +preamble at 63 µs, 64 data bits (each as `!value`/`value` at 63 µs), and a trailing 700 µs sync. + +## Validation + +Decodes REAL IMPORTS captures (IMPORTS/honda Lock and Unlock). Also covered by encode→decode round-trip +and packet-validate unit tests. diff --git a/docs/honda_v1.md b/docs/honda_v1.md new file mode 100644 index 0000000..0772f4d --- /dev/null +++ b/docs/honda_v1.md @@ -0,0 +1,63 @@ +--- +layout: default +--- + +# Honda V1 Protocol + +**Rust module:** `src/protocols/honda_v1.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/honda_v1.c` + +## Overview + +Honda/Acura fixed-code keyfobs — a DIFFERENT protocol from `honda_static`. The on-wire frame carries 68 +bits: 64-bit data + a 4-bit CRC-fold nibble. Encoding is short/long pulse PWM, glued together by a +"pending bit" timing accumulator before classification: sub-`te_delta` runts are summed into `pending`; +a HIGH level extends the running HIGH pulse; a LOW flushes the accumulated HIGH (if it reached +`te_short_min`) as a synthetic symbol, then classifies the LOW. + +Validation is a button-code table (Unlock=0, Lock=8, Trunk=9, Panic=10) plus a CRC-fold checksum. +Emission is gated on the button being valid; `crc_valid` reflects whether the received CRC nibble matches +either wire-order checksum. The strong button gate keeps Honda V1 from false-matching. + +## Timing + +| Parameter | Value | Notes | +|-------------|---------|-----------------------------| +| Short | 1000 µs | ±400 µs (te_delta) | +| Long | 2000 µs | ±400 µs | +| Short min | 600 µs | flush threshold for HIGH | +| End gap | 3500 µs | >te_end commits the frame | +| Min bits | 68 | | + +## Frame Layout (68 bits = 64-bit data + 4-bit CRC nibble) + +After the end gap, `commit` left-shifts the 12-byte bit buffer to drop leading preamble leakage and align +the trailing 68-bit frame. `data` = first 8 bytes (64 bits); the CRC nibble = byte 8's high nibble. + +- **data[63:36]:** serial (28-bit) +- **data[31:28]:** button (nibble) — Unlock=0, Lock=8, Trunk=9, Panic=10 +- **data[15:0]:** counter (16-bit) +- **CRC nibble:** CRC-fold checksum (`honda_v1_checksum*`), validated against either wire-order value + +## RF + +- **Encoding:** short/long pulse PWM with pending-bit timing accumulation +- **RF modulation:** AM (OOK) +- **Encryption:** none — gated on the button-code table + CRC-fold checksum +- **Frequencies:** 315 MHz, 433.92 MHz + +## Decoder Steps + +1. **feed** — accumulate sub-`te_delta` runts into `pending`; flush HIGH pulses and classify LOW pulses into symbols. +2. **symbol** — Reset → Preamble (short/long pulses, needs a long + >5 count) → Data. +3. **Data** — pending-bit accumulation: a short pulse toggles `data_pending` and emits a bit when paired; a long pulse emits directly. After a >te_end gap, commit: align the 68-bit frame, validate the button, and emit. + +## Encoder + +Supported (ENABLE_EMULATE_FEATURE). Builds the 64-bit key from serial/button/counter via the button code +table, then emits a 180-element short-pair preamble + 4 PWM frames (2 per checksum wire value). + +## Validation + +Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests +cover the key/field round trip, full encode→decode, and the button-validity mask. diff --git a/docs/kia_v7.md b/docs/kia_v7.md new file mode 100644 index 0000000..46b62cb --- /dev/null +++ b/docs/kia_v7.md @@ -0,0 +1,55 @@ +--- +layout: default +--- + +# Kia V7 Protocol + +**Rust module:** `src/protocols/kia_v7.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/kia_v7.c` + +## Overview + +Kia V7 uses Manchester encoding at 250/500 µs. 64 bits. The decoded 64-bit word is bit-inverted +(`~data`); a valid frame has a fixed header byte `0x4C` and a CRC8 (poly `0x7F`, init `0x4C`) over bytes +0–6. Emission is gated on header + CRC, so Kia V7 is strongly validated and will not false-match. + +## Timing + +| Parameter | Value | Notes | +|-----------|--------|---------------------| +| Short | 250 µs | ±100 µs (te_delta) | +| Long | 500 µs | ±100 µs | +| Min bits | 64 | | +| Preamble | ≥16 short pairs | before sync | + +## Frame Layout (64 bits / 8 bytes, after inversion) + +- **byte 0:** header `0x4C` +- **bytes 1–2:** counter (16-bit, big-endian) +- **bytes 3–6:** serial (28-bit) — `(b3<<20)|(b4<<12)|(b5<<4)|(b6>>4)`, masked to 0x0FFFFFFF +- **byte 6 low nibble:** button (4-bit) — Lock=1, Unlock=2, Trunk=3, aux=8 +- **byte 7:** CRC8 over bytes 0–6 (poly `0x7F`, init `0x4C`) + +## RF + +- **Encoding:** Manchester (level ? (H,L) : (L,H); decoded word is bit-inverted) +- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`) +- **Encryption:** none — gated on fixed header `0x4C` + CRC8 +- **Frequencies:** 315 MHz, 433.92 MHz + +## Decoder Steps + +1. **Reset** — a short HIGH pulse begins the preamble. +2. **Preamble** — count short pairs; once >15 pairs are seen, a long HIGH pulse transitions to SyncLow and preloads four seed bits (1,0,1,1 = the inverted header's top nibble 0xB). +3. **SyncLow** — a short LOW after the long sync enters Data. +4. **Data** — Manchester-decode the remaining 60 bits. At 64 bits, invert the word, check the header equals `0x4C` and the CRC8 validates, then emit. + +## Encoder + +Supported (matches `kia_v7_encode_key`). Rebuilds the 64-bit key from serial/button/counter with the +CRC8, then emits a 32-pair short preamble, a merged long sync pulse, 64 Manchester bits, and a trailing +gap. + +## Validation + +Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). diff --git a/docs/land_rover_rke.md b/docs/land_rover_rke.md new file mode 100644 index 0000000..b5d073c --- /dev/null +++ b/docs/land_rover_rke.md @@ -0,0 +1,71 @@ +--- +layout: default +--- + +# Land Rover RKE Protocol + +**Rust module:** `src/protocols/land_rover_rke.rs` +**Reference:** `Flipper-ARF lib/subghz/protocols/landrover_rke.c` + +## Overview + +Ported from the Flipper-ARF firmware (D4C1-Labs), itself derived from Pandora DXL 5000 firmware. Land +Rover shares the Ford/Jaguar baseband (firmware protocol ID `0x0E`) but uses a distinct 66-bit frame. +Encoding is fixed-width PWM with a 1000 µs bit period: Bit-1 = 700 µs HIGH + 300 µs LOW, Bit-0 = 300 µs +HIGH + 700 µs LOW. Preamble = 20× (400 µs HIGH + 600 µs LOW); sync = 400 µs HIGH + 9600 µs LOW. + +KeeLoq: the hop code is the raw 32-bit KeeLoq ciphertext. Full decryption needs the per-fob manufacturer +key (provisioned, not in firmware), so KAT exposes the framed fields and leaves the hop encrypted — +`crc_valid = false` since no cryptographic check is performed. Emission is still gated tightly on the long +preamble run, the distinctive 9.6 ms sync gap, and exactly 66 PWM bits, so this loose-looking PWM decoder +does not false-match Kia/Subaru/Ford captures. + +## Timing + +| Parameter | Value | Notes | +|----------------|---------|-----------------------------| +| Bit-1 | 700 µs HIGH + 300 µs LOW | ±20% (relative) | +| Bit-0 | 300 µs HIGH + 700 µs LOW | ±20% | +| Preamble pair | 400 µs HIGH + 600 µs LOW | ×20 | +| Sync | 400 µs HIGH + 9600 µs LOW | | +| Repeat gap | 12000 µs | | +| Min bits | 66 | | +| Preamble min | 16 pairs| | + +(Reported timing profile: te_short 300 µs, te_long 700 µs, te_delta 140 µs.) + +## Frame Layout (66 bits, MSB-first) + +- **[65:34]:** 32-bit KeeLoq encrypted hopping code +- **[33:10]:** 24-bit fixed fob serial +- **[9:6]:** 4-bit button — Lock=0x1, Unlock=0x2, Boot/Tailgate=0x4, Panic=0x8 +- **[5:2]:** 4-bit function/repeat flags +- **[1:0]:** 2-bit status — battery-low=0x1, repeat=0x2 + +66 bits do not fit a u64: `data` holds the low 64 frame bits and `extra` holds the top 2 (the high 2 bits +of the hop code), so encode round-trips the hop losslessly. The KAT counter field surfaces the low 16 +bits of the hop ciphertext. + +## RF + +- **Encoding:** fixed-width PWM +- **RF modulation:** OOK/AM +- **Encryption:** KeeLoq hop left encrypted (no manufacturer key); `crc_valid = false` +- **Frequencies:** 433.92 MHz, 315 MHz + +## Decoder Steps + +1. **Reset** — wait for the first ~400 µs preamble HIGH. +2. **Preamble** — count 400/600 µs pairs; the distinctive ~9600 µs LOW after a ~400 µs HIGH (with ≥16 pairs) enters DataHigh. +3. **DataHigh/DataLow** — read each bit from its HIGH/LOW half pair (±20% windows). At 66 bits, build the signal and emit. + +## Encoder + +Supported. Reconstructs the original frame to preserve hop code / func bits / status, overrides only the +button, and emits 4 repetitions of preamble + sync + 66 MSB-first PWM bits, separated by 12 ms gaps. + +## Validation + +Verified by encode→decode round-trips / synthetic-frame checks (no local capture available). Unit tests +cover pack/unpack round trips, encode→decode across multiple serials/hops/buttons, and rejection of +truncated frames and wrong sync gaps. diff --git a/docs/land_rover_v0.md b/docs/land_rover_v0.md new file mode 100644 index 0000000..09c9ae1 --- /dev/null +++ b/docs/land_rover_v0.md @@ -0,0 +1,69 @@ +--- +layout: default +--- + +# Land Rover V0 Protocol + +**Rust module:** `src/protocols/land_rover_v0.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/land_rover_v0.c` + +## Overview + +Land Rover V0 uses **differential** Manchester (NOT the Flipper transition table) at 250/500 µs, with a +~750 µs sync pulse and a ≥64-pair short preamble. The frame is 81 bits = an 80-bit body (`raw[0..10]`) +plus one trailing `extra_bit`. The 64-bit key reported as `data` is `raw[0..8]` big-endian; `raw[8..10]` +is a 16-bit `tail`. + +The check is a proprietary 3-bit polynomial over the counter (`calculate_check`); the tail is `0xFFFF` +or `0x7FFF` depending on a 1-bit parity of the counter (`calculate_tail`). Emission is gated on the +reserved bits being zero, the check matching, the tail matching, and `extra_bit` set — so Land Rover V0 +is strongly validated and will not false-match. + +## Timing + +| Parameter | Value | Notes | +|-------------|--------|------------------------| +| Short | 250 µs | ±100 µs (te_delta) | +| Long | 500 µs | ±100 µs | +| Sync | 750 µs | ±120 µs | +| Min bits | 81 | 80-bit body + extra_bit| +| Preamble | ≥64 pairs | | + +## Frame Layout (81 bits) + +64-bit key (`raw[0..8]`): + +- **bytes 0–2:** 24-bit command signature — Lock = `0xC20363`, Unlock = `0xA285E3` +- **bytes 3–5:** 24-bit serial +- **byte 6 + byte 7 MSB:** 9-bit counter = `(b6 << 1) | (b7 >> 7)` +- **byte 7 bits 0x78:** 3 reserved bits (must be 0) +- **byte 7 bits 0x07:** 3-bit check (`calculate_check`) + +Then `raw[8..10]` = 16-bit tail (`0xFFFF`/`0x7FFF`), and a trailing `extra_bit` (must be 1). The tail is +stashed in `extra` for the encoder. + +## RF + +- **Encoding:** differential Manchester +- **RF modulation:** FM +- **Encryption:** none — gated on the 3-bit check polynomial + tail parity + reserved-bits + extra_bit +- **Frequencies:** 315 MHz, 433.92 MHz + +## Decoder Steps + +1. **Reset → PreambleLow/PreambleHigh** — count short pairs (≥64), terminated by a ~750 µs sync HIGH. +2. **SyncLow** — a ~750 µs sync LOW seeds the frame (bit 0 = 1) and enters Data. +3. **Data** — the differential machine (`process_transition`/`add_decoded_bit`) tracks `previous_bit`, skips the initial boundary short-high pad, and completes short half-bits via `pending_short`. At 81 bits, `finish_frame` validates and emits. + +## Encoder + +Supported. Builds the 64-bit key from signature/serial/counter, emits a 319-pair preamble, the sync, +differential-Manchester body, the 16-bit tail, and a trailing extra bit. Faithful-port note: the +reference encoder forces frame bit 1 = 0, so the Lock signature `0xC20363` (whose bit 1 is 1) is emitted +as `0x820363`; the Unlock signature `0xA285E3` round-trips cleanly. KAT reproduces this behaviour. + +## Validation + +Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests +cover the check polynomial vs. the reference, the tail parity, Unlock round trips across 256 serial/counter +values, the Lock forced-bit quirk, and rejection of a corrupted check. diff --git a/docs/mazda_siemens.md b/docs/mazda_siemens.md new file mode 100644 index 0000000..c426a9a --- /dev/null +++ b/docs/mazda_siemens.md @@ -0,0 +1,70 @@ +--- +layout: default +--- + +# Mazda Siemens Protocol + +**Rust module:** `src/protocols/mazda_siemens.rs` +**Reference:** `Flipper-ARF lib/subghz/protocols/mazda_siemens.c` + +## Overview + +The Siemens/VDO keyfob cipher used on some Mazda vehicles — a DIFFERENT protocol from KAT's existing +"Mazda V0" (Pandora) decoder, even though both ride a 250/500 µs pair-based stream at 433.92 MHz FM. The +decoder is pair-based: `feed()` ignores `level` and interprets raw durations in pairs (`process_pair`), +collecting bits with *inverted* polarity (`state_bit == 0` → stored 1). 64-bit frame. + +Siemens obfuscation (`mazda_xor_deobfuscate`): parity-dependent XOR mask, then bit-deinterleave of bytes +5/6. The inner Siemens cipher's plaintext is left as-is (no key); only this obfuscation/interleave layer +is reversed. The gate is an additive checksum `sum(data[0..7]) == data[7]`, plus the structural +preamble/sync/bit-count constraints, which keeps it from false-matching other 250/500 µs Manchester +protocols. + +> **Note:** KAT's existing "Mazda V0" decoder implements the same algorithm and keeps first-match +> priority, so on a shared frame Mazda V0 fires first. + +## Timing + +| Parameter | Value | Notes | +|-------------|--------|-----------------------------| +| Short | 250 µs | ±100 µs (te_delta) | +| Long | 500 µs | ±100 µs | +| Min bits | 64 | | +| Preamble | ≥13 short/short pairs | | +| Completion | 80–105 collected bits | | + +## Frame Layout (64 bits / 8 bytes, after deobfuscation) + +The 14-byte buffer's leading sync byte is discarded, leaving 8 bytes: + +- **serial:** `data >> 32` (32-bit) +- **button:** `(data >> 24) & 0xFF` — Lock=0x10, Unlock=0x20, Trunk=0x40 +- **counter:** `(data >> 8) & 0xFFFF` (16-bit) +- **byte 7:** additive checksum of bytes 0–6 + +## RF + +- **Encoding:** pair-based Manchester (inverted polarity), 250/500 µs +- **RF modulation:** FM +- **Encryption:** Siemens obfuscation (parity XOR + bit-interleave); gated on the additive checksum +- **Frequencies:** 433.92 MHz only + +## Decoder Steps + +1. **Reset** — a short pulse begins the preamble. +2. **PreambleSave/PreambleCheck** — count short/short pairs (≥13); a short→long transition seeds the leading sync bit (a 1) and enters data. +3. **DataSave/DataCheck** — process duration pairs into bits; on a non-matching pair, run `check_completion` (discard sync byte, deobfuscate, validate additive checksum) and emit. + +## Encoder + +Supported (`subghz_protocol_encoder_mazda_siemens_get_upload`). Increments the counter byte, recomputes +the checksum, obfuscates (interleave + XOR), then emits a 12-byte 0xFF preamble, a 50 ms gap, `0xFF 0xFF +0xD7`, the 8 obfuscated bytes transmitted as `255 − byte`, a `0x5A` tail, and a trailing 50 ms gap. +Manchester per byte: bit 1 → (H,L), bit 0 → (L,H) at te_short. As in the C, the decoder and encoder are +not a clean raw-timing round-trip pair (the encoder targets a TX upload). + +## Validation + +Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests +cover the obfuscate/deobfuscate XOR+interleave cipher layer (a true inverse, both parity branches), the +field layout, and the TX upload smoke test. diff --git a/docs/porsche_cayenne.md b/docs/porsche_cayenne.md new file mode 100644 index 0000000..b106e89 --- /dev/null +++ b/docs/porsche_cayenne.md @@ -0,0 +1,67 @@ +--- +layout: default +--- + +# Porsche Cayenne Protocol + +**Rust module:** `src/protocols/porsche_cayenne.rs` +**Reference:** `Flipper-ARF lib/subghz/protocols/porsche_cayenne.c` + +## Overview + +Porsche keyfobs (internal firmware header name "Porsche AG"). PWM at 1680/3370 µs: SHORT LOW + LONG HIGH += bit 0, LONG LOW + SHORT HIGH = bit 1. 64-bit MSB-first frame, preceded by a 73-pulse 3370 µs sync +preamble + a 5930 µs gap pair. The cipher is a 24-bit rotating-register VAG cipher; the counter is +recovered by brute-forcing 1..=256 and matching the recomputed encrypted bytes (the C's validity signal). + +> **Note:** Porsche Cayenne shares the wire protocol with KAT's existing **Porsche Touareg** decoder +> (the Touareg port is itself a port of this same `porsche_cayenne.c` source). The two are the **same +> wire protocol** — identical timing, preamble, 64-bit frame, VAG cipher, and validity check. Cayenne is +> registered AFTER Touareg, so Touareg keeps first-match priority and Cayenne never steals a Touareg +> capture. Emission is additionally gated on `frame_type` being one of the three values the C emits +> (`0b001` Cont, `0b010` First, `0b100` Final) — a strict subset of what Touareg accepts. Cayenne adds an +> encoder (the Touareg port is decode-only). + +## Timing + +| Parameter | Value | Notes | +|-------------|---------|------------------------| +| Short | 1680 µs | ±500 µs (te_delta) | +| Long | 3370 µs | ±500 µs | +| Sync | 3370 µs | preamble pulse | +| Gap | 5930 µs | preamble→data boundary | +| Min bits | 64 | | +| Sync min | 15 pairs| (firmware emits 73) | + +## Frame Layout (64 bits / 8 bytes) + +- **byte 0:** `(button << 4) | (frame_type & 0x07)` — button d-pad: Lock=0x01, Unlock=0x02, Trunk=0x04, Open/Panic=0x08; frame_type First=0x02, Cont=0x01, Final=0x04 +- **bytes 1–3:** serial (24-bit, big-endian) +- **bytes 4–7:** encrypted cipher output (24-bit rotating-register VAG cipher seeded from serial, rotated `4 + counter_low` times) + +The `extra` word carries the frame_type; `protocol_display_name` is `Porsche Cayenne [First|Cont|Final]`. + +## RF + +- **Encoding:** PWM (SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1) +- **RF modulation:** AM/OOK +- **Encryption:** 24-bit rotating-register VAG cipher; counter recovered by brute force, `counter != 0` is the validity gate +- **Frequencies:** 433.92 MHz, 868.35 MHz + +## Decoder Steps + +1. **Reset** — wait for a LOW pulse at ~3370 µs. +2. **Sync** — count sync pulses (HIGH and LOW at 3370 µs); a 5930 µs gap with ≥15 sync pulses enters GapHigh/GapLow. +3. **GapHigh/GapLow** — expect the complementary 5930 µs gap pulse, then enter Data. +4. **Data** — decode bit pairs (SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1); at 64 bits, `parse_data` applies the Cayenne frame_type gate + counter recovery and emits (or stays silent so Touareg owns the frame). + +## Encoder + +Supported (matches `porsche_cayenne_build_upload`). Emits a 4-frame burst (frame types 0b010/0b001/0b100/ +0b100, cipher counters cnt+1..cnt+4), each frame being 73 sync pairs + 1 gap pair + 64 MSB-first PWM bits. + +## Validation + +Verified by encode→decode round-trips / synthetic-frame checks (no local capture available). Unit tests +cover the full encoder path, all three frame types, rejection of undocumented frame types and truncated +frames, the counter-recovery limit matching the C, and shared cipher vectors with the Touareg port. diff --git a/docs/psa2.md b/docs/psa2.md new file mode 100644 index 0000000..0108e5e --- /dev/null +++ b/docs/psa2.md @@ -0,0 +1,76 @@ +--- +layout: default +--- + +# PSA2 Protocol + +**Rust module:** `src/protocols/psa2.rs` +**Reference:** `Flipper-ARF lib/subghz/protocols/psa2.c` + +## Overview + +PSA2 (Peugeot/Citroën — internal name "PSA OLD") is the OLDER PSA variant, distinct from KAT's existing +`psa` (modified-TEA/XEA) decoder. Manchester encoding: 250/500 µs symbol (Pattern 1, standard rate) or +125/250 µs (Pattern 2, half rate), using the canonical Flipper `manchester_advance` table. 128-bit frame += key1 (64 bits) + key2/validation word: the decoder collects 64 bits → key1, then 16 more (to 80 bits) +→ the 16-bit validation field / key2_low. + +Crypto: TEA (Tiny Encryption Algorithm) with a dual brute-force fallback (BF1 0x23000000–0x24000000, BF2 +0xF3000000–0xF4000000) and a mode23/mode36 selector, validated via a nibble checksum. + +**Performance:** the live decoder only ever runs the cheap O(1) mode23 XOR path +(`direct_xor_decrypt`) per frame — it NEVER runs the TEA brute force. The bounded TEA brute force +(`decrypt_full`, ~16.7M iters per range) is ported faithfully but is reserved for a deferred-decrypt UI +action, mirroring the C exactly. Beyond the C's nibble-checksum gate, KAT adds two false-positive +suppressors required by its feed-all-decoders model: a `key2_high` precondition and a valid-button check +(PSA2 buttons are Lock=0, Unlock=1, Trunk=2 only). Emission is gated strictly on a successful, +field-bearing mode23 decrypt. + +## Timing + +| Parameter | Value | Notes | +|----------------|--------|-----------------------------| +| Short (P1) | 250 µs | ±100 µs (te_delta) | +| Long (P1) | 500 µs | ±100 µs | +| Short (P2) | 125 µs | ±50 µs (half rate) | +| Long (P2) | 250 µs | ±50 µs | +| End marker | 1000 µs (P1) / 500 µs (P2) | | +| Min bits | 128 | key1 (64) + validation (16) collected | +| Preamble | >0x46 pairs (P1) / >0x45 (P2) | | + +## Frame Layout (128 bits = key1 64-bit + key2/validation) + +The decoder collects 64 bits → key1, then 16 bits → the validation field (key2_low). After the mode23 XOR +decrypt (fields from `extract_fields_mode23`): + +- **serial:** `(buf[2]<<16) | (buf[3]<<8) | buf[4]` (24-bit) +- **button:** `buf[8] & 0xF` — Lock=0, Unlock=1, Trunk=2 +- **counter:** `(buf[5]<<8) | buf[6]` (16-bit) +- **crc:** `buf[7]` + +`data` = `(key1_high << 32) | key1_low` (64 bits). + +## RF + +- **Encoding:** Manchester (canonical Flipper table; standard 250/500 µs or half-rate 125/250 µs) +- **RF modulation:** AM (OOK) +- **Encryption:** TEA + nibble-checksum-gated mode23 XOR path; brute-force fallback reserved for offline decrypt +- **Frequencies:** 433.92 MHz + +## Decoder Steps + +1. **WaitEdge (State0)** — detect the preamble rate (250 µs → Pattern 1, 125 µs → Pattern 2). +2. **CountPattern250/125 (State1/3)** — count preamble pulses past the threshold, then a long pulse enters the Manchester decode state. +3. **DecodeManchester250/125 (State2/4)** — Manchester-decode to 80 bits (key1 + validation), detect end-of-packet, then `finalize_frame` runs the cheap mode23 XOR gate and emits only on a successful field-bearing decrypt. + +## Encoder + +Supported (mode23 path). Increments the counter, builds key1/validation via `encode_mode23` (inverse XOR +stage + nibble checksum), then emits an 80-pair preamble, a sync, 64 key1 bits + 16 validation bits +MSB-first, and an end burst. + +## Validation + +Decodes REAL IMPORTS captures (IMPORTS/GROUPE PSA, serial `0x99EB25`). Unit tests also cover the TEA +round trip, the mode23 encode→decode round trip, the nibble checksum vs. the reference, and the emitted +Manchester frame length. diff --git a/docs/toyota.md b/docs/toyota.md new file mode 100644 index 0000000..74be733 --- /dev/null +++ b/docs/toyota.md @@ -0,0 +1,69 @@ +--- +layout: default +--- + +# Toyota Protocol + +**Rust module:** `src/protocols/toyota.rs` +**Reference:** `Flipper-ARF lib/subghz/protocols/toyota.c` + +## Overview + +Toyota / Lexus KeeLoq keyfobs, a dual-variant decoder. Two variants are detected from the first HIGH +pulse width (threshold 310 µs): + +- **Variant A** (Corolla / 433.92 MHz) — PWM pairs: LS (long HIGH + short LOW) = bit 0, SL (short HIGH + + long LOW) = bit 1. Preamble = repeated short-short pairs; first non-SS pair is the first data bit. + Frame = 68 bits. +- **Variant B** (Tundra / 315 MHz) — NRZ: each individual pulse encodes one bit by a midpoint classifier + (≤287 µs → 0, >287 µs → 1). Preamble = short-HIGH/long-LOW pairs terminated by a sync gap (LOW between + 1500 and 2600 µs). Frame = 67 bits. + +KeeLoq hopping: the hop field is left encrypted (the reference does not decrypt or run a CRC). Emission +is gated tightly (exact frame bit count + structural preamble/sync + non-zero serial) so it does not +false-match the other KeeLoq-PWM protocols. The shared 433 MHz KeeLoq-PWM air encoding means a Variant-A +frame that also satisfies Kia V3/V4's 68-bit/CRC4 structure is claimed by Kia V3/V4 first (earlier in the +registry); Toyota uniquely claims 60-bit frames and Variant-B NRZ at 315 MHz. + +## Timing + +| Parameter | Value | Notes | +|------------------|--------|-----------------------------| +| A short | 400 µs | ±175 µs (Variant A) | +| A long | 800 µs | ±175 µs | +| B short | 200 µs | ±120 µs (Variant B preamble)| +| B long | 390 µs | ±120 µs | +| B NRZ midpoint | 287 µs | ≤ → 0, > → 1 | +| B sync gap | 1500–2600 µs | | +| Min bits | 60 | A frame = 68, B frame = 67 | +| Variant threshold| 310 µs | first HIGH: < B, ≥ A | + +## Frame Layout + +`data = (hop << 32) | (serial << 4) | button` (matches the reference `generic.data`): + +- **hop:** 32-bit KeeLoq ciphertext (left encrypted) +- **serial:** 28-bit +- **button:** 4-bit + +## RF + +- **Encoding:** Variant A = PWM pairs; Variant B = NRZ +- **RF modulation:** AM/OOK +- **Encryption:** KeeLoq hop left encrypted (no key / no CRC); a fully-structured frame of the exact bit count with non-zero serial is the validity criterion +- **Frequencies:** 433.92 MHz (Variant A), 315 MHz (Variant B) + +## Decoder Steps + +1. **Reset** — detect the variant from the first SHORT HIGH (<310 µs → B, ≥310 µs → A). +2. **Variant A** — PreambleA (count SS pairs) → DataA (decode LS/SL pairs, cap at 68 bits, emit on the terminating gap so Kia wins shared frames by registry order). +3. **Variant B** — PreambleB (short-HIGH/long-LOW pairs) → on the sync gap → DataB (each pulse is one NRZ bit; emit at 67 bits or on an end gap). + +## Encoder + +Not supported (the reference `encoder` field is NULL). Decode-only. + +## Validation + +Decodes REAL IMPORTS captures (IMPORTS Toyota + Lexus Camry, NRZ Variant B). Unit tests also cover +synthetic Variant A and Variant B frames and rejection of an all-zero serial. diff --git a/src/app.rs b/src/app.rs index 23a8e34..79d02b3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1898,13 +1898,23 @@ impl App { match protocol { p if p.starts_with("Kia") => "Kia/Hyundai", p if p.starts_with("Ford") => "Ford", + "Honda Static" => "Honda/Acura", + "Honda V1" => "Honda/Acura", p if p.starts_with("Fiat") => "Fiat", "Subaru" => "Subaru", "Suzuki" => "Suzuki", "VAG" | "VW" => "VW/Audi/Seat/Skoda", - "PSA" => "Peugeot/Citroen", + p if p.starts_with("PSA") => "Peugeot/Citroen", "Star Line" => "Star Line", "Scher-Khan" => "Scher-Khan", + "Chrysler V0" => "Chrysler/Dodge/Jeep", + p if p.starts_with("Land Rover") => "Land Rover", + "Toyota" => "Toyota/Lexus", + // Covers both "Mazda V0" and "Mazda Siemens". + p if p.starts_with("Mazda") => "Mazda", + "BMW CAS4" => "BMW", + // Covers "Porsche Touareg" and "Porsche Cayenne [First/Cont/Final]". + p if p.starts_with("Porsche") => "Porsche", _ => "Unknown", } } diff --git a/src/capture.rs b/src/capture.rs index 06942c0..c808889 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -225,19 +225,32 @@ impl Capture { p if p.starts_with("Kia V2") => ModulationType::Manchester, p if p.starts_with("Kia V5") => ModulationType::Manchester, p if p.starts_with("Kia V6") => ModulationType::Manchester, - "Ford V0" => ModulationType::Manchester, + p if p.starts_with("Kia V7") => ModulationType::Manchester, + p if p.starts_with("Ford") => ModulationType::Manchester, + "Honda Static" => ModulationType::Manchester, + "Mazda Siemens" => ModulationType::Manchester, + "BMW CAS4" => ModulationType::Manchester, "Fiat V0" => ModulationType::Manchester, "PSA" => ModulationType::Manchester, + "PSA2" => ModulationType::Manchester, "VAG" => ModulationType::Manchester, + // Differential Manchester + "Land Rover V0" => ModulationType::DifferentialManchester, // PWM-encoded protocols p if p.starts_with("Kia V0") => ModulationType::Pwm, p if p.starts_with("Kia V3") => ModulationType::Pwm, p if p.starts_with("Kia V4") => ModulationType::Pwm, + "Honda V1" => ModulationType::Pwm, "Subaru" => ModulationType::Pwm, "Suzuki" => ModulationType::Pwm, "Star Line" => ModulationType::Pwm, p if p.starts_with("Keeloq (") => ModulationType::Pwm, "Scher-Khan" => ModulationType::Pwm, + "Chrysler V0" => ModulationType::Pwm, + "Land Rover RKE" => ModulationType::Pwm, + "Toyota" => ModulationType::Pwm, + // Display name is "Porsche Cayenne [First/Cont/Final]"; match the prefix. + p if p.starts_with("Porsche Cayenne") => ModulationType::Pwm, // Unknown _ => ModulationType::Unknown, } @@ -252,16 +265,27 @@ impl Capture { p if p.starts_with("Kia V2") => RfModulation::FM, p if p.starts_with("Kia V5") => RfModulation::FM, p if p.starts_with("Kia V6") => RfModulation::FM, + p if p.starts_with("Kia V7") => RfModulation::FM, "Scher-Khan" => RfModulation::FM, "PSA" => RfModulation::FM, + "PSA2" => RfModulation::AM, "Fiat V0" => RfModulation::FM, - "Ford V0" => RfModulation::FM, + p if p.starts_with("Ford") => RfModulation::FM, + "Honda Static" => RfModulation::FM, + "Mazda Siemens" => RfModulation::FM, + "Land Rover V0" => RfModulation::FM, // AM only (SubGhzProtocolFlag_AM) p if p.starts_with("Kia V1") => RfModulation::AM, + "Honda V1" => RfModulation::AM, "VAG" => RfModulation::AM, "Subaru" => RfModulation::AM, "Suzuki" => RfModulation::AM, "Star Line" => RfModulation::AM, + "Chrysler V0" => RfModulation::AM, + "Land Rover RKE" => RfModulation::AM, + "Toyota" => RfModulation::AM, + "BMW CAS4" => RfModulation::AM, + p if p.starts_with("Porsche Cayenne") => RfModulation::AM, p if p.starts_with("Keeloq (") => RfModulation::AM, // Both AM and FM (Kia V3/V4) p if p.starts_with("Kia V3") || p.starts_with("Kia V4") => RfModulation::Both, @@ -276,14 +300,27 @@ impl Capture { "Star Line" => "KeeLoq", p if p.starts_with("Keeloq (") => "KeeLoq", "PSA" => "XTEA/XOR", + "PSA2" => "TEA", "VAG" => "AUT64/XTEA", "Scher-Khan" => "Magic Code", "Subaru" | "Suzuki" => "Rolling Code", + "Chrysler V0" => "Rolling Code", + "Land Rover V0" => "Rolling Code", + "Land Rover RKE" => "KeeLoq", + "Toyota" => "KeeLoq", + "Mazda Siemens" => "Siemens XOR", + "BMW CAS4" => "CAS4 (rolling)", + p if p.starts_with("Porsche Cayenne") => "VAG rolling", + // Ford V1 is rolling code; must precede the generic Ford "Fixed Code" arm below. + "Ford V1" => "Rolling Code", p if p.starts_with("Ford") => "Fixed Code", + "Honda Static" => "Fixed Code", + "Honda V1" => "Fixed Code", p if p.starts_with("Fiat") => "Fixed Code", p if p.starts_with("Kia V0") => "Fixed Code", p if p.starts_with("Kia V1") || p.starts_with("Kia V2") => "Fixed Code", p if p.starts_with("Kia V5") || p.starts_with("Kia V6") => "Fixed Code", + p if p.starts_with("Kia V7") => "Fixed Code", _ => "Unknown", } } diff --git a/src/protocols/bmw_cas4.rs b/src/protocols/bmw_cas4.rs new file mode 100644 index 0000000..8be89c6 --- /dev/null +++ b/src/protocols/bmw_cas4.rs @@ -0,0 +1,461 @@ +//! BMW CAS4 protocol decoder +//! +//! Aligned with Flipper-ARF reference: `lib/subghz/protocols/bmw_cas4.c` and `bmw_cas4.h`. +//! Manchester 500/1000µs (te_delta 150), 64 bits (8 bytes), AM/OOK. The CAS4 rolling cipher's +//! manufacturer key is not available, so the encrypted portion is left as-is — the frame is only +//! framed and validated, not decrypted. Emission is gated on two fixed marker bytes +//! (byte[0]==0x30 && byte[6]==0xC5), which makes the protocol specific and prevents false matches. +//! Decode-only: the reference encoder is a non-functional stub (`yield` returns reset, +//! `deserialize` returns error), so `supports_encoding()` is false and `encode()` is None. +//! +//! Decoder steps: Reset → Preamble (≥10 pulses of 300-700µs) → Data. The preamble→data transition +//! is triggered by a long low gap (≥1800µs). Manchester polarity is `level ? Low : High` (Flipper +//! manchester_decoder.h event order: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh), the same +//! mapping as Ford V0 / common. + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use crate::radio::demodulator::LevelDuration; +use crate::duration_diff; + +const TE_SHORT: u32 = 500; +const TE_LONG: u32 = 1000; +const TE_DELTA: u32 = 150; +const DATA_BITS: usize = 64; +const DATA_BYTES: usize = 8; + +// Preamble pulse window and minimum count (BMW_CAS4_PREAMBLE_PULSE_MIN/MAX, BMW_CAS4_PREAMBLE_MIN). +const PREAMBLE_PULSE_MIN: u32 = 300; +const PREAMBLE_PULSE_MAX: u32 = 700; +const PREAMBLE_MIN: u16 = 10; +// Long low gap that separates the preamble from the data burst (BMW_CAS4_GAP_MIN). +const GAP_MIN: u32 = 1800; + +// Fixed validation markers (BMW_CAS4_BYTE0_MARKER, BMW_CAS4_BYTE6_MARKER). +const BYTE0_MARKER: u8 = 0x30; +const BYTE6_MARKER: u8 = 0xC5; + +/// Manchester state machine (Flipper manchester_decoder.h transition table). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum ManchesterState { + Mid0 = 0, + Mid1 = 1, + Start0 = 2, + Start1 = 3, +} + +/// Decoder step states (matches BmwCas4DecoderStep in bmw_cas4.c). +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + Preamble, + Data, +} + +/// BMW CAS4 protocol decoder (matches SubGhzProtocolDecoderBmwCas4). +pub struct BmwCas4Decoder { + step: DecoderStep, + manchester_state: ManchesterState, + preamble_count: u16, + raw_data: [u8; DATA_BYTES], + bit_count: usize, + decode_data: u64, +} + +impl BmwCas4Decoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + manchester_state: ManchesterState::Mid1, + preamble_count: 0, + raw_data: [0; DATA_BYTES], + bit_count: 0, + decode_data: 0, + } + } + + /// Reset accumulators (matches subghz_protocol_decoder_bmw_cas4_reset). + fn reset_state(&mut self) { + self.step = DecoderStep::Reset; + self.manchester_state = ManchesterState::Mid1; + self.preamble_count = 0; + self.raw_data = [0; DATA_BYTES]; + self.bit_count = 0; + self.decode_data = 0; + } + + fn is_short(d: u32) -> bool { + duration_diff!(d, TE_SHORT) < TE_DELTA + } + + fn is_long(d: u32) -> bool { + duration_diff!(d, TE_LONG) < TE_DELTA + } + + /// True when the duration is within the preamble pulse window (300-700µs). + fn is_preamble_pulse(d: u32) -> bool { + d >= PREAMBLE_PULSE_MIN && d <= PREAMBLE_PULSE_MAX + } + + /// Flipper Manchester transition table. Event 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh. + /// Returns Some(bit) when a bit emits. + fn manchester_advance(&mut self, event: u8) -> Option { + let (new_state, emit) = match (self.manchester_state, event) { + (ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 1) => (ManchesterState::Start1, true), + (ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true), + + (ManchesterState::Mid1, 0) => (ManchesterState::Start0, true), + (ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true), + (ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 1) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start1, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Start1, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 3) => (ManchesterState::Mid1, false), + + _ => (ManchesterState::Mid1, false), + }; + self.manchester_state = new_state; + if emit { Some((event & 1) == 1) } else { None } + } + + /// Map (level, duration) → Manchester event. BMW CAS4 polarity: level ? Low : High + /// (matches the C `event = level ? ManchesterEventShortLow : ManchesterEventShortHigh`). + fn pulse_event(level: bool, duration: u32) -> Option { + if Self::is_short(duration) { + Some(if level { 0 } else { 1 }) + } else if Self::is_long(duration) { + Some(if level { 2 } else { 3 }) + } else { + None + } + } + + /// Append a decoded bit MSB-first into raw_data and the 64-bit accumulator + /// (matches the C bit-packing in the Data step). + fn add_bit(&mut self, bit: bool) { + if self.bit_count < DATA_BITS { + let byte_idx = self.bit_count / 8; + let bit_pos = 7 - (self.bit_count % 8); + if bit { + self.raw_data[byte_idx] |= 1 << bit_pos; + } + self.decode_data = (self.decode_data << 1) | (bit as u64); + } + self.bit_count += 1; + } + + /// Validate the fixed markers and build the decoded signal. + /// Fields: serial = bytes[1..4] (24-bit), button = byte[7], counter = byte[5]. + /// The CAS4 rolling cipher is left undecrypted; the markers serve as the integrity gate. + fn build_signal(&self) -> DecodedSignal { + let b = &self.raw_data; + let serial = ((b[1] as u32) << 16) | ((b[2] as u32) << 8) | (b[3] as u32); + let counter = b[5] as u16; + let button = b[7]; + + DecodedSignal { + serial: Some(serial), + button: Some(button), + counter: Some(counter), + crc_valid: true, // fixed markers (byte[0]==0x30 && byte[6]==0xC5) validated the frame + data: self.decode_data, + data_count_bit: DATA_BITS, + encoder_capable: false, + extra: None, + protocol_display_name: None, + } + } +} + +impl ProtocolDecoder for BmwCas4Decoder { + fn name(&self) -> &'static str { + "BMW CAS4" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: DATA_BITS, + } + } + + fn supported_frequencies(&self) -> &[u32] { + // SubGhzProtocolFlag_433 only (no 315 listed in the C). + &[433_920_000] + } + + fn reset(&mut self) { + self.reset_state(); + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.step { + // Begin on a high preamble pulse within the 300-700µs window. + DecoderStep::Reset => { + if level && Self::is_preamble_pulse(duration) { + self.step = DecoderStep::Preamble; + self.preamble_count = 1; + } + } + + DecoderStep::Preamble => { + if Self::is_preamble_pulse(duration) { + self.preamble_count += 1; + } else if !level && duration >= GAP_MIN { + if self.preamble_count >= PREAMBLE_MIN { + // Enter data: clear accumulators and reset the Manchester state. + self.bit_count = 0; + self.decode_data = 0; + self.raw_data = [0; DATA_BYTES]; + self.manchester_state = ManchesterState::Mid1; + self.step = DecoderStep::Data; + } else { + self.reset_state(); + } + } else { + self.reset_state(); + } + } + + DecoderStep::Data => { + if self.bit_count >= DATA_BITS { + self.reset_state(); + return None; + } + + let event = Self::pulse_event(level, duration); + if let Some(ev) = event { + if let Some(bit) = self.manchester_advance(ev) { + self.add_bit(bit); + + if self.bit_count == DATA_BITS { + let valid = self.raw_data[0] == BYTE0_MARKER + && self.raw_data[6] == BYTE6_MARKER; + let result = if valid { Some(self.build_signal()) } else { None }; + self.reset_state(); + return result; + } + } + } else { + // Out-of-range pulse aborts the frame (matches the C ManchesterEventReset path). + self.reset_state(); + } + } + } + None + } + + fn supports_encoding(&self) -> bool { + false + } + + fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { + None + } +} + +impl Default for BmwCas4Decoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Map a Manchester event index back to its (level, duration) pulse, using the decoder's + /// polarity (level ? Low : High): 0=ShortLow(level,500), 1=ShortHigh(!level,500), + /// 2=LongLow(level,1000), 3=LongHigh(!level,1000). + fn event_pulse(event: u8) -> (bool, u32) { + match event { + 0 => (true, TE_SHORT), + 1 => (false, TE_SHORT), + 2 => (true, TE_LONG), + 3 => (false, TE_LONG), + _ => unreachable!(), + } + } + + /// Pure transition table (same as `manchester_advance`) for the test encoder. Returns + /// (next_state, Some(bit)) when a bit emits. + fn advance(state: ManchesterState, event: u8) -> (ManchesterState, Option) { + use ManchesterState::{Mid0, Mid1, Start0, Start1}; + let (ns, emit) = match (state, event) { + (Mid0, 0) => (Mid0, false), + (Mid0, 1) => (Start1, true), + (Mid0, 2) => (Mid0, false), + (Mid0, 3) => (Mid1, true), + (Mid1, 0) => (Start0, true), + (Mid1, 1) => (Mid1, false), + (Mid1, 2) => (Mid0, true), + (Mid1, 3) => (Mid1, false), + (Start0, 0) => (Mid0, false), + (Start0, 1) => (Mid0, false), + (Start0, 2) => (Mid0, false), + (Start0, 3) => (Mid1, false), + (Start1, 0) => (Mid0, false), + (Start1, 1) => (Mid1, false), + (Start1, 2) => (Mid0, false), + (Start1, 3) => (Mid1, false), + _ => (Mid1, false), + }; + (ns, if emit { Some((event & 1) == 1) } else { None }) + } + + /// Faithfully Manchester-encode `bits` into level/duration pulses by *driving the decoder's + /// own transition table*. This guarantees the produced waveform is one the decoder accepts — + /// it does not assume any closed-form biphase rule (the Flipper table is differential, so a + /// fixed per-bit pattern does not exist). Backtracking DFS over the tiny state space (with a + /// visited set on (state, last_level, bit_index) to break no-emit cycles) finds a pulse stream + /// whose decoded bits equal `bits`, respecting physical level alternation between pulses. + /// + /// Note: the decoder starts at Mid1, which can only emit a `0` first — so the first bit must be + /// 0 (true for any BMW CAS4 frame: byte[0]==0x30 begins with bit 0). + fn manchester_encode_bits(bits: &[bool]) -> Option> { + use std::collections::HashSet; + + fn dfs( + state: ManchesterState, + last_level: Option, + i: usize, + bits: &[bool], + acc: &mut Vec, + seen: &mut std::collections::HashSet<(ManchesterState, Option, usize)>, + ) -> bool { + if i == bits.len() { + return true; + } + let key = (state, last_level, i); + if seen.contains(&key) { + return false; + } + seen.insert(key); + for event in 0u8..4 { + let (level, _dur) = event_pulse(event); + if last_level == Some(level) { + continue; // physical OOK stream must alternate level between pulses + } + let (ns, bit) = advance(state, event); + match bit { + None => { + acc.push(event); + if dfs(ns, Some(level), i, bits, acc, seen) { + return true; + } + acc.pop(); + } + Some(b) if b == bits[i] => { + acc.push(event); + if dfs(ns, Some(level), i + 1, bits, acc, seen) { + return true; + } + acc.pop(); + } + _ => {} + } + } + false + } + + let mut events: Vec = Vec::new(); + let mut seen: HashSet<(ManchesterState, Option, usize)> = HashSet::new(); + if !dfs(ManchesterState::Mid1, None, 0, bits, &mut events, &mut seen) { + return None; + } + + // Translate events → pulses, merging adjacent equal levels (a repeated level is a long). + let mut merged: Vec<(bool, u32)> = Vec::new(); + for ev in events { + let (lvl, dur) = event_pulse(ev); + if let Some(last) = merged.last_mut() { + if last.0 == lvl { + last.1 += dur; + continue; + } + } + merged.push((lvl, dur)); + } + Some(merged) + } + + /// Build a full BMW CAS4 frame: preamble pulses, long low gap, then Manchester data. + fn build_frame(bytes: &[u8; DATA_BYTES]) -> Vec<(bool, u32)> { + let mut pairs: Vec<(bool, u32)> = Vec::new(); + // Preamble: 12 high pulses of ~500µs separated by short lows (within the 300-700 window). + for _ in 0..12 { + pairs.push((true, 500)); + pairs.push((false, 500)); + } + // Long low gap (≥1800µs) ending the preamble. Overwrite the last low with the gap. + if let Some(last) = pairs.last_mut() { + if !last.0 { + last.1 = GAP_MIN + 200; + } + } + // Manchester-encoded data bits, MSB-first. + let mut bits: Vec = Vec::with_capacity(DATA_BITS); + for &byte in bytes.iter() { + for i in (0..8).rev() { + bits.push((byte >> i) & 1 != 0); + } + } + let data = manchester_encode_bits(&bits).expect("test frame must be encodable (first bit 0)"); + pairs.extend(data); + // Trailing gap to flush. + pairs.push((false, GAP_MIN + 500)); + pairs + } + + /// Feed a pair stream through a fresh decoder and return the first decode (trying both + /// polarities, matching the registry behaviour). + fn decode_pairs(pairs: &[(bool, u32)]) -> Option { + for invert in [false, true] { + let mut dec = BmwCas4Decoder::new(); + for &(lvl, dur) in pairs { + let level = if invert { !lvl } else { lvl }; + if let Some(sig) = dec.feed(level, dur) { + return Some(sig); + } + } + } + None + } + + #[test] + fn decodes_synthetic_frame_with_markers() { + // byte[0]=0x30 and byte[6]=0xC5 are the required markers; the rest is the (encrypted) + // payload. serial = bytes[1..4], counter = byte[5], button = byte[7]. + let frame: [u8; DATA_BYTES] = [0x30, 0x12, 0x34, 0x56, 0xAB, 0x07, 0xC5, 0x02]; + let pairs = build_frame(&frame); + let sig = decode_pairs(&pairs).expect("synthetic BMW CAS4 frame should decode"); + + assert!(sig.crc_valid, "fixed markers should set crc_valid=true"); + assert_eq!(sig.data_count_bit, DATA_BITS); + assert_eq!(sig.data, u64::from_be_bytes(frame), "data must equal the 64-bit frame"); + assert_eq!(sig.serial, Some(0x123456), "serial = bytes[1..4]"); + assert_eq!(sig.counter, Some(0x07), "counter = byte[5]"); + assert_eq!(sig.button, Some(0x02), "button = byte[7]"); + } + + #[test] + fn rejects_frame_with_wrong_markers() { + // Same structure but byte[0] and byte[6] are NOT the markers → must not decode. + let frame: [u8; DATA_BYTES] = [0x31, 0x12, 0x34, 0x56, 0xAB, 0x07, 0xC4, 0x02]; + let pairs = build_frame(&frame); + assert!( + decode_pairs(&pairs).is_none(), + "a frame with wrong marker bytes must be rejected" + ); + } +} diff --git a/src/protocols/chrysler_v0.rs b/src/protocols/chrysler_v0.rs new file mode 100644 index 0000000..5d165e8 --- /dev/null +++ b/src/protocols/chrysler_v0.rs @@ -0,0 +1,604 @@ +//! Chrysler V0 protocol decoder/encoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/chrysler_v0.c` and +//! `chrysler_v0.h`. Used by Chrysler/Dodge/Jeep keyfobs. +//! +//! Protocol: PWM with a short HIGH pulse and TWO long-LOW symbols. A "1" payload bit is +//! HIGH≈600µs (te_one_short) + LOW≈3400µs (te_long_a); a "0" payload bit is HIGH≈300µs +//! (te_short) + LOW≈3700µs (te_long_b). te_delta≈150, long_delta≈400, te_gap≈8000, +//! frame_gap≈15600. ~24 preamble pairs (short HIGH + long_b LOW) precede each frame. +//! +//! Frame: 80 bits. The first 64 bits are `decode_data` (payload bytes 0..7), the last 16 bits +//! are `data_2` (payload bytes 8,9). The 80-bit frame exceeds u64, so [DecodedSignal::data] +//! reports the most-significant 64 bits and `data_count_bit = 80` (matches psa.rs/kia_v6.rs). +//! +//! Crypto (proprietary seed-XOR, ported exactly from chrysler_v0.c `decode`): +//! - `seed = reverse6(key[0] >> 2)` — a 6-bit reversed counter used as the transform key. +//! - `transform_block`: XOR all 9 transformed bytes with `xor_table[seed & 0x0F]`, with an extra +//! nibble flip when the (Lock) button is set. +//! - Dual payload A (seed even, carries serial+counter) / B (seed odd, carries serial). The frame +//! is gated on a structural `check_ok` (matches the C), so it does not false-match other +//! protocols. `crc_valid` reflects that `check_ok`. +//! +//! RF: AM, 315 + 433.92 MHz. Encoder present (ENABLE_EMULATE_FEATURE): builds preamble + dual +//! 80-bit PWM frames with frame gaps. + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use crate::radio::demodulator::LevelDuration; +use crate::duration_diff; + +const TE_SHORT: u32 = 0x12C; // 300 +const TE_DELTA: u32 = 0x96; // 150 +const TE_LONG_A: u32 = 0xD48; // 3400 +const TE_LONG_B: u32 = 0xE74; // 3700 +const TE_LONG_DELTA: u32 = 0x190; // 400 +const TE_GAP: u32 = 0x1F40; // 8000 +const TE_ONE_SHORT: u32 = 0x258; // 600 +const FRAME_GAP: u32 = 0x3CF0; // 15600 +const PREAMBLE_PAIRS: usize = 24; +const DECODE_BIT_COUNT: usize = 0x50; // 80 + +/// XOR table (chrysler_v0_xor_table) — indexed by `seed & 0x0F`. +const XOR_TABLE: [u8; 16] = [ + 0x0F, 0x02, 0x40, 0x0C, 0x30, 0x0E, 0x70, 0x08, 0x10, 0x0A, 0x50, 0xF4, 0x2F, 0xF6, 0x6F, 0xF0, +]; + +/// Decoder steps (matches Chrysler_V0DecoderStep) +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + Seek, + Data, +} + +/// Chrysler V0 protocol decoder +pub struct ChryslerV0Decoder { + step: DecoderStep, + packet_bit_count: u16, + te_last: u32, + decode_data: u64, + decode_count_bit: u8, + data_2: u16, +} + +/// Result of `decode_packet`: structural fields extracted from a candidate frame. +struct Decoded { + check_ok: bool, + button: u8, + /// Transform seed (reversed 6-bit counter); retained for diagnostics. + #[allow(dead_code)] + seed: u8, + /// Serial: SnA (counter-frame) or SnB (serial-frame). + serial: u32, + /// Rolling counter (only meaningful for the A/even frame). + counter: u32, +} + +impl ChryslerV0Decoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + packet_bit_count: 0, + te_last: 0, + decode_data: 0, + decode_count_bit: 0, + data_2: 0, + } + } + + fn is_short(d: u32) -> bool { + duration_diff!(d, TE_SHORT) <= TE_DELTA + } + + fn is_long_mark(d: u32) -> bool { + duration_diff!(d, TE_LONG_A) <= TE_LONG_DELTA || duration_diff!(d, TE_LONG_B) <= TE_LONG_DELTA + } + + /// Reverse the low 6 bits of `value` (chrysler_v0_reverse6). + fn reverse6(value: u32) -> u8 { + let mut out: u8 = 0; + let mut v = value; + for _ in 0..6 { + out = (out << 1) | ((v & 1) as u8); + v >>= 1; + } + out + } + + /// Transform 9 bytes with the seed-derived XOR mask (chrysler_v0_transform_block). + /// `button == 1` (Lock) flips a nibble of the mask depending on the seed parity. + fn transform_block(input: &[u8; 9], key: u8, button: u8) -> [u8; 9] { + let mut mask = XOR_TABLE[(key & 0x0F) as usize]; + if button == 1 { + mask ^= if (key & 1) != 0 { 0xF0 } else { 0x0F }; + } + let mut out = [0u8; 9]; + for i in 0..9 { + out[i] = input[i] ^ mask; + } + out + } + + /// Port of chrysler_v0_decode_packet: extract seed/button/check_ok/serial/counter from the + /// 64-bit `data` (payload bytes 0..7) plus the 16-bit `data_2` (payload bytes 8,9). + fn decode_packet(data: u64, data_2: u16) -> Decoded { + let key = data.to_be_bytes(); + let key2 = data_2; + let seed = Self::reverse6((key[0] >> 2) as u32); + + let b1_xor_b6 = key[6] ^ key[1]; + let msb_set = (key[0] & 0x80) != 0; + + let mut check_ok; + let mut button: u8; + + if msb_set { + let key2_low = (key2 & 0xFF) as u8; + check_ok = (key[1] == key[5]) && (b1_xor_b6 == 0x62); + button = if (key2_low ^ key[4]) == 0x10 { 2 } else { 1 }; + } else { + check_ok = false; + button = 1; + + if (key[1] ^ 0xC3) == key[5] { + if b1_xor_b6 == 0x04 { + check_ok = true; + } else { + check_ok = b1_xor_b6 == 0x08; + if b1_xor_b6 == 0x08 { + button = 2; + } + } + } else if b1_xor_b6 == 0x08 { + button = 2; + } + // (b1_xor_b6 == 0x04 with mismatched key[5]: button stays 1, check_ok stays false.) + } + + let encoded: [u8; 9] = [ + key[1], + key[2], + key[3], + key[4], + key[5], + key[6], + key[7], + (key2 >> 8) as u8, + (key2 & 0xFF) as u8, + ]; + let decoded = Self::transform_block(&encoded, seed, button); + + let (serial, counter) = if (seed & 1) != 0 { + // Payload B: serial only. + let sn_b = ((decoded[0] as u32) << 24) + | ((decoded[1] as u32) << 16) + | ((decoded[2] as u32) << 8) + | (decoded[7] as u32); + (sn_b, 0u32) + } else { + // Payload A: serial (SnA) + rolling counter. + let sn_a = ((decoded[0] as u32) << 24) + | ((decoded[1] as u32) << 16) + | ((decoded[2] as u32) << 8) + | (decoded[3] as u32); + let cnt = sn_a; + (sn_a, cnt) + }; + + Decoded { + check_ok, + button, + seed, + serial, + counter, + } + } + + /// Build a DecodedSignal from a committed frame, if the structural check passes. + fn commit(&self) -> Option { + let d = Self::decode_packet(self.decode_data, self.data_2); + if !d.check_ok { + return None; + } + Some(DecodedSignal { + serial: Some(d.serial), + button: Some(d.button), + counter: Some((d.counter & 0xFFFF) as u16), + crc_valid: d.check_ok, + // 80-bit frame: report the most-significant 64 bits (payload bytes 0..7). + data: self.decode_data, + data_count_bit: DECODE_BIT_COUNT, + encoder_capable: true, + // Stash the low 16 bits (data_2 = payload bytes 8,9) for the encoder. + extra: Some(self.data_2 as u64), + protocol_display_name: None, + }) + } + + /// Map a KAT generic button to a Chrysler button code (1=Lock, 2=Unlock). + fn map_button(button: u8) -> u8 { + match button { + 0x01 => 1, // Lock + 0x02 => 2, // Unlock + 0x04 => 1, // Trunk → Lock (Chrysler V0 only models Lock/Unlock) + 0x08 => 2, // Panic → Unlock + _ => 1, + } + } + + /// Read one bit out of an MSB-first payload (chrysler_v0_payload_get_bit). + fn payload_get_bit(payload: &[u8; 10], index: u8) -> u8 { + let byte = payload[(index >> 3) as usize]; + let shift = 7 - (index & 7); + (byte >> shift) & 1 + } + + /// Build a 10-byte payload from the 9 plaintext bytes (chrysler_v0_build_payload). + fn build_payload(plain: &[u8; 9], counter: u8, button: u8, header_low2: u8) -> [u8; 10] { + let transformed = Self::transform_block(plain, counter, button); + let mut out = [0u8; 10]; + out[0] = (Self::reverse6(counter as u32) << 2) | (header_low2 & 0x03); + out[1..10].copy_from_slice(&transformed); + out + } + + /// ADD_LEVEL-style merge: combine adjacent same-level pulses. + fn add_level(signal: &mut Vec, level: bool, duration: u32) { + if let Some(last) = signal.last_mut() { + if last.level == level { + *last = LevelDuration::new(level, last.duration_us + duration); + return; + } + } + signal.push(LevelDuration::new(level, duration)); + } + + /// Emit one 80-bit PWM payload frame (without the leading preamble). + fn emit_payload(signal: &mut Vec, payload: &[u8; 10]) { + for bit in 0..80u8 { + let value = Self::payload_get_bit(payload, bit); + if value != 0 { + Self::add_level(signal, true, TE_ONE_SHORT); + Self::add_level(signal, false, TE_LONG_A); + } else { + Self::add_level(signal, true, TE_SHORT); + Self::add_level(signal, false, TE_LONG_B); + } + } + } + + /// Emit the preamble (24 short-HIGH + long_b-LOW pairs). + fn emit_preamble(signal: &mut Vec) { + for _ in 0..PREAMBLE_PAIRS { + Self::add_level(signal, true, TE_SHORT); + Self::add_level(signal, false, TE_LONG_B); + } + } +} + +impl ProtocolDecoder for ChryslerV0Decoder { + fn name(&self) -> &'static str { + "Chrysler V0" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG_A, + te_delta: TE_DELTA, + min_count_bit: DECODE_BIT_COUNT, + } + } + + fn supported_frequencies(&self) -> &[u32] { + &[315_000_000, 433_920_000] + } + + fn reset(&mut self) { + self.step = DecoderStep::Reset; + self.packet_bit_count = 0; + self.te_last = 0; + self.decode_data = 0; + self.decode_count_bit = 0; + self.data_2 = 0; + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.step { + DecoderStep::Reset => { + if level && Self::is_short(duration) { + self.packet_bit_count = 0; + self.te_last = duration; + self.step = DecoderStep::Seek; + } + } + + DecoderStep::Seek => { + if level { + self.te_last = duration; + return None; + } + + if Self::is_long_mark(duration) { + if Self::is_short(self.te_last) { + self.packet_bit_count += 1; + } else if self.packet_bit_count > 0x0F { + self.data_2 = 0; + self.step = DecoderStep::Data; + self.decode_data = 1; + self.decode_count_bit = 1; + } else { + self.packet_bit_count = 0; + self.step = DecoderStep::Seek; + } + return None; + } + + if duration > TE_GAP && self.packet_bit_count > 0x0F { + self.decode_data = 0; + self.data_2 = 0; + self.decode_count_bit = 0; + self.step = DecoderStep::Data; + return None; + } + + self.step = DecoderStep::Reset; + self.packet_bit_count = 0; + } + + DecoderStep::Data => { + if level { + self.te_last = duration; + return None; + } + + let count = self.decode_count_bit; + + if duration > TE_GAP { + let result = if count as usize > 0x4F { self.commit() } else { None }; + self.step = DecoderStep::Reset; + self.packet_bit_count = 0; + return result; + } + + let bit_value: u8; + if self.te_last < TE_SHORT { + if !Self::is_short(self.te_last) || !Self::is_long_mark(duration) { + let result = if count as usize > 0x4F { self.commit() } else { None }; + self.step = DecoderStep::Reset; + self.packet_bit_count = 0; + return result; + } + bit_value = 1; + } else { + if self.te_last > 0x2EE || !Self::is_long_mark(duration) { + let result = if count as usize > 0x4F { self.commit() } else { None }; + self.step = DecoderStep::Reset; + self.packet_bit_count = 0; + return result; + } + bit_value = if Self::is_short(self.te_last) { 1 } else { 0 }; + } + + let bit = (bit_value ^ 1) as u64; + let new_count = count.wrapping_add(1); + if count <= 0x3F { + self.decode_data = (self.decode_data << 1) | bit; + self.decode_count_bit = new_count; + return None; + } + + self.data_2 = (self.data_2 << 1) | (bit as u16); + self.decode_count_bit = new_count; + if new_count as usize != DECODE_BIT_COUNT { + return None; + } + + let result = self.commit(); + self.decode_data = 0; + self.data_2 = 0; + self.decode_count_bit = 0; + self.step = DecoderStep::Reset; + self.packet_bit_count = 0; + return result; + } + } + + None + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + // Rebuild the 10-byte payload A (high 64 bits + low 16 bits) from the decoded frame, + // re-derive seed/plaintext, then emit preamble + dual PWM frames (matches + // chrysler_v0_build_upload, ENABLE_EMULATE_FEATURE). + let data = decoded.data; + let data_2 = decoded.extra.unwrap_or(0) as u16; + let key = data.to_be_bytes(); + let key2 = data_2; + + // Header low-2 bits live in the top byte's low nibble (matches encoder deserialize: + // plain_header = (data >> 56) & 0x03). + let header_low2 = (key[0]) & 0x03; + let seed = Self::reverse6((key[0] >> 2) as u32); + + // Determine the originally-transmitted button (matches encoder deserialize). + let original_button: u8 = if (key[0] & 0x80) == 0 { + if (key[1] ^ key[6]) == 0x08 { 2 } else { 1 } + } else if (((key2 & 0xFF) as u8) ^ key[4]) == 0x10 { + 2 + } else { + 1 + }; + + // Recover the plaintext (Plain_A/Plain_B both default to this in the C when no explicit + // Plain_A/B is stored — which is our case). + let encoded: [u8; 9] = [ + key[1], + key[2], + key[3], + key[4], + key[5], + key[6], + key[7], + (key2 >> 8) as u8, + (key2 & 0xFF) as u8, + ]; + let generated = Self::transform_block(&encoded, seed, original_button); + let mut plain_a = generated; + let mut plain_b = generated; + + // Apply the requested button (KAT button → Chrysler 1/2). + let tx_button = Self::map_button(button); + if tx_button != original_button { + plain_a[5] ^= 0x0C; + plain_b[3] ^= 0x30; + } + + // Counter handling (matches encoder deserialize): counter_a is the even counter, counter_b + // is counter_a - 1 (wrapping in 6 bits). Derive the base counter from the seed. + let counter = (seed as u32) & 0x3F; + let mut counter_a = (counter & 0x3F) as u8; + if (counter_a & 1) != 0 { + counter_a = counter_a.wrapping_sub(1) & 0x3F; + } + let counter_b = if counter_a == 0 { 0x3F } else { counter_a - 1 }; + + let payload_a = Self::build_payload(&plain_a, counter_a, tx_button, header_low2); + let payload_b = Self::build_payload(&plain_b, counter_b, tx_button, header_low2); + + let mut signal = Vec::with_capacity(PREAMBLE_PAIRS * 4 + 80 * 4 + 16); + + // Frame A: preamble, short + frame_gap, payload A, short + frame_gap. + Self::emit_preamble(&mut signal); + Self::add_level(&mut signal, true, TE_SHORT); + Self::add_level(&mut signal, false, FRAME_GAP); + Self::emit_payload(&mut signal, &payload_a); + Self::add_level(&mut signal, true, TE_SHORT); + Self::add_level(&mut signal, false, FRAME_GAP); + + // Frame B: preamble, short + frame_gap, payload B, short + frame_gap. + Self::emit_preamble(&mut signal); + Self::add_level(&mut signal, true, TE_SHORT); + Self::add_level(&mut signal, false, FRAME_GAP); + Self::emit_payload(&mut signal, &payload_b); + Self::add_level(&mut signal, true, TE_SHORT); + Self::add_level(&mut signal, false, FRAME_GAP); + + Some(signal) + } +} + +impl Default for ChryslerV0Decoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Decode a stream of pairs with a fresh decoder, returning the first frame. + fn decode_stream(pairs: &[LevelDuration]) -> Option { + let mut dec = ChryslerV0Decoder::new(); + for p in pairs { + if let Some(sig) = dec.feed(p.level, p.duration_us) { + return Some(sig); + } + } + None + } + + /// reverse6 is its own inverse on 6-bit values. + #[test] + fn reverse6_is_involution() { + for v in 0u32..64 { + let r = ChryslerV0Decoder::reverse6(v) as u32; + assert_eq!(ChryslerV0Decoder::reverse6(r) as u32, v, "reverse6 not involutive at {v}"); + } + } + + /// transform_block is its own inverse for a given (seed, button) — XOR with a constant mask. + #[test] + fn transform_block_round_trips() { + let plain: [u8; 9] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99]; + for &button in &[1u8, 2u8] { + for seed in 0u8..64 { + let enc = ChryslerV0Decoder::transform_block(&plain, seed, button); + let dec = ChryslerV0Decoder::transform_block(&enc, seed, button); + assert_eq!(dec, plain, "transform not invertible seed={seed} button={button}"); + } + } + } + + /// Build a payload-A frame exactly as the encoder does (even counter, MSB-clear path), encode + /// it to a PWM burst, decode it back, and confirm the frame, check_ok, button and serial all + /// survive the round trip. Payload A is the first frame the encoder emits, so `decode_stream` + /// returns it. + /// + /// Payload-A check_ok requires (in the stored = transformed bytes): `(key[1]^0xC3)==key[5]` + /// and `key[6]^key[1]==0x04`. Since transform XORs every byte with a constant mask, this is + /// equivalent to a check on the *plaintext*: `plain[0]^0xC3==plain[4]` and `plain[5]^plain[0]==0x04`. + #[test] + fn encode_decode_round_trip() { + // Choose an even counter so the encoder's counter_a equals it (exact round trip), and pick + // plaintext satisfying the payload-A invariants. + let counter_a: u8 = 0x14; // even + let button: u8 = 1; // Lock (matches the b1^b6==0x04 branch which keeps button=1) + let header_low2: u8 = 0x02; + + let p0 = 0x5Au8; + let mut plain = [0x00u8; 9]; + plain[0] = p0; + plain[1] = 0x11; + plain[2] = 0x22; + plain[3] = 0x33; + plain[4] = p0 ^ 0xC3; // plain[0]^0xC3 == plain[4] + plain[5] = p0 ^ 0x04; // plain[5]^plain[0] == 0x04 + plain[6] = 0x66; + plain[7] = 0x77; + plain[8] = 0x88; + + // Build the 10-byte payload the way the encoder/transmitter does. + let payload = ChryslerV0Decoder::build_payload(&plain, counter_a, button, header_low2); + let data = u64::from_be_bytes([ + payload[0], payload[1], payload[2], payload[3], payload[4], payload[5], payload[6], + payload[7], + ]); + let data_2: u16 = ((payload[8] as u16) << 8) | payload[9] as u16; + + // Sanity: the raw frame must pass the structural check before we exercise the codec. + let d = ChryslerV0Decoder::decode_packet(data, data_2); + assert!(d.check_ok, "constructed payload-A vector should satisfy check_ok"); + assert_eq!(d.button, button, "constructed button mismatch"); + assert_eq!(d.seed & 1, 0, "payload A must have an even seed"); + + let decoded = DecodedSignal { + serial: Some(d.serial), + button: Some(d.button), + counter: Some((d.counter & 0xFFFF) as u16), + crc_valid: true, + data, + data_count_bit: DECODE_BIT_COUNT, + encoder_capable: true, + extra: Some(data_2 as u64), + protocol_display_name: None, + }; + + let dec = ChryslerV0Decoder::new(); + // Encode with the same (Lock) button so the plaintext is not perturbed. + let burst = dec.encode(&decoded, 0x01).expect("encode should succeed"); + assert!(!burst.is_empty()); + + let got = decode_stream(&burst).expect("encoded burst should decode"); + assert!(got.crc_valid, "decoded frame should pass check"); + // The high 64 bits (payload bytes 0..7) must reproduce the original frame. + assert_eq!(got.data, data, "round-trip data mismatch"); + assert_eq!(got.button, decoded.button, "button mismatch"); + assert_eq!(got.serial, decoded.serial, "serial mismatch"); + } +} diff --git a/src/protocols/ford_v1.rs b/src/protocols/ford_v1.rs new file mode 100644 index 0000000..12674be --- /dev/null +++ b/src/protocols/ford_v1.rs @@ -0,0 +1,778 @@ +//! Ford V1 protocol decoder/encoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v1.c` and `ford_v1.h`. +//! Manchester 65/130µs (te_delta 39), FM. 136 bits / 17 bytes: key1 (bytes 0..7, 56 bits) + +//! key2 (bytes 7..15, 64 bits) + CRC16 (bytes 15..16). Preamble ≥50 long pulses, then a short-pulse +//! sync window (`sync_event_count > 2`) replays buffered Manchester events and enters the 17-byte +//! data collection. This is a ROLLING-code protocol. +//! +//! Crypto: a proprietary parity-based descrambling cipher (`ford_v1_decode_with_flag`) operating on +//! the 9-byte air block `raw[6..15]`, plus CRC16/CCITT (poly 0x1021, init 0x0000) over `raw[3..15]`. +//! Emission is gated on CRC16 validity (with a 17-byte bit-inverted fallback) so it never +//! false-matches. Encryption/rolling detection mirrors the C: a strict branch +//! (`decoded[3]==raw[5] && decoded[4]==raw[6]`) yields plaintext serial/button/counter; otherwise an +//! encode round-trip check classifies it as encrypted/rolling. Encoder supported (6 bursts). +//! +//! Manchester transition table is the Flipper differential-Manchester table (same as Ford V0/V2). + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use crate::radio::demodulator::LevelDuration; +use crate::duration_diff; + +const TE_SHORT: u32 = 65; +const TE_LONG: u32 = 130; +const TE_DELTA: u32 = 39; +const DATA_BITS: usize = 136; +const DATA_BYTES: usize = 17; +const PREAMBLE_MIN: u16 = 50; +/// C uses FORD_V1_DELTA_LONG (40) only for the preamble long-pulse match; data/sync use te_delta (39). +const DELTA_LONG: u32 = 40; +const SILENCE_LONG_MULT: u32 = 3; + +// Manchester event encoding (matches Flipper ManchesterEvent ordinals used in the C buffer): +// 0 = ShortLow, 1 = ShortHigh, 2 = LongLow, 3 = LongHigh. +const EV_SHORT_LOW: u8 = 0; +const EV_SHORT_HIGH: u8 = 1; +const EV_LONG_LOW: u8 = 2; +const EV_LONG_HIGH: u8 = 3; + +// Encoder constants (subghz_protocol_encoder_ford_v1). +const ENC_BURST_COUNT: usize = 6; +const ENC_PREAMBLE_PAIRS: usize = 400; +const ENC_SYNC_SHORT_US: u32 = 65; +const ENC_SYNC_LONG_US: u32 = 130; +const ENC_GAP_REPEAT_US: u32 = 50000; +const ENC_GAP_LAST_US: u32 = 260; +/// Per-burst override of pkt[4] (matches ford_v1_encoder_burst_pkt4_vals). +const ENC_BURST_PKT4: [u8; 6] = [0x08, 0x00, 0x10, 0x08, 0x00, 0x10]; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ManchesterState { + Mid0 = 0, + Mid1 = 1, + Start0 = 2, + Start1 = 3, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + Preamble, + Sync, + Data, +} + +pub struct FordV1Decoder { + step: DecoderStep, + manchester_state: ManchesterState, + preamble_count: u16, + decode_data: u64, + decode_count_bit: usize, + byte_count: usize, + raw_bytes: [u8; DATA_BYTES + 1], + sync_event_idx: u8, + sync_event_count: u8, + sync_events: [u8; 8], +} + +impl FordV1Decoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + manchester_state: ManchesterState::Mid1, + preamble_count: 0, + decode_data: 0, + decode_count_bit: 0, + byte_count: 0, + raw_bytes: [0; DATA_BYTES + 1], + sync_event_idx: 0, + sync_event_count: 0, + sync_events: [0; 8], + } + } + + fn reset_state(&mut self) { + self.step = DecoderStep::Reset; + self.manchester_state = ManchesterState::Mid1; + self.preamble_count = 0; + self.decode_data = 0; + self.decode_count_bit = 0; + self.byte_count = 0; + self.raw_bytes = [0; DATA_BYTES + 1]; + self.sync_event_idx = 0; + self.sync_event_count = 0; + self.sync_events = [0; 8]; + } + + /// Short/long matchers. Data and sync use te_delta (39); preamble-long uses DELTA_LONG (40). + fn is_short(d: u32) -> bool { + duration_diff!(d, TE_SHORT) < TE_DELTA + } + fn is_long(d: u32) -> bool { + duration_diff!(d, TE_LONG) < TE_DELTA + } + fn is_preamble_long(d: u32) -> bool { + duration_diff!(d, TE_LONG) < DELTA_LONG + } + + /// Flipper differential-Manchester transition table (same as Ford V0/V2/Kia V7). + /// Event: 0=ShortLow,1=ShortHigh,2=LongLow,3=LongHigh. Returns Some(bit) when a bit emits. + fn manchester_advance(&mut self, event: u8) -> Option { + let (new_state, emit) = match (self.manchester_state, event) { + (ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 1) => (ManchesterState::Start1, true), + (ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true), + + (ManchesterState::Mid1, 0) => (ManchesterState::Start0, true), + (ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true), + (ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 1) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start1, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Start1, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 3) => (ManchesterState::Mid1, false), + + _ => (ManchesterState::Mid1, false), + }; + self.manchester_state = new_state; + if emit { Some((event & 1) == 1) } else { None } + } + + /// Push a decoded Manchester bit into the byte buffer (matches C feed/data byte assembly). + fn push_bit(&mut self, data_bit: bool) { + self.decode_data = (self.decode_data << 1) | (data_bit as u64); + self.decode_count_bit += 1; + if self.decode_count_bit & 7 == 0 { + let byte_val = (self.decode_data & 0xFF) as u8; + if self.byte_count < DATA_BYTES { + self.raw_bytes[self.byte_count] = byte_val; + self.byte_count += 1; + } + self.decode_data = 0; + } + } + + // ========================================================================= + // CRC16/CCITT (poly 0x1021, init 0x0000) — module-local, matches + // subghz_protocol_blocks_crc16(data, len, 0x1021, 0x0000). + // ========================================================================= + fn crc16(data: &[u8]) -> u16 { + let mut crc: u16 = 0x0000; + for &byte in data { + crc ^= (byte as u16) << 8; + for _ in 0..8 { + if crc & 0x8000 != 0 { + crc = (crc << 1) ^ 0x1021; + } else { + crc <<= 1; + } + } + } + crc + } + + // ========================================================================= + // Descrambling cipher (ford_v1_decode_with_flag) — operates on the 9-byte + // air block in place. Matches the C exactly. + // ========================================================================= + fn decode_with_flag(raw: &mut [u8; 9], flag_byte: u8) { + if flag_byte != 0 { + let xor_byte = raw[7]; + for i in 1..7 { + raw[i] ^= xor_byte; + } + } else { + let xor_byte = raw[6]; + for i in 1..6 { + raw[i] ^= xor_byte; + } + raw[7] ^= xor_byte; + } + + let b6 = raw[6]; + let b7 = raw[7]; + raw[6] = (b6 & 0xAA) | (b7 & 0x55); + raw[7] = (b7 & 0xAA) | (b6 & 0x55); + } + + /// Parity-driven descramble used when neither strict branch matches (ford_v1_decode). + fn decode_air(raw: &mut [u8; 9]) { + let endbyte = raw[8]; + let parity_any = endbyte != 0; + let mut parity = 0u8; + let mut tmp = endbyte; + while tmp != 0 { + parity ^= tmp & 1; + tmp >>= 1; + } + let flag_byte = if parity_any { parity } else { 0 }; + Self::decode_with_flag(raw, flag_byte); + } + + // ========================================================================= + // Inverse cipher (encoder side) — ford_v1_encode_inverse_block. Takes a + // 9-byte plaintext block and produces the air (scrambled) block in place. + // ========================================================================= + fn encode_inverse_block(block: &mut [u8; 9]) { + let mut sum: u8 = 0; + for i in 1..=7 { + sum = sum.wrapping_add(block[i]); + } + + let p6 = block[6]; + let p7 = block[7]; + let post6 = (p6 & 0xAA) | (p7 & 0x55); + let post7 = (p7 & 0xAA) | (p6 & 0x55); + let xorv = post6 ^ post7; + + let xor_byte; + if (sum.count_ones() & 1) != 0 { + block[6] = xorv; + block[7] = post7; + xor_byte = post7; + } else { + block[6] = post6; + block[7] = xorv; + xor_byte = post6; + } + + for i in 1..=5 { + block[i] ^= xor_byte; + } + } + + fn encode_air_9bytes(plain9: &[u8; 9]) -> [u8; 9] { + let mut block = *plain9; + Self::encode_inverse_block(&mut block); + block + } + + /// Recover plaintext from an air block by trying both descramble flags and verifying + /// the result re-encodes to the same air bytes (ford_v1_plain_from_air). + fn plain_from_air(air9: &[u8; 9]) -> Option<[u8; 9]> { + for flag in 0u8..2 { + let mut cand = *air9; + Self::decode_with_flag(&mut cand, flag); + let reair = Self::encode_air_9bytes(&cand); + if reair == *air9 { + return Some(cand); + } + } + None + } + + /// Extract serial/button/counter from a plaintext 9-byte block (ford_v1_fields_from_plain). + fn fields_from_plain(plain9: &[u8; 9]) -> (u32, u8, u32) { + let serial = ((plain9[1] as u32) << 24) + | ((plain9[2] as u32) << 16) + | ((plain9[3] as u32) << 8) + | (plain9[0] as u32); + let btn = (plain9[5] >> 4) & 0x0F; + let cnt = (((plain9[5] & 0x0F) as u32) << 16) | ((plain9[6] as u32) << 8) | (plain9[7] as u32); + (serial, btn, cnt) + } + + // ========================================================================= + // process_data (ford_v1_process_data): CRC16 gate (+ 17-byte inverted + // fallback), descramble, field extraction, dual-branch classification. + // Returns Some(DecodedSignal) when CRC passes. + // ========================================================================= + fn process_data(&self) -> Option { + let mut raw = [0u8; DATA_BYTES]; + raw.copy_from_slice(&self.raw_bytes[..DATA_BYTES]); + + let mut calc_crc = Self::crc16(&raw[3..15]); + let mut recv_crc = ((raw[15] as u16) << 8) | raw[16] as u16; + + // Fallback: bit-invert all 17 bytes and retry the CRC (matches C). + if recv_crc != calc_crc { + for (i, b) in raw.iter_mut().enumerate() { + *b = !self.raw_bytes[i]; + } + calc_crc = Self::crc16(&raw[3..15]); + recv_crc = ((raw[15] as u16) << 8) | raw[16] as u16; + } + + if recv_crc != calc_crc { + return None; + } + + // Air block = raw[6..15] (9 bytes). Try both descramble branches "strictly". + let mut air9 = [0u8; 9]; + air9.copy_from_slice(&raw[6..15]); + + let mut decoded_b0 = air9; + Self::decode_with_flag(&mut decoded_b0, 0); + let mut decoded_b1 = air9; + Self::decode_with_flag(&mut decoded_b1, 1); + + let (decoded, strict_ok): ([u8; 9], bool) = + if decoded_b0[3] == raw[5] && decoded_b0[4] == raw[6] { + (decoded_b0, true) + } else if decoded_b1[3] == raw[5] && decoded_b1[4] == raw[6] { + (decoded_b1, true) + } else if let Some(p) = Self::plain_from_air(&air9) { + // Encrypted/rolling: round-trip recovered but not a strict cleartext match. + (p, false) + } else { + let mut p = air9; + Self::decode_air(&mut p); + (p, false) + }; + + let recalc_crc = Self::crc16(&raw[3..15]); + + // key1 = raw[0..7] (56 bits, big-endian) → DecodedSignal.data. + let mut key1: u64 = 0; + for &b in raw.iter().take(7) { + key1 = (key1 << 8) | b as u64; + } + + let (serial, button, counter) = if strict_ok { + let (s, b, c) = Self::fields_from_plain(&decoded); + (s, b, c) + } else { + // Header-only: device id from raw[3..7], no button/counter (matches C). + let device_id = ((raw[3] as u32) << 24) + | ((raw[4] as u32) << 16) + | ((raw[5] as u32) << 8) + | (raw[6] as u32); + (device_id, 0u8, 0u32) + }; + + // Stash everything the encoder needs to rebuild the full 17-byte frame from fields. + // `data` already carries key1 = raw[0..7] (56 bits). The air block raw[6..15] and the + // CRC bytes are regenerated by re-encoding the plaintext, which is fully determined by + // serial/button/counter EXCEPT plain[4] (the byte the strict branch constrains, equal to + // air9[0]) and the derived checksum plain[8]. So we stash plain[4] plus the strict flag. + // Layout (u64, MSB first): + // [63:48]=crc16 [47:40]=strict_ok [39:32]=plain[4] [31:0]=0 (reserved). + let plain4 = decoded[4]; + let extra = ((recalc_crc as u64) << 48) + | ((strict_ok as u64) << 40) + | ((plain4 as u64) << 32); + + Some(DecodedSignal { + serial: Some(serial), + button: Some(button), + counter: Some(counter as u16), + crc_valid: true, + data: key1, + data_count_bit: DATA_BITS, + encoder_capable: true, + extra: Some(extra), + protocol_display_name: None, + }) + } + + /// Map KAT's generic button command to a Ford V1 4-bit button code + /// (Sync=0, Lock=1, Unlock=2, Trunk=4, Panic=8 — matches ford_v1_get_button_name). + fn map_button(button: u8) -> u8 { + match button { + 0x01 => 0x01, // Lock + 0x02 => 0x02, // Unlock + 0x04 => 0x04, // Trunk + 0x08 => 0x08, // Panic + b => b & 0x0F, + } + } + + /// Apply serial/button/counter onto a plaintext 9-byte block (ford_v1_plain_apply_fields). + fn plain_apply_fields(plain9: &mut [u8; 9], serial: u32, btn: u8, cnt: u32) { + let chk = plain9[8] + .wrapping_sub(plain9[6]) + .wrapping_sub(plain9[7]) + .wrapping_sub(plain9[5]); + plain9[0] = (serial & 0xFF) as u8; + plain9[1] = ((serial >> 24) & 0xFF) as u8; + plain9[2] = ((serial >> 16) & 0xFF) as u8; + plain9[3] = ((serial >> 8) & 0xFF) as u8; + plain9[5] = (((btn & 0x0F) << 4) | (((cnt >> 16) & 0x0F) as u8)) as u8; + plain9[6] = ((cnt >> 8) & 0xFF) as u8; + plain9[7] = (cnt & 0xFF) as u8; + plain9[8] = chk + .wrapping_add(plain9[7]) + .wrapping_add(plain9[6]) + .wrapping_add(plain9[5]); + } + + /// Rebuild the air block (raw[6..15]) and CRC16 (raw[15..17]) from a plaintext block + /// (ford_v1_encoder_rebuild_raw_from_plain). + fn rebuild_raw_from_plain(raw17: &mut [u8; DATA_BYTES], plain9: &[u8; 9]) { + let air9 = Self::encode_air_9bytes(plain9); + raw17[6..15].copy_from_slice(&air9); + let c = Self::crc16(&raw17[3..15]); + raw17[15] = (c >> 8) as u8; + raw17[16] = (c & 0xFF) as u8; + } + + fn enc_add_level(signal: &mut Vec, level: bool, duration: u32) { + if let Some(last) = signal.last_mut() { + if last.level == level { + *last = LevelDuration::new(level, last.duration_us + duration); + return; + } + } + signal.push(LevelDuration::new(level, duration)); + } +} + +impl ProtocolDecoder for FordV1Decoder { + fn name(&self) -> &'static str { + "Ford V1" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: DATA_BITS, + } + } + + fn supported_frequencies(&self) -> &[u32] { + &[315_000_000, 433_920_000] + } + + fn reset(&mut self) { + self.reset_state(); + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.step { + // C: !level && long → Preamble; seed preamble_count = 1. + DecoderStep::Reset => { + if !level && Self::is_preamble_long(duration) { + self.step = DecoderStep::Preamble; + self.preamble_count = 1; + } + } + + DecoderStep::Preamble => { + if Self::is_preamble_long(duration) { + self.preamble_count = self.preamble_count.saturating_add(1); + } else if Self::is_short(duration) { + if self.preamble_count >= PREAMBLE_MIN { + // Enter Sync: buffer the first short event. + self.sync_event_idx = 0; + self.sync_event_count = 1; + self.sync_events[0] = if level { EV_SHORT_HIGH } else { EV_SHORT_LOW }; + self.step = DecoderStep::Sync; + } else { + self.step = DecoderStep::Reset; + } + } else if self.preamble_count < PREAMBLE_MIN { + self.step = DecoderStep::Reset; + } + // else: stay in Preamble (long preamble already satisfied, ignore stray pulse) + } + + DecoderStep::Sync => { + let (ev, is_short) = if Self::is_short(duration) { + (if level { EV_SHORT_HIGH } else { EV_SHORT_LOW }, true) + } else if Self::is_long(duration) { + (if level { EV_LONG_HIGH } else { EV_LONG_LOW }, false) + } else { + self.step = DecoderStep::Preamble; + return None; + }; + + self.sync_event_idx += 1; + if is_short { + self.sync_event_count += 1; + } + if (self.sync_event_idx as usize) < 8 { + self.sync_events[self.sync_event_idx as usize] = ev; + } + + if self.sync_event_count > 2 { + // Sync detected: reset the bit buffer and replay buffered events into Manchester. + self.decode_data = 0; + self.decode_count_bit = 0; + self.byte_count = 0; + self.raw_bytes = [0; DATA_BYTES + 1]; + self.manchester_state = ManchesterState::Mid1; + if self.sync_events[0] == EV_SHORT_LOW { + self.manchester_state = ManchesterState::Mid0; + } + self.step = DecoderStep::Data; + + let last = self.sync_event_idx.min(7); + for i in 0..=last { + let event = self.sync_events[i as usize]; + if let Some(data_bit) = self.manchester_advance(event) { + self.push_bit(data_bit); + } + } + return None; + } + + if self.sync_event_idx >= 7 { + self.step = DecoderStep::Preamble; + } + } + + DecoderStep::Data => { + let event = if Self::is_short(duration) { + if level { EV_SHORT_HIGH } else { EV_SHORT_LOW } + } else if Self::is_long(duration) { + if level { EV_LONG_HIGH } else { EV_LONG_LOW } + } else { + // Idle gap / odd pulse. The C decoder attempts partial-last-byte variants only + // when byte_count==16 and 1-2 bits short; we require all 17 bytes, so any + // non-short/long pulse (including very long inter-burst gaps, dur >= + // te_long*SILENCE_LONG_MULT) ends the attempt and resets so the next burst can + // re-sync from Reset. + let _ = duration >= TE_LONG * SILENCE_LONG_MULT; + self.reset_state(); + return None; + }; + + if let Some(data_bit) = self.manchester_advance(event) { + self.push_bit(data_bit); + + if self.byte_count > 16 { + let result = self.process_data(); + self.reset_state(); + return result; + } + } + } + } + None + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + let serial = decoded.serial?; + let counter = decoded.counter.unwrap_or(0) as u32 & 0xF_FFFF; + let btn = Self::map_button(button) & 0x0F; + + // We can faithfully re-encode only when the original frame's plaintext was recovered at + // decode time (strict branch). `extra` carries: [63:48]=crc16, [47:40]=strict_ok, + // [39:32]=plain[4] (the byte the strict branch constrains). + let extra = decoded.extra?; + let strict_ok = ((extra >> 40) & 0x01) != 0; + if !strict_ok { + return None; + } + let plain4 = ((extra >> 32) & 0xFF) as u8; + + // raw[0..7] = key1 (56 bits), taken from `data` (low 56 bits). The decoder packs key1 as + // raw[0]<<48 | … | raw[6], so the top 7 bytes of the 56-bit value are raw[0..7]. + let mut raw17 = [0u8; DATA_BYTES]; + for (i, b) in raw17.iter_mut().take(7).enumerate() { + *b = (decoded.data >> (48 - i * 8)) as u8; + } + + // Reconstruct the plaintext block from the decoded fields plus the stashed plain[4]. + // plain_apply_fields fills [0,1,2,3,5,6,7,8]; plain[4] is preserved from `extra`. The + // strict-branch invariant the C decoder verified is plain[4]==air9[0]==raw[6] and + // plain[3]==raw[5]; re-encoding plain reproduces the air block and hence those bytes. + let mut plain9 = [0u8; 9]; + plain9[4] = plain4; + // Seed checksum-bearing fields with the *decoded* values so the running checksum delta in + // plain_apply_fields starts from the original plaintext's relationship, then apply the new + // button/counter (same counter; no increment, matching Ford V0's replay policy). + plain9[5] = (((decoded.button.unwrap_or(0) & 0x0F) << 4) + | (((counter >> 16) & 0x0F) as u8)) as u8; + plain9[6] = ((counter >> 8) & 0xFF) as u8; + plain9[7] = (counter & 0xFF) as u8; + plain9[8] = plain9[5].wrapping_add(plain9[6]).wrapping_add(plain9[7]); + Self::plain_apply_fields(&mut plain9, serial, btn, counter); + + // Rebuild raw[5] = plain[3] (strict invariant) and the air block + CRC from plaintext. + raw17[5] = plain9[3]; + Self::rebuild_raw_from_plain(&mut raw17, &plain9); + + // Build the 6-burst upload. + let mut signal = + Vec::with_capacity(ENC_BURST_COUNT * (ENC_PREAMBLE_PAIRS * 2 + 2 + DATA_BYTES * 16 + 1)); + for burst in 0..ENC_BURST_COUNT { + let mut pkt = raw17; + pkt[4] = ENC_BURST_PKT4[burst]; + let crcw = Self::crc16(&pkt[3..15]); + pkt[15] = (crcw >> 8) as u8; + pkt[16] = (crcw & 0xFF) as u8; + + // Preamble: 400 pairs of long high / long low. + for _ in 0..ENC_PREAMBLE_PAIRS { + Self::enc_add_level(&mut signal, true, ENC_SYNC_LONG_US); + Self::enc_add_level(&mut signal, false, ENC_SYNC_LONG_US); + } + // Sync: long high + short low. + Self::enc_add_level(&mut signal, true, ENC_SYNC_LONG_US); + Self::enc_add_level(&mut signal, false, ENC_SYNC_SHORT_US); + + // Data: each bit → (bit, short) then (!bit, short) — Manchester, MSB first. + for &b in pkt.iter() { + for bit_i in (0..8).rev() { + let bit = ((b >> bit_i) & 1) != 0; + Self::enc_add_level(&mut signal, bit, ENC_SYNC_SHORT_US); + Self::enc_add_level(&mut signal, !bit, ENC_SYNC_SHORT_US); + } + } + + // Trailing gap (long for repeats, short for the final burst). + let gap = if burst + 1 == ENC_BURST_COUNT { + ENC_GAP_LAST_US + } else { + ENC_GAP_REPEAT_US + }; + Self::enc_add_level(&mut signal, false, gap); + } + + Some(signal) + } +} + +impl Default for FordV1Decoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// CRC16/CCITT known vector. "123456789" → 0x31C3 for poly 0x1021, init 0x0000 + /// (the standard CRC-16/XMODEM check value). + #[test] + fn crc16_known_vector() { + let v = FordV1Decoder::crc16(b"123456789"); + assert_eq!(v, 0x31C3, "CRC-16/XMODEM check value mismatch: got {:04X}", v); + } + + /// The descramble cipher must be invertible: encode_inverse_block ∘ decode_with_flag(flag) + /// should round-trip for the flag the parity selects. + #[test] + fn descramble_roundtrip() { + // A plaintext block; encode to air, then plain_from_air must recover it exactly. + let plain: [u8; 9] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x26, 0x77, 0x88, 0x99]; + let air = FordV1Decoder::encode_air_9bytes(&plain); + let recovered = FordV1Decoder::plain_from_air(&air).expect("round-trip recover"); + assert_eq!(recovered, plain, "plain_from_air did not recover plaintext"); + } + + /// Build a canonical Ford V1 plaintext (the strict-branch invariant is plain[4]==plain[0], + /// since the cipher leaves byte 0 untouched, so air9[0]==plain[0]). Returns the 17-byte frame. + fn build_frame(serial: u32, button: u8, counter: u32) -> [u8; DATA_BYTES] { + let mut plain = [0u8; 9]; + plain[4] = (serial & 0xFF) as u8; // strict: plain[4] == air9[0] == plain[0] + FordV1Decoder::plain_apply_fields(&mut plain, serial, button, counter); + + let mut raw17 = [0u8; DATA_BYTES]; + // key1 head: raw[0..5] arbitrary-ish, raw[5] must equal plain[3] (strict: decoded[3]==raw[5]). + raw17[0] = 0xC0; + raw17[1] = 0xFF; + raw17[2] = 0xEE; + raw17[3] = (serial >> 24) as u8; + raw17[4] = (serial >> 16) as u8; + raw17[5] = plain[3]; + FordV1Decoder::rebuild_raw_from_plain(&mut raw17, &plain); + raw17 + } + + /// process_data must accept a canonical frame, take the strict branch, and recover fields. + #[test] + fn process_data_strict_branch() { + let (serial, button, counter) = (0x1A2B3C4Du32, 0x02u8, 0x0123u32); + let raw17 = build_frame(serial, button, counter); + + let mut dec = FordV1Decoder::new(); + dec.raw_bytes[..DATA_BYTES].copy_from_slice(&raw17); + dec.byte_count = DATA_BYTES; + let d = dec.process_data().expect("process_data should accept the frame"); + assert!(d.crc_valid, "CRC must be valid"); + assert_eq!(d.serial, Some(serial), "serial mismatch"); + assert_eq!(d.button, Some(button), "button mismatch"); + assert_eq!(d.counter, Some(counter as u16), "counter mismatch"); + assert_eq!(d.data_count_bit, DATA_BITS); + } + + /// Reconstruct the 17 on-air data bytes of the first burst from the encoder's Manchester + /// upload. The encoder emits, per data bit, (bit,short) then (!bit,short); add_level merges + /// adjacent same-level pulses, so a same-bit boundary becomes a long pulse. We re-pair the + /// stream by walking half-cells of `short` width (splitting merged long pulses into two halves) + /// and reading each bit as the level of its first half-cell. + fn first_burst_bytes(upload: &[LevelDuration]) -> [u8; DATA_BYTES] { + // Expand merged pulses into ENC_SYNC_SHORT_US half-cells (long = 2 halves). + let mut halves: Vec = Vec::new(); + for ld in upload { + let n = ((ld.duration_us + ENC_SYNC_SHORT_US / 2) / ENC_SYNC_SHORT_US).max(1); + for _ in 0..n { + halves.push(ld.level); + } + } + // Skip the preamble (400 long pairs = 1600 halves) + sync (long high=2 + short low=1 = 3). + let data_start = ENC_PREAMBLE_PAIRS * 4 + 3; + let mut out = [0u8; DATA_BYTES]; + for (byte_i, b) in out.iter_mut().enumerate() { + for bit_pos in 0..8 { + // Each bit = two half-cells; the bit value is the first half-cell's level. + let idx = data_start + (byte_i * 8 + bit_pos) * 2; + let bit = halves.get(idx).copied().unwrap_or(false); + *b = (*b << 1) | (bit as u8); + } + } + out + } + + /// Full encode→decode round trip at the on-air-frame level. Decode a canonical frame, re-encode + /// it via `encode()`, recover the transmitted 17-byte frame from the first burst, and verify it + /// decodes (via process_data) back to the same serial/button/counter with a valid CRC. + #[test] + fn encode_decode_roundtrip() { + let (serial, button, counter) = (0x1A2B3C4Du32, 0x02u8, 0x0123u32); + let raw17 = build_frame(serial, button, counter); + + let mut dec = FordV1Decoder::new(); + dec.raw_bytes[..DATA_BYTES].copy_from_slice(&raw17); + dec.byte_count = DATA_BYTES; + let decoded = dec.process_data().expect("decode canonical frame"); + + // Re-encode (same button) → Manchester upload (6 bursts). + let upload = FordV1Decoder::new() + .encode(&decoded, button) + .expect("encoder should produce an upload"); + assert!(!upload.is_empty(), "encoder upload must be non-empty"); + + // Recover the transmitted frame from burst 0 and decode it through process_data. + // Burst 0 overrides pkt[4] = 0x08 (ENC_BURST_PKT4[0]) and recomputes the CRC to match, so + // the recovered frame is self-consistent. The strict-branch serial comes from the plaintext + // (decoded[1..4],decoded[0]), independent of raw[4], so serial/button/counter still match. + let tx = first_burst_bytes(&upload); + let mut dec2 = FordV1Decoder::new(); + dec2.raw_bytes[..DATA_BYTES].copy_from_slice(&tx); + dec2.byte_count = DATA_BYTES; + let got = dec2 + .process_data() + .expect("re-encoded burst-0 frame must decode via process_data"); + assert!(got.crc_valid, "round-tripped CRC must be valid"); + assert_eq!(got.serial, Some(serial), "round-trip serial mismatch"); + assert_eq!(got.button, Some(button), "round-trip button mismatch"); + assert_eq!(got.counter, Some(counter as u16), "round-trip counter mismatch"); + + // Structural checks: 6 bursts worth of data, each preamble present. + let long_highs = upload + .iter() + .filter(|l| l.level && l.duration_us >= ENC_SYNC_LONG_US) + .count(); + assert!( + long_highs >= ENC_BURST_COUNT, + "expected at least one long-high preamble pulse per burst" + ); + } +} diff --git a/src/protocols/ford_v2.rs b/src/protocols/ford_v2.rs new file mode 100644 index 0000000..e7e2490 --- /dev/null +++ b/src/protocols/ford_v2.rs @@ -0,0 +1,413 @@ +//! Ford V2 protocol decoder/encoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v2.c` and `ford_v2.h`. +//! Manchester 200/400µs (te_delta 260 → threshold ~460µs between short/long), 104 bits (13 bytes), FM. +//! Frame begins with a 16-bit Manchester sync that equals ~0x7FA7 (the decoder matches the *inverted* +//! shift register against 0x8058); the two sync bytes 0x7F 0xA7 head the 13-byte buffer. Data bits are +//! inverted before packing (`data_bit = !data_bit`). Structure is validated by the two sync bytes plus a +//! known button code. Encoder supported (matches subghz_protocol_encoder_ford_v2). +//! +//! Decoder steps: Reset → Preamble (≥64 shorts) → Sync (find 0x7FA7) → Data (11 bytes). + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use crate::radio::demodulator::LevelDuration; +use crate::duration_diff; + +const TE_SHORT: u32 = 200; +const TE_LONG: u32 = 400; +const TE_DELTA: u32 = 260; +const DATA_BITS: usize = 104; +const DATA_BYTES: usize = 13; +const PREAMBLE_MIN: u16 = 64; +const SYNC_0: u8 = 0x7F; +const SYNC_1: u8 = 0xA7; +const SYNC_BITS: u8 = 16; +const INTER_BURST_GAP_US: u32 = 15000; + +// Encoder constants (subghz_protocol_encoder_ford_v2) +const ENC_TE_SHORT: u32 = 240; +const ENC_PREAMBLE_PAIRS: usize = 70; +const ENC_BURST_COUNT: usize = 6; +const ENC_INTER_BURST_GAP_US: u32 = 16000; +const ENC_SYNC_LO_US: u32 = 476; +const TAIL_RAW_BYTES: usize = 5; + +/// Inverted 16-bit sync the decoder matches against (= !0x7FA7). +const SYNC_SHIFT16_INV: u16 = !(((SYNC_0 as u16) << 8) | SYNC_1 as u16); + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ManchesterState { + Mid0 = 0, + Mid1 = 1, + Start0 = 2, + Start1 = 3, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + Preamble, + Sync, + Data, +} + +pub struct FordV2Decoder { + step: DecoderStep, + manchester_state: ManchesterState, + preamble_count: u16, + raw_bytes: [u8; DATA_BYTES], + byte_count: usize, + decode_data: u16, + decode_count_bit: usize, + sync_shift: u16, + sync_bit_count: u8, +} + +impl FordV2Decoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + manchester_state: ManchesterState::Mid1, + preamble_count: 0, + raw_bytes: [0; DATA_BYTES], + byte_count: 0, + decode_data: 0, + decode_count_bit: 0, + sync_shift: 0, + sync_bit_count: 0, + } + } + + fn reset_state(&mut self) { + self.step = DecoderStep::Reset; + self.manchester_state = ManchesterState::Mid1; + self.preamble_count = 0; + self.raw_bytes = [0; DATA_BYTES]; + self.byte_count = 0; + self.decode_data = 0; + self.decode_count_bit = 0; + self.sync_shift = 0; + self.sync_bit_count = 0; + } + + fn is_short(d: u32) -> bool { + duration_diff!(d, TE_SHORT) < TE_DELTA + } + + fn is_long(d: u32) -> bool { + duration_diff!(d, TE_LONG) < TE_DELTA + } + + /// Flipper Manchester transition table (same as Ford V0). Event 0=ShortLow,1=ShortHigh, + /// 2=LongLow,3=LongHigh. Returns Some(bit) when a bit emits. + fn manchester_advance(&mut self, event: u8) -> Option { + let (new_state, emit) = match (self.manchester_state, event) { + (ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 1) => (ManchesterState::Start1, true), + (ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true), + + (ManchesterState::Mid1, 0) => (ManchesterState::Start0, true), + (ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true), + (ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 1) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start1, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Start1, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 3) => (ManchesterState::Mid1, false), + + _ => (ManchesterState::Mid1, false), + }; + self.manchester_state = new_state; + if emit { Some((event & 1) == 1) } else { None } + } + + /// Map (level, duration) → Manchester event. Ford V2 polarity: level ? High : Low. + fn pulse_event(level: bool, duration: u32) -> Option { + if Self::is_short(duration) { + Some(if level { 1 } else { 0 }) + } else if Self::is_long(duration) { + Some(if level { 3 } else { 2 }) + } else { + None + } + } + + fn button_is_valid(btn: u8) -> bool { + matches!(btn, 0x10 | 0x11 | 0x13 | 0x14 | 0x15) + } + + /// Enter the Sync step from the preamble, seeding state for the triggering long-low pulse. + fn enter_sync_from_preamble(&mut self, level: bool, duration: u32) { + self.step = DecoderStep::Sync; + self.decode_data = 0; + self.decode_count_bit = 0; + self.byte_count = 0; + self.sync_shift = 0; + self.sync_bit_count = 0; + self.raw_bytes = [0; DATA_BYTES]; + self.manchester_state = ManchesterState::Mid1; + + if let Some(event) = Self::pulse_event(level, duration) { + // Low event (0/2) seeds Mid0 (matches the C `if(ev==ShortLow||ev==LongLow) state=Mid0`). + if event == 0 || event == 2 { + self.manchester_state = ManchesterState::Mid0; + } + self.feed_event(event); + } else { + self.reset_state(); + } + } + + /// Process one Manchester event in Sync or Data step. Returns Some when a frame commits. + fn feed_event(&mut self, event: u8) -> Option { + if self.step == DecoderStep::Sync { + if let Some(bit) = self.manchester_advance(event) { + self.sync_shift = (self.sync_shift << 1) | (bit as u16); + if self.sync_bit_count < SYNC_BITS { + self.sync_bit_count += 1; + } + if self.sync_bit_count >= SYNC_BITS && self.sync_shift == SYNC_SHIFT16_INV { + // Enter data: prime sync bytes and bit counter. + self.raw_bytes = [0; DATA_BYTES]; + self.raw_bytes[0] = SYNC_0; + self.raw_bytes[1] = SYNC_1; + self.byte_count = 2; + self.step = DecoderStep::Data; + self.decode_data = 0; + self.decode_count_bit = SYNC_BITS as usize; + } + } + return None; + } + + // Data step + if let Some(bit) = self.manchester_advance(event) { + let data_bit = !bit; // Ford V2 inverts decoded bits + self.decode_data = (self.decode_data << 1) | (data_bit as u16); + self.decode_count_bit += 1; + + if self.decode_count_bit & 7 == 0 { + let byte_val = (self.decode_data & 0xFF) as u8; + if self.byte_count < DATA_BYTES { + self.raw_bytes[self.byte_count] = byte_val; + self.byte_count += 1; + } + self.decode_data = 0; + + if self.byte_count == DATA_BYTES { + let result = self.commit_frame(); + self.reset_state(); + return result; + } + } + } + None + } + + /// Validate sync bytes + structure and build the decoded signal. + fn commit_frame(&self) -> Option { + let k = &self.raw_bytes; + if k[0] != SYNC_0 || k[1] != SYNC_1 { + return None; + } + if !Self::button_is_valid(k[6]) { + return None; + } + + let serial = ((k[2] as u32) << 24) + | ((k[3] as u32) << 16) + | ((k[4] as u32) << 8) + | (k[5] as u32); + let counter = (((k[7] & 0x7F) as u16) << 9) | ((k[8] as u16) << 1) | ((k[9] >> 7) as u16); + + // Top 8 bytes → 64-bit data for display/export (matches generic.data). + let mut data = 0u64; + for &byte in k.iter().take(8) { + data = (data << 8) | byte as u64; + } + // Tail raw bytes k[8..13] → extra (40 bits) so the encoder can rebuild the full frame. + let mut extra = 0u64; + for &byte in k.iter().skip(8).take(TAIL_RAW_BYTES) { + extra = (extra << 8) | byte as u64; + } + + Some(DecodedSignal { + serial: Some(serial), + button: Some(k[6]), + counter: Some(counter), + crc_valid: true, // validated by sync bytes + button structure + data, + data_count_bit: DATA_BITS, + encoder_capable: true, + extra: Some(extra), + protocol_display_name: None, + }) + } + + /// Map KAT's generic button command to a Ford V2 button code. + fn map_button(button: u8) -> u8 { + match button { + 0x01 => 0x10, // Lock + 0x02 => 0x11, // Unlock + 0x04 => 0x13, // Trunk + 0x08 => 0x14, // Panic + b if Self::button_is_valid(b) => b, // already a Ford V2 code + _ => 0x11, + } + } + + fn parity8(mut v: u8) -> u8 { + let mut p = 0u8; + while v != 0 { + p ^= v & 1; + v >>= 1; + } + p + } + + fn enc_add_level(signal: &mut Vec, level: bool, duration: u32) { + if let Some(last) = signal.last_mut() { + if last.level == level { + *last = LevelDuration::new(level, last.duration_us + duration); + return; + } + } + signal.push(LevelDuration::new(level, duration)); + } + + fn enc_manchester_bit(signal: &mut Vec, bit: bool) { + if bit { + Self::enc_add_level(signal, true, ENC_TE_SHORT); + Self::enc_add_level(signal, false, ENC_TE_SHORT); + } else { + Self::enc_add_level(signal, false, ENC_TE_SHORT); + Self::enc_add_level(signal, true, ENC_TE_SHORT); + } + } +} + +impl ProtocolDecoder for FordV2Decoder { + fn name(&self) -> &'static str { + "Ford V2" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: DATA_BITS, + } + } + + fn supported_frequencies(&self) -> &[u32] { + &[315_000_000, 433_920_000] + } + + fn reset(&mut self) { + self.reset_state(); + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.step { + DecoderStep::Reset => { + if Self::is_short(duration) { + self.preamble_count = 1; + self.step = DecoderStep::Preamble; + } + } + + DecoderStep::Preamble => { + if Self::is_short(duration) { + if self.preamble_count < u16::MAX { + self.preamble_count += 1; + } + } else if !level && Self::is_long(duration) { + if self.preamble_count >= PREAMBLE_MIN { + self.enter_sync_from_preamble(level, duration); + } else { + self.reset_state(); + } + } else { + self.reset_state(); + } + } + + DecoderStep::Sync | DecoderStep::Data => { + if let Some(event) = Self::pulse_event(level, duration) { + if let Some(result) = self.feed_event(event) { + return Some(result); + } + } else { + // Non-short/long pulse (gap/out-of-range) ends the attempt. + let _ = duration >= INTER_BURST_GAP_US; + self.reset_state(); + } + } + } + None + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + // Rebuild the 13-byte frame: bytes 0..8 from data, tail bytes 8..13 from extra. + let mut raw = [0u8; DATA_BYTES]; + for (i, b) in raw.iter_mut().take(8).enumerate() { + *b = (decoded.data >> (56 - i * 8)) as u8; + } + let extra = decoded.extra.unwrap_or(0); + for (i, b) in raw.iter_mut().skip(8).take(TAIL_RAW_BYTES).enumerate() { + *b = (extra >> (32 - i * 8)) as u8; + } + + // Apply the requested button and refresh the byte-7 parity MSB. + raw[6] = Self::map_button(button); + if !Self::button_is_valid(raw[6]) { + return None; + } + let parity_msb = Self::parity8(raw[6]) << 7; + raw[7] = (raw[7] & 0x7F) | parity_msb; + // Ensure sync header is present. + raw[0] = SYNC_0; + raw[1] = SYNC_1; + + let mut signal = Vec::with_capacity(ENC_BURST_COUNT * (ENC_PREAMBLE_PAIRS * 2 + DATA_BITS * 2 + 4)); + for burst in 0..ENC_BURST_COUNT { + // Preamble: 70 pairs of (low short, high short) + for _ in 0..ENC_PREAMBLE_PAIRS { + Self::enc_add_level(&mut signal, false, ENC_TE_SHORT); + Self::enc_add_level(&mut signal, true, ENC_TE_SHORT); + } + // Sync low + high short + Self::enc_add_level(&mut signal, false, ENC_SYNC_LO_US); + Self::enc_add_level(&mut signal, true, ENC_TE_SHORT); + // Data bits 1..103 (bit 0 implied by the high short above) + for bit_pos in 1..DATA_BITS { + let byte_idx = bit_pos / 8; + let bit_idx = 7 - (bit_pos % 8); + let bit = (raw[byte_idx] >> bit_idx) & 1 != 0; + Self::enc_manchester_bit(&mut signal, bit); + } + if burst + 1 < ENC_BURST_COUNT { + Self::enc_add_level(&mut signal, true, ENC_INTER_BURST_GAP_US); + } + } + Some(signal) + } +} + +impl Default for FordV2Decoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/ford_v3.rs b/src/protocols/ford_v3.rs new file mode 100644 index 0000000..44df539 --- /dev/null +++ b/src/protocols/ford_v3.rs @@ -0,0 +1,258 @@ +//! Ford V3 protocol decoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v3.c` and `ford_v3.h`. +//! Manchester 240/480µs, 104 bits (13 bytes), FM, plaintext (no CRC/encryption). Decode-only — +//! the reference encoder is NULL. +//! +//! Manchester uses Flipper's manchester_decoder.h transition table (same as Ford V0), but Ford V3 +//! maps level the OPPOSITE way from Ford V0: `level ? ShortHigh/LongHigh : ShortLow/LongLow` +//! (Ford V0 uses `level ? Low : High`). Decoder steps: Reset → Preamble (≥30 shorts) → Data. + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use crate::radio::demodulator::LevelDuration; +use crate::duration_diff; + +const TE_SHORT: u32 = 240; +const TE_LONG: u32 = 480; +const TE_DELTA: u32 = 60; +const DATA_BITS: usize = 104; +const DATA_BYTES: usize = 13; +const PREAMBLE_MIN: u16 = 30; + +const BTN_LOCK: u8 = 0x01; +const BTN_UNLOCK: u8 = 0x02; + +/// Manchester state machine (Flipper manchester_decoder.h transition table; same as Ford V0). +#[derive(Debug, Clone, Copy, PartialEq)] +enum ManchesterState { + Mid0 = 0, + Mid1 = 1, + Start0 = 2, + Start1 = 3, +} + +/// Decoder step states (matches FordV3DecoderStep in ford_v3.c) +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + Preamble, + Data, +} + +/// Ford V3 protocol decoder (matches SubGhzProtocolDecoderFordV3) +pub struct FordV3Decoder { + step: DecoderStep, + manchester_state: ManchesterState, + raw_bytes: [u8; DATA_BYTES], + bit_count: usize, + preamble_count: u16, +} + +impl FordV3Decoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + manchester_state: ManchesterState::Mid1, + raw_bytes: [0; DATA_BYTES], + bit_count: 0, + preamble_count: 0, + } + } + + /// Reset accumulators (matches ford_v3_reset_data) + fn reset_data(&mut self) { + self.raw_bytes = [0; DATA_BYTES]; + self.bit_count = 0; + self.preamble_count = 0; + self.manchester_state = ManchesterState::Mid1; + } + + /// Add a decoded bit MSB-first into the byte buffer (matches ford_v3_add_bit) + fn add_bit(&mut self, bit: bool) { + if self.bit_count >= DATA_BITS { + return; + } + let byte_index = self.bit_count / 8; + let bit_in_byte = 7 - (self.bit_count % 8); + if bit { + self.raw_bytes[byte_index] |= 1 << bit_in_byte; + } + self.bit_count += 1; + } + + /// Manchester state machine (Flipper manchester_advance). + /// Event: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh. Returns Some(bit) when a bit emits. + fn manchester_advance(&mut self, event: u8) -> Option { + let (new_state, emit) = match (self.manchester_state, event) { + (ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 1) => (ManchesterState::Start1, true), + (ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true), + + (ManchesterState::Mid1, 0) => (ManchesterState::Start0, true), + (ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true), + (ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 1) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start1, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Start1, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 3) => (ManchesterState::Mid1, false), + + _ => (ManchesterState::Mid1, false), + }; + + self.manchester_state = new_state; + if emit { + Some((event & 1) == 1) + } else { + None + } + } + + fn is_short(duration: u32) -> bool { + duration_diff!(duration, TE_SHORT) < TE_DELTA + } + + fn is_long(duration: u32) -> bool { + duration_diff!(duration, TE_LONG) < TE_DELTA + } + + /// Build the decoded signal when 104 bits are collected (matches ford_v3_parse_fields). + fn build_signal(&self) -> DecodedSignal { + let b = &self.raw_bytes; + let serial = ((b[1] as u32) << 24) + | ((b[2] as u32) << 16) + | ((b[3] as u32) << 8) + | (b[4] as u32); + // Counter is the bitwise-inverted bytes 7 and 8 (ref: ~b[7], ~b[8]) + let counter = (((!b[7]) as u16) << 8) | ((!b[8]) as u16); + let button = if b[6] & 0x01 != 0 { BTN_UNLOCK } else { BTN_LOCK }; + + // Pack the first 8 bytes (big-endian) into the 64-bit data field for display/export + // (matches ford_v3.c serialize, which stores bytes[0..8] in generic.data). + let mut data = 0u64; + for &byte in b.iter().take(8) { + data = (data << 8) | byte as u64; + } + + DecodedSignal { + serial: Some(serial), + button: Some(button), + counter: Some(counter), + crc_valid: true, // Ford V3 is plaintext with no CRC + data, + data_count_bit: DATA_BITS, + encoder_capable: false, + extra: None, + protocol_display_name: None, + } + } +} + +impl ProtocolDecoder for FordV3Decoder { + fn name(&self) -> &'static str { + "Ford V3" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: DATA_BITS, + } + } + + fn supported_frequencies(&self) -> &[u32] { + &[315_000_000, 433_920_000] + } + + fn reset(&mut self) { + self.step = DecoderStep::Reset; + self.reset_data(); + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.step { + // Ref: any short pulse begins the preamble (no level check). + DecoderStep::Reset => { + if Self::is_short(duration) { + self.reset_data(); + self.preamble_count = 1; + self.step = DecoderStep::Preamble; + } + } + + DecoderStep::Preamble => { + if Self::is_short(duration) { + self.preamble_count += 1; + } else if self.preamble_count >= PREAMBLE_MIN && Self::is_long(duration) { + // First data bit: long pulse, mapped level ? LongHigh : LongLow. + self.manchester_state = ManchesterState::Mid1; + let event = if level { 3 } else { 2 }; + if let Some(bit) = self.manchester_advance(event) { + self.add_bit(bit); + } + self.step = DecoderStep::Data; + } else { + self.step = DecoderStep::Reset; + } + } + + DecoderStep::Data => { + let short = Self::is_short(duration); + let long = Self::is_long(duration); + + if !short && !long { + // Gap / out-of-range pulse ends the frame. + let ready = self.bit_count >= DATA_BITS; + let result = if ready { Some(self.build_signal()) } else { None }; + self.step = DecoderStep::Reset; + self.reset_data(); + return result; + } + + // Ford V3 polarity: level ? High : Low (opposite of Ford V0). + let event = if level { + if short { 1 } else { 3 } // ShortHigh / LongHigh + } else if short { + 0 // ShortLow + } else { + 2 // LongLow + }; + + if let Some(bit) = self.manchester_advance(event) { + self.add_bit(bit); + if self.bit_count >= DATA_BITS { + let result = self.build_signal(); + self.step = DecoderStep::Reset; + self.reset_data(); + return Some(result); + } + } + } + } + + None + } + + fn supports_encoding(&self) -> bool { + false + } + + fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { + None + } +} + +impl Default for FordV3Decoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/honda_static.rs b/src/protocols/honda_static.rs new file mode 100644 index 0000000..8dc7aa2 --- /dev/null +++ b/src/protocols/honda_static.rs @@ -0,0 +1,558 @@ +//! Honda Static protocol decoder/encoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/honda_static.c` and +//! `honda_static.h`. Honda/Acura fixed-code keyfobs. FM, 315 MHz + 433.92 MHz, 64-bit frame. +//! +//! Unlike most KAT decoders, Honda Static does NOT use the Flipper manchester_decoder.h transition +//! table. It buffers a per-element *symbol stream* (one bit per ~63µs element) and then performs a +//! custom Manchester unpack over symbol pairs (see `honda_static_manchester_pack_64` in the C). A +//! short pulse contributes one symbol = the pulse level; a long pulse contributes two symbols of the +//! same level. Anything outside both ranges (e.g. the 700µs sync or a trailing gap) terminates the +//! buffer and triggers a parse attempt. +//! +//! Frame (64 bits, MSB-first into 8 bytes): button (4b) | serial (28b) | counter (24b) | +//! checksum (8b). The checksum is an XOR of bytes[0..7] (the first 7 bytes). Emission is gated on +//! the checksum validating, so Honda Static will not false-match. The parser tries the +//! inverted-Manchester interpretation first (which is what the encoder emits), then non-inverted +//! forward, then a bit-reversed-bytes pass — matching the C. +//! +//! The exported `data` (Key) word is the C `generic.data`: a compact nibble-packed layout +//! (`honda_static_pack_compact`), NOT the raw decoded packet bytes. + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use crate::radio::demodulator::LevelDuration; + +const BIT_COUNT: usize = 64; +const MIN_SYMBOLS: usize = 36; +const SHORT_BASE_US: u32 = 28; +const SHORT_SPAN_US: u32 = 70; +const LONG_BASE_US: u32 = 61; +const LONG_SPAN_US: u32 = 130; +const SYNC_TIME_US: u32 = 700; +const ELEMENT_TIME_US: u32 = 63; +const SYMBOL_CAPACITY: usize = 512; +const PREAMBLE_ALTERNATING_COUNT: usize = 160; +const PREAMBLE_MAX_TRANSITIONS: u16 = 19; + +// Reported timing constants (informational; matches ProtoPirate protocol_items profile). +const TE_SHORT: u32 = ELEMENT_TIME_US; +const TE_LONG: u32 = SYNC_TIME_US; +const TE_DELTA: u32 = 120; + +// KAT generic button codes. +const BTN_LOCK: u8 = 0x01; +const BTN_UNLOCK: u8 = 0x02; +const BTN_TRUNK: u8 = 0x04; +const BTN_PANIC: u8 = 0x08; + +// honda_static_encoder_button_map[4] in the C ({0x02,0x04,0x08,0x05}); used by the encoder remap. +const ENCODER_BUTTON_MAP: [u8; 4] = [0x02, 0x04, 0x08, 0x05]; + +/// Decoded Honda Static fields (matches HondaStaticFields). +#[derive(Debug, Clone, Copy, Default)] +struct HondaStaticFields { + button: u8, + serial: u32, + counter: u32, + /// XOR checksum byte (mirrors the C struct field; validation recomputes it, so this is + /// retained for fidelity/inspection rather than being read on the hot path). + #[allow(dead_code)] + checksum: u8, +} + +/// Honda Static decoder (matches SubGhzProtocolDecoderHondaStatic). +pub struct HondaStaticDecoder { + /// Per-element symbol stream (one bit per ~63µs element). Index 0 is the first received symbol. + symbols: Vec, +} + +impl HondaStaticDecoder { + pub fn new() -> Self { + Self { + symbols: Vec::with_capacity(SYMBOL_CAPACITY), + } + } + + /// Extract `count` bits starting at `start`, MSB-first across the byte array + /// (matches honda_static_get_bits / honda_static_get_bits_u32, shift = (~bit_index)&7). + fn get_bits(data: &[u8], start: usize, count: usize) -> u32 { + let mut value: u32 = 0; + for i in 0..count { + let bit_index = start + i; + let byte = data[bit_index >> 3]; + let shift = (!bit_index) & 0x07; + value = (value << 1) | (((byte >> shift) & 1) as u32); + } + value + } + + /// XOR checksum over the first 7 bytes of the 8-byte packet (matches the inline checksum loop in + /// honda_static_validate_forward_packet / honda_static_build_packet_bytes). + fn packet_checksum(packet: &[u8]) -> u8 { + let mut checksum = 0u8; + for &b in packet.iter().take(7) { + checksum ^= b; + } + checksum + } + + /// Reverse the bit order of a byte (matches pp_reverse_bits8). + fn reverse_bits8(value: u8) -> u8 { + let mut v = value; + v = ((v & 0xF0) >> 4) | ((v & 0x0F) << 4); + v = ((v & 0xCC) >> 2) | ((v & 0x33) << 2); + v = ((v & 0xAA) >> 1) | ((v & 0x55) << 1); + v + } + + fn is_valid_button(button: u8) -> bool { + // honda_static_is_valid_button: button <= 9 && ((0x336 >> button) & 1) + // 0x336 = 0b1100110110 → valid buttons: 1,2,4,5,8,9. + button <= 9 && ((0x336u16 >> button) & 1) != 0 + } + + fn is_valid_serial(serial: u32) -> bool { + serial != 0 && serial != 0x0FFF_FFFF + } + + /// Pack fields into the 64-bit compact "Key" word (matches honda_static_pack_compact → + /// pp_bytes_to_u64_be over the compact[8] nibble-packed layout). + fn pack_compact(fields: &HondaStaticFields) -> u64 { + let mut compact = [0u8; 8]; + compact[0] = fields.button & 0x0F; + compact[1] = (fields.serial >> 20) as u8; + compact[2] = (fields.serial >> 12) as u8; + compact[3] = (fields.serial >> 4) as u8; + compact[4] = (fields.serial << 4) as u8; + compact[5] = (fields.counter >> 16) as u8; + compact[6] = (fields.counter >> 8) as u8; + compact[7] = fields.counter as u8; + u64::from_be_bytes(compact) + } + + /// Build the raw 8-byte (64-bit) packet from fields, MSB-first + /// (matches honda_static_build_packet_bytes; checksum filled into byte 7). + fn build_packet_bytes(fields: &HondaStaticFields) -> [u8; 8] { + let mut packet = [0u8; 8]; + Self::set_bits(&mut packet, 0, 4, (fields.button & 0x0F) as u32); + Self::set_bits(&mut packet, 4, 28, fields.serial); + Self::set_bits(&mut packet, 32, 24, fields.counter); + let checksum = Self::packet_checksum(&packet); + Self::set_bits(&mut packet, 56, 8, checksum as u32); + packet + } + + /// Set `count` bits starting at `start`, MSB-first (matches honda_static_set_bits). + fn set_bits(data: &mut [u8], start: usize, count: usize, value: u32) { + for i in 0..count { + let bit_index = start + i; + let byte_index = bit_index >> 3; + let shift = (!bit_index) & 0x07; + let mask = 1u8 << shift; + let bit = ((value >> (count - 1 - i)) & 1) != 0; + if bit { + data[byte_index] |= mask; + } else { + data[byte_index] &= !mask; + } + } + } + + /// Validate a forward (as-decoded) 8-byte packet (matches honda_static_validate_forward_packet). + fn validate_forward_packet(packet: &[u8; 8]) -> Option { + let button = Self::get_bits(packet, 0, 4) as u8; + let serial = Self::get_bits(packet, 4, 28); + let counter = Self::get_bits(packet, 32, 24); + let checksum = Self::get_bits(packet, 56, 8) as u8; + let checksum_calc = Self::packet_checksum(packet); + + if checksum != checksum_calc { + return None; + } + if !Self::is_valid_button(button) { + return None; + } + if !Self::is_valid_serial(serial) { + return None; + } + + Some(HondaStaticFields { + button, + serial, + counter, + checksum, + }) + } + + /// Validate a bit-reversed packet (matches honda_static_validate_reverse_packet). Note: the C + /// does NOT re-check the checksum here (it reverses bytes then validates button/serial only), + /// so this path is not checksum-gated. We mirror that, but the caller flags crc_valid=false. + fn validate_reverse_packet(packet: &[u8; 8]) -> Option { + let mut reversed = [0u8; 8]; + for (i, b) in packet.iter().enumerate() { + reversed[i] = Self::reverse_bits8(*b); + } + + let button = Self::get_bits(&reversed, 0, 4) as u8; + let serial = Self::get_bits(&reversed, 4, 28); + let counter = Self::get_bits(&reversed, 32, 24); + let checksum = Self::packet_checksum(&reversed); + + if !Self::is_valid_button(button) { + return None; + } + if !Self::is_valid_serial(serial) { + return None; + } + + Some(HondaStaticFields { + button, + serial, + counter, + checksum, + }) + } + + /// Manchester unpack over symbol pairs (matches honda_static_manchester_pack_64). + /// Returns the packed 8-byte packet plus how many bits were collected. `inverted`: + /// bit=1 when (a==0,b==1); non-inverted: bit=1 when (a==1,b==0). Equal adjacent symbols are + /// skipped (advance by 1). + fn manchester_pack_64(symbols: &[bool], start_pos: usize, inverted: bool) -> ([u8; 8], usize) { + let mut packet = [0u8; 8]; + let count = symbols.len(); + let mut pos = start_pos; + let mut bit_count: usize = 0; + + while pos + 1 < count { + if bit_count >= BIT_COUNT { + break; + } + let a = symbols[pos]; + let b = symbols[pos + 1]; + if a == b { + pos += 1; + continue; + } + let bit = if inverted { + !a && b // a==0 && b==1 + } else { + a && !b // a==1 && b==0 + }; + if bit { + let shift = (!bit_count) & 0x07; + packet[bit_count >> 3] |= 1u8 << shift; + } + bit_count += 1; + pos += 2; + } + + (packet, bit_count) + } + + /// Locate the data start (after the alternating preamble + sync run) and unpack/validate. + /// Matches honda_static_parse_symbols. Returns the validated fields and whether the checksum + /// path validated it (forward = true; reverse = false). + fn parse_symbols(symbols: &[bool], inverted: bool) -> Option<(HondaStaticFields, bool)> { + let count = symbols.len(); + if count == 0 { + return None; + } + + // Walk the alternating preamble: count consecutive transitions; when a non-transition + // follows a run longer than PREAMBLE_MAX_TRANSITIONS, that's the preamble/data boundary. + let mut index = 1usize; + let mut transitions: u16 = 0; + while index < count { + if symbols[index] != symbols[index - 1] { + transitions += 1; + } else { + if transitions > PREAMBLE_MAX_TRANSITIONS { + break; + } + transitions = 0; + } + index += 1; + } + if index >= count { + return None; + } + + // Skip forward over the equal-adjacent run (the sync gap). + while (index + 1 < count) && (symbols[index] == symbols[index + 1]) { + index += 1; + } + + let data_start = index; + let (packet, bit_count) = Self::manchester_pack_64(symbols, data_start, inverted); + if bit_count < BIT_COUNT { + return None; + } + + if let Some(fields) = Self::validate_forward_packet(&packet) { + return Some((fields, true)); + } + + if inverted { + return None; + } + + if let Some(fields) = Self::validate_reverse_packet(&packet) { + return Some((fields, false)); + } + + None + } + + /// Build a DecodedSignal from validated fields. `crc_valid` reflects whether the forward + /// (checksum-validated) path matched. + fn build_signal(fields: &HondaStaticFields, crc_valid: bool) -> DecodedSignal { + DecodedSignal { + serial: Some(fields.serial), + button: Some(fields.button), + // KAT counters are u16; Honda's is 24-bit. Truncate to the low 16 bits for the field + // (the full 24-bit counter is preserved in the packed `data` word). + counter: Some(fields.counter as u16), + crc_valid, + data: Self::pack_compact(fields), + data_count_bit: BIT_COUNT, + encoder_capable: true, + extra: None, + protocol_display_name: None, + } + } + + /// Map a KAT generic button command to a Honda Static 4-bit button code. + /// Honda button codes: Lock=1, Unlock=2, Trunk=4, Remote Start=5, Panic=8, Lock x2=9 + /// (see honda_static_button_names + honda_static_is_valid_button). + fn map_button(button: u8) -> u8 { + match button { + BTN_LOCK => 1, + BTN_UNLOCK => 2, + BTN_TRUNK => 4, + BTN_PANIC => 8, + // Already a valid Honda code → pass through. + b if Self::is_valid_button(b) => b, + // honda_static_encoder_remap_button for codes 2..=5. + b if (2..=5).contains(&b) => ENCODER_BUTTON_MAP[(b - 2) as usize], + _ => 1, + } + } + + fn enc_add_level(signal: &mut Vec, level: bool, duration: u32) { + if let Some(last) = signal.last_mut() { + if last.level == level { + *last = LevelDuration::new(level, last.duration_us + duration); + return; + } + } + signal.push(LevelDuration::new(level, duration)); + } +} + +impl ProtocolDecoder for HondaStaticDecoder { + fn name(&self) -> &'static str { + "Honda Static" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: BIT_COUNT, + } + } + + fn supported_frequencies(&self) -> &[u32] { + &[315_000_000, 433_920_000] + } + + fn reset(&mut self) { + self.symbols.clear(); + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + let sym = level; + + // Short pulse → one symbol (matches the SHORT range check in the C feed). + if duration >= SHORT_BASE_US && (duration - SHORT_BASE_US) <= SHORT_SPAN_US { + if self.symbols.len() < SYMBOL_CAPACITY { + self.symbols.push(sym); + } + return None; + } + + // Long pulse → two symbols (same level). + if duration >= LONG_BASE_US && (duration - LONG_BASE_US) <= LONG_SPAN_US { + if self.symbols.len() + 2 <= SYMBOL_CAPACITY { + self.symbols.push(sym); + self.symbols.push(sym); + } + return None; + } + + // Out-of-range pulse (sync 700µs or a gap): try to parse the buffered symbols, then reset. + // Matches the C feed: parse with inverted=true first, then inverted=false. + let mut result = None; + if self.symbols.len() >= MIN_SYMBOLS { + let parsed = Self::parse_symbols(&self.symbols, true) + .or_else(|| Self::parse_symbols(&self.symbols, false)); + if let Some((fields, crc_valid)) = parsed { + // Faithful to the C: both the forward (checksum-validated) and reverse + // (button+serial-validated) packets commit a decode. The strong button/serial + // gates keep this from false-matching (verified: zero false matches across the + // IMPORTS sweep). `crc_valid` reflects whether the checksum-validated forward path + // matched (true) vs. the reverse path (false). + result = Some(Self::build_signal(&fields, crc_valid)); + } + } + + self.symbols.clear(); + result + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + let serial = decoded.serial?; + if !Self::is_valid_serial(serial) { + return None; + } + + let fields = HondaStaticFields { + button: Self::map_button(button), + serial, + counter: decoded.counter.unwrap_or(0) as u32 & 0x00FF_FFFF, + checksum: 0, + }; + let packet = Self::build_packet_bytes(&fields); + + // Matches honda_static_build_upload. + let mut signal = + Vec::with_capacity(1 + PREAMBLE_ALTERNATING_COUNT + 2 * BIT_COUNT + 1); + + // Sync: HIGH 700µs. + Self::enc_add_level(&mut signal, true, SYNC_TIME_US); + + // Alternating preamble: 160 elements at 63µs, level = (i & 1) (starts LOW). + for i in 0..PREAMBLE_ALTERNATING_COUNT { + Self::enc_add_level(&mut signal, (i & 1) != 0, ELEMENT_TIME_US); + } + + // Data, MSB-first: bit → (!value 63µs, value 63µs). + for bit in 0..BIT_COUNT { + let shift = (!bit) & 0x07; + let value = ((packet[bit >> 3] >> shift) & 1) != 0; + Self::enc_add_level(&mut signal, !value, ELEMENT_TIME_US); + Self::enc_add_level(&mut signal, value, ELEMENT_TIME_US); + } + + // Trailing sync: !last_bit for 700µs. + let last_bit = (packet[7] & 1) != 0; + Self::enc_add_level(&mut signal, !last_bit, SYNC_TIME_US); + + Some(signal) + } +} + +impl Default for HondaStaticDecoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Round-trip: encode a known frame, feed it back through the decoder, and confirm the fields + /// survive. The encoder emits the inverted-Manchester interpretation, which the decoder tries + /// first. + #[test] + fn honda_static_encode_decode_roundtrip() { + let serial = 0x0123456u32 & 0x0FFF_FFFF; + let counter = 0x00AB12u32; + let original = DecodedSignal { + serial: Some(serial), + button: Some(BTN_UNLOCK), + counter: Some(counter as u16), + crc_valid: true, + data: 0, + data_count_bit: BIT_COUNT, + encoder_capable: true, + extra: None, + protocol_display_name: None, + }; + + let decoder = HondaStaticDecoder::new(); + let signal = decoder.encode(&original, BTN_UNLOCK).expect("encode"); + + // Feed the encoded pulses, plus a terminating gap to flush the symbol buffer. + let mut dec = HondaStaticDecoder::new(); + let mut decoded = None; + for ld in &signal { + if let Some(d) = dec.feed(ld.level, ld.duration_us) { + decoded = Some(d); + break; + } + } + if decoded.is_none() { + // Terminating out-of-range pulse to flush. + decoded = dec.feed(false, 5000); + } + + let decoded = decoded.expect("expected a decode from the round-tripped frame"); + assert!(decoded.crc_valid, "checksum should validate"); + assert_eq!(decoded.serial, Some(serial), "serial mismatch"); + assert_eq!(decoded.button, Some(2), "button should map to Honda Unlock=2"); + assert_eq!( + decoded.counter, + Some(counter as u16), + "counter (low 16 bits) mismatch" + ); + } + + /// The packed Key word must reproduce the C compact layout for a known field set. + #[test] + fn honda_static_pack_compact_layout() { + let fields = HondaStaticFields { + button: 0x02, + serial: 0x0ABCDEF, + counter: 0x123456, + checksum: 0, + }; + let data = HondaStaticDecoder::pack_compact(&fields); + let bytes = data.to_be_bytes(); + // compact[0] = button & 0x0F + assert_eq!(bytes[0], 0x02); + // serial nibbles: >>20, >>12, >>4, <<4 + assert_eq!(bytes[1], (0x0ABCDEFu32 >> 20) as u8); + assert_eq!(bytes[2], (0x0ABCDEFu32 >> 12) as u8); + assert_eq!(bytes[3], (0x0ABCDEFu32 >> 4) as u8); + assert_eq!(bytes[4], (0x0ABCDEFu32 << 4) as u8); + // counter bytes + assert_eq!(bytes[5], 0x12); + assert_eq!(bytes[6], 0x34); + assert_eq!(bytes[7], 0x56); + } + + /// build_packet_bytes + validate_forward_packet must round-trip the fields, and the checksum + /// must validate. + #[test] + fn honda_static_packet_validate_roundtrip() { + let fields = HondaStaticFields { + button: 0x05, + serial: 0x0FEDCBA, + counter: 0x00FF01, + checksum: 0, + }; + let packet = HondaStaticDecoder::build_packet_bytes(&fields); + let validated = HondaStaticDecoder::validate_forward_packet(&packet) + .expect("forward packet should validate"); + assert_eq!(validated.button, 0x05); + assert_eq!(validated.serial, 0x0FEDCBA); + assert_eq!(validated.counter, 0x00FF01); + } +} diff --git a/src/protocols/honda_v1.rs b/src/protocols/honda_v1.rs new file mode 100644 index 0000000..3c9ef87 --- /dev/null +++ b/src/protocols/honda_v1.rs @@ -0,0 +1,605 @@ +//! Honda V1 protocol decoder/encoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/honda_v1.c` and +//! `honda_v1.h`. Honda/Acura fixed-code keyfobs, a DIFFERENT protocol from `honda_static`. +//! AM (OOK), 315 MHz + 433.92 MHz. The on-wire frame carries 68 bits: 64-bit data + a 4-bit +//! CRC-fold nibble. +//! +//! **Encoding**: short/long pulse PWM. te_short=1000µs, te_long=2000µs, te_delta=400µs, +//! te_end=3500µs, te_short_min=600µs. Each on-wire symbol is one pulse whose width selects 0/1, +//! but the demodulated stream is glued together by a "pending bit" timing accumulator (see `feed`) +//! before being classified. +//! +//! **Pending-bit accumulation** (matches `subghz_protocol_decoder_honda_v1_feed`): sub-`te_delta` +//! runts are summed into `pending`; a HIGH level keeps extending the running HIGH pulse; a LOW +//! level flushes the accumulated HIGH pulse (if it reached `te_short_min`) as a synthetic symbol, +//! then the LOW pulse itself is classified. The symbol layer (`honda_v1_symbol`) walks a +//! Reset→Preamble→Data state machine; in the Data step a short pulse toggles a `data_pending` flag +//! and, when paired, emits the level as a bit, while a long pulse emits directly — this is the +//! exact pending-bit logic ported from the C. +//! +//! **Frame / fields**: after the end gap (>te_end), `commit` requires ≥68 collected bits, then +//! left-shifts the 12-byte bit buffer by `max(1, bit_count-68)` to drop leading preamble leakage +//! and align the trailing frame. `data` = first 8 bytes (64 bits), `k2` (CRC nibble) = byte 8's +//! high nibble. Fields (matches `honda_v1_decode_fields`): serial = data[63:36] (28b), +//! button = data[31:28] (nibble), counter = data[15:0] (16b). +//! +//! **Validation / gating**: a button-code table (Unlock=0, Lock=8, Trunk=9, Panic=10) plus a +//! CRC-fold checksum (`honda_v1_checksum*`). Emission is gated on the button being valid (matches +//! the C `commit`); `crc_valid` reflects whether the received CRC nibble matches either wire-order +//! checksum (`honda_v1_crc_valid`). The strong button gate keeps Honda V1 from false-matching. +//! +//! **Encoder** (matches `honda_v1_build_upload` / `honda_v1_append_frame`, behind +//! `ENABLE_EMULATE_FEATURE`): builds the 64-bit key from serial/button/counter via the button +//! table, then emits a 180-element short-pair preamble + 4 PWM frames (2 per checksum wire value). + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use crate::radio::demodulator::LevelDuration; + +const BIT_COUNT: usize = 68; +const TE_SHORT: u32 = 1000; +const TE_LONG: u32 = 2000; +const TE_DELTA: u32 = 400; +const TE_SHORT_MIN: u32 = 600; +const TE_END: u32 = 3500; +const VALID_MAX: u8 = 0x4B; // honda_v1_add_bit cap (75) +const NIBBLE_MASK: u8 = 0x0F; +const SERIAL_MASK: u32 = 0x0FFF_FFFF; +const COUNTER_MASK: u16 = 0xFFFF; +const BUTTON_MAX: u8 = 10; +const BUTTON_VALID_MASK: u16 = 0x701; // bits set for Unlock(0), Lock(8), Trunk(9), Panic(10) +const DECODE_BUFFER_BYTES: usize = 12; + +// Encoder constants (honda_v1.c, ENABLE_EMULATE_FEATURE). +const PREAMBLE_UPLOAD_COUNT: usize = 180; +const FRAME_SYMBOLS: usize = 80; +const FRAME_START: usize = 12; +const FRAME_SYNC_DROP: usize = 2; +const FRAME_REPEAT_PER_CRC: usize = 2; +const FRAME_GAP_US: u32 = 5000; +const FRAME_CRC_INDEX: usize = 8; + +// HondaV1Button codes (the on-wire button nibble at data[31:28]). +const BTN_CODE_UNLOCK: u8 = 0; +const BTN_CODE_LOCK: u8 = 8; +const BTN_CODE_TRUNK: u8 = 9; +const BTN_CODE_PANIC: u8 = 10; + +// honda_v1_button_codes[] (24-bit table values, used to rebuild the key in the encoder). +const BUTTON_CODE_UNLOCK: u32 = 0x0008_0808; +const BUTTON_CODE_LOCK: u32 = 0x0008_8888; +const BUTTON_CODE_TRUNK: u32 = 0x0009_9190; +const BUTTON_CODE_PANIC: u32 = 0x000F_A7A0; +const BUTTON_FALLBACK_CODE: u32 = 0x0008_8888; + +// KAT generic button codes. +const BTN_LOCK: u8 = 0x01; +const BTN_UNLOCK: u8 = 0x02; +const BTN_TRUNK: u8 = 0x04; +const BTN_PANIC: u8 = 0x08; + +/// Decoder steps (matches HondaV1DecoderStep). +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + Preamble, + Data, +} + +/// Honda V1 decoder (matches SubGhzProtocolDecoderHondaV1). +pub struct HondaV1Decoder { + step: DecoderStep, + preamble_count: u8, + preamble_has_long: bool, + data_pending: bool, + last_level: bool, + bits: [u8; DECODE_BUFFER_BYTES], + bit_count: u8, + // Pending-bit timing accumulator (decoder-level, persists across symbol resets). + pending: u32, + pending_valid: bool, +} + +impl HondaV1Decoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + preamble_count: 0, + preamble_has_long: false, + data_pending: false, + last_level: false, + bits: [0u8; DECODE_BUFFER_BYTES], + bit_count: 0, + pending: 0, + pending_valid: false, + } + } + + /// honda_v1_button_valid: button <= 10 && ((0x701 >> button) & 1). + fn button_valid(b: u8) -> bool { + if b > BUTTON_MAX { + return false; + } + ((BUTTON_VALID_MASK >> b) & 1) != 0 + } + + /// honda_v1_duration_is: |d - t| <= te_delta (saturating both directions). + fn duration_is(d: u32, t: u32) -> bool { + if d >= t { + (d - t) <= TE_DELTA + } else { + (t - d) <= TE_DELTA + } + } + + /// honda_v1_crc_fold. + fn crc_fold(v: u16) -> u8 { + let lo = (v & (NIBBLE_MASK as u16)) as u8; + let hi = v >> 4; + let s: i32 = if (hi & 1) != 0 { + lo as i32 + } else { + -(lo as i32) + }; + let mut out = ((s - (hi as i32)) & 7) as u8; + out |= (((v >> 3) & 1) as u8) << 3; + if ((v >> 1) & 1) != 0 && (((v >> 4) ^ (v >> 5)) & 1) != 0 { + out ^= 0x04; + } + out & NIBBLE_MASK + } + + /// honda_v1_checksum_base. + fn checksum_base(data: u64) -> u8 { + let a = Self::crc_fold((data & (COUNTER_MASK as u64)) as u16); + let b = Self::crc_fold(((data >> 40) & 0xFF) as u16); + (a ^ b ^ 1) & NIBBLE_MASK + } + + /// honda_v1_checksum_alternate. + fn checksum_alternate(checksum: u8) -> u8 { + let mut mask = 0x09u8; + if (checksum & 1) == 0 { + mask = if (checksum & 2) != 0 { 0x0B } else { NIBBLE_MASK }; + } + (checksum ^ mask) & NIBBLE_MASK + } + + /// honda_v1_checksum_wire_order → (first, second). + fn checksum_wire_order(data: u64) -> (u8, u8) { + let checksum = Self::checksum_base(data); + let other = Self::checksum_alternate(checksum); + if (checksum & 0x08) != 0 { + (other, checksum) + } else { + (checksum, other) + } + } + + /// honda_v1_crc_valid: received nibble matches either wire-order checksum. + fn crc_valid(data: u64, crc: u8) -> bool { + let (first, second) = Self::checksum_wire_order(data); + let crc = crc & NIBBLE_MASK; + crc == first || crc == second + } + + /// honda_v1_decode_fields. Returns (serial, button, counter). + fn decode_fields(data: u64) -> (u32, u8, u16) { + let low = (data & 0xFFFF_FFFF) as u32; + let serial = ((data >> 36) & (SERIAL_MASK as u64)) as u32; + let button = ((low >> 28) & (NIBBLE_MASK as u32)) as u8; + let counter = (low & (COUNTER_MASK as u32)) as u16; + (serial, button, counter) + } + + /// honda_v1_button_code (encoder). + fn button_code(button: u8) -> u32 { + if !Self::button_valid(button) { + return BUTTON_FALLBACK_CODE; + } + match button { + BTN_CODE_UNLOCK => BUTTON_CODE_UNLOCK, + BTN_CODE_LOCK => BUTTON_CODE_LOCK, + BTN_CODE_TRUNK => BUTTON_CODE_TRUNK, + BTN_CODE_PANIC => BUTTON_CODE_PANIC, + _ => BUTTON_FALLBACK_CODE, + } + } + + /// honda_v1_build_key. + fn build_key(serial: u32, button: u8, counter: u16) -> u64 { + let table = Self::button_code(button); + let low = ((table & (COUNTER_MASK as u32)) << 16) | (counter as u32); + let high = ((serial & SERIAL_MASK) << 4) | (table >> 16); + ((high as u64) << 32) | (low as u64) + } + + /// Map a KAT generic button command to a Honda V1 button code (nibble at data[31:28]). + fn map_button(button: u8) -> u8 { + match button { + BTN_LOCK => BTN_CODE_LOCK, + BTN_UNLOCK => BTN_CODE_UNLOCK, + BTN_TRUNK => BTN_CODE_TRUNK, + BTN_PANIC => BTN_CODE_PANIC, + // Already a valid Honda V1 code → pass through. + b if Self::button_valid(b) => b, + _ => BTN_CODE_UNLOCK, + } + } + + /// honda_v1_state_reset (symbol-layer state only; does NOT touch pending accumulator). + fn state_reset(&mut self) { + self.step = DecoderStep::Reset; + self.preamble_count = 0; + self.preamble_has_long = false; + self.data_pending = false; + self.last_level = false; + self.bit_count = 0; + self.bits = [0u8; DECODE_BUFFER_BYTES]; + } + + /// honda_v1_add_bit: MSB-first into bits[], capped at VALID_MAX. + fn add_bit(&mut self, bit: bool) { + if self.bit_count > VALID_MAX { + return; + } + if bit { + let byte = (self.bit_count >> 3) as usize; + let shift = (!self.bit_count) & 0x07; + self.bits[byte] |= 1u8 << shift; + } + self.bit_count += 1; + } + + /// honda_v1_commit: align trailing 68-bit frame, validate button, return signal on success. + fn commit(&mut self) -> Option { + if (self.bit_count as usize) < BIT_COUNT { + return None; + } + + let mut aligned = self.bits; + + let mut shift_count = self.bit_count - BIT_COUNT as u8; + if shift_count < 1 { + shift_count = 1; + } + + for _ in 0..shift_count { + for i in 0..(DECODE_BUFFER_BYTES - 1) { + aligned[i] = (aligned[i] << 1) | (aligned[i + 1] >> 7); + } + aligned[DECODE_BUFFER_BYTES - 1] <<= 1; + } + + let button = aligned[4] >> 4; + if !Self::button_valid(button) { + return None; + } + + let data = u64::from_be_bytes(aligned[0..8].try_into().unwrap()); + let k2 = aligned[8] >> 4; + let (serial, btn, counter) = Self::decode_fields(data); + let crc_valid = Self::crc_valid(data, k2); + + Some(DecodedSignal { + serial: Some(serial), + button: Some(btn), + counter: Some(counter), + crc_valid, + data, + data_count_bit: BIT_COUNT, + encoder_capable: true, + extra: None, + protocol_display_name: None, + }) + } + + /// honda_v1_symbol: classify a (level, duration) pulse and drive the state machine. + /// Returns Some(signal) when the end-gap path commits a frame. + fn symbol(&mut self, level: bool, duration: u32) -> Option { + let sh = Self::duration_is(duration, TE_SHORT); + let lg = Self::duration_is(duration, TE_LONG); + + if !sh && !lg { + let mut result = None; + if !level && duration > TE_END && self.step == DecoderStep::Data { + result = self.commit(); + } + self.state_reset(); + return result; + } + + if self.step == DecoderStep::Reset { + if level { + self.step = DecoderStep::Preamble; + self.preamble_count = 1; + self.last_level = level; + } + return None; + } + + if self.step == DecoderStep::Preamble { + if lg { + // honda_v1.c: if(preamble_count < 0xFF) preamble_count++ (saturating). + self.preamble_count = self.preamble_count.saturating_add(1); + self.preamble_has_long = true; + self.last_level = level; + return None; + } + + if sh { + if self.preamble_has_long && self.preamble_count > 5 { + self.step = DecoderStep::Data; + self.bit_count = 0; + self.bits = [0u8; DECODE_BUFFER_BYTES]; + self.data_pending = true; + self.last_level = level; + return None; + } + + self.preamble_count = self.preamble_count.saturating_add(1); + self.last_level = level; + return None; + } + + self.state_reset(); + return None; + } + + // Data step: pending-bit accumulation. + if sh { + if self.data_pending { + self.add_bit(level); + self.data_pending = false; + self.last_level = level; + } else { + self.data_pending = true; + self.last_level = level; + } + } else { + // long pulse + if self.data_pending { + self.add_bit(level); + } else { + self.add_bit(self.last_level); + } + self.last_level = level; + } + + None + } + + fn enc_add_level(signal: &mut Vec, level: bool, duration: u32) { + if let Some(last) = signal.last_mut() { + if last.level == level { + *last = LevelDuration::new(level, last.duration_us + duration); + return; + } + } + signal.push(LevelDuration::new(level, duration)); + } + + /// honda_v1_append_frame: emit one PWM frame (12-symbol sync header + 68 data symbols), + /// dropping the first FRAME_SYNC_DROP entries, then a tail + inter-frame gap. + fn append_frame(signal: &mut Vec, frame: &[u8; 9]) { + // Build the per-frame symbol stream with merge semantics (pp_emit_merge). + let mut generated: Vec = Vec::with_capacity(FRAME_SYMBOLS * 2); + for bit_index in 0..FRAME_SYMBOLS { + let bit = if bit_index >= FRAME_START { + let data_index = (bit_index - FRAME_START) >> 3; + let shift = (11i32 - bit_index as i32) & 0x07; + ((frame[data_index] >> shift) & 0x01) != 0 + } else { + ((!bit_index) & 0x01) != 0 + }; + // bit -> (level=bit, te)(level=!bit, te), merged. + Self::enc_add_level(&mut generated, bit, TE_SHORT); + Self::enc_add_level(&mut generated, !bit, TE_SHORT); + } + + if generated.len() <= FRAME_SYNC_DROP { + return; + } + + // Copy generated[FRAME_SYNC_DROP..] into the upload (still merging at the seam). + for ld in &generated[FRAME_SYNC_DROP..] { + Self::enc_add_level(signal, ld.level, ld.duration_us); + } + + // Tail: !last_level for te_short; if that was low, add a high te_short; then the gap. + let last_level = signal.last().map(|l| l.level).unwrap_or(false); + let tail_level = !last_level; + Self::enc_add_level(signal, tail_level, TE_SHORT); + if !tail_level { + Self::enc_add_level(signal, true, TE_SHORT); + } + Self::enc_add_level(signal, false, FRAME_GAP_US); + } +} + +impl ProtocolDecoder for HondaV1Decoder { + fn name(&self) -> &'static str { + "Honda V1" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: BIT_COUNT, + } + } + + fn supported_frequencies(&self) -> &[u32] { + &[315_000_000, 433_920_000] + } + + fn reset(&mut self) { + self.pending = 0; + self.pending_valid = false; + self.state_reset(); + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + // Pending-bit timing accumulation (subghz_protocol_decoder_honda_v1_feed). + if duration < TE_DELTA { + self.pending = self.pending.saturating_add(duration); + self.pending_valid = true; + return None; + } + + let mut result = None; + + if self.pending_valid { + let p = self.pending; + if level { + self.pending = p.saturating_add(duration); + self.pending_valid = true; + return None; + } + if p >= TE_SHORT_MIN { + result = self.symbol(true, p); + } + self.pending = 0; + self.pending_valid = false; + } + + if level { + self.pending = duration; + self.pending_valid = true; + return result; + } + + // A symbol committed on the flushed HIGH pulse takes priority (the C calls back there); + // otherwise classify this LOW pulse. + let low_result = self.symbol(false, duration); + result.or(low_result) + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + let serial = decoded.serial?; + let counter = decoded.counter.unwrap_or(0) & COUNTER_MASK; + let btn = Self::map_button(button); + let data = Self::build_key(serial & SERIAL_MASK, btn, counter); + + let mut frame = [0u8; 9]; + frame[0..8].copy_from_slice(&data.to_be_bytes()); + let (first, second) = Self::checksum_wire_order(data); + + let mut signal: Vec = Vec::with_capacity(PREAMBLE_UPLOAD_COUNT + 8 * FRAME_SYMBOLS); + + // Preamble: 180 short entries (90 H/L pairs); the final LOW becomes a 5000µs gap. + for _ in 0..(PREAMBLE_UPLOAD_COUNT / 2) { + signal.push(LevelDuration::new(true, TE_SHORT)); + signal.push(LevelDuration::new(false, TE_SHORT)); + } + if let Some(last) = signal.last_mut() { + *last = LevelDuration::new(false, FRAME_GAP_US); + } + + // 4 frames: 2 per checksum wire value (first, then second). + for &crc in &[first, second] { + frame[FRAME_CRC_INDEX] = crc << 4; + for _ in 0..FRAME_REPEAT_PER_CRC { + Self::append_frame(&mut signal, &frame); + } + } + + Some(signal) + } +} + +impl Default for HondaV1Decoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// build_key + decode_fields must round-trip the fields, and the encoder's CRC nibble must + /// validate against honda_v1_crc_valid. + #[test] + fn honda_v1_key_field_roundtrip() { + let serial = 0x0ABCDEFu32 & SERIAL_MASK; + let counter = 0x1234u16; + let data = HondaV1Decoder::build_key(serial, BTN_CODE_TRUNK, counter); + let (s, b, c) = HondaV1Decoder::decode_fields(data); + assert_eq!(s, serial, "serial mismatch"); + assert_eq!(b, BTN_CODE_TRUNK, "button mismatch"); + assert_eq!(c, counter, "counter mismatch"); + + let (first, _second) = HondaV1Decoder::checksum_wire_order(data); + assert!( + HondaV1Decoder::crc_valid(data, first), + "wire-order checksum should validate" + ); + } + + /// Full encode → decode round-trip: emit a frame for known fields, feed the pulses back through + /// the decoder (the pending-bit accumulator + symbol state machine), and confirm the fields and + /// checksum survive. + #[test] + fn honda_v1_encode_decode_roundtrip() { + let serial = 0x0123456u32 & SERIAL_MASK; + let counter = 0x00ABu16; + let original = DecodedSignal { + serial: Some(serial), + button: Some(BTN_UNLOCK), + counter: Some(counter), + crc_valid: true, + data: 0, + data_count_bit: BIT_COUNT, + encoder_capable: true, + extra: None, + protocol_display_name: None, + }; + + let decoder = HondaV1Decoder::new(); + let signal = decoder.encode(&original, BTN_UNLOCK).expect("encode"); + + let mut dec = HondaV1Decoder::new(); + let mut decoded = None; + for ld in &signal { + if let Some(d) = dec.feed(ld.level, ld.duration_us) { + decoded = Some(d); + break; + } + } + // Flush with a terminating end-gap if the inter-frame gaps didn't already commit. + if decoded.is_none() { + decoded = dec.feed(false, TE_END + 1000); + } + + let decoded = decoded.expect("expected a decode from the round-tripped frame"); + assert!(decoded.crc_valid, "checksum should validate"); + assert_eq!(decoded.serial, Some(serial), "serial mismatch"); + assert_eq!( + decoded.button, + Some(BTN_CODE_UNLOCK), + "button should map to Honda V1 Unlock=0" + ); + assert_eq!(decoded.counter, Some(counter), "counter mismatch"); + assert_eq!(decoded.data_count_bit, BIT_COUNT); + } + + /// Button validity mask must match honda_v1_button_valid (0x701 → Unlock/Lock/Trunk/Panic). + #[test] + fn honda_v1_button_validity() { + assert!(HondaV1Decoder::button_valid(BTN_CODE_UNLOCK)); + assert!(HondaV1Decoder::button_valid(BTN_CODE_LOCK)); + assert!(HondaV1Decoder::button_valid(BTN_CODE_TRUNK)); + assert!(HondaV1Decoder::button_valid(BTN_CODE_PANIC)); + // Invalid codes. + assert!(!HondaV1Decoder::button_valid(1)); + assert!(!HondaV1Decoder::button_valid(7)); + assert!(!HondaV1Decoder::button_valid(11)); + } +} diff --git a/src/protocols/keeloq_common.rs b/src/protocols/keeloq_common.rs index 9f151f7..184a3ba 100644 --- a/src/protocols/keeloq_common.rs +++ b/src/protocols/keeloq_common.rs @@ -30,7 +30,9 @@ fn g5(x: u32, a: u32, b: u32, c: u32, d: u32, e: u32) -> u32 { pub fn keeloq_decrypt(data: u32, key: u64) -> u32 { let mut x = data; for r in 0..528u32 { - let key_bit = ((key >> ((15 - r) & 63)) & 1) as u32; + // (15 - r) relies on unsigned wraparound (matches the C reference `(15 - r) & 63`); + // use wrapping_sub so debug builds don't panic on the overflow that release silently wraps. + let key_bit = ((key >> ((15u32.wrapping_sub(r)) & 63)) & 1) as u32; let new_lsb = bit(x, 31) ^ bit(x, 15) ^ key_bit ^ bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30)); x = (x << 1) ^ new_lsb; diff --git a/src/protocols/kia_v7.rs b/src/protocols/kia_v7.rs new file mode 100644 index 0000000..1808668 --- /dev/null +++ b/src/protocols/kia_v7.rs @@ -0,0 +1,314 @@ +//! Kia V7 protocol decoder/encoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v7.c` and `kia_v7.h`. +//! Manchester 250/500µs, 64 bits, FM. The decoded 64-bit word is bit-inverted (`~data`); a valid +//! frame has a fixed high byte 0x4C and a CRC8 (poly 0x7F, init 0x4C) over bytes 0..7. Emission is +//! gated on header + CRC, so Kia V7 is strongly validated and will not false-match. +//! +//! Decoder steps: Reset → Preamble (short pairs, ≥16) → SyncLow → Data. The preamble→sync transition +//! preloads four seed bits (1,0,1,1 = the inverted header's top nibble 0xB) before collecting the +//! remaining 60 Manchester bits. Encoder supported. + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use super::common::crc8; +use crate::radio::demodulator::LevelDuration; +use crate::duration_diff; + +const TE_SHORT: u32 = 250; +const TE_LONG: u32 = 500; +const TE_DELTA: u32 = 100; +const KEY_BITS: usize = 64; +const HEADER: u8 = 0x4C; +const PREAMBLE_MIN_PAIRS: u16 = 16; +const TAIL_GAP_US: u32 = 2000; +const TX_PREAMBLE_PAIRS: usize = 32; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ManchesterState { + Mid0 = 0, + Mid1 = 1, + Start0 = 2, + Start1 = 3, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + Preamble, + SyncLow, + Data, +} + +pub struct KiaV7Decoder { + step: DecoderStep, + manchester_state: ManchesterState, + te_last: u32, + preamble_count: u16, + decode_data: u64, + decode_count_bit: usize, +} + +impl KiaV7Decoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + manchester_state: ManchesterState::Mid1, + te_last: 0, + preamble_count: 0, + decode_data: 0, + decode_count_bit: 0, + } + } + + fn is_short(d: u32) -> bool { + duration_diff!(d, TE_SHORT) < TE_DELTA + } + fn is_long(d: u32) -> bool { + duration_diff!(d, TE_LONG) < TE_DELTA + } + + fn add_bit(&mut self, bit: bool) { + self.decode_data = (self.decode_data << 1) | (bit as u64); + self.decode_count_bit += 1; + } + + fn manchester_advance(&mut self, event: u8) -> Option { + let (new_state, emit) = match (self.manchester_state, event) { + (ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 1) => (ManchesterState::Start1, true), + (ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true), + + (ManchesterState::Mid1, 0) => (ManchesterState::Start0, true), + (ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true), + (ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 1) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start1, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Start1, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 3) => (ManchesterState::Mid1, false), + + _ => (ManchesterState::Mid1, false), + }; + self.manchester_state = new_state; + if emit { Some((event & 1) == 1) } else { None } + } + + /// Decode the 8 plaintext bytes of the (already inverted) key into fields. + /// Returns (serial, button, counter, crc_valid). + fn decode_key(data: u64) -> (u32, u8, u16, bool) { + let bytes = data.to_be_bytes(); + let serial = (((bytes[3] as u32) << 20) + | ((bytes[4] as u32) << 12) + | ((bytes[5] as u32) << 4) + | ((bytes[6] as u32) >> 4)) + & 0x0FFF_FFFF; + let counter = ((bytes[1] as u16) << 8) | bytes[2] as u16; + let button = bytes[6] & 0x0F; + let crc_calc = crc8(&bytes[0..7], 0x7F, 0x4C); + let crc_valid = crc_calc == bytes[7]; + (serial, button, counter, crc_valid) + } + + /// Rebuild the 64-bit key from fields (matches kia_v7_encode_key). + fn encode_key(serial: u32, button: u8, counter: u16) -> u64 { + let serial = serial & 0x0FFF_FFFF; + let button = button & 0x0F; + let mut bytes = [0u8; 8]; + bytes[0] = HEADER; + bytes[1] = (counter >> 8) as u8; + bytes[2] = counter as u8; + bytes[3] = (serial >> 20) as u8; + bytes[4] = (serial >> 12) as u8; + bytes[5] = (serial >> 4) as u8; + bytes[6] = (((serial & 0x0F) as u8) << 4) | button; + bytes[7] = crc8(&bytes[0..7], 0x7F, 0x4C); + u64::from_be_bytes(bytes) + } + + /// Map KAT generic button command to a Kia V7 4-bit button code. + fn map_button(button: u8) -> u8 { + match button { + 0x01 => 0x01, // Lock + 0x02 => 0x02, // Unlock + 0x04 => 0x03, // Trunk + 0x08 => 0x08, // Trunk/aux + b => b & 0x0F, + } + } + + fn enc_add_level(signal: &mut Vec, level: bool, duration: u32) { + if let Some(last) = signal.last_mut() { + if last.level == level { + *last = LevelDuration::new(level, last.duration_us + duration); + return; + } + } + signal.push(LevelDuration::new(level, duration)); + } +} + +impl ProtocolDecoder for KiaV7Decoder { + fn name(&self) -> &'static str { + "Kia V7" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: KEY_BITS, + } + } + + fn supported_frequencies(&self) -> &[u32] { + &[315_000_000, 433_920_000] + } + + fn reset(&mut self) { + self.step = DecoderStep::Reset; + self.manchester_state = ManchesterState::Mid1; + self.te_last = 0; + self.preamble_count = 0; + self.decode_data = 0; + self.decode_count_bit = 0; + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.step { + DecoderStep::Reset => { + if level && Self::is_short(duration) { + self.step = DecoderStep::Preamble; + self.te_last = duration; + self.preamble_count = 0; + self.manchester_state = ManchesterState::Mid1; + } + } + + DecoderStep::Preamble => { + if level { + if Self::is_long(duration) && Self::is_short(self.te_last) { + if self.preamble_count > (PREAMBLE_MIN_PAIRS - 1) { + self.decode_data = 0; + self.decode_count_bit = 0; + self.preamble_count = 0; + // Seed the inverted-header top nibble (1,0,1,1). + self.add_bit(true); + self.add_bit(false); + self.add_bit(true); + self.add_bit(true); + self.te_last = duration; + self.step = DecoderStep::SyncLow; + } else { + self.step = DecoderStep::Reset; + } + } else if Self::is_short(duration) { + self.te_last = duration; + } else { + self.step = DecoderStep::Reset; + } + } else if Self::is_short(duration) && Self::is_short(self.te_last) { + self.preamble_count += 1; + } else { + self.step = DecoderStep::Reset; + } + } + + DecoderStep::SyncLow => { + if !level && Self::is_short(duration) && Self::is_long(self.te_last) { + self.te_last = duration; + self.step = DecoderStep::Data; + } + } + + DecoderStep::Data => { + let event = if Self::is_short(duration) { + Some(if level { 1 } else { 0 }) + } else if Self::is_long(duration) { + Some(if level { 3 } else { 2 }) + } else { + None + }; + + if let Some(ev) = event { + if let Some(bit) = self.manchester_advance(ev) { + self.add_bit(bit); + } + } + + if self.decode_count_bit == KEY_BITS { + let candidate = !self.decode_data; + let hdr = (candidate >> 56) as u8; + self.decode_data = 0; + self.decode_count_bit = 0; + self.step = DecoderStep::Reset; + + if hdr == HEADER { + let (serial, button, counter, crc_valid) = Self::decode_key(candidate); + if crc_valid { + return Some(DecodedSignal { + serial: Some(serial), + button: Some(button), + counter: Some(counter), + crc_valid: true, + data: candidate, + data_count_bit: KEY_BITS, + encoder_capable: true, + extra: None, + protocol_display_name: None, + }); + } + } + } + } + } + None + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + let serial = decoded.serial?; + let counter = decoded.counter.unwrap_or(0); + let key = Self::encode_key(serial, Self::map_button(button), counter); + + let mut signal = Vec::with_capacity(TX_PREAMBLE_PAIRS * 2 + KEY_BITS * 2 + 4); + // Preamble: alternating short pulses. + for _ in 0..TX_PREAMBLE_PAIRS { + Self::enc_add_level(&mut signal, true, TE_SHORT); + Self::enc_add_level(&mut signal, false, TE_SHORT); + } + // Standalone high short (merges with first data-bit high to form the long sync pulse). + Self::enc_add_level(&mut signal, true, TE_SHORT); + // Manchester data, MSB first: bit 1 → (H,L), bit 0 → (L,H). + for bit in (0..KEY_BITS).rev() { + let value = (key >> bit) & 1 != 0; + if value { + Self::enc_add_level(&mut signal, true, TE_SHORT); + Self::enc_add_level(&mut signal, false, TE_SHORT); + } else { + Self::enc_add_level(&mut signal, false, TE_SHORT); + Self::enc_add_level(&mut signal, true, TE_SHORT); + } + } + // Trailing high short + tail gap. + Self::enc_add_level(&mut signal, true, TE_SHORT); + Self::enc_add_level(&mut signal, false, TAIL_GAP_US); + Some(signal) + } +} + +impl Default for KiaV7Decoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/land_rover_rke.rs b/src/protocols/land_rover_rke.rs new file mode 100644 index 0000000..44347e5 --- /dev/null +++ b/src/protocols/land_rover_rke.rs @@ -0,0 +1,531 @@ +//! Land Rover RKE protocol decoder/encoder +//! +//! Ported from the Flipper-ARF firmware (`lib/subghz/protocols/landrover_rke.c` / `.h`, +//! D4C1-Labs), itself derived from Pandora DXL 5000 firmware. Land Rover shares the +//! Ford/Jaguar baseband (firmware protocol ID 0x0E) but uses a distinct 66-bit frame. +//! +//! Encoding: fixed-width PWM, OOK/AM carrier. Bit period 1000µs: +//! Bit-1 = 700µs HIGH + 300µs LOW; Bit-0 = 300µs HIGH + 700µs LOW. +//! Preamble = 20× (400µs HIGH + 600µs LOW); sync = 400µs HIGH + 9600µs LOW. +//! Tolerance ±20% (relative), matching the C `lr_in_range`. +//! +//! Frame (66 bits, MSB-first), matching the C layout: +//! [65:34] 32-bit KeeLoq encrypted hopping code +//! [33:10] 24-bit fixed fob serial +//! [9:6] 4-bit button code (0x1=Lock, 0x2=Unlock, 0x4=Boot/Tailgate, 0x8=Panic) +//! [5:2] 4-bit function/repeat flags +//! [1:0] 2-bit status (0x1=battery low, 0x2=repeat) +//! +//! KeeLoq: the hop code is the raw 32-bit KeeLoq ciphertext. Full decryption needs the +//! per-fob manufacturer key (provisioned, not in the firmware), so KAT exposes the framed +//! fields (serial/button) and leaves the hop encrypted — `crc_valid=false` since no real +//! cryptographic check is performed. Emission is still gated tightly on the structural +//! invariants below so this loose-looking PWM decoder does not false-match +//! Kia/Subaru/Ford captures. +//! +//! Storage: 66 bits do not fit a u64. The canonical `DecodedSignal.data` holds the low 64 +//! frame bits and `DecodedSignal.extra` holds the top 2 (frame bits [65:64], the high 2 bits +//! of the hop code), so encode() round-trips the hop code losslessly. `data_count_bit` = 66. +//! +//! Gating: a valid frame requires a long preamble run (≥16 of the 400/600µs pairs), the very +//! distinctive 400µs-HIGH + 9600µs-LOW sync gap, then exactly 66 bits whose HIGH/LOW halves +//! each fall inside the ±20% PWM windows. The 9.6ms sync gap + 66-bit PWM payload combination +//! is unique among KAT's protocols, so nothing else in the sweep matches it. + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use crate::radio::demodulator::LevelDuration; + +// Timing constants (microseconds) — verbatim from landrover_rke.h. +const PREAMBLE_HIGH_US: u32 = 400; +const PREAMBLE_LOW_US: u32 = 600; +const PREAMBLE_COUNT: u32 = 20; +const SYNC_HIGH_US: u32 = 400; +const SYNC_LOW_US: u32 = 9600; +const BIT1_HIGH_US: u32 = 700; +const BIT1_LOW_US: u32 = 300; +const BIT0_HIGH_US: u32 = 300; +const BIT0_LOW_US: u32 = 700; +const REPEAT_GAP_US: u32 = 12000; +const REPEAT_COUNT: u32 = 4; +const TOLERANCE_PCT: u32 = 20; +const FRAME_BITS: usize = 66; + +// Require a substantial preamble run before accepting the sync gap. The C scans a raw buffer +// for the sync directly, but gating on the preamble too makes the streaming decoder specific. +const PREAMBLE_MIN_PAIRS: u32 = 16; + +// Button codes (frame bits [9:6]). +const BTN_LOCK: u8 = 0x1; +const BTN_UNLOCK: u8 = 0x2; +const BTN_BOOT: u8 = 0x4; +const BTN_PANIC: u8 = 0x8; + +/// Relative-tolerance match, matching the C `lr_in_range`: +/// `|measured - ref| * 100 <= ref * TOLERANCE_PCT`. +#[inline] +fn in_range(measured_us: u32, ref_us: u32) -> bool { + let diff = if measured_us > ref_us { + measured_us - ref_us + } else { + ref_us - measured_us + }; + diff.saturating_mul(100) <= ref_us.saturating_mul(TOLERANCE_PCT) +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + /// Looking for the first preamble HIGH pulse. + Reset, + /// Counting preamble (400µs HIGH + 600µs LOW) pairs; te_last holds the pending HIGH. + Preamble, + /// Sync seen — expecting the next data-bit HIGH half. + DataHigh, + /// Collected a data-bit HIGH in te_last; decide the bit value from the following LOW. + DataLow, +} + +/// Land Rover RKE protocol decoder. +/// +/// Bits are accumulated MSB-first into a 66-element array (index 0 = first bit on air = +/// frame bit 65), mirroring the C `bits[65 - b]` indexing and avoiding any u64 overflow. +pub struct LandRoverRkeDecoder { + step: DecoderStep, + te_last: u32, + preamble_count: u32, + /// Received bits, MSB-first: `rx_bits[i]` is the (i+1)-th bit on air = frame bit `65 - i`. + rx_bits: [u8; FRAME_BITS], + decode_count_bit: usize, +} + +impl LandRoverRkeDecoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + te_last: 0, + preamble_count: 0, + rx_bits: [0u8; FRAME_BITS], + decode_count_bit: 0, + } + } + + /// Map a KAT generic button command to a Land Rover RKE 4-bit button code. + /// KAT: Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08. + /// LR RKE bits [9:6]: Lock=0x1, Unlock=0x2, Boot/Tailgate=0x4, Panic=0x8 — identical mapping. + fn map_button(button: u8) -> u8 { + match button { + 0x01 => BTN_LOCK, + 0x02 => BTN_UNLOCK, + 0x04 => BTN_BOOT, + 0x08 => BTN_PANIC, + b => b & 0x0F, + } + } + + /// Build the logical 66-bit frame array from the field values. + /// + /// The array is indexed by frame bit number (bit 0 = LSB of the whole 66-bit frame). Each + /// field `X` occupying frame bits `[hi:lo]` maps frame bit `(lo + j)` = field bit `j`, so the + /// field's MSB lands at the higher frame-bit index. The encoder transmits frame bit 65 first, + /// giving the documented MSB-first-on-air order. This is the exact inverse of `unpack_frame`. + /// + /// NOTE: the Flipper-ARF C reference's `lr_encode`/`lr_decode` use inconsistent bit + /// endianness for the fields (encode writes `bits[65-i]=field>>i`, decode reads + /// `field|=bits[65-k]<<(31-k)`), so the C does not round-trip. KAT preserves the documented + /// field *layout* and MSB-first wire order while keeping encode/decode mutually consistent. + fn pack_frame(hop_code: u32, serial: u32, button: u8, func_bits: u8, status: u8) -> [u8; FRAME_BITS] { + let mut bits = [0u8; FRAME_BITS]; + // hop_code: frame bits [65:34] (32 bits). + for j in 0..32 { + bits[34 + j] = ((hop_code >> j) & 1) as u8; + } + // serial: frame bits [33:10] (24 bits). + for j in 0..24 { + bits[10 + j] = ((serial >> j) & 1) as u8; + } + // button: frame bits [9:6] (4 bits). + for j in 0..4 { + bits[6 + j] = ((button >> j) & 1) as u8; + } + // func_bits: frame bits [5:2] (4 bits). + for j in 0..4 { + bits[2 + j] = ((func_bits >> j) & 1) as u8; + } + // status: frame bits [1:0] (2 bits). + bits[0] = status & 1; + bits[1] = (status >> 1) & 1; + bits + } + + /// Extract fields from a logical 66-bit frame array (index = frame bit, bit 0 = LSB). + /// Returns (hop_code, serial, button, func_bits, status). Exact inverse of `pack_frame`. + fn unpack_frame(bits: &[u8; FRAME_BITS]) -> (u32, u32, u8, u8, u8) { + let mut hop_code: u32 = 0; + for j in 0..32 { + hop_code |= (bits[34 + j] as u32) << j; + } + let mut serial: u32 = 0; + for j in 0..24 { + serial |= (bits[10 + j] as u32) << j; + } + let mut button: u8 = 0; + for j in 0..4 { + button |= bits[6 + j] << j; + } + let mut func_bits: u8 = 0; + for j in 0..4 { + func_bits |= bits[2 + j] << j; + } + let status = bits[0] | (bits[1] << 1); + (hop_code, serial, button, func_bits, status) + } + + /// Pack a logical 66-bit frame array into `(data, extra)`: + /// `data` = frame bits [63:0], `extra` = frame bits [65:64]. + fn frame_to_data(bits: &[u8; FRAME_BITS]) -> (u64, u64) { + let mut data: u64 = 0; + for i in 0..64 { + data |= (bits[i] as u64) << i; + } + let extra: u64 = (bits[64] as u64) | ((bits[65] as u64) << 1); + (data, extra) + } + + /// Inverse of `frame_to_data`. + fn data_to_frame(data: u64, extra: u64) -> [u8; FRAME_BITS] { + let mut bits = [0u8; FRAME_BITS]; + for i in 0..64 { + bits[i] = ((data >> i) & 1) as u8; + } + bits[64] = (extra & 1) as u8; + bits[65] = ((extra >> 1) & 1) as u8; + bits + } + + /// Convert the received MSB-first `rx_bits` (index 0 = frame bit 65) into the logical + /// frame array (index = frame bit number). + fn rx_to_frame(rx_bits: &[u8; FRAME_BITS]) -> [u8; FRAME_BITS] { + let mut frame = [0u8; FRAME_BITS]; + for i in 0..FRAME_BITS { + frame[65 - i] = rx_bits[i]; + } + frame + } + + /// Build a DecodedSignal from a completed received-bit array. + fn build_signal(rx_bits: &[u8; FRAME_BITS]) -> DecodedSignal { + let frame = Self::rx_to_frame(rx_bits); + let (hop_code, serial, button, _func_bits, _status) = Self::unpack_frame(&frame); + let (data, extra) = Self::frame_to_data(&frame); + DecodedSignal { + serial: Some(serial), + button: Some(button), + // The 16-bit KeeLoq counter is inside the *encrypted* hop code; without the + // manufacturer key we cannot recover it. Surface the low 16 bits of the hop + // ciphertext so the UI has a stable per-press value. + counter: Some((hop_code & 0xFFFF) as u16), + // No cryptographic check is performed (no key), so this is not a verified frame. + crc_valid: false, + data, + data_count_bit: FRAME_BITS, + encoder_capable: true, + // Carry the top 2 frame bits so encode() can faithfully round-trip the hop code. + extra: Some(extra), + protocol_display_name: None, + } + } + + fn push_pair(signal: &mut Vec, high_us: u32, low_us: u32) { + signal.push(LevelDuration::new(true, high_us)); + signal.push(LevelDuration::new(false, low_us)); + } +} + +impl ProtocolDecoder for LandRoverRkeDecoder { + fn name(&self) -> &'static str { + "Land Rover RKE" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: BIT0_HIGH_US, // 300µs + te_long: BIT1_HIGH_US, // 700µs + te_delta: 140, // ~20% of the 700µs long half + min_count_bit: FRAME_BITS, + } + } + + fn supported_frequencies(&self) -> &[u32] { + &[433_920_000, 315_000_000] + } + + fn reset(&mut self) { + self.step = DecoderStep::Reset; + self.te_last = 0; + self.preamble_count = 0; + self.rx_bits = [0u8; FRAME_BITS]; + self.decode_count_bit = 0; + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.step { + DecoderStep::Reset => { + // First preamble HIGH (~400µs). + if level && in_range(duration, PREAMBLE_HIGH_US) { + self.step = DecoderStep::Preamble; + self.te_last = duration; + self.preamble_count = 0; + } + } + + DecoderStep::Preamble => { + if level { + // A HIGH while in preamble: either another preamble HIGH (~400µs) or a sync + // HIGH (also ~400µs) — disambiguated by the LOW that follows. + if in_range(duration, PREAMBLE_HIGH_US) { + self.te_last = duration; + } else { + self.step = DecoderStep::Reset; + } + } else { + // LOW following a preamble/sync HIGH. + if in_range(self.te_last, SYNC_HIGH_US) && in_range(duration, SYNC_LOW_US) { + // Sync gap (~9600µs LOW). Require enough preamble first. + if self.preamble_count >= PREAMBLE_MIN_PAIRS { + self.rx_bits = [0u8; FRAME_BITS]; + self.decode_count_bit = 0; + self.step = DecoderStep::DataHigh; + } else { + self.step = DecoderStep::Reset; + } + } else if in_range(self.te_last, PREAMBLE_HIGH_US) + && in_range(duration, PREAMBLE_LOW_US) + { + // Another preamble pair (~400µs HIGH + ~600µs LOW). + self.preamble_count = self.preamble_count.saturating_add(1); + } else { + self.step = DecoderStep::Reset; + } + } + } + + DecoderStep::DataHigh => { + if level { + // Data-bit HIGH half — must match a Bit-1 or Bit-0 HIGH window. + if in_range(duration, BIT1_HIGH_US) || in_range(duration, BIT0_HIGH_US) { + self.te_last = duration; + self.step = DecoderStep::DataLow; + } else { + self.step = DecoderStep::Reset; + } + } else { + self.step = DecoderStep::Reset; + } + } + + DecoderStep::DataLow => { + if !level { + let hi = self.te_last; + let lo = duration; + let bit = if in_range(hi, BIT1_HIGH_US) && in_range(lo, BIT1_LOW_US) { + Some(1u8) + } else if in_range(hi, BIT0_HIGH_US) && in_range(lo, BIT0_LOW_US) { + Some(0u8) + } else { + None + }; + + match bit { + Some(b) => { + // Store MSB-first: first bit on air → rx_bits[0] (= frame bit 65). + if self.decode_count_bit < FRAME_BITS { + self.rx_bits[self.decode_count_bit] = b; + } + self.decode_count_bit += 1; + + if self.decode_count_bit == FRAME_BITS { + let result = Self::build_signal(&self.rx_bits); + self.reset(); + return Some(result); + } + self.step = DecoderStep::DataHigh; + } + None => { + self.step = DecoderStep::Reset; + } + } + } else { + self.step = DecoderStep::Reset; + } + } + } + + None + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + let serial = decoded.serial?; + // Reconstruct the original frame to preserve the hop code, func bits, and status, then + // override only the button. The hop code lives in (data, extra); recover it. + let extra = decoded.extra.unwrap_or(0); + let orig_frame = Self::data_to_frame(decoded.data, extra); + let (hop_code, _serial, _orig_button, func_bits, status) = Self::unpack_frame(&orig_frame); + + let btn = Self::map_button(button); + let frame = Self::pack_frame(hop_code, serial, btn, func_bits, status); + + let mut signal = Vec::with_capacity( + ((PREAMBLE_COUNT as usize + 1 + FRAME_BITS) * 2 + 1) * REPEAT_COUNT as usize, + ); + + for rep in 0..REPEAT_COUNT { + // Preamble: 20 pairs. + for _ in 0..PREAMBLE_COUNT { + Self::push_pair(&mut signal, PREAMBLE_HIGH_US, PREAMBLE_LOW_US); + } + // Sync. + Self::push_pair(&mut signal, SYNC_HIGH_US, SYNC_LOW_US); + // Data bits, MSB-first (frame bit 65 first on air). + for b in (0..FRAME_BITS).rev() { + if frame[b] != 0 { + Self::push_pair(&mut signal, BIT1_HIGH_US, BIT1_LOW_US); + } else { + Self::push_pair(&mut signal, BIT0_HIGH_US, BIT0_LOW_US); + } + } + // Inter-repetition gap. + if rep < REPEAT_COUNT - 1 { + signal.push(LevelDuration::new(false, REPEAT_GAP_US)); + } + } + + Some(signal) + } +} + +impl Default for LandRoverRkeDecoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Feed an encoded signal back through a fresh decoder and return the first decode. + fn decode_signal(signal: &[LevelDuration]) -> Option { + let mut dec = LandRoverRkeDecoder::new(); + for ld in signal { + if let Some(d) = dec.feed(ld.level, ld.duration_us) { + return Some(d); + } + } + None + } + + /// Build a DecodedSignal seed carrying a chosen hop code + serial (button overridden at encode). + fn seed(hop_code: u32, serial: u32, func_bits: u8, status: u8, button: u8) -> DecodedSignal { + let frame = LandRoverRkeDecoder::pack_frame(hop_code, serial & 0x00FF_FFFF, button, func_bits, status); + let (data, extra) = LandRoverRkeDecoder::frame_to_data(&frame); + DecodedSignal { + serial: Some(serial & 0x00FF_FFFF), + button: Some(button), + counter: Some((hop_code & 0xFFFF) as u16), + crc_valid: false, + data, + data_count_bit: FRAME_BITS, + encoder_capable: true, + extra: Some(extra), + protocol_display_name: None, + } + } + + #[test] + fn pack_unpack_roundtrip() { + // Field packing/unpacking is exact across the full 66-bit layout. + let cases = [ + (0xDEAD_BEEFu32, 0x00AB_CDEFu32, 0x1u8, BTN_UNLOCK, 0x5u8, 0x2u8), + (0x0000_0000, 0x0000_0000, 0x0, 0x0, 0x0, 0x0), + (0xFFFF_FFFF, 0x00FF_FFFF, 0xF, 0xF, 0xF, 0x3), + (0x1234_5678, 0x0055_AA55, 0x4, BTN_LOCK, 0xA, 0x1), + ]; + for &(hop, serial, _btn_field_unused, button, func, status) in &cases { + let frame = LandRoverRkeDecoder::pack_frame(hop, serial, button, func, status); + let (h, s, b, f, st) = LandRoverRkeDecoder::unpack_frame(&frame); + assert_eq!(h, hop, "hop_code mismatch"); + assert_eq!(s, serial, "serial mismatch"); + assert_eq!(b, button, "button mismatch"); + assert_eq!(f, func, "func_bits mismatch"); + assert_eq!(st, status, "status mismatch"); + + // (data, extra) <-> frame is lossless too. + let (data, extra) = LandRoverRkeDecoder::frame_to_data(&frame); + let frame2 = LandRoverRkeDecoder::data_to_frame(data, extra); + assert_eq!(frame, frame2, "data/extra round-trip mismatch"); + } + } + + #[test] + fn encode_decode_roundtrip_multiple() { + // Multiple serials / hop codes (counters) / buttons all round-trip through encode→decode. + let serials = [0x00ABCDEFu32, 0x00123456, 0x00000001, 0x00FFFFFE, 0x005A5A5A]; + let hops = [0xDEADBEEFu32, 0x00000000, 0xFFFFFFFF, 0x12345678, 0xCAFEBABE]; + let buttons = [0x01u8, 0x02, 0x04, 0x08]; // Lock, Unlock, Trunk/Boot, Panic + + for (&serial, &hop) in serials.iter().zip(hops.iter()) { + for &btn in &buttons { + let s = seed(hop, serial, 0x3, 0x1, 0x00); // func/status preserved from seed + let encoder = LandRoverRkeDecoder::new(); + let signal = encoder.encode(&s, btn).expect("encode should succeed"); + let decoded = decode_signal(&signal) + .unwrap_or_else(|| panic!("decode failed for serial {serial:#X} hop {hop:#X} btn {btn:#X}")); + + let expected_btn = LandRoverRkeDecoder::map_button(btn); + assert_eq!(decoded.serial, Some(serial & 0x00FF_FFFF), "serial"); + assert_eq!(decoded.button, Some(expected_btn), "button"); + // hop code survives via data+extra. + let frame = LandRoverRkeDecoder::data_to_frame(decoded.data, decoded.extra.unwrap()); + let (dec_hop, _, _, dec_func, dec_status) = LandRoverRkeDecoder::unpack_frame(&frame); + assert_eq!(dec_hop, hop, "hop_code"); + assert_eq!(dec_func, 0x3, "func_bits preserved"); + assert_eq!(dec_status, 0x1, "status preserved"); + assert_eq!(decoded.data_count_bit, FRAME_BITS, "bit count"); + assert!(!decoded.crc_valid, "no key → crc_valid must be false"); + } + } + } + + #[test] + fn rejects_truncated_frame() { + // A single repetition carrying only 65 of the 66 bits must NOT decode. + let s = seed(0xDEADBEEF, 0x00ABCDEF, 0x0, 0x0, 0x02); + let encoder = LandRoverRkeDecoder::new(); + let signal = encoder.encode(&s, 0x02).unwrap(); + // First repetition is preamble (20 pairs) + sync (1 pair) + 66 bit-pairs. Keep only the + // first 65 bit-pairs so the frame is one bit short, and stop before the next repetition. + let bits_to_keep = FRAME_BITS - 1; + let partial_len = (PREAMBLE_COUNT as usize + 1 + bits_to_keep) * 2; + let partial = &signal[..partial_len]; + assert!(decode_signal(partial).is_none(), "65-bit frame must not decode"); + } + + #[test] + fn rejects_wrong_sync_gap() { + // Same PWM bits but a too-short "sync" gap must not be accepted as a frame. + let s = seed(0x12345678, 0x00112233, 0x0, 0x0, 0x01); + let encoder = LandRoverRkeDecoder::new(); + let mut signal = encoder.encode(&s, 0x01).unwrap(); + // Corrupt the first sync LOW (index = PREAMBLE_COUNT*2 + 1) to a Bit-0-like LOW (700µs), + // which is far outside the 9600µs ±20% window. + let sync_low_idx = PREAMBLE_COUNT as usize * 2 + 1; + signal[sync_low_idx] = LevelDuration::new(false, 700); + // Take just the first repetition so the later (intact) reps don't rescue the decode. + let one_rep_len = (PREAMBLE_COUNT as usize + 1 + FRAME_BITS) * 2; + let partial = &signal[..one_rep_len.min(signal.len())]; + assert!(decode_signal(partial).is_none(), "frame without valid 9.6ms sync must not decode"); + } +} diff --git a/src/protocols/land_rover_v0.rs b/src/protocols/land_rover_v0.rs new file mode 100644 index 0000000..f178287 --- /dev/null +++ b/src/protocols/land_rover_v0.rs @@ -0,0 +1,696 @@ +//! Land Rover V0 protocol decoder/encoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/land_rover_v0.c` and +//! `land_rover_v0.h`. **Differential** Manchester (NOT the Flipper transition table): te_short 250, +//! te_long 500, te_delta 100, with a ~750µs sync pulse and a ≥64-pair short preamble. FM. +//! +//! Frame: 81 bits = an 80-bit body (`raw[0..10]`) plus one trailing `extra_bit`. The 64-bit key +//! reported as `DecodedSignal.data` is `raw[0..8]` big-endian; `raw[8..10]` is a 16-bit `tail`. +//! Field layout in the key bytes: +//! * bytes 0..3 → 24-bit command_signature (Lock = 0xC20363, Unlock = 0xA285E3) +//! * bytes 3..6 → 24-bit serial +//! * byte 6 + byte 7 MSB → 9-bit counter = `(b6 << 1) | (b7 >> 7)` +//! * byte 7 bits 0x78 → 3 reserved bits (must be 0) +//! * byte 7 bits 0x07 → 3-bit check +//! The check is a proprietary 3-bit polynomial over the counter (`calculate_check`); the tail is +//! 0xFFFF or 0x7FFF depending on a 1-bit parity of the counter (`calculate_tail`). Emission is gated +//! on the reserved bits being zero, the check matching, the tail matching, and `extra_bit` set — +//! so Land Rover V0 is strongly validated and will not false-match. Encoder supported. +//! +//! Decode steps: Reset → PreambleLow/PreambleHigh (count short pairs, ≥64) → SyncLow → Data. +//! The Data step ports the C `process_transition`/`add_decoded_bit` differential machine directly: +//! it tracks `previous_bit`, skips the initial boundary short-high pad (`boundary_pad_skipped`), +//! and completes short half-bits via `pending_short`. Bit 0 (= 1) is seeded on entry to Data. +//! +//! Note on the C encoder (faithfully ported): `build_upload` forces frame bit 1 = 0 regardless of +//! the key, so combined with the seeded bit 0 = 1 the top two frame bits are always `10`. The +//! Unlock signature 0xA285E3 satisfies this and round-trips cleanly; the Lock signature 0xC20363 +//! (whose bit 1 is 1) is emitted as 0x820363 by the reference encoder. We reproduce that behaviour. + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use crate::radio::demodulator::LevelDuration; +use crate::duration_diff; + +const TE_SHORT: u32 = 250; +const TE_LONG: u32 = 500; +const TE_DELTA: u32 = 100; +const SYNC_US: u32 = 750; +const SYNC_DELTA_US: u32 = 120; +const MIN_PREAMBLE_PAIRS: u16 = 64; +const COUNT_BIT: usize = 81; +const GAP_US: u32 = 50_000; + +/// TX preamble pair count (matches LAND_ROVER_V0_PREAMBLE_PAIRS). +const TX_PREAMBLE_PAIRS: usize = 319; + +// Button signatures (LAND_ROVER_V0_SIG_*). +const SIG_UNLOCK: u32 = 0x00A2_85E3; +const SIG_LOCK: u32 = 0x00C2_0363; + +// Land Rover button codes (LAND_ROVER_V0_BTN_*). +const LR_BTN_UNKNOWN: u8 = 0x00; +const LR_BTN_LOCK: u8 = 0x02; +const LR_BTN_UNLOCK: u8 = 0x04; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + PreambleLow, + PreambleHigh, + SyncLow, + Data, +} + +pub struct LandRoverV0Decoder { + step: DecoderStep, + preamble_count: u16, + raw: [u8; 10], + bit_count: u8, + extra_bit: bool, + previous_bit: bool, + boundary_pad_skipped: bool, + pending_short: bool, +} + +impl LandRoverV0Decoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + preamble_count: 0, + raw: [0; 10], + bit_count: 0, + extra_bit: false, + previous_bit: true, + boundary_pad_skipped: false, + pending_short: false, + } + } + + fn is_short(d: u32) -> bool { + duration_diff!(d, TE_SHORT) < TE_DELTA + } + fn is_long(d: u32) -> bool { + duration_diff!(d, TE_LONG) < TE_DELTA + } + fn is_sync(d: u32) -> bool { + duration_diff!(d, SYNC_US) < SYNC_DELTA_US + } + + /// Reset the per-frame differential-Manchester state (matches the SyncLow init in the C feed). + fn begin_frame(&mut self) { + self.raw = [0; 10]; + self.bit_count = 0; + self.extra_bit = false; + self.previous_bit = true; + self.boundary_pad_skipped = false; + self.pending_short = false; + } + + /// Append one decoded bit (matches `land_rover_v0_add_decoded_bit`). + /// Bits 0..80 pack MSB-first into `raw`; bit 80 is the trailing `extra_bit`. + fn add_decoded_bit(&mut self, bit: bool) -> bool { + if self.bit_count < 80 { + let byte_index = (self.bit_count / 8) as usize; + let bit_index = 7 - (self.bit_count % 8); + if bit { + self.raw[byte_index] |= 1u8 << bit_index; + } + } else if self.bit_count == 80 { + self.extra_bit = bit; + } else { + return false; + } + self.bit_count += 1; + true + } + + /// Differential-Manchester transition handler (direct port of `land_rover_v0_process_transition`). + fn process_transition(&mut self, level: bool, duration: u32) -> bool { + if !self.boundary_pad_skipped { + if level && Self::is_short(duration) { + self.boundary_pad_skipped = true; + return true; + } + self.boundary_pad_skipped = true; + } + + if self.pending_short { + if !self.previous_bit && !level && Self::is_short(duration) { + self.pending_short = false; + return self.add_decoded_bit(false); + } else if self.previous_bit && level && Self::is_short(duration) { + self.pending_short = false; + return self.add_decoded_bit(true); + } + return false; + } + + if !self.previous_bit { + if level && Self::is_long(duration) { + self.previous_bit = true; + return self.add_decoded_bit(true); + } else if level && Self::is_short(duration) { + self.pending_short = true; + return true; + } + return false; + } + + if !level && Self::is_long(duration) { + self.previous_bit = false; + return self.add_decoded_bit(false); + } else if !level && Self::is_short(duration) { + self.pending_short = true; + return true; + } + + false + } + + /// 3-bit check polynomial over the 9-bit counter (matches `land_rover_v0_calculate_check`). + fn calculate_check(count: u32) -> u8 { + let c0 = ((count >> 1) ^ (count >> 2) ^ (count >> 3) ^ (count >> 4) ^ (count >> 6)) & 1; + let c1 = ((count >> 0) + ^ (count >> 2) + ^ (count >> 3) + ^ (count >> 4) + ^ (count >> 5) + ^ (count >> 6) + ^ 1) + & 1; + let c2 = ((count >> 1) ^ (count >> 3) ^ (count >> 4) ^ (count >> 5) ^ (count >> 6)) & 1; + (c0 | (c1 << 1) | (c2 << 2)) as u8 + } + + /// MSB selector for the 16-bit tail (matches `land_rover_v0_calculate_tail_msb`). + fn calculate_tail_msb(count: u32) -> bool { + (((count >> 0) ^ (count >> 2) ^ (count >> 4) ^ (count >> 5)) & 1) != 0 + } + + /// 16-bit tail value (matches `land_rover_v0_calculate_tail`). + fn calculate_tail(count: u32) -> u16 { + if Self::calculate_tail_msb(count) { + 0xFFFF + } else { + 0x7FFF + } + } + + /// Map a 24-bit command signature to a Land Rover button (matches + /// `land_rover_v0_button_from_signature`). + fn button_from_signature(signature: u32) -> u8 { + match signature { + SIG_UNLOCK => LR_BTN_UNLOCK, + SIG_LOCK => LR_BTN_LOCK, + _ => LR_BTN_UNKNOWN, + } + } + + /// Counter packed in key bytes 6 + 7-MSB (matches the C count extraction). + fn count_from_bytes(b: &[u8; 8]) -> u32 { + ((b[6] as u32) << 1) | ((b[7] >> 7) & 1) as u32 + } + + /// Validate a frame (matches `land_rover_v0_validate_frame`). + /// Returns (check_ok, tail_ok). + fn validate_frame(key: u64, tail: u16, extra_bit: bool) -> (bool, bool) { + let b = key.to_be_bytes(); + let count = Self::count_from_bytes(&b); + let expected_check = Self::calculate_check(count); + let expected_tail = Self::calculate_tail(count); + + let check_ok = (b[7] & 0x78) == 0 && (b[7] & 0x07) == expected_check; + let tail_ok = tail == expected_tail && extra_bit; + (check_ok, tail_ok) + } + + /// Finish a frame: validate, then build the decoded signal (matches + /// `land_rover_v0_finish_frame` + `parse_key_fields`). Returns None when invalid (gated). + fn finish_frame(&self) -> Option { + let key = u64::from_be_bytes([ + self.raw[0], + self.raw[1], + self.raw[2], + self.raw[3], + self.raw[4], + self.raw[5], + self.raw[6], + self.raw[7], + ]); + let tail = ((self.raw[8] as u16) << 8) | self.raw[9] as u16; + + let (check_ok, tail_ok) = Self::validate_frame(key, tail, self.extra_bit); + if !(check_ok && tail_ok) { + return None; + } + + let b = key.to_be_bytes(); + let signature = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32; + let serial = ((b[3] as u32) << 16) | ((b[4] as u32) << 8) | b[5] as u32; + let count = Self::count_from_bytes(&b); + let button = Self::button_from_signature(signature); + + Some(DecodedSignal { + serial: Some(serial), + button: Some(button), + counter: Some(count as u16), + crc_valid: true, // check + tail + reserved-bits validated above + data: key, + data_count_bit: COUNT_BIT, + encoder_capable: true, + // Stash the 16-bit tail so the encoder can rebuild the full frame without recomputation. + extra: Some(tail as u64), + protocol_display_name: None, + }) + } + + /// Map KAT's generic button command to a Land Rover signature (Lock/Unlock). + /// KAT: Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08. Land Rover V0 only defines Lock/Unlock, + /// so Trunk/Panic have no signature and fall back to the decoded frame's signature in `encode`. + fn signature_from_button(button: u8) -> u32 { + match button { + 0x01 => SIG_LOCK, // KAT Lock + 0x02 => SIG_UNLOCK, // KAT Unlock + _ => 0, + } + } + + /// Build the 64-bit key from fields (matches `land_rover_v0_build_key`). + fn build_key(signature: u32, serial: u32, count: u32) -> u64 { + let mut b = [0u8; 8]; + b[0] = (signature >> 16) as u8; + b[1] = (signature >> 8) as u8; + b[2] = signature as u8; + b[3] = (serial >> 16) as u8; + b[4] = (serial >> 8) as u8; + b[5] = serial as u8; + b[6] = (count >> 1) as u8; + let counter_lsb = (count & 1) != 0; + let check = Self::calculate_check(count); + b[7] = (if counter_lsb { 0x80 } else { 0x00 }) | check; + u64::from_be_bytes(b) + } + + fn enc_add_level(signal: &mut Vec, level: bool, duration: u32) { + if let Some(last) = signal.last_mut() { + if last.level == level { + *last = LevelDuration::new(level, last.duration_us + duration); + return; + } + } + signal.push(LevelDuration::new(level, duration)); + } + + /// Emit one differential-Manchester bit (matches `land_rover_v0_encoder_add_bit`). + /// Returns the new `previous_bit`. + fn enc_add_bit(signal: &mut Vec, previous_bit: bool, bit: bool) -> bool { + match (previous_bit, bit) { + (false, false) => { + Self::enc_add_level(signal, true, TE_SHORT); + Self::enc_add_level(signal, false, TE_SHORT); + } + (false, true) => { + Self::enc_add_level(signal, true, TE_LONG); + } + (true, false) => { + Self::enc_add_level(signal, false, TE_LONG); + } + (true, true) => { + Self::enc_add_level(signal, false, TE_SHORT); + Self::enc_add_level(signal, true, TE_SHORT); + } + } + bit + } +} + +impl ProtocolDecoder for LandRoverV0Decoder { + fn name(&self) -> &'static str { + "Land Rover V0" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: COUNT_BIT, + } + } + + fn supported_frequencies(&self) -> &[u32] { + &[315_000_000, 433_920_000] + } + + fn reset(&mut self) { + self.step = DecoderStep::Reset; + self.preamble_count = 0; + self.raw = [0; 10]; + self.bit_count = 0; + self.extra_bit = false; + self.previous_bit = true; + self.boundary_pad_skipped = false; + self.pending_short = false; + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.step { + DecoderStep::Reset => { + if level && Self::is_short(duration) { + self.preamble_count = 0; + self.step = DecoderStep::PreambleLow; + } + } + + DecoderStep::PreambleLow => { + if !level && Self::is_short(duration) { + self.preamble_count += 1; + self.step = DecoderStep::PreambleHigh; + } else { + self.step = DecoderStep::Reset; + } + } + + DecoderStep::PreambleHigh => { + if level && Self::is_short(duration) { + self.step = DecoderStep::PreambleLow; + } else if level + && Self::is_sync(duration) + && self.preamble_count >= MIN_PREAMBLE_PAIRS + { + self.step = DecoderStep::SyncLow; + } else { + self.step = DecoderStep::Reset; + } + } + + DecoderStep::SyncLow => { + if !level && Self::is_sync(duration) { + self.begin_frame(); + self.add_decoded_bit(true); // seed bit 0 = 1 + self.step = DecoderStep::Data; + } else { + self.step = DecoderStep::Reset; + } + } + + DecoderStep::Data => { + if !self.process_transition(level, duration) { + self.step = DecoderStep::Reset; + return None; + } + + if self.bit_count as usize == COUNT_BIT { + let result = self.finish_frame(); + self.step = DecoderStep::Reset; + return result; + } + } + } + None + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + // Derive fields from the decoded signal, preferring the requested button's signature. + let serial = decoded.serial? & 0x00FF_FFFF; + let count = (decoded.counter.unwrap_or(0) as u32) & 0x1FF; + + // Pick the command signature: requested button first, else the decoded frame's signature. + let mut signature = Self::signature_from_button(button); + if signature == 0 { + let b = decoded.data.to_be_bytes(); + signature = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32; + // If still not a known Land Rover signature, refuse (matches C: command_signature==0 → error). + if Self::button_from_signature(signature) == LR_BTN_UNKNOWN { + return None; + } + } + + let key = Self::build_key(signature, serial, count); + let key_bytes = key.to_be_bytes(); + let tail = Self::calculate_tail(count); + + // Capacity: preamble pairs (2 levels) + sync (3) + ~81 bits (≤2 levels each) + gap. + let mut signal = Vec::with_capacity(TX_PREAMBLE_PAIRS * 2 + 3 + COUNT_BIT * 2 + 1); + + // Preamble: alternating short high/low pairs. + for _ in 0..TX_PREAMBLE_PAIRS { + Self::enc_add_level(&mut signal, true, TE_SHORT); + Self::enc_add_level(&mut signal, false, TE_SHORT); + } + + // Sync: high 750, low 750, then a boundary short-high pad (skipped by the decoder). + Self::enc_add_level(&mut signal, true, SYNC_US); + Self::enc_add_level(&mut signal, false, SYNC_US); + Self::enc_add_level(&mut signal, true, TE_SHORT); + + // Differential-Manchester body. previous_bit starts true (bit 0 = 1 is implied by the sync + // trailing short). Bit index 1 is forced to 0 (matches the C build_upload), then bits 2..63 + // come from the key bytes. + let mut previous_bit = true; + previous_bit = Self::enc_add_bit(&mut signal, previous_bit, false); + for bit_index in 2..64u8 { + let byte_index = (bit_index / 8) as usize; + let bit_in_byte = 7 - (bit_index % 8); + let bit = (key_bytes[byte_index] >> bit_in_byte) & 1 != 0; + previous_bit = Self::enc_add_bit(&mut signal, previous_bit, bit); + } + + // 16-bit tail, MSB first. + for bit_index in 0..16u8 { + let bit = (tail >> (15 - bit_index)) & 1 != 0; + previous_bit = Self::enc_add_bit(&mut signal, previous_bit, bit); + } + + // Trailing extra bit = 1. + let _ = Self::enc_add_bit(&mut signal, previous_bit, true); + + // Inter-frame gap. + Self::enc_add_level(&mut signal, false, GAP_US); + + Some(signal) + } +} + +impl Default for LandRoverV0Decoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Drive a full encoded upload through the decoder and return the first decode. + fn decode_upload(upload: &[LevelDuration]) -> Option { + let mut dec = LandRoverV0Decoder::new(); + for ld in upload { + if let Some(sig) = dec.feed(ld.level, ld.duration_us) { + return Some(sig); + } + } + None + } + + /// The 3-bit check polynomial matches the C reference for a couple of hand traces. + #[test] + fn check_polynomial_matches_reference() { + // count = 0: c0=0, c1=(0^1)&1=1, c2=0 → 0b010 = 2 + assert_eq!(LandRoverV0Decoder::calculate_check(0), 0b010); + // Spot-check internal consistency: build_key embeds calculate_check in byte 7. + for count in 0..0x200u32 { + let key = LandRoverV0Decoder::build_key(SIG_UNLOCK, 0x123456, count); + let b = key.to_be_bytes(); + assert_eq!( + b[7] & 0x07, + LandRoverV0Decoder::calculate_check(count), + "check embedded in key byte 7 must equal calculate_check(count) for count={count}" + ); + assert_eq!(b[7] & 0x78, 0, "reserved bits must be zero for count={count}"); + } + } + + /// The tail is 0xFFFF or 0x7FFF per the counter parity, matching the reference. + #[test] + fn tail_matches_reference() { + for count in 0..0x200u32 { + let tail = LandRoverV0Decoder::calculate_tail(count); + assert!(tail == 0xFFFF || tail == 0x7FFF); + let msb_set = (tail >> 15) & 1 == 1; + assert_eq!(msb_set, LandRoverV0Decoder::calculate_tail_msb(count)); + } + } + + /// Primary correctness check: encode an Unlock frame and decode it back. The Unlock signature + /// 0xA285E3 satisfies the encoder's forced-bit-1 = 0 invariant, so it round-trips cleanly: + /// serial, button, counter, key and the 3-bit check all survive. + #[test] + fn unlock_round_trip_preserves_fields() { + let serial = 0x00AB_CDEF & 0x00FF_FFFF; + let counter = 0x123u16; // 9-bit counter + let key = LandRoverV0Decoder::build_key(SIG_UNLOCK, serial, counter as u32); + + let decoded_in = DecodedSignal { + serial: Some(serial), + button: Some(LR_BTN_UNLOCK), + counter: Some(counter), + crc_valid: true, + data: key, + data_count_bit: COUNT_BIT, + encoder_capable: true, + extra: Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64), + protocol_display_name: None, + }; + + let dec = LandRoverV0Decoder::new(); + // KAT Unlock command = 0x02. + let upload = dec.encode(&decoded_in, 0x02).expect("encode Unlock"); + let out = decode_upload(&upload).expect("decode the encoded Unlock frame"); + + assert!(out.crc_valid, "decoded frame must pass check+tail gating"); + assert_eq!(out.data, key, "64-bit key must survive the round trip"); + assert_eq!(out.serial, Some(serial), "serial must survive"); + assert_eq!(out.counter, Some(counter), "counter must survive"); + assert_eq!(out.button, Some(LR_BTN_UNLOCK), "Unlock button must survive"); + assert_eq!(out.data_count_bit, COUNT_BIT); + // The stashed tail must match the counter-derived tail. + assert_eq!(out.extra, Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64)); + // The embedded 3-bit check must match the recomputed value. + let b = out.data.to_be_bytes(); + assert_eq!(b[7] & 0x07, LandRoverV0Decoder::calculate_check(counter as u32)); + assert_eq!(b[7] & 0x78, 0, "reserved bits zero"); + } + + /// Round-trip across many serial/counter values for Unlock. + #[test] + fn unlock_round_trip_many() { + let mut state: u32 = 0x1357_9BDF; + let mut next = || { + // xorshift for deterministic pseudo-random coverage + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + state + }; + + for _ in 0..256 { + let serial = next() & 0x00FF_FFFF; + let counter = (next() & 0x1FF) as u16; + let key = LandRoverV0Decoder::build_key(SIG_UNLOCK, serial, counter as u32); + let decoded_in = DecodedSignal { + serial: Some(serial), + button: Some(LR_BTN_UNLOCK), + counter: Some(counter), + crc_valid: true, + data: key, + data_count_bit: COUNT_BIT, + encoder_capable: true, + extra: Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64), + protocol_display_name: None, + }; + let dec = LandRoverV0Decoder::new(); + let upload = dec.encode(&decoded_in, 0x02).expect("encode"); + let out = decode_upload(&upload) + .unwrap_or_else(|| panic!("decode failed for serial={serial:06X} counter={counter:03X}")); + assert_eq!(out.data, key); + assert_eq!(out.serial, Some(serial)); + assert_eq!(out.counter, Some(counter)); + assert_eq!(out.button, Some(LR_BTN_UNLOCK)); + assert!(out.crc_valid); + } + } + + /// Faithful-port note: the reference encoder forces frame bit 1 = 0. The Lock signature + /// 0xC20363 has bit 1 = 1, so the emitted frame's signature becomes 0x820363, which maps to + /// LR_BTN_UNKNOWN. We assert this exact reference behaviour rather than a "fixed" version. + #[test] + fn lock_signature_forced_bit_matches_reference_quirk() { + let serial = 0x0012_3456; + let counter = 100u16; + let key = LandRoverV0Decoder::build_key(SIG_LOCK, serial, counter as u32); + let decoded_in = DecodedSignal { + serial: Some(serial), + button: Some(LR_BTN_LOCK), + counter: Some(counter), + crc_valid: true, + data: key, + data_count_bit: COUNT_BIT, + encoder_capable: true, + extra: Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64), + protocol_display_name: None, + }; + let dec = LandRoverV0Decoder::new(); + // KAT Lock command = 0x01. + let upload = dec.encode(&decoded_in, 0x01).expect("encode Lock"); + let out = decode_upload(&upload).expect("frame still decodes (check/tail valid)"); + // Top byte 0xC2 -> 0x82 because bit 1 is forced to 0 by the encoder. + let b = out.data.to_be_bytes(); + assert_eq!(b[0], 0x82, "encoder forces frame bit 1 = 0, turning 0xC2 into 0x82"); + assert_eq!(out.button, Some(LR_BTN_UNKNOWN), "0x820363 is not a known signature"); + // Serial / counter / check are still intact. + assert_eq!(out.serial, Some(serial)); + assert_eq!(out.counter, Some(counter)); + assert!(out.crc_valid); + } + + /// A frame with a deliberately wrong check must NOT decode (gating prevents false matches). + #[test] + fn bad_check_is_rejected() { + let serial = 0x00AB_CDEF; + let counter = 0x055u16; + let mut key = LandRoverV0Decoder::build_key(SIG_UNLOCK, serial, counter as u32); + // Corrupt the 3-bit check in byte 7 (XOR a bit so it no longer matches). + key ^= 0x01; + let decoded_in = DecodedSignal { + serial: Some(serial), + button: Some(LR_BTN_UNLOCK), + counter: Some(counter), + crc_valid: false, + data: key, + data_count_bit: COUNT_BIT, + encoder_capable: true, + extra: None, + protocol_display_name: None, + }; + // Hand-build an upload that transmits this corrupted key verbatim (no re-derivation): + // reuse the encoder's level helpers but bypass build_key by injecting the raw key. + let key_bytes = key.to_be_bytes(); + let tail = LandRoverV0Decoder::calculate_tail(counter as u32); + let mut signal: Vec = Vec::new(); + for _ in 0..TX_PREAMBLE_PAIRS { + LandRoverV0Decoder::enc_add_level(&mut signal, true, TE_SHORT); + LandRoverV0Decoder::enc_add_level(&mut signal, false, TE_SHORT); + } + LandRoverV0Decoder::enc_add_level(&mut signal, true, SYNC_US); + LandRoverV0Decoder::enc_add_level(&mut signal, false, SYNC_US); + LandRoverV0Decoder::enc_add_level(&mut signal, true, TE_SHORT); + let mut prev = true; + prev = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, false); + for bit_index in 2..64u8 { + let bi = (bit_index / 8) as usize; + let bib = 7 - (bit_index % 8); + let bit = (key_bytes[bi] >> bib) & 1 != 0; + prev = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, bit); + } + for bit_index in 0..16u8 { + let bit = (tail >> (15 - bit_index)) & 1 != 0; + prev = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, bit); + } + let _ = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, true); + LandRoverV0Decoder::enc_add_level(&mut signal, false, GAP_US); + + // The decoded count comes from the (corrupted) byte 7, so the embedded check now mismatches + // calculate_check(count) → rejected. Note: corrupting bit 0 of the check does not change + // the counter (counter uses byte 7 MSB only), so calculate_check(count) stays the same. + let _ = decoded_in; + assert!( + decode_upload(&signal).is_none(), + "frame with a corrupted check must be rejected by the gate" + ); + } +} diff --git a/src/protocols/mazda_siemens.rs b/src/protocols/mazda_siemens.rs new file mode 100644 index 0000000..b404d92 --- /dev/null +++ b/src/protocols/mazda_siemens.rs @@ -0,0 +1,525 @@ +//! Mazda Siemens protocol decoder/encoder +//! +//! Ported from Flipper-ARF: `lib/subghz/protocols/mazda_siemens.c` / `mazda_siemens.h` +//! (`SUBGHZ_PROTOCOL_MAZDA_SIEMENS_NAME = "MazdaSiemens"`). This is the Siemens/VDO keyfob +//! cipher used on some Mazda vehicles — a DIFFERENT protocol from KAT's existing "Mazda V0" +//! (Pandora) decoder, even though both ride a 250/500µs pair-based stream at 433.92 MHz FM. +//! +//! Profile (matches the C const block): +//! - te_short = 250µs, te_long = 500µs, te_delta = 100µs, 64-bit frame. +//! - Pair-based decoder: `feed()` ignores `level` and interprets raw durations in pairs +//! (`process_pair`), collecting bits with *inverted* polarity (`state_bit == 0` → stored 1). +//! - Preamble: ≥13 short/short pairs, then a short→long transition starts data; the first +//! collected bit is a 1 (sync). A 14-byte buffer accumulates; on a non-matching pair the +//! frame is checked: discard the leading sync byte, take 8 bytes, deobfuscate, validate. +//! - Siemens obfuscation (the layer ported here, `mazda_xor_deobfuscate`): +//! parity = byte_parity(data[7]); odd → mask = data[6], XOR bytes 0..6; +//! even → mask = data[5], XOR bytes 0..5 and byte 6. Then bit-deinterleave bytes 5/6 +//! via `(old5 & 0xAA)|(old6 & 0x55)` / `(old5 & 0x55)|(old6 & 0xAA)`. +//! The inner Siemens cipher's plaintext is left as-is (no key); only this obfuscation/ +//! interleave layer is reversed, matching the C. +//! - Gate: additive checksum `sum(data[0..7]) == data[7]` (matches the C `mazda_check_completion`), +//! plus the structural preamble/sync/bit-count constraints. This is what keeps it from +//! false-matching other 250/500µs Manchester protocols. +//! - Fields (`mazda_parse_data`): serial = data >> 32, button = (data >> 24) & 0xFF, +//! counter = (data >> 8) & 0xFFFF. Button codes: 0x10 Lock, 0x20 Unlock, 0x40 Trunk. +//! - RF: FM (`SubGhzProtocolFlag_FM`); 433.92 MHz only (`SubGhzProtocolFlag_433`). +//! +//! Encoder (`subghz_protocol_encoder_mazda_siemens_get_upload`): increments the counter byte, +//! recomputes the checksum, bit-interleaves + XORs (`mazda_xor_obfuscate`), then emits a 12-byte +//! 0xFF preamble, a 50ms gap, `0xFF 0xFF 0xD7`, the 8 obfuscated bytes transmitted as `255 - byte`, +//! a `0x5A` tail byte, and a trailing 50ms gap. Manchester per byte: bit 1 → (H,L), bit 0 → (L,H), +//! all at te_short. Implemented faithfully in `encode()`. +//! +//! Note: as in the C, the decoder and encoder are NOT a clean raw-timing round-trip pair (the +//! decoder targets real fob air-format; the encoder generates a TX upload). The encode↔decode +//! unit test therefore validates the obfuscate/deobfuscate (XOR + interleave) cipher layer, +//! which is a true inverse. + +use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; +use crate::duration_diff; +use crate::radio::demodulator::LevelDuration; + +const TE_SHORT: u32 = 250; +const TE_LONG: u32 = 500; +const TE_DELTA: u32 = 100; +const MIN_COUNT_BIT: usize = 64; + +const PREAMBLE_MIN: u16 = 13; +const COMPLETION_MIN: u16 = 80; +const COMPLETION_MAX: u16 = 105; +const DATA_BUFFER_SIZE: usize = 14; + +// Encoder constants (mazda_siemens.c). +const TX_PREAMBLE_BYTES: usize = 12; +const TX_GAP_US: u32 = 50_000; +const TX_SYNC_BYTE: u8 = 0xD7; +const TX_TAIL_BYTE: u8 = 0x5A; + +/// Button codes carried in the frame (mazda_get_btn_name in the C). +const BTN_LOCK: u8 = 0x10; +const BTN_UNLOCK: u8 = 0x20; +const BTN_TRUNK: u8 = 0x40; + +/// Decoder states (matches mazda_siemens.c MazdaSiemensDecoderStep). +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + PreambleSave, + PreambleCheck, + DataSave, + DataCheck, +} + +/// Mazda Siemens protocol decoder. +pub struct MazdaSiemensDecoder { + step: DecoderStep, + te_last: u32, + preamble_count: u16, + bit_counter: u16, + prev_state: u8, + data_buffer: [u8; DATA_BUFFER_SIZE], +} + +impl MazdaSiemensDecoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + te_last: 0, + preamble_count: 0, + bit_counter: 0, + prev_state: 0, + data_buffer: [0u8; DATA_BUFFER_SIZE], + } + } + + #[inline] + fn is_short(duration: u32) -> bool { + duration_diff!(duration, TE_SHORT) < TE_DELTA + } + + #[inline] + fn is_long(duration: u32) -> bool { + duration_diff!(duration, TE_LONG) < TE_DELTA + } + + /// Collect one bit into the buffer with inverted polarity (mazda_collect_bit). + /// `state_bit == 0` stores a 1. + fn collect_bit(&mut self, state_bit: u8) { + let byte_idx = (self.bit_counter >> 3) as usize; + if byte_idx < DATA_BUFFER_SIZE { + self.data_buffer[byte_idx] <<= 1; + if state_bit == 0 { + self.data_buffer[byte_idx] |= 1; + } + } + self.bit_counter += 1; + } + + /// Process a duration pair (mazda_process_pair). Returns true if the pair was valid. + fn process_pair(&mut self, dur_first: u32, dur_second: u32) -> bool { + let first_short = Self::is_short(dur_first); + let first_long = Self::is_long(dur_first); + let second_short = Self::is_short(dur_second); + let second_long = Self::is_long(dur_second); + + if first_long && second_short { + self.collect_bit(0); + self.collect_bit(1); + self.prev_state = 1; + return true; + } + + if first_short && second_long { + self.collect_bit(1); + self.prev_state = 0; + return true; + } + + if first_short && second_short { + let ps = self.prev_state; + self.collect_bit(ps); + return true; + } + + if first_long && second_long { + self.collect_bit(0); + self.collect_bit(1); + self.prev_state = 0; + return true; + } + + false + } + + /// Validate a complete frame (mazda_check_completion). On success returns a DecodedSignal. + fn check_completion(&self) -> Option { + if self.bit_counter < COMPLETION_MIN || self.bit_counter > COMPLETION_MAX { + return None; + } + + // Shift buffer by 1 byte (discard the sync/header byte). + let mut data = [0u8; 8]; + for i in 0..8 { + data[i] = self.data_buffer[i + 1]; + } + + Self::xor_deobfuscate(&mut data); + + // Additive checksum: sum(data[0..7]) must equal data[7]. + let mut checksum: u8 = 0; + for &b in data.iter().take(7) { + checksum = checksum.wrapping_add(b); + } + if checksum != data[7] { + return None; + } + + // Pack into u64 (big-endian byte order). + let packed = u64::from_be_bytes(data); + + let (serial, button, counter) = Self::parse_fields(packed); + + Some(DecodedSignal { + serial: Some(serial), + button: Some(button), + counter: Some(counter), + crc_valid: true, + data: packed, + data_count_bit: MIN_COUNT_BIT, + encoder_capable: true, + extra: None, + protocol_display_name: None, + }) + } + + /// Field extraction (mazda_parse_data). + fn parse_fields(packed: u64) -> (u32, u8, u16) { + let serial = (packed >> 32) as u32; + let button = ((packed >> 24) & 0xFF) as u8; + let counter = ((packed >> 8) & 0xFFFF) as u16; + (serial, button, counter) + } + + /// Byte parity: XOR-fold to a single bit (mazda_byte_parity). + fn byte_parity(mut val: u8) -> u8 { + val ^= val >> 4; + val ^= val >> 2; + val ^= val >> 1; + val & 1 + } + + /// Siemens RX deobfuscation (mazda_xor_deobfuscate): + /// parity-dependent XOR mask, then deinterleave bytes 5/6. + fn xor_deobfuscate(data: &mut [u8; 8]) { + let parity = Self::byte_parity(data[7]); + + if parity != 0 { + // Odd parity: mask = byte[6], XOR bytes 0..6. + let mask = data[6]; + for i in 0..6 { + data[i] ^= mask; + } + } else { + // Even parity: mask = byte[5], XOR bytes 0..5 and byte[6]. + let mask = data[5]; + for i in 0..5 { + data[i] ^= mask; + } + data[6] ^= mask; + } + + // Bit deinterleave bytes 5/6. + let old5 = data[5]; + let old6 = data[6]; + data[5] = (old5 & 0xAA) | (old6 & 0x55); + data[6] = (old5 & 0x55) | (old6 & 0xAA); + } + + /// Siemens TX obfuscation (mazda_xor_obfuscate): interleave bytes 5/6, then + /// parity-dependent XOR mask. Inverse of `xor_deobfuscate`. + fn xor_obfuscate(data: &mut [u8; 8]) { + let old5 = data[5]; + let old6 = data[6]; + data[5] = (old5 & 0xAA) | (old6 & 0x55); + data[6] = (old5 & 0x55) | (old6 & 0xAA); + + let parity = Self::byte_parity(data[7]); + + if parity != 0 { + let mask = data[6]; + for i in 0..6 { + data[i] ^= mask; + } + } else { + let mask = data[5]; + for i in 0..5 { + data[i] ^= mask; + } + data[6] ^= mask; + } + } + + /// Button name for display (mazda_get_btn_name). + #[allow(dead_code)] + fn get_button_name(btn: u8) -> &'static str { + match btn { + BTN_LOCK => "Lock", + BTN_UNLOCK => "Unlock", + BTN_TRUNK => "Trunk", + _ => "Unknown", + } + } + + /// Map KAT's generic button command to a Mazda Siemens frame button code. + fn map_button(button: u8) -> u8 { + match button { + 0x01 => BTN_LOCK, // Lock + 0x02 => BTN_UNLOCK, // Unlock + 0x04 => BTN_TRUNK, // Trunk + BTN_LOCK | BTN_UNLOCK | BTN_TRUNK => button, // already a frame code + _ => BTN_UNLOCK, + } + } + + /// Encode one byte as Manchester, MSB-first: bit 1 → (H,L), bit 0 → (L,H) (mazda_encode_byte). + fn enc_byte(signal: &mut Vec, byte: u8) { + for bit in (0..8).rev() { + if (byte >> bit) & 1 != 0 { + signal.push(LevelDuration::new(true, TE_SHORT)); + signal.push(LevelDuration::new(false, TE_SHORT)); + } else { + signal.push(LevelDuration::new(false, TE_SHORT)); + signal.push(LevelDuration::new(true, TE_SHORT)); + } + } + } +} + +impl ProtocolDecoder for MazdaSiemensDecoder { + fn name(&self) -> &'static str { + "Mazda Siemens" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: MIN_COUNT_BIT, + } + } + + fn supported_frequencies(&self) -> &[u32] { + // SubGhzProtocolFlag_433 only. + &[433_920_000] + } + + fn reset(&mut self) { + self.step = DecoderStep::Reset; + self.te_last = 0; + self.preamble_count = 0; + self.bit_counter = 0; + self.prev_state = 0; + self.data_buffer = [0u8; DATA_BUFFER_SIZE]; + } + + fn feed(&mut self, _level: bool, duration: u32) -> Option { + match self.step { + DecoderStep::Reset => { + if Self::is_short(duration) { + self.te_last = duration; + self.preamble_count = 0; + self.step = DecoderStep::PreambleCheck; + } + } + + DecoderStep::PreambleSave => { + self.te_last = duration; + self.step = DecoderStep::PreambleCheck; + } + + DecoderStep::PreambleCheck => { + if Self::is_short(self.te_last) && Self::is_short(duration) { + self.preamble_count += 1; + self.step = DecoderStep::PreambleSave; + } else if Self::is_short(self.te_last) + && Self::is_long(duration) + && self.preamble_count >= PREAMBLE_MIN + { + // Preamble → data: seed the leading sync bit (a 1). + self.bit_counter = 1; + self.data_buffer = [0u8; DATA_BUFFER_SIZE]; + self.collect_bit(1); + self.prev_state = 0; + self.step = DecoderStep::DataSave; + } else { + self.step = DecoderStep::Reset; + } + } + + DecoderStep::DataSave => { + self.te_last = duration; + self.step = DecoderStep::DataCheck; + } + + DecoderStep::DataCheck => { + if self.process_pair(self.te_last, duration) { + self.step = DecoderStep::DataSave; + } else { + let result = self.check_completion(); + self.step = DecoderStep::Reset; + if result.is_some() { + return result; + } + } + } + } + + None + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + // Rebuild the 8 cleartext bytes from the decoded 64-bit word. + let mut data = decoded.data.to_be_bytes(); + + // Apply the requested button into the frame's button field (byte index 3 = data >> 24). + data[3] = Self::map_button(button); + + // Increment the counter byte (mazda_siemens.c get_upload): data[6]++ with carry into data[5]. + let (new6, carry) = data[6].overflowing_add(1); + data[6] = new6; + if carry { + data[5] = data[5].wrapping_add(1); + } + + // Recompute the additive checksum over bytes 0..7. + let mut checksum: u8 = 0; + for &b in data.iter().take(7) { + checksum = checksum.wrapping_add(b); + } + data[7] = checksum; + + // Obfuscate (interleave + XOR) for transmission. + let mut tx_data = data; + Self::xor_obfuscate(&mut tx_data); + + // Build the upload: 12x 0xFF preamble, gap, 0xFF 0xFF, sync 0xD7, + // 8 data bytes transmitted inverted (255 - byte), tail 0x5A, trailing gap. + let mut signal: Vec = Vec::with_capacity((TX_PREAMBLE_BYTES + 12) * 16 + 4); + for _ in 0..TX_PREAMBLE_BYTES { + Self::enc_byte(&mut signal, 0xFF); + } + signal.push(LevelDuration::new(false, TX_GAP_US)); + Self::enc_byte(&mut signal, 0xFF); + Self::enc_byte(&mut signal, 0xFF); + Self::enc_byte(&mut signal, TX_SYNC_BYTE); + for &b in tx_data.iter() { + Self::enc_byte(&mut signal, 255 - b); + } + Self::enc_byte(&mut signal, TX_TAIL_BYTE); + signal.push(LevelDuration::new(false, TX_GAP_US)); + + Some(signal) + } +} + +impl Default for MazdaSiemensDecoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The obfuscate/deobfuscate (XOR + bit-interleave) layer is a true inverse for every + /// valid cleartext (checksum byte fixes the parity branch). This is the cipher-layer + /// round-trip the decoder relies on; validates the ported Siemens obfuscation. + #[test] + fn xor_layer_round_trips() { + // Deterministic LCG over many cleartexts: 7 data bytes + matching additive checksum. + let mut seed: u32 = 0x1234_5678; + let mut next = || { + seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345); + (seed >> 16) as u8 + }; + for _ in 0..20_000 { + let mut clear = [0u8; 8]; + let mut sum: u8 = 0; + for b in clear.iter_mut().take(7) { + *b = next(); + sum = sum.wrapping_add(*b); + } + clear[7] = sum; + + let mut buf = clear; + MazdaSiemensDecoder::xor_obfuscate(&mut buf); + MazdaSiemensDecoder::xor_deobfuscate(&mut buf); + assert_eq!(buf, clear, "obfuscate→deobfuscate must be identity"); + } + } + + /// Deobfuscate is the exact inverse of obfuscate for both parity branches (odd: byte7 + /// has odd popcount; even: zero). Pins the parity-dependent mask selection. + #[test] + fn xor_both_parity_branches() { + // Even-parity checksum byte (0x00 → parity 0): mask = byte[5]. + let mut even = [0x11u8, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x00]; + let orig_even = even; + MazdaSiemensDecoder::xor_obfuscate(&mut even); + MazdaSiemensDecoder::xor_deobfuscate(&mut even); + assert_eq!(even, orig_even); + assert_eq!(MazdaSiemensDecoder::byte_parity(0x00), 0); + + // Odd-parity checksum byte (0x01 → parity 1): mask = byte[6]. + let mut odd = [0xAAu8, 0xBB, 0xCC, 0xDD, 0xEE, 0x12, 0x34, 0x01]; + let orig_odd = odd; + MazdaSiemensDecoder::xor_obfuscate(&mut odd); + MazdaSiemensDecoder::xor_deobfuscate(&mut odd); + assert_eq!(odd, orig_odd); + assert_eq!(MazdaSiemensDecoder::byte_parity(0x01), 1); + } + + /// Field extraction matches mazda_parse_data: serial = >>32, button = >>24, counter = >>8. + #[test] + fn parse_fields_layout() { + let packed: u64 = 0x1234_5678_2000_06_3A; + let (serial, button, counter) = MazdaSiemensDecoder::parse_fields(packed); + assert_eq!(serial, 0x1234_5678); + assert_eq!(button, 0x20); + assert_eq!(counter, 0x0006); + } + + /// The encoder produces a non-empty Manchester upload with the expected leading 0xFF + /// preamble (all alternating short pulses) and a 50ms gap. Smoke test of the TX path. + #[test] + fn encode_produces_upload() { + let dec = MazdaSiemensDecoder::new(); + let signal = DecodedSignal { + serial: Some(0x1234_5678), + button: Some(0x02), + counter: Some(0x0005), + crc_valid: true, + data: 0x1234_5678_2000_05_00, + data_count_bit: MIN_COUNT_BIT, + encoder_capable: true, + extra: None, + protocol_display_name: None, + }; + let up = dec.encode(&signal, 0x02).expect("encode should succeed"); + assert!(!up.is_empty()); + // First 16 pulses are the first 0xFF preamble byte: alternating H/L shorts. + for (i, p) in up.iter().take(16).enumerate() { + assert_eq!(p.duration_us, TE_SHORT); + assert_eq!(p.level, i % 2 == 0, "0xFF Manchester alternates H,L starting high"); + } + // A 50ms gap is present somewhere in the upload. + assert!(up.iter().any(|p| p.duration_us == TX_GAP_US)); + } +} diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index fd3723a..e567615 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -32,8 +32,14 @@ mod kia_v2; mod kia_v3_v4; mod kia_v5; mod kia_v6; +mod kia_v7; mod subaru; mod ford_v0; +mod ford_v1; +mod ford_v2; +mod ford_v3; +mod honda_static; +mod honda_v1; mod vag; mod fiat_v0; mod fiat_v1; @@ -41,9 +47,17 @@ mod suzuki; mod scher_khan; mod star_line; mod psa; +mod psa2; +mod chrysler_v0; mod mazda_v0; +mod mazda_siemens; mod mitsubishi_v0; mod porsche_touareg; +mod porsche_cayenne; +mod bmw_cas4; +mod land_rover_v0; +mod land_rover_rke; +mod toyota; pub use common::DecodedSignal; @@ -108,9 +122,20 @@ impl ProtocolRegistry { Box::new(kia_v3_v4::KiaV3V4Decoder::new()), Box::new(kia_v5::KiaV5Decoder::new()), Box::new(kia_v6::KiaV6Decoder::new()), + Box::new(kia_v7::KiaV7Decoder::new()), // VAG before Ford/Subaru so 500/1000µs VAG streams decode as VAG (ProtoPirate order has VAG after Ford/Subaru but Flipper likely feeds all decoders; KAT uses first-match so VAG must be tried earlier) Box::new(vag::VagDecoder::new()), Box::new(ford_v0::FordV0Decoder::new()), + // Ford V1: distinct 65/130µs Manchester (vs V0 250/500, V2 200/400, V3 240/480) and + // CRC16-gated, so placement in the Ford group is safe (won't steal V0/V2/V3 captures). + Box::new(ford_v1::FordV1Decoder::new()), + Box::new(ford_v3::FordV3Decoder::new()), + Box::new(ford_v2::FordV2Decoder::new()), + // Honda Static after the Kia/Ford block; checksum-gated emission makes order safe. + Box::new(honda_static::HondaStaticDecoder::new()), + // Honda V1: distinct 1000/2000µs PWM (vs Honda Static's 63µs Manchester). Button-gated + // emission makes order safe. + Box::new(honda_v1::HondaV1Decoder::new()), Box::new(subaru::SubaruDecoder::new()), Box::new(fiat_v0::FiatV0Decoder::new()), Box::new(fiat_v1::FiatV1Decoder::new()), @@ -119,9 +144,61 @@ impl ProtocolRegistry { Box::new(star_line::StarLineDecoder::new()), Box::new(keeloq::KeeloqDecoder::new()), Box::new(psa::PsaDecoder::new()), + // PSA2 (Flipper-ARF, internal name "PSA OLD"): the OLDER PSA variant. Manchester + // 250/500µs (or 125/250µs half-rate), 128-bit frame (key1 64 + validation 16), AM/OOK, + // TEA cipher with a mode23 XOR fast-path validated by a nibble checksum (the live + // decoder runs ONLY this O(1) path — the dual TEA brute force is reserved for offline + // decrypt and never runs in feed(), exactly as in the C). Placed AFTER `psa` so the + // existing PSA keeps first-match priority and PSA2 acts as the older fallback. Emission + // is gated on the XOR checksum (or validation nibble == 0xA), so it false-matches + // nothing else. + Box::new(psa2::Psa2Decoder::new()), + // Chrysler V0 (300/3400-3700µs PWM, dual-long symbols). Timing is unique and emission + // is gated on the frame's own structural check, so placement here is low-risk. + Box::new(chrysler_v0::ChryslerV0Decoder::new()), + // Land Rover V0 (differential Manchester 250/500µs, 81 bits). Emission is gated on a + // 3-bit check polynomial + 16-bit tail + zero reserved bits, so placement is safe. + Box::new(land_rover_v0::LandRoverV0Decoder::new()), + // Land Rover RKE (Flipper-ARF): fixed-width PWM (700/300µs bits), 20-pulse preamble, + // a distinctive 400µs+9600µs sync gap, and exactly 66 bits. KeeLoq hop is left + // encrypted (no key), so emission is gated on the preamble + 9.6ms sync + 66-bit + // strict-PWM geometry — unique among KAT protocols, so it false-matches nothing. + Box::new(land_rover_rke::LandRoverRkeDecoder::new()), Box::new(mazda_v0::MazdaV0Decoder::new()), + // Mazda Siemens (Flipper-ARF): the Siemens/VDO keyfob cipher. It rides the SAME + // 250/500µs pair-based stream as Mazda V0 and uses the same additive-checksum gate, + // so the two would match an identical set of frames. Placed AFTER mazda_v0 so Mazda + // V0 keeps first-match priority and Mazda Siemens can never steal its captures. + // Emission is gated on the structural preamble/sync/bit-count + checksum, so it does + // not false-match other 250/500µs Manchester protocols. Adds an encoder (Mazda V0 + // has none). + Box::new(mazda_siemens::MazdaSiemensDecoder::new()), + // BMW CAS4 (Flipper-ARF): Manchester 500/1000µs, 64-bit, AM. The CAS4 rolling cipher's + // manufacturer key is unavailable, so the payload is left encrypted — emission is gated + // on the two fixed marker bytes (byte[0]==0x30 && byte[6]==0xC5), which is unique and + // makes it false-match nothing. Decode-only (the reference encoder is a stub). Marker- + // gated, so registry order here is safe. + Box::new(bmw_cas4::BmwCas4Decoder::new()), Box::new(mitsubishi_v0::MitsubishiV0Decoder::new()), Box::new(porsche_touareg::PorscheTouaregDecoder::new()), + // Porsche Cayenne (Flipper-ARF, internal name "Porsche AG"): the SAME wire protocol + // as Porsche Touareg — Touareg is itself a port of this very porsche_cayenne.c source, + // so they share the identical 1680/3370µs PWM, 73-pulse preamble + 5930µs gap, 64-bit + // MSB-first frame, 24-bit VAG rotating-register cipher, and counter-recovery validity + // gate. Placed AFTER porsche_touareg so Touareg keeps first-match priority and Cayenne + // can NEVER steal a Touareg capture (the registry reports the first decoder that fires + // on a given pulse). Cayenne adds the encoder (the Touareg port is decode-only) and is + // additionally gated on the frame_type being one of the three values the C emits/labels + // (0b001/0b010/0b100) — a strict subset of what Touareg accepts — so on any shared + // frame Touareg also fires and wins by ordering. + Box::new(porsche_cayenne::PorscheCayenneDecoder::new()), + // Toyota/Lexus (Flipper-ARF): dual-variant KeeLoq. Variant A is KeeLoq-PWM at 433 MHz + // and shares its air encoding with Kia V3/V4 — so Toyota MUST stay AFTER kia_v3_v4 + // (which is in the Kia block near the top) to preserve Kia's first-match priority on + // the shared PWM frames. Toyota uniquely claims 60-bit frames and the Variant-B NRZ + // stream at 315 MHz. Emission is gated on the exact frame bit count + structural + // preamble/sync + non-zero serial, so it false-matches nothing else. + Box::new(toyota::ToyotaDecoder::new()), ]; Self { decoders } @@ -343,4 +420,208 @@ mod tests { results.iter().map(|(n, _, _)| n.as_str()).collect::>() ); } + + #[test] + fn ford_v3_decodes_ldv_t80_sub() { + // LDV T80 keyfobs use a Ford-V3-style 240/480µs Manchester frame. + let path = Path::new("IMPORTS/LDV/LDV-T80_lock.sub"); + if !path.exists() { + eprintln!("Skip: {:?} not found (run from crate root)", path); + return; + } + let (freq, raw_pairs) = import_sub_raw(path).unwrap(); + let pairs: Vec = raw_pairs + .iter() + .map(|p| LevelDuration::new(p.level, p.duration_us)) + .collect(); + let mut reg = ProtocolRegistry::new(); + let results = reg.process_signal_stream(&pairs, freq); + assert!( + results.iter().any(|(n, _, _)| n == "Ford V3"), + "expected at least one Ford V3 decode from LDV-T80_lock.sub, got: {:?}", + results.iter().map(|(n, _, _)| n.as_str()).collect::>() + ); + } + + #[test] + fn honda_static_decodes_unlock_honda_sub() { + // Genuine Honda keyfob capture (CC1101 custom preset). Honda Static unpacks it via the + // reverse-packet path (matches honda_static.c), yielding a consistent serial/counter. + let path = Path::new("IMPORTS/honda/Unlock_honda.sub"); + if !path.exists() { + eprintln!("Skip: {:?} not found (run from crate root)", path); + return; + } + let (freq, raw_pairs) = import_sub_raw(path).unwrap(); + let pairs: Vec = raw_pairs + .iter() + .map(|p| LevelDuration::new(p.level, p.duration_us)) + .collect(); + let mut reg = ProtocolRegistry::new(); + let results = reg.process_signal_stream(&pairs, freq); + assert!( + results.iter().any(|(n, _, _)| n == "Honda Static"), + "expected at least one Honda Static decode from Unlock_honda.sub, got: {:?}", + results.iter().map(|(n, _, _)| n.as_str()).collect::>() + ); + } + + #[test] + fn toyota_decodes_camry_variant_b_sub() { + // 312 MHz Toyota Camry capture: Variant-B NRZ frame that Kia V3/V4 (and every other + // decoder) rejects, so Toyota uniquely claims it. Guards the Toyota port + its registry + // placement (after kia_v3_v4). + let path = Path::new("IMPORTS/Toyota + Lexus/19_toyota_camry_l2_u2_t2_p2.sub"); + if !path.exists() { + eprintln!("Skip: {:?} not found (run from crate root)", path); + return; + } + let (freq, raw_pairs) = import_sub_raw(path).unwrap(); + let pairs: Vec = raw_pairs + .iter() + .map(|p| LevelDuration::new(p.level, p.duration_us)) + .collect(); + let mut reg = ProtocolRegistry::new(); + let results = reg.process_signal_stream(&pairs, freq); + assert!( + results.iter().any(|(n, _, _)| n == "Toyota"), + "expected at least one Toyota decode from 19_toyota_camry_l2_u2_t2_p2.sub, got: {:?}", + results.iter().map(|(n, _, _)| n.as_str()).collect::>() + ); + } + + #[test] + fn toyota_does_not_steal_kia_v3_v4_prius_sub() { + // The Prius 433 MHz captures are KeeLoq-PWM frames whose air encoding is shared between + // Toyota Variant A and Kia V3/V4. Kia V3/V4 is earlier in the registry and MUST keep + // first-match priority — so these must still decode as "Kia V3/V4", never "Toyota". + let path = Path::new("IMPORTS/Toyota + Lexus/Toyota_Prius2006_lock.sub"); + if !path.exists() { + eprintln!("Skip: {:?} not found (run from crate root)", path); + return; + } + let (freq, raw_pairs) = import_sub_raw(path).unwrap(); + let pairs: Vec = raw_pairs + .iter() + .map(|p| LevelDuration::new(p.level, p.duration_us)) + .collect(); + let mut reg = ProtocolRegistry::new(); + let results = reg.process_signal_stream(&pairs, freq); + let names: Vec<&str> = results.iter().map(|(n, _, _)| n.as_str()).collect(); + assert!( + names.iter().any(|n| *n == "Kia V3/V4"), + "expected Kia V3/V4 decodes from Toyota_Prius2006_lock.sub, got: {:?}", + names + ); + assert!( + !names.iter().any(|n| *n == "Toyota"), + "Toyota must NOT steal the shared KeeLoq-PWM Prius frames from Kia V3/V4, got: {:?}", + names + ); + } + + #[test] + fn psa2_decodes_groupe_psa_sub() { + // Genuine Peugeot/Citroën keyfob captures. PSA2 ("PSA OLD") decodes them via the mode23 + // XOR fast path (nibble-checksum gated), recovering a stable serial 0x99EB25 with a rolling + // counter. Guards the PSA2 port + its registry placement (after `psa`). + let path = Path::new("IMPORTS/GROUPE PSA/PSA_523_536.sub"); + if !path.exists() { + eprintln!("Skip: {:?} not found (run from crate root)", path); + return; + } + let (freq, raw_pairs) = import_sub_raw(path).unwrap(); + let pairs: Vec = raw_pairs + .iter() + .map(|p| LevelDuration::new(p.level, p.duration_us)) + .collect(); + let mut reg = ProtocolRegistry::new(); + let results = reg.process_signal_stream(&pairs, freq); + let psa2: Vec<_> = results.iter().filter(|(n, _, _)| n == "PSA2").collect(); + assert!( + !psa2.is_empty(), + "expected at least one PSA2 decode from PSA_523_536.sub, got: {:?}", + results.iter().map(|(n, _, _)| n.as_str()).collect::>() + ); + // The decode must carry recovered fields (serial), not a bare undecrypted frame. + assert!( + psa2.iter().any(|(_, d, _)| d.serial == Some(0x99EB25) && d.crc_valid), + "expected a decrypted PSA2 frame with serial 0x99EB25, got serials: {:?}", + psa2.iter().map(|(_, d, _)| d.serial).collect::>() + ); + } + + #[test] + fn psa2_does_not_steal_vag_captures() { + // PSA2 rides the same 250/500µs Manchester rate as several other protocols and the C's + // mode23 nibble checksum is a loose ~1/16 gate. Emission is therefore restricted to a + // field-bearing decrypt, so PSA2 must NOT match these VAG captures (which carry no valid + // PSA2 frame) even though they reach 80 collectable bits at a PSA2-compatible frequency. + for rel in ["VAG/Test_55_unlock_and_55_lock_suran.sub", "VAG/Vw pasat B7.sub"] { + let path = Path::new("IMPORTS").join(rel); + if !path.exists() { + eprintln!("Skip: {:?} not found (run from crate root)", path); + continue; + } + let (freq, raw_pairs) = import_sub_raw(&path).unwrap(); + let pairs: Vec = raw_pairs + .iter() + .map(|p| LevelDuration::new(p.level, p.duration_us)) + .collect(); + let mut reg = ProtocolRegistry::new(); + let results = reg.process_signal_stream(&pairs, freq); + let names: Vec<&str> = results.iter().map(|(n, _, _)| n.as_str()).collect(); + assert!( + !names.iter().any(|n| *n == "PSA2"), + "PSA2 must not false-match {rel}, got: {names:?}" + ); + } + } + + /// Diagnostic: sweep every IMPORTS/*.sub and print which protocol(s) each decodes as. + /// `#[ignore]`d (noisy, always passes); run with + /// `cargo test sweep_imports_decodes -- --ignored --nocapture`. + #[test] + #[ignore] + fn sweep_imports_decodes() { + use std::fs; + let root = Path::new("IMPORTS"); + if !root.exists() { + eprintln!("Skip: IMPORTS not found"); + return; + } + // Collect IMPORTS//*.sub + let mut subs: Vec = Vec::new(); + if let Ok(makes) = fs::read_dir(root) { + for make in makes.flatten() { + if make.path().is_dir() { + if let Ok(files) = fs::read_dir(make.path()) { + for f in files.flatten() { + let p = f.path(); + if p.extension().map(|e| e == "sub").unwrap_or(false) { + subs.push(p); + } + } + } + } + } + } + subs.sort(); + let mut reg = ProtocolRegistry::new(); + for path in &subs { + match import_sub_raw(path) { + Ok((freq, raw_pairs)) => { + let pairs: Vec = raw_pairs + .iter() + .map(|p| LevelDuration::new(p.level, p.duration_us)) + .collect(); + let results = reg.process_signal_stream(&pairs, freq); + let names: Vec<&str> = results.iter().map(|(n, _, _)| n.as_str()).collect(); + let rel = path.strip_prefix("IMPORTS").unwrap_or(path); + eprintln!("{:<42} {:>9}Hz => {:?}", rel.display(), freq, names); + } + Err(e) => eprintln!("{:<42} ERR {:?}", path.display(), e), + } + } + } } diff --git a/src/protocols/porsche_cayenne.rs b/src/protocols/porsche_cayenne.rs new file mode 100644 index 0000000..205c3af --- /dev/null +++ b/src/protocols/porsche_cayenne.rs @@ -0,0 +1,613 @@ +//! Porsche Cayenne protocol decoder/encoder +//! +//! Aligned with Flipper-ARF reference: `lib/subghz/protocols/porsche_cayenne.c` +//! (internal protocol name in the firmware header is "Porsche AG"). +//! +//! # Relationship to the existing `Porsche Touareg` decoder +//! +//! KAT already ships `porsche_touareg.rs`, which is itself a port of this very same +//! `porsche_cayenne.c` source (its own doc comment and the `porsche_cayenne_compute_frame` +//! function name make that explicit). The two protocols are therefore **the same wire +//! protocol**: identical 1680/3370µs PWM, identical 73-pulse 3370µs preamble + 5930µs gap +//! pair, identical 64-bit MSB-first frame, identical 24-bit rotating-register VAG cipher, +//! and identical brute-force counter recovery / validity check (`counter != 0`). +//! +//! The only meaningful differences captured here versus Touareg: +//! * Display name is `"Porsche Cayenne"` (vs `"Porsche Touareg"`). +//! * This decoder ADDS the encoder from the C reference (the Touareg port is decode-only): +//! a 4-frame burst (frame types 0b010/0b001/0b100/0b100), 73 sync pairs + gap pair + +//! 64 MSB-first PWM data bits per frame. +//! +//! Because the two share an identical frame and validity gate, this decoder is registered +//! AFTER `porsche_touareg` so Touareg keeps first-match priority and Cayenne can never steal +//! a Touareg capture (the registry reports the first decoder that fires on a given pulse — +//! see `process_signal_*_inner` in `mod.rs`). To keep Cayenne a faithful but strictly +//! non-stealing decoder, emission is additionally gated on the frame_type being one of the +//! three values the C only ever emits/labels (`0b001` Cont, `0b010` First, `0b100` Final). +//! That is a strict subset of what Touareg accepts, so on any shared frame Touareg fires too +//! and wins by ordering; Cayenne only ever fires on frames Touareg also accepts. +//! +//! Protocol characteristics: +//! - PWM bit pairs: SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1 +//! - 64 bits total; sync preamble of 15+ LOW/HIGH pairs at 3370µs, then 5930µs gap pair, then data +//! - Field layout: pkt[0]=(btn<<4)|(frame_type&0x07), pkt[1..3]=serial 24-bit, pkt[4..7]=encrypted +//! - Counter recovery via brute-force matching of computed encrypted bytes against received bytes +//! - Frame types: 0x02="First", 0x01="Cont", 0x04="Final" +//! - RF: AM/OOK. Frequencies: 433.92 MHz and 868.35 MHz (C flags 433|868) + +use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; +use crate::duration_diff; +use crate::radio::demodulator::LevelDuration; + +const TE_SHORT: u32 = 1680; +const TE_LONG: u32 = 3370; +const TE_DELTA: u32 = 500; +const MIN_COUNT_BIT: usize = 64; + +const PC_TE_SYNC: u32 = 3370; +const PC_TE_GAP: u32 = 5930; +const PC_SYNC_MIN: u16 = 15; +/// Actual preamble pulse-pair count emitted by the firmware (PC_SYNC_COUNT). +const PC_SYNC_COUNT: usize = 73; + +/// KAT generic button codes (Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08). +const BTN_LOCK: u8 = 0x01; +const BTN_UNLOCK: u8 = 0x02; +const BTN_TRUNK: u8 = 0x04; +const BTN_PANIC: u8 = 0x08; + +/// Decoder states (matches PCDecoderStep in porsche_cayenne.c). +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + Sync, + GapHigh, + GapLow, + Data, +} + +/// Porsche Cayenne protocol decoder/encoder. +pub struct PorscheCayenneDecoder { + step: DecoderStep, + sync_count: u16, + raw_data: u64, + bit_count: usize, + te_last: u32, +} + +/// Circular left-shift of a 24-bit register stored in three bytes (h, m, l). +/// +/// Each byte shifts left by 1, receiving the MSB of the next byte in the chain: +/// h gets MSB of m, m gets MSB of l, l gets MSB of h (wrap-around). +/// +/// Matches the ROTATE24 macro in porsche_cayenne.c exactly. +#[inline] +fn rotate24(r_h: &mut u8, r_m: &mut u8, r_l: &mut u8) { + let ch = (*r_h >> 7) & 1; + let cm = (*r_m >> 7) & 1; + let cl = (*r_l >> 7) & 1; + *r_h = (*r_h << 1) | cm; + *r_m = (*r_m << 1) | cl; + *r_l = (*r_l << 1) | ch; +} + +/// Compute an 8-byte frame from serial, button, counter, and frame_type. +/// +/// Direct port of `porsche_cayenne_compute_frame` from the C reference. The cipher +/// increments `counter` by 1 internally. pkt[0..3] = plaintext header, pkt[4..7] = cipher +/// output derived from a 24-bit rotate register seeded from serial bytes and rotated +/// (4 + counter_low) times. +fn compute_frame(serial24: u32, btn: u8, counter: u16, frame_type: u8) -> [u8; 8] { + let b0 = (btn << 4) | (frame_type & 0x07); + let b1 = ((serial24 >> 16) & 0xFF) as u8; + let b2 = ((serial24 >> 8) & 0xFF) as u8; + let b3 = (serial24 & 0xFF) as u8; + + // Internal counter increment (firmware @ 0x14122). + let cnt = counter.wrapping_add(1); + let cnt_lo = (cnt & 0xFF) as u8; + let cnt_hi = ((cnt >> 8) & 0xFF) as u8; + + // Seed 24-bit register: r_h <- serial LSB, r_m <- serial MSB, r_l <- serial mid. + let mut r_h = b3; + let mut r_m = b1; + let mut r_l = b2; + + // Loop 1: 4 fixed rotations. + for _ in 0..4 { + rotate24(&mut r_h, &mut r_m, &mut r_l); + } + // Loop 2: cnt_lo additional rotations. + for _ in 0..cnt_lo as u16 { + rotate24(&mut r_h, &mut r_m, &mut r_l); + } + + // 9A: XOR of r_h with base byte. + let a9a = r_h ^ b0; + + // 9B: three masked slices of (~cnt_lo / ~cnt_hi) XOR r_m. + let nb9b_p1 = ((!cnt_lo).wrapping_shl(2) & 0xFC) ^ r_m; + let nb9b_p2 = ((!cnt_hi).wrapping_shl(2) & 0xFC) ^ r_m; + let nb9b_p3 = ((!cnt_hi).wrapping_shr(6) & 0x03) ^ r_m; + let a9b = (nb9b_p1 & 0xCC) | (nb9b_p2 & 0x30) | (nb9b_p3 & 0x03); + + // 9C: three masked slices of (~cnt_lo / ~cnt_hi) XOR r_l. + let nb9c_p1 = ((!cnt_lo).wrapping_shr(2) & 0x3F) ^ r_l; + let nb9c_p2 = ((!cnt_hi & 0x03).wrapping_shl(6)) ^ r_l; + let nb9c_p3 = ((!cnt_hi).wrapping_shr(2) & 0x3F) ^ r_l; + let a9c = (nb9c_p1 & 0x33) | (nb9c_p2 & 0xC0) | (nb9c_p3 & 0x0C); + + let mut pkt = [0u8; 8]; + pkt[0] = b0; + pkt[1] = b1; + pkt[2] = b2; + pkt[3] = b3; + pkt[4] = ((a9a >> 2) & 0x3F) | ((!cnt_lo & 0x03) << 6); + pkt[5] = (!cnt_lo & 0xC0) | ((a9a & 0x03) << 4) | (a9b & 0x0C) | ((!cnt_lo).wrapping_shr(2) & 0x03); + pkt[6] = ((a9b & 0x03) << 6) | ((a9c >> 2) & 0x3C) | ((!cnt_lo).wrapping_shr(4) & 0x03); + pkt[7] = ((a9b >> 4) & 0x0F) | ((a9c & 0x0F) << 4); + + pkt +} + +/// Unpack a raw 64-bit frame into its 8 bytes (big-endian: pkt[0] is the MSB). +fn unpack_bytes(data: u64) -> [u8; 8] { + let mut pkt = [0u8; 8]; + let mut raw = data; + for i in (0..8).rev() { + pkt[i] = (raw & 0xFF) as u8; + raw >>= 8; + } + pkt +} + +/// Brute-force counter recovery: try counter values 1..=256 (matching the C loop) and +/// compare the recomputed cipher bytes pkt[4..7]. Returns 0 if no match (invalid frame). +fn recover_counter(serial: u32, btn: u8, frame_type: u8, pkt: &[u8; 8]) -> u16 { + for try_cnt in 1u16..=256 { + // The cipher increments internally, so pass try_cnt - 1. + let try_pkt = compute_frame(serial, btn, try_cnt - 1, frame_type); + if try_pkt[4] == pkt[4] + && try_pkt[5] == pkt[5] + && try_pkt[6] == pkt[6] + && try_pkt[7] == pkt[7] + { + return try_cnt; + } + } + 0 +} + +/// Parse raw 64-bit data into a DecodedSignal, or `None` if it is not a valid Cayenne frame. +/// +/// Cayenne-specific gate (keeps this decoder from stealing Touareg captures): the frame_type +/// must be one of the three values the C reference emits/labels (0b001 Cont, 0b010 First, +/// 0b100 Final), AND the counter must be recoverable (cipher-consistent, `counter != 0`). +fn parse_data(data: u64) -> Option { + let pkt = unpack_bytes(data); + + let serial = ((pkt[1] as u32) << 16) | ((pkt[2] as u32) << 8) | (pkt[3] as u32); + let btn = pkt[0] >> 4; + let frame_type = pkt[0] & 0x07; + + // Cayenne-specific invariant: only the three documented frame types. + let frame_type_name = match frame_type { + 0b010 => "First", + 0b001 => "Cont", + 0b100 => "Final", + _ => return None, + }; + + // Cipher consistency check (also the C's validity signal). + let counter = recover_counter(serial, btn, frame_type, &pkt); + if counter == 0 { + return None; + } + + Some(DecodedSignal { + serial: Some(serial), + button: Some(btn), + counter: Some(counter), + crc_valid: true, // cipher-consistent frame + data, + data_count_bit: MIN_COUNT_BIT, + encoder_capable: true, + extra: Some(frame_type as u64), + protocol_display_name: Some(format!("Porsche Cayenne [{}]", frame_type_name)), + }) +} + +impl PorscheCayenneDecoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + sync_count: 0, + raw_data: 0, + bit_count: 0, + te_last: 0, + } + } + + /// Map a KAT generic button command to a Porsche Cayenne / VAG 4-bit button code. + /// KAT: Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08. + /// The C `porsche_cayenne_get_btn_code` maps d-pad: Up=0x01 Lock, Down=0x02 Unlock, + /// Left=0x04 Trunk, Right=0x08 Open — an identical set, so the codes pass through. + fn map_button(button: u8) -> u8 { + match button { + BTN_LOCK | BTN_UNLOCK | BTN_TRUNK | BTN_PANIC => button & 0x0F, + b => b & 0x0F, + } + } + + /// Append one PWM data bit as a (LOW, HIGH) pair, matching the C encoder: + /// bit 0: SHORT LOW + LONG HIGH; bit 1: LONG LOW + SHORT HIGH. + fn push_bit(signal: &mut Vec, bit: bool) { + if bit { + signal.push(LevelDuration::new(false, TE_LONG)); + signal.push(LevelDuration::new(true, TE_SHORT)); + } else { + signal.push(LevelDuration::new(false, TE_SHORT)); + signal.push(LevelDuration::new(true, TE_LONG)); + } + } + + /// Emit one full frame (73 sync pairs + gap pair + 64 MSB-first data bits) into `signal`. + /// Matches `porsche_cayenne_build_upload`'s per-frame body. + fn push_frame(signal: &mut Vec, pkt: &[u8; 8]) { + // Preamble: 73 × (LOW LONG + HIGH LONG). + for _ in 0..PC_SYNC_COUNT { + signal.push(LevelDuration::new(false, TE_LONG)); + signal.push(LevelDuration::new(true, TE_LONG)); + } + // Gap: LOW GAP + HIGH GAP. + signal.push(LevelDuration::new(false, PC_TE_GAP)); + signal.push(LevelDuration::new(true, PC_TE_GAP)); + // 64 data bits, MSB first, byte order pkt[0]->pkt[7]. + for &byte in pkt.iter() { + for bit in (0..8).rev() { + Self::push_bit(signal, (byte >> bit) & 1 != 0); + } + } + } +} + +impl ProtocolDecoder for PorscheCayenneDecoder { + fn name(&self) -> &'static str { + "Porsche Cayenne" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: MIN_COUNT_BIT, + } + } + + fn supported_frequencies(&self) -> &[u32] { + // C flags: SubGhzProtocolFlag_433 | SubGhzProtocolFlag_868. + &[433_920_000, 868_350_000] + } + + fn reset(&mut self) { + self.step = DecoderStep::Reset; + self.sync_count = 0; + self.raw_data = 0; + self.bit_count = 0; + self.te_last = 0; + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.step { + // Reset: wait for a LOW pulse matching sync timing (3370µs). + DecoderStep::Reset => { + if !level && duration_diff!(duration, PC_TE_SYNC) < TE_DELTA { + self.sync_count = 1; + self.step = DecoderStep::Sync; + } + } + + // Sync: count sync pulses (HIGH and LOW at 3370µs). + // On a gap pulse (5930µs) with enough sync pulses, transition to GapHigh/GapLow. + DecoderStep::Sync => { + if level { + if duration_diff!(duration, PC_TE_SYNC) < TE_DELTA { + // Good sync HIGH — keep collecting. + } else if self.sync_count >= PC_SYNC_MIN + && duration_diff!(duration, PC_TE_GAP) < TE_DELTA + { + // HIGH gap after sufficient sync pulses. + self.step = DecoderStep::GapLow; + } else { + self.step = DecoderStep::Reset; + } + } else { + // LOW pulse. + if duration_diff!(duration, PC_TE_SYNC) < TE_DELTA { + self.sync_count += 1; + } else if self.sync_count >= PC_SYNC_MIN + && duration_diff!(duration, PC_TE_GAP) < TE_DELTA + { + // LOW gap after sufficient sync pulses. + self.step = DecoderStep::GapHigh; + } else { + self.step = DecoderStep::Reset; + } + } + } + + // GapHigh: expect the complementary HIGH gap pulse. + DecoderStep::GapHigh => { + if level && duration_diff!(duration, PC_TE_GAP) < TE_DELTA { + self.raw_data = 0; + self.bit_count = 0; + self.step = DecoderStep::Data; + } else { + self.step = DecoderStep::Reset; + } + } + + // GapLow: expect the complementary LOW gap pulse. + DecoderStep::GapLow => { + if !level && duration_diff!(duration, PC_TE_GAP) < TE_DELTA { + self.raw_data = 0; + self.bit_count = 0; + self.step = DecoderStep::Data; + } else { + self.step = DecoderStep::Reset; + } + } + + // Data: decode bit pairs. + // LOW pulses are saved in te_last; HIGH pulses complete the bit: + // SHORT LOW + LONG HIGH = bit 0 + // LONG LOW + SHORT HIGH = bit 1 + DecoderStep::Data => { + if level { + let bit_value; + if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA + && duration_diff!(duration, TE_LONG) < TE_DELTA + { + bit_value = false; // bit 0 + } else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA + && duration_diff!(duration, TE_SHORT) < TE_DELTA + { + bit_value = true; // bit 1 + } else { + self.step = DecoderStep::Reset; + return None; + } + + self.raw_data = (self.raw_data << 1) | (bit_value as u64); + self.bit_count += 1; + + if self.bit_count >= MIN_COUNT_BIT { + let data = self.raw_data; + self.step = DecoderStep::Reset; + // parse_data returns None if the Cayenne gate fails (not a Cayenne + // frame), so this decoder stays silent on those and never steals them. + return parse_data(data); + } + } else { + // LOW pulse: save duration for the bit pair. + self.te_last = duration; + } + } + } + + None + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + let serial = decoded.serial? & 0xFFFFFF; + let cnt = decoded.counter.unwrap_or(0); + let btn = Self::map_button(button); + + // 4-frame burst (matches porsche_cayenne_build_upload): + // Frame 0: frame_type=0b010, cipher counter = cnt+1 + // Frame 1: frame_type=0b001, cipher counter = cnt+2 + // Frame 2: frame_type=0b100, cipher counter = cnt+3 + // Frame 3: frame_type=0b100, cipher counter = cnt+4 + // (compute_frame increments the passed counter by 1 internally.) + const FRAME_TYPES: [u8; 4] = [0b010, 0b001, 0b100, 0b100]; + + // Per-frame size: 73 sync pairs + 1 gap pair + 64 bit pairs = (73 + 1 + 64) * 2. + let mut signal = Vec::with_capacity((PC_SYNC_COUNT + 1 + 64) * 2 * 4); + + for (f, &ft) in FRAME_TYPES.iter().enumerate() { + let pkt = compute_frame(serial, btn, cnt.wrapping_add(f as u16), ft); + Self::push_frame(&mut signal, &pkt); + } + + Some(signal) + } +} + +impl Default for PorscheCayenneDecoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Feed an encoded signal back through a fresh decoder and return the first decode. + fn decode_signal(signal: &[LevelDuration]) -> Option { + let mut dec = PorscheCayenneDecoder::new(); + for ld in signal { + if let Some(d) = dec.feed(ld.level, ld.duration_us) { + return Some(d); + } + } + None + } + + /// Build a one-frame on-air signal for a single (serial, btn, counter, frame_type) and + /// decode it back. The frame's cipher counter is `counter+1` (compute_frame increments), + /// and the decoder's brute-force recovery reports that incremented value. + fn roundtrip_single(serial: u32, btn: u8, counter: u16, frame_type: u8) -> DecodedSignal { + let pkt = compute_frame(serial, btn, counter, frame_type); + let mut signal = Vec::new(); + PorscheCayenneDecoder::push_frame(&mut signal, &pkt); + decode_signal(&signal) + .unwrap_or_else(|| panic!("decode failed for serial {serial:#X} btn {btn:#X} cnt {counter} ft {frame_type:#b}")) + } + + #[test] + fn encode_decode_roundtrip_via_encoder() { + // Full encoder path: encode() emits a 4-frame burst; the first frame (type 0b010, + // cipher counter = cnt+1) must decode back with the right serial/button, and the + // recovered counter must survive the rolling cipher (== cnt+1). + let serials = [0x00ABCDEFu32, 0x00123456, 0x00000001, 0x00FFFFFE, 0x005A5A5A]; + let buttons = [BTN_LOCK, BTN_UNLOCK, BTN_TRUNK, BTN_PANIC]; + // Counters kept within the C reference's recoverable range: its decoder only + // brute-forces cnt_lo (256 values), so the first burst frame's cipher counter + // (seed+1) must be <= 256, i.e. seed <= 255. See `counter_recovery_limit_matches_c`. + let counters = [0u16, 1, 42, 200, 254, 255]; + + for (&serial, &counter) in serials.iter().zip(counters.iter()) { + for &btn in &buttons { + let seed = DecodedSignal { + serial: Some(serial), + button: Some(btn), + counter: Some(counter), + crc_valid: true, + data: 0, + data_count_bit: MIN_COUNT_BIT, + encoder_capable: true, + extra: None, + protocol_display_name: None, + }; + let encoder = PorscheCayenneDecoder::new(); + let signal = encoder.encode(&seed, btn).expect("encode should succeed"); + let decoded = decode_signal(&signal).unwrap_or_else(|| { + panic!("decode failed for serial {serial:#X} btn {btn:#X} cnt {counter}") + }); + + let expected_btn = PorscheCayenneDecoder::map_button(btn); + assert_eq!(decoded.serial, Some(serial & 0xFFFFFF), "serial"); + assert_eq!(decoded.button, Some(expected_btn), "button"); + // First frame's cipher counter is cnt+1 (compute_frame increments). + assert_eq!( + decoded.counter, + Some(counter.wrapping_add(1)), + "counter must survive the rolling cipher" + ); + assert_eq!(decoded.data_count_bit, MIN_COUNT_BIT, "bit count"); + assert!(decoded.crc_valid, "cipher-consistent frame → crc_valid"); + // First burst frame is type 0b010 = "First". + assert_eq!(decoded.extra, Some(0b010), "frame_type First"); + assert_eq!( + decoded.protocol_display_name.as_deref(), + Some("Porsche Cayenne [First]") + ); + } + } + } + + #[test] + fn roundtrip_all_frame_types() { + // Each of the three documented frame types decodes and labels correctly, and the + // counter survives across the cipher for a spread of counter values. + let cases = [ + (0b010u8, "Porsche Cayenne [First]"), + (0b001u8, "Porsche Cayenne [Cont]"), + (0b100u8, "Porsche Cayenne [Final]"), + ]; + for &(ft, name) in &cases { + // Counters within the C's recoverable cnt_lo range (cipher counter seed+1 <= 256). + for &counter in &[0u16, 7, 200, 254, 255] { + let d = roundtrip_single(0x00C0FFEE, BTN_UNLOCK, counter, ft); + assert_eq!(d.serial, Some(0x00C0FFEE), "serial ft={ft:#b}"); + assert_eq!(d.button, Some(BTN_UNLOCK), "button ft={ft:#b}"); + assert_eq!( + d.counter, + Some(counter.wrapping_add(1)), + "counter survives cipher ft={ft:#b} cnt={counter}" + ); + assert_eq!(d.extra, Some(ft as u64), "frame_type ft={ft:#b}"); + assert_eq!(d.protocol_display_name.as_deref(), Some(name)); + assert!(d.crc_valid); + } + } + } + + #[test] + fn rejects_undocumented_frame_type() { + // A structurally valid PWM frame whose frame_type is NOT one of {0b001,0b010,0b100} + // must be rejected by Cayenne's gate — this is what keeps it from stealing arbitrary + // 1680/3370µs PWM frames (and what makes Touareg, ordered first, the owner of any + // shared frame). frame_type 0b011 is unused by the C reference. + let pkt = compute_frame(0x00123456, BTN_LOCK, 5, 0b011); + let mut signal = Vec::new(); + PorscheCayenneDecoder::push_frame(&mut signal, &pkt); + assert!( + decode_signal(&signal).is_none(), + "undocumented frame_type 0b011 must not decode as Cayenne" + ); + } + + #[test] + fn rejects_truncated_frame() { + // 63 of 64 data bits must not decode. + let pkt = compute_frame(0x00ABCDEF, BTN_LOCK, 3, 0b010); + let mut signal = Vec::new(); + PorscheCayenneDecoder::push_frame(&mut signal, &pkt); + // Frame = 73 sync pairs + 1 gap pair + 64 bit pairs. Drop the last bit pair. + let keep = (PC_SYNC_COUNT + 1 + 63) * 2; + assert!( + decode_signal(&signal[..keep]).is_none(), + "63-bit frame must not decode" + ); + } + + #[test] + fn counter_recovery_limit_matches_c() { + // Faithful limitation of the C reference: its decoder brute-forces only cnt_lo + // (try_cnt 1..=256), so frames whose cipher counter (passed counter + 1) exceeds 256 + // cannot have their counter recovered. recover_counter returns 0 for those, and the + // frame is reported invalid (does not decode) — exactly as the C would behave. + // seed=255 -> cipher counter 256 -> recoverable; seed=256 -> 257 -> NOT recoverable. + let pkt_ok = compute_frame(0x00ABCDEF, BTN_LOCK, 255, 0b010); + assert_eq!( + recover_counter(0x00ABCDEF, BTN_LOCK, 0b010, &pkt_ok), + 256, + "cipher counter 256 is the highest recoverable value" + ); + let pkt_oob = compute_frame(0x00ABCDEF, BTN_LOCK, 256, 0b010); + assert_eq!( + recover_counter(0x00ABCDEF, BTN_LOCK, 0b010, &pkt_oob), + 0, + "cipher counter 257 is beyond the C's cnt_lo brute-force range" + ); + // And such an out-of-range frame must not decode (parse_data rejects counter==0). + let mut signal = Vec::new(); + PorscheCayenneDecoder::push_frame(&mut signal, &pkt_oob); + assert!( + decode_signal(&signal).is_none(), + "frame with unrecoverable counter must not decode (matches C)" + ); + } + + #[test] + fn cipher_matches_touareg_reference_vectors() { + // The cipher/frame is shared with the Touareg port (same C source). Spot-check that + // compute_frame -> unpack header bytes are exactly as laid out by the C reference: + // pkt[0]=(btn<<4)|ft, pkt[1..3]=serial big-endian. + let pkt = compute_frame(0x00ABCDEF, 0x02, 0x10, 0b010); + assert_eq!(pkt[0], (0x02 << 4) | 0b010, "pkt[0] = (btn<<4)|ft"); + assert_eq!(pkt[1], 0xAB, "serial MSB"); + assert_eq!(pkt[2], 0xCD, "serial mid"); + assert_eq!(pkt[3], 0xEF, "serial LSB"); + // Recovering the counter from this exact frame yields cnt+1 = 0x11. + let cnt = recover_counter(0x00ABCDEF, 0x02, 0b010, &pkt); + assert_eq!(cnt, 0x11, "brute-force counter recovery = passed counter + 1"); + } +} diff --git a/src/protocols/psa2.rs b/src/protocols/psa2.rs new file mode 100644 index 0000000..9d675a5 --- /dev/null +++ b/src/protocols/psa2.rs @@ -0,0 +1,919 @@ +//! PSA2 (Peugeot/Citroën — "PSA OLD") protocol decoder/encoder +//! +//! Aligned with Flipper-ARF reference: `lib/subghz/protocols/psa2.c` and `psa2.h` +//! (internal name `SUBGHZ_PROTOCOL_PSA2_NAME = "PSA OLD"`). This is the OLDER PSA variant, +//! distinct from KAT's existing `psa` (modified-TEA/XEA) decoder. +//! +//! Protocol characteristics: +//! - Manchester encoding: 250/500µs symbol (Pattern 1, standard rate) or 125/250µs (Pattern 2, +//! half rate). Canonical Flipper `manchester_advance` table (events ShortLow=0, ShortHigh=2, +//! LongLow=4, LongHigh=6), seeded `ManchesterStateMid1`. +//! - 128-bit frame = key1 (64 bits) + key2/validation word. The decoder collects 64 bits → key1, +//! then 16 more (to 80 bits = `KEY2_BITS`) → the 16-bit validation field / key2_low. +//! - RF: AM (OOK). Frequency: 433.92 MHz. +//! - Crypto: TEA (Tiny Encryption Algorithm) with a dual brute-force fallback (BF1 +//! 0x23000000–0x24000000, BF2 0xF3000000–0xF4000000) and a mode23/mode36 selector, validated +//! via a nibble checksum. +//! +//! ## Decoder structure & PERFORMANCE +//! The C live decoder (`subghz_protocol_decoder_psa2_feed`) only ever runs the cheap mode23 XOR +//! path (`psa_decrypt_fast`) per frame — it NEVER runs the TEA brute force. The brute force +//! (`psa_decrypt_full`, marked `__attribute__((unused))`) is reserved for the deferred-decrypt +//! UI button, not the streaming decoder. KAT mirrors this exactly. Per frame: +//! +//! 1. Manchester-collect to exactly 80 bits (key1 + validation) with end-of-packet detection. +//! 2. Run the O(1) `direct_xor_decrypt` (mode23) gated on its nibble checksum. +//! 3. Emit only on a successful, field-bearing decrypt (see `finalize_frame` for why the C's bare +//! `(validation & 0xF) == 0xA` emission is not reproduced in KAT's feed-all model). +//! +//! The bounded TEA brute force (BF1/BF2, ~16.7M iters each) is ported faithfully in +//! `Psa2Decoder::decrypt_full` but is NEVER called from `feed()` — only the cheap O(1) gate runs +//! per pulse, so the test sweep stays fast. +//! +//! Decoder steps: State0 (wait preamble) → State1/State3 (count preamble pulses) → +//! State2/State4 (Manchester decode + decrypt). Encoder supported (mode23 path). + +use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; +use crate::duration_diff; +use crate::radio::demodulator::LevelDuration; + +// Standard-rate timings (Pattern 1) +const TE_SHORT: u32 = 250; +const TE_LONG: u32 = 500; +const TE_DELTA: u32 = 100; +const MIN_COUNT_BIT: usize = 128; + +// Half-rate timings (Pattern 2 / State 3-4) +const TE_SHORT_HALF: u32 = 125; +const TE_LONG_HALF: u32 = 250; +const TOL_HALF: u32 = 50; + +// End-of-packet markers +const TE_END_1000: u32 = 1000; +const TE_END_500: u32 = 500; + +// Bit counts +const KEY1_BITS: usize = 64; // 0x40 +const KEY2_BITS: usize = 80; // 0x50 +const MAX_BITS: usize = 121; // 0x79 + +// Preamble pulse-count thresholds (C: PSA_PATTERN_THRESHOLD_1/2) +const PATTERN_THRESHOLD_1: u16 = 0x46; +const PATTERN_THRESHOLD_2: u16 = 0x45; + +// Validation nibble for the mode23 path: (validation_field & 0xF) == 0xA +const VALID_NIBBLE: u16 = 0xA; + +// "decrypted" success marker (C uses 0x50) +const DECRYPTED_OK: u16 = 0x50; + +// Mode selectors (stored as ASCII chars in firmware) +const MODE_23: u8 = 0x23; // '#' +const MODE_36: u8 = 0x36; // '6' + +// PSA2 button codes are Lock=0, Unlock=1, Trunk=2 (psa_button_name). Decodes whose 4-bit button +// exceeds this are coincidental matches on unrelated Manchester data and are rejected. +const BTN_MAX_VALID: u8 = 0x2; + +// TEA constants +const TEA_DELTA: u32 = 0x9E3779B9; +const TEA_ROUNDS: u32 = 32; + +// BF1 brute-force range + constants (FUN_08028f94 / FUN_080291c0) +const BF1_START: u32 = 0x2300_0000; +const BF1_END: u32 = 0x2400_0000; +const BF1_CONST_U4: u32 = 0x0E0F_5C41; +const BF1_CONST_U5: u32 = 0x0F5C_4123; +const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A43_4915, 0xD674_3C2B, 0x1F29_D308, 0xE6B7_9A64]; + +// BF2 brute-force range + key schedule (FUN_080290f8) +const BF2_START: u32 = 0xF300_0000; +const BF2_END: u32 = 0xF400_0000; +const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039_C240, 0xEDA9_2CAB, 0x4306_C02A, 0x0219_2A04]; + +/// Canonical Flipper Manchester states (lib/toolbox/manchester_decoder.c order: +/// Start1=0, Mid1=1, Mid0=2, Start0=3). +#[derive(Debug, Clone, Copy, PartialEq)] +enum ManchesterState { + Start1 = 0, + Mid1 = 1, + Mid0 = 2, + Start0 = 3, +} + +/// Decoder states (matches PSADecoderState0-4 in psa2.c). +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderState { + /// State0: wait for preamble start. + WaitEdge, + /// State1: count standard-rate (250µs) preamble pulses. + CountPattern250, + /// State2: receive key1 + key2/validation at standard rate. + DecodeManchester250, + /// State3: count half-rate (125µs) preamble pulses. + CountPattern125, + /// State4: receive key1 + key2/validation at half rate. + DecodeManchester125, +} + +/// Result of a successful decrypt: (serial, button, counter, crc, type). +type DecryptResult = (u32, u8, u32, u16, u8); + +/// PSA2 ("PSA OLD") protocol decoder. +pub struct Psa2Decoder { + state: DecoderState, + prev_duration: u32, + manchester_state: ManchesterState, + pattern_counter: u16, + data_low: u32, + data_high: u32, + bit_count: usize, + // Decoded fields + key1_low: u32, + key1_high: u32, + validation_field: u16, + key2_low: u32, + key2_high: u32, +} + +impl Psa2Decoder { + pub fn new() -> Self { + Self { + state: DecoderState::WaitEdge, + prev_duration: 0, + manchester_state: ManchesterState::Mid1, + pattern_counter: 0, + data_low: 0, + data_high: 0, + bit_count: 0, + key1_low: 0, + key1_high: 0, + validation_field: 0, + key2_low: 0, + key2_high: 0, + } + } + + fn near(dur: u32, target: u32, tol: u32) -> bool { + duration_diff!(dur, target) <= tol + } + + // ========================================================================= + // Manchester — canonical Flipper transition table (transitions[] in + // manchester_decoder.c). `event` ∈ {0,2,4,6}; returns Some(bit) on emit. + // ========================================================================= + fn manchester_advance(&mut self, event: u8) -> Option { + const TRANSITIONS: [u8; 4] = [0b0000_0001, 0b1001_0001, 0b1001_1011, 0b1111_1011]; + let state_idx = self.manchester_state as usize; + let new_idx = (TRANSITIONS[state_idx] >> event) & 0x3; + let new_state = match new_idx { + 0 => ManchesterState::Start1, + 1 => ManchesterState::Mid1, + 2 => ManchesterState::Mid0, + _ => ManchesterState::Start0, + }; + + if new_idx as usize == state_idx { + // No progress → reset to Mid1, emit nothing. + self.manchester_state = ManchesterState::Mid1; + return None; + } + self.manchester_state = new_state; + match new_state { + ManchesterState::Mid0 => Some(false), + ManchesterState::Mid1 => Some(true), + _ => None, + } + } + + fn manchester_reset(&mut self) { + self.manchester_state = ManchesterState::Mid1; + } + + /// Shift one decoded bit into the 64-bit accumulator (data_high:data_low), + /// latching key1 at 64 bits (matches psa2.c State2/State4 add path). + fn add_bit(&mut self, bit: bool) { + let carry = (self.data_low >> 31) & 1; + self.data_low = (self.data_low << 1) | (bit as u32); + self.data_high = (self.data_high << 1) | carry; + self.bit_count += 1; + if self.bit_count == KEY1_BITS { + self.key1_low = self.data_low; + self.key1_high = self.data_high; + self.data_low = 0; + self.data_high = 0; + } + } + + fn init_preamble_state(&mut self) { + self.data_low = 0; + self.data_high = 0; + self.pattern_counter = 0; + self.bit_count = 0; + self.manchester_reset(); + } + + // ========================================================================= + // CRYPTO PRIMITIVES (faithful to psa2.c) + // ========================================================================= + + /// TEA encrypt (FUN_08028e14): dynamic key index `sum&3` then `(sum>>11)&3`. + fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) { + let (mut a, mut b) = (*v0, *v1); + let mut sum: u32 = 0; + for _ in 0..TEA_ROUNDS { + let t = key[(sum & 3) as usize].wrapping_add(sum); + sum = sum.wrapping_add(TEA_DELTA); + a = a.wrapping_add(t ^ ((b >> 5) ^ (b << 4)).wrapping_add(b)); + let t = key[((sum >> 11) & 3) as usize].wrapping_add(sum); + b = b.wrapping_add(t ^ ((a >> 5) ^ (a << 4)).wrapping_add(a)); + } + *v0 = a; + *v1 = b; + } + + /// TEA decrypt (FUN_08028e14 inverse): unwinds the encrypt rounds. + fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) { + let (mut a, mut b) = (*v0, *v1); + let mut sum: u32 = TEA_DELTA.wrapping_mul(TEA_ROUNDS); + for _ in 0..TEA_ROUNDS { + let t = key[((sum >> 11) & 3) as usize].wrapping_add(sum); + sum = sum.wrapping_sub(TEA_DELTA); + b = b.wrapping_sub(t ^ ((a >> 5) ^ (a << 4)).wrapping_add(a)); + let t = key[(sum & 3) as usize].wrapping_add(sum); + a = a.wrapping_sub(t ^ ((b >> 5) ^ (b << 4)).wrapping_add(b)); + } + *v0 = a; + *v1 = b; + } + + /// Byte-sum CRC over 7 bytes of TEA output (FUN_08028e60). + fn calculate_tea_crc(v0: u32, v1: u32) -> u8 { + let mut crc: u32 = ((v0 >> 24) & 0xFF) + ((v0 >> 16) & 0xFF) + ((v0 >> 8) & 0xFF) + (v0 & 0xFF); + crc += ((v1 >> 24) & 0xFF) + ((v1 >> 16) & 0xFF) + ((v1 >> 8) & 0xFF); + (crc & 0xFF) as u8 + } + + /// CRC-16/BUYPASS (poly 0x8005, init 0, no reflection) (FUN_08029098). + fn calculate_crc16_bf2(data: &[u8]) -> u16 { + let mut crc: u16 = 0; + for &byte in data { + crc ^= (byte as u16) << 8; + for _ in 0..8 { + if crc & 0x8000 != 0 { + crc = (crc << 1) ^ 0x8005; + } else { + crc <<= 1; + } + } + } + crc + } + + /// Fill buf[0..9] from key1/key2 (FUN_080291c0 first loop / psa_setup_byte_buffer). + /// key1 big-endian reversed into buf[7..0]; key2_low low/high bytes into buf[9]/buf[8]. + fn setup_byte_buffer(buf: &mut [u8], key1_low: u32, key1_high: u32, key2_low: u32) { + for i in 0..8usize { + let shift = i * 8; + let b = if shift < 32 { + (key1_low >> shift) as u8 + } else { + (key1_high >> (shift - 32)) as u8 + }; + buf[7 - i] = b; + } + buf[9] = (key2_low & 0xFF) as u8; + buf[8] = ((key2_low >> 8) & 0xFF) as u8; + } + + /// Nibble checksum over buf[2..8] → buf[11] (FUN_08028cf8). + fn calculate_checksum(buf: &mut [u8]) { + let mut sum: u32 = 0; + for &b in buf.iter().take(8).skip(2) { + sum += (b & 0xF) as u32 + ((b >> 4) & 0xF) as u32; + } + buf[11] = (sum.wrapping_mul(0x10) & 0xFF) as u8; + } + + /// XOR decrypt second stage (FUN_08028d54 + psa_copy_reverse FUN_08028d24). + fn second_stage_xor_decrypt(buf: &mut [u8]) { + // psa_copy_reverse + let t = [ + buf[5], buf[4], buf[3], buf[2], buf[9], buf[8], buf[7], buf[6], + ]; + buf[2] = t[0] ^ t[6]; + buf[3] = t[2] ^ t[0]; + buf[4] = t[6] ^ t[3]; + buf[5] = t[7] ^ t[1]; + buf[6] = t[3] ^ t[1]; + buf[7] = t[6] ^ t[4] ^ t[5]; + } + + /// Inverse of `second_stage_xor_decrypt`, used by the encoder. + fn second_stage_xor_encrypt(buf: &mut [u8]) { + let e6 = buf[8]; + let e7 = buf[9]; + let (p0, p1, p2, p3, p4, p5) = (buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]); + let e5 = p5 ^ e7 ^ e6; + let e0 = p2 ^ e5; + let e2 = p4 ^ e0; + let e4 = p3 ^ e2; + let e3 = p0 ^ e5; + let e1 = p1 ^ e3; + buf[2] = e0; + buf[3] = e1; + buf[4] = e2; + buf[5] = e3; + buf[6] = e4; + buf[7] = e5; + } + + /// Pack buf[2..9] into two TEA words (FUN_08028f4c). + fn prepare_tea_data(buf: &[u8]) -> (u32, u32) { + let w0 = ((buf[2] as u32) << 24) | ((buf[3] as u32) << 16) | ((buf[4] as u32) << 8) | buf[5] as u32; + let w1 = ((buf[6] as u32) << 24) | ((buf[7] as u32) << 16) | ((buf[8] as u32) << 8) | buf[9] as u32; + (w0, w1) + } + + /// Unpack two TEA words back into buf[2..9] (FUN_08028e88). + fn unpack_tea_result(buf: &mut [u8], v0: u32, v1: u32) { + buf[2] = (v0 >> 24) as u8; + buf[3] = (v0 >> 16) as u8; + buf[4] = (v0 >> 8) as u8; + buf[5] = v0 as u8; + buf[6] = (v1 >> 24) as u8; + buf[7] = (v1 >> 16) as u8; + buf[8] = (v1 >> 8) as u8; + buf[9] = v1 as u8; + } + + // ========================================================================= + // FIELD EXTRACTION (FUN_08028f10) + // ========================================================================= + + fn extract_fields_mode23(buf: &[u8]) -> DecryptResult { + let button = buf[8] & 0xF; + let serial = ((buf[2] as u32) << 16) | ((buf[3] as u32) << 8) | buf[4] as u32; + let counter = (buf[6] as u32) | ((buf[5] as u32) << 8); + let crc = buf[7] as u16; + (serial, button, counter, crc, MODE_23) + } + + fn extract_fields_mode36(buf: &[u8]) -> DecryptResult { + let button = (buf[5] >> 4) & 0xF; + let serial = ((buf[2] as u32) << 16) | ((buf[3] as u32) << 8) | buf[4] as u32; + let counter = ((buf[7] as u32) << 8) + | ((buf[6] as u32) << 16) + | (buf[8] as u32) + | (((buf[5] as u32) & 0xF) << 24); + let crc = buf[9] as u16; + (serial, button, counter, crc, MODE_36) + } + + // ========================================================================= + // DECRYPTION PATHS (faithful to psa2.c) + // ========================================================================= + + /// key2-high gate (matches KAT's sibling `psa::direct_xor_allowed_by_key2`). The PSA2 C + /// `psa_direct_xor_decrypt` validates only on the 4-bit checksum nibble `(checksum ^ key2_high) + /// & 0xF0 == 0`, which has a ~1/16 false-positive rate on arbitrary 80-bit Manchester data. In + /// KAT's "feed every frequency-compatible decoder, first match wins" model that lets PSA2 steal + /// unrelated 250/500µs frames (e.g. VAG at 434 MHz). This precondition — the exact filter KAT's + /// existing `psa` decoder already applies before its XOR path — is added on top of the C gate to + /// suppress those false positives without dropping any genuine PSA2 decode. + fn direct_xor_allowed_by_key2(key2_high_byte: u8) -> bool { + let lo = key2_high_byte & 0xF; + if lo < 3 { + return true; + } + if lo < 7 && (key2_high_byte & 0xC) != 0 { + return true; + } + false + } + + /// mode23 XOR path (FUN_08028d98 / psa_direct_xor_decrypt). O(1) — the only path the + /// live decoder runs. Returns the extracted fields on checksum validation. + /// + /// Beyond the C's nibble-checksum gate, this applies two false-positive suppressors required by + /// KAT's feed-all-decoders model: the `direct_xor_allowed_by_key2` precondition and a valid- + /// button check (PSA2 buttons are Lock=0, Unlock=1, Trunk=2 only — see `psa_button_name`). Both + /// cleanly separate genuine PSA2 frames from coincidental matches on unrelated Manchester data. + fn direct_xor_decrypt(key1_low: u32, key1_high: u32, key2_low: u32) -> Option { + let mut buf = [0u8; 48]; + Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low); + + let key2_high = buf[8]; + if !Self::direct_xor_allowed_by_key2(key2_high) { + return None; + } + + Self::calculate_checksum(&mut buf); + let checksum = buf[11]; + let validation = (checksum ^ key2_high) & 0xF0; + + if validation == 0 { + // Firmware: update buf[8] high nibble before XOR stage. + buf[8] = (buf[8] & 0x0F) | (checksum & 0xF0); + buf[13] = buf[9] ^ buf[8]; + Self::second_stage_xor_decrypt(&mut buf); + let fields = Self::extract_fields_mode23(&buf); + // Reject implausible button codes (PSA2: Lock=0/Unlock=1/Trunk=2). + if fields.1 > BTN_MAX_VALID { + return None; + } + return Some(fields); + } + None + } + + /// BF1 brute force, range 0x23000000–0x24000000 (FUN_08028f94). + /// + /// PERFORMANCE: up to ~16.7M TEA iterations. NEVER called from `feed()`. Only invoked from + /// [`Self::decrypt_full`], which itself runs only after the cheap structural gate. Returns + /// `(result, seed)` on success. + fn brute_force_decrypt_bf1(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<(DecryptResult, u32)> { + let mut buf = [0u8; 48]; + Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low); + let (w0, w1) = Self::prepare_tea_data(&buf); + + for counter in BF1_START..BF1_END { + // Derive the working key with two TEA encrypts. + let (mut wk2, mut wk3) = (BF1_CONST_U4, counter); + Self::tea_encrypt(&mut wk2, &mut wk3, &BF1_KEY_SCHEDULE); + let (mut wk0, mut wk1) = ((counter << 8) | 0x0E, BF1_CONST_U5); + Self::tea_encrypt(&mut wk0, &mut wk1, &BF1_KEY_SCHEDULE); + let wkey = [wk0, wk1, wk2, wk3]; + + let (mut dv0, mut dv1) = (w0, w1); + Self::tea_decrypt(&mut dv0, &mut dv1, &wkey); + + if (counter & 0xFFFFFF) == (dv0 >> 8) { + let crc = Self::calculate_tea_crc(dv0, dv1); + if crc == (dv1 & 0xFF) as u8 { + let mut out = [0u8; 48]; + Self::unpack_tea_result(&mut out, dv0, dv1); + return Some((Self::extract_fields_mode36(&out), counter)); + } + } + } + None + } + + /// BF2 brute force, range 0xF3000000–0xF4000000 (FUN_080290f8). + /// + /// PERFORMANCE: up to ~16.7M TEA iterations. NEVER called from `feed()` (see BF1 note). + fn brute_force_decrypt_bf2(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<(DecryptResult, u32)> { + let mut buf = [0u8; 48]; + Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low); + let (w0, w1) = Self::prepare_tea_data(&buf); + + for counter in BF2_START..BF2_END { + let wkey = [ + BF2_KEY_SCHEDULE[0] ^ counter, + BF2_KEY_SCHEDULE[1] ^ counter, + BF2_KEY_SCHEDULE[2] ^ counter, + BF2_KEY_SCHEDULE[3] ^ counter, + ]; + let (mut dv0, mut dv1) = (w0, w1); + Self::tea_decrypt(&mut dv0, &mut dv1, &wkey); + + if (counter & 0xFFFFFF) == (dv0 >> 8) { + let crc_buf = [ + (dv0 >> 24) as u8, + (dv0 >> 16) as u8, + (dv0 >> 8) as u8, + dv0 as u8, + (dv1 >> 24) as u8, + (dv1 >> 16) as u8, + ]; + let crc16 = Self::calculate_crc16_bf2(&crc_buf); + let expected = ((dv1 & 0xFF) | (((dv1 >> 16) & 0xFF) << 8)) as u16; + if crc16 == expected { + let mut out = [0u8; 48]; + Self::unpack_tea_result(&mut out, dv0, dv1); + return Some((Self::extract_fields_mode36(&out), counter)); + } + } + } + None + } + + /// Full decrypt router (FUN_080291c0, the `__attribute__((unused))` `psa_decrypt_full`): + /// try XOR (mode23), then BF1, then BF2. + /// + /// PERFORMANCE: this can run the bounded brute force (~33M TEA iterations worst case). It is + /// NOT part of the live `feed()` path — `feed()` uses only `direct_xor_decrypt`. This mirrors + /// the C, where `psa_decrypt_full` is unused by the decoder and the feed callback calls + /// `psa_decrypt_fast` (XOR only). Exposed for completeness / offline decrypt; gate any caller + /// behind the structural frame check so the brute force never runs on arbitrary data. + #[allow(dead_code)] + fn decrypt_full(key1_low: u32, key1_high: u32, key2_low: u32) -> Option { + if let Some(r) = Self::direct_xor_decrypt(key1_low, key1_high, key2_low) { + return Some(r); + } + if let Some((r, _seed)) = Self::brute_force_decrypt_bf1(key1_low, key1_high, key2_low) { + return Some(r); + } + if let Some((r, _seed)) = Self::brute_force_decrypt_bf2(key1_low, key1_high, key2_low) { + return Some(r); + } + None + } + + /// Build the encoded key material for a mode23 frame (psa_build_encrypt_mode23 / + /// FUN_08029028). Returns `(key1_high, key1_low, validation_field)`. + fn encode_mode23(serial: u32, button: u8, counter: u16) -> (u32, u32, u16) { + let mut buf = [0u8; 48]; + buf[2] = (serial >> 16) as u8; + buf[3] = (serial >> 8) as u8; + buf[4] = serial as u8; + buf[5] = (counter >> 8) as u8; + buf[6] = counter as u8; + buf[7] = 0; // CRC placeholder + buf[8] = button & 0xF; + buf[9] = 0; // key2_low low byte + Self::second_stage_xor_encrypt(&mut buf); + Self::calculate_checksum(&mut buf); + buf[8] = (buf[8] & 0x0F) | (buf[11] & 0xF0); + buf[13] = buf[9] ^ buf[8]; + // buf[0]/buf[1] preamble bytes (no original key material → derive from data). + buf[0] = buf[2] ^ buf[6]; + buf[1] = buf[3] ^ buf[7]; + + let key1_high = + ((buf[0] as u32) << 24) | ((buf[1] as u32) << 16) | ((buf[2] as u32) << 8) | buf[3] as u32; + let key1_low = + ((buf[4] as u32) << 24) | ((buf[5] as u32) << 16) | ((buf[6] as u32) << 8) | buf[7] as u32; + let validation = ((buf[8] as u16) << 8) | buf[9] as u16; + (key1_high, key1_low, validation) + } + + /// Map KAT's generic button command to a PSA2 button code (Lock=0, Unlock=1, Trunk=2). + fn map_button(button: u8) -> u8 { + match button { + 0x01 => 0x0, // Lock + 0x02 => 0x1, // Unlock + 0x04 => 0x2, // Trunk + 0x08 => 0x2, // Panic → Trunk (PSA2 has no panic code) + b => b & 0x0F, + } + } + + /// Finalize a collected 80-bit frame: latch validation/key2, run the cheap mode23 XOR gate, + /// emit only on successful decryption. NEVER runs the brute force. + /// + /// The C feed (`subghz_protocol_decoder_psa2_feed`) also emits on a bare + /// `(validation_field & 0xF) == 0xA` even when the XOR decrypt fails — but that path yields a + /// raw, *undecrypted* frame (no serial/button/counter) that the firmware UI keeps so the user + /// can later run the brute-force button. KAT's pipeline feeds every frequency-compatible + /// decoder and reports the first that fires, so a 1/16-probability nibble match with no fields + /// is indistinguishable from noise and steals unrelated 250/500µs frames (e.g. VAG at 434 MHz). + /// We therefore gate emission strictly on a successful, field-bearing decrypt — the same path + /// that produces the genuine GROUPE PSA decodes — which is the meaningful half of the C gate. + fn finalize_frame(&mut self) -> Option { + // C: validation_field = decode_data_low & 0xFFFF; key2_low = decode_data_low. + self.validation_field = (self.data_low & 0xFFFF) as u16; + self.key2_low = self.data_low; + self.key2_high = self.data_high; + + // C key2_low for decrypt is the 16-bit validation word in the low position. + let decrypt = + Self::direct_xor_decrypt(self.key1_low, self.key1_high, self.validation_field as u32); + + // Reset collection regardless (the C feed always rewinds to State0 after a frame attempt). + self.data_low = 0; + self.data_high = 0; + self.bit_count = 0; + self.state = DecoderState::WaitEdge; + + let (serial, button, counter, _crc, _type) = decrypt?; + let _ = (DECRYPTED_OK, VALID_NIBBLE); // C markers documented; emission gated on decrypt. + let data = ((self.key1_high as u64) << 32) | self.key1_low as u64; + + Some(DecodedSignal { + serial: Some(serial), + button: Some(button), + counter: Some(counter as u16), + crc_valid: true, + data, + data_count_bit: MIN_COUNT_BIT, + encoder_capable: true, + extra: None, + protocol_display_name: None, + }) + } +} + +impl ProtocolDecoder for Psa2Decoder { + fn name(&self) -> &'static str { + "PSA2" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: TE_SHORT, + te_long: TE_LONG, + te_delta: TE_DELTA, + min_count_bit: MIN_COUNT_BIT, + } + } + + fn supported_frequencies(&self) -> &[u32] { + &[433_920_000] + } + + fn reset(&mut self) { + self.state = DecoderState::WaitEdge; + self.prev_duration = 0; + self.manchester_state = ManchesterState::Mid1; + self.pattern_counter = 0; + self.data_low = 0; + self.data_high = 0; + self.bit_count = 0; + self.key1_low = 0; + self.key1_high = 0; + self.validation_field = 0; + self.key2_low = 0; + self.key2_high = 0; + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.state { + // State0: detect preamble pattern type. + DecoderState::WaitEdge => { + if !level { + return None; + } + self.init_preamble_state(); + self.prev_duration = duration; + if Self::near(duration, TE_SHORT, TE_DELTA) { + self.state = DecoderState::CountPattern250; + } else if Self::near(duration, TE_SHORT_HALF, TOL_HALF) { + self.state = DecoderState::CountPattern125; + } + } + + // State1: count standard-rate (250µs) preamble pulses. + DecoderState::CountPattern250 => { + if level { + return None; + } + if Self::near(duration, TE_SHORT, TE_DELTA) { + if Self::near(self.prev_duration, TE_SHORT, TE_DELTA) { + self.pattern_counter += 1; + } + self.prev_duration = duration; + return None; + } + if Self::near(duration, TE_LONG, TE_DELTA) { + if self.pattern_counter > PATTERN_THRESHOLD_1 { + self.data_low = 0; + self.data_high = 0; + self.bit_count = 0; + self.manchester_reset(); + self.state = DecoderState::DecodeManchester250; + } + self.pattern_counter = 0; + self.prev_duration = duration; + return None; + } + self.state = DecoderState::WaitEdge; + self.pattern_counter = 0; + } + + // State2: receive key1 + key2/validation at standard rate. + DecoderState::DecodeManchester250 => { + if self.bit_count >= MAX_BITS { + self.state = DecoderState::WaitEdge; + return None; + } + // End-of-packet detection at KEY2_BITS. + if level && self.bit_count == KEY2_BITS && Self::near(duration, TE_END_1000, 199) { + return self.finalize_frame(); + } + + let event: Option; + if Self::near(duration, TE_SHORT, TE_DELTA) { + event = Some(((level as u8 ^ 1) & 0x7F) << 1); + } else if Self::near(duration, TE_LONG, TE_DELTA) { + event = Some(if level { 4 } else { 6 }); + } else { + // Out-of-range low pulse: secondary end marker when 80 bits collected. The C + // also gates this on a (stale) nibble==0xA; emission is now decided in + // finalize_frame (decrypt-or-reject), so we trigger on the end geometry alone. + if !level && Self::near(duration, TE_END_1000, 199) && self.bit_count == KEY2_BITS + { + return self.finalize_frame(); + } + return None; + } + + if let Some(ev) = event { + if self.bit_count < KEY2_BITS { + if let Some(bit) = self.manchester_advance(ev) { + self.add_bit(bit); + } + } + } + self.prev_duration = duration; + } + + // State3: count half-rate (125µs) preamble pulses. + DecoderState::CountPattern125 => { + if level { + return None; + } + if Self::near(duration, TE_SHORT_HALF, TOL_HALF) { + if Self::near(self.prev_duration, TE_SHORT_HALF, TOL_HALF) { + self.pattern_counter += 1; + } else { + self.pattern_counter = 0; + } + self.prev_duration = duration; + return None; + } + if (TE_LONG_HALF..0x12C).contains(&duration) { + if self.pattern_counter > PATTERN_THRESHOLD_2 { + self.data_low = 0; + self.data_high = 0; + self.bit_count = 0; + self.manchester_reset(); + self.state = DecoderState::DecodeManchester125; + } + self.pattern_counter = 0; + self.prev_duration = duration; + return None; + } + self.state = DecoderState::WaitEdge; + } + + // State4: receive key1 + key2/validation at half rate. + DecoderState::DecodeManchester125 => { + if self.bit_count >= MAX_BITS { + self.state = DecoderState::WaitEdge; + return None; + } + if !level { + let event: Option = if Self::near(duration, TE_SHORT_HALF, TOL_HALF) { + Some(((level as u8 ^ 1) & 0x7F) << 1) + } else if (TE_LONG_HALF..0x12C).contains(&duration) { + Some(if level { 4 } else { 6 }) + } else { + None + }; + if let Some(ev) = event { + if let Some(bit) = self.manchester_advance(ev) { + self.add_bit(bit); + } + } else { + return None; + } + } else { + // Rising edge: end-of-packet at 500µs. + if Self::near(duration, TE_END_500, 99) { + if self.bit_count != KEY2_BITS { + return None; + } + return self.finalize_frame(); + } + } + self.prev_duration = duration; + } + } + None + } + + fn supports_encoding(&self) -> bool { + true + } + + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + let serial = decoded.serial?; + // Increment counter on TX (matches FUN_080299a0 rolling-counter increment). + let counter = decoded.counter.unwrap_or(0).wrapping_add(1); + + let (key1_high, key1_low, validation) = + Self::encode_mode23(serial, Self::map_button(button), counter); + + // mode23 timings: te=250µs, sync long=500µs, end=1000µs. + let te = TE_SHORT; + let te_long_sync = TE_LONG; + let end_dur = TE_END_1000; + + let mut signal = Vec::with_capacity(600); + + // Preamble: 80 pairs of (HIGH te)+(LOW te). + for _ in 0..80 { + signal.push(LevelDuration::new(true, te)); + signal.push(LevelDuration::new(false, te)); + } + + // Sync: (LOW te) + (HIGH te_long) + (LOW te). + signal.push(LevelDuration::new(false, te)); + signal.push(LevelDuration::new(true, te_long_sync)); + signal.push(LevelDuration::new(false, te)); + + // key1: 64 bits MSB-first. bit=1 → (HIGH,LOW); bit=0 → (LOW,HIGH). + let k1 = ((key1_high as u64) << 32) | key1_low as u64; + for bit in (0..64).rev() { + let b = (k1 >> bit) & 1 == 1; + signal.push(LevelDuration::new(b, te)); + signal.push(LevelDuration::new(!b, te)); + } + + // validation_field: 16 bits MSB-first. + for bit in (0..16).rev() { + let b = (validation >> bit) & 1 == 1; + signal.push(LevelDuration::new(b, te)); + signal.push(LevelDuration::new(!b, te)); + } + + // End burst: (HIGH end) + (LOW end). + signal.push(LevelDuration::new(true, end_dur)); + signal.push(LevelDuration::new(false, end_dur)); + + Some(signal) + } +} + +impl Default for Psa2Decoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tea_round_trip() { + // TEA encrypt then decrypt must be the identity for any key. + let key = BF1_KEY_SCHEDULE; + let (mut v0, mut v1) = (0x1234_5678u32, 0x9ABC_DEF0u32); + let (o0, o1) = (v0, v1); + Psa2Decoder::tea_encrypt(&mut v0, &mut v1, &key); + Psa2Decoder::tea_decrypt(&mut v0, &mut v1, &key); + assert_eq!((v0, v1), (o0, o1), "TEA encrypt/decrypt not invertible"); + } + + #[test] + fn mode23_encode_decode_round_trip() { + // Encode (serial,button,counter) → key1/validation, then run the mode23 XOR decrypt and + // confirm the fields (and a passing nibble checksum) come back exactly. Validates TEA-free + // mode23 path: second_stage_xor + nibble checksum + field packing. + for &(serial, btn, cnt) in &[ + (0x99EB25u32, 0u8, 0x039Bu16), + (0x123456, 2, 0x0042), + (0xABCDEF, 1, 0x1234), + (0x0000FF, 0, 0x0001), + ] { + let (k1h, k1l, vf) = Psa2Decoder::encode_mode23(serial, btn, cnt); + let decrypt = Psa2Decoder::direct_xor_decrypt(k1l, k1h, vf as u32); + assert!( + decrypt.is_some(), + "mode23 XOR decrypt failed to validate for serial={serial:06X} btn={btn} cnt={cnt:04X}" + ); + let (ds, db, dc, _crc, ty) = decrypt.unwrap(); + assert_eq!(ds, serial, "serial mismatch"); + assert_eq!(db, btn, "button mismatch"); + assert_eq!(dc, cnt as u32, "counter mismatch"); + assert_eq!(ty, MODE_23, "mode mismatch"); + } + } + + #[test] + fn checksum_matches_reference() { + // Spot-check the nibble checksum against the hand-computed C formula. + let mut buf = [0u8; 48]; + for (i, b) in buf.iter_mut().enumerate().take(8).skip(2) { + *b = (i as u8) * 0x11; // 0x22,0x33,0x44,0x55,0x66,0x77 + } + Psa2Decoder::calculate_checksum(&mut buf); + // bytes 0x22,0x33,0x44,0x55,0x66,0x77 → nibble sum = (2+2)+(3+3)+(4+4)+(5+5)+(6+6)+(7+7) = 54 + let expected = ((54u32 * 0x10) & 0xFF) as u8; // 54*16 = 864 = 0x360 → 0x60 + assert_eq!(buf[11], expected); + } + + #[test] + fn encode_emits_manchester_frame() { + // The encoder produces a non-trivial 250/500µs Manchester upload. + let dec = Psa2Decoder::new(); + let decoded = DecodedSignal { + serial: Some(0x99EB25), + button: Some(0x01), + counter: Some(0x039A), + crc_valid: true, + data: 0, + data_count_bit: MIN_COUNT_BIT, + encoder_capable: true, + extra: None, + protocol_display_name: None, + }; + let sig = dec.encode(&decoded, 0x01).expect("encode should succeed"); + // 80 preamble pairs (160) + 3 sync + 64*2 + 16*2 + 2 end = 160+3+128+32+2 = 325. + assert_eq!(sig.len(), 325, "unexpected upload length"); + assert!(sig.iter().all(|p| p.duration_us > 0)); + } +} diff --git a/src/protocols/toyota.rs b/src/protocols/toyota.rs new file mode 100644 index 0000000..6e38736 --- /dev/null +++ b/src/protocols/toyota.rs @@ -0,0 +1,579 @@ +//! Toyota / Lexus KeeLoq protocol decoder (dual variant) +//! +//! Ported from Flipper-ARF reference: `lib/subghz/protocols/toyota.c` and `toyota.h`. +//! Decode-only — the reference `encoder` field is NULL. +//! +//! Two variants, detected from the first HIGH pulse width (threshold 310µs): +//! +//! - **Variant A** — Corolla / 433.92 MHz. PWM pairs: te_short=400µs, te_long=800µs, +//! delta=175µs. LS (long HIGH + short LOW) = bit 0, SL (short HIGH + long LOW) = bit 1. +//! Preamble = repeated short-short (SS) pairs; first non-SS pair is the first data bit. +//! Frame = 68 bits; min_count_bit = 60. +//! - **Variant B** — Tundra / 315 MHz. NRZ: each individual pulse encodes one bit by a +//! midpoint classifier (`<= 287µs` -> 0, `> 287µs` -> 1). Preamble = short-HIGH / +//! long-LOW pairs (te_short=200µs, te_long=390µs, delta=120µs) terminated by a sync gap +//! (LOW between 1500 and 2600µs). Frame = 67 bits; min_count_bit = 60. +//! +//! KeeLoq hopping: the hop field is left encrypted (the reference does not decrypt or run a +//! CRC). `data` layout matches the reference `generic.data`: +//! `(hop << 32) | (serial << 4) | button`, with hop=32 bits, serial=28 bits, button=4 bits. +//! +//! Emission is gated tightly (exact frame bit count + structural preamble/sync + non-zero +//! serial) so it does not false-match the other KeeLoq-PWM protocols (Kia V3/V4, Subaru, +//! Suzuki, etc.). The shared 433 MHz KeeLoq-PWM air encoding means a Toyota Variant-A frame +//! that also satisfies Kia V3/V4's 68-bit/CRC4 structure is claimed by Kia V3/V4 first (it is +//! earlier in the registry); Toyota uniquely claims 60-bit frames and Variant-B NRZ at 315 MHz. + +use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use crate::radio::demodulator::LevelDuration; +use crate::duration_diff; + +// Variant A physical constants (Corolla / 433 MHz) +const A_TE_SHORT: u32 = 400; +const A_TE_LONG: u32 = 800; +const A_TE_DELTA: u32 = 175; + +// Variant B physical constants (Tundra / 315 MHz, preamble classification only) +const B_TE_SHORT: u32 = 200; +const B_TE_LONG: u32 = 390; +const B_TE_DELTA: u32 = 120; + +const MIN_COUNT_BIT: usize = 60; + +/// NRZ midpoint for Variant B data pulses (`<= 287` -> 0, `> 287` -> 1). +const B_NRZ_MIDPOINT: u32 = 287; + +/// Sync gap (LOW) separating preamble from data in Variant B. +const B_SYNC_GAP_MIN: u32 = 1500; +const B_SYNC_GAP_MAX: u32 = 2600; + +/// Minimum preamble pairs before a frame is accepted. +const A_PREAMBLE_MIN: u16 = 6; +const B_PREAMBLE_MIN: u16 = 6; + +/// Frame lengths in bits. +const A_BITS: usize = 68; +const B_BITS: usize = 67; + +/// First HIGH duration below this -> Variant B, at or above -> Variant A. +const VARIANT_THRESH: u32 = 310; + +#[inline] +fn a_is_short(d: u32) -> bool { + duration_diff!(d, A_TE_SHORT) < A_TE_DELTA +} +#[inline] +fn a_is_long(d: u32) -> bool { + duration_diff!(d, A_TE_LONG) < A_TE_DELTA +} +#[inline] +fn b_is_short(d: u32) -> bool { + duration_diff!(d, B_TE_SHORT) < B_TE_DELTA +} +#[inline] +fn b_is_long(d: u32) -> bool { + duration_diff!(d, B_TE_LONG) < B_TE_DELTA +} + +/// Decoder steps (matches ToyotaDecoderStep in toyota.c). +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + PreambleA, + DataA, + PreambleB, + DataB, +} + +/// Toyota / Lexus protocol decoder (matches SubGhzProtocolDecoderToyota). +pub struct ToyotaDecoder { + step: DecoderStep, + /// 128-bit shift accumulator (hi:lo), matching the C `bits_hi`/`bits_lo`. + bits_hi: u64, + bits_lo: u64, + bit_count: usize, + te_last: u32, + have_high: bool, + preamble_count: u16, + /// 0 = Variant A (Corolla / 433 MHz), 1 = Variant B (Tundra / 315 MHz). + variant: u8, +} + +impl ToyotaDecoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + bits_hi: 0, + bits_lo: 0, + bit_count: 0, + te_last: 0, + have_high: false, + preamble_count: 0, + variant: 0, + } + } + + /// Reset the parse state. Matches `subghz_protocol_decoder_toyota_reset`, which intentionally + /// does NOT clear `variant` (detected once per session); the variant is re-detected on the next + /// pulse from the Reset step anyway. + fn reset_state(&mut self) { + self.step = DecoderStep::Reset; + self.bits_hi = 0; + self.bits_lo = 0; + self.bit_count = 0; + self.te_last = 0; + self.have_high = false; + self.preamble_count = 0; + } + + /// Push one bit into the 128-bit accumulator (matches `toyota_push_bit`). + fn push_bit(&mut self, bit: u8) { + let carry = (self.bits_lo >> 63) & 1; + self.bits_hi = (self.bits_hi << 1) | carry; + self.bits_lo = (self.bits_lo << 1) | (bit as u64 & 1); + self.bit_count += 1; + } + + /// Extract `length` bits at `offset` from the end of the accumulator (matches `toyota_extract`). + fn extract(&self, offset: usize, length: usize) -> u32 { + let mut result: u32 = 0; + let total = self.bit_count as isize; + for i in 0..length { + let pos = (total - 1) - (offset as isize + i as isize); + let b = if pos >= 64 { + ((self.bits_hi >> (pos - 64)) & 1) as u32 + } else if pos >= 0 { + ((self.bits_lo >> pos) & 1) as u32 + } else { + 0 + }; + result = (result << 1) | b; + } + result + } + + /// Build the decoded signal once a full frame is collected (matches `toyota_decode_and_fire`). + /// Returns `None` if the structural gate (bit count / serial) fails. + fn decode_and_fire(&self) -> Option { + if self.bit_count < MIN_COUNT_BIT { + return None; + } + + let hop = self.extract(0, 32); + let serial = self.extract(32, 28); + let button = self.extract(60, 4) as u8; + + // Tight gate: require a non-zero serial so a run of all-zero/garbage bits that happens to + // reach the bit count cannot emit a Toyota frame. + if serial == 0 { + return None; + } + + // generic.data = (hop << 32) | (serial << 4) | button + let data = ((hop as u64) << 32) | ((serial as u64) << 4) | (button as u64 & 0x0F); + + Some(DecodedSignal { + serial: Some(serial), + button: Some(button), + // KeeLoq hop is left encrypted (no key / no decrypt in the reference). + counter: None, + // No CRC in the reference; a fully-structured frame of the exact bit count is the + // validity criterion (the callback only fires at min_count_bit). + crc_valid: true, + data, + data_count_bit: self.bit_count, + encoder_capable: false, + extra: None, + protocol_display_name: None, + }) + } + + /// Feed for Variant A (Corolla / 433 MHz) — PWM pair encoding (matches `toyota_feed_variant_a`). + /// + /// Faithful to the reference's gap-terminated emission path. The reference ALSO self-fires the + /// instant `bit_count` reaches 68; that is intentionally dropped here. Variant A is the same + /// KeeLoq-PWM air protocol as Kia V3/V4, and the early self-fire (on the normal short LOW that + /// completes bit 68) lands several pulses BEFORE Kia V3/V4's sync-terminated fire — which, in + /// KAT's "first decoder to fire on a pulse wins" stream, would let Toyota steal every shared + /// 68-bit frame from Kia. By emitting only on the terminating gap (the same pulse Kia fires on), + /// the registry order (Kia earlier) resolves the shared frames in Kia's favour, while Toyota + /// still uniquely claims 60–67-bit Variant-A frames that Kia rejects. Bit accumulation is capped + /// at 68 so the field layout is preserved regardless of trailing repeats. + fn feed_variant_a(&mut self, level: bool, duration: u32) -> Option { + match self.step { + DecoderStep::PreambleA => { + if level { + self.te_last = duration; + self.have_high = true; + return None; + } + + if !self.have_high { + self.reset_state(); + return None; + } + self.have_high = false; + + let hs = a_is_short(self.te_last); + let hl = a_is_long(self.te_last); + let ls = a_is_short(duration); + let ll = a_is_long(duration); + + if hs && ls { + self.preamble_count += 1; + return None; + } + + if self.preamble_count < A_PREAMBLE_MIN { + self.reset_state(); + return None; + } + + self.bits_hi = 0; + self.bits_lo = 0; + self.bit_count = 0; + + if hl && ls { + self.push_bit(0); + } else if hs && ll { + self.push_bit(1); + } + + self.step = DecoderStep::DataA; + None + } + + DecoderStep::DataA => { + if level { + if a_is_short(duration) || a_is_long(duration) { + self.te_last = duration; + self.have_high = true; + } else { + // Terminating gap / out-of-range HIGH: emit (deferring to Kia on shared + // 68-bit frames via registry order — see fn doc-comment). + let result = if self.bit_count >= MIN_COUNT_BIT { + self.decode_and_fire() + } else { + None + }; + self.reset_state(); + return result; + } + return None; + } + + if !self.have_high { + return None; + } + self.have_high = false; + + // Cap accumulation at A_BITS: once a full 68-bit frame is collected, stop pushing + // more bits and wait for the terminating gap. This keeps the field layout fixed and + // makes emission coincide with Kia V3/V4's sync-terminated fire so Kia wins shared + // frames by registry order. + if self.bit_count >= A_BITS { + return None; + } + + let hs = a_is_short(self.te_last); + let hl = a_is_long(self.te_last); + let ls = a_is_short(duration); + let ll = a_is_long(duration); + + if hl && ls { + self.push_bit(0); + } else if hs && ll { + self.push_bit(1); + } else { + let result = if self.bit_count >= MIN_COUNT_BIT { + self.decode_and_fire() + } else { + None + }; + self.reset_state(); + return result; + } + None + } + + _ => None, + } + } + + /// Feed for Variant B (Tundra / 315 MHz) — NRZ encoding (matches `toyota_feed_variant_b`). + fn feed_variant_b(&mut self, level: bool, duration: u32) -> Option { + match self.step { + DecoderStep::PreambleB => { + if level { + if b_is_short(duration) { + self.te_last = duration; + self.have_high = true; + } else { + self.reset_state(); + } + return None; + } + + // Falling edge + if !self.have_high { + self.reset_state(); + return None; + } + self.have_high = false; + + // Sync gap: LOW ~1938µs -> transition to data + if duration >= B_SYNC_GAP_MIN && duration <= B_SYNC_GAP_MAX { + if self.preamble_count >= B_PREAMBLE_MIN { + self.bits_hi = 0; + self.bits_lo = 0; + self.bit_count = 0; + self.have_high = false; + self.step = DecoderStep::DataB; + } else { + self.reset_state(); + } + return None; + } + + // Normal preamble LOW must be LONG + if b_is_long(duration) { + self.preamble_count += 1; + return None; + } + + self.reset_state(); + None + } + + DecoderStep::DataB => { + // Every pulse (HIGH or LOW) encodes one bit. A pulse >= sync-gap min ends the frame. + if duration >= B_SYNC_GAP_MIN { + let result = if self.bit_count >= MIN_COUNT_BIT { + self.decode_and_fire() + } else { + None + }; + self.reset_state(); + return result; + } + + let bit = if duration > B_NRZ_MIDPOINT { 1 } else { 0 }; + self.push_bit(bit); + + if self.bit_count >= B_BITS { + let result = self.decode_and_fire(); + self.reset_state(); + return result; + } + None + } + + _ => None, + } + } +} + +impl ProtocolDecoder for ToyotaDecoder { + fn name(&self) -> &'static str { + "Toyota" + } + + fn timing(&self) -> ProtocolTiming { + ProtocolTiming { + te_short: A_TE_SHORT, + te_long: A_TE_LONG, + te_delta: A_TE_DELTA, + min_count_bit: MIN_COUNT_BIT, + } + } + + fn supported_frequencies(&self) -> &[u32] { + // Variant A 433.92 MHz, Variant B 315 MHz. + &[433_920_000, 315_000_000] + } + + fn reset(&mut self) { + self.reset_state(); + // Full reset between segments: also clear the detected variant. + self.variant = 0; + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + if self.step == DecoderStep::Reset { + if !level { + return None; + } + + // Variant detection from the first SHORT HIGH pulse: + // < 310µs -> Variant B (Tundra 315 MHz, te_short ~200µs) + // >= 310µs -> Variant A (Corolla 433 MHz, te_short ~400µs) + let fits_b = b_is_short(duration) && duration < VARIANT_THRESH; + let fits_a = a_is_short(duration) && duration >= VARIANT_THRESH; + + if fits_b { + self.variant = 1; + self.te_last = duration; + self.have_high = true; + self.preamble_count = 0; + self.step = DecoderStep::PreambleB; + } else if fits_a { + self.variant = 0; + self.te_last = duration; + self.have_high = true; + self.preamble_count = 0; + self.step = DecoderStep::PreambleA; + } + return None; + } + + if self.variant == 1 { + self.feed_variant_b(level, duration) + } else { + self.feed_variant_a(level, duration) + } + } + + fn supports_encoding(&self) -> bool { + false + } + + fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { + None + } +} + +impl Default for ToyotaDecoder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Build a Variant A (PWM) frame: preamble SS pairs, then `bits` MSB-first. + /// LS (long HIGH + short LOW) = 0, SL (short HIGH + long LOW) = 1. Terminated by a long gap. + fn build_variant_a(bits: &[u8]) -> Vec { + let mut v = Vec::new(); + // 8 short-short preamble pairs (>= A_PREAMBLE_MIN = 6). + for _ in 0..8 { + v.push(LevelDuration::new(true, A_TE_SHORT)); + v.push(LevelDuration::new(false, A_TE_SHORT)); + } + for &b in bits { + if b == 0 { + v.push(LevelDuration::new(true, A_TE_LONG)); + v.push(LevelDuration::new(false, A_TE_SHORT)); + } else { + v.push(LevelDuration::new(true, A_TE_SHORT)); + v.push(LevelDuration::new(false, A_TE_LONG)); + } + } + // Trailing HIGH gap (out of TE range) flushes the frame. + v.push(LevelDuration::new(true, 5000)); + v + } + + /// Build a Variant B (NRZ) frame: short-HIGH/long-LOW preamble pairs, sync gap, then each bit + /// as one pulse (short = 0, long = 1), terminated by a sync-gap-length pulse. + fn build_variant_b(bits: &[u8]) -> Vec { + let mut v = Vec::new(); + // 8 preamble pairs: short HIGH (~200µs) + long LOW (~390µs). + for _ in 0..8 { + v.push(LevelDuration::new(true, B_TE_SHORT)); + v.push(LevelDuration::new(false, B_TE_LONG)); + } + // Last preamble HIGH, then the sync-gap LOW. + v.push(LevelDuration::new(true, B_TE_SHORT)); + v.push(LevelDuration::new(false, 1938)); + // Data: each bit one pulse, alternating level (polarity is ignored by the NRZ classifier). + for (i, &b) in bits.iter().enumerate() { + let level = i % 2 == 0; + let dur = if b == 1 { 380 } else { 200 }; + v.push(LevelDuration::new(level, dur)); + } + // End-of-frame gap. + v.push(LevelDuration::new(false, 2000)); + v + } + + fn feed_all(dec: &mut ToyotaDecoder, pairs: &[LevelDuration]) -> Option { + for p in pairs { + if let Some(sig) = dec.feed(p.level, p.duration_us) { + return Some(sig); + } + } + None + } + + #[test] + fn variant_a_synthetic_decodes() { + // 68-bit frame: a recognizable pattern with a non-zero serial and a known button nibble. + // bits[0..32] = hop, bits[32..60] = serial, bits[60..64] = button, bits[64..68] = padding. + let mut bits = vec![0u8; A_BITS]; + // hop = 0x9ABCDEF0 (MSB first in bits[0..32]) + let hop: u32 = 0x9ABC_DEF0; + for i in 0..32 { + bits[i] = ((hop >> (31 - i)) & 1) as u8; + } + // serial = 0x0123456 (28 bits) in bits[32..60] + let serial: u32 = 0x012_3456; + for i in 0..28 { + bits[32 + i] = ((serial >> (27 - i)) & 1) as u8; + } + // button = 0x8 (Lock) in bits[60..64] + let button: u8 = 0x8; + for i in 0..4 { + bits[60 + i] = (button >> (3 - i)) & 1; + } + // bits[64..68] padding = 0 + + let frame = build_variant_a(&bits); + let mut dec = ToyotaDecoder::new(); + let sig = feed_all(&mut dec, &frame).expect("variant A frame should decode"); + + assert_eq!(sig.data_count_bit, A_BITS); + assert_eq!(sig.serial, Some(serial)); + assert_eq!(sig.button, Some(button)); + // hop is the top 32 bits of data + assert_eq!((sig.data >> 32) as u32, hop); + assert!(sig.crc_valid); + } + + #[test] + fn variant_b_synthetic_decodes() { + // 67-bit NRZ frame. + let mut bits = vec![0u8; B_BITS]; + let hop: u32 = 0x1357_9BDF; + for i in 0..32 { + bits[i] = ((hop >> (31 - i)) & 1) as u8; + } + let serial: u32 = 0x0AB_CDEF; + for i in 0..28 { + bits[32 + i] = ((serial >> (27 - i)) & 1) as u8; + } + // button (bits[60..64]); only 3 bits of button fit before the 67-bit end (bits[60..63]), + // bit 63..67 truncated — extract(60,4) reads bits[63..67] from the END. Just assert serial. + for i in 0..7 { + bits[60 + i] = if i % 2 == 0 { 1 } else { 0 }; + } + + let frame = build_variant_b(&bits); + let mut dec = ToyotaDecoder::new(); + let sig = feed_all(&mut dec, &frame).expect("variant B frame should decode"); + + assert_eq!(sig.data_count_bit, B_BITS); + assert_eq!(sig.serial, Some(serial)); + assert!(sig.crc_valid); + } + + #[test] + fn rejects_all_zero_serial() { + // A structurally valid 68-bit frame with an all-zero serial must NOT emit (tight gate). + let bits = vec![0u8; A_BITS]; + let frame = build_variant_a(&bits); + let mut dec = ToyotaDecoder::new(); + assert!(feed_all(&mut dec, &frame).is_none()); + } +}