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.
This commit is contained in:
KaraZajac
2026-03-22 12:05:01 -04:00
parent 84b75ee6b5
commit 48ee413c9b
22 changed files with 2318 additions and 305 deletions
Generated
+1 -1
View File
@@ -444,7 +444,7 @@ dependencies = [
[[package]] [[package]]
name = "kat" name = "kat"
version = "1.1.3" version = "1.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atty", "atty",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "kat" name = "kat"
version = "1.1.3" version = "1.2.0"
edition = "2021" edition = "2021"
description = "Keyfob Analysis Toolkit - HackRF/RTL-SDR signal capture, decode, and transmit (HackRF only)" description = "Keyfob Analysis Toolkit - HackRF/RTL-SDR signal capture, decode, and transmit (HackRF only)"
authors = ["KAT Team"] authors = ["KAT Team"]
+13 -6
View File
@@ -17,7 +17,7 @@ A terminal-based RF signal analysis tool for capturing, decoding, and retransmit
## Features ## 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) - **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 V0V6, 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 V0V6, 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 doesnt 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 - **KeeLoq generic fallback** — when a signal doesnt 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 - **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 - **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 V2 | Manchester | FM | Fixed Code | 315 / 433.92 MHz |
| Kia V3/V4 | PWM | AM/FM | KeeLoq | 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 V5 | Manchester | FM | Fixed Code | 433.92 MHz |
| Kia V6 | Manchester | FM | Fixed Code | 433.92 MHz | | Kia V6 | Manchester | FM | AES-128 | 433.92 MHz |
| Ford V0 | Manchester | FM | Fixed Code | 433.92 MHz | | Ford V0 | Manchester | FM | Rolling Code | 315 / 433.92 MHz |
| Fiat V0 | Manchester | FM | Fixed Code | 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 | | Subaru | PWM | AM | Rolling Code | 433.92 MHz |
| Suzuki | 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 | | Scher-Khan | PWM | FM | Magic Code | 433.92 MHz |
| Star Line | PWM | AM | KeeLoq | 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). **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 ### 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). - **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) - **AUT64** — 12-round block cipher for VAG type 1/3/4 (aut64)
- **Keystore** — manufacturer keys (Kia, VAG, etc.) built in; see `src/keystore/` - **Keystore** — manufacturer keys (Kia, VAG, etc.) built in; see `src/keystore/`
@@ -302,7 +308,8 @@ src/
│ ├── aut64.rs # AUT64 block cipher (VAG) │ ├── aut64.rs # AUT64 block cipher (VAG)
│ ├── keys.rs # Key loading (embedded + optional file), KIA/VAG │ ├── keys.rs # Key loading (embedded + optional file), KIA/VAG
│ ├── kia_v0..kia_v6.rs │ ├── 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) │ ├── vag.rs # VAG decoder/encoder (4 sub-types; encode from capture via extra)
│ ├── scher_khan.rs, star_line.rs, psa.rs │ ├── scher_khan.rs, star_line.rs, psa.rs
│ └── ... │ └── ...
+4
View File
@@ -16,6 +16,10 @@ This folder describes how each keyfob protocol supported by KAT works. Each docu
| Subaru | `subaru.rs` | [subaru.md](subaru.md) | | Subaru | `subaru.rs` | [subaru.md](subaru.md) |
| VAG | `vag.rs` | [vag.md](vag.md) | | VAG | `vag.rs` | [vag.md](vag.md) |
| Fiat V0 | `fiat_v0.rs` | [fiat_v0.md](fiat_v0.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) | | Suzuki | `suzuki.rs` | [suzuki.md](suzuki.md) |
| Scher-Khan | `scher_khan.rs` | [scher_khan.md](scher_khan.md) | | Scher-Khan | `scher_khan.rs` | [scher_khan.md](scher_khan.md) |
| Star Line | `star_line.rs` | [star_line.md](star_line.md) | | Star Line | `star_line.rs` | [star_line.md](star_line.md) |
+60
View File
@@ -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.
+2 -2
View File
@@ -9,7 +9,7 @@ layout: default
## Overview ## 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 ## Timing
@@ -28,7 +28,7 @@ Manchester: symbol duration short or long; bit value from transition direction.
- 32 bits: serial - 32 bits: serial
- 8 bits: button - 8 bits: button
- 12 bits: counter - 12 bits: counter
- 4 bits: CRC4 (checksum with offset rules) - 4 bits: CRC4 (XOR of nibbles, 7 bytes, offset 1)
## Decoder Steps ## Decoder Steps
+2 -2
View File
@@ -30,7 +30,7 @@ Manchester (event mapping 0/2/4/6 for level convention). Three 64/64/16-bit segm
- Part2: 64 bits (inverted) - Part2: 64 bits (inverted)
- Part3: 16 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 ## Decoder Steps
@@ -40,7 +40,7 @@ AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH
## Encoder ## 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 ## Frequencies
+73
View File
@@ -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.
+59
View File
@@ -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.
+72
View File
@@ -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.
+44 -15
View File
@@ -2,27 +2,32 @@
layout: default 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` **Reference:** `REFERENCES/ProtoPirate/protocols/psa.c`
## Overview ## 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 ## Timing
| Parameter | Value | Notes | | Parameter | Value | Notes |
|------------|--------|---------| |-------------|--------|---------|
| Symbol short | 250 µs | ±100 µs | | Symbol short | 250 us | +/-100 us |
| Symbol long | 500 µs | ±100 µs | | Symbol long | 500 us | +/-100 us |
| Preamble | 125/250 µs sub-symbols | | | 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 | | | 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) ## Frame Layout (128 bits)
@@ -30,18 +35,42 @@ Manchester; preamble uses 125/250 µs; then 250/500 µs symbols. TEA encrypt; mo
- validation: 16 bits - validation: 16 bits
- key2/rest: 48 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 ## Decoder Steps
1. **WaitEdge** — Wait for edge/preamble. 1. **WaitEdge (State0)** -- Detect preamble pattern type from first HIGH pulse (250 us -> Pattern 1, 125 us -> Pattern 2).
2. **CountPattern** Count preamble pattern (125/250 µs). 2. **CountPattern250 (State1)** -- Count 250 us preamble pairs; on long pulse with count > 70, transition to Manchester decode.
3. **DecodeManchester** Manchester decode 128 bits; TEA decrypt; apply mode (0x23 XOR or 0x36); extract fields. 3. **DecodeManchester250 (State2)** -- Manchester decode at 250/500 us. End on 1000 us marker or 121+ bits.
4. **End** — End marker (e.g. 1000 µs); return decode. 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 ## 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 ## Frequencies
+56 -66
View File
@@ -4,9 +4,9 @@
//! `fiat_v0.h`. Preamble: count short pulses (HIGH or LOW, 200±100µs); when preamble_count >= 150 //! `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 //! (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 //! (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, //! 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 6 btn bits (btn_to_send = btn >> 1); end //! last LOW replaced by 800µs gap; 64 data bits then 7 endbyte bits (endbyte & 0x7F); end
//! marker te_short*8 LOW. //! marker te_short*4 LOW.
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff; use crate::duration_diff;
@@ -294,8 +294,9 @@ impl ProtocolDecoder for FiatV0Decoder {
self.data_high = 0; self.data_high = 0;
} }
if self.bit_count > 0x46 { if self.bit_count == 0x47 {
self.btn = ((self.data_low << 1) | 1) as u8; // C: endbyte = (uint8_t)data_low (no transform)
self.btn = self.data_low as u8;
let result = self.parse_data(); let result = self.parse_data();
self.data_low = 0; self.data_low = 0;
self.data_high = 0; self.data_high = 0;
@@ -304,6 +305,19 @@ impl ProtocolDecoder for FiatV0Decoder {
return Some(result); 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; self.te_last = duration;
} }
@@ -319,80 +333,56 @@ impl ProtocolDecoder for FiatV0Decoder {
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> { fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?; let serial = decoded.serial?;
let cnt = decoded.counter.unwrap_or(0) as u32; 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 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 mut signal = Vec::with_capacity(1024);
let te_short = TE_SHORT;
let te_long = TE_LONG;
for burst in 0..TOTAL_BURSTS { for burst in 0..TOTAL_BURSTS {
if burst > 0 { if burst > 0 {
signal.push(LevelDuration::new(false, INTER_BURST_GAP)); signal.push(LevelDuration::new(false, INTER_BURST_GAP));
} }
// Preamble: HIGH-LOW pairs; last LOW replaced by gap (ref) // Preamble: alternating short pulses; last LOW extended to gap
for i in 0..PREAMBLE_PAIRS { for _ in 0..PREAMBLE_PAIRS {
signal.push(LevelDuration::new(true, te_short)); 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));
} }
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) Some(signal)
+435
View File
@@ -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<bool> {
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<DecodedSignal> {
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<Vec<LevelDuration>> {
None
}
}
impl Default for FiatV1Decoder {
fn default() -> Self {
Self::new()
}
}
+23 -10
View File
@@ -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 { fn calculate_bs(count: u32, button: u8, bs_magic: u8) -> u8 {
let result: u16 = (count as u16 & 0xFF) let result: u16 = (count as u16 & 0xFF)
.wrapping_add(bs_magic as u16) .wrapping_add(bs_magic as u16)
@@ -651,21 +667,18 @@ impl ProtocolDecoder for FordV0Decoder {
decoded.counter.unwrap_or(0) as u32 decoded.counter.unwrap_or(0) as u32
}) & 0xFFFFF; // 20-bit }) & 0xFFFFF; // 20-bit
// Use stored bs_magic (or default to 0x6F for backward compatibility) // Calculate checksum from serial + count + button (matches ford_v0_calculate_checksum in C)
let bs_magic = if self.bs_magic != 0 { self.bs_magic } else { 0x6F }; let checksum = Self::calculate_checksum(serial, count, button);
// Calculate BS from count + button + bs_magic (matches C ford_v0_calculate_bs)
let bs = Self::calculate_bs(count, button, bs_magic);
// Extract header byte from the original key1 (first byte) // Extract header byte from the original key1 (first byte)
let header_byte = (decoded.data >> 56) as u8; let header_byte = (decoded.data >> 56) as u8;
// Encode key1 from fields (same count, new button) // 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 // Calculate CRC for key2
let crc = Self::calculate_crc_for_tx(new_key1, bs); let crc = Self::calculate_crc_for_tx(new_key1, checksum);
let new_key2 = ((bs as u16) << 8) | (crc as u16); 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) // Build one 6-burst block and repeat TX_REPEAT times (matches reference encoder.repeat = 10)
let single = Self::build_upload(new_key1, new_key2); let single = Self::build_upload(new_key1, new_key2);
+19 -29
View File
@@ -7,7 +7,7 @@
//! - Manchester encoding: 800/1600µs timing //! - Manchester encoding: 800/1600µs timing
//! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC) //! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC)
//! - Long preamble of ~90 long pairs //! - 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 super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration; use crate::radio::demodulator::LevelDuration;
@@ -66,13 +66,14 @@ impl KiaV1Decoder {
(crc.wrapping_add(offset)) & 0x0F (crc.wrapping_add(offset)) & 0x0F
} }
/// Manchester state machine /// Manchester state machine (Flipper convention: level ? ShortLow : ShortHigh)
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> { fn manchester_advance(&mut self, is_short: bool, level: bool) -> Option<bool> {
let event = match (is_short, is_high) { // C: event = level ? ManchesterEventShortLow : ManchesterEventShortHigh (inverted)
(true, false) => 0, // Short Low let event = match (is_short, level) {
(true, true) => 1, // Short High (true, true) => 0, // level=true → ShortLow
(false, false) => 2, // Long Low (true, false) => 1, // level=false → ShortHigh
(false, true) => 3, // Long High (false, true) => 2, // level=true → LongLow
(false, false) => 3, // level=false → LongHigh
}; };
let (new_state, output) = match (self.manchester_state, event) { let (new_state, output) = match (self.manchester_state, event) {
@@ -115,15 +116,9 @@ impl KiaV1Decoder {
char_data[4] = button; char_data[4] = button;
char_data[5] = (counter & 0xFF) as u8; char_data[5] = (counter & 0xFF) as u8;
let crc = if cnt_high == 0 { // CRC4: always 7 bytes with offset 1 (matches updated ProtoPirate kia_v1.c)
let offset = if counter >= 0x098 { button } else { 1 }; char_data[6] = cnt_high as u8;
Self::crc4(&char_data[..6], offset) let crc = Self::crc4(&char_data, 1);
} else if cnt_high >= 0x6 {
char_data[6] = cnt_high as u8;
Self::crc4(&char_data, 1)
} else {
Self::crc4(&char_data[..6], 1)
};
DecodedSignal { DecodedSignal {
serial: Some(serial), serial: Some(serial),
@@ -194,8 +189,9 @@ impl ProtocolDecoder for KiaV1Decoder {
if self.header_count > 70 { if self.header_count > 70 {
if !level && is_short && duration_diff!(self.te_last, TE_LONG) < TE_DELTA { if !level && is_short && duration_diff!(self.te_last, TE_LONG) < TE_DELTA {
self.decode_count_bit = 1; // C: decode_count_bit=1, then add_bit(1) increments to 2
self.decode_data = 1; // Add first bit self.decode_count_bit = 2;
self.decode_data = 1;
self.header_count = 0; self.header_count = 0;
self.step = DecoderStep::DecodeData; self.step = DecoderStep::DecodeData;
} }
@@ -218,7 +214,7 @@ impl ProtocolDecoder for KiaV1Decoder {
return None; return None;
} }
if self.decode_count_bit >= MIN_COUNT_BIT { if self.decode_count_bit == MIN_COUNT_BIT {
let result = self.parse_data(); let result = self.parse_data();
self.step = DecoderStep::Reset; self.step = DecoderStep::Reset;
return Some(result); return Some(result);
@@ -247,15 +243,9 @@ impl ProtocolDecoder for KiaV1Decoder {
char_data[4] = button; char_data[4] = button;
char_data[5] = (counter & 0xFF) as u8; char_data[5] = (counter & 0xFF) as u8;
let crc = if cnt_high == 0 { // CRC4: always 7 bytes with offset 1 (matches updated ProtoPirate kia_v1.c)
let offset = if counter >= 0x098 { button } else { 1 }; char_data[6] = cnt_high;
Self::crc4(&char_data[..6], offset) let crc = Self::crc4(&char_data, 1);
} else if cnt_high >= 0x6 {
char_data[6] = cnt_high;
Self::crc4(&char_data, 1)
} else {
Self::crc4(&char_data[..6], 1)
};
// Build data // Build data
let data: u64 = ((serial as u64) << 24) | let data: u64 = ((serial as u64) << 24) |
+16 -15
View File
@@ -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 { fn calculate_crc(data: u64) -> u8 {
let serial = ((data >> 20) & 0xFFFFFFFF) as u32; // C code: data_without_crc = data >> 4; then read 6 sequential bytes (bits 4..51)
let u_var4 = (data & 0xFFFFFFFF) as u32; let data_without_crc = data >> 4;
let mut bytes = [0u8; 6]; let mut bytes = [0u8; 6];
bytes[0] = (u_var4 >> 20) as u8; bytes[0] = (data_without_crc & 0xFF) as u8;
bytes[1] = ((u_var4 >> 28) | ((serial & 0x0F) << 4)) as u8; bytes[1] = ((data_without_crc >> 8) & 0xFF) as u8;
bytes[2] = (serial >> 4) as u8; bytes[2] = ((data_without_crc >> 16) & 0xFF) as u8;
bytes[3] = (serial >> 12) as u8; bytes[3] = ((data_without_crc >> 24) & 0xFF) as u8;
bytes[4] = (u_var4 >> 4) as u8; bytes[4] = ((data_without_crc >> 32) & 0xFF) as u8;
bytes[5] = (u_var4 >> 12) as u8; bytes[5] = ((data_without_crc >> 40) & 0xFF) as u8;
let mut crc: u8 = 0; let mut crc: u8 = 0;
for &byte in &bytes { for &byte in &bytes {
@@ -181,19 +180,21 @@ impl ProtocolDecoder for KiaV2Decoder {
self.te_last = duration; self.te_last = duration;
self.header_count += 1; self.header_count += 1;
} else if is_short && self.header_count >= 100 { } 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.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 1;
self.step = DecoderStep::CollectRawBits;
self.decode_data = 1; // First bit self.decode_data = 1; // First bit
self.decode_count_bit = 2;
self.step = DecoderStep::CollectRawBits;
} else { } else {
self.step = DecoderStep::Reset; self.te_last = duration; // C stays in CheckPreamble, updates te_last
} }
} else { } else {
if is_long { if is_long {
self.header_count += 1; self.header_count += 1;
self.te_last = duration; 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; self.step = DecoderStep::Reset;
} }
} }
+197 -7
View File
@@ -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`. //! 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. //! 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: //! Protocol characteristics:
//! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance) //! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance)
@@ -275,6 +275,181 @@ impl KiaV6Decoder {
*data = state; *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<LevelDuration> {
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<LevelDuration>, 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) /// AES-128 key from V6 keystores A+B with XOR_MASK_LOW/HIGH (matches kia_v6.c)
fn get_aes_key() -> [u8; 16] { fn get_aes_key() -> [u8; 16] {
let keystore_a = Self::get_keystore_a(); let keystore_a = Self::get_keystore_a();
@@ -306,6 +481,13 @@ impl KiaV6Decoder {
aes_key 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 /// Decrypt 16-byte block: byte layout matches kia_v6.c; AES-128 then CRC8 check
fn decrypt(&self) -> Option<(u32, u8, u32, bool)> { fn decrypt(&self) -> Option<(u32, u8, u32, bool)> {
let mut encrypted_data = [0u8; 16]; let mut encrypted_data = [0u8; 16];
@@ -528,6 +710,7 @@ impl ProtocolDecoder for KiaV6Decoder {
if let Some((serial, button, counter, crc_valid)) = self.decrypt() { if let Some((serial, button, counter, crc_valid)) = self.decrypt() {
let key_data = ((self.stored_part1_high as u64) << 32) | let key_data = ((self.stored_part1_high as u64) << 32) |
(self.stored_part1_low as u64); (self.stored_part1_low as u64);
let fx_field = self.extract_fx_field();
self.step = DecoderStep::Reset; self.step = DecoderStep::Reset;
return Some(DecodedSignal { return Some(DecodedSignal {
@@ -537,8 +720,8 @@ impl ProtocolDecoder for KiaV6Decoder {
crc_valid, crc_valid,
data: key_data, data: key_data,
data_count_bit: MIN_COUNT_BIT, data_count_bit: MIN_COUNT_BIT,
encoder_capable: false, encoder_capable: true,
extra: None, extra: Some(fx_field as u64),
protocol_display_name: None, protocol_display_name: None,
}); });
} }
@@ -552,11 +735,18 @@ impl ProtocolDecoder for KiaV6Decoder {
} }
fn supports_encoding(&self) -> bool { fn supports_encoding(&self) -> bool {
false // V6 is decode-only true
} }
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> { fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
None // V6 decode-only in protopirate 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))
} }
} }
+312
View File
@@ -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<DecodedSignal> {
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<DecodedSignal> {
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<Vec<LevelDuration>> {
None
}
}
impl Default for MazdaV0Decoder {
fn default() -> Self {
Self::new()
}
}
+244
View File
@@ -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<DecodedSignal> {
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<Vec<LevelDuration>> {
None
}
}
impl Default for MitsubishiV0Decoder {
fn default() -> Self {
Self::new()
}
}
+8
View File
@@ -36,10 +36,14 @@ mod subaru;
mod ford_v0; mod ford_v0;
mod vag; mod vag;
mod fiat_v0; mod fiat_v0;
mod fiat_v1;
mod suzuki; mod suzuki;
mod scher_khan; mod scher_khan;
mod star_line; mod star_line;
mod psa; mod psa;
mod mazda_v0;
mod mitsubishi_v0;
mod porsche_touareg;
pub use common::DecodedSignal; pub use common::DecodedSignal;
@@ -109,11 +113,15 @@ impl ProtocolRegistry {
Box::new(ford_v0::FordV0Decoder::new()), Box::new(ford_v0::FordV0Decoder::new()),
Box::new(subaru::SubaruDecoder::new()), Box::new(subaru::SubaruDecoder::new()),
Box::new(fiat_v0::FiatV0Decoder::new()), Box::new(fiat_v0::FiatV0Decoder::new()),
Box::new(fiat_v1::FiatV1Decoder::new()),
Box::new(suzuki::SuzukiDecoder::new()), Box::new(suzuki::SuzukiDecoder::new()),
Box::new(scher_khan::ScherKhanDecoder::new()), Box::new(scher_khan::ScherKhanDecoder::new()),
Box::new(star_line::StarLineDecoder::new()), Box::new(star_line::StarLineDecoder::new()),
Box::new(keeloq::KeeloqDecoder::new()), Box::new(keeloq::KeeloqDecoder::new()),
Box::new(psa::PsaDecoder::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 } Self { decoders }
+321
View File
@@ -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<DecodedSignal> {
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<Vec<LevelDuration>> {
None
}
}
impl Default for PorscheTouaregDecoder {
fn default() -> Self {
Self::new()
}
}
+352 -147
View File
@@ -1,13 +1,14 @@
//! PSA (Peugeot/Citroën) protocol decoder/encoder //! PSA (Peugeot/Citroën) protocol decoder/encoder
//! //!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`. //! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`.
//! Decode/encode logic (Manchester, preamble, TEA, XOR, mode 0x23/0x36) matches reference.
//! //!
//! Protocol characteristics: //! Protocol characteristics:
//! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble) //! - 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 //! - 128 bits total: key1 (64) + validation (16) + key2/rest (48)
//! - TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds XOR layer //! - Modified TEA (XTEA-like) with dynamic key selection (sum&3, (sum>>11)&3)
//! - Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule) //! - 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 super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff; use crate::duration_diff;
@@ -21,7 +22,6 @@ const MIN_COUNT_BIT: usize = 128;
// Internal timing for Manchester sub-symbol detection // Internal timing for Manchester sub-symbol detection
const TE_SHORT_125: u32 = 125; const TE_SHORT_125: u32 = 125;
const TE_LONG_250: u32 = 250; const TE_LONG_250: u32 = 250;
const TE_TOLERANCE_49: u32 = 49;
const TE_TOLERANCE_50: u32 = 50; const TE_TOLERANCE_50: u32 = 50;
const TE_TOLERANCE_99: u32 = 99; const TE_TOLERANCE_99: u32 = 99;
const TE_END_1000: u32 = 1000; const TE_END_1000: u32 = 1000;
@@ -45,13 +45,19 @@ enum ManchesterState {
Start1, Start1,
} }
/// Decoder states (matches protopirate's PsaDecoderState) /// Decoder states (matches protopirate's PsaDecoderState 0-4)
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderState { enum DecoderState {
/// State0: Wait for first edge (pattern detection)
WaitEdge, WaitEdge,
CountPattern, /// State1: Count 250µs preamble patterns (Pattern 1)
DecodeManchester, CountPattern250,
End, /// 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 /// 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]) { fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS); let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
for _ in 0..TEA_ROUNDS { for _ in 0..TEA_ROUNDS {
*v1 = v1.wrapping_sub( let k_idx2 = ((sum >> 11) & 3) as usize;
(v0.wrapping_shl(4).wrapping_add(key[2])) let temp = key[k_idx2].wrapping_add(sum);
^ (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])),
);
sum = sum.wrapping_sub(TEA_DELTA); 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]) { fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
let mut sum: u32 = 0; let mut sum: u32 = 0;
for _ in 0..TEA_ROUNDS { 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); sum = sum.wrapping_add(TEA_DELTA);
*v0 = v0.wrapping_add( *v0 = v0.wrapping_add(
(v1.wrapping_shl(4).wrapping_add(key[0])) temp ^ (v1.wrapping_shr(5) ^ v1.wrapping_shl(4)).wrapping_add(*v1),
^ (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);
*v1 = v1.wrapping_add( *v1 = v1.wrapping_add(
(v0.wrapping_shl(4).wrapping_add(key[2])) temp ^ (v0.wrapping_shr(5) ^ v0.wrapping_shl(4)).wrapping_add(*v0),
^ (v0.wrapping_add(sum))
^ (v0.wrapping_shr(5).wrapping_add(key[3])),
); );
} }
} }
/// 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]) { 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 e6 = buffer[8];
let e7 = buffer[9]; let e7 = buffer[9];
let e5 = buffer[7]; let p0 = buffer[2];
let e0 = buffer[2]; let p1 = buffer[3];
let e1 = buffer[3]; let p2 = buffer[4];
let e2 = buffer[4]; let p3 = buffer[5];
let e3 = buffer[5]; let p4 = buffer[6];
let e4 = buffer[6]; let p5 = buffer[7];
buffer[2] = e0 ^ e5; let ne5 = p5 ^ e7 ^ e6;
buffer[3] = e1 ^ (e0 ^ e5 ^ e6 ^ e7); let ne0 = p2 ^ ne5;
buffer[4] = e2 ^ e0; let ne2 = p4 ^ ne0;
buffer[5] = e3 ^ (e0 ^ e5 ^ e6 ^ e7); let ne4 = p3 ^ ne2;
buffer[6] = e4 ^ e2; let ne3 = p0 ^ ne5;
buffer[7] = e5 ^ e6 ^ e7; 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)> { fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> {
// Try mode 0x23 first (seed byte 0x23) // C: key2_low = decode_data_low (the 16-bit validation sits in the low word)
let seed_byte = (self.key1_high >> 24) as u8; let key2_low = self.validation_field as u32;
if seed_byte >= 0x23 && seed_byte < 0x24 { let mut buffer = [0u8; 48];
// Mode 0x23 - TEA + XOR Self::setup_byte_buffer(&mut buffer, self.key1_low, self.key1_high, key2_low);
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; 10]; let key2_high_byte = buffer[8];
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;
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) if validation_result == 0 {
| ((buffer[3] as u32) << 8) // Direct XOR decrypt succeeded validation
| (buffer[4] as u32); Self::xor_decrypt(&mut buffer);
let counter = ((buffer[5] as u32) << 8) | (buffer[6] as u32);
let crc = buffer[7] as u16;
let btn = buffer[8] & 0x0F;
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 brute-force path
// Mode 0x36 - TEA + different key schedule // Try direct TEA decrypt with BF1 key schedule for a quick decode attempt
let mut v0 = self.key1_high; {
let mut v1 = self.key1_low; let mut w0 = ((buffer[3] as u32) << 16)
Self::tea_decrypt(&mut v0, &mut v1, &BF2_KEY_SCHEDULE); | ((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); Self::tea_decrypt(&mut w0, &mut w1, &BF1_KEY_SCHEDULE);
let counter = v1 >> 16;
let btn = ((v1 >> 8) & 0xF) as u8;
let crc = (v1 & 0xFF) as u16;
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<DecodedSignal> {
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 None
} }
@@ -317,24 +480,96 @@ impl ProtocolDecoder for PsaDecoder {
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> { fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.state { match self.state {
// State0: detect preamble pattern type
DecoderState::WaitEdge => { DecoderState::WaitEdge => {
if level && duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_49 { if !level {
self.state = DecoderState::CountPattern; 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.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_125 = duration_diff!(duration, TE_SHORT_125);
let diff_250 = duration_diff!(duration, TE_LONG_250); let diff_250 = duration_diff!(duration, TE_LONG_250);
if diff_125 < TE_TOLERANCE_50 { if diff_125 < TE_TOLERANCE_50 {
self.pattern_counter += 1; self.pattern_counter += 1;
self.prev_duration = duration; self.prev_duration = duration;
} else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x46 { } else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x45 {
// Found end of preamble, start Manchester decoding // Transition to Manchester decode at 125/250µs
self.state = DecoderState::DecodeManchester; self.state = DecoderState::DecodeManchester125;
self.data_low = 0; self.data_low = 0;
self.data_high = 0; self.data_high = 0;
self.bit_count = 0; self.bit_count = 0;
@@ -347,23 +582,17 @@ impl ProtocolDecoder for PsaDecoder {
} }
} }
DecoderState::DecodeManchester => { // State4: Manchester decode at 125/250µs (Pattern 2)
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA; DecoderState::DecodeManchester125 => {
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA; if self.bit_count >= 121 {
let is_end = duration > TE_END_1000; 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 { if is_end {
// End of data return self.finalize_frame();
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_short || is_long { if is_short || is_long {
@@ -373,13 +602,8 @@ impl ProtocolDecoder for PsaDecoder {
} else { } else {
self.state = DecoderState::WaitEdge; self.state = DecoderState::WaitEdge;
} }
self.prev_duration = duration; self.prev_duration = duration;
} }
DecoderState::End => {
self.state = DecoderState::WaitEdge;
}
} }
None None
@@ -406,31 +630,8 @@ impl ProtocolDecoder for PsaDecoder {
buffer[8] = button & 0x0F; buffer[8] = button & 0x0F;
buffer[9] = 0; buffer[9] = 0;
// XOR encrypt // XOR encrypt (matches psa.c psa_second_stage_xor_encrypt)
{ Self::xor_encrypt(&mut buffer);
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;
}
// TEA encrypt // TEA encrypt
let mut v0 = ((buffer[0] as u32) << 24) let mut v0 = ((buffer[0] as u32) << 24)
@@ -450,38 +651,42 @@ impl ProtocolDecoder for PsaDecoder {
let mut signal = Vec::with_capacity(512); let mut signal = Vec::with_capacity(512);
// Preamble + sync (matches protopirate psa encode) // Preamble: 80 iterations at 250us HIGH+LOW (matches C: te = PSA_TE_LONG_250)
for _ in 0..70 { for _ in 0..80 {
signal.push(LevelDuration::new(true, TE_SHORT_125)); signal.push(LevelDuration::new(true, TE_LONG_250));
signal.push(LevelDuration::new(false, TE_SHORT_125)); 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)); 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); let key1 = ((key1_high as u64) << 32) | (key1_low as u64);
for bit in (0..64).rev() { for bit in (0..64).rev() {
if (key1 >> bit) & 1 == 1 { if (key1 >> bit) & 1 == 1 {
signal.push(LevelDuration::new(false, TE_SHORT)); signal.push(LevelDuration::new(true, TE_LONG_250));
signal.push(LevelDuration::new(true, TE_SHORT)); signal.push(LevelDuration::new(false, TE_LONG_250));
} else { } else {
signal.push(LevelDuration::new(true, TE_SHORT)); signal.push(LevelDuration::new(false, TE_LONG_250));
signal.push(LevelDuration::new(false, TE_SHORT)); 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() { for bit in (0..16).rev() {
if (validation >> bit) & 1 == 1 { if (validation >> bit) & 1 == 1 {
signal.push(LevelDuration::new(false, TE_SHORT)); signal.push(LevelDuration::new(true, TE_LONG_250));
signal.push(LevelDuration::new(true, TE_SHORT)); signal.push(LevelDuration::new(false, TE_LONG_250));
} else { } else {
signal.push(LevelDuration::new(true, TE_SHORT)); signal.push(LevelDuration::new(false, TE_LONG_250));
signal.push(LevelDuration::new(false, TE_SHORT)); 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)); signal.push(LevelDuration::new(false, TE_END_1000));
Some(signal) Some(signal)