v1.3.0: Add 14 keyfob protocols (8 ProtoPirate + 6 Flipper-ARF), fix KeeLoq overflow
Deploy to Pages / build (push) Waiting to run
Deploy to Pages / deploy (push) Blocked by required conditions

Brings KAT to 32 protocol decoders. Ported from the ProtoPirate reference and the
D4C1-Labs/Flipper-ARF firmware:

  ProtoPirate (8): Kia V7, Ford V1, Ford V2, Ford V3, Chrysler V0, Honda Static,
  Honda V1, Land Rover V0.
  Flipper-ARF (6): Toyota, Land Rover RKE, Mazda Siemens, BMW CAS4,
  Porsche Cayenne, PSA2.

Each decoder is gated (CRC / checksum / fixed markers / frame structure) so it
cannot false-match existing protocols; every previously-decoding IMPORTS capture
decodes unchanged. Real-capture decodes added for Ford V3 (LDV T80), Honda Static
(Honda), Toyota (Camry NRZ variant), and PSA2 (Groupe PSA, serial 0x99EB25); the
rest are validated by encode/decode round-trip and synthetic-frame tests.

Also fixes a debug-only underflow panic in keeloq_common::keeloq_decrypt
(15 - r underflowed; now wrapping_sub to match the reference's unsigned wrap;
release behavior unchanged).

Adds per-protocol docs, updates the README protocol table, capture metadata
(encoding / RF / encryption), make-suggestion mapping, and CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEfaKqzB3T4qsrybwfEi73
This commit is contained in:
KaraZajac
2026-06-23 20:08:54 -04:00
parent 118eca0948
commit 754af4134a
37 changed files with 9157 additions and 7 deletions
+18
View File
@@ -2,6 +2,24 @@
All notable changes to KAT are documented here. All notable changes to KAT are documented here.
## [1.3.0] - 2026-06-23
### Added
- **14 new protocol decoders** (total now 32), ported from the [ProtoPirate](https://protopirate.net/ProtoPirate/ProtoPirate) reference and the [Flipper-ARF](https://github.com/D4C1-Labs/Flipper-ARF) firmware:
- **ProtoPirate (8):** **Kia V7** (Manchester 250/500, header 0x4C + CRC8), **Ford V1** (Manchester 65/130, descramble + CRC16/CCITT), **Ford V2** (Manchester 200/400, 0x7FA7 sync), **Ford V3** (Manchester 240/480, plaintext, decode-only), **Chrysler V0** (PWM, seed-XOR transform; Dodge/Jeep), **Honda Static** (FM, XOR checksum), **Honda V1** (PWM/PPM, CRC-fold), **Land Rover V0** (differential Manchester, 3-bit check polynomial).
- **Flipper-ARF (6):** **Toyota** (dual PWM + NRZ variants, KeeLoq, decode-only; Lexus), **Land Rover RKE** (PWM, KeeLoq), **Mazda Siemens** (Siemens XOR/interleave), **BMW CAS4** (Manchester, 0x30/0xC5 markers, decode-only), **Porsche Cayenne** (PWM, VAG rolling-register cipher; adds an encoder to the shared Touareg air protocol), **PSA2 / "PSA OLD"** (Manchester, TEA with bounded brute-force gated behind a structural/checksum check).
- **Real-capture validation** — decoders verified against `IMPORTS/` samples where they exist: **Ford V3** (LDV T80), **Honda Static** (Honda), **Toyota** (Camry NRZ variant), **PSA2** (Groupe PSA, recovered serial 0x99EB25 + rolling counter). The rest are validated by in-module encode→decode round-trip and synthetic-frame unit tests.
- **Metadata, make-suggestion, and docs** — each new protocol is tagged with Encoding / RF (AM/FM) / Encryption in the signal detail panel, mapped to a Make suggestion (Honda/Acura, Chrysler/Dodge/Jeep, Toyota/Lexus, Land Rover, BMW, Porsche, Mazda, Peugeot/Citroën), and documented under `docs/`.
### Fixed
- **KeeLoq decrypt debug-panic** — `keeloq_common::keeloq_decrypt` computed `15 - r` (with `r` up to 527), underflowing and panicking in debug builds / `cargo test`; now uses `wrapping_sub` to match the reference's unsigned wraparound (release behavior, which silently wrapped, is unchanged).
### Notes
- All new decoders are gated (CRC / checksum / fixed markers / frame structure) so they do not false-match existing protocols — every previously-decoding `IMPORTS/*.sub` capture decodes unchanged. Where two decoders share a wire protocol (Porsche Cayenne ↔ Touareg; Mazda Siemens ↔ Mazda V0; Toyota variant-A ↔ Kia V3/V4 KeeLoq), the pre-existing decoder keeps first-match priority.
## [1.1.3] - 2026-02-20 ## [1.1.3] - 2026-02-20
### Added ### Added
Generated
+1 -1
View File
@@ -444,7 +444,7 @@ dependencies = [
[[package]] [[package]]
name = "kat" name = "kat"
version = "1.2.0" version = "1.3.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atty", "atty",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "kat" name = "kat"
version = "1.2.0" version = "1.3.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"]
+15 -1
View File
@@ -17,7 +17,7 @@ A terminal-based RF signal analysis tool for capturing, decoding, and retransmit
## Features ## Features
- **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies (HackRF uses AM envelope detection; FM protocols are tagged for display and may decode when signal is strong) - **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies (HackRF uses AM envelope detection; FM protocols are tagged for display and may decode when signal is strong)
- **Multi-protocol decoding** — 18 protocol decoders: Kia V0V6, Ford V0, Fiat V0/V1, Mazda V0, Mitsubishi V0, Porsche Touareg, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions - **Multi-protocol decoding** — 32 protocol decoders: Kia V0V7, Ford V0V3, Chrysler V0 (Dodge/Jeep), Honda Static, Honda V1, Toyota (Lexus), Land Rover V0, Land Rover RKE, Fiat V0/V1, Mazda V0, Mazda Siemens, Mitsubishi V0, BMW CAS4, Porsche Touareg, Porsche Cayenne, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, PSA2, Scher-Khan, Star Line; adaptive demodulation for real-world conditions
- **KeeLoq generic fallback** — when a signal doesnt match any known protocol, KAT tries decoding it as KeeLoq using every manufacturer key in the embedded keystore (Kia V3/V4 and Star Line air formats); successful decodes appear as **Keeloq (keystore name)** in the capture list - **KeeLoq generic fallback** — when a signal doesnt match any known protocol, KAT tries decoding it as KeeLoq using every manufacturer key in the embedded keystore (Kia V3/V4 and Star Line air formats); successful decodes appear as **Keeloq (keystore name)** in the capture list
- **RF modulation metadata** — each protocol tagged as AM, FM, or both (from ProtoPirate); shown in signal detail and exported in .fob - **RF modulation metadata** — each protocol tagged as AM, FM, or both (from ProtoPirate); shown in signal detail and exported in .fob
- **Rich signal detail** — encoding (PWM/Manchester), RF (AM/FM), encryption, serial, counter, key data, CRC, frequency, and raw level/duration pairs - **Rich signal detail** — encoding (PWM/Manchester), RF (AM/FM), encryption, serial, counter, key data, CRC, frequency, and raw level/duration pairs
@@ -234,18 +234,32 @@ Protocol behavior and RF modulation (AM/FM) follow the ProtoPirate reference. KA
| Kia V3/V4 | PWM | AM/FM | KeeLoq | 315 / 433.92 MHz | | Kia 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 | AES-128 | 433.92 MHz | | Kia V6 | Manchester | FM | AES-128 | 433.92 MHz |
| Kia V7 | Manchester | FM | Fixed Code (CRC8) | 315 / 433.92 MHz |
| Ford V0 | Manchester | FM | Rolling Code | 315 / 433.92 MHz | | Ford V0 | Manchester | FM | Rolling Code | 315 / 433.92 MHz |
| Ford V1 | Manchester | FM | Rolling Code (descramble + CRC16) | 315 / 433.92 MHz |
| Ford V2 | Manchester | FM | Fixed Code (0x7FA7 sync) | 315 / 433.92 MHz |
| Ford V3 | Manchester | FM | Fixed Code (decode-only) | 315 / 433.92 MHz |
| Chrysler V0 (Dodge/Jeep) | PWM | AM | Rolling Code (seed-XOR) | 315 / 433.92 MHz |
| Honda Static | Manchester | FM | Fixed Code | 315 / 433.92 MHz |
| Honda V1 | PWM | AM | Fixed Code | 315 / 433.92 MHz |
| Toyota (Lexus) | PWM / NRZ | AM | KeeLoq (decode-only) | 315 / 433.92 MHz |
| Land Rover V0 | Diff. Manchester | FM | Rolling Code (3-bit check) | 315 / 433.92 MHz |
| Land Rover RKE | PWM | AM | KeeLoq | 315 / 433.92 MHz |
| Fiat V0 | Manchester | FM | Fixed Code | 433.92 MHz | | Fiat V0 | Manchester | FM | Fixed Code | 433.92 MHz |
| Fiat V1 (Magneti Marelli) | Manchester | FM | Rolling Code | 433.92 MHz | | Fiat V1 (Magneti Marelli) | Manchester | FM | Rolling Code | 433.92 MHz |
| Mazda V0 | Pair-based | FM | XOR Deobfuscation | 433.92 MHz | | Mazda V0 | Pair-based | FM | XOR Deobfuscation | 433.92 MHz |
| Mazda Siemens | Manchester | FM | Siemens XOR/interleave | 433.92 MHz |
| Mitsubishi V0 | PWM | FM | Bit Negation + XOR | 868.35 MHz | | Mitsubishi V0 | PWM | FM | Bit Negation + XOR | 868.35 MHz |
| BMW CAS4 | Manchester | AM | CAS4 rolling (decode-only) | 433.92 MHz |
| Porsche Touareg | PWM | AM | Rotation Cipher | 433.92 / 868.35 MHz | | Porsche Touareg | PWM | AM | Rotation Cipher | 433.92 / 868.35 MHz |
| Porsche Cayenne | PWM | AM | VAG rolling register | 433.92 / 868.35 MHz |
| Subaru | PWM | AM | Rolling Code | 433.92 MHz | | 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 | | 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 | Modified TEA/XOR | 433.92 MHz | | PSA (Peugeot/Citroën) | Manchester | FM | Modified TEA/XOR | 433.92 MHz |
| PSA2 (PSA OLD) | Manchester | AM | TEA | 433.92 MHz |
**KeeLoq generic fallback:** If no protocol decodes a capture, KAT tries KeeLoq with every keystore manufacturer key (Kia V3/V4 and Star Line bit layouts). On success the protocol is shown as **Keeloq (*keystore name*)** (e.g. Keeloq (Alligator), Keeloq (Pandora_PRO)). See [docs/keeloq_generic.md](docs/keeloq_generic.md). **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).
+14
View File
@@ -12,18 +12,32 @@ This folder describes how each keyfob protocol supported by KAT works. Each docu
| Kia V3/V4 | `kia_v3_v4.rs` | [kia_v3_v4.md](kia_v3_v4.md) | | Kia V3/V4 | `kia_v3_v4.rs` | [kia_v3_v4.md](kia_v3_v4.md) |
| Kia V5 | `kia_v5.rs` | [kia_v5.md](kia_v5.md) | | Kia V5 | `kia_v5.rs` | [kia_v5.md](kia_v5.md) |
| Kia V6 | `kia_v6.rs` | [kia_v6.md](kia_v6.md) | | Kia V6 | `kia_v6.rs` | [kia_v6.md](kia_v6.md) |
| Kia V7 | `kia_v7.rs` | [kia_v7.md](kia_v7.md) |
| Ford V0 | `ford_v0.rs` | [ford_v0.md](ford_v0.md) | | Ford V0 | `ford_v0.rs` | [ford_v0.md](ford_v0.md) |
| Ford V1 | `ford_v1.rs` | [ford_v1.md](ford_v1.md) |
| Ford V2 | `ford_v2.rs` | [ford_v2.md](ford_v2.md) |
| Ford V3 | `ford_v3.rs` | [ford_v3.md](ford_v3.md) |
| Honda Static | `honda_static.rs` | [honda_static.md](honda_static.md) |
| Honda V1 | `honda_v1.rs` | [honda_v1.md](honda_v1.md) |
| Chrysler V0 | `chrysler_v0.rs` | [chrysler_v0.md](chrysler_v0.md) |
| Subaru | `subaru.rs` | [subaru.md](subaru.md) | | Subaru | `subaru.rs` | [subaru.md](subaru.md) |
| Toyota | `toyota.rs` | [toyota.md](toyota.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) | | Fiat V1 | `fiat_v1.rs` | [fiat_v1.md](fiat_v1.md) |
| Mazda V0 | `mazda_v0.rs` | [mazda_v0.md](mazda_v0.md) | | Mazda V0 | `mazda_v0.rs` | [mazda_v0.md](mazda_v0.md) |
| Mazda Siemens | `mazda_siemens.rs` | [mazda_siemens.md](mazda_siemens.md) |
| Mitsubishi V0 | `mitsubishi_v0.rs` | [mitsubishi_v0.md](mitsubishi_v0.md) | | Mitsubishi V0 | `mitsubishi_v0.rs` | [mitsubishi_v0.md](mitsubishi_v0.md) |
| Land Rover V0 | `land_rover_v0.rs` | [land_rover_v0.md](land_rover_v0.md) |
| Land Rover RKE | `land_rover_rke.rs` | [land_rover_rke.md](land_rover_rke.md) |
| BMW CAS4 | `bmw_cas4.rs` | [bmw_cas4.md](bmw_cas4.md) |
| Porsche Touareg | `porsche_touareg.rs` | [porsche_touareg.md](porsche_touareg.md) | | Porsche Touareg | `porsche_touareg.rs` | [porsche_touareg.md](porsche_touareg.md) |
| Porsche Cayenne | `porsche_cayenne.rs` | [porsche_cayenne.md](porsche_cayenne.md) |
| Suzuki | `suzuki.rs` | [suzuki.md](suzuki.md) | | 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) |
| PSA | `psa.rs` | [psa.md](psa.md) | | PSA | `psa.rs` | [psa.md](psa.md) |
| PSA2 (PSA OLD) | `psa2.rs` | [psa2.md](psa2.md) |
| KeeLoq generic (fallback) | `keeloq_generic.rs` | [keeloq_generic.md](keeloq_generic.md) | | KeeLoq generic (fallback) | `keeloq_generic.rs` | [keeloq_generic.md](keeloq_generic.md) |
**KeeLoq generic** is not a registered decoder; it runs when no protocol matches and tries KeeLoq with every keystore manufacturer key (using `keeloq_common`). Successful decodes appear as **Keeloq (*keystore name*)**. **KeeLoq generic** is not a registered decoder; it runs when no protocol matches and tries KeeLoq with every keystore manufacturer key (using `keeloq_common`). Successful decodes appear as **Keeloq (*keystore name*)**.
+64
View File
@@ -0,0 +1,64 @@
---
layout: default
---
# BMW CAS4 Protocol
**Rust module:** `src/protocols/bmw_cas4.rs`
**Reference:** `Flipper-ARF lib/subghz/protocols/bmw_cas4.c`
## Overview
BMW CAS4 uses Manchester encoding at 500/1000 µs. 64 bits (8 bytes), AM/OOK. The CAS4 rolling cipher's
manufacturer key is not available, so the encrypted portion is left as-is — the frame is only framed and
validated, not decrypted. Emission is gated on two fixed marker bytes (`byte[0] == 0x30` and
`byte[6] == 0xC5`), which makes the protocol specific and prevents false matches.
The Manchester decoder uses Flipper's `manchester_decoder.h` transition table, with polarity
`level ? Low : High` (the same mapping as Ford V0 / common).
## Timing
| Parameter | Value | Notes |
|----------------|---------|-----------------------------|
| Short | 500 µs | ±150 µs (te_delta) |
| Long | 1000 µs | ±150 µs |
| Preamble pulse | 300700 µs | |
| Gap | ≥1800 µs | separates preamble from data |
| Min bits | 64 | 8 bytes |
| Preamble | ≥10 pulses | |
## Frame Layout (64 bits / 8 bytes)
- **byte 0:** fixed marker `0x30`
- **bytes 13:** serial (24-bit)
- **byte 5:** counter
- **byte 6:** fixed marker `0xC5`
- **byte 7:** button
The CAS4 rolling cipher is left undecrypted; the two markers serve as the integrity gate.
## RF
- **Encoding:** Manchester (Flipper table, polarity `level ? Low : High`)
- **RF modulation:** AM/OOK
- **Encryption:** CAS4 rolling cipher left undecrypted (no manufacturer key); gated on fixed markers `0x30`/`0xC5`
- **Frequencies:** 433.92 MHz only
## Decoder Steps
1. **Reset** — begin on a HIGH preamble pulse within the 300700 µs window.
2. **Preamble** — count preamble pulses; a long LOW gap (≥1800 µs) with ≥10 pulses enters Data.
3. **Data** — Manchester-decode 64 bits MSB-first; at the 64th bit, require `byte[0]==0x30 && byte[6]==0xC5`, build the signal on success, and reset. An out-of-range pulse aborts.
## Encoder
Not supported (the reference encoder is a non-functional stub: `yield` returns reset, `deserialize`
returns error). Decode-only.
## Validation
Verified by a synthetic-frame check (no local capture available): a unit test Manchester-encodes a frame
by driving the decoder's own transition table (the Flipper table is differential, so there is no
fixed per-bit pattern), confirms it decodes with the right serial/counter/button, and confirms a frame
with wrong marker bytes is rejected.
+66
View File
@@ -0,0 +1,66 @@
---
layout: default
---
# Chrysler V0 Protocol
**Rust module:** `src/protocols/chrysler_v0.rs`
**Reference:** `REFERENCES/ProtoPirate/protocols/chrysler_v0.c`
## Overview
Chrysler/Dodge/Jeep keyfobs. PWM with a short HIGH pulse and two long-LOW symbols: a "1" payload bit is
HIGH ≈600 µs + LOW ≈3400 µs; a "0" payload bit is HIGH ≈300 µs + LOW ≈3700 µs. ~24 preamble pairs
(short HIGH + long_b LOW) precede each frame. 80-bit frame: the first 64 bits are `decode_data` (payload
bytes 07), the last 16 bits are `data_2` (payload bytes 8,9). The 80-bit frame exceeds u64, so `data`
reports the most-significant 64 bits and `data_count_bit = 80`.
Crypto is a proprietary seed-XOR (ported exactly from the reference): `seed = reverse6(key[0] >> 2)` (a
6-bit reversed counter); `transform_block` XORs all 9 transformed bytes with `xor_table[seed & 0x0F]`,
with an extra nibble flip when the Lock button is set. Dual payload: A (seed even, carries serial +
counter) / B (seed odd, carries serial). The frame is gated on a structural `check_ok`, so it does not
false-match; `crc_valid` reflects that check.
## Timing
| Parameter | Value | Notes |
|-------------|---------|------------------------|
| Short HIGH | 300 µs | ±150 µs (te_delta) |
| One-short HIGH | 600 µs | "1" bit HIGH width |
| Long LOW A | 3400 µs | ±400 µs (long_delta) |
| Long LOW B | 3700 µs | ±400 µs |
| Gap | 8000 µs | te_gap |
| Frame gap | 15600 µs| |
| Min bits | 80 | |
| Preamble | 24 pairs| |
## Frame Layout (80 bits / 10 payload bytes)
- **byte 0:** `(reverse6(counter) << 2) | header_low2` — the seed/counter byte
- **bytes 19:** transformed (XOR-masked) payload; after `transform_block`:
- Payload A (even seed): serial = decoded[0..3], rolling counter
- Payload B (odd seed): serial = decoded[0..2] + decoded[7]
- Button: Lock=1, Unlock=2 (derived from structural relationships between key bytes)
## RF
- **Encoding:** PWM (short HIGH + dual long LOW)
- **RF modulation:** AM
- **Encryption:** proprietary seed-XOR (`transform_block` + `xor_table`); gated on structural `check_ok`
- **Frequencies:** 315 MHz, 433.92 MHz
## Decoder Steps
1. **Reset** — a short HIGH pulse begins the seek.
2. **Seek** — count preamble pairs (short HIGH + long LOW); after >15 pairs, a non-short HIGH transitions to Data.
3. **Data** — classify each HIGH/LOW pair into a payload bit (`bit ^ 1`); collect 64 bits into `decode_data` then 16 into `data_2`. At 80 bits (or on a terminating gap with >0x4F bits) run `decode_packet`, check `check_ok`, and emit.
## Encoder
Supported (ENABLE_EMULATE_FEATURE). Rebuilds the 10-byte payload, re-derives seed/plaintext, applies the
requested button, then emits preamble + dual 80-bit PWM frames (A and B) separated by frame gaps.
## Validation
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
cover `reverse6` involution, `transform_block` invertibility, and a full payload-A round trip.
+68
View File
@@ -0,0 +1,68 @@
---
layout: default
---
# Ford V1 Protocol
**Rust module:** `src/protocols/ford_v1.rs`
**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v1.c`
## Overview
Ford V1 uses Manchester encoding at 65/130 µs. 136 bits / 17 bytes: key1 (bytes 06, 56 bits) + key2
(bytes 714, 64 bits) + CRC16 (bytes 1516). Preamble ≥50 long pulses, then a short-pulse sync window
(`sync_event_count > 2`) replays buffered Manchester events and enters the 17-byte data collection. This
is a ROLLING-code protocol.
Crypto: a proprietary parity-based descrambling cipher operating on the 9-byte air block `raw[6..15]`,
plus CRC16/CCITT (poly `0x1021`, init `0x0000`) over `raw[3..15]`. Emission is gated on CRC16 validity
(with a 17-byte bit-inverted fallback) so it never false-matches. A strict branch
(`decoded[3]==raw[5] && decoded[4]==raw[6]`) yields plaintext serial/button/counter; otherwise it is
classified as encrypted/rolling (device id only). The Manchester transition table is the Flipper
differential-Manchester table (same as Ford V0/V2).
## Timing
| Parameter | Value | Notes |
|-------------|--------|------------------------------|
| Short | 65 µs | ±39 µs (te_delta) |
| Long | 130 µs | ±39 µs (preamble uses ±40) |
| Min bits | 136 | 17 bytes |
| Preamble | ≥50 long pulses | |
## Frame Layout (136 bits / 17 bytes)
- **bytes 06:** key1 (56 bits, big-endian) → `DecodedSignal.data`
- **bytes 614:** air block (9 bytes) — descrambled to plaintext
- **bytes 1516:** CRC16/CCITT over bytes 314
Plaintext fields (when the strict branch matches): serial = `plain[1..3] + plain[0]`, button = `plain[5]>>4`
(Sync=0, Lock=1, Unlock=2, Trunk=4, Panic=8), counter = `((plain[5] & 0x0F) << 16) | plain[6..7]`. The
`extra` word stashes the CRC16, the strict flag, and `plain[4]` so the encoder can rebuild the full frame.
## RF
- **Encoding:** Manchester (Flipper differential table)
- **RF modulation:** FM
- **Encryption:** proprietary parity descramble cipher + CRC16/CCITT gate; rolling code
- **Frequencies:** 315 MHz, 433.92 MHz
## Decoder Steps
1. **Reset** — a long LOW pulse begins the preamble (count = 1).
2. **Preamble** — count long pulses; after ≥50, a short pulse enters Sync.
3. **Sync** — buffer short/long events; once `sync_event_count > 2`, replay the buffered events into Manchester and enter Data.
4. **Data** — Manchester-decode and pack 17 bytes; at the 17th byte run `process_data` (CRC16 gate + descramble + field extraction) and emit on a valid CRC.
## Encoder
Supported (6 bursts). Faithful re-encode is only possible when the original frame's plaintext was
recovered (strict branch); the encoder rebuilds the plaintext from fields + the stashed `plain[4]`,
re-derives the air block + CRC16, and emits 6 bursts of a 400-pair long preamble + sync + 136 Manchester
bits, with a per-burst `pkt[4]` override.
## Validation
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
cover a CRC16/XMODEM known vector, the descramble round trip, the strict-branch decode, and a full
encode→decode at the on-air-frame level.
+62
View File
@@ -0,0 +1,62 @@
---
layout: default
---
# Ford V2 Protocol
**Rust module:** `src/protocols/ford_v2.rs`
**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v2.c`
## Overview
Ford V2 uses Manchester encoding at 200/400 µs. 104 bits total (13 bytes). The frame begins with a
16-bit Manchester sync that equals `0x7FA7` (the decoder matches the *inverted* shift register against
`!0x7FA7`); those two sync bytes `0x7F 0xA7` head the 13-byte buffer. Decoded data bits are inverted
before packing (`data_bit = !bit`). There is no CRC — structure is validated by the two sync bytes plus
a known button code, so Ford V2 will not false-match.
The Manchester decoder uses Flipper's transition table (same table as Ford V0), with Ford V2 polarity
`level ? High : Low`.
## Timing
| Parameter | Value | Notes |
|-----------|--------|-----------------------------|
| Short | 200 µs | ±260 µs (te_delta) |
| Long | 400 µs | ±260 µs |
| Min bits | 104 | 13 bytes |
| Preamble | ≥64 short pulses | before the sync |
## Frame Layout (104 bits / 13 bytes)
- **bytes 01:** sync `0x7F 0xA7`
- **bytes 25:** serial (32-bit, big-endian)
- **byte 6:** button — valid codes `0x10` (Lock), `0x11` (Unlock), `0x13` (Trunk), `0x14` (Panic), `0x15`
- **bytes 78 + byte 9 MSB:** counter = `((b7 & 0x7F) << 9) | (b8 << 1) | (b9 >> 7)`; byte 7 MSB carries a button parity bit (refreshed by the encoder)
- **bytes 912:** rolling/hop tail (stashed in `extra` so the encoder can rebuild the full frame)
The exported `data` (Key) word is the top 8 bytes; bytes 812 (40 bits) are carried in `extra`.
## RF
- **Encoding:** Manchester (Ford V2 polarity `level ? High : Low`; decoded bits inverted)
- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`)
- **Encryption:** none — validated by sync bytes + valid button code
- **Frequencies:** 315 MHz, 433.92 MHz
## Decoder Steps
1. **Reset** → a short pulse (~200 µs) begins the preamble.
2. **Preamble** — count short pulses; after ≥64 shorts a long LOW pulse enters Sync.
3. **Sync** — Manchester-decode bits into a 16-bit shift register; when it matches `!0x7FA7`, prime the two sync bytes and enter Data.
4. **Data** — Manchester events feed the state machine; decoded bits are inverted and packed MSB-first into 13 bytes. At 104 bits the sync bytes and button are validated and the frame committed. A non-short/long pulse (gap) ends the attempt.
## Encoder
Supported (matches `subghz_protocol_encoder_ford_v2`). Rebuilds the 13-byte frame, applies the requested
button, refreshes byte-7 parity, then emits 6 bursts of a 70-pair preamble + sync + 104 Manchester bits
(encoder short 240 µs), separated by ~16 ms inter-burst gaps.
## Validation
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available).
+55
View File
@@ -0,0 +1,55 @@
---
layout: default
---
# Ford V3 Protocol
**Rust module:** `src/protocols/ford_v3.rs`
**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v3.c`
## Overview
Ford V3 uses Manchester encoding at 240/480 µs. 104 bits total (13 bytes), transmitted as plaintext —
there is no CRC or encryption. Decode-only: the ProtoPirate reference ships a NULL encoder, so KAT
does not transmit Ford V3 (Replay still works on the raw capture).
The Manchester decoder uses Flipper's `manchester_decoder.h` transition table (same table as Ford V0),
but Ford V3 maps the level the opposite way from Ford V0: `level ? High : Low`.
## Timing
| Parameter | Value | Notes |
|-----------|--------|--------------------|
| Short | 240 µs | ±60 µs (te_delta) |
| Long | 480 µs | ±60 µs |
| Min bits | 104 | 13 bytes |
| Preamble | ≥30 short pulses | before first long |
## Frame Layout (104 bits / 13 bytes)
- **byte 0:** header
- **bytes 14:** serial (32-bit, big-endian)
- **byte 5:** hop/reserved
- **byte 6:** button — bit 0 set → Unlock, else Lock
- **bytes 78:** counter, stored bitwise-inverted (`~b[7]`, `~b[8]`)
- **bytes 912:** rolling/hop tail (not parsed)
## Decoder Steps
1. **Reset** — a short pulse (~240 µs) starts the preamble (count = 1).
2. **Preamble** — count short pulses; once ≥30 shorts are seen, a long pulse begins the data
(Manchester state seeded to Mid1, first bit decoded from the long pulse).
3. **Data** — Manchester events (short/long × level) feed the state machine; bits pack MSB-first into
13 bytes. At 104 bits the fields are parsed and the signal is emitted. A non-short/non-long pulse
(gap) ends the frame.
## RF
- **Encoding:** Manchester
- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`)
- **Encryption:** none (plaintext)
- **Frequencies:** 315 MHz, 433.92 MHz
## Encoder
Not supported (reference encoder is NULL). Decode-only.
+65
View File
@@ -0,0 +1,65 @@
---
layout: default
---
# Honda Static Protocol
**Rust module:** `src/protocols/honda_static.rs`
**Reference:** `REFERENCES/ProtoPirate/protocols/honda_static.c`
## Overview
Honda/Acura fixed-code keyfobs. 64-bit frame. Unlike most KAT decoders, Honda Static does NOT use the
Flipper `manchester_decoder.h` transition table: it buffers a per-element *symbol stream* (one bit per
~63 µs element) and then performs a custom Manchester unpack over symbol pairs. A short pulse contributes
one symbol equal to the pulse level; a long pulse contributes two symbols of the same level. Anything
outside both ranges (e.g. the 700 µs sync or a trailing gap) terminates the buffer and triggers a parse.
The checksum is an XOR of the first 7 packet bytes. Emission is gated on the checksum, so Honda Static
will not false-match. The parser tries the inverted-Manchester interpretation first (what the encoder
emits), then non-inverted forward, then a bit-reversed-bytes pass.
## Timing
| Parameter | Value | Notes |
|----------------|--------|--------------------------------|
| Element | 63 µs | one symbol per element |
| Short pulse | 2898 µs | base 28 µs, span 70 µs |
| Long pulse | 61191 µs | base 61 µs, span 130 µs (two symbols) |
| Sync | 700 µs | terminates the symbol buffer |
| Min symbols | 36 | before a parse is attempted |
| Min bits | 64 | |
(Reported timing profile: te_short 63 µs, te_long 700 µs, te_delta 120 µs.)
## Frame Layout (64 bits, MSB-first into 8 bytes)
- **bits 03:** button (4-bit) — Lock=1, Unlock=2, Trunk=4, Remote Start=5, Panic=8, Lock×2=9
- **bits 431:** serial (28-bit)
- **bits 3255:** counter (24-bit)
- **bits 5663:** checksum (XOR of bytes 06)
The exported `data` (Key) word is the C `generic.data`: a compact nibble-packed layout, NOT the raw
decoded packet bytes. The KAT counter field is the low 16 bits of the 24-bit counter.
## RF
- **Encoding:** custom symbol-stream Manchester over ~63 µs elements
- **RF modulation:** FM
- **Encryption:** none — gated on the XOR checksum + valid button + valid serial (serial ≠ 0 and ≠ 0x0FFFFFFF)
- **Frequencies:** 315 MHz, 433.92 MHz
## Decoder Steps
1. **feed** — classify each pulse: short → 1 symbol, long → 2 symbols; buffer them.
2. On an out-of-range pulse (700 µs sync or gap), if ≥36 symbols are buffered, walk past the alternating preamble + sync run, Manchester-unpack 64 bits over symbol pairs (inverted first, then non-inverted), validate, and emit. Then clear the buffer.
## Encoder
Supported (matches `honda_static_build_upload`). Emits a 700 µs HIGH sync, a 160-element alternating
preamble at 63 µs, 64 data bits (each as `!value`/`value` at 63 µs), and a trailing 700 µs sync.
## Validation
Decodes REAL IMPORTS captures (IMPORTS/honda Lock and Unlock). Also covered by encode→decode round-trip
and packet-validate unit tests.
+63
View File
@@ -0,0 +1,63 @@
---
layout: default
---
# Honda V1 Protocol
**Rust module:** `src/protocols/honda_v1.rs`
**Reference:** `REFERENCES/ProtoPirate/protocols/honda_v1.c`
## Overview
Honda/Acura fixed-code keyfobs — a DIFFERENT protocol from `honda_static`. The on-wire frame carries 68
bits: 64-bit data + a 4-bit CRC-fold nibble. Encoding is short/long pulse PWM, glued together by a
"pending bit" timing accumulator before classification: sub-`te_delta` runts are summed into `pending`;
a HIGH level extends the running HIGH pulse; a LOW flushes the accumulated HIGH (if it reached
`te_short_min`) as a synthetic symbol, then classifies the LOW.
Validation is a button-code table (Unlock=0, Lock=8, Trunk=9, Panic=10) plus a CRC-fold checksum.
Emission is gated on the button being valid; `crc_valid` reflects whether the received CRC nibble matches
either wire-order checksum. The strong button gate keeps Honda V1 from false-matching.
## Timing
| Parameter | Value | Notes |
|-------------|---------|-----------------------------|
| Short | 1000 µs | ±400 µs (te_delta) |
| Long | 2000 µs | ±400 µs |
| Short min | 600 µs | flush threshold for HIGH |
| End gap | 3500 µs | >te_end commits the frame |
| Min bits | 68 | |
## Frame Layout (68 bits = 64-bit data + 4-bit CRC nibble)
After the end gap, `commit` left-shifts the 12-byte bit buffer to drop leading preamble leakage and align
the trailing 68-bit frame. `data` = first 8 bytes (64 bits); the CRC nibble = byte 8's high nibble.
- **data[63:36]:** serial (28-bit)
- **data[31:28]:** button (nibble) — Unlock=0, Lock=8, Trunk=9, Panic=10
- **data[15:0]:** counter (16-bit)
- **CRC nibble:** CRC-fold checksum (`honda_v1_checksum*`), validated against either wire-order value
## RF
- **Encoding:** short/long pulse PWM with pending-bit timing accumulation
- **RF modulation:** AM (OOK)
- **Encryption:** none — gated on the button-code table + CRC-fold checksum
- **Frequencies:** 315 MHz, 433.92 MHz
## Decoder Steps
1. **feed** — accumulate sub-`te_delta` runts into `pending`; flush HIGH pulses and classify LOW pulses into symbols.
2. **symbol** — Reset → Preamble (short/long pulses, needs a long + >5 count) → Data.
3. **Data** — pending-bit accumulation: a short pulse toggles `data_pending` and emits a bit when paired; a long pulse emits directly. After a >te_end gap, commit: align the 68-bit frame, validate the button, and emit.
## Encoder
Supported (ENABLE_EMULATE_FEATURE). Builds the 64-bit key from serial/button/counter via the button code
table, then emits a 180-element short-pair preamble + 4 PWM frames (2 per checksum wire value).
## Validation
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
cover the key/field round trip, full encode→decode, and the button-validity mask.
+55
View File
@@ -0,0 +1,55 @@
---
layout: default
---
# Kia V7 Protocol
**Rust module:** `src/protocols/kia_v7.rs`
**Reference:** `REFERENCES/ProtoPirate/protocols/kia_v7.c`
## Overview
Kia V7 uses Manchester encoding at 250/500 µs. 64 bits. The decoded 64-bit word is bit-inverted
(`~data`); a valid frame has a fixed header byte `0x4C` and a CRC8 (poly `0x7F`, init `0x4C`) over bytes
06. Emission is gated on header + CRC, so Kia V7 is strongly validated and will not false-match.
## Timing
| Parameter | Value | Notes |
|-----------|--------|---------------------|
| Short | 250 µs | ±100 µs (te_delta) |
| Long | 500 µs | ±100 µs |
| Min bits | 64 | |
| Preamble | ≥16 short pairs | before sync |
## Frame Layout (64 bits / 8 bytes, after inversion)
- **byte 0:** header `0x4C`
- **bytes 12:** counter (16-bit, big-endian)
- **bytes 36:** serial (28-bit) — `(b3<<20)|(b4<<12)|(b5<<4)|(b6>>4)`, masked to 0x0FFFFFFF
- **byte 6 low nibble:** button (4-bit) — Lock=1, Unlock=2, Trunk=3, aux=8
- **byte 7:** CRC8 over bytes 06 (poly `0x7F`, init `0x4C`)
## RF
- **Encoding:** Manchester (level ? (H,L) : (L,H); decoded word is bit-inverted)
- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`)
- **Encryption:** none — gated on fixed header `0x4C` + CRC8
- **Frequencies:** 315 MHz, 433.92 MHz
## Decoder Steps
1. **Reset** — a short HIGH pulse begins the preamble.
2. **Preamble** — count short pairs; once >15 pairs are seen, a long HIGH pulse transitions to SyncLow and preloads four seed bits (1,0,1,1 = the inverted header's top nibble 0xB).
3. **SyncLow** — a short LOW after the long sync enters Data.
4. **Data** — Manchester-decode the remaining 60 bits. At 64 bits, invert the word, check the header equals `0x4C` and the CRC8 validates, then emit.
## Encoder
Supported (matches `kia_v7_encode_key`). Rebuilds the 64-bit key from serial/button/counter with the
CRC8, then emits a 32-pair short preamble, a merged long sync pulse, 64 Manchester bits, and a trailing
gap.
## Validation
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available).
+71
View File
@@ -0,0 +1,71 @@
---
layout: default
---
# Land Rover RKE Protocol
**Rust module:** `src/protocols/land_rover_rke.rs`
**Reference:** `Flipper-ARF lib/subghz/protocols/landrover_rke.c`
## Overview
Ported from the Flipper-ARF firmware (D4C1-Labs), itself derived from Pandora DXL 5000 firmware. Land
Rover shares the Ford/Jaguar baseband (firmware protocol ID `0x0E`) but uses a distinct 66-bit frame.
Encoding is fixed-width PWM with a 1000 µs bit period: Bit-1 = 700 µs HIGH + 300 µs LOW, Bit-0 = 300 µs
HIGH + 700 µs LOW. Preamble = 20× (400 µs HIGH + 600 µs LOW); sync = 400 µs HIGH + 9600 µs LOW.
KeeLoq: the hop code is the raw 32-bit KeeLoq ciphertext. Full decryption needs the per-fob manufacturer
key (provisioned, not in firmware), so KAT exposes the framed fields and leaves the hop encrypted —
`crc_valid = false` since no cryptographic check is performed. Emission is still gated tightly on the long
preamble run, the distinctive 9.6 ms sync gap, and exactly 66 PWM bits, so this loose-looking PWM decoder
does not false-match Kia/Subaru/Ford captures.
## Timing
| Parameter | Value | Notes |
|----------------|---------|-----------------------------|
| Bit-1 | 700 µs HIGH + 300 µs LOW | ±20% (relative) |
| Bit-0 | 300 µs HIGH + 700 µs LOW | ±20% |
| Preamble pair | 400 µs HIGH + 600 µs LOW | ×20 |
| Sync | 400 µs HIGH + 9600 µs LOW | |
| Repeat gap | 12000 µs | |
| Min bits | 66 | |
| Preamble min | 16 pairs| |
(Reported timing profile: te_short 300 µs, te_long 700 µs, te_delta 140 µs.)
## Frame Layout (66 bits, MSB-first)
- **[65:34]:** 32-bit KeeLoq encrypted hopping code
- **[33:10]:** 24-bit fixed fob serial
- **[9:6]:** 4-bit button — Lock=0x1, Unlock=0x2, Boot/Tailgate=0x4, Panic=0x8
- **[5:2]:** 4-bit function/repeat flags
- **[1:0]:** 2-bit status — battery-low=0x1, repeat=0x2
66 bits do not fit a u64: `data` holds the low 64 frame bits and `extra` holds the top 2 (the high 2 bits
of the hop code), so encode round-trips the hop losslessly. The KAT counter field surfaces the low 16
bits of the hop ciphertext.
## RF
- **Encoding:** fixed-width PWM
- **RF modulation:** OOK/AM
- **Encryption:** KeeLoq hop left encrypted (no manufacturer key); `crc_valid = false`
- **Frequencies:** 433.92 MHz, 315 MHz
## Decoder Steps
1. **Reset** — wait for the first ~400 µs preamble HIGH.
2. **Preamble** — count 400/600 µs pairs; the distinctive ~9600 µs LOW after a ~400 µs HIGH (with ≥16 pairs) enters DataHigh.
3. **DataHigh/DataLow** — read each bit from its HIGH/LOW half pair (±20% windows). At 66 bits, build the signal and emit.
## Encoder
Supported. Reconstructs the original frame to preserve hop code / func bits / status, overrides only the
button, and emits 4 repetitions of preamble + sync + 66 MSB-first PWM bits, separated by 12 ms gaps.
## Validation
Verified by encode→decode round-trips / synthetic-frame checks (no local capture available). Unit tests
cover pack/unpack round trips, encode→decode across multiple serials/hops/buttons, and rejection of
truncated frames and wrong sync gaps.
+69
View File
@@ -0,0 +1,69 @@
---
layout: default
---
# Land Rover V0 Protocol
**Rust module:** `src/protocols/land_rover_v0.rs`
**Reference:** `REFERENCES/ProtoPirate/protocols/land_rover_v0.c`
## Overview
Land Rover V0 uses **differential** Manchester (NOT the Flipper transition table) at 250/500 µs, with a
~750 µs sync pulse and a ≥64-pair short preamble. The frame is 81 bits = an 80-bit body (`raw[0..10]`)
plus one trailing `extra_bit`. The 64-bit key reported as `data` is `raw[0..8]` big-endian; `raw[8..10]`
is a 16-bit `tail`.
The check is a proprietary 3-bit polynomial over the counter (`calculate_check`); the tail is `0xFFFF`
or `0x7FFF` depending on a 1-bit parity of the counter (`calculate_tail`). Emission is gated on the
reserved bits being zero, the check matching, the tail matching, and `extra_bit` set — so Land Rover V0
is strongly validated and will not false-match.
## Timing
| Parameter | Value | Notes |
|-------------|--------|------------------------|
| Short | 250 µs | ±100 µs (te_delta) |
| Long | 500 µs | ±100 µs |
| Sync | 750 µs | ±120 µs |
| Min bits | 81 | 80-bit body + extra_bit|
| Preamble | ≥64 pairs | |
## Frame Layout (81 bits)
64-bit key (`raw[0..8]`):
- **bytes 02:** 24-bit command signature — Lock = `0xC20363`, Unlock = `0xA285E3`
- **bytes 35:** 24-bit serial
- **byte 6 + byte 7 MSB:** 9-bit counter = `(b6 << 1) | (b7 >> 7)`
- **byte 7 bits 0x78:** 3 reserved bits (must be 0)
- **byte 7 bits 0x07:** 3-bit check (`calculate_check`)
Then `raw[8..10]` = 16-bit tail (`0xFFFF`/`0x7FFF`), and a trailing `extra_bit` (must be 1). The tail is
stashed in `extra` for the encoder.
## RF
- **Encoding:** differential Manchester
- **RF modulation:** FM
- **Encryption:** none — gated on the 3-bit check polynomial + tail parity + reserved-bits + extra_bit
- **Frequencies:** 315 MHz, 433.92 MHz
## Decoder Steps
1. **Reset → PreambleLow/PreambleHigh** — count short pairs (≥64), terminated by a ~750 µs sync HIGH.
2. **SyncLow** — a ~750 µs sync LOW seeds the frame (bit 0 = 1) and enters Data.
3. **Data** — the differential machine (`process_transition`/`add_decoded_bit`) tracks `previous_bit`, skips the initial boundary short-high pad, and completes short half-bits via `pending_short`. At 81 bits, `finish_frame` validates and emits.
## Encoder
Supported. Builds the 64-bit key from signature/serial/counter, emits a 319-pair preamble, the sync,
differential-Manchester body, the 16-bit tail, and a trailing extra bit. Faithful-port note: the
reference encoder forces frame bit 1 = 0, so the Lock signature `0xC20363` (whose bit 1 is 1) is emitted
as `0x820363`; the Unlock signature `0xA285E3` round-trips cleanly. KAT reproduces this behaviour.
## Validation
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
cover the check polynomial vs. the reference, the tail parity, Unlock round trips across 256 serial/counter
values, the Lock forced-bit quirk, and rejection of a corrupted check.
+70
View File
@@ -0,0 +1,70 @@
---
layout: default
---
# Mazda Siemens Protocol
**Rust module:** `src/protocols/mazda_siemens.rs`
**Reference:** `Flipper-ARF lib/subghz/protocols/mazda_siemens.c`
## Overview
The Siemens/VDO keyfob cipher used on some Mazda vehicles — a DIFFERENT protocol from KAT's existing
"Mazda V0" (Pandora) decoder, even though both ride a 250/500 µs pair-based stream at 433.92 MHz FM. The
decoder is pair-based: `feed()` ignores `level` and interprets raw durations in pairs (`process_pair`),
collecting bits with *inverted* polarity (`state_bit == 0` → stored 1). 64-bit frame.
Siemens obfuscation (`mazda_xor_deobfuscate`): parity-dependent XOR mask, then bit-deinterleave of bytes
5/6. The inner Siemens cipher's plaintext is left as-is (no key); only this obfuscation/interleave layer
is reversed. The gate is an additive checksum `sum(data[0..7]) == data[7]`, plus the structural
preamble/sync/bit-count constraints, which keeps it from false-matching other 250/500 µs Manchester
protocols.
> **Note:** KAT's existing "Mazda V0" decoder implements the same algorithm and keeps first-match
> priority, so on a shared frame Mazda V0 fires first.
## Timing
| Parameter | Value | Notes |
|-------------|--------|-----------------------------|
| Short | 250 µs | ±100 µs (te_delta) |
| Long | 500 µs | ±100 µs |
| Min bits | 64 | |
| Preamble | ≥13 short/short pairs | |
| Completion | 80105 collected bits | |
## Frame Layout (64 bits / 8 bytes, after deobfuscation)
The 14-byte buffer's leading sync byte is discarded, leaving 8 bytes:
- **serial:** `data >> 32` (32-bit)
- **button:** `(data >> 24) & 0xFF` — Lock=0x10, Unlock=0x20, Trunk=0x40
- **counter:** `(data >> 8) & 0xFFFF` (16-bit)
- **byte 7:** additive checksum of bytes 06
## RF
- **Encoding:** pair-based Manchester (inverted polarity), 250/500 µs
- **RF modulation:** FM
- **Encryption:** Siemens obfuscation (parity XOR + bit-interleave); gated on the additive checksum
- **Frequencies:** 433.92 MHz only
## Decoder Steps
1. **Reset** — a short pulse begins the preamble.
2. **PreambleSave/PreambleCheck** — count short/short pairs (≥13); a short→long transition seeds the leading sync bit (a 1) and enters data.
3. **DataSave/DataCheck** — process duration pairs into bits; on a non-matching pair, run `check_completion` (discard sync byte, deobfuscate, validate additive checksum) and emit.
## Encoder
Supported (`subghz_protocol_encoder_mazda_siemens_get_upload`). Increments the counter byte, recomputes
the checksum, obfuscates (interleave + XOR), then emits a 12-byte 0xFF preamble, a 50 ms gap, `0xFF 0xFF
0xD7`, the 8 obfuscated bytes transmitted as `255 byte`, a `0x5A` tail, and a trailing 50 ms gap.
Manchester per byte: bit 1 → (H,L), bit 0 → (L,H) at te_short. As in the C, the decoder and encoder are
not a clean raw-timing round-trip pair (the encoder targets a TX upload).
## Validation
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
cover the obfuscate/deobfuscate XOR+interleave cipher layer (a true inverse, both parity branches), the
field layout, and the TX upload smoke test.
+67
View File
@@ -0,0 +1,67 @@
---
layout: default
---
# Porsche Cayenne Protocol
**Rust module:** `src/protocols/porsche_cayenne.rs`
**Reference:** `Flipper-ARF lib/subghz/protocols/porsche_cayenne.c`
## Overview
Porsche keyfobs (internal firmware header name "Porsche AG"). PWM at 1680/3370 µs: SHORT LOW + LONG HIGH
= bit 0, LONG LOW + SHORT HIGH = bit 1. 64-bit MSB-first frame, preceded by a 73-pulse 3370 µs sync
preamble + a 5930 µs gap pair. The cipher is a 24-bit rotating-register VAG cipher; the counter is
recovered by brute-forcing 1..=256 and matching the recomputed encrypted bytes (the C's validity signal).
> **Note:** Porsche Cayenne shares the wire protocol with KAT's existing **Porsche Touareg** decoder
> (the Touareg port is itself a port of this same `porsche_cayenne.c` source). The two are the **same
> wire protocol** — identical timing, preamble, 64-bit frame, VAG cipher, and validity check. Cayenne is
> registered AFTER Touareg, so Touareg keeps first-match priority and Cayenne never steals a Touareg
> capture. Emission is additionally gated on `frame_type` being one of the three values the C emits
> (`0b001` Cont, `0b010` First, `0b100` Final) — a strict subset of what Touareg accepts. Cayenne adds an
> encoder (the Touareg port is decode-only).
## Timing
| Parameter | Value | Notes |
|-------------|---------|------------------------|
| Short | 1680 µs | ±500 µs (te_delta) |
| Long | 3370 µs | ±500 µs |
| Sync | 3370 µs | preamble pulse |
| Gap | 5930 µs | preamble→data boundary |
| Min bits | 64 | |
| Sync min | 15 pairs| (firmware emits 73) |
## Frame Layout (64 bits / 8 bytes)
- **byte 0:** `(button << 4) | (frame_type & 0x07)` — button d-pad: Lock=0x01, Unlock=0x02, Trunk=0x04, Open/Panic=0x08; frame_type First=0x02, Cont=0x01, Final=0x04
- **bytes 13:** serial (24-bit, big-endian)
- **bytes 47:** encrypted cipher output (24-bit rotating-register VAG cipher seeded from serial, rotated `4 + counter_low` times)
The `extra` word carries the frame_type; `protocol_display_name` is `Porsche Cayenne [First|Cont|Final]`.
## RF
- **Encoding:** PWM (SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1)
- **RF modulation:** AM/OOK
- **Encryption:** 24-bit rotating-register VAG cipher; counter recovered by brute force, `counter != 0` is the validity gate
- **Frequencies:** 433.92 MHz, 868.35 MHz
## Decoder Steps
1. **Reset** — wait for a LOW pulse at ~3370 µs.
2. **Sync** — count sync pulses (HIGH and LOW at 3370 µs); a 5930 µs gap with ≥15 sync pulses enters GapHigh/GapLow.
3. **GapHigh/GapLow** — expect the complementary 5930 µs gap pulse, then enter Data.
4. **Data** — decode bit pairs (SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1); at 64 bits, `parse_data` applies the Cayenne frame_type gate + counter recovery and emits (or stays silent so Touareg owns the frame).
## Encoder
Supported (matches `porsche_cayenne_build_upload`). Emits a 4-frame burst (frame types 0b010/0b001/0b100/
0b100, cipher counters cnt+1..cnt+4), each frame being 73 sync pairs + 1 gap pair + 64 MSB-first PWM bits.
## Validation
Verified by encode→decode round-trips / synthetic-frame checks (no local capture available). Unit tests
cover the full encoder path, all three frame types, rejection of undocumented frame types and truncated
frames, the counter-recovery limit matching the C, and shared cipher vectors with the Touareg port.
+76
View File
@@ -0,0 +1,76 @@
---
layout: default
---
# PSA2 Protocol
**Rust module:** `src/protocols/psa2.rs`
**Reference:** `Flipper-ARF lib/subghz/protocols/psa2.c`
## Overview
PSA2 (Peugeot/Citroën — internal name "PSA OLD") is the OLDER PSA variant, distinct from KAT's existing
`psa` (modified-TEA/XEA) decoder. Manchester encoding: 250/500 µs symbol (Pattern 1, standard rate) or
125/250 µs (Pattern 2, half rate), using the canonical Flipper `manchester_advance` table. 128-bit frame
= key1 (64 bits) + key2/validation word: the decoder collects 64 bits → key1, then 16 more (to 80 bits)
→ the 16-bit validation field / key2_low.
Crypto: TEA (Tiny Encryption Algorithm) with a dual brute-force fallback (BF1 0x230000000x24000000, BF2
0xF30000000xF4000000) and a mode23/mode36 selector, validated via a nibble checksum.
**Performance:** the live decoder only ever runs the cheap O(1) mode23 XOR path
(`direct_xor_decrypt`) per frame — it NEVER runs the TEA brute force. The bounded TEA brute force
(`decrypt_full`, ~16.7M iters per range) is ported faithfully but is reserved for a deferred-decrypt UI
action, mirroring the C exactly. Beyond the C's nibble-checksum gate, KAT adds two false-positive
suppressors required by its feed-all-decoders model: a `key2_high` precondition and a valid-button check
(PSA2 buttons are Lock=0, Unlock=1, Trunk=2 only). Emission is gated strictly on a successful,
field-bearing mode23 decrypt.
## Timing
| Parameter | Value | Notes |
|----------------|--------|-----------------------------|
| Short (P1) | 250 µs | ±100 µs (te_delta) |
| Long (P1) | 500 µs | ±100 µs |
| Short (P2) | 125 µs | ±50 µs (half rate) |
| Long (P2) | 250 µs | ±50 µs |
| End marker | 1000 µs (P1) / 500 µs (P2) | |
| Min bits | 128 | key1 (64) + validation (16) collected |
| Preamble | >0x46 pairs (P1) / >0x45 (P2) | |
## Frame Layout (128 bits = key1 64-bit + key2/validation)
The decoder collects 64 bits → key1, then 16 bits → the validation field (key2_low). After the mode23 XOR
decrypt (fields from `extract_fields_mode23`):
- **serial:** `(buf[2]<<16) | (buf[3]<<8) | buf[4]` (24-bit)
- **button:** `buf[8] & 0xF` — Lock=0, Unlock=1, Trunk=2
- **counter:** `(buf[5]<<8) | buf[6]` (16-bit)
- **crc:** `buf[7]`
`data` = `(key1_high << 32) | key1_low` (64 bits).
## RF
- **Encoding:** Manchester (canonical Flipper table; standard 250/500 µs or half-rate 125/250 µs)
- **RF modulation:** AM (OOK)
- **Encryption:** TEA + nibble-checksum-gated mode23 XOR path; brute-force fallback reserved for offline decrypt
- **Frequencies:** 433.92 MHz
## Decoder Steps
1. **WaitEdge (State0)** — detect the preamble rate (250 µs → Pattern 1, 125 µs → Pattern 2).
2. **CountPattern250/125 (State1/3)** — count preamble pulses past the threshold, then a long pulse enters the Manchester decode state.
3. **DecodeManchester250/125 (State2/4)** — Manchester-decode to 80 bits (key1 + validation), detect end-of-packet, then `finalize_frame` runs the cheap mode23 XOR gate and emits only on a successful field-bearing decrypt.
## Encoder
Supported (mode23 path). Increments the counter, builds key1/validation via `encode_mode23` (inverse XOR
stage + nibble checksum), then emits an 80-pair preamble, a sync, 64 key1 bits + 16 validation bits
MSB-first, and an end burst.
## Validation
Decodes REAL IMPORTS captures (IMPORTS/GROUPE PSA, serial `0x99EB25`). Unit tests also cover the TEA
round trip, the mode23 encode→decode round trip, the nibble checksum vs. the reference, and the emitted
Manchester frame length.
+69
View File
@@ -0,0 +1,69 @@
---
layout: default
---
# Toyota Protocol
**Rust module:** `src/protocols/toyota.rs`
**Reference:** `Flipper-ARF lib/subghz/protocols/toyota.c`
## Overview
Toyota / Lexus KeeLoq keyfobs, a dual-variant decoder. Two variants are detected from the first HIGH
pulse width (threshold 310 µs):
- **Variant A** (Corolla / 433.92 MHz) — PWM pairs: LS (long HIGH + short LOW) = bit 0, SL (short HIGH +
long LOW) = bit 1. Preamble = repeated short-short pairs; first non-SS pair is the first data bit.
Frame = 68 bits.
- **Variant B** (Tundra / 315 MHz) — NRZ: each individual pulse encodes one bit by a midpoint classifier
(≤287 µs → 0, >287 µs → 1). Preamble = short-HIGH/long-LOW pairs terminated by a sync gap (LOW between
1500 and 2600 µs). Frame = 67 bits.
KeeLoq hopping: the hop field is left encrypted (the reference does not decrypt or run a CRC). Emission
is gated tightly (exact frame bit count + structural preamble/sync + non-zero serial) so it does not
false-match the other KeeLoq-PWM protocols. The shared 433 MHz KeeLoq-PWM air encoding means a Variant-A
frame that also satisfies Kia V3/V4's 68-bit/CRC4 structure is claimed by Kia V3/V4 first (earlier in the
registry); Toyota uniquely claims 60-bit frames and Variant-B NRZ at 315 MHz.
## Timing
| Parameter | Value | Notes |
|------------------|--------|-----------------------------|
| A short | 400 µs | ±175 µs (Variant A) |
| A long | 800 µs | ±175 µs |
| B short | 200 µs | ±120 µs (Variant B preamble)|
| B long | 390 µs | ±120 µs |
| B NRZ midpoint | 287 µs | ≤ → 0, > → 1 |
| B sync gap | 15002600 µs | |
| Min bits | 60 | A frame = 68, B frame = 67 |
| Variant threshold| 310 µs | first HIGH: < B, ≥ A |
## Frame Layout
`data = (hop << 32) | (serial << 4) | button` (matches the reference `generic.data`):
- **hop:** 32-bit KeeLoq ciphertext (left encrypted)
- **serial:** 28-bit
- **button:** 4-bit
## RF
- **Encoding:** Variant A = PWM pairs; Variant B = NRZ
- **RF modulation:** AM/OOK
- **Encryption:** KeeLoq hop left encrypted (no key / no CRC); a fully-structured frame of the exact bit count with non-zero serial is the validity criterion
- **Frequencies:** 433.92 MHz (Variant A), 315 MHz (Variant B)
## Decoder Steps
1. **Reset** — detect the variant from the first SHORT HIGH (<310 µs → B, ≥310 µs → A).
2. **Variant A** — PreambleA (count SS pairs) → DataA (decode LS/SL pairs, cap at 68 bits, emit on the terminating gap so Kia wins shared frames by registry order).
3. **Variant B** — PreambleB (short-HIGH/long-LOW pairs) → on the sync gap → DataB (each pulse is one NRZ bit; emit at 67 bits or on an end gap).
## Encoder
Not supported (the reference `encoder` field is NULL). Decode-only.
## Validation
Decodes REAL IMPORTS captures (IMPORTS Toyota + Lexus Camry, NRZ Variant B). Unit tests also cover
synthetic Variant A and Variant B frames and rejection of an all-zero serial.
+11 -1
View File
@@ -1898,13 +1898,23 @@ impl App {
match protocol { match protocol {
p if p.starts_with("Kia") => "Kia/Hyundai", p if p.starts_with("Kia") => "Kia/Hyundai",
p if p.starts_with("Ford") => "Ford", p if p.starts_with("Ford") => "Ford",
"Honda Static" => "Honda/Acura",
"Honda V1" => "Honda/Acura",
p if p.starts_with("Fiat") => "Fiat", p if p.starts_with("Fiat") => "Fiat",
"Subaru" => "Subaru", "Subaru" => "Subaru",
"Suzuki" => "Suzuki", "Suzuki" => "Suzuki",
"VAG" | "VW" => "VW/Audi/Seat/Skoda", "VAG" | "VW" => "VW/Audi/Seat/Skoda",
"PSA" => "Peugeot/Citroen", p if p.starts_with("PSA") => "Peugeot/Citroen",
"Star Line" => "Star Line", "Star Line" => "Star Line",
"Scher-Khan" => "Scher-Khan", "Scher-Khan" => "Scher-Khan",
"Chrysler V0" => "Chrysler/Dodge/Jeep",
p if p.starts_with("Land Rover") => "Land Rover",
"Toyota" => "Toyota/Lexus",
// Covers both "Mazda V0" and "Mazda Siemens".
p if p.starts_with("Mazda") => "Mazda",
"BMW CAS4" => "BMW",
// Covers "Porsche Touareg" and "Porsche Cayenne [First/Cont/Final]".
p if p.starts_with("Porsche") => "Porsche",
_ => "Unknown", _ => "Unknown",
} }
} }
+39 -2
View File
@@ -225,19 +225,32 @@ impl Capture {
p if p.starts_with("Kia V2") => ModulationType::Manchester, p if p.starts_with("Kia V2") => ModulationType::Manchester,
p if p.starts_with("Kia V5") => ModulationType::Manchester, p if p.starts_with("Kia V5") => ModulationType::Manchester,
p if p.starts_with("Kia V6") => ModulationType::Manchester, p if p.starts_with("Kia V6") => ModulationType::Manchester,
"Ford V0" => ModulationType::Manchester, p if p.starts_with("Kia V7") => ModulationType::Manchester,
p if p.starts_with("Ford") => ModulationType::Manchester,
"Honda Static" => ModulationType::Manchester,
"Mazda Siemens" => ModulationType::Manchester,
"BMW CAS4" => ModulationType::Manchester,
"Fiat V0" => ModulationType::Manchester, "Fiat V0" => ModulationType::Manchester,
"PSA" => ModulationType::Manchester, "PSA" => ModulationType::Manchester,
"PSA2" => ModulationType::Manchester,
"VAG" => ModulationType::Manchester, "VAG" => ModulationType::Manchester,
// Differential Manchester
"Land Rover V0" => ModulationType::DifferentialManchester,
// PWM-encoded protocols // PWM-encoded protocols
p if p.starts_with("Kia V0") => ModulationType::Pwm, p if p.starts_with("Kia V0") => ModulationType::Pwm,
p if p.starts_with("Kia V3") => ModulationType::Pwm, p if p.starts_with("Kia V3") => ModulationType::Pwm,
p if p.starts_with("Kia V4") => ModulationType::Pwm, p if p.starts_with("Kia V4") => ModulationType::Pwm,
"Honda V1" => ModulationType::Pwm,
"Subaru" => ModulationType::Pwm, "Subaru" => ModulationType::Pwm,
"Suzuki" => ModulationType::Pwm, "Suzuki" => ModulationType::Pwm,
"Star Line" => ModulationType::Pwm, "Star Line" => ModulationType::Pwm,
p if p.starts_with("Keeloq (") => ModulationType::Pwm, p if p.starts_with("Keeloq (") => ModulationType::Pwm,
"Scher-Khan" => ModulationType::Pwm, "Scher-Khan" => ModulationType::Pwm,
"Chrysler V0" => ModulationType::Pwm,
"Land Rover RKE" => ModulationType::Pwm,
"Toyota" => ModulationType::Pwm,
// Display name is "Porsche Cayenne [First/Cont/Final]"; match the prefix.
p if p.starts_with("Porsche Cayenne") => ModulationType::Pwm,
// Unknown // Unknown
_ => ModulationType::Unknown, _ => ModulationType::Unknown,
} }
@@ -252,16 +265,27 @@ impl Capture {
p if p.starts_with("Kia V2") => RfModulation::FM, p if p.starts_with("Kia V2") => RfModulation::FM,
p if p.starts_with("Kia V5") => RfModulation::FM, p if p.starts_with("Kia V5") => RfModulation::FM,
p if p.starts_with("Kia V6") => RfModulation::FM, p if p.starts_with("Kia V6") => RfModulation::FM,
p if p.starts_with("Kia V7") => RfModulation::FM,
"Scher-Khan" => RfModulation::FM, "Scher-Khan" => RfModulation::FM,
"PSA" => RfModulation::FM, "PSA" => RfModulation::FM,
"PSA2" => RfModulation::AM,
"Fiat V0" => RfModulation::FM, "Fiat V0" => RfModulation::FM,
"Ford V0" => RfModulation::FM, p if p.starts_with("Ford") => RfModulation::FM,
"Honda Static" => RfModulation::FM,
"Mazda Siemens" => RfModulation::FM,
"Land Rover V0" => RfModulation::FM,
// AM only (SubGhzProtocolFlag_AM) // AM only (SubGhzProtocolFlag_AM)
p if p.starts_with("Kia V1") => RfModulation::AM, p if p.starts_with("Kia V1") => RfModulation::AM,
"Honda V1" => RfModulation::AM,
"VAG" => RfModulation::AM, "VAG" => RfModulation::AM,
"Subaru" => RfModulation::AM, "Subaru" => RfModulation::AM,
"Suzuki" => RfModulation::AM, "Suzuki" => RfModulation::AM,
"Star Line" => RfModulation::AM, "Star Line" => RfModulation::AM,
"Chrysler V0" => RfModulation::AM,
"Land Rover RKE" => RfModulation::AM,
"Toyota" => RfModulation::AM,
"BMW CAS4" => RfModulation::AM,
p if p.starts_with("Porsche Cayenne") => RfModulation::AM,
p if p.starts_with("Keeloq (") => RfModulation::AM, p if p.starts_with("Keeloq (") => RfModulation::AM,
// Both AM and FM (Kia V3/V4) // Both AM and FM (Kia V3/V4)
p if p.starts_with("Kia V3") || p.starts_with("Kia V4") => RfModulation::Both, p if p.starts_with("Kia V3") || p.starts_with("Kia V4") => RfModulation::Both,
@@ -276,14 +300,27 @@ impl Capture {
"Star Line" => "KeeLoq", "Star Line" => "KeeLoq",
p if p.starts_with("Keeloq (") => "KeeLoq", p if p.starts_with("Keeloq (") => "KeeLoq",
"PSA" => "XTEA/XOR", "PSA" => "XTEA/XOR",
"PSA2" => "TEA",
"VAG" => "AUT64/XTEA", "VAG" => "AUT64/XTEA",
"Scher-Khan" => "Magic Code", "Scher-Khan" => "Magic Code",
"Subaru" | "Suzuki" => "Rolling Code", "Subaru" | "Suzuki" => "Rolling Code",
"Chrysler V0" => "Rolling Code",
"Land Rover V0" => "Rolling Code",
"Land Rover RKE" => "KeeLoq",
"Toyota" => "KeeLoq",
"Mazda Siemens" => "Siemens XOR",
"BMW CAS4" => "CAS4 (rolling)",
p if p.starts_with("Porsche Cayenne") => "VAG rolling",
// Ford V1 is rolling code; must precede the generic Ford "Fixed Code" arm below.
"Ford V1" => "Rolling Code",
p if p.starts_with("Ford") => "Fixed Code", p if p.starts_with("Ford") => "Fixed Code",
"Honda Static" => "Fixed Code",
"Honda V1" => "Fixed Code",
p if p.starts_with("Fiat") => "Fixed Code", p if p.starts_with("Fiat") => "Fixed Code",
p if p.starts_with("Kia V0") => "Fixed Code", p if p.starts_with("Kia V0") => "Fixed Code",
p if p.starts_with("Kia V1") || p.starts_with("Kia V2") => "Fixed Code", p if p.starts_with("Kia V1") || p.starts_with("Kia V2") => "Fixed Code",
p if p.starts_with("Kia V5") || p.starts_with("Kia V6") => "Fixed Code", p if p.starts_with("Kia V5") || p.starts_with("Kia V6") => "Fixed Code",
p if p.starts_with("Kia V7") => "Fixed Code",
_ => "Unknown", _ => "Unknown",
} }
} }
+461
View File
@@ -0,0 +1,461 @@
//! BMW CAS4 protocol decoder
//!
//! Aligned with Flipper-ARF reference: `lib/subghz/protocols/bmw_cas4.c` and `bmw_cas4.h`.
//! Manchester 500/1000µs (te_delta 150), 64 bits (8 bytes), AM/OOK. The CAS4 rolling cipher's
//! manufacturer key is not available, so the encrypted portion is left as-is — the frame is only
//! framed and validated, not decrypted. Emission is gated on two fixed marker bytes
//! (byte[0]==0x30 && byte[6]==0xC5), which makes the protocol specific and prevents false matches.
//! Decode-only: the reference encoder is a non-functional stub (`yield` returns reset,
//! `deserialize` returns error), so `supports_encoding()` is false and `encode()` is None.
//!
//! Decoder steps: Reset → Preamble (≥10 pulses of 300-700µs) → Data. The preamble→data transition
//! is triggered by a long low gap (≥1800µs). Manchester polarity is `level ? Low : High` (Flipper
//! manchester_decoder.h event order: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh), the same
//! mapping as Ford V0 / common.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 500;
const TE_LONG: u32 = 1000;
const TE_DELTA: u32 = 150;
const DATA_BITS: usize = 64;
const DATA_BYTES: usize = 8;
// Preamble pulse window and minimum count (BMW_CAS4_PREAMBLE_PULSE_MIN/MAX, BMW_CAS4_PREAMBLE_MIN).
const PREAMBLE_PULSE_MIN: u32 = 300;
const PREAMBLE_PULSE_MAX: u32 = 700;
const PREAMBLE_MIN: u16 = 10;
// Long low gap that separates the preamble from the data burst (BMW_CAS4_GAP_MIN).
const GAP_MIN: u32 = 1800;
// Fixed validation markers (BMW_CAS4_BYTE0_MARKER, BMW_CAS4_BYTE6_MARKER).
const BYTE0_MARKER: u8 = 0x30;
const BYTE6_MARKER: u8 = 0xC5;
/// Manchester state machine (Flipper manchester_decoder.h transition table).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum ManchesterState {
Mid0 = 0,
Mid1 = 1,
Start0 = 2,
Start1 = 3,
}
/// Decoder step states (matches BmwCas4DecoderStep in bmw_cas4.c).
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Preamble,
Data,
}
/// BMW CAS4 protocol decoder (matches SubGhzProtocolDecoderBmwCas4).
pub struct BmwCas4Decoder {
step: DecoderStep,
manchester_state: ManchesterState,
preamble_count: u16,
raw_data: [u8; DATA_BYTES],
bit_count: usize,
decode_data: u64,
}
impl BmwCas4Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
manchester_state: ManchesterState::Mid1,
preamble_count: 0,
raw_data: [0; DATA_BYTES],
bit_count: 0,
decode_data: 0,
}
}
/// Reset accumulators (matches subghz_protocol_decoder_bmw_cas4_reset).
fn reset_state(&mut self) {
self.step = DecoderStep::Reset;
self.manchester_state = ManchesterState::Mid1;
self.preamble_count = 0;
self.raw_data = [0; DATA_BYTES];
self.bit_count = 0;
self.decode_data = 0;
}
fn is_short(d: u32) -> bool {
duration_diff!(d, TE_SHORT) < TE_DELTA
}
fn is_long(d: u32) -> bool {
duration_diff!(d, TE_LONG) < TE_DELTA
}
/// True when the duration is within the preamble pulse window (300-700µs).
fn is_preamble_pulse(d: u32) -> bool {
d >= PREAMBLE_PULSE_MIN && d <= PREAMBLE_PULSE_MAX
}
/// Flipper Manchester transition table. Event 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh.
/// Returns Some(bit) when a bit emits.
fn manchester_advance(&mut self, event: u8) -> Option<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 }
}
/// Map (level, duration) → Manchester event. BMW CAS4 polarity: level ? Low : High
/// (matches the C `event = level ? ManchesterEventShortLow : ManchesterEventShortHigh`).
fn pulse_event(level: bool, duration: u32) -> Option<u8> {
if Self::is_short(duration) {
Some(if level { 0 } else { 1 })
} else if Self::is_long(duration) {
Some(if level { 2 } else { 3 })
} else {
None
}
}
/// Append a decoded bit MSB-first into raw_data and the 64-bit accumulator
/// (matches the C bit-packing in the Data step).
fn add_bit(&mut self, bit: bool) {
if self.bit_count < DATA_BITS {
let byte_idx = self.bit_count / 8;
let bit_pos = 7 - (self.bit_count % 8);
if bit {
self.raw_data[byte_idx] |= 1 << bit_pos;
}
self.decode_data = (self.decode_data << 1) | (bit as u64);
}
self.bit_count += 1;
}
/// Validate the fixed markers and build the decoded signal.
/// Fields: serial = bytes[1..4] (24-bit), button = byte[7], counter = byte[5].
/// The CAS4 rolling cipher is left undecrypted; the markers serve as the integrity gate.
fn build_signal(&self) -> DecodedSignal {
let b = &self.raw_data;
let serial = ((b[1] as u32) << 16) | ((b[2] as u32) << 8) | (b[3] as u32);
let counter = b[5] as u16;
let button = b[7];
DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid: true, // fixed markers (byte[0]==0x30 && byte[6]==0xC5) validated the frame
data: self.decode_data,
data_count_bit: DATA_BITS,
encoder_capable: false,
extra: None,
protocol_display_name: None,
}
}
}
impl ProtocolDecoder for BmwCas4Decoder {
fn name(&self) -> &'static str {
"BMW CAS4"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: DATA_BITS,
}
}
fn supported_frequencies(&self) -> &[u32] {
// SubGhzProtocolFlag_433 only (no 315 listed in the C).
&[433_920_000]
}
fn reset(&mut self) {
self.reset_state();
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
// Begin on a high preamble pulse within the 300-700µs window.
DecoderStep::Reset => {
if level && Self::is_preamble_pulse(duration) {
self.step = DecoderStep::Preamble;
self.preamble_count = 1;
}
}
DecoderStep::Preamble => {
if Self::is_preamble_pulse(duration) {
self.preamble_count += 1;
} else if !level && duration >= GAP_MIN {
if self.preamble_count >= PREAMBLE_MIN {
// Enter data: clear accumulators and reset the Manchester state.
self.bit_count = 0;
self.decode_data = 0;
self.raw_data = [0; DATA_BYTES];
self.manchester_state = ManchesterState::Mid1;
self.step = DecoderStep::Data;
} else {
self.reset_state();
}
} else {
self.reset_state();
}
}
DecoderStep::Data => {
if self.bit_count >= DATA_BITS {
self.reset_state();
return None;
}
let event = Self::pulse_event(level, duration);
if let Some(ev) = event {
if let Some(bit) = self.manchester_advance(ev) {
self.add_bit(bit);
if self.bit_count == DATA_BITS {
let valid = self.raw_data[0] == BYTE0_MARKER
&& self.raw_data[6] == BYTE6_MARKER;
let result = if valid { Some(self.build_signal()) } else { None };
self.reset_state();
return result;
}
}
} else {
// Out-of-range pulse aborts the frame (matches the C ManchesterEventReset path).
self.reset_state();
}
}
}
None
}
fn supports_encoding(&self) -> bool {
false
}
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
None
}
}
impl Default for BmwCas4Decoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Map a Manchester event index back to its (level, duration) pulse, using the decoder's
/// polarity (level ? Low : High): 0=ShortLow(level,500), 1=ShortHigh(!level,500),
/// 2=LongLow(level,1000), 3=LongHigh(!level,1000).
fn event_pulse(event: u8) -> (bool, u32) {
match event {
0 => (true, TE_SHORT),
1 => (false, TE_SHORT),
2 => (true, TE_LONG),
3 => (false, TE_LONG),
_ => unreachable!(),
}
}
/// Pure transition table (same as `manchester_advance`) for the test encoder. Returns
/// (next_state, Some(bit)) when a bit emits.
fn advance(state: ManchesterState, event: u8) -> (ManchesterState, Option<bool>) {
use ManchesterState::{Mid0, Mid1, Start0, Start1};
let (ns, emit) = match (state, event) {
(Mid0, 0) => (Mid0, false),
(Mid0, 1) => (Start1, true),
(Mid0, 2) => (Mid0, false),
(Mid0, 3) => (Mid1, true),
(Mid1, 0) => (Start0, true),
(Mid1, 1) => (Mid1, false),
(Mid1, 2) => (Mid0, true),
(Mid1, 3) => (Mid1, false),
(Start0, 0) => (Mid0, false),
(Start0, 1) => (Mid0, false),
(Start0, 2) => (Mid0, false),
(Start0, 3) => (Mid1, false),
(Start1, 0) => (Mid0, false),
(Start1, 1) => (Mid1, false),
(Start1, 2) => (Mid0, false),
(Start1, 3) => (Mid1, false),
_ => (Mid1, false),
};
(ns, if emit { Some((event & 1) == 1) } else { None })
}
/// Faithfully Manchester-encode `bits` into level/duration pulses by *driving the decoder's
/// own transition table*. This guarantees the produced waveform is one the decoder accepts —
/// it does not assume any closed-form biphase rule (the Flipper table is differential, so a
/// fixed per-bit pattern does not exist). Backtracking DFS over the tiny state space (with a
/// visited set on (state, last_level, bit_index) to break no-emit cycles) finds a pulse stream
/// whose decoded bits equal `bits`, respecting physical level alternation between pulses.
///
/// Note: the decoder starts at Mid1, which can only emit a `0` first — so the first bit must be
/// 0 (true for any BMW CAS4 frame: byte[0]==0x30 begins with bit 0).
fn manchester_encode_bits(bits: &[bool]) -> Option<Vec<(bool, u32)>> {
use std::collections::HashSet;
fn dfs(
state: ManchesterState,
last_level: Option<bool>,
i: usize,
bits: &[bool],
acc: &mut Vec<u8>,
seen: &mut std::collections::HashSet<(ManchesterState, Option<bool>, usize)>,
) -> bool {
if i == bits.len() {
return true;
}
let key = (state, last_level, i);
if seen.contains(&key) {
return false;
}
seen.insert(key);
for event in 0u8..4 {
let (level, _dur) = event_pulse(event);
if last_level == Some(level) {
continue; // physical OOK stream must alternate level between pulses
}
let (ns, bit) = advance(state, event);
match bit {
None => {
acc.push(event);
if dfs(ns, Some(level), i, bits, acc, seen) {
return true;
}
acc.pop();
}
Some(b) if b == bits[i] => {
acc.push(event);
if dfs(ns, Some(level), i + 1, bits, acc, seen) {
return true;
}
acc.pop();
}
_ => {}
}
}
false
}
let mut events: Vec<u8> = Vec::new();
let mut seen: HashSet<(ManchesterState, Option<bool>, usize)> = HashSet::new();
if !dfs(ManchesterState::Mid1, None, 0, bits, &mut events, &mut seen) {
return None;
}
// Translate events → pulses, merging adjacent equal levels (a repeated level is a long).
let mut merged: Vec<(bool, u32)> = Vec::new();
for ev in events {
let (lvl, dur) = event_pulse(ev);
if let Some(last) = merged.last_mut() {
if last.0 == lvl {
last.1 += dur;
continue;
}
}
merged.push((lvl, dur));
}
Some(merged)
}
/// Build a full BMW CAS4 frame: preamble pulses, long low gap, then Manchester data.
fn build_frame(bytes: &[u8; DATA_BYTES]) -> Vec<(bool, u32)> {
let mut pairs: Vec<(bool, u32)> = Vec::new();
// Preamble: 12 high pulses of ~500µs separated by short lows (within the 300-700 window).
for _ in 0..12 {
pairs.push((true, 500));
pairs.push((false, 500));
}
// Long low gap (≥1800µs) ending the preamble. Overwrite the last low with the gap.
if let Some(last) = pairs.last_mut() {
if !last.0 {
last.1 = GAP_MIN + 200;
}
}
// Manchester-encoded data bits, MSB-first.
let mut bits: Vec<bool> = Vec::with_capacity(DATA_BITS);
for &byte in bytes.iter() {
for i in (0..8).rev() {
bits.push((byte >> i) & 1 != 0);
}
}
let data = manchester_encode_bits(&bits).expect("test frame must be encodable (first bit 0)");
pairs.extend(data);
// Trailing gap to flush.
pairs.push((false, GAP_MIN + 500));
pairs
}
/// Feed a pair stream through a fresh decoder and return the first decode (trying both
/// polarities, matching the registry behaviour).
fn decode_pairs(pairs: &[(bool, u32)]) -> Option<DecodedSignal> {
for invert in [false, true] {
let mut dec = BmwCas4Decoder::new();
for &(lvl, dur) in pairs {
let level = if invert { !lvl } else { lvl };
if let Some(sig) = dec.feed(level, dur) {
return Some(sig);
}
}
}
None
}
#[test]
fn decodes_synthetic_frame_with_markers() {
// byte[0]=0x30 and byte[6]=0xC5 are the required markers; the rest is the (encrypted)
// payload. serial = bytes[1..4], counter = byte[5], button = byte[7].
let frame: [u8; DATA_BYTES] = [0x30, 0x12, 0x34, 0x56, 0xAB, 0x07, 0xC5, 0x02];
let pairs = build_frame(&frame);
let sig = decode_pairs(&pairs).expect("synthetic BMW CAS4 frame should decode");
assert!(sig.crc_valid, "fixed markers should set crc_valid=true");
assert_eq!(sig.data_count_bit, DATA_BITS);
assert_eq!(sig.data, u64::from_be_bytes(frame), "data must equal the 64-bit frame");
assert_eq!(sig.serial, Some(0x123456), "serial = bytes[1..4]");
assert_eq!(sig.counter, Some(0x07), "counter = byte[5]");
assert_eq!(sig.button, Some(0x02), "button = byte[7]");
}
#[test]
fn rejects_frame_with_wrong_markers() {
// Same structure but byte[0] and byte[6] are NOT the markers → must not decode.
let frame: [u8; DATA_BYTES] = [0x31, 0x12, 0x34, 0x56, 0xAB, 0x07, 0xC4, 0x02];
let pairs = build_frame(&frame);
assert!(
decode_pairs(&pairs).is_none(),
"a frame with wrong marker bytes must be rejected"
);
}
}
+604
View File
@@ -0,0 +1,604 @@
//! Chrysler V0 protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/chrysler_v0.c` and
//! `chrysler_v0.h`. Used by Chrysler/Dodge/Jeep keyfobs.
//!
//! Protocol: PWM with a short HIGH pulse and TWO long-LOW symbols. A "1" payload bit is
//! HIGH≈600µs (te_one_short) + LOW≈3400µs (te_long_a); a "0" payload bit is HIGH≈300µs
//! (te_short) + LOW≈3700µs (te_long_b). te_delta≈150, long_delta≈400, te_gap≈8000,
//! frame_gap≈15600. ~24 preamble pairs (short HIGH + long_b LOW) precede each frame.
//!
//! Frame: 80 bits. The first 64 bits are `decode_data` (payload bytes 0..7), the last 16 bits
//! are `data_2` (payload bytes 8,9). The 80-bit frame exceeds u64, so [DecodedSignal::data]
//! reports the most-significant 64 bits and `data_count_bit = 80` (matches psa.rs/kia_v6.rs).
//!
//! Crypto (proprietary seed-XOR, ported exactly from chrysler_v0.c `decode`):
//! - `seed = reverse6(key[0] >> 2)` — a 6-bit reversed counter used as the transform key.
//! - `transform_block`: XOR all 9 transformed bytes with `xor_table[seed & 0x0F]`, with an extra
//! nibble flip when the (Lock) button is set.
//! - Dual payload A (seed even, carries serial+counter) / B (seed odd, carries serial). The frame
//! is gated on a structural `check_ok` (matches the C), so it does not false-match other
//! protocols. `crc_valid` reflects that `check_ok`.
//!
//! RF: AM, 315 + 433.92 MHz. Encoder present (ENABLE_EMULATE_FEATURE): builds preamble + dual
//! 80-bit PWM frames with frame gaps.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 0x12C; // 300
const TE_DELTA: u32 = 0x96; // 150
const TE_LONG_A: u32 = 0xD48; // 3400
const TE_LONG_B: u32 = 0xE74; // 3700
const TE_LONG_DELTA: u32 = 0x190; // 400
const TE_GAP: u32 = 0x1F40; // 8000
const TE_ONE_SHORT: u32 = 0x258; // 600
const FRAME_GAP: u32 = 0x3CF0; // 15600
const PREAMBLE_PAIRS: usize = 24;
const DECODE_BIT_COUNT: usize = 0x50; // 80
/// XOR table (chrysler_v0_xor_table) — indexed by `seed & 0x0F`.
const XOR_TABLE: [u8; 16] = [
0x0F, 0x02, 0x40, 0x0C, 0x30, 0x0E, 0x70, 0x08, 0x10, 0x0A, 0x50, 0xF4, 0x2F, 0xF6, 0x6F, 0xF0,
];
/// Decoder steps (matches Chrysler_V0DecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Seek,
Data,
}
/// Chrysler V0 protocol decoder
pub struct ChryslerV0Decoder {
step: DecoderStep,
packet_bit_count: u16,
te_last: u32,
decode_data: u64,
decode_count_bit: u8,
data_2: u16,
}
/// Result of `decode_packet`: structural fields extracted from a candidate frame.
struct Decoded {
check_ok: bool,
button: u8,
/// Transform seed (reversed 6-bit counter); retained for diagnostics.
#[allow(dead_code)]
seed: u8,
/// Serial: SnA (counter-frame) or SnB (serial-frame).
serial: u32,
/// Rolling counter (only meaningful for the A/even frame).
counter: u32,
}
impl ChryslerV0Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
packet_bit_count: 0,
te_last: 0,
decode_data: 0,
decode_count_bit: 0,
data_2: 0,
}
}
fn is_short(d: u32) -> bool {
duration_diff!(d, TE_SHORT) <= TE_DELTA
}
fn is_long_mark(d: u32) -> bool {
duration_diff!(d, TE_LONG_A) <= TE_LONG_DELTA || duration_diff!(d, TE_LONG_B) <= TE_LONG_DELTA
}
/// Reverse the low 6 bits of `value` (chrysler_v0_reverse6).
fn reverse6(value: u32) -> u8 {
let mut out: u8 = 0;
let mut v = value;
for _ in 0..6 {
out = (out << 1) | ((v & 1) as u8);
v >>= 1;
}
out
}
/// Transform 9 bytes with the seed-derived XOR mask (chrysler_v0_transform_block).
/// `button == 1` (Lock) flips a nibble of the mask depending on the seed parity.
fn transform_block(input: &[u8; 9], key: u8, button: u8) -> [u8; 9] {
let mut mask = XOR_TABLE[(key & 0x0F) as usize];
if button == 1 {
mask ^= if (key & 1) != 0 { 0xF0 } else { 0x0F };
}
let mut out = [0u8; 9];
for i in 0..9 {
out[i] = input[i] ^ mask;
}
out
}
/// Port of chrysler_v0_decode_packet: extract seed/button/check_ok/serial/counter from the
/// 64-bit `data` (payload bytes 0..7) plus the 16-bit `data_2` (payload bytes 8,9).
fn decode_packet(data: u64, data_2: u16) -> Decoded {
let key = data.to_be_bytes();
let key2 = data_2;
let seed = Self::reverse6((key[0] >> 2) as u32);
let b1_xor_b6 = key[6] ^ key[1];
let msb_set = (key[0] & 0x80) != 0;
let mut check_ok;
let mut button: u8;
if msb_set {
let key2_low = (key2 & 0xFF) as u8;
check_ok = (key[1] == key[5]) && (b1_xor_b6 == 0x62);
button = if (key2_low ^ key[4]) == 0x10 { 2 } else { 1 };
} else {
check_ok = false;
button = 1;
if (key[1] ^ 0xC3) == key[5] {
if b1_xor_b6 == 0x04 {
check_ok = true;
} else {
check_ok = b1_xor_b6 == 0x08;
if b1_xor_b6 == 0x08 {
button = 2;
}
}
} else if b1_xor_b6 == 0x08 {
button = 2;
}
// (b1_xor_b6 == 0x04 with mismatched key[5]: button stays 1, check_ok stays false.)
}
let encoded: [u8; 9] = [
key[1],
key[2],
key[3],
key[4],
key[5],
key[6],
key[7],
(key2 >> 8) as u8,
(key2 & 0xFF) as u8,
];
let decoded = Self::transform_block(&encoded, seed, button);
let (serial, counter) = if (seed & 1) != 0 {
// Payload B: serial only.
let sn_b = ((decoded[0] as u32) << 24)
| ((decoded[1] as u32) << 16)
| ((decoded[2] as u32) << 8)
| (decoded[7] as u32);
(sn_b, 0u32)
} else {
// Payload A: serial (SnA) + rolling counter.
let sn_a = ((decoded[0] as u32) << 24)
| ((decoded[1] as u32) << 16)
| ((decoded[2] as u32) << 8)
| (decoded[3] as u32);
let cnt = sn_a;
(sn_a, cnt)
};
Decoded {
check_ok,
button,
seed,
serial,
counter,
}
}
/// Build a DecodedSignal from a committed frame, if the structural check passes.
fn commit(&self) -> Option<DecodedSignal> {
let d = Self::decode_packet(self.decode_data, self.data_2);
if !d.check_ok {
return None;
}
Some(DecodedSignal {
serial: Some(d.serial),
button: Some(d.button),
counter: Some((d.counter & 0xFFFF) as u16),
crc_valid: d.check_ok,
// 80-bit frame: report the most-significant 64 bits (payload bytes 0..7).
data: self.decode_data,
data_count_bit: DECODE_BIT_COUNT,
encoder_capable: true,
// Stash the low 16 bits (data_2 = payload bytes 8,9) for the encoder.
extra: Some(self.data_2 as u64),
protocol_display_name: None,
})
}
/// Map a KAT generic button to a Chrysler button code (1=Lock, 2=Unlock).
fn map_button(button: u8) -> u8 {
match button {
0x01 => 1, // Lock
0x02 => 2, // Unlock
0x04 => 1, // Trunk → Lock (Chrysler V0 only models Lock/Unlock)
0x08 => 2, // Panic → Unlock
_ => 1,
}
}
/// Read one bit out of an MSB-first payload (chrysler_v0_payload_get_bit).
fn payload_get_bit(payload: &[u8; 10], index: u8) -> u8 {
let byte = payload[(index >> 3) as usize];
let shift = 7 - (index & 7);
(byte >> shift) & 1
}
/// Build a 10-byte payload from the 9 plaintext bytes (chrysler_v0_build_payload).
fn build_payload(plain: &[u8; 9], counter: u8, button: u8, header_low2: u8) -> [u8; 10] {
let transformed = Self::transform_block(plain, counter, button);
let mut out = [0u8; 10];
out[0] = (Self::reverse6(counter as u32) << 2) | (header_low2 & 0x03);
out[1..10].copy_from_slice(&transformed);
out
}
/// ADD_LEVEL-style merge: combine adjacent same-level pulses.
fn add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
if let Some(last) = signal.last_mut() {
if last.level == level {
*last = LevelDuration::new(level, last.duration_us + duration);
return;
}
}
signal.push(LevelDuration::new(level, duration));
}
/// Emit one 80-bit PWM payload frame (without the leading preamble).
fn emit_payload(signal: &mut Vec<LevelDuration>, payload: &[u8; 10]) {
for bit in 0..80u8 {
let value = Self::payload_get_bit(payload, bit);
if value != 0 {
Self::add_level(signal, true, TE_ONE_SHORT);
Self::add_level(signal, false, TE_LONG_A);
} else {
Self::add_level(signal, true, TE_SHORT);
Self::add_level(signal, false, TE_LONG_B);
}
}
}
/// Emit the preamble (24 short-HIGH + long_b-LOW pairs).
fn emit_preamble(signal: &mut Vec<LevelDuration>) {
for _ in 0..PREAMBLE_PAIRS {
Self::add_level(signal, true, TE_SHORT);
Self::add_level(signal, false, TE_LONG_B);
}
}
}
impl ProtocolDecoder for ChryslerV0Decoder {
fn name(&self) -> &'static str {
"Chrysler V0"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG_A,
te_delta: TE_DELTA,
min_count_bit: DECODE_BIT_COUNT,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.packet_bit_count = 0;
self.te_last = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
self.data_2 = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if level && Self::is_short(duration) {
self.packet_bit_count = 0;
self.te_last = duration;
self.step = DecoderStep::Seek;
}
}
DecoderStep::Seek => {
if level {
self.te_last = duration;
return None;
}
if Self::is_long_mark(duration) {
if Self::is_short(self.te_last) {
self.packet_bit_count += 1;
} else if self.packet_bit_count > 0x0F {
self.data_2 = 0;
self.step = DecoderStep::Data;
self.decode_data = 1;
self.decode_count_bit = 1;
} else {
self.packet_bit_count = 0;
self.step = DecoderStep::Seek;
}
return None;
}
if duration > TE_GAP && self.packet_bit_count > 0x0F {
self.decode_data = 0;
self.data_2 = 0;
self.decode_count_bit = 0;
self.step = DecoderStep::Data;
return None;
}
self.step = DecoderStep::Reset;
self.packet_bit_count = 0;
}
DecoderStep::Data => {
if level {
self.te_last = duration;
return None;
}
let count = self.decode_count_bit;
if duration > TE_GAP {
let result = if count as usize > 0x4F { self.commit() } else { None };
self.step = DecoderStep::Reset;
self.packet_bit_count = 0;
return result;
}
let bit_value: u8;
if self.te_last < TE_SHORT {
if !Self::is_short(self.te_last) || !Self::is_long_mark(duration) {
let result = if count as usize > 0x4F { self.commit() } else { None };
self.step = DecoderStep::Reset;
self.packet_bit_count = 0;
return result;
}
bit_value = 1;
} else {
if self.te_last > 0x2EE || !Self::is_long_mark(duration) {
let result = if count as usize > 0x4F { self.commit() } else { None };
self.step = DecoderStep::Reset;
self.packet_bit_count = 0;
return result;
}
bit_value = if Self::is_short(self.te_last) { 1 } else { 0 };
}
let bit = (bit_value ^ 1) as u64;
let new_count = count.wrapping_add(1);
if count <= 0x3F {
self.decode_data = (self.decode_data << 1) | bit;
self.decode_count_bit = new_count;
return None;
}
self.data_2 = (self.data_2 << 1) | (bit as u16);
self.decode_count_bit = new_count;
if new_count as usize != DECODE_BIT_COUNT {
return None;
}
let result = self.commit();
self.decode_data = 0;
self.data_2 = 0;
self.decode_count_bit = 0;
self.step = DecoderStep::Reset;
self.packet_bit_count = 0;
return result;
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
// Rebuild the 10-byte payload A (high 64 bits + low 16 bits) from the decoded frame,
// re-derive seed/plaintext, then emit preamble + dual PWM frames (matches
// chrysler_v0_build_upload, ENABLE_EMULATE_FEATURE).
let data = decoded.data;
let data_2 = decoded.extra.unwrap_or(0) as u16;
let key = data.to_be_bytes();
let key2 = data_2;
// Header low-2 bits live in the top byte's low nibble (matches encoder deserialize:
// plain_header = (data >> 56) & 0x03).
let header_low2 = (key[0]) & 0x03;
let seed = Self::reverse6((key[0] >> 2) as u32);
// Determine the originally-transmitted button (matches encoder deserialize).
let original_button: u8 = if (key[0] & 0x80) == 0 {
if (key[1] ^ key[6]) == 0x08 { 2 } else { 1 }
} else if (((key2 & 0xFF) as u8) ^ key[4]) == 0x10 {
2
} else {
1
};
// Recover the plaintext (Plain_A/Plain_B both default to this in the C when no explicit
// Plain_A/B is stored — which is our case).
let encoded: [u8; 9] = [
key[1],
key[2],
key[3],
key[4],
key[5],
key[6],
key[7],
(key2 >> 8) as u8,
(key2 & 0xFF) as u8,
];
let generated = Self::transform_block(&encoded, seed, original_button);
let mut plain_a = generated;
let mut plain_b = generated;
// Apply the requested button (KAT button → Chrysler 1/2).
let tx_button = Self::map_button(button);
if tx_button != original_button {
plain_a[5] ^= 0x0C;
plain_b[3] ^= 0x30;
}
// Counter handling (matches encoder deserialize): counter_a is the even counter, counter_b
// is counter_a - 1 (wrapping in 6 bits). Derive the base counter from the seed.
let counter = (seed as u32) & 0x3F;
let mut counter_a = (counter & 0x3F) as u8;
if (counter_a & 1) != 0 {
counter_a = counter_a.wrapping_sub(1) & 0x3F;
}
let counter_b = if counter_a == 0 { 0x3F } else { counter_a - 1 };
let payload_a = Self::build_payload(&plain_a, counter_a, tx_button, header_low2);
let payload_b = Self::build_payload(&plain_b, counter_b, tx_button, header_low2);
let mut signal = Vec::with_capacity(PREAMBLE_PAIRS * 4 + 80 * 4 + 16);
// Frame A: preamble, short + frame_gap, payload A, short + frame_gap.
Self::emit_preamble(&mut signal);
Self::add_level(&mut signal, true, TE_SHORT);
Self::add_level(&mut signal, false, FRAME_GAP);
Self::emit_payload(&mut signal, &payload_a);
Self::add_level(&mut signal, true, TE_SHORT);
Self::add_level(&mut signal, false, FRAME_GAP);
// Frame B: preamble, short + frame_gap, payload B, short + frame_gap.
Self::emit_preamble(&mut signal);
Self::add_level(&mut signal, true, TE_SHORT);
Self::add_level(&mut signal, false, FRAME_GAP);
Self::emit_payload(&mut signal, &payload_b);
Self::add_level(&mut signal, true, TE_SHORT);
Self::add_level(&mut signal, false, FRAME_GAP);
Some(signal)
}
}
impl Default for ChryslerV0Decoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Decode a stream of pairs with a fresh decoder, returning the first frame.
fn decode_stream(pairs: &[LevelDuration]) -> Option<DecodedSignal> {
let mut dec = ChryslerV0Decoder::new();
for p in pairs {
if let Some(sig) = dec.feed(p.level, p.duration_us) {
return Some(sig);
}
}
None
}
/// reverse6 is its own inverse on 6-bit values.
#[test]
fn reverse6_is_involution() {
for v in 0u32..64 {
let r = ChryslerV0Decoder::reverse6(v) as u32;
assert_eq!(ChryslerV0Decoder::reverse6(r) as u32, v, "reverse6 not involutive at {v}");
}
}
/// transform_block is its own inverse for a given (seed, button) — XOR with a constant mask.
#[test]
fn transform_block_round_trips() {
let plain: [u8; 9] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99];
for &button in &[1u8, 2u8] {
for seed in 0u8..64 {
let enc = ChryslerV0Decoder::transform_block(&plain, seed, button);
let dec = ChryslerV0Decoder::transform_block(&enc, seed, button);
assert_eq!(dec, plain, "transform not invertible seed={seed} button={button}");
}
}
}
/// Build a payload-A frame exactly as the encoder does (even counter, MSB-clear path), encode
/// it to a PWM burst, decode it back, and confirm the frame, check_ok, button and serial all
/// survive the round trip. Payload A is the first frame the encoder emits, so `decode_stream`
/// returns it.
///
/// Payload-A check_ok requires (in the stored = transformed bytes): `(key[1]^0xC3)==key[5]`
/// and `key[6]^key[1]==0x04`. Since transform XORs every byte with a constant mask, this is
/// equivalent to a check on the *plaintext*: `plain[0]^0xC3==plain[4]` and `plain[5]^plain[0]==0x04`.
#[test]
fn encode_decode_round_trip() {
// Choose an even counter so the encoder's counter_a equals it (exact round trip), and pick
// plaintext satisfying the payload-A invariants.
let counter_a: u8 = 0x14; // even
let button: u8 = 1; // Lock (matches the b1^b6==0x04 branch which keeps button=1)
let header_low2: u8 = 0x02;
let p0 = 0x5Au8;
let mut plain = [0x00u8; 9];
plain[0] = p0;
plain[1] = 0x11;
plain[2] = 0x22;
plain[3] = 0x33;
plain[4] = p0 ^ 0xC3; // plain[0]^0xC3 == plain[4]
plain[5] = p0 ^ 0x04; // plain[5]^plain[0] == 0x04
plain[6] = 0x66;
plain[7] = 0x77;
plain[8] = 0x88;
// Build the 10-byte payload the way the encoder/transmitter does.
let payload = ChryslerV0Decoder::build_payload(&plain, counter_a, button, header_low2);
let data = u64::from_be_bytes([
payload[0], payload[1], payload[2], payload[3], payload[4], payload[5], payload[6],
payload[7],
]);
let data_2: u16 = ((payload[8] as u16) << 8) | payload[9] as u16;
// Sanity: the raw frame must pass the structural check before we exercise the codec.
let d = ChryslerV0Decoder::decode_packet(data, data_2);
assert!(d.check_ok, "constructed payload-A vector should satisfy check_ok");
assert_eq!(d.button, button, "constructed button mismatch");
assert_eq!(d.seed & 1, 0, "payload A must have an even seed");
let decoded = DecodedSignal {
serial: Some(d.serial),
button: Some(d.button),
counter: Some((d.counter & 0xFFFF) as u16),
crc_valid: true,
data,
data_count_bit: DECODE_BIT_COUNT,
encoder_capable: true,
extra: Some(data_2 as u64),
protocol_display_name: None,
};
let dec = ChryslerV0Decoder::new();
// Encode with the same (Lock) button so the plaintext is not perturbed.
let burst = dec.encode(&decoded, 0x01).expect("encode should succeed");
assert!(!burst.is_empty());
let got = decode_stream(&burst).expect("encoded burst should decode");
assert!(got.crc_valid, "decoded frame should pass check");
// The high 64 bits (payload bytes 0..7) must reproduce the original frame.
assert_eq!(got.data, data, "round-trip data mismatch");
assert_eq!(got.button, decoded.button, "button mismatch");
assert_eq!(got.serial, decoded.serial, "serial mismatch");
}
}
+778
View File
@@ -0,0 +1,778 @@
//! Ford V1 protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v1.c` and `ford_v1.h`.
//! Manchester 65/130µs (te_delta 39), FM. 136 bits / 17 bytes: key1 (bytes 0..7, 56 bits) +
//! key2 (bytes 7..15, 64 bits) + CRC16 (bytes 15..16). Preamble ≥50 long pulses, then a short-pulse
//! sync window (`sync_event_count > 2`) replays buffered Manchester events and enters the 17-byte
//! data collection. This is a ROLLING-code protocol.
//!
//! Crypto: a proprietary parity-based descrambling cipher (`ford_v1_decode_with_flag`) operating on
//! the 9-byte air block `raw[6..15]`, plus CRC16/CCITT (poly 0x1021, init 0x0000) over `raw[3..15]`.
//! Emission is gated on CRC16 validity (with a 17-byte bit-inverted fallback) so it never
//! false-matches. Encryption/rolling detection mirrors the C: a strict branch
//! (`decoded[3]==raw[5] && decoded[4]==raw[6]`) yields plaintext serial/button/counter; otherwise an
//! encode round-trip check classifies it as encrypted/rolling. Encoder supported (6 bursts).
//!
//! Manchester transition table is the Flipper differential-Manchester table (same as Ford V0/V2).
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 65;
const TE_LONG: u32 = 130;
const TE_DELTA: u32 = 39;
const DATA_BITS: usize = 136;
const DATA_BYTES: usize = 17;
const PREAMBLE_MIN: u16 = 50;
/// C uses FORD_V1_DELTA_LONG (40) only for the preamble long-pulse match; data/sync use te_delta (39).
const DELTA_LONG: u32 = 40;
const SILENCE_LONG_MULT: u32 = 3;
// Manchester event encoding (matches Flipper ManchesterEvent ordinals used in the C buffer):
// 0 = ShortLow, 1 = ShortHigh, 2 = LongLow, 3 = LongHigh.
const EV_SHORT_LOW: u8 = 0;
const EV_SHORT_HIGH: u8 = 1;
const EV_LONG_LOW: u8 = 2;
const EV_LONG_HIGH: u8 = 3;
// Encoder constants (subghz_protocol_encoder_ford_v1).
const ENC_BURST_COUNT: usize = 6;
const ENC_PREAMBLE_PAIRS: usize = 400;
const ENC_SYNC_SHORT_US: u32 = 65;
const ENC_SYNC_LONG_US: u32 = 130;
const ENC_GAP_REPEAT_US: u32 = 50000;
const ENC_GAP_LAST_US: u32 = 260;
/// Per-burst override of pkt[4] (matches ford_v1_encoder_burst_pkt4_vals).
const ENC_BURST_PKT4: [u8; 6] = [0x08, 0x00, 0x10, 0x08, 0x00, 0x10];
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0 = 0,
Mid1 = 1,
Start0 = 2,
Start1 = 3,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Preamble,
Sync,
Data,
}
pub struct FordV1Decoder {
step: DecoderStep,
manchester_state: ManchesterState,
preamble_count: u16,
decode_data: u64,
decode_count_bit: usize,
byte_count: usize,
raw_bytes: [u8; DATA_BYTES + 1],
sync_event_idx: u8,
sync_event_count: u8,
sync_events: [u8; 8],
}
impl FordV1Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
manchester_state: ManchesterState::Mid1,
preamble_count: 0,
decode_data: 0,
decode_count_bit: 0,
byte_count: 0,
raw_bytes: [0; DATA_BYTES + 1],
sync_event_idx: 0,
sync_event_count: 0,
sync_events: [0; 8],
}
}
fn reset_state(&mut self) {
self.step = DecoderStep::Reset;
self.manchester_state = ManchesterState::Mid1;
self.preamble_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
self.byte_count = 0;
self.raw_bytes = [0; DATA_BYTES + 1];
self.sync_event_idx = 0;
self.sync_event_count = 0;
self.sync_events = [0; 8];
}
/// Short/long matchers. Data and sync use te_delta (39); preamble-long uses DELTA_LONG (40).
fn is_short(d: u32) -> bool {
duration_diff!(d, TE_SHORT) < TE_DELTA
}
fn is_long(d: u32) -> bool {
duration_diff!(d, TE_LONG) < TE_DELTA
}
fn is_preamble_long(d: u32) -> bool {
duration_diff!(d, TE_LONG) < DELTA_LONG
}
/// Flipper differential-Manchester transition table (same as Ford V0/V2/Kia V7).
/// Event: 0=ShortLow,1=ShortHigh,2=LongLow,3=LongHigh. Returns Some(bit) when a bit emits.
fn manchester_advance(&mut self, event: u8) -> Option<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 }
}
/// Push a decoded Manchester bit into the byte buffer (matches C feed/data byte assembly).
fn push_bit(&mut self, data_bit: bool) {
self.decode_data = (self.decode_data << 1) | (data_bit as u64);
self.decode_count_bit += 1;
if self.decode_count_bit & 7 == 0 {
let byte_val = (self.decode_data & 0xFF) as u8;
if self.byte_count < DATA_BYTES {
self.raw_bytes[self.byte_count] = byte_val;
self.byte_count += 1;
}
self.decode_data = 0;
}
}
// =========================================================================
// CRC16/CCITT (poly 0x1021, init 0x0000) — module-local, matches
// subghz_protocol_blocks_crc16(data, len, 0x1021, 0x0000).
// =========================================================================
fn crc16(data: &[u8]) -> u16 {
let mut crc: u16 = 0x0000;
for &byte in data {
crc ^= (byte as u16) << 8;
for _ in 0..8 {
if crc & 0x8000 != 0 {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
crc
}
// =========================================================================
// Descrambling cipher (ford_v1_decode_with_flag) — operates on the 9-byte
// air block in place. Matches the C exactly.
// =========================================================================
fn decode_with_flag(raw: &mut [u8; 9], flag_byte: u8) {
if flag_byte != 0 {
let xor_byte = raw[7];
for i in 1..7 {
raw[i] ^= xor_byte;
}
} else {
let xor_byte = raw[6];
for i in 1..6 {
raw[i] ^= xor_byte;
}
raw[7] ^= xor_byte;
}
let b6 = raw[6];
let b7 = raw[7];
raw[6] = (b6 & 0xAA) | (b7 & 0x55);
raw[7] = (b7 & 0xAA) | (b6 & 0x55);
}
/// Parity-driven descramble used when neither strict branch matches (ford_v1_decode).
fn decode_air(raw: &mut [u8; 9]) {
let endbyte = raw[8];
let parity_any = endbyte != 0;
let mut parity = 0u8;
let mut tmp = endbyte;
while tmp != 0 {
parity ^= tmp & 1;
tmp >>= 1;
}
let flag_byte = if parity_any { parity } else { 0 };
Self::decode_with_flag(raw, flag_byte);
}
// =========================================================================
// Inverse cipher (encoder side) — ford_v1_encode_inverse_block. Takes a
// 9-byte plaintext block and produces the air (scrambled) block in place.
// =========================================================================
fn encode_inverse_block(block: &mut [u8; 9]) {
let mut sum: u8 = 0;
for i in 1..=7 {
sum = sum.wrapping_add(block[i]);
}
let p6 = block[6];
let p7 = block[7];
let post6 = (p6 & 0xAA) | (p7 & 0x55);
let post7 = (p7 & 0xAA) | (p6 & 0x55);
let xorv = post6 ^ post7;
let xor_byte;
if (sum.count_ones() & 1) != 0 {
block[6] = xorv;
block[7] = post7;
xor_byte = post7;
} else {
block[6] = post6;
block[7] = xorv;
xor_byte = post6;
}
for i in 1..=5 {
block[i] ^= xor_byte;
}
}
fn encode_air_9bytes(plain9: &[u8; 9]) -> [u8; 9] {
let mut block = *plain9;
Self::encode_inverse_block(&mut block);
block
}
/// Recover plaintext from an air block by trying both descramble flags and verifying
/// the result re-encodes to the same air bytes (ford_v1_plain_from_air).
fn plain_from_air(air9: &[u8; 9]) -> Option<[u8; 9]> {
for flag in 0u8..2 {
let mut cand = *air9;
Self::decode_with_flag(&mut cand, flag);
let reair = Self::encode_air_9bytes(&cand);
if reair == *air9 {
return Some(cand);
}
}
None
}
/// Extract serial/button/counter from a plaintext 9-byte block (ford_v1_fields_from_plain).
fn fields_from_plain(plain9: &[u8; 9]) -> (u32, u8, u32) {
let serial = ((plain9[1] as u32) << 24)
| ((plain9[2] as u32) << 16)
| ((plain9[3] as u32) << 8)
| (plain9[0] as u32);
let btn = (plain9[5] >> 4) & 0x0F;
let cnt = (((plain9[5] & 0x0F) as u32) << 16) | ((plain9[6] as u32) << 8) | (plain9[7] as u32);
(serial, btn, cnt)
}
// =========================================================================
// process_data (ford_v1_process_data): CRC16 gate (+ 17-byte inverted
// fallback), descramble, field extraction, dual-branch classification.
// Returns Some(DecodedSignal) when CRC passes.
// =========================================================================
fn process_data(&self) -> Option<DecodedSignal> {
let mut raw = [0u8; DATA_BYTES];
raw.copy_from_slice(&self.raw_bytes[..DATA_BYTES]);
let mut calc_crc = Self::crc16(&raw[3..15]);
let mut recv_crc = ((raw[15] as u16) << 8) | raw[16] as u16;
// Fallback: bit-invert all 17 bytes and retry the CRC (matches C).
if recv_crc != calc_crc {
for (i, b) in raw.iter_mut().enumerate() {
*b = !self.raw_bytes[i];
}
calc_crc = Self::crc16(&raw[3..15]);
recv_crc = ((raw[15] as u16) << 8) | raw[16] as u16;
}
if recv_crc != calc_crc {
return None;
}
// Air block = raw[6..15] (9 bytes). Try both descramble branches "strictly".
let mut air9 = [0u8; 9];
air9.copy_from_slice(&raw[6..15]);
let mut decoded_b0 = air9;
Self::decode_with_flag(&mut decoded_b0, 0);
let mut decoded_b1 = air9;
Self::decode_with_flag(&mut decoded_b1, 1);
let (decoded, strict_ok): ([u8; 9], bool) =
if decoded_b0[3] == raw[5] && decoded_b0[4] == raw[6] {
(decoded_b0, true)
} else if decoded_b1[3] == raw[5] && decoded_b1[4] == raw[6] {
(decoded_b1, true)
} else if let Some(p) = Self::plain_from_air(&air9) {
// Encrypted/rolling: round-trip recovered but not a strict cleartext match.
(p, false)
} else {
let mut p = air9;
Self::decode_air(&mut p);
(p, false)
};
let recalc_crc = Self::crc16(&raw[3..15]);
// key1 = raw[0..7] (56 bits, big-endian) → DecodedSignal.data.
let mut key1: u64 = 0;
for &b in raw.iter().take(7) {
key1 = (key1 << 8) | b as u64;
}
let (serial, button, counter) = if strict_ok {
let (s, b, c) = Self::fields_from_plain(&decoded);
(s, b, c)
} else {
// Header-only: device id from raw[3..7], no button/counter (matches C).
let device_id = ((raw[3] as u32) << 24)
| ((raw[4] as u32) << 16)
| ((raw[5] as u32) << 8)
| (raw[6] as u32);
(device_id, 0u8, 0u32)
};
// Stash everything the encoder needs to rebuild the full 17-byte frame from fields.
// `data` already carries key1 = raw[0..7] (56 bits). The air block raw[6..15] and the
// CRC bytes are regenerated by re-encoding the plaintext, which is fully determined by
// serial/button/counter EXCEPT plain[4] (the byte the strict branch constrains, equal to
// air9[0]) and the derived checksum plain[8]. So we stash plain[4] plus the strict flag.
// Layout (u64, MSB first):
// [63:48]=crc16 [47:40]=strict_ok [39:32]=plain[4] [31:0]=0 (reserved).
let plain4 = decoded[4];
let extra = ((recalc_crc as u64) << 48)
| ((strict_ok as u64) << 40)
| ((plain4 as u64) << 32);
Some(DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter as u16),
crc_valid: true,
data: key1,
data_count_bit: DATA_BITS,
encoder_capable: true,
extra: Some(extra),
protocol_display_name: None,
})
}
/// Map KAT's generic button command to a Ford V1 4-bit button code
/// (Sync=0, Lock=1, Unlock=2, Trunk=4, Panic=8 — matches ford_v1_get_button_name).
fn map_button(button: u8) -> u8 {
match button {
0x01 => 0x01, // Lock
0x02 => 0x02, // Unlock
0x04 => 0x04, // Trunk
0x08 => 0x08, // Panic
b => b & 0x0F,
}
}
/// Apply serial/button/counter onto a plaintext 9-byte block (ford_v1_plain_apply_fields).
fn plain_apply_fields(plain9: &mut [u8; 9], serial: u32, btn: u8, cnt: u32) {
let chk = plain9[8]
.wrapping_sub(plain9[6])
.wrapping_sub(plain9[7])
.wrapping_sub(plain9[5]);
plain9[0] = (serial & 0xFF) as u8;
plain9[1] = ((serial >> 24) & 0xFF) as u8;
plain9[2] = ((serial >> 16) & 0xFF) as u8;
plain9[3] = ((serial >> 8) & 0xFF) as u8;
plain9[5] = (((btn & 0x0F) << 4) | (((cnt >> 16) & 0x0F) as u8)) as u8;
plain9[6] = ((cnt >> 8) & 0xFF) as u8;
plain9[7] = (cnt & 0xFF) as u8;
plain9[8] = chk
.wrapping_add(plain9[7])
.wrapping_add(plain9[6])
.wrapping_add(plain9[5]);
}
/// Rebuild the air block (raw[6..15]) and CRC16 (raw[15..17]) from a plaintext block
/// (ford_v1_encoder_rebuild_raw_from_plain).
fn rebuild_raw_from_plain(raw17: &mut [u8; DATA_BYTES], plain9: &[u8; 9]) {
let air9 = Self::encode_air_9bytes(plain9);
raw17[6..15].copy_from_slice(&air9);
let c = Self::crc16(&raw17[3..15]);
raw17[15] = (c >> 8) as u8;
raw17[16] = (c & 0xFF) as u8;
}
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
if let Some(last) = signal.last_mut() {
if last.level == level {
*last = LevelDuration::new(level, last.duration_us + duration);
return;
}
}
signal.push(LevelDuration::new(level, duration));
}
}
impl ProtocolDecoder for FordV1Decoder {
fn name(&self) -> &'static str {
"Ford V1"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: DATA_BITS,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.reset_state();
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
// C: !level && long → Preamble; seed preamble_count = 1.
DecoderStep::Reset => {
if !level && Self::is_preamble_long(duration) {
self.step = DecoderStep::Preamble;
self.preamble_count = 1;
}
}
DecoderStep::Preamble => {
if Self::is_preamble_long(duration) {
self.preamble_count = self.preamble_count.saturating_add(1);
} else if Self::is_short(duration) {
if self.preamble_count >= PREAMBLE_MIN {
// Enter Sync: buffer the first short event.
self.sync_event_idx = 0;
self.sync_event_count = 1;
self.sync_events[0] = if level { EV_SHORT_HIGH } else { EV_SHORT_LOW };
self.step = DecoderStep::Sync;
} else {
self.step = DecoderStep::Reset;
}
} else if self.preamble_count < PREAMBLE_MIN {
self.step = DecoderStep::Reset;
}
// else: stay in Preamble (long preamble already satisfied, ignore stray pulse)
}
DecoderStep::Sync => {
let (ev, is_short) = if Self::is_short(duration) {
(if level { EV_SHORT_HIGH } else { EV_SHORT_LOW }, true)
} else if Self::is_long(duration) {
(if level { EV_LONG_HIGH } else { EV_LONG_LOW }, false)
} else {
self.step = DecoderStep::Preamble;
return None;
};
self.sync_event_idx += 1;
if is_short {
self.sync_event_count += 1;
}
if (self.sync_event_idx as usize) < 8 {
self.sync_events[self.sync_event_idx as usize] = ev;
}
if self.sync_event_count > 2 {
// Sync detected: reset the bit buffer and replay buffered events into Manchester.
self.decode_data = 0;
self.decode_count_bit = 0;
self.byte_count = 0;
self.raw_bytes = [0; DATA_BYTES + 1];
self.manchester_state = ManchesterState::Mid1;
if self.sync_events[0] == EV_SHORT_LOW {
self.manchester_state = ManchesterState::Mid0;
}
self.step = DecoderStep::Data;
let last = self.sync_event_idx.min(7);
for i in 0..=last {
let event = self.sync_events[i as usize];
if let Some(data_bit) = self.manchester_advance(event) {
self.push_bit(data_bit);
}
}
return None;
}
if self.sync_event_idx >= 7 {
self.step = DecoderStep::Preamble;
}
}
DecoderStep::Data => {
let event = if Self::is_short(duration) {
if level { EV_SHORT_HIGH } else { EV_SHORT_LOW }
} else if Self::is_long(duration) {
if level { EV_LONG_HIGH } else { EV_LONG_LOW }
} else {
// Idle gap / odd pulse. The C decoder attempts partial-last-byte variants only
// when byte_count==16 and 1-2 bits short; we require all 17 bytes, so any
// non-short/long pulse (including very long inter-burst gaps, dur >=
// te_long*SILENCE_LONG_MULT) ends the attempt and resets so the next burst can
// re-sync from Reset.
let _ = duration >= TE_LONG * SILENCE_LONG_MULT;
self.reset_state();
return None;
};
if let Some(data_bit) = self.manchester_advance(event) {
self.push_bit(data_bit);
if self.byte_count > 16 {
let result = self.process_data();
self.reset_state();
return result;
}
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0) as u32 & 0xF_FFFF;
let btn = Self::map_button(button) & 0x0F;
// We can faithfully re-encode only when the original frame's plaintext was recovered at
// decode time (strict branch). `extra` carries: [63:48]=crc16, [47:40]=strict_ok,
// [39:32]=plain[4] (the byte the strict branch constrains).
let extra = decoded.extra?;
let strict_ok = ((extra >> 40) & 0x01) != 0;
if !strict_ok {
return None;
}
let plain4 = ((extra >> 32) & 0xFF) as u8;
// raw[0..7] = key1 (56 bits), taken from `data` (low 56 bits). The decoder packs key1 as
// raw[0]<<48 | … | raw[6], so the top 7 bytes of the 56-bit value are raw[0..7].
let mut raw17 = [0u8; DATA_BYTES];
for (i, b) in raw17.iter_mut().take(7).enumerate() {
*b = (decoded.data >> (48 - i * 8)) as u8;
}
// Reconstruct the plaintext block from the decoded fields plus the stashed plain[4].
// plain_apply_fields fills [0,1,2,3,5,6,7,8]; plain[4] is preserved from `extra`. The
// strict-branch invariant the C decoder verified is plain[4]==air9[0]==raw[6] and
// plain[3]==raw[5]; re-encoding plain reproduces the air block and hence those bytes.
let mut plain9 = [0u8; 9];
plain9[4] = plain4;
// Seed checksum-bearing fields with the *decoded* values so the running checksum delta in
// plain_apply_fields starts from the original plaintext's relationship, then apply the new
// button/counter (same counter; no increment, matching Ford V0's replay policy).
plain9[5] = (((decoded.button.unwrap_or(0) & 0x0F) << 4)
| (((counter >> 16) & 0x0F) as u8)) as u8;
plain9[6] = ((counter >> 8) & 0xFF) as u8;
plain9[7] = (counter & 0xFF) as u8;
plain9[8] = plain9[5].wrapping_add(plain9[6]).wrapping_add(plain9[7]);
Self::plain_apply_fields(&mut plain9, serial, btn, counter);
// Rebuild raw[5] = plain[3] (strict invariant) and the air block + CRC from plaintext.
raw17[5] = plain9[3];
Self::rebuild_raw_from_plain(&mut raw17, &plain9);
// Build the 6-burst upload.
let mut signal =
Vec::with_capacity(ENC_BURST_COUNT * (ENC_PREAMBLE_PAIRS * 2 + 2 + DATA_BYTES * 16 + 1));
for burst in 0..ENC_BURST_COUNT {
let mut pkt = raw17;
pkt[4] = ENC_BURST_PKT4[burst];
let crcw = Self::crc16(&pkt[3..15]);
pkt[15] = (crcw >> 8) as u8;
pkt[16] = (crcw & 0xFF) as u8;
// Preamble: 400 pairs of long high / long low.
for _ in 0..ENC_PREAMBLE_PAIRS {
Self::enc_add_level(&mut signal, true, ENC_SYNC_LONG_US);
Self::enc_add_level(&mut signal, false, ENC_SYNC_LONG_US);
}
// Sync: long high + short low.
Self::enc_add_level(&mut signal, true, ENC_SYNC_LONG_US);
Self::enc_add_level(&mut signal, false, ENC_SYNC_SHORT_US);
// Data: each bit → (bit, short) then (!bit, short) — Manchester, MSB first.
for &b in pkt.iter() {
for bit_i in (0..8).rev() {
let bit = ((b >> bit_i) & 1) != 0;
Self::enc_add_level(&mut signal, bit, ENC_SYNC_SHORT_US);
Self::enc_add_level(&mut signal, !bit, ENC_SYNC_SHORT_US);
}
}
// Trailing gap (long for repeats, short for the final burst).
let gap = if burst + 1 == ENC_BURST_COUNT {
ENC_GAP_LAST_US
} else {
ENC_GAP_REPEAT_US
};
Self::enc_add_level(&mut signal, false, gap);
}
Some(signal)
}
}
impl Default for FordV1Decoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// CRC16/CCITT known vector. "123456789" → 0x31C3 for poly 0x1021, init 0x0000
/// (the standard CRC-16/XMODEM check value).
#[test]
fn crc16_known_vector() {
let v = FordV1Decoder::crc16(b"123456789");
assert_eq!(v, 0x31C3, "CRC-16/XMODEM check value mismatch: got {:04X}", v);
}
/// The descramble cipher must be invertible: encode_inverse_block ∘ decode_with_flag(flag)
/// should round-trip for the flag the parity selects.
#[test]
fn descramble_roundtrip() {
// A plaintext block; encode to air, then plain_from_air must recover it exactly.
let plain: [u8; 9] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x26, 0x77, 0x88, 0x99];
let air = FordV1Decoder::encode_air_9bytes(&plain);
let recovered = FordV1Decoder::plain_from_air(&air).expect("round-trip recover");
assert_eq!(recovered, plain, "plain_from_air did not recover plaintext");
}
/// Build a canonical Ford V1 plaintext (the strict-branch invariant is plain[4]==plain[0],
/// since the cipher leaves byte 0 untouched, so air9[0]==plain[0]). Returns the 17-byte frame.
fn build_frame(serial: u32, button: u8, counter: u32) -> [u8; DATA_BYTES] {
let mut plain = [0u8; 9];
plain[4] = (serial & 0xFF) as u8; // strict: plain[4] == air9[0] == plain[0]
FordV1Decoder::plain_apply_fields(&mut plain, serial, button, counter);
let mut raw17 = [0u8; DATA_BYTES];
// key1 head: raw[0..5] arbitrary-ish, raw[5] must equal plain[3] (strict: decoded[3]==raw[5]).
raw17[0] = 0xC0;
raw17[1] = 0xFF;
raw17[2] = 0xEE;
raw17[3] = (serial >> 24) as u8;
raw17[4] = (serial >> 16) as u8;
raw17[5] = plain[3];
FordV1Decoder::rebuild_raw_from_plain(&mut raw17, &plain);
raw17
}
/// process_data must accept a canonical frame, take the strict branch, and recover fields.
#[test]
fn process_data_strict_branch() {
let (serial, button, counter) = (0x1A2B3C4Du32, 0x02u8, 0x0123u32);
let raw17 = build_frame(serial, button, counter);
let mut dec = FordV1Decoder::new();
dec.raw_bytes[..DATA_BYTES].copy_from_slice(&raw17);
dec.byte_count = DATA_BYTES;
let d = dec.process_data().expect("process_data should accept the frame");
assert!(d.crc_valid, "CRC must be valid");
assert_eq!(d.serial, Some(serial), "serial mismatch");
assert_eq!(d.button, Some(button), "button mismatch");
assert_eq!(d.counter, Some(counter as u16), "counter mismatch");
assert_eq!(d.data_count_bit, DATA_BITS);
}
/// Reconstruct the 17 on-air data bytes of the first burst from the encoder's Manchester
/// upload. The encoder emits, per data bit, (bit,short) then (!bit,short); add_level merges
/// adjacent same-level pulses, so a same-bit boundary becomes a long pulse. We re-pair the
/// stream by walking half-cells of `short` width (splitting merged long pulses into two halves)
/// and reading each bit as the level of its first half-cell.
fn first_burst_bytes(upload: &[LevelDuration]) -> [u8; DATA_BYTES] {
// Expand merged pulses into ENC_SYNC_SHORT_US half-cells (long = 2 halves).
let mut halves: Vec<bool> = Vec::new();
for ld in upload {
let n = ((ld.duration_us + ENC_SYNC_SHORT_US / 2) / ENC_SYNC_SHORT_US).max(1);
for _ in 0..n {
halves.push(ld.level);
}
}
// Skip the preamble (400 long pairs = 1600 halves) + sync (long high=2 + short low=1 = 3).
let data_start = ENC_PREAMBLE_PAIRS * 4 + 3;
let mut out = [0u8; DATA_BYTES];
for (byte_i, b) in out.iter_mut().enumerate() {
for bit_pos in 0..8 {
// Each bit = two half-cells; the bit value is the first half-cell's level.
let idx = data_start + (byte_i * 8 + bit_pos) * 2;
let bit = halves.get(idx).copied().unwrap_or(false);
*b = (*b << 1) | (bit as u8);
}
}
out
}
/// Full encode→decode round trip at the on-air-frame level. Decode a canonical frame, re-encode
/// it via `encode()`, recover the transmitted 17-byte frame from the first burst, and verify it
/// decodes (via process_data) back to the same serial/button/counter with a valid CRC.
#[test]
fn encode_decode_roundtrip() {
let (serial, button, counter) = (0x1A2B3C4Du32, 0x02u8, 0x0123u32);
let raw17 = build_frame(serial, button, counter);
let mut dec = FordV1Decoder::new();
dec.raw_bytes[..DATA_BYTES].copy_from_slice(&raw17);
dec.byte_count = DATA_BYTES;
let decoded = dec.process_data().expect("decode canonical frame");
// Re-encode (same button) → Manchester upload (6 bursts).
let upload = FordV1Decoder::new()
.encode(&decoded, button)
.expect("encoder should produce an upload");
assert!(!upload.is_empty(), "encoder upload must be non-empty");
// Recover the transmitted frame from burst 0 and decode it through process_data.
// Burst 0 overrides pkt[4] = 0x08 (ENC_BURST_PKT4[0]) and recomputes the CRC to match, so
// the recovered frame is self-consistent. The strict-branch serial comes from the plaintext
// (decoded[1..4],decoded[0]), independent of raw[4], so serial/button/counter still match.
let tx = first_burst_bytes(&upload);
let mut dec2 = FordV1Decoder::new();
dec2.raw_bytes[..DATA_BYTES].copy_from_slice(&tx);
dec2.byte_count = DATA_BYTES;
let got = dec2
.process_data()
.expect("re-encoded burst-0 frame must decode via process_data");
assert!(got.crc_valid, "round-tripped CRC must be valid");
assert_eq!(got.serial, Some(serial), "round-trip serial mismatch");
assert_eq!(got.button, Some(button), "round-trip button mismatch");
assert_eq!(got.counter, Some(counter as u16), "round-trip counter mismatch");
// Structural checks: 6 bursts worth of data, each preamble present.
let long_highs = upload
.iter()
.filter(|l| l.level && l.duration_us >= ENC_SYNC_LONG_US)
.count();
assert!(
long_highs >= ENC_BURST_COUNT,
"expected at least one long-high preamble pulse per burst"
);
}
}
+413
View File
@@ -0,0 +1,413 @@
//! Ford V2 protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v2.c` and `ford_v2.h`.
//! Manchester 200/400µs (te_delta 260 → threshold ~460µs between short/long), 104 bits (13 bytes), FM.
//! Frame begins with a 16-bit Manchester sync that equals ~0x7FA7 (the decoder matches the *inverted*
//! shift register against 0x8058); the two sync bytes 0x7F 0xA7 head the 13-byte buffer. Data bits are
//! inverted before packing (`data_bit = !data_bit`). Structure is validated by the two sync bytes plus a
//! known button code. Encoder supported (matches subghz_protocol_encoder_ford_v2).
//!
//! Decoder steps: Reset → Preamble (≥64 shorts) → Sync (find 0x7FA7) → Data (11 bytes).
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 200;
const TE_LONG: u32 = 400;
const TE_DELTA: u32 = 260;
const DATA_BITS: usize = 104;
const DATA_BYTES: usize = 13;
const PREAMBLE_MIN: u16 = 64;
const SYNC_0: u8 = 0x7F;
const SYNC_1: u8 = 0xA7;
const SYNC_BITS: u8 = 16;
const INTER_BURST_GAP_US: u32 = 15000;
// Encoder constants (subghz_protocol_encoder_ford_v2)
const ENC_TE_SHORT: u32 = 240;
const ENC_PREAMBLE_PAIRS: usize = 70;
const ENC_BURST_COUNT: usize = 6;
const ENC_INTER_BURST_GAP_US: u32 = 16000;
const ENC_SYNC_LO_US: u32 = 476;
const TAIL_RAW_BYTES: usize = 5;
/// Inverted 16-bit sync the decoder matches against (= !0x7FA7).
const SYNC_SHIFT16_INV: u16 = !(((SYNC_0 as u16) << 8) | SYNC_1 as u16);
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0 = 0,
Mid1 = 1,
Start0 = 2,
Start1 = 3,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Preamble,
Sync,
Data,
}
pub struct FordV2Decoder {
step: DecoderStep,
manchester_state: ManchesterState,
preamble_count: u16,
raw_bytes: [u8; DATA_BYTES],
byte_count: usize,
decode_data: u16,
decode_count_bit: usize,
sync_shift: u16,
sync_bit_count: u8,
}
impl FordV2Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
manchester_state: ManchesterState::Mid1,
preamble_count: 0,
raw_bytes: [0; DATA_BYTES],
byte_count: 0,
decode_data: 0,
decode_count_bit: 0,
sync_shift: 0,
sync_bit_count: 0,
}
}
fn reset_state(&mut self) {
self.step = DecoderStep::Reset;
self.manchester_state = ManchesterState::Mid1;
self.preamble_count = 0;
self.raw_bytes = [0; DATA_BYTES];
self.byte_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
self.sync_shift = 0;
self.sync_bit_count = 0;
}
fn is_short(d: u32) -> bool {
duration_diff!(d, TE_SHORT) < TE_DELTA
}
fn is_long(d: u32) -> bool {
duration_diff!(d, TE_LONG) < TE_DELTA
}
/// Flipper Manchester transition table (same as Ford V0). Event 0=ShortLow,1=ShortHigh,
/// 2=LongLow,3=LongHigh. Returns Some(bit) when a bit emits.
fn manchester_advance(&mut self, event: u8) -> Option<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 }
}
/// Map (level, duration) → Manchester event. Ford V2 polarity: level ? High : Low.
fn pulse_event(level: bool, duration: u32) -> Option<u8> {
if Self::is_short(duration) {
Some(if level { 1 } else { 0 })
} else if Self::is_long(duration) {
Some(if level { 3 } else { 2 })
} else {
None
}
}
fn button_is_valid(btn: u8) -> bool {
matches!(btn, 0x10 | 0x11 | 0x13 | 0x14 | 0x15)
}
/// Enter the Sync step from the preamble, seeding state for the triggering long-low pulse.
fn enter_sync_from_preamble(&mut self, level: bool, duration: u32) {
self.step = DecoderStep::Sync;
self.decode_data = 0;
self.decode_count_bit = 0;
self.byte_count = 0;
self.sync_shift = 0;
self.sync_bit_count = 0;
self.raw_bytes = [0; DATA_BYTES];
self.manchester_state = ManchesterState::Mid1;
if let Some(event) = Self::pulse_event(level, duration) {
// Low event (0/2) seeds Mid0 (matches the C `if(ev==ShortLow||ev==LongLow) state=Mid0`).
if event == 0 || event == 2 {
self.manchester_state = ManchesterState::Mid0;
}
self.feed_event(event);
} else {
self.reset_state();
}
}
/// Process one Manchester event in Sync or Data step. Returns Some when a frame commits.
fn feed_event(&mut self, event: u8) -> Option<DecodedSignal> {
if self.step == DecoderStep::Sync {
if let Some(bit) = self.manchester_advance(event) {
self.sync_shift = (self.sync_shift << 1) | (bit as u16);
if self.sync_bit_count < SYNC_BITS {
self.sync_bit_count += 1;
}
if self.sync_bit_count >= SYNC_BITS && self.sync_shift == SYNC_SHIFT16_INV {
// Enter data: prime sync bytes and bit counter.
self.raw_bytes = [0; DATA_BYTES];
self.raw_bytes[0] = SYNC_0;
self.raw_bytes[1] = SYNC_1;
self.byte_count = 2;
self.step = DecoderStep::Data;
self.decode_data = 0;
self.decode_count_bit = SYNC_BITS as usize;
}
}
return None;
}
// Data step
if let Some(bit) = self.manchester_advance(event) {
let data_bit = !bit; // Ford V2 inverts decoded bits
self.decode_data = (self.decode_data << 1) | (data_bit as u16);
self.decode_count_bit += 1;
if self.decode_count_bit & 7 == 0 {
let byte_val = (self.decode_data & 0xFF) as u8;
if self.byte_count < DATA_BYTES {
self.raw_bytes[self.byte_count] = byte_val;
self.byte_count += 1;
}
self.decode_data = 0;
if self.byte_count == DATA_BYTES {
let result = self.commit_frame();
self.reset_state();
return result;
}
}
}
None
}
/// Validate sync bytes + structure and build the decoded signal.
fn commit_frame(&self) -> Option<DecodedSignal> {
let k = &self.raw_bytes;
if k[0] != SYNC_0 || k[1] != SYNC_1 {
return None;
}
if !Self::button_is_valid(k[6]) {
return None;
}
let serial = ((k[2] as u32) << 24)
| ((k[3] as u32) << 16)
| ((k[4] as u32) << 8)
| (k[5] as u32);
let counter = (((k[7] & 0x7F) as u16) << 9) | ((k[8] as u16) << 1) | ((k[9] >> 7) as u16);
// Top 8 bytes → 64-bit data for display/export (matches generic.data).
let mut data = 0u64;
for &byte in k.iter().take(8) {
data = (data << 8) | byte as u64;
}
// Tail raw bytes k[8..13] → extra (40 bits) so the encoder can rebuild the full frame.
let mut extra = 0u64;
for &byte in k.iter().skip(8).take(TAIL_RAW_BYTES) {
extra = (extra << 8) | byte as u64;
}
Some(DecodedSignal {
serial: Some(serial),
button: Some(k[6]),
counter: Some(counter),
crc_valid: true, // validated by sync bytes + button structure
data,
data_count_bit: DATA_BITS,
encoder_capable: true,
extra: Some(extra),
protocol_display_name: None,
})
}
/// Map KAT's generic button command to a Ford V2 button code.
fn map_button(button: u8) -> u8 {
match button {
0x01 => 0x10, // Lock
0x02 => 0x11, // Unlock
0x04 => 0x13, // Trunk
0x08 => 0x14, // Panic
b if Self::button_is_valid(b) => b, // already a Ford V2 code
_ => 0x11,
}
}
fn parity8(mut v: u8) -> u8 {
let mut p = 0u8;
while v != 0 {
p ^= v & 1;
v >>= 1;
}
p
}
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
if let Some(last) = signal.last_mut() {
if last.level == level {
*last = LevelDuration::new(level, last.duration_us + duration);
return;
}
}
signal.push(LevelDuration::new(level, duration));
}
fn enc_manchester_bit(signal: &mut Vec<LevelDuration>, bit: bool) {
if bit {
Self::enc_add_level(signal, true, ENC_TE_SHORT);
Self::enc_add_level(signal, false, ENC_TE_SHORT);
} else {
Self::enc_add_level(signal, false, ENC_TE_SHORT);
Self::enc_add_level(signal, true, ENC_TE_SHORT);
}
}
}
impl ProtocolDecoder for FordV2Decoder {
fn name(&self) -> &'static str {
"Ford V2"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: DATA_BITS,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.reset_state();
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if Self::is_short(duration) {
self.preamble_count = 1;
self.step = DecoderStep::Preamble;
}
}
DecoderStep::Preamble => {
if Self::is_short(duration) {
if self.preamble_count < u16::MAX {
self.preamble_count += 1;
}
} else if !level && Self::is_long(duration) {
if self.preamble_count >= PREAMBLE_MIN {
self.enter_sync_from_preamble(level, duration);
} else {
self.reset_state();
}
} else {
self.reset_state();
}
}
DecoderStep::Sync | DecoderStep::Data => {
if let Some(event) = Self::pulse_event(level, duration) {
if let Some(result) = self.feed_event(event) {
return Some(result);
}
} else {
// Non-short/long pulse (gap/out-of-range) ends the attempt.
let _ = duration >= INTER_BURST_GAP_US;
self.reset_state();
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
// Rebuild the 13-byte frame: bytes 0..8 from data, tail bytes 8..13 from extra.
let mut raw = [0u8; DATA_BYTES];
for (i, b) in raw.iter_mut().take(8).enumerate() {
*b = (decoded.data >> (56 - i * 8)) as u8;
}
let extra = decoded.extra.unwrap_or(0);
for (i, b) in raw.iter_mut().skip(8).take(TAIL_RAW_BYTES).enumerate() {
*b = (extra >> (32 - i * 8)) as u8;
}
// Apply the requested button and refresh the byte-7 parity MSB.
raw[6] = Self::map_button(button);
if !Self::button_is_valid(raw[6]) {
return None;
}
let parity_msb = Self::parity8(raw[6]) << 7;
raw[7] = (raw[7] & 0x7F) | parity_msb;
// Ensure sync header is present.
raw[0] = SYNC_0;
raw[1] = SYNC_1;
let mut signal = Vec::with_capacity(ENC_BURST_COUNT * (ENC_PREAMBLE_PAIRS * 2 + DATA_BITS * 2 + 4));
for burst in 0..ENC_BURST_COUNT {
// Preamble: 70 pairs of (low short, high short)
for _ in 0..ENC_PREAMBLE_PAIRS {
Self::enc_add_level(&mut signal, false, ENC_TE_SHORT);
Self::enc_add_level(&mut signal, true, ENC_TE_SHORT);
}
// Sync low + high short
Self::enc_add_level(&mut signal, false, ENC_SYNC_LO_US);
Self::enc_add_level(&mut signal, true, ENC_TE_SHORT);
// Data bits 1..103 (bit 0 implied by the high short above)
for bit_pos in 1..DATA_BITS {
let byte_idx = bit_pos / 8;
let bit_idx = 7 - (bit_pos % 8);
let bit = (raw[byte_idx] >> bit_idx) & 1 != 0;
Self::enc_manchester_bit(&mut signal, bit);
}
if burst + 1 < ENC_BURST_COUNT {
Self::enc_add_level(&mut signal, true, ENC_INTER_BURST_GAP_US);
}
}
Some(signal)
}
}
impl Default for FordV2Decoder {
fn default() -> Self {
Self::new()
}
}
+258
View File
@@ -0,0 +1,258 @@
//! Ford V3 protocol decoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v3.c` and `ford_v3.h`.
//! Manchester 240/480µs, 104 bits (13 bytes), FM, plaintext (no CRC/encryption). Decode-only —
//! the reference encoder is NULL.
//!
//! Manchester uses Flipper's manchester_decoder.h transition table (same as Ford V0), but Ford V3
//! maps level the OPPOSITE way from Ford V0: `level ? ShortHigh/LongHigh : ShortLow/LongLow`
//! (Ford V0 uses `level ? Low : High`). Decoder steps: Reset → Preamble (≥30 shorts) → Data.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 240;
const TE_LONG: u32 = 480;
const TE_DELTA: u32 = 60;
const DATA_BITS: usize = 104;
const DATA_BYTES: usize = 13;
const PREAMBLE_MIN: u16 = 30;
const BTN_LOCK: u8 = 0x01;
const BTN_UNLOCK: u8 = 0x02;
/// Manchester state machine (Flipper manchester_decoder.h transition table; same as Ford V0).
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0 = 0,
Mid1 = 1,
Start0 = 2,
Start1 = 3,
}
/// Decoder step states (matches FordV3DecoderStep in ford_v3.c)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Preamble,
Data,
}
/// Ford V3 protocol decoder (matches SubGhzProtocolDecoderFordV3)
pub struct FordV3Decoder {
step: DecoderStep,
manchester_state: ManchesterState,
raw_bytes: [u8; DATA_BYTES],
bit_count: usize,
preamble_count: u16,
}
impl FordV3Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
manchester_state: ManchesterState::Mid1,
raw_bytes: [0; DATA_BYTES],
bit_count: 0,
preamble_count: 0,
}
}
/// Reset accumulators (matches ford_v3_reset_data)
fn reset_data(&mut self) {
self.raw_bytes = [0; DATA_BYTES];
self.bit_count = 0;
self.preamble_count = 0;
self.manchester_state = ManchesterState::Mid1;
}
/// Add a decoded bit MSB-first into the byte buffer (matches ford_v3_add_bit)
fn add_bit(&mut self, bit: bool) {
if self.bit_count >= DATA_BITS {
return;
}
let byte_index = self.bit_count / 8;
let bit_in_byte = 7 - (self.bit_count % 8);
if bit {
self.raw_bytes[byte_index] |= 1 << bit_in_byte;
}
self.bit_count += 1;
}
/// Manchester state machine (Flipper manchester_advance).
/// Event: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh. Returns Some(bit) when a bit emits.
fn manchester_advance(&mut self, event: u8) -> Option<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
}
}
fn is_short(duration: u32) -> bool {
duration_diff!(duration, TE_SHORT) < TE_DELTA
}
fn is_long(duration: u32) -> bool {
duration_diff!(duration, TE_LONG) < TE_DELTA
}
/// Build the decoded signal when 104 bits are collected (matches ford_v3_parse_fields).
fn build_signal(&self) -> DecodedSignal {
let b = &self.raw_bytes;
let serial = ((b[1] as u32) << 24)
| ((b[2] as u32) << 16)
| ((b[3] as u32) << 8)
| (b[4] as u32);
// Counter is the bitwise-inverted bytes 7 and 8 (ref: ~b[7], ~b[8])
let counter = (((!b[7]) as u16) << 8) | ((!b[8]) as u16);
let button = if b[6] & 0x01 != 0 { BTN_UNLOCK } else { BTN_LOCK };
// Pack the first 8 bytes (big-endian) into the 64-bit data field for display/export
// (matches ford_v3.c serialize, which stores bytes[0..8] in generic.data).
let mut data = 0u64;
for &byte in b.iter().take(8) {
data = (data << 8) | byte as u64;
}
DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid: true, // Ford V3 is plaintext with no CRC
data,
data_count_bit: DATA_BITS,
encoder_capable: false,
extra: None,
protocol_display_name: None,
}
}
}
impl ProtocolDecoder for FordV3Decoder {
fn name(&self) -> &'static str {
"Ford V3"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: DATA_BITS,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.reset_data();
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
// Ref: any short pulse begins the preamble (no level check).
DecoderStep::Reset => {
if Self::is_short(duration) {
self.reset_data();
self.preamble_count = 1;
self.step = DecoderStep::Preamble;
}
}
DecoderStep::Preamble => {
if Self::is_short(duration) {
self.preamble_count += 1;
} else if self.preamble_count >= PREAMBLE_MIN && Self::is_long(duration) {
// First data bit: long pulse, mapped level ? LongHigh : LongLow.
self.manchester_state = ManchesterState::Mid1;
let event = if level { 3 } else { 2 };
if let Some(bit) = self.manchester_advance(event) {
self.add_bit(bit);
}
self.step = DecoderStep::Data;
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::Data => {
let short = Self::is_short(duration);
let long = Self::is_long(duration);
if !short && !long {
// Gap / out-of-range pulse ends the frame.
let ready = self.bit_count >= DATA_BITS;
let result = if ready { Some(self.build_signal()) } else { None };
self.step = DecoderStep::Reset;
self.reset_data();
return result;
}
// Ford V3 polarity: level ? High : Low (opposite of Ford V0).
let event = if level {
if short { 1 } else { 3 } // ShortHigh / LongHigh
} else if short {
0 // ShortLow
} else {
2 // LongLow
};
if let Some(bit) = self.manchester_advance(event) {
self.add_bit(bit);
if self.bit_count >= DATA_BITS {
let result = self.build_signal();
self.step = DecoderStep::Reset;
self.reset_data();
return Some(result);
}
}
}
}
None
}
fn supports_encoding(&self) -> bool {
false
}
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
None
}
}
impl Default for FordV3Decoder {
fn default() -> Self {
Self::new()
}
}
+558
View File
@@ -0,0 +1,558 @@
//! Honda Static protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/honda_static.c` and
//! `honda_static.h`. Honda/Acura fixed-code keyfobs. FM, 315 MHz + 433.92 MHz, 64-bit frame.
//!
//! Unlike most KAT decoders, Honda Static does NOT use the Flipper manchester_decoder.h transition
//! table. It buffers a per-element *symbol stream* (one bit per ~63µs element) and then performs a
//! custom Manchester unpack over symbol pairs (see `honda_static_manchester_pack_64` in the C). A
//! short pulse contributes one symbol = the pulse level; a long pulse contributes two symbols of the
//! same level. Anything outside both ranges (e.g. the 700µs sync or a trailing gap) terminates the
//! buffer and triggers a parse attempt.
//!
//! Frame (64 bits, MSB-first into 8 bytes): button (4b) | serial (28b) | counter (24b) |
//! checksum (8b). The checksum is an XOR of bytes[0..7] (the first 7 bytes). Emission is gated on
//! the checksum validating, so Honda Static will not false-match. The parser tries the
//! inverted-Manchester interpretation first (which is what the encoder emits), then non-inverted
//! forward, then a bit-reversed-bytes pass — matching the C.
//!
//! The exported `data` (Key) word is the C `generic.data`: a compact nibble-packed layout
//! (`honda_static_pack_compact`), NOT the raw decoded packet bytes.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
const BIT_COUNT: usize = 64;
const MIN_SYMBOLS: usize = 36;
const SHORT_BASE_US: u32 = 28;
const SHORT_SPAN_US: u32 = 70;
const LONG_BASE_US: u32 = 61;
const LONG_SPAN_US: u32 = 130;
const SYNC_TIME_US: u32 = 700;
const ELEMENT_TIME_US: u32 = 63;
const SYMBOL_CAPACITY: usize = 512;
const PREAMBLE_ALTERNATING_COUNT: usize = 160;
const PREAMBLE_MAX_TRANSITIONS: u16 = 19;
// Reported timing constants (informational; matches ProtoPirate protocol_items profile).
const TE_SHORT: u32 = ELEMENT_TIME_US;
const TE_LONG: u32 = SYNC_TIME_US;
const TE_DELTA: u32 = 120;
// KAT generic button codes.
const BTN_LOCK: u8 = 0x01;
const BTN_UNLOCK: u8 = 0x02;
const BTN_TRUNK: u8 = 0x04;
const BTN_PANIC: u8 = 0x08;
// honda_static_encoder_button_map[4] in the C ({0x02,0x04,0x08,0x05}); used by the encoder remap.
const ENCODER_BUTTON_MAP: [u8; 4] = [0x02, 0x04, 0x08, 0x05];
/// Decoded Honda Static fields (matches HondaStaticFields).
#[derive(Debug, Clone, Copy, Default)]
struct HondaStaticFields {
button: u8,
serial: u32,
counter: u32,
/// XOR checksum byte (mirrors the C struct field; validation recomputes it, so this is
/// retained for fidelity/inspection rather than being read on the hot path).
#[allow(dead_code)]
checksum: u8,
}
/// Honda Static decoder (matches SubGhzProtocolDecoderHondaStatic).
pub struct HondaStaticDecoder {
/// Per-element symbol stream (one bit per ~63µs element). Index 0 is the first received symbol.
symbols: Vec<bool>,
}
impl HondaStaticDecoder {
pub fn new() -> Self {
Self {
symbols: Vec::with_capacity(SYMBOL_CAPACITY),
}
}
/// Extract `count` bits starting at `start`, MSB-first across the byte array
/// (matches honda_static_get_bits / honda_static_get_bits_u32, shift = (~bit_index)&7).
fn get_bits(data: &[u8], start: usize, count: usize) -> u32 {
let mut value: u32 = 0;
for i in 0..count {
let bit_index = start + i;
let byte = data[bit_index >> 3];
let shift = (!bit_index) & 0x07;
value = (value << 1) | (((byte >> shift) & 1) as u32);
}
value
}
/// XOR checksum over the first 7 bytes of the 8-byte packet (matches the inline checksum loop in
/// honda_static_validate_forward_packet / honda_static_build_packet_bytes).
fn packet_checksum(packet: &[u8]) -> u8 {
let mut checksum = 0u8;
for &b in packet.iter().take(7) {
checksum ^= b;
}
checksum
}
/// Reverse the bit order of a byte (matches pp_reverse_bits8).
fn reverse_bits8(value: u8) -> u8 {
let mut v = value;
v = ((v & 0xF0) >> 4) | ((v & 0x0F) << 4);
v = ((v & 0xCC) >> 2) | ((v & 0x33) << 2);
v = ((v & 0xAA) >> 1) | ((v & 0x55) << 1);
v
}
fn is_valid_button(button: u8) -> bool {
// honda_static_is_valid_button: button <= 9 && ((0x336 >> button) & 1)
// 0x336 = 0b1100110110 → valid buttons: 1,2,4,5,8,9.
button <= 9 && ((0x336u16 >> button) & 1) != 0
}
fn is_valid_serial(serial: u32) -> bool {
serial != 0 && serial != 0x0FFF_FFFF
}
/// Pack fields into the 64-bit compact "Key" word (matches honda_static_pack_compact →
/// pp_bytes_to_u64_be over the compact[8] nibble-packed layout).
fn pack_compact(fields: &HondaStaticFields) -> u64 {
let mut compact = [0u8; 8];
compact[0] = fields.button & 0x0F;
compact[1] = (fields.serial >> 20) as u8;
compact[2] = (fields.serial >> 12) as u8;
compact[3] = (fields.serial >> 4) as u8;
compact[4] = (fields.serial << 4) as u8;
compact[5] = (fields.counter >> 16) as u8;
compact[6] = (fields.counter >> 8) as u8;
compact[7] = fields.counter as u8;
u64::from_be_bytes(compact)
}
/// Build the raw 8-byte (64-bit) packet from fields, MSB-first
/// (matches honda_static_build_packet_bytes; checksum filled into byte 7).
fn build_packet_bytes(fields: &HondaStaticFields) -> [u8; 8] {
let mut packet = [0u8; 8];
Self::set_bits(&mut packet, 0, 4, (fields.button & 0x0F) as u32);
Self::set_bits(&mut packet, 4, 28, fields.serial);
Self::set_bits(&mut packet, 32, 24, fields.counter);
let checksum = Self::packet_checksum(&packet);
Self::set_bits(&mut packet, 56, 8, checksum as u32);
packet
}
/// Set `count` bits starting at `start`, MSB-first (matches honda_static_set_bits).
fn set_bits(data: &mut [u8], start: usize, count: usize, value: u32) {
for i in 0..count {
let bit_index = start + i;
let byte_index = bit_index >> 3;
let shift = (!bit_index) & 0x07;
let mask = 1u8 << shift;
let bit = ((value >> (count - 1 - i)) & 1) != 0;
if bit {
data[byte_index] |= mask;
} else {
data[byte_index] &= !mask;
}
}
}
/// Validate a forward (as-decoded) 8-byte packet (matches honda_static_validate_forward_packet).
fn validate_forward_packet(packet: &[u8; 8]) -> Option<HondaStaticFields> {
let button = Self::get_bits(packet, 0, 4) as u8;
let serial = Self::get_bits(packet, 4, 28);
let counter = Self::get_bits(packet, 32, 24);
let checksum = Self::get_bits(packet, 56, 8) as u8;
let checksum_calc = Self::packet_checksum(packet);
if checksum != checksum_calc {
return None;
}
if !Self::is_valid_button(button) {
return None;
}
if !Self::is_valid_serial(serial) {
return None;
}
Some(HondaStaticFields {
button,
serial,
counter,
checksum,
})
}
/// Validate a bit-reversed packet (matches honda_static_validate_reverse_packet). Note: the C
/// does NOT re-check the checksum here (it reverses bytes then validates button/serial only),
/// so this path is not checksum-gated. We mirror that, but the caller flags crc_valid=false.
fn validate_reverse_packet(packet: &[u8; 8]) -> Option<HondaStaticFields> {
let mut reversed = [0u8; 8];
for (i, b) in packet.iter().enumerate() {
reversed[i] = Self::reverse_bits8(*b);
}
let button = Self::get_bits(&reversed, 0, 4) as u8;
let serial = Self::get_bits(&reversed, 4, 28);
let counter = Self::get_bits(&reversed, 32, 24);
let checksum = Self::packet_checksum(&reversed);
if !Self::is_valid_button(button) {
return None;
}
if !Self::is_valid_serial(serial) {
return None;
}
Some(HondaStaticFields {
button,
serial,
counter,
checksum,
})
}
/// Manchester unpack over symbol pairs (matches honda_static_manchester_pack_64).
/// Returns the packed 8-byte packet plus how many bits were collected. `inverted`:
/// bit=1 when (a==0,b==1); non-inverted: bit=1 when (a==1,b==0). Equal adjacent symbols are
/// skipped (advance by 1).
fn manchester_pack_64(symbols: &[bool], start_pos: usize, inverted: bool) -> ([u8; 8], usize) {
let mut packet = [0u8; 8];
let count = symbols.len();
let mut pos = start_pos;
let mut bit_count: usize = 0;
while pos + 1 < count {
if bit_count >= BIT_COUNT {
break;
}
let a = symbols[pos];
let b = symbols[pos + 1];
if a == b {
pos += 1;
continue;
}
let bit = if inverted {
!a && b // a==0 && b==1
} else {
a && !b // a==1 && b==0
};
if bit {
let shift = (!bit_count) & 0x07;
packet[bit_count >> 3] |= 1u8 << shift;
}
bit_count += 1;
pos += 2;
}
(packet, bit_count)
}
/// Locate the data start (after the alternating preamble + sync run) and unpack/validate.
/// Matches honda_static_parse_symbols. Returns the validated fields and whether the checksum
/// path validated it (forward = true; reverse = false).
fn parse_symbols(symbols: &[bool], inverted: bool) -> Option<(HondaStaticFields, bool)> {
let count = symbols.len();
if count == 0 {
return None;
}
// Walk the alternating preamble: count consecutive transitions; when a non-transition
// follows a run longer than PREAMBLE_MAX_TRANSITIONS, that's the preamble/data boundary.
let mut index = 1usize;
let mut transitions: u16 = 0;
while index < count {
if symbols[index] != symbols[index - 1] {
transitions += 1;
} else {
if transitions > PREAMBLE_MAX_TRANSITIONS {
break;
}
transitions = 0;
}
index += 1;
}
if index >= count {
return None;
}
// Skip forward over the equal-adjacent run (the sync gap).
while (index + 1 < count) && (symbols[index] == symbols[index + 1]) {
index += 1;
}
let data_start = index;
let (packet, bit_count) = Self::manchester_pack_64(symbols, data_start, inverted);
if bit_count < BIT_COUNT {
return None;
}
if let Some(fields) = Self::validate_forward_packet(&packet) {
return Some((fields, true));
}
if inverted {
return None;
}
if let Some(fields) = Self::validate_reverse_packet(&packet) {
return Some((fields, false));
}
None
}
/// Build a DecodedSignal from validated fields. `crc_valid` reflects whether the forward
/// (checksum-validated) path matched.
fn build_signal(fields: &HondaStaticFields, crc_valid: bool) -> DecodedSignal {
DecodedSignal {
serial: Some(fields.serial),
button: Some(fields.button),
// KAT counters are u16; Honda's is 24-bit. Truncate to the low 16 bits for the field
// (the full 24-bit counter is preserved in the packed `data` word).
counter: Some(fields.counter as u16),
crc_valid,
data: Self::pack_compact(fields),
data_count_bit: BIT_COUNT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
}
}
/// Map a KAT generic button command to a Honda Static 4-bit button code.
/// Honda button codes: Lock=1, Unlock=2, Trunk=4, Remote Start=5, Panic=8, Lock x2=9
/// (see honda_static_button_names + honda_static_is_valid_button).
fn map_button(button: u8) -> u8 {
match button {
BTN_LOCK => 1,
BTN_UNLOCK => 2,
BTN_TRUNK => 4,
BTN_PANIC => 8,
// Already a valid Honda code → pass through.
b if Self::is_valid_button(b) => b,
// honda_static_encoder_remap_button for codes 2..=5.
b if (2..=5).contains(&b) => ENCODER_BUTTON_MAP[(b - 2) as usize],
_ => 1,
}
}
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
if let Some(last) = signal.last_mut() {
if last.level == level {
*last = LevelDuration::new(level, last.duration_us + duration);
return;
}
}
signal.push(LevelDuration::new(level, duration));
}
}
impl ProtocolDecoder for HondaStaticDecoder {
fn name(&self) -> &'static str {
"Honda Static"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: BIT_COUNT,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.symbols.clear();
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
let sym = level;
// Short pulse → one symbol (matches the SHORT range check in the C feed).
if duration >= SHORT_BASE_US && (duration - SHORT_BASE_US) <= SHORT_SPAN_US {
if self.symbols.len() < SYMBOL_CAPACITY {
self.symbols.push(sym);
}
return None;
}
// Long pulse → two symbols (same level).
if duration >= LONG_BASE_US && (duration - LONG_BASE_US) <= LONG_SPAN_US {
if self.symbols.len() + 2 <= SYMBOL_CAPACITY {
self.symbols.push(sym);
self.symbols.push(sym);
}
return None;
}
// Out-of-range pulse (sync 700µs or a gap): try to parse the buffered symbols, then reset.
// Matches the C feed: parse with inverted=true first, then inverted=false.
let mut result = None;
if self.symbols.len() >= MIN_SYMBOLS {
let parsed = Self::parse_symbols(&self.symbols, true)
.or_else(|| Self::parse_symbols(&self.symbols, false));
if let Some((fields, crc_valid)) = parsed {
// Faithful to the C: both the forward (checksum-validated) and reverse
// (button+serial-validated) packets commit a decode. The strong button/serial
// gates keep this from false-matching (verified: zero false matches across the
// IMPORTS sweep). `crc_valid` reflects whether the checksum-validated forward path
// matched (true) vs. the reverse path (false).
result = Some(Self::build_signal(&fields, crc_valid));
}
}
self.symbols.clear();
result
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
if !Self::is_valid_serial(serial) {
return None;
}
let fields = HondaStaticFields {
button: Self::map_button(button),
serial,
counter: decoded.counter.unwrap_or(0) as u32 & 0x00FF_FFFF,
checksum: 0,
};
let packet = Self::build_packet_bytes(&fields);
// Matches honda_static_build_upload.
let mut signal =
Vec::with_capacity(1 + PREAMBLE_ALTERNATING_COUNT + 2 * BIT_COUNT + 1);
// Sync: HIGH 700µs.
Self::enc_add_level(&mut signal, true, SYNC_TIME_US);
// Alternating preamble: 160 elements at 63µs, level = (i & 1) (starts LOW).
for i in 0..PREAMBLE_ALTERNATING_COUNT {
Self::enc_add_level(&mut signal, (i & 1) != 0, ELEMENT_TIME_US);
}
// Data, MSB-first: bit → (!value 63µs, value 63µs).
for bit in 0..BIT_COUNT {
let shift = (!bit) & 0x07;
let value = ((packet[bit >> 3] >> shift) & 1) != 0;
Self::enc_add_level(&mut signal, !value, ELEMENT_TIME_US);
Self::enc_add_level(&mut signal, value, ELEMENT_TIME_US);
}
// Trailing sync: !last_bit for 700µs.
let last_bit = (packet[7] & 1) != 0;
Self::enc_add_level(&mut signal, !last_bit, SYNC_TIME_US);
Some(signal)
}
}
impl Default for HondaStaticDecoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Round-trip: encode a known frame, feed it back through the decoder, and confirm the fields
/// survive. The encoder emits the inverted-Manchester interpretation, which the decoder tries
/// first.
#[test]
fn honda_static_encode_decode_roundtrip() {
let serial = 0x0123456u32 & 0x0FFF_FFFF;
let counter = 0x00AB12u32;
let original = DecodedSignal {
serial: Some(serial),
button: Some(BTN_UNLOCK),
counter: Some(counter as u16),
crc_valid: true,
data: 0,
data_count_bit: BIT_COUNT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
};
let decoder = HondaStaticDecoder::new();
let signal = decoder.encode(&original, BTN_UNLOCK).expect("encode");
// Feed the encoded pulses, plus a terminating gap to flush the symbol buffer.
let mut dec = HondaStaticDecoder::new();
let mut decoded = None;
for ld in &signal {
if let Some(d) = dec.feed(ld.level, ld.duration_us) {
decoded = Some(d);
break;
}
}
if decoded.is_none() {
// Terminating out-of-range pulse to flush.
decoded = dec.feed(false, 5000);
}
let decoded = decoded.expect("expected a decode from the round-tripped frame");
assert!(decoded.crc_valid, "checksum should validate");
assert_eq!(decoded.serial, Some(serial), "serial mismatch");
assert_eq!(decoded.button, Some(2), "button should map to Honda Unlock=2");
assert_eq!(
decoded.counter,
Some(counter as u16),
"counter (low 16 bits) mismatch"
);
}
/// The packed Key word must reproduce the C compact layout for a known field set.
#[test]
fn honda_static_pack_compact_layout() {
let fields = HondaStaticFields {
button: 0x02,
serial: 0x0ABCDEF,
counter: 0x123456,
checksum: 0,
};
let data = HondaStaticDecoder::pack_compact(&fields);
let bytes = data.to_be_bytes();
// compact[0] = button & 0x0F
assert_eq!(bytes[0], 0x02);
// serial nibbles: >>20, >>12, >>4, <<4
assert_eq!(bytes[1], (0x0ABCDEFu32 >> 20) as u8);
assert_eq!(bytes[2], (0x0ABCDEFu32 >> 12) as u8);
assert_eq!(bytes[3], (0x0ABCDEFu32 >> 4) as u8);
assert_eq!(bytes[4], (0x0ABCDEFu32 << 4) as u8);
// counter bytes
assert_eq!(bytes[5], 0x12);
assert_eq!(bytes[6], 0x34);
assert_eq!(bytes[7], 0x56);
}
/// build_packet_bytes + validate_forward_packet must round-trip the fields, and the checksum
/// must validate.
#[test]
fn honda_static_packet_validate_roundtrip() {
let fields = HondaStaticFields {
button: 0x05,
serial: 0x0FEDCBA,
counter: 0x00FF01,
checksum: 0,
};
let packet = HondaStaticDecoder::build_packet_bytes(&fields);
let validated = HondaStaticDecoder::validate_forward_packet(&packet)
.expect("forward packet should validate");
assert_eq!(validated.button, 0x05);
assert_eq!(validated.serial, 0x0FEDCBA);
assert_eq!(validated.counter, 0x00FF01);
}
}
+605
View File
@@ -0,0 +1,605 @@
//! Honda V1 protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/honda_v1.c` and
//! `honda_v1.h`. Honda/Acura fixed-code keyfobs, a DIFFERENT protocol from `honda_static`.
//! AM (OOK), 315 MHz + 433.92 MHz. The on-wire frame carries 68 bits: 64-bit data + a 4-bit
//! CRC-fold nibble.
//!
//! **Encoding**: short/long pulse PWM. te_short=1000µs, te_long=2000µs, te_delta=400µs,
//! te_end=3500µs, te_short_min=600µs. Each on-wire symbol is one pulse whose width selects 0/1,
//! but the demodulated stream is glued together by a "pending bit" timing accumulator (see `feed`)
//! before being classified.
//!
//! **Pending-bit accumulation** (matches `subghz_protocol_decoder_honda_v1_feed`): sub-`te_delta`
//! runts are summed into `pending`; a HIGH level keeps extending the running HIGH pulse; a LOW
//! level flushes the accumulated HIGH pulse (if it reached `te_short_min`) as a synthetic symbol,
//! then the LOW pulse itself is classified. The symbol layer (`honda_v1_symbol`) walks a
//! Reset→Preamble→Data state machine; in the Data step a short pulse toggles a `data_pending` flag
//! and, when paired, emits the level as a bit, while a long pulse emits directly — this is the
//! exact pending-bit logic ported from the C.
//!
//! **Frame / fields**: after the end gap (>te_end), `commit` requires ≥68 collected bits, then
//! left-shifts the 12-byte bit buffer by `max(1, bit_count-68)` to drop leading preamble leakage
//! and align the trailing frame. `data` = first 8 bytes (64 bits), `k2` (CRC nibble) = byte 8's
//! high nibble. Fields (matches `honda_v1_decode_fields`): serial = data[63:36] (28b),
//! button = data[31:28] (nibble), counter = data[15:0] (16b).
//!
//! **Validation / gating**: a button-code table (Unlock=0, Lock=8, Trunk=9, Panic=10) plus a
//! CRC-fold checksum (`honda_v1_checksum*`). Emission is gated on the button being valid (matches
//! the C `commit`); `crc_valid` reflects whether the received CRC nibble matches either wire-order
//! checksum (`honda_v1_crc_valid`). The strong button gate keeps Honda V1 from false-matching.
//!
//! **Encoder** (matches `honda_v1_build_upload` / `honda_v1_append_frame`, behind
//! `ENABLE_EMULATE_FEATURE`): builds the 64-bit key from serial/button/counter via the button
//! table, then emits a 180-element short-pair preamble + 4 PWM frames (2 per checksum wire value).
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
const BIT_COUNT: usize = 68;
const TE_SHORT: u32 = 1000;
const TE_LONG: u32 = 2000;
const TE_DELTA: u32 = 400;
const TE_SHORT_MIN: u32 = 600;
const TE_END: u32 = 3500;
const VALID_MAX: u8 = 0x4B; // honda_v1_add_bit cap (75)
const NIBBLE_MASK: u8 = 0x0F;
const SERIAL_MASK: u32 = 0x0FFF_FFFF;
const COUNTER_MASK: u16 = 0xFFFF;
const BUTTON_MAX: u8 = 10;
const BUTTON_VALID_MASK: u16 = 0x701; // bits set for Unlock(0), Lock(8), Trunk(9), Panic(10)
const DECODE_BUFFER_BYTES: usize = 12;
// Encoder constants (honda_v1.c, ENABLE_EMULATE_FEATURE).
const PREAMBLE_UPLOAD_COUNT: usize = 180;
const FRAME_SYMBOLS: usize = 80;
const FRAME_START: usize = 12;
const FRAME_SYNC_DROP: usize = 2;
const FRAME_REPEAT_PER_CRC: usize = 2;
const FRAME_GAP_US: u32 = 5000;
const FRAME_CRC_INDEX: usize = 8;
// HondaV1Button codes (the on-wire button nibble at data[31:28]).
const BTN_CODE_UNLOCK: u8 = 0;
const BTN_CODE_LOCK: u8 = 8;
const BTN_CODE_TRUNK: u8 = 9;
const BTN_CODE_PANIC: u8 = 10;
// honda_v1_button_codes[] (24-bit table values, used to rebuild the key in the encoder).
const BUTTON_CODE_UNLOCK: u32 = 0x0008_0808;
const BUTTON_CODE_LOCK: u32 = 0x0008_8888;
const BUTTON_CODE_TRUNK: u32 = 0x0009_9190;
const BUTTON_CODE_PANIC: u32 = 0x000F_A7A0;
const BUTTON_FALLBACK_CODE: u32 = 0x0008_8888;
// KAT generic button codes.
const BTN_LOCK: u8 = 0x01;
const BTN_UNLOCK: u8 = 0x02;
const BTN_TRUNK: u8 = 0x04;
const BTN_PANIC: u8 = 0x08;
/// Decoder steps (matches HondaV1DecoderStep).
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Preamble,
Data,
}
/// Honda V1 decoder (matches SubGhzProtocolDecoderHondaV1).
pub struct HondaV1Decoder {
step: DecoderStep,
preamble_count: u8,
preamble_has_long: bool,
data_pending: bool,
last_level: bool,
bits: [u8; DECODE_BUFFER_BYTES],
bit_count: u8,
// Pending-bit timing accumulator (decoder-level, persists across symbol resets).
pending: u32,
pending_valid: bool,
}
impl HondaV1Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
preamble_count: 0,
preamble_has_long: false,
data_pending: false,
last_level: false,
bits: [0u8; DECODE_BUFFER_BYTES],
bit_count: 0,
pending: 0,
pending_valid: false,
}
}
/// honda_v1_button_valid: button <= 10 && ((0x701 >> button) & 1).
fn button_valid(b: u8) -> bool {
if b > BUTTON_MAX {
return false;
}
((BUTTON_VALID_MASK >> b) & 1) != 0
}
/// honda_v1_duration_is: |d - t| <= te_delta (saturating both directions).
fn duration_is(d: u32, t: u32) -> bool {
if d >= t {
(d - t) <= TE_DELTA
} else {
(t - d) <= TE_DELTA
}
}
/// honda_v1_crc_fold.
fn crc_fold(v: u16) -> u8 {
let lo = (v & (NIBBLE_MASK as u16)) as u8;
let hi = v >> 4;
let s: i32 = if (hi & 1) != 0 {
lo as i32
} else {
-(lo as i32)
};
let mut out = ((s - (hi as i32)) & 7) as u8;
out |= (((v >> 3) & 1) as u8) << 3;
if ((v >> 1) & 1) != 0 && (((v >> 4) ^ (v >> 5)) & 1) != 0 {
out ^= 0x04;
}
out & NIBBLE_MASK
}
/// honda_v1_checksum_base.
fn checksum_base(data: u64) -> u8 {
let a = Self::crc_fold((data & (COUNTER_MASK as u64)) as u16);
let b = Self::crc_fold(((data >> 40) & 0xFF) as u16);
(a ^ b ^ 1) & NIBBLE_MASK
}
/// honda_v1_checksum_alternate.
fn checksum_alternate(checksum: u8) -> u8 {
let mut mask = 0x09u8;
if (checksum & 1) == 0 {
mask = if (checksum & 2) != 0 { 0x0B } else { NIBBLE_MASK };
}
(checksum ^ mask) & NIBBLE_MASK
}
/// honda_v1_checksum_wire_order → (first, second).
fn checksum_wire_order(data: u64) -> (u8, u8) {
let checksum = Self::checksum_base(data);
let other = Self::checksum_alternate(checksum);
if (checksum & 0x08) != 0 {
(other, checksum)
} else {
(checksum, other)
}
}
/// honda_v1_crc_valid: received nibble matches either wire-order checksum.
fn crc_valid(data: u64, crc: u8) -> bool {
let (first, second) = Self::checksum_wire_order(data);
let crc = crc & NIBBLE_MASK;
crc == first || crc == second
}
/// honda_v1_decode_fields. Returns (serial, button, counter).
fn decode_fields(data: u64) -> (u32, u8, u16) {
let low = (data & 0xFFFF_FFFF) as u32;
let serial = ((data >> 36) & (SERIAL_MASK as u64)) as u32;
let button = ((low >> 28) & (NIBBLE_MASK as u32)) as u8;
let counter = (low & (COUNTER_MASK as u32)) as u16;
(serial, button, counter)
}
/// honda_v1_button_code (encoder).
fn button_code(button: u8) -> u32 {
if !Self::button_valid(button) {
return BUTTON_FALLBACK_CODE;
}
match button {
BTN_CODE_UNLOCK => BUTTON_CODE_UNLOCK,
BTN_CODE_LOCK => BUTTON_CODE_LOCK,
BTN_CODE_TRUNK => BUTTON_CODE_TRUNK,
BTN_CODE_PANIC => BUTTON_CODE_PANIC,
_ => BUTTON_FALLBACK_CODE,
}
}
/// honda_v1_build_key.
fn build_key(serial: u32, button: u8, counter: u16) -> u64 {
let table = Self::button_code(button);
let low = ((table & (COUNTER_MASK as u32)) << 16) | (counter as u32);
let high = ((serial & SERIAL_MASK) << 4) | (table >> 16);
((high as u64) << 32) | (low as u64)
}
/// Map a KAT generic button command to a Honda V1 button code (nibble at data[31:28]).
fn map_button(button: u8) -> u8 {
match button {
BTN_LOCK => BTN_CODE_LOCK,
BTN_UNLOCK => BTN_CODE_UNLOCK,
BTN_TRUNK => BTN_CODE_TRUNK,
BTN_PANIC => BTN_CODE_PANIC,
// Already a valid Honda V1 code → pass through.
b if Self::button_valid(b) => b,
_ => BTN_CODE_UNLOCK,
}
}
/// honda_v1_state_reset (symbol-layer state only; does NOT touch pending accumulator).
fn state_reset(&mut self) {
self.step = DecoderStep::Reset;
self.preamble_count = 0;
self.preamble_has_long = false;
self.data_pending = false;
self.last_level = false;
self.bit_count = 0;
self.bits = [0u8; DECODE_BUFFER_BYTES];
}
/// honda_v1_add_bit: MSB-first into bits[], capped at VALID_MAX.
fn add_bit(&mut self, bit: bool) {
if self.bit_count > VALID_MAX {
return;
}
if bit {
let byte = (self.bit_count >> 3) as usize;
let shift = (!self.bit_count) & 0x07;
self.bits[byte] |= 1u8 << shift;
}
self.bit_count += 1;
}
/// honda_v1_commit: align trailing 68-bit frame, validate button, return signal on success.
fn commit(&mut self) -> Option<DecodedSignal> {
if (self.bit_count as usize) < BIT_COUNT {
return None;
}
let mut aligned = self.bits;
let mut shift_count = self.bit_count - BIT_COUNT as u8;
if shift_count < 1 {
shift_count = 1;
}
for _ in 0..shift_count {
for i in 0..(DECODE_BUFFER_BYTES - 1) {
aligned[i] = (aligned[i] << 1) | (aligned[i + 1] >> 7);
}
aligned[DECODE_BUFFER_BYTES - 1] <<= 1;
}
let button = aligned[4] >> 4;
if !Self::button_valid(button) {
return None;
}
let data = u64::from_be_bytes(aligned[0..8].try_into().unwrap());
let k2 = aligned[8] >> 4;
let (serial, btn, counter) = Self::decode_fields(data);
let crc_valid = Self::crc_valid(data, k2);
Some(DecodedSignal {
serial: Some(serial),
button: Some(btn),
counter: Some(counter),
crc_valid,
data,
data_count_bit: BIT_COUNT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
})
}
/// honda_v1_symbol: classify a (level, duration) pulse and drive the state machine.
/// Returns Some(signal) when the end-gap path commits a frame.
fn symbol(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
let sh = Self::duration_is(duration, TE_SHORT);
let lg = Self::duration_is(duration, TE_LONG);
if !sh && !lg {
let mut result = None;
if !level && duration > TE_END && self.step == DecoderStep::Data {
result = self.commit();
}
self.state_reset();
return result;
}
if self.step == DecoderStep::Reset {
if level {
self.step = DecoderStep::Preamble;
self.preamble_count = 1;
self.last_level = level;
}
return None;
}
if self.step == DecoderStep::Preamble {
if lg {
// honda_v1.c: if(preamble_count < 0xFF) preamble_count++ (saturating).
self.preamble_count = self.preamble_count.saturating_add(1);
self.preamble_has_long = true;
self.last_level = level;
return None;
}
if sh {
if self.preamble_has_long && self.preamble_count > 5 {
self.step = DecoderStep::Data;
self.bit_count = 0;
self.bits = [0u8; DECODE_BUFFER_BYTES];
self.data_pending = true;
self.last_level = level;
return None;
}
self.preamble_count = self.preamble_count.saturating_add(1);
self.last_level = level;
return None;
}
self.state_reset();
return None;
}
// Data step: pending-bit accumulation.
if sh {
if self.data_pending {
self.add_bit(level);
self.data_pending = false;
self.last_level = level;
} else {
self.data_pending = true;
self.last_level = level;
}
} else {
// long pulse
if self.data_pending {
self.add_bit(level);
} else {
self.add_bit(self.last_level);
}
self.last_level = level;
}
None
}
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
if let Some(last) = signal.last_mut() {
if last.level == level {
*last = LevelDuration::new(level, last.duration_us + duration);
return;
}
}
signal.push(LevelDuration::new(level, duration));
}
/// honda_v1_append_frame: emit one PWM frame (12-symbol sync header + 68 data symbols),
/// dropping the first FRAME_SYNC_DROP entries, then a tail + inter-frame gap.
fn append_frame(signal: &mut Vec<LevelDuration>, frame: &[u8; 9]) {
// Build the per-frame symbol stream with merge semantics (pp_emit_merge).
let mut generated: Vec<LevelDuration> = Vec::with_capacity(FRAME_SYMBOLS * 2);
for bit_index in 0..FRAME_SYMBOLS {
let bit = if bit_index >= FRAME_START {
let data_index = (bit_index - FRAME_START) >> 3;
let shift = (11i32 - bit_index as i32) & 0x07;
((frame[data_index] >> shift) & 0x01) != 0
} else {
((!bit_index) & 0x01) != 0
};
// bit -> (level=bit, te)(level=!bit, te), merged.
Self::enc_add_level(&mut generated, bit, TE_SHORT);
Self::enc_add_level(&mut generated, !bit, TE_SHORT);
}
if generated.len() <= FRAME_SYNC_DROP {
return;
}
// Copy generated[FRAME_SYNC_DROP..] into the upload (still merging at the seam).
for ld in &generated[FRAME_SYNC_DROP..] {
Self::enc_add_level(signal, ld.level, ld.duration_us);
}
// Tail: !last_level for te_short; if that was low, add a high te_short; then the gap.
let last_level = signal.last().map(|l| l.level).unwrap_or(false);
let tail_level = !last_level;
Self::enc_add_level(signal, tail_level, TE_SHORT);
if !tail_level {
Self::enc_add_level(signal, true, TE_SHORT);
}
Self::enc_add_level(signal, false, FRAME_GAP_US);
}
}
impl ProtocolDecoder for HondaV1Decoder {
fn name(&self) -> &'static str {
"Honda V1"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: BIT_COUNT,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.pending = 0;
self.pending_valid = false;
self.state_reset();
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
// Pending-bit timing accumulation (subghz_protocol_decoder_honda_v1_feed).
if duration < TE_DELTA {
self.pending = self.pending.saturating_add(duration);
self.pending_valid = true;
return None;
}
let mut result = None;
if self.pending_valid {
let p = self.pending;
if level {
self.pending = p.saturating_add(duration);
self.pending_valid = true;
return None;
}
if p >= TE_SHORT_MIN {
result = self.symbol(true, p);
}
self.pending = 0;
self.pending_valid = false;
}
if level {
self.pending = duration;
self.pending_valid = true;
return result;
}
// A symbol committed on the flushed HIGH pulse takes priority (the C calls back there);
// otherwise classify this LOW pulse.
let low_result = self.symbol(false, duration);
result.or(low_result)
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0) & COUNTER_MASK;
let btn = Self::map_button(button);
let data = Self::build_key(serial & SERIAL_MASK, btn, counter);
let mut frame = [0u8; 9];
frame[0..8].copy_from_slice(&data.to_be_bytes());
let (first, second) = Self::checksum_wire_order(data);
let mut signal: Vec<LevelDuration> = Vec::with_capacity(PREAMBLE_UPLOAD_COUNT + 8 * FRAME_SYMBOLS);
// Preamble: 180 short entries (90 H/L pairs); the final LOW becomes a 5000µs gap.
for _ in 0..(PREAMBLE_UPLOAD_COUNT / 2) {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
if let Some(last) = signal.last_mut() {
*last = LevelDuration::new(false, FRAME_GAP_US);
}
// 4 frames: 2 per checksum wire value (first, then second).
for &crc in &[first, second] {
frame[FRAME_CRC_INDEX] = crc << 4;
for _ in 0..FRAME_REPEAT_PER_CRC {
Self::append_frame(&mut signal, &frame);
}
}
Some(signal)
}
}
impl Default for HondaV1Decoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// build_key + decode_fields must round-trip the fields, and the encoder's CRC nibble must
/// validate against honda_v1_crc_valid.
#[test]
fn honda_v1_key_field_roundtrip() {
let serial = 0x0ABCDEFu32 & SERIAL_MASK;
let counter = 0x1234u16;
let data = HondaV1Decoder::build_key(serial, BTN_CODE_TRUNK, counter);
let (s, b, c) = HondaV1Decoder::decode_fields(data);
assert_eq!(s, serial, "serial mismatch");
assert_eq!(b, BTN_CODE_TRUNK, "button mismatch");
assert_eq!(c, counter, "counter mismatch");
let (first, _second) = HondaV1Decoder::checksum_wire_order(data);
assert!(
HondaV1Decoder::crc_valid(data, first),
"wire-order checksum should validate"
);
}
/// Full encode → decode round-trip: emit a frame for known fields, feed the pulses back through
/// the decoder (the pending-bit accumulator + symbol state machine), and confirm the fields and
/// checksum survive.
#[test]
fn honda_v1_encode_decode_roundtrip() {
let serial = 0x0123456u32 & SERIAL_MASK;
let counter = 0x00ABu16;
let original = DecodedSignal {
serial: Some(serial),
button: Some(BTN_UNLOCK),
counter: Some(counter),
crc_valid: true,
data: 0,
data_count_bit: BIT_COUNT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
};
let decoder = HondaV1Decoder::new();
let signal = decoder.encode(&original, BTN_UNLOCK).expect("encode");
let mut dec = HondaV1Decoder::new();
let mut decoded = None;
for ld in &signal {
if let Some(d) = dec.feed(ld.level, ld.duration_us) {
decoded = Some(d);
break;
}
}
// Flush with a terminating end-gap if the inter-frame gaps didn't already commit.
if decoded.is_none() {
decoded = dec.feed(false, TE_END + 1000);
}
let decoded = decoded.expect("expected a decode from the round-tripped frame");
assert!(decoded.crc_valid, "checksum should validate");
assert_eq!(decoded.serial, Some(serial), "serial mismatch");
assert_eq!(
decoded.button,
Some(BTN_CODE_UNLOCK),
"button should map to Honda V1 Unlock=0"
);
assert_eq!(decoded.counter, Some(counter), "counter mismatch");
assert_eq!(decoded.data_count_bit, BIT_COUNT);
}
/// Button validity mask must match honda_v1_button_valid (0x701 → Unlock/Lock/Trunk/Panic).
#[test]
fn honda_v1_button_validity() {
assert!(HondaV1Decoder::button_valid(BTN_CODE_UNLOCK));
assert!(HondaV1Decoder::button_valid(BTN_CODE_LOCK));
assert!(HondaV1Decoder::button_valid(BTN_CODE_TRUNK));
assert!(HondaV1Decoder::button_valid(BTN_CODE_PANIC));
// Invalid codes.
assert!(!HondaV1Decoder::button_valid(1));
assert!(!HondaV1Decoder::button_valid(7));
assert!(!HondaV1Decoder::button_valid(11));
}
}
+3 -1
View File
@@ -30,7 +30,9 @@ fn g5(x: u32, a: u32, b: u32, c: u32, d: u32, e: u32) -> u32 {
pub fn keeloq_decrypt(data: u32, key: u64) -> u32 { pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
let mut x = data; let mut x = data;
for r in 0..528u32 { for r in 0..528u32 {
let key_bit = ((key >> ((15 - r) & 63)) & 1) as u32; // (15 - r) relies on unsigned wraparound (matches the C reference `(15 - r) & 63`);
// use wrapping_sub so debug builds don't panic on the overflow that release silently wraps.
let key_bit = ((key >> ((15u32.wrapping_sub(r)) & 63)) & 1) as u32;
let new_lsb = bit(x, 31) ^ bit(x, 15) ^ key_bit let new_lsb = bit(x, 31) ^ bit(x, 15) ^ key_bit
^ bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30)); ^ bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30));
x = (x << 1) ^ new_lsb; x = (x << 1) ^ new_lsb;
+314
View File
@@ -0,0 +1,314 @@
//! Kia V7 protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v7.c` and `kia_v7.h`.
//! Manchester 250/500µs, 64 bits, FM. The decoded 64-bit word is bit-inverted (`~data`); a valid
//! frame has a fixed high byte 0x4C and a CRC8 (poly 0x7F, init 0x4C) over bytes 0..7. Emission is
//! gated on header + CRC, so Kia V7 is strongly validated and will not false-match.
//!
//! Decoder steps: Reset → Preamble (short pairs, ≥16) → SyncLow → Data. The preamble→sync transition
//! preloads four seed bits (1,0,1,1 = the inverted header's top nibble 0xB) before collecting the
//! remaining 60 Manchester bits. Encoder supported.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use super::common::crc8;
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 250;
const TE_LONG: u32 = 500;
const TE_DELTA: u32 = 100;
const KEY_BITS: usize = 64;
const HEADER: u8 = 0x4C;
const PREAMBLE_MIN_PAIRS: u16 = 16;
const TAIL_GAP_US: u32 = 2000;
const TX_PREAMBLE_PAIRS: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0 = 0,
Mid1 = 1,
Start0 = 2,
Start1 = 3,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Preamble,
SyncLow,
Data,
}
pub struct KiaV7Decoder {
step: DecoderStep,
manchester_state: ManchesterState,
te_last: u32,
preamble_count: u16,
decode_data: u64,
decode_count_bit: usize,
}
impl KiaV7Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
manchester_state: ManchesterState::Mid1,
te_last: 0,
preamble_count: 0,
decode_data: 0,
decode_count_bit: 0,
}
}
fn is_short(d: u32) -> bool {
duration_diff!(d, TE_SHORT) < TE_DELTA
}
fn is_long(d: u32) -> bool {
duration_diff!(d, TE_LONG) < TE_DELTA
}
fn add_bit(&mut self, bit: bool) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
fn manchester_advance(&mut self, event: u8) -> Option<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 }
}
/// Decode the 8 plaintext bytes of the (already inverted) key into fields.
/// Returns (serial, button, counter, crc_valid).
fn decode_key(data: u64) -> (u32, u8, u16, bool) {
let bytes = data.to_be_bytes();
let serial = (((bytes[3] as u32) << 20)
| ((bytes[4] as u32) << 12)
| ((bytes[5] as u32) << 4)
| ((bytes[6] as u32) >> 4))
& 0x0FFF_FFFF;
let counter = ((bytes[1] as u16) << 8) | bytes[2] as u16;
let button = bytes[6] & 0x0F;
let crc_calc = crc8(&bytes[0..7], 0x7F, 0x4C);
let crc_valid = crc_calc == bytes[7];
(serial, button, counter, crc_valid)
}
/// Rebuild the 64-bit key from fields (matches kia_v7_encode_key).
fn encode_key(serial: u32, button: u8, counter: u16) -> u64 {
let serial = serial & 0x0FFF_FFFF;
let button = button & 0x0F;
let mut bytes = [0u8; 8];
bytes[0] = HEADER;
bytes[1] = (counter >> 8) as u8;
bytes[2] = counter as u8;
bytes[3] = (serial >> 20) as u8;
bytes[4] = (serial >> 12) as u8;
bytes[5] = (serial >> 4) as u8;
bytes[6] = (((serial & 0x0F) as u8) << 4) | button;
bytes[7] = crc8(&bytes[0..7], 0x7F, 0x4C);
u64::from_be_bytes(bytes)
}
/// Map KAT generic button command to a Kia V7 4-bit button code.
fn map_button(button: u8) -> u8 {
match button {
0x01 => 0x01, // Lock
0x02 => 0x02, // Unlock
0x04 => 0x03, // Trunk
0x08 => 0x08, // Trunk/aux
b => b & 0x0F,
}
}
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
if let Some(last) = signal.last_mut() {
if last.level == level {
*last = LevelDuration::new(level, last.duration_us + duration);
return;
}
}
signal.push(LevelDuration::new(level, duration));
}
}
impl ProtocolDecoder for KiaV7Decoder {
fn name(&self) -> &'static str {
"Kia V7"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: KEY_BITS,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.manchester_state = ManchesterState::Mid1;
self.te_last = 0;
self.preamble_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if level && Self::is_short(duration) {
self.step = DecoderStep::Preamble;
self.te_last = duration;
self.preamble_count = 0;
self.manchester_state = ManchesterState::Mid1;
}
}
DecoderStep::Preamble => {
if level {
if Self::is_long(duration) && Self::is_short(self.te_last) {
if self.preamble_count > (PREAMBLE_MIN_PAIRS - 1) {
self.decode_data = 0;
self.decode_count_bit = 0;
self.preamble_count = 0;
// Seed the inverted-header top nibble (1,0,1,1).
self.add_bit(true);
self.add_bit(false);
self.add_bit(true);
self.add_bit(true);
self.te_last = duration;
self.step = DecoderStep::SyncLow;
} else {
self.step = DecoderStep::Reset;
}
} else if Self::is_short(duration) {
self.te_last = duration;
} else {
self.step = DecoderStep::Reset;
}
} else if Self::is_short(duration) && Self::is_short(self.te_last) {
self.preamble_count += 1;
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::SyncLow => {
if !level && Self::is_short(duration) && Self::is_long(self.te_last) {
self.te_last = duration;
self.step = DecoderStep::Data;
}
}
DecoderStep::Data => {
let event = if Self::is_short(duration) {
Some(if level { 1 } else { 0 })
} else if Self::is_long(duration) {
Some(if level { 3 } else { 2 })
} else {
None
};
if let Some(ev) = event {
if let Some(bit) = self.manchester_advance(ev) {
self.add_bit(bit);
}
}
if self.decode_count_bit == KEY_BITS {
let candidate = !self.decode_data;
let hdr = (candidate >> 56) as u8;
self.decode_data = 0;
self.decode_count_bit = 0;
self.step = DecoderStep::Reset;
if hdr == HEADER {
let (serial, button, counter, crc_valid) = Self::decode_key(candidate);
if crc_valid {
return Some(DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid: true,
data: candidate,
data_count_bit: KEY_BITS,
encoder_capable: true,
extra: None,
protocol_display_name: None,
});
}
}
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0);
let key = Self::encode_key(serial, Self::map_button(button), counter);
let mut signal = Vec::with_capacity(TX_PREAMBLE_PAIRS * 2 + KEY_BITS * 2 + 4);
// Preamble: alternating short pulses.
for _ in 0..TX_PREAMBLE_PAIRS {
Self::enc_add_level(&mut signal, true, TE_SHORT);
Self::enc_add_level(&mut signal, false, TE_SHORT);
}
// Standalone high short (merges with first data-bit high to form the long sync pulse).
Self::enc_add_level(&mut signal, true, TE_SHORT);
// Manchester data, MSB first: bit 1 → (H,L), bit 0 → (L,H).
for bit in (0..KEY_BITS).rev() {
let value = (key >> bit) & 1 != 0;
if value {
Self::enc_add_level(&mut signal, true, TE_SHORT);
Self::enc_add_level(&mut signal, false, TE_SHORT);
} else {
Self::enc_add_level(&mut signal, false, TE_SHORT);
Self::enc_add_level(&mut signal, true, TE_SHORT);
}
}
// Trailing high short + tail gap.
Self::enc_add_level(&mut signal, true, TE_SHORT);
Self::enc_add_level(&mut signal, false, TAIL_GAP_US);
Some(signal)
}
}
impl Default for KiaV7Decoder {
fn default() -> Self {
Self::new()
}
}
+531
View File
@@ -0,0 +1,531 @@
//! Land Rover RKE protocol decoder/encoder
//!
//! Ported from the Flipper-ARF firmware (`lib/subghz/protocols/landrover_rke.c` / `.h`,
//! D4C1-Labs), itself derived from Pandora DXL 5000 firmware. Land Rover shares the
//! Ford/Jaguar baseband (firmware protocol ID 0x0E) but uses a distinct 66-bit frame.
//!
//! Encoding: fixed-width PWM, OOK/AM carrier. Bit period 1000µs:
//! Bit-1 = 700µs HIGH + 300µs LOW; Bit-0 = 300µs HIGH + 700µs LOW.
//! Preamble = 20× (400µs HIGH + 600µs LOW); sync = 400µs HIGH + 9600µs LOW.
//! Tolerance ±20% (relative), matching the C `lr_in_range`.
//!
//! Frame (66 bits, MSB-first), matching the C layout:
//! [65:34] 32-bit KeeLoq encrypted hopping code
//! [33:10] 24-bit fixed fob serial
//! [9:6] 4-bit button code (0x1=Lock, 0x2=Unlock, 0x4=Boot/Tailgate, 0x8=Panic)
//! [5:2] 4-bit function/repeat flags
//! [1:0] 2-bit status (0x1=battery low, 0x2=repeat)
//!
//! KeeLoq: the hop code is the raw 32-bit KeeLoq ciphertext. Full decryption needs the
//! per-fob manufacturer key (provisioned, not in the firmware), so KAT exposes the framed
//! fields (serial/button) and leaves the hop encrypted — `crc_valid=false` since no real
//! cryptographic check is performed. Emission is still gated tightly on the structural
//! invariants below so this loose-looking PWM decoder does not false-match
//! Kia/Subaru/Ford captures.
//!
//! Storage: 66 bits do not fit a u64. The canonical `DecodedSignal.data` holds the low 64
//! frame bits and `DecodedSignal.extra` holds the top 2 (frame bits [65:64], the high 2 bits
//! of the hop code), so encode() round-trips the hop code losslessly. `data_count_bit` = 66.
//!
//! Gating: a valid frame requires a long preamble run (≥16 of the 400/600µs pairs), the very
//! distinctive 400µs-HIGH + 9600µs-LOW sync gap, then exactly 66 bits whose HIGH/LOW halves
//! each fall inside the ±20% PWM windows. The 9.6ms sync gap + 66-bit PWM payload combination
//! is unique among KAT's protocols, so nothing else in the sweep matches it.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
// Timing constants (microseconds) — verbatim from landrover_rke.h.
const PREAMBLE_HIGH_US: u32 = 400;
const PREAMBLE_LOW_US: u32 = 600;
const PREAMBLE_COUNT: u32 = 20;
const SYNC_HIGH_US: u32 = 400;
const SYNC_LOW_US: u32 = 9600;
const BIT1_HIGH_US: u32 = 700;
const BIT1_LOW_US: u32 = 300;
const BIT0_HIGH_US: u32 = 300;
const BIT0_LOW_US: u32 = 700;
const REPEAT_GAP_US: u32 = 12000;
const REPEAT_COUNT: u32 = 4;
const TOLERANCE_PCT: u32 = 20;
const FRAME_BITS: usize = 66;
// Require a substantial preamble run before accepting the sync gap. The C scans a raw buffer
// for the sync directly, but gating on the preamble too makes the streaming decoder specific.
const PREAMBLE_MIN_PAIRS: u32 = 16;
// Button codes (frame bits [9:6]).
const BTN_LOCK: u8 = 0x1;
const BTN_UNLOCK: u8 = 0x2;
const BTN_BOOT: u8 = 0x4;
const BTN_PANIC: u8 = 0x8;
/// Relative-tolerance match, matching the C `lr_in_range`:
/// `|measured - ref| * 100 <= ref * TOLERANCE_PCT`.
#[inline]
fn in_range(measured_us: u32, ref_us: u32) -> bool {
let diff = if measured_us > ref_us {
measured_us - ref_us
} else {
ref_us - measured_us
};
diff.saturating_mul(100) <= ref_us.saturating_mul(TOLERANCE_PCT)
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
/// Looking for the first preamble HIGH pulse.
Reset,
/// Counting preamble (400µs HIGH + 600µs LOW) pairs; te_last holds the pending HIGH.
Preamble,
/// Sync seen — expecting the next data-bit HIGH half.
DataHigh,
/// Collected a data-bit HIGH in te_last; decide the bit value from the following LOW.
DataLow,
}
/// Land Rover RKE protocol decoder.
///
/// Bits are accumulated MSB-first into a 66-element array (index 0 = first bit on air =
/// frame bit 65), mirroring the C `bits[65 - b]` indexing and avoiding any u64 overflow.
pub struct LandRoverRkeDecoder {
step: DecoderStep,
te_last: u32,
preamble_count: u32,
/// Received bits, MSB-first: `rx_bits[i]` is the (i+1)-th bit on air = frame bit `65 - i`.
rx_bits: [u8; FRAME_BITS],
decode_count_bit: usize,
}
impl LandRoverRkeDecoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
preamble_count: 0,
rx_bits: [0u8; FRAME_BITS],
decode_count_bit: 0,
}
}
/// Map a KAT generic button command to a Land Rover RKE 4-bit button code.
/// KAT: Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08.
/// LR RKE bits [9:6]: Lock=0x1, Unlock=0x2, Boot/Tailgate=0x4, Panic=0x8 — identical mapping.
fn map_button(button: u8) -> u8 {
match button {
0x01 => BTN_LOCK,
0x02 => BTN_UNLOCK,
0x04 => BTN_BOOT,
0x08 => BTN_PANIC,
b => b & 0x0F,
}
}
/// Build the logical 66-bit frame array from the field values.
///
/// The array is indexed by frame bit number (bit 0 = LSB of the whole 66-bit frame). Each
/// field `X` occupying frame bits `[hi:lo]` maps frame bit `(lo + j)` = field bit `j`, so the
/// field's MSB lands at the higher frame-bit index. The encoder transmits frame bit 65 first,
/// giving the documented MSB-first-on-air order. This is the exact inverse of `unpack_frame`.
///
/// NOTE: the Flipper-ARF C reference's `lr_encode`/`lr_decode` use inconsistent bit
/// endianness for the fields (encode writes `bits[65-i]=field>>i`, decode reads
/// `field|=bits[65-k]<<(31-k)`), so the C does not round-trip. KAT preserves the documented
/// field *layout* and MSB-first wire order while keeping encode/decode mutually consistent.
fn pack_frame(hop_code: u32, serial: u32, button: u8, func_bits: u8, status: u8) -> [u8; FRAME_BITS] {
let mut bits = [0u8; FRAME_BITS];
// hop_code: frame bits [65:34] (32 bits).
for j in 0..32 {
bits[34 + j] = ((hop_code >> j) & 1) as u8;
}
// serial: frame bits [33:10] (24 bits).
for j in 0..24 {
bits[10 + j] = ((serial >> j) & 1) as u8;
}
// button: frame bits [9:6] (4 bits).
for j in 0..4 {
bits[6 + j] = ((button >> j) & 1) as u8;
}
// func_bits: frame bits [5:2] (4 bits).
for j in 0..4 {
bits[2 + j] = ((func_bits >> j) & 1) as u8;
}
// status: frame bits [1:0] (2 bits).
bits[0] = status & 1;
bits[1] = (status >> 1) & 1;
bits
}
/// Extract fields from a logical 66-bit frame array (index = frame bit, bit 0 = LSB).
/// Returns (hop_code, serial, button, func_bits, status). Exact inverse of `pack_frame`.
fn unpack_frame(bits: &[u8; FRAME_BITS]) -> (u32, u32, u8, u8, u8) {
let mut hop_code: u32 = 0;
for j in 0..32 {
hop_code |= (bits[34 + j] as u32) << j;
}
let mut serial: u32 = 0;
for j in 0..24 {
serial |= (bits[10 + j] as u32) << j;
}
let mut button: u8 = 0;
for j in 0..4 {
button |= bits[6 + j] << j;
}
let mut func_bits: u8 = 0;
for j in 0..4 {
func_bits |= bits[2 + j] << j;
}
let status = bits[0] | (bits[1] << 1);
(hop_code, serial, button, func_bits, status)
}
/// Pack a logical 66-bit frame array into `(data, extra)`:
/// `data` = frame bits [63:0], `extra` = frame bits [65:64].
fn frame_to_data(bits: &[u8; FRAME_BITS]) -> (u64, u64) {
let mut data: u64 = 0;
for i in 0..64 {
data |= (bits[i] as u64) << i;
}
let extra: u64 = (bits[64] as u64) | ((bits[65] as u64) << 1);
(data, extra)
}
/// Inverse of `frame_to_data`.
fn data_to_frame(data: u64, extra: u64) -> [u8; FRAME_BITS] {
let mut bits = [0u8; FRAME_BITS];
for i in 0..64 {
bits[i] = ((data >> i) & 1) as u8;
}
bits[64] = (extra & 1) as u8;
bits[65] = ((extra >> 1) & 1) as u8;
bits
}
/// Convert the received MSB-first `rx_bits` (index 0 = frame bit 65) into the logical
/// frame array (index = frame bit number).
fn rx_to_frame(rx_bits: &[u8; FRAME_BITS]) -> [u8; FRAME_BITS] {
let mut frame = [0u8; FRAME_BITS];
for i in 0..FRAME_BITS {
frame[65 - i] = rx_bits[i];
}
frame
}
/// Build a DecodedSignal from a completed received-bit array.
fn build_signal(rx_bits: &[u8; FRAME_BITS]) -> DecodedSignal {
let frame = Self::rx_to_frame(rx_bits);
let (hop_code, serial, button, _func_bits, _status) = Self::unpack_frame(&frame);
let (data, extra) = Self::frame_to_data(&frame);
DecodedSignal {
serial: Some(serial),
button: Some(button),
// The 16-bit KeeLoq counter is inside the *encrypted* hop code; without the
// manufacturer key we cannot recover it. Surface the low 16 bits of the hop
// ciphertext so the UI has a stable per-press value.
counter: Some((hop_code & 0xFFFF) as u16),
// No cryptographic check is performed (no key), so this is not a verified frame.
crc_valid: false,
data,
data_count_bit: FRAME_BITS,
encoder_capable: true,
// Carry the top 2 frame bits so encode() can faithfully round-trip the hop code.
extra: Some(extra),
protocol_display_name: None,
}
}
fn push_pair(signal: &mut Vec<LevelDuration>, high_us: u32, low_us: u32) {
signal.push(LevelDuration::new(true, high_us));
signal.push(LevelDuration::new(false, low_us));
}
}
impl ProtocolDecoder for LandRoverRkeDecoder {
fn name(&self) -> &'static str {
"Land Rover RKE"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: BIT0_HIGH_US, // 300µs
te_long: BIT1_HIGH_US, // 700µs
te_delta: 140, // ~20% of the 700µs long half
min_count_bit: FRAME_BITS,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[433_920_000, 315_000_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.te_last = 0;
self.preamble_count = 0;
self.rx_bits = [0u8; FRAME_BITS];
self.decode_count_bit = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
// First preamble HIGH (~400µs).
if level && in_range(duration, PREAMBLE_HIGH_US) {
self.step = DecoderStep::Preamble;
self.te_last = duration;
self.preamble_count = 0;
}
}
DecoderStep::Preamble => {
if level {
// A HIGH while in preamble: either another preamble HIGH (~400µs) or a sync
// HIGH (also ~400µs) — disambiguated by the LOW that follows.
if in_range(duration, PREAMBLE_HIGH_US) {
self.te_last = duration;
} else {
self.step = DecoderStep::Reset;
}
} else {
// LOW following a preamble/sync HIGH.
if in_range(self.te_last, SYNC_HIGH_US) && in_range(duration, SYNC_LOW_US) {
// Sync gap (~9600µs LOW). Require enough preamble first.
if self.preamble_count >= PREAMBLE_MIN_PAIRS {
self.rx_bits = [0u8; FRAME_BITS];
self.decode_count_bit = 0;
self.step = DecoderStep::DataHigh;
} else {
self.step = DecoderStep::Reset;
}
} else if in_range(self.te_last, PREAMBLE_HIGH_US)
&& in_range(duration, PREAMBLE_LOW_US)
{
// Another preamble pair (~400µs HIGH + ~600µs LOW).
self.preamble_count = self.preamble_count.saturating_add(1);
} else {
self.step = DecoderStep::Reset;
}
}
}
DecoderStep::DataHigh => {
if level {
// Data-bit HIGH half — must match a Bit-1 or Bit-0 HIGH window.
if in_range(duration, BIT1_HIGH_US) || in_range(duration, BIT0_HIGH_US) {
self.te_last = duration;
self.step = DecoderStep::DataLow;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::DataLow => {
if !level {
let hi = self.te_last;
let lo = duration;
let bit = if in_range(hi, BIT1_HIGH_US) && in_range(lo, BIT1_LOW_US) {
Some(1u8)
} else if in_range(hi, BIT0_HIGH_US) && in_range(lo, BIT0_LOW_US) {
Some(0u8)
} else {
None
};
match bit {
Some(b) => {
// Store MSB-first: first bit on air → rx_bits[0] (= frame bit 65).
if self.decode_count_bit < FRAME_BITS {
self.rx_bits[self.decode_count_bit] = b;
}
self.decode_count_bit += 1;
if self.decode_count_bit == FRAME_BITS {
let result = Self::build_signal(&self.rx_bits);
self.reset();
return Some(result);
}
self.step = DecoderStep::DataHigh;
}
None => {
self.step = DecoderStep::Reset;
}
}
} else {
self.step = DecoderStep::Reset;
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
// Reconstruct the original frame to preserve the hop code, func bits, and status, then
// override only the button. The hop code lives in (data, extra); recover it.
let extra = decoded.extra.unwrap_or(0);
let orig_frame = Self::data_to_frame(decoded.data, extra);
let (hop_code, _serial, _orig_button, func_bits, status) = Self::unpack_frame(&orig_frame);
let btn = Self::map_button(button);
let frame = Self::pack_frame(hop_code, serial, btn, func_bits, status);
let mut signal = Vec::with_capacity(
((PREAMBLE_COUNT as usize + 1 + FRAME_BITS) * 2 + 1) * REPEAT_COUNT as usize,
);
for rep in 0..REPEAT_COUNT {
// Preamble: 20 pairs.
for _ in 0..PREAMBLE_COUNT {
Self::push_pair(&mut signal, PREAMBLE_HIGH_US, PREAMBLE_LOW_US);
}
// Sync.
Self::push_pair(&mut signal, SYNC_HIGH_US, SYNC_LOW_US);
// Data bits, MSB-first (frame bit 65 first on air).
for b in (0..FRAME_BITS).rev() {
if frame[b] != 0 {
Self::push_pair(&mut signal, BIT1_HIGH_US, BIT1_LOW_US);
} else {
Self::push_pair(&mut signal, BIT0_HIGH_US, BIT0_LOW_US);
}
}
// Inter-repetition gap.
if rep < REPEAT_COUNT - 1 {
signal.push(LevelDuration::new(false, REPEAT_GAP_US));
}
}
Some(signal)
}
}
impl Default for LandRoverRkeDecoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Feed an encoded signal back through a fresh decoder and return the first decode.
fn decode_signal(signal: &[LevelDuration]) -> Option<DecodedSignal> {
let mut dec = LandRoverRkeDecoder::new();
for ld in signal {
if let Some(d) = dec.feed(ld.level, ld.duration_us) {
return Some(d);
}
}
None
}
/// Build a DecodedSignal seed carrying a chosen hop code + serial (button overridden at encode).
fn seed(hop_code: u32, serial: u32, func_bits: u8, status: u8, button: u8) -> DecodedSignal {
let frame = LandRoverRkeDecoder::pack_frame(hop_code, serial & 0x00FF_FFFF, button, func_bits, status);
let (data, extra) = LandRoverRkeDecoder::frame_to_data(&frame);
DecodedSignal {
serial: Some(serial & 0x00FF_FFFF),
button: Some(button),
counter: Some((hop_code & 0xFFFF) as u16),
crc_valid: false,
data,
data_count_bit: FRAME_BITS,
encoder_capable: true,
extra: Some(extra),
protocol_display_name: None,
}
}
#[test]
fn pack_unpack_roundtrip() {
// Field packing/unpacking is exact across the full 66-bit layout.
let cases = [
(0xDEAD_BEEFu32, 0x00AB_CDEFu32, 0x1u8, BTN_UNLOCK, 0x5u8, 0x2u8),
(0x0000_0000, 0x0000_0000, 0x0, 0x0, 0x0, 0x0),
(0xFFFF_FFFF, 0x00FF_FFFF, 0xF, 0xF, 0xF, 0x3),
(0x1234_5678, 0x0055_AA55, 0x4, BTN_LOCK, 0xA, 0x1),
];
for &(hop, serial, _btn_field_unused, button, func, status) in &cases {
let frame = LandRoverRkeDecoder::pack_frame(hop, serial, button, func, status);
let (h, s, b, f, st) = LandRoverRkeDecoder::unpack_frame(&frame);
assert_eq!(h, hop, "hop_code mismatch");
assert_eq!(s, serial, "serial mismatch");
assert_eq!(b, button, "button mismatch");
assert_eq!(f, func, "func_bits mismatch");
assert_eq!(st, status, "status mismatch");
// (data, extra) <-> frame is lossless too.
let (data, extra) = LandRoverRkeDecoder::frame_to_data(&frame);
let frame2 = LandRoverRkeDecoder::data_to_frame(data, extra);
assert_eq!(frame, frame2, "data/extra round-trip mismatch");
}
}
#[test]
fn encode_decode_roundtrip_multiple() {
// Multiple serials / hop codes (counters) / buttons all round-trip through encode→decode.
let serials = [0x00ABCDEFu32, 0x00123456, 0x00000001, 0x00FFFFFE, 0x005A5A5A];
let hops = [0xDEADBEEFu32, 0x00000000, 0xFFFFFFFF, 0x12345678, 0xCAFEBABE];
let buttons = [0x01u8, 0x02, 0x04, 0x08]; // Lock, Unlock, Trunk/Boot, Panic
for (&serial, &hop) in serials.iter().zip(hops.iter()) {
for &btn in &buttons {
let s = seed(hop, serial, 0x3, 0x1, 0x00); // func/status preserved from seed
let encoder = LandRoverRkeDecoder::new();
let signal = encoder.encode(&s, btn).expect("encode should succeed");
let decoded = decode_signal(&signal)
.unwrap_or_else(|| panic!("decode failed for serial {serial:#X} hop {hop:#X} btn {btn:#X}"));
let expected_btn = LandRoverRkeDecoder::map_button(btn);
assert_eq!(decoded.serial, Some(serial & 0x00FF_FFFF), "serial");
assert_eq!(decoded.button, Some(expected_btn), "button");
// hop code survives via data+extra.
let frame = LandRoverRkeDecoder::data_to_frame(decoded.data, decoded.extra.unwrap());
let (dec_hop, _, _, dec_func, dec_status) = LandRoverRkeDecoder::unpack_frame(&frame);
assert_eq!(dec_hop, hop, "hop_code");
assert_eq!(dec_func, 0x3, "func_bits preserved");
assert_eq!(dec_status, 0x1, "status preserved");
assert_eq!(decoded.data_count_bit, FRAME_BITS, "bit count");
assert!(!decoded.crc_valid, "no key → crc_valid must be false");
}
}
}
#[test]
fn rejects_truncated_frame() {
// A single repetition carrying only 65 of the 66 bits must NOT decode.
let s = seed(0xDEADBEEF, 0x00ABCDEF, 0x0, 0x0, 0x02);
let encoder = LandRoverRkeDecoder::new();
let signal = encoder.encode(&s, 0x02).unwrap();
// First repetition is preamble (20 pairs) + sync (1 pair) + 66 bit-pairs. Keep only the
// first 65 bit-pairs so the frame is one bit short, and stop before the next repetition.
let bits_to_keep = FRAME_BITS - 1;
let partial_len = (PREAMBLE_COUNT as usize + 1 + bits_to_keep) * 2;
let partial = &signal[..partial_len];
assert!(decode_signal(partial).is_none(), "65-bit frame must not decode");
}
#[test]
fn rejects_wrong_sync_gap() {
// Same PWM bits but a too-short "sync" gap must not be accepted as a frame.
let s = seed(0x12345678, 0x00112233, 0x0, 0x0, 0x01);
let encoder = LandRoverRkeDecoder::new();
let mut signal = encoder.encode(&s, 0x01).unwrap();
// Corrupt the first sync LOW (index = PREAMBLE_COUNT*2 + 1) to a Bit-0-like LOW (700µs),
// which is far outside the 9600µs ±20% window.
let sync_low_idx = PREAMBLE_COUNT as usize * 2 + 1;
signal[sync_low_idx] = LevelDuration::new(false, 700);
// Take just the first repetition so the later (intact) reps don't rescue the decode.
let one_rep_len = (PREAMBLE_COUNT as usize + 1 + FRAME_BITS) * 2;
let partial = &signal[..one_rep_len.min(signal.len())];
assert!(decode_signal(partial).is_none(), "frame without valid 9.6ms sync must not decode");
}
}
+696
View File
@@ -0,0 +1,696 @@
//! Land Rover V0 protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/land_rover_v0.c` and
//! `land_rover_v0.h`. **Differential** Manchester (NOT the Flipper transition table): te_short 250,
//! te_long 500, te_delta 100, with a ~750µs sync pulse and a ≥64-pair short preamble. FM.
//!
//! Frame: 81 bits = an 80-bit body (`raw[0..10]`) plus one trailing `extra_bit`. The 64-bit key
//! reported as `DecodedSignal.data` is `raw[0..8]` big-endian; `raw[8..10]` is a 16-bit `tail`.
//! Field layout in the key bytes:
//! * bytes 0..3 → 24-bit command_signature (Lock = 0xC20363, Unlock = 0xA285E3)
//! * bytes 3..6 → 24-bit serial
//! * byte 6 + byte 7 MSB → 9-bit counter = `(b6 << 1) | (b7 >> 7)`
//! * byte 7 bits 0x78 → 3 reserved bits (must be 0)
//! * byte 7 bits 0x07 → 3-bit check
//! The check is a proprietary 3-bit polynomial over the counter (`calculate_check`); the tail is
//! 0xFFFF or 0x7FFF depending on a 1-bit parity of the counter (`calculate_tail`). Emission is gated
//! on the reserved bits being zero, the check matching, the tail matching, and `extra_bit` set —
//! so Land Rover V0 is strongly validated and will not false-match. Encoder supported.
//!
//! Decode steps: Reset → PreambleLow/PreambleHigh (count short pairs, ≥64) → SyncLow → Data.
//! The Data step ports the C `process_transition`/`add_decoded_bit` differential machine directly:
//! it tracks `previous_bit`, skips the initial boundary short-high pad (`boundary_pad_skipped`),
//! and completes short half-bits via `pending_short`. Bit 0 (= 1) is seeded on entry to Data.
//!
//! Note on the C encoder (faithfully ported): `build_upload` forces frame bit 1 = 0 regardless of
//! the key, so combined with the seeded bit 0 = 1 the top two frame bits are always `10`. The
//! Unlock signature 0xA285E3 satisfies this and round-trips cleanly; the Lock signature 0xC20363
//! (whose bit 1 is 1) is emitted as 0x820363 by the reference encoder. We reproduce that behaviour.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 250;
const TE_LONG: u32 = 500;
const TE_DELTA: u32 = 100;
const SYNC_US: u32 = 750;
const SYNC_DELTA_US: u32 = 120;
const MIN_PREAMBLE_PAIRS: u16 = 64;
const COUNT_BIT: usize = 81;
const GAP_US: u32 = 50_000;
/// TX preamble pair count (matches LAND_ROVER_V0_PREAMBLE_PAIRS).
const TX_PREAMBLE_PAIRS: usize = 319;
// Button signatures (LAND_ROVER_V0_SIG_*).
const SIG_UNLOCK: u32 = 0x00A2_85E3;
const SIG_LOCK: u32 = 0x00C2_0363;
// Land Rover button codes (LAND_ROVER_V0_BTN_*).
const LR_BTN_UNKNOWN: u8 = 0x00;
const LR_BTN_LOCK: u8 = 0x02;
const LR_BTN_UNLOCK: u8 = 0x04;
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
PreambleLow,
PreambleHigh,
SyncLow,
Data,
}
pub struct LandRoverV0Decoder {
step: DecoderStep,
preamble_count: u16,
raw: [u8; 10],
bit_count: u8,
extra_bit: bool,
previous_bit: bool,
boundary_pad_skipped: bool,
pending_short: bool,
}
impl LandRoverV0Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
preamble_count: 0,
raw: [0; 10],
bit_count: 0,
extra_bit: false,
previous_bit: true,
boundary_pad_skipped: false,
pending_short: false,
}
}
fn is_short(d: u32) -> bool {
duration_diff!(d, TE_SHORT) < TE_DELTA
}
fn is_long(d: u32) -> bool {
duration_diff!(d, TE_LONG) < TE_DELTA
}
fn is_sync(d: u32) -> bool {
duration_diff!(d, SYNC_US) < SYNC_DELTA_US
}
/// Reset the per-frame differential-Manchester state (matches the SyncLow init in the C feed).
fn begin_frame(&mut self) {
self.raw = [0; 10];
self.bit_count = 0;
self.extra_bit = false;
self.previous_bit = true;
self.boundary_pad_skipped = false;
self.pending_short = false;
}
/// Append one decoded bit (matches `land_rover_v0_add_decoded_bit`).
/// Bits 0..80 pack MSB-first into `raw`; bit 80 is the trailing `extra_bit`.
fn add_decoded_bit(&mut self, bit: bool) -> bool {
if self.bit_count < 80 {
let byte_index = (self.bit_count / 8) as usize;
let bit_index = 7 - (self.bit_count % 8);
if bit {
self.raw[byte_index] |= 1u8 << bit_index;
}
} else if self.bit_count == 80 {
self.extra_bit = bit;
} else {
return false;
}
self.bit_count += 1;
true
}
/// Differential-Manchester transition handler (direct port of `land_rover_v0_process_transition`).
fn process_transition(&mut self, level: bool, duration: u32) -> bool {
if !self.boundary_pad_skipped {
if level && Self::is_short(duration) {
self.boundary_pad_skipped = true;
return true;
}
self.boundary_pad_skipped = true;
}
if self.pending_short {
if !self.previous_bit && !level && Self::is_short(duration) {
self.pending_short = false;
return self.add_decoded_bit(false);
} else if self.previous_bit && level && Self::is_short(duration) {
self.pending_short = false;
return self.add_decoded_bit(true);
}
return false;
}
if !self.previous_bit {
if level && Self::is_long(duration) {
self.previous_bit = true;
return self.add_decoded_bit(true);
} else if level && Self::is_short(duration) {
self.pending_short = true;
return true;
}
return false;
}
if !level && Self::is_long(duration) {
self.previous_bit = false;
return self.add_decoded_bit(false);
} else if !level && Self::is_short(duration) {
self.pending_short = true;
return true;
}
false
}
/// 3-bit check polynomial over the 9-bit counter (matches `land_rover_v0_calculate_check`).
fn calculate_check(count: u32) -> u8 {
let c0 = ((count >> 1) ^ (count >> 2) ^ (count >> 3) ^ (count >> 4) ^ (count >> 6)) & 1;
let c1 = ((count >> 0)
^ (count >> 2)
^ (count >> 3)
^ (count >> 4)
^ (count >> 5)
^ (count >> 6)
^ 1)
& 1;
let c2 = ((count >> 1) ^ (count >> 3) ^ (count >> 4) ^ (count >> 5) ^ (count >> 6)) & 1;
(c0 | (c1 << 1) | (c2 << 2)) as u8
}
/// MSB selector for the 16-bit tail (matches `land_rover_v0_calculate_tail_msb`).
fn calculate_tail_msb(count: u32) -> bool {
(((count >> 0) ^ (count >> 2) ^ (count >> 4) ^ (count >> 5)) & 1) != 0
}
/// 16-bit tail value (matches `land_rover_v0_calculate_tail`).
fn calculate_tail(count: u32) -> u16 {
if Self::calculate_tail_msb(count) {
0xFFFF
} else {
0x7FFF
}
}
/// Map a 24-bit command signature to a Land Rover button (matches
/// `land_rover_v0_button_from_signature`).
fn button_from_signature(signature: u32) -> u8 {
match signature {
SIG_UNLOCK => LR_BTN_UNLOCK,
SIG_LOCK => LR_BTN_LOCK,
_ => LR_BTN_UNKNOWN,
}
}
/// Counter packed in key bytes 6 + 7-MSB (matches the C count extraction).
fn count_from_bytes(b: &[u8; 8]) -> u32 {
((b[6] as u32) << 1) | ((b[7] >> 7) & 1) as u32
}
/// Validate a frame (matches `land_rover_v0_validate_frame`).
/// Returns (check_ok, tail_ok).
fn validate_frame(key: u64, tail: u16, extra_bit: bool) -> (bool, bool) {
let b = key.to_be_bytes();
let count = Self::count_from_bytes(&b);
let expected_check = Self::calculate_check(count);
let expected_tail = Self::calculate_tail(count);
let check_ok = (b[7] & 0x78) == 0 && (b[7] & 0x07) == expected_check;
let tail_ok = tail == expected_tail && extra_bit;
(check_ok, tail_ok)
}
/// Finish a frame: validate, then build the decoded signal (matches
/// `land_rover_v0_finish_frame` + `parse_key_fields`). Returns None when invalid (gated).
fn finish_frame(&self) -> Option<DecodedSignal> {
let key = u64::from_be_bytes([
self.raw[0],
self.raw[1],
self.raw[2],
self.raw[3],
self.raw[4],
self.raw[5],
self.raw[6],
self.raw[7],
]);
let tail = ((self.raw[8] as u16) << 8) | self.raw[9] as u16;
let (check_ok, tail_ok) = Self::validate_frame(key, tail, self.extra_bit);
if !(check_ok && tail_ok) {
return None;
}
let b = key.to_be_bytes();
let signature = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
let serial = ((b[3] as u32) << 16) | ((b[4] as u32) << 8) | b[5] as u32;
let count = Self::count_from_bytes(&b);
let button = Self::button_from_signature(signature);
Some(DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(count as u16),
crc_valid: true, // check + tail + reserved-bits validated above
data: key,
data_count_bit: COUNT_BIT,
encoder_capable: true,
// Stash the 16-bit tail so the encoder can rebuild the full frame without recomputation.
extra: Some(tail as u64),
protocol_display_name: None,
})
}
/// Map KAT's generic button command to a Land Rover signature (Lock/Unlock).
/// KAT: Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08. Land Rover V0 only defines Lock/Unlock,
/// so Trunk/Panic have no signature and fall back to the decoded frame's signature in `encode`.
fn signature_from_button(button: u8) -> u32 {
match button {
0x01 => SIG_LOCK, // KAT Lock
0x02 => SIG_UNLOCK, // KAT Unlock
_ => 0,
}
}
/// Build the 64-bit key from fields (matches `land_rover_v0_build_key`).
fn build_key(signature: u32, serial: u32, count: u32) -> u64 {
let mut b = [0u8; 8];
b[0] = (signature >> 16) as u8;
b[1] = (signature >> 8) as u8;
b[2] = signature as u8;
b[3] = (serial >> 16) as u8;
b[4] = (serial >> 8) as u8;
b[5] = serial as u8;
b[6] = (count >> 1) as u8;
let counter_lsb = (count & 1) != 0;
let check = Self::calculate_check(count);
b[7] = (if counter_lsb { 0x80 } else { 0x00 }) | check;
u64::from_be_bytes(b)
}
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
if let Some(last) = signal.last_mut() {
if last.level == level {
*last = LevelDuration::new(level, last.duration_us + duration);
return;
}
}
signal.push(LevelDuration::new(level, duration));
}
/// Emit one differential-Manchester bit (matches `land_rover_v0_encoder_add_bit`).
/// Returns the new `previous_bit`.
fn enc_add_bit(signal: &mut Vec<LevelDuration>, previous_bit: bool, bit: bool) -> bool {
match (previous_bit, bit) {
(false, false) => {
Self::enc_add_level(signal, true, TE_SHORT);
Self::enc_add_level(signal, false, TE_SHORT);
}
(false, true) => {
Self::enc_add_level(signal, true, TE_LONG);
}
(true, false) => {
Self::enc_add_level(signal, false, TE_LONG);
}
(true, true) => {
Self::enc_add_level(signal, false, TE_SHORT);
Self::enc_add_level(signal, true, TE_SHORT);
}
}
bit
}
}
impl ProtocolDecoder for LandRoverV0Decoder {
fn name(&self) -> &'static str {
"Land Rover V0"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: COUNT_BIT,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.preamble_count = 0;
self.raw = [0; 10];
self.bit_count = 0;
self.extra_bit = false;
self.previous_bit = true;
self.boundary_pad_skipped = false;
self.pending_short = false;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if level && Self::is_short(duration) {
self.preamble_count = 0;
self.step = DecoderStep::PreambleLow;
}
}
DecoderStep::PreambleLow => {
if !level && Self::is_short(duration) {
self.preamble_count += 1;
self.step = DecoderStep::PreambleHigh;
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::PreambleHigh => {
if level && Self::is_short(duration) {
self.step = DecoderStep::PreambleLow;
} else if level
&& Self::is_sync(duration)
&& self.preamble_count >= MIN_PREAMBLE_PAIRS
{
self.step = DecoderStep::SyncLow;
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::SyncLow => {
if !level && Self::is_sync(duration) {
self.begin_frame();
self.add_decoded_bit(true); // seed bit 0 = 1
self.step = DecoderStep::Data;
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::Data => {
if !self.process_transition(level, duration) {
self.step = DecoderStep::Reset;
return None;
}
if self.bit_count as usize == COUNT_BIT {
let result = self.finish_frame();
self.step = DecoderStep::Reset;
return result;
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
// Derive fields from the decoded signal, preferring the requested button's signature.
let serial = decoded.serial? & 0x00FF_FFFF;
let count = (decoded.counter.unwrap_or(0) as u32) & 0x1FF;
// Pick the command signature: requested button first, else the decoded frame's signature.
let mut signature = Self::signature_from_button(button);
if signature == 0 {
let b = decoded.data.to_be_bytes();
signature = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
// If still not a known Land Rover signature, refuse (matches C: command_signature==0 → error).
if Self::button_from_signature(signature) == LR_BTN_UNKNOWN {
return None;
}
}
let key = Self::build_key(signature, serial, count);
let key_bytes = key.to_be_bytes();
let tail = Self::calculate_tail(count);
// Capacity: preamble pairs (2 levels) + sync (3) + ~81 bits (≤2 levels each) + gap.
let mut signal = Vec::with_capacity(TX_PREAMBLE_PAIRS * 2 + 3 + COUNT_BIT * 2 + 1);
// Preamble: alternating short high/low pairs.
for _ in 0..TX_PREAMBLE_PAIRS {
Self::enc_add_level(&mut signal, true, TE_SHORT);
Self::enc_add_level(&mut signal, false, TE_SHORT);
}
// Sync: high 750, low 750, then a boundary short-high pad (skipped by the decoder).
Self::enc_add_level(&mut signal, true, SYNC_US);
Self::enc_add_level(&mut signal, false, SYNC_US);
Self::enc_add_level(&mut signal, true, TE_SHORT);
// Differential-Manchester body. previous_bit starts true (bit 0 = 1 is implied by the sync
// trailing short). Bit index 1 is forced to 0 (matches the C build_upload), then bits 2..63
// come from the key bytes.
let mut previous_bit = true;
previous_bit = Self::enc_add_bit(&mut signal, previous_bit, false);
for bit_index in 2..64u8 {
let byte_index = (bit_index / 8) as usize;
let bit_in_byte = 7 - (bit_index % 8);
let bit = (key_bytes[byte_index] >> bit_in_byte) & 1 != 0;
previous_bit = Self::enc_add_bit(&mut signal, previous_bit, bit);
}
// 16-bit tail, MSB first.
for bit_index in 0..16u8 {
let bit = (tail >> (15 - bit_index)) & 1 != 0;
previous_bit = Self::enc_add_bit(&mut signal, previous_bit, bit);
}
// Trailing extra bit = 1.
let _ = Self::enc_add_bit(&mut signal, previous_bit, true);
// Inter-frame gap.
Self::enc_add_level(&mut signal, false, GAP_US);
Some(signal)
}
}
impl Default for LandRoverV0Decoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Drive a full encoded upload through the decoder and return the first decode.
fn decode_upload(upload: &[LevelDuration]) -> Option<DecodedSignal> {
let mut dec = LandRoverV0Decoder::new();
for ld in upload {
if let Some(sig) = dec.feed(ld.level, ld.duration_us) {
return Some(sig);
}
}
None
}
/// The 3-bit check polynomial matches the C reference for a couple of hand traces.
#[test]
fn check_polynomial_matches_reference() {
// count = 0: c0=0, c1=(0^1)&1=1, c2=0 → 0b010 = 2
assert_eq!(LandRoverV0Decoder::calculate_check(0), 0b010);
// Spot-check internal consistency: build_key embeds calculate_check in byte 7.
for count in 0..0x200u32 {
let key = LandRoverV0Decoder::build_key(SIG_UNLOCK, 0x123456, count);
let b = key.to_be_bytes();
assert_eq!(
b[7] & 0x07,
LandRoverV0Decoder::calculate_check(count),
"check embedded in key byte 7 must equal calculate_check(count) for count={count}"
);
assert_eq!(b[7] & 0x78, 0, "reserved bits must be zero for count={count}");
}
}
/// The tail is 0xFFFF or 0x7FFF per the counter parity, matching the reference.
#[test]
fn tail_matches_reference() {
for count in 0..0x200u32 {
let tail = LandRoverV0Decoder::calculate_tail(count);
assert!(tail == 0xFFFF || tail == 0x7FFF);
let msb_set = (tail >> 15) & 1 == 1;
assert_eq!(msb_set, LandRoverV0Decoder::calculate_tail_msb(count));
}
}
/// Primary correctness check: encode an Unlock frame and decode it back. The Unlock signature
/// 0xA285E3 satisfies the encoder's forced-bit-1 = 0 invariant, so it round-trips cleanly:
/// serial, button, counter, key and the 3-bit check all survive.
#[test]
fn unlock_round_trip_preserves_fields() {
let serial = 0x00AB_CDEF & 0x00FF_FFFF;
let counter = 0x123u16; // 9-bit counter
let key = LandRoverV0Decoder::build_key(SIG_UNLOCK, serial, counter as u32);
let decoded_in = DecodedSignal {
serial: Some(serial),
button: Some(LR_BTN_UNLOCK),
counter: Some(counter),
crc_valid: true,
data: key,
data_count_bit: COUNT_BIT,
encoder_capable: true,
extra: Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64),
protocol_display_name: None,
};
let dec = LandRoverV0Decoder::new();
// KAT Unlock command = 0x02.
let upload = dec.encode(&decoded_in, 0x02).expect("encode Unlock");
let out = decode_upload(&upload).expect("decode the encoded Unlock frame");
assert!(out.crc_valid, "decoded frame must pass check+tail gating");
assert_eq!(out.data, key, "64-bit key must survive the round trip");
assert_eq!(out.serial, Some(serial), "serial must survive");
assert_eq!(out.counter, Some(counter), "counter must survive");
assert_eq!(out.button, Some(LR_BTN_UNLOCK), "Unlock button must survive");
assert_eq!(out.data_count_bit, COUNT_BIT);
// The stashed tail must match the counter-derived tail.
assert_eq!(out.extra, Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64));
// The embedded 3-bit check must match the recomputed value.
let b = out.data.to_be_bytes();
assert_eq!(b[7] & 0x07, LandRoverV0Decoder::calculate_check(counter as u32));
assert_eq!(b[7] & 0x78, 0, "reserved bits zero");
}
/// Round-trip across many serial/counter values for Unlock.
#[test]
fn unlock_round_trip_many() {
let mut state: u32 = 0x1357_9BDF;
let mut next = || {
// xorshift for deterministic pseudo-random coverage
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
state
};
for _ in 0..256 {
let serial = next() & 0x00FF_FFFF;
let counter = (next() & 0x1FF) as u16;
let key = LandRoverV0Decoder::build_key(SIG_UNLOCK, serial, counter as u32);
let decoded_in = DecodedSignal {
serial: Some(serial),
button: Some(LR_BTN_UNLOCK),
counter: Some(counter),
crc_valid: true,
data: key,
data_count_bit: COUNT_BIT,
encoder_capable: true,
extra: Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64),
protocol_display_name: None,
};
let dec = LandRoverV0Decoder::new();
let upload = dec.encode(&decoded_in, 0x02).expect("encode");
let out = decode_upload(&upload)
.unwrap_or_else(|| panic!("decode failed for serial={serial:06X} counter={counter:03X}"));
assert_eq!(out.data, key);
assert_eq!(out.serial, Some(serial));
assert_eq!(out.counter, Some(counter));
assert_eq!(out.button, Some(LR_BTN_UNLOCK));
assert!(out.crc_valid);
}
}
/// Faithful-port note: the reference encoder forces frame bit 1 = 0. The Lock signature
/// 0xC20363 has bit 1 = 1, so the emitted frame's signature becomes 0x820363, which maps to
/// LR_BTN_UNKNOWN. We assert this exact reference behaviour rather than a "fixed" version.
#[test]
fn lock_signature_forced_bit_matches_reference_quirk() {
let serial = 0x0012_3456;
let counter = 100u16;
let key = LandRoverV0Decoder::build_key(SIG_LOCK, serial, counter as u32);
let decoded_in = DecodedSignal {
serial: Some(serial),
button: Some(LR_BTN_LOCK),
counter: Some(counter),
crc_valid: true,
data: key,
data_count_bit: COUNT_BIT,
encoder_capable: true,
extra: Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64),
protocol_display_name: None,
};
let dec = LandRoverV0Decoder::new();
// KAT Lock command = 0x01.
let upload = dec.encode(&decoded_in, 0x01).expect("encode Lock");
let out = decode_upload(&upload).expect("frame still decodes (check/tail valid)");
// Top byte 0xC2 -> 0x82 because bit 1 is forced to 0 by the encoder.
let b = out.data.to_be_bytes();
assert_eq!(b[0], 0x82, "encoder forces frame bit 1 = 0, turning 0xC2 into 0x82");
assert_eq!(out.button, Some(LR_BTN_UNKNOWN), "0x820363 is not a known signature");
// Serial / counter / check are still intact.
assert_eq!(out.serial, Some(serial));
assert_eq!(out.counter, Some(counter));
assert!(out.crc_valid);
}
/// A frame with a deliberately wrong check must NOT decode (gating prevents false matches).
#[test]
fn bad_check_is_rejected() {
let serial = 0x00AB_CDEF;
let counter = 0x055u16;
let mut key = LandRoverV0Decoder::build_key(SIG_UNLOCK, serial, counter as u32);
// Corrupt the 3-bit check in byte 7 (XOR a bit so it no longer matches).
key ^= 0x01;
let decoded_in = DecodedSignal {
serial: Some(serial),
button: Some(LR_BTN_UNLOCK),
counter: Some(counter),
crc_valid: false,
data: key,
data_count_bit: COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
};
// Hand-build an upload that transmits this corrupted key verbatim (no re-derivation):
// reuse the encoder's level helpers but bypass build_key by injecting the raw key.
let key_bytes = key.to_be_bytes();
let tail = LandRoverV0Decoder::calculate_tail(counter as u32);
let mut signal: Vec<LevelDuration> = Vec::new();
for _ in 0..TX_PREAMBLE_PAIRS {
LandRoverV0Decoder::enc_add_level(&mut signal, true, TE_SHORT);
LandRoverV0Decoder::enc_add_level(&mut signal, false, TE_SHORT);
}
LandRoverV0Decoder::enc_add_level(&mut signal, true, SYNC_US);
LandRoverV0Decoder::enc_add_level(&mut signal, false, SYNC_US);
LandRoverV0Decoder::enc_add_level(&mut signal, true, TE_SHORT);
let mut prev = true;
prev = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, false);
for bit_index in 2..64u8 {
let bi = (bit_index / 8) as usize;
let bib = 7 - (bit_index % 8);
let bit = (key_bytes[bi] >> bib) & 1 != 0;
prev = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, bit);
}
for bit_index in 0..16u8 {
let bit = (tail >> (15 - bit_index)) & 1 != 0;
prev = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, bit);
}
let _ = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, true);
LandRoverV0Decoder::enc_add_level(&mut signal, false, GAP_US);
// The decoded count comes from the (corrupted) byte 7, so the embedded check now mismatches
// calculate_check(count) → rejected. Note: corrupting bit 0 of the check does not change
// the counter (counter uses byte 7 MSB only), so calculate_check(count) stays the same.
let _ = decoded_in;
assert!(
decode_upload(&signal).is_none(),
"frame with a corrupted check must be rejected by the gate"
);
}
}
+525
View File
@@ -0,0 +1,525 @@
//! Mazda Siemens protocol decoder/encoder
//!
//! Ported from Flipper-ARF: `lib/subghz/protocols/mazda_siemens.c` / `mazda_siemens.h`
//! (`SUBGHZ_PROTOCOL_MAZDA_SIEMENS_NAME = "MazdaSiemens"`). This is the Siemens/VDO keyfob
//! cipher used on some Mazda vehicles — a DIFFERENT protocol from KAT's existing "Mazda V0"
//! (Pandora) decoder, even though both ride a 250/500µs pair-based stream at 433.92 MHz FM.
//!
//! Profile (matches the C const block):
//! - te_short = 250µs, te_long = 500µs, te_delta = 100µs, 64-bit frame.
//! - Pair-based decoder: `feed()` ignores `level` and interprets raw durations in pairs
//! (`process_pair`), collecting bits with *inverted* polarity (`state_bit == 0` → stored 1).
//! - Preamble: ≥13 short/short pairs, then a short→long transition starts data; the first
//! collected bit is a 1 (sync). A 14-byte buffer accumulates; on a non-matching pair the
//! frame is checked: discard the leading sync byte, take 8 bytes, deobfuscate, validate.
//! - Siemens obfuscation (the layer ported here, `mazda_xor_deobfuscate`):
//! parity = byte_parity(data[7]); odd → mask = data[6], XOR bytes 0..6;
//! even → mask = data[5], XOR bytes 0..5 and byte 6. Then bit-deinterleave bytes 5/6
//! via `(old5 & 0xAA)|(old6 & 0x55)` / `(old5 & 0x55)|(old6 & 0xAA)`.
//! The inner Siemens cipher's plaintext is left as-is (no key); only this obfuscation/
//! interleave layer is reversed, matching the C.
//! - Gate: additive checksum `sum(data[0..7]) == data[7]` (matches the C `mazda_check_completion`),
//! plus the structural preamble/sync/bit-count constraints. This is what keeps it from
//! false-matching other 250/500µs Manchester protocols.
//! - Fields (`mazda_parse_data`): serial = data >> 32, button = (data >> 24) & 0xFF,
//! counter = (data >> 8) & 0xFFFF. Button codes: 0x10 Lock, 0x20 Unlock, 0x40 Trunk.
//! - RF: FM (`SubGhzProtocolFlag_FM`); 433.92 MHz only (`SubGhzProtocolFlag_433`).
//!
//! Encoder (`subghz_protocol_encoder_mazda_siemens_get_upload`): increments the counter byte,
//! recomputes the checksum, bit-interleaves + XORs (`mazda_xor_obfuscate`), then emits a 12-byte
//! 0xFF preamble, a 50ms gap, `0xFF 0xFF 0xD7`, the 8 obfuscated bytes transmitted as `255 - byte`,
//! a `0x5A` tail byte, and a trailing 50ms gap. Manchester per byte: bit 1 → (H,L), bit 0 → (L,H),
//! all at te_short. Implemented faithfully in `encode()`.
//!
//! Note: as in the C, the decoder and encoder are NOT a clean raw-timing round-trip pair (the
//! decoder targets real fob air-format; the encoder generates a TX upload). The encode↔decode
//! unit test therefore validates the obfuscate/deobfuscate (XOR + interleave) cipher layer,
//! which is a true inverse.
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
use crate::radio::demodulator::LevelDuration;
const TE_SHORT: u32 = 250;
const TE_LONG: u32 = 500;
const TE_DELTA: u32 = 100;
const MIN_COUNT_BIT: usize = 64;
const PREAMBLE_MIN: u16 = 13;
const COMPLETION_MIN: u16 = 80;
const COMPLETION_MAX: u16 = 105;
const DATA_BUFFER_SIZE: usize = 14;
// Encoder constants (mazda_siemens.c).
const TX_PREAMBLE_BYTES: usize = 12;
const TX_GAP_US: u32 = 50_000;
const TX_SYNC_BYTE: u8 = 0xD7;
const TX_TAIL_BYTE: u8 = 0x5A;
/// Button codes carried in the frame (mazda_get_btn_name in the C).
const BTN_LOCK: u8 = 0x10;
const BTN_UNLOCK: u8 = 0x20;
const BTN_TRUNK: u8 = 0x40;
/// Decoder states (matches mazda_siemens.c MazdaSiemensDecoderStep).
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
PreambleSave,
PreambleCheck,
DataSave,
DataCheck,
}
/// Mazda Siemens protocol decoder.
pub struct MazdaSiemensDecoder {
step: DecoderStep,
te_last: u32,
preamble_count: u16,
bit_counter: u16,
prev_state: u8,
data_buffer: [u8; DATA_BUFFER_SIZE],
}
impl MazdaSiemensDecoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
preamble_count: 0,
bit_counter: 0,
prev_state: 0,
data_buffer: [0u8; DATA_BUFFER_SIZE],
}
}
#[inline]
fn is_short(duration: u32) -> bool {
duration_diff!(duration, TE_SHORT) < TE_DELTA
}
#[inline]
fn is_long(duration: u32) -> bool {
duration_diff!(duration, TE_LONG) < TE_DELTA
}
/// Collect one bit into the buffer with inverted polarity (mazda_collect_bit).
/// `state_bit == 0` stores a 1.
fn collect_bit(&mut self, state_bit: u8) {
let byte_idx = (self.bit_counter >> 3) as usize;
if byte_idx < DATA_BUFFER_SIZE {
self.data_buffer[byte_idx] <<= 1;
if state_bit == 0 {
self.data_buffer[byte_idx] |= 1;
}
}
self.bit_counter += 1;
}
/// Process a duration pair (mazda_process_pair). Returns true if the pair was valid.
fn process_pair(&mut self, dur_first: u32, dur_second: u32) -> bool {
let first_short = Self::is_short(dur_first);
let first_long = Self::is_long(dur_first);
let second_short = Self::is_short(dur_second);
let second_long = Self::is_long(dur_second);
if first_long && second_short {
self.collect_bit(0);
self.collect_bit(1);
self.prev_state = 1;
return true;
}
if first_short && second_long {
self.collect_bit(1);
self.prev_state = 0;
return true;
}
if first_short && second_short {
let ps = self.prev_state;
self.collect_bit(ps);
return true;
}
if first_long && second_long {
self.collect_bit(0);
self.collect_bit(1);
self.prev_state = 0;
return true;
}
false
}
/// Validate a complete frame (mazda_check_completion). On success returns a DecodedSignal.
fn check_completion(&self) -> Option<DecodedSignal> {
if self.bit_counter < COMPLETION_MIN || self.bit_counter > COMPLETION_MAX {
return None;
}
// Shift buffer by 1 byte (discard the sync/header byte).
let mut data = [0u8; 8];
for i in 0..8 {
data[i] = self.data_buffer[i + 1];
}
Self::xor_deobfuscate(&mut data);
// Additive checksum: sum(data[0..7]) must equal data[7].
let mut checksum: u8 = 0;
for &b in data.iter().take(7) {
checksum = checksum.wrapping_add(b);
}
if checksum != data[7] {
return None;
}
// Pack into u64 (big-endian byte order).
let packed = u64::from_be_bytes(data);
let (serial, button, counter) = Self::parse_fields(packed);
Some(DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid: true,
data: packed,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
})
}
/// Field extraction (mazda_parse_data).
fn parse_fields(packed: u64) -> (u32, u8, u16) {
let serial = (packed >> 32) as u32;
let button = ((packed >> 24) & 0xFF) as u8;
let counter = ((packed >> 8) & 0xFFFF) as u16;
(serial, button, counter)
}
/// Byte parity: XOR-fold to a single bit (mazda_byte_parity).
fn byte_parity(mut val: u8) -> u8 {
val ^= val >> 4;
val ^= val >> 2;
val ^= val >> 1;
val & 1
}
/// Siemens RX deobfuscation (mazda_xor_deobfuscate):
/// parity-dependent XOR mask, then deinterleave bytes 5/6.
fn xor_deobfuscate(data: &mut [u8; 8]) {
let parity = Self::byte_parity(data[7]);
if parity != 0 {
// Odd parity: mask = byte[6], XOR bytes 0..6.
let mask = data[6];
for i in 0..6 {
data[i] ^= mask;
}
} else {
// Even parity: mask = byte[5], XOR bytes 0..5 and byte[6].
let mask = data[5];
for i in 0..5 {
data[i] ^= mask;
}
data[6] ^= mask;
}
// Bit deinterleave bytes 5/6.
let old5 = data[5];
let old6 = data[6];
data[5] = (old5 & 0xAA) | (old6 & 0x55);
data[6] = (old5 & 0x55) | (old6 & 0xAA);
}
/// Siemens TX obfuscation (mazda_xor_obfuscate): interleave bytes 5/6, then
/// parity-dependent XOR mask. Inverse of `xor_deobfuscate`.
fn xor_obfuscate(data: &mut [u8; 8]) {
let old5 = data[5];
let old6 = data[6];
data[5] = (old5 & 0xAA) | (old6 & 0x55);
data[6] = (old5 & 0x55) | (old6 & 0xAA);
let parity = Self::byte_parity(data[7]);
if parity != 0 {
let mask = data[6];
for i in 0..6 {
data[i] ^= mask;
}
} else {
let mask = data[5];
for i in 0..5 {
data[i] ^= mask;
}
data[6] ^= mask;
}
}
/// Button name for display (mazda_get_btn_name).
#[allow(dead_code)]
fn get_button_name(btn: u8) -> &'static str {
match btn {
BTN_LOCK => "Lock",
BTN_UNLOCK => "Unlock",
BTN_TRUNK => "Trunk",
_ => "Unknown",
}
}
/// Map KAT's generic button command to a Mazda Siemens frame button code.
fn map_button(button: u8) -> u8 {
match button {
0x01 => BTN_LOCK, // Lock
0x02 => BTN_UNLOCK, // Unlock
0x04 => BTN_TRUNK, // Trunk
BTN_LOCK | BTN_UNLOCK | BTN_TRUNK => button, // already a frame code
_ => BTN_UNLOCK,
}
}
/// Encode one byte as Manchester, MSB-first: bit 1 → (H,L), bit 0 → (L,H) (mazda_encode_byte).
fn enc_byte(signal: &mut Vec<LevelDuration>, byte: u8) {
for bit in (0..8).rev() {
if (byte >> bit) & 1 != 0 {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
} else {
signal.push(LevelDuration::new(false, TE_SHORT));
signal.push(LevelDuration::new(true, TE_SHORT));
}
}
}
}
impl ProtocolDecoder for MazdaSiemensDecoder {
fn name(&self) -> &'static str {
"Mazda Siemens"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: MIN_COUNT_BIT,
}
}
fn supported_frequencies(&self) -> &[u32] {
// SubGhzProtocolFlag_433 only.
&[433_920_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.te_last = 0;
self.preamble_count = 0;
self.bit_counter = 0;
self.prev_state = 0;
self.data_buffer = [0u8; DATA_BUFFER_SIZE];
}
fn feed(&mut self, _level: bool, duration: u32) -> Option<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
{
// Preamble → data: seed the leading sync bit (a 1).
self.bit_counter = 1;
self.data_buffer = [0u8; DATA_BUFFER_SIZE];
self.collect_bit(1);
self.prev_state = 0;
self.step = DecoderStep::DataSave;
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::DataSave => {
self.te_last = duration;
self.step = DecoderStep::DataCheck;
}
DecoderStep::DataCheck => {
if self.process_pair(self.te_last, duration) {
self.step = DecoderStep::DataSave;
} else {
let result = self.check_completion();
self.step = DecoderStep::Reset;
if result.is_some() {
return result;
}
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
// Rebuild the 8 cleartext bytes from the decoded 64-bit word.
let mut data = decoded.data.to_be_bytes();
// Apply the requested button into the frame's button field (byte index 3 = data >> 24).
data[3] = Self::map_button(button);
// Increment the counter byte (mazda_siemens.c get_upload): data[6]++ with carry into data[5].
let (new6, carry) = data[6].overflowing_add(1);
data[6] = new6;
if carry {
data[5] = data[5].wrapping_add(1);
}
// Recompute the additive checksum over bytes 0..7.
let mut checksum: u8 = 0;
for &b in data.iter().take(7) {
checksum = checksum.wrapping_add(b);
}
data[7] = checksum;
// Obfuscate (interleave + XOR) for transmission.
let mut tx_data = data;
Self::xor_obfuscate(&mut tx_data);
// Build the upload: 12x 0xFF preamble, gap, 0xFF 0xFF, sync 0xD7,
// 8 data bytes transmitted inverted (255 - byte), tail 0x5A, trailing gap.
let mut signal: Vec<LevelDuration> = Vec::with_capacity((TX_PREAMBLE_BYTES + 12) * 16 + 4);
for _ in 0..TX_PREAMBLE_BYTES {
Self::enc_byte(&mut signal, 0xFF);
}
signal.push(LevelDuration::new(false, TX_GAP_US));
Self::enc_byte(&mut signal, 0xFF);
Self::enc_byte(&mut signal, 0xFF);
Self::enc_byte(&mut signal, TX_SYNC_BYTE);
for &b in tx_data.iter() {
Self::enc_byte(&mut signal, 255 - b);
}
Self::enc_byte(&mut signal, TX_TAIL_BYTE);
signal.push(LevelDuration::new(false, TX_GAP_US));
Some(signal)
}
}
impl Default for MazdaSiemensDecoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The obfuscate/deobfuscate (XOR + bit-interleave) layer is a true inverse for every
/// valid cleartext (checksum byte fixes the parity branch). This is the cipher-layer
/// round-trip the decoder relies on; validates the ported Siemens obfuscation.
#[test]
fn xor_layer_round_trips() {
// Deterministic LCG over many cleartexts: 7 data bytes + matching additive checksum.
let mut seed: u32 = 0x1234_5678;
let mut next = || {
seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
(seed >> 16) as u8
};
for _ in 0..20_000 {
let mut clear = [0u8; 8];
let mut sum: u8 = 0;
for b in clear.iter_mut().take(7) {
*b = next();
sum = sum.wrapping_add(*b);
}
clear[7] = sum;
let mut buf = clear;
MazdaSiemensDecoder::xor_obfuscate(&mut buf);
MazdaSiemensDecoder::xor_deobfuscate(&mut buf);
assert_eq!(buf, clear, "obfuscate→deobfuscate must be identity");
}
}
/// Deobfuscate is the exact inverse of obfuscate for both parity branches (odd: byte7
/// has odd popcount; even: zero). Pins the parity-dependent mask selection.
#[test]
fn xor_both_parity_branches() {
// Even-parity checksum byte (0x00 → parity 0): mask = byte[5].
let mut even = [0x11u8, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x00];
let orig_even = even;
MazdaSiemensDecoder::xor_obfuscate(&mut even);
MazdaSiemensDecoder::xor_deobfuscate(&mut even);
assert_eq!(even, orig_even);
assert_eq!(MazdaSiemensDecoder::byte_parity(0x00), 0);
// Odd-parity checksum byte (0x01 → parity 1): mask = byte[6].
let mut odd = [0xAAu8, 0xBB, 0xCC, 0xDD, 0xEE, 0x12, 0x34, 0x01];
let orig_odd = odd;
MazdaSiemensDecoder::xor_obfuscate(&mut odd);
MazdaSiemensDecoder::xor_deobfuscate(&mut odd);
assert_eq!(odd, orig_odd);
assert_eq!(MazdaSiemensDecoder::byte_parity(0x01), 1);
}
/// Field extraction matches mazda_parse_data: serial = >>32, button = >>24, counter = >>8.
#[test]
fn parse_fields_layout() {
let packed: u64 = 0x1234_5678_2000_06_3A;
let (serial, button, counter) = MazdaSiemensDecoder::parse_fields(packed);
assert_eq!(serial, 0x1234_5678);
assert_eq!(button, 0x20);
assert_eq!(counter, 0x0006);
}
/// The encoder produces a non-empty Manchester upload with the expected leading 0xFF
/// preamble (all alternating short pulses) and a 50ms gap. Smoke test of the TX path.
#[test]
fn encode_produces_upload() {
let dec = MazdaSiemensDecoder::new();
let signal = DecodedSignal {
serial: Some(0x1234_5678),
button: Some(0x02),
counter: Some(0x0005),
crc_valid: true,
data: 0x1234_5678_2000_05_00,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
};
let up = dec.encode(&signal, 0x02).expect("encode should succeed");
assert!(!up.is_empty());
// First 16 pulses are the first 0xFF preamble byte: alternating H/L shorts.
for (i, p) in up.iter().take(16).enumerate() {
assert_eq!(p.duration_us, TE_SHORT);
assert_eq!(p.level, i % 2 == 0, "0xFF Manchester alternates H,L starting high");
}
// A 50ms gap is present somewhere in the upload.
assert!(up.iter().any(|p| p.duration_us == TX_GAP_US));
}
}
+281
View File
@@ -32,8 +32,14 @@ mod kia_v2;
mod kia_v3_v4; mod kia_v3_v4;
mod kia_v5; mod kia_v5;
mod kia_v6; mod kia_v6;
mod kia_v7;
mod subaru; mod subaru;
mod ford_v0; mod ford_v0;
mod ford_v1;
mod ford_v2;
mod ford_v3;
mod honda_static;
mod honda_v1;
mod vag; mod vag;
mod fiat_v0; mod fiat_v0;
mod fiat_v1; mod fiat_v1;
@@ -41,9 +47,17 @@ mod suzuki;
mod scher_khan; mod scher_khan;
mod star_line; mod star_line;
mod psa; mod psa;
mod psa2;
mod chrysler_v0;
mod mazda_v0; mod mazda_v0;
mod mazda_siemens;
mod mitsubishi_v0; mod mitsubishi_v0;
mod porsche_touareg; mod porsche_touareg;
mod porsche_cayenne;
mod bmw_cas4;
mod land_rover_v0;
mod land_rover_rke;
mod toyota;
pub use common::DecodedSignal; pub use common::DecodedSignal;
@@ -108,9 +122,20 @@ impl ProtocolRegistry {
Box::new(kia_v3_v4::KiaV3V4Decoder::new()), Box::new(kia_v3_v4::KiaV3V4Decoder::new()),
Box::new(kia_v5::KiaV5Decoder::new()), Box::new(kia_v5::KiaV5Decoder::new()),
Box::new(kia_v6::KiaV6Decoder::new()), Box::new(kia_v6::KiaV6Decoder::new()),
Box::new(kia_v7::KiaV7Decoder::new()),
// VAG before Ford/Subaru so 500/1000µs VAG streams decode as VAG (ProtoPirate order has VAG after Ford/Subaru but Flipper likely feeds all decoders; KAT uses first-match so VAG must be tried earlier) // VAG before Ford/Subaru so 500/1000µs VAG streams decode as VAG (ProtoPirate order has VAG after Ford/Subaru but Flipper likely feeds all decoders; KAT uses first-match so VAG must be tried earlier)
Box::new(vag::VagDecoder::new()), Box::new(vag::VagDecoder::new()),
Box::new(ford_v0::FordV0Decoder::new()), Box::new(ford_v0::FordV0Decoder::new()),
// Ford V1: distinct 65/130µs Manchester (vs V0 250/500, V2 200/400, V3 240/480) and
// CRC16-gated, so placement in the Ford group is safe (won't steal V0/V2/V3 captures).
Box::new(ford_v1::FordV1Decoder::new()),
Box::new(ford_v3::FordV3Decoder::new()),
Box::new(ford_v2::FordV2Decoder::new()),
// Honda Static after the Kia/Ford block; checksum-gated emission makes order safe.
Box::new(honda_static::HondaStaticDecoder::new()),
// Honda V1: distinct 1000/2000µs PWM (vs Honda Static's 63µs Manchester). Button-gated
// emission makes order safe.
Box::new(honda_v1::HondaV1Decoder::new()),
Box::new(subaru::SubaruDecoder::new()), Box::new(subaru::SubaruDecoder::new()),
Box::new(fiat_v0::FiatV0Decoder::new()), Box::new(fiat_v0::FiatV0Decoder::new()),
Box::new(fiat_v1::FiatV1Decoder::new()), Box::new(fiat_v1::FiatV1Decoder::new()),
@@ -119,9 +144,61 @@ impl ProtocolRegistry {
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()),
// PSA2 (Flipper-ARF, internal name "PSA OLD"): the OLDER PSA variant. Manchester
// 250/500µs (or 125/250µs half-rate), 128-bit frame (key1 64 + validation 16), AM/OOK,
// TEA cipher with a mode23 XOR fast-path validated by a nibble checksum (the live
// decoder runs ONLY this O(1) path — the dual TEA brute force is reserved for offline
// decrypt and never runs in feed(), exactly as in the C). Placed AFTER `psa` so the
// existing PSA keeps first-match priority and PSA2 acts as the older fallback. Emission
// is gated on the XOR checksum (or validation nibble == 0xA), so it false-matches
// nothing else.
Box::new(psa2::Psa2Decoder::new()),
// Chrysler V0 (300/3400-3700µs PWM, dual-long symbols). Timing is unique and emission
// is gated on the frame's own structural check, so placement here is low-risk.
Box::new(chrysler_v0::ChryslerV0Decoder::new()),
// Land Rover V0 (differential Manchester 250/500µs, 81 bits). Emission is gated on a
// 3-bit check polynomial + 16-bit tail + zero reserved bits, so placement is safe.
Box::new(land_rover_v0::LandRoverV0Decoder::new()),
// Land Rover RKE (Flipper-ARF): fixed-width PWM (700/300µs bits), 20-pulse preamble,
// a distinctive 400µs+9600µs sync gap, and exactly 66 bits. KeeLoq hop is left
// encrypted (no key), so emission is gated on the preamble + 9.6ms sync + 66-bit
// strict-PWM geometry — unique among KAT protocols, so it false-matches nothing.
Box::new(land_rover_rke::LandRoverRkeDecoder::new()),
Box::new(mazda_v0::MazdaV0Decoder::new()), Box::new(mazda_v0::MazdaV0Decoder::new()),
// Mazda Siemens (Flipper-ARF): the Siemens/VDO keyfob cipher. It rides the SAME
// 250/500µs pair-based stream as Mazda V0 and uses the same additive-checksum gate,
// so the two would match an identical set of frames. Placed AFTER mazda_v0 so Mazda
// V0 keeps first-match priority and Mazda Siemens can never steal its captures.
// Emission is gated on the structural preamble/sync/bit-count + checksum, so it does
// not false-match other 250/500µs Manchester protocols. Adds an encoder (Mazda V0
// has none).
Box::new(mazda_siemens::MazdaSiemensDecoder::new()),
// BMW CAS4 (Flipper-ARF): Manchester 500/1000µs, 64-bit, AM. The CAS4 rolling cipher's
// manufacturer key is unavailable, so the payload is left encrypted — emission is gated
// on the two fixed marker bytes (byte[0]==0x30 && byte[6]==0xC5), which is unique and
// makes it false-match nothing. Decode-only (the reference encoder is a stub). Marker-
// gated, so registry order here is safe.
Box::new(bmw_cas4::BmwCas4Decoder::new()),
Box::new(mitsubishi_v0::MitsubishiV0Decoder::new()), Box::new(mitsubishi_v0::MitsubishiV0Decoder::new()),
Box::new(porsche_touareg::PorscheTouaregDecoder::new()), Box::new(porsche_touareg::PorscheTouaregDecoder::new()),
// Porsche Cayenne (Flipper-ARF, internal name "Porsche AG"): the SAME wire protocol
// as Porsche Touareg — Touareg is itself a port of this very porsche_cayenne.c source,
// so they share the identical 1680/3370µs PWM, 73-pulse preamble + 5930µs gap, 64-bit
// MSB-first frame, 24-bit VAG rotating-register cipher, and counter-recovery validity
// gate. Placed AFTER porsche_touareg so Touareg keeps first-match priority and Cayenne
// can NEVER steal a Touareg capture (the registry reports the first decoder that fires
// on a given pulse). Cayenne adds the encoder (the Touareg port is decode-only) and is
// additionally gated on the frame_type being one of the three values the C emits/labels
// (0b001/0b010/0b100) — a strict subset of what Touareg accepts — so on any shared
// frame Touareg also fires and wins by ordering.
Box::new(porsche_cayenne::PorscheCayenneDecoder::new()),
// Toyota/Lexus (Flipper-ARF): dual-variant KeeLoq. Variant A is KeeLoq-PWM at 433 MHz
// and shares its air encoding with Kia V3/V4 — so Toyota MUST stay AFTER kia_v3_v4
// (which is in the Kia block near the top) to preserve Kia's first-match priority on
// the shared PWM frames. Toyota uniquely claims 60-bit frames and the Variant-B NRZ
// stream at 315 MHz. Emission is gated on the exact frame bit count + structural
// preamble/sync + non-zero serial, so it false-matches nothing else.
Box::new(toyota::ToyotaDecoder::new()),
]; ];
Self { decoders } Self { decoders }
@@ -343,4 +420,208 @@ mod tests {
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>() results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
); );
} }
#[test]
fn ford_v3_decodes_ldv_t80_sub() {
// LDV T80 keyfobs use a Ford-V3-style 240/480µs Manchester frame.
let path = Path::new("IMPORTS/LDV/LDV-T80_lock.sub");
if !path.exists() {
eprintln!("Skip: {:?} not found (run from crate root)", path);
return;
}
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
let pairs: Vec<LevelDuration> = raw_pairs
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
let mut reg = ProtocolRegistry::new();
let results = reg.process_signal_stream(&pairs, freq);
assert!(
results.iter().any(|(n, _, _)| n == "Ford V3"),
"expected at least one Ford V3 decode from LDV-T80_lock.sub, got: {:?}",
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
);
}
#[test]
fn honda_static_decodes_unlock_honda_sub() {
// Genuine Honda keyfob capture (CC1101 custom preset). Honda Static unpacks it via the
// reverse-packet path (matches honda_static.c), yielding a consistent serial/counter.
let path = Path::new("IMPORTS/honda/Unlock_honda.sub");
if !path.exists() {
eprintln!("Skip: {:?} not found (run from crate root)", path);
return;
}
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
let pairs: Vec<LevelDuration> = raw_pairs
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
let mut reg = ProtocolRegistry::new();
let results = reg.process_signal_stream(&pairs, freq);
assert!(
results.iter().any(|(n, _, _)| n == "Honda Static"),
"expected at least one Honda Static decode from Unlock_honda.sub, got: {:?}",
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
);
}
#[test]
fn toyota_decodes_camry_variant_b_sub() {
// 312 MHz Toyota Camry capture: Variant-B NRZ frame that Kia V3/V4 (and every other
// decoder) rejects, so Toyota uniquely claims it. Guards the Toyota port + its registry
// placement (after kia_v3_v4).
let path = Path::new("IMPORTS/Toyota + Lexus/19_toyota_camry_l2_u2_t2_p2.sub");
if !path.exists() {
eprintln!("Skip: {:?} not found (run from crate root)", path);
return;
}
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
let pairs: Vec<LevelDuration> = raw_pairs
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
let mut reg = ProtocolRegistry::new();
let results = reg.process_signal_stream(&pairs, freq);
assert!(
results.iter().any(|(n, _, _)| n == "Toyota"),
"expected at least one Toyota decode from 19_toyota_camry_l2_u2_t2_p2.sub, got: {:?}",
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
);
}
#[test]
fn toyota_does_not_steal_kia_v3_v4_prius_sub() {
// The Prius 433 MHz captures are KeeLoq-PWM frames whose air encoding is shared between
// Toyota Variant A and Kia V3/V4. Kia V3/V4 is earlier in the registry and MUST keep
// first-match priority — so these must still decode as "Kia V3/V4", never "Toyota".
let path = Path::new("IMPORTS/Toyota + Lexus/Toyota_Prius2006_lock.sub");
if !path.exists() {
eprintln!("Skip: {:?} not found (run from crate root)", path);
return;
}
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
let pairs: Vec<LevelDuration> = raw_pairs
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
let mut reg = ProtocolRegistry::new();
let results = reg.process_signal_stream(&pairs, freq);
let names: Vec<&str> = results.iter().map(|(n, _, _)| n.as_str()).collect();
assert!(
names.iter().any(|n| *n == "Kia V3/V4"),
"expected Kia V3/V4 decodes from Toyota_Prius2006_lock.sub, got: {:?}",
names
);
assert!(
!names.iter().any(|n| *n == "Toyota"),
"Toyota must NOT steal the shared KeeLoq-PWM Prius frames from Kia V3/V4, got: {:?}",
names
);
}
#[test]
fn psa2_decodes_groupe_psa_sub() {
// Genuine Peugeot/Citroën keyfob captures. PSA2 ("PSA OLD") decodes them via the mode23
// XOR fast path (nibble-checksum gated), recovering a stable serial 0x99EB25 with a rolling
// counter. Guards the PSA2 port + its registry placement (after `psa`).
let path = Path::new("IMPORTS/GROUPE PSA/PSA_523_536.sub");
if !path.exists() {
eprintln!("Skip: {:?} not found (run from crate root)", path);
return;
}
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
let pairs: Vec<LevelDuration> = raw_pairs
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
let mut reg = ProtocolRegistry::new();
let results = reg.process_signal_stream(&pairs, freq);
let psa2: Vec<_> = results.iter().filter(|(n, _, _)| n == "PSA2").collect();
assert!(
!psa2.is_empty(),
"expected at least one PSA2 decode from PSA_523_536.sub, got: {:?}",
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
);
// The decode must carry recovered fields (serial), not a bare undecrypted frame.
assert!(
psa2.iter().any(|(_, d, _)| d.serial == Some(0x99EB25) && d.crc_valid),
"expected a decrypted PSA2 frame with serial 0x99EB25, got serials: {:?}",
psa2.iter().map(|(_, d, _)| d.serial).collect::<Vec<_>>()
);
}
#[test]
fn psa2_does_not_steal_vag_captures() {
// PSA2 rides the same 250/500µs Manchester rate as several other protocols and the C's
// mode23 nibble checksum is a loose ~1/16 gate. Emission is therefore restricted to a
// field-bearing decrypt, so PSA2 must NOT match these VAG captures (which carry no valid
// PSA2 frame) even though they reach 80 collectable bits at a PSA2-compatible frequency.
for rel in ["VAG/Test_55_unlock_and_55_lock_suran.sub", "VAG/Vw pasat B7.sub"] {
let path = Path::new("IMPORTS").join(rel);
if !path.exists() {
eprintln!("Skip: {:?} not found (run from crate root)", path);
continue;
}
let (freq, raw_pairs) = import_sub_raw(&path).unwrap();
let pairs: Vec<LevelDuration> = raw_pairs
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
let mut reg = ProtocolRegistry::new();
let results = reg.process_signal_stream(&pairs, freq);
let names: Vec<&str> = results.iter().map(|(n, _, _)| n.as_str()).collect();
assert!(
!names.iter().any(|n| *n == "PSA2"),
"PSA2 must not false-match {rel}, got: {names:?}"
);
}
}
/// Diagnostic: sweep every IMPORTS/*.sub and print which protocol(s) each decodes as.
/// `#[ignore]`d (noisy, always passes); run with
/// `cargo test sweep_imports_decodes -- --ignored --nocapture`.
#[test]
#[ignore]
fn sweep_imports_decodes() {
use std::fs;
let root = Path::new("IMPORTS");
if !root.exists() {
eprintln!("Skip: IMPORTS not found");
return;
}
// Collect IMPORTS/<make>/*.sub
let mut subs: Vec<std::path::PathBuf> = Vec::new();
if let Ok(makes) = fs::read_dir(root) {
for make in makes.flatten() {
if make.path().is_dir() {
if let Ok(files) = fs::read_dir(make.path()) {
for f in files.flatten() {
let p = f.path();
if p.extension().map(|e| e == "sub").unwrap_or(false) {
subs.push(p);
}
}
}
}
}
}
subs.sort();
let mut reg = ProtocolRegistry::new();
for path in &subs {
match import_sub_raw(path) {
Ok((freq, raw_pairs)) => {
let pairs: Vec<LevelDuration> = raw_pairs
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
let results = reg.process_signal_stream(&pairs, freq);
let names: Vec<&str> = results.iter().map(|(n, _, _)| n.as_str()).collect();
let rel = path.strip_prefix("IMPORTS").unwrap_or(path);
eprintln!("{:<42} {:>9}Hz => {:?}", rel.display(), freq, names);
}
Err(e) => eprintln!("{:<42} ERR {:?}", path.display(), e),
}
}
}
} }
+613
View File
@@ -0,0 +1,613 @@
//! Porsche Cayenne protocol decoder/encoder
//!
//! Aligned with Flipper-ARF reference: `lib/subghz/protocols/porsche_cayenne.c`
//! (internal protocol name in the firmware header is "Porsche AG").
//!
//! # Relationship to the existing `Porsche Touareg` decoder
//!
//! KAT already ships `porsche_touareg.rs`, which is itself a port of this very same
//! `porsche_cayenne.c` source (its own doc comment and the `porsche_cayenne_compute_frame`
//! function name make that explicit). The two protocols are therefore **the same wire
//! protocol**: identical 1680/3370µs PWM, identical 73-pulse 3370µs preamble + 5930µs gap
//! pair, identical 64-bit MSB-first frame, identical 24-bit rotating-register VAG cipher,
//! and identical brute-force counter recovery / validity check (`counter != 0`).
//!
//! The only meaningful differences captured here versus Touareg:
//! * Display name is `"Porsche Cayenne"` (vs `"Porsche Touareg"`).
//! * This decoder ADDS the encoder from the C reference (the Touareg port is decode-only):
//! a 4-frame burst (frame types 0b010/0b001/0b100/0b100), 73 sync pairs + gap pair +
//! 64 MSB-first PWM data bits per frame.
//!
//! Because the two share an identical frame and validity gate, this decoder is registered
//! AFTER `porsche_touareg` so Touareg keeps first-match priority and Cayenne can never steal
//! a Touareg capture (the registry reports the first decoder that fires on a given pulse —
//! see `process_signal_*_inner` in `mod.rs`). To keep Cayenne a faithful but strictly
//! non-stealing decoder, emission is additionally gated on the frame_type being one of the
//! three values the C only ever emits/labels (`0b001` Cont, `0b010` First, `0b100` Final).
//! That is a strict subset of what Touareg accepts, so on any shared frame Touareg fires too
//! and wins by ordering; Cayenne only ever fires on frames Touareg also accepts.
//!
//! Protocol characteristics:
//! - PWM bit pairs: SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1
//! - 64 bits total; sync preamble of 15+ LOW/HIGH pairs at 3370µs, then 5930µs gap pair, then data
//! - Field layout: pkt[0]=(btn<<4)|(frame_type&0x07), pkt[1..3]=serial 24-bit, pkt[4..7]=encrypted
//! - Counter recovery via brute-force matching of computed encrypted bytes against received bytes
//! - Frame types: 0x02="First", 0x01="Cont", 0x04="Final"
//! - RF: AM/OOK. Frequencies: 433.92 MHz and 868.35 MHz (C flags 433|868)
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
use crate::radio::demodulator::LevelDuration;
const TE_SHORT: u32 = 1680;
const TE_LONG: u32 = 3370;
const TE_DELTA: u32 = 500;
const MIN_COUNT_BIT: usize = 64;
const PC_TE_SYNC: u32 = 3370;
const PC_TE_GAP: u32 = 5930;
const PC_SYNC_MIN: u16 = 15;
/// Actual preamble pulse-pair count emitted by the firmware (PC_SYNC_COUNT).
const PC_SYNC_COUNT: usize = 73;
/// KAT generic button codes (Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08).
const BTN_LOCK: u8 = 0x01;
const BTN_UNLOCK: u8 = 0x02;
const BTN_TRUNK: u8 = 0x04;
const BTN_PANIC: u8 = 0x08;
/// Decoder states (matches PCDecoderStep in porsche_cayenne.c).
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Sync,
GapHigh,
GapLow,
Data,
}
/// Porsche Cayenne protocol decoder/encoder.
pub struct PorscheCayenneDecoder {
step: DecoderStep,
sync_count: u16,
raw_data: u64,
bit_count: usize,
te_last: u32,
}
/// Circular left-shift of a 24-bit register stored in three bytes (h, m, l).
///
/// Each byte shifts left by 1, receiving the MSB of the next byte in the chain:
/// h gets MSB of m, m gets MSB of l, l gets MSB of h (wrap-around).
///
/// Matches the ROTATE24 macro in porsche_cayenne.c exactly.
#[inline]
fn rotate24(r_h: &mut u8, r_m: &mut u8, r_l: &mut u8) {
let ch = (*r_h >> 7) & 1;
let cm = (*r_m >> 7) & 1;
let cl = (*r_l >> 7) & 1;
*r_h = (*r_h << 1) | cm;
*r_m = (*r_m << 1) | cl;
*r_l = (*r_l << 1) | ch;
}
/// Compute an 8-byte frame from serial, button, counter, and frame_type.
///
/// Direct port of `porsche_cayenne_compute_frame` from the C reference. The cipher
/// increments `counter` by 1 internally. pkt[0..3] = plaintext header, pkt[4..7] = cipher
/// output derived from a 24-bit rotate register seeded from serial bytes and rotated
/// (4 + counter_low) times.
fn compute_frame(serial24: u32, btn: u8, counter: u16, frame_type: u8) -> [u8; 8] {
let b0 = (btn << 4) | (frame_type & 0x07);
let b1 = ((serial24 >> 16) & 0xFF) as u8;
let b2 = ((serial24 >> 8) & 0xFF) as u8;
let b3 = (serial24 & 0xFF) as u8;
// Internal counter increment (firmware @ 0x14122).
let cnt = counter.wrapping_add(1);
let cnt_lo = (cnt & 0xFF) as u8;
let cnt_hi = ((cnt >> 8) & 0xFF) as u8;
// Seed 24-bit register: r_h <- serial LSB, r_m <- serial MSB, r_l <- serial mid.
let mut r_h = b3;
let mut r_m = b1;
let mut r_l = b2;
// Loop 1: 4 fixed rotations.
for _ in 0..4 {
rotate24(&mut r_h, &mut r_m, &mut r_l);
}
// Loop 2: cnt_lo additional rotations.
for _ in 0..cnt_lo as u16 {
rotate24(&mut r_h, &mut r_m, &mut r_l);
}
// 9A: XOR of r_h with base byte.
let a9a = r_h ^ b0;
// 9B: three masked slices of (~cnt_lo / ~cnt_hi) XOR r_m.
let nb9b_p1 = ((!cnt_lo).wrapping_shl(2) & 0xFC) ^ r_m;
let nb9b_p2 = ((!cnt_hi).wrapping_shl(2) & 0xFC) ^ r_m;
let nb9b_p3 = ((!cnt_hi).wrapping_shr(6) & 0x03) ^ r_m;
let a9b = (nb9b_p1 & 0xCC) | (nb9b_p2 & 0x30) | (nb9b_p3 & 0x03);
// 9C: three masked slices of (~cnt_lo / ~cnt_hi) XOR r_l.
let nb9c_p1 = ((!cnt_lo).wrapping_shr(2) & 0x3F) ^ r_l;
let nb9c_p2 = ((!cnt_hi & 0x03).wrapping_shl(6)) ^ r_l;
let nb9c_p3 = ((!cnt_hi).wrapping_shr(2) & 0x3F) ^ r_l;
let a9c = (nb9c_p1 & 0x33) | (nb9c_p2 & 0xC0) | (nb9c_p3 & 0x0C);
let mut pkt = [0u8; 8];
pkt[0] = b0;
pkt[1] = b1;
pkt[2] = b2;
pkt[3] = b3;
pkt[4] = ((a9a >> 2) & 0x3F) | ((!cnt_lo & 0x03) << 6);
pkt[5] = (!cnt_lo & 0xC0) | ((a9a & 0x03) << 4) | (a9b & 0x0C) | ((!cnt_lo).wrapping_shr(2) & 0x03);
pkt[6] = ((a9b & 0x03) << 6) | ((a9c >> 2) & 0x3C) | ((!cnt_lo).wrapping_shr(4) & 0x03);
pkt[7] = ((a9b >> 4) & 0x0F) | ((a9c & 0x0F) << 4);
pkt
}
/// Unpack a raw 64-bit frame into its 8 bytes (big-endian: pkt[0] is the MSB).
fn unpack_bytes(data: u64) -> [u8; 8] {
let mut pkt = [0u8; 8];
let mut raw = data;
for i in (0..8).rev() {
pkt[i] = (raw & 0xFF) as u8;
raw >>= 8;
}
pkt
}
/// Brute-force counter recovery: try counter values 1..=256 (matching the C loop) and
/// compare the recomputed cipher bytes pkt[4..7]. Returns 0 if no match (invalid frame).
fn recover_counter(serial: u32, btn: u8, frame_type: u8, pkt: &[u8; 8]) -> u16 {
for try_cnt in 1u16..=256 {
// The cipher increments internally, so pass try_cnt - 1.
let try_pkt = compute_frame(serial, btn, try_cnt - 1, frame_type);
if try_pkt[4] == pkt[4]
&& try_pkt[5] == pkt[5]
&& try_pkt[6] == pkt[6]
&& try_pkt[7] == pkt[7]
{
return try_cnt;
}
}
0
}
/// Parse raw 64-bit data into a DecodedSignal, or `None` if it is not a valid Cayenne frame.
///
/// Cayenne-specific gate (keeps this decoder from stealing Touareg captures): the frame_type
/// must be one of the three values the C reference emits/labels (0b001 Cont, 0b010 First,
/// 0b100 Final), AND the counter must be recoverable (cipher-consistent, `counter != 0`).
fn parse_data(data: u64) -> Option<DecodedSignal> {
let pkt = unpack_bytes(data);
let serial = ((pkt[1] as u32) << 16) | ((pkt[2] as u32) << 8) | (pkt[3] as u32);
let btn = pkt[0] >> 4;
let frame_type = pkt[0] & 0x07;
// Cayenne-specific invariant: only the three documented frame types.
let frame_type_name = match frame_type {
0b010 => "First",
0b001 => "Cont",
0b100 => "Final",
_ => return None,
};
// Cipher consistency check (also the C's validity signal).
let counter = recover_counter(serial, btn, frame_type, &pkt);
if counter == 0 {
return None;
}
Some(DecodedSignal {
serial: Some(serial),
button: Some(btn),
counter: Some(counter),
crc_valid: true, // cipher-consistent frame
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: Some(frame_type as u64),
protocol_display_name: Some(format!("Porsche Cayenne [{}]", frame_type_name)),
})
}
impl PorscheCayenneDecoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
sync_count: 0,
raw_data: 0,
bit_count: 0,
te_last: 0,
}
}
/// Map a KAT generic button command to a Porsche Cayenne / VAG 4-bit button code.
/// KAT: Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08.
/// The C `porsche_cayenne_get_btn_code` maps d-pad: Up=0x01 Lock, Down=0x02 Unlock,
/// Left=0x04 Trunk, Right=0x08 Open — an identical set, so the codes pass through.
fn map_button(button: u8) -> u8 {
match button {
BTN_LOCK | BTN_UNLOCK | BTN_TRUNK | BTN_PANIC => button & 0x0F,
b => b & 0x0F,
}
}
/// Append one PWM data bit as a (LOW, HIGH) pair, matching the C encoder:
/// bit 0: SHORT LOW + LONG HIGH; bit 1: LONG LOW + SHORT HIGH.
fn push_bit(signal: &mut Vec<LevelDuration>, bit: bool) {
if bit {
signal.push(LevelDuration::new(false, TE_LONG));
signal.push(LevelDuration::new(true, TE_SHORT));
} else {
signal.push(LevelDuration::new(false, TE_SHORT));
signal.push(LevelDuration::new(true, TE_LONG));
}
}
/// Emit one full frame (73 sync pairs + gap pair + 64 MSB-first data bits) into `signal`.
/// Matches `porsche_cayenne_build_upload`'s per-frame body.
fn push_frame(signal: &mut Vec<LevelDuration>, pkt: &[u8; 8]) {
// Preamble: 73 × (LOW LONG + HIGH LONG).
for _ in 0..PC_SYNC_COUNT {
signal.push(LevelDuration::new(false, TE_LONG));
signal.push(LevelDuration::new(true, TE_LONG));
}
// Gap: LOW GAP + HIGH GAP.
signal.push(LevelDuration::new(false, PC_TE_GAP));
signal.push(LevelDuration::new(true, PC_TE_GAP));
// 64 data bits, MSB first, byte order pkt[0]->pkt[7].
for &byte in pkt.iter() {
for bit in (0..8).rev() {
Self::push_bit(signal, (byte >> bit) & 1 != 0);
}
}
}
}
impl ProtocolDecoder for PorscheCayenneDecoder {
fn name(&self) -> &'static str {
"Porsche Cayenne"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: MIN_COUNT_BIT,
}
}
fn supported_frequencies(&self) -> &[u32] {
// C flags: SubGhzProtocolFlag_433 | SubGhzProtocolFlag_868.
&[433_920_000, 868_350_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.sync_count = 0;
self.raw_data = 0;
self.bit_count = 0;
self.te_last = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<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 {
// Good sync HIGH — keep collecting.
} else if self.sync_count >= PC_SYNC_MIN
&& duration_diff!(duration, PC_TE_GAP) < TE_DELTA
{
// HIGH gap after sufficient sync pulses.
self.step = DecoderStep::GapLow;
} else {
self.step = DecoderStep::Reset;
}
} else {
// LOW pulse.
if duration_diff!(duration, PC_TE_SYNC) < TE_DELTA {
self.sync_count += 1;
} else if self.sync_count >= PC_SYNC_MIN
&& duration_diff!(duration, PC_TE_GAP) < TE_DELTA
{
// LOW gap after sufficient sync pulses.
self.step = DecoderStep::GapHigh;
} else {
self.step = DecoderStep::Reset;
}
}
}
// GapHigh: expect the complementary HIGH gap pulse.
DecoderStep::GapHigh => {
if level && duration_diff!(duration, PC_TE_GAP) < TE_DELTA {
self.raw_data = 0;
self.bit_count = 0;
self.step = DecoderStep::Data;
} else {
self.step = DecoderStep::Reset;
}
}
// GapLow: expect the complementary LOW gap pulse.
DecoderStep::GapLow => {
if !level && duration_diff!(duration, PC_TE_GAP) < TE_DELTA {
self.raw_data = 0;
self.bit_count = 0;
self.step = DecoderStep::Data;
} else {
self.step = DecoderStep::Reset;
}
}
// Data: decode bit pairs.
// LOW pulses are saved in te_last; HIGH pulses complete the bit:
// SHORT LOW + LONG HIGH = bit 0
// LONG LOW + SHORT HIGH = bit 1
DecoderStep::Data => {
if level {
let bit_value;
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA
&& duration_diff!(duration, TE_LONG) < TE_DELTA
{
bit_value = false; // bit 0
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA
&& duration_diff!(duration, TE_SHORT) < TE_DELTA
{
bit_value = true; // bit 1
} else {
self.step = DecoderStep::Reset;
return None;
}
self.raw_data = (self.raw_data << 1) | (bit_value as u64);
self.bit_count += 1;
if self.bit_count >= MIN_COUNT_BIT {
let data = self.raw_data;
self.step = DecoderStep::Reset;
// parse_data returns None if the Cayenne gate fails (not a Cayenne
// frame), so this decoder stays silent on those and never steals them.
return parse_data(data);
}
} else {
// LOW pulse: save duration for the bit pair.
self.te_last = duration;
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial? & 0xFFFFFF;
let cnt = decoded.counter.unwrap_or(0);
let btn = Self::map_button(button);
// 4-frame burst (matches porsche_cayenne_build_upload):
// Frame 0: frame_type=0b010, cipher counter = cnt+1
// Frame 1: frame_type=0b001, cipher counter = cnt+2
// Frame 2: frame_type=0b100, cipher counter = cnt+3
// Frame 3: frame_type=0b100, cipher counter = cnt+4
// (compute_frame increments the passed counter by 1 internally.)
const FRAME_TYPES: [u8; 4] = [0b010, 0b001, 0b100, 0b100];
// Per-frame size: 73 sync pairs + 1 gap pair + 64 bit pairs = (73 + 1 + 64) * 2.
let mut signal = Vec::with_capacity((PC_SYNC_COUNT + 1 + 64) * 2 * 4);
for (f, &ft) in FRAME_TYPES.iter().enumerate() {
let pkt = compute_frame(serial, btn, cnt.wrapping_add(f as u16), ft);
Self::push_frame(&mut signal, &pkt);
}
Some(signal)
}
}
impl Default for PorscheCayenneDecoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Feed an encoded signal back through a fresh decoder and return the first decode.
fn decode_signal(signal: &[LevelDuration]) -> Option<DecodedSignal> {
let mut dec = PorscheCayenneDecoder::new();
for ld in signal {
if let Some(d) = dec.feed(ld.level, ld.duration_us) {
return Some(d);
}
}
None
}
/// Build a one-frame on-air signal for a single (serial, btn, counter, frame_type) and
/// decode it back. The frame's cipher counter is `counter+1` (compute_frame increments),
/// and the decoder's brute-force recovery reports that incremented value.
fn roundtrip_single(serial: u32, btn: u8, counter: u16, frame_type: u8) -> DecodedSignal {
let pkt = compute_frame(serial, btn, counter, frame_type);
let mut signal = Vec::new();
PorscheCayenneDecoder::push_frame(&mut signal, &pkt);
decode_signal(&signal)
.unwrap_or_else(|| panic!("decode failed for serial {serial:#X} btn {btn:#X} cnt {counter} ft {frame_type:#b}"))
}
#[test]
fn encode_decode_roundtrip_via_encoder() {
// Full encoder path: encode() emits a 4-frame burst; the first frame (type 0b010,
// cipher counter = cnt+1) must decode back with the right serial/button, and the
// recovered counter must survive the rolling cipher (== cnt+1).
let serials = [0x00ABCDEFu32, 0x00123456, 0x00000001, 0x00FFFFFE, 0x005A5A5A];
let buttons = [BTN_LOCK, BTN_UNLOCK, BTN_TRUNK, BTN_PANIC];
// Counters kept within the C reference's recoverable range: its decoder only
// brute-forces cnt_lo (256 values), so the first burst frame's cipher counter
// (seed+1) must be <= 256, i.e. seed <= 255. See `counter_recovery_limit_matches_c`.
let counters = [0u16, 1, 42, 200, 254, 255];
for (&serial, &counter) in serials.iter().zip(counters.iter()) {
for &btn in &buttons {
let seed = DecodedSignal {
serial: Some(serial),
button: Some(btn),
counter: Some(counter),
crc_valid: true,
data: 0,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
};
let encoder = PorscheCayenneDecoder::new();
let signal = encoder.encode(&seed, btn).expect("encode should succeed");
let decoded = decode_signal(&signal).unwrap_or_else(|| {
panic!("decode failed for serial {serial:#X} btn {btn:#X} cnt {counter}")
});
let expected_btn = PorscheCayenneDecoder::map_button(btn);
assert_eq!(decoded.serial, Some(serial & 0xFFFFFF), "serial");
assert_eq!(decoded.button, Some(expected_btn), "button");
// First frame's cipher counter is cnt+1 (compute_frame increments).
assert_eq!(
decoded.counter,
Some(counter.wrapping_add(1)),
"counter must survive the rolling cipher"
);
assert_eq!(decoded.data_count_bit, MIN_COUNT_BIT, "bit count");
assert!(decoded.crc_valid, "cipher-consistent frame → crc_valid");
// First burst frame is type 0b010 = "First".
assert_eq!(decoded.extra, Some(0b010), "frame_type First");
assert_eq!(
decoded.protocol_display_name.as_deref(),
Some("Porsche Cayenne [First]")
);
}
}
}
#[test]
fn roundtrip_all_frame_types() {
// Each of the three documented frame types decodes and labels correctly, and the
// counter survives across the cipher for a spread of counter values.
let cases = [
(0b010u8, "Porsche Cayenne [First]"),
(0b001u8, "Porsche Cayenne [Cont]"),
(0b100u8, "Porsche Cayenne [Final]"),
];
for &(ft, name) in &cases {
// Counters within the C's recoverable cnt_lo range (cipher counter seed+1 <= 256).
for &counter in &[0u16, 7, 200, 254, 255] {
let d = roundtrip_single(0x00C0FFEE, BTN_UNLOCK, counter, ft);
assert_eq!(d.serial, Some(0x00C0FFEE), "serial ft={ft:#b}");
assert_eq!(d.button, Some(BTN_UNLOCK), "button ft={ft:#b}");
assert_eq!(
d.counter,
Some(counter.wrapping_add(1)),
"counter survives cipher ft={ft:#b} cnt={counter}"
);
assert_eq!(d.extra, Some(ft as u64), "frame_type ft={ft:#b}");
assert_eq!(d.protocol_display_name.as_deref(), Some(name));
assert!(d.crc_valid);
}
}
}
#[test]
fn rejects_undocumented_frame_type() {
// A structurally valid PWM frame whose frame_type is NOT one of {0b001,0b010,0b100}
// must be rejected by Cayenne's gate — this is what keeps it from stealing arbitrary
// 1680/3370µs PWM frames (and what makes Touareg, ordered first, the owner of any
// shared frame). frame_type 0b011 is unused by the C reference.
let pkt = compute_frame(0x00123456, BTN_LOCK, 5, 0b011);
let mut signal = Vec::new();
PorscheCayenneDecoder::push_frame(&mut signal, &pkt);
assert!(
decode_signal(&signal).is_none(),
"undocumented frame_type 0b011 must not decode as Cayenne"
);
}
#[test]
fn rejects_truncated_frame() {
// 63 of 64 data bits must not decode.
let pkt = compute_frame(0x00ABCDEF, BTN_LOCK, 3, 0b010);
let mut signal = Vec::new();
PorscheCayenneDecoder::push_frame(&mut signal, &pkt);
// Frame = 73 sync pairs + 1 gap pair + 64 bit pairs. Drop the last bit pair.
let keep = (PC_SYNC_COUNT + 1 + 63) * 2;
assert!(
decode_signal(&signal[..keep]).is_none(),
"63-bit frame must not decode"
);
}
#[test]
fn counter_recovery_limit_matches_c() {
// Faithful limitation of the C reference: its decoder brute-forces only cnt_lo
// (try_cnt 1..=256), so frames whose cipher counter (passed counter + 1) exceeds 256
// cannot have their counter recovered. recover_counter returns 0 for those, and the
// frame is reported invalid (does not decode) — exactly as the C would behave.
// seed=255 -> cipher counter 256 -> recoverable; seed=256 -> 257 -> NOT recoverable.
let pkt_ok = compute_frame(0x00ABCDEF, BTN_LOCK, 255, 0b010);
assert_eq!(
recover_counter(0x00ABCDEF, BTN_LOCK, 0b010, &pkt_ok),
256,
"cipher counter 256 is the highest recoverable value"
);
let pkt_oob = compute_frame(0x00ABCDEF, BTN_LOCK, 256, 0b010);
assert_eq!(
recover_counter(0x00ABCDEF, BTN_LOCK, 0b010, &pkt_oob),
0,
"cipher counter 257 is beyond the C's cnt_lo brute-force range"
);
// And such an out-of-range frame must not decode (parse_data rejects counter==0).
let mut signal = Vec::new();
PorscheCayenneDecoder::push_frame(&mut signal, &pkt_oob);
assert!(
decode_signal(&signal).is_none(),
"frame with unrecoverable counter must not decode (matches C)"
);
}
#[test]
fn cipher_matches_touareg_reference_vectors() {
// The cipher/frame is shared with the Touareg port (same C source). Spot-check that
// compute_frame -> unpack header bytes are exactly as laid out by the C reference:
// pkt[0]=(btn<<4)|ft, pkt[1..3]=serial big-endian.
let pkt = compute_frame(0x00ABCDEF, 0x02, 0x10, 0b010);
assert_eq!(pkt[0], (0x02 << 4) | 0b010, "pkt[0] = (btn<<4)|ft");
assert_eq!(pkt[1], 0xAB, "serial MSB");
assert_eq!(pkt[2], 0xCD, "serial mid");
assert_eq!(pkt[3], 0xEF, "serial LSB");
// Recovering the counter from this exact frame yields cnt+1 = 0x11.
let cnt = recover_counter(0x00ABCDEF, 0x02, 0b010, &pkt);
assert_eq!(cnt, 0x11, "brute-force counter recovery = passed counter + 1");
}
}
+919
View File
@@ -0,0 +1,919 @@
//! PSA2 (Peugeot/Citroën — "PSA OLD") protocol decoder/encoder
//!
//! Aligned with Flipper-ARF reference: `lib/subghz/protocols/psa2.c` and `psa2.h`
//! (internal name `SUBGHZ_PROTOCOL_PSA2_NAME = "PSA OLD"`). This is the OLDER PSA variant,
//! distinct from KAT's existing `psa` (modified-TEA/XEA) decoder.
//!
//! Protocol characteristics:
//! - Manchester encoding: 250/500µs symbol (Pattern 1, standard rate) or 125/250µs (Pattern 2,
//! half rate). Canonical Flipper `manchester_advance` table (events ShortLow=0, ShortHigh=2,
//! LongLow=4, LongHigh=6), seeded `ManchesterStateMid1`.
//! - 128-bit frame = key1 (64 bits) + key2/validation word. The decoder collects 64 bits → key1,
//! then 16 more (to 80 bits = `KEY2_BITS`) → the 16-bit validation field / key2_low.
//! - RF: AM (OOK). Frequency: 433.92 MHz.
//! - Crypto: TEA (Tiny Encryption Algorithm) with a dual brute-force fallback (BF1
//! 0x230000000x24000000, BF2 0xF30000000xF4000000) and a mode23/mode36 selector, validated
//! via a nibble checksum.
//!
//! ## Decoder structure & PERFORMANCE
//! The C live decoder (`subghz_protocol_decoder_psa2_feed`) only ever runs the cheap mode23 XOR
//! path (`psa_decrypt_fast`) per frame — it NEVER runs the TEA brute force. The brute force
//! (`psa_decrypt_full`, marked `__attribute__((unused))`) is reserved for the deferred-decrypt
//! UI button, not the streaming decoder. KAT mirrors this exactly. Per frame:
//!
//! 1. Manchester-collect to exactly 80 bits (key1 + validation) with end-of-packet detection.
//! 2. Run the O(1) `direct_xor_decrypt` (mode23) gated on its nibble checksum.
//! 3. Emit only on a successful, field-bearing decrypt (see `finalize_frame` for why the C's bare
//! `(validation & 0xF) == 0xA` emission is not reproduced in KAT's feed-all model).
//!
//! The bounded TEA brute force (BF1/BF2, ~16.7M iters each) is ported faithfully in
//! `Psa2Decoder::decrypt_full` but is NEVER called from `feed()` — only the cheap O(1) gate runs
//! per pulse, so the test sweep stays fast.
//!
//! Decoder steps: State0 (wait preamble) → State1/State3 (count preamble pulses) →
//! State2/State4 (Manchester decode + decrypt). Encoder supported (mode23 path).
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
use crate::radio::demodulator::LevelDuration;
// Standard-rate timings (Pattern 1)
const TE_SHORT: u32 = 250;
const TE_LONG: u32 = 500;
const TE_DELTA: u32 = 100;
const MIN_COUNT_BIT: usize = 128;
// Half-rate timings (Pattern 2 / State 3-4)
const TE_SHORT_HALF: u32 = 125;
const TE_LONG_HALF: u32 = 250;
const TOL_HALF: u32 = 50;
// End-of-packet markers
const TE_END_1000: u32 = 1000;
const TE_END_500: u32 = 500;
// Bit counts
const KEY1_BITS: usize = 64; // 0x40
const KEY2_BITS: usize = 80; // 0x50
const MAX_BITS: usize = 121; // 0x79
// Preamble pulse-count thresholds (C: PSA_PATTERN_THRESHOLD_1/2)
const PATTERN_THRESHOLD_1: u16 = 0x46;
const PATTERN_THRESHOLD_2: u16 = 0x45;
// Validation nibble for the mode23 path: (validation_field & 0xF) == 0xA
const VALID_NIBBLE: u16 = 0xA;
// "decrypted" success marker (C uses 0x50)
const DECRYPTED_OK: u16 = 0x50;
// Mode selectors (stored as ASCII chars in firmware)
const MODE_23: u8 = 0x23; // '#'
const MODE_36: u8 = 0x36; // '6'
// PSA2 button codes are Lock=0, Unlock=1, Trunk=2 (psa_button_name). Decodes whose 4-bit button
// exceeds this are coincidental matches on unrelated Manchester data and are rejected.
const BTN_MAX_VALID: u8 = 0x2;
// TEA constants
const TEA_DELTA: u32 = 0x9E3779B9;
const TEA_ROUNDS: u32 = 32;
// BF1 brute-force range + constants (FUN_08028f94 / FUN_080291c0)
const BF1_START: u32 = 0x2300_0000;
const BF1_END: u32 = 0x2400_0000;
const BF1_CONST_U4: u32 = 0x0E0F_5C41;
const BF1_CONST_U5: u32 = 0x0F5C_4123;
const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A43_4915, 0xD674_3C2B, 0x1F29_D308, 0xE6B7_9A64];
// BF2 brute-force range + key schedule (FUN_080290f8)
const BF2_START: u32 = 0xF300_0000;
const BF2_END: u32 = 0xF400_0000;
const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039_C240, 0xEDA9_2CAB, 0x4306_C02A, 0x0219_2A04];
/// Canonical Flipper Manchester states (lib/toolbox/manchester_decoder.c order:
/// Start1=0, Mid1=1, Mid0=2, Start0=3).
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Start1 = 0,
Mid1 = 1,
Mid0 = 2,
Start0 = 3,
}
/// Decoder states (matches PSADecoderState0-4 in psa2.c).
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderState {
/// State0: wait for preamble start.
WaitEdge,
/// State1: count standard-rate (250µs) preamble pulses.
CountPattern250,
/// State2: receive key1 + key2/validation at standard rate.
DecodeManchester250,
/// State3: count half-rate (125µs) preamble pulses.
CountPattern125,
/// State4: receive key1 + key2/validation at half rate.
DecodeManchester125,
}
/// Result of a successful decrypt: (serial, button, counter, crc, type).
type DecryptResult = (u32, u8, u32, u16, u8);
/// PSA2 ("PSA OLD") protocol decoder.
pub struct Psa2Decoder {
state: DecoderState,
prev_duration: u32,
manchester_state: ManchesterState,
pattern_counter: u16,
data_low: u32,
data_high: u32,
bit_count: usize,
// Decoded fields
key1_low: u32,
key1_high: u32,
validation_field: u16,
key2_low: u32,
key2_high: u32,
}
impl Psa2Decoder {
pub fn new() -> Self {
Self {
state: DecoderState::WaitEdge,
prev_duration: 0,
manchester_state: ManchesterState::Mid1,
pattern_counter: 0,
data_low: 0,
data_high: 0,
bit_count: 0,
key1_low: 0,
key1_high: 0,
validation_field: 0,
key2_low: 0,
key2_high: 0,
}
}
fn near(dur: u32, target: u32, tol: u32) -> bool {
duration_diff!(dur, target) <= tol
}
// =========================================================================
// Manchester — canonical Flipper transition table (transitions[] in
// manchester_decoder.c). `event` ∈ {0,2,4,6}; returns Some(bit) on emit.
// =========================================================================
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
const TRANSITIONS: [u8; 4] = [0b0000_0001, 0b1001_0001, 0b1001_1011, 0b1111_1011];
let state_idx = self.manchester_state as usize;
let new_idx = (TRANSITIONS[state_idx] >> event) & 0x3;
let new_state = match new_idx {
0 => ManchesterState::Start1,
1 => ManchesterState::Mid1,
2 => ManchesterState::Mid0,
_ => ManchesterState::Start0,
};
if new_idx as usize == state_idx {
// No progress → reset to Mid1, emit nothing.
self.manchester_state = ManchesterState::Mid1;
return None;
}
self.manchester_state = new_state;
match new_state {
ManchesterState::Mid0 => Some(false),
ManchesterState::Mid1 => Some(true),
_ => None,
}
}
fn manchester_reset(&mut self) {
self.manchester_state = ManchesterState::Mid1;
}
/// Shift one decoded bit into the 64-bit accumulator (data_high:data_low),
/// latching key1 at 64 bits (matches psa2.c State2/State4 add path).
fn add_bit(&mut self, bit: bool) {
let carry = (self.data_low >> 31) & 1;
self.data_low = (self.data_low << 1) | (bit as u32);
self.data_high = (self.data_high << 1) | carry;
self.bit_count += 1;
if self.bit_count == KEY1_BITS {
self.key1_low = self.data_low;
self.key1_high = self.data_high;
self.data_low = 0;
self.data_high = 0;
}
}
fn init_preamble_state(&mut self) {
self.data_low = 0;
self.data_high = 0;
self.pattern_counter = 0;
self.bit_count = 0;
self.manchester_reset();
}
// =========================================================================
// CRYPTO PRIMITIVES (faithful to psa2.c)
// =========================================================================
/// TEA encrypt (FUN_08028e14): dynamic key index `sum&3` then `(sum>>11)&3`.
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
let (mut a, mut b) = (*v0, *v1);
let mut sum: u32 = 0;
for _ in 0..TEA_ROUNDS {
let t = key[(sum & 3) as usize].wrapping_add(sum);
sum = sum.wrapping_add(TEA_DELTA);
a = a.wrapping_add(t ^ ((b >> 5) ^ (b << 4)).wrapping_add(b));
let t = key[((sum >> 11) & 3) as usize].wrapping_add(sum);
b = b.wrapping_add(t ^ ((a >> 5) ^ (a << 4)).wrapping_add(a));
}
*v0 = a;
*v1 = b;
}
/// TEA decrypt (FUN_08028e14 inverse): unwinds the encrypt rounds.
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
let (mut a, mut b) = (*v0, *v1);
let mut sum: u32 = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
for _ in 0..TEA_ROUNDS {
let t = key[((sum >> 11) & 3) as usize].wrapping_add(sum);
sum = sum.wrapping_sub(TEA_DELTA);
b = b.wrapping_sub(t ^ ((a >> 5) ^ (a << 4)).wrapping_add(a));
let t = key[(sum & 3) as usize].wrapping_add(sum);
a = a.wrapping_sub(t ^ ((b >> 5) ^ (b << 4)).wrapping_add(b));
}
*v0 = a;
*v1 = b;
}
/// Byte-sum CRC over 7 bytes of TEA output (FUN_08028e60).
fn calculate_tea_crc(v0: u32, v1: u32) -> u8 {
let mut crc: u32 = ((v0 >> 24) & 0xFF) + ((v0 >> 16) & 0xFF) + ((v0 >> 8) & 0xFF) + (v0 & 0xFF);
crc += ((v1 >> 24) & 0xFF) + ((v1 >> 16) & 0xFF) + ((v1 >> 8) & 0xFF);
(crc & 0xFF) as u8
}
/// CRC-16/BUYPASS (poly 0x8005, init 0, no reflection) (FUN_08029098).
fn calculate_crc16_bf2(data: &[u8]) -> u16 {
let mut crc: u16 = 0;
for &byte in data {
crc ^= (byte as u16) << 8;
for _ in 0..8 {
if crc & 0x8000 != 0 {
crc = (crc << 1) ^ 0x8005;
} else {
crc <<= 1;
}
}
}
crc
}
/// Fill buf[0..9] from key1/key2 (FUN_080291c0 first loop / psa_setup_byte_buffer).
/// key1 big-endian reversed into buf[7..0]; key2_low low/high bytes into buf[9]/buf[8].
fn setup_byte_buffer(buf: &mut [u8], key1_low: u32, key1_high: u32, key2_low: u32) {
for i in 0..8usize {
let shift = i * 8;
let b = if shift < 32 {
(key1_low >> shift) as u8
} else {
(key1_high >> (shift - 32)) as u8
};
buf[7 - i] = b;
}
buf[9] = (key2_low & 0xFF) as u8;
buf[8] = ((key2_low >> 8) & 0xFF) as u8;
}
/// Nibble checksum over buf[2..8] → buf[11] (FUN_08028cf8).
fn calculate_checksum(buf: &mut [u8]) {
let mut sum: u32 = 0;
for &b in buf.iter().take(8).skip(2) {
sum += (b & 0xF) as u32 + ((b >> 4) & 0xF) as u32;
}
buf[11] = (sum.wrapping_mul(0x10) & 0xFF) as u8;
}
/// XOR decrypt second stage (FUN_08028d54 + psa_copy_reverse FUN_08028d24).
fn second_stage_xor_decrypt(buf: &mut [u8]) {
// psa_copy_reverse
let t = [
buf[5], buf[4], buf[3], buf[2], buf[9], buf[8], buf[7], buf[6],
];
buf[2] = t[0] ^ t[6];
buf[3] = t[2] ^ t[0];
buf[4] = t[6] ^ t[3];
buf[5] = t[7] ^ t[1];
buf[6] = t[3] ^ t[1];
buf[7] = t[6] ^ t[4] ^ t[5];
}
/// Inverse of `second_stage_xor_decrypt`, used by the encoder.
fn second_stage_xor_encrypt(buf: &mut [u8]) {
let e6 = buf[8];
let e7 = buf[9];
let (p0, p1, p2, p3, p4, p5) = (buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]);
let e5 = p5 ^ e7 ^ e6;
let e0 = p2 ^ e5;
let e2 = p4 ^ e0;
let e4 = p3 ^ e2;
let e3 = p0 ^ e5;
let e1 = p1 ^ e3;
buf[2] = e0;
buf[3] = e1;
buf[4] = e2;
buf[5] = e3;
buf[6] = e4;
buf[7] = e5;
}
/// Pack buf[2..9] into two TEA words (FUN_08028f4c).
fn prepare_tea_data(buf: &[u8]) -> (u32, u32) {
let w0 = ((buf[2] as u32) << 24) | ((buf[3] as u32) << 16) | ((buf[4] as u32) << 8) | buf[5] as u32;
let w1 = ((buf[6] as u32) << 24) | ((buf[7] as u32) << 16) | ((buf[8] as u32) << 8) | buf[9] as u32;
(w0, w1)
}
/// Unpack two TEA words back into buf[2..9] (FUN_08028e88).
fn unpack_tea_result(buf: &mut [u8], v0: u32, v1: u32) {
buf[2] = (v0 >> 24) as u8;
buf[3] = (v0 >> 16) as u8;
buf[4] = (v0 >> 8) as u8;
buf[5] = v0 as u8;
buf[6] = (v1 >> 24) as u8;
buf[7] = (v1 >> 16) as u8;
buf[8] = (v1 >> 8) as u8;
buf[9] = v1 as u8;
}
// =========================================================================
// FIELD EXTRACTION (FUN_08028f10)
// =========================================================================
fn extract_fields_mode23(buf: &[u8]) -> DecryptResult {
let button = buf[8] & 0xF;
let serial = ((buf[2] as u32) << 16) | ((buf[3] as u32) << 8) | buf[4] as u32;
let counter = (buf[6] as u32) | ((buf[5] as u32) << 8);
let crc = buf[7] as u16;
(serial, button, counter, crc, MODE_23)
}
fn extract_fields_mode36(buf: &[u8]) -> DecryptResult {
let button = (buf[5] >> 4) & 0xF;
let serial = ((buf[2] as u32) << 16) | ((buf[3] as u32) << 8) | buf[4] as u32;
let counter = ((buf[7] as u32) << 8)
| ((buf[6] as u32) << 16)
| (buf[8] as u32)
| (((buf[5] as u32) & 0xF) << 24);
let crc = buf[9] as u16;
(serial, button, counter, crc, MODE_36)
}
// =========================================================================
// DECRYPTION PATHS (faithful to psa2.c)
// =========================================================================
/// key2-high gate (matches KAT's sibling `psa::direct_xor_allowed_by_key2`). The PSA2 C
/// `psa_direct_xor_decrypt` validates only on the 4-bit checksum nibble `(checksum ^ key2_high)
/// & 0xF0 == 0`, which has a ~1/16 false-positive rate on arbitrary 80-bit Manchester data. In
/// KAT's "feed every frequency-compatible decoder, first match wins" model that lets PSA2 steal
/// unrelated 250/500µs frames (e.g. VAG at 434 MHz). This precondition — the exact filter KAT's
/// existing `psa` decoder already applies before its XOR path — is added on top of the C gate to
/// suppress those false positives without dropping any genuine PSA2 decode.
fn direct_xor_allowed_by_key2(key2_high_byte: u8) -> bool {
let lo = key2_high_byte & 0xF;
if lo < 3 {
return true;
}
if lo < 7 && (key2_high_byte & 0xC) != 0 {
return true;
}
false
}
/// mode23 XOR path (FUN_08028d98 / psa_direct_xor_decrypt). O(1) — the only path the
/// live decoder runs. Returns the extracted fields on checksum validation.
///
/// Beyond the C's nibble-checksum gate, this applies two false-positive suppressors required by
/// KAT's feed-all-decoders model: the `direct_xor_allowed_by_key2` precondition and a valid-
/// button check (PSA2 buttons are Lock=0, Unlock=1, Trunk=2 only — see `psa_button_name`). Both
/// cleanly separate genuine PSA2 frames from coincidental matches on unrelated Manchester data.
fn direct_xor_decrypt(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<DecryptResult> {
let mut buf = [0u8; 48];
Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low);
let key2_high = buf[8];
if !Self::direct_xor_allowed_by_key2(key2_high) {
return None;
}
Self::calculate_checksum(&mut buf);
let checksum = buf[11];
let validation = (checksum ^ key2_high) & 0xF0;
if validation == 0 {
// Firmware: update buf[8] high nibble before XOR stage.
buf[8] = (buf[8] & 0x0F) | (checksum & 0xF0);
buf[13] = buf[9] ^ buf[8];
Self::second_stage_xor_decrypt(&mut buf);
let fields = Self::extract_fields_mode23(&buf);
// Reject implausible button codes (PSA2: Lock=0/Unlock=1/Trunk=2).
if fields.1 > BTN_MAX_VALID {
return None;
}
return Some(fields);
}
None
}
/// BF1 brute force, range 0x230000000x24000000 (FUN_08028f94).
///
/// PERFORMANCE: up to ~16.7M TEA iterations. NEVER called from `feed()`. Only invoked from
/// [`Self::decrypt_full`], which itself runs only after the cheap structural gate. Returns
/// `(result, seed)` on success.
fn brute_force_decrypt_bf1(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<(DecryptResult, u32)> {
let mut buf = [0u8; 48];
Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low);
let (w0, w1) = Self::prepare_tea_data(&buf);
for counter in BF1_START..BF1_END {
// Derive the working key with two TEA encrypts.
let (mut wk2, mut wk3) = (BF1_CONST_U4, counter);
Self::tea_encrypt(&mut wk2, &mut wk3, &BF1_KEY_SCHEDULE);
let (mut wk0, mut wk1) = ((counter << 8) | 0x0E, BF1_CONST_U5);
Self::tea_encrypt(&mut wk0, &mut wk1, &BF1_KEY_SCHEDULE);
let wkey = [wk0, wk1, wk2, wk3];
let (mut dv0, mut dv1) = (w0, w1);
Self::tea_decrypt(&mut dv0, &mut dv1, &wkey);
if (counter & 0xFFFFFF) == (dv0 >> 8) {
let crc = Self::calculate_tea_crc(dv0, dv1);
if crc == (dv1 & 0xFF) as u8 {
let mut out = [0u8; 48];
Self::unpack_tea_result(&mut out, dv0, dv1);
return Some((Self::extract_fields_mode36(&out), counter));
}
}
}
None
}
/// BF2 brute force, range 0xF30000000xF4000000 (FUN_080290f8).
///
/// PERFORMANCE: up to ~16.7M TEA iterations. NEVER called from `feed()` (see BF1 note).
fn brute_force_decrypt_bf2(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<(DecryptResult, u32)> {
let mut buf = [0u8; 48];
Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low);
let (w0, w1) = Self::prepare_tea_data(&buf);
for counter in BF2_START..BF2_END {
let wkey = [
BF2_KEY_SCHEDULE[0] ^ counter,
BF2_KEY_SCHEDULE[1] ^ counter,
BF2_KEY_SCHEDULE[2] ^ counter,
BF2_KEY_SCHEDULE[3] ^ counter,
];
let (mut dv0, mut dv1) = (w0, w1);
Self::tea_decrypt(&mut dv0, &mut dv1, &wkey);
if (counter & 0xFFFFFF) == (dv0 >> 8) {
let crc_buf = [
(dv0 >> 24) as u8,
(dv0 >> 16) as u8,
(dv0 >> 8) as u8,
dv0 as u8,
(dv1 >> 24) as u8,
(dv1 >> 16) as u8,
];
let crc16 = Self::calculate_crc16_bf2(&crc_buf);
let expected = ((dv1 & 0xFF) | (((dv1 >> 16) & 0xFF) << 8)) as u16;
if crc16 == expected {
let mut out = [0u8; 48];
Self::unpack_tea_result(&mut out, dv0, dv1);
return Some((Self::extract_fields_mode36(&out), counter));
}
}
}
None
}
/// Full decrypt router (FUN_080291c0, the `__attribute__((unused))` `psa_decrypt_full`):
/// try XOR (mode23), then BF1, then BF2.
///
/// PERFORMANCE: this can run the bounded brute force (~33M TEA iterations worst case). It is
/// NOT part of the live `feed()` path — `feed()` uses only `direct_xor_decrypt`. This mirrors
/// the C, where `psa_decrypt_full` is unused by the decoder and the feed callback calls
/// `psa_decrypt_fast` (XOR only). Exposed for completeness / offline decrypt; gate any caller
/// behind the structural frame check so the brute force never runs on arbitrary data.
#[allow(dead_code)]
fn decrypt_full(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<DecryptResult> {
if let Some(r) = Self::direct_xor_decrypt(key1_low, key1_high, key2_low) {
return Some(r);
}
if let Some((r, _seed)) = Self::brute_force_decrypt_bf1(key1_low, key1_high, key2_low) {
return Some(r);
}
if let Some((r, _seed)) = Self::brute_force_decrypt_bf2(key1_low, key1_high, key2_low) {
return Some(r);
}
None
}
/// Build the encoded key material for a mode23 frame (psa_build_encrypt_mode23 /
/// FUN_08029028). Returns `(key1_high, key1_low, validation_field)`.
fn encode_mode23(serial: u32, button: u8, counter: u16) -> (u32, u32, u16) {
let mut buf = [0u8; 48];
buf[2] = (serial >> 16) as u8;
buf[3] = (serial >> 8) as u8;
buf[4] = serial as u8;
buf[5] = (counter >> 8) as u8;
buf[6] = counter as u8;
buf[7] = 0; // CRC placeholder
buf[8] = button & 0xF;
buf[9] = 0; // key2_low low byte
Self::second_stage_xor_encrypt(&mut buf);
Self::calculate_checksum(&mut buf);
buf[8] = (buf[8] & 0x0F) | (buf[11] & 0xF0);
buf[13] = buf[9] ^ buf[8];
// buf[0]/buf[1] preamble bytes (no original key material → derive from data).
buf[0] = buf[2] ^ buf[6];
buf[1] = buf[3] ^ buf[7];
let key1_high =
((buf[0] as u32) << 24) | ((buf[1] as u32) << 16) | ((buf[2] as u32) << 8) | buf[3] as u32;
let key1_low =
((buf[4] as u32) << 24) | ((buf[5] as u32) << 16) | ((buf[6] as u32) << 8) | buf[7] as u32;
let validation = ((buf[8] as u16) << 8) | buf[9] as u16;
(key1_high, key1_low, validation)
}
/// Map KAT's generic button command to a PSA2 button code (Lock=0, Unlock=1, Trunk=2).
fn map_button(button: u8) -> u8 {
match button {
0x01 => 0x0, // Lock
0x02 => 0x1, // Unlock
0x04 => 0x2, // Trunk
0x08 => 0x2, // Panic → Trunk (PSA2 has no panic code)
b => b & 0x0F,
}
}
/// Finalize a collected 80-bit frame: latch validation/key2, run the cheap mode23 XOR gate,
/// emit only on successful decryption. NEVER runs the brute force.
///
/// The C feed (`subghz_protocol_decoder_psa2_feed`) also emits on a bare
/// `(validation_field & 0xF) == 0xA` even when the XOR decrypt fails — but that path yields a
/// raw, *undecrypted* frame (no serial/button/counter) that the firmware UI keeps so the user
/// can later run the brute-force button. KAT's pipeline feeds every frequency-compatible
/// decoder and reports the first that fires, so a 1/16-probability nibble match with no fields
/// is indistinguishable from noise and steals unrelated 250/500µs frames (e.g. VAG at 434 MHz).
/// We therefore gate emission strictly on a successful, field-bearing decrypt — the same path
/// that produces the genuine GROUPE PSA decodes — which is the meaningful half of the C gate.
fn finalize_frame(&mut self) -> Option<DecodedSignal> {
// C: validation_field = decode_data_low & 0xFFFF; key2_low = decode_data_low.
self.validation_field = (self.data_low & 0xFFFF) as u16;
self.key2_low = self.data_low;
self.key2_high = self.data_high;
// C key2_low for decrypt is the 16-bit validation word in the low position.
let decrypt =
Self::direct_xor_decrypt(self.key1_low, self.key1_high, self.validation_field as u32);
// Reset collection regardless (the C feed always rewinds to State0 after a frame attempt).
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.state = DecoderState::WaitEdge;
let (serial, button, counter, _crc, _type) = decrypt?;
let _ = (DECRYPTED_OK, VALID_NIBBLE); // C markers documented; emission gated on decrypt.
let data = ((self.key1_high as u64) << 32) | self.key1_low as u64;
Some(DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter as u16),
crc_valid: true,
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
})
}
}
impl ProtocolDecoder for Psa2Decoder {
fn name(&self) -> &'static str {
"PSA2"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: MIN_COUNT_BIT,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[433_920_000]
}
fn reset(&mut self) {
self.state = DecoderState::WaitEdge;
self.prev_duration = 0;
self.manchester_state = ManchesterState::Mid1;
self.pattern_counter = 0;
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.key1_low = 0;
self.key1_high = 0;
self.validation_field = 0;
self.key2_low = 0;
self.key2_high = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.state {
// State0: detect preamble pattern type.
DecoderState::WaitEdge => {
if !level {
return None;
}
self.init_preamble_state();
self.prev_duration = duration;
if Self::near(duration, TE_SHORT, TE_DELTA) {
self.state = DecoderState::CountPattern250;
} else if Self::near(duration, TE_SHORT_HALF, TOL_HALF) {
self.state = DecoderState::CountPattern125;
}
}
// State1: count standard-rate (250µs) preamble pulses.
DecoderState::CountPattern250 => {
if level {
return None;
}
if Self::near(duration, TE_SHORT, TE_DELTA) {
if Self::near(self.prev_duration, TE_SHORT, TE_DELTA) {
self.pattern_counter += 1;
}
self.prev_duration = duration;
return None;
}
if Self::near(duration, TE_LONG, TE_DELTA) {
if self.pattern_counter > PATTERN_THRESHOLD_1 {
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.manchester_reset();
self.state = DecoderState::DecodeManchester250;
}
self.pattern_counter = 0;
self.prev_duration = duration;
return None;
}
self.state = DecoderState::WaitEdge;
self.pattern_counter = 0;
}
// State2: receive key1 + key2/validation at standard rate.
DecoderState::DecodeManchester250 => {
if self.bit_count >= MAX_BITS {
self.state = DecoderState::WaitEdge;
return None;
}
// End-of-packet detection at KEY2_BITS.
if level && self.bit_count == KEY2_BITS && Self::near(duration, TE_END_1000, 199) {
return self.finalize_frame();
}
let event: Option<u8>;
if Self::near(duration, TE_SHORT, TE_DELTA) {
event = Some(((level as u8 ^ 1) & 0x7F) << 1);
} else if Self::near(duration, TE_LONG, TE_DELTA) {
event = Some(if level { 4 } else { 6 });
} else {
// Out-of-range low pulse: secondary end marker when 80 bits collected. The C
// also gates this on a (stale) nibble==0xA; emission is now decided in
// finalize_frame (decrypt-or-reject), so we trigger on the end geometry alone.
if !level && Self::near(duration, TE_END_1000, 199) && self.bit_count == KEY2_BITS
{
return self.finalize_frame();
}
return None;
}
if let Some(ev) = event {
if self.bit_count < KEY2_BITS {
if let Some(bit) = self.manchester_advance(ev) {
self.add_bit(bit);
}
}
}
self.prev_duration = duration;
}
// State3: count half-rate (125µs) preamble pulses.
DecoderState::CountPattern125 => {
if level {
return None;
}
if Self::near(duration, TE_SHORT_HALF, TOL_HALF) {
if Self::near(self.prev_duration, TE_SHORT_HALF, TOL_HALF) {
self.pattern_counter += 1;
} else {
self.pattern_counter = 0;
}
self.prev_duration = duration;
return None;
}
if (TE_LONG_HALF..0x12C).contains(&duration) {
if self.pattern_counter > PATTERN_THRESHOLD_2 {
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.manchester_reset();
self.state = DecoderState::DecodeManchester125;
}
self.pattern_counter = 0;
self.prev_duration = duration;
return None;
}
self.state = DecoderState::WaitEdge;
}
// State4: receive key1 + key2/validation at half rate.
DecoderState::DecodeManchester125 => {
if self.bit_count >= MAX_BITS {
self.state = DecoderState::WaitEdge;
return None;
}
if !level {
let event: Option<u8> = if Self::near(duration, TE_SHORT_HALF, TOL_HALF) {
Some(((level as u8 ^ 1) & 0x7F) << 1)
} else if (TE_LONG_HALF..0x12C).contains(&duration) {
Some(if level { 4 } else { 6 })
} else {
None
};
if let Some(ev) = event {
if let Some(bit) = self.manchester_advance(ev) {
self.add_bit(bit);
}
} else {
return None;
}
} else {
// Rising edge: end-of-packet at 500µs.
if Self::near(duration, TE_END_500, 99) {
if self.bit_count != KEY2_BITS {
return None;
}
return self.finalize_frame();
}
}
self.prev_duration = duration;
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
// Increment counter on TX (matches FUN_080299a0 rolling-counter increment).
let counter = decoded.counter.unwrap_or(0).wrapping_add(1);
let (key1_high, key1_low, validation) =
Self::encode_mode23(serial, Self::map_button(button), counter);
// mode23 timings: te=250µs, sync long=500µs, end=1000µs.
let te = TE_SHORT;
let te_long_sync = TE_LONG;
let end_dur = TE_END_1000;
let mut signal = Vec::with_capacity(600);
// Preamble: 80 pairs of (HIGH te)+(LOW te).
for _ in 0..80 {
signal.push(LevelDuration::new(true, te));
signal.push(LevelDuration::new(false, te));
}
// Sync: (LOW te) + (HIGH te_long) + (LOW te).
signal.push(LevelDuration::new(false, te));
signal.push(LevelDuration::new(true, te_long_sync));
signal.push(LevelDuration::new(false, te));
// key1: 64 bits MSB-first. bit=1 → (HIGH,LOW); bit=0 → (LOW,HIGH).
let k1 = ((key1_high as u64) << 32) | key1_low as u64;
for bit in (0..64).rev() {
let b = (k1 >> bit) & 1 == 1;
signal.push(LevelDuration::new(b, te));
signal.push(LevelDuration::new(!b, te));
}
// validation_field: 16 bits MSB-first.
for bit in (0..16).rev() {
let b = (validation >> bit) & 1 == 1;
signal.push(LevelDuration::new(b, te));
signal.push(LevelDuration::new(!b, te));
}
// End burst: (HIGH end) + (LOW end).
signal.push(LevelDuration::new(true, end_dur));
signal.push(LevelDuration::new(false, end_dur));
Some(signal)
}
}
impl Default for Psa2Decoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tea_round_trip() {
// TEA encrypt then decrypt must be the identity for any key.
let key = BF1_KEY_SCHEDULE;
let (mut v0, mut v1) = (0x1234_5678u32, 0x9ABC_DEF0u32);
let (o0, o1) = (v0, v1);
Psa2Decoder::tea_encrypt(&mut v0, &mut v1, &key);
Psa2Decoder::tea_decrypt(&mut v0, &mut v1, &key);
assert_eq!((v0, v1), (o0, o1), "TEA encrypt/decrypt not invertible");
}
#[test]
fn mode23_encode_decode_round_trip() {
// Encode (serial,button,counter) → key1/validation, then run the mode23 XOR decrypt and
// confirm the fields (and a passing nibble checksum) come back exactly. Validates TEA-free
// mode23 path: second_stage_xor + nibble checksum + field packing.
for &(serial, btn, cnt) in &[
(0x99EB25u32, 0u8, 0x039Bu16),
(0x123456, 2, 0x0042),
(0xABCDEF, 1, 0x1234),
(0x0000FF, 0, 0x0001),
] {
let (k1h, k1l, vf) = Psa2Decoder::encode_mode23(serial, btn, cnt);
let decrypt = Psa2Decoder::direct_xor_decrypt(k1l, k1h, vf as u32);
assert!(
decrypt.is_some(),
"mode23 XOR decrypt failed to validate for serial={serial:06X} btn={btn} cnt={cnt:04X}"
);
let (ds, db, dc, _crc, ty) = decrypt.unwrap();
assert_eq!(ds, serial, "serial mismatch");
assert_eq!(db, btn, "button mismatch");
assert_eq!(dc, cnt as u32, "counter mismatch");
assert_eq!(ty, MODE_23, "mode mismatch");
}
}
#[test]
fn checksum_matches_reference() {
// Spot-check the nibble checksum against the hand-computed C formula.
let mut buf = [0u8; 48];
for (i, b) in buf.iter_mut().enumerate().take(8).skip(2) {
*b = (i as u8) * 0x11; // 0x22,0x33,0x44,0x55,0x66,0x77
}
Psa2Decoder::calculate_checksum(&mut buf);
// bytes 0x22,0x33,0x44,0x55,0x66,0x77 → nibble sum = (2+2)+(3+3)+(4+4)+(5+5)+(6+6)+(7+7) = 54
let expected = ((54u32 * 0x10) & 0xFF) as u8; // 54*16 = 864 = 0x360 → 0x60
assert_eq!(buf[11], expected);
}
#[test]
fn encode_emits_manchester_frame() {
// The encoder produces a non-trivial 250/500µs Manchester upload.
let dec = Psa2Decoder::new();
let decoded = DecodedSignal {
serial: Some(0x99EB25),
button: Some(0x01),
counter: Some(0x039A),
crc_valid: true,
data: 0,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
};
let sig = dec.encode(&decoded, 0x01).expect("encode should succeed");
// 80 preamble pairs (160) + 3 sync + 64*2 + 16*2 + 2 end = 160+3+128+32+2 = 325.
assert_eq!(sig.len(), 325, "unexpected upload length");
assert!(sig.iter().all(|p| p.duration_us > 0));
}
}
+579
View File
@@ -0,0 +1,579 @@
//! Toyota / Lexus KeeLoq protocol decoder (dual variant)
//!
//! Ported from Flipper-ARF reference: `lib/subghz/protocols/toyota.c` and `toyota.h`.
//! Decode-only — the reference `encoder` field is NULL.
//!
//! Two variants, detected from the first HIGH pulse width (threshold 310µs):
//!
//! - **Variant A** — Corolla / 433.92 MHz. PWM pairs: te_short=400µs, te_long=800µs,
//! delta=175µs. LS (long HIGH + short LOW) = bit 0, SL (short HIGH + long LOW) = bit 1.
//! Preamble = repeated short-short (SS) pairs; first non-SS pair is the first data bit.
//! Frame = 68 bits; min_count_bit = 60.
//! - **Variant B** — Tundra / 315 MHz. NRZ: each individual pulse encodes one bit by a
//! midpoint classifier (`<= 287µs` -> 0, `> 287µs` -> 1). Preamble = short-HIGH /
//! long-LOW pairs (te_short=200µs, te_long=390µs, delta=120µs) terminated by a sync gap
//! (LOW between 1500 and 2600µs). Frame = 67 bits; min_count_bit = 60.
//!
//! KeeLoq hopping: the hop field is left encrypted (the reference does not decrypt or run a
//! CRC). `data` layout matches the reference `generic.data`:
//! `(hop << 32) | (serial << 4) | button`, with hop=32 bits, serial=28 bits, button=4 bits.
//!
//! Emission is gated tightly (exact frame bit count + structural preamble/sync + non-zero
//! serial) so it does not false-match the other KeeLoq-PWM protocols (Kia V3/V4, Subaru,
//! Suzuki, etc.). The shared 433 MHz KeeLoq-PWM air encoding means a Toyota Variant-A frame
//! that also satisfies Kia V3/V4's 68-bit/CRC4 structure is claimed by Kia V3/V4 first (it is
//! earlier in the registry); Toyota uniquely claims 60-bit frames and Variant-B NRZ at 315 MHz.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
// Variant A physical constants (Corolla / 433 MHz)
const A_TE_SHORT: u32 = 400;
const A_TE_LONG: u32 = 800;
const A_TE_DELTA: u32 = 175;
// Variant B physical constants (Tundra / 315 MHz, preamble classification only)
const B_TE_SHORT: u32 = 200;
const B_TE_LONG: u32 = 390;
const B_TE_DELTA: u32 = 120;
const MIN_COUNT_BIT: usize = 60;
/// NRZ midpoint for Variant B data pulses (`<= 287` -> 0, `> 287` -> 1).
const B_NRZ_MIDPOINT: u32 = 287;
/// Sync gap (LOW) separating preamble from data in Variant B.
const B_SYNC_GAP_MIN: u32 = 1500;
const B_SYNC_GAP_MAX: u32 = 2600;
/// Minimum preamble pairs before a frame is accepted.
const A_PREAMBLE_MIN: u16 = 6;
const B_PREAMBLE_MIN: u16 = 6;
/// Frame lengths in bits.
const A_BITS: usize = 68;
const B_BITS: usize = 67;
/// First HIGH duration below this -> Variant B, at or above -> Variant A.
const VARIANT_THRESH: u32 = 310;
#[inline]
fn a_is_short(d: u32) -> bool {
duration_diff!(d, A_TE_SHORT) < A_TE_DELTA
}
#[inline]
fn a_is_long(d: u32) -> bool {
duration_diff!(d, A_TE_LONG) < A_TE_DELTA
}
#[inline]
fn b_is_short(d: u32) -> bool {
duration_diff!(d, B_TE_SHORT) < B_TE_DELTA
}
#[inline]
fn b_is_long(d: u32) -> bool {
duration_diff!(d, B_TE_LONG) < B_TE_DELTA
}
/// Decoder steps (matches ToyotaDecoderStep in toyota.c).
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
PreambleA,
DataA,
PreambleB,
DataB,
}
/// Toyota / Lexus protocol decoder (matches SubGhzProtocolDecoderToyota).
pub struct ToyotaDecoder {
step: DecoderStep,
/// 128-bit shift accumulator (hi:lo), matching the C `bits_hi`/`bits_lo`.
bits_hi: u64,
bits_lo: u64,
bit_count: usize,
te_last: u32,
have_high: bool,
preamble_count: u16,
/// 0 = Variant A (Corolla / 433 MHz), 1 = Variant B (Tundra / 315 MHz).
variant: u8,
}
impl ToyotaDecoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
bits_hi: 0,
bits_lo: 0,
bit_count: 0,
te_last: 0,
have_high: false,
preamble_count: 0,
variant: 0,
}
}
/// Reset the parse state. Matches `subghz_protocol_decoder_toyota_reset`, which intentionally
/// does NOT clear `variant` (detected once per session); the variant is re-detected on the next
/// pulse from the Reset step anyway.
fn reset_state(&mut self) {
self.step = DecoderStep::Reset;
self.bits_hi = 0;
self.bits_lo = 0;
self.bit_count = 0;
self.te_last = 0;
self.have_high = false;
self.preamble_count = 0;
}
/// Push one bit into the 128-bit accumulator (matches `toyota_push_bit`).
fn push_bit(&mut self, bit: u8) {
let carry = (self.bits_lo >> 63) & 1;
self.bits_hi = (self.bits_hi << 1) | carry;
self.bits_lo = (self.bits_lo << 1) | (bit as u64 & 1);
self.bit_count += 1;
}
/// Extract `length` bits at `offset` from the end of the accumulator (matches `toyota_extract`).
fn extract(&self, offset: usize, length: usize) -> u32 {
let mut result: u32 = 0;
let total = self.bit_count as isize;
for i in 0..length {
let pos = (total - 1) - (offset as isize + i as isize);
let b = if pos >= 64 {
((self.bits_hi >> (pos - 64)) & 1) as u32
} else if pos >= 0 {
((self.bits_lo >> pos) & 1) as u32
} else {
0
};
result = (result << 1) | b;
}
result
}
/// Build the decoded signal once a full frame is collected (matches `toyota_decode_and_fire`).
/// Returns `None` if the structural gate (bit count / serial) fails.
fn decode_and_fire(&self) -> Option<DecodedSignal> {
if self.bit_count < MIN_COUNT_BIT {
return None;
}
let hop = self.extract(0, 32);
let serial = self.extract(32, 28);
let button = self.extract(60, 4) as u8;
// Tight gate: require a non-zero serial so a run of all-zero/garbage bits that happens to
// reach the bit count cannot emit a Toyota frame.
if serial == 0 {
return None;
}
// generic.data = (hop << 32) | (serial << 4) | button
let data = ((hop as u64) << 32) | ((serial as u64) << 4) | (button as u64 & 0x0F);
Some(DecodedSignal {
serial: Some(serial),
button: Some(button),
// KeeLoq hop is left encrypted (no key / no decrypt in the reference).
counter: None,
// No CRC in the reference; a fully-structured frame of the exact bit count is the
// validity criterion (the callback only fires at min_count_bit).
crc_valid: true,
data,
data_count_bit: self.bit_count,
encoder_capable: false,
extra: None,
protocol_display_name: None,
})
}
/// Feed for Variant A (Corolla / 433 MHz) — PWM pair encoding (matches `toyota_feed_variant_a`).
///
/// Faithful to the reference's gap-terminated emission path. The reference ALSO self-fires the
/// instant `bit_count` reaches 68; that is intentionally dropped here. Variant A is the same
/// KeeLoq-PWM air protocol as Kia V3/V4, and the early self-fire (on the normal short LOW that
/// completes bit 68) lands several pulses BEFORE Kia V3/V4's sync-terminated fire — which, in
/// KAT's "first decoder to fire on a pulse wins" stream, would let Toyota steal every shared
/// 68-bit frame from Kia. By emitting only on the terminating gap (the same pulse Kia fires on),
/// the registry order (Kia earlier) resolves the shared frames in Kia's favour, while Toyota
/// still uniquely claims 6067-bit Variant-A frames that Kia rejects. Bit accumulation is capped
/// at 68 so the field layout is preserved regardless of trailing repeats.
fn feed_variant_a(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::PreambleA => {
if level {
self.te_last = duration;
self.have_high = true;
return None;
}
if !self.have_high {
self.reset_state();
return None;
}
self.have_high = false;
let hs = a_is_short(self.te_last);
let hl = a_is_long(self.te_last);
let ls = a_is_short(duration);
let ll = a_is_long(duration);
if hs && ls {
self.preamble_count += 1;
return None;
}
if self.preamble_count < A_PREAMBLE_MIN {
self.reset_state();
return None;
}
self.bits_hi = 0;
self.bits_lo = 0;
self.bit_count = 0;
if hl && ls {
self.push_bit(0);
} else if hs && ll {
self.push_bit(1);
}
self.step = DecoderStep::DataA;
None
}
DecoderStep::DataA => {
if level {
if a_is_short(duration) || a_is_long(duration) {
self.te_last = duration;
self.have_high = true;
} else {
// Terminating gap / out-of-range HIGH: emit (deferring to Kia on shared
// 68-bit frames via registry order — see fn doc-comment).
let result = if self.bit_count >= MIN_COUNT_BIT {
self.decode_and_fire()
} else {
None
};
self.reset_state();
return result;
}
return None;
}
if !self.have_high {
return None;
}
self.have_high = false;
// Cap accumulation at A_BITS: once a full 68-bit frame is collected, stop pushing
// more bits and wait for the terminating gap. This keeps the field layout fixed and
// makes emission coincide with Kia V3/V4's sync-terminated fire so Kia wins shared
// frames by registry order.
if self.bit_count >= A_BITS {
return None;
}
let hs = a_is_short(self.te_last);
let hl = a_is_long(self.te_last);
let ls = a_is_short(duration);
let ll = a_is_long(duration);
if hl && ls {
self.push_bit(0);
} else if hs && ll {
self.push_bit(1);
} else {
let result = if self.bit_count >= MIN_COUNT_BIT {
self.decode_and_fire()
} else {
None
};
self.reset_state();
return result;
}
None
}
_ => None,
}
}
/// Feed for Variant B (Tundra / 315 MHz) — NRZ encoding (matches `toyota_feed_variant_b`).
fn feed_variant_b(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::PreambleB => {
if level {
if b_is_short(duration) {
self.te_last = duration;
self.have_high = true;
} else {
self.reset_state();
}
return None;
}
// Falling edge
if !self.have_high {
self.reset_state();
return None;
}
self.have_high = false;
// Sync gap: LOW ~1938µs -> transition to data
if duration >= B_SYNC_GAP_MIN && duration <= B_SYNC_GAP_MAX {
if self.preamble_count >= B_PREAMBLE_MIN {
self.bits_hi = 0;
self.bits_lo = 0;
self.bit_count = 0;
self.have_high = false;
self.step = DecoderStep::DataB;
} else {
self.reset_state();
}
return None;
}
// Normal preamble LOW must be LONG
if b_is_long(duration) {
self.preamble_count += 1;
return None;
}
self.reset_state();
None
}
DecoderStep::DataB => {
// Every pulse (HIGH or LOW) encodes one bit. A pulse >= sync-gap min ends the frame.
if duration >= B_SYNC_GAP_MIN {
let result = if self.bit_count >= MIN_COUNT_BIT {
self.decode_and_fire()
} else {
None
};
self.reset_state();
return result;
}
let bit = if duration > B_NRZ_MIDPOINT { 1 } else { 0 };
self.push_bit(bit);
if self.bit_count >= B_BITS {
let result = self.decode_and_fire();
self.reset_state();
return result;
}
None
}
_ => None,
}
}
}
impl ProtocolDecoder for ToyotaDecoder {
fn name(&self) -> &'static str {
"Toyota"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: A_TE_SHORT,
te_long: A_TE_LONG,
te_delta: A_TE_DELTA,
min_count_bit: MIN_COUNT_BIT,
}
}
fn supported_frequencies(&self) -> &[u32] {
// Variant A 433.92 MHz, Variant B 315 MHz.
&[433_920_000, 315_000_000]
}
fn reset(&mut self) {
self.reset_state();
// Full reset between segments: also clear the detected variant.
self.variant = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
if self.step == DecoderStep::Reset {
if !level {
return None;
}
// Variant detection from the first SHORT HIGH pulse:
// < 310µs -> Variant B (Tundra 315 MHz, te_short ~200µs)
// >= 310µs -> Variant A (Corolla 433 MHz, te_short ~400µs)
let fits_b = b_is_short(duration) && duration < VARIANT_THRESH;
let fits_a = a_is_short(duration) && duration >= VARIANT_THRESH;
if fits_b {
self.variant = 1;
self.te_last = duration;
self.have_high = true;
self.preamble_count = 0;
self.step = DecoderStep::PreambleB;
} else if fits_a {
self.variant = 0;
self.te_last = duration;
self.have_high = true;
self.preamble_count = 0;
self.step = DecoderStep::PreambleA;
}
return None;
}
if self.variant == 1 {
self.feed_variant_b(level, duration)
} else {
self.feed_variant_a(level, duration)
}
}
fn supports_encoding(&self) -> bool {
false
}
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
None
}
}
impl Default for ToyotaDecoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Build a Variant A (PWM) frame: preamble SS pairs, then `bits` MSB-first.
/// LS (long HIGH + short LOW) = 0, SL (short HIGH + long LOW) = 1. Terminated by a long gap.
fn build_variant_a(bits: &[u8]) -> Vec<LevelDuration> {
let mut v = Vec::new();
// 8 short-short preamble pairs (>= A_PREAMBLE_MIN = 6).
for _ in 0..8 {
v.push(LevelDuration::new(true, A_TE_SHORT));
v.push(LevelDuration::new(false, A_TE_SHORT));
}
for &b in bits {
if b == 0 {
v.push(LevelDuration::new(true, A_TE_LONG));
v.push(LevelDuration::new(false, A_TE_SHORT));
} else {
v.push(LevelDuration::new(true, A_TE_SHORT));
v.push(LevelDuration::new(false, A_TE_LONG));
}
}
// Trailing HIGH gap (out of TE range) flushes the frame.
v.push(LevelDuration::new(true, 5000));
v
}
/// Build a Variant B (NRZ) frame: short-HIGH/long-LOW preamble pairs, sync gap, then each bit
/// as one pulse (short = 0, long = 1), terminated by a sync-gap-length pulse.
fn build_variant_b(bits: &[u8]) -> Vec<LevelDuration> {
let mut v = Vec::new();
// 8 preamble pairs: short HIGH (~200µs) + long LOW (~390µs).
for _ in 0..8 {
v.push(LevelDuration::new(true, B_TE_SHORT));
v.push(LevelDuration::new(false, B_TE_LONG));
}
// Last preamble HIGH, then the sync-gap LOW.
v.push(LevelDuration::new(true, B_TE_SHORT));
v.push(LevelDuration::new(false, 1938));
// Data: each bit one pulse, alternating level (polarity is ignored by the NRZ classifier).
for (i, &b) in bits.iter().enumerate() {
let level = i % 2 == 0;
let dur = if b == 1 { 380 } else { 200 };
v.push(LevelDuration::new(level, dur));
}
// End-of-frame gap.
v.push(LevelDuration::new(false, 2000));
v
}
fn feed_all(dec: &mut ToyotaDecoder, pairs: &[LevelDuration]) -> Option<DecodedSignal> {
for p in pairs {
if let Some(sig) = dec.feed(p.level, p.duration_us) {
return Some(sig);
}
}
None
}
#[test]
fn variant_a_synthetic_decodes() {
// 68-bit frame: a recognizable pattern with a non-zero serial and a known button nibble.
// bits[0..32] = hop, bits[32..60] = serial, bits[60..64] = button, bits[64..68] = padding.
let mut bits = vec![0u8; A_BITS];
// hop = 0x9ABCDEF0 (MSB first in bits[0..32])
let hop: u32 = 0x9ABC_DEF0;
for i in 0..32 {
bits[i] = ((hop >> (31 - i)) & 1) as u8;
}
// serial = 0x0123456 (28 bits) in bits[32..60]
let serial: u32 = 0x012_3456;
for i in 0..28 {
bits[32 + i] = ((serial >> (27 - i)) & 1) as u8;
}
// button = 0x8 (Lock) in bits[60..64]
let button: u8 = 0x8;
for i in 0..4 {
bits[60 + i] = (button >> (3 - i)) & 1;
}
// bits[64..68] padding = 0
let frame = build_variant_a(&bits);
let mut dec = ToyotaDecoder::new();
let sig = feed_all(&mut dec, &frame).expect("variant A frame should decode");
assert_eq!(sig.data_count_bit, A_BITS);
assert_eq!(sig.serial, Some(serial));
assert_eq!(sig.button, Some(button));
// hop is the top 32 bits of data
assert_eq!((sig.data >> 32) as u32, hop);
assert!(sig.crc_valid);
}
#[test]
fn variant_b_synthetic_decodes() {
// 67-bit NRZ frame.
let mut bits = vec![0u8; B_BITS];
let hop: u32 = 0x1357_9BDF;
for i in 0..32 {
bits[i] = ((hop >> (31 - i)) & 1) as u8;
}
let serial: u32 = 0x0AB_CDEF;
for i in 0..28 {
bits[32 + i] = ((serial >> (27 - i)) & 1) as u8;
}
// button (bits[60..64]); only 3 bits of button fit before the 67-bit end (bits[60..63]),
// bit 63..67 truncated — extract(60,4) reads bits[63..67] from the END. Just assert serial.
for i in 0..7 {
bits[60 + i] = if i % 2 == 0 { 1 } else { 0 };
}
let frame = build_variant_b(&bits);
let mut dec = ToyotaDecoder::new();
let sig = feed_all(&mut dec, &frame).expect("variant B frame should decode");
assert_eq!(sig.data_count_bit, B_BITS);
assert_eq!(sig.serial, Some(serial));
assert!(sig.crc_valid);
}
#[test]
fn rejects_all_zero_serial() {
// A structurally valid 68-bit frame with an all-zero serial must NOT emit (tight gate).
let bits = vec![0u8; A_BITS];
let frame = build_variant_a(&bits);
let mut dec = ToyotaDecoder::new();
assert!(feed_all(&mut dec, &frame).is_none());
}
}