From 48ee413c9bf49d92f9d07578360db1b41707ab2b Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Sun, 22 Mar 2026 12:05:01 -0400 Subject: [PATCH] v1.2.0: Add 4 new protocols, fix 5 existing, port Kia V6 encoder New protocols (from ProtoPirate): - Mazda V0: 433MHz, pair-based decoding, XOR deobfuscation - Mitsubishi V0: 868MHz, PWM, bit negation + XOR unscrambling - Porsche Touareg: 433/868MHz, sync preamble, 24-bit rotate cipher - Fiat V1 (Magneti Marelli BSI): 433MHz, auto-detected timing variants Protocol fixes aligned with ProtoPirate reference: - Kia V1: Fix Manchester level mapping (inverted convention), off-by-one bit count, CRC4 simplified to 7 bytes + offset 1 - Kia V2: Fix CRC byte layout (was double-counting nibbles), off-by-one bit count, preamble short-HIGH handling - Fiat V0: Fix endbyte transform (remove (<<1)|1), standard Manchester encoder (was differential), 7 btn bits (was 6), gap path fallback - PSA: Fix modified TEA (XTEA-like dynamic key selection), XOR decrypt byte mapping, critical key2_low construction bug, encoder polarity and preamble, add key1_high nibble validation, dual preamble patterns - Ford V0: Add calculate_checksum (sums all serial+count bytes) for encoder Kia V6 encoder ported: - Forward AES-128 (SubBytes, ShiftRows, MixColumns, encrypt) - encrypt_payload: build plaintext, AES encrypt, pack into 3 parts - Two-pass Manchester upload (640 + 38 preamble pairs) - fx_field extraction stored in DecodedSignal.extra for encode roundtrip Updated README, protocol docs, and version bump to 1.2.0. --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 19 +- docs/README.md | 4 + docs/fiat_v1.md | 60 ++++ docs/kia_v1.md | 4 +- docs/kia_v6.md | 4 +- docs/mazda_v0.md | 73 +++++ docs/mitsubishi_v0.md | 59 ++++ docs/porsche_touareg.md | 72 +++++ docs/psa.md | 67 +++-- src/protocols/fiat_v0.rs | 122 ++++---- src/protocols/fiat_v1.rs | 435 +++++++++++++++++++++++++++ src/protocols/ford_v0.rs | 33 +- src/protocols/kia_v1.rs | 48 ++- src/protocols/kia_v2.rs | 31 +- src/protocols/kia_v6.rs | 204 ++++++++++++- src/protocols/mazda_v0.rs | 312 +++++++++++++++++++ src/protocols/mitsubishi_v0.rs | 244 +++++++++++++++ src/protocols/mod.rs | 8 + src/protocols/porsche_touareg.rs | 321 ++++++++++++++++++++ src/protocols/psa.rs | 499 ++++++++++++++++++++++--------- 22 files changed, 2318 insertions(+), 305 deletions(-) create mode 100644 docs/fiat_v1.md create mode 100644 docs/mazda_v0.md create mode 100644 docs/mitsubishi_v0.md create mode 100644 docs/porsche_touareg.md create mode 100644 src/protocols/fiat_v1.rs create mode 100644 src/protocols/mazda_v0.rs create mode 100644 src/protocols/mitsubishi_v0.rs create mode 100644 src/protocols/porsche_touareg.rs diff --git a/Cargo.lock b/Cargo.lock index c0b866f..215bd82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,7 +444,7 @@ dependencies = [ [[package]] name = "kat" -version = "1.1.3" +version = "1.2.0" dependencies = [ "anyhow", "atty", diff --git a/Cargo.toml b/Cargo.toml index 00eb8f7..68590d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kat" -version = "1.1.3" +version = "1.2.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 d3fa415..a4efea3 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** — 14 protocol decoders: Kia V0–V6, Ford V0, Fiat V0, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions +- **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 - **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 @@ -233,15 +233,19 @@ Protocol behavior and RF modulation (AM/FM) follow the ProtoPirate reference. KA | Kia V2 | Manchester | FM | Fixed Code | 315 / 433.92 MHz | | Kia V3/V4 | PWM | AM/FM | KeeLoq | 315 / 433.92 MHz | | Kia V5 | Manchester | FM | Fixed Code | 433.92 MHz | -| Kia V6 | Manchester | FM | Fixed Code | 433.92 MHz | -| Ford V0 | Manchester | FM | Fixed Code | 433.92 MHz | +| Kia V6 | Manchester | FM | AES-128 | 433.92 MHz | +| Ford V0 | Manchester | FM | Rolling Code | 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 | +| Mitsubishi V0 | PWM | FM | Bit Negation + XOR | 868.35 MHz | +| Porsche Touareg | PWM | AM | Rotation Cipher | 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 (decode + encode) | +| 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 | XTEA/XOR | 433.92 MHz | +| PSA (Peugeot/Citroën) | Manchester | FM | Modified TEA/XOR | 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). @@ -262,6 +266,8 @@ Details (exact model/year scope) are in `src/vuln_db.rs`. The app shows the NVD ### Cryptographic modules - **KeeLoq** — encrypt/decrypt with normal, secure, FAAC, and magic serial/XOR learning key derivation (keeloq_common, keys). Unknown signals are tried as KeeLoq with every keystore key via **keeloq_generic** (uses keeloq_common only). +- **AES-128** — full encrypt/decrypt for Kia V6 (key derived from keystore A/B with XOR masks) +- **Modified TEA** — XTEA-like cipher with dynamic key selection for PSA (Peugeot/Citroën) - **AUT64** — 12-round block cipher for VAG type 1/3/4 (aut64) - **Keystore** — manufacturer keys (Kia, VAG, etc.) built in; see `src/keystore/` @@ -302,7 +308,8 @@ src/ │ ├── aut64.rs # AUT64 block cipher (VAG) │ ├── keys.rs # Key loading (embedded + optional file), KIA/VAG │ ├── kia_v0..kia_v6.rs -│ ├── ford_v0.rs, fiat_v0.rs, subaru.rs, suzuki.rs +│ ├── ford_v0.rs, fiat_v0.rs, fiat_v1.rs, subaru.rs, suzuki.rs +│ ├── mazda_v0.rs, mitsubishi_v0.rs, porsche_touareg.rs │ ├── vag.rs # VAG decoder/encoder (4 sub-types; encode from capture via extra) │ ├── scher_khan.rs, star_line.rs, psa.rs │ └── ... diff --git a/docs/README.md b/docs/README.md index 637e826..b5f35bd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,6 +16,10 @@ This folder describes how each keyfob protocol supported by KAT works. Each docu | Subaru | `subaru.rs` | [subaru.md](subaru.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) | +| Mitsubishi V0 | `mitsubishi_v0.rs` | [mitsubishi_v0.md](mitsubishi_v0.md) | +| Porsche Touareg | `porsche_touareg.rs` | [porsche_touareg.md](porsche_touareg.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) | diff --git a/docs/fiat_v1.md b/docs/fiat_v1.md new file mode 100644 index 0000000..446c8e9 --- /dev/null +++ b/docs/fiat_v1.md @@ -0,0 +1,60 @@ +--- +layout: default +--- + +# Fiat V1 Protocol (Magneti Marelli BSI / PCF7946) + +**Rust module:** `src/protocols/fiat_v1.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/fiat_v1.c` + +## Overview + +Fiat V1 is the Magneti Marelli BSI keyfob protocol (PCF7946), found on Fiat Panda, Grande Punto, and possibly other Fiat/Lancia/Alfa ~2003-2012. Uses Manchester encoding with auto-detected timing from preamble pulse averaging. Two timing variants: Type A (~260 us, e.g. Panda) and Type B (~100 us, e.g. Grande Punto). This is a different protocol from Fiat V0. + +## Timing + +| Parameter | Default | Notes | +|-----------|---------|-------| +| Short | 260 us | Auto-detected from preamble | +| Long | 520 us | 2x detected TE | +| Delta | 80 us | Minimum 30 us | +| Preamble | 80+ pulses | 50-350 us range | +| Min bits | 80 | Max 104 bits | + +### Auto-Detection + +Preamble pulses (50-350 us) are accumulated. After 80+ pulses, `te_detected = te_sum / te_count` becomes the reference TE. Type A/B boundary is at 180 us. + +## Frame Layout (80-104 bits = 10-13 bytes) + +| Bytes | Content | +|-------|---------| +| 0-1 | Preamble residue (0xFFFF/0xFFFC) | +| 2-5 | Serial (32 bits) | +| 6 | Button:4 \| Epoch:4 | +| 7 | Counter:5 \| Scramble:2 \| Fixed:1 | +| 8-12 | Encrypted payload (40 bits) | + +## Decoder Steps + +1. **Reset** -- Wait for HIGH pulse in 50-350 us range. Also detect retransmission gaps (>5000 us). +2. **Preamble** -- Count pulses, accumulate for TE averaging. On gap >= te_detected * 4, transition to Sync. +3. **Sync** -- Expect HIGH sync pulse of te_detected * 4 to te_detected * 12 duration. +4. **Data** -- Manchester decode up to 104 bits into 13-byte raw buffer. +5. **RetxSync** -- After gap >5000 us, look for sync pulse 400-2800 us (retransmission). + +## Buttons + +| Code | Name | +|------|--------| +| 0x7 | Lock | +| 0xB | Unlock | +| 0xD | Trunk | + +## Encoder + +Not supported (decode-only). + +## Frequencies + +433.92 MHz. diff --git a/docs/kia_v1.md b/docs/kia_v1.md index c8ccc3a..f00b53f 100644 --- a/docs/kia_v1.md +++ b/docs/kia_v1.md @@ -9,7 +9,7 @@ layout: default ## Overview -Kia V1 uses Manchester encoding at 800/1600 µs. 57 bits total: 32 serial + 8 button + 12 counter + 4 CRC. Long preamble (~90 long pairs). CRC4 with offset rules (cnt_high 0 vs ≥ 6). +Kia V1 uses Manchester encoding at 800/1600 us. 57 bits total: 32 serial + 8 button + 12 counter + 4 CRC. Long preamble (~90 long pairs). CRC4 uses XOR of nibbles over 7 bytes (serial + button + cnt_low + cnt_high) with offset 1. ## Timing @@ -28,7 +28,7 @@ Manchester: symbol duration short or long; bit value from transition direction. - 32 bits: serial - 8 bits: button - 12 bits: counter -- 4 bits: CRC4 (checksum with offset rules) +- 4 bits: CRC4 (XOR of nibbles, 7 bytes, offset 1) ## Decoder Steps diff --git a/docs/kia_v6.md b/docs/kia_v6.md index 9fd5d15..dd7b2c3 100644 --- a/docs/kia_v6.md +++ b/docs/kia_v6.md @@ -30,7 +30,7 @@ Manchester (event mapping 0/2/4/6 for level convention). Three 64/64/16-bit segm - Part2: 64 bits (inverted) - Part3: 16 bits (inverted) -AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH). Serial/button/counter and CRC extracted after decryption. +AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH). Serial/button/counter, CRC, and fx_field extracted after decryption. The fx_field is derived from the top 2 bytes of stored_part1_high and stored in `DecodedSignal.extra`. ## Decoder Steps @@ -40,7 +40,7 @@ AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH ## Encoder -Decode-only in KAT (no encoder in reference). +Supported. Ported from ProtoPirate (`ENABLE_EMULATE_FEATURE`). Builds plaintext (fx_field, serial, button, counter, S-box CRC, CRC8), AES-128 encrypts, packs into 3 parts, then Manchester encodes with two-pass preamble (640 pairs + data, gap, 38 pairs + data). Requires fx_field from decoded signal's `extra` field. ## Frequencies diff --git a/docs/mazda_v0.md b/docs/mazda_v0.md new file mode 100644 index 0000000..b7c9e56 --- /dev/null +++ b/docs/mazda_v0.md @@ -0,0 +1,73 @@ +--- +layout: default +--- + +# Mazda V0 Protocol + +**Rust module:** `src/protocols/mazda_v0.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/mazda_v0.c` + +## Overview + +Mazda V0 uses a custom pair-based encoding (not standard Manchester). The `level` parameter is ignored; the decoder processes raw duration pairs. Preamble: 13+ short/short pairs followed by a short+long transition into data. Data bits are collected using a prev_state tracker with inverted polarity, then XOR-deobfuscated, checksum-validated, and parsed. + +## Timing + +| Parameter | Value | Notes | +|-----------|--------|--------------| +| Short | 250 us | +/-100 us | +| Long | 500 us | +/-100 us | +| Min bits | 64 | | +| Completion| 80-105 bits | | + +## Frame Layout (64 bits after deobfuscation) + +Raw data collects into a 14-byte buffer. First byte is discarded (sync); bytes [1..9] form the 8-byte data frame. + +After XOR deobfuscation: +- Bytes 0-3: Serial (32 bits) +- Byte 4: Button (8 bits) +- Bytes 5-6: Counter (16 bits) +- Byte 7: Checksum (additive sum of bytes 0-6) + +## Pair-Based Bit Decoding + +| Pair (te_last, duration) | Action | +|--------------------------|--------| +| Long + Short | Collect bit(0), bit(1), prev_state=1 | +| Short + Long | Collect bit(1), prev_state=0 | +| Short + Short | Collect bit(prev_state) | +| Long + Long | Collect bit(0), bit(1), prev_state=0 | + +Bit collection uses inverted polarity: state_bit=0 stores a 1. + +## XOR Deobfuscation + +1. Compute parity of data[7] (XOR fold to single bit). +2. If parity is odd: XOR bytes [0..6] with data[6] as mask. +3. If parity is even: XOR bytes [0..5] with data[5] as mask, also XOR data[6] with data[5]. +4. Bit interleave swap bytes 5 and 6: swap even/odd bit positions between them. + +## Decoder Steps + +1. **Reset** -- Wait for short pulse; save and go to PreambleCheck. +2. **PreambleSave** -- Save duration, go to PreambleCheck. +3. **PreambleCheck** -- Count short+short pairs; on short+long with count >= 13, start data. +4. **DataSave** -- Save duration, go to DataCheck. +5. **DataCheck** -- Process pair; if valid continue; if invalid, check completion (80-105 bits + checksum). + +## Buttons + +| Code | Name | +|------|--------| +| 0x10 | Lock | +| 0x20 | Unlock | +| 0x40 | Trunk | + +## Encoder + +Not supported (decode-only). + +## Frequencies + +433.92 MHz. diff --git a/docs/mitsubishi_v0.md b/docs/mitsubishi_v0.md new file mode 100644 index 0000000..aaa4468 --- /dev/null +++ b/docs/mitsubishi_v0.md @@ -0,0 +1,59 @@ +--- +layout: default +--- + +# Mitsubishi V0 Protocol + +**Rust module:** `src/protocols/mitsubishi_v0.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/mitsubishi_v0.c` + +## Overview + +Mitsubishi V0 uses PWM encoding at 250/500 us. 96-bit frame (12 bytes). Level-aware: HIGH pulses are saved, LOW pulses complete the pair. After collection, data is unscrambled via bitwise NOT + counter-derived XOR mask. + +## Timing + +| Parameter | Value | Notes | +|-----------|--------|--------------| +| Short | 250 us | +/-100 us | +| Long | 500 us | +/-100 us | +| Min bits | 80 | | +| Frame | 96 bits| 12 bytes | + +## PWM Bit Encoding + +| Pair (HIGH, LOW) | Bit | +|------------------|-----| +| Short HIGH + Long LOW | 1 | +| Long HIGH + Short LOW | 0 | + +Bits collected MSB-first into a 12-byte buffer. + +## Frame Layout (96 bits) + +After unscrambling: +- Bytes 0-3: Serial (32 bits, big-endian) +- Bytes 4-5: Counter (16 bits) +- Byte 6: Button (8 bits) +- Bytes 7-11: Remaining payload + +## Unscramble Algorithm + +1. Bitwise NOT the first 8 bytes. +2. Extract counter from bytes [4..5]: `hi = (counter >> 8) & 0xFF`, `lo = counter & 0xFF`. +3. Compute masks: `mask1 = (hi & 0xAA) | (lo & 0x55)`, `mask2 = (lo & 0xAA) | (hi & 0x55)`, `mask3 = mask1 ^ mask2`. +4. XOR bytes [0..5] with mask3. + +## Decoder Steps + +1. **Reset** -- Wait for HIGH pulse; save duration. +2. **DataSave** -- On HIGH: save duration. On LOW (unexpected): reset. +3. **DataCheck** -- On LOW: decode pair. If 96 bits collected, unscramble and return. Otherwise continue. + +## Encoder + +Not supported (decode-only). + +## Frequencies + +868.35 MHz. diff --git a/docs/porsche_touareg.md b/docs/porsche_touareg.md new file mode 100644 index 0000000..eb9bbe9 --- /dev/null +++ b/docs/porsche_touareg.md @@ -0,0 +1,72 @@ +--- +layout: default +--- + +# Porsche Touareg Protocol + +**Rust module:** `src/protocols/porsche_touareg.rs` +**Reference:** `REFERENCES/ProtoPirate/protocols/porsche_touareg.c` + +## Overview + +Porsche Touareg uses PWM encoding with very long timing (1680/3370 us). 64-bit frame with a sync preamble (at least 15 sync pulses at 3370 us). Counter is recovered via brute-force using a 24-bit rotation cipher. Originally designed for the Porsche Cayenne. + +## Timing + +| Parameter | Value | Notes | +|------------|----------|--------------| +| Short | 1680 us | +/-500 us | +| Long | 3370 us | +/-500 us | +| Sync | 3370 us | Same as long | +| Gap | 5930 us | +/-500 us | +| Sync min | 15 pulses| | +| Min bits | 64 | | + +## PWM Bit Encoding + +| Pair (LOW, HIGH) | Bit | +|------------------|-----| +| Short LOW + Long HIGH | 0 | +| Long LOW + Short HIGH | 1 | + +## Frame Layout (64 bits = 8 bytes) + +| Byte | Content | +|------|---------| +| pkt[0] | (button << 4) \| (frame_type & 0x07) | +| pkt[1] | serial bits [23:16] | +| pkt[2] | serial bits [15:8] | +| pkt[3] | serial bits [7:0] | +| pkt[4..7] | Encrypted counter/rolling code | + +- **Serial**: 24 bits from pkt[1..3] +- **Button**: 4 bits (pkt[0] >> 4) +- **Frame type**: 3 bits (pkt[0] & 0x07): 0x02=First, 0x01=Cont, 0x04=Final + +## Counter Recovery (Brute-Force) + +Counter is not in plaintext. The decoder tries counter values 1-256, calling `compute_frame()` for each, and checks if computed bytes [4..7] match received bytes [4..7]. + +### Compute Frame Algorithm (24-bit Rotate Cipher) + +1. Initialize 24-bit rotate register from serial bytes: r_h=b3, r_m=b1, r_l=b2. +2. ROTATE24: circular left shift across 3 bytes (h<-m, m<-l, l<-h). +3. Rotate 4 times + counter_low more times. +4. Compute encrypted bytes a9a/a9b/a9c from rotated values XOR'd with inverted counter bits. +5. Assemble pkt[4..7] using bitfield packing. + +## Decoder Steps + +1. **Reset** -- Wait for LOW pulse matching sync (3370 us). +2. **Sync** -- Count sync pulses (both HIGH and LOW). When count >= 15 and gap detected (5930 us), transition to GapHigh or GapLow. +3. **GapHigh** -- Expect HIGH gap (5930 us); init data on match. +4. **GapLow** -- Expect LOW gap (5930 us); init data on match. +5. **Data** -- Decode bit pairs (LOW saved, HIGH completes). At 64 bits, parse data and brute-force counter. + +## Encoder + +Not supported (decode-only). + +## Frequencies + +433.92 MHz, 868.35 MHz. diff --git a/docs/psa.md b/docs/psa.md index 60855a1..31a16b4 100644 --- a/docs/psa.md +++ b/docs/psa.md @@ -2,46 +2,75 @@ layout: default --- -# PSA (Peugeot/Citroën) Protocol +# PSA (Peugeot/Citroen) Protocol -**Rust module:** `src/protocols/psa.rs` +**Rust module:** `src/protocols/psa.rs` **Reference:** `REFERENCES/ProtoPirate/protocols/psa.c` ## Overview -PSA uses Manchester at 250/500 µs symbol (125/250 µs sub-symbol for preamble). 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation. TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds an XOR layer. Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule). +PSA uses Manchester encoding with dual preamble pattern support. 128 bits total: key1 (64) + validation (16) + key2/rest (48). Modified TEA (XTEA-like) with dynamic key selection (`key[sum & 3]` and `key[(sum >> 11) & 3]`). Two decode modes: mode 0x23 (direct XOR decrypt with checksum validation) and mode 0x36 (TEA with BF1/BF2 key schedules). Brute-force decryption for mode 0x36 is deferred/partial. ## Timing -| Parameter | Value | Notes | -|------------|--------|---------| -| Symbol short | 250 µs | ±100 µs | -| Symbol long | 500 µs | ±100 µs | -| Preamble | 125/250 µs sub-symbols | | +| Parameter | Value | Notes | +|-------------|--------|---------| +| Symbol short | 250 us | +/-100 us | +| Symbol long | 500 us | +/-100 us | +| Preamble P1 | 250 us sub-symbols | Pattern 1 | +| Preamble P2 | 125 us sub-symbols | Pattern 2 | +| End marker | 1000 us (P1), 500 us (P2) | | | Min bits | 128 | | -## Encoding +## Dual Preamble Patterns -Manchester; preamble uses 125/250 µs; then 250/500 µs symbols. TEA encrypt; mode 0x23 adds XOR. +- **Pattern 1 (250 us):** Preamble uses 250 us sub-symbols; Manchester decode at 250/500 us. Threshold: 70+ transitions. +- **Pattern 2 (125 us):** Preamble uses 125 us sub-symbols; Manchester decode at 125/250 us. Threshold: 69+ transitions. + +Pattern type is auto-detected from the first preamble pulse duration. ## Frame Layout (128 bits) -- key1: 64 bits -- validation: 16 bits -- key2/rest: 48 bits +- key1: 64 bits +- validation: 16 bits +- key2/rest: 48 bits -TEA decrypt key1 (and validation); mode 0x23: XOR with BF1 key schedule; mode 0x36: TEA with BF2 key schedule. Serial/button/counter extracted from decrypted key1. +## Encryption + +### Modified TEA (XTEA-like) + +Uses dynamic key word selection per round: +- Encrypt: `k_idx1 = sum & 3`, then `sum += DELTA`, then `k_idx2 = (sum >> 11) & 3` +- Decrypt: `k_idx2 = (sum >> 11) & 3`, then `sum -= DELTA`, then `k_idx1 = sum & 3` +- Round function: `(key[k_idx] + sum) ^ (((v >> 5) ^ (v << 4)) + v)` + +### Mode 0x23 (Direct XOR) + +1. Setup byte buffer from key1/key2 (little-endian unpack). +2. Calculate checksum over buffer[2..8] (nibble sum * 16). +3. Validate: `(checksum ^ key2_high_byte) & 0xF0 == 0`. +4. XOR decrypt using `psa_copy_reverse` byte reordering. +5. Extract: serial (24-bit), counter (16-bit), button (4-bit), CRC. + +### Mode 0x36 (TEA Brute-Force) + +Direct TEA decrypt attempted with BF1 and BF2 key schedules. Full brute-force (16M iterations per schedule) is available in ProtoPirate but not fully ported to KAT. + +Key schedules: +- BF1: `[0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A64]` +- BF2: `[0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04]` ## Decoder Steps -1. **WaitEdge** — Wait for edge/preamble. -2. **CountPattern** — Count preamble pattern (125/250 µs). -3. **DecodeManchester** — Manchester decode 128 bits; TEA decrypt; apply mode (0x23 XOR or 0x36); extract fields. -4. **End** — End marker (e.g. 1000 µs); return decode. +1. **WaitEdge (State0)** -- Detect preamble pattern type from first HIGH pulse (250 us -> Pattern 1, 125 us -> Pattern 2). +2. **CountPattern250 (State1)** -- Count 250 us preamble pairs; on long pulse with count > 70, transition to Manchester decode. +3. **DecodeManchester250 (State2)** -- Manchester decode at 250/500 us. End on 1000 us marker or 121+ bits. +4. **CountPattern125 (State3)** -- Count 125 us preamble pairs; on 250 us pulse with count >= 69, transition to Manchester decode. +5. **DecodeManchester125 (State4)** -- Manchester decode at 125/250 us. End on 500 us marker or 121+ bits. ## Encoder -Supported; preamble, Manchester 128 bits, TEA (+ XOR for 0x23). +Supported (mode 0x23 only). XOR encrypt, then modified TEA encrypt with BF1 key schedule. Preamble: 70 cycles of 125/125 us + 250/250 us sync. Manchester encoded key1 (64 bits) + validation (16 bits). End marker: 1000 us LOW. ## Frequencies diff --git a/src/protocols/fiat_v0.rs b/src/protocols/fiat_v0.rs index 53a3c69..4cedbaf 100644 --- a/src/protocols/fiat_v0.rs +++ b/src/protocols/fiat_v0.rs @@ -4,9 +4,9 @@ //! `fiat_v0.h`. Preamble: count short pulses (HIGH or LOW, 200±100µs); when preamble_count >= 150 //! (0x96), accept 800µs LOW gap (gap_threshold 800, te_delta 100) and enter Data. Data: 64 bits //! (serial=data_low, cnt=data_high) then 7 more bits; complete when bit_count > 0x46 with -//! btn = (data_low << 1) | 1, 71 bits total. Encoder: differential Manchester, 150 preamble pairs, -//! last LOW replaced by 800µs gap; 64 data bits then 6 btn bits (btn_to_send = btn >> 1); end -//! marker te_short*8 LOW. +//! btn = (uint8_t)data_low, 71 bits total. Encoder: standard Manchester, 150 preamble pairs, +//! last LOW replaced by 800µs gap; 64 data bits then 7 endbyte bits (endbyte & 0x7F); end +//! marker te_short*4 LOW. use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; use crate::duration_diff; @@ -294,8 +294,9 @@ impl ProtocolDecoder for FiatV0Decoder { self.data_high = 0; } - if self.bit_count > 0x46 { - self.btn = ((self.data_low << 1) | 1) as u8; + if self.bit_count == 0x47 { + // C: endbyte = (uint8_t)data_low (no transform) + self.btn = self.data_low as u8; let result = self.parse_data(); self.data_low = 0; self.data_high = 0; @@ -304,6 +305,19 @@ impl ProtocolDecoder for FiatV0Decoder { return Some(result); } } + } else { + // Manchester reset event (gap path) — C extracts at exactly 71 bits + if self.bit_count == 0x47 { + self.btn = self.data_low as u8; + let result = self.parse_data(); + self.data_low = 0; + self.data_high = 0; + self.bit_count = 0; + self.step = DecoderStep::Reset; + return Some(result); + } else if self.bit_count < 0x40 { + self.step = DecoderStep::Reset; + } } self.te_last = duration; } @@ -319,80 +333,56 @@ impl ProtocolDecoder for FiatV0Decoder { fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { let serial = decoded.serial?; let cnt = decoded.counter.unwrap_or(0) as u32; - let btn = decoded.button.unwrap_or(0).max(button); + let endbyte = if button != 0 { button } else { decoded.button.unwrap_or(0) }; - // Ref: data = (cnt<<32)|serial; btn_to_send = btn >> 1 (reverse decoder's (x<<1)|1) + // C: data = (hop << 32) | fix; endbyte sent as-is (7 bits, & 0x7F) let data = ((cnt as u64) << 32) | (serial as u64); - let btn_to_send = btn >> 1; + let te_short = TE_SHORT; let mut signal = Vec::with_capacity(1024); - let te_short = TE_SHORT; - let te_long = TE_LONG; for burst in 0..TOTAL_BURSTS { if burst > 0 { signal.push(LevelDuration::new(false, INTER_BURST_GAP)); } - // Preamble: HIGH-LOW pairs; last LOW replaced by gap (ref) - for i in 0..PREAMBLE_PAIRS { + // Preamble: alternating short pulses; last LOW extended to gap + for _ in 0..PREAMBLE_PAIRS { signal.push(LevelDuration::new(true, te_short)); - signal.push(LevelDuration::new( - false, - if i == PREAMBLE_PAIRS - 1 { GAP_US } else { te_short }, - )); - } - - // First bit (bit 63) - differential Manchester - let first_bit = (data >> 63) & 1 == 1; - if first_bit { - signal.push(LevelDuration::new(true, te_long)); - } else { - signal.push(LevelDuration::new(true, te_short)); - signal.push(LevelDuration::new(false, te_long)); - } - - let mut prev_bit = first_bit; - - // Remaining 63 data bits - for bit in (0..63).rev() { - let curr_bit = (data >> bit) & 1 == 1; - if !prev_bit && !curr_bit { - signal.push(LevelDuration::new(true, te_short)); - signal.push(LevelDuration::new(false, te_short)); - } else if !prev_bit && curr_bit { - signal.push(LevelDuration::new(true, te_long)); - } else if prev_bit && !curr_bit { - signal.push(LevelDuration::new(false, te_long)); - } else { - signal.push(LevelDuration::new(false, te_short)); - signal.push(LevelDuration::new(true, te_short)); - } - prev_bit = curr_bit; - } - - // 6 btn bits (ref: for bit 5 down to 0) - for bit in (0..6).rev() { - let curr_bit = (btn_to_send >> bit) & 1 == 1; - if !prev_bit && !curr_bit { - signal.push(LevelDuration::new(true, te_short)); - signal.push(LevelDuration::new(false, te_short)); - } else if !prev_bit && curr_bit { - signal.push(LevelDuration::new(true, te_long)); - } else if prev_bit && !curr_bit { - signal.push(LevelDuration::new(false, te_long)); - } else { - signal.push(LevelDuration::new(false, te_short)); - signal.push(LevelDuration::new(true, te_short)); - } - prev_bit = curr_bit; - } - - // End marker (ref) - if prev_bit { signal.push(LevelDuration::new(false, te_short)); } - signal.push(LevelDuration::new(false, te_short * 8)); + // Extend last LOW to create gap (matches C: upload[index-1] = gap) + if let Some(last) = signal.last_mut() { + *last = LevelDuration::new(false, GAP_US); + } + + // Standard Manchester encode 64 bits of data (matches C) + for bit in (0..64).rev() { + let curr_bit = (data >> bit) & 1 == 1; + if curr_bit { + 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)); + } + } + + // Standard Manchester encode 7 bits of endbyte (matches C: endbyte & 0x7F, bits 6..0) + let endbyte_masked = endbyte & 0x7F; + for bit in (0..7).rev() { + let curr_bit = (endbyte_masked >> bit) & 1 == 1; + if curr_bit { + 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)); + } + } + + // End marker: te_short * 4 LOW (matches C) + signal.push(LevelDuration::new(false, te_short * 4)); } Some(signal) diff --git a/src/protocols/fiat_v1.rs b/src/protocols/fiat_v1.rs new file mode 100644 index 0000000..c392541 --- /dev/null +++ b/src/protocols/fiat_v1.rs @@ -0,0 +1,435 @@ +//! Fiat V1 protocol decoder (Magneti Marelli BSI / PCF7946) +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/fiat_v1.c` and +//! `fiat_v1.h`. Found on Fiat Panda, Grande Punto, and possibly other Fiat/Lancia/Alfa ~2003-2012. +//! +//! RF: 433.92 MHz, Manchester encoding. +//! Two timing variants with identical frame structure: +//! Type A (e.g. Panda): te_short ~260us, te_long ~520us +//! Type B (e.g. Grande Punto): te_short ~100us, te_long ~200us +//! TE is auto-detected from preamble pulse averaging (boundary at 180us). +//! +//! Frame layout (103-104 bits = 13 bytes): +//! Bytes 0-1: 0xFFFF/0xFFFC preamble residue +//! Bytes 2-5: Serial (32 bits) +//! Byte 6: [Button:4 | Epoch:4] +//! Byte 7: [Counter:5 | Scramble:2 | Fixed:1] +//! Bytes 8-12: Encrypted payload (40 bits) +//! +//! State machine: Reset -> Preamble -> Sync -> Data (-> RetxSync -> Data) +//! No encoder (decode-only). +//! +//! Original C implementation by @lupettohf + +use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; +use crate::duration_diff; +use crate::radio::demodulator::LevelDuration; + +// Default timing constants (Type A) +const TE_SHORT: u32 = 260; +#[allow(dead_code)] +const TE_LONG: u32 = 520; +#[allow(dead_code)] +const TE_DELTA: u32 = 80; +#[allow(dead_code)] +const MIN_COUNT_BIT: usize = 80; + +// Preamble / sync / data constants (from C defines) +const PREAMBLE_PULSE_MIN: u32 = 50; +const PREAMBLE_PULSE_MAX: u32 = 350; +const PREAMBLE_MIN: u16 = 80; +const MAX_DATA_BITS: u8 = 104; +const MIN_DATA_BITS: u8 = 80; +const GAP_TE_MULT: u32 = 4; +const SYNC_TE_MIN_MULT: u32 = 4; +const SYNC_TE_MAX_MULT: u32 = 12; +const RETX_GAP_MIN: u32 = 5000; +const RETX_SYNC_MIN: u32 = 400; +const RETX_SYNC_MAX: u32 = 2800; +#[allow(dead_code)] +const TE_TYPE_AB_BOUNDARY: u32 = 180; + +/// Manchester state machine (Flipper-style, same as fiat_v0.rs / common.rs). +#[derive(Debug, Clone, Copy, PartialEq)] +enum ManchesterState { + Mid0 = 0, + Mid1 = 1, + Start0 = 2, + Start1 = 3, +} + +/// Decoder state machine steps. +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + Preamble, + Sync, + Data, + RetxSync, +} + +pub struct FiatV1Decoder { + step: DecoderStep, + manchester_state: ManchesterState, + + // Preamble tracking + preamble_count: u16, + + // Auto-detected TE from preamble averaging + te_sum: u32, + te_count: u16, + te_detected: u32, + + // Data accumulation + raw_data: [u8; 13], + bit_count: u8, + data: u64, // first 64 bits + extra_data: u32, // bits 65+ + + // Parsed fields + serial: u32, + btn: u8, + cnt: u8, + + te_last: u32, +} + +impl FiatV1Decoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + manchester_state: ManchesterState::Mid1, + preamble_count: 0, + te_sum: 0, + te_count: 0, + te_detected: 0, + raw_data: [0u8; 13], + bit_count: 0, + data: 0, + extra_data: 0, + serial: 0, + btn: 0, + cnt: 0, + te_last: 0, + } + } + + /// Advance the Manchester state machine by one event. + /// Event encoding: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh. + /// Returns `Some(bit)` when a data bit is emitted, `None` otherwise. + 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 + } + } + + /// Reset Manchester state machine (to Mid1, matching C reset). + fn manchester_reset(&mut self) { + self.manchester_state = ManchesterState::Mid1; + } + + /// Prepare for data reception: clear accumulators, reset Manchester, enter Data step. + /// Matches `fiat_marelli_prepare_data` in the C reference. + fn prepare_data(&mut self) { + self.bit_count = 0; + self.extra_data = 0; + self.data = 0; + self.raw_data = [0u8; 13]; + self.manchester_reset(); + self.step = DecoderStep::Data; + } + + /// Get the effective te_short (auto-detected or default). + fn te_short(&self) -> u32 { + if self.te_detected != 0 { + self.te_detected + } else { + TE_SHORT + } + } + + /// Get the effective te_long (2 * te_short). + fn te_long(&self) -> u32 { + self.te_short() * 2 + } + + /// Get the effective te_delta (te_short / 2, minimum 30). + fn te_delta(&self) -> u32 { + let d = self.te_short() / 2; + if d < 30 { 30 } else { d } + } + + /// Map button code to name (matches C fiat_marelli_button_name). + #[allow(dead_code)] + fn button_name(btn: u8) -> &'static str { + match btn { + 0x7 => "Lock", + 0xB => "Unlock", + 0xD => "Trunk", + _ => "Unknown", + } + } + + /// Build a DecodedSignal from the current state. + fn parse_data(&self) -> DecodedSignal { + // Pack extra bytes (bits 65+) into extra field + let extra = if self.bit_count > 64 { + Some(self.extra_data as u64) + } else { + None + }; + + DecodedSignal { + serial: Some(self.serial), + button: Some(self.btn), + counter: Some(self.cnt as u16), + crc_valid: true, // no CRC defined for this protocol + data: self.data, + data_count_bit: self.bit_count as usize, + encoder_capable: false, + extra, + protocol_display_name: None, + } + } +} + +impl ProtocolDecoder for FiatV1Decoder { + fn name(&self) -> &'static str { + "Fiat V1" + } + + 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.step = DecoderStep::Reset; + self.preamble_count = 0; + self.bit_count = 0; + self.extra_data = 0; + self.te_last = 0; + self.te_sum = 0; + self.te_count = 0; + self.te_detected = 0; + self.data = 0; + self.raw_data = [0u8; 13]; + self.serial = 0; + self.btn = 0; + self.cnt = 0; + self.manchester_state = ManchesterState::Mid1; + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + let te_short = self.te_short(); + let te_long = self.te_long(); + let te_delta = self.te_delta(); + + match self.step { + // ========================================================= + // Reset: wait for a HIGH pulse in preamble range, or detect + // retransmission gap (LOW > 5000us with valid te_detected). + // ========================================================= + DecoderStep::Reset => { + if level { + if duration >= PREAMBLE_PULSE_MIN && duration <= PREAMBLE_PULSE_MAX { + self.step = DecoderStep::Preamble; + self.preamble_count = 1; + self.te_sum = duration; + self.te_count = 1; + self.te_last = duration; + } + } else if duration > RETX_GAP_MIN && self.te_detected != 0 { + self.step = DecoderStep::RetxSync; + self.te_last = duration; + } + } + + // ========================================================= + // Preamble: accumulate pulses in 50-350us range for TE + // averaging. When count >= 80 and we see a LOW gap >= 4*TE, + // transition to Sync. + // ========================================================= + DecoderStep::Preamble => { + if duration >= PREAMBLE_PULSE_MIN && duration <= PREAMBLE_PULSE_MAX { + // Pulse in valid preamble range (either HIGH or LOW) + self.preamble_count += 1; + self.te_sum += duration; + self.te_count += 1; + self.te_last = duration; + } else if !level { + // LOW pulse outside preamble range -- check if it's the gap after preamble + if self.preamble_count >= PREAMBLE_MIN && self.te_count > 0 { + self.te_detected = self.te_sum / (self.te_count as u32); + let gap_threshold = self.te_detected * GAP_TE_MULT; + + if duration > gap_threshold { + self.step = DecoderStep::Sync; + self.te_last = duration; + } else { + self.step = DecoderStep::Reset; + } + } else { + self.step = DecoderStep::Reset; + } + } else { + // HIGH pulse outside preamble range + self.step = DecoderStep::Reset; + } + } + + // ========================================================= + // Sync: expect HIGH sync pulse of te_detected*4 to + // te_detected*12 duration. On match, prepare data decoder. + // ========================================================= + DecoderStep::Sync => { + let sync_min = self.te_detected * SYNC_TE_MIN_MULT; + let sync_max = self.te_detected * SYNC_TE_MAX_MULT; + + if level && duration >= sync_min && duration <= sync_max { + self.prepare_data(); + self.te_last = duration; + } else { + self.step = DecoderStep::Reset; + } + } + + // ========================================================= + // RetxSync: after retransmission gap (>5000us LOW), look for + // HIGH sync pulse in 400-2800us range. + // ========================================================= + DecoderStep::RetxSync => { + if level && duration >= RETX_SYNC_MIN && duration <= RETX_SYNC_MAX { + self.prepare_data(); + self.te_last = duration; + } else { + self.step = DecoderStep::Reset; + } + } + + // ========================================================= + // Data: Manchester decode up to 104 bits. Complete frame on + // 104 bits reached or gap with >= 80 bits collected. + // ========================================================= + DecoderStep::Data => { + let mut event: u8 = 4; // 4 = ManchesterEventReset (invalid) + let mut frame_complete = false; + + // Check for short pulse match + let diff_short = duration_diff!(duration, te_short); + if diff_short < te_delta { + // Flipper convention: level=HIGH -> ShortLow(0), level=LOW -> ShortHigh(1) + event = if level { 0 } else { 1 }; + } else { + // Check for long pulse match + let diff_long = duration_diff!(duration, te_long); + if diff_long < te_delta { + event = if level { 2 } else { 3 }; + } + } + + if event != 4 { + // Valid Manchester event + if let Some(data_bit) = self.manchester_advance(event) { + let new_bit: u32 = if data_bit { 1 } else { 0 }; + + // Store bit into raw_data byte array (MSB-first) + if self.bit_count < MAX_DATA_BITS { + let byte_idx = (self.bit_count / 8) as usize; + let bit_pos = 7 - (self.bit_count % 8); + if new_bit != 0 { + self.raw_data[byte_idx] |= 1u8 << bit_pos; + } + } + + // Also accumulate into u64 data / u32 extra_data + if self.bit_count < 64 { + self.data = (self.data << 1) | (new_bit as u64); + } else { + self.extra_data = (self.extra_data << 1) | new_bit; + } + + self.bit_count += 1; + if self.bit_count >= MAX_DATA_BITS { + frame_complete = true; + } + } + } else if self.bit_count >= MIN_DATA_BITS { + // Gap or invalid pulse but we have enough bits + frame_complete = true; + } else { + // Not enough bits and invalid pulse -- abort + self.step = DecoderStep::Reset; + } + + if frame_complete { + // Parse serial, button, counter from raw_data + self.serial = ((self.raw_data[2] as u32) << 24) + | ((self.raw_data[3] as u32) << 16) + | ((self.raw_data[4] as u32) << 8) + | (self.raw_data[5] as u32); + self.btn = (self.raw_data[6] >> 4) & 0x0F; + self.cnt = (self.raw_data[7] >> 3) & 0x1F; + + let result = self.parse_data(); + self.step = DecoderStep::Reset; + self.te_last = duration; + return Some(result); + } + + self.te_last = duration; + } + } + + None + } + + fn supports_encoding(&self) -> bool { + false + } + + fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { + None + } +} + +impl Default for FiatV1Decoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/ford_v0.rs b/src/protocols/ford_v0.rs index 45900be..2e473cc 100644 --- a/src/protocols/ford_v0.rs +++ b/src/protocols/ford_v0.rs @@ -225,10 +225,26 @@ impl FordV0Decoder { } // ========================================================================= - // BS calculation + // Checksum / BS calculation // ========================================================================= - /// Calculate BS = (count_low_byte + bs_magic + (button << 4)) with overflow handling + /// Calculate checksum from serial, count, and button (matches psa.c ford_v0_calculate_checksum). + /// Sums all bytes of serial + all bytes of count + (button << 4), truncated to u8. + fn calculate_checksum(serial: u32, count: u32, button: u8) -> u8 { + let sum: u32 = ((count >> 24) & 0xFF) + .wrapping_add((count >> 16) & 0xFF) + .wrapping_add((count >> 8) & 0xFF) + .wrapping_add(count & 0xFF) + .wrapping_add((serial >> 24) & 0xFF) + .wrapping_add((serial >> 16) & 0xFF) + .wrapping_add((serial >> 8) & 0xFF) + .wrapping_add(serial & 0xFF) + .wrapping_add((button as u32) << 4); + (sum & 0xFF) as u8 + } + + /// Legacy BS calculation (kept for reference; encoder now uses calculate_checksum) + #[allow(dead_code)] fn calculate_bs(count: u32, button: u8, bs_magic: u8) -> u8 { let result: u16 = (count as u16 & 0xFF) .wrapping_add(bs_magic as u16) @@ -651,21 +667,18 @@ impl ProtocolDecoder for FordV0Decoder { decoded.counter.unwrap_or(0) as u32 }) & 0xFFFFF; // 20-bit - // Use stored bs_magic (or default to 0x6F for backward compatibility) - let bs_magic = if self.bs_magic != 0 { self.bs_magic } else { 0x6F }; - - // Calculate BS from count + button + bs_magic (matches C ford_v0_calculate_bs) - let bs = Self::calculate_bs(count, button, bs_magic); + // Calculate checksum from serial + count + button (matches ford_v0_calculate_checksum in C) + let checksum = Self::calculate_checksum(serial, count, button); // Extract header byte from the original key1 (first byte) let header_byte = (decoded.data >> 56) as u8; // Encode key1 from fields (same count, new button) - let new_key1 = Self::encode_ford_v0(header_byte, serial, button, count, bs); + let new_key1 = Self::encode_ford_v0(header_byte, serial, button, count, checksum); // Calculate CRC for key2 - let crc = Self::calculate_crc_for_tx(new_key1, bs); - let new_key2 = ((bs as u16) << 8) | (crc as u16); + let crc = Self::calculate_crc_for_tx(new_key1, checksum); + let new_key2 = ((checksum as u16) << 8) | (crc as u16); // Build one 6-burst block and repeat TX_REPEAT times (matches reference encoder.repeat = 10) let single = Self::build_upload(new_key1, new_key2); diff --git a/src/protocols/kia_v1.rs b/src/protocols/kia_v1.rs index 7300d0c..d74c751 100644 --- a/src/protocols/kia_v1.rs +++ b/src/protocols/kia_v1.rs @@ -7,7 +7,7 @@ //! - Manchester encoding: 800/1600µs timing //! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC) //! - Long preamble of ~90 long pairs -//! - CRC4 checksum with offset rules (cnt_high 0 / >= 6) +//! - CRC4 checksum (XOR of nibbles + offset 1, 7 bytes including cnt_high) use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use crate::radio::demodulator::LevelDuration; @@ -66,13 +66,14 @@ impl KiaV1Decoder { (crc.wrapping_add(offset)) & 0x0F } - /// Manchester state machine - fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option { - let event = match (is_short, is_high) { - (true, false) => 0, // Short Low - (true, true) => 1, // Short High - (false, false) => 2, // Long Low - (false, true) => 3, // Long High + /// Manchester state machine (Flipper convention: level ? ShortLow : ShortHigh) + fn manchester_advance(&mut self, is_short: bool, level: bool) -> Option { + // C: event = level ? ManchesterEventShortLow : ManchesterEventShortHigh (inverted) + let event = match (is_short, level) { + (true, true) => 0, // level=true → ShortLow + (true, false) => 1, // level=false → ShortHigh + (false, true) => 2, // level=true → LongLow + (false, false) => 3, // level=false → LongHigh }; let (new_state, output) = match (self.manchester_state, event) { @@ -115,15 +116,9 @@ impl KiaV1Decoder { char_data[4] = button; char_data[5] = (counter & 0xFF) as u8; - let crc = if cnt_high == 0 { - let offset = if counter >= 0x098 { button } else { 1 }; - Self::crc4(&char_data[..6], offset) - } else if cnt_high >= 0x6 { - char_data[6] = cnt_high as u8; - Self::crc4(&char_data, 1) - } else { - Self::crc4(&char_data[..6], 1) - }; + // CRC4: always 7 bytes with offset 1 (matches updated ProtoPirate kia_v1.c) + char_data[6] = cnt_high as u8; + let crc = Self::crc4(&char_data, 1); DecodedSignal { serial: Some(serial), @@ -194,8 +189,9 @@ impl ProtocolDecoder for KiaV1Decoder { if self.header_count > 70 { if !level && is_short && duration_diff!(self.te_last, TE_LONG) < TE_DELTA { - self.decode_count_bit = 1; - self.decode_data = 1; // Add first bit + // C: decode_count_bit=1, then add_bit(1) increments to 2 + self.decode_count_bit = 2; + self.decode_data = 1; self.header_count = 0; self.step = DecoderStep::DecodeData; } @@ -218,7 +214,7 @@ impl ProtocolDecoder for KiaV1Decoder { return None; } - if self.decode_count_bit >= MIN_COUNT_BIT { + if self.decode_count_bit == MIN_COUNT_BIT { let result = self.parse_data(); self.step = DecoderStep::Reset; return Some(result); @@ -247,15 +243,9 @@ impl ProtocolDecoder for KiaV1Decoder { char_data[4] = button; char_data[5] = (counter & 0xFF) as u8; - let crc = if cnt_high == 0 { - let offset = if counter >= 0x098 { button } else { 1 }; - Self::crc4(&char_data[..6], offset) - } else if cnt_high >= 0x6 { - char_data[6] = cnt_high; - Self::crc4(&char_data, 1) - } else { - Self::crc4(&char_data[..6], 1) - }; + // CRC4: always 7 bytes with offset 1 (matches updated ProtoPirate kia_v1.c) + char_data[6] = cnt_high; + let crc = Self::crc4(&char_data, 1); // Build data let data: u64 = ((serial as u64) << 24) | diff --git a/src/protocols/kia_v2.rs b/src/protocols/kia_v2.rs index 6339025..743d87a 100644 --- a/src/protocols/kia_v2.rs +++ b/src/protocols/kia_v2.rs @@ -57,18 +57,17 @@ impl KiaV2Decoder { } } - /// CRC4 for Kia V2 (matches kia_v2.c: 6-byte permuted input, XOR nibbles, offset 1) + /// CRC4 for Kia V2 (matches kia_v2.c: strip CRC nibble, XOR all remaining nibbles, + offset 1) fn calculate_crc(data: u64) -> u8 { - let serial = ((data >> 20) & 0xFFFFFFFF) as u32; - let u_var4 = (data & 0xFFFFFFFF) as u32; - + // C code: data_without_crc = data >> 4; then read 6 sequential bytes (bits 4..51) + let data_without_crc = data >> 4; let mut bytes = [0u8; 6]; - bytes[0] = (u_var4 >> 20) as u8; - bytes[1] = ((u_var4 >> 28) | ((serial & 0x0F) << 4)) as u8; - bytes[2] = (serial >> 4) as u8; - bytes[3] = (serial >> 12) as u8; - bytes[4] = (u_var4 >> 4) as u8; - bytes[5] = (u_var4 >> 12) as u8; + bytes[0] = (data_without_crc & 0xFF) as u8; + bytes[1] = ((data_without_crc >> 8) & 0xFF) as u8; + bytes[2] = ((data_without_crc >> 16) & 0xFF) as u8; + bytes[3] = ((data_without_crc >> 24) & 0xFF) as u8; + bytes[4] = ((data_without_crc >> 32) & 0xFF) as u8; + bytes[5] = ((data_without_crc >> 40) & 0xFF) as u8; let mut crc: u8 = 0; for &byte in &bytes { @@ -181,19 +180,21 @@ impl ProtocolDecoder for KiaV2Decoder { self.te_last = duration; self.header_count += 1; } else if is_short && self.header_count >= 100 { + // C code: decode_count_bit=1, then add_bit(1) which shifts and increments to 2 self.header_count = 0; - self.decode_data = 0; - self.decode_count_bit = 1; - self.step = DecoderStep::CollectRawBits; self.decode_data = 1; // First bit + self.decode_count_bit = 2; + self.step = DecoderStep::CollectRawBits; } else { - self.step = DecoderStep::Reset; + self.te_last = duration; // C stays in CheckPreamble, updates te_last } } else { if is_long { self.header_count += 1; self.te_last = duration; - } else if !is_short { + } else if is_short { + self.te_last = duration; // C stays in CheckPreamble for short LOW + } else { self.step = DecoderStep::Reset; } } diff --git a/src/protocols/kia_v6.rs b/src/protocols/kia_v6.rs index 10bf416..ec98526 100644 --- a/src/protocols/kia_v6.rs +++ b/src/protocols/kia_v6.rs @@ -1,8 +1,8 @@ -//! Kia V6 protocol decoder (decode-only) +//! Kia V6 protocol decoder/encoder //! //! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v6.c`. //! Decode logic (Manchester level mapping, 3-part 144-bit frame, AES-128, CRC8, keystore XOR) matches reference. -//! No encoder in protopirate. +//! Encoder ported from protopirate (ENABLE_EMULATE_FEATURE): AES-128 encrypt, two-pass Manchester. //! //! Protocol characteristics: //! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance) @@ -275,6 +275,181 @@ impl KiaV6Decoder { *data = state; } + // ========================================================================= + // Forward AES functions for encoder (matches kia_v6.c ENABLE_EMULATE_FEATURE) + // ========================================================================= + + /// AES forward SubBytes + fn aes_subbytes(state: &mut [u8; 16]) { + for i in 0..16 { + state[i] = AES_SBOX[state[i] as usize]; + } + } + + /// AES forward ShiftRows + fn aes_shiftrows(state: &mut [u8; 16]) { + let temp = state[1]; + state[1] = state[5]; + state[5] = state[9]; + state[9] = state[13]; + state[13] = temp; + + let temp = state[2]; + state[2] = state[10]; + state[10] = temp; + let temp = state[6]; + state[6] = state[14]; + state[14] = temp; + + let temp = state[3]; + state[3] = state[15]; + state[15] = state[11]; + state[11] = state[7]; + state[7] = temp; + } + + /// AES forward MixColumns + fn aes_mixcolumns(state: &mut [u8; 16]) { + for i in 0..4 { + let a = state[i * 4]; + let b = state[i * 4 + 1]; + let c = state[i * 4 + 2]; + let d = state[i * 4 + 3]; + state[i * 4] = Self::gf_mul2(a) ^ Self::gf_mul2(b) ^ b ^ c ^ d; + state[i * 4 + 1] = a ^ Self::gf_mul2(b) ^ Self::gf_mul2(c) ^ c ^ d; + state[i * 4 + 2] = a ^ b ^ Self::gf_mul2(c) ^ Self::gf_mul2(d) ^ d; + state[i * 4 + 3] = Self::gf_mul2(a) ^ a ^ b ^ c ^ Self::gf_mul2(d); + } + } + + /// AES-128 encrypt + fn aes128_encrypt(expanded_key: &[u8; 176], data: &mut [u8; 16]) { + let mut state = *data; + + Self::aes_addroundkey(&mut state, &expanded_key[0..16]); + + for round in 1..10 { + Self::aes_subbytes(&mut state); + Self::aes_shiftrows(&mut state); + Self::aes_mixcolumns(&mut state); + Self::aes_addroundkey(&mut state, &expanded_key[round * 16..(round + 1) * 16]); + } + + Self::aes_subbytes(&mut state); + Self::aes_shiftrows(&mut state); + Self::aes_addroundkey(&mut state, &expanded_key[160..176]); + + *data = state; + } + + /// Encrypt payload for transmission (matches kia_v6.c kia_v6_encrypt_payload) + fn encrypt_payload( + fx_field: u8, + serial: u32, + button: u8, + counter: u32, + ) -> (u32, u32, u32, u32, u16) { + let mut plain = [0u8; 16]; + plain[0] = fx_field; + plain[4] = ((serial >> 16) & 0xFF) as u8; + plain[5] = ((serial >> 8) & 0xFF) as u8; + plain[6] = (serial & 0xFF) as u8; + plain[7] = button & 0x0F; + plain[8] = ((counter >> 24) & 0xFF) as u8; + plain[9] = ((counter >> 16) & 0xFF) as u8; + plain[10] = ((counter >> 8) & 0xFF) as u8; + plain[11] = (counter & 0xFF) as u8; + plain[12] = AES_SBOX[(counter & 0xFF) as usize]; + plain[15] = Self::crc8(&plain[..15], 0xFF, 0x07); + + let aes_key = Self::get_aes_key(); + let expanded_key = Self::aes_key_expansion(&aes_key); + Self::aes128_encrypt(&expanded_key, &mut plain); + + let fx_hi = 0x20 | (fx_field >> 4); + let fx_lo = fx_field & 0x0F; + let part1_high = ((fx_hi as u32) << 24) + | ((fx_lo as u32) << 16) + | ((plain[0] as u32) << 8) + | (plain[1] as u32); + let part1_low = ((plain[2] as u32) << 24) + | ((plain[3] as u32) << 16) + | ((plain[4] as u32) << 8) + | (plain[5] as u32); + let part2_high = ((plain[6] as u32) << 24) + | ((plain[7] as u32) << 16) + | ((plain[8] as u32) << 8) + | (plain[9] as u32); + let part2_low = ((plain[10] as u32) << 24) + | ((plain[11] as u32) << 16) + | ((plain[12] as u32) << 8) + | (plain[13] as u32); + let part3 = ((plain[14] as u16) << 8) | (plain[15] as u16); + + (part1_low, part1_high, part2_low, part2_high, part3) + } + + /// Build encoder signal: two-pass Manchester with preambles (matches kia_v6.c) + fn build_upload( + p1_lo: u32, p1_hi: u32, + p2_lo: u32, p2_hi: u32, + p3: u16, + ) -> Vec { + let mut signal = Vec::with_capacity(2000); + + // Two passes: 640 preamble pairs, then 38 preamble pairs + for &preamble_pairs in &[640u32, 38u32] { + // Preamble: short/short pairs + for _ in 0..preamble_pairs { + signal.push(LevelDuration::new(true, TE_SHORT)); + signal.push(LevelDuration::new(false, TE_SHORT)); + } + + // Sync: short LOW, long HIGH, short LOW + signal.push(LevelDuration::new(false, TE_SHORT)); + signal.push(LevelDuration::new(true, TE_LONG)); + signal.push(LevelDuration::new(false, TE_SHORT)); + + // Part1: bits 60 down to 0 (61 bits), inverted + for b in (0..=60).rev() { + let word = if b >= 32 { p1_hi } else { p1_lo }; + let shift = if b >= 32 { b - 32 } else { b }; + let bit = ((!word) >> shift) & 1 == 1; + Self::encode_manchester_bit(&mut signal, bit); + } + + // Part2: bits 63 down to 0 (64 bits), inverted + for b in (0..=63).rev() { + let word = if b >= 32 { p2_hi } else { p2_lo }; + let shift = if b >= 32 { b - 32 } else { b }; + let bit = ((!word) >> shift) & 1 == 1; + Self::encode_manchester_bit(&mut signal, bit); + } + + // Part3: bits 15 down to 0 (16 bits), inverted + for b in (0..=15).rev() { + let bit = ((!p3) >> b) & 1 == 1; + Self::encode_manchester_bit(&mut signal, bit); + } + + // Gap between passes + signal.push(LevelDuration::new(false, TE_LONG)); + } + + signal + } + + /// Encode one Manchester bit (matches kia_v6.c kia_v6_encode_manchester_bit) + fn encode_manchester_bit(signal: &mut Vec, bit: bool) { + if bit { + signal.push(LevelDuration::new(false, TE_SHORT)); + signal.push(LevelDuration::new(true, TE_SHORT)); + } else { + signal.push(LevelDuration::new(true, TE_SHORT)); + signal.push(LevelDuration::new(false, TE_SHORT)); + } + } + /// AES-128 key from V6 keystores A+B with XOR_MASK_LOW/HIGH (matches kia_v6.c) fn get_aes_key() -> [u8; 16] { let keystore_a = Self::get_keystore_a(); @@ -306,6 +481,13 @@ impl KiaV6Decoder { aes_key } + /// Extract fx_field from stored_part1_high (matches kia_v6.c fx_field extraction) + fn extract_fx_field(&self) -> u8 { + let fx_byte0 = ((self.stored_part1_high >> 24) & 0xFF) as u8; + let fx_byte1 = ((self.stored_part1_high >> 16) & 0xFF) as u8; + ((fx_byte0 & 0xF) << 4) | (fx_byte1 & 0xF) + } + /// Decrypt 16-byte block: byte layout matches kia_v6.c; AES-128 then CRC8 check fn decrypt(&self) -> Option<(u32, u8, u32, bool)> { let mut encrypted_data = [0u8; 16]; @@ -528,6 +710,7 @@ impl ProtocolDecoder for KiaV6Decoder { if let Some((serial, button, counter, crc_valid)) = self.decrypt() { let key_data = ((self.stored_part1_high as u64) << 32) | (self.stored_part1_low as u64); + let fx_field = self.extract_fx_field(); self.step = DecoderStep::Reset; return Some(DecodedSignal { @@ -537,8 +720,8 @@ impl ProtocolDecoder for KiaV6Decoder { crc_valid, data: key_data, data_count_bit: MIN_COUNT_BIT, - encoder_capable: false, - extra: None, + encoder_capable: true, + extra: Some(fx_field as u64), protocol_display_name: None, }); } @@ -552,11 +735,18 @@ impl ProtocolDecoder for KiaV6Decoder { } fn supports_encoding(&self) -> bool { - false // V6 is decode-only + true } - fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { - None // V6 decode-only in protopirate + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { + let serial = decoded.serial?; + let counter = decoded.counter.unwrap_or(0) as u32; + let fx_field = decoded.extra.unwrap_or(0) as u8; + + let (p1_lo, p1_hi, p2_lo, p2_hi, p3) = + Self::encrypt_payload(fx_field, serial, button, counter); + + Some(Self::build_upload(p1_lo, p1_hi, p2_lo, p2_hi, p3)) } } diff --git a/src/protocols/mazda_v0.rs b/src/protocols/mazda_v0.rs new file mode 100644 index 0000000..1d9c16a --- /dev/null +++ b/src/protocols/mazda_v0.rs @@ -0,0 +1,312 @@ +//! Mazda V0 protocol decoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/mazda_v0.c`. +//! Original implementation by @lupettohf. +//! +//! Protocol characteristics: +//! - 433.92 MHz FM, decode-only (no encoder) +//! - Pair-based decoding: feed() ignores level, processes raw durations in pairs +//! - Preamble: minimum 13 short/short pairs before data starts +//! - Data uses a 14-byte buffer with inverted bit polarity +//! - XOR deobfuscation with parity-based mask selection + bit interleave swap +//! - Additive checksum over first 7 bytes must equal byte 8 +//! - Field layout: serial (32-bit), button (8-bit), counter (16-bit) +//! - Button codes: 0x10=Lock, 0x20=Unlock, 0x40=Trunk + +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; + +/// Decoder states (matches mazda_v0.c MazdaDecoderStep) +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + PreambleSave, + PreambleCheck, + DataSave, + DataCheck, +} + +/// Mazda V0 protocol decoder +pub struct MazdaV0Decoder { + step: DecoderStep, + te_last: u32, + preamble_count: u16, + bit_counter: u16, + prev_state: u8, + data_buffer: [u8; DATA_BUFFER_SIZE], +} + +impl MazdaV0Decoder { + 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 a single bit into the data buffer. + /// Inverted polarity: state_bit == 0 means stored bit is 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 and collect bits. + /// Returns true if the pair was valid, false otherwise. + 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 + } + + /// Check whether enough bits have been collected and validate the frame. + /// 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 sync/header byte) + let mut data = [0u8; 8]; + for i in 0..8 { + data[i] = self.data_buffer[i + 1]; + } + + // XOR deobfuscation + Self::xor_deobfuscate(&mut data); + + // Checksum: sum of data[0..7] must equal data[7] + let mut checksum: u8 = 0; + for i in 0..7 { + checksum = checksum.wrapping_add(data[i]); + } + if checksum != data[7] { + return None; + } + + // Pack into u64 + let mut packed: u64 = 0; + for i in 0..8 { + packed = (packed << 8) | (data[i] as u64); + } + + // Parse fields + let serial = (packed >> 32) as u32; + let btn = ((packed >> 24) & 0xFF) as u8; + let cnt = ((packed >> 8) & 0xFFFF) as u16; + + Some(DecodedSignal { + serial: Some(serial), + button: Some(btn), + counter: Some(cnt), + crc_valid: true, + data: packed, + data_count_bit: MIN_COUNT_BIT, + encoder_capable: false, + extra: None, + protocol_display_name: None, + }) + } + + /// Byte parity: XOR-fold to single bit (matches mazda_byte_parity in C) + fn byte_parity(mut value: u8) -> u8 { + value ^= value >> 4; + value ^= value >> 2; + value ^= value >> 1; + value & 1 + } + + /// XOR deobfuscation with parity-based mask selection + bit interleave swap + /// (matches mazda_xor_deobfuscate in C) + fn xor_deobfuscate(data: &mut [u8; 8]) { + 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; + } + + // Bit interleave swap between bytes 5 and 6 + let old5 = data[5]; + let old6 = data[6]; + data[5] = (old5 & 0xAA) | (old6 & 0x55); + data[6] = (old5 & 0x55) | (old6 & 0xAA); + } + + /// Get button name for display + #[allow(dead_code)] + fn get_button_name(btn: u8) -> &'static str { + match btn { + 0x10 => "Lock", + 0x20 => "Unlock", + 0x40 => "Trunk", + _ => "Unknown", + } + } +} + +impl ProtocolDecoder for MazdaV0Decoder { + fn name(&self) -> &'static str { + "Mazda V0" + } + + 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.step = DecoderStep::Reset; + self.preamble_count = 0; + self.bit_counter = 0; + self.prev_state = 0; + self.te_last = 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 + { + // Transition from preamble to data + 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 { + // Pair was invalid - check if we have a complete frame + let result = self.check_completion(); + self.step = DecoderStep::Reset; + if result.is_some() { + return result; + } + } + } + } + + None + } + + fn supports_encoding(&self) -> bool { + false + } + + fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { + None + } +} + +impl Default for MazdaV0Decoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/mitsubishi_v0.rs b/src/protocols/mitsubishi_v0.rs new file mode 100644 index 0000000..9dbbbdc --- /dev/null +++ b/src/protocols/mitsubishi_v0.rs @@ -0,0 +1,244 @@ +//! Mitsubishi V0 protocol decoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/mitsubishi_v0.c`. +//! Original implementation by @lupettohf. +//! +//! Protocol characteristics: +//! - PWM encoding: Short HIGH + Long LOW = bit 1, Long HIGH + Short LOW = bit 0 +//! - 96-bit frame (12 bytes), collected MSB-first into byte buffer +//! - Level-aware state machine: HIGH pulses are saved, LOW pulses complete the pair +//! - Unscramble: NOT first 8 bytes, extract counter from bytes[4..5], compute XOR mask, apply to bytes[0..5] +//! - Field layout: serial = bytes[0..3] (32-bit), counter = bytes[4..5] (16-bit), button = byte[6] +//! - 868 MHz, FM modulation, decode-only (no encoder) + +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; +#[allow(dead_code)] +const MIN_COUNT_BIT: usize = 80; +const BIT_COUNT: usize = 96; +const DATA_BYTES: usize = 12; + +/// Decoder states (matches mitsubishi_v0.c MitsubishiDecoderStep) +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + DataSave, + DataCheck, +} + +/// Mitsubishi V0 protocol decoder +pub struct MitsubishiV0Decoder { + step: DecoderStep, + te_last: u32, + bit_count: usize, + decode_data: [u8; DATA_BYTES], +} + +impl MitsubishiV0Decoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + te_last: 0, + bit_count: 0, + decode_data: [0u8; DATA_BYTES], + } + } + + #[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 + } + + fn reset_payload(&mut self) { + self.bit_count = 0; + self.decode_data = [0u8; DATA_BYTES]; + } + + /// Collect a HIGH+LOW pair and decode one bit. + /// Short HIGH + Long LOW = bit 1; Long HIGH + Short LOW = bit 0. + /// Bits are stored MSB-first into the byte buffer. + fn collect_pair(&mut self, high: u32, low: u32) -> bool { + let bit_value; + + if Self::is_short(high) && Self::is_long(low) { + bit_value = true; + } else if Self::is_long(high) && Self::is_short(low) { + bit_value = false; + } else { + return false; + } + + let bit_index = self.bit_count; + if bit_index < BIT_COUNT { + if bit_value { + let byte_index = bit_index >> 3; + let bit_position = 7 - (bit_index & 0x07); + self.decode_data[byte_index] |= 1u8 << bit_position; + } + self.bit_count += 1; + } + + true + } + + /// Unscramble the payload (matches mitsubishi_unscramble_payload in C reference). + /// 1. Bitwise NOT first 8 bytes + /// 2. Extract counter from bytes[4..5] + /// 3. Compute masks from counter and XOR bytes[0..5] + fn unscramble_payload(payload: &mut [u8; DATA_BYTES]) { + // Step 1: NOT first 8 bytes + for i in 0..8 { + payload[i] = !payload[i]; + } + + // Step 2: Extract counter + let counter = ((payload[4] as u16) << 8) | (payload[5] as u16); + let hi = ((counter >> 8) & 0xFF) as u8; + let lo = (counter & 0xFF) as u8; + + // Step 3: Compute masks + let mask1 = (hi & 0xAA) | (lo & 0x55); + let mask2 = (lo & 0xAA) | (hi & 0x55); + let mask3 = mask1 ^ mask2; + + // Step 4: XOR bytes[0..5] with mask3 + for i in 0..5 { + payload[i] ^= mask3; + } + } + + /// Parse the unscrambled payload into a DecodedSignal. + fn publish_frame(&self) -> DecodedSignal { + let mut payload = self.decode_data; + Self::unscramble_payload(&mut payload); + + let serial = ((payload[0] as u32) << 24) + | ((payload[1] as u32) << 16) + | ((payload[2] as u32) << 8) + | (payload[3] as u32); + + let counter = ((payload[4] as u16) << 8) | (payload[5] as u16); + let button = payload[6]; + + // Store first 8 bytes (post-unscramble) as u64 data field + let data = ((payload[0] as u64) << 56) + | ((payload[1] as u64) << 48) + | ((payload[2] as u64) << 40) + | ((payload[3] as u64) << 32) + | ((payload[4] as u64) << 24) + | ((payload[5] as u64) << 16) + | ((payload[6] as u64) << 8) + | (payload[7] as u64); + + DecodedSignal { + serial: Some(serial), + button: Some(button), + counter: Some(counter), + crc_valid: true, + data, + data_count_bit: BIT_COUNT, + encoder_capable: false, + extra: None, + protocol_display_name: None, + } + } +} + +impl ProtocolDecoder for MitsubishiV0Decoder { + fn name(&self) -> &'static str { + "Mitsubishi V0" + } + + 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] { + &[868_350_000] + } + + fn reset(&mut self) { + self.step = DecoderStep::Reset; + self.te_last = 0; + self.reset_payload(); + } + + fn feed(&mut self, level: bool, duration: u32) -> Option { + match self.step { + DecoderStep::Reset => { + // Wait for a HIGH pulse to start + if level { + self.te_last = duration; + self.step = DecoderStep::DataCheck; + } + } + + DecoderStep::DataSave => { + if level { + // HIGH pulse: save duration and move to DataCheck + self.te_last = duration; + self.step = DecoderStep::DataCheck; + } else { + // LOW pulse without preceding HIGH data check: reset + self.step = DecoderStep::Reset; + self.reset_payload(); + } + } + + DecoderStep::DataCheck => { + if !level { + // LOW pulse: complete the HIGH+LOW pair + if self.collect_pair(self.te_last, duration) { + if self.bit_count >= BIT_COUNT { + // Full frame received + let result = self.publish_frame(); + self.reset_payload(); + self.step = DecoderStep::Reset; + return Some(result); + } else { + self.step = DecoderStep::DataSave; + } + } else { + // Invalid pair: reset + self.reset_payload(); + self.step = DecoderStep::Reset; + } + } else { + // Another HIGH pulse while expecting LOW: update te_last + self.te_last = duration; + } + } + } + + None + } + + fn supports_encoding(&self) -> bool { + false + } + + fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { + None + } +} + +impl Default for MitsubishiV0Decoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 76c4fcd..fd3723a 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -36,10 +36,14 @@ mod subaru; mod ford_v0; mod vag; mod fiat_v0; +mod fiat_v1; mod suzuki; mod scher_khan; mod star_line; mod psa; +mod mazda_v0; +mod mitsubishi_v0; +mod porsche_touareg; pub use common::DecodedSignal; @@ -109,11 +113,15 @@ impl ProtocolRegistry { Box::new(ford_v0::FordV0Decoder::new()), Box::new(subaru::SubaruDecoder::new()), Box::new(fiat_v0::FiatV0Decoder::new()), + Box::new(fiat_v1::FiatV1Decoder::new()), Box::new(suzuki::SuzukiDecoder::new()), Box::new(scher_khan::ScherKhanDecoder::new()), Box::new(star_line::StarLineDecoder::new()), Box::new(keeloq::KeeloqDecoder::new()), Box::new(psa::PsaDecoder::new()), + Box::new(mazda_v0::MazdaV0Decoder::new()), + Box::new(mitsubishi_v0::MitsubishiV0Decoder::new()), + Box::new(porsche_touareg::PorscheTouaregDecoder::new()), ]; Self { decoders } diff --git a/src/protocols/porsche_touareg.rs b/src/protocols/porsche_touareg.rs new file mode 100644 index 0000000..ebfbf65 --- /dev/null +++ b/src/protocols/porsche_touareg.rs @@ -0,0 +1,321 @@ +//! Porsche Touareg protocol decoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/porsche_touareg.c`. +//! Original implementation by @lupettohf +//! +//! Protocol characteristics: +//! - PWM encoding with Manchester-like 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" +//! - Frequencies: 433.92 MHz and 868.35 MHz (AM demodulation) + +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; + +/// Decoder states (matches PorscheCayenneDecoderStep in porsche_touareg.c) +#[derive(Debug, Clone, Copy, PartialEq)] +enum DecoderStep { + Reset, + Sync, + GapHigh, + GapLow, + Data, +} + +/// Porsche Touareg protocol decoder +pub struct PorscheTouaregDecoder { + 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_touareg.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. +/// +/// This is a direct port of `porsche_cayenne_compute_frame` from the C reference. +/// pkt[0..3] = plaintext header, pkt[4..7] = encrypted payload 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; + + let cnt = counter.wrapping_add(1); + let cnt_lo = (cnt & 0xFF) as u8; + let cnt_hi = ((cnt >> 8) & 0xFF) as u8; + + let mut r_h = b3; + let mut r_m = b1; + let mut r_l = b2; + + // Rotate 4 times initially + for _ in 0..4 { + rotate24(&mut r_h, &mut r_m, &mut r_l); + } + // Then rotate cnt_lo more times + for _ in 0..cnt_lo as u16 { + rotate24(&mut r_h, &mut r_m, &mut r_l); + } + + let a9a = r_h ^ b0; + + 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); + + 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 +} + +/// Parse raw 64-bit data into a DecodedSignal. +/// +/// Extracts serial (24-bit), button (4-bit), frame_type (3-bit), then brute-forces +/// the counter (1..=256) by calling compute_frame and comparing encrypted bytes. +fn parse_data(data: u64) -> DecodedSignal { + // Unpack 64-bit data into 8 bytes (big-endian) + let mut pkt = [0u8; 8]; + let mut raw = data; + for i in (0..8).rev() { + pkt[i] = (raw & 0xFF) as u8; + raw >>= 8; + } + + 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; + + // Brute-force counter recovery: try counter values 1..=256 + let mut counter: u16 = 0; + for try_cnt in 1u16..=256 { + 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] + { + counter = try_cnt; + break; + } + } + + // Determine frame type name for extra info + let frame_type_name = match frame_type { + 0x02 => "First", + 0x01 => "Cont", + 0x04 => "Final", + _ => "??", + }; + + DecodedSignal { + serial: Some(serial), + button: Some(btn), + counter: Some(counter), + crc_valid: counter != 0, + data, + data_count_bit: MIN_COUNT_BIT, + encoder_capable: false, + extra: Some(frame_type as u64), + protocol_display_name: Some(format!( + "Porsche Touareg [{}]", + frame_type_name + )), + } +} + +impl PorscheTouaregDecoder { + pub fn new() -> Self { + Self { + step: DecoderStep::Reset, + sync_count: 0, + raw_data: 0, + bit_count: 0, + te_last: 0, + } + } +} + +impl ProtocolDecoder for PorscheTouaregDecoder { + fn name(&self) -> &'static str { + "Porsche Touareg" + } + + 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, 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 { + // Keep collecting sync pairs -- HIGH sync pulse, stay in Sync + } 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 { + // HIGH pulse completes a bit pair + 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 result = parse_data(self.raw_data); + self.step = DecoderStep::Reset; + return Some(result); + } + } else { + // LOW pulse: save duration for the bit pair + self.te_last = duration; + } + } + } + + None + } + + fn supports_encoding(&self) -> bool { + false + } + + fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { + None + } +} + +impl Default for PorscheTouaregDecoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/psa.rs b/src/protocols/psa.rs index 483d8d1..f5be170 100644 --- a/src/protocols/psa.rs +++ b/src/protocols/psa.rs @@ -1,13 +1,14 @@ //! PSA (Peugeot/Citroën) protocol decoder/encoder //! //! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`. -//! Decode/encode logic (Manchester, preamble, TEA, XOR, mode 0x23/0x36) matches reference. //! //! Protocol characteristics: //! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble) -//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation -//! - TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds XOR layer -//! - Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule) +//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48) +//! - Modified TEA (XTEA-like) with dynamic key selection (sum&3, (sum>>11)&3) +//! - Mode 0x23: direct XOR decrypt with checksum validation +//! - Mode 0x36: TEA brute-force with BF1/BF2 key schedules (deferred) +//! - Dual preamble: Pattern 1 (250µs) and Pattern 2 (125µs) use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; use crate::duration_diff; @@ -21,7 +22,6 @@ const MIN_COUNT_BIT: usize = 128; // Internal timing for Manchester sub-symbol detection const TE_SHORT_125: u32 = 125; const TE_LONG_250: u32 = 250; -const TE_TOLERANCE_49: u32 = 49; const TE_TOLERANCE_50: u32 = 50; const TE_TOLERANCE_99: u32 = 99; const TE_END_1000: u32 = 1000; @@ -45,13 +45,19 @@ enum ManchesterState { Start1, } -/// Decoder states (matches protopirate's PsaDecoderState) +/// Decoder states (matches protopirate's PsaDecoderState 0-4) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderState { + /// State0: Wait for first edge (pattern detection) WaitEdge, - CountPattern, - DecodeManchester, - End, + /// State1: Count 250µs preamble patterns (Pattern 1) + CountPattern250, + /// State2: Manchester decode at 250/500µs (Pattern 1) + DecodeManchester250, + /// State3: Count 125µs preamble patterns (Pattern 2) + CountPattern125, + /// State4: Manchester decode at 125/250µs (Pattern 2) + DecodeManchester125, } /// PSA protocol decoder @@ -140,111 +146,268 @@ impl PsaDecoder { } } - /// TEA decrypt (matches psa.c / standard TEA) + /// Modified TEA decrypt with dynamic key selection (matches psa.c psa_tea_decrypt) + /// Uses XTEA-like key scheduling: key[sum&3] and key[(sum>>11)&3] fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) { let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS); for _ in 0..TEA_ROUNDS { - *v1 = v1.wrapping_sub( - (v0.wrapping_shl(4).wrapping_add(key[2])) - ^ (v0.wrapping_add(sum)) - ^ (v0.wrapping_shr(5).wrapping_add(key[3])), - ); - *v0 = v0.wrapping_sub( - (v1.wrapping_shl(4).wrapping_add(key[0])) - ^ (v1.wrapping_add(sum)) - ^ (v1.wrapping_shr(5).wrapping_add(key[1])), - ); + let k_idx2 = ((sum >> 11) & 3) as usize; + let temp = key[k_idx2].wrapping_add(sum); sum = sum.wrapping_sub(TEA_DELTA); + *v1 = v1.wrapping_sub( + temp ^ (v0.wrapping_shr(5) ^ v0.wrapping_shl(4)).wrapping_add(*v0), + ); + let k_idx1 = (sum & 3) as usize; + let temp = key[k_idx1].wrapping_add(sum); + *v0 = v0.wrapping_sub( + temp ^ (v1.wrapping_shr(5) ^ v1.wrapping_shl(4)).wrapping_add(*v1), + ); } } - /// TEA encrypt (matches psa.c / standard TEA) + /// Modified TEA encrypt with dynamic key selection (matches psa.c psa_tea_encrypt) fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) { let mut sum: u32 = 0; for _ in 0..TEA_ROUNDS { + let k_idx1 = (sum & 3) as usize; + let temp = key[k_idx1].wrapping_add(sum); sum = sum.wrapping_add(TEA_DELTA); *v0 = v0.wrapping_add( - (v1.wrapping_shl(4).wrapping_add(key[0])) - ^ (v1.wrapping_add(sum)) - ^ (v1.wrapping_shr(5).wrapping_add(key[1])), + temp ^ (v1.wrapping_shr(5) ^ v1.wrapping_shl(4)).wrapping_add(*v1), ); + let k_idx2 = ((sum >> 11) & 3) as usize; + let temp = key[k_idx2].wrapping_add(sum); *v1 = v1.wrapping_add( - (v0.wrapping_shl(4).wrapping_add(key[2])) - ^ (v0.wrapping_add(sum)) - ^ (v0.wrapping_shr(5).wrapping_add(key[3])), + temp ^ (v0.wrapping_shr(5) ^ v0.wrapping_shl(4)).wrapping_add(*v0), ); } } - /// XOR decrypt for mode 0x23 (matches psa.c) + /// XOR decrypt for mode 0x23 (matches psa.c psa_second_stage_xor_decrypt) + /// Uses psa_copy_reverse byte reordering then XOR operations fn xor_decrypt(buffer: &mut [u8]) { + // psa_copy_reverse: reorder source bytes + let temp = [ + buffer[5], // temp[0] = source[5] + buffer[4], // temp[1] = source[4] + buffer[3], // temp[2] = source[3] + buffer[2], // temp[3] = source[2] + buffer[9], // temp[4] = source[9] + buffer[8], // temp[5] = source[8] + buffer[7], // temp[6] = source[7] + buffer[6], // temp[7] = source[6] + ]; + buffer[2] = temp[0] ^ temp[6]; + buffer[3] = temp[2] ^ temp[0]; + buffer[4] = temp[6] ^ temp[3]; + buffer[5] = temp[7] ^ temp[1]; + buffer[6] = temp[3] ^ temp[1]; + buffer[7] = temp[6] ^ temp[4] ^ temp[5]; + } + + /// XOR encrypt for mode 0x23 encoding (matches psa.c psa_second_stage_xor_encrypt) + fn xor_encrypt(buffer: &mut [u8]) { let e6 = buffer[8]; let e7 = buffer[9]; - let e5 = buffer[7]; - let e0 = buffer[2]; - let e1 = buffer[3]; - let e2 = buffer[4]; - let e3 = buffer[5]; - let e4 = buffer[6]; + let p0 = buffer[2]; + let p1 = buffer[3]; + let p2 = buffer[4]; + let p3 = buffer[5]; + let p4 = buffer[6]; + let p5 = buffer[7]; - buffer[2] = e0 ^ e5; - buffer[3] = e1 ^ (e0 ^ e5 ^ e6 ^ e7); - buffer[4] = e2 ^ e0; - buffer[5] = e3 ^ (e0 ^ e5 ^ e6 ^ e7); - buffer[6] = e4 ^ e2; - buffer[7] = e5 ^ e6 ^ e7; + let ne5 = p5 ^ e7 ^ e6; + let ne0 = p2 ^ ne5; + let ne2 = p4 ^ ne0; + let ne4 = p3 ^ ne2; + let ne3 = p0 ^ ne5; + let ne1 = p1 ^ ne3; + + buffer[2] = ne0; + buffer[3] = ne1; + buffer[4] = ne2; + buffer[5] = ne3; + buffer[6] = ne4; + buffer[7] = ne5; } - /// Decrypt key1 + validation: mode 0x23 (TEA+XOR) or 0x36 (TEA, BF2) — matches psa.c + /// Calculate checksum over buffer[2..8] (matches psa.c psa_calculate_checksum) + fn calculate_checksum(buffer: &[u8]) -> u8 { + let mut checksum: u32 = 0; + for i in 2..8 { + checksum += (buffer[i] & 0xF) as u32 + ((buffer[i] >> 4) & 0xF) as u32; + } + ((checksum.wrapping_mul(0x10)) & 0xFF) as u8 + } + + /// Check if direct XOR is allowed by key2 high byte (matches psa.c) + 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 + } + + /// Setup byte buffer from key1/key2 (matches psa.c psa_setup_byte_buffer) + fn setup_byte_buffer(buffer: &mut [u8], key1_low: u32, key1_high: u32, key2_low: u32) { + for i in 0..8usize { + let shift = i * 8; + let byte_val = if shift < 32 { + ((key1_low >> shift) & 0xFF) as u8 + } else { + ((key1_high >> (shift - 32)) & 0xFF) as u8 + }; + buffer[7 - i] = byte_val; + } + buffer[9] = (key2_low & 0xFF) as u8; + buffer[8] = ((key2_low >> 8) & 0xFF) as u8; + } + + /// Decrypt using the C code's approach: setup_byte_buffer then attempt direct XOR + /// with checksum validation; mode 0x36 is marked for brute-force (matches psa.c) fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> { - // Try mode 0x23 first (seed byte 0x23) - let seed_byte = (self.key1_high >> 24) as u8; + // C: key2_low = decode_data_low (the 16-bit validation sits in the low word) + let key2_low = self.validation_field as u32; - if seed_byte >= 0x23 && seed_byte < 0x24 { - // Mode 0x23 - TEA + XOR - let mut v0 = self.key1_high; - let mut v1 = self.key1_low; - Self::tea_decrypt(&mut v0, &mut v1, &BF1_KEY_SCHEDULE); + let mut buffer = [0u8; 48]; + Self::setup_byte_buffer(&mut buffer, self.key1_low, self.key1_high, key2_low); - let mut buffer = [0u8; 10]; - buffer[0] = (v0 >> 24) as u8; - buffer[1] = (v0 >> 16) as u8; - buffer[2] = (v0 >> 8) as u8; - buffer[3] = (v0 >> 0) as u8; - buffer[4] = (v1 >> 24) as u8; - buffer[5] = (v1 >> 16) as u8; - buffer[6] = (v1 >> 8) as u8; - buffer[7] = (v1 >> 0) as u8; - buffer[8] = (self.validation_field >> 8) as u8; - buffer[9] = (self.validation_field & 0xFF) as u8; + let key2_high_byte = buffer[8]; - Self::xor_decrypt(&mut buffer); + // Try direct XOR decrypt (mode 0x23) if allowed by key2 filter + if Self::direct_xor_allowed_by_key2(key2_high_byte) { + let checksum = Self::calculate_checksum(&buffer); + let validation_result = (checksum ^ key2_high_byte) & 0xF0; - let serial = ((buffer[2] as u32) << 16) - | ((buffer[3] as u32) << 8) - | (buffer[4] as u32); - let counter = ((buffer[5] as u32) << 8) | (buffer[6] as u32); - let crc = buffer[7] as u16; - let btn = buffer[8] & 0x0F; + if validation_result == 0 { + // Direct XOR decrypt succeeded validation + Self::xor_decrypt(&mut buffer); - return Some((serial, btn, counter, crc, 0x23)); + let serial = ((buffer[3] as u32) << 8) + | ((buffer[2] as u32) << 16) + | (buffer[4] as u32); + let counter = (buffer[6] as u32) | ((buffer[5] as u32) << 8); + let crc = buffer[7] as u16; + let btn = buffer[8] & 0x0F; + + return Some((serial, btn, counter, crc, 0x23)); + } } - if seed_byte >= 0xF3 && seed_byte < 0xF4 { - // Mode 0x36 - TEA + different key schedule - let mut v0 = self.key1_high; - let mut v1 = self.key1_low; - Self::tea_decrypt(&mut v0, &mut v1, &BF2_KEY_SCHEDULE); + // Mode 0x36 - TEA brute-force path + // Try direct TEA decrypt with BF1 key schedule for a quick decode attempt + { + let mut w0 = ((buffer[3] as u32) << 16) + | ((buffer[2] as u32) << 24) + | ((buffer[4] as u32) << 8) + | (buffer[5] as u32); + let mut w1 = ((buffer[7] as u32) << 16) + | ((buffer[6] as u32) << 24) + | ((buffer[8] as u32) << 8) + | (buffer[9] as u32); - let serial = ((v0 >> 8) & 0xFFFF00) | ((v0 & 0xFF) as u32); - let counter = v1 >> 16; - let btn = ((v1 >> 8) & 0xF) as u8; - let crc = (v1 & 0xFF) as u16; + Self::tea_decrypt(&mut w0, &mut w1, &BF1_KEY_SCHEDULE); - return Some((serial, btn, counter, crc, 0x36)); + // Check if the TEA CRC validates (sum of bytes) + let crc_calc = Self::calculate_tea_crc(w0, w1); + if crc_calc == (w1 & 0xFF) as u8 { + let mut dec_buffer = [0u8; 48]; + dec_buffer[2] = ((w0 >> 24) & 0xFF) as u8; + dec_buffer[3] = ((w0 >> 16) & 0xFF) as u8; + dec_buffer[4] = ((w0 >> 8) & 0xFF) as u8; + dec_buffer[5] = (w0 & 0xFF) as u8; + dec_buffer[6] = ((w1 >> 24) & 0xFF) as u8; + dec_buffer[7] = ((w1 >> 16) & 0xFF) as u8; + dec_buffer[8] = ((w1 >> 8) & 0xFF) as u8; + dec_buffer[9] = (w1 & 0xFF) as u8; + + let btn = (dec_buffer[5] >> 4) & 0xF; + let serial = ((dec_buffer[3] as u32) << 8) + | ((dec_buffer[2] as u32) << 16) + | (dec_buffer[4] as u32); + let counter = ((dec_buffer[7] as u32) << 8) + | ((dec_buffer[6] as u32) << 16) + | (dec_buffer[8] as u32) + | (((dec_buffer[5] as u32) & 0xF) << 24); + let crc = dec_buffer[9] as u16; + + return Some((serial, btn, counter, crc, 0x36)); + } } - // Cannot decrypt - return raw data + // Also try BF2 key schedule directly (XOR-derived keys) + { + let mut w0 = ((buffer[3] as u32) << 16) + | ((buffer[2] as u32) << 24) + | ((buffer[4] as u32) << 8) + | (buffer[5] as u32); + let mut w1 = ((buffer[7] as u32) << 16) + | ((buffer[6] as u32) << 24) + | ((buffer[8] as u32) << 8) + | (buffer[9] as u32); + + Self::tea_decrypt(&mut w0, &mut w1, &BF2_KEY_SCHEDULE); + + let crc_calc = Self::calculate_tea_crc(w0, w1); + if crc_calc == (w1 & 0xFF) as u8 { + let mut dec_buffer = [0u8; 48]; + dec_buffer[2] = ((w0 >> 24) & 0xFF) as u8; + dec_buffer[3] = ((w0 >> 16) & 0xFF) as u8; + dec_buffer[4] = ((w0 >> 8) & 0xFF) as u8; + dec_buffer[5] = (w0 & 0xFF) as u8; + dec_buffer[6] = ((w1 >> 24) & 0xFF) as u8; + dec_buffer[7] = ((w1 >> 16) & 0xFF) as u8; + dec_buffer[8] = ((w1 >> 8) & 0xFF) as u8; + dec_buffer[9] = (w1 & 0xFF) as u8; + + let btn = (dec_buffer[5] >> 4) & 0xF; + let serial = ((dec_buffer[3] as u32) << 8) + | ((dec_buffer[2] as u32) << 16) + | (dec_buffer[4] as u32); + let counter = ((dec_buffer[7] as u32) << 8) + | ((dec_buffer[6] as u32) << 16) + | (dec_buffer[8] as u32) + | (((dec_buffer[5] as u32) & 0xF) << 24); + let crc = dec_buffer[9] as u16; + + return Some((serial, btn, counter, crc, 0x36)); + } + } + + None + } + + /// Calculate TEA CRC (matches psa.c psa_calculate_tea_crc) + fn calculate_tea_crc(v0: u32, v1: u32) -> u8 { + let mut crc: u32 = 0; + crc += (v0 >> 24) & 0xFF; + crc += (v0 >> 16) & 0xFF; + crc += (v0 >> 8) & 0xFF; + crc += v0 & 0xFF; + crc += (v1 >> 24) & 0xFF; + crc += (v1 >> 16) & 0xFF; + crc += (v1 >> 8) & 0xFF; + (crc & 0xFF) as u8 + } + + fn init_preamble_state(&mut self) { + self.data_low = 0; + self.data_high = 0; + self.pattern_counter = 0; + self.bit_count = 0; + self.manchester_state = ManchesterState::Mid1; + } + + fn finalize_frame(&mut self) -> Option { + self.state = DecoderState::WaitEdge; + if self.bit_count >= 80 { + // C validation: ((key1_high >> 16) & 0xF) == 0xA + if ((self.key1_high >> 16) & 0xF) != 0xA { + return None; + } + let result = self.parse_data(); + return Some(result); + } None } @@ -317,24 +480,96 @@ impl ProtocolDecoder for PsaDecoder { fn feed(&mut self, level: bool, duration: u32) -> Option { match self.state { + // State0: detect preamble pattern type DecoderState::WaitEdge => { - if level && duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_49 { - self.state = DecoderState::CountPattern; + if !level { + return None; + } + let diff_250 = duration_diff!(duration, TE_SHORT); + let diff_125 = duration_diff!(duration, TE_SHORT_125); + + if diff_250 < TE_TOLERANCE_99 { + // Pattern 1: 250µs preamble + self.init_preamble_state(); + self.state = DecoderState::CountPattern250; + } else if diff_125 < 40 && duration <= 180 { + // Pattern 2: 125µs preamble + self.init_preamble_state(); + self.state = DecoderState::CountPattern125; + } + self.prev_duration = duration; + } + + // State1: count 250µs preamble (Pattern 1) + DecoderState::CountPattern250 => { + if level { + return None; + } + let diff_short = duration_diff!(duration, TE_SHORT); + if diff_short < TE_TOLERANCE_99 + 1 { + let prev_diff = duration_diff!(self.prev_duration, TE_SHORT); + if prev_diff <= TE_TOLERANCE_99 { + self.pattern_counter += 1; + } self.prev_duration = duration; - self.pattern_counter = 0; + } else { + let diff_long = duration_diff!(duration, TE_LONG); + if diff_long < 100 && self.pattern_counter > 0x46 { + // Transition to Manchester decode at 250/500µs + self.state = DecoderState::DecodeManchester250; + self.data_low = 0; + self.data_high = 0; + self.bit_count = 0; + self.manchester_state = ManchesterState::Mid1; + self.pattern_counter = 0; + self.prev_duration = duration; + } else { + self.state = DecoderState::WaitEdge; + self.pattern_counter = 0; + } } } - DecoderState::CountPattern => { + // State2: Manchester decode at 250/500µs (Pattern 1) + DecoderState::DecodeManchester250 => { + if self.bit_count >= 121 { + return self.finalize_frame(); + } + // Check for end-of-frame marker + if level && self.bit_count == 80 && duration >= 800 { + let end_diff = duration_diff!(duration, TE_END_1000); + if end_diff <= 199 { + return self.finalize_frame(); + } + } + let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA; + let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA; + + if duration > 10000 { + self.state = DecoderState::WaitEdge; + self.pattern_counter = 0; + return None; + } + + if is_short || is_long { + if let Some(bit) = self.manchester_advance(is_short, level) { + self.add_bit(bit); + } + } + self.prev_duration = duration; + } + + // State3: count 125µs preamble (Pattern 2) + DecoderState::CountPattern125 => { let diff_125 = duration_diff!(duration, TE_SHORT_125); let diff_250 = duration_diff!(duration, TE_LONG_250); if diff_125 < TE_TOLERANCE_50 { self.pattern_counter += 1; self.prev_duration = duration; - } else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x46 { - // Found end of preamble, start Manchester decoding - self.state = DecoderState::DecodeManchester; + } else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x45 { + // Transition to Manchester decode at 125/250µs + self.state = DecoderState::DecodeManchester125; self.data_low = 0; self.data_high = 0; self.bit_count = 0; @@ -347,23 +582,17 @@ impl ProtocolDecoder for PsaDecoder { } } - DecoderState::DecodeManchester => { - let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA; - let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA; - let is_end = duration > TE_END_1000; + // State4: Manchester decode at 125/250µs (Pattern 2) + DecoderState::DecodeManchester125 => { + if self.bit_count >= 121 { + return self.finalize_frame(); + } + let is_short = duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_50; + let is_long = duration_diff!(duration, TE_LONG_250) < TE_TOLERANCE_99; + let is_end = duration > 500; - if is_end || self.bit_count >= 121 { - // End of data - self.state = DecoderState::End; - - if self.bit_count >= 96 { - // Got enough data - let result = self.parse_data(); - self.state = DecoderState::WaitEdge; - return Some(result); - } - self.state = DecoderState::WaitEdge; - return None; + if is_end { + return self.finalize_frame(); } if is_short || is_long { @@ -373,13 +602,8 @@ impl ProtocolDecoder for PsaDecoder { } else { self.state = DecoderState::WaitEdge; } - self.prev_duration = duration; } - - DecoderState::End => { - self.state = DecoderState::WaitEdge; - } } None @@ -406,31 +630,8 @@ impl ProtocolDecoder for PsaDecoder { buffer[8] = button & 0x0F; buffer[9] = 0; - // XOR encrypt - { - let e6 = buffer[8]; - let e7 = buffer[9]; - let p0 = buffer[2]; - let p1 = buffer[3]; - let p2 = buffer[4]; - let p3 = buffer[5]; - let p4 = buffer[6]; - let p5 = buffer[7]; - - let ne5 = p5 ^ e7 ^ e6; - let ne0 = p2 ^ ne5; - let ne2 = p4 ^ ne0; - let ne4 = p3 ^ ne2; - let ne3 = p0 ^ ne5; - let ne1 = p1 ^ ne3; - - buffer[2] = ne0; - buffer[3] = ne1; - buffer[4] = ne2; - buffer[5] = ne3; - buffer[6] = ne4; - buffer[7] = ne5; - } + // XOR encrypt (matches psa.c psa_second_stage_xor_encrypt) + Self::xor_encrypt(&mut buffer); // TEA encrypt let mut v0 = ((buffer[0] as u32) << 24) @@ -450,38 +651,42 @@ impl ProtocolDecoder for PsaDecoder { let mut signal = Vec::with_capacity(512); - // Preamble + sync (matches protopirate psa encode) - for _ in 0..70 { - signal.push(LevelDuration::new(true, TE_SHORT_125)); - signal.push(LevelDuration::new(false, TE_SHORT_125)); + // Preamble: 80 iterations at 250us HIGH+LOW (matches C: te = PSA_TE_LONG_250) + for _ in 0..80 { + signal.push(LevelDuration::new(true, TE_LONG_250)); + signal.push(LevelDuration::new(false, TE_LONG_250)); } - signal.push(LevelDuration::new(true, TE_LONG_250)); + + // Sync transition: LOW 250us, HIGH 500us, LOW 250us (matches C) + signal.push(LevelDuration::new(false, TE_LONG_250)); + signal.push(LevelDuration::new(true, TE_LONG)); signal.push(LevelDuration::new(false, TE_LONG_250)); - // Key1: 64 bits Manchester, then validation 16 bits + // Key1: 64 bits Manchester at 250us (C: bit 1 = true,false; bit 0 = false,true) let key1 = ((key1_high as u64) << 32) | (key1_low as u64); for bit in (0..64).rev() { if (key1 >> bit) & 1 == 1 { - signal.push(LevelDuration::new(false, TE_SHORT)); - signal.push(LevelDuration::new(true, TE_SHORT)); + signal.push(LevelDuration::new(true, TE_LONG_250)); + signal.push(LevelDuration::new(false, TE_LONG_250)); } else { - signal.push(LevelDuration::new(true, TE_SHORT)); - signal.push(LevelDuration::new(false, TE_SHORT)); + signal.push(LevelDuration::new(false, TE_LONG_250)); + signal.push(LevelDuration::new(true, TE_LONG_250)); } } - // Validation: 16 bits Manchester encoded + // Validation: 16 bits Manchester at 250us for bit in (0..16).rev() { if (validation >> bit) & 1 == 1 { - signal.push(LevelDuration::new(false, TE_SHORT)); - signal.push(LevelDuration::new(true, TE_SHORT)); + signal.push(LevelDuration::new(true, TE_LONG_250)); + signal.push(LevelDuration::new(false, TE_LONG_250)); } else { - signal.push(LevelDuration::new(true, TE_SHORT)); - signal.push(LevelDuration::new(false, TE_SHORT)); + signal.push(LevelDuration::new(false, TE_LONG_250)); + signal.push(LevelDuration::new(true, TE_LONG_250)); } } - // End marker + // End marker: HIGH 1000us + LOW 1000us (matches C) + signal.push(LevelDuration::new(true, TE_END_1000)); signal.push(LevelDuration::new(false, TE_END_1000)); Some(signal)