v1.3.0: Add 14 keyfob protocols (8 ProtoPirate + 6 Flipper-ARF), fix KeeLoq overflow
Brings KAT to 32 protocol decoders. Ported from the ProtoPirate reference and the D4C1-Labs/Flipper-ARF firmware: ProtoPirate (8): Kia V7, Ford V1, Ford V2, Ford V3, Chrysler V0, Honda Static, Honda V1, Land Rover V0. Flipper-ARF (6): Toyota, Land Rover RKE, Mazda Siemens, BMW CAS4, Porsche Cayenne, PSA2. Each decoder is gated (CRC / checksum / fixed markers / frame structure) so it cannot false-match existing protocols; every previously-decoding IMPORTS capture decodes unchanged. Real-capture decodes added for Ford V3 (LDV T80), Honda Static (Honda), Toyota (Camry NRZ variant), and PSA2 (Groupe PSA, serial 0x99EB25); the rest are validated by encode/decode round-trip and synthetic-frame tests. Also fixes a debug-only underflow panic in keeloq_common::keeloq_decrypt (15 - r underflowed; now wrapping_sub to match the reference's unsigned wrap; release behavior unchanged). Adds per-protocol docs, updates the README protocol table, capture metadata (encoding / RF / encryption), make-suggestion mapping, and CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEfaKqzB3T4qsrybwfEi73
This commit is contained in:
@@ -2,6 +2,24 @@
|
||||
|
||||
All notable changes to KAT are documented here.
|
||||
|
||||
## [1.3.0] - 2026-06-23
|
||||
|
||||
### Added
|
||||
|
||||
- **14 new protocol decoders** (total now 32), ported from the [ProtoPirate](https://protopirate.net/ProtoPirate/ProtoPirate) reference and the [Flipper-ARF](https://github.com/D4C1-Labs/Flipper-ARF) firmware:
|
||||
- **ProtoPirate (8):** **Kia V7** (Manchester 250/500, header 0x4C + CRC8), **Ford V1** (Manchester 65/130, descramble + CRC16/CCITT), **Ford V2** (Manchester 200/400, 0x7FA7 sync), **Ford V3** (Manchester 240/480, plaintext, decode-only), **Chrysler V0** (PWM, seed-XOR transform; Dodge/Jeep), **Honda Static** (FM, XOR checksum), **Honda V1** (PWM/PPM, CRC-fold), **Land Rover V0** (differential Manchester, 3-bit check polynomial).
|
||||
- **Flipper-ARF (6):** **Toyota** (dual PWM + NRZ variants, KeeLoq, decode-only; Lexus), **Land Rover RKE** (PWM, KeeLoq), **Mazda Siemens** (Siemens XOR/interleave), **BMW CAS4** (Manchester, 0x30/0xC5 markers, decode-only), **Porsche Cayenne** (PWM, VAG rolling-register cipher; adds an encoder to the shared Touareg air protocol), **PSA2 / "PSA OLD"** (Manchester, TEA with bounded brute-force gated behind a structural/checksum check).
|
||||
- **Real-capture validation** — decoders verified against `IMPORTS/` samples where they exist: **Ford V3** (LDV T80), **Honda Static** (Honda), **Toyota** (Camry NRZ variant), **PSA2** (Groupe PSA, recovered serial 0x99EB25 + rolling counter). The rest are validated by in-module encode→decode round-trip and synthetic-frame unit tests.
|
||||
- **Metadata, make-suggestion, and docs** — each new protocol is tagged with Encoding / RF (AM/FM) / Encryption in the signal detail panel, mapped to a Make suggestion (Honda/Acura, Chrysler/Dodge/Jeep, Toyota/Lexus, Land Rover, BMW, Porsche, Mazda, Peugeot/Citroën), and documented under `docs/`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **KeeLoq decrypt debug-panic** — `keeloq_common::keeloq_decrypt` computed `15 - r` (with `r` up to 527), underflowing and panicking in debug builds / `cargo test`; now uses `wrapping_sub` to match the reference's unsigned wraparound (release behavior, which silently wrapped, is unchanged).
|
||||
|
||||
### Notes
|
||||
|
||||
- All new decoders are gated (CRC / checksum / fixed markers / frame structure) so they do not false-match existing protocols — every previously-decoding `IMPORTS/*.sub` capture decodes unchanged. Where two decoders share a wire protocol (Porsche Cayenne ↔ Touareg; Mazda Siemens ↔ Mazda V0; Toyota variant-A ↔ Kia V3/V4 KeeLoq), the pre-existing decoder keeps first-match priority.
|
||||
|
||||
## [1.1.3] - 2026-02-20
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+1
-1
@@ -444,7 +444,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kat"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atty",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kat"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
edition = "2021"
|
||||
description = "Keyfob Analysis Toolkit - HackRF/RTL-SDR signal capture, decode, and transmit (HackRF only)"
|
||||
authors = ["KAT Team"]
|
||||
|
||||
@@ -17,7 +17,7 @@ A terminal-based RF signal analysis tool for capturing, decoding, and retransmit
|
||||
## Features
|
||||
|
||||
- **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies (HackRF uses AM envelope detection; FM protocols are tagged for display and may decode when signal is strong)
|
||||
- **Multi-protocol decoding** — 18 protocol decoders: Kia V0–V6, Ford V0, Fiat V0/V1, Mazda V0, Mitsubishi V0, Porsche Touareg, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions
|
||||
- **Multi-protocol decoding** — 32 protocol decoders: Kia V0–V7, Ford V0–V3, Chrysler V0 (Dodge/Jeep), Honda Static, Honda V1, Toyota (Lexus), Land Rover V0, Land Rover RKE, Fiat V0/V1, Mazda V0, Mazda Siemens, Mitsubishi V0, BMW CAS4, Porsche Touareg, Porsche Cayenne, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, PSA2, Scher-Khan, Star Line; adaptive demodulation for real-world conditions
|
||||
- **KeeLoq generic fallback** — when a signal doesn’t match any known protocol, KAT tries decoding it as KeeLoq using every manufacturer key in the embedded keystore (Kia V3/V4 and Star Line air formats); successful decodes appear as **Keeloq (keystore name)** in the capture list
|
||||
- **RF modulation metadata** — each protocol tagged as AM, FM, or both (from ProtoPirate); shown in signal detail and exported in .fob
|
||||
- **Rich signal detail** — encoding (PWM/Manchester), RF (AM/FM), encryption, serial, counter, key data, CRC, frequency, and raw level/duration pairs
|
||||
@@ -234,18 +234,32 @@ Protocol behavior and RF modulation (AM/FM) follow the ProtoPirate reference. KA
|
||||
| Kia V3/V4 | PWM | AM/FM | KeeLoq | 315 / 433.92 MHz |
|
||||
| Kia V5 | Manchester | FM | Fixed Code | 433.92 MHz |
|
||||
| Kia V6 | Manchester | FM | AES-128 | 433.92 MHz |
|
||||
| Kia V7 | Manchester | FM | Fixed Code (CRC8) | 315 / 433.92 MHz |
|
||||
| Ford V0 | Manchester | FM | Rolling Code | 315 / 433.92 MHz |
|
||||
| Ford V1 | Manchester | FM | Rolling Code (descramble + CRC16) | 315 / 433.92 MHz |
|
||||
| Ford V2 | Manchester | FM | Fixed Code (0x7FA7 sync) | 315 / 433.92 MHz |
|
||||
| Ford V3 | Manchester | FM | Fixed Code (decode-only) | 315 / 433.92 MHz |
|
||||
| Chrysler V0 (Dodge/Jeep) | PWM | AM | Rolling Code (seed-XOR) | 315 / 433.92 MHz |
|
||||
| Honda Static | Manchester | FM | Fixed Code | 315 / 433.92 MHz |
|
||||
| Honda V1 | PWM | AM | Fixed Code | 315 / 433.92 MHz |
|
||||
| Toyota (Lexus) | PWM / NRZ | AM | KeeLoq (decode-only) | 315 / 433.92 MHz |
|
||||
| Land Rover V0 | Diff. Manchester | FM | Rolling Code (3-bit check) | 315 / 433.92 MHz |
|
||||
| Land Rover RKE | PWM | AM | KeeLoq | 315 / 433.92 MHz |
|
||||
| Fiat V0 | Manchester | FM | Fixed Code | 433.92 MHz |
|
||||
| Fiat V1 (Magneti Marelli) | Manchester | FM | Rolling Code | 433.92 MHz |
|
||||
| Mazda V0 | Pair-based | FM | XOR Deobfuscation | 433.92 MHz |
|
||||
| Mazda Siemens | Manchester | FM | Siemens XOR/interleave | 433.92 MHz |
|
||||
| Mitsubishi V0 | PWM | FM | Bit Negation + XOR | 868.35 MHz |
|
||||
| BMW CAS4 | Manchester | AM | CAS4 rolling (decode-only) | 433.92 MHz |
|
||||
| Porsche Touareg | PWM | AM | Rotation Cipher | 433.92 / 868.35 MHz |
|
||||
| Porsche Cayenne | PWM | AM | VAG rolling register | 433.92 / 868.35 MHz |
|
||||
| Subaru | PWM | AM | Rolling Code | 433.92 MHz |
|
||||
| Suzuki | PWM | AM | Rolling Code | 433.92 MHz |
|
||||
| VAG (VW/Audi/Seat/Skoda) | Manchester | AM | AUT64/XTEA | 433.92 / 434.42 MHz |
|
||||
| Scher-Khan | PWM | FM | Magic Code | 433.92 MHz |
|
||||
| Star Line | PWM | AM | KeeLoq | 433.92 MHz |
|
||||
| PSA (Peugeot/Citroën) | Manchester | FM | Modified TEA/XOR | 433.92 MHz |
|
||||
| PSA2 (PSA OLD) | Manchester | AM | TEA | 433.92 MHz |
|
||||
|
||||
**KeeLoq generic fallback:** If no protocol decodes a capture, KAT tries KeeLoq with every keystore manufacturer key (Kia V3/V4 and Star Line bit layouts). On success the protocol is shown as **Keeloq (*keystore name*)** (e.g. Keeloq (Alligator), Keeloq (Pandora_PRO)). See [docs/keeloq_generic.md](docs/keeloq_generic.md).
|
||||
|
||||
|
||||
@@ -12,18 +12,32 @@ This folder describes how each keyfob protocol supported by KAT works. Each docu
|
||||
| Kia V3/V4 | `kia_v3_v4.rs` | [kia_v3_v4.md](kia_v3_v4.md) |
|
||||
| Kia V5 | `kia_v5.rs` | [kia_v5.md](kia_v5.md) |
|
||||
| Kia V6 | `kia_v6.rs` | [kia_v6.md](kia_v6.md) |
|
||||
| Kia V7 | `kia_v7.rs` | [kia_v7.md](kia_v7.md) |
|
||||
| Ford V0 | `ford_v0.rs` | [ford_v0.md](ford_v0.md) |
|
||||
| Ford V1 | `ford_v1.rs` | [ford_v1.md](ford_v1.md) |
|
||||
| Ford V2 | `ford_v2.rs` | [ford_v2.md](ford_v2.md) |
|
||||
| Ford V3 | `ford_v3.rs` | [ford_v3.md](ford_v3.md) |
|
||||
| Honda Static | `honda_static.rs` | [honda_static.md](honda_static.md) |
|
||||
| Honda V1 | `honda_v1.rs` | [honda_v1.md](honda_v1.md) |
|
||||
| Chrysler V0 | `chrysler_v0.rs` | [chrysler_v0.md](chrysler_v0.md) |
|
||||
| Subaru | `subaru.rs` | [subaru.md](subaru.md) |
|
||||
| Toyota | `toyota.rs` | [toyota.md](toyota.md) |
|
||||
| VAG | `vag.rs` | [vag.md](vag.md) |
|
||||
| Fiat V0 | `fiat_v0.rs` | [fiat_v0.md](fiat_v0.md) |
|
||||
| Fiat V1 | `fiat_v1.rs` | [fiat_v1.md](fiat_v1.md) |
|
||||
| Mazda V0 | `mazda_v0.rs` | [mazda_v0.md](mazda_v0.md) |
|
||||
| Mazda Siemens | `mazda_siemens.rs` | [mazda_siemens.md](mazda_siemens.md) |
|
||||
| Mitsubishi V0 | `mitsubishi_v0.rs` | [mitsubishi_v0.md](mitsubishi_v0.md) |
|
||||
| Land Rover V0 | `land_rover_v0.rs` | [land_rover_v0.md](land_rover_v0.md) |
|
||||
| Land Rover RKE | `land_rover_rke.rs` | [land_rover_rke.md](land_rover_rke.md) |
|
||||
| BMW CAS4 | `bmw_cas4.rs` | [bmw_cas4.md](bmw_cas4.md) |
|
||||
| Porsche Touareg | `porsche_touareg.rs` | [porsche_touareg.md](porsche_touareg.md) |
|
||||
| Porsche Cayenne | `porsche_cayenne.rs` | [porsche_cayenne.md](porsche_cayenne.md) |
|
||||
| Suzuki | `suzuki.rs` | [suzuki.md](suzuki.md) |
|
||||
| Scher-Khan | `scher_khan.rs` | [scher_khan.md](scher_khan.md) |
|
||||
| Star Line | `star_line.rs` | [star_line.md](star_line.md) |
|
||||
| PSA | `psa.rs` | [psa.md](psa.md) |
|
||||
| PSA2 (PSA OLD) | `psa2.rs` | [psa2.md](psa2.md) |
|
||||
| KeeLoq generic (fallback) | `keeloq_generic.rs` | [keeloq_generic.md](keeloq_generic.md) |
|
||||
|
||||
**KeeLoq generic** is not a registered decoder; it runs when no protocol matches and tries KeeLoq with every keystore manufacturer key (using `keeloq_common`). Successful decodes appear as **Keeloq (*keystore name*)**.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# BMW CAS4 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/bmw_cas4.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/bmw_cas4.c`
|
||||
|
||||
## Overview
|
||||
|
||||
BMW CAS4 uses Manchester encoding at 500/1000 µs. 64 bits (8 bytes), AM/OOK. The CAS4 rolling cipher's
|
||||
manufacturer key is not available, so the encrypted portion is left as-is — the frame is only framed and
|
||||
validated, not decrypted. Emission is gated on two fixed marker bytes (`byte[0] == 0x30` and
|
||||
`byte[6] == 0xC5`), which makes the protocol specific and prevents false matches.
|
||||
|
||||
The Manchester decoder uses Flipper's `manchester_decoder.h` transition table, with polarity
|
||||
`level ? Low : High` (the same mapping as Ford V0 / common).
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|----------------|---------|-----------------------------|
|
||||
| Short | 500 µs | ±150 µs (te_delta) |
|
||||
| Long | 1000 µs | ±150 µs |
|
||||
| Preamble pulse | 300–700 µs | |
|
||||
| Gap | ≥1800 µs | separates preamble from data |
|
||||
| Min bits | 64 | 8 bytes |
|
||||
| Preamble | ≥10 pulses | |
|
||||
|
||||
## Frame Layout (64 bits / 8 bytes)
|
||||
|
||||
- **byte 0:** fixed marker `0x30`
|
||||
- **bytes 1–3:** serial (24-bit)
|
||||
- **byte 5:** counter
|
||||
- **byte 6:** fixed marker `0xC5`
|
||||
- **byte 7:** button
|
||||
|
||||
The CAS4 rolling cipher is left undecrypted; the two markers serve as the integrity gate.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester (Flipper table, polarity `level ? Low : High`)
|
||||
- **RF modulation:** AM/OOK
|
||||
- **Encryption:** CAS4 rolling cipher left undecrypted (no manufacturer key); gated on fixed markers `0x30`/`0xC5`
|
||||
- **Frequencies:** 433.92 MHz only
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — begin on a HIGH preamble pulse within the 300–700 µs window.
|
||||
2. **Preamble** — count preamble pulses; a long LOW gap (≥1800 µs) with ≥10 pulses enters Data.
|
||||
3. **Data** — Manchester-decode 64 bits MSB-first; at the 64th bit, require `byte[0]==0x30 && byte[6]==0xC5`, build the signal on success, and reset. An out-of-range pulse aborts.
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (the reference encoder is a non-functional stub: `yield` returns reset, `deserialize`
|
||||
returns error). Decode-only.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by a synthetic-frame check (no local capture available): a unit test Manchester-encodes a frame
|
||||
by driving the decoder's own transition table (the Flipper table is differential, so there is no
|
||||
fixed per-bit pattern), confirms it decodes with the right serial/counter/button, and confirms a frame
|
||||
with wrong marker bytes is rejected.
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Chrysler V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/chrysler_v0.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/chrysler_v0.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Chrysler/Dodge/Jeep keyfobs. PWM with a short HIGH pulse and two long-LOW symbols: a "1" payload bit is
|
||||
HIGH ≈600 µs + LOW ≈3400 µs; a "0" payload bit is HIGH ≈300 µs + LOW ≈3700 µs. ~24 preamble pairs
|
||||
(short HIGH + long_b LOW) precede each frame. 80-bit frame: the first 64 bits are `decode_data` (payload
|
||||
bytes 0–7), the last 16 bits are `data_2` (payload bytes 8,9). The 80-bit frame exceeds u64, so `data`
|
||||
reports the most-significant 64 bits and `data_count_bit = 80`.
|
||||
|
||||
Crypto is a proprietary seed-XOR (ported exactly from the reference): `seed = reverse6(key[0] >> 2)` (a
|
||||
6-bit reversed counter); `transform_block` XORs all 9 transformed bytes with `xor_table[seed & 0x0F]`,
|
||||
with an extra nibble flip when the Lock button is set. Dual payload: A (seed even, carries serial +
|
||||
counter) / B (seed odd, carries serial). The frame is gated on a structural `check_ok`, so it does not
|
||||
false-match; `crc_valid` reflects that check.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|---------|------------------------|
|
||||
| Short HIGH | 300 µs | ±150 µs (te_delta) |
|
||||
| One-short HIGH | 600 µs | "1" bit HIGH width |
|
||||
| Long LOW A | 3400 µs | ±400 µs (long_delta) |
|
||||
| Long LOW B | 3700 µs | ±400 µs |
|
||||
| Gap | 8000 µs | te_gap |
|
||||
| Frame gap | 15600 µs| |
|
||||
| Min bits | 80 | |
|
||||
| Preamble | 24 pairs| |
|
||||
|
||||
## Frame Layout (80 bits / 10 payload bytes)
|
||||
|
||||
- **byte 0:** `(reverse6(counter) << 2) | header_low2` — the seed/counter byte
|
||||
- **bytes 1–9:** transformed (XOR-masked) payload; after `transform_block`:
|
||||
- Payload A (even seed): serial = decoded[0..3], rolling counter
|
||||
- Payload B (odd seed): serial = decoded[0..2] + decoded[7]
|
||||
- Button: Lock=1, Unlock=2 (derived from structural relationships between key bytes)
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** PWM (short HIGH + dual long LOW)
|
||||
- **RF modulation:** AM
|
||||
- **Encryption:** proprietary seed-XOR (`transform_block` + `xor_table`); gated on structural `check_ok`
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — a short HIGH pulse begins the seek.
|
||||
2. **Seek** — count preamble pairs (short HIGH + long LOW); after >15 pairs, a non-short HIGH transitions to Data.
|
||||
3. **Data** — classify each HIGH/LOW pair into a payload bit (`bit ^ 1`); collect 64 bits into `decode_data` then 16 into `data_2`. At 80 bits (or on a terminating gap with >0x4F bits) run `decode_packet`, check `check_ok`, and emit.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (ENABLE_EMULATE_FEATURE). Rebuilds the 10-byte payload, re-derives seed/plaintext, applies the
|
||||
requested button, then emits preamble + dual 80-bit PWM frames (A and B) separated by frame gaps.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
|
||||
cover `reverse6` involution, `transform_block` invertibility, and a full payload-A round trip.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Ford V1 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/ford_v1.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v1.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Ford V1 uses Manchester encoding at 65/130 µs. 136 bits / 17 bytes: key1 (bytes 0–6, 56 bits) + key2
|
||||
(bytes 7–14, 64 bits) + CRC16 (bytes 15–16). Preamble ≥50 long pulses, then a short-pulse sync window
|
||||
(`sync_event_count > 2`) replays buffered Manchester events and enters the 17-byte data collection. This
|
||||
is a ROLLING-code protocol.
|
||||
|
||||
Crypto: a proprietary parity-based descrambling cipher operating on the 9-byte air block `raw[6..15]`,
|
||||
plus CRC16/CCITT (poly `0x1021`, init `0x0000`) over `raw[3..15]`. Emission is gated on CRC16 validity
|
||||
(with a 17-byte bit-inverted fallback) so it never false-matches. A strict branch
|
||||
(`decoded[3]==raw[5] && decoded[4]==raw[6]`) yields plaintext serial/button/counter; otherwise it is
|
||||
classified as encrypted/rolling (device id only). The Manchester transition table is the Flipper
|
||||
differential-Manchester table (same as Ford V0/V2).
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|--------|------------------------------|
|
||||
| Short | 65 µs | ±39 µs (te_delta) |
|
||||
| Long | 130 µs | ±39 µs (preamble uses ±40) |
|
||||
| Min bits | 136 | 17 bytes |
|
||||
| Preamble | ≥50 long pulses | |
|
||||
|
||||
## Frame Layout (136 bits / 17 bytes)
|
||||
|
||||
- **bytes 0–6:** key1 (56 bits, big-endian) → `DecodedSignal.data`
|
||||
- **bytes 6–14:** air block (9 bytes) — descrambled to plaintext
|
||||
- **bytes 15–16:** CRC16/CCITT over bytes 3–14
|
||||
|
||||
Plaintext fields (when the strict branch matches): serial = `plain[1..3] + plain[0]`, button = `plain[5]>>4`
|
||||
(Sync=0, Lock=1, Unlock=2, Trunk=4, Panic=8), counter = `((plain[5] & 0x0F) << 16) | plain[6..7]`. The
|
||||
`extra` word stashes the CRC16, the strict flag, and `plain[4]` so the encoder can rebuild the full frame.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester (Flipper differential table)
|
||||
- **RF modulation:** FM
|
||||
- **Encryption:** proprietary parity descramble cipher + CRC16/CCITT gate; rolling code
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — a long LOW pulse begins the preamble (count = 1).
|
||||
2. **Preamble** — count long pulses; after ≥50, a short pulse enters Sync.
|
||||
3. **Sync** — buffer short/long events; once `sync_event_count > 2`, replay the buffered events into Manchester and enter Data.
|
||||
4. **Data** — Manchester-decode and pack 17 bytes; at the 17th byte run `process_data` (CRC16 gate + descramble + field extraction) and emit on a valid CRC.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (6 bursts). Faithful re-encode is only possible when the original frame's plaintext was
|
||||
recovered (strict branch); the encoder rebuilds the plaintext from fields + the stashed `plain[4]`,
|
||||
re-derives the air block + CRC16, and emits 6 bursts of a 400-pair long preamble + sync + 136 Manchester
|
||||
bits, with a per-burst `pkt[4]` override.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
|
||||
cover a CRC16/XMODEM known vector, the descramble round trip, the strict-branch decode, and a full
|
||||
encode→decode at the on-air-frame level.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Ford V2 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/ford_v2.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v2.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Ford V2 uses Manchester encoding at 200/400 µs. 104 bits total (13 bytes). The frame begins with a
|
||||
16-bit Manchester sync that equals `0x7FA7` (the decoder matches the *inverted* shift register against
|
||||
`!0x7FA7`); those two sync bytes `0x7F 0xA7` head the 13-byte buffer. Decoded data bits are inverted
|
||||
before packing (`data_bit = !bit`). There is no CRC — structure is validated by the two sync bytes plus
|
||||
a known button code, so Ford V2 will not false-match.
|
||||
|
||||
The Manchester decoder uses Flipper's transition table (same table as Ford V0), with Ford V2 polarity
|
||||
`level ? High : Low`.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|--------|-----------------------------|
|
||||
| Short | 200 µs | ±260 µs (te_delta) |
|
||||
| Long | 400 µs | ±260 µs |
|
||||
| Min bits | 104 | 13 bytes |
|
||||
| Preamble | ≥64 short pulses | before the sync |
|
||||
|
||||
## Frame Layout (104 bits / 13 bytes)
|
||||
|
||||
- **bytes 0–1:** sync `0x7F 0xA7`
|
||||
- **bytes 2–5:** serial (32-bit, big-endian)
|
||||
- **byte 6:** button — valid codes `0x10` (Lock), `0x11` (Unlock), `0x13` (Trunk), `0x14` (Panic), `0x15`
|
||||
- **bytes 7–8 + byte 9 MSB:** counter = `((b7 & 0x7F) << 9) | (b8 << 1) | (b9 >> 7)`; byte 7 MSB carries a button parity bit (refreshed by the encoder)
|
||||
- **bytes 9–12:** rolling/hop tail (stashed in `extra` so the encoder can rebuild the full frame)
|
||||
|
||||
The exported `data` (Key) word is the top 8 bytes; bytes 8–12 (40 bits) are carried in `extra`.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester (Ford V2 polarity `level ? High : Low`; decoded bits inverted)
|
||||
- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`)
|
||||
- **Encryption:** none — validated by sync bytes + valid button code
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** → a short pulse (~200 µs) begins the preamble.
|
||||
2. **Preamble** — count short pulses; after ≥64 shorts a long LOW pulse enters Sync.
|
||||
3. **Sync** — Manchester-decode bits into a 16-bit shift register; when it matches `!0x7FA7`, prime the two sync bytes and enter Data.
|
||||
4. **Data** — Manchester events feed the state machine; decoded bits are inverted and packed MSB-first into 13 bytes. At 104 bits the sync bytes and button are validated and the frame committed. A non-short/long pulse (gap) ends the attempt.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (matches `subghz_protocol_encoder_ford_v2`). Rebuilds the 13-byte frame, applies the requested
|
||||
button, refreshes byte-7 parity, then emits 6 bursts of a 70-pair preamble + sync + 104 Manchester bits
|
||||
(encoder short 240 µs), separated by ~16 ms inter-burst gaps.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available).
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Ford V3 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/ford_v3.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v3.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Ford V3 uses Manchester encoding at 240/480 µs. 104 bits total (13 bytes), transmitted as plaintext —
|
||||
there is no CRC or encryption. Decode-only: the ProtoPirate reference ships a NULL encoder, so KAT
|
||||
does not transmit Ford V3 (Replay still works on the raw capture).
|
||||
|
||||
The Manchester decoder uses Flipper's `manchester_decoder.h` transition table (same table as Ford V0),
|
||||
but Ford V3 maps the level the opposite way from Ford V0: `level ? High : Low`.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|--------|--------------------|
|
||||
| Short | 240 µs | ±60 µs (te_delta) |
|
||||
| Long | 480 µs | ±60 µs |
|
||||
| Min bits | 104 | 13 bytes |
|
||||
| Preamble | ≥30 short pulses | before first long |
|
||||
|
||||
## Frame Layout (104 bits / 13 bytes)
|
||||
|
||||
- **byte 0:** header
|
||||
- **bytes 1–4:** serial (32-bit, big-endian)
|
||||
- **byte 5:** hop/reserved
|
||||
- **byte 6:** button — bit 0 set → Unlock, else Lock
|
||||
- **bytes 7–8:** counter, stored bitwise-inverted (`~b[7]`, `~b[8]`)
|
||||
- **bytes 9–12:** rolling/hop tail (not parsed)
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — a short pulse (~240 µs) starts the preamble (count = 1).
|
||||
2. **Preamble** — count short pulses; once ≥30 shorts are seen, a long pulse begins the data
|
||||
(Manchester state seeded to Mid1, first bit decoded from the long pulse).
|
||||
3. **Data** — Manchester events (short/long × level) feed the state machine; bits pack MSB-first into
|
||||
13 bytes. At 104 bits the fields are parsed and the signal is emitted. A non-short/non-long pulse
|
||||
(gap) ends the frame.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester
|
||||
- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`)
|
||||
- **Encryption:** none (plaintext)
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (reference encoder is NULL). Decode-only.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Honda Static Protocol
|
||||
|
||||
**Rust module:** `src/protocols/honda_static.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/honda_static.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Honda/Acura fixed-code keyfobs. 64-bit frame. Unlike most KAT decoders, Honda Static does NOT use the
|
||||
Flipper `manchester_decoder.h` transition table: it buffers a per-element *symbol stream* (one bit per
|
||||
~63 µs element) and then performs a custom Manchester unpack over symbol pairs. A short pulse contributes
|
||||
one symbol equal to the pulse level; a long pulse contributes two symbols of the same level. Anything
|
||||
outside both ranges (e.g. the 700 µs sync or a trailing gap) terminates the buffer and triggers a parse.
|
||||
|
||||
The checksum is an XOR of the first 7 packet bytes. Emission is gated on the checksum, so Honda Static
|
||||
will not false-match. The parser tries the inverted-Manchester interpretation first (what the encoder
|
||||
emits), then non-inverted forward, then a bit-reversed-bytes pass.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|----------------|--------|--------------------------------|
|
||||
| Element | 63 µs | one symbol per element |
|
||||
| Short pulse | 28–98 µs | base 28 µs, span 70 µs |
|
||||
| Long pulse | 61–191 µs | base 61 µs, span 130 µs (two symbols) |
|
||||
| Sync | 700 µs | terminates the symbol buffer |
|
||||
| Min symbols | 36 | before a parse is attempted |
|
||||
| Min bits | 64 | |
|
||||
|
||||
(Reported timing profile: te_short 63 µs, te_long 700 µs, te_delta 120 µs.)
|
||||
|
||||
## Frame Layout (64 bits, MSB-first into 8 bytes)
|
||||
|
||||
- **bits 0–3:** button (4-bit) — Lock=1, Unlock=2, Trunk=4, Remote Start=5, Panic=8, Lock×2=9
|
||||
- **bits 4–31:** serial (28-bit)
|
||||
- **bits 32–55:** counter (24-bit)
|
||||
- **bits 56–63:** checksum (XOR of bytes 0–6)
|
||||
|
||||
The exported `data` (Key) word is the C `generic.data`: a compact nibble-packed layout, NOT the raw
|
||||
decoded packet bytes. The KAT counter field is the low 16 bits of the 24-bit counter.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** custom symbol-stream Manchester over ~63 µs elements
|
||||
- **RF modulation:** FM
|
||||
- **Encryption:** none — gated on the XOR checksum + valid button + valid serial (serial ≠ 0 and ≠ 0x0FFFFFFF)
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **feed** — classify each pulse: short → 1 symbol, long → 2 symbols; buffer them.
|
||||
2. On an out-of-range pulse (700 µs sync or gap), if ≥36 symbols are buffered, walk past the alternating preamble + sync run, Manchester-unpack 64 bits over symbol pairs (inverted first, then non-inverted), validate, and emit. Then clear the buffer.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (matches `honda_static_build_upload`). Emits a 700 µs HIGH sync, a 160-element alternating
|
||||
preamble at 63 µs, 64 data bits (each as `!value`/`value` at 63 µs), and a trailing 700 µs sync.
|
||||
|
||||
## Validation
|
||||
|
||||
Decodes REAL IMPORTS captures (IMPORTS/honda Lock and Unlock). Also covered by encode→decode round-trip
|
||||
and packet-validate unit tests.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V7 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v7.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/kia_v7.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Kia V7 uses Manchester encoding at 250/500 µs. 64 bits. The decoded 64-bit word is bit-inverted
|
||||
(`~data`); a valid frame has a fixed header byte `0x4C` and a CRC8 (poly `0x7F`, init `0x4C`) over bytes
|
||||
0–6. Emission is gated on header + CRC, so Kia V7 is strongly validated and will not false-match.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|--------|---------------------|
|
||||
| Short | 250 µs | ±100 µs (te_delta) |
|
||||
| Long | 500 µs | ±100 µs |
|
||||
| Min bits | 64 | |
|
||||
| Preamble | ≥16 short pairs | before sync |
|
||||
|
||||
## Frame Layout (64 bits / 8 bytes, after inversion)
|
||||
|
||||
- **byte 0:** header `0x4C`
|
||||
- **bytes 1–2:** counter (16-bit, big-endian)
|
||||
- **bytes 3–6:** serial (28-bit) — `(b3<<20)|(b4<<12)|(b5<<4)|(b6>>4)`, masked to 0x0FFFFFFF
|
||||
- **byte 6 low nibble:** button (4-bit) — Lock=1, Unlock=2, Trunk=3, aux=8
|
||||
- **byte 7:** CRC8 over bytes 0–6 (poly `0x7F`, init `0x4C`)
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester (level ? (H,L) : (L,H); decoded word is bit-inverted)
|
||||
- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`)
|
||||
- **Encryption:** none — gated on fixed header `0x4C` + CRC8
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — a short HIGH pulse begins the preamble.
|
||||
2. **Preamble** — count short pairs; once >15 pairs are seen, a long HIGH pulse transitions to SyncLow and preloads four seed bits (1,0,1,1 = the inverted header's top nibble 0xB).
|
||||
3. **SyncLow** — a short LOW after the long sync enters Data.
|
||||
4. **Data** — Manchester-decode the remaining 60 bits. At 64 bits, invert the word, check the header equals `0x4C` and the CRC8 validates, then emit.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (matches `kia_v7_encode_key`). Rebuilds the 64-bit key from serial/button/counter with the
|
||||
CRC8, then emits a 32-pair short preamble, a merged long sync pulse, 64 Manchester bits, and a trailing
|
||||
gap.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available).
|
||||
@@ -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.
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Land Rover V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/land_rover_v0.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/land_rover_v0.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Land Rover V0 uses **differential** Manchester (NOT the Flipper transition table) at 250/500 µs, with a
|
||||
~750 µs sync pulse and a ≥64-pair short preamble. The frame is 81 bits = an 80-bit body (`raw[0..10]`)
|
||||
plus one trailing `extra_bit`. The 64-bit key reported as `data` is `raw[0..8]` big-endian; `raw[8..10]`
|
||||
is a 16-bit `tail`.
|
||||
|
||||
The check is a proprietary 3-bit polynomial over the counter (`calculate_check`); the tail is `0xFFFF`
|
||||
or `0x7FFF` depending on a 1-bit parity of the counter (`calculate_tail`). Emission is gated on the
|
||||
reserved bits being zero, the check matching, the tail matching, and `extra_bit` set — so Land Rover V0
|
||||
is strongly validated and will not false-match.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|--------|------------------------|
|
||||
| Short | 250 µs | ±100 µs (te_delta) |
|
||||
| Long | 500 µs | ±100 µs |
|
||||
| Sync | 750 µs | ±120 µs |
|
||||
| Min bits | 81 | 80-bit body + extra_bit|
|
||||
| Preamble | ≥64 pairs | |
|
||||
|
||||
## Frame Layout (81 bits)
|
||||
|
||||
64-bit key (`raw[0..8]`):
|
||||
|
||||
- **bytes 0–2:** 24-bit command signature — Lock = `0xC20363`, Unlock = `0xA285E3`
|
||||
- **bytes 3–5:** 24-bit serial
|
||||
- **byte 6 + byte 7 MSB:** 9-bit counter = `(b6 << 1) | (b7 >> 7)`
|
||||
- **byte 7 bits 0x78:** 3 reserved bits (must be 0)
|
||||
- **byte 7 bits 0x07:** 3-bit check (`calculate_check`)
|
||||
|
||||
Then `raw[8..10]` = 16-bit tail (`0xFFFF`/`0x7FFF`), and a trailing `extra_bit` (must be 1). The tail is
|
||||
stashed in `extra` for the encoder.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** differential Manchester
|
||||
- **RF modulation:** FM
|
||||
- **Encryption:** none — gated on the 3-bit check polynomial + tail parity + reserved-bits + extra_bit
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset → PreambleLow/PreambleHigh** — count short pairs (≥64), terminated by a ~750 µs sync HIGH.
|
||||
2. **SyncLow** — a ~750 µs sync LOW seeds the frame (bit 0 = 1) and enters Data.
|
||||
3. **Data** — the differential machine (`process_transition`/`add_decoded_bit`) tracks `previous_bit`, skips the initial boundary short-high pad, and completes short half-bits via `pending_short`. At 81 bits, `finish_frame` validates and emits.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported. Builds the 64-bit key from signature/serial/counter, emits a 319-pair preamble, the sync,
|
||||
differential-Manchester body, the 16-bit tail, and a trailing extra bit. Faithful-port note: the
|
||||
reference encoder forces frame bit 1 = 0, so the Lock signature `0xC20363` (whose bit 1 is 1) is emitted
|
||||
as `0x820363`; the Unlock signature `0xA285E3` round-trips cleanly. KAT reproduces this behaviour.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
|
||||
cover the check polynomial vs. the reference, the tail parity, Unlock round trips across 256 serial/counter
|
||||
values, the Lock forced-bit quirk, and rejection of a corrupted check.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Mazda Siemens Protocol
|
||||
|
||||
**Rust module:** `src/protocols/mazda_siemens.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/mazda_siemens.c`
|
||||
|
||||
## Overview
|
||||
|
||||
The Siemens/VDO keyfob cipher used on some Mazda vehicles — a DIFFERENT protocol from KAT's existing
|
||||
"Mazda V0" (Pandora) decoder, even though both ride a 250/500 µs pair-based stream at 433.92 MHz FM. The
|
||||
decoder is pair-based: `feed()` ignores `level` and interprets raw durations in pairs (`process_pair`),
|
||||
collecting bits with *inverted* polarity (`state_bit == 0` → stored 1). 64-bit frame.
|
||||
|
||||
Siemens obfuscation (`mazda_xor_deobfuscate`): parity-dependent XOR mask, then bit-deinterleave of bytes
|
||||
5/6. The inner Siemens cipher's plaintext is left as-is (no key); only this obfuscation/interleave layer
|
||||
is reversed. The gate is an additive checksum `sum(data[0..7]) == data[7]`, plus the structural
|
||||
preamble/sync/bit-count constraints, which keeps it from false-matching other 250/500 µs Manchester
|
||||
protocols.
|
||||
|
||||
> **Note:** KAT's existing "Mazda V0" decoder implements the same algorithm and keeps first-match
|
||||
> priority, so on a shared frame Mazda V0 fires first.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|--------|-----------------------------|
|
||||
| Short | 250 µs | ±100 µs (te_delta) |
|
||||
| Long | 500 µs | ±100 µs |
|
||||
| Min bits | 64 | |
|
||||
| Preamble | ≥13 short/short pairs | |
|
||||
| Completion | 80–105 collected bits | |
|
||||
|
||||
## Frame Layout (64 bits / 8 bytes, after deobfuscation)
|
||||
|
||||
The 14-byte buffer's leading sync byte is discarded, leaving 8 bytes:
|
||||
|
||||
- **serial:** `data >> 32` (32-bit)
|
||||
- **button:** `(data >> 24) & 0xFF` — Lock=0x10, Unlock=0x20, Trunk=0x40
|
||||
- **counter:** `(data >> 8) & 0xFFFF` (16-bit)
|
||||
- **byte 7:** additive checksum of bytes 0–6
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** pair-based Manchester (inverted polarity), 250/500 µs
|
||||
- **RF modulation:** FM
|
||||
- **Encryption:** Siemens obfuscation (parity XOR + bit-interleave); gated on the additive checksum
|
||||
- **Frequencies:** 433.92 MHz only
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — a short pulse begins the preamble.
|
||||
2. **PreambleSave/PreambleCheck** — count short/short pairs (≥13); a short→long transition seeds the leading sync bit (a 1) and enters data.
|
||||
3. **DataSave/DataCheck** — process duration pairs into bits; on a non-matching pair, run `check_completion` (discard sync byte, deobfuscate, validate additive checksum) and emit.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (`subghz_protocol_encoder_mazda_siemens_get_upload`). Increments the counter byte, recomputes
|
||||
the checksum, obfuscates (interleave + XOR), then emits a 12-byte 0xFF preamble, a 50 ms gap, `0xFF 0xFF
|
||||
0xD7`, the 8 obfuscated bytes transmitted as `255 − byte`, a `0x5A` tail, and a trailing 50 ms gap.
|
||||
Manchester per byte: bit 1 → (H,L), bit 0 → (L,H) at te_short. As in the C, the decoder and encoder are
|
||||
not a clean raw-timing round-trip pair (the encoder targets a TX upload).
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
|
||||
cover the obfuscate/deobfuscate XOR+interleave cipher layer (a true inverse, both parity branches), the
|
||||
field layout, and the TX upload smoke test.
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Porsche Cayenne Protocol
|
||||
|
||||
**Rust module:** `src/protocols/porsche_cayenne.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/porsche_cayenne.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Porsche keyfobs (internal firmware header name "Porsche AG"). PWM at 1680/3370 µs: SHORT LOW + LONG HIGH
|
||||
= bit 0, LONG LOW + SHORT HIGH = bit 1. 64-bit MSB-first frame, preceded by a 73-pulse 3370 µs sync
|
||||
preamble + a 5930 µs gap pair. The cipher is a 24-bit rotating-register VAG cipher; the counter is
|
||||
recovered by brute-forcing 1..=256 and matching the recomputed encrypted bytes (the C's validity signal).
|
||||
|
||||
> **Note:** Porsche Cayenne shares the wire protocol with KAT's existing **Porsche Touareg** decoder
|
||||
> (the Touareg port is itself a port of this same `porsche_cayenne.c` source). The two are the **same
|
||||
> wire protocol** — identical timing, preamble, 64-bit frame, VAG cipher, and validity check. Cayenne is
|
||||
> registered AFTER Touareg, so Touareg keeps first-match priority and Cayenne never steals a Touareg
|
||||
> capture. Emission is additionally gated on `frame_type` being one of the three values the C emits
|
||||
> (`0b001` Cont, `0b010` First, `0b100` Final) — a strict subset of what Touareg accepts. Cayenne adds an
|
||||
> encoder (the Touareg port is decode-only).
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|---------|------------------------|
|
||||
| Short | 1680 µs | ±500 µs (te_delta) |
|
||||
| Long | 3370 µs | ±500 µs |
|
||||
| Sync | 3370 µs | preamble pulse |
|
||||
| Gap | 5930 µs | preamble→data boundary |
|
||||
| Min bits | 64 | |
|
||||
| Sync min | 15 pairs| (firmware emits 73) |
|
||||
|
||||
## Frame Layout (64 bits / 8 bytes)
|
||||
|
||||
- **byte 0:** `(button << 4) | (frame_type & 0x07)` — button d-pad: Lock=0x01, Unlock=0x02, Trunk=0x04, Open/Panic=0x08; frame_type First=0x02, Cont=0x01, Final=0x04
|
||||
- **bytes 1–3:** serial (24-bit, big-endian)
|
||||
- **bytes 4–7:** encrypted cipher output (24-bit rotating-register VAG cipher seeded from serial, rotated `4 + counter_low` times)
|
||||
|
||||
The `extra` word carries the frame_type; `protocol_display_name` is `Porsche Cayenne [First|Cont|Final]`.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** PWM (SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1)
|
||||
- **RF modulation:** AM/OOK
|
||||
- **Encryption:** 24-bit rotating-register VAG cipher; counter recovered by brute force, `counter != 0` is the validity gate
|
||||
- **Frequencies:** 433.92 MHz, 868.35 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — wait for a LOW pulse at ~3370 µs.
|
||||
2. **Sync** — count sync pulses (HIGH and LOW at 3370 µs); a 5930 µs gap with ≥15 sync pulses enters GapHigh/GapLow.
|
||||
3. **GapHigh/GapLow** — expect the complementary 5930 µs gap pulse, then enter Data.
|
||||
4. **Data** — decode bit pairs (SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1); at 64 bits, `parse_data` applies the Cayenne frame_type gate + counter recovery and emits (or stays silent so Touareg owns the frame).
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (matches `porsche_cayenne_build_upload`). Emits a 4-frame burst (frame types 0b010/0b001/0b100/
|
||||
0b100, cipher counters cnt+1..cnt+4), each frame being 73 sync pairs + 1 gap pair + 64 MSB-first PWM bits.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by encode→decode round-trips / synthetic-frame checks (no local capture available). Unit tests
|
||||
cover the full encoder path, all three frame types, rejection of undocumented frame types and truncated
|
||||
frames, the counter-recovery limit matching the C, and shared cipher vectors with the Touareg port.
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# PSA2 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/psa2.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/psa2.c`
|
||||
|
||||
## Overview
|
||||
|
||||
PSA2 (Peugeot/Citroën — internal name "PSA OLD") is the OLDER PSA variant, distinct from KAT's existing
|
||||
`psa` (modified-TEA/XEA) decoder. Manchester encoding: 250/500 µs symbol (Pattern 1, standard rate) or
|
||||
125/250 µs (Pattern 2, half rate), using the canonical Flipper `manchester_advance` table. 128-bit frame
|
||||
= key1 (64 bits) + key2/validation word: the decoder collects 64 bits → key1, then 16 more (to 80 bits)
|
||||
→ the 16-bit validation field / key2_low.
|
||||
|
||||
Crypto: TEA (Tiny Encryption Algorithm) with a dual brute-force fallback (BF1 0x23000000–0x24000000, BF2
|
||||
0xF3000000–0xF4000000) and a mode23/mode36 selector, validated via a nibble checksum.
|
||||
|
||||
**Performance:** the live decoder only ever runs the cheap O(1) mode23 XOR path
|
||||
(`direct_xor_decrypt`) per frame — it NEVER runs the TEA brute force. The bounded TEA brute force
|
||||
(`decrypt_full`, ~16.7M iters per range) is ported faithfully but is reserved for a deferred-decrypt UI
|
||||
action, mirroring the C exactly. Beyond the C's nibble-checksum gate, KAT adds two false-positive
|
||||
suppressors required by its feed-all-decoders model: a `key2_high` precondition and a valid-button check
|
||||
(PSA2 buttons are Lock=0, Unlock=1, Trunk=2 only). Emission is gated strictly on a successful,
|
||||
field-bearing mode23 decrypt.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|----------------|--------|-----------------------------|
|
||||
| Short (P1) | 250 µs | ±100 µs (te_delta) |
|
||||
| Long (P1) | 500 µs | ±100 µs |
|
||||
| Short (P2) | 125 µs | ±50 µs (half rate) |
|
||||
| Long (P2) | 250 µs | ±50 µs |
|
||||
| End marker | 1000 µs (P1) / 500 µs (P2) | |
|
||||
| Min bits | 128 | key1 (64) + validation (16) collected |
|
||||
| Preamble | >0x46 pairs (P1) / >0x45 (P2) | |
|
||||
|
||||
## Frame Layout (128 bits = key1 64-bit + key2/validation)
|
||||
|
||||
The decoder collects 64 bits → key1, then 16 bits → the validation field (key2_low). After the mode23 XOR
|
||||
decrypt (fields from `extract_fields_mode23`):
|
||||
|
||||
- **serial:** `(buf[2]<<16) | (buf[3]<<8) | buf[4]` (24-bit)
|
||||
- **button:** `buf[8] & 0xF` — Lock=0, Unlock=1, Trunk=2
|
||||
- **counter:** `(buf[5]<<8) | buf[6]` (16-bit)
|
||||
- **crc:** `buf[7]`
|
||||
|
||||
`data` = `(key1_high << 32) | key1_low` (64 bits).
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester (canonical Flipper table; standard 250/500 µs or half-rate 125/250 µs)
|
||||
- **RF modulation:** AM (OOK)
|
||||
- **Encryption:** TEA + nibble-checksum-gated mode23 XOR path; brute-force fallback reserved for offline decrypt
|
||||
- **Frequencies:** 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **WaitEdge (State0)** — detect the preamble rate (250 µs → Pattern 1, 125 µs → Pattern 2).
|
||||
2. **CountPattern250/125 (State1/3)** — count preamble pulses past the threshold, then a long pulse enters the Manchester decode state.
|
||||
3. **DecodeManchester250/125 (State2/4)** — Manchester-decode to 80 bits (key1 + validation), detect end-of-packet, then `finalize_frame` runs the cheap mode23 XOR gate and emits only on a successful field-bearing decrypt.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (mode23 path). Increments the counter, builds key1/validation via `encode_mode23` (inverse XOR
|
||||
stage + nibble checksum), then emits an 80-pair preamble, a sync, 64 key1 bits + 16 validation bits
|
||||
MSB-first, and an end burst.
|
||||
|
||||
## Validation
|
||||
|
||||
Decodes REAL IMPORTS captures (IMPORTS/GROUPE PSA, serial `0x99EB25`). Unit tests also cover the TEA
|
||||
round trip, the mode23 encode→decode round trip, the nibble checksum vs. the reference, and the emitted
|
||||
Manchester frame length.
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Toyota Protocol
|
||||
|
||||
**Rust module:** `src/protocols/toyota.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/toyota.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Toyota / Lexus KeeLoq keyfobs, a dual-variant decoder. Two variants are detected from the first HIGH
|
||||
pulse width (threshold 310 µs):
|
||||
|
||||
- **Variant A** (Corolla / 433.92 MHz) — PWM pairs: LS (long HIGH + short LOW) = bit 0, SL (short HIGH +
|
||||
long LOW) = bit 1. Preamble = repeated short-short pairs; first non-SS pair is the first data bit.
|
||||
Frame = 68 bits.
|
||||
- **Variant B** (Tundra / 315 MHz) — NRZ: each individual pulse encodes one bit by a midpoint classifier
|
||||
(≤287 µs → 0, >287 µs → 1). Preamble = short-HIGH/long-LOW pairs terminated by a sync gap (LOW between
|
||||
1500 and 2600 µs). Frame = 67 bits.
|
||||
|
||||
KeeLoq hopping: the hop field is left encrypted (the reference does not decrypt or run a CRC). Emission
|
||||
is gated tightly (exact frame bit count + structural preamble/sync + non-zero serial) so it does not
|
||||
false-match the other KeeLoq-PWM protocols. The shared 433 MHz KeeLoq-PWM air encoding means a Variant-A
|
||||
frame that also satisfies Kia V3/V4's 68-bit/CRC4 structure is claimed by Kia V3/V4 first (earlier in the
|
||||
registry); Toyota uniquely claims 60-bit frames and Variant-B NRZ at 315 MHz.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|------------------|--------|-----------------------------|
|
||||
| A short | 400 µs | ±175 µs (Variant A) |
|
||||
| A long | 800 µs | ±175 µs |
|
||||
| B short | 200 µs | ±120 µs (Variant B preamble)|
|
||||
| B long | 390 µs | ±120 µs |
|
||||
| B NRZ midpoint | 287 µs | ≤ → 0, > → 1 |
|
||||
| B sync gap | 1500–2600 µs | |
|
||||
| Min bits | 60 | A frame = 68, B frame = 67 |
|
||||
| Variant threshold| 310 µs | first HIGH: < B, ≥ A |
|
||||
|
||||
## Frame Layout
|
||||
|
||||
`data = (hop << 32) | (serial << 4) | button` (matches the reference `generic.data`):
|
||||
|
||||
- **hop:** 32-bit KeeLoq ciphertext (left encrypted)
|
||||
- **serial:** 28-bit
|
||||
- **button:** 4-bit
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Variant A = PWM pairs; Variant B = NRZ
|
||||
- **RF modulation:** AM/OOK
|
||||
- **Encryption:** KeeLoq hop left encrypted (no key / no CRC); a fully-structured frame of the exact bit count with non-zero serial is the validity criterion
|
||||
- **Frequencies:** 433.92 MHz (Variant A), 315 MHz (Variant B)
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — detect the variant from the first SHORT HIGH (<310 µs → B, ≥310 µs → A).
|
||||
2. **Variant A** — PreambleA (count SS pairs) → DataA (decode LS/SL pairs, cap at 68 bits, emit on the terminating gap so Kia wins shared frames by registry order).
|
||||
3. **Variant B** — PreambleB (short-HIGH/long-LOW pairs) → on the sync gap → DataB (each pulse is one NRZ bit; emit at 67 bits or on an end gap).
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (the reference `encoder` field is NULL). Decode-only.
|
||||
|
||||
## Validation
|
||||
|
||||
Decodes REAL IMPORTS captures (IMPORTS Toyota + Lexus Camry, NRZ Variant B). Unit tests also cover
|
||||
synthetic Variant A and Variant B frames and rejection of an all-zero serial.
|
||||
+11
-1
@@ -1898,13 +1898,23 @@ impl App {
|
||||
match protocol {
|
||||
p if p.starts_with("Kia") => "Kia/Hyundai",
|
||||
p if p.starts_with("Ford") => "Ford",
|
||||
"Honda Static" => "Honda/Acura",
|
||||
"Honda V1" => "Honda/Acura",
|
||||
p if p.starts_with("Fiat") => "Fiat",
|
||||
"Subaru" => "Subaru",
|
||||
"Suzuki" => "Suzuki",
|
||||
"VAG" | "VW" => "VW/Audi/Seat/Skoda",
|
||||
"PSA" => "Peugeot/Citroen",
|
||||
p if p.starts_with("PSA") => "Peugeot/Citroen",
|
||||
"Star Line" => "Star Line",
|
||||
"Scher-Khan" => "Scher-Khan",
|
||||
"Chrysler V0" => "Chrysler/Dodge/Jeep",
|
||||
p if p.starts_with("Land Rover") => "Land Rover",
|
||||
"Toyota" => "Toyota/Lexus",
|
||||
// Covers both "Mazda V0" and "Mazda Siemens".
|
||||
p if p.starts_with("Mazda") => "Mazda",
|
||||
"BMW CAS4" => "BMW",
|
||||
// Covers "Porsche Touareg" and "Porsche Cayenne [First/Cont/Final]".
|
||||
p if p.starts_with("Porsche") => "Porsche",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
+39
-2
@@ -225,19 +225,32 @@ impl Capture {
|
||||
p if p.starts_with("Kia V2") => ModulationType::Manchester,
|
||||
p if p.starts_with("Kia V5") => ModulationType::Manchester,
|
||||
p if p.starts_with("Kia V6") => ModulationType::Manchester,
|
||||
"Ford V0" => ModulationType::Manchester,
|
||||
p if p.starts_with("Kia V7") => ModulationType::Manchester,
|
||||
p if p.starts_with("Ford") => ModulationType::Manchester,
|
||||
"Honda Static" => ModulationType::Manchester,
|
||||
"Mazda Siemens" => ModulationType::Manchester,
|
||||
"BMW CAS4" => ModulationType::Manchester,
|
||||
"Fiat V0" => ModulationType::Manchester,
|
||||
"PSA" => ModulationType::Manchester,
|
||||
"PSA2" => ModulationType::Manchester,
|
||||
"VAG" => ModulationType::Manchester,
|
||||
// Differential Manchester
|
||||
"Land Rover V0" => ModulationType::DifferentialManchester,
|
||||
// PWM-encoded protocols
|
||||
p if p.starts_with("Kia V0") => ModulationType::Pwm,
|
||||
p if p.starts_with("Kia V3") => ModulationType::Pwm,
|
||||
p if p.starts_with("Kia V4") => ModulationType::Pwm,
|
||||
"Honda V1" => ModulationType::Pwm,
|
||||
"Subaru" => ModulationType::Pwm,
|
||||
"Suzuki" => ModulationType::Pwm,
|
||||
"Star Line" => ModulationType::Pwm,
|
||||
p if p.starts_with("Keeloq (") => ModulationType::Pwm,
|
||||
"Scher-Khan" => ModulationType::Pwm,
|
||||
"Chrysler V0" => ModulationType::Pwm,
|
||||
"Land Rover RKE" => ModulationType::Pwm,
|
||||
"Toyota" => ModulationType::Pwm,
|
||||
// Display name is "Porsche Cayenne [First/Cont/Final]"; match the prefix.
|
||||
p if p.starts_with("Porsche Cayenne") => ModulationType::Pwm,
|
||||
// Unknown
|
||||
_ => ModulationType::Unknown,
|
||||
}
|
||||
@@ -252,16 +265,27 @@ impl Capture {
|
||||
p if p.starts_with("Kia V2") => RfModulation::FM,
|
||||
p if p.starts_with("Kia V5") => RfModulation::FM,
|
||||
p if p.starts_with("Kia V6") => RfModulation::FM,
|
||||
p if p.starts_with("Kia V7") => RfModulation::FM,
|
||||
"Scher-Khan" => RfModulation::FM,
|
||||
"PSA" => RfModulation::FM,
|
||||
"PSA2" => RfModulation::AM,
|
||||
"Fiat V0" => RfModulation::FM,
|
||||
"Ford V0" => RfModulation::FM,
|
||||
p if p.starts_with("Ford") => RfModulation::FM,
|
||||
"Honda Static" => RfModulation::FM,
|
||||
"Mazda Siemens" => RfModulation::FM,
|
||||
"Land Rover V0" => RfModulation::FM,
|
||||
// AM only (SubGhzProtocolFlag_AM)
|
||||
p if p.starts_with("Kia V1") => RfModulation::AM,
|
||||
"Honda V1" => RfModulation::AM,
|
||||
"VAG" => RfModulation::AM,
|
||||
"Subaru" => RfModulation::AM,
|
||||
"Suzuki" => RfModulation::AM,
|
||||
"Star Line" => RfModulation::AM,
|
||||
"Chrysler V0" => RfModulation::AM,
|
||||
"Land Rover RKE" => RfModulation::AM,
|
||||
"Toyota" => RfModulation::AM,
|
||||
"BMW CAS4" => RfModulation::AM,
|
||||
p if p.starts_with("Porsche Cayenne") => RfModulation::AM,
|
||||
p if p.starts_with("Keeloq (") => RfModulation::AM,
|
||||
// Both AM and FM (Kia V3/V4)
|
||||
p if p.starts_with("Kia V3") || p.starts_with("Kia V4") => RfModulation::Both,
|
||||
@@ -276,14 +300,27 @@ impl Capture {
|
||||
"Star Line" => "KeeLoq",
|
||||
p if p.starts_with("Keeloq (") => "KeeLoq",
|
||||
"PSA" => "XTEA/XOR",
|
||||
"PSA2" => "TEA",
|
||||
"VAG" => "AUT64/XTEA",
|
||||
"Scher-Khan" => "Magic Code",
|
||||
"Subaru" | "Suzuki" => "Rolling Code",
|
||||
"Chrysler V0" => "Rolling Code",
|
||||
"Land Rover V0" => "Rolling Code",
|
||||
"Land Rover RKE" => "KeeLoq",
|
||||
"Toyota" => "KeeLoq",
|
||||
"Mazda Siemens" => "Siemens XOR",
|
||||
"BMW CAS4" => "CAS4 (rolling)",
|
||||
p if p.starts_with("Porsche Cayenne") => "VAG rolling",
|
||||
// Ford V1 is rolling code; must precede the generic Ford "Fixed Code" arm below.
|
||||
"Ford V1" => "Rolling Code",
|
||||
p if p.starts_with("Ford") => "Fixed Code",
|
||||
"Honda Static" => "Fixed Code",
|
||||
"Honda V1" => "Fixed Code",
|
||||
p if p.starts_with("Fiat") => "Fixed Code",
|
||||
p if p.starts_with("Kia V0") => "Fixed Code",
|
||||
p if p.starts_with("Kia V1") || p.starts_with("Kia V2") => "Fixed Code",
|
||||
p if p.starts_with("Kia V5") || p.starts_with("Kia V6") => "Fixed Code",
|
||||
p if p.starts_with("Kia V7") => "Fixed Code",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,9 @@ fn g5(x: u32, a: u32, b: u32, c: u32, d: u32, e: u32) -> u32 {
|
||||
pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
|
||||
let mut x = data;
|
||||
for r in 0..528u32 {
|
||||
let key_bit = ((key >> ((15 - r) & 63)) & 1) as u32;
|
||||
// (15 - r) relies on unsigned wraparound (matches the C reference `(15 - r) & 63`);
|
||||
// use wrapping_sub so debug builds don't panic on the overflow that release silently wraps.
|
||||
let key_bit = ((key >> ((15u32.wrapping_sub(r)) & 63)) & 1) as u32;
|
||||
let new_lsb = bit(x, 31) ^ bit(x, 15) ^ key_bit
|
||||
^ bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30));
|
||||
x = (x << 1) ^ new_lsb;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,14 @@ mod kia_v2;
|
||||
mod kia_v3_v4;
|
||||
mod kia_v5;
|
||||
mod kia_v6;
|
||||
mod kia_v7;
|
||||
mod subaru;
|
||||
mod ford_v0;
|
||||
mod ford_v1;
|
||||
mod ford_v2;
|
||||
mod ford_v3;
|
||||
mod honda_static;
|
||||
mod honda_v1;
|
||||
mod vag;
|
||||
mod fiat_v0;
|
||||
mod fiat_v1;
|
||||
@@ -41,9 +47,17 @@ mod suzuki;
|
||||
mod scher_khan;
|
||||
mod star_line;
|
||||
mod psa;
|
||||
mod psa2;
|
||||
mod chrysler_v0;
|
||||
mod mazda_v0;
|
||||
mod mazda_siemens;
|
||||
mod mitsubishi_v0;
|
||||
mod porsche_touareg;
|
||||
mod porsche_cayenne;
|
||||
mod bmw_cas4;
|
||||
mod land_rover_v0;
|
||||
mod land_rover_rke;
|
||||
mod toyota;
|
||||
|
||||
pub use common::DecodedSignal;
|
||||
|
||||
@@ -108,9 +122,20 @@ impl ProtocolRegistry {
|
||||
Box::new(kia_v3_v4::KiaV3V4Decoder::new()),
|
||||
Box::new(kia_v5::KiaV5Decoder::new()),
|
||||
Box::new(kia_v6::KiaV6Decoder::new()),
|
||||
Box::new(kia_v7::KiaV7Decoder::new()),
|
||||
// VAG before Ford/Subaru so 500/1000µs VAG streams decode as VAG (ProtoPirate order has VAG after Ford/Subaru but Flipper likely feeds all decoders; KAT uses first-match so VAG must be tried earlier)
|
||||
Box::new(vag::VagDecoder::new()),
|
||||
Box::new(ford_v0::FordV0Decoder::new()),
|
||||
// Ford V1: distinct 65/130µs Manchester (vs V0 250/500, V2 200/400, V3 240/480) and
|
||||
// CRC16-gated, so placement in the Ford group is safe (won't steal V0/V2/V3 captures).
|
||||
Box::new(ford_v1::FordV1Decoder::new()),
|
||||
Box::new(ford_v3::FordV3Decoder::new()),
|
||||
Box::new(ford_v2::FordV2Decoder::new()),
|
||||
// Honda Static after the Kia/Ford block; checksum-gated emission makes order safe.
|
||||
Box::new(honda_static::HondaStaticDecoder::new()),
|
||||
// Honda V1: distinct 1000/2000µs PWM (vs Honda Static's 63µs Manchester). Button-gated
|
||||
// emission makes order safe.
|
||||
Box::new(honda_v1::HondaV1Decoder::new()),
|
||||
Box::new(subaru::SubaruDecoder::new()),
|
||||
Box::new(fiat_v0::FiatV0Decoder::new()),
|
||||
Box::new(fiat_v1::FiatV1Decoder::new()),
|
||||
@@ -119,9 +144,61 @@ impl ProtocolRegistry {
|
||||
Box::new(star_line::StarLineDecoder::new()),
|
||||
Box::new(keeloq::KeeloqDecoder::new()),
|
||||
Box::new(psa::PsaDecoder::new()),
|
||||
// PSA2 (Flipper-ARF, internal name "PSA OLD"): the OLDER PSA variant. Manchester
|
||||
// 250/500µs (or 125/250µs half-rate), 128-bit frame (key1 64 + validation 16), AM/OOK,
|
||||
// TEA cipher with a mode23 XOR fast-path validated by a nibble checksum (the live
|
||||
// decoder runs ONLY this O(1) path — the dual TEA brute force is reserved for offline
|
||||
// decrypt and never runs in feed(), exactly as in the C). Placed AFTER `psa` so the
|
||||
// existing PSA keeps first-match priority and PSA2 acts as the older fallback. Emission
|
||||
// is gated on the XOR checksum (or validation nibble == 0xA), so it false-matches
|
||||
// nothing else.
|
||||
Box::new(psa2::Psa2Decoder::new()),
|
||||
// Chrysler V0 (300/3400-3700µs PWM, dual-long symbols). Timing is unique and emission
|
||||
// is gated on the frame's own structural check, so placement here is low-risk.
|
||||
Box::new(chrysler_v0::ChryslerV0Decoder::new()),
|
||||
// Land Rover V0 (differential Manchester 250/500µs, 81 bits). Emission is gated on a
|
||||
// 3-bit check polynomial + 16-bit tail + zero reserved bits, so placement is safe.
|
||||
Box::new(land_rover_v0::LandRoverV0Decoder::new()),
|
||||
// Land Rover RKE (Flipper-ARF): fixed-width PWM (700/300µs bits), 20-pulse preamble,
|
||||
// a distinctive 400µs+9600µs sync gap, and exactly 66 bits. KeeLoq hop is left
|
||||
// encrypted (no key), so emission is gated on the preamble + 9.6ms sync + 66-bit
|
||||
// strict-PWM geometry — unique among KAT protocols, so it false-matches nothing.
|
||||
Box::new(land_rover_rke::LandRoverRkeDecoder::new()),
|
||||
Box::new(mazda_v0::MazdaV0Decoder::new()),
|
||||
// Mazda Siemens (Flipper-ARF): the Siemens/VDO keyfob cipher. It rides the SAME
|
||||
// 250/500µs pair-based stream as Mazda V0 and uses the same additive-checksum gate,
|
||||
// so the two would match an identical set of frames. Placed AFTER mazda_v0 so Mazda
|
||||
// V0 keeps first-match priority and Mazda Siemens can never steal its captures.
|
||||
// Emission is gated on the structural preamble/sync/bit-count + checksum, so it does
|
||||
// not false-match other 250/500µs Manchester protocols. Adds an encoder (Mazda V0
|
||||
// has none).
|
||||
Box::new(mazda_siemens::MazdaSiemensDecoder::new()),
|
||||
// BMW CAS4 (Flipper-ARF): Manchester 500/1000µs, 64-bit, AM. The CAS4 rolling cipher's
|
||||
// manufacturer key is unavailable, so the payload is left encrypted — emission is gated
|
||||
// on the two fixed marker bytes (byte[0]==0x30 && byte[6]==0xC5), which is unique and
|
||||
// makes it false-match nothing. Decode-only (the reference encoder is a stub). Marker-
|
||||
// gated, so registry order here is safe.
|
||||
Box::new(bmw_cas4::BmwCas4Decoder::new()),
|
||||
Box::new(mitsubishi_v0::MitsubishiV0Decoder::new()),
|
||||
Box::new(porsche_touareg::PorscheTouaregDecoder::new()),
|
||||
// Porsche Cayenne (Flipper-ARF, internal name "Porsche AG"): the SAME wire protocol
|
||||
// as Porsche Touareg — Touareg is itself a port of this very porsche_cayenne.c source,
|
||||
// so they share the identical 1680/3370µs PWM, 73-pulse preamble + 5930µs gap, 64-bit
|
||||
// MSB-first frame, 24-bit VAG rotating-register cipher, and counter-recovery validity
|
||||
// gate. Placed AFTER porsche_touareg so Touareg keeps first-match priority and Cayenne
|
||||
// can NEVER steal a Touareg capture (the registry reports the first decoder that fires
|
||||
// on a given pulse). Cayenne adds the encoder (the Touareg port is decode-only) and is
|
||||
// additionally gated on the frame_type being one of the three values the C emits/labels
|
||||
// (0b001/0b010/0b100) — a strict subset of what Touareg accepts — so on any shared
|
||||
// frame Touareg also fires and wins by ordering.
|
||||
Box::new(porsche_cayenne::PorscheCayenneDecoder::new()),
|
||||
// Toyota/Lexus (Flipper-ARF): dual-variant KeeLoq. Variant A is KeeLoq-PWM at 433 MHz
|
||||
// and shares its air encoding with Kia V3/V4 — so Toyota MUST stay AFTER kia_v3_v4
|
||||
// (which is in the Kia block near the top) to preserve Kia's first-match priority on
|
||||
// the shared PWM frames. Toyota uniquely claims 60-bit frames and the Variant-B NRZ
|
||||
// stream at 315 MHz. Emission is gated on the exact frame bit count + structural
|
||||
// preamble/sync + non-zero serial, so it false-matches nothing else.
|
||||
Box::new(toyota::ToyotaDecoder::new()),
|
||||
];
|
||||
|
||||
Self { decoders }
|
||||
@@ -343,4 +420,208 @@ mod tests {
|
||||
results.iter().map(|(n, _, _)| n.as_str()).collect::<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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,919 @@
|
||||
//! PSA2 (Peugeot/Citroën — "PSA OLD") protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with Flipper-ARF reference: `lib/subghz/protocols/psa2.c` and `psa2.h`
|
||||
//! (internal name `SUBGHZ_PROTOCOL_PSA2_NAME = "PSA OLD"`). This is the OLDER PSA variant,
|
||||
//! distinct from KAT's existing `psa` (modified-TEA/XEA) decoder.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 250/500µs symbol (Pattern 1, standard rate) or 125/250µs (Pattern 2,
|
||||
//! half rate). Canonical Flipper `manchester_advance` table (events ShortLow=0, ShortHigh=2,
|
||||
//! LongLow=4, LongHigh=6), seeded `ManchesterStateMid1`.
|
||||
//! - 128-bit frame = key1 (64 bits) + key2/validation word. The decoder collects 64 bits → key1,
|
||||
//! then 16 more (to 80 bits = `KEY2_BITS`) → the 16-bit validation field / key2_low.
|
||||
//! - RF: AM (OOK). Frequency: 433.92 MHz.
|
||||
//! - Crypto: TEA (Tiny Encryption Algorithm) with a dual brute-force fallback (BF1
|
||||
//! 0x23000000–0x24000000, BF2 0xF3000000–0xF4000000) and a mode23/mode36 selector, validated
|
||||
//! via a nibble checksum.
|
||||
//!
|
||||
//! ## Decoder structure & PERFORMANCE
|
||||
//! The C live decoder (`subghz_protocol_decoder_psa2_feed`) only ever runs the cheap mode23 XOR
|
||||
//! path (`psa_decrypt_fast`) per frame — it NEVER runs the TEA brute force. The brute force
|
||||
//! (`psa_decrypt_full`, marked `__attribute__((unused))`) is reserved for the deferred-decrypt
|
||||
//! UI button, not the streaming decoder. KAT mirrors this exactly. Per frame:
|
||||
//!
|
||||
//! 1. Manchester-collect to exactly 80 bits (key1 + validation) with end-of-packet detection.
|
||||
//! 2. Run the O(1) `direct_xor_decrypt` (mode23) gated on its nibble checksum.
|
||||
//! 3. Emit only on a successful, field-bearing decrypt (see `finalize_frame` for why the C's bare
|
||||
//! `(validation & 0xF) == 0xA` emission is not reproduced in KAT's feed-all model).
|
||||
//!
|
||||
//! The bounded TEA brute force (BF1/BF2, ~16.7M iters each) is ported faithfully in
|
||||
//! `Psa2Decoder::decrypt_full` but is NEVER called from `feed()` — only the cheap O(1) gate runs
|
||||
//! per pulse, so the test sweep stays fast.
|
||||
//!
|
||||
//! Decoder steps: State0 (wait preamble) → State1/State3 (count preamble pulses) →
|
||||
//! State2/State4 (Manchester decode + decrypt). Encoder supported (mode23 path).
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
// Standard-rate timings (Pattern 1)
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 128;
|
||||
|
||||
// Half-rate timings (Pattern 2 / State 3-4)
|
||||
const TE_SHORT_HALF: u32 = 125;
|
||||
const TE_LONG_HALF: u32 = 250;
|
||||
const TOL_HALF: u32 = 50;
|
||||
|
||||
// End-of-packet markers
|
||||
const TE_END_1000: u32 = 1000;
|
||||
const TE_END_500: u32 = 500;
|
||||
|
||||
// Bit counts
|
||||
const KEY1_BITS: usize = 64; // 0x40
|
||||
const KEY2_BITS: usize = 80; // 0x50
|
||||
const MAX_BITS: usize = 121; // 0x79
|
||||
|
||||
// Preamble pulse-count thresholds (C: PSA_PATTERN_THRESHOLD_1/2)
|
||||
const PATTERN_THRESHOLD_1: u16 = 0x46;
|
||||
const PATTERN_THRESHOLD_2: u16 = 0x45;
|
||||
|
||||
// Validation nibble for the mode23 path: (validation_field & 0xF) == 0xA
|
||||
const VALID_NIBBLE: u16 = 0xA;
|
||||
|
||||
// "decrypted" success marker (C uses 0x50)
|
||||
const DECRYPTED_OK: u16 = 0x50;
|
||||
|
||||
// Mode selectors (stored as ASCII chars in firmware)
|
||||
const MODE_23: u8 = 0x23; // '#'
|
||||
const MODE_36: u8 = 0x36; // '6'
|
||||
|
||||
// PSA2 button codes are Lock=0, Unlock=1, Trunk=2 (psa_button_name). Decodes whose 4-bit button
|
||||
// exceeds this are coincidental matches on unrelated Manchester data and are rejected.
|
||||
const BTN_MAX_VALID: u8 = 0x2;
|
||||
|
||||
// TEA constants
|
||||
const TEA_DELTA: u32 = 0x9E3779B9;
|
||||
const TEA_ROUNDS: u32 = 32;
|
||||
|
||||
// BF1 brute-force range + constants (FUN_08028f94 / FUN_080291c0)
|
||||
const BF1_START: u32 = 0x2300_0000;
|
||||
const BF1_END: u32 = 0x2400_0000;
|
||||
const BF1_CONST_U4: u32 = 0x0E0F_5C41;
|
||||
const BF1_CONST_U5: u32 = 0x0F5C_4123;
|
||||
const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A43_4915, 0xD674_3C2B, 0x1F29_D308, 0xE6B7_9A64];
|
||||
|
||||
// BF2 brute-force range + key schedule (FUN_080290f8)
|
||||
const BF2_START: u32 = 0xF300_0000;
|
||||
const BF2_END: u32 = 0xF400_0000;
|
||||
const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039_C240, 0xEDA9_2CAB, 0x4306_C02A, 0x0219_2A04];
|
||||
|
||||
/// Canonical Flipper Manchester states (lib/toolbox/manchester_decoder.c order:
|
||||
/// Start1=0, Mid1=1, Mid0=2, Start0=3).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Start1 = 0,
|
||||
Mid1 = 1,
|
||||
Mid0 = 2,
|
||||
Start0 = 3,
|
||||
}
|
||||
|
||||
/// Decoder states (matches PSADecoderState0-4 in psa2.c).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderState {
|
||||
/// State0: wait for preamble start.
|
||||
WaitEdge,
|
||||
/// State1: count standard-rate (250µs) preamble pulses.
|
||||
CountPattern250,
|
||||
/// State2: receive key1 + key2/validation at standard rate.
|
||||
DecodeManchester250,
|
||||
/// State3: count half-rate (125µs) preamble pulses.
|
||||
CountPattern125,
|
||||
/// State4: receive key1 + key2/validation at half rate.
|
||||
DecodeManchester125,
|
||||
}
|
||||
|
||||
/// Result of a successful decrypt: (serial, button, counter, crc, type).
|
||||
type DecryptResult = (u32, u8, u32, u16, u8);
|
||||
|
||||
/// PSA2 ("PSA OLD") protocol decoder.
|
||||
pub struct Psa2Decoder {
|
||||
state: DecoderState,
|
||||
prev_duration: u32,
|
||||
manchester_state: ManchesterState,
|
||||
pattern_counter: u16,
|
||||
data_low: u32,
|
||||
data_high: u32,
|
||||
bit_count: usize,
|
||||
// Decoded fields
|
||||
key1_low: u32,
|
||||
key1_high: u32,
|
||||
validation_field: u16,
|
||||
key2_low: u32,
|
||||
key2_high: u32,
|
||||
}
|
||||
|
||||
impl Psa2Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: DecoderState::WaitEdge,
|
||||
prev_duration: 0,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
pattern_counter: 0,
|
||||
data_low: 0,
|
||||
data_high: 0,
|
||||
bit_count: 0,
|
||||
key1_low: 0,
|
||||
key1_high: 0,
|
||||
validation_field: 0,
|
||||
key2_low: 0,
|
||||
key2_high: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn near(dur: u32, target: u32, tol: u32) -> bool {
|
||||
duration_diff!(dur, target) <= tol
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Manchester — canonical Flipper transition table (transitions[] in
|
||||
// manchester_decoder.c). `event` ∈ {0,2,4,6}; returns Some(bit) on emit.
|
||||
// =========================================================================
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<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 0x23000000–0x24000000 (FUN_08028f94).
|
||||
///
|
||||
/// PERFORMANCE: up to ~16.7M TEA iterations. NEVER called from `feed()`. Only invoked from
|
||||
/// [`Self::decrypt_full`], which itself runs only after the cheap structural gate. Returns
|
||||
/// `(result, seed)` on success.
|
||||
fn brute_force_decrypt_bf1(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<(DecryptResult, u32)> {
|
||||
let mut buf = [0u8; 48];
|
||||
Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low);
|
||||
let (w0, w1) = Self::prepare_tea_data(&buf);
|
||||
|
||||
for counter in BF1_START..BF1_END {
|
||||
// Derive the working key with two TEA encrypts.
|
||||
let (mut wk2, mut wk3) = (BF1_CONST_U4, counter);
|
||||
Self::tea_encrypt(&mut wk2, &mut wk3, &BF1_KEY_SCHEDULE);
|
||||
let (mut wk0, mut wk1) = ((counter << 8) | 0x0E, BF1_CONST_U5);
|
||||
Self::tea_encrypt(&mut wk0, &mut wk1, &BF1_KEY_SCHEDULE);
|
||||
let wkey = [wk0, wk1, wk2, wk3];
|
||||
|
||||
let (mut dv0, mut dv1) = (w0, w1);
|
||||
Self::tea_decrypt(&mut dv0, &mut dv1, &wkey);
|
||||
|
||||
if (counter & 0xFFFFFF) == (dv0 >> 8) {
|
||||
let crc = Self::calculate_tea_crc(dv0, dv1);
|
||||
if crc == (dv1 & 0xFF) as u8 {
|
||||
let mut out = [0u8; 48];
|
||||
Self::unpack_tea_result(&mut out, dv0, dv1);
|
||||
return Some((Self::extract_fields_mode36(&out), counter));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// BF2 brute force, range 0xF3000000–0xF4000000 (FUN_080290f8).
|
||||
///
|
||||
/// PERFORMANCE: up to ~16.7M TEA iterations. NEVER called from `feed()` (see BF1 note).
|
||||
fn brute_force_decrypt_bf2(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<(DecryptResult, u32)> {
|
||||
let mut buf = [0u8; 48];
|
||||
Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low);
|
||||
let (w0, w1) = Self::prepare_tea_data(&buf);
|
||||
|
||||
for counter in BF2_START..BF2_END {
|
||||
let wkey = [
|
||||
BF2_KEY_SCHEDULE[0] ^ counter,
|
||||
BF2_KEY_SCHEDULE[1] ^ counter,
|
||||
BF2_KEY_SCHEDULE[2] ^ counter,
|
||||
BF2_KEY_SCHEDULE[3] ^ counter,
|
||||
];
|
||||
let (mut dv0, mut dv1) = (w0, w1);
|
||||
Self::tea_decrypt(&mut dv0, &mut dv1, &wkey);
|
||||
|
||||
if (counter & 0xFFFFFF) == (dv0 >> 8) {
|
||||
let crc_buf = [
|
||||
(dv0 >> 24) as u8,
|
||||
(dv0 >> 16) as u8,
|
||||
(dv0 >> 8) as u8,
|
||||
dv0 as u8,
|
||||
(dv1 >> 24) as u8,
|
||||
(dv1 >> 16) as u8,
|
||||
];
|
||||
let crc16 = Self::calculate_crc16_bf2(&crc_buf);
|
||||
let expected = ((dv1 & 0xFF) | (((dv1 >> 16) & 0xFF) << 8)) as u16;
|
||||
if crc16 == expected {
|
||||
let mut out = [0u8; 48];
|
||||
Self::unpack_tea_result(&mut out, dv0, dv1);
|
||||
return Some((Self::extract_fields_mode36(&out), counter));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Full decrypt router (FUN_080291c0, the `__attribute__((unused))` `psa_decrypt_full`):
|
||||
/// try XOR (mode23), then BF1, then BF2.
|
||||
///
|
||||
/// PERFORMANCE: this can run the bounded brute force (~33M TEA iterations worst case). It is
|
||||
/// NOT part of the live `feed()` path — `feed()` uses only `direct_xor_decrypt`. This mirrors
|
||||
/// the C, where `psa_decrypt_full` is unused by the decoder and the feed callback calls
|
||||
/// `psa_decrypt_fast` (XOR only). Exposed for completeness / offline decrypt; gate any caller
|
||||
/// behind the structural frame check so the brute force never runs on arbitrary data.
|
||||
#[allow(dead_code)]
|
||||
fn decrypt_full(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<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));
|
||||
}
|
||||
}
|
||||
@@ -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 60–67-bit Variant-A frames that Kia rejects. Bit accumulation is capped
|
||||
/// at 68 so the field layout is preserved regardless of trailing repeats.
|
||||
fn feed_variant_a(&mut self, level: bool, duration: u32) -> Option<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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user