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:
Generated
+1
-1
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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 V0–V6, Ford V0, Fiat V0, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions
|
- **Multi-protocol decoding** — 18 protocol decoders: Kia V0–V6, Ford V0, Fiat V0/V1, Mazda V0, Mitsubishi V0, Porsche Touareg, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions
|
||||||
- **KeeLoq generic fallback** — when a signal doesn’t match any known protocol, KAT tries decoding it as KeeLoq using every manufacturer key in the embedded keystore (Kia V3/V4 and Star Line air formats); successful decodes appear as **Keeloq (keystore name)** in the capture list
|
- **KeeLoq generic fallback** — when a signal doesn’t match any known protocol, KAT tries decoding it as KeeLoq using every manufacturer key in the embedded keystore (Kia V3/V4 and Star Line air formats); successful decodes appear as **Keeloq (keystore name)** in the capture list
|
||||||
- **RF modulation metadata** — each protocol tagged as AM, FM, or both (from ProtoPirate); shown in signal detail and exported in .fob
|
- **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
|
||||||
│ └── ...
|
│ └── ...
|
||||||
|
|||||||
@@ -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) |
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
+43
-14
@@ -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
|
||||||
|
|
||||||
|
|||||||
+39
-49
@@ -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(
|
signal.push(LevelDuration::new(false, te_short));
|
||||||
false,
|
}
|
||||||
if i == PREAMBLE_PAIRS - 1 { GAP_US } else { te_short },
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// First bit (bit 63) - differential Manchester
|
// Standard Manchester encode 64 bits of data (matches C)
|
||||||
let first_bit = (data >> 63) & 1 == 1;
|
for bit in (0..64).rev() {
|
||||||
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;
|
let curr_bit = (data >> bit) & 1 == 1;
|
||||||
if !prev_bit && !curr_bit {
|
if curr_bit {
|
||||||
signal.push(LevelDuration::new(true, te_short));
|
signal.push(LevelDuration::new(true, te_short));
|
||||||
signal.push(LevelDuration::new(false, 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 {
|
} else {
|
||||||
signal.push(LevelDuration::new(false, te_short));
|
signal.push(LevelDuration::new(false, te_short));
|
||||||
signal.push(LevelDuration::new(true, te_short));
|
signal.push(LevelDuration::new(true, te_short));
|
||||||
}
|
}
|
||||||
prev_bit = curr_bit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6 btn bits (ref: for bit 5 down to 0)
|
// Standard Manchester encode 7 bits of endbyte (matches C: endbyte & 0x7F, bits 6..0)
|
||||||
for bit in (0..6).rev() {
|
let endbyte_masked = endbyte & 0x7F;
|
||||||
let curr_bit = (btn_to_send >> bit) & 1 == 1;
|
for bit in (0..7).rev() {
|
||||||
if !prev_bit && !curr_bit {
|
let curr_bit = (endbyte_masked >> bit) & 1 == 1;
|
||||||
|
if curr_bit {
|
||||||
signal.push(LevelDuration::new(true, te_short));
|
signal.push(LevelDuration::new(true, te_short));
|
||||||
signal.push(LevelDuration::new(false, 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 {
|
} else {
|
||||||
signal.push(LevelDuration::new(false, te_short));
|
signal.push(LevelDuration::new(false, te_short));
|
||||||
signal.push(LevelDuration::new(true, te_short));
|
signal.push(LevelDuration::new(true, te_short));
|
||||||
}
|
}
|
||||||
prev_bit = curr_bit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// End marker (ref)
|
// End marker: te_short * 4 LOW (matches C)
|
||||||
if prev_bit {
|
signal.push(LevelDuration::new(false, te_short * 4));
|
||||||
signal.push(LevelDuration::new(false, te_short));
|
|
||||||
}
|
|
||||||
signal.push(LevelDuration::new(false, te_short * 8));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(signal)
|
Some(signal)
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
+17
-27
@@ -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 };
|
|
||||||
Self::crc4(&char_data[..6], offset)
|
|
||||||
} else if cnt_high >= 0x6 {
|
|
||||||
char_data[6] = cnt_high as u8;
|
char_data[6] = cnt_high as u8;
|
||||||
Self::crc4(&char_data, 1)
|
let crc = 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 };
|
|
||||||
Self::crc4(&char_data[..6], offset)
|
|
||||||
} else if cnt_high >= 0x6 {
|
|
||||||
char_data[6] = cnt_high;
|
char_data[6] = cnt_high;
|
||||||
Self::crc4(&char_data, 1)
|
let crc = 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
@@ -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
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+345
-140
@@ -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;
|
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if validation_result == 0 {
|
||||||
|
// Direct XOR decrypt succeeded validation
|
||||||
Self::xor_decrypt(&mut buffer);
|
Self::xor_decrypt(&mut buffer);
|
||||||
|
|
||||||
let serial = ((buffer[2] as u32) << 16)
|
let serial = ((buffer[3] as u32) << 8)
|
||||||
| ((buffer[3] as u32) << 8)
|
| ((buffer[2] as u32) << 16)
|
||||||
| (buffer[4] as u32);
|
| (buffer[4] as u32);
|
||||||
let counter = ((buffer[5] as u32) << 8) | (buffer[6] as u32);
|
let counter = (buffer[6] as u32) | ((buffer[5] as u32) << 8);
|
||||||
let crc = buffer[7] as u16;
|
let crc = buffer[7] as u16;
|
||||||
let btn = buffer[8] & 0x0F;
|
let btn = buffer[8] & 0x0F;
|
||||||
|
|
||||||
return Some((serial, btn, counter, crc, 0x23));
|
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;
|
// Check if the TEA CRC validates (sum of bytes)
|
||||||
let crc = (v1 & 0xFF) as u16;
|
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));
|
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;
|
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;
|
||||||
|
} 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;
|
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();
|
||||||
|
|
||||||
if is_end || self.bit_count >= 121 {
|
|
||||||
// End of data
|
|
||||||
self.state = DecoderState::End;
|
|
||||||
|
|
||||||
if self.bit_count >= 96 {
|
|
||||||
// Got enough data
|
|
||||||
let result = self.parse_data();
|
|
||||||
self.state = DecoderState::WaitEdge;
|
|
||||||
return Some(result);
|
|
||||||
}
|
}
|
||||||
self.state = DecoderState::WaitEdge;
|
let is_short = duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_50;
|
||||||
return None;
|
let is_long = duration_diff!(duration, TE_LONG_250) < TE_TOLERANCE_99;
|
||||||
|
let is_end = duration > 500;
|
||||||
|
|
||||||
|
if is_end {
|
||||||
|
return self.finalize_frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
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(false, TE_SHORT_125));
|
|
||||||
}
|
|
||||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||||
|
}
|
||||||
|
|
||||||
// Key1: 64 bits Manchester, then validation 16 bits
|
// Sync transition: LOW 250us, HIGH 500us, LOW 250us (matches C)
|
||||||
|
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||||
|
signal.push(LevelDuration::new(true, TE_LONG));
|
||||||
|
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||||
|
|
||||||
|
// Key1: 64 bits Manchester 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user