4 Commits

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