Compare commits
15 Commits
v1.1.2
..
754af4134a
| Author | SHA1 | Date | |
|---|---|---|---|
| 754af4134a | |||
| 118eca0948 | |||
| 3c336e0a57 | |||
| cea473b654 | |||
| 48ee413c9b | |||
| 84b75ee6b5 | |||
| 295e4c4d4a | |||
| a58d13362a | |||
| 23431c4a7a | |||
| c0de9effc8 | |||
| bd19816e91 | |||
| 5d0bd97954 | |||
| 334491db4e | |||
| 39944e072b | |||
| 9e90f88a15 |
@@ -0,0 +1,43 @@
|
||||
# Build and deploy Jekyll site to GitHub Pages
|
||||
name: Deploy to Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
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
|
||||
|
||||
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
|
||||
@@ -2,6 +2,42 @@
|
||||
|
||||
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
|
||||
|
||||
- **Command field** — Capture metadata (press **i**) and .fob export now include **Command** (e.g. Unlock, Lock, Trunk, Panic). Export filename for unknown protocol uses Year_Make_Model_Region_Command; 8-hex suffix is shown in the filename field and saved in uppercase (e.g. `…_A1B2C3D4.fob`). .fob vehicle info and import support optional `command`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Unknown signals** — Shown by default (`research_mode` default is now `true`). Config comment and storage docs clarify that no keystore directory is used or created (keys are embedded).
|
||||
- **Signal separation** — End-of-signal gap increased from 20 ms to **80 ms** so one button press (multiple bursts with 25–50 ms gaps) produces a single capture instead of 3–4.
|
||||
- **Short signals** — Demodulator now emits captures with **5+** level-duration pairs (was 10), so short or weak unknown keyfob bursts are no longer dropped (RSSI spike but no capture).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Export filename** — Unknown-protocol .fob exports always get the 8-hex suffix (including when filename ends with `Unknown`); suffix is uppercase.
|
||||
|
||||
---
|
||||
|
||||
## [1.1.2] - 2026-02-20
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+1
-3
@@ -444,7 +444,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kat"
|
||||
version = "1.1.1"
|
||||
version = "1.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atty",
|
||||
@@ -482,8 +482,6 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
[[package]]
|
||||
name = "libhackrf"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e640122f67f490129cd3ab1517149b4091eeca0a14de3df7510626df258196b9"
|
||||
dependencies = [
|
||||
"num-complex",
|
||||
]
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kat"
|
||||
version = "1.1.2"
|
||||
version = "1.3.0"
|
||||
edition = "2021"
|
||||
description = "Keyfob Analysis Toolkit - HackRF/RTL-SDR signal capture, decode, and transmit (HackRF only)"
|
||||
authors = ["KAT Team"]
|
||||
@@ -54,6 +54,10 @@ atty = "0.2"
|
||||
[build-dependencies]
|
||||
pkg-config = "0.3"
|
||||
|
||||
[patch.crates-io]
|
||||
# Vendored with ARM fix: c_char is u8 on ARM, so the upstream transmute fails
|
||||
libhackrf = { path = "vendor/libhackrf" }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
|
||||
@@ -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** — 14 protocol decoders: Kia V0–V6, Ford V0, Fiat V0, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions
|
||||
- **Multi-protocol decoding** — 32 protocol decoders: Kia V0–V7, Ford V0–V3, Chrysler V0 (Dodge/Jeep), Honda Static, Honda V1, Toyota (Lexus), Land Rover V0, Land Rover RKE, Fiat V0/V1, Mazda V0, Mazda Siemens, Mitsubishi V0, BMW CAS4, Porsche Touareg, Porsche Cayenne, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, PSA2, Scher-Khan, Star Line; adaptive demodulation for real-world conditions
|
||||
- **KeeLoq generic fallback** — when a signal doesn’t match any known protocol, KAT tries decoding it as KeeLoq using every manufacturer key in the embedded keystore (Kia V3/V4 and Star Line air formats); successful decodes appear as **Keeloq (keystore name)** in the capture list
|
||||
- **RF modulation metadata** — each protocol tagged as AM, FM, or both (from ProtoPirate); shown in signal detail and exported in .fob
|
||||
- **Rich signal detail** — encoding (PWM/Manchester), RF (AM/FM), encryption, serial, counter, key data, CRC, frequency, and raw level/duration pairs
|
||||
@@ -119,11 +119,12 @@ Press `Enter` on a capture to open the action menu. When using RTL-SDR (receive-
|
||||
|
||||
When exporting to `.fob`, a metadata form collects filename and optional vehicle info:
|
||||
|
||||
- **File** — output filename (extension added by format)
|
||||
- **File** — output filename (extension added by format). For unknown protocol, a unique 8-character hex suffix (e.g. `A1B2C3D4`) is shown in the field and appended to the filename so each export has a distinct file.
|
||||
- **Year** — vehicle model year
|
||||
- **Make** — manufacturer (auto-suggested from protocol)
|
||||
- **Model** — vehicle model
|
||||
- **Region** — region/market
|
||||
- **Command** — button/command label (e.g. Unlock, Lock, Trunk, Panic); used in the default filename for unknown protocol and stored in the .fob vehicle section.
|
||||
- **Notes** — free-form notes
|
||||
|
||||
The exported `.fob` file is a versioned JSON document (v2.0, format `kat-fob`) containing:
|
||||
@@ -153,17 +154,22 @@ The exported `.fob` file is a versioned JSON document (v2.0, format `kat-fob`) c
|
||||
"year": 2023,
|
||||
"make": "Kia",
|
||||
"model": "Sportage",
|
||||
"region": "",
|
||||
"region": "NA",
|
||||
"command": "Lock",
|
||||
"notes": ""
|
||||
},
|
||||
"capture": {
|
||||
"timestamp": "2026-02-07T12:00:00Z",
|
||||
"raw_data_hex": "0x...",
|
||||
"raw_pair_count": 0,
|
||||
"raw_pairs": [{"level": true, "duration_us": 400}, {"level": false, "duration_us": 800}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`rf_modulation` is AM, FM, or AM/FM per protocol (from ProtoPirate). Raw pairs are included when config `include_raw_pairs` is true.
|
||||
- **vehicle.command** — optional; user-editable command label (e.g. Unlock, Lock). Set in the export form or via **i** (capture metadata). Used for unknown-protocol export filenames (`Year_Make_Model_Region_Command_8HEX.fob`).
|
||||
- **rf_modulation** — AM, FM, or AM/FM per protocol (from ProtoPirate). Omitted when unknown.
|
||||
- **capture** — `raw_data_hex`, `raw_pair_count`, and optionally `raw_pairs` when config `include_raw_pairs` is true.
|
||||
|
||||
### VIM-Style Commands
|
||||
|
||||
@@ -213,7 +219,7 @@ include_raw_pairs = true
|
||||
```
|
||||
|
||||
- **import_directory** — directory scanned at startup for .fob and .sub files to import (default `~/.config/KAT/import`). Exports are still saved to **export_directory**.
|
||||
- **research_mode** — when `false` (default), only successfully decoded signals appear in the list; when `true`, unknown (unidentified) signals are also shown.
|
||||
- **research_mode** — when `true` (default), unknown (unidentified) signals are shown in addition to decoded ones; when `false`, only successfully decoded signals appear.
|
||||
- **include_raw_pairs** — when `true`, .fob exports include raw level/duration pairs for replay.
|
||||
|
||||
## Supported Protocols
|
||||
@@ -227,15 +233,33 @@ Protocol behavior and RF modulation (AM/FM) follow the ProtoPirate reference. KA
|
||||
| Kia V2 | Manchester | FM | Fixed Code | 315 / 433.92 MHz |
|
||||
| Kia V3/V4 | PWM | AM/FM | KeeLoq | 315 / 433.92 MHz |
|
||||
| Kia V5 | Manchester | FM | Fixed Code | 433.92 MHz |
|
||||
| Kia V6 | Manchester | FM | Fixed Code | 433.92 MHz |
|
||||
| Ford V0 | 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 (decode + encode) |
|
||||
| VAG (VW/Audi/Seat/Skoda) | Manchester | AM | AUT64/XTEA | 433.92 / 434.42 MHz |
|
||||
| Scher-Khan | PWM | FM | Magic Code | 433.92 MHz |
|
||||
| Star Line | PWM | AM | KeeLoq | 433.92 MHz |
|
||||
| PSA (Peugeot/Citroën) | Manchester | FM | XTEA/XOR | 433.92 MHz |
|
||||
| PSA (Peugeot/Citroën) | Manchester | FM | Modified TEA/XOR | 433.92 MHz |
|
||||
| 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).
|
||||
|
||||
@@ -256,6 +280,8 @@ Details (exact model/year scope) are in `src/vuln_db.rs`. The app shows the NVD
|
||||
### Cryptographic modules
|
||||
|
||||
- **KeeLoq** — encrypt/decrypt with normal, secure, FAAC, and magic serial/XOR learning key derivation (keeloq_common, keys). Unknown signals are tried as KeeLoq with every keystore key via **keeloq_generic** (uses keeloq_common only).
|
||||
- **AES-128** — full encrypt/decrypt for Kia V6 (key derived from keystore A/B with XOR masks)
|
||||
- **Modified TEA** — XTEA-like cipher with dynamic key selection for PSA (Peugeot/Citroën)
|
||||
- **AUT64** — 12-round block cipher for VAG type 1/3/4 (aut64)
|
||||
- **Keystore** — manufacturer keys (Kia, VAG, etc.) built in; see `src/keystore/`
|
||||
|
||||
@@ -268,7 +294,7 @@ The **AM/OOK** demodulator turns IQ samples into level/duration pairs for protoc
|
||||
- **Exponential moving average** — magnitude smoothing
|
||||
- **Schmitt trigger hysteresis** — reduces chattering at the decision boundary
|
||||
- **Debounce** — 40µs minimum pulse width to reject noise spikes
|
||||
- **Gap detection** — 20 ms gap treated as end of signal
|
||||
- **Gap detection** — 80 ms gap treated as end of signal (multi-burst keyfob presses stay one capture)
|
||||
|
||||
## IMPORTS folder
|
||||
|
||||
@@ -296,7 +322,8 @@ src/
|
||||
│ ├── aut64.rs # AUT64 block cipher (VAG)
|
||||
│ ├── keys.rs # Key loading (embedded + optional file), KIA/VAG
|
||||
│ ├── kia_v0..kia_v6.rs
|
||||
│ ├── ford_v0.rs, fiat_v0.rs, subaru.rs, suzuki.rs
|
||||
│ ├── ford_v0.rs, fiat_v0.rs, fiat_v1.rs, subaru.rs, suzuki.rs
|
||||
│ ├── mazda_v0.rs, mitsubishi_v0.rs, porsche_touareg.rs
|
||||
│ ├── vag.rs # VAG decoder/encoder (4 sub-types; encode from capture via extra)
|
||||
│ ├── scher_khan.rs, star_line.rs, psa.rs
|
||||
│ └── ...
|
||||
@@ -314,21 +341,20 @@ src/
|
||||
└── status_bar.rs # Status bar
|
||||
```
|
||||
|
||||
## Call for researchers
|
||||
|
||||
**We need your help.** KAT’s protocol decoders and future analysis depend on real-world keyfob captures. If you use KAT for security research, authorized testing, or protocol development, please consider:
|
||||
|
||||
- **Contributing to protocol analysis** — share timing, encoding, or decoder feedback; report bugs or suggest new protocols.
|
||||
- **Contributing captures to the research library** — add your `.fob` files to the **[FOB Research Library](https://github.com/KaraZajac/FOB_Research_Library)** so others can use them for decoder development, CVE validation, and protocol studies.
|
||||
|
||||
The [FOB Research Library](https://github.com/KaraZajac/FOB_Research_Library) is a community collection of KAT `.fob` captures organized by manufacturer. Your submissions (with proper metadata: year, make, model, region, command) help grow test vectors and support the wider keyfob research community.
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
KAT is developed by **Kara Zajac (.leviathan)**. KAT would not be possible without [ProtoPirate](https://protopirate.net/ProtoPirate/ProtoPirate)—the protocol decoders, reference implementations, and community work are the foundation this tool is built on. Truly standing on the shoulders of giants.
|
||||
|
||||
### ProtoPirate Development Team
|
||||
|
||||
RocketGod, MMX, Leeroy, gullradriel, Skorp (thanks for the Weather App inspiration!), Vadim's Radio Driver.
|
||||
|
||||
### Protocol Magic
|
||||
|
||||
L0rdDiakon, YougZ, RocketGod, MMX, DoobTheGoober, Skorp, Slackware, Trikk, Wootini, Li0ard, Leeroy.
|
||||
|
||||
### Reverse Engineering Support
|
||||
|
||||
DoobTheGoober, MMX, NeedNotApply, RocketGod, Slackware, Trikk, Li0ard.
|
||||
KAT is developed by **Kara Zajac (.leviathan)**. KAT would not be possible without [ProtoPirate](https://protopirate.net/ProtoPirate/ProtoPirate)—the protocol decoders, reference implementations, and community work are the foundation this tool is built on. I am truly standing on the shoulders of giants.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -12,14 +12,32 @@ This folder describes how each keyfob protocol supported by KAT works. Each docu
|
||||
| Kia V3/V4 | `kia_v3_v4.rs` | [kia_v3_v4.md](kia_v3_v4.md) |
|
||||
| Kia V5 | `kia_v5.rs` | [kia_v5.md](kia_v5.md) |
|
||||
| Kia V6 | `kia_v6.rs` | [kia_v6.md](kia_v6.md) |
|
||||
| Kia V7 | `kia_v7.rs` | [kia_v7.md](kia_v7.md) |
|
||||
| Ford V0 | `ford_v0.rs` | [ford_v0.md](ford_v0.md) |
|
||||
| Ford V1 | `ford_v1.rs` | [ford_v1.md](ford_v1.md) |
|
||||
| Ford V2 | `ford_v2.rs` | [ford_v2.md](ford_v2.md) |
|
||||
| Ford V3 | `ford_v3.rs` | [ford_v3.md](ford_v3.md) |
|
||||
| Honda Static | `honda_static.rs` | [honda_static.md](honda_static.md) |
|
||||
| Honda V1 | `honda_v1.rs` | [honda_v1.md](honda_v1.md) |
|
||||
| Chrysler V0 | `chrysler_v0.rs` | [chrysler_v0.md](chrysler_v0.md) |
|
||||
| Subaru | `subaru.rs` | [subaru.md](subaru.md) |
|
||||
| Toyota | `toyota.rs` | [toyota.md](toyota.md) |
|
||||
| VAG | `vag.rs` | [vag.md](vag.md) |
|
||||
| Fiat V0 | `fiat_v0.rs` | [fiat_v0.md](fiat_v0.md) |
|
||||
| Fiat V1 | `fiat_v1.rs` | [fiat_v1.md](fiat_v1.md) |
|
||||
| Mazda V0 | `mazda_v0.rs` | [mazda_v0.md](mazda_v0.md) |
|
||||
| Mazda Siemens | `mazda_siemens.rs` | [mazda_siemens.md](mazda_siemens.md) |
|
||||
| Mitsubishi V0 | `mitsubishi_v0.rs` | [mitsubishi_v0.md](mitsubishi_v0.md) |
|
||||
| Land Rover V0 | `land_rover_v0.rs` | [land_rover_v0.md](land_rover_v0.md) |
|
||||
| Land Rover RKE | `land_rover_rke.rs` | [land_rover_rke.md](land_rover_rke.md) |
|
||||
| BMW CAS4 | `bmw_cas4.rs` | [bmw_cas4.md](bmw_cas4.md) |
|
||||
| Porsche Touareg | `porsche_touareg.rs` | [porsche_touareg.md](porsche_touareg.md) |
|
||||
| Porsche Cayenne | `porsche_cayenne.rs` | [porsche_cayenne.md](porsche_cayenne.md) |
|
||||
| Suzuki | `suzuki.rs` | [suzuki.md](suzuki.md) |
|
||||
| Scher-Khan | `scher_khan.rs` | [scher_khan.md](scher_khan.md) |
|
||||
| Star Line | `star_line.rs` | [star_line.md](star_line.md) |
|
||||
| PSA | `psa.rs` | [psa.md](psa.md) |
|
||||
| PSA2 (PSA OLD) | `psa2.rs` | [psa2.md](psa2.md) |
|
||||
| KeeLoq generic (fallback) | `keeloq_generic.rs` | [keeloq_generic.md](keeloq_generic.md) |
|
||||
|
||||
**KeeLoq generic** is not a registered decoder; it runs when no protocol matches and tries KeeLoq with every keystore manufacturer key (using `keeloq_common`). Successful decodes appear as **Keeloq (*keystore name*)**.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
title: KAT — Keyfob Analysis Toolkit
|
||||
description: Terminal-based RF signal analysis for automotive keyfobs
|
||||
theme: jekyll-theme-minimal
|
||||
show_downloads: false
|
||||
exclude:
|
||||
- README.md
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# BMW CAS4 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/bmw_cas4.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/bmw_cas4.c`
|
||||
|
||||
## Overview
|
||||
|
||||
BMW CAS4 uses Manchester encoding at 500/1000 µs. 64 bits (8 bytes), AM/OOK. The CAS4 rolling cipher's
|
||||
manufacturer key is not available, so the encrypted portion is left as-is — the frame is only framed and
|
||||
validated, not decrypted. Emission is gated on two fixed marker bytes (`byte[0] == 0x30` and
|
||||
`byte[6] == 0xC5`), which makes the protocol specific and prevents false matches.
|
||||
|
||||
The Manchester decoder uses Flipper's `manchester_decoder.h` transition table, with polarity
|
||||
`level ? Low : High` (the same mapping as Ford V0 / common).
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|----------------|---------|-----------------------------|
|
||||
| Short | 500 µs | ±150 µs (te_delta) |
|
||||
| Long | 1000 µs | ±150 µs |
|
||||
| Preamble pulse | 300–700 µs | |
|
||||
| Gap | ≥1800 µs | separates preamble from data |
|
||||
| Min bits | 64 | 8 bytes |
|
||||
| Preamble | ≥10 pulses | |
|
||||
|
||||
## Frame Layout (64 bits / 8 bytes)
|
||||
|
||||
- **byte 0:** fixed marker `0x30`
|
||||
- **bytes 1–3:** serial (24-bit)
|
||||
- **byte 5:** counter
|
||||
- **byte 6:** fixed marker `0xC5`
|
||||
- **byte 7:** button
|
||||
|
||||
The CAS4 rolling cipher is left undecrypted; the two markers serve as the integrity gate.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester (Flipper table, polarity `level ? Low : High`)
|
||||
- **RF modulation:** AM/OOK
|
||||
- **Encryption:** CAS4 rolling cipher left undecrypted (no manufacturer key); gated on fixed markers `0x30`/`0xC5`
|
||||
- **Frequencies:** 433.92 MHz only
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — begin on a HIGH preamble pulse within the 300–700 µs window.
|
||||
2. **Preamble** — count preamble pulses; a long LOW gap (≥1800 µs) with ≥10 pulses enters Data.
|
||||
3. **Data** — Manchester-decode 64 bits MSB-first; at the 64th bit, require `byte[0]==0x30 && byte[6]==0xC5`, build the signal on success, and reset. An out-of-range pulse aborts.
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (the reference encoder is a non-functional stub: `yield` returns reset, `deserialize`
|
||||
returns error). Decode-only.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by a synthetic-frame check (no local capture available): a unit test Manchester-encodes a frame
|
||||
by driving the decoder's own transition table (the Flipper table is differential, so there is no
|
||||
fixed per-bit pattern), confirms it decodes with the right serial/counter/button, and confirms a frame
|
||||
with wrong marker bytes is rejected.
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Chrysler V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/chrysler_v0.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/chrysler_v0.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Chrysler/Dodge/Jeep keyfobs. PWM with a short HIGH pulse and two long-LOW symbols: a "1" payload bit is
|
||||
HIGH ≈600 µs + LOW ≈3400 µs; a "0" payload bit is HIGH ≈300 µs + LOW ≈3700 µs. ~24 preamble pairs
|
||||
(short HIGH + long_b LOW) precede each frame. 80-bit frame: the first 64 bits are `decode_data` (payload
|
||||
bytes 0–7), the last 16 bits are `data_2` (payload bytes 8,9). The 80-bit frame exceeds u64, so `data`
|
||||
reports the most-significant 64 bits and `data_count_bit = 80`.
|
||||
|
||||
Crypto is a proprietary seed-XOR (ported exactly from the reference): `seed = reverse6(key[0] >> 2)` (a
|
||||
6-bit reversed counter); `transform_block` XORs all 9 transformed bytes with `xor_table[seed & 0x0F]`,
|
||||
with an extra nibble flip when the Lock button is set. Dual payload: A (seed even, carries serial +
|
||||
counter) / B (seed odd, carries serial). The frame is gated on a structural `check_ok`, so it does not
|
||||
false-match; `crc_valid` reflects that check.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|---------|------------------------|
|
||||
| Short HIGH | 300 µs | ±150 µs (te_delta) |
|
||||
| One-short HIGH | 600 µs | "1" bit HIGH width |
|
||||
| Long LOW A | 3400 µs | ±400 µs (long_delta) |
|
||||
| Long LOW B | 3700 µs | ±400 µs |
|
||||
| Gap | 8000 µs | te_gap |
|
||||
| Frame gap | 15600 µs| |
|
||||
| Min bits | 80 | |
|
||||
| Preamble | 24 pairs| |
|
||||
|
||||
## Frame Layout (80 bits / 10 payload bytes)
|
||||
|
||||
- **byte 0:** `(reverse6(counter) << 2) | header_low2` — the seed/counter byte
|
||||
- **bytes 1–9:** transformed (XOR-masked) payload; after `transform_block`:
|
||||
- Payload A (even seed): serial = decoded[0..3], rolling counter
|
||||
- Payload B (odd seed): serial = decoded[0..2] + decoded[7]
|
||||
- Button: Lock=1, Unlock=2 (derived from structural relationships between key bytes)
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** PWM (short HIGH + dual long LOW)
|
||||
- **RF modulation:** AM
|
||||
- **Encryption:** proprietary seed-XOR (`transform_block` + `xor_table`); gated on structural `check_ok`
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — a short HIGH pulse begins the seek.
|
||||
2. **Seek** — count preamble pairs (short HIGH + long LOW); after >15 pairs, a non-short HIGH transitions to Data.
|
||||
3. **Data** — classify each HIGH/LOW pair into a payload bit (`bit ^ 1`); collect 64 bits into `decode_data` then 16 into `data_2`. At 80 bits (or on a terminating gap with >0x4F bits) run `decode_packet`, check `check_ok`, and emit.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (ENABLE_EMULATE_FEATURE). Rebuilds the 10-byte payload, re-derives seed/plaintext, applies the
|
||||
requested button, then emits preamble + dual 80-bit PWM frames (A and B) separated by frame gaps.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
|
||||
cover `reverse6` involution, `transform_block` invertibility, and a full payload-A round trip.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Fiat V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/fiat_v0.rs`
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Fiat V1 Protocol (Magneti Marelli BSI / PCF7946)
|
||||
|
||||
**Rust module:** `src/protocols/fiat_v1.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/fiat_v1.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Fiat V1 is the Magneti Marelli BSI keyfob protocol (PCF7946), found on Fiat Panda, Grande Punto, and possibly other Fiat/Lancia/Alfa ~2003-2012. Uses Manchester encoding with auto-detected timing from preamble pulse averaging. Two timing variants: Type A (~260 us, e.g. Panda) and Type B (~100 us, e.g. Grande Punto). This is a different protocol from Fiat V0.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Default | Notes |
|
||||
|-----------|---------|-------|
|
||||
| Short | 260 us | Auto-detected from preamble |
|
||||
| Long | 520 us | 2x detected TE |
|
||||
| Delta | 80 us | Minimum 30 us |
|
||||
| Preamble | 80+ pulses | 50-350 us range |
|
||||
| Min bits | 80 | Max 104 bits |
|
||||
|
||||
### Auto-Detection
|
||||
|
||||
Preamble pulses (50-350 us) are accumulated. After 80+ pulses, `te_detected = te_sum / te_count` becomes the reference TE. Type A/B boundary is at 180 us.
|
||||
|
||||
## Frame Layout (80-104 bits = 10-13 bytes)
|
||||
|
||||
| Bytes | Content |
|
||||
|-------|---------|
|
||||
| 0-1 | Preamble residue (0xFFFF/0xFFFC) |
|
||||
| 2-5 | Serial (32 bits) |
|
||||
| 6 | Button:4 \| Epoch:4 |
|
||||
| 7 | Counter:5 \| Scramble:2 \| Fixed:1 |
|
||||
| 8-12 | Encrypted payload (40 bits) |
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** -- Wait for HIGH pulse in 50-350 us range. Also detect retransmission gaps (>5000 us).
|
||||
2. **Preamble** -- Count pulses, accumulate for TE averaging. On gap >= te_detected * 4, transition to Sync.
|
||||
3. **Sync** -- Expect HIGH sync pulse of te_detected * 4 to te_detected * 12 duration.
|
||||
4. **Data** -- Manchester decode up to 104 bits into 13-byte raw buffer.
|
||||
5. **RetxSync** -- After gap >5000 us, look for sync pulse 400-2800 us (retransmission).
|
||||
|
||||
## Buttons
|
||||
|
||||
| Code | Name |
|
||||
|------|--------|
|
||||
| 0x7 | Lock |
|
||||
| 0xB | Unlock |
|
||||
| 0xD | Trunk |
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (decode-only).
|
||||
|
||||
## Frequencies
|
||||
|
||||
433.92 MHz.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Ford V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/ford_v0.rs`
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Ford V1 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/ford_v1.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v1.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Ford V1 uses Manchester encoding at 65/130 µs. 136 bits / 17 bytes: key1 (bytes 0–6, 56 bits) + key2
|
||||
(bytes 7–14, 64 bits) + CRC16 (bytes 15–16). Preamble ≥50 long pulses, then a short-pulse sync window
|
||||
(`sync_event_count > 2`) replays buffered Manchester events and enters the 17-byte data collection. This
|
||||
is a ROLLING-code protocol.
|
||||
|
||||
Crypto: a proprietary parity-based descrambling cipher operating on the 9-byte air block `raw[6..15]`,
|
||||
plus CRC16/CCITT (poly `0x1021`, init `0x0000`) over `raw[3..15]`. Emission is gated on CRC16 validity
|
||||
(with a 17-byte bit-inverted fallback) so it never false-matches. A strict branch
|
||||
(`decoded[3]==raw[5] && decoded[4]==raw[6]`) yields plaintext serial/button/counter; otherwise it is
|
||||
classified as encrypted/rolling (device id only). The Manchester transition table is the Flipper
|
||||
differential-Manchester table (same as Ford V0/V2).
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|--------|------------------------------|
|
||||
| Short | 65 µs | ±39 µs (te_delta) |
|
||||
| Long | 130 µs | ±39 µs (preamble uses ±40) |
|
||||
| Min bits | 136 | 17 bytes |
|
||||
| Preamble | ≥50 long pulses | |
|
||||
|
||||
## Frame Layout (136 bits / 17 bytes)
|
||||
|
||||
- **bytes 0–6:** key1 (56 bits, big-endian) → `DecodedSignal.data`
|
||||
- **bytes 6–14:** air block (9 bytes) — descrambled to plaintext
|
||||
- **bytes 15–16:** CRC16/CCITT over bytes 3–14
|
||||
|
||||
Plaintext fields (when the strict branch matches): serial = `plain[1..3] + plain[0]`, button = `plain[5]>>4`
|
||||
(Sync=0, Lock=1, Unlock=2, Trunk=4, Panic=8), counter = `((plain[5] & 0x0F) << 16) | plain[6..7]`. The
|
||||
`extra` word stashes the CRC16, the strict flag, and `plain[4]` so the encoder can rebuild the full frame.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester (Flipper differential table)
|
||||
- **RF modulation:** FM
|
||||
- **Encryption:** proprietary parity descramble cipher + CRC16/CCITT gate; rolling code
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — a long LOW pulse begins the preamble (count = 1).
|
||||
2. **Preamble** — count long pulses; after ≥50, a short pulse enters Sync.
|
||||
3. **Sync** — buffer short/long events; once `sync_event_count > 2`, replay the buffered events into Manchester and enter Data.
|
||||
4. **Data** — Manchester-decode and pack 17 bytes; at the 17th byte run `process_data` (CRC16 gate + descramble + field extraction) and emit on a valid CRC.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (6 bursts). Faithful re-encode is only possible when the original frame's plaintext was
|
||||
recovered (strict branch); the encoder rebuilds the plaintext from fields + the stashed `plain[4]`,
|
||||
re-derives the air block + CRC16, and emits 6 bursts of a 400-pair long preamble + sync + 136 Manchester
|
||||
bits, with a per-burst `pkt[4]` override.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
|
||||
cover a CRC16/XMODEM known vector, the descramble round trip, the strict-branch decode, and a full
|
||||
encode→decode at the on-air-frame level.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Ford V2 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/ford_v2.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v2.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Ford V2 uses Manchester encoding at 200/400 µs. 104 bits total (13 bytes). The frame begins with a
|
||||
16-bit Manchester sync that equals `0x7FA7` (the decoder matches the *inverted* shift register against
|
||||
`!0x7FA7`); those two sync bytes `0x7F 0xA7` head the 13-byte buffer. Decoded data bits are inverted
|
||||
before packing (`data_bit = !bit`). There is no CRC — structure is validated by the two sync bytes plus
|
||||
a known button code, so Ford V2 will not false-match.
|
||||
|
||||
The Manchester decoder uses Flipper's transition table (same table as Ford V0), with Ford V2 polarity
|
||||
`level ? High : Low`.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|--------|-----------------------------|
|
||||
| Short | 200 µs | ±260 µs (te_delta) |
|
||||
| Long | 400 µs | ±260 µs |
|
||||
| Min bits | 104 | 13 bytes |
|
||||
| Preamble | ≥64 short pulses | before the sync |
|
||||
|
||||
## Frame Layout (104 bits / 13 bytes)
|
||||
|
||||
- **bytes 0–1:** sync `0x7F 0xA7`
|
||||
- **bytes 2–5:** serial (32-bit, big-endian)
|
||||
- **byte 6:** button — valid codes `0x10` (Lock), `0x11` (Unlock), `0x13` (Trunk), `0x14` (Panic), `0x15`
|
||||
- **bytes 7–8 + byte 9 MSB:** counter = `((b7 & 0x7F) << 9) | (b8 << 1) | (b9 >> 7)`; byte 7 MSB carries a button parity bit (refreshed by the encoder)
|
||||
- **bytes 9–12:** rolling/hop tail (stashed in `extra` so the encoder can rebuild the full frame)
|
||||
|
||||
The exported `data` (Key) word is the top 8 bytes; bytes 8–12 (40 bits) are carried in `extra`.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester (Ford V2 polarity `level ? High : Low`; decoded bits inverted)
|
||||
- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`)
|
||||
- **Encryption:** none — validated by sync bytes + valid button code
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** → a short pulse (~200 µs) begins the preamble.
|
||||
2. **Preamble** — count short pulses; after ≥64 shorts a long LOW pulse enters Sync.
|
||||
3. **Sync** — Manchester-decode bits into a 16-bit shift register; when it matches `!0x7FA7`, prime the two sync bytes and enter Data.
|
||||
4. **Data** — Manchester events feed the state machine; decoded bits are inverted and packed MSB-first into 13 bytes. At 104 bits the sync bytes and button are validated and the frame committed. A non-short/long pulse (gap) ends the attempt.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (matches `subghz_protocol_encoder_ford_v2`). Rebuilds the 13-byte frame, applies the requested
|
||||
button, refreshes byte-7 parity, then emits 6 bursts of a 70-pair preamble + sync + 104 Manchester bits
|
||||
(encoder short 240 µs), separated by ~16 ms inter-burst gaps.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available).
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Ford V3 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/ford_v3.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/ford_v3.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Ford V3 uses Manchester encoding at 240/480 µs. 104 bits total (13 bytes), transmitted as plaintext —
|
||||
there is no CRC or encryption. Decode-only: the ProtoPirate reference ships a NULL encoder, so KAT
|
||||
does not transmit Ford V3 (Replay still works on the raw capture).
|
||||
|
||||
The Manchester decoder uses Flipper's `manchester_decoder.h` transition table (same table as Ford V0),
|
||||
but Ford V3 maps the level the opposite way from Ford V0: `level ? High : Low`.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|--------|--------------------|
|
||||
| Short | 240 µs | ±60 µs (te_delta) |
|
||||
| Long | 480 µs | ±60 µs |
|
||||
| Min bits | 104 | 13 bytes |
|
||||
| Preamble | ≥30 short pulses | before first long |
|
||||
|
||||
## Frame Layout (104 bits / 13 bytes)
|
||||
|
||||
- **byte 0:** header
|
||||
- **bytes 1–4:** serial (32-bit, big-endian)
|
||||
- **byte 5:** hop/reserved
|
||||
- **byte 6:** button — bit 0 set → Unlock, else Lock
|
||||
- **bytes 7–8:** counter, stored bitwise-inverted (`~b[7]`, `~b[8]`)
|
||||
- **bytes 9–12:** rolling/hop tail (not parsed)
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — a short pulse (~240 µs) starts the preamble (count = 1).
|
||||
2. **Preamble** — count short pulses; once ≥30 shorts are seen, a long pulse begins the data
|
||||
(Manchester state seeded to Mid1, first bit decoded from the long pulse).
|
||||
3. **Data** — Manchester events (short/long × level) feed the state machine; bits pack MSB-first into
|
||||
13 bytes. At 104 bits the fields are parsed and the signal is emitted. A non-short/non-long pulse
|
||||
(gap) ends the frame.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester
|
||||
- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`)
|
||||
- **Encryption:** none (plaintext)
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (reference encoder is NULL). Decode-only.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Honda Static Protocol
|
||||
|
||||
**Rust module:** `src/protocols/honda_static.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/honda_static.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Honda/Acura fixed-code keyfobs. 64-bit frame. Unlike most KAT decoders, Honda Static does NOT use the
|
||||
Flipper `manchester_decoder.h` transition table: it buffers a per-element *symbol stream* (one bit per
|
||||
~63 µs element) and then performs a custom Manchester unpack over symbol pairs. A short pulse contributes
|
||||
one symbol equal to the pulse level; a long pulse contributes two symbols of the same level. Anything
|
||||
outside both ranges (e.g. the 700 µs sync or a trailing gap) terminates the buffer and triggers a parse.
|
||||
|
||||
The checksum is an XOR of the first 7 packet bytes. Emission is gated on the checksum, so Honda Static
|
||||
will not false-match. The parser tries the inverted-Manchester interpretation first (what the encoder
|
||||
emits), then non-inverted forward, then a bit-reversed-bytes pass.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|----------------|--------|--------------------------------|
|
||||
| Element | 63 µs | one symbol per element |
|
||||
| Short pulse | 28–98 µs | base 28 µs, span 70 µs |
|
||||
| Long pulse | 61–191 µs | base 61 µs, span 130 µs (two symbols) |
|
||||
| Sync | 700 µs | terminates the symbol buffer |
|
||||
| Min symbols | 36 | before a parse is attempted |
|
||||
| Min bits | 64 | |
|
||||
|
||||
(Reported timing profile: te_short 63 µs, te_long 700 µs, te_delta 120 µs.)
|
||||
|
||||
## Frame Layout (64 bits, MSB-first into 8 bytes)
|
||||
|
||||
- **bits 0–3:** button (4-bit) — Lock=1, Unlock=2, Trunk=4, Remote Start=5, Panic=8, Lock×2=9
|
||||
- **bits 4–31:** serial (28-bit)
|
||||
- **bits 32–55:** counter (24-bit)
|
||||
- **bits 56–63:** checksum (XOR of bytes 0–6)
|
||||
|
||||
The exported `data` (Key) word is the C `generic.data`: a compact nibble-packed layout, NOT the raw
|
||||
decoded packet bytes. The KAT counter field is the low 16 bits of the 24-bit counter.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** custom symbol-stream Manchester over ~63 µs elements
|
||||
- **RF modulation:** FM
|
||||
- **Encryption:** none — gated on the XOR checksum + valid button + valid serial (serial ≠ 0 and ≠ 0x0FFFFFFF)
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **feed** — classify each pulse: short → 1 symbol, long → 2 symbols; buffer them.
|
||||
2. On an out-of-range pulse (700 µs sync or gap), if ≥36 symbols are buffered, walk past the alternating preamble + sync run, Manchester-unpack 64 bits over symbol pairs (inverted first, then non-inverted), validate, and emit. Then clear the buffer.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (matches `honda_static_build_upload`). Emits a 700 µs HIGH sync, a 160-element alternating
|
||||
preamble at 63 µs, 64 data bits (each as `!value`/`value` at 63 µs), and a trailing 700 µs sync.
|
||||
|
||||
## Validation
|
||||
|
||||
Decodes REAL IMPORTS captures (IMPORTS/honda Lock and Unlock). Also covered by encode→decode round-trip
|
||||
and packet-validate unit tests.
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Honda V1 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/honda_v1.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/honda_v1.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Honda/Acura fixed-code keyfobs — a DIFFERENT protocol from `honda_static`. The on-wire frame carries 68
|
||||
bits: 64-bit data + a 4-bit CRC-fold nibble. Encoding is short/long pulse PWM, glued together by a
|
||||
"pending bit" timing accumulator before classification: sub-`te_delta` runts are summed into `pending`;
|
||||
a HIGH level extends the running HIGH pulse; a LOW flushes the accumulated HIGH (if it reached
|
||||
`te_short_min`) as a synthetic symbol, then classifies the LOW.
|
||||
|
||||
Validation is a button-code table (Unlock=0, Lock=8, Trunk=9, Panic=10) plus a CRC-fold checksum.
|
||||
Emission is gated on the button being valid; `crc_valid` reflects whether the received CRC nibble matches
|
||||
either wire-order checksum. The strong button gate keeps Honda V1 from false-matching.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|---------|-----------------------------|
|
||||
| Short | 1000 µs | ±400 µs (te_delta) |
|
||||
| Long | 2000 µs | ±400 µs |
|
||||
| Short min | 600 µs | flush threshold for HIGH |
|
||||
| End gap | 3500 µs | >te_end commits the frame |
|
||||
| Min bits | 68 | |
|
||||
|
||||
## Frame Layout (68 bits = 64-bit data + 4-bit CRC nibble)
|
||||
|
||||
After the end gap, `commit` left-shifts the 12-byte bit buffer to drop leading preamble leakage and align
|
||||
the trailing 68-bit frame. `data` = first 8 bytes (64 bits); the CRC nibble = byte 8's high nibble.
|
||||
|
||||
- **data[63:36]:** serial (28-bit)
|
||||
- **data[31:28]:** button (nibble) — Unlock=0, Lock=8, Trunk=9, Panic=10
|
||||
- **data[15:0]:** counter (16-bit)
|
||||
- **CRC nibble:** CRC-fold checksum (`honda_v1_checksum*`), validated against either wire-order value
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** short/long pulse PWM with pending-bit timing accumulation
|
||||
- **RF modulation:** AM (OOK)
|
||||
- **Encryption:** none — gated on the button-code table + CRC-fold checksum
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **feed** — accumulate sub-`te_delta` runts into `pending`; flush HIGH pulses and classify LOW pulses into symbols.
|
||||
2. **symbol** — Reset → Preamble (short/long pulses, needs a long + >5 count) → Data.
|
||||
3. **Data** — pending-bit accumulation: a short pulse toggles `data_pending` and emits a bit when paired; a long pulse emits directly. After a >te_end gap, commit: align the 68-bit frame, validate the button, and emit.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (ENABLE_EMULATE_FEATURE). Builds the 64-bit key from serial/button/counter via the button code
|
||||
table, then emits a 180-element short-pair preamble + 4 PWM frames (2 per checksum wire value).
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
|
||||
cover the key/field round trip, full encode→decode, and the button-validity mask.
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KAT — Keyfob Analysis Toolkit</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1>KAT — Keyfob Analysis Toolkit</h1>
|
||||
<p class="tagline">Terminal-based RF signal analysis for automotive keyfobs</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<ul>
|
||||
<li><a href="#features">Features</a></li>
|
||||
<li><a href="#supported-protocols">Protocols</a></li>
|
||||
<li><a href="#requirements">Requirements</a></li>
|
||||
<li><a href="#documentation">Docs</a></li>
|
||||
<li><a href="https://github.com/KaraZajac/KAT" target="_blank">GitHub</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h2>Capture, Decode & Analyze Keyfob Signals</h2>
|
||||
<p>A real-time terminal UI for RF signal analysis with support for HackRF One and RTL-SDR hardware.</p>
|
||||
<div class="cta-buttons">
|
||||
<a href="https://github.com/KaraZajac/KAT#installation" class="btn btn-primary">Get Started</a>
|
||||
<a href="https://github.com/KaraZajac/KAT" class="btn btn-secondary">View on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="features" class="features">
|
||||
<div class="container">
|
||||
<h2>Key Features</h2>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<h3>📡 Real-Time Capture</h3>
|
||||
<p>Receive and demodulate AM/OOK keyfob signals at configurable frequencies with HackRF or RTL-SDR</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>🔓 Multi-Protocol Decoding</h3>
|
||||
<p>18 protocol decoders including Kia V0–V6, Ford, Fiat, Mazda, Mitsubishi, Porsche, Subaru, Suzuki, VAG, PSA, and more</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>🔑 KeeLoq Keystore</h3>
|
||||
<p>Generic KeeLoq fallback with embedded manufacturer keys for automatic decoding</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>📤 Signal Retransmission</h3>
|
||||
<p>Transmit Lock, Unlock, Trunk, and Panic commands from decoded signals (HackRF only)</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>💾 Multiple Export Formats</h3>
|
||||
<p>.fob (JSON) and .sub (Flipper Zero compatible) formats for signal storage and sharing</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>🔧 Rich Signal Details</h3>
|
||||
<p>View encoding, RF modulation, encryption, serial, counter, key data, CRC, and raw timing data</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="supported-protocols" class="protocols">
|
||||
<div class="container">
|
||||
<h2>Supported Protocols</h2>
|
||||
<div class="protocol-list">
|
||||
<div class="protocol-column">
|
||||
<h3>Kia</h3>
|
||||
<ul>
|
||||
<li>Kia V0</li>
|
||||
<li>Kia V1</li>
|
||||
<li>Kia V2</li>
|
||||
<li>Kia V3 / V4</li>
|
||||
<li>Kia V5</li>
|
||||
<li>Kia V6</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="protocol-column">
|
||||
<h3>Other Manufacturers</h3>
|
||||
<ul>
|
||||
<li>Ford 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>
|
||||
<li>PSA (Peugeot/Citroën)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="protocol-column">
|
||||
<h3>Generic</h3>
|
||||
<ul>
|
||||
<li>Scher-Khan</li>
|
||||
<li>Star Line</li>
|
||||
<li>KeeLoq (Generic)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="requirements" class="requirements">
|
||||
<div class="container">
|
||||
<h2>Requirements</h2>
|
||||
<h3>Hardware</h3>
|
||||
<ul>
|
||||
<li><strong>HackRF One</strong> (or compatible) — Full receive and transmit capabilities</li>
|
||||
<li><strong>RTL433 / RTL-SDR</strong> — Receive-only; no transmit supported</li>
|
||||
</ul>
|
||||
|
||||
<h3>Software</h3>
|
||||
<ul>
|
||||
<li>Rust 1.75+ (for building from source)</li>
|
||||
<li>libhackrf — HackRF C library and headers</li>
|
||||
<li>libusb — For RTL433/RTL-SDR support</li>
|
||||
</ul>
|
||||
|
||||
<h3>Operating System</h3>
|
||||
<p>macOS, Linux, and other Unix-like systems. Precompiled binaries and installation instructions available on GitHub.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="documentation" class="documentation">
|
||||
<div class="container">
|
||||
<h2>Protocol Documentation</h2>
|
||||
<p>Detailed protocol specifications and decoder documentation:</p>
|
||||
<div class="doc-links">
|
||||
<a href="kia_v0.html" class="doc-link">Kia V0</a>
|
||||
<a href="kia_v1.html" class="doc-link">Kia V1</a>
|
||||
<a href="kia_v2.html" class="doc-link">Kia V2</a>
|
||||
<a href="kia_v3_v4.html" class="doc-link">Kia V3/V4</a>
|
||||
<a href="kia_v5.html" class="doc-link">Kia V5</a>
|
||||
<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>
|
||||
<a href="psa.html" class="doc-link">PSA</a>
|
||||
<a href="scher_khan.html" class="doc-link">Scher-Khan</a>
|
||||
<a href="star_line.html" class="doc-link">Star Line</a>
|
||||
<a href="keeloq_generic.html" class="doc-link">KeeLoq (Generic)</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="security-notice">
|
||||
<div class="container">
|
||||
<h3>⚠️ Security and Legal Disclaimer</h3>
|
||||
<p>Use KAT only on systems and vehicles you own or have explicit, written permission to test. Capturing, decoding, or transmitting keyfob and vehicle-access signals without authorization may be illegal in your jurisdiction. You are solely responsible for ensuring your use complies with applicable laws. KAT is intended for security research, authorized penetration testing, education, and legitimate testing on your own equipment.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2024 KAT — Keyfob Analysis Toolkit. Licensed under the terms in LICENSE file. <a href="https://github.com/KaraZajac/KAT" target="_blank">View on GitHub</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# KeeLoq Generic Fallback
|
||||
|
||||
**Rust module:** `src/protocols/keeloq_generic.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v0.rs`
|
||||
|
||||
+6
-2
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V1 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v1.rs`
|
||||
@@ -5,7 +9,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Kia V1 uses Manchester encoding at 800/1600 µs. 57 bits total: 32 serial + 8 button + 12 counter + 4 CRC. Long preamble (~90 long pairs). CRC4 with offset rules (cnt_high 0 vs ≥ 6).
|
||||
Kia V1 uses Manchester encoding at 800/1600 us. 57 bits total: 32 serial + 8 button + 12 counter + 4 CRC. Long preamble (~90 long pairs). CRC4 uses XOR of nibbles over 7 bytes (serial + button + cnt_low + cnt_high) with offset 1.
|
||||
|
||||
## Timing
|
||||
|
||||
@@ -24,7 +28,7 @@ Manchester: symbol duration short or long; bit value from transition direction.
|
||||
- 32 bits: serial
|
||||
- 8 bits: button
|
||||
- 12 bits: counter
|
||||
- 4 bits: CRC4 (checksum with offset rules)
|
||||
- 4 bits: CRC4 (XOR of nibbles, 7 bytes, offset 1)
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V2 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v2.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V3/V4 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v3_v4.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V5 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v5.rs`
|
||||
|
||||
+6
-2
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V6 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v6.rs`
|
||||
@@ -26,7 +30,7 @@ Manchester (event mapping 0/2/4/6 for level convention). Three 64/64/16-bit segm
|
||||
- Part2: 64 bits (inverted)
|
||||
- Part3: 16 bits (inverted)
|
||||
|
||||
AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH). Serial/button/counter and CRC extracted after decryption.
|
||||
AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH). Serial/button/counter, CRC, and fx_field extracted after decryption. The fx_field is derived from the top 2 bytes of stored_part1_high and stored in `DecodedSignal.extra`.
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
@@ -36,7 +40,7 @@ AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH
|
||||
|
||||
## Encoder
|
||||
|
||||
Decode-only in KAT (no encoder in reference).
|
||||
Supported. Ported from ProtoPirate (`ENABLE_EMULATE_FEATURE`). Builds plaintext (fx_field, serial, button, counter, S-box CRC, CRC8), AES-128 encrypts, packs into 3 parts, then Manchester encodes with two-pass preamble (640 pairs + data, gap, 38 pairs + data). Requires fx_field from decoded signal's `extra` field.
|
||||
|
||||
## Frequencies
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V7 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v7.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/kia_v7.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Kia V7 uses Manchester encoding at 250/500 µs. 64 bits. The decoded 64-bit word is bit-inverted
|
||||
(`~data`); a valid frame has a fixed header byte `0x4C` and a CRC8 (poly `0x7F`, init `0x4C`) over bytes
|
||||
0–6. Emission is gated on header + CRC, so Kia V7 is strongly validated and will not false-match.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|--------|---------------------|
|
||||
| Short | 250 µs | ±100 µs (te_delta) |
|
||||
| Long | 500 µs | ±100 µs |
|
||||
| Min bits | 64 | |
|
||||
| Preamble | ≥16 short pairs | before sync |
|
||||
|
||||
## Frame Layout (64 bits / 8 bytes, after inversion)
|
||||
|
||||
- **byte 0:** header `0x4C`
|
||||
- **bytes 1–2:** counter (16-bit, big-endian)
|
||||
- **bytes 3–6:** serial (28-bit) — `(b3<<20)|(b4<<12)|(b5<<4)|(b6>>4)`, masked to 0x0FFFFFFF
|
||||
- **byte 6 low nibble:** button (4-bit) — Lock=1, Unlock=2, Trunk=3, aux=8
|
||||
- **byte 7:** CRC8 over bytes 0–6 (poly `0x7F`, init `0x4C`)
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester (level ? (H,L) : (L,H); decoded word is bit-inverted)
|
||||
- **RF modulation:** FM (per ProtoPirate `SubGhzProtocolFlag_FM`)
|
||||
- **Encryption:** none — gated on fixed header `0x4C` + CRC8
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — a short HIGH pulse begins the preamble.
|
||||
2. **Preamble** — count short pairs; once >15 pairs are seen, a long HIGH pulse transitions to SyncLow and preloads four seed bits (1,0,1,1 = the inverted header's top nibble 0xB).
|
||||
3. **SyncLow** — a short LOW after the long sync enters Data.
|
||||
4. **Data** — Manchester-decode the remaining 60 bits. At 64 bits, invert the word, check the header equals `0x4C` and the CRC8 validates, then emit.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (matches `kia_v7_encode_key`). Rebuilds the 64-bit key from serial/button/counter with the
|
||||
CRC8, then emits a 32-pair short preamble, a merged long sync pulse, 64 Manchester bits, and a trailing
|
||||
gap.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available).
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Land Rover RKE Protocol
|
||||
|
||||
**Rust module:** `src/protocols/land_rover_rke.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/landrover_rke.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Ported from the Flipper-ARF firmware (D4C1-Labs), itself derived from Pandora DXL 5000 firmware. Land
|
||||
Rover shares the Ford/Jaguar baseband (firmware protocol ID `0x0E`) but uses a distinct 66-bit frame.
|
||||
Encoding is fixed-width PWM with a 1000 µs bit period: Bit-1 = 700 µs HIGH + 300 µs LOW, Bit-0 = 300 µs
|
||||
HIGH + 700 µs LOW. Preamble = 20× (400 µs HIGH + 600 µs LOW); sync = 400 µs HIGH + 9600 µs LOW.
|
||||
|
||||
KeeLoq: the hop code is the raw 32-bit KeeLoq ciphertext. Full decryption needs the per-fob manufacturer
|
||||
key (provisioned, not in firmware), so KAT exposes the framed fields and leaves the hop encrypted —
|
||||
`crc_valid = false` since no cryptographic check is performed. Emission is still gated tightly on the long
|
||||
preamble run, the distinctive 9.6 ms sync gap, and exactly 66 PWM bits, so this loose-looking PWM decoder
|
||||
does not false-match Kia/Subaru/Ford captures.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|----------------|---------|-----------------------------|
|
||||
| Bit-1 | 700 µs HIGH + 300 µs LOW | ±20% (relative) |
|
||||
| Bit-0 | 300 µs HIGH + 700 µs LOW | ±20% |
|
||||
| Preamble pair | 400 µs HIGH + 600 µs LOW | ×20 |
|
||||
| Sync | 400 µs HIGH + 9600 µs LOW | |
|
||||
| Repeat gap | 12000 µs | |
|
||||
| Min bits | 66 | |
|
||||
| Preamble min | 16 pairs| |
|
||||
|
||||
(Reported timing profile: te_short 300 µs, te_long 700 µs, te_delta 140 µs.)
|
||||
|
||||
## Frame Layout (66 bits, MSB-first)
|
||||
|
||||
- **[65:34]:** 32-bit KeeLoq encrypted hopping code
|
||||
- **[33:10]:** 24-bit fixed fob serial
|
||||
- **[9:6]:** 4-bit button — Lock=0x1, Unlock=0x2, Boot/Tailgate=0x4, Panic=0x8
|
||||
- **[5:2]:** 4-bit function/repeat flags
|
||||
- **[1:0]:** 2-bit status — battery-low=0x1, repeat=0x2
|
||||
|
||||
66 bits do not fit a u64: `data` holds the low 64 frame bits and `extra` holds the top 2 (the high 2 bits
|
||||
of the hop code), so encode round-trips the hop losslessly. The KAT counter field surfaces the low 16
|
||||
bits of the hop ciphertext.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** fixed-width PWM
|
||||
- **RF modulation:** OOK/AM
|
||||
- **Encryption:** KeeLoq hop left encrypted (no manufacturer key); `crc_valid = false`
|
||||
- **Frequencies:** 433.92 MHz, 315 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — wait for the first ~400 µs preamble HIGH.
|
||||
2. **Preamble** — count 400/600 µs pairs; the distinctive ~9600 µs LOW after a ~400 µs HIGH (with ≥16 pairs) enters DataHigh.
|
||||
3. **DataHigh/DataLow** — read each bit from its HIGH/LOW half pair (±20% windows). At 66 bits, build the signal and emit.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported. Reconstructs the original frame to preserve hop code / func bits / status, overrides only the
|
||||
button, and emits 4 repetitions of preamble + sync + 66 MSB-first PWM bits, separated by 12 ms gaps.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by encode→decode round-trips / synthetic-frame checks (no local capture available). Unit tests
|
||||
cover pack/unpack round trips, encode→decode across multiple serials/hops/buttons, and rejection of
|
||||
truncated frames and wrong sync gaps.
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Land Rover V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/land_rover_v0.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/land_rover_v0.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Land Rover V0 uses **differential** Manchester (NOT the Flipper transition table) at 250/500 µs, with a
|
||||
~750 µs sync pulse and a ≥64-pair short preamble. The frame is 81 bits = an 80-bit body (`raw[0..10]`)
|
||||
plus one trailing `extra_bit`. The 64-bit key reported as `data` is `raw[0..8]` big-endian; `raw[8..10]`
|
||||
is a 16-bit `tail`.
|
||||
|
||||
The check is a proprietary 3-bit polynomial over the counter (`calculate_check`); the tail is `0xFFFF`
|
||||
or `0x7FFF` depending on a 1-bit parity of the counter (`calculate_tail`). Emission is gated on the
|
||||
reserved bits being zero, the check matching, the tail matching, and `extra_bit` set — so Land Rover V0
|
||||
is strongly validated and will not false-match.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|--------|------------------------|
|
||||
| Short | 250 µs | ±100 µs (te_delta) |
|
||||
| Long | 500 µs | ±100 µs |
|
||||
| Sync | 750 µs | ±120 µs |
|
||||
| Min bits | 81 | 80-bit body + extra_bit|
|
||||
| Preamble | ≥64 pairs | |
|
||||
|
||||
## Frame Layout (81 bits)
|
||||
|
||||
64-bit key (`raw[0..8]`):
|
||||
|
||||
- **bytes 0–2:** 24-bit command signature — Lock = `0xC20363`, Unlock = `0xA285E3`
|
||||
- **bytes 3–5:** 24-bit serial
|
||||
- **byte 6 + byte 7 MSB:** 9-bit counter = `(b6 << 1) | (b7 >> 7)`
|
||||
- **byte 7 bits 0x78:** 3 reserved bits (must be 0)
|
||||
- **byte 7 bits 0x07:** 3-bit check (`calculate_check`)
|
||||
|
||||
Then `raw[8..10]` = 16-bit tail (`0xFFFF`/`0x7FFF`), and a trailing `extra_bit` (must be 1). The tail is
|
||||
stashed in `extra` for the encoder.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** differential Manchester
|
||||
- **RF modulation:** FM
|
||||
- **Encryption:** none — gated on the 3-bit check polynomial + tail parity + reserved-bits + extra_bit
|
||||
- **Frequencies:** 315 MHz, 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset → PreambleLow/PreambleHigh** — count short pairs (≥64), terminated by a ~750 µs sync HIGH.
|
||||
2. **SyncLow** — a ~750 µs sync LOW seeds the frame (bit 0 = 1) and enters Data.
|
||||
3. **Data** — the differential machine (`process_transition`/`add_decoded_bit`) tracks `previous_bit`, skips the initial boundary short-high pad, and completes short half-bits via `pending_short`. At 81 bits, `finish_frame` validates and emits.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported. Builds the 64-bit key from signature/serial/counter, emits a 319-pair preamble, the sync,
|
||||
differential-Manchester body, the 16-bit tail, and a trailing extra bit. Faithful-port note: the
|
||||
reference encoder forces frame bit 1 = 0, so the Lock signature `0xC20363` (whose bit 1 is 1) is emitted
|
||||
as `0x820363`; the Unlock signature `0xA285E3` round-trips cleanly. KAT reproduces this behaviour.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
|
||||
cover the check polynomial vs. the reference, the tail parity, Unlock round trips across 256 serial/counter
|
||||
values, the Lock forced-bit quirk, and rejection of a corrupted check.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Mazda Siemens Protocol
|
||||
|
||||
**Rust module:** `src/protocols/mazda_siemens.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/mazda_siemens.c`
|
||||
|
||||
## Overview
|
||||
|
||||
The Siemens/VDO keyfob cipher used on some Mazda vehicles — a DIFFERENT protocol from KAT's existing
|
||||
"Mazda V0" (Pandora) decoder, even though both ride a 250/500 µs pair-based stream at 433.92 MHz FM. The
|
||||
decoder is pair-based: `feed()` ignores `level` and interprets raw durations in pairs (`process_pair`),
|
||||
collecting bits with *inverted* polarity (`state_bit == 0` → stored 1). 64-bit frame.
|
||||
|
||||
Siemens obfuscation (`mazda_xor_deobfuscate`): parity-dependent XOR mask, then bit-deinterleave of bytes
|
||||
5/6. The inner Siemens cipher's plaintext is left as-is (no key); only this obfuscation/interleave layer
|
||||
is reversed. The gate is an additive checksum `sum(data[0..7]) == data[7]`, plus the structural
|
||||
preamble/sync/bit-count constraints, which keeps it from false-matching other 250/500 µs Manchester
|
||||
protocols.
|
||||
|
||||
> **Note:** KAT's existing "Mazda V0" decoder implements the same algorithm and keeps first-match
|
||||
> priority, so on a shared frame Mazda V0 fires first.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|--------|-----------------------------|
|
||||
| Short | 250 µs | ±100 µs (te_delta) |
|
||||
| Long | 500 µs | ±100 µs |
|
||||
| Min bits | 64 | |
|
||||
| Preamble | ≥13 short/short pairs | |
|
||||
| Completion | 80–105 collected bits | |
|
||||
|
||||
## Frame Layout (64 bits / 8 bytes, after deobfuscation)
|
||||
|
||||
The 14-byte buffer's leading sync byte is discarded, leaving 8 bytes:
|
||||
|
||||
- **serial:** `data >> 32` (32-bit)
|
||||
- **button:** `(data >> 24) & 0xFF` — Lock=0x10, Unlock=0x20, Trunk=0x40
|
||||
- **counter:** `(data >> 8) & 0xFFFF` (16-bit)
|
||||
- **byte 7:** additive checksum of bytes 0–6
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** pair-based Manchester (inverted polarity), 250/500 µs
|
||||
- **RF modulation:** FM
|
||||
- **Encryption:** Siemens obfuscation (parity XOR + bit-interleave); gated on the additive checksum
|
||||
- **Frequencies:** 433.92 MHz only
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — a short pulse begins the preamble.
|
||||
2. **PreambleSave/PreambleCheck** — count short/short pairs (≥13); a short→long transition seeds the leading sync bit (a 1) and enters data.
|
||||
3. **DataSave/DataCheck** — process duration pairs into bits; on a non-matching pair, run `check_completion` (discard sync byte, deobfuscate, validate additive checksum) and emit.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (`subghz_protocol_encoder_mazda_siemens_get_upload`). Increments the counter byte, recomputes
|
||||
the checksum, obfuscates (interleave + XOR), then emits a 12-byte 0xFF preamble, a 50 ms gap, `0xFF 0xFF
|
||||
0xD7`, the 8 obfuscated bytes transmitted as `255 − byte`, a `0x5A` tail, and a trailing 50 ms gap.
|
||||
Manchester per byte: bit 1 → (H,L), bit 0 → (L,H) at te_short. As in the C, the decoder and encoder are
|
||||
not a clean raw-timing round-trip pair (the encoder targets a TX upload).
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by an encode→decode round-trip / synthetic-frame check (no local capture available). Unit tests
|
||||
cover the obfuscate/deobfuscate XOR+interleave cipher layer (a true inverse, both parity branches), the
|
||||
field layout, and the TX upload smoke test.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Mazda V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/mazda_v0.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/mazda_v0.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Mazda V0 uses a custom pair-based encoding (not standard Manchester). The `level` parameter is ignored; the decoder processes raw duration pairs. Preamble: 13+ short/short pairs followed by a short+long transition into data. Data bits are collected using a prev_state tracker with inverted polarity, then XOR-deobfuscated, checksum-validated, and parsed.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|--------|--------------|
|
||||
| Short | 250 us | +/-100 us |
|
||||
| Long | 500 us | +/-100 us |
|
||||
| Min bits | 64 | |
|
||||
| Completion| 80-105 bits | |
|
||||
|
||||
## Frame Layout (64 bits after deobfuscation)
|
||||
|
||||
Raw data collects into a 14-byte buffer. First byte is discarded (sync); bytes [1..9] form the 8-byte data frame.
|
||||
|
||||
After XOR deobfuscation:
|
||||
- Bytes 0-3: Serial (32 bits)
|
||||
- Byte 4: Button (8 bits)
|
||||
- Bytes 5-6: Counter (16 bits)
|
||||
- Byte 7: Checksum (additive sum of bytes 0-6)
|
||||
|
||||
## Pair-Based Bit Decoding
|
||||
|
||||
| Pair (te_last, duration) | Action |
|
||||
|--------------------------|--------|
|
||||
| Long + Short | Collect bit(0), bit(1), prev_state=1 |
|
||||
| Short + Long | Collect bit(1), prev_state=0 |
|
||||
| Short + Short | Collect bit(prev_state) |
|
||||
| Long + Long | Collect bit(0), bit(1), prev_state=0 |
|
||||
|
||||
Bit collection uses inverted polarity: state_bit=0 stores a 1.
|
||||
|
||||
## XOR Deobfuscation
|
||||
|
||||
1. Compute parity of data[7] (XOR fold to single bit).
|
||||
2. If parity is odd: XOR bytes [0..6] with data[6] as mask.
|
||||
3. If parity is even: XOR bytes [0..5] with data[5] as mask, also XOR data[6] with data[5].
|
||||
4. Bit interleave swap bytes 5 and 6: swap even/odd bit positions between them.
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** -- Wait for short pulse; save and go to PreambleCheck.
|
||||
2. **PreambleSave** -- Save duration, go to PreambleCheck.
|
||||
3. **PreambleCheck** -- Count short+short pairs; on short+long with count >= 13, start data.
|
||||
4. **DataSave** -- Save duration, go to DataCheck.
|
||||
5. **DataCheck** -- Process pair; if valid continue; if invalid, check completion (80-105 bits + checksum).
|
||||
|
||||
## Buttons
|
||||
|
||||
| Code | Name |
|
||||
|------|--------|
|
||||
| 0x10 | Lock |
|
||||
| 0x20 | Unlock |
|
||||
| 0x40 | Trunk |
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (decode-only).
|
||||
|
||||
## Frequencies
|
||||
|
||||
433.92 MHz.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Mitsubishi V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/mitsubishi_v0.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/mitsubishi_v0.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Mitsubishi V0 uses PWM encoding at 250/500 us. 96-bit frame (12 bytes). Level-aware: HIGH pulses are saved, LOW pulses complete the pair. After collection, data is unscrambled via bitwise NOT + counter-derived XOR mask.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|--------|--------------|
|
||||
| Short | 250 us | +/-100 us |
|
||||
| Long | 500 us | +/-100 us |
|
||||
| Min bits | 80 | |
|
||||
| Frame | 96 bits| 12 bytes |
|
||||
|
||||
## PWM Bit Encoding
|
||||
|
||||
| Pair (HIGH, LOW) | Bit |
|
||||
|------------------|-----|
|
||||
| Short HIGH + Long LOW | 1 |
|
||||
| Long HIGH + Short LOW | 0 |
|
||||
|
||||
Bits collected MSB-first into a 12-byte buffer.
|
||||
|
||||
## Frame Layout (96 bits)
|
||||
|
||||
After unscrambling:
|
||||
- Bytes 0-3: Serial (32 bits, big-endian)
|
||||
- Bytes 4-5: Counter (16 bits)
|
||||
- Byte 6: Button (8 bits)
|
||||
- Bytes 7-11: Remaining payload
|
||||
|
||||
## Unscramble Algorithm
|
||||
|
||||
1. Bitwise NOT the first 8 bytes.
|
||||
2. Extract counter from bytes [4..5]: `hi = (counter >> 8) & 0xFF`, `lo = counter & 0xFF`.
|
||||
3. Compute masks: `mask1 = (hi & 0xAA) | (lo & 0x55)`, `mask2 = (lo & 0xAA) | (hi & 0x55)`, `mask3 = mask1 ^ mask2`.
|
||||
4. XOR bytes [0..5] with mask3.
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** -- Wait for HIGH pulse; save duration.
|
||||
2. **DataSave** -- On HIGH: save duration. On LOW (unexpected): reset.
|
||||
3. **DataCheck** -- On LOW: decode pair. If 96 bits collected, unscramble and return. Otherwise continue.
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (decode-only).
|
||||
|
||||
## Frequencies
|
||||
|
||||
868.35 MHz.
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Porsche Cayenne Protocol
|
||||
|
||||
**Rust module:** `src/protocols/porsche_cayenne.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/porsche_cayenne.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Porsche keyfobs (internal firmware header name "Porsche AG"). PWM at 1680/3370 µs: SHORT LOW + LONG HIGH
|
||||
= bit 0, LONG LOW + SHORT HIGH = bit 1. 64-bit MSB-first frame, preceded by a 73-pulse 3370 µs sync
|
||||
preamble + a 5930 µs gap pair. The cipher is a 24-bit rotating-register VAG cipher; the counter is
|
||||
recovered by brute-forcing 1..=256 and matching the recomputed encrypted bytes (the C's validity signal).
|
||||
|
||||
> **Note:** Porsche Cayenne shares the wire protocol with KAT's existing **Porsche Touareg** decoder
|
||||
> (the Touareg port is itself a port of this same `porsche_cayenne.c` source). The two are the **same
|
||||
> wire protocol** — identical timing, preamble, 64-bit frame, VAG cipher, and validity check. Cayenne is
|
||||
> registered AFTER Touareg, so Touareg keeps first-match priority and Cayenne never steals a Touareg
|
||||
> capture. Emission is additionally gated on `frame_type` being one of the three values the C emits
|
||||
> (`0b001` Cont, `0b010` First, `0b100` Final) — a strict subset of what Touareg accepts. Cayenne adds an
|
||||
> encoder (the Touareg port is decode-only).
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|---------|------------------------|
|
||||
| Short | 1680 µs | ±500 µs (te_delta) |
|
||||
| Long | 3370 µs | ±500 µs |
|
||||
| Sync | 3370 µs | preamble pulse |
|
||||
| Gap | 5930 µs | preamble→data boundary |
|
||||
| Min bits | 64 | |
|
||||
| Sync min | 15 pairs| (firmware emits 73) |
|
||||
|
||||
## Frame Layout (64 bits / 8 bytes)
|
||||
|
||||
- **byte 0:** `(button << 4) | (frame_type & 0x07)` — button d-pad: Lock=0x01, Unlock=0x02, Trunk=0x04, Open/Panic=0x08; frame_type First=0x02, Cont=0x01, Final=0x04
|
||||
- **bytes 1–3:** serial (24-bit, big-endian)
|
||||
- **bytes 4–7:** encrypted cipher output (24-bit rotating-register VAG cipher seeded from serial, rotated `4 + counter_low` times)
|
||||
|
||||
The `extra` word carries the frame_type; `protocol_display_name` is `Porsche Cayenne [First|Cont|Final]`.
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** PWM (SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1)
|
||||
- **RF modulation:** AM/OOK
|
||||
- **Encryption:** 24-bit rotating-register VAG cipher; counter recovered by brute force, `counter != 0` is the validity gate
|
||||
- **Frequencies:** 433.92 MHz, 868.35 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — wait for a LOW pulse at ~3370 µs.
|
||||
2. **Sync** — count sync pulses (HIGH and LOW at 3370 µs); a 5930 µs gap with ≥15 sync pulses enters GapHigh/GapLow.
|
||||
3. **GapHigh/GapLow** — expect the complementary 5930 µs gap pulse, then enter Data.
|
||||
4. **Data** — decode bit pairs (SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1); at 64 bits, `parse_data` applies the Cayenne frame_type gate + counter recovery and emits (or stays silent so Touareg owns the frame).
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (matches `porsche_cayenne_build_upload`). Emits a 4-frame burst (frame types 0b010/0b001/0b100/
|
||||
0b100, cipher counters cnt+1..cnt+4), each frame being 73 sync pairs + 1 gap pair + 64 MSB-first PWM bits.
|
||||
|
||||
## Validation
|
||||
|
||||
Verified by encode→decode round-trips / synthetic-frame checks (no local capture available). Unit tests
|
||||
cover the full encoder path, all three frame types, rejection of undocumented frame types and truncated
|
||||
frames, the counter-recovery limit matching the C, and shared cipher vectors with the Touareg port.
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Porsche Touareg Protocol
|
||||
|
||||
**Rust module:** `src/protocols/porsche_touareg.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/porsche_touareg.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Porsche Touareg uses PWM encoding with very long timing (1680/3370 us). 64-bit frame with a sync preamble (at least 15 sync pulses at 3370 us). Counter is recovered via brute-force using a 24-bit rotation cipher. Originally designed for the Porsche Cayenne.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|------------|----------|--------------|
|
||||
| Short | 1680 us | +/-500 us |
|
||||
| Long | 3370 us | +/-500 us |
|
||||
| Sync | 3370 us | Same as long |
|
||||
| Gap | 5930 us | +/-500 us |
|
||||
| Sync min | 15 pulses| |
|
||||
| Min bits | 64 | |
|
||||
|
||||
## PWM Bit Encoding
|
||||
|
||||
| Pair (LOW, HIGH) | Bit |
|
||||
|------------------|-----|
|
||||
| Short LOW + Long HIGH | 0 |
|
||||
| Long LOW + Short HIGH | 1 |
|
||||
|
||||
## Frame Layout (64 bits = 8 bytes)
|
||||
|
||||
| Byte | Content |
|
||||
|------|---------|
|
||||
| pkt[0] | (button << 4) \| (frame_type & 0x07) |
|
||||
| pkt[1] | serial bits [23:16] |
|
||||
| pkt[2] | serial bits [15:8] |
|
||||
| pkt[3] | serial bits [7:0] |
|
||||
| pkt[4..7] | Encrypted counter/rolling code |
|
||||
|
||||
- **Serial**: 24 bits from pkt[1..3]
|
||||
- **Button**: 4 bits (pkt[0] >> 4)
|
||||
- **Frame type**: 3 bits (pkt[0] & 0x07): 0x02=First, 0x01=Cont, 0x04=Final
|
||||
|
||||
## Counter Recovery (Brute-Force)
|
||||
|
||||
Counter is not in plaintext. The decoder tries counter values 1-256, calling `compute_frame()` for each, and checks if computed bytes [4..7] match received bytes [4..7].
|
||||
|
||||
### Compute Frame Algorithm (24-bit Rotate Cipher)
|
||||
|
||||
1. Initialize 24-bit rotate register from serial bytes: r_h=b3, r_m=b1, r_l=b2.
|
||||
2. ROTATE24: circular left shift across 3 bytes (h<-m, m<-l, l<-h).
|
||||
3. Rotate 4 times + counter_low more times.
|
||||
4. Compute encrypted bytes a9a/a9b/a9c from rotated values XOR'd with inverted counter bits.
|
||||
5. Assemble pkt[4..7] using bitfield packing.
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** -- Wait for LOW pulse matching sync (3370 us).
|
||||
2. **Sync** -- Count sync pulses (both HIGH and LOW). When count >= 15 and gap detected (5930 us), transition to GapHigh or GapLow.
|
||||
3. **GapHigh** -- Expect HIGH gap (5930 us); init data on match.
|
||||
4. **GapLow** -- Expect LOW gap (5930 us); init data on match.
|
||||
5. **Data** -- Decode bit pairs (LOW saved, HIGH completes). At 64 bits, parse data and brute-force counter.
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (decode-only).
|
||||
|
||||
## Frequencies
|
||||
|
||||
433.92 MHz, 868.35 MHz.
|
||||
+52
-19
@@ -1,43 +1,76 @@
|
||||
# PSA (Peugeot/Citroën) Protocol
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
**Rust module:** `src/protocols/psa.rs`
|
||||
# PSA (Peugeot/Citroen) Protocol
|
||||
|
||||
**Rust module:** `src/protocols/psa.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/psa.c`
|
||||
|
||||
## Overview
|
||||
|
||||
PSA uses Manchester at 250/500 µs symbol (125/250 µs sub-symbol for preamble). 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation. TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds an XOR layer. Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule).
|
||||
PSA uses Manchester encoding with dual preamble pattern support. 128 bits total: key1 (64) + validation (16) + key2/rest (48). Modified TEA (XTEA-like) with dynamic key selection (`key[sum & 3]` and `key[(sum >> 11) & 3]`). Two decode modes: mode 0x23 (direct XOR decrypt with checksum validation) and mode 0x36 (TEA with BF1/BF2 key schedules). Brute-force decryption for mode 0x36 is deferred/partial.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|------------|--------|---------|
|
||||
| Symbol short | 250 µs | ±100 µs |
|
||||
| Symbol long | 500 µs | ±100 µs |
|
||||
| Preamble | 125/250 µs sub-symbols | |
|
||||
| Parameter | Value | Notes |
|
||||
|-------------|--------|---------|
|
||||
| Symbol short | 250 us | +/-100 us |
|
||||
| Symbol long | 500 us | +/-100 us |
|
||||
| Preamble P1 | 250 us sub-symbols | Pattern 1 |
|
||||
| Preamble P2 | 125 us sub-symbols | Pattern 2 |
|
||||
| End marker | 1000 us (P1), 500 us (P2) | |
|
||||
| Min bits | 128 | |
|
||||
|
||||
## Encoding
|
||||
## Dual Preamble Patterns
|
||||
|
||||
Manchester; preamble uses 125/250 µs; then 250/500 µs symbols. TEA encrypt; mode 0x23 adds XOR.
|
||||
- **Pattern 1 (250 us):** Preamble uses 250 us sub-symbols; Manchester decode at 250/500 us. Threshold: 70+ transitions.
|
||||
- **Pattern 2 (125 us):** Preamble uses 125 us sub-symbols; Manchester decode at 125/250 us. Threshold: 69+ transitions.
|
||||
|
||||
Pattern type is auto-detected from the first preamble pulse duration.
|
||||
|
||||
## Frame Layout (128 bits)
|
||||
|
||||
- key1: 64 bits
|
||||
- validation: 16 bits
|
||||
- key2/rest: 48 bits
|
||||
- key1: 64 bits
|
||||
- validation: 16 bits
|
||||
- key2/rest: 48 bits
|
||||
|
||||
TEA decrypt key1 (and validation); mode 0x23: XOR with BF1 key schedule; mode 0x36: TEA with BF2 key schedule. Serial/button/counter extracted from decrypted key1.
|
||||
## Encryption
|
||||
|
||||
### Modified TEA (XTEA-like)
|
||||
|
||||
Uses dynamic key word selection per round:
|
||||
- Encrypt: `k_idx1 = sum & 3`, then `sum += DELTA`, then `k_idx2 = (sum >> 11) & 3`
|
||||
- Decrypt: `k_idx2 = (sum >> 11) & 3`, then `sum -= DELTA`, then `k_idx1 = sum & 3`
|
||||
- Round function: `(key[k_idx] + sum) ^ (((v >> 5) ^ (v << 4)) + v)`
|
||||
|
||||
### Mode 0x23 (Direct XOR)
|
||||
|
||||
1. Setup byte buffer from key1/key2 (little-endian unpack).
|
||||
2. Calculate checksum over buffer[2..8] (nibble sum * 16).
|
||||
3. Validate: `(checksum ^ key2_high_byte) & 0xF0 == 0`.
|
||||
4. XOR decrypt using `psa_copy_reverse` byte reordering.
|
||||
5. Extract: serial (24-bit), counter (16-bit), button (4-bit), CRC.
|
||||
|
||||
### Mode 0x36 (TEA Brute-Force)
|
||||
|
||||
Direct TEA decrypt attempted with BF1 and BF2 key schedules. Full brute-force (16M iterations per schedule) is available in ProtoPirate but not fully ported to KAT.
|
||||
|
||||
Key schedules:
|
||||
- BF1: `[0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A64]`
|
||||
- BF2: `[0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04]`
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **WaitEdge** — Wait for edge/preamble.
|
||||
2. **CountPattern** — Count preamble pattern (125/250 µs).
|
||||
3. **DecodeManchester** — Manchester decode 128 bits; TEA decrypt; apply mode (0x23 XOR or 0x36); extract fields.
|
||||
4. **End** — End marker (e.g. 1000 µs); return decode.
|
||||
1. **WaitEdge (State0)** -- Detect preamble pattern type from first HIGH pulse (250 us -> Pattern 1, 125 us -> Pattern 2).
|
||||
2. **CountPattern250 (State1)** -- Count 250 us preamble pairs; on long pulse with count > 70, transition to Manchester decode.
|
||||
3. **DecodeManchester250 (State2)** -- Manchester decode at 250/500 us. End on 1000 us marker or 121+ bits.
|
||||
4. **CountPattern125 (State3)** -- Count 125 us preamble pairs; on 250 us pulse with count >= 69, transition to Manchester decode.
|
||||
5. **DecodeManchester125 (State4)** -- Manchester decode at 125/250 us. End on 500 us marker or 121+ bits.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported; preamble, Manchester 128 bits, TEA (+ XOR for 0x23).
|
||||
Supported (mode 0x23 only). XOR encrypt, then modified TEA encrypt with BF1 key schedule. Preamble: 70 cycles of 125/125 us + 250/250 us sync. Manchester encoded key1 (64 bits) + validation (16 bits). End marker: 1000 us LOW.
|
||||
|
||||
## Frequencies
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# PSA2 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/psa2.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/psa2.c`
|
||||
|
||||
## Overview
|
||||
|
||||
PSA2 (Peugeot/Citroën — internal name "PSA OLD") is the OLDER PSA variant, distinct from KAT's existing
|
||||
`psa` (modified-TEA/XEA) decoder. Manchester encoding: 250/500 µs symbol (Pattern 1, standard rate) or
|
||||
125/250 µs (Pattern 2, half rate), using the canonical Flipper `manchester_advance` table. 128-bit frame
|
||||
= key1 (64 bits) + key2/validation word: the decoder collects 64 bits → key1, then 16 more (to 80 bits)
|
||||
→ the 16-bit validation field / key2_low.
|
||||
|
||||
Crypto: TEA (Tiny Encryption Algorithm) with a dual brute-force fallback (BF1 0x23000000–0x24000000, BF2
|
||||
0xF3000000–0xF4000000) and a mode23/mode36 selector, validated via a nibble checksum.
|
||||
|
||||
**Performance:** the live decoder only ever runs the cheap O(1) mode23 XOR path
|
||||
(`direct_xor_decrypt`) per frame — it NEVER runs the TEA brute force. The bounded TEA brute force
|
||||
(`decrypt_full`, ~16.7M iters per range) is ported faithfully but is reserved for a deferred-decrypt UI
|
||||
action, mirroring the C exactly. Beyond the C's nibble-checksum gate, KAT adds two false-positive
|
||||
suppressors required by its feed-all-decoders model: a `key2_high` precondition and a valid-button check
|
||||
(PSA2 buttons are Lock=0, Unlock=1, Trunk=2 only). Emission is gated strictly on a successful,
|
||||
field-bearing mode23 decrypt.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|----------------|--------|-----------------------------|
|
||||
| Short (P1) | 250 µs | ±100 µs (te_delta) |
|
||||
| Long (P1) | 500 µs | ±100 µs |
|
||||
| Short (P2) | 125 µs | ±50 µs (half rate) |
|
||||
| Long (P2) | 250 µs | ±50 µs |
|
||||
| End marker | 1000 µs (P1) / 500 µs (P2) | |
|
||||
| Min bits | 128 | key1 (64) + validation (16) collected |
|
||||
| Preamble | >0x46 pairs (P1) / >0x45 (P2) | |
|
||||
|
||||
## Frame Layout (128 bits = key1 64-bit + key2/validation)
|
||||
|
||||
The decoder collects 64 bits → key1, then 16 bits → the validation field (key2_low). After the mode23 XOR
|
||||
decrypt (fields from `extract_fields_mode23`):
|
||||
|
||||
- **serial:** `(buf[2]<<16) | (buf[3]<<8) | buf[4]` (24-bit)
|
||||
- **button:** `buf[8] & 0xF` — Lock=0, Unlock=1, Trunk=2
|
||||
- **counter:** `(buf[5]<<8) | buf[6]` (16-bit)
|
||||
- **crc:** `buf[7]`
|
||||
|
||||
`data` = `(key1_high << 32) | key1_low` (64 bits).
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Manchester (canonical Flipper table; standard 250/500 µs or half-rate 125/250 µs)
|
||||
- **RF modulation:** AM (OOK)
|
||||
- **Encryption:** TEA + nibble-checksum-gated mode23 XOR path; brute-force fallback reserved for offline decrypt
|
||||
- **Frequencies:** 433.92 MHz
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **WaitEdge (State0)** — detect the preamble rate (250 µs → Pattern 1, 125 µs → Pattern 2).
|
||||
2. **CountPattern250/125 (State1/3)** — count preamble pulses past the threshold, then a long pulse enters the Manchester decode state.
|
||||
3. **DecodeManchester250/125 (State2/4)** — Manchester-decode to 80 bits (key1 + validation), detect end-of-packet, then `finalize_frame` runs the cheap mode23 XOR gate and emits only on a successful field-bearing decrypt.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported (mode23 path). Increments the counter, builds key1/validation via `encode_mode23` (inverse XOR
|
||||
stage + nibble checksum), then emits an 80-pair preamble, a sync, 64 key1 bits + 16 validation bits
|
||||
MSB-first, and an end burst.
|
||||
|
||||
## Validation
|
||||
|
||||
Decodes REAL IMPORTS captures (IMPORTS/GROUPE PSA, serial `0x99EB25`). Unit tests also cover the TEA
|
||||
round trip, the mode23 encode→decode round trip, the nibble checksum vs. the reference, and the emitted
|
||||
Manchester frame length.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Scher-Khan Protocol
|
||||
|
||||
**Rust module:** `src/protocols/scher_khan.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Star Line Protocol
|
||||
|
||||
**Rust module:** `src/protocols/star_line.rs`
|
||||
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #6366f1;
|
||||
--secondary-color: #8b5cf6;
|
||||
--text-color: #1f2937;
|
||||
--light-bg: #f9fafb;
|
||||
--border-color: #e5e7eb;
|
||||
--success-color: #10b981;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
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 {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
padding: 40px 0 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
header .tagline {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.navbar ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
padding: 1rem 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.navbar a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
background: var(--light-bg);
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.1rem;
|
||||
color: #6b7280;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.cta-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 28px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #4f46e5;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: var(--primary-color);
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--light-bg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Features Section */
|
||||
.features {
|
||||
padding: 60px 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.features h2 {
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: 2rem;
|
||||
background: var(--light-bg);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--primary-color);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Protocols Section */
|
||||
.protocols {
|
||||
padding: 60px 0;
|
||||
background: var(--light-bg);
|
||||
}
|
||||
|
||||
.protocols h2 {
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.protocol-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.protocol-column h3 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.protocol-column ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.protocol-column li {
|
||||
padding: 0.5rem 0;
|
||||
color: #4b5563;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.protocol-column li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Requirements Section */
|
||||
.requirements {
|
||||
padding: 60px 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.requirements h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.requirements h3 {
|
||||
color: var(--primary-color);
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.requirements ul {
|
||||
list-style: none;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.requirements li {
|
||||
padding: 0.75rem 0 0.75rem 2rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.requirements li:before {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--success-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Documentation Section */
|
||||
.documentation {
|
||||
padding: 60px 0;
|
||||
background: var(--light-bg);
|
||||
}
|
||||
|
||||
.documentation h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.documentation > .container > p {
|
||||
margin-bottom: 2rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.doc-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.doc-link {
|
||||
padding: 12px 16px;
|
||||
background: white;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.doc-link:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Security Notice */
|
||||
.security-notice {
|
||||
padding: 40px 0;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.security-notice h3 {
|
||||
color: #dc2626;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.security-notice p {
|
||||
color: #7f1d1d;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
background: var(--text-color);
|
||||
color: white;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.features h2,
|
||||
.protocols h2,
|
||||
.requirements h2,
|
||||
.documentation h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar ul {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cta-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Subaru Protocol
|
||||
|
||||
**Rust module:** `src/protocols/subaru.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Suzuki Protocol
|
||||
|
||||
**Rust module:** `src/protocols/suzuki.rs`
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Toyota Protocol
|
||||
|
||||
**Rust module:** `src/protocols/toyota.rs`
|
||||
**Reference:** `Flipper-ARF lib/subghz/protocols/toyota.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Toyota / Lexus KeeLoq keyfobs, a dual-variant decoder. Two variants are detected from the first HIGH
|
||||
pulse width (threshold 310 µs):
|
||||
|
||||
- **Variant A** (Corolla / 433.92 MHz) — PWM pairs: LS (long HIGH + short LOW) = bit 0, SL (short HIGH +
|
||||
long LOW) = bit 1. Preamble = repeated short-short pairs; first non-SS pair is the first data bit.
|
||||
Frame = 68 bits.
|
||||
- **Variant B** (Tundra / 315 MHz) — NRZ: each individual pulse encodes one bit by a midpoint classifier
|
||||
(≤287 µs → 0, >287 µs → 1). Preamble = short-HIGH/long-LOW pairs terminated by a sync gap (LOW between
|
||||
1500 and 2600 µs). Frame = 67 bits.
|
||||
|
||||
KeeLoq hopping: the hop field is left encrypted (the reference does not decrypt or run a CRC). Emission
|
||||
is gated tightly (exact frame bit count + structural preamble/sync + non-zero serial) so it does not
|
||||
false-match the other KeeLoq-PWM protocols. The shared 433 MHz KeeLoq-PWM air encoding means a Variant-A
|
||||
frame that also satisfies Kia V3/V4's 68-bit/CRC4 structure is claimed by Kia V3/V4 first (earlier in the
|
||||
registry); Toyota uniquely claims 60-bit frames and Variant-B NRZ at 315 MHz.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|------------------|--------|-----------------------------|
|
||||
| A short | 400 µs | ±175 µs (Variant A) |
|
||||
| A long | 800 µs | ±175 µs |
|
||||
| B short | 200 µs | ±120 µs (Variant B preamble)|
|
||||
| B long | 390 µs | ±120 µs |
|
||||
| B NRZ midpoint | 287 µs | ≤ → 0, > → 1 |
|
||||
| B sync gap | 1500–2600 µs | |
|
||||
| Min bits | 60 | A frame = 68, B frame = 67 |
|
||||
| Variant threshold| 310 µs | first HIGH: < B, ≥ A |
|
||||
|
||||
## Frame Layout
|
||||
|
||||
`data = (hop << 32) | (serial << 4) | button` (matches the reference `generic.data`):
|
||||
|
||||
- **hop:** 32-bit KeeLoq ciphertext (left encrypted)
|
||||
- **serial:** 28-bit
|
||||
- **button:** 4-bit
|
||||
|
||||
## RF
|
||||
|
||||
- **Encoding:** Variant A = PWM pairs; Variant B = NRZ
|
||||
- **RF modulation:** AM/OOK
|
||||
- **Encryption:** KeeLoq hop left encrypted (no key / no CRC); a fully-structured frame of the exact bit count with non-zero serial is the validity criterion
|
||||
- **Frequencies:** 433.92 MHz (Variant A), 315 MHz (Variant B)
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** — detect the variant from the first SHORT HIGH (<310 µs → B, ≥310 µs → A).
|
||||
2. **Variant A** — PreambleA (count SS pairs) → DataA (decode LS/SL pairs, cap at 68 bits, emit on the terminating gap so Kia wins shared frames by registry order).
|
||||
3. **Variant B** — PreambleB (short-HIGH/long-LOW pairs) → on the sync gap → DataB (each pulse is one NRZ bit; emit at 67 bits or on an end gap).
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (the reference `encoder` field is NULL). Decode-only.
|
||||
|
||||
## Validation
|
||||
|
||||
Decodes REAL IMPORTS captures (IMPORTS Toyota + Lexus Camry, NRZ Variant B). Unit tests also cover
|
||||
synthetic Variant A and Variant B frames and rejection of an all-zero serial.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# VAG (VW/Audi/Seat/Skoda) Protocol
|
||||
|
||||
**Rust module:** `src/protocols/vag.rs`
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 813 KiB |
+122
-14
@@ -125,13 +125,16 @@ pub enum InputMode {
|
||||
FobMetaModel,
|
||||
/// Fob export metadata: editing region field
|
||||
FobMetaRegion,
|
||||
/// Fob export metadata: editing command field
|
||||
FobMetaCommand,
|
||||
/// Fob export metadata: editing notes field
|
||||
FobMetaNotes,
|
||||
/// Capture metadata (Year/Make/Model/Region) for vuln lookup — press i on a capture
|
||||
/// Capture metadata (Year/Make/Model/Region/Command) for vuln lookup — press i on a capture
|
||||
CaptureMetaYear,
|
||||
CaptureMetaMake,
|
||||
CaptureMetaModel,
|
||||
CaptureMetaRegion,
|
||||
CaptureMetaCommand,
|
||||
/// License overlay (centered box)
|
||||
License,
|
||||
/// Credits overlay (centered box)
|
||||
@@ -217,7 +220,7 @@ impl SettingsField {
|
||||
}
|
||||
|
||||
/// Common keyfob frequencies (Hz)
|
||||
pub const PRESET_FREQUENCIES: [(u32, &str); 9] = [
|
||||
pub const PRESET_FREQUENCIES: [(u32, &str); 10] = [
|
||||
(300_000_000, "300.00 MHz"),
|
||||
(303_875_000, "303.875 MHz"),
|
||||
(310_000_000, "310.00 MHz"),
|
||||
@@ -225,6 +228,7 @@ pub const PRESET_FREQUENCIES: [(u32, &str); 9] = [
|
||||
(318_000_000, "318.00 MHz"),
|
||||
(390_000_000, "390.00 MHz"),
|
||||
(433_920_000, "433.92 MHz"),
|
||||
(434_420_000, "434.42 MHz"),
|
||||
(868_350_000, "868.35 MHz"),
|
||||
(915_000_000, "915.00 MHz"),
|
||||
];
|
||||
@@ -356,14 +360,17 @@ pub struct App {
|
||||
pub fob_meta_model: String,
|
||||
/// Region input buffer (e.g. NA, EU, APAC, etc.)
|
||||
pub fob_meta_region: String,
|
||||
/// Command input buffer (e.g. Unlock, Lock)
|
||||
pub fob_meta_command: String,
|
||||
/// Notes input buffer
|
||||
pub fob_meta_notes: String,
|
||||
|
||||
// -- Capture metadata (Year/Make/Model/Region for vuln lookup, set via 'i') --
|
||||
// -- Capture metadata (Year/Make/Model/Region/Command for vuln lookup, set via 'i') --
|
||||
pub capture_meta_year: String,
|
||||
pub capture_meta_make: String,
|
||||
pub capture_meta_model: String,
|
||||
pub capture_meta_region: String,
|
||||
pub capture_meta_command: String,
|
||||
/// Which capture is being edited (when in CaptureMeta* modes)
|
||||
pub capture_meta_capture_id: Option<u32>,
|
||||
|
||||
@@ -485,11 +492,13 @@ impl App {
|
||||
fob_meta_make: String::new(),
|
||||
fob_meta_model: String::new(),
|
||||
fob_meta_region: String::new(),
|
||||
fob_meta_command: String::new(),
|
||||
fob_meta_notes: String::new(),
|
||||
capture_meta_year: String::new(),
|
||||
capture_meta_make: String::new(),
|
||||
capture_meta_model: String::new(),
|
||||
capture_meta_region: String::new(),
|
||||
capture_meta_command: String::new(),
|
||||
capture_meta_capture_id: None,
|
||||
pending_transmit_queue: Vec::new(),
|
||||
pending_transmit_restore: None,
|
||||
@@ -1298,12 +1307,52 @@ impl App {
|
||||
.unwrap_or_else(|_| path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// Default export filename for .fob: Year_Make_Model_Region_Command (same format for all captures).
|
||||
/// Uses capture metadata when set; fallbacks: make from protocol for known, command from button_name(), else "Unknown".
|
||||
fn default_export_filename(capture: &Capture) -> String {
|
||||
format!(
|
||||
"{}_{}",
|
||||
capture.protocol_name().replace(' ', "_").to_lowercase(),
|
||||
capture.serial_hex()
|
||||
)
|
||||
let year = capture
|
||||
.year
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
let make = capture
|
||||
.make
|
||||
.as_deref()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.trim().replace(' ', "_"))
|
||||
.unwrap_or_else(|| {
|
||||
if capture.protocol_name().eq_ignore_ascii_case("unknown") {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
Self::get_make_for_protocol(capture.protocol_name())
|
||||
.trim()
|
||||
.replace(' ', "_")
|
||||
}
|
||||
});
|
||||
let model = capture
|
||||
.model
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
let region = capture
|
||||
.region
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
let cmd_str = capture
|
||||
.command
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| capture.button_name())
|
||||
.trim();
|
||||
let command = if cmd_str.is_empty() || cmd_str == "-" {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
cmd_str.replace(' ', "_")
|
||||
};
|
||||
format!("{}_{}_{}_{}_{}", year, make, model, region, command)
|
||||
}
|
||||
|
||||
/// Start .fob export by entering filename input mode
|
||||
@@ -1313,13 +1362,19 @@ impl App {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Pre-fill filename from protocol + serial
|
||||
let default_name = self.captures.iter().find(|c| c.id == id)
|
||||
// Pre-fill filename: Year_Make_Model_Region_Command_8HEX for all .fob exports
|
||||
let capture = self.captures.iter().find(|c| c.id == id);
|
||||
let default_name = capture
|
||||
.map(|c| Self::default_export_filename(c))
|
||||
.unwrap_or_else(|| format!("capture_{}", id));
|
||||
let suffix_nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64;
|
||||
let suffix = (suffix_nanos.wrapping_add(id as u64 * 2654435761) % 0x100_000_000) as u32;
|
||||
self.export_filename = format!("{}_{:08X}", default_name, suffix);
|
||||
|
||||
// Pre-fill metadata from capture if set, otherwise make from protocol
|
||||
let capture = self.captures.iter().find(|c| c.id == id);
|
||||
let make = capture
|
||||
.and_then(|c| c.make.as_ref().map(String::clone))
|
||||
.filter(|s| !s.is_empty())
|
||||
@@ -1329,7 +1384,6 @@ impl App {
|
||||
.unwrap_or_default()
|
||||
});
|
||||
self.export_capture_id = Some(id);
|
||||
self.export_filename = default_name;
|
||||
self.export_format = Some(ExportFormat::Fob);
|
||||
self.fob_meta_year = capture
|
||||
.and_then(|c| c.year.as_ref())
|
||||
@@ -1344,6 +1398,16 @@ impl App {
|
||||
.and_then(|c| c.region.as_ref())
|
||||
.map(String::clone)
|
||||
.unwrap_or_default();
|
||||
self.fob_meta_command = capture
|
||||
.and_then(|c| c.command.clone())
|
||||
.unwrap_or_else(|| {
|
||||
let b = capture.map(|c| c.button_name().to_string()).unwrap_or_default();
|
||||
if b.is_empty() || b == "-" {
|
||||
String::new()
|
||||
} else {
|
||||
b
|
||||
}
|
||||
});
|
||||
self.fob_meta_notes = String::new();
|
||||
self.input_mode = InputMode::ExportFilename;
|
||||
Ok(())
|
||||
@@ -1377,10 +1441,32 @@ impl App {
|
||||
make: self.fob_meta_make.clone(),
|
||||
model: self.fob_meta_model.clone(),
|
||||
region: self.fob_meta_region.clone(),
|
||||
command: self.fob_meta_command.clone(),
|
||||
notes: self.fob_meta_notes.clone(),
|
||||
};
|
||||
|
||||
let filename = format!("{}.fob", self.export_filename);
|
||||
// All .fob exports use Year_Make_Model_Region_Command_8HEX; append 8-hex if user removed it
|
||||
let already_has_8hex = self.export_filename.len() >= 9
|
||||
&& self.export_filename.as_bytes()[self.export_filename.len() - 9] == b'_'
|
||||
&& self.export_filename[self.export_filename.len() - 8..]
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_hexdigit());
|
||||
let filename = if already_has_8hex {
|
||||
format!("{}.fob", self.export_filename.trim())
|
||||
} else {
|
||||
let base = self.export_filename.trim();
|
||||
let suffix_nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64;
|
||||
let suffix = (suffix_nanos.wrapping_add(id as u64 * 2654435761) % 0x100_000_000) as u32;
|
||||
let hex_suffix = format!("{:08X}", suffix);
|
||||
if base.is_empty() {
|
||||
format!("unknown_{}_{}.fob", id, hex_suffix)
|
||||
} else {
|
||||
format!("{}_{}.fob", base, hex_suffix)
|
||||
}
|
||||
};
|
||||
let path = export_dir.join(&filename);
|
||||
|
||||
crate::export::fob::export_fob(
|
||||
@@ -1415,6 +1501,16 @@ impl App {
|
||||
.and_then(|c| c.region.as_ref())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default();
|
||||
self.capture_meta_command = capture
|
||||
.and_then(|c| c.command.clone())
|
||||
.unwrap_or_else(|| {
|
||||
let b = capture.map(|c| c.button_name().to_string()).unwrap_or_default();
|
||||
if b.is_empty() || b == "-" {
|
||||
String::new()
|
||||
} else {
|
||||
b
|
||||
}
|
||||
});
|
||||
self.capture_meta_capture_id = Some(capture_id);
|
||||
self.input_mode = InputMode::CaptureMetaYear;
|
||||
}
|
||||
@@ -1434,6 +1530,7 @@ impl App {
|
||||
capture.make = Some(self.capture_meta_make.clone()).filter(|s| !s.is_empty());
|
||||
capture.model = Some(self.capture_meta_model.clone()).filter(|s| !s.is_empty());
|
||||
capture.region = Some(self.capture_meta_region.clone()).filter(|s| !s.is_empty());
|
||||
capture.command = Some(self.capture_meta_command.clone()).filter(|s| !s.is_empty());
|
||||
}
|
||||
self.input_mode = InputMode::Normal;
|
||||
self.capture_meta_capture_id = None;
|
||||
@@ -1801,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",
|
||||
}
|
||||
}
|
||||
@@ -1834,6 +1941,7 @@ impl App {
|
||||
make: None,
|
||||
model: None,
|
||||
region: None,
|
||||
command: None,
|
||||
source_file: None,
|
||||
};
|
||||
self.next_capture_id += 1;
|
||||
|
||||
+43
-2
@@ -76,6 +76,9 @@ pub struct Capture {
|
||||
/// Region (e.g. NA, EU) for vulnerability lookup and .fob export.
|
||||
#[serde(default)]
|
||||
pub region: Option<String>,
|
||||
/// User-editable command label (e.g. Unlock, Lock) for .fob export and filename; set via 'i' or export form.
|
||||
#[serde(default)]
|
||||
pub command: Option<String>,
|
||||
/// Source file path when imported from .sub or .fob; None for live captures.
|
||||
#[serde(default)]
|
||||
pub source_file: Option<String>,
|
||||
@@ -157,6 +160,7 @@ impl Capture {
|
||||
make: None,
|
||||
model: None,
|
||||
region: None,
|
||||
command: None,
|
||||
source_file: None,
|
||||
}
|
||||
}
|
||||
@@ -221,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,
|
||||
}
|
||||
@@ -248,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,
|
||||
@@ -272,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",
|
||||
}
|
||||
}
|
||||
|
||||
+21
-4
@@ -14,6 +14,8 @@ pub struct FobMetadata {
|
||||
pub make: String,
|
||||
pub model: String,
|
||||
pub region: String,
|
||||
/// Command label (e.g. Unlock, Lock) for export filename and .fob vehicle info.
|
||||
pub command: String,
|
||||
pub notes: String,
|
||||
}
|
||||
|
||||
@@ -61,6 +63,9 @@ pub struct FobVehicleInfo {
|
||||
pub model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub region: Option<String>,
|
||||
/// Command label (e.g. Unlock, Lock); optional for backwards compatibility.
|
||||
#[serde(default)]
|
||||
pub command: Option<String>,
|
||||
#[serde(default)]
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
@@ -119,6 +124,14 @@ pub fn export_fob(
|
||||
Some(m.notes.clone())
|
||||
}
|
||||
});
|
||||
let command = metadata.and_then(|m| {
|
||||
let s = m.command.trim();
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s.to_string())
|
||||
}
|
||||
});
|
||||
|
||||
let raw_pairs = if include_raw && !capture.raw_pairs.is_empty() {
|
||||
Some(
|
||||
@@ -163,6 +176,7 @@ pub fn export_fob(
|
||||
make,
|
||||
model,
|
||||
region,
|
||||
command,
|
||||
notes,
|
||||
},
|
||||
capture: FobCapture {
|
||||
@@ -287,6 +301,7 @@ fn import_fob_v2(fob: &FobFile, next_id: u32) -> Result<Capture> {
|
||||
CaptureStatus::Unknown
|
||||
};
|
||||
|
||||
let vehicle = &fob.vehicle;
|
||||
Ok(Capture {
|
||||
id: next_id,
|
||||
timestamp,
|
||||
@@ -302,10 +317,11 @@ fn import_fob_v2(fob: &FobFile, next_id: u32) -> Result<Capture> {
|
||||
raw_pairs,
|
||||
status,
|
||||
received_rf: None,
|
||||
year: None,
|
||||
make: None,
|
||||
model: None,
|
||||
region: None,
|
||||
year: vehicle.year.map(|y| y.to_string()),
|
||||
make: Some(vehicle.make.clone()).filter(|s| !s.is_empty()),
|
||||
model: vehicle.model.clone().filter(|s| !s.is_empty()),
|
||||
region: vehicle.region.clone().filter(|s| !s.is_empty()),
|
||||
command: vehicle.command.clone().filter(|s| !s.is_empty()),
|
||||
source_file: None,
|
||||
})
|
||||
}
|
||||
@@ -377,6 +393,7 @@ fn import_fob_v1(fob: &FobFileV1, next_id: u32) -> Result<Capture> {
|
||||
make: None,
|
||||
model: None,
|
||||
region: None,
|
||||
command: None,
|
||||
source_file: None,
|
||||
})
|
||||
}
|
||||
|
||||
+37
-4
@@ -388,10 +388,10 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Region -> Notes
|
||||
// .fob export metadata: Region -> Command
|
||||
InputMode::FobMetaRegion => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.input_mode = InputMode::FobMetaNotes;
|
||||
app.input_mode = InputMode::FobMetaCommand;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.fob_meta_region.push(c);
|
||||
@@ -406,6 +406,24 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Command -> Notes
|
||||
InputMode::FobMetaCommand => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.input_mode = InputMode::FobMetaNotes;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.fob_meta_command.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.fob_meta_command.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.export_capture_id = None;
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Notes -> Export
|
||||
InputMode::FobMetaNotes => match key.code {
|
||||
KeyCode::Enter => {
|
||||
@@ -425,7 +443,7 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// Capture metadata (Year/Make/Model/Region for vuln lookup)
|
||||
// Capture metadata (Year/Make/Model/Region/Command for vuln lookup)
|
||||
InputMode::CaptureMetaYear => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.input_mode = InputMode::CaptureMetaMake;
|
||||
@@ -475,7 +493,7 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
},
|
||||
InputMode::CaptureMetaRegion => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.save_capture_meta();
|
||||
app.input_mode = InputMode::CaptureMetaCommand;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.capture_meta_region.push(c);
|
||||
@@ -488,6 +506,21 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::CaptureMetaCommand => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.save_capture_meta();
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.capture_meta_command.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.capture_meta_command.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.cancel_capture_meta();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
InputMode::LoadFileBrowser => {
|
||||
const VISIBLE: usize = 16;
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
//! BMW CAS4 protocol decoder
|
||||
//!
|
||||
//! Aligned with Flipper-ARF reference: `lib/subghz/protocols/bmw_cas4.c` and `bmw_cas4.h`.
|
||||
//! Manchester 500/1000µs (te_delta 150), 64 bits (8 bytes), AM/OOK. The CAS4 rolling cipher's
|
||||
//! manufacturer key is not available, so the encrypted portion is left as-is — the frame is only
|
||||
//! framed and validated, not decrypted. Emission is gated on two fixed marker bytes
|
||||
//! (byte[0]==0x30 && byte[6]==0xC5), which makes the protocol specific and prevents false matches.
|
||||
//! Decode-only: the reference encoder is a non-functional stub (`yield` returns reset,
|
||||
//! `deserialize` returns error), so `supports_encoding()` is false and `encode()` is None.
|
||||
//!
|
||||
//! Decoder steps: Reset → Preamble (≥10 pulses of 300-700µs) → Data. The preamble→data transition
|
||||
//! is triggered by a long low gap (≥1800µs). Manchester polarity is `level ? Low : High` (Flipper
|
||||
//! manchester_decoder.h event order: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh), the same
|
||||
//! mapping as Ford V0 / common.
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 500;
|
||||
const TE_LONG: u32 = 1000;
|
||||
const TE_DELTA: u32 = 150;
|
||||
const DATA_BITS: usize = 64;
|
||||
const DATA_BYTES: usize = 8;
|
||||
|
||||
// Preamble pulse window and minimum count (BMW_CAS4_PREAMBLE_PULSE_MIN/MAX, BMW_CAS4_PREAMBLE_MIN).
|
||||
const PREAMBLE_PULSE_MIN: u32 = 300;
|
||||
const PREAMBLE_PULSE_MAX: u32 = 700;
|
||||
const PREAMBLE_MIN: u16 = 10;
|
||||
// Long low gap that separates the preamble from the data burst (BMW_CAS4_GAP_MIN).
|
||||
const GAP_MIN: u32 = 1800;
|
||||
|
||||
// Fixed validation markers (BMW_CAS4_BYTE0_MARKER, BMW_CAS4_BYTE6_MARKER).
|
||||
const BYTE0_MARKER: u8 = 0x30;
|
||||
const BYTE6_MARKER: u8 = 0xC5;
|
||||
|
||||
/// Manchester state machine (Flipper manchester_decoder.h transition table).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum ManchesterState {
|
||||
Mid0 = 0,
|
||||
Mid1 = 1,
|
||||
Start0 = 2,
|
||||
Start1 = 3,
|
||||
}
|
||||
|
||||
/// Decoder step states (matches BmwCas4DecoderStep in bmw_cas4.c).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble,
|
||||
Data,
|
||||
}
|
||||
|
||||
/// BMW CAS4 protocol decoder (matches SubGhzProtocolDecoderBmwCas4).
|
||||
pub struct BmwCas4Decoder {
|
||||
step: DecoderStep,
|
||||
manchester_state: ManchesterState,
|
||||
preamble_count: u16,
|
||||
raw_data: [u8; DATA_BYTES],
|
||||
bit_count: usize,
|
||||
decode_data: u64,
|
||||
}
|
||||
|
||||
impl BmwCas4Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
preamble_count: 0,
|
||||
raw_data: [0; DATA_BYTES],
|
||||
bit_count: 0,
|
||||
decode_data: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset accumulators (matches subghz_protocol_decoder_bmw_cas4_reset).
|
||||
fn reset_state(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.preamble_count = 0;
|
||||
self.raw_data = [0; DATA_BYTES];
|
||||
self.bit_count = 0;
|
||||
self.decode_data = 0;
|
||||
}
|
||||
|
||||
fn is_short(d: u32) -> bool {
|
||||
duration_diff!(d, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
|
||||
fn is_long(d: u32) -> bool {
|
||||
duration_diff!(d, TE_LONG) < TE_DELTA
|
||||
}
|
||||
|
||||
/// True when the duration is within the preamble pulse window (300-700µs).
|
||||
fn is_preamble_pulse(d: u32) -> bool {
|
||||
d >= PREAMBLE_PULSE_MIN && d <= PREAMBLE_PULSE_MAX
|
||||
}
|
||||
|
||||
/// Flipper Manchester transition table. Event 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh.
|
||||
/// Returns Some(bit) when a bit emits.
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||
let (new_state, emit) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
|
||||
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
|
||||
|
||||
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
|
||||
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
|
||||
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
_ => (ManchesterState::Mid1, false),
|
||||
};
|
||||
self.manchester_state = new_state;
|
||||
if emit { Some((event & 1) == 1) } else { None }
|
||||
}
|
||||
|
||||
/// Map (level, duration) → Manchester event. BMW CAS4 polarity: level ? Low : High
|
||||
/// (matches the C `event = level ? ManchesterEventShortLow : ManchesterEventShortHigh`).
|
||||
fn pulse_event(level: bool, duration: u32) -> Option<u8> {
|
||||
if Self::is_short(duration) {
|
||||
Some(if level { 0 } else { 1 })
|
||||
} else if Self::is_long(duration) {
|
||||
Some(if level { 2 } else { 3 })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a decoded bit MSB-first into raw_data and the 64-bit accumulator
|
||||
/// (matches the C bit-packing in the Data step).
|
||||
fn add_bit(&mut self, bit: bool) {
|
||||
if self.bit_count < DATA_BITS {
|
||||
let byte_idx = self.bit_count / 8;
|
||||
let bit_pos = 7 - (self.bit_count % 8);
|
||||
if bit {
|
||||
self.raw_data[byte_idx] |= 1 << bit_pos;
|
||||
}
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
}
|
||||
self.bit_count += 1;
|
||||
}
|
||||
|
||||
/// Validate the fixed markers and build the decoded signal.
|
||||
/// Fields: serial = bytes[1..4] (24-bit), button = byte[7], counter = byte[5].
|
||||
/// The CAS4 rolling cipher is left undecrypted; the markers serve as the integrity gate.
|
||||
fn build_signal(&self) -> DecodedSignal {
|
||||
let b = &self.raw_data;
|
||||
let serial = ((b[1] as u32) << 16) | ((b[2] as u32) << 8) | (b[3] as u32);
|
||||
let counter = b[5] as u16;
|
||||
let button = b[7];
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: true, // fixed markers (byte[0]==0x30 && byte[6]==0xC5) validated the frame
|
||||
data: self.decode_data,
|
||||
data_count_bit: DATA_BITS,
|
||||
encoder_capable: false,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for BmwCas4Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"BMW CAS4"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: DATA_BITS,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
// SubGhzProtocolFlag_433 only (no 315 listed in the C).
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.reset_state();
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
// Begin on a high preamble pulse within the 300-700µs window.
|
||||
DecoderStep::Reset => {
|
||||
if level && Self::is_preamble_pulse(duration) {
|
||||
self.step = DecoderStep::Preamble;
|
||||
self.preamble_count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Preamble => {
|
||||
if Self::is_preamble_pulse(duration) {
|
||||
self.preamble_count += 1;
|
||||
} else if !level && duration >= GAP_MIN {
|
||||
if self.preamble_count >= PREAMBLE_MIN {
|
||||
// Enter data: clear accumulators and reset the Manchester state.
|
||||
self.bit_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.raw_data = [0; DATA_BYTES];
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.step = DecoderStep::Data;
|
||||
} else {
|
||||
self.reset_state();
|
||||
}
|
||||
} else {
|
||||
self.reset_state();
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Data => {
|
||||
if self.bit_count >= DATA_BITS {
|
||||
self.reset_state();
|
||||
return None;
|
||||
}
|
||||
|
||||
let event = Self::pulse_event(level, duration);
|
||||
if let Some(ev) = event {
|
||||
if let Some(bit) = self.manchester_advance(ev) {
|
||||
self.add_bit(bit);
|
||||
|
||||
if self.bit_count == DATA_BITS {
|
||||
let valid = self.raw_data[0] == BYTE0_MARKER
|
||||
&& self.raw_data[6] == BYTE6_MARKER;
|
||||
let result = if valid { Some(self.build_signal()) } else { None };
|
||||
self.reset_state();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Out-of-range pulse aborts the frame (matches the C ManchesterEventReset path).
|
||||
self.reset_state();
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BmwCas4Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Map a Manchester event index back to its (level, duration) pulse, using the decoder's
|
||||
/// polarity (level ? Low : High): 0=ShortLow(level,500), 1=ShortHigh(!level,500),
|
||||
/// 2=LongLow(level,1000), 3=LongHigh(!level,1000).
|
||||
fn event_pulse(event: u8) -> (bool, u32) {
|
||||
match event {
|
||||
0 => (true, TE_SHORT),
|
||||
1 => (false, TE_SHORT),
|
||||
2 => (true, TE_LONG),
|
||||
3 => (false, TE_LONG),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure transition table (same as `manchester_advance`) for the test encoder. Returns
|
||||
/// (next_state, Some(bit)) when a bit emits.
|
||||
fn advance(state: ManchesterState, event: u8) -> (ManchesterState, Option<bool>) {
|
||||
use ManchesterState::{Mid0, Mid1, Start0, Start1};
|
||||
let (ns, emit) = match (state, event) {
|
||||
(Mid0, 0) => (Mid0, false),
|
||||
(Mid0, 1) => (Start1, true),
|
||||
(Mid0, 2) => (Mid0, false),
|
||||
(Mid0, 3) => (Mid1, true),
|
||||
(Mid1, 0) => (Start0, true),
|
||||
(Mid1, 1) => (Mid1, false),
|
||||
(Mid1, 2) => (Mid0, true),
|
||||
(Mid1, 3) => (Mid1, false),
|
||||
(Start0, 0) => (Mid0, false),
|
||||
(Start0, 1) => (Mid0, false),
|
||||
(Start0, 2) => (Mid0, false),
|
||||
(Start0, 3) => (Mid1, false),
|
||||
(Start1, 0) => (Mid0, false),
|
||||
(Start1, 1) => (Mid1, false),
|
||||
(Start1, 2) => (Mid0, false),
|
||||
(Start1, 3) => (Mid1, false),
|
||||
_ => (Mid1, false),
|
||||
};
|
||||
(ns, if emit { Some((event & 1) == 1) } else { None })
|
||||
}
|
||||
|
||||
/// Faithfully Manchester-encode `bits` into level/duration pulses by *driving the decoder's
|
||||
/// own transition table*. This guarantees the produced waveform is one the decoder accepts —
|
||||
/// it does not assume any closed-form biphase rule (the Flipper table is differential, so a
|
||||
/// fixed per-bit pattern does not exist). Backtracking DFS over the tiny state space (with a
|
||||
/// visited set on (state, last_level, bit_index) to break no-emit cycles) finds a pulse stream
|
||||
/// whose decoded bits equal `bits`, respecting physical level alternation between pulses.
|
||||
///
|
||||
/// Note: the decoder starts at Mid1, which can only emit a `0` first — so the first bit must be
|
||||
/// 0 (true for any BMW CAS4 frame: byte[0]==0x30 begins with bit 0).
|
||||
fn manchester_encode_bits(bits: &[bool]) -> Option<Vec<(bool, u32)>> {
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn dfs(
|
||||
state: ManchesterState,
|
||||
last_level: Option<bool>,
|
||||
i: usize,
|
||||
bits: &[bool],
|
||||
acc: &mut Vec<u8>,
|
||||
seen: &mut std::collections::HashSet<(ManchesterState, Option<bool>, usize)>,
|
||||
) -> bool {
|
||||
if i == bits.len() {
|
||||
return true;
|
||||
}
|
||||
let key = (state, last_level, i);
|
||||
if seen.contains(&key) {
|
||||
return false;
|
||||
}
|
||||
seen.insert(key);
|
||||
for event in 0u8..4 {
|
||||
let (level, _dur) = event_pulse(event);
|
||||
if last_level == Some(level) {
|
||||
continue; // physical OOK stream must alternate level between pulses
|
||||
}
|
||||
let (ns, bit) = advance(state, event);
|
||||
match bit {
|
||||
None => {
|
||||
acc.push(event);
|
||||
if dfs(ns, Some(level), i, bits, acc, seen) {
|
||||
return true;
|
||||
}
|
||||
acc.pop();
|
||||
}
|
||||
Some(b) if b == bits[i] => {
|
||||
acc.push(event);
|
||||
if dfs(ns, Some(level), i + 1, bits, acc, seen) {
|
||||
return true;
|
||||
}
|
||||
acc.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
let mut events: Vec<u8> = Vec::new();
|
||||
let mut seen: HashSet<(ManchesterState, Option<bool>, usize)> = HashSet::new();
|
||||
if !dfs(ManchesterState::Mid1, None, 0, bits, &mut events, &mut seen) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Translate events → pulses, merging adjacent equal levels (a repeated level is a long).
|
||||
let mut merged: Vec<(bool, u32)> = Vec::new();
|
||||
for ev in events {
|
||||
let (lvl, dur) = event_pulse(ev);
|
||||
if let Some(last) = merged.last_mut() {
|
||||
if last.0 == lvl {
|
||||
last.1 += dur;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
merged.push((lvl, dur));
|
||||
}
|
||||
Some(merged)
|
||||
}
|
||||
|
||||
/// Build a full BMW CAS4 frame: preamble pulses, long low gap, then Manchester data.
|
||||
fn build_frame(bytes: &[u8; DATA_BYTES]) -> Vec<(bool, u32)> {
|
||||
let mut pairs: Vec<(bool, u32)> = Vec::new();
|
||||
// Preamble: 12 high pulses of ~500µs separated by short lows (within the 300-700 window).
|
||||
for _ in 0..12 {
|
||||
pairs.push((true, 500));
|
||||
pairs.push((false, 500));
|
||||
}
|
||||
// Long low gap (≥1800µs) ending the preamble. Overwrite the last low with the gap.
|
||||
if let Some(last) = pairs.last_mut() {
|
||||
if !last.0 {
|
||||
last.1 = GAP_MIN + 200;
|
||||
}
|
||||
}
|
||||
// Manchester-encoded data bits, MSB-first.
|
||||
let mut bits: Vec<bool> = Vec::with_capacity(DATA_BITS);
|
||||
for &byte in bytes.iter() {
|
||||
for i in (0..8).rev() {
|
||||
bits.push((byte >> i) & 1 != 0);
|
||||
}
|
||||
}
|
||||
let data = manchester_encode_bits(&bits).expect("test frame must be encodable (first bit 0)");
|
||||
pairs.extend(data);
|
||||
// Trailing gap to flush.
|
||||
pairs.push((false, GAP_MIN + 500));
|
||||
pairs
|
||||
}
|
||||
|
||||
/// Feed a pair stream through a fresh decoder and return the first decode (trying both
|
||||
/// polarities, matching the registry behaviour).
|
||||
fn decode_pairs(pairs: &[(bool, u32)]) -> Option<DecodedSignal> {
|
||||
for invert in [false, true] {
|
||||
let mut dec = BmwCas4Decoder::new();
|
||||
for &(lvl, dur) in pairs {
|
||||
let level = if invert { !lvl } else { lvl };
|
||||
if let Some(sig) = dec.feed(level, dur) {
|
||||
return Some(sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_synthetic_frame_with_markers() {
|
||||
// byte[0]=0x30 and byte[6]=0xC5 are the required markers; the rest is the (encrypted)
|
||||
// payload. serial = bytes[1..4], counter = byte[5], button = byte[7].
|
||||
let frame: [u8; DATA_BYTES] = [0x30, 0x12, 0x34, 0x56, 0xAB, 0x07, 0xC5, 0x02];
|
||||
let pairs = build_frame(&frame);
|
||||
let sig = decode_pairs(&pairs).expect("synthetic BMW CAS4 frame should decode");
|
||||
|
||||
assert!(sig.crc_valid, "fixed markers should set crc_valid=true");
|
||||
assert_eq!(sig.data_count_bit, DATA_BITS);
|
||||
assert_eq!(sig.data, u64::from_be_bytes(frame), "data must equal the 64-bit frame");
|
||||
assert_eq!(sig.serial, Some(0x123456), "serial = bytes[1..4]");
|
||||
assert_eq!(sig.counter, Some(0x07), "counter = byte[5]");
|
||||
assert_eq!(sig.button, Some(0x02), "button = byte[7]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_frame_with_wrong_markers() {
|
||||
// Same structure but byte[0] and byte[6] are NOT the markers → must not decode.
|
||||
let frame: [u8; DATA_BYTES] = [0x31, 0x12, 0x34, 0x56, 0xAB, 0x07, 0xC4, 0x02];
|
||||
let pairs = build_frame(&frame);
|
||||
assert!(
|
||||
decode_pairs(&pairs).is_none(),
|
||||
"a frame with wrong marker bytes must be rejected"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
//! Chrysler V0 protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/chrysler_v0.c` and
|
||||
//! `chrysler_v0.h`. Used by Chrysler/Dodge/Jeep keyfobs.
|
||||
//!
|
||||
//! Protocol: PWM with a short HIGH pulse and TWO long-LOW symbols. A "1" payload bit is
|
||||
//! HIGH≈600µs (te_one_short) + LOW≈3400µs (te_long_a); a "0" payload bit is HIGH≈300µs
|
||||
//! (te_short) + LOW≈3700µs (te_long_b). te_delta≈150, long_delta≈400, te_gap≈8000,
|
||||
//! frame_gap≈15600. ~24 preamble pairs (short HIGH + long_b LOW) precede each frame.
|
||||
//!
|
||||
//! Frame: 80 bits. The first 64 bits are `decode_data` (payload bytes 0..7), the last 16 bits
|
||||
//! are `data_2` (payload bytes 8,9). The 80-bit frame exceeds u64, so [DecodedSignal::data]
|
||||
//! reports the most-significant 64 bits and `data_count_bit = 80` (matches psa.rs/kia_v6.rs).
|
||||
//!
|
||||
//! Crypto (proprietary seed-XOR, ported exactly from chrysler_v0.c `decode`):
|
||||
//! - `seed = reverse6(key[0] >> 2)` — a 6-bit reversed counter used as the transform key.
|
||||
//! - `transform_block`: XOR all 9 transformed bytes with `xor_table[seed & 0x0F]`, with an extra
|
||||
//! nibble flip when the (Lock) button is set.
|
||||
//! - Dual payload A (seed even, carries serial+counter) / B (seed odd, carries serial). The frame
|
||||
//! is gated on a structural `check_ok` (matches the C), so it does not false-match other
|
||||
//! protocols. `crc_valid` reflects that `check_ok`.
|
||||
//!
|
||||
//! RF: AM, 315 + 433.92 MHz. Encoder present (ENABLE_EMULATE_FEATURE): builds preamble + dual
|
||||
//! 80-bit PWM frames with frame gaps.
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 0x12C; // 300
|
||||
const TE_DELTA: u32 = 0x96; // 150
|
||||
const TE_LONG_A: u32 = 0xD48; // 3400
|
||||
const TE_LONG_B: u32 = 0xE74; // 3700
|
||||
const TE_LONG_DELTA: u32 = 0x190; // 400
|
||||
const TE_GAP: u32 = 0x1F40; // 8000
|
||||
const TE_ONE_SHORT: u32 = 0x258; // 600
|
||||
const FRAME_GAP: u32 = 0x3CF0; // 15600
|
||||
const PREAMBLE_PAIRS: usize = 24;
|
||||
const DECODE_BIT_COUNT: usize = 0x50; // 80
|
||||
|
||||
/// XOR table (chrysler_v0_xor_table) — indexed by `seed & 0x0F`.
|
||||
const XOR_TABLE: [u8; 16] = [
|
||||
0x0F, 0x02, 0x40, 0x0C, 0x30, 0x0E, 0x70, 0x08, 0x10, 0x0A, 0x50, 0xF4, 0x2F, 0xF6, 0x6F, 0xF0,
|
||||
];
|
||||
|
||||
/// Decoder steps (matches Chrysler_V0DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Seek,
|
||||
Data,
|
||||
}
|
||||
|
||||
/// Chrysler V0 protocol decoder
|
||||
pub struct ChryslerV0Decoder {
|
||||
step: DecoderStep,
|
||||
packet_bit_count: u16,
|
||||
te_last: u32,
|
||||
decode_data: u64,
|
||||
decode_count_bit: u8,
|
||||
data_2: u16,
|
||||
}
|
||||
|
||||
/// Result of `decode_packet`: structural fields extracted from a candidate frame.
|
||||
struct Decoded {
|
||||
check_ok: bool,
|
||||
button: u8,
|
||||
/// Transform seed (reversed 6-bit counter); retained for diagnostics.
|
||||
#[allow(dead_code)]
|
||||
seed: u8,
|
||||
/// Serial: SnA (counter-frame) or SnB (serial-frame).
|
||||
serial: u32,
|
||||
/// Rolling counter (only meaningful for the A/even frame).
|
||||
counter: u32,
|
||||
}
|
||||
|
||||
impl ChryslerV0Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
packet_bit_count: 0,
|
||||
te_last: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
data_2: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_short(d: u32) -> bool {
|
||||
duration_diff!(d, TE_SHORT) <= TE_DELTA
|
||||
}
|
||||
|
||||
fn is_long_mark(d: u32) -> bool {
|
||||
duration_diff!(d, TE_LONG_A) <= TE_LONG_DELTA || duration_diff!(d, TE_LONG_B) <= TE_LONG_DELTA
|
||||
}
|
||||
|
||||
/// Reverse the low 6 bits of `value` (chrysler_v0_reverse6).
|
||||
fn reverse6(value: u32) -> u8 {
|
||||
let mut out: u8 = 0;
|
||||
let mut v = value;
|
||||
for _ in 0..6 {
|
||||
out = (out << 1) | ((v & 1) as u8);
|
||||
v >>= 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Transform 9 bytes with the seed-derived XOR mask (chrysler_v0_transform_block).
|
||||
/// `button == 1` (Lock) flips a nibble of the mask depending on the seed parity.
|
||||
fn transform_block(input: &[u8; 9], key: u8, button: u8) -> [u8; 9] {
|
||||
let mut mask = XOR_TABLE[(key & 0x0F) as usize];
|
||||
if button == 1 {
|
||||
mask ^= if (key & 1) != 0 { 0xF0 } else { 0x0F };
|
||||
}
|
||||
let mut out = [0u8; 9];
|
||||
for i in 0..9 {
|
||||
out[i] = input[i] ^ mask;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Port of chrysler_v0_decode_packet: extract seed/button/check_ok/serial/counter from the
|
||||
/// 64-bit `data` (payload bytes 0..7) plus the 16-bit `data_2` (payload bytes 8,9).
|
||||
fn decode_packet(data: u64, data_2: u16) -> Decoded {
|
||||
let key = data.to_be_bytes();
|
||||
let key2 = data_2;
|
||||
let seed = Self::reverse6((key[0] >> 2) as u32);
|
||||
|
||||
let b1_xor_b6 = key[6] ^ key[1];
|
||||
let msb_set = (key[0] & 0x80) != 0;
|
||||
|
||||
let mut check_ok;
|
||||
let mut button: u8;
|
||||
|
||||
if msb_set {
|
||||
let key2_low = (key2 & 0xFF) as u8;
|
||||
check_ok = (key[1] == key[5]) && (b1_xor_b6 == 0x62);
|
||||
button = if (key2_low ^ key[4]) == 0x10 { 2 } else { 1 };
|
||||
} else {
|
||||
check_ok = false;
|
||||
button = 1;
|
||||
|
||||
if (key[1] ^ 0xC3) == key[5] {
|
||||
if b1_xor_b6 == 0x04 {
|
||||
check_ok = true;
|
||||
} else {
|
||||
check_ok = b1_xor_b6 == 0x08;
|
||||
if b1_xor_b6 == 0x08 {
|
||||
button = 2;
|
||||
}
|
||||
}
|
||||
} else if b1_xor_b6 == 0x08 {
|
||||
button = 2;
|
||||
}
|
||||
// (b1_xor_b6 == 0x04 with mismatched key[5]: button stays 1, check_ok stays false.)
|
||||
}
|
||||
|
||||
let encoded: [u8; 9] = [
|
||||
key[1],
|
||||
key[2],
|
||||
key[3],
|
||||
key[4],
|
||||
key[5],
|
||||
key[6],
|
||||
key[7],
|
||||
(key2 >> 8) as u8,
|
||||
(key2 & 0xFF) as u8,
|
||||
];
|
||||
let decoded = Self::transform_block(&encoded, seed, button);
|
||||
|
||||
let (serial, counter) = if (seed & 1) != 0 {
|
||||
// Payload B: serial only.
|
||||
let sn_b = ((decoded[0] as u32) << 24)
|
||||
| ((decoded[1] as u32) << 16)
|
||||
| ((decoded[2] as u32) << 8)
|
||||
| (decoded[7] as u32);
|
||||
(sn_b, 0u32)
|
||||
} else {
|
||||
// Payload A: serial (SnA) + rolling counter.
|
||||
let sn_a = ((decoded[0] as u32) << 24)
|
||||
| ((decoded[1] as u32) << 16)
|
||||
| ((decoded[2] as u32) << 8)
|
||||
| (decoded[3] as u32);
|
||||
let cnt = sn_a;
|
||||
(sn_a, cnt)
|
||||
};
|
||||
|
||||
Decoded {
|
||||
check_ok,
|
||||
button,
|
||||
seed,
|
||||
serial,
|
||||
counter,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a DecodedSignal from a committed frame, if the structural check passes.
|
||||
fn commit(&self) -> Option<DecodedSignal> {
|
||||
let d = Self::decode_packet(self.decode_data, self.data_2);
|
||||
if !d.check_ok {
|
||||
return None;
|
||||
}
|
||||
Some(DecodedSignal {
|
||||
serial: Some(d.serial),
|
||||
button: Some(d.button),
|
||||
counter: Some((d.counter & 0xFFFF) as u16),
|
||||
crc_valid: d.check_ok,
|
||||
// 80-bit frame: report the most-significant 64 bits (payload bytes 0..7).
|
||||
data: self.decode_data,
|
||||
data_count_bit: DECODE_BIT_COUNT,
|
||||
encoder_capable: true,
|
||||
// Stash the low 16 bits (data_2 = payload bytes 8,9) for the encoder.
|
||||
extra: Some(self.data_2 as u64),
|
||||
protocol_display_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Map a KAT generic button to a Chrysler button code (1=Lock, 2=Unlock).
|
||||
fn map_button(button: u8) -> u8 {
|
||||
match button {
|
||||
0x01 => 1, // Lock
|
||||
0x02 => 2, // Unlock
|
||||
0x04 => 1, // Trunk → Lock (Chrysler V0 only models Lock/Unlock)
|
||||
0x08 => 2, // Panic → Unlock
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read one bit out of an MSB-first payload (chrysler_v0_payload_get_bit).
|
||||
fn payload_get_bit(payload: &[u8; 10], index: u8) -> u8 {
|
||||
let byte = payload[(index >> 3) as usize];
|
||||
let shift = 7 - (index & 7);
|
||||
(byte >> shift) & 1
|
||||
}
|
||||
|
||||
/// Build a 10-byte payload from the 9 plaintext bytes (chrysler_v0_build_payload).
|
||||
fn build_payload(plain: &[u8; 9], counter: u8, button: u8, header_low2: u8) -> [u8; 10] {
|
||||
let transformed = Self::transform_block(plain, counter, button);
|
||||
let mut out = [0u8; 10];
|
||||
out[0] = (Self::reverse6(counter as u32) << 2) | (header_low2 & 0x03);
|
||||
out[1..10].copy_from_slice(&transformed);
|
||||
out
|
||||
}
|
||||
|
||||
/// ADD_LEVEL-style merge: combine adjacent same-level pulses.
|
||||
fn add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
||||
if let Some(last) = signal.last_mut() {
|
||||
if last.level == level {
|
||||
*last = LevelDuration::new(level, last.duration_us + duration);
|
||||
return;
|
||||
}
|
||||
}
|
||||
signal.push(LevelDuration::new(level, duration));
|
||||
}
|
||||
|
||||
/// Emit one 80-bit PWM payload frame (without the leading preamble).
|
||||
fn emit_payload(signal: &mut Vec<LevelDuration>, payload: &[u8; 10]) {
|
||||
for bit in 0..80u8 {
|
||||
let value = Self::payload_get_bit(payload, bit);
|
||||
if value != 0 {
|
||||
Self::add_level(signal, true, TE_ONE_SHORT);
|
||||
Self::add_level(signal, false, TE_LONG_A);
|
||||
} else {
|
||||
Self::add_level(signal, true, TE_SHORT);
|
||||
Self::add_level(signal, false, TE_LONG_B);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the preamble (24 short-HIGH + long_b-LOW pairs).
|
||||
fn emit_preamble(signal: &mut Vec<LevelDuration>) {
|
||||
for _ in 0..PREAMBLE_PAIRS {
|
||||
Self::add_level(signal, true, TE_SHORT);
|
||||
Self::add_level(signal, false, TE_LONG_B);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for ChryslerV0Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Chrysler V0"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG_A,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: DECODE_BIT_COUNT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.packet_bit_count = 0;
|
||||
self.te_last = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.data_2 = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && Self::is_short(duration) {
|
||||
self.packet_bit_count = 0;
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::Seek;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Seek => {
|
||||
if level {
|
||||
self.te_last = duration;
|
||||
return None;
|
||||
}
|
||||
|
||||
if Self::is_long_mark(duration) {
|
||||
if Self::is_short(self.te_last) {
|
||||
self.packet_bit_count += 1;
|
||||
} else if self.packet_bit_count > 0x0F {
|
||||
self.data_2 = 0;
|
||||
self.step = DecoderStep::Data;
|
||||
self.decode_data = 1;
|
||||
self.decode_count_bit = 1;
|
||||
} else {
|
||||
self.packet_bit_count = 0;
|
||||
self.step = DecoderStep::Seek;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if duration > TE_GAP && self.packet_bit_count > 0x0F {
|
||||
self.decode_data = 0;
|
||||
self.data_2 = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.step = DecoderStep::Data;
|
||||
return None;
|
||||
}
|
||||
|
||||
self.step = DecoderStep::Reset;
|
||||
self.packet_bit_count = 0;
|
||||
}
|
||||
|
||||
DecoderStep::Data => {
|
||||
if level {
|
||||
self.te_last = duration;
|
||||
return None;
|
||||
}
|
||||
|
||||
let count = self.decode_count_bit;
|
||||
|
||||
if duration > TE_GAP {
|
||||
let result = if count as usize > 0x4F { self.commit() } else { None };
|
||||
self.step = DecoderStep::Reset;
|
||||
self.packet_bit_count = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
let bit_value: u8;
|
||||
if self.te_last < TE_SHORT {
|
||||
if !Self::is_short(self.te_last) || !Self::is_long_mark(duration) {
|
||||
let result = if count as usize > 0x4F { self.commit() } else { None };
|
||||
self.step = DecoderStep::Reset;
|
||||
self.packet_bit_count = 0;
|
||||
return result;
|
||||
}
|
||||
bit_value = 1;
|
||||
} else {
|
||||
if self.te_last > 0x2EE || !Self::is_long_mark(duration) {
|
||||
let result = if count as usize > 0x4F { self.commit() } else { None };
|
||||
self.step = DecoderStep::Reset;
|
||||
self.packet_bit_count = 0;
|
||||
return result;
|
||||
}
|
||||
bit_value = if Self::is_short(self.te_last) { 1 } else { 0 };
|
||||
}
|
||||
|
||||
let bit = (bit_value ^ 1) as u64;
|
||||
let new_count = count.wrapping_add(1);
|
||||
if count <= 0x3F {
|
||||
self.decode_data = (self.decode_data << 1) | bit;
|
||||
self.decode_count_bit = new_count;
|
||||
return None;
|
||||
}
|
||||
|
||||
self.data_2 = (self.data_2 << 1) | (bit as u16);
|
||||
self.decode_count_bit = new_count;
|
||||
if new_count as usize != DECODE_BIT_COUNT {
|
||||
return None;
|
||||
}
|
||||
|
||||
let result = self.commit();
|
||||
self.decode_data = 0;
|
||||
self.data_2 = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
self.packet_bit_count = 0;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
// Rebuild the 10-byte payload A (high 64 bits + low 16 bits) from the decoded frame,
|
||||
// re-derive seed/plaintext, then emit preamble + dual PWM frames (matches
|
||||
// chrysler_v0_build_upload, ENABLE_EMULATE_FEATURE).
|
||||
let data = decoded.data;
|
||||
let data_2 = decoded.extra.unwrap_or(0) as u16;
|
||||
let key = data.to_be_bytes();
|
||||
let key2 = data_2;
|
||||
|
||||
// Header low-2 bits live in the top byte's low nibble (matches encoder deserialize:
|
||||
// plain_header = (data >> 56) & 0x03).
|
||||
let header_low2 = (key[0]) & 0x03;
|
||||
let seed = Self::reverse6((key[0] >> 2) as u32);
|
||||
|
||||
// Determine the originally-transmitted button (matches encoder deserialize).
|
||||
let original_button: u8 = if (key[0] & 0x80) == 0 {
|
||||
if (key[1] ^ key[6]) == 0x08 { 2 } else { 1 }
|
||||
} else if (((key2 & 0xFF) as u8) ^ key[4]) == 0x10 {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
// Recover the plaintext (Plain_A/Plain_B both default to this in the C when no explicit
|
||||
// Plain_A/B is stored — which is our case).
|
||||
let encoded: [u8; 9] = [
|
||||
key[1],
|
||||
key[2],
|
||||
key[3],
|
||||
key[4],
|
||||
key[5],
|
||||
key[6],
|
||||
key[7],
|
||||
(key2 >> 8) as u8,
|
||||
(key2 & 0xFF) as u8,
|
||||
];
|
||||
let generated = Self::transform_block(&encoded, seed, original_button);
|
||||
let mut plain_a = generated;
|
||||
let mut plain_b = generated;
|
||||
|
||||
// Apply the requested button (KAT button → Chrysler 1/2).
|
||||
let tx_button = Self::map_button(button);
|
||||
if tx_button != original_button {
|
||||
plain_a[5] ^= 0x0C;
|
||||
plain_b[3] ^= 0x30;
|
||||
}
|
||||
|
||||
// Counter handling (matches encoder deserialize): counter_a is the even counter, counter_b
|
||||
// is counter_a - 1 (wrapping in 6 bits). Derive the base counter from the seed.
|
||||
let counter = (seed as u32) & 0x3F;
|
||||
let mut counter_a = (counter & 0x3F) as u8;
|
||||
if (counter_a & 1) != 0 {
|
||||
counter_a = counter_a.wrapping_sub(1) & 0x3F;
|
||||
}
|
||||
let counter_b = if counter_a == 0 { 0x3F } else { counter_a - 1 };
|
||||
|
||||
let payload_a = Self::build_payload(&plain_a, counter_a, tx_button, header_low2);
|
||||
let payload_b = Self::build_payload(&plain_b, counter_b, tx_button, header_low2);
|
||||
|
||||
let mut signal = Vec::with_capacity(PREAMBLE_PAIRS * 4 + 80 * 4 + 16);
|
||||
|
||||
// Frame A: preamble, short + frame_gap, payload A, short + frame_gap.
|
||||
Self::emit_preamble(&mut signal);
|
||||
Self::add_level(&mut signal, true, TE_SHORT);
|
||||
Self::add_level(&mut signal, false, FRAME_GAP);
|
||||
Self::emit_payload(&mut signal, &payload_a);
|
||||
Self::add_level(&mut signal, true, TE_SHORT);
|
||||
Self::add_level(&mut signal, false, FRAME_GAP);
|
||||
|
||||
// Frame B: preamble, short + frame_gap, payload B, short + frame_gap.
|
||||
Self::emit_preamble(&mut signal);
|
||||
Self::add_level(&mut signal, true, TE_SHORT);
|
||||
Self::add_level(&mut signal, false, FRAME_GAP);
|
||||
Self::emit_payload(&mut signal, &payload_b);
|
||||
Self::add_level(&mut signal, true, TE_SHORT);
|
||||
Self::add_level(&mut signal, false, FRAME_GAP);
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChryslerV0Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Decode a stream of pairs with a fresh decoder, returning the first frame.
|
||||
fn decode_stream(pairs: &[LevelDuration]) -> Option<DecodedSignal> {
|
||||
let mut dec = ChryslerV0Decoder::new();
|
||||
for p in pairs {
|
||||
if let Some(sig) = dec.feed(p.level, p.duration_us) {
|
||||
return Some(sig);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// reverse6 is its own inverse on 6-bit values.
|
||||
#[test]
|
||||
fn reverse6_is_involution() {
|
||||
for v in 0u32..64 {
|
||||
let r = ChryslerV0Decoder::reverse6(v) as u32;
|
||||
assert_eq!(ChryslerV0Decoder::reverse6(r) as u32, v, "reverse6 not involutive at {v}");
|
||||
}
|
||||
}
|
||||
|
||||
/// transform_block is its own inverse for a given (seed, button) — XOR with a constant mask.
|
||||
#[test]
|
||||
fn transform_block_round_trips() {
|
||||
let plain: [u8; 9] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99];
|
||||
for &button in &[1u8, 2u8] {
|
||||
for seed in 0u8..64 {
|
||||
let enc = ChryslerV0Decoder::transform_block(&plain, seed, button);
|
||||
let dec = ChryslerV0Decoder::transform_block(&enc, seed, button);
|
||||
assert_eq!(dec, plain, "transform not invertible seed={seed} button={button}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a payload-A frame exactly as the encoder does (even counter, MSB-clear path), encode
|
||||
/// it to a PWM burst, decode it back, and confirm the frame, check_ok, button and serial all
|
||||
/// survive the round trip. Payload A is the first frame the encoder emits, so `decode_stream`
|
||||
/// returns it.
|
||||
///
|
||||
/// Payload-A check_ok requires (in the stored = transformed bytes): `(key[1]^0xC3)==key[5]`
|
||||
/// and `key[6]^key[1]==0x04`. Since transform XORs every byte with a constant mask, this is
|
||||
/// equivalent to a check on the *plaintext*: `plain[0]^0xC3==plain[4]` and `plain[5]^plain[0]==0x04`.
|
||||
#[test]
|
||||
fn encode_decode_round_trip() {
|
||||
// Choose an even counter so the encoder's counter_a equals it (exact round trip), and pick
|
||||
// plaintext satisfying the payload-A invariants.
|
||||
let counter_a: u8 = 0x14; // even
|
||||
let button: u8 = 1; // Lock (matches the b1^b6==0x04 branch which keeps button=1)
|
||||
let header_low2: u8 = 0x02;
|
||||
|
||||
let p0 = 0x5Au8;
|
||||
let mut plain = [0x00u8; 9];
|
||||
plain[0] = p0;
|
||||
plain[1] = 0x11;
|
||||
plain[2] = 0x22;
|
||||
plain[3] = 0x33;
|
||||
plain[4] = p0 ^ 0xC3; // plain[0]^0xC3 == plain[4]
|
||||
plain[5] = p0 ^ 0x04; // plain[5]^plain[0] == 0x04
|
||||
plain[6] = 0x66;
|
||||
plain[7] = 0x77;
|
||||
plain[8] = 0x88;
|
||||
|
||||
// Build the 10-byte payload the way the encoder/transmitter does.
|
||||
let payload = ChryslerV0Decoder::build_payload(&plain, counter_a, button, header_low2);
|
||||
let data = u64::from_be_bytes([
|
||||
payload[0], payload[1], payload[2], payload[3], payload[4], payload[5], payload[6],
|
||||
payload[7],
|
||||
]);
|
||||
let data_2: u16 = ((payload[8] as u16) << 8) | payload[9] as u16;
|
||||
|
||||
// Sanity: the raw frame must pass the structural check before we exercise the codec.
|
||||
let d = ChryslerV0Decoder::decode_packet(data, data_2);
|
||||
assert!(d.check_ok, "constructed payload-A vector should satisfy check_ok");
|
||||
assert_eq!(d.button, button, "constructed button mismatch");
|
||||
assert_eq!(d.seed & 1, 0, "payload A must have an even seed");
|
||||
|
||||
let decoded = DecodedSignal {
|
||||
serial: Some(d.serial),
|
||||
button: Some(d.button),
|
||||
counter: Some((d.counter & 0xFFFF) as u16),
|
||||
crc_valid: true,
|
||||
data,
|
||||
data_count_bit: DECODE_BIT_COUNT,
|
||||
encoder_capable: true,
|
||||
extra: Some(data_2 as u64),
|
||||
protocol_display_name: None,
|
||||
};
|
||||
|
||||
let dec = ChryslerV0Decoder::new();
|
||||
// Encode with the same (Lock) button so the plaintext is not perturbed.
|
||||
let burst = dec.encode(&decoded, 0x01).expect("encode should succeed");
|
||||
assert!(!burst.is_empty());
|
||||
|
||||
let got = decode_stream(&burst).expect("encoded burst should decode");
|
||||
assert!(got.crc_valid, "decoded frame should pass check");
|
||||
// The high 64 bits (payload bytes 0..7) must reproduce the original frame.
|
||||
assert_eq!(got.data, data, "round-trip data mismatch");
|
||||
assert_eq!(got.button, decoded.button, "button mismatch");
|
||||
assert_eq!(got.serial, decoded.serial, "serial mismatch");
|
||||
}
|
||||
}
|
||||
+56
-66
@@ -4,9 +4,9 @@
|
||||
//! `fiat_v0.h`. Preamble: count short pulses (HIGH or LOW, 200±100µs); when preamble_count >= 150
|
||||
//! (0x96), accept 800µs LOW gap (gap_threshold 800, te_delta 100) and enter Data. Data: 64 bits
|
||||
//! (serial=data_low, cnt=data_high) then 7 more bits; complete when bit_count > 0x46 with
|
||||
//! btn = (data_low << 1) | 1, 71 bits total. Encoder: differential Manchester, 150 preamble pairs,
|
||||
//! last LOW replaced by 800µs gap; 64 data bits then 6 btn bits (btn_to_send = btn >> 1); end
|
||||
//! marker te_short*8 LOW.
|
||||
//! btn = (uint8_t)data_low, 71 bits total. Encoder: standard Manchester, 150 preamble pairs,
|
||||
//! last LOW replaced by 800µs gap; 64 data bits then 7 endbyte bits (endbyte & 0x7F); end
|
||||
//! marker te_short*4 LOW.
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -294,8 +294,9 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
self.data_high = 0;
|
||||
}
|
||||
|
||||
if self.bit_count > 0x46 {
|
||||
self.btn = ((self.data_low << 1) | 1) as u8;
|
||||
if self.bit_count == 0x47 {
|
||||
// C: endbyte = (uint8_t)data_low (no transform)
|
||||
self.btn = self.data_low as u8;
|
||||
let result = self.parse_data();
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
@@ -304,6 +305,19 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Manchester reset event (gap path) — C extracts at exactly 71 bits
|
||||
if self.bit_count == 0x47 {
|
||||
self.btn = self.data_low as u8;
|
||||
let result = self.parse_data();
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
} else if self.bit_count < 0x40 {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
self.te_last = duration;
|
||||
}
|
||||
@@ -319,80 +333,56 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let cnt = decoded.counter.unwrap_or(0) as u32;
|
||||
let btn = decoded.button.unwrap_or(0).max(button);
|
||||
let endbyte = if button != 0 { button } else { decoded.button.unwrap_or(0) };
|
||||
|
||||
// Ref: data = (cnt<<32)|serial; btn_to_send = btn >> 1 (reverse decoder's (x<<1)|1)
|
||||
// C: data = (hop << 32) | fix; endbyte sent as-is (7 bits, & 0x7F)
|
||||
let data = ((cnt as u64) << 32) | (serial as u64);
|
||||
let btn_to_send = btn >> 1;
|
||||
let te_short = TE_SHORT;
|
||||
|
||||
let mut signal = Vec::with_capacity(1024);
|
||||
let te_short = TE_SHORT;
|
||||
let te_long = TE_LONG;
|
||||
|
||||
for burst in 0..TOTAL_BURSTS {
|
||||
if burst > 0 {
|
||||
signal.push(LevelDuration::new(false, INTER_BURST_GAP));
|
||||
}
|
||||
|
||||
// Preamble: HIGH-LOW pairs; last LOW replaced by gap (ref)
|
||||
for i in 0..PREAMBLE_PAIRS {
|
||||
// Preamble: alternating short pulses; last LOW extended to gap
|
||||
for _ in 0..PREAMBLE_PAIRS {
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
signal.push(LevelDuration::new(
|
||||
false,
|
||||
if i == PREAMBLE_PAIRS - 1 { GAP_US } else { te_short },
|
||||
));
|
||||
}
|
||||
|
||||
// First bit (bit 63) - differential Manchester
|
||||
let first_bit = (data >> 63) & 1 == 1;
|
||||
if first_bit {
|
||||
signal.push(LevelDuration::new(true, te_long));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
signal.push(LevelDuration::new(false, te_long));
|
||||
}
|
||||
|
||||
let mut prev_bit = first_bit;
|
||||
|
||||
// Remaining 63 data bits
|
||||
for bit in (0..63).rev() {
|
||||
let curr_bit = (data >> bit) & 1 == 1;
|
||||
if !prev_bit && !curr_bit {
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
} else if !prev_bit && curr_bit {
|
||||
signal.push(LevelDuration::new(true, te_long));
|
||||
} else if prev_bit && !curr_bit {
|
||||
signal.push(LevelDuration::new(false, te_long));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
}
|
||||
prev_bit = curr_bit;
|
||||
}
|
||||
|
||||
// 6 btn bits (ref: for bit 5 down to 0)
|
||||
for bit in (0..6).rev() {
|
||||
let curr_bit = (btn_to_send >> bit) & 1 == 1;
|
||||
if !prev_bit && !curr_bit {
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
} else if !prev_bit && curr_bit {
|
||||
signal.push(LevelDuration::new(true, te_long));
|
||||
} else if prev_bit && !curr_bit {
|
||||
signal.push(LevelDuration::new(false, te_long));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
}
|
||||
prev_bit = curr_bit;
|
||||
}
|
||||
|
||||
// End marker (ref)
|
||||
if prev_bit {
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
}
|
||||
signal.push(LevelDuration::new(false, te_short * 8));
|
||||
// Extend last LOW to create gap (matches C: upload[index-1] = gap)
|
||||
if let Some(last) = signal.last_mut() {
|
||||
*last = LevelDuration::new(false, GAP_US);
|
||||
}
|
||||
|
||||
// Standard Manchester encode 64 bits of data (matches C)
|
||||
for bit in (0..64).rev() {
|
||||
let curr_bit = (data >> bit) & 1 == 1;
|
||||
if curr_bit {
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
}
|
||||
}
|
||||
|
||||
// Standard Manchester encode 7 bits of endbyte (matches C: endbyte & 0x7F, bits 6..0)
|
||||
let endbyte_masked = endbyte & 0x7F;
|
||||
for bit in (0..7).rev() {
|
||||
let curr_bit = (endbyte_masked >> bit) & 1 == 1;
|
||||
if curr_bit {
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
}
|
||||
}
|
||||
|
||||
// End marker: te_short * 4 LOW (matches C)
|
||||
signal.push(LevelDuration::new(false, te_short * 4));
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
//! Fiat V1 protocol decoder (Magneti Marelli BSI / PCF7946)
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/fiat_v1.c` and
|
||||
//! `fiat_v1.h`. Found on Fiat Panda, Grande Punto, and possibly other Fiat/Lancia/Alfa ~2003-2012.
|
||||
//!
|
||||
//! RF: 433.92 MHz, Manchester encoding.
|
||||
//! Two timing variants with identical frame structure:
|
||||
//! Type A (e.g. Panda): te_short ~260us, te_long ~520us
|
||||
//! Type B (e.g. Grande Punto): te_short ~100us, te_long ~200us
|
||||
//! TE is auto-detected from preamble pulse averaging (boundary at 180us).
|
||||
//!
|
||||
//! Frame layout (103-104 bits = 13 bytes):
|
||||
//! Bytes 0-1: 0xFFFF/0xFFFC preamble residue
|
||||
//! Bytes 2-5: Serial (32 bits)
|
||||
//! Byte 6: [Button:4 | Epoch:4]
|
||||
//! Byte 7: [Counter:5 | Scramble:2 | Fixed:1]
|
||||
//! Bytes 8-12: Encrypted payload (40 bits)
|
||||
//!
|
||||
//! State machine: Reset -> Preamble -> Sync -> Data (-> RetxSync -> Data)
|
||||
//! No encoder (decode-only).
|
||||
//!
|
||||
//! Original C implementation by @lupettohf
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
// Default timing constants (Type A)
|
||||
const TE_SHORT: u32 = 260;
|
||||
#[allow(dead_code)]
|
||||
const TE_LONG: u32 = 520;
|
||||
#[allow(dead_code)]
|
||||
const TE_DELTA: u32 = 80;
|
||||
#[allow(dead_code)]
|
||||
const MIN_COUNT_BIT: usize = 80;
|
||||
|
||||
// Preamble / sync / data constants (from C defines)
|
||||
const PREAMBLE_PULSE_MIN: u32 = 50;
|
||||
const PREAMBLE_PULSE_MAX: u32 = 350;
|
||||
const PREAMBLE_MIN: u16 = 80;
|
||||
const MAX_DATA_BITS: u8 = 104;
|
||||
const MIN_DATA_BITS: u8 = 80;
|
||||
const GAP_TE_MULT: u32 = 4;
|
||||
const SYNC_TE_MIN_MULT: u32 = 4;
|
||||
const SYNC_TE_MAX_MULT: u32 = 12;
|
||||
const RETX_GAP_MIN: u32 = 5000;
|
||||
const RETX_SYNC_MIN: u32 = 400;
|
||||
const RETX_SYNC_MAX: u32 = 2800;
|
||||
#[allow(dead_code)]
|
||||
const TE_TYPE_AB_BOUNDARY: u32 = 180;
|
||||
|
||||
/// Manchester state machine (Flipper-style, same as fiat_v0.rs / common.rs).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0 = 0,
|
||||
Mid1 = 1,
|
||||
Start0 = 2,
|
||||
Start1 = 3,
|
||||
}
|
||||
|
||||
/// Decoder state machine steps.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble,
|
||||
Sync,
|
||||
Data,
|
||||
RetxSync,
|
||||
}
|
||||
|
||||
pub struct FiatV1Decoder {
|
||||
step: DecoderStep,
|
||||
manchester_state: ManchesterState,
|
||||
|
||||
// Preamble tracking
|
||||
preamble_count: u16,
|
||||
|
||||
// Auto-detected TE from preamble averaging
|
||||
te_sum: u32,
|
||||
te_count: u16,
|
||||
te_detected: u32,
|
||||
|
||||
// Data accumulation
|
||||
raw_data: [u8; 13],
|
||||
bit_count: u8,
|
||||
data: u64, // first 64 bits
|
||||
extra_data: u32, // bits 65+
|
||||
|
||||
// Parsed fields
|
||||
serial: u32,
|
||||
btn: u8,
|
||||
cnt: u8,
|
||||
|
||||
te_last: u32,
|
||||
}
|
||||
|
||||
impl FiatV1Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
preamble_count: 0,
|
||||
te_sum: 0,
|
||||
te_count: 0,
|
||||
te_detected: 0,
|
||||
raw_data: [0u8; 13],
|
||||
bit_count: 0,
|
||||
data: 0,
|
||||
extra_data: 0,
|
||||
serial: 0,
|
||||
btn: 0,
|
||||
cnt: 0,
|
||||
te_last: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the Manchester state machine by one event.
|
||||
/// Event encoding: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh.
|
||||
/// Returns `Some(bit)` when a data bit is emitted, `None` otherwise.
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||
let (new_state, emit) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
|
||||
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
|
||||
|
||||
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
|
||||
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
|
||||
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
_ => (ManchesterState::Mid1, false),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
if emit {
|
||||
Some((event & 1) == 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset Manchester state machine (to Mid1, matching C reset).
|
||||
fn manchester_reset(&mut self) {
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
/// Prepare for data reception: clear accumulators, reset Manchester, enter Data step.
|
||||
/// Matches `fiat_marelli_prepare_data` in the C reference.
|
||||
fn prepare_data(&mut self) {
|
||||
self.bit_count = 0;
|
||||
self.extra_data = 0;
|
||||
self.data = 0;
|
||||
self.raw_data = [0u8; 13];
|
||||
self.manchester_reset();
|
||||
self.step = DecoderStep::Data;
|
||||
}
|
||||
|
||||
/// Get the effective te_short (auto-detected or default).
|
||||
fn te_short(&self) -> u32 {
|
||||
if self.te_detected != 0 {
|
||||
self.te_detected
|
||||
} else {
|
||||
TE_SHORT
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the effective te_long (2 * te_short).
|
||||
fn te_long(&self) -> u32 {
|
||||
self.te_short() * 2
|
||||
}
|
||||
|
||||
/// Get the effective te_delta (te_short / 2, minimum 30).
|
||||
fn te_delta(&self) -> u32 {
|
||||
let d = self.te_short() / 2;
|
||||
if d < 30 { 30 } else { d }
|
||||
}
|
||||
|
||||
/// Map button code to name (matches C fiat_marelli_button_name).
|
||||
#[allow(dead_code)]
|
||||
fn button_name(btn: u8) -> &'static str {
|
||||
match btn {
|
||||
0x7 => "Lock",
|
||||
0xB => "Unlock",
|
||||
0xD => "Trunk",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a DecodedSignal from the current state.
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
// Pack extra bytes (bits 65+) into extra field
|
||||
let extra = if self.bit_count > 64 {
|
||||
Some(self.extra_data as u64)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(self.serial),
|
||||
button: Some(self.btn),
|
||||
counter: Some(self.cnt as u16),
|
||||
crc_valid: true, // no CRC defined for this protocol
|
||||
data: self.data,
|
||||
data_count_bit: self.bit_count as usize,
|
||||
encoder_capable: false,
|
||||
extra,
|
||||
protocol_display_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for FiatV1Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Fiat V1"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.preamble_count = 0;
|
||||
self.bit_count = 0;
|
||||
self.extra_data = 0;
|
||||
self.te_last = 0;
|
||||
self.te_sum = 0;
|
||||
self.te_count = 0;
|
||||
self.te_detected = 0;
|
||||
self.data = 0;
|
||||
self.raw_data = [0u8; 13];
|
||||
self.serial = 0;
|
||||
self.btn = 0;
|
||||
self.cnt = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
let te_short = self.te_short();
|
||||
let te_long = self.te_long();
|
||||
let te_delta = self.te_delta();
|
||||
|
||||
match self.step {
|
||||
// =========================================================
|
||||
// Reset: wait for a HIGH pulse in preamble range, or detect
|
||||
// retransmission gap (LOW > 5000us with valid te_detected).
|
||||
// =========================================================
|
||||
DecoderStep::Reset => {
|
||||
if level {
|
||||
if duration >= PREAMBLE_PULSE_MIN && duration <= PREAMBLE_PULSE_MAX {
|
||||
self.step = DecoderStep::Preamble;
|
||||
self.preamble_count = 1;
|
||||
self.te_sum = duration;
|
||||
self.te_count = 1;
|
||||
self.te_last = duration;
|
||||
}
|
||||
} else if duration > RETX_GAP_MIN && self.te_detected != 0 {
|
||||
self.step = DecoderStep::RetxSync;
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Preamble: accumulate pulses in 50-350us range for TE
|
||||
// averaging. When count >= 80 and we see a LOW gap >= 4*TE,
|
||||
// transition to Sync.
|
||||
// =========================================================
|
||||
DecoderStep::Preamble => {
|
||||
if duration >= PREAMBLE_PULSE_MIN && duration <= PREAMBLE_PULSE_MAX {
|
||||
// Pulse in valid preamble range (either HIGH or LOW)
|
||||
self.preamble_count += 1;
|
||||
self.te_sum += duration;
|
||||
self.te_count += 1;
|
||||
self.te_last = duration;
|
||||
} else if !level {
|
||||
// LOW pulse outside preamble range -- check if it's the gap after preamble
|
||||
if self.preamble_count >= PREAMBLE_MIN && self.te_count > 0 {
|
||||
self.te_detected = self.te_sum / (self.te_count as u32);
|
||||
let gap_threshold = self.te_detected * GAP_TE_MULT;
|
||||
|
||||
if duration > gap_threshold {
|
||||
self.step = DecoderStep::Sync;
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
// HIGH pulse outside preamble range
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Sync: expect HIGH sync pulse of te_detected*4 to
|
||||
// te_detected*12 duration. On match, prepare data decoder.
|
||||
// =========================================================
|
||||
DecoderStep::Sync => {
|
||||
let sync_min = self.te_detected * SYNC_TE_MIN_MULT;
|
||||
let sync_max = self.te_detected * SYNC_TE_MAX_MULT;
|
||||
|
||||
if level && duration >= sync_min && duration <= sync_max {
|
||||
self.prepare_data();
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// RetxSync: after retransmission gap (>5000us LOW), look for
|
||||
// HIGH sync pulse in 400-2800us range.
|
||||
// =========================================================
|
||||
DecoderStep::RetxSync => {
|
||||
if level && duration >= RETX_SYNC_MIN && duration <= RETX_SYNC_MAX {
|
||||
self.prepare_data();
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Data: Manchester decode up to 104 bits. Complete frame on
|
||||
// 104 bits reached or gap with >= 80 bits collected.
|
||||
// =========================================================
|
||||
DecoderStep::Data => {
|
||||
let mut event: u8 = 4; // 4 = ManchesterEventReset (invalid)
|
||||
let mut frame_complete = false;
|
||||
|
||||
// Check for short pulse match
|
||||
let diff_short = duration_diff!(duration, te_short);
|
||||
if diff_short < te_delta {
|
||||
// Flipper convention: level=HIGH -> ShortLow(0), level=LOW -> ShortHigh(1)
|
||||
event = if level { 0 } else { 1 };
|
||||
} else {
|
||||
// Check for long pulse match
|
||||
let diff_long = duration_diff!(duration, te_long);
|
||||
if diff_long < te_delta {
|
||||
event = if level { 2 } else { 3 };
|
||||
}
|
||||
}
|
||||
|
||||
if event != 4 {
|
||||
// Valid Manchester event
|
||||
if let Some(data_bit) = self.manchester_advance(event) {
|
||||
let new_bit: u32 = if data_bit { 1 } else { 0 };
|
||||
|
||||
// Store bit into raw_data byte array (MSB-first)
|
||||
if self.bit_count < MAX_DATA_BITS {
|
||||
let byte_idx = (self.bit_count / 8) as usize;
|
||||
let bit_pos = 7 - (self.bit_count % 8);
|
||||
if new_bit != 0 {
|
||||
self.raw_data[byte_idx] |= 1u8 << bit_pos;
|
||||
}
|
||||
}
|
||||
|
||||
// Also accumulate into u64 data / u32 extra_data
|
||||
if self.bit_count < 64 {
|
||||
self.data = (self.data << 1) | (new_bit as u64);
|
||||
} else {
|
||||
self.extra_data = (self.extra_data << 1) | new_bit;
|
||||
}
|
||||
|
||||
self.bit_count += 1;
|
||||
if self.bit_count >= MAX_DATA_BITS {
|
||||
frame_complete = true;
|
||||
}
|
||||
}
|
||||
} else if self.bit_count >= MIN_DATA_BITS {
|
||||
// Gap or invalid pulse but we have enough bits
|
||||
frame_complete = true;
|
||||
} else {
|
||||
// Not enough bits and invalid pulse -- abort
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
|
||||
if frame_complete {
|
||||
// Parse serial, button, counter from raw_data
|
||||
self.serial = ((self.raw_data[2] as u32) << 24)
|
||||
| ((self.raw_data[3] as u32) << 16)
|
||||
| ((self.raw_data[4] as u32) << 8)
|
||||
| (self.raw_data[5] as u32);
|
||||
self.btn = (self.raw_data[6] >> 4) & 0x0F;
|
||||
self.cnt = (self.raw_data[7] >> 3) & 0x1F;
|
||||
|
||||
let result = self.parse_data();
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = duration;
|
||||
return Some(result);
|
||||
}
|
||||
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FiatV1Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
+23
-10
@@ -225,10 +225,26 @@ impl FordV0Decoder {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// BS calculation
|
||||
// Checksum / BS calculation
|
||||
// =========================================================================
|
||||
|
||||
/// Calculate BS = (count_low_byte + bs_magic + (button << 4)) with overflow handling
|
||||
/// Calculate checksum from serial, count, and button (matches psa.c ford_v0_calculate_checksum).
|
||||
/// Sums all bytes of serial + all bytes of count + (button << 4), truncated to u8.
|
||||
fn calculate_checksum(serial: u32, count: u32, button: u8) -> u8 {
|
||||
let sum: u32 = ((count >> 24) & 0xFF)
|
||||
.wrapping_add((count >> 16) & 0xFF)
|
||||
.wrapping_add((count >> 8) & 0xFF)
|
||||
.wrapping_add(count & 0xFF)
|
||||
.wrapping_add((serial >> 24) & 0xFF)
|
||||
.wrapping_add((serial >> 16) & 0xFF)
|
||||
.wrapping_add((serial >> 8) & 0xFF)
|
||||
.wrapping_add(serial & 0xFF)
|
||||
.wrapping_add((button as u32) << 4);
|
||||
(sum & 0xFF) as u8
|
||||
}
|
||||
|
||||
/// Legacy BS calculation (kept for reference; encoder now uses calculate_checksum)
|
||||
#[allow(dead_code)]
|
||||
fn calculate_bs(count: u32, button: u8, bs_magic: u8) -> u8 {
|
||||
let result: u16 = (count as u16 & 0xFF)
|
||||
.wrapping_add(bs_magic as u16)
|
||||
@@ -651,21 +667,18 @@ impl ProtocolDecoder for FordV0Decoder {
|
||||
decoded.counter.unwrap_or(0) as u32
|
||||
}) & 0xFFFFF; // 20-bit
|
||||
|
||||
// Use stored bs_magic (or default to 0x6F for backward compatibility)
|
||||
let bs_magic = if self.bs_magic != 0 { self.bs_magic } else { 0x6F };
|
||||
|
||||
// Calculate BS from count + button + bs_magic (matches C ford_v0_calculate_bs)
|
||||
let bs = Self::calculate_bs(count, button, bs_magic);
|
||||
// Calculate checksum from serial + count + button (matches ford_v0_calculate_checksum in C)
|
||||
let checksum = Self::calculate_checksum(serial, count, button);
|
||||
|
||||
// Extract header byte from the original key1 (first byte)
|
||||
let header_byte = (decoded.data >> 56) as u8;
|
||||
|
||||
// Encode key1 from fields (same count, new button)
|
||||
let new_key1 = Self::encode_ford_v0(header_byte, serial, button, count, bs);
|
||||
let new_key1 = Self::encode_ford_v0(header_byte, serial, button, count, checksum);
|
||||
|
||||
// Calculate CRC for key2
|
||||
let crc = Self::calculate_crc_for_tx(new_key1, bs);
|
||||
let new_key2 = ((bs as u16) << 8) | (crc as u16);
|
||||
let crc = Self::calculate_crc_for_tx(new_key1, checksum);
|
||||
let new_key2 = ((checksum as u16) << 8) | (crc as u16);
|
||||
|
||||
// Build one 6-burst block and repeat TX_REPEAT times (matches reference encoder.repeat = 10)
|
||||
let single = Self::build_upload(new_key1, new_key2);
|
||||
|
||||
@@ -0,0 +1,778 @@
|
||||
//! Ford V1 protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v1.c` and `ford_v1.h`.
|
||||
//! Manchester 65/130µs (te_delta 39), FM. 136 bits / 17 bytes: key1 (bytes 0..7, 56 bits) +
|
||||
//! key2 (bytes 7..15, 64 bits) + CRC16 (bytes 15..16). Preamble ≥50 long pulses, then a short-pulse
|
||||
//! sync window (`sync_event_count > 2`) replays buffered Manchester events and enters the 17-byte
|
||||
//! data collection. This is a ROLLING-code protocol.
|
||||
//!
|
||||
//! Crypto: a proprietary parity-based descrambling cipher (`ford_v1_decode_with_flag`) operating on
|
||||
//! the 9-byte air block `raw[6..15]`, plus CRC16/CCITT (poly 0x1021, init 0x0000) over `raw[3..15]`.
|
||||
//! Emission is gated on CRC16 validity (with a 17-byte bit-inverted fallback) so it never
|
||||
//! false-matches. Encryption/rolling detection mirrors the C: a strict branch
|
||||
//! (`decoded[3]==raw[5] && decoded[4]==raw[6]`) yields plaintext serial/button/counter; otherwise an
|
||||
//! encode round-trip check classifies it as encrypted/rolling. Encoder supported (6 bursts).
|
||||
//!
|
||||
//! Manchester transition table is the Flipper differential-Manchester table (same as Ford V0/V2).
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 65;
|
||||
const TE_LONG: u32 = 130;
|
||||
const TE_DELTA: u32 = 39;
|
||||
const DATA_BITS: usize = 136;
|
||||
const DATA_BYTES: usize = 17;
|
||||
const PREAMBLE_MIN: u16 = 50;
|
||||
/// C uses FORD_V1_DELTA_LONG (40) only for the preamble long-pulse match; data/sync use te_delta (39).
|
||||
const DELTA_LONG: u32 = 40;
|
||||
const SILENCE_LONG_MULT: u32 = 3;
|
||||
|
||||
// Manchester event encoding (matches Flipper ManchesterEvent ordinals used in the C buffer):
|
||||
// 0 = ShortLow, 1 = ShortHigh, 2 = LongLow, 3 = LongHigh.
|
||||
const EV_SHORT_LOW: u8 = 0;
|
||||
const EV_SHORT_HIGH: u8 = 1;
|
||||
const EV_LONG_LOW: u8 = 2;
|
||||
const EV_LONG_HIGH: u8 = 3;
|
||||
|
||||
// Encoder constants (subghz_protocol_encoder_ford_v1).
|
||||
const ENC_BURST_COUNT: usize = 6;
|
||||
const ENC_PREAMBLE_PAIRS: usize = 400;
|
||||
const ENC_SYNC_SHORT_US: u32 = 65;
|
||||
const ENC_SYNC_LONG_US: u32 = 130;
|
||||
const ENC_GAP_REPEAT_US: u32 = 50000;
|
||||
const ENC_GAP_LAST_US: u32 = 260;
|
||||
/// Per-burst override of pkt[4] (matches ford_v1_encoder_burst_pkt4_vals).
|
||||
const ENC_BURST_PKT4: [u8; 6] = [0x08, 0x00, 0x10, 0x08, 0x00, 0x10];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0 = 0,
|
||||
Mid1 = 1,
|
||||
Start0 = 2,
|
||||
Start1 = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble,
|
||||
Sync,
|
||||
Data,
|
||||
}
|
||||
|
||||
pub struct FordV1Decoder {
|
||||
step: DecoderStep,
|
||||
manchester_state: ManchesterState,
|
||||
preamble_count: u16,
|
||||
decode_data: u64,
|
||||
decode_count_bit: usize,
|
||||
byte_count: usize,
|
||||
raw_bytes: [u8; DATA_BYTES + 1],
|
||||
sync_event_idx: u8,
|
||||
sync_event_count: u8,
|
||||
sync_events: [u8; 8],
|
||||
}
|
||||
|
||||
impl FordV1Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
preamble_count: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
byte_count: 0,
|
||||
raw_bytes: [0; DATA_BYTES + 1],
|
||||
sync_event_idx: 0,
|
||||
sync_event_count: 0,
|
||||
sync_events: [0; 8],
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_state(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.preamble_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.byte_count = 0;
|
||||
self.raw_bytes = [0; DATA_BYTES + 1];
|
||||
self.sync_event_idx = 0;
|
||||
self.sync_event_count = 0;
|
||||
self.sync_events = [0; 8];
|
||||
}
|
||||
|
||||
/// Short/long matchers. Data and sync use te_delta (39); preamble-long uses DELTA_LONG (40).
|
||||
fn is_short(d: u32) -> bool {
|
||||
duration_diff!(d, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
fn is_long(d: u32) -> bool {
|
||||
duration_diff!(d, TE_LONG) < TE_DELTA
|
||||
}
|
||||
fn is_preamble_long(d: u32) -> bool {
|
||||
duration_diff!(d, TE_LONG) < DELTA_LONG
|
||||
}
|
||||
|
||||
/// Flipper differential-Manchester transition table (same as Ford V0/V2/Kia V7).
|
||||
/// Event: 0=ShortLow,1=ShortHigh,2=LongLow,3=LongHigh. Returns Some(bit) when a bit emits.
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||
let (new_state, emit) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
|
||||
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
|
||||
|
||||
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
|
||||
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
|
||||
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
_ => (ManchesterState::Mid1, false),
|
||||
};
|
||||
self.manchester_state = new_state;
|
||||
if emit { Some((event & 1) == 1) } else { None }
|
||||
}
|
||||
|
||||
/// Push a decoded Manchester bit into the byte buffer (matches C feed/data byte assembly).
|
||||
fn push_bit(&mut self, data_bit: bool) {
|
||||
self.decode_data = (self.decode_data << 1) | (data_bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
if self.decode_count_bit & 7 == 0 {
|
||||
let byte_val = (self.decode_data & 0xFF) as u8;
|
||||
if self.byte_count < DATA_BYTES {
|
||||
self.raw_bytes[self.byte_count] = byte_val;
|
||||
self.byte_count += 1;
|
||||
}
|
||||
self.decode_data = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CRC16/CCITT (poly 0x1021, init 0x0000) — module-local, matches
|
||||
// subghz_protocol_blocks_crc16(data, len, 0x1021, 0x0000).
|
||||
// =========================================================================
|
||||
fn crc16(data: &[u8]) -> u16 {
|
||||
let mut crc: u16 = 0x0000;
|
||||
for &byte in data {
|
||||
crc ^= (byte as u16) << 8;
|
||||
for _ in 0..8 {
|
||||
if crc & 0x8000 != 0 {
|
||||
crc = (crc << 1) ^ 0x1021;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Descrambling cipher (ford_v1_decode_with_flag) — operates on the 9-byte
|
||||
// air block in place. Matches the C exactly.
|
||||
// =========================================================================
|
||||
fn decode_with_flag(raw: &mut [u8; 9], flag_byte: u8) {
|
||||
if flag_byte != 0 {
|
||||
let xor_byte = raw[7];
|
||||
for i in 1..7 {
|
||||
raw[i] ^= xor_byte;
|
||||
}
|
||||
} else {
|
||||
let xor_byte = raw[6];
|
||||
for i in 1..6 {
|
||||
raw[i] ^= xor_byte;
|
||||
}
|
||||
raw[7] ^= xor_byte;
|
||||
}
|
||||
|
||||
let b6 = raw[6];
|
||||
let b7 = raw[7];
|
||||
raw[6] = (b6 & 0xAA) | (b7 & 0x55);
|
||||
raw[7] = (b7 & 0xAA) | (b6 & 0x55);
|
||||
}
|
||||
|
||||
/// Parity-driven descramble used when neither strict branch matches (ford_v1_decode).
|
||||
fn decode_air(raw: &mut [u8; 9]) {
|
||||
let endbyte = raw[8];
|
||||
let parity_any = endbyte != 0;
|
||||
let mut parity = 0u8;
|
||||
let mut tmp = endbyte;
|
||||
while tmp != 0 {
|
||||
parity ^= tmp & 1;
|
||||
tmp >>= 1;
|
||||
}
|
||||
let flag_byte = if parity_any { parity } else { 0 };
|
||||
Self::decode_with_flag(raw, flag_byte);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Inverse cipher (encoder side) — ford_v1_encode_inverse_block. Takes a
|
||||
// 9-byte plaintext block and produces the air (scrambled) block in place.
|
||||
// =========================================================================
|
||||
fn encode_inverse_block(block: &mut [u8; 9]) {
|
||||
let mut sum: u8 = 0;
|
||||
for i in 1..=7 {
|
||||
sum = sum.wrapping_add(block[i]);
|
||||
}
|
||||
|
||||
let p6 = block[6];
|
||||
let p7 = block[7];
|
||||
let post6 = (p6 & 0xAA) | (p7 & 0x55);
|
||||
let post7 = (p7 & 0xAA) | (p6 & 0x55);
|
||||
let xorv = post6 ^ post7;
|
||||
|
||||
let xor_byte;
|
||||
if (sum.count_ones() & 1) != 0 {
|
||||
block[6] = xorv;
|
||||
block[7] = post7;
|
||||
xor_byte = post7;
|
||||
} else {
|
||||
block[6] = post6;
|
||||
block[7] = xorv;
|
||||
xor_byte = post6;
|
||||
}
|
||||
|
||||
for i in 1..=5 {
|
||||
block[i] ^= xor_byte;
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_air_9bytes(plain9: &[u8; 9]) -> [u8; 9] {
|
||||
let mut block = *plain9;
|
||||
Self::encode_inverse_block(&mut block);
|
||||
block
|
||||
}
|
||||
|
||||
/// Recover plaintext from an air block by trying both descramble flags and verifying
|
||||
/// the result re-encodes to the same air bytes (ford_v1_plain_from_air).
|
||||
fn plain_from_air(air9: &[u8; 9]) -> Option<[u8; 9]> {
|
||||
for flag in 0u8..2 {
|
||||
let mut cand = *air9;
|
||||
Self::decode_with_flag(&mut cand, flag);
|
||||
let reair = Self::encode_air_9bytes(&cand);
|
||||
if reair == *air9 {
|
||||
return Some(cand);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract serial/button/counter from a plaintext 9-byte block (ford_v1_fields_from_plain).
|
||||
fn fields_from_plain(plain9: &[u8; 9]) -> (u32, u8, u32) {
|
||||
let serial = ((plain9[1] as u32) << 24)
|
||||
| ((plain9[2] as u32) << 16)
|
||||
| ((plain9[3] as u32) << 8)
|
||||
| (plain9[0] as u32);
|
||||
let btn = (plain9[5] >> 4) & 0x0F;
|
||||
let cnt = (((plain9[5] & 0x0F) as u32) << 16) | ((plain9[6] as u32) << 8) | (plain9[7] as u32);
|
||||
(serial, btn, cnt)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// process_data (ford_v1_process_data): CRC16 gate (+ 17-byte inverted
|
||||
// fallback), descramble, field extraction, dual-branch classification.
|
||||
// Returns Some(DecodedSignal) when CRC passes.
|
||||
// =========================================================================
|
||||
fn process_data(&self) -> Option<DecodedSignal> {
|
||||
let mut raw = [0u8; DATA_BYTES];
|
||||
raw.copy_from_slice(&self.raw_bytes[..DATA_BYTES]);
|
||||
|
||||
let mut calc_crc = Self::crc16(&raw[3..15]);
|
||||
let mut recv_crc = ((raw[15] as u16) << 8) | raw[16] as u16;
|
||||
|
||||
// Fallback: bit-invert all 17 bytes and retry the CRC (matches C).
|
||||
if recv_crc != calc_crc {
|
||||
for (i, b) in raw.iter_mut().enumerate() {
|
||||
*b = !self.raw_bytes[i];
|
||||
}
|
||||
calc_crc = Self::crc16(&raw[3..15]);
|
||||
recv_crc = ((raw[15] as u16) << 8) | raw[16] as u16;
|
||||
}
|
||||
|
||||
if recv_crc != calc_crc {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Air block = raw[6..15] (9 bytes). Try both descramble branches "strictly".
|
||||
let mut air9 = [0u8; 9];
|
||||
air9.copy_from_slice(&raw[6..15]);
|
||||
|
||||
let mut decoded_b0 = air9;
|
||||
Self::decode_with_flag(&mut decoded_b0, 0);
|
||||
let mut decoded_b1 = air9;
|
||||
Self::decode_with_flag(&mut decoded_b1, 1);
|
||||
|
||||
let (decoded, strict_ok): ([u8; 9], bool) =
|
||||
if decoded_b0[3] == raw[5] && decoded_b0[4] == raw[6] {
|
||||
(decoded_b0, true)
|
||||
} else if decoded_b1[3] == raw[5] && decoded_b1[4] == raw[6] {
|
||||
(decoded_b1, true)
|
||||
} else if let Some(p) = Self::plain_from_air(&air9) {
|
||||
// Encrypted/rolling: round-trip recovered but not a strict cleartext match.
|
||||
(p, false)
|
||||
} else {
|
||||
let mut p = air9;
|
||||
Self::decode_air(&mut p);
|
||||
(p, false)
|
||||
};
|
||||
|
||||
let recalc_crc = Self::crc16(&raw[3..15]);
|
||||
|
||||
// key1 = raw[0..7] (56 bits, big-endian) → DecodedSignal.data.
|
||||
let mut key1: u64 = 0;
|
||||
for &b in raw.iter().take(7) {
|
||||
key1 = (key1 << 8) | b as u64;
|
||||
}
|
||||
|
||||
let (serial, button, counter) = if strict_ok {
|
||||
let (s, b, c) = Self::fields_from_plain(&decoded);
|
||||
(s, b, c)
|
||||
} else {
|
||||
// Header-only: device id from raw[3..7], no button/counter (matches C).
|
||||
let device_id = ((raw[3] as u32) << 24)
|
||||
| ((raw[4] as u32) << 16)
|
||||
| ((raw[5] as u32) << 8)
|
||||
| (raw[6] as u32);
|
||||
(device_id, 0u8, 0u32)
|
||||
};
|
||||
|
||||
// Stash everything the encoder needs to rebuild the full 17-byte frame from fields.
|
||||
// `data` already carries key1 = raw[0..7] (56 bits). The air block raw[6..15] and the
|
||||
// CRC bytes are regenerated by re-encoding the plaintext, which is fully determined by
|
||||
// serial/button/counter EXCEPT plain[4] (the byte the strict branch constrains, equal to
|
||||
// air9[0]) and the derived checksum plain[8]. So we stash plain[4] plus the strict flag.
|
||||
// Layout (u64, MSB first):
|
||||
// [63:48]=crc16 [47:40]=strict_ok [39:32]=plain[4] [31:0]=0 (reserved).
|
||||
let plain4 = decoded[4];
|
||||
let extra = ((recalc_crc as u64) << 48)
|
||||
| ((strict_ok as u64) << 40)
|
||||
| ((plain4 as u64) << 32);
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter as u16),
|
||||
crc_valid: true,
|
||||
data: key1,
|
||||
data_count_bit: DATA_BITS,
|
||||
encoder_capable: true,
|
||||
extra: Some(extra),
|
||||
protocol_display_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Map KAT's generic button command to a Ford V1 4-bit button code
|
||||
/// (Sync=0, Lock=1, Unlock=2, Trunk=4, Panic=8 — matches ford_v1_get_button_name).
|
||||
fn map_button(button: u8) -> u8 {
|
||||
match button {
|
||||
0x01 => 0x01, // Lock
|
||||
0x02 => 0x02, // Unlock
|
||||
0x04 => 0x04, // Trunk
|
||||
0x08 => 0x08, // Panic
|
||||
b => b & 0x0F,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply serial/button/counter onto a plaintext 9-byte block (ford_v1_plain_apply_fields).
|
||||
fn plain_apply_fields(plain9: &mut [u8; 9], serial: u32, btn: u8, cnt: u32) {
|
||||
let chk = plain9[8]
|
||||
.wrapping_sub(plain9[6])
|
||||
.wrapping_sub(plain9[7])
|
||||
.wrapping_sub(plain9[5]);
|
||||
plain9[0] = (serial & 0xFF) as u8;
|
||||
plain9[1] = ((serial >> 24) & 0xFF) as u8;
|
||||
plain9[2] = ((serial >> 16) & 0xFF) as u8;
|
||||
plain9[3] = ((serial >> 8) & 0xFF) as u8;
|
||||
plain9[5] = (((btn & 0x0F) << 4) | (((cnt >> 16) & 0x0F) as u8)) as u8;
|
||||
plain9[6] = ((cnt >> 8) & 0xFF) as u8;
|
||||
plain9[7] = (cnt & 0xFF) as u8;
|
||||
plain9[8] = chk
|
||||
.wrapping_add(plain9[7])
|
||||
.wrapping_add(plain9[6])
|
||||
.wrapping_add(plain9[5]);
|
||||
}
|
||||
|
||||
/// Rebuild the air block (raw[6..15]) and CRC16 (raw[15..17]) from a plaintext block
|
||||
/// (ford_v1_encoder_rebuild_raw_from_plain).
|
||||
fn rebuild_raw_from_plain(raw17: &mut [u8; DATA_BYTES], plain9: &[u8; 9]) {
|
||||
let air9 = Self::encode_air_9bytes(plain9);
|
||||
raw17[6..15].copy_from_slice(&air9);
|
||||
let c = Self::crc16(&raw17[3..15]);
|
||||
raw17[15] = (c >> 8) as u8;
|
||||
raw17[16] = (c & 0xFF) as u8;
|
||||
}
|
||||
|
||||
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
||||
if let Some(last) = signal.last_mut() {
|
||||
if last.level == level {
|
||||
*last = LevelDuration::new(level, last.duration_us + duration);
|
||||
return;
|
||||
}
|
||||
}
|
||||
signal.push(LevelDuration::new(level, duration));
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for FordV1Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Ford V1"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: DATA_BITS,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.reset_state();
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
// C: !level && long → Preamble; seed preamble_count = 1.
|
||||
DecoderStep::Reset => {
|
||||
if !level && Self::is_preamble_long(duration) {
|
||||
self.step = DecoderStep::Preamble;
|
||||
self.preamble_count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Preamble => {
|
||||
if Self::is_preamble_long(duration) {
|
||||
self.preamble_count = self.preamble_count.saturating_add(1);
|
||||
} else if Self::is_short(duration) {
|
||||
if self.preamble_count >= PREAMBLE_MIN {
|
||||
// Enter Sync: buffer the first short event.
|
||||
self.sync_event_idx = 0;
|
||||
self.sync_event_count = 1;
|
||||
self.sync_events[0] = if level { EV_SHORT_HIGH } else { EV_SHORT_LOW };
|
||||
self.step = DecoderStep::Sync;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else if self.preamble_count < PREAMBLE_MIN {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
// else: stay in Preamble (long preamble already satisfied, ignore stray pulse)
|
||||
}
|
||||
|
||||
DecoderStep::Sync => {
|
||||
let (ev, is_short) = if Self::is_short(duration) {
|
||||
(if level { EV_SHORT_HIGH } else { EV_SHORT_LOW }, true)
|
||||
} else if Self::is_long(duration) {
|
||||
(if level { EV_LONG_HIGH } else { EV_LONG_LOW }, false)
|
||||
} else {
|
||||
self.step = DecoderStep::Preamble;
|
||||
return None;
|
||||
};
|
||||
|
||||
self.sync_event_idx += 1;
|
||||
if is_short {
|
||||
self.sync_event_count += 1;
|
||||
}
|
||||
if (self.sync_event_idx as usize) < 8 {
|
||||
self.sync_events[self.sync_event_idx as usize] = ev;
|
||||
}
|
||||
|
||||
if self.sync_event_count > 2 {
|
||||
// Sync detected: reset the bit buffer and replay buffered events into Manchester.
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.byte_count = 0;
|
||||
self.raw_bytes = [0; DATA_BYTES + 1];
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
if self.sync_events[0] == EV_SHORT_LOW {
|
||||
self.manchester_state = ManchesterState::Mid0;
|
||||
}
|
||||
self.step = DecoderStep::Data;
|
||||
|
||||
let last = self.sync_event_idx.min(7);
|
||||
for i in 0..=last {
|
||||
let event = self.sync_events[i as usize];
|
||||
if let Some(data_bit) = self.manchester_advance(event) {
|
||||
self.push_bit(data_bit);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.sync_event_idx >= 7 {
|
||||
self.step = DecoderStep::Preamble;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Data => {
|
||||
let event = if Self::is_short(duration) {
|
||||
if level { EV_SHORT_HIGH } else { EV_SHORT_LOW }
|
||||
} else if Self::is_long(duration) {
|
||||
if level { EV_LONG_HIGH } else { EV_LONG_LOW }
|
||||
} else {
|
||||
// Idle gap / odd pulse. The C decoder attempts partial-last-byte variants only
|
||||
// when byte_count==16 and 1-2 bits short; we require all 17 bytes, so any
|
||||
// non-short/long pulse (including very long inter-burst gaps, dur >=
|
||||
// te_long*SILENCE_LONG_MULT) ends the attempt and resets so the next burst can
|
||||
// re-sync from Reset.
|
||||
let _ = duration >= TE_LONG * SILENCE_LONG_MULT;
|
||||
self.reset_state();
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(data_bit) = self.manchester_advance(event) {
|
||||
self.push_bit(data_bit);
|
||||
|
||||
if self.byte_count > 16 {
|
||||
let result = self.process_data();
|
||||
self.reset_state();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0) as u32 & 0xF_FFFF;
|
||||
let btn = Self::map_button(button) & 0x0F;
|
||||
|
||||
// We can faithfully re-encode only when the original frame's plaintext was recovered at
|
||||
// decode time (strict branch). `extra` carries: [63:48]=crc16, [47:40]=strict_ok,
|
||||
// [39:32]=plain[4] (the byte the strict branch constrains).
|
||||
let extra = decoded.extra?;
|
||||
let strict_ok = ((extra >> 40) & 0x01) != 0;
|
||||
if !strict_ok {
|
||||
return None;
|
||||
}
|
||||
let plain4 = ((extra >> 32) & 0xFF) as u8;
|
||||
|
||||
// raw[0..7] = key1 (56 bits), taken from `data` (low 56 bits). The decoder packs key1 as
|
||||
// raw[0]<<48 | … | raw[6], so the top 7 bytes of the 56-bit value are raw[0..7].
|
||||
let mut raw17 = [0u8; DATA_BYTES];
|
||||
for (i, b) in raw17.iter_mut().take(7).enumerate() {
|
||||
*b = (decoded.data >> (48 - i * 8)) as u8;
|
||||
}
|
||||
|
||||
// Reconstruct the plaintext block from the decoded fields plus the stashed plain[4].
|
||||
// plain_apply_fields fills [0,1,2,3,5,6,7,8]; plain[4] is preserved from `extra`. The
|
||||
// strict-branch invariant the C decoder verified is plain[4]==air9[0]==raw[6] and
|
||||
// plain[3]==raw[5]; re-encoding plain reproduces the air block and hence those bytes.
|
||||
let mut plain9 = [0u8; 9];
|
||||
plain9[4] = plain4;
|
||||
// Seed checksum-bearing fields with the *decoded* values so the running checksum delta in
|
||||
// plain_apply_fields starts from the original plaintext's relationship, then apply the new
|
||||
// button/counter (same counter; no increment, matching Ford V0's replay policy).
|
||||
plain9[5] = (((decoded.button.unwrap_or(0) & 0x0F) << 4)
|
||||
| (((counter >> 16) & 0x0F) as u8)) as u8;
|
||||
plain9[6] = ((counter >> 8) & 0xFF) as u8;
|
||||
plain9[7] = (counter & 0xFF) as u8;
|
||||
plain9[8] = plain9[5].wrapping_add(plain9[6]).wrapping_add(plain9[7]);
|
||||
Self::plain_apply_fields(&mut plain9, serial, btn, counter);
|
||||
|
||||
// Rebuild raw[5] = plain[3] (strict invariant) and the air block + CRC from plaintext.
|
||||
raw17[5] = plain9[3];
|
||||
Self::rebuild_raw_from_plain(&mut raw17, &plain9);
|
||||
|
||||
// Build the 6-burst upload.
|
||||
let mut signal =
|
||||
Vec::with_capacity(ENC_BURST_COUNT * (ENC_PREAMBLE_PAIRS * 2 + 2 + DATA_BYTES * 16 + 1));
|
||||
for burst in 0..ENC_BURST_COUNT {
|
||||
let mut pkt = raw17;
|
||||
pkt[4] = ENC_BURST_PKT4[burst];
|
||||
let crcw = Self::crc16(&pkt[3..15]);
|
||||
pkt[15] = (crcw >> 8) as u8;
|
||||
pkt[16] = (crcw & 0xFF) as u8;
|
||||
|
||||
// Preamble: 400 pairs of long high / long low.
|
||||
for _ in 0..ENC_PREAMBLE_PAIRS {
|
||||
Self::enc_add_level(&mut signal, true, ENC_SYNC_LONG_US);
|
||||
Self::enc_add_level(&mut signal, false, ENC_SYNC_LONG_US);
|
||||
}
|
||||
// Sync: long high + short low.
|
||||
Self::enc_add_level(&mut signal, true, ENC_SYNC_LONG_US);
|
||||
Self::enc_add_level(&mut signal, false, ENC_SYNC_SHORT_US);
|
||||
|
||||
// Data: each bit → (bit, short) then (!bit, short) — Manchester, MSB first.
|
||||
for &b in pkt.iter() {
|
||||
for bit_i in (0..8).rev() {
|
||||
let bit = ((b >> bit_i) & 1) != 0;
|
||||
Self::enc_add_level(&mut signal, bit, ENC_SYNC_SHORT_US);
|
||||
Self::enc_add_level(&mut signal, !bit, ENC_SYNC_SHORT_US);
|
||||
}
|
||||
}
|
||||
|
||||
// Trailing gap (long for repeats, short for the final burst).
|
||||
let gap = if burst + 1 == ENC_BURST_COUNT {
|
||||
ENC_GAP_LAST_US
|
||||
} else {
|
||||
ENC_GAP_REPEAT_US
|
||||
};
|
||||
Self::enc_add_level(&mut signal, false, gap);
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FordV1Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// CRC16/CCITT known vector. "123456789" → 0x31C3 for poly 0x1021, init 0x0000
|
||||
/// (the standard CRC-16/XMODEM check value).
|
||||
#[test]
|
||||
fn crc16_known_vector() {
|
||||
let v = FordV1Decoder::crc16(b"123456789");
|
||||
assert_eq!(v, 0x31C3, "CRC-16/XMODEM check value mismatch: got {:04X}", v);
|
||||
}
|
||||
|
||||
/// The descramble cipher must be invertible: encode_inverse_block ∘ decode_with_flag(flag)
|
||||
/// should round-trip for the flag the parity selects.
|
||||
#[test]
|
||||
fn descramble_roundtrip() {
|
||||
// A plaintext block; encode to air, then plain_from_air must recover it exactly.
|
||||
let plain: [u8; 9] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x26, 0x77, 0x88, 0x99];
|
||||
let air = FordV1Decoder::encode_air_9bytes(&plain);
|
||||
let recovered = FordV1Decoder::plain_from_air(&air).expect("round-trip recover");
|
||||
assert_eq!(recovered, plain, "plain_from_air did not recover plaintext");
|
||||
}
|
||||
|
||||
/// Build a canonical Ford V1 plaintext (the strict-branch invariant is plain[4]==plain[0],
|
||||
/// since the cipher leaves byte 0 untouched, so air9[0]==plain[0]). Returns the 17-byte frame.
|
||||
fn build_frame(serial: u32, button: u8, counter: u32) -> [u8; DATA_BYTES] {
|
||||
let mut plain = [0u8; 9];
|
||||
plain[4] = (serial & 0xFF) as u8; // strict: plain[4] == air9[0] == plain[0]
|
||||
FordV1Decoder::plain_apply_fields(&mut plain, serial, button, counter);
|
||||
|
||||
let mut raw17 = [0u8; DATA_BYTES];
|
||||
// key1 head: raw[0..5] arbitrary-ish, raw[5] must equal plain[3] (strict: decoded[3]==raw[5]).
|
||||
raw17[0] = 0xC0;
|
||||
raw17[1] = 0xFF;
|
||||
raw17[2] = 0xEE;
|
||||
raw17[3] = (serial >> 24) as u8;
|
||||
raw17[4] = (serial >> 16) as u8;
|
||||
raw17[5] = plain[3];
|
||||
FordV1Decoder::rebuild_raw_from_plain(&mut raw17, &plain);
|
||||
raw17
|
||||
}
|
||||
|
||||
/// process_data must accept a canonical frame, take the strict branch, and recover fields.
|
||||
#[test]
|
||||
fn process_data_strict_branch() {
|
||||
let (serial, button, counter) = (0x1A2B3C4Du32, 0x02u8, 0x0123u32);
|
||||
let raw17 = build_frame(serial, button, counter);
|
||||
|
||||
let mut dec = FordV1Decoder::new();
|
||||
dec.raw_bytes[..DATA_BYTES].copy_from_slice(&raw17);
|
||||
dec.byte_count = DATA_BYTES;
|
||||
let d = dec.process_data().expect("process_data should accept the frame");
|
||||
assert!(d.crc_valid, "CRC must be valid");
|
||||
assert_eq!(d.serial, Some(serial), "serial mismatch");
|
||||
assert_eq!(d.button, Some(button), "button mismatch");
|
||||
assert_eq!(d.counter, Some(counter as u16), "counter mismatch");
|
||||
assert_eq!(d.data_count_bit, DATA_BITS);
|
||||
}
|
||||
|
||||
/// Reconstruct the 17 on-air data bytes of the first burst from the encoder's Manchester
|
||||
/// upload. The encoder emits, per data bit, (bit,short) then (!bit,short); add_level merges
|
||||
/// adjacent same-level pulses, so a same-bit boundary becomes a long pulse. We re-pair the
|
||||
/// stream by walking half-cells of `short` width (splitting merged long pulses into two halves)
|
||||
/// and reading each bit as the level of its first half-cell.
|
||||
fn first_burst_bytes(upload: &[LevelDuration]) -> [u8; DATA_BYTES] {
|
||||
// Expand merged pulses into ENC_SYNC_SHORT_US half-cells (long = 2 halves).
|
||||
let mut halves: Vec<bool> = Vec::new();
|
||||
for ld in upload {
|
||||
let n = ((ld.duration_us + ENC_SYNC_SHORT_US / 2) / ENC_SYNC_SHORT_US).max(1);
|
||||
for _ in 0..n {
|
||||
halves.push(ld.level);
|
||||
}
|
||||
}
|
||||
// Skip the preamble (400 long pairs = 1600 halves) + sync (long high=2 + short low=1 = 3).
|
||||
let data_start = ENC_PREAMBLE_PAIRS * 4 + 3;
|
||||
let mut out = [0u8; DATA_BYTES];
|
||||
for (byte_i, b) in out.iter_mut().enumerate() {
|
||||
for bit_pos in 0..8 {
|
||||
// Each bit = two half-cells; the bit value is the first half-cell's level.
|
||||
let idx = data_start + (byte_i * 8 + bit_pos) * 2;
|
||||
let bit = halves.get(idx).copied().unwrap_or(false);
|
||||
*b = (*b << 1) | (bit as u8);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Full encode→decode round trip at the on-air-frame level. Decode a canonical frame, re-encode
|
||||
/// it via `encode()`, recover the transmitted 17-byte frame from the first burst, and verify it
|
||||
/// decodes (via process_data) back to the same serial/button/counter with a valid CRC.
|
||||
#[test]
|
||||
fn encode_decode_roundtrip() {
|
||||
let (serial, button, counter) = (0x1A2B3C4Du32, 0x02u8, 0x0123u32);
|
||||
let raw17 = build_frame(serial, button, counter);
|
||||
|
||||
let mut dec = FordV1Decoder::new();
|
||||
dec.raw_bytes[..DATA_BYTES].copy_from_slice(&raw17);
|
||||
dec.byte_count = DATA_BYTES;
|
||||
let decoded = dec.process_data().expect("decode canonical frame");
|
||||
|
||||
// Re-encode (same button) → Manchester upload (6 bursts).
|
||||
let upload = FordV1Decoder::new()
|
||||
.encode(&decoded, button)
|
||||
.expect("encoder should produce an upload");
|
||||
assert!(!upload.is_empty(), "encoder upload must be non-empty");
|
||||
|
||||
// Recover the transmitted frame from burst 0 and decode it through process_data.
|
||||
// Burst 0 overrides pkt[4] = 0x08 (ENC_BURST_PKT4[0]) and recomputes the CRC to match, so
|
||||
// the recovered frame is self-consistent. The strict-branch serial comes from the plaintext
|
||||
// (decoded[1..4],decoded[0]), independent of raw[4], so serial/button/counter still match.
|
||||
let tx = first_burst_bytes(&upload);
|
||||
let mut dec2 = FordV1Decoder::new();
|
||||
dec2.raw_bytes[..DATA_BYTES].copy_from_slice(&tx);
|
||||
dec2.byte_count = DATA_BYTES;
|
||||
let got = dec2
|
||||
.process_data()
|
||||
.expect("re-encoded burst-0 frame must decode via process_data");
|
||||
assert!(got.crc_valid, "round-tripped CRC must be valid");
|
||||
assert_eq!(got.serial, Some(serial), "round-trip serial mismatch");
|
||||
assert_eq!(got.button, Some(button), "round-trip button mismatch");
|
||||
assert_eq!(got.counter, Some(counter as u16), "round-trip counter mismatch");
|
||||
|
||||
// Structural checks: 6 bursts worth of data, each preamble present.
|
||||
let long_highs = upload
|
||||
.iter()
|
||||
.filter(|l| l.level && l.duration_us >= ENC_SYNC_LONG_US)
|
||||
.count();
|
||||
assert!(
|
||||
long_highs >= ENC_BURST_COUNT,
|
||||
"expected at least one long-high preamble pulse per burst"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
//! Ford V2 protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v2.c` and `ford_v2.h`.
|
||||
//! Manchester 200/400µs (te_delta 260 → threshold ~460µs between short/long), 104 bits (13 bytes), FM.
|
||||
//! Frame begins with a 16-bit Manchester sync that equals ~0x7FA7 (the decoder matches the *inverted*
|
||||
//! shift register against 0x8058); the two sync bytes 0x7F 0xA7 head the 13-byte buffer. Data bits are
|
||||
//! inverted before packing (`data_bit = !data_bit`). Structure is validated by the two sync bytes plus a
|
||||
//! known button code. Encoder supported (matches subghz_protocol_encoder_ford_v2).
|
||||
//!
|
||||
//! Decoder steps: Reset → Preamble (≥64 shorts) → Sync (find 0x7FA7) → Data (11 bytes).
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 200;
|
||||
const TE_LONG: u32 = 400;
|
||||
const TE_DELTA: u32 = 260;
|
||||
const DATA_BITS: usize = 104;
|
||||
const DATA_BYTES: usize = 13;
|
||||
const PREAMBLE_MIN: u16 = 64;
|
||||
const SYNC_0: u8 = 0x7F;
|
||||
const SYNC_1: u8 = 0xA7;
|
||||
const SYNC_BITS: u8 = 16;
|
||||
const INTER_BURST_GAP_US: u32 = 15000;
|
||||
|
||||
// Encoder constants (subghz_protocol_encoder_ford_v2)
|
||||
const ENC_TE_SHORT: u32 = 240;
|
||||
const ENC_PREAMBLE_PAIRS: usize = 70;
|
||||
const ENC_BURST_COUNT: usize = 6;
|
||||
const ENC_INTER_BURST_GAP_US: u32 = 16000;
|
||||
const ENC_SYNC_LO_US: u32 = 476;
|
||||
const TAIL_RAW_BYTES: usize = 5;
|
||||
|
||||
/// Inverted 16-bit sync the decoder matches against (= !0x7FA7).
|
||||
const SYNC_SHIFT16_INV: u16 = !(((SYNC_0 as u16) << 8) | SYNC_1 as u16);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0 = 0,
|
||||
Mid1 = 1,
|
||||
Start0 = 2,
|
||||
Start1 = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble,
|
||||
Sync,
|
||||
Data,
|
||||
}
|
||||
|
||||
pub struct FordV2Decoder {
|
||||
step: DecoderStep,
|
||||
manchester_state: ManchesterState,
|
||||
preamble_count: u16,
|
||||
raw_bytes: [u8; DATA_BYTES],
|
||||
byte_count: usize,
|
||||
decode_data: u16,
|
||||
decode_count_bit: usize,
|
||||
sync_shift: u16,
|
||||
sync_bit_count: u8,
|
||||
}
|
||||
|
||||
impl FordV2Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
preamble_count: 0,
|
||||
raw_bytes: [0; DATA_BYTES],
|
||||
byte_count: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
sync_shift: 0,
|
||||
sync_bit_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_state(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.preamble_count = 0;
|
||||
self.raw_bytes = [0; DATA_BYTES];
|
||||
self.byte_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.sync_shift = 0;
|
||||
self.sync_bit_count = 0;
|
||||
}
|
||||
|
||||
fn is_short(d: u32) -> bool {
|
||||
duration_diff!(d, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
|
||||
fn is_long(d: u32) -> bool {
|
||||
duration_diff!(d, TE_LONG) < TE_DELTA
|
||||
}
|
||||
|
||||
/// Flipper Manchester transition table (same as Ford V0). Event 0=ShortLow,1=ShortHigh,
|
||||
/// 2=LongLow,3=LongHigh. Returns Some(bit) when a bit emits.
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||
let (new_state, emit) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
|
||||
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
|
||||
|
||||
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
|
||||
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
|
||||
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
_ => (ManchesterState::Mid1, false),
|
||||
};
|
||||
self.manchester_state = new_state;
|
||||
if emit { Some((event & 1) == 1) } else { None }
|
||||
}
|
||||
|
||||
/// Map (level, duration) → Manchester event. Ford V2 polarity: level ? High : Low.
|
||||
fn pulse_event(level: bool, duration: u32) -> Option<u8> {
|
||||
if Self::is_short(duration) {
|
||||
Some(if level { 1 } else { 0 })
|
||||
} else if Self::is_long(duration) {
|
||||
Some(if level { 3 } else { 2 })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn button_is_valid(btn: u8) -> bool {
|
||||
matches!(btn, 0x10 | 0x11 | 0x13 | 0x14 | 0x15)
|
||||
}
|
||||
|
||||
/// Enter the Sync step from the preamble, seeding state for the triggering long-low pulse.
|
||||
fn enter_sync_from_preamble(&mut self, level: bool, duration: u32) {
|
||||
self.step = DecoderStep::Sync;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.byte_count = 0;
|
||||
self.sync_shift = 0;
|
||||
self.sync_bit_count = 0;
|
||||
self.raw_bytes = [0; DATA_BYTES];
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
|
||||
if let Some(event) = Self::pulse_event(level, duration) {
|
||||
// Low event (0/2) seeds Mid0 (matches the C `if(ev==ShortLow||ev==LongLow) state=Mid0`).
|
||||
if event == 0 || event == 2 {
|
||||
self.manchester_state = ManchesterState::Mid0;
|
||||
}
|
||||
self.feed_event(event);
|
||||
} else {
|
||||
self.reset_state();
|
||||
}
|
||||
}
|
||||
|
||||
/// Process one Manchester event in Sync or Data step. Returns Some when a frame commits.
|
||||
fn feed_event(&mut self, event: u8) -> Option<DecodedSignal> {
|
||||
if self.step == DecoderStep::Sync {
|
||||
if let Some(bit) = self.manchester_advance(event) {
|
||||
self.sync_shift = (self.sync_shift << 1) | (bit as u16);
|
||||
if self.sync_bit_count < SYNC_BITS {
|
||||
self.sync_bit_count += 1;
|
||||
}
|
||||
if self.sync_bit_count >= SYNC_BITS && self.sync_shift == SYNC_SHIFT16_INV {
|
||||
// Enter data: prime sync bytes and bit counter.
|
||||
self.raw_bytes = [0; DATA_BYTES];
|
||||
self.raw_bytes[0] = SYNC_0;
|
||||
self.raw_bytes[1] = SYNC_1;
|
||||
self.byte_count = 2;
|
||||
self.step = DecoderStep::Data;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = SYNC_BITS as usize;
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Data step
|
||||
if let Some(bit) = self.manchester_advance(event) {
|
||||
let data_bit = !bit; // Ford V2 inverts decoded bits
|
||||
self.decode_data = (self.decode_data << 1) | (data_bit as u16);
|
||||
self.decode_count_bit += 1;
|
||||
|
||||
if self.decode_count_bit & 7 == 0 {
|
||||
let byte_val = (self.decode_data & 0xFF) as u8;
|
||||
if self.byte_count < DATA_BYTES {
|
||||
self.raw_bytes[self.byte_count] = byte_val;
|
||||
self.byte_count += 1;
|
||||
}
|
||||
self.decode_data = 0;
|
||||
|
||||
if self.byte_count == DATA_BYTES {
|
||||
let result = self.commit_frame();
|
||||
self.reset_state();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Validate sync bytes + structure and build the decoded signal.
|
||||
fn commit_frame(&self) -> Option<DecodedSignal> {
|
||||
let k = &self.raw_bytes;
|
||||
if k[0] != SYNC_0 || k[1] != SYNC_1 {
|
||||
return None;
|
||||
}
|
||||
if !Self::button_is_valid(k[6]) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let serial = ((k[2] as u32) << 24)
|
||||
| ((k[3] as u32) << 16)
|
||||
| ((k[4] as u32) << 8)
|
||||
| (k[5] as u32);
|
||||
let counter = (((k[7] & 0x7F) as u16) << 9) | ((k[8] as u16) << 1) | ((k[9] >> 7) as u16);
|
||||
|
||||
// Top 8 bytes → 64-bit data for display/export (matches generic.data).
|
||||
let mut data = 0u64;
|
||||
for &byte in k.iter().take(8) {
|
||||
data = (data << 8) | byte as u64;
|
||||
}
|
||||
// Tail raw bytes k[8..13] → extra (40 bits) so the encoder can rebuild the full frame.
|
||||
let mut extra = 0u64;
|
||||
for &byte in k.iter().skip(8).take(TAIL_RAW_BYTES) {
|
||||
extra = (extra << 8) | byte as u64;
|
||||
}
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(k[6]),
|
||||
counter: Some(counter),
|
||||
crc_valid: true, // validated by sync bytes + button structure
|
||||
data,
|
||||
data_count_bit: DATA_BITS,
|
||||
encoder_capable: true,
|
||||
extra: Some(extra),
|
||||
protocol_display_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Map KAT's generic button command to a Ford V2 button code.
|
||||
fn map_button(button: u8) -> u8 {
|
||||
match button {
|
||||
0x01 => 0x10, // Lock
|
||||
0x02 => 0x11, // Unlock
|
||||
0x04 => 0x13, // Trunk
|
||||
0x08 => 0x14, // Panic
|
||||
b if Self::button_is_valid(b) => b, // already a Ford V2 code
|
||||
_ => 0x11,
|
||||
}
|
||||
}
|
||||
|
||||
fn parity8(mut v: u8) -> u8 {
|
||||
let mut p = 0u8;
|
||||
while v != 0 {
|
||||
p ^= v & 1;
|
||||
v >>= 1;
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
||||
if let Some(last) = signal.last_mut() {
|
||||
if last.level == level {
|
||||
*last = LevelDuration::new(level, last.duration_us + duration);
|
||||
return;
|
||||
}
|
||||
}
|
||||
signal.push(LevelDuration::new(level, duration));
|
||||
}
|
||||
|
||||
fn enc_manchester_bit(signal: &mut Vec<LevelDuration>, bit: bool) {
|
||||
if bit {
|
||||
Self::enc_add_level(signal, true, ENC_TE_SHORT);
|
||||
Self::enc_add_level(signal, false, ENC_TE_SHORT);
|
||||
} else {
|
||||
Self::enc_add_level(signal, false, ENC_TE_SHORT);
|
||||
Self::enc_add_level(signal, true, ENC_TE_SHORT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for FordV2Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Ford V2"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: DATA_BITS,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.reset_state();
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if Self::is_short(duration) {
|
||||
self.preamble_count = 1;
|
||||
self.step = DecoderStep::Preamble;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Preamble => {
|
||||
if Self::is_short(duration) {
|
||||
if self.preamble_count < u16::MAX {
|
||||
self.preamble_count += 1;
|
||||
}
|
||||
} else if !level && Self::is_long(duration) {
|
||||
if self.preamble_count >= PREAMBLE_MIN {
|
||||
self.enter_sync_from_preamble(level, duration);
|
||||
} else {
|
||||
self.reset_state();
|
||||
}
|
||||
} else {
|
||||
self.reset_state();
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Sync | DecoderStep::Data => {
|
||||
if let Some(event) = Self::pulse_event(level, duration) {
|
||||
if let Some(result) = self.feed_event(event) {
|
||||
return Some(result);
|
||||
}
|
||||
} else {
|
||||
// Non-short/long pulse (gap/out-of-range) ends the attempt.
|
||||
let _ = duration >= INTER_BURST_GAP_US;
|
||||
self.reset_state();
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
// Rebuild the 13-byte frame: bytes 0..8 from data, tail bytes 8..13 from extra.
|
||||
let mut raw = [0u8; DATA_BYTES];
|
||||
for (i, b) in raw.iter_mut().take(8).enumerate() {
|
||||
*b = (decoded.data >> (56 - i * 8)) as u8;
|
||||
}
|
||||
let extra = decoded.extra.unwrap_or(0);
|
||||
for (i, b) in raw.iter_mut().skip(8).take(TAIL_RAW_BYTES).enumerate() {
|
||||
*b = (extra >> (32 - i * 8)) as u8;
|
||||
}
|
||||
|
||||
// Apply the requested button and refresh the byte-7 parity MSB.
|
||||
raw[6] = Self::map_button(button);
|
||||
if !Self::button_is_valid(raw[6]) {
|
||||
return None;
|
||||
}
|
||||
let parity_msb = Self::parity8(raw[6]) << 7;
|
||||
raw[7] = (raw[7] & 0x7F) | parity_msb;
|
||||
// Ensure sync header is present.
|
||||
raw[0] = SYNC_0;
|
||||
raw[1] = SYNC_1;
|
||||
|
||||
let mut signal = Vec::with_capacity(ENC_BURST_COUNT * (ENC_PREAMBLE_PAIRS * 2 + DATA_BITS * 2 + 4));
|
||||
for burst in 0..ENC_BURST_COUNT {
|
||||
// Preamble: 70 pairs of (low short, high short)
|
||||
for _ in 0..ENC_PREAMBLE_PAIRS {
|
||||
Self::enc_add_level(&mut signal, false, ENC_TE_SHORT);
|
||||
Self::enc_add_level(&mut signal, true, ENC_TE_SHORT);
|
||||
}
|
||||
// Sync low + high short
|
||||
Self::enc_add_level(&mut signal, false, ENC_SYNC_LO_US);
|
||||
Self::enc_add_level(&mut signal, true, ENC_TE_SHORT);
|
||||
// Data bits 1..103 (bit 0 implied by the high short above)
|
||||
for bit_pos in 1..DATA_BITS {
|
||||
let byte_idx = bit_pos / 8;
|
||||
let bit_idx = 7 - (bit_pos % 8);
|
||||
let bit = (raw[byte_idx] >> bit_idx) & 1 != 0;
|
||||
Self::enc_manchester_bit(&mut signal, bit);
|
||||
}
|
||||
if burst + 1 < ENC_BURST_COUNT {
|
||||
Self::enc_add_level(&mut signal, true, ENC_INTER_BURST_GAP_US);
|
||||
}
|
||||
}
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FordV2Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
//! Ford V3 protocol decoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v3.c` and `ford_v3.h`.
|
||||
//! Manchester 240/480µs, 104 bits (13 bytes), FM, plaintext (no CRC/encryption). Decode-only —
|
||||
//! the reference encoder is NULL.
|
||||
//!
|
||||
//! Manchester uses Flipper's manchester_decoder.h transition table (same as Ford V0), but Ford V3
|
||||
//! maps level the OPPOSITE way from Ford V0: `level ? ShortHigh/LongHigh : ShortLow/LongLow`
|
||||
//! (Ford V0 uses `level ? Low : High`). Decoder steps: Reset → Preamble (≥30 shorts) → Data.
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 240;
|
||||
const TE_LONG: u32 = 480;
|
||||
const TE_DELTA: u32 = 60;
|
||||
const DATA_BITS: usize = 104;
|
||||
const DATA_BYTES: usize = 13;
|
||||
const PREAMBLE_MIN: u16 = 30;
|
||||
|
||||
const BTN_LOCK: u8 = 0x01;
|
||||
const BTN_UNLOCK: u8 = 0x02;
|
||||
|
||||
/// Manchester state machine (Flipper manchester_decoder.h transition table; same as Ford V0).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0 = 0,
|
||||
Mid1 = 1,
|
||||
Start0 = 2,
|
||||
Start1 = 3,
|
||||
}
|
||||
|
||||
/// Decoder step states (matches FordV3DecoderStep in ford_v3.c)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble,
|
||||
Data,
|
||||
}
|
||||
|
||||
/// Ford V3 protocol decoder (matches SubGhzProtocolDecoderFordV3)
|
||||
pub struct FordV3Decoder {
|
||||
step: DecoderStep,
|
||||
manchester_state: ManchesterState,
|
||||
raw_bytes: [u8; DATA_BYTES],
|
||||
bit_count: usize,
|
||||
preamble_count: u16,
|
||||
}
|
||||
|
||||
impl FordV3Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
raw_bytes: [0; DATA_BYTES],
|
||||
bit_count: 0,
|
||||
preamble_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset accumulators (matches ford_v3_reset_data)
|
||||
fn reset_data(&mut self) {
|
||||
self.raw_bytes = [0; DATA_BYTES];
|
||||
self.bit_count = 0;
|
||||
self.preamble_count = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
/// Add a decoded bit MSB-first into the byte buffer (matches ford_v3_add_bit)
|
||||
fn add_bit(&mut self, bit: bool) {
|
||||
if self.bit_count >= DATA_BITS {
|
||||
return;
|
||||
}
|
||||
let byte_index = self.bit_count / 8;
|
||||
let bit_in_byte = 7 - (self.bit_count % 8);
|
||||
if bit {
|
||||
self.raw_bytes[byte_index] |= 1 << bit_in_byte;
|
||||
}
|
||||
self.bit_count += 1;
|
||||
}
|
||||
|
||||
/// Manchester state machine (Flipper manchester_advance).
|
||||
/// Event: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh. Returns Some(bit) when a bit emits.
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||
let (new_state, emit) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
|
||||
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
|
||||
|
||||
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
|
||||
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
|
||||
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
_ => (ManchesterState::Mid1, false),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
if emit {
|
||||
Some((event & 1) == 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_short(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
|
||||
fn is_long(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
}
|
||||
|
||||
/// Build the decoded signal when 104 bits are collected (matches ford_v3_parse_fields).
|
||||
fn build_signal(&self) -> DecodedSignal {
|
||||
let b = &self.raw_bytes;
|
||||
let serial = ((b[1] as u32) << 24)
|
||||
| ((b[2] as u32) << 16)
|
||||
| ((b[3] as u32) << 8)
|
||||
| (b[4] as u32);
|
||||
// Counter is the bitwise-inverted bytes 7 and 8 (ref: ~b[7], ~b[8])
|
||||
let counter = (((!b[7]) as u16) << 8) | ((!b[8]) as u16);
|
||||
let button = if b[6] & 0x01 != 0 { BTN_UNLOCK } else { BTN_LOCK };
|
||||
|
||||
// Pack the first 8 bytes (big-endian) into the 64-bit data field for display/export
|
||||
// (matches ford_v3.c serialize, which stores bytes[0..8] in generic.data).
|
||||
let mut data = 0u64;
|
||||
for &byte in b.iter().take(8) {
|
||||
data = (data << 8) | byte as u64;
|
||||
}
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: true, // Ford V3 is plaintext with no CRC
|
||||
data,
|
||||
data_count_bit: DATA_BITS,
|
||||
encoder_capable: false,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for FordV3Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Ford V3"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: DATA_BITS,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.reset_data();
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
// Ref: any short pulse begins the preamble (no level check).
|
||||
DecoderStep::Reset => {
|
||||
if Self::is_short(duration) {
|
||||
self.reset_data();
|
||||
self.preamble_count = 1;
|
||||
self.step = DecoderStep::Preamble;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Preamble => {
|
||||
if Self::is_short(duration) {
|
||||
self.preamble_count += 1;
|
||||
} else if self.preamble_count >= PREAMBLE_MIN && Self::is_long(duration) {
|
||||
// First data bit: long pulse, mapped level ? LongHigh : LongLow.
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
let event = if level { 3 } else { 2 };
|
||||
if let Some(bit) = self.manchester_advance(event) {
|
||||
self.add_bit(bit);
|
||||
}
|
||||
self.step = DecoderStep::Data;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Data => {
|
||||
let short = Self::is_short(duration);
|
||||
let long = Self::is_long(duration);
|
||||
|
||||
if !short && !long {
|
||||
// Gap / out-of-range pulse ends the frame.
|
||||
let ready = self.bit_count >= DATA_BITS;
|
||||
let result = if ready { Some(self.build_signal()) } else { None };
|
||||
self.step = DecoderStep::Reset;
|
||||
self.reset_data();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ford V3 polarity: level ? High : Low (opposite of Ford V0).
|
||||
let event = if level {
|
||||
if short { 1 } else { 3 } // ShortHigh / LongHigh
|
||||
} else if short {
|
||||
0 // ShortLow
|
||||
} else {
|
||||
2 // LongLow
|
||||
};
|
||||
|
||||
if let Some(bit) = self.manchester_advance(event) {
|
||||
self.add_bit(bit);
|
||||
if self.bit_count >= DATA_BITS {
|
||||
let result = self.build_signal();
|
||||
self.step = DecoderStep::Reset;
|
||||
self.reset_data();
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FordV3Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
//! Honda Static protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/honda_static.c` and
|
||||
//! `honda_static.h`. Honda/Acura fixed-code keyfobs. FM, 315 MHz + 433.92 MHz, 64-bit frame.
|
||||
//!
|
||||
//! Unlike most KAT decoders, Honda Static does NOT use the Flipper manchester_decoder.h transition
|
||||
//! table. It buffers a per-element *symbol stream* (one bit per ~63µs element) and then performs a
|
||||
//! custom Manchester unpack over symbol pairs (see `honda_static_manchester_pack_64` in the C). A
|
||||
//! short pulse contributes one symbol = the pulse level; a long pulse contributes two symbols of the
|
||||
//! same level. Anything outside both ranges (e.g. the 700µs sync or a trailing gap) terminates the
|
||||
//! buffer and triggers a parse attempt.
|
||||
//!
|
||||
//! Frame (64 bits, MSB-first into 8 bytes): button (4b) | serial (28b) | counter (24b) |
|
||||
//! checksum (8b). The checksum is an XOR of bytes[0..7] (the first 7 bytes). Emission is gated on
|
||||
//! the checksum validating, so Honda Static will not false-match. The parser tries the
|
||||
//! inverted-Manchester interpretation first (which is what the encoder emits), then non-inverted
|
||||
//! forward, then a bit-reversed-bytes pass — matching the C.
|
||||
//!
|
||||
//! The exported `data` (Key) word is the C `generic.data`: a compact nibble-packed layout
|
||||
//! (`honda_static_pack_compact`), NOT the raw decoded packet bytes.
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const BIT_COUNT: usize = 64;
|
||||
const MIN_SYMBOLS: usize = 36;
|
||||
const SHORT_BASE_US: u32 = 28;
|
||||
const SHORT_SPAN_US: u32 = 70;
|
||||
const LONG_BASE_US: u32 = 61;
|
||||
const LONG_SPAN_US: u32 = 130;
|
||||
const SYNC_TIME_US: u32 = 700;
|
||||
const ELEMENT_TIME_US: u32 = 63;
|
||||
const SYMBOL_CAPACITY: usize = 512;
|
||||
const PREAMBLE_ALTERNATING_COUNT: usize = 160;
|
||||
const PREAMBLE_MAX_TRANSITIONS: u16 = 19;
|
||||
|
||||
// Reported timing constants (informational; matches ProtoPirate protocol_items profile).
|
||||
const TE_SHORT: u32 = ELEMENT_TIME_US;
|
||||
const TE_LONG: u32 = SYNC_TIME_US;
|
||||
const TE_DELTA: u32 = 120;
|
||||
|
||||
// KAT generic button codes.
|
||||
const BTN_LOCK: u8 = 0x01;
|
||||
const BTN_UNLOCK: u8 = 0x02;
|
||||
const BTN_TRUNK: u8 = 0x04;
|
||||
const BTN_PANIC: u8 = 0x08;
|
||||
|
||||
// honda_static_encoder_button_map[4] in the C ({0x02,0x04,0x08,0x05}); used by the encoder remap.
|
||||
const ENCODER_BUTTON_MAP: [u8; 4] = [0x02, 0x04, 0x08, 0x05];
|
||||
|
||||
/// Decoded Honda Static fields (matches HondaStaticFields).
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct HondaStaticFields {
|
||||
button: u8,
|
||||
serial: u32,
|
||||
counter: u32,
|
||||
/// XOR checksum byte (mirrors the C struct field; validation recomputes it, so this is
|
||||
/// retained for fidelity/inspection rather than being read on the hot path).
|
||||
#[allow(dead_code)]
|
||||
checksum: u8,
|
||||
}
|
||||
|
||||
/// Honda Static decoder (matches SubGhzProtocolDecoderHondaStatic).
|
||||
pub struct HondaStaticDecoder {
|
||||
/// Per-element symbol stream (one bit per ~63µs element). Index 0 is the first received symbol.
|
||||
symbols: Vec<bool>,
|
||||
}
|
||||
|
||||
impl HondaStaticDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
symbols: Vec::with_capacity(SYMBOL_CAPACITY),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract `count` bits starting at `start`, MSB-first across the byte array
|
||||
/// (matches honda_static_get_bits / honda_static_get_bits_u32, shift = (~bit_index)&7).
|
||||
fn get_bits(data: &[u8], start: usize, count: usize) -> u32 {
|
||||
let mut value: u32 = 0;
|
||||
for i in 0..count {
|
||||
let bit_index = start + i;
|
||||
let byte = data[bit_index >> 3];
|
||||
let shift = (!bit_index) & 0x07;
|
||||
value = (value << 1) | (((byte >> shift) & 1) as u32);
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
/// XOR checksum over the first 7 bytes of the 8-byte packet (matches the inline checksum loop in
|
||||
/// honda_static_validate_forward_packet / honda_static_build_packet_bytes).
|
||||
fn packet_checksum(packet: &[u8]) -> u8 {
|
||||
let mut checksum = 0u8;
|
||||
for &b in packet.iter().take(7) {
|
||||
checksum ^= b;
|
||||
}
|
||||
checksum
|
||||
}
|
||||
|
||||
/// Reverse the bit order of a byte (matches pp_reverse_bits8).
|
||||
fn reverse_bits8(value: u8) -> u8 {
|
||||
let mut v = value;
|
||||
v = ((v & 0xF0) >> 4) | ((v & 0x0F) << 4);
|
||||
v = ((v & 0xCC) >> 2) | ((v & 0x33) << 2);
|
||||
v = ((v & 0xAA) >> 1) | ((v & 0x55) << 1);
|
||||
v
|
||||
}
|
||||
|
||||
fn is_valid_button(button: u8) -> bool {
|
||||
// honda_static_is_valid_button: button <= 9 && ((0x336 >> button) & 1)
|
||||
// 0x336 = 0b1100110110 → valid buttons: 1,2,4,5,8,9.
|
||||
button <= 9 && ((0x336u16 >> button) & 1) != 0
|
||||
}
|
||||
|
||||
fn is_valid_serial(serial: u32) -> bool {
|
||||
serial != 0 && serial != 0x0FFF_FFFF
|
||||
}
|
||||
|
||||
/// Pack fields into the 64-bit compact "Key" word (matches honda_static_pack_compact →
|
||||
/// pp_bytes_to_u64_be over the compact[8] nibble-packed layout).
|
||||
fn pack_compact(fields: &HondaStaticFields) -> u64 {
|
||||
let mut compact = [0u8; 8];
|
||||
compact[0] = fields.button & 0x0F;
|
||||
compact[1] = (fields.serial >> 20) as u8;
|
||||
compact[2] = (fields.serial >> 12) as u8;
|
||||
compact[3] = (fields.serial >> 4) as u8;
|
||||
compact[4] = (fields.serial << 4) as u8;
|
||||
compact[5] = (fields.counter >> 16) as u8;
|
||||
compact[6] = (fields.counter >> 8) as u8;
|
||||
compact[7] = fields.counter as u8;
|
||||
u64::from_be_bytes(compact)
|
||||
}
|
||||
|
||||
/// Build the raw 8-byte (64-bit) packet from fields, MSB-first
|
||||
/// (matches honda_static_build_packet_bytes; checksum filled into byte 7).
|
||||
fn build_packet_bytes(fields: &HondaStaticFields) -> [u8; 8] {
|
||||
let mut packet = [0u8; 8];
|
||||
Self::set_bits(&mut packet, 0, 4, (fields.button & 0x0F) as u32);
|
||||
Self::set_bits(&mut packet, 4, 28, fields.serial);
|
||||
Self::set_bits(&mut packet, 32, 24, fields.counter);
|
||||
let checksum = Self::packet_checksum(&packet);
|
||||
Self::set_bits(&mut packet, 56, 8, checksum as u32);
|
||||
packet
|
||||
}
|
||||
|
||||
/// Set `count` bits starting at `start`, MSB-first (matches honda_static_set_bits).
|
||||
fn set_bits(data: &mut [u8], start: usize, count: usize, value: u32) {
|
||||
for i in 0..count {
|
||||
let bit_index = start + i;
|
||||
let byte_index = bit_index >> 3;
|
||||
let shift = (!bit_index) & 0x07;
|
||||
let mask = 1u8 << shift;
|
||||
let bit = ((value >> (count - 1 - i)) & 1) != 0;
|
||||
if bit {
|
||||
data[byte_index] |= mask;
|
||||
} else {
|
||||
data[byte_index] &= !mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate a forward (as-decoded) 8-byte packet (matches honda_static_validate_forward_packet).
|
||||
fn validate_forward_packet(packet: &[u8; 8]) -> Option<HondaStaticFields> {
|
||||
let button = Self::get_bits(packet, 0, 4) as u8;
|
||||
let serial = Self::get_bits(packet, 4, 28);
|
||||
let counter = Self::get_bits(packet, 32, 24);
|
||||
let checksum = Self::get_bits(packet, 56, 8) as u8;
|
||||
let checksum_calc = Self::packet_checksum(packet);
|
||||
|
||||
if checksum != checksum_calc {
|
||||
return None;
|
||||
}
|
||||
if !Self::is_valid_button(button) {
|
||||
return None;
|
||||
}
|
||||
if !Self::is_valid_serial(serial) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(HondaStaticFields {
|
||||
button,
|
||||
serial,
|
||||
counter,
|
||||
checksum,
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate a bit-reversed packet (matches honda_static_validate_reverse_packet). Note: the C
|
||||
/// does NOT re-check the checksum here (it reverses bytes then validates button/serial only),
|
||||
/// so this path is not checksum-gated. We mirror that, but the caller flags crc_valid=false.
|
||||
fn validate_reverse_packet(packet: &[u8; 8]) -> Option<HondaStaticFields> {
|
||||
let mut reversed = [0u8; 8];
|
||||
for (i, b) in packet.iter().enumerate() {
|
||||
reversed[i] = Self::reverse_bits8(*b);
|
||||
}
|
||||
|
||||
let button = Self::get_bits(&reversed, 0, 4) as u8;
|
||||
let serial = Self::get_bits(&reversed, 4, 28);
|
||||
let counter = Self::get_bits(&reversed, 32, 24);
|
||||
let checksum = Self::packet_checksum(&reversed);
|
||||
|
||||
if !Self::is_valid_button(button) {
|
||||
return None;
|
||||
}
|
||||
if !Self::is_valid_serial(serial) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(HondaStaticFields {
|
||||
button,
|
||||
serial,
|
||||
counter,
|
||||
checksum,
|
||||
})
|
||||
}
|
||||
|
||||
/// Manchester unpack over symbol pairs (matches honda_static_manchester_pack_64).
|
||||
/// Returns the packed 8-byte packet plus how many bits were collected. `inverted`:
|
||||
/// bit=1 when (a==0,b==1); non-inverted: bit=1 when (a==1,b==0). Equal adjacent symbols are
|
||||
/// skipped (advance by 1).
|
||||
fn manchester_pack_64(symbols: &[bool], start_pos: usize, inverted: bool) -> ([u8; 8], usize) {
|
||||
let mut packet = [0u8; 8];
|
||||
let count = symbols.len();
|
||||
let mut pos = start_pos;
|
||||
let mut bit_count: usize = 0;
|
||||
|
||||
while pos + 1 < count {
|
||||
if bit_count >= BIT_COUNT {
|
||||
break;
|
||||
}
|
||||
let a = symbols[pos];
|
||||
let b = symbols[pos + 1];
|
||||
if a == b {
|
||||
pos += 1;
|
||||
continue;
|
||||
}
|
||||
let bit = if inverted {
|
||||
!a && b // a==0 && b==1
|
||||
} else {
|
||||
a && !b // a==1 && b==0
|
||||
};
|
||||
if bit {
|
||||
let shift = (!bit_count) & 0x07;
|
||||
packet[bit_count >> 3] |= 1u8 << shift;
|
||||
}
|
||||
bit_count += 1;
|
||||
pos += 2;
|
||||
}
|
||||
|
||||
(packet, bit_count)
|
||||
}
|
||||
|
||||
/// Locate the data start (after the alternating preamble + sync run) and unpack/validate.
|
||||
/// Matches honda_static_parse_symbols. Returns the validated fields and whether the checksum
|
||||
/// path validated it (forward = true; reverse = false).
|
||||
fn parse_symbols(symbols: &[bool], inverted: bool) -> Option<(HondaStaticFields, bool)> {
|
||||
let count = symbols.len();
|
||||
if count == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Walk the alternating preamble: count consecutive transitions; when a non-transition
|
||||
// follows a run longer than PREAMBLE_MAX_TRANSITIONS, that's the preamble/data boundary.
|
||||
let mut index = 1usize;
|
||||
let mut transitions: u16 = 0;
|
||||
while index < count {
|
||||
if symbols[index] != symbols[index - 1] {
|
||||
transitions += 1;
|
||||
} else {
|
||||
if transitions > PREAMBLE_MAX_TRANSITIONS {
|
||||
break;
|
||||
}
|
||||
transitions = 0;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if index >= count {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Skip forward over the equal-adjacent run (the sync gap).
|
||||
while (index + 1 < count) && (symbols[index] == symbols[index + 1]) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
let data_start = index;
|
||||
let (packet, bit_count) = Self::manchester_pack_64(symbols, data_start, inverted);
|
||||
if bit_count < BIT_COUNT {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(fields) = Self::validate_forward_packet(&packet) {
|
||||
return Some((fields, true));
|
||||
}
|
||||
|
||||
if inverted {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(fields) = Self::validate_reverse_packet(&packet) {
|
||||
return Some((fields, false));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Build a DecodedSignal from validated fields. `crc_valid` reflects whether the forward
|
||||
/// (checksum-validated) path matched.
|
||||
fn build_signal(fields: &HondaStaticFields, crc_valid: bool) -> DecodedSignal {
|
||||
DecodedSignal {
|
||||
serial: Some(fields.serial),
|
||||
button: Some(fields.button),
|
||||
// KAT counters are u16; Honda's is 24-bit. Truncate to the low 16 bits for the field
|
||||
// (the full 24-bit counter is preserved in the packed `data` word).
|
||||
counter: Some(fields.counter as u16),
|
||||
crc_valid,
|
||||
data: Self::pack_compact(fields),
|
||||
data_count_bit: BIT_COUNT,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a KAT generic button command to a Honda Static 4-bit button code.
|
||||
/// Honda button codes: Lock=1, Unlock=2, Trunk=4, Remote Start=5, Panic=8, Lock x2=9
|
||||
/// (see honda_static_button_names + honda_static_is_valid_button).
|
||||
fn map_button(button: u8) -> u8 {
|
||||
match button {
|
||||
BTN_LOCK => 1,
|
||||
BTN_UNLOCK => 2,
|
||||
BTN_TRUNK => 4,
|
||||
BTN_PANIC => 8,
|
||||
// Already a valid Honda code → pass through.
|
||||
b if Self::is_valid_button(b) => b,
|
||||
// honda_static_encoder_remap_button for codes 2..=5.
|
||||
b if (2..=5).contains(&b) => ENCODER_BUTTON_MAP[(b - 2) as usize],
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
||||
if let Some(last) = signal.last_mut() {
|
||||
if last.level == level {
|
||||
*last = LevelDuration::new(level, last.duration_us + duration);
|
||||
return;
|
||||
}
|
||||
}
|
||||
signal.push(LevelDuration::new(level, duration));
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for HondaStaticDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Honda Static"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: BIT_COUNT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.symbols.clear();
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
let sym = level;
|
||||
|
||||
// Short pulse → one symbol (matches the SHORT range check in the C feed).
|
||||
if duration >= SHORT_BASE_US && (duration - SHORT_BASE_US) <= SHORT_SPAN_US {
|
||||
if self.symbols.len() < SYMBOL_CAPACITY {
|
||||
self.symbols.push(sym);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Long pulse → two symbols (same level).
|
||||
if duration >= LONG_BASE_US && (duration - LONG_BASE_US) <= LONG_SPAN_US {
|
||||
if self.symbols.len() + 2 <= SYMBOL_CAPACITY {
|
||||
self.symbols.push(sym);
|
||||
self.symbols.push(sym);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Out-of-range pulse (sync 700µs or a gap): try to parse the buffered symbols, then reset.
|
||||
// Matches the C feed: parse with inverted=true first, then inverted=false.
|
||||
let mut result = None;
|
||||
if self.symbols.len() >= MIN_SYMBOLS {
|
||||
let parsed = Self::parse_symbols(&self.symbols, true)
|
||||
.or_else(|| Self::parse_symbols(&self.symbols, false));
|
||||
if let Some((fields, crc_valid)) = parsed {
|
||||
// Faithful to the C: both the forward (checksum-validated) and reverse
|
||||
// (button+serial-validated) packets commit a decode. The strong button/serial
|
||||
// gates keep this from false-matching (verified: zero false matches across the
|
||||
// IMPORTS sweep). `crc_valid` reflects whether the checksum-validated forward path
|
||||
// matched (true) vs. the reverse path (false).
|
||||
result = Some(Self::build_signal(&fields, crc_valid));
|
||||
}
|
||||
}
|
||||
|
||||
self.symbols.clear();
|
||||
result
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
if !Self::is_valid_serial(serial) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let fields = HondaStaticFields {
|
||||
button: Self::map_button(button),
|
||||
serial,
|
||||
counter: decoded.counter.unwrap_or(0) as u32 & 0x00FF_FFFF,
|
||||
checksum: 0,
|
||||
};
|
||||
let packet = Self::build_packet_bytes(&fields);
|
||||
|
||||
// Matches honda_static_build_upload.
|
||||
let mut signal =
|
||||
Vec::with_capacity(1 + PREAMBLE_ALTERNATING_COUNT + 2 * BIT_COUNT + 1);
|
||||
|
||||
// Sync: HIGH 700µs.
|
||||
Self::enc_add_level(&mut signal, true, SYNC_TIME_US);
|
||||
|
||||
// Alternating preamble: 160 elements at 63µs, level = (i & 1) (starts LOW).
|
||||
for i in 0..PREAMBLE_ALTERNATING_COUNT {
|
||||
Self::enc_add_level(&mut signal, (i & 1) != 0, ELEMENT_TIME_US);
|
||||
}
|
||||
|
||||
// Data, MSB-first: bit → (!value 63µs, value 63µs).
|
||||
for bit in 0..BIT_COUNT {
|
||||
let shift = (!bit) & 0x07;
|
||||
let value = ((packet[bit >> 3] >> shift) & 1) != 0;
|
||||
Self::enc_add_level(&mut signal, !value, ELEMENT_TIME_US);
|
||||
Self::enc_add_level(&mut signal, value, ELEMENT_TIME_US);
|
||||
}
|
||||
|
||||
// Trailing sync: !last_bit for 700µs.
|
||||
let last_bit = (packet[7] & 1) != 0;
|
||||
Self::enc_add_level(&mut signal, !last_bit, SYNC_TIME_US);
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HondaStaticDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Round-trip: encode a known frame, feed it back through the decoder, and confirm the fields
|
||||
/// survive. The encoder emits the inverted-Manchester interpretation, which the decoder tries
|
||||
/// first.
|
||||
#[test]
|
||||
fn honda_static_encode_decode_roundtrip() {
|
||||
let serial = 0x0123456u32 & 0x0FFF_FFFF;
|
||||
let counter = 0x00AB12u32;
|
||||
let original = DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(BTN_UNLOCK),
|
||||
counter: Some(counter as u16),
|
||||
crc_valid: true,
|
||||
data: 0,
|
||||
data_count_bit: BIT_COUNT,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
};
|
||||
|
||||
let decoder = HondaStaticDecoder::new();
|
||||
let signal = decoder.encode(&original, BTN_UNLOCK).expect("encode");
|
||||
|
||||
// Feed the encoded pulses, plus a terminating gap to flush the symbol buffer.
|
||||
let mut dec = HondaStaticDecoder::new();
|
||||
let mut decoded = None;
|
||||
for ld in &signal {
|
||||
if let Some(d) = dec.feed(ld.level, ld.duration_us) {
|
||||
decoded = Some(d);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if decoded.is_none() {
|
||||
// Terminating out-of-range pulse to flush.
|
||||
decoded = dec.feed(false, 5000);
|
||||
}
|
||||
|
||||
let decoded = decoded.expect("expected a decode from the round-tripped frame");
|
||||
assert!(decoded.crc_valid, "checksum should validate");
|
||||
assert_eq!(decoded.serial, Some(serial), "serial mismatch");
|
||||
assert_eq!(decoded.button, Some(2), "button should map to Honda Unlock=2");
|
||||
assert_eq!(
|
||||
decoded.counter,
|
||||
Some(counter as u16),
|
||||
"counter (low 16 bits) mismatch"
|
||||
);
|
||||
}
|
||||
|
||||
/// The packed Key word must reproduce the C compact layout for a known field set.
|
||||
#[test]
|
||||
fn honda_static_pack_compact_layout() {
|
||||
let fields = HondaStaticFields {
|
||||
button: 0x02,
|
||||
serial: 0x0ABCDEF,
|
||||
counter: 0x123456,
|
||||
checksum: 0,
|
||||
};
|
||||
let data = HondaStaticDecoder::pack_compact(&fields);
|
||||
let bytes = data.to_be_bytes();
|
||||
// compact[0] = button & 0x0F
|
||||
assert_eq!(bytes[0], 0x02);
|
||||
// serial nibbles: >>20, >>12, >>4, <<4
|
||||
assert_eq!(bytes[1], (0x0ABCDEFu32 >> 20) as u8);
|
||||
assert_eq!(bytes[2], (0x0ABCDEFu32 >> 12) as u8);
|
||||
assert_eq!(bytes[3], (0x0ABCDEFu32 >> 4) as u8);
|
||||
assert_eq!(bytes[4], (0x0ABCDEFu32 << 4) as u8);
|
||||
// counter bytes
|
||||
assert_eq!(bytes[5], 0x12);
|
||||
assert_eq!(bytes[6], 0x34);
|
||||
assert_eq!(bytes[7], 0x56);
|
||||
}
|
||||
|
||||
/// build_packet_bytes + validate_forward_packet must round-trip the fields, and the checksum
|
||||
/// must validate.
|
||||
#[test]
|
||||
fn honda_static_packet_validate_roundtrip() {
|
||||
let fields = HondaStaticFields {
|
||||
button: 0x05,
|
||||
serial: 0x0FEDCBA,
|
||||
counter: 0x00FF01,
|
||||
checksum: 0,
|
||||
};
|
||||
let packet = HondaStaticDecoder::build_packet_bytes(&fields);
|
||||
let validated = HondaStaticDecoder::validate_forward_packet(&packet)
|
||||
.expect("forward packet should validate");
|
||||
assert_eq!(validated.button, 0x05);
|
||||
assert_eq!(validated.serial, 0x0FEDCBA);
|
||||
assert_eq!(validated.counter, 0x00FF01);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
//! Honda V1 protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/honda_v1.c` and
|
||||
//! `honda_v1.h`. Honda/Acura fixed-code keyfobs, a DIFFERENT protocol from `honda_static`.
|
||||
//! AM (OOK), 315 MHz + 433.92 MHz. The on-wire frame carries 68 bits: 64-bit data + a 4-bit
|
||||
//! CRC-fold nibble.
|
||||
//!
|
||||
//! **Encoding**: short/long pulse PWM. te_short=1000µs, te_long=2000µs, te_delta=400µs,
|
||||
//! te_end=3500µs, te_short_min=600µs. Each on-wire symbol is one pulse whose width selects 0/1,
|
||||
//! but the demodulated stream is glued together by a "pending bit" timing accumulator (see `feed`)
|
||||
//! before being classified.
|
||||
//!
|
||||
//! **Pending-bit accumulation** (matches `subghz_protocol_decoder_honda_v1_feed`): sub-`te_delta`
|
||||
//! runts are summed into `pending`; a HIGH level keeps extending the running HIGH pulse; a LOW
|
||||
//! level flushes the accumulated HIGH pulse (if it reached `te_short_min`) as a synthetic symbol,
|
||||
//! then the LOW pulse itself is classified. The symbol layer (`honda_v1_symbol`) walks a
|
||||
//! Reset→Preamble→Data state machine; in the Data step a short pulse toggles a `data_pending` flag
|
||||
//! and, when paired, emits the level as a bit, while a long pulse emits directly — this is the
|
||||
//! exact pending-bit logic ported from the C.
|
||||
//!
|
||||
//! **Frame / fields**: after the end gap (>te_end), `commit` requires ≥68 collected bits, then
|
||||
//! left-shifts the 12-byte bit buffer by `max(1, bit_count-68)` to drop leading preamble leakage
|
||||
//! and align the trailing frame. `data` = first 8 bytes (64 bits), `k2` (CRC nibble) = byte 8's
|
||||
//! high nibble. Fields (matches `honda_v1_decode_fields`): serial = data[63:36] (28b),
|
||||
//! button = data[31:28] (nibble), counter = data[15:0] (16b).
|
||||
//!
|
||||
//! **Validation / gating**: a button-code table (Unlock=0, Lock=8, Trunk=9, Panic=10) plus a
|
||||
//! CRC-fold checksum (`honda_v1_checksum*`). Emission is gated on the button being valid (matches
|
||||
//! the C `commit`); `crc_valid` reflects whether the received CRC nibble matches either wire-order
|
||||
//! checksum (`honda_v1_crc_valid`). The strong button gate keeps Honda V1 from false-matching.
|
||||
//!
|
||||
//! **Encoder** (matches `honda_v1_build_upload` / `honda_v1_append_frame`, behind
|
||||
//! `ENABLE_EMULATE_FEATURE`): builds the 64-bit key from serial/button/counter via the button
|
||||
//! table, then emits a 180-element short-pair preamble + 4 PWM frames (2 per checksum wire value).
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const BIT_COUNT: usize = 68;
|
||||
const TE_SHORT: u32 = 1000;
|
||||
const TE_LONG: u32 = 2000;
|
||||
const TE_DELTA: u32 = 400;
|
||||
const TE_SHORT_MIN: u32 = 600;
|
||||
const TE_END: u32 = 3500;
|
||||
const VALID_MAX: u8 = 0x4B; // honda_v1_add_bit cap (75)
|
||||
const NIBBLE_MASK: u8 = 0x0F;
|
||||
const SERIAL_MASK: u32 = 0x0FFF_FFFF;
|
||||
const COUNTER_MASK: u16 = 0xFFFF;
|
||||
const BUTTON_MAX: u8 = 10;
|
||||
const BUTTON_VALID_MASK: u16 = 0x701; // bits set for Unlock(0), Lock(8), Trunk(9), Panic(10)
|
||||
const DECODE_BUFFER_BYTES: usize = 12;
|
||||
|
||||
// Encoder constants (honda_v1.c, ENABLE_EMULATE_FEATURE).
|
||||
const PREAMBLE_UPLOAD_COUNT: usize = 180;
|
||||
const FRAME_SYMBOLS: usize = 80;
|
||||
const FRAME_START: usize = 12;
|
||||
const FRAME_SYNC_DROP: usize = 2;
|
||||
const FRAME_REPEAT_PER_CRC: usize = 2;
|
||||
const FRAME_GAP_US: u32 = 5000;
|
||||
const FRAME_CRC_INDEX: usize = 8;
|
||||
|
||||
// HondaV1Button codes (the on-wire button nibble at data[31:28]).
|
||||
const BTN_CODE_UNLOCK: u8 = 0;
|
||||
const BTN_CODE_LOCK: u8 = 8;
|
||||
const BTN_CODE_TRUNK: u8 = 9;
|
||||
const BTN_CODE_PANIC: u8 = 10;
|
||||
|
||||
// honda_v1_button_codes[] (24-bit table values, used to rebuild the key in the encoder).
|
||||
const BUTTON_CODE_UNLOCK: u32 = 0x0008_0808;
|
||||
const BUTTON_CODE_LOCK: u32 = 0x0008_8888;
|
||||
const BUTTON_CODE_TRUNK: u32 = 0x0009_9190;
|
||||
const BUTTON_CODE_PANIC: u32 = 0x000F_A7A0;
|
||||
const BUTTON_FALLBACK_CODE: u32 = 0x0008_8888;
|
||||
|
||||
// KAT generic button codes.
|
||||
const BTN_LOCK: u8 = 0x01;
|
||||
const BTN_UNLOCK: u8 = 0x02;
|
||||
const BTN_TRUNK: u8 = 0x04;
|
||||
const BTN_PANIC: u8 = 0x08;
|
||||
|
||||
/// Decoder steps (matches HondaV1DecoderStep).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble,
|
||||
Data,
|
||||
}
|
||||
|
||||
/// Honda V1 decoder (matches SubGhzProtocolDecoderHondaV1).
|
||||
pub struct HondaV1Decoder {
|
||||
step: DecoderStep,
|
||||
preamble_count: u8,
|
||||
preamble_has_long: bool,
|
||||
data_pending: bool,
|
||||
last_level: bool,
|
||||
bits: [u8; DECODE_BUFFER_BYTES],
|
||||
bit_count: u8,
|
||||
// Pending-bit timing accumulator (decoder-level, persists across symbol resets).
|
||||
pending: u32,
|
||||
pending_valid: bool,
|
||||
}
|
||||
|
||||
impl HondaV1Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
preamble_count: 0,
|
||||
preamble_has_long: false,
|
||||
data_pending: false,
|
||||
last_level: false,
|
||||
bits: [0u8; DECODE_BUFFER_BYTES],
|
||||
bit_count: 0,
|
||||
pending: 0,
|
||||
pending_valid: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// honda_v1_button_valid: button <= 10 && ((0x701 >> button) & 1).
|
||||
fn button_valid(b: u8) -> bool {
|
||||
if b > BUTTON_MAX {
|
||||
return false;
|
||||
}
|
||||
((BUTTON_VALID_MASK >> b) & 1) != 0
|
||||
}
|
||||
|
||||
/// honda_v1_duration_is: |d - t| <= te_delta (saturating both directions).
|
||||
fn duration_is(d: u32, t: u32) -> bool {
|
||||
if d >= t {
|
||||
(d - t) <= TE_DELTA
|
||||
} else {
|
||||
(t - d) <= TE_DELTA
|
||||
}
|
||||
}
|
||||
|
||||
/// honda_v1_crc_fold.
|
||||
fn crc_fold(v: u16) -> u8 {
|
||||
let lo = (v & (NIBBLE_MASK as u16)) as u8;
|
||||
let hi = v >> 4;
|
||||
let s: i32 = if (hi & 1) != 0 {
|
||||
lo as i32
|
||||
} else {
|
||||
-(lo as i32)
|
||||
};
|
||||
let mut out = ((s - (hi as i32)) & 7) as u8;
|
||||
out |= (((v >> 3) & 1) as u8) << 3;
|
||||
if ((v >> 1) & 1) != 0 && (((v >> 4) ^ (v >> 5)) & 1) != 0 {
|
||||
out ^= 0x04;
|
||||
}
|
||||
out & NIBBLE_MASK
|
||||
}
|
||||
|
||||
/// honda_v1_checksum_base.
|
||||
fn checksum_base(data: u64) -> u8 {
|
||||
let a = Self::crc_fold((data & (COUNTER_MASK as u64)) as u16);
|
||||
let b = Self::crc_fold(((data >> 40) & 0xFF) as u16);
|
||||
(a ^ b ^ 1) & NIBBLE_MASK
|
||||
}
|
||||
|
||||
/// honda_v1_checksum_alternate.
|
||||
fn checksum_alternate(checksum: u8) -> u8 {
|
||||
let mut mask = 0x09u8;
|
||||
if (checksum & 1) == 0 {
|
||||
mask = if (checksum & 2) != 0 { 0x0B } else { NIBBLE_MASK };
|
||||
}
|
||||
(checksum ^ mask) & NIBBLE_MASK
|
||||
}
|
||||
|
||||
/// honda_v1_checksum_wire_order → (first, second).
|
||||
fn checksum_wire_order(data: u64) -> (u8, u8) {
|
||||
let checksum = Self::checksum_base(data);
|
||||
let other = Self::checksum_alternate(checksum);
|
||||
if (checksum & 0x08) != 0 {
|
||||
(other, checksum)
|
||||
} else {
|
||||
(checksum, other)
|
||||
}
|
||||
}
|
||||
|
||||
/// honda_v1_crc_valid: received nibble matches either wire-order checksum.
|
||||
fn crc_valid(data: u64, crc: u8) -> bool {
|
||||
let (first, second) = Self::checksum_wire_order(data);
|
||||
let crc = crc & NIBBLE_MASK;
|
||||
crc == first || crc == second
|
||||
}
|
||||
|
||||
/// honda_v1_decode_fields. Returns (serial, button, counter).
|
||||
fn decode_fields(data: u64) -> (u32, u8, u16) {
|
||||
let low = (data & 0xFFFF_FFFF) as u32;
|
||||
let serial = ((data >> 36) & (SERIAL_MASK as u64)) as u32;
|
||||
let button = ((low >> 28) & (NIBBLE_MASK as u32)) as u8;
|
||||
let counter = (low & (COUNTER_MASK as u32)) as u16;
|
||||
(serial, button, counter)
|
||||
}
|
||||
|
||||
/// honda_v1_button_code (encoder).
|
||||
fn button_code(button: u8) -> u32 {
|
||||
if !Self::button_valid(button) {
|
||||
return BUTTON_FALLBACK_CODE;
|
||||
}
|
||||
match button {
|
||||
BTN_CODE_UNLOCK => BUTTON_CODE_UNLOCK,
|
||||
BTN_CODE_LOCK => BUTTON_CODE_LOCK,
|
||||
BTN_CODE_TRUNK => BUTTON_CODE_TRUNK,
|
||||
BTN_CODE_PANIC => BUTTON_CODE_PANIC,
|
||||
_ => BUTTON_FALLBACK_CODE,
|
||||
}
|
||||
}
|
||||
|
||||
/// honda_v1_build_key.
|
||||
fn build_key(serial: u32, button: u8, counter: u16) -> u64 {
|
||||
let table = Self::button_code(button);
|
||||
let low = ((table & (COUNTER_MASK as u32)) << 16) | (counter as u32);
|
||||
let high = ((serial & SERIAL_MASK) << 4) | (table >> 16);
|
||||
((high as u64) << 32) | (low as u64)
|
||||
}
|
||||
|
||||
/// Map a KAT generic button command to a Honda V1 button code (nibble at data[31:28]).
|
||||
fn map_button(button: u8) -> u8 {
|
||||
match button {
|
||||
BTN_LOCK => BTN_CODE_LOCK,
|
||||
BTN_UNLOCK => BTN_CODE_UNLOCK,
|
||||
BTN_TRUNK => BTN_CODE_TRUNK,
|
||||
BTN_PANIC => BTN_CODE_PANIC,
|
||||
// Already a valid Honda V1 code → pass through.
|
||||
b if Self::button_valid(b) => b,
|
||||
_ => BTN_CODE_UNLOCK,
|
||||
}
|
||||
}
|
||||
|
||||
/// honda_v1_state_reset (symbol-layer state only; does NOT touch pending accumulator).
|
||||
fn state_reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.preamble_count = 0;
|
||||
self.preamble_has_long = false;
|
||||
self.data_pending = false;
|
||||
self.last_level = false;
|
||||
self.bit_count = 0;
|
||||
self.bits = [0u8; DECODE_BUFFER_BYTES];
|
||||
}
|
||||
|
||||
/// honda_v1_add_bit: MSB-first into bits[], capped at VALID_MAX.
|
||||
fn add_bit(&mut self, bit: bool) {
|
||||
if self.bit_count > VALID_MAX {
|
||||
return;
|
||||
}
|
||||
if bit {
|
||||
let byte = (self.bit_count >> 3) as usize;
|
||||
let shift = (!self.bit_count) & 0x07;
|
||||
self.bits[byte] |= 1u8 << shift;
|
||||
}
|
||||
self.bit_count += 1;
|
||||
}
|
||||
|
||||
/// honda_v1_commit: align trailing 68-bit frame, validate button, return signal on success.
|
||||
fn commit(&mut self) -> Option<DecodedSignal> {
|
||||
if (self.bit_count as usize) < BIT_COUNT {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut aligned = self.bits;
|
||||
|
||||
let mut shift_count = self.bit_count - BIT_COUNT as u8;
|
||||
if shift_count < 1 {
|
||||
shift_count = 1;
|
||||
}
|
||||
|
||||
for _ in 0..shift_count {
|
||||
for i in 0..(DECODE_BUFFER_BYTES - 1) {
|
||||
aligned[i] = (aligned[i] << 1) | (aligned[i + 1] >> 7);
|
||||
}
|
||||
aligned[DECODE_BUFFER_BYTES - 1] <<= 1;
|
||||
}
|
||||
|
||||
let button = aligned[4] >> 4;
|
||||
if !Self::button_valid(button) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let data = u64::from_be_bytes(aligned[0..8].try_into().unwrap());
|
||||
let k2 = aligned[8] >> 4;
|
||||
let (serial, btn, counter) = Self::decode_fields(data);
|
||||
let crc_valid = Self::crc_valid(data, k2);
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: Some(counter),
|
||||
crc_valid,
|
||||
data,
|
||||
data_count_bit: BIT_COUNT,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// honda_v1_symbol: classify a (level, duration) pulse and drive the state machine.
|
||||
/// Returns Some(signal) when the end-gap path commits a frame.
|
||||
fn symbol(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
let sh = Self::duration_is(duration, TE_SHORT);
|
||||
let lg = Self::duration_is(duration, TE_LONG);
|
||||
|
||||
if !sh && !lg {
|
||||
let mut result = None;
|
||||
if !level && duration > TE_END && self.step == DecoderStep::Data {
|
||||
result = self.commit();
|
||||
}
|
||||
self.state_reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
if self.step == DecoderStep::Reset {
|
||||
if level {
|
||||
self.step = DecoderStep::Preamble;
|
||||
self.preamble_count = 1;
|
||||
self.last_level = level;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.step == DecoderStep::Preamble {
|
||||
if lg {
|
||||
// honda_v1.c: if(preamble_count < 0xFF) preamble_count++ (saturating).
|
||||
self.preamble_count = self.preamble_count.saturating_add(1);
|
||||
self.preamble_has_long = true;
|
||||
self.last_level = level;
|
||||
return None;
|
||||
}
|
||||
|
||||
if sh {
|
||||
if self.preamble_has_long && self.preamble_count > 5 {
|
||||
self.step = DecoderStep::Data;
|
||||
self.bit_count = 0;
|
||||
self.bits = [0u8; DECODE_BUFFER_BYTES];
|
||||
self.data_pending = true;
|
||||
self.last_level = level;
|
||||
return None;
|
||||
}
|
||||
|
||||
self.preamble_count = self.preamble_count.saturating_add(1);
|
||||
self.last_level = level;
|
||||
return None;
|
||||
}
|
||||
|
||||
self.state_reset();
|
||||
return None;
|
||||
}
|
||||
|
||||
// Data step: pending-bit accumulation.
|
||||
if sh {
|
||||
if self.data_pending {
|
||||
self.add_bit(level);
|
||||
self.data_pending = false;
|
||||
self.last_level = level;
|
||||
} else {
|
||||
self.data_pending = true;
|
||||
self.last_level = level;
|
||||
}
|
||||
} else {
|
||||
// long pulse
|
||||
if self.data_pending {
|
||||
self.add_bit(level);
|
||||
} else {
|
||||
self.add_bit(self.last_level);
|
||||
}
|
||||
self.last_level = level;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
||||
if let Some(last) = signal.last_mut() {
|
||||
if last.level == level {
|
||||
*last = LevelDuration::new(level, last.duration_us + duration);
|
||||
return;
|
||||
}
|
||||
}
|
||||
signal.push(LevelDuration::new(level, duration));
|
||||
}
|
||||
|
||||
/// honda_v1_append_frame: emit one PWM frame (12-symbol sync header + 68 data symbols),
|
||||
/// dropping the first FRAME_SYNC_DROP entries, then a tail + inter-frame gap.
|
||||
fn append_frame(signal: &mut Vec<LevelDuration>, frame: &[u8; 9]) {
|
||||
// Build the per-frame symbol stream with merge semantics (pp_emit_merge).
|
||||
let mut generated: Vec<LevelDuration> = Vec::with_capacity(FRAME_SYMBOLS * 2);
|
||||
for bit_index in 0..FRAME_SYMBOLS {
|
||||
let bit = if bit_index >= FRAME_START {
|
||||
let data_index = (bit_index - FRAME_START) >> 3;
|
||||
let shift = (11i32 - bit_index as i32) & 0x07;
|
||||
((frame[data_index] >> shift) & 0x01) != 0
|
||||
} else {
|
||||
((!bit_index) & 0x01) != 0
|
||||
};
|
||||
// bit -> (level=bit, te)(level=!bit, te), merged.
|
||||
Self::enc_add_level(&mut generated, bit, TE_SHORT);
|
||||
Self::enc_add_level(&mut generated, !bit, TE_SHORT);
|
||||
}
|
||||
|
||||
if generated.len() <= FRAME_SYNC_DROP {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy generated[FRAME_SYNC_DROP..] into the upload (still merging at the seam).
|
||||
for ld in &generated[FRAME_SYNC_DROP..] {
|
||||
Self::enc_add_level(signal, ld.level, ld.duration_us);
|
||||
}
|
||||
|
||||
// Tail: !last_level for te_short; if that was low, add a high te_short; then the gap.
|
||||
let last_level = signal.last().map(|l| l.level).unwrap_or(false);
|
||||
let tail_level = !last_level;
|
||||
Self::enc_add_level(signal, tail_level, TE_SHORT);
|
||||
if !tail_level {
|
||||
Self::enc_add_level(signal, true, TE_SHORT);
|
||||
}
|
||||
Self::enc_add_level(signal, false, FRAME_GAP_US);
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for HondaV1Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Honda V1"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: BIT_COUNT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.pending = 0;
|
||||
self.pending_valid = false;
|
||||
self.state_reset();
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
// Pending-bit timing accumulation (subghz_protocol_decoder_honda_v1_feed).
|
||||
if duration < TE_DELTA {
|
||||
self.pending = self.pending.saturating_add(duration);
|
||||
self.pending_valid = true;
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut result = None;
|
||||
|
||||
if self.pending_valid {
|
||||
let p = self.pending;
|
||||
if level {
|
||||
self.pending = p.saturating_add(duration);
|
||||
self.pending_valid = true;
|
||||
return None;
|
||||
}
|
||||
if p >= TE_SHORT_MIN {
|
||||
result = self.symbol(true, p);
|
||||
}
|
||||
self.pending = 0;
|
||||
self.pending_valid = false;
|
||||
}
|
||||
|
||||
if level {
|
||||
self.pending = duration;
|
||||
self.pending_valid = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// A symbol committed on the flushed HIGH pulse takes priority (the C calls back there);
|
||||
// otherwise classify this LOW pulse.
|
||||
let low_result = self.symbol(false, duration);
|
||||
result.or(low_result)
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0) & COUNTER_MASK;
|
||||
let btn = Self::map_button(button);
|
||||
let data = Self::build_key(serial & SERIAL_MASK, btn, counter);
|
||||
|
||||
let mut frame = [0u8; 9];
|
||||
frame[0..8].copy_from_slice(&data.to_be_bytes());
|
||||
let (first, second) = Self::checksum_wire_order(data);
|
||||
|
||||
let mut signal: Vec<LevelDuration> = Vec::with_capacity(PREAMBLE_UPLOAD_COUNT + 8 * FRAME_SYMBOLS);
|
||||
|
||||
// Preamble: 180 short entries (90 H/L pairs); the final LOW becomes a 5000µs gap.
|
||||
for _ in 0..(PREAMBLE_UPLOAD_COUNT / 2) {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
if let Some(last) = signal.last_mut() {
|
||||
*last = LevelDuration::new(false, FRAME_GAP_US);
|
||||
}
|
||||
|
||||
// 4 frames: 2 per checksum wire value (first, then second).
|
||||
for &crc in &[first, second] {
|
||||
frame[FRAME_CRC_INDEX] = crc << 4;
|
||||
for _ in 0..FRAME_REPEAT_PER_CRC {
|
||||
Self::append_frame(&mut signal, &frame);
|
||||
}
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HondaV1Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// build_key + decode_fields must round-trip the fields, and the encoder's CRC nibble must
|
||||
/// validate against honda_v1_crc_valid.
|
||||
#[test]
|
||||
fn honda_v1_key_field_roundtrip() {
|
||||
let serial = 0x0ABCDEFu32 & SERIAL_MASK;
|
||||
let counter = 0x1234u16;
|
||||
let data = HondaV1Decoder::build_key(serial, BTN_CODE_TRUNK, counter);
|
||||
let (s, b, c) = HondaV1Decoder::decode_fields(data);
|
||||
assert_eq!(s, serial, "serial mismatch");
|
||||
assert_eq!(b, BTN_CODE_TRUNK, "button mismatch");
|
||||
assert_eq!(c, counter, "counter mismatch");
|
||||
|
||||
let (first, _second) = HondaV1Decoder::checksum_wire_order(data);
|
||||
assert!(
|
||||
HondaV1Decoder::crc_valid(data, first),
|
||||
"wire-order checksum should validate"
|
||||
);
|
||||
}
|
||||
|
||||
/// Full encode → decode round-trip: emit a frame for known fields, feed the pulses back through
|
||||
/// the decoder (the pending-bit accumulator + symbol state machine), and confirm the fields and
|
||||
/// checksum survive.
|
||||
#[test]
|
||||
fn honda_v1_encode_decode_roundtrip() {
|
||||
let serial = 0x0123456u32 & SERIAL_MASK;
|
||||
let counter = 0x00ABu16;
|
||||
let original = DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(BTN_UNLOCK),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data: 0,
|
||||
data_count_bit: BIT_COUNT,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
};
|
||||
|
||||
let decoder = HondaV1Decoder::new();
|
||||
let signal = decoder.encode(&original, BTN_UNLOCK).expect("encode");
|
||||
|
||||
let mut dec = HondaV1Decoder::new();
|
||||
let mut decoded = None;
|
||||
for ld in &signal {
|
||||
if let Some(d) = dec.feed(ld.level, ld.duration_us) {
|
||||
decoded = Some(d);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Flush with a terminating end-gap if the inter-frame gaps didn't already commit.
|
||||
if decoded.is_none() {
|
||||
decoded = dec.feed(false, TE_END + 1000);
|
||||
}
|
||||
|
||||
let decoded = decoded.expect("expected a decode from the round-tripped frame");
|
||||
assert!(decoded.crc_valid, "checksum should validate");
|
||||
assert_eq!(decoded.serial, Some(serial), "serial mismatch");
|
||||
assert_eq!(
|
||||
decoded.button,
|
||||
Some(BTN_CODE_UNLOCK),
|
||||
"button should map to Honda V1 Unlock=0"
|
||||
);
|
||||
assert_eq!(decoded.counter, Some(counter), "counter mismatch");
|
||||
assert_eq!(decoded.data_count_bit, BIT_COUNT);
|
||||
}
|
||||
|
||||
/// Button validity mask must match honda_v1_button_valid (0x701 → Unlock/Lock/Trunk/Panic).
|
||||
#[test]
|
||||
fn honda_v1_button_validity() {
|
||||
assert!(HondaV1Decoder::button_valid(BTN_CODE_UNLOCK));
|
||||
assert!(HondaV1Decoder::button_valid(BTN_CODE_LOCK));
|
||||
assert!(HondaV1Decoder::button_valid(BTN_CODE_TRUNK));
|
||||
assert!(HondaV1Decoder::button_valid(BTN_CODE_PANIC));
|
||||
// Invalid codes.
|
||||
assert!(!HondaV1Decoder::button_valid(1));
|
||||
assert!(!HondaV1Decoder::button_valid(7));
|
||||
assert!(!HondaV1Decoder::button_valid(11));
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,9 @@ fn g5(x: u32, a: u32, b: u32, c: u32, d: u32, e: u32) -> u32 {
|
||||
pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
|
||||
let mut x = data;
|
||||
for r in 0..528u32 {
|
||||
let key_bit = ((key >> ((15 - r) & 63)) & 1) as u32;
|
||||
// (15 - r) relies on unsigned wraparound (matches the C reference `(15 - r) & 63`);
|
||||
// use wrapping_sub so debug builds don't panic on the overflow that release silently wraps.
|
||||
let key_bit = ((key >> ((15u32.wrapping_sub(r)) & 63)) & 1) as u32;
|
||||
let new_lsb = bit(x, 31) ^ bit(x, 15) ^ key_bit
|
||||
^ bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30));
|
||||
x = (x << 1) ^ new_lsb;
|
||||
|
||||
+19
-29
@@ -7,7 +7,7 @@
|
||||
//! - Manchester encoding: 800/1600µs timing
|
||||
//! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC)
|
||||
//! - Long preamble of ~90 long pairs
|
||||
//! - CRC4 checksum with offset rules (cnt_high 0 / >= 6)
|
||||
//! - CRC4 checksum (XOR of nibbles + offset 1, 7 bytes including cnt_high)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
@@ -66,13 +66,14 @@ impl KiaV1Decoder {
|
||||
(crc.wrapping_add(offset)) & 0x0F
|
||||
}
|
||||
|
||||
/// Manchester state machine
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, false) => 0, // Short Low
|
||||
(true, true) => 1, // Short High
|
||||
(false, false) => 2, // Long Low
|
||||
(false, true) => 3, // Long High
|
||||
/// Manchester state machine (Flipper convention: level ? ShortLow : ShortHigh)
|
||||
fn manchester_advance(&mut self, is_short: bool, level: bool) -> Option<bool> {
|
||||
// C: event = level ? ManchesterEventShortLow : ManchesterEventShortHigh (inverted)
|
||||
let event = match (is_short, level) {
|
||||
(true, true) => 0, // level=true → ShortLow
|
||||
(true, false) => 1, // level=false → ShortHigh
|
||||
(false, true) => 2, // level=true → LongLow
|
||||
(false, false) => 3, // level=false → LongHigh
|
||||
};
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
@@ -115,15 +116,9 @@ impl KiaV1Decoder {
|
||||
char_data[4] = button;
|
||||
char_data[5] = (counter & 0xFF) as u8;
|
||||
|
||||
let crc = if cnt_high == 0 {
|
||||
let offset = if counter >= 0x098 { button } else { 1 };
|
||||
Self::crc4(&char_data[..6], offset)
|
||||
} else if cnt_high >= 0x6 {
|
||||
char_data[6] = cnt_high as u8;
|
||||
Self::crc4(&char_data, 1)
|
||||
} else {
|
||||
Self::crc4(&char_data[..6], 1)
|
||||
};
|
||||
// CRC4: always 7 bytes with offset 1 (matches updated ProtoPirate kia_v1.c)
|
||||
char_data[6] = cnt_high as u8;
|
||||
let crc = Self::crc4(&char_data, 1);
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
@@ -194,8 +189,9 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
|
||||
if self.header_count > 70 {
|
||||
if !level && is_short && duration_diff!(self.te_last, TE_LONG) < TE_DELTA {
|
||||
self.decode_count_bit = 1;
|
||||
self.decode_data = 1; // Add first bit
|
||||
// C: decode_count_bit=1, then add_bit(1) increments to 2
|
||||
self.decode_count_bit = 2;
|
||||
self.decode_data = 1;
|
||||
self.header_count = 0;
|
||||
self.step = DecoderStep::DecodeData;
|
||||
}
|
||||
@@ -218,7 +214,7 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.decode_count_bit >= MIN_COUNT_BIT {
|
||||
if self.decode_count_bit == MIN_COUNT_BIT {
|
||||
let result = self.parse_data();
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
@@ -247,15 +243,9 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
char_data[4] = button;
|
||||
char_data[5] = (counter & 0xFF) as u8;
|
||||
|
||||
let crc = if cnt_high == 0 {
|
||||
let offset = if counter >= 0x098 { button } else { 1 };
|
||||
Self::crc4(&char_data[..6], offset)
|
||||
} else if cnt_high >= 0x6 {
|
||||
char_data[6] = cnt_high;
|
||||
Self::crc4(&char_data, 1)
|
||||
} else {
|
||||
Self::crc4(&char_data[..6], 1)
|
||||
};
|
||||
// CRC4: always 7 bytes with offset 1 (matches updated ProtoPirate kia_v1.c)
|
||||
char_data[6] = cnt_high;
|
||||
let crc = Self::crc4(&char_data, 1);
|
||||
|
||||
// Build data
|
||||
let data: u64 = ((serial as u64) << 24) |
|
||||
|
||||
+16
-15
@@ -57,18 +57,17 @@ impl KiaV2Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC4 for Kia V2 (matches kia_v2.c: 6-byte permuted input, XOR nibbles, offset 1)
|
||||
/// CRC4 for Kia V2 (matches kia_v2.c: strip CRC nibble, XOR all remaining nibbles, + offset 1)
|
||||
fn calculate_crc(data: u64) -> u8 {
|
||||
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
||||
let u_var4 = (data & 0xFFFFFFFF) as u32;
|
||||
|
||||
// C code: data_without_crc = data >> 4; then read 6 sequential bytes (bits 4..51)
|
||||
let data_without_crc = data >> 4;
|
||||
let mut bytes = [0u8; 6];
|
||||
bytes[0] = (u_var4 >> 20) as u8;
|
||||
bytes[1] = ((u_var4 >> 28) | ((serial & 0x0F) << 4)) as u8;
|
||||
bytes[2] = (serial >> 4) as u8;
|
||||
bytes[3] = (serial >> 12) as u8;
|
||||
bytes[4] = (u_var4 >> 4) as u8;
|
||||
bytes[5] = (u_var4 >> 12) as u8;
|
||||
bytes[0] = (data_without_crc & 0xFF) as u8;
|
||||
bytes[1] = ((data_without_crc >> 8) & 0xFF) as u8;
|
||||
bytes[2] = ((data_without_crc >> 16) & 0xFF) as u8;
|
||||
bytes[3] = ((data_without_crc >> 24) & 0xFF) as u8;
|
||||
bytes[4] = ((data_without_crc >> 32) & 0xFF) as u8;
|
||||
bytes[5] = ((data_without_crc >> 40) & 0xFF) as u8;
|
||||
|
||||
let mut crc: u8 = 0;
|
||||
for &byte in &bytes {
|
||||
@@ -181,19 +180,21 @@ impl ProtocolDecoder for KiaV2Decoder {
|
||||
self.te_last = duration;
|
||||
self.header_count += 1;
|
||||
} else if is_short && self.header_count >= 100 {
|
||||
// C code: decode_count_bit=1, then add_bit(1) which shifts and increments to 2
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 1;
|
||||
self.step = DecoderStep::CollectRawBits;
|
||||
self.decode_data = 1; // First bit
|
||||
self.decode_count_bit = 2;
|
||||
self.step = DecoderStep::CollectRawBits;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = duration; // C stays in CheckPreamble, updates te_last
|
||||
}
|
||||
} else {
|
||||
if is_long {
|
||||
self.header_count += 1;
|
||||
self.te_last = duration;
|
||||
} else if !is_short {
|
||||
} else if is_short {
|
||||
self.te_last = duration; // C stays in CheckPreamble for short LOW
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
+197
-7
@@ -1,8 +1,8 @@
|
||||
//! Kia V6 protocol decoder (decode-only)
|
||||
//! Kia V6 protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v6.c`.
|
||||
//! Decode logic (Manchester level mapping, 3-part 144-bit frame, AES-128, CRC8, keystore XOR) matches reference.
|
||||
//! No encoder in protopirate.
|
||||
//! Encoder ported from protopirate (ENABLE_EMULATE_FEATURE): AES-128 encrypt, two-pass Manchester.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance)
|
||||
@@ -275,6 +275,181 @@ impl KiaV6Decoder {
|
||||
*data = state;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Forward AES functions for encoder (matches kia_v6.c ENABLE_EMULATE_FEATURE)
|
||||
// =========================================================================
|
||||
|
||||
/// AES forward SubBytes
|
||||
fn aes_subbytes(state: &mut [u8; 16]) {
|
||||
for i in 0..16 {
|
||||
state[i] = AES_SBOX[state[i] as usize];
|
||||
}
|
||||
}
|
||||
|
||||
/// AES forward ShiftRows
|
||||
fn aes_shiftrows(state: &mut [u8; 16]) {
|
||||
let temp = state[1];
|
||||
state[1] = state[5];
|
||||
state[5] = state[9];
|
||||
state[9] = state[13];
|
||||
state[13] = temp;
|
||||
|
||||
let temp = state[2];
|
||||
state[2] = state[10];
|
||||
state[10] = temp;
|
||||
let temp = state[6];
|
||||
state[6] = state[14];
|
||||
state[14] = temp;
|
||||
|
||||
let temp = state[3];
|
||||
state[3] = state[15];
|
||||
state[15] = state[11];
|
||||
state[11] = state[7];
|
||||
state[7] = temp;
|
||||
}
|
||||
|
||||
/// AES forward MixColumns
|
||||
fn aes_mixcolumns(state: &mut [u8; 16]) {
|
||||
for i in 0..4 {
|
||||
let a = state[i * 4];
|
||||
let b = state[i * 4 + 1];
|
||||
let c = state[i * 4 + 2];
|
||||
let d = state[i * 4 + 3];
|
||||
state[i * 4] = Self::gf_mul2(a) ^ Self::gf_mul2(b) ^ b ^ c ^ d;
|
||||
state[i * 4 + 1] = a ^ Self::gf_mul2(b) ^ Self::gf_mul2(c) ^ c ^ d;
|
||||
state[i * 4 + 2] = a ^ b ^ Self::gf_mul2(c) ^ Self::gf_mul2(d) ^ d;
|
||||
state[i * 4 + 3] = Self::gf_mul2(a) ^ a ^ b ^ c ^ Self::gf_mul2(d);
|
||||
}
|
||||
}
|
||||
|
||||
/// AES-128 encrypt
|
||||
fn aes128_encrypt(expanded_key: &[u8; 176], data: &mut [u8; 16]) {
|
||||
let mut state = *data;
|
||||
|
||||
Self::aes_addroundkey(&mut state, &expanded_key[0..16]);
|
||||
|
||||
for round in 1..10 {
|
||||
Self::aes_subbytes(&mut state);
|
||||
Self::aes_shiftrows(&mut state);
|
||||
Self::aes_mixcolumns(&mut state);
|
||||
Self::aes_addroundkey(&mut state, &expanded_key[round * 16..(round + 1) * 16]);
|
||||
}
|
||||
|
||||
Self::aes_subbytes(&mut state);
|
||||
Self::aes_shiftrows(&mut state);
|
||||
Self::aes_addroundkey(&mut state, &expanded_key[160..176]);
|
||||
|
||||
*data = state;
|
||||
}
|
||||
|
||||
/// Encrypt payload for transmission (matches kia_v6.c kia_v6_encrypt_payload)
|
||||
fn encrypt_payload(
|
||||
fx_field: u8,
|
||||
serial: u32,
|
||||
button: u8,
|
||||
counter: u32,
|
||||
) -> (u32, u32, u32, u32, u16) {
|
||||
let mut plain = [0u8; 16];
|
||||
plain[0] = fx_field;
|
||||
plain[4] = ((serial >> 16) & 0xFF) as u8;
|
||||
plain[5] = ((serial >> 8) & 0xFF) as u8;
|
||||
plain[6] = (serial & 0xFF) as u8;
|
||||
plain[7] = button & 0x0F;
|
||||
plain[8] = ((counter >> 24) & 0xFF) as u8;
|
||||
plain[9] = ((counter >> 16) & 0xFF) as u8;
|
||||
plain[10] = ((counter >> 8) & 0xFF) as u8;
|
||||
plain[11] = (counter & 0xFF) as u8;
|
||||
plain[12] = AES_SBOX[(counter & 0xFF) as usize];
|
||||
plain[15] = Self::crc8(&plain[..15], 0xFF, 0x07);
|
||||
|
||||
let aes_key = Self::get_aes_key();
|
||||
let expanded_key = Self::aes_key_expansion(&aes_key);
|
||||
Self::aes128_encrypt(&expanded_key, &mut plain);
|
||||
|
||||
let fx_hi = 0x20 | (fx_field >> 4);
|
||||
let fx_lo = fx_field & 0x0F;
|
||||
let part1_high = ((fx_hi as u32) << 24)
|
||||
| ((fx_lo as u32) << 16)
|
||||
| ((plain[0] as u32) << 8)
|
||||
| (plain[1] as u32);
|
||||
let part1_low = ((plain[2] as u32) << 24)
|
||||
| ((plain[3] as u32) << 16)
|
||||
| ((plain[4] as u32) << 8)
|
||||
| (plain[5] as u32);
|
||||
let part2_high = ((plain[6] as u32) << 24)
|
||||
| ((plain[7] as u32) << 16)
|
||||
| ((plain[8] as u32) << 8)
|
||||
| (plain[9] as u32);
|
||||
let part2_low = ((plain[10] as u32) << 24)
|
||||
| ((plain[11] as u32) << 16)
|
||||
| ((plain[12] as u32) << 8)
|
||||
| (plain[13] as u32);
|
||||
let part3 = ((plain[14] as u16) << 8) | (plain[15] as u16);
|
||||
|
||||
(part1_low, part1_high, part2_low, part2_high, part3)
|
||||
}
|
||||
|
||||
/// Build encoder signal: two-pass Manchester with preambles (matches kia_v6.c)
|
||||
fn build_upload(
|
||||
p1_lo: u32, p1_hi: u32,
|
||||
p2_lo: u32, p2_hi: u32,
|
||||
p3: u16,
|
||||
) -> Vec<LevelDuration> {
|
||||
let mut signal = Vec::with_capacity(2000);
|
||||
|
||||
// Two passes: 640 preamble pairs, then 38 preamble pairs
|
||||
for &preamble_pairs in &[640u32, 38u32] {
|
||||
// Preamble: short/short pairs
|
||||
for _ in 0..preamble_pairs {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// Sync: short LOW, long HIGH, short LOW
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
|
||||
// Part1: bits 60 down to 0 (61 bits), inverted
|
||||
for b in (0..=60).rev() {
|
||||
let word = if b >= 32 { p1_hi } else { p1_lo };
|
||||
let shift = if b >= 32 { b - 32 } else { b };
|
||||
let bit = ((!word) >> shift) & 1 == 1;
|
||||
Self::encode_manchester_bit(&mut signal, bit);
|
||||
}
|
||||
|
||||
// Part2: bits 63 down to 0 (64 bits), inverted
|
||||
for b in (0..=63).rev() {
|
||||
let word = if b >= 32 { p2_hi } else { p2_lo };
|
||||
let shift = if b >= 32 { b - 32 } else { b };
|
||||
let bit = ((!word) >> shift) & 1 == 1;
|
||||
Self::encode_manchester_bit(&mut signal, bit);
|
||||
}
|
||||
|
||||
// Part3: bits 15 down to 0 (16 bits), inverted
|
||||
for b in (0..=15).rev() {
|
||||
let bit = ((!p3) >> b) & 1 == 1;
|
||||
Self::encode_manchester_bit(&mut signal, bit);
|
||||
}
|
||||
|
||||
// Gap between passes
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
}
|
||||
|
||||
signal
|
||||
}
|
||||
|
||||
/// Encode one Manchester bit (matches kia_v6.c kia_v6_encode_manchester_bit)
|
||||
fn encode_manchester_bit(signal: &mut Vec<LevelDuration>, bit: bool) {
|
||||
if bit {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
}
|
||||
|
||||
/// AES-128 key from V6 keystores A+B with XOR_MASK_LOW/HIGH (matches kia_v6.c)
|
||||
fn get_aes_key() -> [u8; 16] {
|
||||
let keystore_a = Self::get_keystore_a();
|
||||
@@ -306,6 +481,13 @@ impl KiaV6Decoder {
|
||||
aes_key
|
||||
}
|
||||
|
||||
/// Extract fx_field from stored_part1_high (matches kia_v6.c fx_field extraction)
|
||||
fn extract_fx_field(&self) -> u8 {
|
||||
let fx_byte0 = ((self.stored_part1_high >> 24) & 0xFF) as u8;
|
||||
let fx_byte1 = ((self.stored_part1_high >> 16) & 0xFF) as u8;
|
||||
((fx_byte0 & 0xF) << 4) | (fx_byte1 & 0xF)
|
||||
}
|
||||
|
||||
/// Decrypt 16-byte block: byte layout matches kia_v6.c; AES-128 then CRC8 check
|
||||
fn decrypt(&self) -> Option<(u32, u8, u32, bool)> {
|
||||
let mut encrypted_data = [0u8; 16];
|
||||
@@ -528,6 +710,7 @@ impl ProtocolDecoder for KiaV6Decoder {
|
||||
if let Some((serial, button, counter, crc_valid)) = self.decrypt() {
|
||||
let key_data = ((self.stored_part1_high as u64) << 32) |
|
||||
(self.stored_part1_low as u64);
|
||||
let fx_field = self.extract_fx_field();
|
||||
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(DecodedSignal {
|
||||
@@ -537,8 +720,8 @@ impl ProtocolDecoder for KiaV6Decoder {
|
||||
crc_valid,
|
||||
data: key_data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: false,
|
||||
extra: None,
|
||||
encoder_capable: true,
|
||||
extra: Some(fx_field as u64),
|
||||
protocol_display_name: None,
|
||||
});
|
||||
}
|
||||
@@ -552,11 +735,18 @@ impl ProtocolDecoder for KiaV6Decoder {
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false // V6 is decode-only
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None // V6 decode-only in protopirate
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0) as u32;
|
||||
let fx_field = decoded.extra.unwrap_or(0) as u8;
|
||||
|
||||
let (p1_lo, p1_hi, p2_lo, p2_hi, p3) =
|
||||
Self::encrypt_payload(fx_field, serial, button, counter);
|
||||
|
||||
Some(Self::build_upload(p1_lo, p1_hi, p2_lo, p2_hi, p3))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
//! Kia V7 protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v7.c` and `kia_v7.h`.
|
||||
//! Manchester 250/500µs, 64 bits, FM. The decoded 64-bit word is bit-inverted (`~data`); a valid
|
||||
//! frame has a fixed high byte 0x4C and a CRC8 (poly 0x7F, init 0x4C) over bytes 0..7. Emission is
|
||||
//! gated on header + CRC, so Kia V7 is strongly validated and will not false-match.
|
||||
//!
|
||||
//! Decoder steps: Reset → Preamble (short pairs, ≥16) → SyncLow → Data. The preamble→sync transition
|
||||
//! preloads four seed bits (1,0,1,1 = the inverted header's top nibble 0xB) before collecting the
|
||||
//! remaining 60 Manchester bits. Encoder supported.
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::common::crc8;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const KEY_BITS: usize = 64;
|
||||
const HEADER: u8 = 0x4C;
|
||||
const PREAMBLE_MIN_PAIRS: u16 = 16;
|
||||
const TAIL_GAP_US: u32 = 2000;
|
||||
const TX_PREAMBLE_PAIRS: usize = 32;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0 = 0,
|
||||
Mid1 = 1,
|
||||
Start0 = 2,
|
||||
Start1 = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble,
|
||||
SyncLow,
|
||||
Data,
|
||||
}
|
||||
|
||||
pub struct KiaV7Decoder {
|
||||
step: DecoderStep,
|
||||
manchester_state: ManchesterState,
|
||||
te_last: u32,
|
||||
preamble_count: u16,
|
||||
decode_data: u64,
|
||||
decode_count_bit: usize,
|
||||
}
|
||||
|
||||
impl KiaV7Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
te_last: 0,
|
||||
preamble_count: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_short(d: u32) -> bool {
|
||||
duration_diff!(d, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
fn is_long(d: u32) -> bool {
|
||||
duration_diff!(d, TE_LONG) < TE_DELTA
|
||||
}
|
||||
|
||||
fn add_bit(&mut self, bit: bool) {
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||
let (new_state, emit) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
|
||||
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
|
||||
|
||||
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
|
||||
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
|
||||
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
_ => (ManchesterState::Mid1, false),
|
||||
};
|
||||
self.manchester_state = new_state;
|
||||
if emit { Some((event & 1) == 1) } else { None }
|
||||
}
|
||||
|
||||
/// Decode the 8 plaintext bytes of the (already inverted) key into fields.
|
||||
/// Returns (serial, button, counter, crc_valid).
|
||||
fn decode_key(data: u64) -> (u32, u8, u16, bool) {
|
||||
let bytes = data.to_be_bytes();
|
||||
let serial = (((bytes[3] as u32) << 20)
|
||||
| ((bytes[4] as u32) << 12)
|
||||
| ((bytes[5] as u32) << 4)
|
||||
| ((bytes[6] as u32) >> 4))
|
||||
& 0x0FFF_FFFF;
|
||||
let counter = ((bytes[1] as u16) << 8) | bytes[2] as u16;
|
||||
let button = bytes[6] & 0x0F;
|
||||
let crc_calc = crc8(&bytes[0..7], 0x7F, 0x4C);
|
||||
let crc_valid = crc_calc == bytes[7];
|
||||
(serial, button, counter, crc_valid)
|
||||
}
|
||||
|
||||
/// Rebuild the 64-bit key from fields (matches kia_v7_encode_key).
|
||||
fn encode_key(serial: u32, button: u8, counter: u16) -> u64 {
|
||||
let serial = serial & 0x0FFF_FFFF;
|
||||
let button = button & 0x0F;
|
||||
let mut bytes = [0u8; 8];
|
||||
bytes[0] = HEADER;
|
||||
bytes[1] = (counter >> 8) as u8;
|
||||
bytes[2] = counter as u8;
|
||||
bytes[3] = (serial >> 20) as u8;
|
||||
bytes[4] = (serial >> 12) as u8;
|
||||
bytes[5] = (serial >> 4) as u8;
|
||||
bytes[6] = (((serial & 0x0F) as u8) << 4) | button;
|
||||
bytes[7] = crc8(&bytes[0..7], 0x7F, 0x4C);
|
||||
u64::from_be_bytes(bytes)
|
||||
}
|
||||
|
||||
/// Map KAT generic button command to a Kia V7 4-bit button code.
|
||||
fn map_button(button: u8) -> u8 {
|
||||
match button {
|
||||
0x01 => 0x01, // Lock
|
||||
0x02 => 0x02, // Unlock
|
||||
0x04 => 0x03, // Trunk
|
||||
0x08 => 0x08, // Trunk/aux
|
||||
b => b & 0x0F,
|
||||
}
|
||||
}
|
||||
|
||||
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
||||
if let Some(last) = signal.last_mut() {
|
||||
if last.level == level {
|
||||
*last = LevelDuration::new(level, last.duration_us + duration);
|
||||
return;
|
||||
}
|
||||
}
|
||||
signal.push(LevelDuration::new(level, duration));
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for KiaV7Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Kia V7"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: KEY_BITS,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.te_last = 0;
|
||||
self.preamble_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && Self::is_short(duration) {
|
||||
self.step = DecoderStep::Preamble;
|
||||
self.te_last = duration;
|
||||
self.preamble_count = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Preamble => {
|
||||
if level {
|
||||
if Self::is_long(duration) && Self::is_short(self.te_last) {
|
||||
if self.preamble_count > (PREAMBLE_MIN_PAIRS - 1) {
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.preamble_count = 0;
|
||||
// Seed the inverted-header top nibble (1,0,1,1).
|
||||
self.add_bit(true);
|
||||
self.add_bit(false);
|
||||
self.add_bit(true);
|
||||
self.add_bit(true);
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::SyncLow;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else if Self::is_short(duration) {
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else if Self::is_short(duration) && Self::is_short(self.te_last) {
|
||||
self.preamble_count += 1;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::SyncLow => {
|
||||
if !level && Self::is_short(duration) && Self::is_long(self.te_last) {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::Data;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Data => {
|
||||
let event = if Self::is_short(duration) {
|
||||
Some(if level { 1 } else { 0 })
|
||||
} else if Self::is_long(duration) {
|
||||
Some(if level { 3 } else { 2 })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(ev) = event {
|
||||
if let Some(bit) = self.manchester_advance(ev) {
|
||||
self.add_bit(bit);
|
||||
}
|
||||
}
|
||||
|
||||
if self.decode_count_bit == KEY_BITS {
|
||||
let candidate = !self.decode_data;
|
||||
let hdr = (candidate >> 56) as u8;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
|
||||
if hdr == HEADER {
|
||||
let (serial, button, counter, crc_valid) = Self::decode_key(candidate);
|
||||
if crc_valid {
|
||||
return Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data: candidate,
|
||||
data_count_bit: KEY_BITS,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0);
|
||||
let key = Self::encode_key(serial, Self::map_button(button), counter);
|
||||
|
||||
let mut signal = Vec::with_capacity(TX_PREAMBLE_PAIRS * 2 + KEY_BITS * 2 + 4);
|
||||
// Preamble: alternating short pulses.
|
||||
for _ in 0..TX_PREAMBLE_PAIRS {
|
||||
Self::enc_add_level(&mut signal, true, TE_SHORT);
|
||||
Self::enc_add_level(&mut signal, false, TE_SHORT);
|
||||
}
|
||||
// Standalone high short (merges with first data-bit high to form the long sync pulse).
|
||||
Self::enc_add_level(&mut signal, true, TE_SHORT);
|
||||
// Manchester data, MSB first: bit 1 → (H,L), bit 0 → (L,H).
|
||||
for bit in (0..KEY_BITS).rev() {
|
||||
let value = (key >> bit) & 1 != 0;
|
||||
if value {
|
||||
Self::enc_add_level(&mut signal, true, TE_SHORT);
|
||||
Self::enc_add_level(&mut signal, false, TE_SHORT);
|
||||
} else {
|
||||
Self::enc_add_level(&mut signal, false, TE_SHORT);
|
||||
Self::enc_add_level(&mut signal, true, TE_SHORT);
|
||||
}
|
||||
}
|
||||
// Trailing high short + tail gap.
|
||||
Self::enc_add_level(&mut signal, true, TE_SHORT);
|
||||
Self::enc_add_level(&mut signal, false, TAIL_GAP_US);
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV7Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
//! Land Rover RKE protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from the Flipper-ARF firmware (`lib/subghz/protocols/landrover_rke.c` / `.h`,
|
||||
//! D4C1-Labs), itself derived from Pandora DXL 5000 firmware. Land Rover shares the
|
||||
//! Ford/Jaguar baseband (firmware protocol ID 0x0E) but uses a distinct 66-bit frame.
|
||||
//!
|
||||
//! Encoding: fixed-width PWM, OOK/AM carrier. Bit period 1000µs:
|
||||
//! Bit-1 = 700µs HIGH + 300µs LOW; Bit-0 = 300µs HIGH + 700µs LOW.
|
||||
//! Preamble = 20× (400µs HIGH + 600µs LOW); sync = 400µs HIGH + 9600µs LOW.
|
||||
//! Tolerance ±20% (relative), matching the C `lr_in_range`.
|
||||
//!
|
||||
//! Frame (66 bits, MSB-first), matching the C layout:
|
||||
//! [65:34] 32-bit KeeLoq encrypted hopping code
|
||||
//! [33:10] 24-bit fixed fob serial
|
||||
//! [9:6] 4-bit button code (0x1=Lock, 0x2=Unlock, 0x4=Boot/Tailgate, 0x8=Panic)
|
||||
//! [5:2] 4-bit function/repeat flags
|
||||
//! [1:0] 2-bit status (0x1=battery low, 0x2=repeat)
|
||||
//!
|
||||
//! KeeLoq: the hop code is the raw 32-bit KeeLoq ciphertext. Full decryption needs the
|
||||
//! per-fob manufacturer key (provisioned, not in the firmware), so KAT exposes the framed
|
||||
//! fields (serial/button) and leaves the hop encrypted — `crc_valid=false` since no real
|
||||
//! cryptographic check is performed. Emission is still gated tightly on the structural
|
||||
//! invariants below so this loose-looking PWM decoder does not false-match
|
||||
//! Kia/Subaru/Ford captures.
|
||||
//!
|
||||
//! Storage: 66 bits do not fit a u64. The canonical `DecodedSignal.data` holds the low 64
|
||||
//! frame bits and `DecodedSignal.extra` holds the top 2 (frame bits [65:64], the high 2 bits
|
||||
//! of the hop code), so encode() round-trips the hop code losslessly. `data_count_bit` = 66.
|
||||
//!
|
||||
//! Gating: a valid frame requires a long preamble run (≥16 of the 400/600µs pairs), the very
|
||||
//! distinctive 400µs-HIGH + 9600µs-LOW sync gap, then exactly 66 bits whose HIGH/LOW halves
|
||||
//! each fall inside the ±20% PWM windows. The 9.6ms sync gap + 66-bit PWM payload combination
|
||||
//! is unique among KAT's protocols, so nothing else in the sweep matches it.
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
// Timing constants (microseconds) — verbatim from landrover_rke.h.
|
||||
const PREAMBLE_HIGH_US: u32 = 400;
|
||||
const PREAMBLE_LOW_US: u32 = 600;
|
||||
const PREAMBLE_COUNT: u32 = 20;
|
||||
const SYNC_HIGH_US: u32 = 400;
|
||||
const SYNC_LOW_US: u32 = 9600;
|
||||
const BIT1_HIGH_US: u32 = 700;
|
||||
const BIT1_LOW_US: u32 = 300;
|
||||
const BIT0_HIGH_US: u32 = 300;
|
||||
const BIT0_LOW_US: u32 = 700;
|
||||
const REPEAT_GAP_US: u32 = 12000;
|
||||
const REPEAT_COUNT: u32 = 4;
|
||||
const TOLERANCE_PCT: u32 = 20;
|
||||
const FRAME_BITS: usize = 66;
|
||||
|
||||
// Require a substantial preamble run before accepting the sync gap. The C scans a raw buffer
|
||||
// for the sync directly, but gating on the preamble too makes the streaming decoder specific.
|
||||
const PREAMBLE_MIN_PAIRS: u32 = 16;
|
||||
|
||||
// Button codes (frame bits [9:6]).
|
||||
const BTN_LOCK: u8 = 0x1;
|
||||
const BTN_UNLOCK: u8 = 0x2;
|
||||
const BTN_BOOT: u8 = 0x4;
|
||||
const BTN_PANIC: u8 = 0x8;
|
||||
|
||||
/// Relative-tolerance match, matching the C `lr_in_range`:
|
||||
/// `|measured - ref| * 100 <= ref * TOLERANCE_PCT`.
|
||||
#[inline]
|
||||
fn in_range(measured_us: u32, ref_us: u32) -> bool {
|
||||
let diff = if measured_us > ref_us {
|
||||
measured_us - ref_us
|
||||
} else {
|
||||
ref_us - measured_us
|
||||
};
|
||||
diff.saturating_mul(100) <= ref_us.saturating_mul(TOLERANCE_PCT)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
/// Looking for the first preamble HIGH pulse.
|
||||
Reset,
|
||||
/// Counting preamble (400µs HIGH + 600µs LOW) pairs; te_last holds the pending HIGH.
|
||||
Preamble,
|
||||
/// Sync seen — expecting the next data-bit HIGH half.
|
||||
DataHigh,
|
||||
/// Collected a data-bit HIGH in te_last; decide the bit value from the following LOW.
|
||||
DataLow,
|
||||
}
|
||||
|
||||
/// Land Rover RKE protocol decoder.
|
||||
///
|
||||
/// Bits are accumulated MSB-first into a 66-element array (index 0 = first bit on air =
|
||||
/// frame bit 65), mirroring the C `bits[65 - b]` indexing and avoiding any u64 overflow.
|
||||
pub struct LandRoverRkeDecoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
preamble_count: u32,
|
||||
/// Received bits, MSB-first: `rx_bits[i]` is the (i+1)-th bit on air = frame bit `65 - i`.
|
||||
rx_bits: [u8; FRAME_BITS],
|
||||
decode_count_bit: usize,
|
||||
}
|
||||
|
||||
impl LandRoverRkeDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
preamble_count: 0,
|
||||
rx_bits: [0u8; FRAME_BITS],
|
||||
decode_count_bit: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a KAT generic button command to a Land Rover RKE 4-bit button code.
|
||||
/// KAT: Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08.
|
||||
/// LR RKE bits [9:6]: Lock=0x1, Unlock=0x2, Boot/Tailgate=0x4, Panic=0x8 — identical mapping.
|
||||
fn map_button(button: u8) -> u8 {
|
||||
match button {
|
||||
0x01 => BTN_LOCK,
|
||||
0x02 => BTN_UNLOCK,
|
||||
0x04 => BTN_BOOT,
|
||||
0x08 => BTN_PANIC,
|
||||
b => b & 0x0F,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the logical 66-bit frame array from the field values.
|
||||
///
|
||||
/// The array is indexed by frame bit number (bit 0 = LSB of the whole 66-bit frame). Each
|
||||
/// field `X` occupying frame bits `[hi:lo]` maps frame bit `(lo + j)` = field bit `j`, so the
|
||||
/// field's MSB lands at the higher frame-bit index. The encoder transmits frame bit 65 first,
|
||||
/// giving the documented MSB-first-on-air order. This is the exact inverse of `unpack_frame`.
|
||||
///
|
||||
/// NOTE: the Flipper-ARF C reference's `lr_encode`/`lr_decode` use inconsistent bit
|
||||
/// endianness for the fields (encode writes `bits[65-i]=field>>i`, decode reads
|
||||
/// `field|=bits[65-k]<<(31-k)`), so the C does not round-trip. KAT preserves the documented
|
||||
/// field *layout* and MSB-first wire order while keeping encode/decode mutually consistent.
|
||||
fn pack_frame(hop_code: u32, serial: u32, button: u8, func_bits: u8, status: u8) -> [u8; FRAME_BITS] {
|
||||
let mut bits = [0u8; FRAME_BITS];
|
||||
// hop_code: frame bits [65:34] (32 bits).
|
||||
for j in 0..32 {
|
||||
bits[34 + j] = ((hop_code >> j) & 1) as u8;
|
||||
}
|
||||
// serial: frame bits [33:10] (24 bits).
|
||||
for j in 0..24 {
|
||||
bits[10 + j] = ((serial >> j) & 1) as u8;
|
||||
}
|
||||
// button: frame bits [9:6] (4 bits).
|
||||
for j in 0..4 {
|
||||
bits[6 + j] = ((button >> j) & 1) as u8;
|
||||
}
|
||||
// func_bits: frame bits [5:2] (4 bits).
|
||||
for j in 0..4 {
|
||||
bits[2 + j] = ((func_bits >> j) & 1) as u8;
|
||||
}
|
||||
// status: frame bits [1:0] (2 bits).
|
||||
bits[0] = status & 1;
|
||||
bits[1] = (status >> 1) & 1;
|
||||
bits
|
||||
}
|
||||
|
||||
/// Extract fields from a logical 66-bit frame array (index = frame bit, bit 0 = LSB).
|
||||
/// Returns (hop_code, serial, button, func_bits, status). Exact inverse of `pack_frame`.
|
||||
fn unpack_frame(bits: &[u8; FRAME_BITS]) -> (u32, u32, u8, u8, u8) {
|
||||
let mut hop_code: u32 = 0;
|
||||
for j in 0..32 {
|
||||
hop_code |= (bits[34 + j] as u32) << j;
|
||||
}
|
||||
let mut serial: u32 = 0;
|
||||
for j in 0..24 {
|
||||
serial |= (bits[10 + j] as u32) << j;
|
||||
}
|
||||
let mut button: u8 = 0;
|
||||
for j in 0..4 {
|
||||
button |= bits[6 + j] << j;
|
||||
}
|
||||
let mut func_bits: u8 = 0;
|
||||
for j in 0..4 {
|
||||
func_bits |= bits[2 + j] << j;
|
||||
}
|
||||
let status = bits[0] | (bits[1] << 1);
|
||||
(hop_code, serial, button, func_bits, status)
|
||||
}
|
||||
|
||||
/// Pack a logical 66-bit frame array into `(data, extra)`:
|
||||
/// `data` = frame bits [63:0], `extra` = frame bits [65:64].
|
||||
fn frame_to_data(bits: &[u8; FRAME_BITS]) -> (u64, u64) {
|
||||
let mut data: u64 = 0;
|
||||
for i in 0..64 {
|
||||
data |= (bits[i] as u64) << i;
|
||||
}
|
||||
let extra: u64 = (bits[64] as u64) | ((bits[65] as u64) << 1);
|
||||
(data, extra)
|
||||
}
|
||||
|
||||
/// Inverse of `frame_to_data`.
|
||||
fn data_to_frame(data: u64, extra: u64) -> [u8; FRAME_BITS] {
|
||||
let mut bits = [0u8; FRAME_BITS];
|
||||
for i in 0..64 {
|
||||
bits[i] = ((data >> i) & 1) as u8;
|
||||
}
|
||||
bits[64] = (extra & 1) as u8;
|
||||
bits[65] = ((extra >> 1) & 1) as u8;
|
||||
bits
|
||||
}
|
||||
|
||||
/// Convert the received MSB-first `rx_bits` (index 0 = frame bit 65) into the logical
|
||||
/// frame array (index = frame bit number).
|
||||
fn rx_to_frame(rx_bits: &[u8; FRAME_BITS]) -> [u8; FRAME_BITS] {
|
||||
let mut frame = [0u8; FRAME_BITS];
|
||||
for i in 0..FRAME_BITS {
|
||||
frame[65 - i] = rx_bits[i];
|
||||
}
|
||||
frame
|
||||
}
|
||||
|
||||
/// Build a DecodedSignal from a completed received-bit array.
|
||||
fn build_signal(rx_bits: &[u8; FRAME_BITS]) -> DecodedSignal {
|
||||
let frame = Self::rx_to_frame(rx_bits);
|
||||
let (hop_code, serial, button, _func_bits, _status) = Self::unpack_frame(&frame);
|
||||
let (data, extra) = Self::frame_to_data(&frame);
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
// The 16-bit KeeLoq counter is inside the *encrypted* hop code; without the
|
||||
// manufacturer key we cannot recover it. Surface the low 16 bits of the hop
|
||||
// ciphertext so the UI has a stable per-press value.
|
||||
counter: Some((hop_code & 0xFFFF) as u16),
|
||||
// No cryptographic check is performed (no key), so this is not a verified frame.
|
||||
crc_valid: false,
|
||||
data,
|
||||
data_count_bit: FRAME_BITS,
|
||||
encoder_capable: true,
|
||||
// Carry the top 2 frame bits so encode() can faithfully round-trip the hop code.
|
||||
extra: Some(extra),
|
||||
protocol_display_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn push_pair(signal: &mut Vec<LevelDuration>, high_us: u32, low_us: u32) {
|
||||
signal.push(LevelDuration::new(true, high_us));
|
||||
signal.push(LevelDuration::new(false, low_us));
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for LandRoverRkeDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Land Rover RKE"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: BIT0_HIGH_US, // 300µs
|
||||
te_long: BIT1_HIGH_US, // 700µs
|
||||
te_delta: 140, // ~20% of the 700µs long half
|
||||
min_count_bit: FRAME_BITS,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000, 315_000_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.preamble_count = 0;
|
||||
self.rx_bits = [0u8; FRAME_BITS];
|
||||
self.decode_count_bit = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
// First preamble HIGH (~400µs).
|
||||
if level && in_range(duration, PREAMBLE_HIGH_US) {
|
||||
self.step = DecoderStep::Preamble;
|
||||
self.te_last = duration;
|
||||
self.preamble_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Preamble => {
|
||||
if level {
|
||||
// A HIGH while in preamble: either another preamble HIGH (~400µs) or a sync
|
||||
// HIGH (also ~400µs) — disambiguated by the LOW that follows.
|
||||
if in_range(duration, PREAMBLE_HIGH_US) {
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
// LOW following a preamble/sync HIGH.
|
||||
if in_range(self.te_last, SYNC_HIGH_US) && in_range(duration, SYNC_LOW_US) {
|
||||
// Sync gap (~9600µs LOW). Require enough preamble first.
|
||||
if self.preamble_count >= PREAMBLE_MIN_PAIRS {
|
||||
self.rx_bits = [0u8; FRAME_BITS];
|
||||
self.decode_count_bit = 0;
|
||||
self.step = DecoderStep::DataHigh;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else if in_range(self.te_last, PREAMBLE_HIGH_US)
|
||||
&& in_range(duration, PREAMBLE_LOW_US)
|
||||
{
|
||||
// Another preamble pair (~400µs HIGH + ~600µs LOW).
|
||||
self.preamble_count = self.preamble_count.saturating_add(1);
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DataHigh => {
|
||||
if level {
|
||||
// Data-bit HIGH half — must match a Bit-1 or Bit-0 HIGH window.
|
||||
if in_range(duration, BIT1_HIGH_US) || in_range(duration, BIT0_HIGH_US) {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::DataLow;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DataLow => {
|
||||
if !level {
|
||||
let hi = self.te_last;
|
||||
let lo = duration;
|
||||
let bit = if in_range(hi, BIT1_HIGH_US) && in_range(lo, BIT1_LOW_US) {
|
||||
Some(1u8)
|
||||
} else if in_range(hi, BIT0_HIGH_US) && in_range(lo, BIT0_LOW_US) {
|
||||
Some(0u8)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match bit {
|
||||
Some(b) => {
|
||||
// Store MSB-first: first bit on air → rx_bits[0] (= frame bit 65).
|
||||
if self.decode_count_bit < FRAME_BITS {
|
||||
self.rx_bits[self.decode_count_bit] = b;
|
||||
}
|
||||
self.decode_count_bit += 1;
|
||||
|
||||
if self.decode_count_bit == FRAME_BITS {
|
||||
let result = Self::build_signal(&self.rx_bits);
|
||||
self.reset();
|
||||
return Some(result);
|
||||
}
|
||||
self.step = DecoderStep::DataHigh;
|
||||
}
|
||||
None => {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
// Reconstruct the original frame to preserve the hop code, func bits, and status, then
|
||||
// override only the button. The hop code lives in (data, extra); recover it.
|
||||
let extra = decoded.extra.unwrap_or(0);
|
||||
let orig_frame = Self::data_to_frame(decoded.data, extra);
|
||||
let (hop_code, _serial, _orig_button, func_bits, status) = Self::unpack_frame(&orig_frame);
|
||||
|
||||
let btn = Self::map_button(button);
|
||||
let frame = Self::pack_frame(hop_code, serial, btn, func_bits, status);
|
||||
|
||||
let mut signal = Vec::with_capacity(
|
||||
((PREAMBLE_COUNT as usize + 1 + FRAME_BITS) * 2 + 1) * REPEAT_COUNT as usize,
|
||||
);
|
||||
|
||||
for rep in 0..REPEAT_COUNT {
|
||||
// Preamble: 20 pairs.
|
||||
for _ in 0..PREAMBLE_COUNT {
|
||||
Self::push_pair(&mut signal, PREAMBLE_HIGH_US, PREAMBLE_LOW_US);
|
||||
}
|
||||
// Sync.
|
||||
Self::push_pair(&mut signal, SYNC_HIGH_US, SYNC_LOW_US);
|
||||
// Data bits, MSB-first (frame bit 65 first on air).
|
||||
for b in (0..FRAME_BITS).rev() {
|
||||
if frame[b] != 0 {
|
||||
Self::push_pair(&mut signal, BIT1_HIGH_US, BIT1_LOW_US);
|
||||
} else {
|
||||
Self::push_pair(&mut signal, BIT0_HIGH_US, BIT0_LOW_US);
|
||||
}
|
||||
}
|
||||
// Inter-repetition gap.
|
||||
if rep < REPEAT_COUNT - 1 {
|
||||
signal.push(LevelDuration::new(false, REPEAT_GAP_US));
|
||||
}
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LandRoverRkeDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Feed an encoded signal back through a fresh decoder and return the first decode.
|
||||
fn decode_signal(signal: &[LevelDuration]) -> Option<DecodedSignal> {
|
||||
let mut dec = LandRoverRkeDecoder::new();
|
||||
for ld in signal {
|
||||
if let Some(d) = dec.feed(ld.level, ld.duration_us) {
|
||||
return Some(d);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Build a DecodedSignal seed carrying a chosen hop code + serial (button overridden at encode).
|
||||
fn seed(hop_code: u32, serial: u32, func_bits: u8, status: u8, button: u8) -> DecodedSignal {
|
||||
let frame = LandRoverRkeDecoder::pack_frame(hop_code, serial & 0x00FF_FFFF, button, func_bits, status);
|
||||
let (data, extra) = LandRoverRkeDecoder::frame_to_data(&frame);
|
||||
DecodedSignal {
|
||||
serial: Some(serial & 0x00FF_FFFF),
|
||||
button: Some(button),
|
||||
counter: Some((hop_code & 0xFFFF) as u16),
|
||||
crc_valid: false,
|
||||
data,
|
||||
data_count_bit: FRAME_BITS,
|
||||
encoder_capable: true,
|
||||
extra: Some(extra),
|
||||
protocol_display_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_unpack_roundtrip() {
|
||||
// Field packing/unpacking is exact across the full 66-bit layout.
|
||||
let cases = [
|
||||
(0xDEAD_BEEFu32, 0x00AB_CDEFu32, 0x1u8, BTN_UNLOCK, 0x5u8, 0x2u8),
|
||||
(0x0000_0000, 0x0000_0000, 0x0, 0x0, 0x0, 0x0),
|
||||
(0xFFFF_FFFF, 0x00FF_FFFF, 0xF, 0xF, 0xF, 0x3),
|
||||
(0x1234_5678, 0x0055_AA55, 0x4, BTN_LOCK, 0xA, 0x1),
|
||||
];
|
||||
for &(hop, serial, _btn_field_unused, button, func, status) in &cases {
|
||||
let frame = LandRoverRkeDecoder::pack_frame(hop, serial, button, func, status);
|
||||
let (h, s, b, f, st) = LandRoverRkeDecoder::unpack_frame(&frame);
|
||||
assert_eq!(h, hop, "hop_code mismatch");
|
||||
assert_eq!(s, serial, "serial mismatch");
|
||||
assert_eq!(b, button, "button mismatch");
|
||||
assert_eq!(f, func, "func_bits mismatch");
|
||||
assert_eq!(st, status, "status mismatch");
|
||||
|
||||
// (data, extra) <-> frame is lossless too.
|
||||
let (data, extra) = LandRoverRkeDecoder::frame_to_data(&frame);
|
||||
let frame2 = LandRoverRkeDecoder::data_to_frame(data, extra);
|
||||
assert_eq!(frame, frame2, "data/extra round-trip mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_decode_roundtrip_multiple() {
|
||||
// Multiple serials / hop codes (counters) / buttons all round-trip through encode→decode.
|
||||
let serials = [0x00ABCDEFu32, 0x00123456, 0x00000001, 0x00FFFFFE, 0x005A5A5A];
|
||||
let hops = [0xDEADBEEFu32, 0x00000000, 0xFFFFFFFF, 0x12345678, 0xCAFEBABE];
|
||||
let buttons = [0x01u8, 0x02, 0x04, 0x08]; // Lock, Unlock, Trunk/Boot, Panic
|
||||
|
||||
for (&serial, &hop) in serials.iter().zip(hops.iter()) {
|
||||
for &btn in &buttons {
|
||||
let s = seed(hop, serial, 0x3, 0x1, 0x00); // func/status preserved from seed
|
||||
let encoder = LandRoverRkeDecoder::new();
|
||||
let signal = encoder.encode(&s, btn).expect("encode should succeed");
|
||||
let decoded = decode_signal(&signal)
|
||||
.unwrap_or_else(|| panic!("decode failed for serial {serial:#X} hop {hop:#X} btn {btn:#X}"));
|
||||
|
||||
let expected_btn = LandRoverRkeDecoder::map_button(btn);
|
||||
assert_eq!(decoded.serial, Some(serial & 0x00FF_FFFF), "serial");
|
||||
assert_eq!(decoded.button, Some(expected_btn), "button");
|
||||
// hop code survives via data+extra.
|
||||
let frame = LandRoverRkeDecoder::data_to_frame(decoded.data, decoded.extra.unwrap());
|
||||
let (dec_hop, _, _, dec_func, dec_status) = LandRoverRkeDecoder::unpack_frame(&frame);
|
||||
assert_eq!(dec_hop, hop, "hop_code");
|
||||
assert_eq!(dec_func, 0x3, "func_bits preserved");
|
||||
assert_eq!(dec_status, 0x1, "status preserved");
|
||||
assert_eq!(decoded.data_count_bit, FRAME_BITS, "bit count");
|
||||
assert!(!decoded.crc_valid, "no key → crc_valid must be false");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_truncated_frame() {
|
||||
// A single repetition carrying only 65 of the 66 bits must NOT decode.
|
||||
let s = seed(0xDEADBEEF, 0x00ABCDEF, 0x0, 0x0, 0x02);
|
||||
let encoder = LandRoverRkeDecoder::new();
|
||||
let signal = encoder.encode(&s, 0x02).unwrap();
|
||||
// First repetition is preamble (20 pairs) + sync (1 pair) + 66 bit-pairs. Keep only the
|
||||
// first 65 bit-pairs so the frame is one bit short, and stop before the next repetition.
|
||||
let bits_to_keep = FRAME_BITS - 1;
|
||||
let partial_len = (PREAMBLE_COUNT as usize + 1 + bits_to_keep) * 2;
|
||||
let partial = &signal[..partial_len];
|
||||
assert!(decode_signal(partial).is_none(), "65-bit frame must not decode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_wrong_sync_gap() {
|
||||
// Same PWM bits but a too-short "sync" gap must not be accepted as a frame.
|
||||
let s = seed(0x12345678, 0x00112233, 0x0, 0x0, 0x01);
|
||||
let encoder = LandRoverRkeDecoder::new();
|
||||
let mut signal = encoder.encode(&s, 0x01).unwrap();
|
||||
// Corrupt the first sync LOW (index = PREAMBLE_COUNT*2 + 1) to a Bit-0-like LOW (700µs),
|
||||
// which is far outside the 9600µs ±20% window.
|
||||
let sync_low_idx = PREAMBLE_COUNT as usize * 2 + 1;
|
||||
signal[sync_low_idx] = LevelDuration::new(false, 700);
|
||||
// Take just the first repetition so the later (intact) reps don't rescue the decode.
|
||||
let one_rep_len = (PREAMBLE_COUNT as usize + 1 + FRAME_BITS) * 2;
|
||||
let partial = &signal[..one_rep_len.min(signal.len())];
|
||||
assert!(decode_signal(partial).is_none(), "frame without valid 9.6ms sync must not decode");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,696 @@
|
||||
//! Land Rover V0 protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/land_rover_v0.c` and
|
||||
//! `land_rover_v0.h`. **Differential** Manchester (NOT the Flipper transition table): te_short 250,
|
||||
//! te_long 500, te_delta 100, with a ~750µs sync pulse and a ≥64-pair short preamble. FM.
|
||||
//!
|
||||
//! Frame: 81 bits = an 80-bit body (`raw[0..10]`) plus one trailing `extra_bit`. The 64-bit key
|
||||
//! reported as `DecodedSignal.data` is `raw[0..8]` big-endian; `raw[8..10]` is a 16-bit `tail`.
|
||||
//! Field layout in the key bytes:
|
||||
//! * bytes 0..3 → 24-bit command_signature (Lock = 0xC20363, Unlock = 0xA285E3)
|
||||
//! * bytes 3..6 → 24-bit serial
|
||||
//! * byte 6 + byte 7 MSB → 9-bit counter = `(b6 << 1) | (b7 >> 7)`
|
||||
//! * byte 7 bits 0x78 → 3 reserved bits (must be 0)
|
||||
//! * byte 7 bits 0x07 → 3-bit check
|
||||
//! The check is a proprietary 3-bit polynomial over the counter (`calculate_check`); the tail is
|
||||
//! 0xFFFF or 0x7FFF depending on a 1-bit parity of the counter (`calculate_tail`). Emission is gated
|
||||
//! on the reserved bits being zero, the check matching, the tail matching, and `extra_bit` set —
|
||||
//! so Land Rover V0 is strongly validated and will not false-match. Encoder supported.
|
||||
//!
|
||||
//! Decode steps: Reset → PreambleLow/PreambleHigh (count short pairs, ≥64) → SyncLow → Data.
|
||||
//! The Data step ports the C `process_transition`/`add_decoded_bit` differential machine directly:
|
||||
//! it tracks `previous_bit`, skips the initial boundary short-high pad (`boundary_pad_skipped`),
|
||||
//! and completes short half-bits via `pending_short`. Bit 0 (= 1) is seeded on entry to Data.
|
||||
//!
|
||||
//! Note on the C encoder (faithfully ported): `build_upload` forces frame bit 1 = 0 regardless of
|
||||
//! the key, so combined with the seeded bit 0 = 1 the top two frame bits are always `10`. The
|
||||
//! Unlock signature 0xA285E3 satisfies this and round-trips cleanly; the Lock signature 0xC20363
|
||||
//! (whose bit 1 is 1) is emitted as 0x820363 by the reference encoder. We reproduce that behaviour.
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const SYNC_US: u32 = 750;
|
||||
const SYNC_DELTA_US: u32 = 120;
|
||||
const MIN_PREAMBLE_PAIRS: u16 = 64;
|
||||
const COUNT_BIT: usize = 81;
|
||||
const GAP_US: u32 = 50_000;
|
||||
|
||||
/// TX preamble pair count (matches LAND_ROVER_V0_PREAMBLE_PAIRS).
|
||||
const TX_PREAMBLE_PAIRS: usize = 319;
|
||||
|
||||
// Button signatures (LAND_ROVER_V0_SIG_*).
|
||||
const SIG_UNLOCK: u32 = 0x00A2_85E3;
|
||||
const SIG_LOCK: u32 = 0x00C2_0363;
|
||||
|
||||
// Land Rover button codes (LAND_ROVER_V0_BTN_*).
|
||||
const LR_BTN_UNKNOWN: u8 = 0x00;
|
||||
const LR_BTN_LOCK: u8 = 0x02;
|
||||
const LR_BTN_UNLOCK: u8 = 0x04;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
PreambleLow,
|
||||
PreambleHigh,
|
||||
SyncLow,
|
||||
Data,
|
||||
}
|
||||
|
||||
pub struct LandRoverV0Decoder {
|
||||
step: DecoderStep,
|
||||
preamble_count: u16,
|
||||
raw: [u8; 10],
|
||||
bit_count: u8,
|
||||
extra_bit: bool,
|
||||
previous_bit: bool,
|
||||
boundary_pad_skipped: bool,
|
||||
pending_short: bool,
|
||||
}
|
||||
|
||||
impl LandRoverV0Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
preamble_count: 0,
|
||||
raw: [0; 10],
|
||||
bit_count: 0,
|
||||
extra_bit: false,
|
||||
previous_bit: true,
|
||||
boundary_pad_skipped: false,
|
||||
pending_short: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_short(d: u32) -> bool {
|
||||
duration_diff!(d, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
fn is_long(d: u32) -> bool {
|
||||
duration_diff!(d, TE_LONG) < TE_DELTA
|
||||
}
|
||||
fn is_sync(d: u32) -> bool {
|
||||
duration_diff!(d, SYNC_US) < SYNC_DELTA_US
|
||||
}
|
||||
|
||||
/// Reset the per-frame differential-Manchester state (matches the SyncLow init in the C feed).
|
||||
fn begin_frame(&mut self) {
|
||||
self.raw = [0; 10];
|
||||
self.bit_count = 0;
|
||||
self.extra_bit = false;
|
||||
self.previous_bit = true;
|
||||
self.boundary_pad_skipped = false;
|
||||
self.pending_short = false;
|
||||
}
|
||||
|
||||
/// Append one decoded bit (matches `land_rover_v0_add_decoded_bit`).
|
||||
/// Bits 0..80 pack MSB-first into `raw`; bit 80 is the trailing `extra_bit`.
|
||||
fn add_decoded_bit(&mut self, bit: bool) -> bool {
|
||||
if self.bit_count < 80 {
|
||||
let byte_index = (self.bit_count / 8) as usize;
|
||||
let bit_index = 7 - (self.bit_count % 8);
|
||||
if bit {
|
||||
self.raw[byte_index] |= 1u8 << bit_index;
|
||||
}
|
||||
} else if self.bit_count == 80 {
|
||||
self.extra_bit = bit;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
self.bit_count += 1;
|
||||
true
|
||||
}
|
||||
|
||||
/// Differential-Manchester transition handler (direct port of `land_rover_v0_process_transition`).
|
||||
fn process_transition(&mut self, level: bool, duration: u32) -> bool {
|
||||
if !self.boundary_pad_skipped {
|
||||
if level && Self::is_short(duration) {
|
||||
self.boundary_pad_skipped = true;
|
||||
return true;
|
||||
}
|
||||
self.boundary_pad_skipped = true;
|
||||
}
|
||||
|
||||
if self.pending_short {
|
||||
if !self.previous_bit && !level && Self::is_short(duration) {
|
||||
self.pending_short = false;
|
||||
return self.add_decoded_bit(false);
|
||||
} else if self.previous_bit && level && Self::is_short(duration) {
|
||||
self.pending_short = false;
|
||||
return self.add_decoded_bit(true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.previous_bit {
|
||||
if level && Self::is_long(duration) {
|
||||
self.previous_bit = true;
|
||||
return self.add_decoded_bit(true);
|
||||
} else if level && Self::is_short(duration) {
|
||||
self.pending_short = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if !level && Self::is_long(duration) {
|
||||
self.previous_bit = false;
|
||||
return self.add_decoded_bit(false);
|
||||
} else if !level && Self::is_short(duration) {
|
||||
self.pending_short = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// 3-bit check polynomial over the 9-bit counter (matches `land_rover_v0_calculate_check`).
|
||||
fn calculate_check(count: u32) -> u8 {
|
||||
let c0 = ((count >> 1) ^ (count >> 2) ^ (count >> 3) ^ (count >> 4) ^ (count >> 6)) & 1;
|
||||
let c1 = ((count >> 0)
|
||||
^ (count >> 2)
|
||||
^ (count >> 3)
|
||||
^ (count >> 4)
|
||||
^ (count >> 5)
|
||||
^ (count >> 6)
|
||||
^ 1)
|
||||
& 1;
|
||||
let c2 = ((count >> 1) ^ (count >> 3) ^ (count >> 4) ^ (count >> 5) ^ (count >> 6)) & 1;
|
||||
(c0 | (c1 << 1) | (c2 << 2)) as u8
|
||||
}
|
||||
|
||||
/// MSB selector for the 16-bit tail (matches `land_rover_v0_calculate_tail_msb`).
|
||||
fn calculate_tail_msb(count: u32) -> bool {
|
||||
(((count >> 0) ^ (count >> 2) ^ (count >> 4) ^ (count >> 5)) & 1) != 0
|
||||
}
|
||||
|
||||
/// 16-bit tail value (matches `land_rover_v0_calculate_tail`).
|
||||
fn calculate_tail(count: u32) -> u16 {
|
||||
if Self::calculate_tail_msb(count) {
|
||||
0xFFFF
|
||||
} else {
|
||||
0x7FFF
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a 24-bit command signature to a Land Rover button (matches
|
||||
/// `land_rover_v0_button_from_signature`).
|
||||
fn button_from_signature(signature: u32) -> u8 {
|
||||
match signature {
|
||||
SIG_UNLOCK => LR_BTN_UNLOCK,
|
||||
SIG_LOCK => LR_BTN_LOCK,
|
||||
_ => LR_BTN_UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
/// Counter packed in key bytes 6 + 7-MSB (matches the C count extraction).
|
||||
fn count_from_bytes(b: &[u8; 8]) -> u32 {
|
||||
((b[6] as u32) << 1) | ((b[7] >> 7) & 1) as u32
|
||||
}
|
||||
|
||||
/// Validate a frame (matches `land_rover_v0_validate_frame`).
|
||||
/// Returns (check_ok, tail_ok).
|
||||
fn validate_frame(key: u64, tail: u16, extra_bit: bool) -> (bool, bool) {
|
||||
let b = key.to_be_bytes();
|
||||
let count = Self::count_from_bytes(&b);
|
||||
let expected_check = Self::calculate_check(count);
|
||||
let expected_tail = Self::calculate_tail(count);
|
||||
|
||||
let check_ok = (b[7] & 0x78) == 0 && (b[7] & 0x07) == expected_check;
|
||||
let tail_ok = tail == expected_tail && extra_bit;
|
||||
(check_ok, tail_ok)
|
||||
}
|
||||
|
||||
/// Finish a frame: validate, then build the decoded signal (matches
|
||||
/// `land_rover_v0_finish_frame` + `parse_key_fields`). Returns None when invalid (gated).
|
||||
fn finish_frame(&self) -> Option<DecodedSignal> {
|
||||
let key = u64::from_be_bytes([
|
||||
self.raw[0],
|
||||
self.raw[1],
|
||||
self.raw[2],
|
||||
self.raw[3],
|
||||
self.raw[4],
|
||||
self.raw[5],
|
||||
self.raw[6],
|
||||
self.raw[7],
|
||||
]);
|
||||
let tail = ((self.raw[8] as u16) << 8) | self.raw[9] as u16;
|
||||
|
||||
let (check_ok, tail_ok) = Self::validate_frame(key, tail, self.extra_bit);
|
||||
if !(check_ok && tail_ok) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let b = key.to_be_bytes();
|
||||
let signature = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
|
||||
let serial = ((b[3] as u32) << 16) | ((b[4] as u32) << 8) | b[5] as u32;
|
||||
let count = Self::count_from_bytes(&b);
|
||||
let button = Self::button_from_signature(signature);
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(count as u16),
|
||||
crc_valid: true, // check + tail + reserved-bits validated above
|
||||
data: key,
|
||||
data_count_bit: COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
// Stash the 16-bit tail so the encoder can rebuild the full frame without recomputation.
|
||||
extra: Some(tail as u64),
|
||||
protocol_display_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Map KAT's generic button command to a Land Rover signature (Lock/Unlock).
|
||||
/// KAT: Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08. Land Rover V0 only defines Lock/Unlock,
|
||||
/// so Trunk/Panic have no signature and fall back to the decoded frame's signature in `encode`.
|
||||
fn signature_from_button(button: u8) -> u32 {
|
||||
match button {
|
||||
0x01 => SIG_LOCK, // KAT Lock
|
||||
0x02 => SIG_UNLOCK, // KAT Unlock
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the 64-bit key from fields (matches `land_rover_v0_build_key`).
|
||||
fn build_key(signature: u32, serial: u32, count: u32) -> u64 {
|
||||
let mut b = [0u8; 8];
|
||||
b[0] = (signature >> 16) as u8;
|
||||
b[1] = (signature >> 8) as u8;
|
||||
b[2] = signature as u8;
|
||||
b[3] = (serial >> 16) as u8;
|
||||
b[4] = (serial >> 8) as u8;
|
||||
b[5] = serial as u8;
|
||||
b[6] = (count >> 1) as u8;
|
||||
let counter_lsb = (count & 1) != 0;
|
||||
let check = Self::calculate_check(count);
|
||||
b[7] = (if counter_lsb { 0x80 } else { 0x00 }) | check;
|
||||
u64::from_be_bytes(b)
|
||||
}
|
||||
|
||||
fn enc_add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
||||
if let Some(last) = signal.last_mut() {
|
||||
if last.level == level {
|
||||
*last = LevelDuration::new(level, last.duration_us + duration);
|
||||
return;
|
||||
}
|
||||
}
|
||||
signal.push(LevelDuration::new(level, duration));
|
||||
}
|
||||
|
||||
/// Emit one differential-Manchester bit (matches `land_rover_v0_encoder_add_bit`).
|
||||
/// Returns the new `previous_bit`.
|
||||
fn enc_add_bit(signal: &mut Vec<LevelDuration>, previous_bit: bool, bit: bool) -> bool {
|
||||
match (previous_bit, bit) {
|
||||
(false, false) => {
|
||||
Self::enc_add_level(signal, true, TE_SHORT);
|
||||
Self::enc_add_level(signal, false, TE_SHORT);
|
||||
}
|
||||
(false, true) => {
|
||||
Self::enc_add_level(signal, true, TE_LONG);
|
||||
}
|
||||
(true, false) => {
|
||||
Self::enc_add_level(signal, false, TE_LONG);
|
||||
}
|
||||
(true, true) => {
|
||||
Self::enc_add_level(signal, false, TE_SHORT);
|
||||
Self::enc_add_level(signal, true, TE_SHORT);
|
||||
}
|
||||
}
|
||||
bit
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for LandRoverV0Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Land Rover V0"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.preamble_count = 0;
|
||||
self.raw = [0; 10];
|
||||
self.bit_count = 0;
|
||||
self.extra_bit = false;
|
||||
self.previous_bit = true;
|
||||
self.boundary_pad_skipped = false;
|
||||
self.pending_short = false;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && Self::is_short(duration) {
|
||||
self.preamble_count = 0;
|
||||
self.step = DecoderStep::PreambleLow;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::PreambleLow => {
|
||||
if !level && Self::is_short(duration) {
|
||||
self.preamble_count += 1;
|
||||
self.step = DecoderStep::PreambleHigh;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::PreambleHigh => {
|
||||
if level && Self::is_short(duration) {
|
||||
self.step = DecoderStep::PreambleLow;
|
||||
} else if level
|
||||
&& Self::is_sync(duration)
|
||||
&& self.preamble_count >= MIN_PREAMBLE_PAIRS
|
||||
{
|
||||
self.step = DecoderStep::SyncLow;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::SyncLow => {
|
||||
if !level && Self::is_sync(duration) {
|
||||
self.begin_frame();
|
||||
self.add_decoded_bit(true); // seed bit 0 = 1
|
||||
self.step = DecoderStep::Data;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Data => {
|
||||
if !self.process_transition(level, duration) {
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.bit_count as usize == COUNT_BIT {
|
||||
let result = self.finish_frame();
|
||||
self.step = DecoderStep::Reset;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
// Derive fields from the decoded signal, preferring the requested button's signature.
|
||||
let serial = decoded.serial? & 0x00FF_FFFF;
|
||||
let count = (decoded.counter.unwrap_or(0) as u32) & 0x1FF;
|
||||
|
||||
// Pick the command signature: requested button first, else the decoded frame's signature.
|
||||
let mut signature = Self::signature_from_button(button);
|
||||
if signature == 0 {
|
||||
let b = decoded.data.to_be_bytes();
|
||||
signature = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
|
||||
// If still not a known Land Rover signature, refuse (matches C: command_signature==0 → error).
|
||||
if Self::button_from_signature(signature) == LR_BTN_UNKNOWN {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let key = Self::build_key(signature, serial, count);
|
||||
let key_bytes = key.to_be_bytes();
|
||||
let tail = Self::calculate_tail(count);
|
||||
|
||||
// Capacity: preamble pairs (2 levels) + sync (3) + ~81 bits (≤2 levels each) + gap.
|
||||
let mut signal = Vec::with_capacity(TX_PREAMBLE_PAIRS * 2 + 3 + COUNT_BIT * 2 + 1);
|
||||
|
||||
// Preamble: alternating short high/low pairs.
|
||||
for _ in 0..TX_PREAMBLE_PAIRS {
|
||||
Self::enc_add_level(&mut signal, true, TE_SHORT);
|
||||
Self::enc_add_level(&mut signal, false, TE_SHORT);
|
||||
}
|
||||
|
||||
// Sync: high 750, low 750, then a boundary short-high pad (skipped by the decoder).
|
||||
Self::enc_add_level(&mut signal, true, SYNC_US);
|
||||
Self::enc_add_level(&mut signal, false, SYNC_US);
|
||||
Self::enc_add_level(&mut signal, true, TE_SHORT);
|
||||
|
||||
// Differential-Manchester body. previous_bit starts true (bit 0 = 1 is implied by the sync
|
||||
// trailing short). Bit index 1 is forced to 0 (matches the C build_upload), then bits 2..63
|
||||
// come from the key bytes.
|
||||
let mut previous_bit = true;
|
||||
previous_bit = Self::enc_add_bit(&mut signal, previous_bit, false);
|
||||
for bit_index in 2..64u8 {
|
||||
let byte_index = (bit_index / 8) as usize;
|
||||
let bit_in_byte = 7 - (bit_index % 8);
|
||||
let bit = (key_bytes[byte_index] >> bit_in_byte) & 1 != 0;
|
||||
previous_bit = Self::enc_add_bit(&mut signal, previous_bit, bit);
|
||||
}
|
||||
|
||||
// 16-bit tail, MSB first.
|
||||
for bit_index in 0..16u8 {
|
||||
let bit = (tail >> (15 - bit_index)) & 1 != 0;
|
||||
previous_bit = Self::enc_add_bit(&mut signal, previous_bit, bit);
|
||||
}
|
||||
|
||||
// Trailing extra bit = 1.
|
||||
let _ = Self::enc_add_bit(&mut signal, previous_bit, true);
|
||||
|
||||
// Inter-frame gap.
|
||||
Self::enc_add_level(&mut signal, false, GAP_US);
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LandRoverV0Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Drive a full encoded upload through the decoder and return the first decode.
|
||||
fn decode_upload(upload: &[LevelDuration]) -> Option<DecodedSignal> {
|
||||
let mut dec = LandRoverV0Decoder::new();
|
||||
for ld in upload {
|
||||
if let Some(sig) = dec.feed(ld.level, ld.duration_us) {
|
||||
return Some(sig);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The 3-bit check polynomial matches the C reference for a couple of hand traces.
|
||||
#[test]
|
||||
fn check_polynomial_matches_reference() {
|
||||
// count = 0: c0=0, c1=(0^1)&1=1, c2=0 → 0b010 = 2
|
||||
assert_eq!(LandRoverV0Decoder::calculate_check(0), 0b010);
|
||||
// Spot-check internal consistency: build_key embeds calculate_check in byte 7.
|
||||
for count in 0..0x200u32 {
|
||||
let key = LandRoverV0Decoder::build_key(SIG_UNLOCK, 0x123456, count);
|
||||
let b = key.to_be_bytes();
|
||||
assert_eq!(
|
||||
b[7] & 0x07,
|
||||
LandRoverV0Decoder::calculate_check(count),
|
||||
"check embedded in key byte 7 must equal calculate_check(count) for count={count}"
|
||||
);
|
||||
assert_eq!(b[7] & 0x78, 0, "reserved bits must be zero for count={count}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The tail is 0xFFFF or 0x7FFF per the counter parity, matching the reference.
|
||||
#[test]
|
||||
fn tail_matches_reference() {
|
||||
for count in 0..0x200u32 {
|
||||
let tail = LandRoverV0Decoder::calculate_tail(count);
|
||||
assert!(tail == 0xFFFF || tail == 0x7FFF);
|
||||
let msb_set = (tail >> 15) & 1 == 1;
|
||||
assert_eq!(msb_set, LandRoverV0Decoder::calculate_tail_msb(count));
|
||||
}
|
||||
}
|
||||
|
||||
/// Primary correctness check: encode an Unlock frame and decode it back. The Unlock signature
|
||||
/// 0xA285E3 satisfies the encoder's forced-bit-1 = 0 invariant, so it round-trips cleanly:
|
||||
/// serial, button, counter, key and the 3-bit check all survive.
|
||||
#[test]
|
||||
fn unlock_round_trip_preserves_fields() {
|
||||
let serial = 0x00AB_CDEF & 0x00FF_FFFF;
|
||||
let counter = 0x123u16; // 9-bit counter
|
||||
let key = LandRoverV0Decoder::build_key(SIG_UNLOCK, serial, counter as u32);
|
||||
|
||||
let decoded_in = DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(LR_BTN_UNLOCK),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data: key,
|
||||
data_count_bit: COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
extra: Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64),
|
||||
protocol_display_name: None,
|
||||
};
|
||||
|
||||
let dec = LandRoverV0Decoder::new();
|
||||
// KAT Unlock command = 0x02.
|
||||
let upload = dec.encode(&decoded_in, 0x02).expect("encode Unlock");
|
||||
let out = decode_upload(&upload).expect("decode the encoded Unlock frame");
|
||||
|
||||
assert!(out.crc_valid, "decoded frame must pass check+tail gating");
|
||||
assert_eq!(out.data, key, "64-bit key must survive the round trip");
|
||||
assert_eq!(out.serial, Some(serial), "serial must survive");
|
||||
assert_eq!(out.counter, Some(counter), "counter must survive");
|
||||
assert_eq!(out.button, Some(LR_BTN_UNLOCK), "Unlock button must survive");
|
||||
assert_eq!(out.data_count_bit, COUNT_BIT);
|
||||
// The stashed tail must match the counter-derived tail.
|
||||
assert_eq!(out.extra, Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64));
|
||||
// The embedded 3-bit check must match the recomputed value.
|
||||
let b = out.data.to_be_bytes();
|
||||
assert_eq!(b[7] & 0x07, LandRoverV0Decoder::calculate_check(counter as u32));
|
||||
assert_eq!(b[7] & 0x78, 0, "reserved bits zero");
|
||||
}
|
||||
|
||||
/// Round-trip across many serial/counter values for Unlock.
|
||||
#[test]
|
||||
fn unlock_round_trip_many() {
|
||||
let mut state: u32 = 0x1357_9BDF;
|
||||
let mut next = || {
|
||||
// xorshift for deterministic pseudo-random coverage
|
||||
state ^= state << 13;
|
||||
state ^= state >> 17;
|
||||
state ^= state << 5;
|
||||
state
|
||||
};
|
||||
|
||||
for _ in 0..256 {
|
||||
let serial = next() & 0x00FF_FFFF;
|
||||
let counter = (next() & 0x1FF) as u16;
|
||||
let key = LandRoverV0Decoder::build_key(SIG_UNLOCK, serial, counter as u32);
|
||||
let decoded_in = DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(LR_BTN_UNLOCK),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data: key,
|
||||
data_count_bit: COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
extra: Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64),
|
||||
protocol_display_name: None,
|
||||
};
|
||||
let dec = LandRoverV0Decoder::new();
|
||||
let upload = dec.encode(&decoded_in, 0x02).expect("encode");
|
||||
let out = decode_upload(&upload)
|
||||
.unwrap_or_else(|| panic!("decode failed for serial={serial:06X} counter={counter:03X}"));
|
||||
assert_eq!(out.data, key);
|
||||
assert_eq!(out.serial, Some(serial));
|
||||
assert_eq!(out.counter, Some(counter));
|
||||
assert_eq!(out.button, Some(LR_BTN_UNLOCK));
|
||||
assert!(out.crc_valid);
|
||||
}
|
||||
}
|
||||
|
||||
/// Faithful-port note: the reference encoder forces frame bit 1 = 0. The Lock signature
|
||||
/// 0xC20363 has bit 1 = 1, so the emitted frame's signature becomes 0x820363, which maps to
|
||||
/// LR_BTN_UNKNOWN. We assert this exact reference behaviour rather than a "fixed" version.
|
||||
#[test]
|
||||
fn lock_signature_forced_bit_matches_reference_quirk() {
|
||||
let serial = 0x0012_3456;
|
||||
let counter = 100u16;
|
||||
let key = LandRoverV0Decoder::build_key(SIG_LOCK, serial, counter as u32);
|
||||
let decoded_in = DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(LR_BTN_LOCK),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data: key,
|
||||
data_count_bit: COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
extra: Some(LandRoverV0Decoder::calculate_tail(counter as u32) as u64),
|
||||
protocol_display_name: None,
|
||||
};
|
||||
let dec = LandRoverV0Decoder::new();
|
||||
// KAT Lock command = 0x01.
|
||||
let upload = dec.encode(&decoded_in, 0x01).expect("encode Lock");
|
||||
let out = decode_upload(&upload).expect("frame still decodes (check/tail valid)");
|
||||
// Top byte 0xC2 -> 0x82 because bit 1 is forced to 0 by the encoder.
|
||||
let b = out.data.to_be_bytes();
|
||||
assert_eq!(b[0], 0x82, "encoder forces frame bit 1 = 0, turning 0xC2 into 0x82");
|
||||
assert_eq!(out.button, Some(LR_BTN_UNKNOWN), "0x820363 is not a known signature");
|
||||
// Serial / counter / check are still intact.
|
||||
assert_eq!(out.serial, Some(serial));
|
||||
assert_eq!(out.counter, Some(counter));
|
||||
assert!(out.crc_valid);
|
||||
}
|
||||
|
||||
/// A frame with a deliberately wrong check must NOT decode (gating prevents false matches).
|
||||
#[test]
|
||||
fn bad_check_is_rejected() {
|
||||
let serial = 0x00AB_CDEF;
|
||||
let counter = 0x055u16;
|
||||
let mut key = LandRoverV0Decoder::build_key(SIG_UNLOCK, serial, counter as u32);
|
||||
// Corrupt the 3-bit check in byte 7 (XOR a bit so it no longer matches).
|
||||
key ^= 0x01;
|
||||
let decoded_in = DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(LR_BTN_UNLOCK),
|
||||
counter: Some(counter),
|
||||
crc_valid: false,
|
||||
data: key,
|
||||
data_count_bit: COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
};
|
||||
// Hand-build an upload that transmits this corrupted key verbatim (no re-derivation):
|
||||
// reuse the encoder's level helpers but bypass build_key by injecting the raw key.
|
||||
let key_bytes = key.to_be_bytes();
|
||||
let tail = LandRoverV0Decoder::calculate_tail(counter as u32);
|
||||
let mut signal: Vec<LevelDuration> = Vec::new();
|
||||
for _ in 0..TX_PREAMBLE_PAIRS {
|
||||
LandRoverV0Decoder::enc_add_level(&mut signal, true, TE_SHORT);
|
||||
LandRoverV0Decoder::enc_add_level(&mut signal, false, TE_SHORT);
|
||||
}
|
||||
LandRoverV0Decoder::enc_add_level(&mut signal, true, SYNC_US);
|
||||
LandRoverV0Decoder::enc_add_level(&mut signal, false, SYNC_US);
|
||||
LandRoverV0Decoder::enc_add_level(&mut signal, true, TE_SHORT);
|
||||
let mut prev = true;
|
||||
prev = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, false);
|
||||
for bit_index in 2..64u8 {
|
||||
let bi = (bit_index / 8) as usize;
|
||||
let bib = 7 - (bit_index % 8);
|
||||
let bit = (key_bytes[bi] >> bib) & 1 != 0;
|
||||
prev = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, bit);
|
||||
}
|
||||
for bit_index in 0..16u8 {
|
||||
let bit = (tail >> (15 - bit_index)) & 1 != 0;
|
||||
prev = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, bit);
|
||||
}
|
||||
let _ = LandRoverV0Decoder::enc_add_bit(&mut signal, prev, true);
|
||||
LandRoverV0Decoder::enc_add_level(&mut signal, false, GAP_US);
|
||||
|
||||
// The decoded count comes from the (corrupted) byte 7, so the embedded check now mismatches
|
||||
// calculate_check(count) → rejected. Note: corrupting bit 0 of the check does not change
|
||||
// the counter (counter uses byte 7 MSB only), so calculate_check(count) stays the same.
|
||||
let _ = decoded_in;
|
||||
assert!(
|
||||
decode_upload(&signal).is_none(),
|
||||
"frame with a corrupted check must be rejected by the gate"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
//! Mazda Siemens protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from Flipper-ARF: `lib/subghz/protocols/mazda_siemens.c` / `mazda_siemens.h`
|
||||
//! (`SUBGHZ_PROTOCOL_MAZDA_SIEMENS_NAME = "MazdaSiemens"`). This is the Siemens/VDO keyfob
|
||||
//! cipher used on some Mazda vehicles — a DIFFERENT protocol from KAT's existing "Mazda V0"
|
||||
//! (Pandora) decoder, even though both ride a 250/500µs pair-based stream at 433.92 MHz FM.
|
||||
//!
|
||||
//! Profile (matches the C const block):
|
||||
//! - te_short = 250µs, te_long = 500µs, te_delta = 100µs, 64-bit frame.
|
||||
//! - Pair-based decoder: `feed()` ignores `level` and interprets raw durations in pairs
|
||||
//! (`process_pair`), collecting bits with *inverted* polarity (`state_bit == 0` → stored 1).
|
||||
//! - Preamble: ≥13 short/short pairs, then a short→long transition starts data; the first
|
||||
//! collected bit is a 1 (sync). A 14-byte buffer accumulates; on a non-matching pair the
|
||||
//! frame is checked: discard the leading sync byte, take 8 bytes, deobfuscate, validate.
|
||||
//! - Siemens obfuscation (the layer ported here, `mazda_xor_deobfuscate`):
|
||||
//! parity = byte_parity(data[7]); odd → mask = data[6], XOR bytes 0..6;
|
||||
//! even → mask = data[5], XOR bytes 0..5 and byte 6. Then bit-deinterleave bytes 5/6
|
||||
//! via `(old5 & 0xAA)|(old6 & 0x55)` / `(old5 & 0x55)|(old6 & 0xAA)`.
|
||||
//! The inner Siemens cipher's plaintext is left as-is (no key); only this obfuscation/
|
||||
//! interleave layer is reversed, matching the C.
|
||||
//! - Gate: additive checksum `sum(data[0..7]) == data[7]` (matches the C `mazda_check_completion`),
|
||||
//! plus the structural preamble/sync/bit-count constraints. This is what keeps it from
|
||||
//! false-matching other 250/500µs Manchester protocols.
|
||||
//! - Fields (`mazda_parse_data`): serial = data >> 32, button = (data >> 24) & 0xFF,
|
||||
//! counter = (data >> 8) & 0xFFFF. Button codes: 0x10 Lock, 0x20 Unlock, 0x40 Trunk.
|
||||
//! - RF: FM (`SubGhzProtocolFlag_FM`); 433.92 MHz only (`SubGhzProtocolFlag_433`).
|
||||
//!
|
||||
//! Encoder (`subghz_protocol_encoder_mazda_siemens_get_upload`): increments the counter byte,
|
||||
//! recomputes the checksum, bit-interleaves + XORs (`mazda_xor_obfuscate`), then emits a 12-byte
|
||||
//! 0xFF preamble, a 50ms gap, `0xFF 0xFF 0xD7`, the 8 obfuscated bytes transmitted as `255 - byte`,
|
||||
//! a `0x5A` tail byte, and a trailing 50ms gap. Manchester per byte: bit 1 → (H,L), bit 0 → (L,H),
|
||||
//! all at te_short. Implemented faithfully in `encode()`.
|
||||
//!
|
||||
//! Note: as in the C, the decoder and encoder are NOT a clean raw-timing round-trip pair (the
|
||||
//! decoder targets real fob air-format; the encoder generates a TX upload). The encode↔decode
|
||||
//! unit test therefore validates the obfuscate/deobfuscate (XOR + interleave) cipher layer,
|
||||
//! which is a true inverse.
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
|
||||
const PREAMBLE_MIN: u16 = 13;
|
||||
const COMPLETION_MIN: u16 = 80;
|
||||
const COMPLETION_MAX: u16 = 105;
|
||||
const DATA_BUFFER_SIZE: usize = 14;
|
||||
|
||||
// Encoder constants (mazda_siemens.c).
|
||||
const TX_PREAMBLE_BYTES: usize = 12;
|
||||
const TX_GAP_US: u32 = 50_000;
|
||||
const TX_SYNC_BYTE: u8 = 0xD7;
|
||||
const TX_TAIL_BYTE: u8 = 0x5A;
|
||||
|
||||
/// Button codes carried in the frame (mazda_get_btn_name in the C).
|
||||
const BTN_LOCK: u8 = 0x10;
|
||||
const BTN_UNLOCK: u8 = 0x20;
|
||||
const BTN_TRUNK: u8 = 0x40;
|
||||
|
||||
/// Decoder states (matches mazda_siemens.c MazdaSiemensDecoderStep).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
PreambleSave,
|
||||
PreambleCheck,
|
||||
DataSave,
|
||||
DataCheck,
|
||||
}
|
||||
|
||||
/// Mazda Siemens protocol decoder.
|
||||
pub struct MazdaSiemensDecoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
preamble_count: u16,
|
||||
bit_counter: u16,
|
||||
prev_state: u8,
|
||||
data_buffer: [u8; DATA_BUFFER_SIZE],
|
||||
}
|
||||
|
||||
impl MazdaSiemensDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
preamble_count: 0,
|
||||
bit_counter: 0,
|
||||
prev_state: 0,
|
||||
data_buffer: [0u8; DATA_BUFFER_SIZE],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_short(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_long(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
}
|
||||
|
||||
/// Collect one bit into the buffer with inverted polarity (mazda_collect_bit).
|
||||
/// `state_bit == 0` stores a 1.
|
||||
fn collect_bit(&mut self, state_bit: u8) {
|
||||
let byte_idx = (self.bit_counter >> 3) as usize;
|
||||
if byte_idx < DATA_BUFFER_SIZE {
|
||||
self.data_buffer[byte_idx] <<= 1;
|
||||
if state_bit == 0 {
|
||||
self.data_buffer[byte_idx] |= 1;
|
||||
}
|
||||
}
|
||||
self.bit_counter += 1;
|
||||
}
|
||||
|
||||
/// Process a duration pair (mazda_process_pair). Returns true if the pair was valid.
|
||||
fn process_pair(&mut self, dur_first: u32, dur_second: u32) -> bool {
|
||||
let first_short = Self::is_short(dur_first);
|
||||
let first_long = Self::is_long(dur_first);
|
||||
let second_short = Self::is_short(dur_second);
|
||||
let second_long = Self::is_long(dur_second);
|
||||
|
||||
if first_long && second_short {
|
||||
self.collect_bit(0);
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if first_short && second_long {
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if first_short && second_short {
|
||||
let ps = self.prev_state;
|
||||
self.collect_bit(ps);
|
||||
return true;
|
||||
}
|
||||
|
||||
if first_long && second_long {
|
||||
self.collect_bit(0);
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Validate a complete frame (mazda_check_completion). On success returns a DecodedSignal.
|
||||
fn check_completion(&self) -> Option<DecodedSignal> {
|
||||
if self.bit_counter < COMPLETION_MIN || self.bit_counter > COMPLETION_MAX {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Shift buffer by 1 byte (discard the sync/header byte).
|
||||
let mut data = [0u8; 8];
|
||||
for i in 0..8 {
|
||||
data[i] = self.data_buffer[i + 1];
|
||||
}
|
||||
|
||||
Self::xor_deobfuscate(&mut data);
|
||||
|
||||
// Additive checksum: sum(data[0..7]) must equal data[7].
|
||||
let mut checksum: u8 = 0;
|
||||
for &b in data.iter().take(7) {
|
||||
checksum = checksum.wrapping_add(b);
|
||||
}
|
||||
if checksum != data[7] {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Pack into u64 (big-endian byte order).
|
||||
let packed = u64::from_be_bytes(data);
|
||||
|
||||
let (serial, button, counter) = Self::parse_fields(packed);
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data: packed,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Field extraction (mazda_parse_data).
|
||||
fn parse_fields(packed: u64) -> (u32, u8, u16) {
|
||||
let serial = (packed >> 32) as u32;
|
||||
let button = ((packed >> 24) & 0xFF) as u8;
|
||||
let counter = ((packed >> 8) & 0xFFFF) as u16;
|
||||
(serial, button, counter)
|
||||
}
|
||||
|
||||
/// Byte parity: XOR-fold to a single bit (mazda_byte_parity).
|
||||
fn byte_parity(mut val: u8) -> u8 {
|
||||
val ^= val >> 4;
|
||||
val ^= val >> 2;
|
||||
val ^= val >> 1;
|
||||
val & 1
|
||||
}
|
||||
|
||||
/// Siemens RX deobfuscation (mazda_xor_deobfuscate):
|
||||
/// parity-dependent XOR mask, then deinterleave bytes 5/6.
|
||||
fn xor_deobfuscate(data: &mut [u8; 8]) {
|
||||
let parity = Self::byte_parity(data[7]);
|
||||
|
||||
if parity != 0 {
|
||||
// Odd parity: mask = byte[6], XOR bytes 0..6.
|
||||
let mask = data[6];
|
||||
for i in 0..6 {
|
||||
data[i] ^= mask;
|
||||
}
|
||||
} else {
|
||||
// Even parity: mask = byte[5], XOR bytes 0..5 and byte[6].
|
||||
let mask = data[5];
|
||||
for i in 0..5 {
|
||||
data[i] ^= mask;
|
||||
}
|
||||
data[6] ^= mask;
|
||||
}
|
||||
|
||||
// Bit deinterleave bytes 5/6.
|
||||
let old5 = data[5];
|
||||
let old6 = data[6];
|
||||
data[5] = (old5 & 0xAA) | (old6 & 0x55);
|
||||
data[6] = (old5 & 0x55) | (old6 & 0xAA);
|
||||
}
|
||||
|
||||
/// Siemens TX obfuscation (mazda_xor_obfuscate): interleave bytes 5/6, then
|
||||
/// parity-dependent XOR mask. Inverse of `xor_deobfuscate`.
|
||||
fn xor_obfuscate(data: &mut [u8; 8]) {
|
||||
let old5 = data[5];
|
||||
let old6 = data[6];
|
||||
data[5] = (old5 & 0xAA) | (old6 & 0x55);
|
||||
data[6] = (old5 & 0x55) | (old6 & 0xAA);
|
||||
|
||||
let parity = Self::byte_parity(data[7]);
|
||||
|
||||
if parity != 0 {
|
||||
let mask = data[6];
|
||||
for i in 0..6 {
|
||||
data[i] ^= mask;
|
||||
}
|
||||
} else {
|
||||
let mask = data[5];
|
||||
for i in 0..5 {
|
||||
data[i] ^= mask;
|
||||
}
|
||||
data[6] ^= mask;
|
||||
}
|
||||
}
|
||||
|
||||
/// Button name for display (mazda_get_btn_name).
|
||||
#[allow(dead_code)]
|
||||
fn get_button_name(btn: u8) -> &'static str {
|
||||
match btn {
|
||||
BTN_LOCK => "Lock",
|
||||
BTN_UNLOCK => "Unlock",
|
||||
BTN_TRUNK => "Trunk",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Map KAT's generic button command to a Mazda Siemens frame button code.
|
||||
fn map_button(button: u8) -> u8 {
|
||||
match button {
|
||||
0x01 => BTN_LOCK, // Lock
|
||||
0x02 => BTN_UNLOCK, // Unlock
|
||||
0x04 => BTN_TRUNK, // Trunk
|
||||
BTN_LOCK | BTN_UNLOCK | BTN_TRUNK => button, // already a frame code
|
||||
_ => BTN_UNLOCK,
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode one byte as Manchester, MSB-first: bit 1 → (H,L), bit 0 → (L,H) (mazda_encode_byte).
|
||||
fn enc_byte(signal: &mut Vec<LevelDuration>, byte: u8) {
|
||||
for bit in (0..8).rev() {
|
||||
if (byte >> bit) & 1 != 0 {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for MazdaSiemensDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Mazda Siemens"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
// SubGhzProtocolFlag_433 only.
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.preamble_count = 0;
|
||||
self.bit_counter = 0;
|
||||
self.prev_state = 0;
|
||||
self.data_buffer = [0u8; DATA_BUFFER_SIZE];
|
||||
}
|
||||
|
||||
fn feed(&mut self, _level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if Self::is_short(duration) {
|
||||
self.te_last = duration;
|
||||
self.preamble_count = 0;
|
||||
self.step = DecoderStep::PreambleCheck;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::PreambleSave => {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::PreambleCheck;
|
||||
}
|
||||
|
||||
DecoderStep::PreambleCheck => {
|
||||
if Self::is_short(self.te_last) && Self::is_short(duration) {
|
||||
self.preamble_count += 1;
|
||||
self.step = DecoderStep::PreambleSave;
|
||||
} else if Self::is_short(self.te_last)
|
||||
&& Self::is_long(duration)
|
||||
&& self.preamble_count >= PREAMBLE_MIN
|
||||
{
|
||||
// Preamble → data: seed the leading sync bit (a 1).
|
||||
self.bit_counter = 1;
|
||||
self.data_buffer = [0u8; DATA_BUFFER_SIZE];
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 0;
|
||||
self.step = DecoderStep::DataSave;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DataSave => {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::DataCheck;
|
||||
}
|
||||
|
||||
DecoderStep::DataCheck => {
|
||||
if self.process_pair(self.te_last, duration) {
|
||||
self.step = DecoderStep::DataSave;
|
||||
} else {
|
||||
let result = self.check_completion();
|
||||
self.step = DecoderStep::Reset;
|
||||
if result.is_some() {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
// Rebuild the 8 cleartext bytes from the decoded 64-bit word.
|
||||
let mut data = decoded.data.to_be_bytes();
|
||||
|
||||
// Apply the requested button into the frame's button field (byte index 3 = data >> 24).
|
||||
data[3] = Self::map_button(button);
|
||||
|
||||
// Increment the counter byte (mazda_siemens.c get_upload): data[6]++ with carry into data[5].
|
||||
let (new6, carry) = data[6].overflowing_add(1);
|
||||
data[6] = new6;
|
||||
if carry {
|
||||
data[5] = data[5].wrapping_add(1);
|
||||
}
|
||||
|
||||
// Recompute the additive checksum over bytes 0..7.
|
||||
let mut checksum: u8 = 0;
|
||||
for &b in data.iter().take(7) {
|
||||
checksum = checksum.wrapping_add(b);
|
||||
}
|
||||
data[7] = checksum;
|
||||
|
||||
// Obfuscate (interleave + XOR) for transmission.
|
||||
let mut tx_data = data;
|
||||
Self::xor_obfuscate(&mut tx_data);
|
||||
|
||||
// Build the upload: 12x 0xFF preamble, gap, 0xFF 0xFF, sync 0xD7,
|
||||
// 8 data bytes transmitted inverted (255 - byte), tail 0x5A, trailing gap.
|
||||
let mut signal: Vec<LevelDuration> = Vec::with_capacity((TX_PREAMBLE_BYTES + 12) * 16 + 4);
|
||||
for _ in 0..TX_PREAMBLE_BYTES {
|
||||
Self::enc_byte(&mut signal, 0xFF);
|
||||
}
|
||||
signal.push(LevelDuration::new(false, TX_GAP_US));
|
||||
Self::enc_byte(&mut signal, 0xFF);
|
||||
Self::enc_byte(&mut signal, 0xFF);
|
||||
Self::enc_byte(&mut signal, TX_SYNC_BYTE);
|
||||
for &b in tx_data.iter() {
|
||||
Self::enc_byte(&mut signal, 255 - b);
|
||||
}
|
||||
Self::enc_byte(&mut signal, TX_TAIL_BYTE);
|
||||
signal.push(LevelDuration::new(false, TX_GAP_US));
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MazdaSiemensDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The obfuscate/deobfuscate (XOR + bit-interleave) layer is a true inverse for every
|
||||
/// valid cleartext (checksum byte fixes the parity branch). This is the cipher-layer
|
||||
/// round-trip the decoder relies on; validates the ported Siemens obfuscation.
|
||||
#[test]
|
||||
fn xor_layer_round_trips() {
|
||||
// Deterministic LCG over many cleartexts: 7 data bytes + matching additive checksum.
|
||||
let mut seed: u32 = 0x1234_5678;
|
||||
let mut next = || {
|
||||
seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
|
||||
(seed >> 16) as u8
|
||||
};
|
||||
for _ in 0..20_000 {
|
||||
let mut clear = [0u8; 8];
|
||||
let mut sum: u8 = 0;
|
||||
for b in clear.iter_mut().take(7) {
|
||||
*b = next();
|
||||
sum = sum.wrapping_add(*b);
|
||||
}
|
||||
clear[7] = sum;
|
||||
|
||||
let mut buf = clear;
|
||||
MazdaSiemensDecoder::xor_obfuscate(&mut buf);
|
||||
MazdaSiemensDecoder::xor_deobfuscate(&mut buf);
|
||||
assert_eq!(buf, clear, "obfuscate→deobfuscate must be identity");
|
||||
}
|
||||
}
|
||||
|
||||
/// Deobfuscate is the exact inverse of obfuscate for both parity branches (odd: byte7
|
||||
/// has odd popcount; even: zero). Pins the parity-dependent mask selection.
|
||||
#[test]
|
||||
fn xor_both_parity_branches() {
|
||||
// Even-parity checksum byte (0x00 → parity 0): mask = byte[5].
|
||||
let mut even = [0x11u8, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x00];
|
||||
let orig_even = even;
|
||||
MazdaSiemensDecoder::xor_obfuscate(&mut even);
|
||||
MazdaSiemensDecoder::xor_deobfuscate(&mut even);
|
||||
assert_eq!(even, orig_even);
|
||||
assert_eq!(MazdaSiemensDecoder::byte_parity(0x00), 0);
|
||||
|
||||
// Odd-parity checksum byte (0x01 → parity 1): mask = byte[6].
|
||||
let mut odd = [0xAAu8, 0xBB, 0xCC, 0xDD, 0xEE, 0x12, 0x34, 0x01];
|
||||
let orig_odd = odd;
|
||||
MazdaSiemensDecoder::xor_obfuscate(&mut odd);
|
||||
MazdaSiemensDecoder::xor_deobfuscate(&mut odd);
|
||||
assert_eq!(odd, orig_odd);
|
||||
assert_eq!(MazdaSiemensDecoder::byte_parity(0x01), 1);
|
||||
}
|
||||
|
||||
/// Field extraction matches mazda_parse_data: serial = >>32, button = >>24, counter = >>8.
|
||||
#[test]
|
||||
fn parse_fields_layout() {
|
||||
let packed: u64 = 0x1234_5678_2000_06_3A;
|
||||
let (serial, button, counter) = MazdaSiemensDecoder::parse_fields(packed);
|
||||
assert_eq!(serial, 0x1234_5678);
|
||||
assert_eq!(button, 0x20);
|
||||
assert_eq!(counter, 0x0006);
|
||||
}
|
||||
|
||||
/// The encoder produces a non-empty Manchester upload with the expected leading 0xFF
|
||||
/// preamble (all alternating short pulses) and a 50ms gap. Smoke test of the TX path.
|
||||
#[test]
|
||||
fn encode_produces_upload() {
|
||||
let dec = MazdaSiemensDecoder::new();
|
||||
let signal = DecodedSignal {
|
||||
serial: Some(0x1234_5678),
|
||||
button: Some(0x02),
|
||||
counter: Some(0x0005),
|
||||
crc_valid: true,
|
||||
data: 0x1234_5678_2000_05_00,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
};
|
||||
let up = dec.encode(&signal, 0x02).expect("encode should succeed");
|
||||
assert!(!up.is_empty());
|
||||
// First 16 pulses are the first 0xFF preamble byte: alternating H/L shorts.
|
||||
for (i, p) in up.iter().take(16).enumerate() {
|
||||
assert_eq!(p.duration_us, TE_SHORT);
|
||||
assert_eq!(p.level, i % 2 == 0, "0xFF Manchester alternates H,L starting high");
|
||||
}
|
||||
// A 50ms gap is present somewhere in the upload.
|
||||
assert!(up.iter().any(|p| p.duration_us == TX_GAP_US));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
//! Mazda V0 protocol decoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/mazda_v0.c`.
|
||||
//! Original implementation by @lupettohf.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - 433.92 MHz FM, decode-only (no encoder)
|
||||
//! - Pair-based decoding: feed() ignores level, processes raw durations in pairs
|
||||
//! - Preamble: minimum 13 short/short pairs before data starts
|
||||
//! - Data uses a 14-byte buffer with inverted bit polarity
|
||||
//! - XOR deobfuscation with parity-based mask selection + bit interleave swap
|
||||
//! - Additive checksum over first 7 bytes must equal byte 8
|
||||
//! - Field layout: serial (32-bit), button (8-bit), counter (16-bit)
|
||||
//! - Button codes: 0x10=Lock, 0x20=Unlock, 0x40=Trunk
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
|
||||
const PREAMBLE_MIN: u16 = 13;
|
||||
const COMPLETION_MIN: u16 = 80;
|
||||
const COMPLETION_MAX: u16 = 105;
|
||||
const DATA_BUFFER_SIZE: usize = 14;
|
||||
|
||||
/// Decoder states (matches mazda_v0.c MazdaDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
PreambleSave,
|
||||
PreambleCheck,
|
||||
DataSave,
|
||||
DataCheck,
|
||||
}
|
||||
|
||||
/// Mazda V0 protocol decoder
|
||||
pub struct MazdaV0Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
preamble_count: u16,
|
||||
bit_counter: u16,
|
||||
prev_state: u8,
|
||||
data_buffer: [u8; DATA_BUFFER_SIZE],
|
||||
}
|
||||
|
||||
impl MazdaV0Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
preamble_count: 0,
|
||||
bit_counter: 0,
|
||||
prev_state: 0,
|
||||
data_buffer: [0u8; DATA_BUFFER_SIZE],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_short(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_long(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
}
|
||||
|
||||
/// Collect a single bit into the data buffer.
|
||||
/// Inverted polarity: state_bit == 0 means stored bit is 1.
|
||||
fn collect_bit(&mut self, state_bit: u8) {
|
||||
let byte_idx = (self.bit_counter >> 3) as usize;
|
||||
if byte_idx < DATA_BUFFER_SIZE {
|
||||
self.data_buffer[byte_idx] <<= 1;
|
||||
if state_bit == 0 {
|
||||
self.data_buffer[byte_idx] |= 1;
|
||||
}
|
||||
}
|
||||
self.bit_counter += 1;
|
||||
}
|
||||
|
||||
/// Process a duration pair and collect bits.
|
||||
/// Returns true if the pair was valid, false otherwise.
|
||||
fn process_pair(&mut self, dur_first: u32, dur_second: u32) -> bool {
|
||||
let first_short = Self::is_short(dur_first);
|
||||
let first_long = Self::is_long(dur_first);
|
||||
let second_short = Self::is_short(dur_second);
|
||||
let second_long = Self::is_long(dur_second);
|
||||
|
||||
if first_long && second_short {
|
||||
self.collect_bit(0);
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if first_short && second_long {
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if first_short && second_short {
|
||||
let ps = self.prev_state;
|
||||
self.collect_bit(ps);
|
||||
return true;
|
||||
}
|
||||
|
||||
if first_long && second_long {
|
||||
self.collect_bit(0);
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check whether enough bits have been collected and validate the frame.
|
||||
/// On success, returns a DecodedSignal.
|
||||
fn check_completion(&self) -> Option<DecodedSignal> {
|
||||
if self.bit_counter < COMPLETION_MIN || self.bit_counter > COMPLETION_MAX {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Shift buffer by 1 byte (discard sync/header byte)
|
||||
let mut data = [0u8; 8];
|
||||
for i in 0..8 {
|
||||
data[i] = self.data_buffer[i + 1];
|
||||
}
|
||||
|
||||
// XOR deobfuscation
|
||||
Self::xor_deobfuscate(&mut data);
|
||||
|
||||
// Checksum: sum of data[0..7] must equal data[7]
|
||||
let mut checksum: u8 = 0;
|
||||
for i in 0..7 {
|
||||
checksum = checksum.wrapping_add(data[i]);
|
||||
}
|
||||
if checksum != data[7] {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Pack into u64
|
||||
let mut packed: u64 = 0;
|
||||
for i in 0..8 {
|
||||
packed = (packed << 8) | (data[i] as u64);
|
||||
}
|
||||
|
||||
// Parse fields
|
||||
let serial = (packed >> 32) as u32;
|
||||
let btn = ((packed >> 24) & 0xFF) as u8;
|
||||
let cnt = ((packed >> 8) & 0xFFFF) as u16;
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: Some(cnt),
|
||||
crc_valid: true,
|
||||
data: packed,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: false,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Byte parity: XOR-fold to single bit (matches mazda_byte_parity in C)
|
||||
fn byte_parity(mut value: u8) -> u8 {
|
||||
value ^= value >> 4;
|
||||
value ^= value >> 2;
|
||||
value ^= value >> 1;
|
||||
value & 1
|
||||
}
|
||||
|
||||
/// XOR deobfuscation with parity-based mask selection + bit interleave swap
|
||||
/// (matches mazda_xor_deobfuscate in C)
|
||||
fn xor_deobfuscate(data: &mut [u8; 8]) {
|
||||
let parity = Self::byte_parity(data[7]);
|
||||
|
||||
if parity != 0 {
|
||||
let mask = data[6];
|
||||
for i in 0..6 {
|
||||
data[i] ^= mask;
|
||||
}
|
||||
} else {
|
||||
let mask = data[5];
|
||||
for i in 0..5 {
|
||||
data[i] ^= mask;
|
||||
}
|
||||
data[6] ^= mask;
|
||||
}
|
||||
|
||||
// Bit interleave swap between bytes 5 and 6
|
||||
let old5 = data[5];
|
||||
let old6 = data[6];
|
||||
data[5] = (old5 & 0xAA) | (old6 & 0x55);
|
||||
data[6] = (old5 & 0x55) | (old6 & 0xAA);
|
||||
}
|
||||
|
||||
/// Get button name for display
|
||||
#[allow(dead_code)]
|
||||
fn get_button_name(btn: u8) -> &'static str {
|
||||
match btn {
|
||||
0x10 => "Lock",
|
||||
0x20 => "Unlock",
|
||||
0x40 => "Trunk",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for MazdaV0Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Mazda V0"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.preamble_count = 0;
|
||||
self.bit_counter = 0;
|
||||
self.prev_state = 0;
|
||||
self.te_last = 0;
|
||||
self.data_buffer = [0u8; DATA_BUFFER_SIZE];
|
||||
}
|
||||
|
||||
fn feed(&mut self, _level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if Self::is_short(duration) {
|
||||
self.te_last = duration;
|
||||
self.preamble_count = 0;
|
||||
self.step = DecoderStep::PreambleCheck;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::PreambleSave => {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::PreambleCheck;
|
||||
}
|
||||
|
||||
DecoderStep::PreambleCheck => {
|
||||
if Self::is_short(self.te_last) && Self::is_short(duration) {
|
||||
self.preamble_count += 1;
|
||||
self.step = DecoderStep::PreambleSave;
|
||||
} else if Self::is_short(self.te_last)
|
||||
&& Self::is_long(duration)
|
||||
&& self.preamble_count >= PREAMBLE_MIN
|
||||
{
|
||||
// Transition from preamble to data
|
||||
self.bit_counter = 1;
|
||||
self.data_buffer = [0u8; DATA_BUFFER_SIZE];
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 0;
|
||||
self.step = DecoderStep::DataSave;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DataSave => {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::DataCheck;
|
||||
}
|
||||
|
||||
DecoderStep::DataCheck => {
|
||||
if self.process_pair(self.te_last, duration) {
|
||||
self.step = DecoderStep::DataSave;
|
||||
} else {
|
||||
// Pair was invalid - check if we have a complete frame
|
||||
let result = self.check_completion();
|
||||
self.step = DecoderStep::Reset;
|
||||
if result.is_some() {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MazdaV0Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
//! Mitsubishi V0 protocol decoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/mitsubishi_v0.c`.
|
||||
//! Original implementation by @lupettohf.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: Short HIGH + Long LOW = bit 1, Long HIGH + Short LOW = bit 0
|
||||
//! - 96-bit frame (12 bytes), collected MSB-first into byte buffer
|
||||
//! - Level-aware state machine: HIGH pulses are saved, LOW pulses complete the pair
|
||||
//! - Unscramble: NOT first 8 bytes, extract counter from bytes[4..5], compute XOR mask, apply to bytes[0..5]
|
||||
//! - Field layout: serial = bytes[0..3] (32-bit), counter = bytes[4..5] (16-bit), button = byte[6]
|
||||
//! - 868 MHz, FM modulation, decode-only (no encoder)
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
#[allow(dead_code)]
|
||||
const MIN_COUNT_BIT: usize = 80;
|
||||
const BIT_COUNT: usize = 96;
|
||||
const DATA_BYTES: usize = 12;
|
||||
|
||||
/// Decoder states (matches mitsubishi_v0.c MitsubishiDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
DataSave,
|
||||
DataCheck,
|
||||
}
|
||||
|
||||
/// Mitsubishi V0 protocol decoder
|
||||
pub struct MitsubishiV0Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
bit_count: usize,
|
||||
decode_data: [u8; DATA_BYTES],
|
||||
}
|
||||
|
||||
impl MitsubishiV0Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
bit_count: 0,
|
||||
decode_data: [0u8; DATA_BYTES],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_short(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_long(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
}
|
||||
|
||||
fn reset_payload(&mut self) {
|
||||
self.bit_count = 0;
|
||||
self.decode_data = [0u8; DATA_BYTES];
|
||||
}
|
||||
|
||||
/// Collect a HIGH+LOW pair and decode one bit.
|
||||
/// Short HIGH + Long LOW = bit 1; Long HIGH + Short LOW = bit 0.
|
||||
/// Bits are stored MSB-first into the byte buffer.
|
||||
fn collect_pair(&mut self, high: u32, low: u32) -> bool {
|
||||
let bit_value;
|
||||
|
||||
if Self::is_short(high) && Self::is_long(low) {
|
||||
bit_value = true;
|
||||
} else if Self::is_long(high) && Self::is_short(low) {
|
||||
bit_value = false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
let bit_index = self.bit_count;
|
||||
if bit_index < BIT_COUNT {
|
||||
if bit_value {
|
||||
let byte_index = bit_index >> 3;
|
||||
let bit_position = 7 - (bit_index & 0x07);
|
||||
self.decode_data[byte_index] |= 1u8 << bit_position;
|
||||
}
|
||||
self.bit_count += 1;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Unscramble the payload (matches mitsubishi_unscramble_payload in C reference).
|
||||
/// 1. Bitwise NOT first 8 bytes
|
||||
/// 2. Extract counter from bytes[4..5]
|
||||
/// 3. Compute masks from counter and XOR bytes[0..5]
|
||||
fn unscramble_payload(payload: &mut [u8; DATA_BYTES]) {
|
||||
// Step 1: NOT first 8 bytes
|
||||
for i in 0..8 {
|
||||
payload[i] = !payload[i];
|
||||
}
|
||||
|
||||
// Step 2: Extract counter
|
||||
let counter = ((payload[4] as u16) << 8) | (payload[5] as u16);
|
||||
let hi = ((counter >> 8) & 0xFF) as u8;
|
||||
let lo = (counter & 0xFF) as u8;
|
||||
|
||||
// Step 3: Compute masks
|
||||
let mask1 = (hi & 0xAA) | (lo & 0x55);
|
||||
let mask2 = (lo & 0xAA) | (hi & 0x55);
|
||||
let mask3 = mask1 ^ mask2;
|
||||
|
||||
// Step 4: XOR bytes[0..5] with mask3
|
||||
for i in 0..5 {
|
||||
payload[i] ^= mask3;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the unscrambled payload into a DecodedSignal.
|
||||
fn publish_frame(&self) -> DecodedSignal {
|
||||
let mut payload = self.decode_data;
|
||||
Self::unscramble_payload(&mut payload);
|
||||
|
||||
let serial = ((payload[0] as u32) << 24)
|
||||
| ((payload[1] as u32) << 16)
|
||||
| ((payload[2] as u32) << 8)
|
||||
| (payload[3] as u32);
|
||||
|
||||
let counter = ((payload[4] as u16) << 8) | (payload[5] as u16);
|
||||
let button = payload[6];
|
||||
|
||||
// Store first 8 bytes (post-unscramble) as u64 data field
|
||||
let data = ((payload[0] as u64) << 56)
|
||||
| ((payload[1] as u64) << 48)
|
||||
| ((payload[2] as u64) << 40)
|
||||
| ((payload[3] as u64) << 32)
|
||||
| ((payload[4] as u64) << 24)
|
||||
| ((payload[5] as u64) << 16)
|
||||
| ((payload[6] as u64) << 8)
|
||||
| (payload[7] as u64);
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data,
|
||||
data_count_bit: BIT_COUNT,
|
||||
encoder_capable: false,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for MitsubishiV0Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Mitsubishi V0"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[868_350_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.reset_payload();
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
// Wait for a HIGH pulse to start
|
||||
if level {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::DataCheck;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DataSave => {
|
||||
if level {
|
||||
// HIGH pulse: save duration and move to DataCheck
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::DataCheck;
|
||||
} else {
|
||||
// LOW pulse without preceding HIGH data check: reset
|
||||
self.step = DecoderStep::Reset;
|
||||
self.reset_payload();
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DataCheck => {
|
||||
if !level {
|
||||
// LOW pulse: complete the HIGH+LOW pair
|
||||
if self.collect_pair(self.te_last, duration) {
|
||||
if self.bit_count >= BIT_COUNT {
|
||||
// Full frame received
|
||||
let result = self.publish_frame();
|
||||
self.reset_payload();
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
} else {
|
||||
self.step = DecoderStep::DataSave;
|
||||
}
|
||||
} else {
|
||||
// Invalid pair: reset
|
||||
self.reset_payload();
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
// Another HIGH pulse while expecting LOW: update te_last
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MitsubishiV0Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -32,14 +32,32 @@ 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;
|
||||
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;
|
||||
|
||||
@@ -104,16 +122,83 @@ 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()),
|
||||
Box::new(suzuki::SuzukiDecoder::new()),
|
||||
Box::new(scher_khan::ScherKhanDecoder::new()),
|
||||
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 }
|
||||
@@ -335,4 +420,208 @@ mod tests {
|
||||
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ford_v3_decodes_ldv_t80_sub() {
|
||||
// LDV T80 keyfobs use a Ford-V3-style 240/480µs Manchester frame.
|
||||
let path = Path::new("IMPORTS/LDV/LDV-T80_lock.sub");
|
||||
if !path.exists() {
|
||||
eprintln!("Skip: {:?} not found (run from crate root)", path);
|
||||
return;
|
||||
}
|
||||
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
|
||||
let pairs: Vec<LevelDuration> = raw_pairs
|
||||
.iter()
|
||||
.map(|p| LevelDuration::new(p.level, p.duration_us))
|
||||
.collect();
|
||||
let mut reg = ProtocolRegistry::new();
|
||||
let results = reg.process_signal_stream(&pairs, freq);
|
||||
assert!(
|
||||
results.iter().any(|(n, _, _)| n == "Ford V3"),
|
||||
"expected at least one Ford V3 decode from LDV-T80_lock.sub, got: {:?}",
|
||||
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn honda_static_decodes_unlock_honda_sub() {
|
||||
// Genuine Honda keyfob capture (CC1101 custom preset). Honda Static unpacks it via the
|
||||
// reverse-packet path (matches honda_static.c), yielding a consistent serial/counter.
|
||||
let path = Path::new("IMPORTS/honda/Unlock_honda.sub");
|
||||
if !path.exists() {
|
||||
eprintln!("Skip: {:?} not found (run from crate root)", path);
|
||||
return;
|
||||
}
|
||||
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
|
||||
let pairs: Vec<LevelDuration> = raw_pairs
|
||||
.iter()
|
||||
.map(|p| LevelDuration::new(p.level, p.duration_us))
|
||||
.collect();
|
||||
let mut reg = ProtocolRegistry::new();
|
||||
let results = reg.process_signal_stream(&pairs, freq);
|
||||
assert!(
|
||||
results.iter().any(|(n, _, _)| n == "Honda Static"),
|
||||
"expected at least one Honda Static decode from Unlock_honda.sub, got: {:?}",
|
||||
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toyota_decodes_camry_variant_b_sub() {
|
||||
// 312 MHz Toyota Camry capture: Variant-B NRZ frame that Kia V3/V4 (and every other
|
||||
// decoder) rejects, so Toyota uniquely claims it. Guards the Toyota port + its registry
|
||||
// placement (after kia_v3_v4).
|
||||
let path = Path::new("IMPORTS/Toyota + Lexus/19_toyota_camry_l2_u2_t2_p2.sub");
|
||||
if !path.exists() {
|
||||
eprintln!("Skip: {:?} not found (run from crate root)", path);
|
||||
return;
|
||||
}
|
||||
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
|
||||
let pairs: Vec<LevelDuration> = raw_pairs
|
||||
.iter()
|
||||
.map(|p| LevelDuration::new(p.level, p.duration_us))
|
||||
.collect();
|
||||
let mut reg = ProtocolRegistry::new();
|
||||
let results = reg.process_signal_stream(&pairs, freq);
|
||||
assert!(
|
||||
results.iter().any(|(n, _, _)| n == "Toyota"),
|
||||
"expected at least one Toyota decode from 19_toyota_camry_l2_u2_t2_p2.sub, got: {:?}",
|
||||
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toyota_does_not_steal_kia_v3_v4_prius_sub() {
|
||||
// The Prius 433 MHz captures are KeeLoq-PWM frames whose air encoding is shared between
|
||||
// Toyota Variant A and Kia V3/V4. Kia V3/V4 is earlier in the registry and MUST keep
|
||||
// first-match priority — so these must still decode as "Kia V3/V4", never "Toyota".
|
||||
let path = Path::new("IMPORTS/Toyota + Lexus/Toyota_Prius2006_lock.sub");
|
||||
if !path.exists() {
|
||||
eprintln!("Skip: {:?} not found (run from crate root)", path);
|
||||
return;
|
||||
}
|
||||
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
|
||||
let pairs: Vec<LevelDuration> = raw_pairs
|
||||
.iter()
|
||||
.map(|p| LevelDuration::new(p.level, p.duration_us))
|
||||
.collect();
|
||||
let mut reg = ProtocolRegistry::new();
|
||||
let results = reg.process_signal_stream(&pairs, freq);
|
||||
let names: Vec<&str> = results.iter().map(|(n, _, _)| n.as_str()).collect();
|
||||
assert!(
|
||||
names.iter().any(|n| *n == "Kia V3/V4"),
|
||||
"expected Kia V3/V4 decodes from Toyota_Prius2006_lock.sub, got: {:?}",
|
||||
names
|
||||
);
|
||||
assert!(
|
||||
!names.iter().any(|n| *n == "Toyota"),
|
||||
"Toyota must NOT steal the shared KeeLoq-PWM Prius frames from Kia V3/V4, got: {:?}",
|
||||
names
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn psa2_decodes_groupe_psa_sub() {
|
||||
// Genuine Peugeot/Citroën keyfob captures. PSA2 ("PSA OLD") decodes them via the mode23
|
||||
// XOR fast path (nibble-checksum gated), recovering a stable serial 0x99EB25 with a rolling
|
||||
// counter. Guards the PSA2 port + its registry placement (after `psa`).
|
||||
let path = Path::new("IMPORTS/GROUPE PSA/PSA_523_536.sub");
|
||||
if !path.exists() {
|
||||
eprintln!("Skip: {:?} not found (run from crate root)", path);
|
||||
return;
|
||||
}
|
||||
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
|
||||
let pairs: Vec<LevelDuration> = raw_pairs
|
||||
.iter()
|
||||
.map(|p| LevelDuration::new(p.level, p.duration_us))
|
||||
.collect();
|
||||
let mut reg = ProtocolRegistry::new();
|
||||
let results = reg.process_signal_stream(&pairs, freq);
|
||||
let psa2: Vec<_> = results.iter().filter(|(n, _, _)| n == "PSA2").collect();
|
||||
assert!(
|
||||
!psa2.is_empty(),
|
||||
"expected at least one PSA2 decode from PSA_523_536.sub, got: {:?}",
|
||||
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
|
||||
);
|
||||
// The decode must carry recovered fields (serial), not a bare undecrypted frame.
|
||||
assert!(
|
||||
psa2.iter().any(|(_, d, _)| d.serial == Some(0x99EB25) && d.crc_valid),
|
||||
"expected a decrypted PSA2 frame with serial 0x99EB25, got serials: {:?}",
|
||||
psa2.iter().map(|(_, d, _)| d.serial).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn psa2_does_not_steal_vag_captures() {
|
||||
// PSA2 rides the same 250/500µs Manchester rate as several other protocols and the C's
|
||||
// mode23 nibble checksum is a loose ~1/16 gate. Emission is therefore restricted to a
|
||||
// field-bearing decrypt, so PSA2 must NOT match these VAG captures (which carry no valid
|
||||
// PSA2 frame) even though they reach 80 collectable bits at a PSA2-compatible frequency.
|
||||
for rel in ["VAG/Test_55_unlock_and_55_lock_suran.sub", "VAG/Vw pasat B7.sub"] {
|
||||
let path = Path::new("IMPORTS").join(rel);
|
||||
if !path.exists() {
|
||||
eprintln!("Skip: {:?} not found (run from crate root)", path);
|
||||
continue;
|
||||
}
|
||||
let (freq, raw_pairs) = import_sub_raw(&path).unwrap();
|
||||
let pairs: Vec<LevelDuration> = raw_pairs
|
||||
.iter()
|
||||
.map(|p| LevelDuration::new(p.level, p.duration_us))
|
||||
.collect();
|
||||
let mut reg = ProtocolRegistry::new();
|
||||
let results = reg.process_signal_stream(&pairs, freq);
|
||||
let names: Vec<&str> = results.iter().map(|(n, _, _)| n.as_str()).collect();
|
||||
assert!(
|
||||
!names.iter().any(|n| *n == "PSA2"),
|
||||
"PSA2 must not false-match {rel}, got: {names:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Diagnostic: sweep every IMPORTS/*.sub and print which protocol(s) each decodes as.
|
||||
/// `#[ignore]`d (noisy, always passes); run with
|
||||
/// `cargo test sweep_imports_decodes -- --ignored --nocapture`.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn sweep_imports_decodes() {
|
||||
use std::fs;
|
||||
let root = Path::new("IMPORTS");
|
||||
if !root.exists() {
|
||||
eprintln!("Skip: IMPORTS not found");
|
||||
return;
|
||||
}
|
||||
// Collect IMPORTS/<make>/*.sub
|
||||
let mut subs: Vec<std::path::PathBuf> = Vec::new();
|
||||
if let Ok(makes) = fs::read_dir(root) {
|
||||
for make in makes.flatten() {
|
||||
if make.path().is_dir() {
|
||||
if let Ok(files) = fs::read_dir(make.path()) {
|
||||
for f in files.flatten() {
|
||||
let p = f.path();
|
||||
if p.extension().map(|e| e == "sub").unwrap_or(false) {
|
||||
subs.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
subs.sort();
|
||||
let mut reg = ProtocolRegistry::new();
|
||||
for path in &subs {
|
||||
match import_sub_raw(path) {
|
||||
Ok((freq, raw_pairs)) => {
|
||||
let pairs: Vec<LevelDuration> = raw_pairs
|
||||
.iter()
|
||||
.map(|p| LevelDuration::new(p.level, p.duration_us))
|
||||
.collect();
|
||||
let results = reg.process_signal_stream(&pairs, freq);
|
||||
let names: Vec<&str> = results.iter().map(|(n, _, _)| n.as_str()).collect();
|
||||
let rel = path.strip_prefix("IMPORTS").unwrap_or(path);
|
||||
eprintln!("{:<42} {:>9}Hz => {:?}", rel.display(), freq, names);
|
||||
}
|
||||
Err(e) => eprintln!("{:<42} ERR {:?}", path.display(), e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,613 @@
|
||||
//! Porsche Cayenne protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with Flipper-ARF reference: `lib/subghz/protocols/porsche_cayenne.c`
|
||||
//! (internal protocol name in the firmware header is "Porsche AG").
|
||||
//!
|
||||
//! # Relationship to the existing `Porsche Touareg` decoder
|
||||
//!
|
||||
//! KAT already ships `porsche_touareg.rs`, which is itself a port of this very same
|
||||
//! `porsche_cayenne.c` source (its own doc comment and the `porsche_cayenne_compute_frame`
|
||||
//! function name make that explicit). The two protocols are therefore **the same wire
|
||||
//! protocol**: identical 1680/3370µs PWM, identical 73-pulse 3370µs preamble + 5930µs gap
|
||||
//! pair, identical 64-bit MSB-first frame, identical 24-bit rotating-register VAG cipher,
|
||||
//! and identical brute-force counter recovery / validity check (`counter != 0`).
|
||||
//!
|
||||
//! The only meaningful differences captured here versus Touareg:
|
||||
//! * Display name is `"Porsche Cayenne"` (vs `"Porsche Touareg"`).
|
||||
//! * This decoder ADDS the encoder from the C reference (the Touareg port is decode-only):
|
||||
//! a 4-frame burst (frame types 0b010/0b001/0b100/0b100), 73 sync pairs + gap pair +
|
||||
//! 64 MSB-first PWM data bits per frame.
|
||||
//!
|
||||
//! Because the two share an identical frame and validity gate, this decoder is registered
|
||||
//! AFTER `porsche_touareg` so Touareg keeps first-match priority and Cayenne can never steal
|
||||
//! a Touareg capture (the registry reports the first decoder that fires on a given pulse —
|
||||
//! see `process_signal_*_inner` in `mod.rs`). To keep Cayenne a faithful but strictly
|
||||
//! non-stealing decoder, emission is additionally gated on the frame_type being one of the
|
||||
//! three values the C only ever emits/labels (`0b001` Cont, `0b010` First, `0b100` Final).
|
||||
//! That is a strict subset of what Touareg accepts, so on any shared frame Touareg fires too
|
||||
//! and wins by ordering; Cayenne only ever fires on frames Touareg also accepts.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM bit pairs: SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1
|
||||
//! - 64 bits total; sync preamble of 15+ LOW/HIGH pairs at 3370µs, then 5930µs gap pair, then data
|
||||
//! - Field layout: pkt[0]=(btn<<4)|(frame_type&0x07), pkt[1..3]=serial 24-bit, pkt[4..7]=encrypted
|
||||
//! - Counter recovery via brute-force matching of computed encrypted bytes against received bytes
|
||||
//! - Frame types: 0x02="First", 0x01="Cont", 0x04="Final"
|
||||
//! - RF: AM/OOK. Frequencies: 433.92 MHz and 868.35 MHz (C flags 433|868)
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 1680;
|
||||
const TE_LONG: u32 = 3370;
|
||||
const TE_DELTA: u32 = 500;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
|
||||
const PC_TE_SYNC: u32 = 3370;
|
||||
const PC_TE_GAP: u32 = 5930;
|
||||
const PC_SYNC_MIN: u16 = 15;
|
||||
/// Actual preamble pulse-pair count emitted by the firmware (PC_SYNC_COUNT).
|
||||
const PC_SYNC_COUNT: usize = 73;
|
||||
|
||||
/// KAT generic button codes (Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08).
|
||||
const BTN_LOCK: u8 = 0x01;
|
||||
const BTN_UNLOCK: u8 = 0x02;
|
||||
const BTN_TRUNK: u8 = 0x04;
|
||||
const BTN_PANIC: u8 = 0x08;
|
||||
|
||||
/// Decoder states (matches PCDecoderStep in porsche_cayenne.c).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Sync,
|
||||
GapHigh,
|
||||
GapLow,
|
||||
Data,
|
||||
}
|
||||
|
||||
/// Porsche Cayenne protocol decoder/encoder.
|
||||
pub struct PorscheCayenneDecoder {
|
||||
step: DecoderStep,
|
||||
sync_count: u16,
|
||||
raw_data: u64,
|
||||
bit_count: usize,
|
||||
te_last: u32,
|
||||
}
|
||||
|
||||
/// Circular left-shift of a 24-bit register stored in three bytes (h, m, l).
|
||||
///
|
||||
/// Each byte shifts left by 1, receiving the MSB of the next byte in the chain:
|
||||
/// h gets MSB of m, m gets MSB of l, l gets MSB of h (wrap-around).
|
||||
///
|
||||
/// Matches the ROTATE24 macro in porsche_cayenne.c exactly.
|
||||
#[inline]
|
||||
fn rotate24(r_h: &mut u8, r_m: &mut u8, r_l: &mut u8) {
|
||||
let ch = (*r_h >> 7) & 1;
|
||||
let cm = (*r_m >> 7) & 1;
|
||||
let cl = (*r_l >> 7) & 1;
|
||||
*r_h = (*r_h << 1) | cm;
|
||||
*r_m = (*r_m << 1) | cl;
|
||||
*r_l = (*r_l << 1) | ch;
|
||||
}
|
||||
|
||||
/// Compute an 8-byte frame from serial, button, counter, and frame_type.
|
||||
///
|
||||
/// Direct port of `porsche_cayenne_compute_frame` from the C reference. The cipher
|
||||
/// increments `counter` by 1 internally. pkt[0..3] = plaintext header, pkt[4..7] = cipher
|
||||
/// output derived from a 24-bit rotate register seeded from serial bytes and rotated
|
||||
/// (4 + counter_low) times.
|
||||
fn compute_frame(serial24: u32, btn: u8, counter: u16, frame_type: u8) -> [u8; 8] {
|
||||
let b0 = (btn << 4) | (frame_type & 0x07);
|
||||
let b1 = ((serial24 >> 16) & 0xFF) as u8;
|
||||
let b2 = ((serial24 >> 8) & 0xFF) as u8;
|
||||
let b3 = (serial24 & 0xFF) as u8;
|
||||
|
||||
// Internal counter increment (firmware @ 0x14122).
|
||||
let cnt = counter.wrapping_add(1);
|
||||
let cnt_lo = (cnt & 0xFF) as u8;
|
||||
let cnt_hi = ((cnt >> 8) & 0xFF) as u8;
|
||||
|
||||
// Seed 24-bit register: r_h <- serial LSB, r_m <- serial MSB, r_l <- serial mid.
|
||||
let mut r_h = b3;
|
||||
let mut r_m = b1;
|
||||
let mut r_l = b2;
|
||||
|
||||
// Loop 1: 4 fixed rotations.
|
||||
for _ in 0..4 {
|
||||
rotate24(&mut r_h, &mut r_m, &mut r_l);
|
||||
}
|
||||
// Loop 2: cnt_lo additional rotations.
|
||||
for _ in 0..cnt_lo as u16 {
|
||||
rotate24(&mut r_h, &mut r_m, &mut r_l);
|
||||
}
|
||||
|
||||
// 9A: XOR of r_h with base byte.
|
||||
let a9a = r_h ^ b0;
|
||||
|
||||
// 9B: three masked slices of (~cnt_lo / ~cnt_hi) XOR r_m.
|
||||
let nb9b_p1 = ((!cnt_lo).wrapping_shl(2) & 0xFC) ^ r_m;
|
||||
let nb9b_p2 = ((!cnt_hi).wrapping_shl(2) & 0xFC) ^ r_m;
|
||||
let nb9b_p3 = ((!cnt_hi).wrapping_shr(6) & 0x03) ^ r_m;
|
||||
let a9b = (nb9b_p1 & 0xCC) | (nb9b_p2 & 0x30) | (nb9b_p3 & 0x03);
|
||||
|
||||
// 9C: three masked slices of (~cnt_lo / ~cnt_hi) XOR r_l.
|
||||
let nb9c_p1 = ((!cnt_lo).wrapping_shr(2) & 0x3F) ^ r_l;
|
||||
let nb9c_p2 = ((!cnt_hi & 0x03).wrapping_shl(6)) ^ r_l;
|
||||
let nb9c_p3 = ((!cnt_hi).wrapping_shr(2) & 0x3F) ^ r_l;
|
||||
let a9c = (nb9c_p1 & 0x33) | (nb9c_p2 & 0xC0) | (nb9c_p3 & 0x0C);
|
||||
|
||||
let mut pkt = [0u8; 8];
|
||||
pkt[0] = b0;
|
||||
pkt[1] = b1;
|
||||
pkt[2] = b2;
|
||||
pkt[3] = b3;
|
||||
pkt[4] = ((a9a >> 2) & 0x3F) | ((!cnt_lo & 0x03) << 6);
|
||||
pkt[5] = (!cnt_lo & 0xC0) | ((a9a & 0x03) << 4) | (a9b & 0x0C) | ((!cnt_lo).wrapping_shr(2) & 0x03);
|
||||
pkt[6] = ((a9b & 0x03) << 6) | ((a9c >> 2) & 0x3C) | ((!cnt_lo).wrapping_shr(4) & 0x03);
|
||||
pkt[7] = ((a9b >> 4) & 0x0F) | ((a9c & 0x0F) << 4);
|
||||
|
||||
pkt
|
||||
}
|
||||
|
||||
/// Unpack a raw 64-bit frame into its 8 bytes (big-endian: pkt[0] is the MSB).
|
||||
fn unpack_bytes(data: u64) -> [u8; 8] {
|
||||
let mut pkt = [0u8; 8];
|
||||
let mut raw = data;
|
||||
for i in (0..8).rev() {
|
||||
pkt[i] = (raw & 0xFF) as u8;
|
||||
raw >>= 8;
|
||||
}
|
||||
pkt
|
||||
}
|
||||
|
||||
/// Brute-force counter recovery: try counter values 1..=256 (matching the C loop) and
|
||||
/// compare the recomputed cipher bytes pkt[4..7]. Returns 0 if no match (invalid frame).
|
||||
fn recover_counter(serial: u32, btn: u8, frame_type: u8, pkt: &[u8; 8]) -> u16 {
|
||||
for try_cnt in 1u16..=256 {
|
||||
// The cipher increments internally, so pass try_cnt - 1.
|
||||
let try_pkt = compute_frame(serial, btn, try_cnt - 1, frame_type);
|
||||
if try_pkt[4] == pkt[4]
|
||||
&& try_pkt[5] == pkt[5]
|
||||
&& try_pkt[6] == pkt[6]
|
||||
&& try_pkt[7] == pkt[7]
|
||||
{
|
||||
return try_cnt;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// Parse raw 64-bit data into a DecodedSignal, or `None` if it is not a valid Cayenne frame.
|
||||
///
|
||||
/// Cayenne-specific gate (keeps this decoder from stealing Touareg captures): the frame_type
|
||||
/// must be one of the three values the C reference emits/labels (0b001 Cont, 0b010 First,
|
||||
/// 0b100 Final), AND the counter must be recoverable (cipher-consistent, `counter != 0`).
|
||||
fn parse_data(data: u64) -> Option<DecodedSignal> {
|
||||
let pkt = unpack_bytes(data);
|
||||
|
||||
let serial = ((pkt[1] as u32) << 16) | ((pkt[2] as u32) << 8) | (pkt[3] as u32);
|
||||
let btn = pkt[0] >> 4;
|
||||
let frame_type = pkt[0] & 0x07;
|
||||
|
||||
// Cayenne-specific invariant: only the three documented frame types.
|
||||
let frame_type_name = match frame_type {
|
||||
0b010 => "First",
|
||||
0b001 => "Cont",
|
||||
0b100 => "Final",
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Cipher consistency check (also the C's validity signal).
|
||||
let counter = recover_counter(serial, btn, frame_type, &pkt);
|
||||
if counter == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: Some(counter),
|
||||
crc_valid: true, // cipher-consistent frame
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
extra: Some(frame_type as u64),
|
||||
protocol_display_name: Some(format!("Porsche Cayenne [{}]", frame_type_name)),
|
||||
})
|
||||
}
|
||||
|
||||
impl PorscheCayenneDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
sync_count: 0,
|
||||
raw_data: 0,
|
||||
bit_count: 0,
|
||||
te_last: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a KAT generic button command to a Porsche Cayenne / VAG 4-bit button code.
|
||||
/// KAT: Lock=0x01, Unlock=0x02, Trunk=0x04, Panic=0x08.
|
||||
/// The C `porsche_cayenne_get_btn_code` maps d-pad: Up=0x01 Lock, Down=0x02 Unlock,
|
||||
/// Left=0x04 Trunk, Right=0x08 Open — an identical set, so the codes pass through.
|
||||
fn map_button(button: u8) -> u8 {
|
||||
match button {
|
||||
BTN_LOCK | BTN_UNLOCK | BTN_TRUNK | BTN_PANIC => button & 0x0F,
|
||||
b => b & 0x0F,
|
||||
}
|
||||
}
|
||||
|
||||
/// Append one PWM data bit as a (LOW, HIGH) pair, matching the C encoder:
|
||||
/// bit 0: SHORT LOW + LONG HIGH; bit 1: LONG LOW + SHORT HIGH.
|
||||
fn push_bit(signal: &mut Vec<LevelDuration>, bit: bool) {
|
||||
if bit {
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit one full frame (73 sync pairs + gap pair + 64 MSB-first data bits) into `signal`.
|
||||
/// Matches `porsche_cayenne_build_upload`'s per-frame body.
|
||||
fn push_frame(signal: &mut Vec<LevelDuration>, pkt: &[u8; 8]) {
|
||||
// Preamble: 73 × (LOW LONG + HIGH LONG).
|
||||
for _ in 0..PC_SYNC_COUNT {
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
// Gap: LOW GAP + HIGH GAP.
|
||||
signal.push(LevelDuration::new(false, PC_TE_GAP));
|
||||
signal.push(LevelDuration::new(true, PC_TE_GAP));
|
||||
// 64 data bits, MSB first, byte order pkt[0]->pkt[7].
|
||||
for &byte in pkt.iter() {
|
||||
for bit in (0..8).rev() {
|
||||
Self::push_bit(signal, (byte >> bit) & 1 != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for PorscheCayenneDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Porsche Cayenne"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
// C flags: SubGhzProtocolFlag_433 | SubGhzProtocolFlag_868.
|
||||
&[433_920_000, 868_350_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.sync_count = 0;
|
||||
self.raw_data = 0;
|
||||
self.bit_count = 0;
|
||||
self.te_last = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
// Reset: wait for a LOW pulse matching sync timing (3370µs).
|
||||
DecoderStep::Reset => {
|
||||
if !level && duration_diff!(duration, PC_TE_SYNC) < TE_DELTA {
|
||||
self.sync_count = 1;
|
||||
self.step = DecoderStep::Sync;
|
||||
}
|
||||
}
|
||||
|
||||
// Sync: count sync pulses (HIGH and LOW at 3370µs).
|
||||
// On a gap pulse (5930µs) with enough sync pulses, transition to GapHigh/GapLow.
|
||||
DecoderStep::Sync => {
|
||||
if level {
|
||||
if duration_diff!(duration, PC_TE_SYNC) < TE_DELTA {
|
||||
// Good sync HIGH — keep collecting.
|
||||
} else if self.sync_count >= PC_SYNC_MIN
|
||||
&& duration_diff!(duration, PC_TE_GAP) < TE_DELTA
|
||||
{
|
||||
// HIGH gap after sufficient sync pulses.
|
||||
self.step = DecoderStep::GapLow;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
// LOW pulse.
|
||||
if duration_diff!(duration, PC_TE_SYNC) < TE_DELTA {
|
||||
self.sync_count += 1;
|
||||
} else if self.sync_count >= PC_SYNC_MIN
|
||||
&& duration_diff!(duration, PC_TE_GAP) < TE_DELTA
|
||||
{
|
||||
// LOW gap after sufficient sync pulses.
|
||||
self.step = DecoderStep::GapHigh;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GapHigh: expect the complementary HIGH gap pulse.
|
||||
DecoderStep::GapHigh => {
|
||||
if level && duration_diff!(duration, PC_TE_GAP) < TE_DELTA {
|
||||
self.raw_data = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Data;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// GapLow: expect the complementary LOW gap pulse.
|
||||
DecoderStep::GapLow => {
|
||||
if !level && duration_diff!(duration, PC_TE_GAP) < TE_DELTA {
|
||||
self.raw_data = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Data;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// Data: decode bit pairs.
|
||||
// LOW pulses are saved in te_last; HIGH pulses complete the bit:
|
||||
// SHORT LOW + LONG HIGH = bit 0
|
||||
// LONG LOW + SHORT HIGH = bit 1
|
||||
DecoderStep::Data => {
|
||||
if level {
|
||||
let bit_value;
|
||||
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA
|
||||
&& duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
{
|
||||
bit_value = false; // bit 0
|
||||
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA
|
||||
&& duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
{
|
||||
bit_value = true; // bit 1
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
self.raw_data = (self.raw_data << 1) | (bit_value as u64);
|
||||
self.bit_count += 1;
|
||||
|
||||
if self.bit_count >= MIN_COUNT_BIT {
|
||||
let data = self.raw_data;
|
||||
self.step = DecoderStep::Reset;
|
||||
// parse_data returns None if the Cayenne gate fails (not a Cayenne
|
||||
// frame), so this decoder stays silent on those and never steals them.
|
||||
return parse_data(data);
|
||||
}
|
||||
} else {
|
||||
// LOW pulse: save duration for the bit pair.
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial? & 0xFFFFFF;
|
||||
let cnt = decoded.counter.unwrap_or(0);
|
||||
let btn = Self::map_button(button);
|
||||
|
||||
// 4-frame burst (matches porsche_cayenne_build_upload):
|
||||
// Frame 0: frame_type=0b010, cipher counter = cnt+1
|
||||
// Frame 1: frame_type=0b001, cipher counter = cnt+2
|
||||
// Frame 2: frame_type=0b100, cipher counter = cnt+3
|
||||
// Frame 3: frame_type=0b100, cipher counter = cnt+4
|
||||
// (compute_frame increments the passed counter by 1 internally.)
|
||||
const FRAME_TYPES: [u8; 4] = [0b010, 0b001, 0b100, 0b100];
|
||||
|
||||
// Per-frame size: 73 sync pairs + 1 gap pair + 64 bit pairs = (73 + 1 + 64) * 2.
|
||||
let mut signal = Vec::with_capacity((PC_SYNC_COUNT + 1 + 64) * 2 * 4);
|
||||
|
||||
for (f, &ft) in FRAME_TYPES.iter().enumerate() {
|
||||
let pkt = compute_frame(serial, btn, cnt.wrapping_add(f as u16), ft);
|
||||
Self::push_frame(&mut signal, &pkt);
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PorscheCayenneDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Feed an encoded signal back through a fresh decoder and return the first decode.
|
||||
fn decode_signal(signal: &[LevelDuration]) -> Option<DecodedSignal> {
|
||||
let mut dec = PorscheCayenneDecoder::new();
|
||||
for ld in signal {
|
||||
if let Some(d) = dec.feed(ld.level, ld.duration_us) {
|
||||
return Some(d);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Build a one-frame on-air signal for a single (serial, btn, counter, frame_type) and
|
||||
/// decode it back. The frame's cipher counter is `counter+1` (compute_frame increments),
|
||||
/// and the decoder's brute-force recovery reports that incremented value.
|
||||
fn roundtrip_single(serial: u32, btn: u8, counter: u16, frame_type: u8) -> DecodedSignal {
|
||||
let pkt = compute_frame(serial, btn, counter, frame_type);
|
||||
let mut signal = Vec::new();
|
||||
PorscheCayenneDecoder::push_frame(&mut signal, &pkt);
|
||||
decode_signal(&signal)
|
||||
.unwrap_or_else(|| panic!("decode failed for serial {serial:#X} btn {btn:#X} cnt {counter} ft {frame_type:#b}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_decode_roundtrip_via_encoder() {
|
||||
// Full encoder path: encode() emits a 4-frame burst; the first frame (type 0b010,
|
||||
// cipher counter = cnt+1) must decode back with the right serial/button, and the
|
||||
// recovered counter must survive the rolling cipher (== cnt+1).
|
||||
let serials = [0x00ABCDEFu32, 0x00123456, 0x00000001, 0x00FFFFFE, 0x005A5A5A];
|
||||
let buttons = [BTN_LOCK, BTN_UNLOCK, BTN_TRUNK, BTN_PANIC];
|
||||
// Counters kept within the C reference's recoverable range: its decoder only
|
||||
// brute-forces cnt_lo (256 values), so the first burst frame's cipher counter
|
||||
// (seed+1) must be <= 256, i.e. seed <= 255. See `counter_recovery_limit_matches_c`.
|
||||
let counters = [0u16, 1, 42, 200, 254, 255];
|
||||
|
||||
for (&serial, &counter) in serials.iter().zip(counters.iter()) {
|
||||
for &btn in &buttons {
|
||||
let seed = DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data: 0,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
};
|
||||
let encoder = PorscheCayenneDecoder::new();
|
||||
let signal = encoder.encode(&seed, btn).expect("encode should succeed");
|
||||
let decoded = decode_signal(&signal).unwrap_or_else(|| {
|
||||
panic!("decode failed for serial {serial:#X} btn {btn:#X} cnt {counter}")
|
||||
});
|
||||
|
||||
let expected_btn = PorscheCayenneDecoder::map_button(btn);
|
||||
assert_eq!(decoded.serial, Some(serial & 0xFFFFFF), "serial");
|
||||
assert_eq!(decoded.button, Some(expected_btn), "button");
|
||||
// First frame's cipher counter is cnt+1 (compute_frame increments).
|
||||
assert_eq!(
|
||||
decoded.counter,
|
||||
Some(counter.wrapping_add(1)),
|
||||
"counter must survive the rolling cipher"
|
||||
);
|
||||
assert_eq!(decoded.data_count_bit, MIN_COUNT_BIT, "bit count");
|
||||
assert!(decoded.crc_valid, "cipher-consistent frame → crc_valid");
|
||||
// First burst frame is type 0b010 = "First".
|
||||
assert_eq!(decoded.extra, Some(0b010), "frame_type First");
|
||||
assert_eq!(
|
||||
decoded.protocol_display_name.as_deref(),
|
||||
Some("Porsche Cayenne [First]")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_all_frame_types() {
|
||||
// Each of the three documented frame types decodes and labels correctly, and the
|
||||
// counter survives across the cipher for a spread of counter values.
|
||||
let cases = [
|
||||
(0b010u8, "Porsche Cayenne [First]"),
|
||||
(0b001u8, "Porsche Cayenne [Cont]"),
|
||||
(0b100u8, "Porsche Cayenne [Final]"),
|
||||
];
|
||||
for &(ft, name) in &cases {
|
||||
// Counters within the C's recoverable cnt_lo range (cipher counter seed+1 <= 256).
|
||||
for &counter in &[0u16, 7, 200, 254, 255] {
|
||||
let d = roundtrip_single(0x00C0FFEE, BTN_UNLOCK, counter, ft);
|
||||
assert_eq!(d.serial, Some(0x00C0FFEE), "serial ft={ft:#b}");
|
||||
assert_eq!(d.button, Some(BTN_UNLOCK), "button ft={ft:#b}");
|
||||
assert_eq!(
|
||||
d.counter,
|
||||
Some(counter.wrapping_add(1)),
|
||||
"counter survives cipher ft={ft:#b} cnt={counter}"
|
||||
);
|
||||
assert_eq!(d.extra, Some(ft as u64), "frame_type ft={ft:#b}");
|
||||
assert_eq!(d.protocol_display_name.as_deref(), Some(name));
|
||||
assert!(d.crc_valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_undocumented_frame_type() {
|
||||
// A structurally valid PWM frame whose frame_type is NOT one of {0b001,0b010,0b100}
|
||||
// must be rejected by Cayenne's gate — this is what keeps it from stealing arbitrary
|
||||
// 1680/3370µs PWM frames (and what makes Touareg, ordered first, the owner of any
|
||||
// shared frame). frame_type 0b011 is unused by the C reference.
|
||||
let pkt = compute_frame(0x00123456, BTN_LOCK, 5, 0b011);
|
||||
let mut signal = Vec::new();
|
||||
PorscheCayenneDecoder::push_frame(&mut signal, &pkt);
|
||||
assert!(
|
||||
decode_signal(&signal).is_none(),
|
||||
"undocumented frame_type 0b011 must not decode as Cayenne"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_truncated_frame() {
|
||||
// 63 of 64 data bits must not decode.
|
||||
let pkt = compute_frame(0x00ABCDEF, BTN_LOCK, 3, 0b010);
|
||||
let mut signal = Vec::new();
|
||||
PorscheCayenneDecoder::push_frame(&mut signal, &pkt);
|
||||
// Frame = 73 sync pairs + 1 gap pair + 64 bit pairs. Drop the last bit pair.
|
||||
let keep = (PC_SYNC_COUNT + 1 + 63) * 2;
|
||||
assert!(
|
||||
decode_signal(&signal[..keep]).is_none(),
|
||||
"63-bit frame must not decode"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counter_recovery_limit_matches_c() {
|
||||
// Faithful limitation of the C reference: its decoder brute-forces only cnt_lo
|
||||
// (try_cnt 1..=256), so frames whose cipher counter (passed counter + 1) exceeds 256
|
||||
// cannot have their counter recovered. recover_counter returns 0 for those, and the
|
||||
// frame is reported invalid (does not decode) — exactly as the C would behave.
|
||||
// seed=255 -> cipher counter 256 -> recoverable; seed=256 -> 257 -> NOT recoverable.
|
||||
let pkt_ok = compute_frame(0x00ABCDEF, BTN_LOCK, 255, 0b010);
|
||||
assert_eq!(
|
||||
recover_counter(0x00ABCDEF, BTN_LOCK, 0b010, &pkt_ok),
|
||||
256,
|
||||
"cipher counter 256 is the highest recoverable value"
|
||||
);
|
||||
let pkt_oob = compute_frame(0x00ABCDEF, BTN_LOCK, 256, 0b010);
|
||||
assert_eq!(
|
||||
recover_counter(0x00ABCDEF, BTN_LOCK, 0b010, &pkt_oob),
|
||||
0,
|
||||
"cipher counter 257 is beyond the C's cnt_lo brute-force range"
|
||||
);
|
||||
// And such an out-of-range frame must not decode (parse_data rejects counter==0).
|
||||
let mut signal = Vec::new();
|
||||
PorscheCayenneDecoder::push_frame(&mut signal, &pkt_oob);
|
||||
assert!(
|
||||
decode_signal(&signal).is_none(),
|
||||
"frame with unrecoverable counter must not decode (matches C)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cipher_matches_touareg_reference_vectors() {
|
||||
// The cipher/frame is shared with the Touareg port (same C source). Spot-check that
|
||||
// compute_frame -> unpack header bytes are exactly as laid out by the C reference:
|
||||
// pkt[0]=(btn<<4)|ft, pkt[1..3]=serial big-endian.
|
||||
let pkt = compute_frame(0x00ABCDEF, 0x02, 0x10, 0b010);
|
||||
assert_eq!(pkt[0], (0x02 << 4) | 0b010, "pkt[0] = (btn<<4)|ft");
|
||||
assert_eq!(pkt[1], 0xAB, "serial MSB");
|
||||
assert_eq!(pkt[2], 0xCD, "serial mid");
|
||||
assert_eq!(pkt[3], 0xEF, "serial LSB");
|
||||
// Recovering the counter from this exact frame yields cnt+1 = 0x11.
|
||||
let cnt = recover_counter(0x00ABCDEF, 0x02, 0b010, &pkt);
|
||||
assert_eq!(cnt, 0x11, "brute-force counter recovery = passed counter + 1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
//! Porsche Touareg protocol decoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/porsche_touareg.c`.
|
||||
//! Original implementation by @lupettohf
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding with Manchester-like bit pairs: SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1
|
||||
//! - 64 bits total; sync preamble of 15+ LOW/HIGH pairs at 3370µs, then 5930µs gap pair, then data
|
||||
//! - Field layout: pkt[0]=(btn<<4)|(frame_type&0x07), pkt[1..3]=serial 24-bit, pkt[4..7]=encrypted
|
||||
//! - Counter recovery via brute-force matching of computed encrypted bytes against received bytes
|
||||
//! - Frame types: 0x02="First", 0x01="Cont", 0x04="Final"
|
||||
//! - Frequencies: 433.92 MHz and 868.35 MHz (AM demodulation)
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 1680;
|
||||
const TE_LONG: u32 = 3370;
|
||||
const TE_DELTA: u32 = 500;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
|
||||
const PC_TE_SYNC: u32 = 3370;
|
||||
const PC_TE_GAP: u32 = 5930;
|
||||
const PC_SYNC_MIN: u16 = 15;
|
||||
|
||||
/// Decoder states (matches PorscheCayenneDecoderStep in porsche_touareg.c)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Sync,
|
||||
GapHigh,
|
||||
GapLow,
|
||||
Data,
|
||||
}
|
||||
|
||||
/// Porsche Touareg protocol decoder
|
||||
pub struct PorscheTouaregDecoder {
|
||||
step: DecoderStep,
|
||||
sync_count: u16,
|
||||
raw_data: u64,
|
||||
bit_count: usize,
|
||||
te_last: u32,
|
||||
}
|
||||
|
||||
/// Circular left-shift of a 24-bit register stored in three bytes (h, m, l).
|
||||
///
|
||||
/// Each byte shifts left by 1, receiving the MSB of the next byte in the chain:
|
||||
/// h gets MSB of m, m gets MSB of l, l gets MSB of h (wrap-around).
|
||||
///
|
||||
/// Matches the ROTATE24 macro in porsche_touareg.c exactly.
|
||||
#[inline]
|
||||
fn rotate24(r_h: &mut u8, r_m: &mut u8, r_l: &mut u8) {
|
||||
let ch = (*r_h >> 7) & 1;
|
||||
let cm = (*r_m >> 7) & 1;
|
||||
let cl = (*r_l >> 7) & 1;
|
||||
*r_h = (*r_h << 1) | cm;
|
||||
*r_m = (*r_m << 1) | cl;
|
||||
*r_l = (*r_l << 1) | ch;
|
||||
}
|
||||
|
||||
/// Compute an 8-byte frame from serial, button, counter, and frame_type.
|
||||
///
|
||||
/// This is a direct port of `porsche_cayenne_compute_frame` from the C reference.
|
||||
/// pkt[0..3] = plaintext header, pkt[4..7] = encrypted payload derived from
|
||||
/// a 24-bit rotate register seeded from serial bytes and rotated (4 + counter_low) times.
|
||||
fn compute_frame(serial24: u32, btn: u8, counter: u16, frame_type: u8) -> [u8; 8] {
|
||||
let b0 = (btn << 4) | (frame_type & 0x07);
|
||||
let b1 = ((serial24 >> 16) & 0xFF) as u8;
|
||||
let b2 = ((serial24 >> 8) & 0xFF) as u8;
|
||||
let b3 = (serial24 & 0xFF) as u8;
|
||||
|
||||
let cnt = counter.wrapping_add(1);
|
||||
let cnt_lo = (cnt & 0xFF) as u8;
|
||||
let cnt_hi = ((cnt >> 8) & 0xFF) as u8;
|
||||
|
||||
let mut r_h = b3;
|
||||
let mut r_m = b1;
|
||||
let mut r_l = b2;
|
||||
|
||||
// Rotate 4 times initially
|
||||
for _ in 0..4 {
|
||||
rotate24(&mut r_h, &mut r_m, &mut r_l);
|
||||
}
|
||||
// Then rotate cnt_lo more times
|
||||
for _ in 0..cnt_lo as u16 {
|
||||
rotate24(&mut r_h, &mut r_m, &mut r_l);
|
||||
}
|
||||
|
||||
let a9a = r_h ^ b0;
|
||||
|
||||
let nb9b_p1 = ((!cnt_lo).wrapping_shl(2) & 0xFC) ^ r_m;
|
||||
let nb9b_p2 = ((!cnt_hi).wrapping_shl(2) & 0xFC) ^ r_m;
|
||||
let nb9b_p3 = ((!cnt_hi).wrapping_shr(6) & 0x03) ^ r_m;
|
||||
let a9b = (nb9b_p1 & 0xCC) | (nb9b_p2 & 0x30) | (nb9b_p3 & 0x03);
|
||||
|
||||
let nb9c_p1 = ((!cnt_lo).wrapping_shr(2) & 0x3F) ^ r_l;
|
||||
let nb9c_p2 = ((!cnt_hi & 0x03).wrapping_shl(6)) ^ r_l;
|
||||
let nb9c_p3 = ((!cnt_hi).wrapping_shr(2) & 0x3F) ^ r_l;
|
||||
let a9c = (nb9c_p1 & 0x33) | (nb9c_p2 & 0xC0) | (nb9c_p3 & 0x0C);
|
||||
|
||||
let mut pkt = [0u8; 8];
|
||||
pkt[0] = b0;
|
||||
pkt[1] = b1;
|
||||
pkt[2] = b2;
|
||||
pkt[3] = b3;
|
||||
pkt[4] = ((a9a >> 2) & 0x3F) | ((!cnt_lo & 0x03) << 6);
|
||||
pkt[5] = (!cnt_lo & 0xC0) | ((a9a & 0x03) << 4) | (a9b & 0x0C) | ((!cnt_lo).wrapping_shr(2) & 0x03);
|
||||
pkt[6] = ((a9b & 0x03) << 6) | ((a9c >> 2) & 0x3C) | ((!cnt_lo).wrapping_shr(4) & 0x03);
|
||||
pkt[7] = ((a9b >> 4) & 0x0F) | ((a9c & 0x0F) << 4);
|
||||
|
||||
pkt
|
||||
}
|
||||
|
||||
/// Parse raw 64-bit data into a DecodedSignal.
|
||||
///
|
||||
/// Extracts serial (24-bit), button (4-bit), frame_type (3-bit), then brute-forces
|
||||
/// the counter (1..=256) by calling compute_frame and comparing encrypted bytes.
|
||||
fn parse_data(data: u64) -> DecodedSignal {
|
||||
// Unpack 64-bit data into 8 bytes (big-endian)
|
||||
let mut pkt = [0u8; 8];
|
||||
let mut raw = data;
|
||||
for i in (0..8).rev() {
|
||||
pkt[i] = (raw & 0xFF) as u8;
|
||||
raw >>= 8;
|
||||
}
|
||||
|
||||
let serial = ((pkt[1] as u32) << 16) | ((pkt[2] as u32) << 8) | (pkt[3] as u32);
|
||||
let btn = pkt[0] >> 4;
|
||||
let frame_type = pkt[0] & 0x07;
|
||||
|
||||
// Brute-force counter recovery: try counter values 1..=256
|
||||
let mut counter: u16 = 0;
|
||||
for try_cnt in 1u16..=256 {
|
||||
let try_pkt = compute_frame(serial, btn, try_cnt - 1, frame_type);
|
||||
if try_pkt[4] == pkt[4]
|
||||
&& try_pkt[5] == pkt[5]
|
||||
&& try_pkt[6] == pkt[6]
|
||||
&& try_pkt[7] == pkt[7]
|
||||
{
|
||||
counter = try_cnt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine frame type name for extra info
|
||||
let frame_type_name = match frame_type {
|
||||
0x02 => "First",
|
||||
0x01 => "Cont",
|
||||
0x04 => "Final",
|
||||
_ => "??",
|
||||
};
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: Some(counter),
|
||||
crc_valid: counter != 0,
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: false,
|
||||
extra: Some(frame_type as u64),
|
||||
protocol_display_name: Some(format!(
|
||||
"Porsche Touareg [{}]",
|
||||
frame_type_name
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
impl PorscheTouaregDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
sync_count: 0,
|
||||
raw_data: 0,
|
||||
bit_count: 0,
|
||||
te_last: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for PorscheTouaregDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Porsche Touareg"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000, 868_350_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.sync_count = 0;
|
||||
self.raw_data = 0;
|
||||
self.bit_count = 0;
|
||||
self.te_last = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
// Reset: wait for a LOW pulse matching sync timing (3370µs)
|
||||
DecoderStep::Reset => {
|
||||
if !level && duration_diff!(duration, PC_TE_SYNC) < TE_DELTA {
|
||||
self.sync_count = 1;
|
||||
self.step = DecoderStep::Sync;
|
||||
}
|
||||
}
|
||||
|
||||
// Sync: count sync pulses (HIGH and LOW at 3370µs).
|
||||
// On a gap pulse (5930µs) with enough sync pulses, transition to GapHigh/GapLow.
|
||||
DecoderStep::Sync => {
|
||||
if level {
|
||||
if duration_diff!(duration, PC_TE_SYNC) < TE_DELTA {
|
||||
// Keep collecting sync pairs -- HIGH sync pulse, stay in Sync
|
||||
} else if self.sync_count >= PC_SYNC_MIN
|
||||
&& duration_diff!(duration, PC_TE_GAP) < TE_DELTA
|
||||
{
|
||||
// HIGH gap after sufficient sync pulses
|
||||
self.step = DecoderStep::GapLow;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
// LOW pulse
|
||||
if duration_diff!(duration, PC_TE_SYNC) < TE_DELTA {
|
||||
self.sync_count += 1;
|
||||
} else if self.sync_count >= PC_SYNC_MIN
|
||||
&& duration_diff!(duration, PC_TE_GAP) < TE_DELTA
|
||||
{
|
||||
// LOW gap after sufficient sync pulses
|
||||
self.step = DecoderStep::GapHigh;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GapHigh: expect the complementary HIGH gap pulse
|
||||
DecoderStep::GapHigh => {
|
||||
if level && duration_diff!(duration, PC_TE_GAP) < TE_DELTA {
|
||||
self.raw_data = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Data;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// GapLow: expect the complementary LOW gap pulse
|
||||
DecoderStep::GapLow => {
|
||||
if !level && duration_diff!(duration, PC_TE_GAP) < TE_DELTA {
|
||||
self.raw_data = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Data;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// Data: decode bit pairs.
|
||||
// LOW pulses are saved in te_last; HIGH pulses complete the bit:
|
||||
// SHORT LOW + LONG HIGH = bit 0
|
||||
// LONG LOW + SHORT HIGH = bit 1
|
||||
DecoderStep::Data => {
|
||||
if level {
|
||||
// HIGH pulse completes a bit pair
|
||||
let bit_value;
|
||||
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA
|
||||
&& duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
{
|
||||
bit_value = false; // bit 0
|
||||
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA
|
||||
&& duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
{
|
||||
bit_value = true; // bit 1
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
self.raw_data = (self.raw_data << 1) | (bit_value as u64);
|
||||
self.bit_count += 1;
|
||||
|
||||
if self.bit_count >= MIN_COUNT_BIT {
|
||||
let result = parse_data(self.raw_data);
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
} else {
|
||||
// LOW pulse: save duration for the bit pair
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PorscheTouaregDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
+352
-147
@@ -1,13 +1,14 @@
|
||||
//! PSA (Peugeot/Citroën) protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`.
|
||||
//! Decode/encode logic (Manchester, preamble, TEA, XOR, mode 0x23/0x36) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble)
|
||||
//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation
|
||||
//! - TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds XOR layer
|
||||
//! - Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule)
|
||||
//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48)
|
||||
//! - Modified TEA (XTEA-like) with dynamic key selection (sum&3, (sum>>11)&3)
|
||||
//! - Mode 0x23: direct XOR decrypt with checksum validation
|
||||
//! - Mode 0x36: TEA brute-force with BF1/BF2 key schedules (deferred)
|
||||
//! - Dual preamble: Pattern 1 (250µs) and Pattern 2 (125µs)
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -21,7 +22,6 @@ const MIN_COUNT_BIT: usize = 128;
|
||||
// Internal timing for Manchester sub-symbol detection
|
||||
const TE_SHORT_125: u32 = 125;
|
||||
const TE_LONG_250: u32 = 250;
|
||||
const TE_TOLERANCE_49: u32 = 49;
|
||||
const TE_TOLERANCE_50: u32 = 50;
|
||||
const TE_TOLERANCE_99: u32 = 99;
|
||||
const TE_END_1000: u32 = 1000;
|
||||
@@ -45,13 +45,19 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states (matches protopirate's PsaDecoderState)
|
||||
/// Decoder states (matches protopirate's PsaDecoderState 0-4)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderState {
|
||||
/// State0: Wait for first edge (pattern detection)
|
||||
WaitEdge,
|
||||
CountPattern,
|
||||
DecodeManchester,
|
||||
End,
|
||||
/// State1: Count 250µs preamble patterns (Pattern 1)
|
||||
CountPattern250,
|
||||
/// State2: Manchester decode at 250/500µs (Pattern 1)
|
||||
DecodeManchester250,
|
||||
/// State3: Count 125µs preamble patterns (Pattern 2)
|
||||
CountPattern125,
|
||||
/// State4: Manchester decode at 125/250µs (Pattern 2)
|
||||
DecodeManchester125,
|
||||
}
|
||||
|
||||
/// PSA protocol decoder
|
||||
@@ -140,111 +146,268 @@ impl PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA decrypt (matches psa.c / standard TEA)
|
||||
/// Modified TEA decrypt with dynamic key selection (matches psa.c psa_tea_decrypt)
|
||||
/// Uses XTEA-like key scheduling: key[sum&3] and key[(sum>>11)&3]
|
||||
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
*v1 = v1.wrapping_sub(
|
||||
(v0.wrapping_shl(4).wrapping_add(key[2]))
|
||||
^ (v0.wrapping_add(sum))
|
||||
^ (v0.wrapping_shr(5).wrapping_add(key[3])),
|
||||
);
|
||||
*v0 = v0.wrapping_sub(
|
||||
(v1.wrapping_shl(4).wrapping_add(key[0]))
|
||||
^ (v1.wrapping_add(sum))
|
||||
^ (v1.wrapping_shr(5).wrapping_add(key[1])),
|
||||
);
|
||||
let k_idx2 = ((sum >> 11) & 3) as usize;
|
||||
let temp = key[k_idx2].wrapping_add(sum);
|
||||
sum = sum.wrapping_sub(TEA_DELTA);
|
||||
*v1 = v1.wrapping_sub(
|
||||
temp ^ (v0.wrapping_shr(5) ^ v0.wrapping_shl(4)).wrapping_add(*v0),
|
||||
);
|
||||
let k_idx1 = (sum & 3) as usize;
|
||||
let temp = key[k_idx1].wrapping_add(sum);
|
||||
*v0 = v0.wrapping_sub(
|
||||
temp ^ (v1.wrapping_shr(5) ^ v1.wrapping_shl(4)).wrapping_add(*v1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA encrypt (matches psa.c / standard TEA)
|
||||
/// Modified TEA encrypt with dynamic key selection (matches psa.c psa_tea_encrypt)
|
||||
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let mut sum: u32 = 0;
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
let k_idx1 = (sum & 3) as usize;
|
||||
let temp = key[k_idx1].wrapping_add(sum);
|
||||
sum = sum.wrapping_add(TEA_DELTA);
|
||||
*v0 = v0.wrapping_add(
|
||||
(v1.wrapping_shl(4).wrapping_add(key[0]))
|
||||
^ (v1.wrapping_add(sum))
|
||||
^ (v1.wrapping_shr(5).wrapping_add(key[1])),
|
||||
temp ^ (v1.wrapping_shr(5) ^ v1.wrapping_shl(4)).wrapping_add(*v1),
|
||||
);
|
||||
let k_idx2 = ((sum >> 11) & 3) as usize;
|
||||
let temp = key[k_idx2].wrapping_add(sum);
|
||||
*v1 = v1.wrapping_add(
|
||||
(v0.wrapping_shl(4).wrapping_add(key[2]))
|
||||
^ (v0.wrapping_add(sum))
|
||||
^ (v0.wrapping_shr(5).wrapping_add(key[3])),
|
||||
temp ^ (v0.wrapping_shr(5) ^ v0.wrapping_shl(4)).wrapping_add(*v0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// XOR decrypt for mode 0x23 (matches psa.c)
|
||||
/// XOR decrypt for mode 0x23 (matches psa.c psa_second_stage_xor_decrypt)
|
||||
/// Uses psa_copy_reverse byte reordering then XOR operations
|
||||
fn xor_decrypt(buffer: &mut [u8]) {
|
||||
// psa_copy_reverse: reorder source bytes
|
||||
let temp = [
|
||||
buffer[5], // temp[0] = source[5]
|
||||
buffer[4], // temp[1] = source[4]
|
||||
buffer[3], // temp[2] = source[3]
|
||||
buffer[2], // temp[3] = source[2]
|
||||
buffer[9], // temp[4] = source[9]
|
||||
buffer[8], // temp[5] = source[8]
|
||||
buffer[7], // temp[6] = source[7]
|
||||
buffer[6], // temp[7] = source[6]
|
||||
];
|
||||
buffer[2] = temp[0] ^ temp[6];
|
||||
buffer[3] = temp[2] ^ temp[0];
|
||||
buffer[4] = temp[6] ^ temp[3];
|
||||
buffer[5] = temp[7] ^ temp[1];
|
||||
buffer[6] = temp[3] ^ temp[1];
|
||||
buffer[7] = temp[6] ^ temp[4] ^ temp[5];
|
||||
}
|
||||
|
||||
/// XOR encrypt for mode 0x23 encoding (matches psa.c psa_second_stage_xor_encrypt)
|
||||
fn xor_encrypt(buffer: &mut [u8]) {
|
||||
let e6 = buffer[8];
|
||||
let e7 = buffer[9];
|
||||
let e5 = buffer[7];
|
||||
let e0 = buffer[2];
|
||||
let e1 = buffer[3];
|
||||
let e2 = buffer[4];
|
||||
let e3 = buffer[5];
|
||||
let e4 = buffer[6];
|
||||
let p0 = buffer[2];
|
||||
let p1 = buffer[3];
|
||||
let p2 = buffer[4];
|
||||
let p3 = buffer[5];
|
||||
let p4 = buffer[6];
|
||||
let p5 = buffer[7];
|
||||
|
||||
buffer[2] = e0 ^ e5;
|
||||
buffer[3] = e1 ^ (e0 ^ e5 ^ e6 ^ e7);
|
||||
buffer[4] = e2 ^ e0;
|
||||
buffer[5] = e3 ^ (e0 ^ e5 ^ e6 ^ e7);
|
||||
buffer[6] = e4 ^ e2;
|
||||
buffer[7] = e5 ^ e6 ^ e7;
|
||||
let ne5 = p5 ^ e7 ^ e6;
|
||||
let ne0 = p2 ^ ne5;
|
||||
let ne2 = p4 ^ ne0;
|
||||
let ne4 = p3 ^ ne2;
|
||||
let ne3 = p0 ^ ne5;
|
||||
let ne1 = p1 ^ ne3;
|
||||
|
||||
buffer[2] = ne0;
|
||||
buffer[3] = ne1;
|
||||
buffer[4] = ne2;
|
||||
buffer[5] = ne3;
|
||||
buffer[6] = ne4;
|
||||
buffer[7] = ne5;
|
||||
}
|
||||
|
||||
/// Decrypt key1 + validation: mode 0x23 (TEA+XOR) or 0x36 (TEA, BF2) — matches psa.c
|
||||
/// Calculate checksum over buffer[2..8] (matches psa.c psa_calculate_checksum)
|
||||
fn calculate_checksum(buffer: &[u8]) -> u8 {
|
||||
let mut checksum: u32 = 0;
|
||||
for i in 2..8 {
|
||||
checksum += (buffer[i] & 0xF) as u32 + ((buffer[i] >> 4) & 0xF) as u32;
|
||||
}
|
||||
((checksum.wrapping_mul(0x10)) & 0xFF) as u8
|
||||
}
|
||||
|
||||
/// Check if direct XOR is allowed by key2 high byte (matches psa.c)
|
||||
fn direct_xor_allowed_by_key2(key2_high_byte: u8) -> bool {
|
||||
let lo = key2_high_byte & 0xF;
|
||||
if lo < 3 { return true; }
|
||||
if lo < 7 && (key2_high_byte & 0xC) != 0 { return true; }
|
||||
false
|
||||
}
|
||||
|
||||
/// Setup byte buffer from key1/key2 (matches psa.c psa_setup_byte_buffer)
|
||||
fn setup_byte_buffer(buffer: &mut [u8], key1_low: u32, key1_high: u32, key2_low: u32) {
|
||||
for i in 0..8usize {
|
||||
let shift = i * 8;
|
||||
let byte_val = if shift < 32 {
|
||||
((key1_low >> shift) & 0xFF) as u8
|
||||
} else {
|
||||
((key1_high >> (shift - 32)) & 0xFF) as u8
|
||||
};
|
||||
buffer[7 - i] = byte_val;
|
||||
}
|
||||
buffer[9] = (key2_low & 0xFF) as u8;
|
||||
buffer[8] = ((key2_low >> 8) & 0xFF) as u8;
|
||||
}
|
||||
|
||||
/// Decrypt using the C code's approach: setup_byte_buffer then attempt direct XOR
|
||||
/// with checksum validation; mode 0x36 is marked for brute-force (matches psa.c)
|
||||
fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> {
|
||||
// Try mode 0x23 first (seed byte 0x23)
|
||||
let seed_byte = (self.key1_high >> 24) as u8;
|
||||
// C: key2_low = decode_data_low (the 16-bit validation sits in the low word)
|
||||
let key2_low = self.validation_field as u32;
|
||||
|
||||
if seed_byte >= 0x23 && seed_byte < 0x24 {
|
||||
// Mode 0x23 - TEA + XOR
|
||||
let mut v0 = self.key1_high;
|
||||
let mut v1 = self.key1_low;
|
||||
Self::tea_decrypt(&mut v0, &mut v1, &BF1_KEY_SCHEDULE);
|
||||
let mut buffer = [0u8; 48];
|
||||
Self::setup_byte_buffer(&mut buffer, self.key1_low, self.key1_high, key2_low);
|
||||
|
||||
let mut buffer = [0u8; 10];
|
||||
buffer[0] = (v0 >> 24) as u8;
|
||||
buffer[1] = (v0 >> 16) as u8;
|
||||
buffer[2] = (v0 >> 8) as u8;
|
||||
buffer[3] = (v0 >> 0) as u8;
|
||||
buffer[4] = (v1 >> 24) as u8;
|
||||
buffer[5] = (v1 >> 16) as u8;
|
||||
buffer[6] = (v1 >> 8) as u8;
|
||||
buffer[7] = (v1 >> 0) as u8;
|
||||
buffer[8] = (self.validation_field >> 8) as u8;
|
||||
buffer[9] = (self.validation_field & 0xFF) as u8;
|
||||
let key2_high_byte = buffer[8];
|
||||
|
||||
Self::xor_decrypt(&mut buffer);
|
||||
// Try direct XOR decrypt (mode 0x23) if allowed by key2 filter
|
||||
if Self::direct_xor_allowed_by_key2(key2_high_byte) {
|
||||
let checksum = Self::calculate_checksum(&buffer);
|
||||
let validation_result = (checksum ^ key2_high_byte) & 0xF0;
|
||||
|
||||
let serial = ((buffer[2] as u32) << 16)
|
||||
| ((buffer[3] as u32) << 8)
|
||||
| (buffer[4] as u32);
|
||||
let counter = ((buffer[5] as u32) << 8) | (buffer[6] as u32);
|
||||
let crc = buffer[7] as u16;
|
||||
let btn = buffer[8] & 0x0F;
|
||||
if validation_result == 0 {
|
||||
// Direct XOR decrypt succeeded validation
|
||||
Self::xor_decrypt(&mut buffer);
|
||||
|
||||
return Some((serial, btn, counter, crc, 0x23));
|
||||
let serial = ((buffer[3] as u32) << 8)
|
||||
| ((buffer[2] as u32) << 16)
|
||||
| (buffer[4] as u32);
|
||||
let counter = (buffer[6] as u32) | ((buffer[5] as u32) << 8);
|
||||
let crc = buffer[7] as u16;
|
||||
let btn = buffer[8] & 0x0F;
|
||||
|
||||
return Some((serial, btn, counter, crc, 0x23));
|
||||
}
|
||||
}
|
||||
|
||||
if seed_byte >= 0xF3 && seed_byte < 0xF4 {
|
||||
// Mode 0x36 - TEA + different key schedule
|
||||
let mut v0 = self.key1_high;
|
||||
let mut v1 = self.key1_low;
|
||||
Self::tea_decrypt(&mut v0, &mut v1, &BF2_KEY_SCHEDULE);
|
||||
// Mode 0x36 - TEA brute-force path
|
||||
// Try direct TEA decrypt with BF1 key schedule for a quick decode attempt
|
||||
{
|
||||
let mut w0 = ((buffer[3] as u32) << 16)
|
||||
| ((buffer[2] as u32) << 24)
|
||||
| ((buffer[4] as u32) << 8)
|
||||
| (buffer[5] as u32);
|
||||
let mut w1 = ((buffer[7] as u32) << 16)
|
||||
| ((buffer[6] as u32) << 24)
|
||||
| ((buffer[8] as u32) << 8)
|
||||
| (buffer[9] as u32);
|
||||
|
||||
let serial = ((v0 >> 8) & 0xFFFF00) | ((v0 & 0xFF) as u32);
|
||||
let counter = v1 >> 16;
|
||||
let btn = ((v1 >> 8) & 0xF) as u8;
|
||||
let crc = (v1 & 0xFF) as u16;
|
||||
Self::tea_decrypt(&mut w0, &mut w1, &BF1_KEY_SCHEDULE);
|
||||
|
||||
return Some((serial, btn, counter, crc, 0x36));
|
||||
// Check if the TEA CRC validates (sum of bytes)
|
||||
let crc_calc = Self::calculate_tea_crc(w0, w1);
|
||||
if crc_calc == (w1 & 0xFF) as u8 {
|
||||
let mut dec_buffer = [0u8; 48];
|
||||
dec_buffer[2] = ((w0 >> 24) & 0xFF) as u8;
|
||||
dec_buffer[3] = ((w0 >> 16) & 0xFF) as u8;
|
||||
dec_buffer[4] = ((w0 >> 8) & 0xFF) as u8;
|
||||
dec_buffer[5] = (w0 & 0xFF) as u8;
|
||||
dec_buffer[6] = ((w1 >> 24) & 0xFF) as u8;
|
||||
dec_buffer[7] = ((w1 >> 16) & 0xFF) as u8;
|
||||
dec_buffer[8] = ((w1 >> 8) & 0xFF) as u8;
|
||||
dec_buffer[9] = (w1 & 0xFF) as u8;
|
||||
|
||||
let btn = (dec_buffer[5] >> 4) & 0xF;
|
||||
let serial = ((dec_buffer[3] as u32) << 8)
|
||||
| ((dec_buffer[2] as u32) << 16)
|
||||
| (dec_buffer[4] as u32);
|
||||
let counter = ((dec_buffer[7] as u32) << 8)
|
||||
| ((dec_buffer[6] as u32) << 16)
|
||||
| (dec_buffer[8] as u32)
|
||||
| (((dec_buffer[5] as u32) & 0xF) << 24);
|
||||
let crc = dec_buffer[9] as u16;
|
||||
|
||||
return Some((serial, btn, counter, crc, 0x36));
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot decrypt - return raw data
|
||||
// Also try BF2 key schedule directly (XOR-derived keys)
|
||||
{
|
||||
let mut w0 = ((buffer[3] as u32) << 16)
|
||||
| ((buffer[2] as u32) << 24)
|
||||
| ((buffer[4] as u32) << 8)
|
||||
| (buffer[5] as u32);
|
||||
let mut w1 = ((buffer[7] as u32) << 16)
|
||||
| ((buffer[6] as u32) << 24)
|
||||
| ((buffer[8] as u32) << 8)
|
||||
| (buffer[9] as u32);
|
||||
|
||||
Self::tea_decrypt(&mut w0, &mut w1, &BF2_KEY_SCHEDULE);
|
||||
|
||||
let crc_calc = Self::calculate_tea_crc(w0, w1);
|
||||
if crc_calc == (w1 & 0xFF) as u8 {
|
||||
let mut dec_buffer = [0u8; 48];
|
||||
dec_buffer[2] = ((w0 >> 24) & 0xFF) as u8;
|
||||
dec_buffer[3] = ((w0 >> 16) & 0xFF) as u8;
|
||||
dec_buffer[4] = ((w0 >> 8) & 0xFF) as u8;
|
||||
dec_buffer[5] = (w0 & 0xFF) as u8;
|
||||
dec_buffer[6] = ((w1 >> 24) & 0xFF) as u8;
|
||||
dec_buffer[7] = ((w1 >> 16) & 0xFF) as u8;
|
||||
dec_buffer[8] = ((w1 >> 8) & 0xFF) as u8;
|
||||
dec_buffer[9] = (w1 & 0xFF) as u8;
|
||||
|
||||
let btn = (dec_buffer[5] >> 4) & 0xF;
|
||||
let serial = ((dec_buffer[3] as u32) << 8)
|
||||
| ((dec_buffer[2] as u32) << 16)
|
||||
| (dec_buffer[4] as u32);
|
||||
let counter = ((dec_buffer[7] as u32) << 8)
|
||||
| ((dec_buffer[6] as u32) << 16)
|
||||
| (dec_buffer[8] as u32)
|
||||
| (((dec_buffer[5] as u32) & 0xF) << 24);
|
||||
let crc = dec_buffer[9] as u16;
|
||||
|
||||
return Some((serial, btn, counter, crc, 0x36));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Calculate TEA CRC (matches psa.c psa_calculate_tea_crc)
|
||||
fn calculate_tea_crc(v0: u32, v1: u32) -> u8 {
|
||||
let mut crc: u32 = 0;
|
||||
crc += (v0 >> 24) & 0xFF;
|
||||
crc += (v0 >> 16) & 0xFF;
|
||||
crc += (v0 >> 8) & 0xFF;
|
||||
crc += v0 & 0xFF;
|
||||
crc += (v1 >> 24) & 0xFF;
|
||||
crc += (v1 >> 16) & 0xFF;
|
||||
crc += (v1 >> 8) & 0xFF;
|
||||
(crc & 0xFF) as u8
|
||||
}
|
||||
|
||||
fn init_preamble_state(&mut self) {
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.pattern_counter = 0;
|
||||
self.bit_count = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
fn finalize_frame(&mut self) -> Option<DecodedSignal> {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
if self.bit_count >= 80 {
|
||||
// C validation: ((key1_high >> 16) & 0xF) == 0xA
|
||||
if ((self.key1_high >> 16) & 0xF) != 0xA {
|
||||
return None;
|
||||
}
|
||||
let result = self.parse_data();
|
||||
return Some(result);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -317,24 +480,96 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.state {
|
||||
// State0: detect preamble pattern type
|
||||
DecoderState::WaitEdge => {
|
||||
if level && duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_49 {
|
||||
self.state = DecoderState::CountPattern;
|
||||
if !level {
|
||||
return None;
|
||||
}
|
||||
let diff_250 = duration_diff!(duration, TE_SHORT);
|
||||
let diff_125 = duration_diff!(duration, TE_SHORT_125);
|
||||
|
||||
if diff_250 < TE_TOLERANCE_99 {
|
||||
// Pattern 1: 250µs preamble
|
||||
self.init_preamble_state();
|
||||
self.state = DecoderState::CountPattern250;
|
||||
} else if diff_125 < 40 && duration <= 180 {
|
||||
// Pattern 2: 125µs preamble
|
||||
self.init_preamble_state();
|
||||
self.state = DecoderState::CountPattern125;
|
||||
}
|
||||
self.prev_duration = duration;
|
||||
}
|
||||
|
||||
// State1: count 250µs preamble (Pattern 1)
|
||||
DecoderState::CountPattern250 => {
|
||||
if level {
|
||||
return None;
|
||||
}
|
||||
let diff_short = duration_diff!(duration, TE_SHORT);
|
||||
if diff_short < TE_TOLERANCE_99 + 1 {
|
||||
let prev_diff = duration_diff!(self.prev_duration, TE_SHORT);
|
||||
if prev_diff <= TE_TOLERANCE_99 {
|
||||
self.pattern_counter += 1;
|
||||
}
|
||||
self.prev_duration = duration;
|
||||
self.pattern_counter = 0;
|
||||
} else {
|
||||
let diff_long = duration_diff!(duration, TE_LONG);
|
||||
if diff_long < 100 && self.pattern_counter > 0x46 {
|
||||
// Transition to Manchester decode at 250/500µs
|
||||
self.state = DecoderState::DecodeManchester250;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.pattern_counter = 0;
|
||||
self.prev_duration = duration;
|
||||
} else {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
self.pattern_counter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DecoderState::CountPattern => {
|
||||
// State2: Manchester decode at 250/500µs (Pattern 1)
|
||||
DecoderState::DecodeManchester250 => {
|
||||
if self.bit_count >= 121 {
|
||||
return self.finalize_frame();
|
||||
}
|
||||
// Check for end-of-frame marker
|
||||
if level && self.bit_count == 80 && duration >= 800 {
|
||||
let end_diff = duration_diff!(duration, TE_END_1000);
|
||||
if end_diff <= 199 {
|
||||
return self.finalize_frame();
|
||||
}
|
||||
}
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
|
||||
if duration > 10000 {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
self.pattern_counter = 0;
|
||||
return None;
|
||||
}
|
||||
|
||||
if is_short || is_long {
|
||||
if let Some(bit) = self.manchester_advance(is_short, level) {
|
||||
self.add_bit(bit);
|
||||
}
|
||||
}
|
||||
self.prev_duration = duration;
|
||||
}
|
||||
|
||||
// State3: count 125µs preamble (Pattern 2)
|
||||
DecoderState::CountPattern125 => {
|
||||
let diff_125 = duration_diff!(duration, TE_SHORT_125);
|
||||
let diff_250 = duration_diff!(duration, TE_LONG_250);
|
||||
|
||||
if diff_125 < TE_TOLERANCE_50 {
|
||||
self.pattern_counter += 1;
|
||||
self.prev_duration = duration;
|
||||
} else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x46 {
|
||||
// Found end of preamble, start Manchester decoding
|
||||
self.state = DecoderState::DecodeManchester;
|
||||
} else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x45 {
|
||||
// Transition to Manchester decode at 125/250µs
|
||||
self.state = DecoderState::DecodeManchester125;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
@@ -347,23 +582,17 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
DecoderState::DecodeManchester => {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
let is_end = duration > TE_END_1000;
|
||||
// State4: Manchester decode at 125/250µs (Pattern 2)
|
||||
DecoderState::DecodeManchester125 => {
|
||||
if self.bit_count >= 121 {
|
||||
return self.finalize_frame();
|
||||
}
|
||||
let is_short = duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_50;
|
||||
let is_long = duration_diff!(duration, TE_LONG_250) < TE_TOLERANCE_99;
|
||||
let is_end = duration > 500;
|
||||
|
||||
if is_end || self.bit_count >= 121 {
|
||||
// End of data
|
||||
self.state = DecoderState::End;
|
||||
|
||||
if self.bit_count >= 96 {
|
||||
// Got enough data
|
||||
let result = self.parse_data();
|
||||
self.state = DecoderState::WaitEdge;
|
||||
return Some(result);
|
||||
}
|
||||
self.state = DecoderState::WaitEdge;
|
||||
return None;
|
||||
if is_end {
|
||||
return self.finalize_frame();
|
||||
}
|
||||
|
||||
if is_short || is_long {
|
||||
@@ -373,13 +602,8 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
} else {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
}
|
||||
|
||||
self.prev_duration = duration;
|
||||
}
|
||||
|
||||
DecoderState::End => {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
@@ -406,31 +630,8 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
buffer[8] = button & 0x0F;
|
||||
buffer[9] = 0;
|
||||
|
||||
// XOR encrypt
|
||||
{
|
||||
let e6 = buffer[8];
|
||||
let e7 = buffer[9];
|
||||
let p0 = buffer[2];
|
||||
let p1 = buffer[3];
|
||||
let p2 = buffer[4];
|
||||
let p3 = buffer[5];
|
||||
let p4 = buffer[6];
|
||||
let p5 = buffer[7];
|
||||
|
||||
let ne5 = p5 ^ e7 ^ e6;
|
||||
let ne0 = p2 ^ ne5;
|
||||
let ne2 = p4 ^ ne0;
|
||||
let ne4 = p3 ^ ne2;
|
||||
let ne3 = p0 ^ ne5;
|
||||
let ne1 = p1 ^ ne3;
|
||||
|
||||
buffer[2] = ne0;
|
||||
buffer[3] = ne1;
|
||||
buffer[4] = ne2;
|
||||
buffer[5] = ne3;
|
||||
buffer[6] = ne4;
|
||||
buffer[7] = ne5;
|
||||
}
|
||||
// XOR encrypt (matches psa.c psa_second_stage_xor_encrypt)
|
||||
Self::xor_encrypt(&mut buffer);
|
||||
|
||||
// TEA encrypt
|
||||
let mut v0 = ((buffer[0] as u32) << 24)
|
||||
@@ -450,38 +651,42 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(512);
|
||||
|
||||
// Preamble + sync (matches protopirate psa encode)
|
||||
for _ in 0..70 {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT_125));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT_125));
|
||||
// Preamble: 80 iterations at 250us HIGH+LOW (matches C: te = PSA_TE_LONG_250)
|
||||
for _ in 0..80 {
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
}
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
|
||||
// Sync transition: LOW 250us, HIGH 500us, LOW 250us (matches C)
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
|
||||
// Key1: 64 bits Manchester, then validation 16 bits
|
||||
// Key1: 64 bits Manchester at 250us (C: bit 1 = true,false; bit 0 = false,true)
|
||||
let key1 = ((key1_high as u64) << 32) | (key1_low as u64);
|
||||
for bit in (0..64).rev() {
|
||||
if (key1 >> bit) & 1 == 1 {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
}
|
||||
}
|
||||
|
||||
// Validation: 16 bits Manchester encoded
|
||||
// Validation: 16 bits Manchester at 250us
|
||||
for bit in (0..16).rev() {
|
||||
if (validation >> bit) & 1 == 1 {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
}
|
||||
}
|
||||
|
||||
// End marker
|
||||
// End marker: HIGH 1000us + LOW 1000us (matches C)
|
||||
signal.push(LevelDuration::new(true, TE_END_1000));
|
||||
signal.push(LevelDuration::new(false, TE_END_1000));
|
||||
|
||||
Some(signal)
|
||||
|
||||
@@ -0,0 +1,919 @@
|
||||
//! PSA2 (Peugeot/Citroën — "PSA OLD") protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with Flipper-ARF reference: `lib/subghz/protocols/psa2.c` and `psa2.h`
|
||||
//! (internal name `SUBGHZ_PROTOCOL_PSA2_NAME = "PSA OLD"`). This is the OLDER PSA variant,
|
||||
//! distinct from KAT's existing `psa` (modified-TEA/XEA) decoder.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 250/500µs symbol (Pattern 1, standard rate) or 125/250µs (Pattern 2,
|
||||
//! half rate). Canonical Flipper `manchester_advance` table (events ShortLow=0, ShortHigh=2,
|
||||
//! LongLow=4, LongHigh=6), seeded `ManchesterStateMid1`.
|
||||
//! - 128-bit frame = key1 (64 bits) + key2/validation word. The decoder collects 64 bits → key1,
|
||||
//! then 16 more (to 80 bits = `KEY2_BITS`) → the 16-bit validation field / key2_low.
|
||||
//! - RF: AM (OOK). Frequency: 433.92 MHz.
|
||||
//! - Crypto: TEA (Tiny Encryption Algorithm) with a dual brute-force fallback (BF1
|
||||
//! 0x23000000–0x24000000, BF2 0xF3000000–0xF4000000) and a mode23/mode36 selector, validated
|
||||
//! via a nibble checksum.
|
||||
//!
|
||||
//! ## Decoder structure & PERFORMANCE
|
||||
//! The C live decoder (`subghz_protocol_decoder_psa2_feed`) only ever runs the cheap mode23 XOR
|
||||
//! path (`psa_decrypt_fast`) per frame — it NEVER runs the TEA brute force. The brute force
|
||||
//! (`psa_decrypt_full`, marked `__attribute__((unused))`) is reserved for the deferred-decrypt
|
||||
//! UI button, not the streaming decoder. KAT mirrors this exactly. Per frame:
|
||||
//!
|
||||
//! 1. Manchester-collect to exactly 80 bits (key1 + validation) with end-of-packet detection.
|
||||
//! 2. Run the O(1) `direct_xor_decrypt` (mode23) gated on its nibble checksum.
|
||||
//! 3. Emit only on a successful, field-bearing decrypt (see `finalize_frame` for why the C's bare
|
||||
//! `(validation & 0xF) == 0xA` emission is not reproduced in KAT's feed-all model).
|
||||
//!
|
||||
//! The bounded TEA brute force (BF1/BF2, ~16.7M iters each) is ported faithfully in
|
||||
//! `Psa2Decoder::decrypt_full` but is NEVER called from `feed()` — only the cheap O(1) gate runs
|
||||
//! per pulse, so the test sweep stays fast.
|
||||
//!
|
||||
//! Decoder steps: State0 (wait preamble) → State1/State3 (count preamble pulses) →
|
||||
//! State2/State4 (Manchester decode + decrypt). Encoder supported (mode23 path).
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
// Standard-rate timings (Pattern 1)
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 128;
|
||||
|
||||
// Half-rate timings (Pattern 2 / State 3-4)
|
||||
const TE_SHORT_HALF: u32 = 125;
|
||||
const TE_LONG_HALF: u32 = 250;
|
||||
const TOL_HALF: u32 = 50;
|
||||
|
||||
// End-of-packet markers
|
||||
const TE_END_1000: u32 = 1000;
|
||||
const TE_END_500: u32 = 500;
|
||||
|
||||
// Bit counts
|
||||
const KEY1_BITS: usize = 64; // 0x40
|
||||
const KEY2_BITS: usize = 80; // 0x50
|
||||
const MAX_BITS: usize = 121; // 0x79
|
||||
|
||||
// Preamble pulse-count thresholds (C: PSA_PATTERN_THRESHOLD_1/2)
|
||||
const PATTERN_THRESHOLD_1: u16 = 0x46;
|
||||
const PATTERN_THRESHOLD_2: u16 = 0x45;
|
||||
|
||||
// Validation nibble for the mode23 path: (validation_field & 0xF) == 0xA
|
||||
const VALID_NIBBLE: u16 = 0xA;
|
||||
|
||||
// "decrypted" success marker (C uses 0x50)
|
||||
const DECRYPTED_OK: u16 = 0x50;
|
||||
|
||||
// Mode selectors (stored as ASCII chars in firmware)
|
||||
const MODE_23: u8 = 0x23; // '#'
|
||||
const MODE_36: u8 = 0x36; // '6'
|
||||
|
||||
// PSA2 button codes are Lock=0, Unlock=1, Trunk=2 (psa_button_name). Decodes whose 4-bit button
|
||||
// exceeds this are coincidental matches on unrelated Manchester data and are rejected.
|
||||
const BTN_MAX_VALID: u8 = 0x2;
|
||||
|
||||
// TEA constants
|
||||
const TEA_DELTA: u32 = 0x9E3779B9;
|
||||
const TEA_ROUNDS: u32 = 32;
|
||||
|
||||
// BF1 brute-force range + constants (FUN_08028f94 / FUN_080291c0)
|
||||
const BF1_START: u32 = 0x2300_0000;
|
||||
const BF1_END: u32 = 0x2400_0000;
|
||||
const BF1_CONST_U4: u32 = 0x0E0F_5C41;
|
||||
const BF1_CONST_U5: u32 = 0x0F5C_4123;
|
||||
const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A43_4915, 0xD674_3C2B, 0x1F29_D308, 0xE6B7_9A64];
|
||||
|
||||
// BF2 brute-force range + key schedule (FUN_080290f8)
|
||||
const BF2_START: u32 = 0xF300_0000;
|
||||
const BF2_END: u32 = 0xF400_0000;
|
||||
const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039_C240, 0xEDA9_2CAB, 0x4306_C02A, 0x0219_2A04];
|
||||
|
||||
/// Canonical Flipper Manchester states (lib/toolbox/manchester_decoder.c order:
|
||||
/// Start1=0, Mid1=1, Mid0=2, Start0=3).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Start1 = 0,
|
||||
Mid1 = 1,
|
||||
Mid0 = 2,
|
||||
Start0 = 3,
|
||||
}
|
||||
|
||||
/// Decoder states (matches PSADecoderState0-4 in psa2.c).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderState {
|
||||
/// State0: wait for preamble start.
|
||||
WaitEdge,
|
||||
/// State1: count standard-rate (250µs) preamble pulses.
|
||||
CountPattern250,
|
||||
/// State2: receive key1 + key2/validation at standard rate.
|
||||
DecodeManchester250,
|
||||
/// State3: count half-rate (125µs) preamble pulses.
|
||||
CountPattern125,
|
||||
/// State4: receive key1 + key2/validation at half rate.
|
||||
DecodeManchester125,
|
||||
}
|
||||
|
||||
/// Result of a successful decrypt: (serial, button, counter, crc, type).
|
||||
type DecryptResult = (u32, u8, u32, u16, u8);
|
||||
|
||||
/// PSA2 ("PSA OLD") protocol decoder.
|
||||
pub struct Psa2Decoder {
|
||||
state: DecoderState,
|
||||
prev_duration: u32,
|
||||
manchester_state: ManchesterState,
|
||||
pattern_counter: u16,
|
||||
data_low: u32,
|
||||
data_high: u32,
|
||||
bit_count: usize,
|
||||
// Decoded fields
|
||||
key1_low: u32,
|
||||
key1_high: u32,
|
||||
validation_field: u16,
|
||||
key2_low: u32,
|
||||
key2_high: u32,
|
||||
}
|
||||
|
||||
impl Psa2Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: DecoderState::WaitEdge,
|
||||
prev_duration: 0,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
pattern_counter: 0,
|
||||
data_low: 0,
|
||||
data_high: 0,
|
||||
bit_count: 0,
|
||||
key1_low: 0,
|
||||
key1_high: 0,
|
||||
validation_field: 0,
|
||||
key2_low: 0,
|
||||
key2_high: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn near(dur: u32, target: u32, tol: u32) -> bool {
|
||||
duration_diff!(dur, target) <= tol
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Manchester — canonical Flipper transition table (transitions[] in
|
||||
// manchester_decoder.c). `event` ∈ {0,2,4,6}; returns Some(bit) on emit.
|
||||
// =========================================================================
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||
const TRANSITIONS: [u8; 4] = [0b0000_0001, 0b1001_0001, 0b1001_1011, 0b1111_1011];
|
||||
let state_idx = self.manchester_state as usize;
|
||||
let new_idx = (TRANSITIONS[state_idx] >> event) & 0x3;
|
||||
let new_state = match new_idx {
|
||||
0 => ManchesterState::Start1,
|
||||
1 => ManchesterState::Mid1,
|
||||
2 => ManchesterState::Mid0,
|
||||
_ => ManchesterState::Start0,
|
||||
};
|
||||
|
||||
if new_idx as usize == state_idx {
|
||||
// No progress → reset to Mid1, emit nothing.
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
return None;
|
||||
}
|
||||
self.manchester_state = new_state;
|
||||
match new_state {
|
||||
ManchesterState::Mid0 => Some(false),
|
||||
ManchesterState::Mid1 => Some(true),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn manchester_reset(&mut self) {
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
/// Shift one decoded bit into the 64-bit accumulator (data_high:data_low),
|
||||
/// latching key1 at 64 bits (matches psa2.c State2/State4 add path).
|
||||
fn add_bit(&mut self, bit: bool) {
|
||||
let carry = (self.data_low >> 31) & 1;
|
||||
self.data_low = (self.data_low << 1) | (bit as u32);
|
||||
self.data_high = (self.data_high << 1) | carry;
|
||||
self.bit_count += 1;
|
||||
if self.bit_count == KEY1_BITS {
|
||||
self.key1_low = self.data_low;
|
||||
self.key1_high = self.data_high;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn init_preamble_state(&mut self) {
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.pattern_counter = 0;
|
||||
self.bit_count = 0;
|
||||
self.manchester_reset();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CRYPTO PRIMITIVES (faithful to psa2.c)
|
||||
// =========================================================================
|
||||
|
||||
/// TEA encrypt (FUN_08028e14): dynamic key index `sum&3` then `(sum>>11)&3`.
|
||||
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let (mut a, mut b) = (*v0, *v1);
|
||||
let mut sum: u32 = 0;
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
let t = key[(sum & 3) as usize].wrapping_add(sum);
|
||||
sum = sum.wrapping_add(TEA_DELTA);
|
||||
a = a.wrapping_add(t ^ ((b >> 5) ^ (b << 4)).wrapping_add(b));
|
||||
let t = key[((sum >> 11) & 3) as usize].wrapping_add(sum);
|
||||
b = b.wrapping_add(t ^ ((a >> 5) ^ (a << 4)).wrapping_add(a));
|
||||
}
|
||||
*v0 = a;
|
||||
*v1 = b;
|
||||
}
|
||||
|
||||
/// TEA decrypt (FUN_08028e14 inverse): unwinds the encrypt rounds.
|
||||
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let (mut a, mut b) = (*v0, *v1);
|
||||
let mut sum: u32 = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
let t = key[((sum >> 11) & 3) as usize].wrapping_add(sum);
|
||||
sum = sum.wrapping_sub(TEA_DELTA);
|
||||
b = b.wrapping_sub(t ^ ((a >> 5) ^ (a << 4)).wrapping_add(a));
|
||||
let t = key[(sum & 3) as usize].wrapping_add(sum);
|
||||
a = a.wrapping_sub(t ^ ((b >> 5) ^ (b << 4)).wrapping_add(b));
|
||||
}
|
||||
*v0 = a;
|
||||
*v1 = b;
|
||||
}
|
||||
|
||||
/// Byte-sum CRC over 7 bytes of TEA output (FUN_08028e60).
|
||||
fn calculate_tea_crc(v0: u32, v1: u32) -> u8 {
|
||||
let mut crc: u32 = ((v0 >> 24) & 0xFF) + ((v0 >> 16) & 0xFF) + ((v0 >> 8) & 0xFF) + (v0 & 0xFF);
|
||||
crc += ((v1 >> 24) & 0xFF) + ((v1 >> 16) & 0xFF) + ((v1 >> 8) & 0xFF);
|
||||
(crc & 0xFF) as u8
|
||||
}
|
||||
|
||||
/// CRC-16/BUYPASS (poly 0x8005, init 0, no reflection) (FUN_08029098).
|
||||
fn calculate_crc16_bf2(data: &[u8]) -> u16 {
|
||||
let mut crc: u16 = 0;
|
||||
for &byte in data {
|
||||
crc ^= (byte as u16) << 8;
|
||||
for _ in 0..8 {
|
||||
if crc & 0x8000 != 0 {
|
||||
crc = (crc << 1) ^ 0x8005;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
/// Fill buf[0..9] from key1/key2 (FUN_080291c0 first loop / psa_setup_byte_buffer).
|
||||
/// key1 big-endian reversed into buf[7..0]; key2_low low/high bytes into buf[9]/buf[8].
|
||||
fn setup_byte_buffer(buf: &mut [u8], key1_low: u32, key1_high: u32, key2_low: u32) {
|
||||
for i in 0..8usize {
|
||||
let shift = i * 8;
|
||||
let b = if shift < 32 {
|
||||
(key1_low >> shift) as u8
|
||||
} else {
|
||||
(key1_high >> (shift - 32)) as u8
|
||||
};
|
||||
buf[7 - i] = b;
|
||||
}
|
||||
buf[9] = (key2_low & 0xFF) as u8;
|
||||
buf[8] = ((key2_low >> 8) & 0xFF) as u8;
|
||||
}
|
||||
|
||||
/// Nibble checksum over buf[2..8] → buf[11] (FUN_08028cf8).
|
||||
fn calculate_checksum(buf: &mut [u8]) {
|
||||
let mut sum: u32 = 0;
|
||||
for &b in buf.iter().take(8).skip(2) {
|
||||
sum += (b & 0xF) as u32 + ((b >> 4) & 0xF) as u32;
|
||||
}
|
||||
buf[11] = (sum.wrapping_mul(0x10) & 0xFF) as u8;
|
||||
}
|
||||
|
||||
/// XOR decrypt second stage (FUN_08028d54 + psa_copy_reverse FUN_08028d24).
|
||||
fn second_stage_xor_decrypt(buf: &mut [u8]) {
|
||||
// psa_copy_reverse
|
||||
let t = [
|
||||
buf[5], buf[4], buf[3], buf[2], buf[9], buf[8], buf[7], buf[6],
|
||||
];
|
||||
buf[2] = t[0] ^ t[6];
|
||||
buf[3] = t[2] ^ t[0];
|
||||
buf[4] = t[6] ^ t[3];
|
||||
buf[5] = t[7] ^ t[1];
|
||||
buf[6] = t[3] ^ t[1];
|
||||
buf[7] = t[6] ^ t[4] ^ t[5];
|
||||
}
|
||||
|
||||
/// Inverse of `second_stage_xor_decrypt`, used by the encoder.
|
||||
fn second_stage_xor_encrypt(buf: &mut [u8]) {
|
||||
let e6 = buf[8];
|
||||
let e7 = buf[9];
|
||||
let (p0, p1, p2, p3, p4, p5) = (buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]);
|
||||
let e5 = p5 ^ e7 ^ e6;
|
||||
let e0 = p2 ^ e5;
|
||||
let e2 = p4 ^ e0;
|
||||
let e4 = p3 ^ e2;
|
||||
let e3 = p0 ^ e5;
|
||||
let e1 = p1 ^ e3;
|
||||
buf[2] = e0;
|
||||
buf[3] = e1;
|
||||
buf[4] = e2;
|
||||
buf[5] = e3;
|
||||
buf[6] = e4;
|
||||
buf[7] = e5;
|
||||
}
|
||||
|
||||
/// Pack buf[2..9] into two TEA words (FUN_08028f4c).
|
||||
fn prepare_tea_data(buf: &[u8]) -> (u32, u32) {
|
||||
let w0 = ((buf[2] as u32) << 24) | ((buf[3] as u32) << 16) | ((buf[4] as u32) << 8) | buf[5] as u32;
|
||||
let w1 = ((buf[6] as u32) << 24) | ((buf[7] as u32) << 16) | ((buf[8] as u32) << 8) | buf[9] as u32;
|
||||
(w0, w1)
|
||||
}
|
||||
|
||||
/// Unpack two TEA words back into buf[2..9] (FUN_08028e88).
|
||||
fn unpack_tea_result(buf: &mut [u8], v0: u32, v1: u32) {
|
||||
buf[2] = (v0 >> 24) as u8;
|
||||
buf[3] = (v0 >> 16) as u8;
|
||||
buf[4] = (v0 >> 8) as u8;
|
||||
buf[5] = v0 as u8;
|
||||
buf[6] = (v1 >> 24) as u8;
|
||||
buf[7] = (v1 >> 16) as u8;
|
||||
buf[8] = (v1 >> 8) as u8;
|
||||
buf[9] = v1 as u8;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// FIELD EXTRACTION (FUN_08028f10)
|
||||
// =========================================================================
|
||||
|
||||
fn extract_fields_mode23(buf: &[u8]) -> DecryptResult {
|
||||
let button = buf[8] & 0xF;
|
||||
let serial = ((buf[2] as u32) << 16) | ((buf[3] as u32) << 8) | buf[4] as u32;
|
||||
let counter = (buf[6] as u32) | ((buf[5] as u32) << 8);
|
||||
let crc = buf[7] as u16;
|
||||
(serial, button, counter, crc, MODE_23)
|
||||
}
|
||||
|
||||
fn extract_fields_mode36(buf: &[u8]) -> DecryptResult {
|
||||
let button = (buf[5] >> 4) & 0xF;
|
||||
let serial = ((buf[2] as u32) << 16) | ((buf[3] as u32) << 8) | buf[4] as u32;
|
||||
let counter = ((buf[7] as u32) << 8)
|
||||
| ((buf[6] as u32) << 16)
|
||||
| (buf[8] as u32)
|
||||
| (((buf[5] as u32) & 0xF) << 24);
|
||||
let crc = buf[9] as u16;
|
||||
(serial, button, counter, crc, MODE_36)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DECRYPTION PATHS (faithful to psa2.c)
|
||||
// =========================================================================
|
||||
|
||||
/// key2-high gate (matches KAT's sibling `psa::direct_xor_allowed_by_key2`). The PSA2 C
|
||||
/// `psa_direct_xor_decrypt` validates only on the 4-bit checksum nibble `(checksum ^ key2_high)
|
||||
/// & 0xF0 == 0`, which has a ~1/16 false-positive rate on arbitrary 80-bit Manchester data. In
|
||||
/// KAT's "feed every frequency-compatible decoder, first match wins" model that lets PSA2 steal
|
||||
/// unrelated 250/500µs frames (e.g. VAG at 434 MHz). This precondition — the exact filter KAT's
|
||||
/// existing `psa` decoder already applies before its XOR path — is added on top of the C gate to
|
||||
/// suppress those false positives without dropping any genuine PSA2 decode.
|
||||
fn direct_xor_allowed_by_key2(key2_high_byte: u8) -> bool {
|
||||
let lo = key2_high_byte & 0xF;
|
||||
if lo < 3 {
|
||||
return true;
|
||||
}
|
||||
if lo < 7 && (key2_high_byte & 0xC) != 0 {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// mode23 XOR path (FUN_08028d98 / psa_direct_xor_decrypt). O(1) — the only path the
|
||||
/// live decoder runs. Returns the extracted fields on checksum validation.
|
||||
///
|
||||
/// Beyond the C's nibble-checksum gate, this applies two false-positive suppressors required by
|
||||
/// KAT's feed-all-decoders model: the `direct_xor_allowed_by_key2` precondition and a valid-
|
||||
/// button check (PSA2 buttons are Lock=0, Unlock=1, Trunk=2 only — see `psa_button_name`). Both
|
||||
/// cleanly separate genuine PSA2 frames from coincidental matches on unrelated Manchester data.
|
||||
fn direct_xor_decrypt(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<DecryptResult> {
|
||||
let mut buf = [0u8; 48];
|
||||
Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low);
|
||||
|
||||
let key2_high = buf[8];
|
||||
if !Self::direct_xor_allowed_by_key2(key2_high) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Self::calculate_checksum(&mut buf);
|
||||
let checksum = buf[11];
|
||||
let validation = (checksum ^ key2_high) & 0xF0;
|
||||
|
||||
if validation == 0 {
|
||||
// Firmware: update buf[8] high nibble before XOR stage.
|
||||
buf[8] = (buf[8] & 0x0F) | (checksum & 0xF0);
|
||||
buf[13] = buf[9] ^ buf[8];
|
||||
Self::second_stage_xor_decrypt(&mut buf);
|
||||
let fields = Self::extract_fields_mode23(&buf);
|
||||
// Reject implausible button codes (PSA2: Lock=0/Unlock=1/Trunk=2).
|
||||
if fields.1 > BTN_MAX_VALID {
|
||||
return None;
|
||||
}
|
||||
return Some(fields);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// BF1 brute force, range 0x23000000–0x24000000 (FUN_08028f94).
|
||||
///
|
||||
/// PERFORMANCE: up to ~16.7M TEA iterations. NEVER called from `feed()`. Only invoked from
|
||||
/// [`Self::decrypt_full`], which itself runs only after the cheap structural gate. Returns
|
||||
/// `(result, seed)` on success.
|
||||
fn brute_force_decrypt_bf1(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<(DecryptResult, u32)> {
|
||||
let mut buf = [0u8; 48];
|
||||
Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low);
|
||||
let (w0, w1) = Self::prepare_tea_data(&buf);
|
||||
|
||||
for counter in BF1_START..BF1_END {
|
||||
// Derive the working key with two TEA encrypts.
|
||||
let (mut wk2, mut wk3) = (BF1_CONST_U4, counter);
|
||||
Self::tea_encrypt(&mut wk2, &mut wk3, &BF1_KEY_SCHEDULE);
|
||||
let (mut wk0, mut wk1) = ((counter << 8) | 0x0E, BF1_CONST_U5);
|
||||
Self::tea_encrypt(&mut wk0, &mut wk1, &BF1_KEY_SCHEDULE);
|
||||
let wkey = [wk0, wk1, wk2, wk3];
|
||||
|
||||
let (mut dv0, mut dv1) = (w0, w1);
|
||||
Self::tea_decrypt(&mut dv0, &mut dv1, &wkey);
|
||||
|
||||
if (counter & 0xFFFFFF) == (dv0 >> 8) {
|
||||
let crc = Self::calculate_tea_crc(dv0, dv1);
|
||||
if crc == (dv1 & 0xFF) as u8 {
|
||||
let mut out = [0u8; 48];
|
||||
Self::unpack_tea_result(&mut out, dv0, dv1);
|
||||
return Some((Self::extract_fields_mode36(&out), counter));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// BF2 brute force, range 0xF3000000–0xF4000000 (FUN_080290f8).
|
||||
///
|
||||
/// PERFORMANCE: up to ~16.7M TEA iterations. NEVER called from `feed()` (see BF1 note).
|
||||
fn brute_force_decrypt_bf2(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<(DecryptResult, u32)> {
|
||||
let mut buf = [0u8; 48];
|
||||
Self::setup_byte_buffer(&mut buf, key1_low, key1_high, key2_low);
|
||||
let (w0, w1) = Self::prepare_tea_data(&buf);
|
||||
|
||||
for counter in BF2_START..BF2_END {
|
||||
let wkey = [
|
||||
BF2_KEY_SCHEDULE[0] ^ counter,
|
||||
BF2_KEY_SCHEDULE[1] ^ counter,
|
||||
BF2_KEY_SCHEDULE[2] ^ counter,
|
||||
BF2_KEY_SCHEDULE[3] ^ counter,
|
||||
];
|
||||
let (mut dv0, mut dv1) = (w0, w1);
|
||||
Self::tea_decrypt(&mut dv0, &mut dv1, &wkey);
|
||||
|
||||
if (counter & 0xFFFFFF) == (dv0 >> 8) {
|
||||
let crc_buf = [
|
||||
(dv0 >> 24) as u8,
|
||||
(dv0 >> 16) as u8,
|
||||
(dv0 >> 8) as u8,
|
||||
dv0 as u8,
|
||||
(dv1 >> 24) as u8,
|
||||
(dv1 >> 16) as u8,
|
||||
];
|
||||
let crc16 = Self::calculate_crc16_bf2(&crc_buf);
|
||||
let expected = ((dv1 & 0xFF) | (((dv1 >> 16) & 0xFF) << 8)) as u16;
|
||||
if crc16 == expected {
|
||||
let mut out = [0u8; 48];
|
||||
Self::unpack_tea_result(&mut out, dv0, dv1);
|
||||
return Some((Self::extract_fields_mode36(&out), counter));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Full decrypt router (FUN_080291c0, the `__attribute__((unused))` `psa_decrypt_full`):
|
||||
/// try XOR (mode23), then BF1, then BF2.
|
||||
///
|
||||
/// PERFORMANCE: this can run the bounded brute force (~33M TEA iterations worst case). It is
|
||||
/// NOT part of the live `feed()` path — `feed()` uses only `direct_xor_decrypt`. This mirrors
|
||||
/// the C, where `psa_decrypt_full` is unused by the decoder and the feed callback calls
|
||||
/// `psa_decrypt_fast` (XOR only). Exposed for completeness / offline decrypt; gate any caller
|
||||
/// behind the structural frame check so the brute force never runs on arbitrary data.
|
||||
#[allow(dead_code)]
|
||||
fn decrypt_full(key1_low: u32, key1_high: u32, key2_low: u32) -> Option<DecryptResult> {
|
||||
if let Some(r) = Self::direct_xor_decrypt(key1_low, key1_high, key2_low) {
|
||||
return Some(r);
|
||||
}
|
||||
if let Some((r, _seed)) = Self::brute_force_decrypt_bf1(key1_low, key1_high, key2_low) {
|
||||
return Some(r);
|
||||
}
|
||||
if let Some((r, _seed)) = Self::brute_force_decrypt_bf2(key1_low, key1_high, key2_low) {
|
||||
return Some(r);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Build the encoded key material for a mode23 frame (psa_build_encrypt_mode23 /
|
||||
/// FUN_08029028). Returns `(key1_high, key1_low, validation_field)`.
|
||||
fn encode_mode23(serial: u32, button: u8, counter: u16) -> (u32, u32, u16) {
|
||||
let mut buf = [0u8; 48];
|
||||
buf[2] = (serial >> 16) as u8;
|
||||
buf[3] = (serial >> 8) as u8;
|
||||
buf[4] = serial as u8;
|
||||
buf[5] = (counter >> 8) as u8;
|
||||
buf[6] = counter as u8;
|
||||
buf[7] = 0; // CRC placeholder
|
||||
buf[8] = button & 0xF;
|
||||
buf[9] = 0; // key2_low low byte
|
||||
Self::second_stage_xor_encrypt(&mut buf);
|
||||
Self::calculate_checksum(&mut buf);
|
||||
buf[8] = (buf[8] & 0x0F) | (buf[11] & 0xF0);
|
||||
buf[13] = buf[9] ^ buf[8];
|
||||
// buf[0]/buf[1] preamble bytes (no original key material → derive from data).
|
||||
buf[0] = buf[2] ^ buf[6];
|
||||
buf[1] = buf[3] ^ buf[7];
|
||||
|
||||
let key1_high =
|
||||
((buf[0] as u32) << 24) | ((buf[1] as u32) << 16) | ((buf[2] as u32) << 8) | buf[3] as u32;
|
||||
let key1_low =
|
||||
((buf[4] as u32) << 24) | ((buf[5] as u32) << 16) | ((buf[6] as u32) << 8) | buf[7] as u32;
|
||||
let validation = ((buf[8] as u16) << 8) | buf[9] as u16;
|
||||
(key1_high, key1_low, validation)
|
||||
}
|
||||
|
||||
/// Map KAT's generic button command to a PSA2 button code (Lock=0, Unlock=1, Trunk=2).
|
||||
fn map_button(button: u8) -> u8 {
|
||||
match button {
|
||||
0x01 => 0x0, // Lock
|
||||
0x02 => 0x1, // Unlock
|
||||
0x04 => 0x2, // Trunk
|
||||
0x08 => 0x2, // Panic → Trunk (PSA2 has no panic code)
|
||||
b => b & 0x0F,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finalize a collected 80-bit frame: latch validation/key2, run the cheap mode23 XOR gate,
|
||||
/// emit only on successful decryption. NEVER runs the brute force.
|
||||
///
|
||||
/// The C feed (`subghz_protocol_decoder_psa2_feed`) also emits on a bare
|
||||
/// `(validation_field & 0xF) == 0xA` even when the XOR decrypt fails — but that path yields a
|
||||
/// raw, *undecrypted* frame (no serial/button/counter) that the firmware UI keeps so the user
|
||||
/// can later run the brute-force button. KAT's pipeline feeds every frequency-compatible
|
||||
/// decoder and reports the first that fires, so a 1/16-probability nibble match with no fields
|
||||
/// is indistinguishable from noise and steals unrelated 250/500µs frames (e.g. VAG at 434 MHz).
|
||||
/// We therefore gate emission strictly on a successful, field-bearing decrypt — the same path
|
||||
/// that produces the genuine GROUPE PSA decodes — which is the meaningful half of the C gate.
|
||||
fn finalize_frame(&mut self) -> Option<DecodedSignal> {
|
||||
// C: validation_field = decode_data_low & 0xFFFF; key2_low = decode_data_low.
|
||||
self.validation_field = (self.data_low & 0xFFFF) as u16;
|
||||
self.key2_low = self.data_low;
|
||||
self.key2_high = self.data_high;
|
||||
|
||||
// C key2_low for decrypt is the 16-bit validation word in the low position.
|
||||
let decrypt =
|
||||
Self::direct_xor_decrypt(self.key1_low, self.key1_high, self.validation_field as u32);
|
||||
|
||||
// Reset collection regardless (the C feed always rewinds to State0 after a frame attempt).
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.state = DecoderState::WaitEdge;
|
||||
|
||||
let (serial, button, counter, _crc, _type) = decrypt?;
|
||||
let _ = (DECRYPTED_OK, VALID_NIBBLE); // C markers documented; emission gated on decrypt.
|
||||
let data = ((self.key1_high as u64) << 32) | self.key1_low as u64;
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter as u16),
|
||||
crc_valid: true,
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for Psa2Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"PSA2"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
self.prev_duration = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.pattern_counter = 0;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.key1_low = 0;
|
||||
self.key1_high = 0;
|
||||
self.validation_field = 0;
|
||||
self.key2_low = 0;
|
||||
self.key2_high = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.state {
|
||||
// State0: detect preamble pattern type.
|
||||
DecoderState::WaitEdge => {
|
||||
if !level {
|
||||
return None;
|
||||
}
|
||||
self.init_preamble_state();
|
||||
self.prev_duration = duration;
|
||||
if Self::near(duration, TE_SHORT, TE_DELTA) {
|
||||
self.state = DecoderState::CountPattern250;
|
||||
} else if Self::near(duration, TE_SHORT_HALF, TOL_HALF) {
|
||||
self.state = DecoderState::CountPattern125;
|
||||
}
|
||||
}
|
||||
|
||||
// State1: count standard-rate (250µs) preamble pulses.
|
||||
DecoderState::CountPattern250 => {
|
||||
if level {
|
||||
return None;
|
||||
}
|
||||
if Self::near(duration, TE_SHORT, TE_DELTA) {
|
||||
if Self::near(self.prev_duration, TE_SHORT, TE_DELTA) {
|
||||
self.pattern_counter += 1;
|
||||
}
|
||||
self.prev_duration = duration;
|
||||
return None;
|
||||
}
|
||||
if Self::near(duration, TE_LONG, TE_DELTA) {
|
||||
if self.pattern_counter > PATTERN_THRESHOLD_1 {
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.manchester_reset();
|
||||
self.state = DecoderState::DecodeManchester250;
|
||||
}
|
||||
self.pattern_counter = 0;
|
||||
self.prev_duration = duration;
|
||||
return None;
|
||||
}
|
||||
self.state = DecoderState::WaitEdge;
|
||||
self.pattern_counter = 0;
|
||||
}
|
||||
|
||||
// State2: receive key1 + key2/validation at standard rate.
|
||||
DecoderState::DecodeManchester250 => {
|
||||
if self.bit_count >= MAX_BITS {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
return None;
|
||||
}
|
||||
// End-of-packet detection at KEY2_BITS.
|
||||
if level && self.bit_count == KEY2_BITS && Self::near(duration, TE_END_1000, 199) {
|
||||
return self.finalize_frame();
|
||||
}
|
||||
|
||||
let event: Option<u8>;
|
||||
if Self::near(duration, TE_SHORT, TE_DELTA) {
|
||||
event = Some(((level as u8 ^ 1) & 0x7F) << 1);
|
||||
} else if Self::near(duration, TE_LONG, TE_DELTA) {
|
||||
event = Some(if level { 4 } else { 6 });
|
||||
} else {
|
||||
// Out-of-range low pulse: secondary end marker when 80 bits collected. The C
|
||||
// also gates this on a (stale) nibble==0xA; emission is now decided in
|
||||
// finalize_frame (decrypt-or-reject), so we trigger on the end geometry alone.
|
||||
if !level && Self::near(duration, TE_END_1000, 199) && self.bit_count == KEY2_BITS
|
||||
{
|
||||
return self.finalize_frame();
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(ev) = event {
|
||||
if self.bit_count < KEY2_BITS {
|
||||
if let Some(bit) = self.manchester_advance(ev) {
|
||||
self.add_bit(bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.prev_duration = duration;
|
||||
}
|
||||
|
||||
// State3: count half-rate (125µs) preamble pulses.
|
||||
DecoderState::CountPattern125 => {
|
||||
if level {
|
||||
return None;
|
||||
}
|
||||
if Self::near(duration, TE_SHORT_HALF, TOL_HALF) {
|
||||
if Self::near(self.prev_duration, TE_SHORT_HALF, TOL_HALF) {
|
||||
self.pattern_counter += 1;
|
||||
} else {
|
||||
self.pattern_counter = 0;
|
||||
}
|
||||
self.prev_duration = duration;
|
||||
return None;
|
||||
}
|
||||
if (TE_LONG_HALF..0x12C).contains(&duration) {
|
||||
if self.pattern_counter > PATTERN_THRESHOLD_2 {
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.manchester_reset();
|
||||
self.state = DecoderState::DecodeManchester125;
|
||||
}
|
||||
self.pattern_counter = 0;
|
||||
self.prev_duration = duration;
|
||||
return None;
|
||||
}
|
||||
self.state = DecoderState::WaitEdge;
|
||||
}
|
||||
|
||||
// State4: receive key1 + key2/validation at half rate.
|
||||
DecoderState::DecodeManchester125 => {
|
||||
if self.bit_count >= MAX_BITS {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
return None;
|
||||
}
|
||||
if !level {
|
||||
let event: Option<u8> = if Self::near(duration, TE_SHORT_HALF, TOL_HALF) {
|
||||
Some(((level as u8 ^ 1) & 0x7F) << 1)
|
||||
} else if (TE_LONG_HALF..0x12C).contains(&duration) {
|
||||
Some(if level { 4 } else { 6 })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(ev) = event {
|
||||
if let Some(bit) = self.manchester_advance(ev) {
|
||||
self.add_bit(bit);
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
// Rising edge: end-of-packet at 500µs.
|
||||
if Self::near(duration, TE_END_500, 99) {
|
||||
if self.bit_count != KEY2_BITS {
|
||||
return None;
|
||||
}
|
||||
return self.finalize_frame();
|
||||
}
|
||||
}
|
||||
self.prev_duration = duration;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
// Increment counter on TX (matches FUN_080299a0 rolling-counter increment).
|
||||
let counter = decoded.counter.unwrap_or(0).wrapping_add(1);
|
||||
|
||||
let (key1_high, key1_low, validation) =
|
||||
Self::encode_mode23(serial, Self::map_button(button), counter);
|
||||
|
||||
// mode23 timings: te=250µs, sync long=500µs, end=1000µs.
|
||||
let te = TE_SHORT;
|
||||
let te_long_sync = TE_LONG;
|
||||
let end_dur = TE_END_1000;
|
||||
|
||||
let mut signal = Vec::with_capacity(600);
|
||||
|
||||
// Preamble: 80 pairs of (HIGH te)+(LOW te).
|
||||
for _ in 0..80 {
|
||||
signal.push(LevelDuration::new(true, te));
|
||||
signal.push(LevelDuration::new(false, te));
|
||||
}
|
||||
|
||||
// Sync: (LOW te) + (HIGH te_long) + (LOW te).
|
||||
signal.push(LevelDuration::new(false, te));
|
||||
signal.push(LevelDuration::new(true, te_long_sync));
|
||||
signal.push(LevelDuration::new(false, te));
|
||||
|
||||
// key1: 64 bits MSB-first. bit=1 → (HIGH,LOW); bit=0 → (LOW,HIGH).
|
||||
let k1 = ((key1_high as u64) << 32) | key1_low as u64;
|
||||
for bit in (0..64).rev() {
|
||||
let b = (k1 >> bit) & 1 == 1;
|
||||
signal.push(LevelDuration::new(b, te));
|
||||
signal.push(LevelDuration::new(!b, te));
|
||||
}
|
||||
|
||||
// validation_field: 16 bits MSB-first.
|
||||
for bit in (0..16).rev() {
|
||||
let b = (validation >> bit) & 1 == 1;
|
||||
signal.push(LevelDuration::new(b, te));
|
||||
signal.push(LevelDuration::new(!b, te));
|
||||
}
|
||||
|
||||
// End burst: (HIGH end) + (LOW end).
|
||||
signal.push(LevelDuration::new(true, end_dur));
|
||||
signal.push(LevelDuration::new(false, end_dur));
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Psa2Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tea_round_trip() {
|
||||
// TEA encrypt then decrypt must be the identity for any key.
|
||||
let key = BF1_KEY_SCHEDULE;
|
||||
let (mut v0, mut v1) = (0x1234_5678u32, 0x9ABC_DEF0u32);
|
||||
let (o0, o1) = (v0, v1);
|
||||
Psa2Decoder::tea_encrypt(&mut v0, &mut v1, &key);
|
||||
Psa2Decoder::tea_decrypt(&mut v0, &mut v1, &key);
|
||||
assert_eq!((v0, v1), (o0, o1), "TEA encrypt/decrypt not invertible");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode23_encode_decode_round_trip() {
|
||||
// Encode (serial,button,counter) → key1/validation, then run the mode23 XOR decrypt and
|
||||
// confirm the fields (and a passing nibble checksum) come back exactly. Validates TEA-free
|
||||
// mode23 path: second_stage_xor + nibble checksum + field packing.
|
||||
for &(serial, btn, cnt) in &[
|
||||
(0x99EB25u32, 0u8, 0x039Bu16),
|
||||
(0x123456, 2, 0x0042),
|
||||
(0xABCDEF, 1, 0x1234),
|
||||
(0x0000FF, 0, 0x0001),
|
||||
] {
|
||||
let (k1h, k1l, vf) = Psa2Decoder::encode_mode23(serial, btn, cnt);
|
||||
let decrypt = Psa2Decoder::direct_xor_decrypt(k1l, k1h, vf as u32);
|
||||
assert!(
|
||||
decrypt.is_some(),
|
||||
"mode23 XOR decrypt failed to validate for serial={serial:06X} btn={btn} cnt={cnt:04X}"
|
||||
);
|
||||
let (ds, db, dc, _crc, ty) = decrypt.unwrap();
|
||||
assert_eq!(ds, serial, "serial mismatch");
|
||||
assert_eq!(db, btn, "button mismatch");
|
||||
assert_eq!(dc, cnt as u32, "counter mismatch");
|
||||
assert_eq!(ty, MODE_23, "mode mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checksum_matches_reference() {
|
||||
// Spot-check the nibble checksum against the hand-computed C formula.
|
||||
let mut buf = [0u8; 48];
|
||||
for (i, b) in buf.iter_mut().enumerate().take(8).skip(2) {
|
||||
*b = (i as u8) * 0x11; // 0x22,0x33,0x44,0x55,0x66,0x77
|
||||
}
|
||||
Psa2Decoder::calculate_checksum(&mut buf);
|
||||
// bytes 0x22,0x33,0x44,0x55,0x66,0x77 → nibble sum = (2+2)+(3+3)+(4+4)+(5+5)+(6+6)+(7+7) = 54
|
||||
let expected = ((54u32 * 0x10) & 0xFF) as u8; // 54*16 = 864 = 0x360 → 0x60
|
||||
assert_eq!(buf[11], expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_emits_manchester_frame() {
|
||||
// The encoder produces a non-trivial 250/500µs Manchester upload.
|
||||
let dec = Psa2Decoder::new();
|
||||
let decoded = DecodedSignal {
|
||||
serial: Some(0x99EB25),
|
||||
button: Some(0x01),
|
||||
counter: Some(0x039A),
|
||||
crc_valid: true,
|
||||
data: 0,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
};
|
||||
let sig = dec.encode(&decoded, 0x01).expect("encode should succeed");
|
||||
// 80 preamble pairs (160) + 3 sync + 64*2 + 16*2 + 2 end = 160+3+128+32+2 = 325.
|
||||
assert_eq!(sig.len(), 325, "unexpected upload length");
|
||||
assert!(sig.iter().all(|p| p.duration_us > 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
//! Toyota / Lexus KeeLoq protocol decoder (dual variant)
|
||||
//!
|
||||
//! Ported from Flipper-ARF reference: `lib/subghz/protocols/toyota.c` and `toyota.h`.
|
||||
//! Decode-only — the reference `encoder` field is NULL.
|
||||
//!
|
||||
//! Two variants, detected from the first HIGH pulse width (threshold 310µs):
|
||||
//!
|
||||
//! - **Variant A** — Corolla / 433.92 MHz. PWM pairs: te_short=400µs, te_long=800µs,
|
||||
//! delta=175µs. LS (long HIGH + short LOW) = bit 0, SL (short HIGH + long LOW) = bit 1.
|
||||
//! Preamble = repeated short-short (SS) pairs; first non-SS pair is the first data bit.
|
||||
//! Frame = 68 bits; min_count_bit = 60.
|
||||
//! - **Variant B** — Tundra / 315 MHz. NRZ: each individual pulse encodes one bit by a
|
||||
//! midpoint classifier (`<= 287µs` -> 0, `> 287µs` -> 1). Preamble = short-HIGH /
|
||||
//! long-LOW pairs (te_short=200µs, te_long=390µs, delta=120µs) terminated by a sync gap
|
||||
//! (LOW between 1500 and 2600µs). Frame = 67 bits; min_count_bit = 60.
|
||||
//!
|
||||
//! KeeLoq hopping: the hop field is left encrypted (the reference does not decrypt or run a
|
||||
//! CRC). `data` layout matches the reference `generic.data`:
|
||||
//! `(hop << 32) | (serial << 4) | button`, with hop=32 bits, serial=28 bits, button=4 bits.
|
||||
//!
|
||||
//! Emission is gated tightly (exact frame bit count + structural preamble/sync + non-zero
|
||||
//! serial) so it does not false-match the other KeeLoq-PWM protocols (Kia V3/V4, Subaru,
|
||||
//! Suzuki, etc.). The shared 433 MHz KeeLoq-PWM air encoding means a Toyota Variant-A frame
|
||||
//! that also satisfies Kia V3/V4's 68-bit/CRC4 structure is claimed by Kia V3/V4 first (it is
|
||||
//! earlier in the registry); Toyota uniquely claims 60-bit frames and Variant-B NRZ at 315 MHz.
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
// Variant A physical constants (Corolla / 433 MHz)
|
||||
const A_TE_SHORT: u32 = 400;
|
||||
const A_TE_LONG: u32 = 800;
|
||||
const A_TE_DELTA: u32 = 175;
|
||||
|
||||
// Variant B physical constants (Tundra / 315 MHz, preamble classification only)
|
||||
const B_TE_SHORT: u32 = 200;
|
||||
const B_TE_LONG: u32 = 390;
|
||||
const B_TE_DELTA: u32 = 120;
|
||||
|
||||
const MIN_COUNT_BIT: usize = 60;
|
||||
|
||||
/// NRZ midpoint for Variant B data pulses (`<= 287` -> 0, `> 287` -> 1).
|
||||
const B_NRZ_MIDPOINT: u32 = 287;
|
||||
|
||||
/// Sync gap (LOW) separating preamble from data in Variant B.
|
||||
const B_SYNC_GAP_MIN: u32 = 1500;
|
||||
const B_SYNC_GAP_MAX: u32 = 2600;
|
||||
|
||||
/// Minimum preamble pairs before a frame is accepted.
|
||||
const A_PREAMBLE_MIN: u16 = 6;
|
||||
const B_PREAMBLE_MIN: u16 = 6;
|
||||
|
||||
/// Frame lengths in bits.
|
||||
const A_BITS: usize = 68;
|
||||
const B_BITS: usize = 67;
|
||||
|
||||
/// First HIGH duration below this -> Variant B, at or above -> Variant A.
|
||||
const VARIANT_THRESH: u32 = 310;
|
||||
|
||||
#[inline]
|
||||
fn a_is_short(d: u32) -> bool {
|
||||
duration_diff!(d, A_TE_SHORT) < A_TE_DELTA
|
||||
}
|
||||
#[inline]
|
||||
fn a_is_long(d: u32) -> bool {
|
||||
duration_diff!(d, A_TE_LONG) < A_TE_DELTA
|
||||
}
|
||||
#[inline]
|
||||
fn b_is_short(d: u32) -> bool {
|
||||
duration_diff!(d, B_TE_SHORT) < B_TE_DELTA
|
||||
}
|
||||
#[inline]
|
||||
fn b_is_long(d: u32) -> bool {
|
||||
duration_diff!(d, B_TE_LONG) < B_TE_DELTA
|
||||
}
|
||||
|
||||
/// Decoder steps (matches ToyotaDecoderStep in toyota.c).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
PreambleA,
|
||||
DataA,
|
||||
PreambleB,
|
||||
DataB,
|
||||
}
|
||||
|
||||
/// Toyota / Lexus protocol decoder (matches SubGhzProtocolDecoderToyota).
|
||||
pub struct ToyotaDecoder {
|
||||
step: DecoderStep,
|
||||
/// 128-bit shift accumulator (hi:lo), matching the C `bits_hi`/`bits_lo`.
|
||||
bits_hi: u64,
|
||||
bits_lo: u64,
|
||||
bit_count: usize,
|
||||
te_last: u32,
|
||||
have_high: bool,
|
||||
preamble_count: u16,
|
||||
/// 0 = Variant A (Corolla / 433 MHz), 1 = Variant B (Tundra / 315 MHz).
|
||||
variant: u8,
|
||||
}
|
||||
|
||||
impl ToyotaDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
bits_hi: 0,
|
||||
bits_lo: 0,
|
||||
bit_count: 0,
|
||||
te_last: 0,
|
||||
have_high: false,
|
||||
preamble_count: 0,
|
||||
variant: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the parse state. Matches `subghz_protocol_decoder_toyota_reset`, which intentionally
|
||||
/// does NOT clear `variant` (detected once per session); the variant is re-detected on the next
|
||||
/// pulse from the Reset step anyway.
|
||||
fn reset_state(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.bits_hi = 0;
|
||||
self.bits_lo = 0;
|
||||
self.bit_count = 0;
|
||||
self.te_last = 0;
|
||||
self.have_high = false;
|
||||
self.preamble_count = 0;
|
||||
}
|
||||
|
||||
/// Push one bit into the 128-bit accumulator (matches `toyota_push_bit`).
|
||||
fn push_bit(&mut self, bit: u8) {
|
||||
let carry = (self.bits_lo >> 63) & 1;
|
||||
self.bits_hi = (self.bits_hi << 1) | carry;
|
||||
self.bits_lo = (self.bits_lo << 1) | (bit as u64 & 1);
|
||||
self.bit_count += 1;
|
||||
}
|
||||
|
||||
/// Extract `length` bits at `offset` from the end of the accumulator (matches `toyota_extract`).
|
||||
fn extract(&self, offset: usize, length: usize) -> u32 {
|
||||
let mut result: u32 = 0;
|
||||
let total = self.bit_count as isize;
|
||||
for i in 0..length {
|
||||
let pos = (total - 1) - (offset as isize + i as isize);
|
||||
let b = if pos >= 64 {
|
||||
((self.bits_hi >> (pos - 64)) & 1) as u32
|
||||
} else if pos >= 0 {
|
||||
((self.bits_lo >> pos) & 1) as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
result = (result << 1) | b;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Build the decoded signal once a full frame is collected (matches `toyota_decode_and_fire`).
|
||||
/// Returns `None` if the structural gate (bit count / serial) fails.
|
||||
fn decode_and_fire(&self) -> Option<DecodedSignal> {
|
||||
if self.bit_count < MIN_COUNT_BIT {
|
||||
return None;
|
||||
}
|
||||
|
||||
let hop = self.extract(0, 32);
|
||||
let serial = self.extract(32, 28);
|
||||
let button = self.extract(60, 4) as u8;
|
||||
|
||||
// Tight gate: require a non-zero serial so a run of all-zero/garbage bits that happens to
|
||||
// reach the bit count cannot emit a Toyota frame.
|
||||
if serial == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// generic.data = (hop << 32) | (serial << 4) | button
|
||||
let data = ((hop as u64) << 32) | ((serial as u64) << 4) | (button as u64 & 0x0F);
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
// KeeLoq hop is left encrypted (no key / no decrypt in the reference).
|
||||
counter: None,
|
||||
// No CRC in the reference; a fully-structured frame of the exact bit count is the
|
||||
// validity criterion (the callback only fires at min_count_bit).
|
||||
crc_valid: true,
|
||||
data,
|
||||
data_count_bit: self.bit_count,
|
||||
encoder_capable: false,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Feed for Variant A (Corolla / 433 MHz) — PWM pair encoding (matches `toyota_feed_variant_a`).
|
||||
///
|
||||
/// Faithful to the reference's gap-terminated emission path. The reference ALSO self-fires the
|
||||
/// instant `bit_count` reaches 68; that is intentionally dropped here. Variant A is the same
|
||||
/// KeeLoq-PWM air protocol as Kia V3/V4, and the early self-fire (on the normal short LOW that
|
||||
/// completes bit 68) lands several pulses BEFORE Kia V3/V4's sync-terminated fire — which, in
|
||||
/// KAT's "first decoder to fire on a pulse wins" stream, would let Toyota steal every shared
|
||||
/// 68-bit frame from Kia. By emitting only on the terminating gap (the same pulse Kia fires on),
|
||||
/// the registry order (Kia earlier) resolves the shared frames in Kia's favour, while Toyota
|
||||
/// still uniquely claims 60–67-bit Variant-A frames that Kia rejects. Bit accumulation is capped
|
||||
/// at 68 so the field layout is preserved regardless of trailing repeats.
|
||||
fn feed_variant_a(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::PreambleA => {
|
||||
if level {
|
||||
self.te_last = duration;
|
||||
self.have_high = true;
|
||||
return None;
|
||||
}
|
||||
|
||||
if !self.have_high {
|
||||
self.reset_state();
|
||||
return None;
|
||||
}
|
||||
self.have_high = false;
|
||||
|
||||
let hs = a_is_short(self.te_last);
|
||||
let hl = a_is_long(self.te_last);
|
||||
let ls = a_is_short(duration);
|
||||
let ll = a_is_long(duration);
|
||||
|
||||
if hs && ls {
|
||||
self.preamble_count += 1;
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.preamble_count < A_PREAMBLE_MIN {
|
||||
self.reset_state();
|
||||
return None;
|
||||
}
|
||||
|
||||
self.bits_hi = 0;
|
||||
self.bits_lo = 0;
|
||||
self.bit_count = 0;
|
||||
|
||||
if hl && ls {
|
||||
self.push_bit(0);
|
||||
} else if hs && ll {
|
||||
self.push_bit(1);
|
||||
}
|
||||
|
||||
self.step = DecoderStep::DataA;
|
||||
None
|
||||
}
|
||||
|
||||
DecoderStep::DataA => {
|
||||
if level {
|
||||
if a_is_short(duration) || a_is_long(duration) {
|
||||
self.te_last = duration;
|
||||
self.have_high = true;
|
||||
} else {
|
||||
// Terminating gap / out-of-range HIGH: emit (deferring to Kia on shared
|
||||
// 68-bit frames via registry order — see fn doc-comment).
|
||||
let result = if self.bit_count >= MIN_COUNT_BIT {
|
||||
self.decode_and_fire()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.reset_state();
|
||||
return result;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if !self.have_high {
|
||||
return None;
|
||||
}
|
||||
self.have_high = false;
|
||||
|
||||
// Cap accumulation at A_BITS: once a full 68-bit frame is collected, stop pushing
|
||||
// more bits and wait for the terminating gap. This keeps the field layout fixed and
|
||||
// makes emission coincide with Kia V3/V4's sync-terminated fire so Kia wins shared
|
||||
// frames by registry order.
|
||||
if self.bit_count >= A_BITS {
|
||||
return None;
|
||||
}
|
||||
|
||||
let hs = a_is_short(self.te_last);
|
||||
let hl = a_is_long(self.te_last);
|
||||
let ls = a_is_short(duration);
|
||||
let ll = a_is_long(duration);
|
||||
|
||||
if hl && ls {
|
||||
self.push_bit(0);
|
||||
} else if hs && ll {
|
||||
self.push_bit(1);
|
||||
} else {
|
||||
let result = if self.bit_count >= MIN_COUNT_BIT {
|
||||
self.decode_and_fire()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.reset_state();
|
||||
return result;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed for Variant B (Tundra / 315 MHz) — NRZ encoding (matches `toyota_feed_variant_b`).
|
||||
fn feed_variant_b(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::PreambleB => {
|
||||
if level {
|
||||
if b_is_short(duration) {
|
||||
self.te_last = duration;
|
||||
self.have_high = true;
|
||||
} else {
|
||||
self.reset_state();
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Falling edge
|
||||
if !self.have_high {
|
||||
self.reset_state();
|
||||
return None;
|
||||
}
|
||||
self.have_high = false;
|
||||
|
||||
// Sync gap: LOW ~1938µs -> transition to data
|
||||
if duration >= B_SYNC_GAP_MIN && duration <= B_SYNC_GAP_MAX {
|
||||
if self.preamble_count >= B_PREAMBLE_MIN {
|
||||
self.bits_hi = 0;
|
||||
self.bits_lo = 0;
|
||||
self.bit_count = 0;
|
||||
self.have_high = false;
|
||||
self.step = DecoderStep::DataB;
|
||||
} else {
|
||||
self.reset_state();
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Normal preamble LOW must be LONG
|
||||
if b_is_long(duration) {
|
||||
self.preamble_count += 1;
|
||||
return None;
|
||||
}
|
||||
|
||||
self.reset_state();
|
||||
None
|
||||
}
|
||||
|
||||
DecoderStep::DataB => {
|
||||
// Every pulse (HIGH or LOW) encodes one bit. A pulse >= sync-gap min ends the frame.
|
||||
if duration >= B_SYNC_GAP_MIN {
|
||||
let result = if self.bit_count >= MIN_COUNT_BIT {
|
||||
self.decode_and_fire()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.reset_state();
|
||||
return result;
|
||||
}
|
||||
|
||||
let bit = if duration > B_NRZ_MIDPOINT { 1 } else { 0 };
|
||||
self.push_bit(bit);
|
||||
|
||||
if self.bit_count >= B_BITS {
|
||||
let result = self.decode_and_fire();
|
||||
self.reset_state();
|
||||
return result;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for ToyotaDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Toyota"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: A_TE_SHORT,
|
||||
te_long: A_TE_LONG,
|
||||
te_delta: A_TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
// Variant A 433.92 MHz, Variant B 315 MHz.
|
||||
&[433_920_000, 315_000_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.reset_state();
|
||||
// Full reset between segments: also clear the detected variant.
|
||||
self.variant = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
if self.step == DecoderStep::Reset {
|
||||
if !level {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Variant detection from the first SHORT HIGH pulse:
|
||||
// < 310µs -> Variant B (Tundra 315 MHz, te_short ~200µs)
|
||||
// >= 310µs -> Variant A (Corolla 433 MHz, te_short ~400µs)
|
||||
let fits_b = b_is_short(duration) && duration < VARIANT_THRESH;
|
||||
let fits_a = a_is_short(duration) && duration >= VARIANT_THRESH;
|
||||
|
||||
if fits_b {
|
||||
self.variant = 1;
|
||||
self.te_last = duration;
|
||||
self.have_high = true;
|
||||
self.preamble_count = 0;
|
||||
self.step = DecoderStep::PreambleB;
|
||||
} else if fits_a {
|
||||
self.variant = 0;
|
||||
self.te_last = duration;
|
||||
self.have_high = true;
|
||||
self.preamble_count = 0;
|
||||
self.step = DecoderStep::PreambleA;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.variant == 1 {
|
||||
self.feed_variant_b(level, duration)
|
||||
} else {
|
||||
self.feed_variant_a(level, duration)
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToyotaDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Build a Variant A (PWM) frame: preamble SS pairs, then `bits` MSB-first.
|
||||
/// LS (long HIGH + short LOW) = 0, SL (short HIGH + long LOW) = 1. Terminated by a long gap.
|
||||
fn build_variant_a(bits: &[u8]) -> Vec<LevelDuration> {
|
||||
let mut v = Vec::new();
|
||||
// 8 short-short preamble pairs (>= A_PREAMBLE_MIN = 6).
|
||||
for _ in 0..8 {
|
||||
v.push(LevelDuration::new(true, A_TE_SHORT));
|
||||
v.push(LevelDuration::new(false, A_TE_SHORT));
|
||||
}
|
||||
for &b in bits {
|
||||
if b == 0 {
|
||||
v.push(LevelDuration::new(true, A_TE_LONG));
|
||||
v.push(LevelDuration::new(false, A_TE_SHORT));
|
||||
} else {
|
||||
v.push(LevelDuration::new(true, A_TE_SHORT));
|
||||
v.push(LevelDuration::new(false, A_TE_LONG));
|
||||
}
|
||||
}
|
||||
// Trailing HIGH gap (out of TE range) flushes the frame.
|
||||
v.push(LevelDuration::new(true, 5000));
|
||||
v
|
||||
}
|
||||
|
||||
/// Build a Variant B (NRZ) frame: short-HIGH/long-LOW preamble pairs, sync gap, then each bit
|
||||
/// as one pulse (short = 0, long = 1), terminated by a sync-gap-length pulse.
|
||||
fn build_variant_b(bits: &[u8]) -> Vec<LevelDuration> {
|
||||
let mut v = Vec::new();
|
||||
// 8 preamble pairs: short HIGH (~200µs) + long LOW (~390µs).
|
||||
for _ in 0..8 {
|
||||
v.push(LevelDuration::new(true, B_TE_SHORT));
|
||||
v.push(LevelDuration::new(false, B_TE_LONG));
|
||||
}
|
||||
// Last preamble HIGH, then the sync-gap LOW.
|
||||
v.push(LevelDuration::new(true, B_TE_SHORT));
|
||||
v.push(LevelDuration::new(false, 1938));
|
||||
// Data: each bit one pulse, alternating level (polarity is ignored by the NRZ classifier).
|
||||
for (i, &b) in bits.iter().enumerate() {
|
||||
let level = i % 2 == 0;
|
||||
let dur = if b == 1 { 380 } else { 200 };
|
||||
v.push(LevelDuration::new(level, dur));
|
||||
}
|
||||
// End-of-frame gap.
|
||||
v.push(LevelDuration::new(false, 2000));
|
||||
v
|
||||
}
|
||||
|
||||
fn feed_all(dec: &mut ToyotaDecoder, pairs: &[LevelDuration]) -> Option<DecodedSignal> {
|
||||
for p in pairs {
|
||||
if let Some(sig) = dec.feed(p.level, p.duration_us) {
|
||||
return Some(sig);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variant_a_synthetic_decodes() {
|
||||
// 68-bit frame: a recognizable pattern with a non-zero serial and a known button nibble.
|
||||
// bits[0..32] = hop, bits[32..60] = serial, bits[60..64] = button, bits[64..68] = padding.
|
||||
let mut bits = vec![0u8; A_BITS];
|
||||
// hop = 0x9ABCDEF0 (MSB first in bits[0..32])
|
||||
let hop: u32 = 0x9ABC_DEF0;
|
||||
for i in 0..32 {
|
||||
bits[i] = ((hop >> (31 - i)) & 1) as u8;
|
||||
}
|
||||
// serial = 0x0123456 (28 bits) in bits[32..60]
|
||||
let serial: u32 = 0x012_3456;
|
||||
for i in 0..28 {
|
||||
bits[32 + i] = ((serial >> (27 - i)) & 1) as u8;
|
||||
}
|
||||
// button = 0x8 (Lock) in bits[60..64]
|
||||
let button: u8 = 0x8;
|
||||
for i in 0..4 {
|
||||
bits[60 + i] = (button >> (3 - i)) & 1;
|
||||
}
|
||||
// bits[64..68] padding = 0
|
||||
|
||||
let frame = build_variant_a(&bits);
|
||||
let mut dec = ToyotaDecoder::new();
|
||||
let sig = feed_all(&mut dec, &frame).expect("variant A frame should decode");
|
||||
|
||||
assert_eq!(sig.data_count_bit, A_BITS);
|
||||
assert_eq!(sig.serial, Some(serial));
|
||||
assert_eq!(sig.button, Some(button));
|
||||
// hop is the top 32 bits of data
|
||||
assert_eq!((sig.data >> 32) as u32, hop);
|
||||
assert!(sig.crc_valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variant_b_synthetic_decodes() {
|
||||
// 67-bit NRZ frame.
|
||||
let mut bits = vec![0u8; B_BITS];
|
||||
let hop: u32 = 0x1357_9BDF;
|
||||
for i in 0..32 {
|
||||
bits[i] = ((hop >> (31 - i)) & 1) as u8;
|
||||
}
|
||||
let serial: u32 = 0x0AB_CDEF;
|
||||
for i in 0..28 {
|
||||
bits[32 + i] = ((serial >> (27 - i)) & 1) as u8;
|
||||
}
|
||||
// button (bits[60..64]); only 3 bits of button fit before the 67-bit end (bits[60..63]),
|
||||
// bit 63..67 truncated — extract(60,4) reads bits[63..67] from the END. Just assert serial.
|
||||
for i in 0..7 {
|
||||
bits[60 + i] = if i % 2 == 0 { 1 } else { 0 };
|
||||
}
|
||||
|
||||
let frame = build_variant_b(&bits);
|
||||
let mut dec = ToyotaDecoder::new();
|
||||
let sig = feed_all(&mut dec, &frame).expect("variant B frame should decode");
|
||||
|
||||
assert_eq!(sig.data_count_bit, B_BITS);
|
||||
assert_eq!(sig.serial, Some(serial));
|
||||
assert!(sig.crc_valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_all_zero_serial() {
|
||||
// A structurally valid 68-bit frame with an all-zero serial must NOT emit (tight gate).
|
||||
let bits = vec![0u8; A_BITS];
|
||||
let frame = build_variant_a(&bits);
|
||||
let mut dec = ToyotaDecoder::new();
|
||||
assert!(feed_all(&mut dec, &frame).is_none());
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ impl Demodulator {
|
||||
pairs: Vec::with_capacity(2048),
|
||||
total_samples: 0,
|
||||
min_duration_us: 40, // 40µs debounce (was 50 — slightly more permissive)
|
||||
max_gap_us: 20_000, // 20ms gap = end of signal (was 10ms — wider to avoid splitting signals with internal gaps)
|
||||
max_gap_us: 80_000, // 80ms gap = end of signal; keeps multi-burst keyfob presses (e.g. 3–4 bursts with 25–50ms gaps) as one capture
|
||||
samples_since_edge: 0,
|
||||
}
|
||||
}
|
||||
@@ -253,11 +253,11 @@ impl Demodulator {
|
||||
.push(LevelDuration::new(self.current_level, duration_us));
|
||||
}
|
||||
|
||||
// Return the pairs and reset
|
||||
// Return the pairs and reset (min 5 pairs so short/unknown keyfob bursts still show)
|
||||
let result = std::mem::take(&mut self.pairs);
|
||||
self.reset_state();
|
||||
|
||||
if result.len() >= 10 {
|
||||
if result.len() >= 5 {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
@@ -405,7 +405,7 @@ impl FmDemodulator {
|
||||
pending_count: 0,
|
||||
pairs: Vec::with_capacity(2048),
|
||||
min_duration_us: 40,
|
||||
max_gap_us: 20_000,
|
||||
max_gap_us: 80_000, // match AM: 80ms so one button press (multi-burst) stays one capture
|
||||
samples_since_edge: 0,
|
||||
}
|
||||
}
|
||||
@@ -495,7 +495,7 @@ impl FmDemodulator {
|
||||
}
|
||||
let result = std::mem::take(&mut self.pairs);
|
||||
self.fm_reset_state();
|
||||
if result.len() >= 10 {
|
||||
if result.len() >= 5 {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
@@ -559,8 +559,8 @@ mod tests {
|
||||
// Process (won't return signal yet since no long gap)
|
||||
let _ = demod.process_samples(&buf);
|
||||
|
||||
// Add a long gap to flush
|
||||
let gap_buf: Vec<i8> = vec![1, 0].repeat(50_000); // 25ms LOW
|
||||
// Add a long gap to flush (>= max_gap_us: 80ms at 2MHz = 160k samples)
|
||||
let gap_buf: Vec<i8> = vec![1, 0].repeat(80_000); // 80ms LOW
|
||||
if let Some(pairs) = demod.process_samples(&gap_buf) {
|
||||
// Verify no consecutive same-level pairs
|
||||
for window in pairs.windows(2) {
|
||||
|
||||
+5
-3
@@ -1,6 +1,7 @@
|
||||
//! Storage management for configuration, exports, and keystores.
|
||||
//! Storage management for configuration and exports.
|
||||
//!
|
||||
//! All application data lives under `~/.config/KAT/`:
|
||||
//! All application data lives under `~/.config/KAT/`. **No keystore directory is created**
|
||||
//! — keys are embedded in the binary (see [crate::protocols::keys] and [crate::keystore]).
|
||||
//!
|
||||
//! ```text
|
||||
//! ~/.config/KAT/
|
||||
@@ -57,7 +58,7 @@ impl Config {
|
||||
export_directory: config_dir.join("exports"),
|
||||
import_directory: config_dir.join("import"),
|
||||
max_captures: 100,
|
||||
research_mode: false,
|
||||
research_mode: true, // show unknown signals by default (researchers need to see them)
|
||||
default_frequency: 433_920_000,
|
||||
default_lna_gain: 24,
|
||||
default_vga_gain: 20,
|
||||
@@ -160,6 +161,7 @@ impl Config {
|
||||
; Location: {path}
|
||||
;
|
||||
; Edit this file to change default settings.
|
||||
; Keys are embedded in the program — no keystore directory is used or created.
|
||||
; Lines starting with ; or # are comments.
|
||||
|
||||
[general]
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn render_captures_list(frame: &mut Frame, area: Rect, app: &App) {
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(6), // Table (flexible, takes remaining)
|
||||
Constraint::Length(12), // Detail panel (fixed height)
|
||||
Constraint::Length(18), // Detail panel (signal + vulnerability; taller for multiple CVEs)
|
||||
])
|
||||
.split(area)
|
||||
} else {
|
||||
|
||||
+3
-1
@@ -53,6 +53,7 @@ pub fn render_command_line(frame: &mut Frame, area: Rect, app: &App) {
|
||||
| InputMode::FobMetaMake
|
||||
| InputMode::FobMetaModel
|
||||
| InputMode::FobMetaRegion
|
||||
| InputMode::FobMetaCommand
|
||||
| InputMode::FobMetaNotes => (
|
||||
String::new(),
|
||||
"EXPORT",
|
||||
@@ -61,7 +62,8 @@ pub fn render_command_line(frame: &mut Frame, area: Rect, app: &App) {
|
||||
InputMode::CaptureMetaYear
|
||||
| InputMode::CaptureMetaMake
|
||||
| InputMode::CaptureMetaModel
|
||||
| InputMode::CaptureMetaRegion => (
|
||||
| InputMode::CaptureMetaRegion
|
||||
| InputMode::CaptureMetaCommand => (
|
||||
String::new(),
|
||||
"META",
|
||||
Style::default().fg(Color::Cyan),
|
||||
|
||||
+24
-6
@@ -33,7 +33,7 @@ pub fn draw_ui(frame: &mut Frame, app: &App) {
|
||||
let main_area = frame.area();
|
||||
let mut v_constraints = vec![
|
||||
Constraint::Length(3), // Header (full width)
|
||||
Constraint::Min(8), // Middle: captures + RX bar
|
||||
Constraint::Min(26), // Middle: captures table + detail panel (signal + vulnerability)
|
||||
Constraint::Length(3), // Status bar (full width)
|
||||
Constraint::Length(1), // Help bar (full width)
|
||||
];
|
||||
@@ -105,6 +105,7 @@ pub fn draw_ui(frame: &mut Frame, app: &App) {
|
||||
| InputMode::FobMetaMake
|
||||
| InputMode::FobMetaModel
|
||||
| InputMode::FobMetaRegion
|
||||
| InputMode::FobMetaCommand
|
||||
| InputMode::FobMetaNotes
|
||||
) {
|
||||
render_export_form(frame, app);
|
||||
@@ -116,6 +117,7 @@ pub fn draw_ui(frame: &mut Frame, app: &App) {
|
||||
| InputMode::CaptureMetaMake
|
||||
| InputMode::CaptureMetaModel
|
||||
| InputMode::CaptureMetaRegion
|
||||
| InputMode::CaptureMetaCommand
|
||||
) {
|
||||
render_capture_meta_form(frame, app);
|
||||
}
|
||||
@@ -250,12 +252,14 @@ fn render_help_bar(frame: &mut Frame, area: Rect, app: &App) {
|
||||
InputMode::FobMetaYear
|
||||
| InputMode::FobMetaMake
|
||||
| InputMode::FobMetaModel
|
||||
| InputMode::FobMetaRegion => "Enter: Next Field | Esc: Cancel Export",
|
||||
| InputMode::FobMetaRegion
|
||||
| InputMode::FobMetaCommand => "Enter: Next Field | Esc: Cancel Export",
|
||||
InputMode::FobMetaNotes => "Enter: Save & Export | Esc: Cancel Export",
|
||||
InputMode::CaptureMetaYear
|
||||
| InputMode::CaptureMetaMake
|
||||
| InputMode::CaptureMetaModel => "Enter: Next Field | Esc: Cancel",
|
||||
InputMode::CaptureMetaRegion => "Enter: Save | Esc: Cancel",
|
||||
| InputMode::CaptureMetaModel
|
||||
| InputMode::CaptureMetaRegion => "Enter: Next Field | Esc: Cancel",
|
||||
InputMode::CaptureMetaCommand => "Enter: Save | Esc: Cancel",
|
||||
InputMode::License | InputMode::Credits => "Esc/Enter: Close | Up/Down: Scroll",
|
||||
InputMode::LoadFileBrowser => "Up/Down: Navigate | Enter: Open/Import | Esc: Close",
|
||||
};
|
||||
@@ -464,6 +468,7 @@ fn render_export_form(frame: &mut Frame, app: &App) {
|
||||
InputMode::FobMetaMake,
|
||||
InputMode::FobMetaModel,
|
||||
InputMode::FobMetaRegion,
|
||||
InputMode::FobMetaCommand,
|
||||
InputMode::FobMetaNotes,
|
||||
]
|
||||
} else {
|
||||
@@ -566,11 +571,17 @@ fn render_export_form(frame: &mut Frame, app: &App) {
|
||||
placeholder: "(e.g. NA, EU, APAC, MEA)",
|
||||
idx: 4,
|
||||
},
|
||||
FormField {
|
||||
label: " Command: ",
|
||||
value: &app.fob_meta_command,
|
||||
placeholder: "(e.g. Unlock, Lock, Trunk, Panic)",
|
||||
idx: 5,
|
||||
},
|
||||
FormField {
|
||||
label: " Notes: ",
|
||||
value: &app.fob_meta_notes,
|
||||
placeholder: "(optional — color, trim, VIN, etc.)",
|
||||
idx: 5,
|
||||
idx: 6,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -670,6 +681,7 @@ fn render_capture_meta_form(frame: &mut Frame, app: &App) {
|
||||
InputMode::CaptureMetaMake,
|
||||
InputMode::CaptureMetaModel,
|
||||
InputMode::CaptureMetaRegion,
|
||||
InputMode::CaptureMetaCommand,
|
||||
];
|
||||
let current_idx = field_modes
|
||||
.iter()
|
||||
@@ -726,7 +738,7 @@ fn render_capture_meta_form(frame: &mut Frame, app: &App) {
|
||||
placeholder: &'a str,
|
||||
idx: usize,
|
||||
}
|
||||
let fields: [FormField; 4] = [
|
||||
let fields: [FormField; 5] = [
|
||||
FormField {
|
||||
label: " Year: ",
|
||||
value: &app.capture_meta_year,
|
||||
@@ -751,6 +763,12 @@ fn render_capture_meta_form(frame: &mut Frame, app: &App) {
|
||||
placeholder: "(e.g. NA, EU, or ALL)",
|
||||
idx: 3,
|
||||
},
|
||||
FormField {
|
||||
label: " Command: ",
|
||||
value: &app.capture_meta_command,
|
||||
placeholder: "(e.g. Unlock, Lock, Trunk, Panic)",
|
||||
idx: 4,
|
||||
},
|
||||
];
|
||||
|
||||
for field in &fields {
|
||||
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "libhackrf"
|
||||
version = "0.1.1"
|
||||
authors = ["Connor Slade <connor@connorcode.com>"]
|
||||
description = "A modern libhackrf wrapper that supports receiving and transmitting. (Patched for ARM cross-compilation)"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/connorslade/libhackrf-rs"
|
||||
|
||||
[lib]
|
||||
name = "libhackrf"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies.num-complex]
|
||||
version = "0.4.6"
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
pub enum DeviceType {
|
||||
Jellybean = 0,
|
||||
Jawbreaker = 1,
|
||||
/// HackRF One prior to r9
|
||||
Hackrf1Og = 2,
|
||||
Rad1O = 3,
|
||||
Hackrf1R9 = 4,
|
||||
/// Tried detection but did not recognize board
|
||||
Unrecognized = 0xFE,
|
||||
/// detection not yet attempted
|
||||
Undetected = 0xFF,
|
||||
}
|
||||
|
||||
impl DeviceType {
|
||||
pub fn from_id(id: u8) -> Self {
|
||||
match id {
|
||||
0 => DeviceType::Jellybean,
|
||||
1 => DeviceType::Jawbreaker,
|
||||
2 => DeviceType::Hackrf1Og,
|
||||
3 => DeviceType::Rad1O,
|
||||
4 => DeviceType::Hackrf1R9,
|
||||
0xFE => DeviceType::Unrecognized,
|
||||
0xFF => DeviceType::Undetected,
|
||||
_ => DeviceType::Undetected,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, HackrfError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HackrfError {
|
||||
InvalidParam = -2,
|
||||
NotFound = -5,
|
||||
Busy = -6,
|
||||
NoMem = -11,
|
||||
Libusb = -1000,
|
||||
Thread = -1001,
|
||||
StreamingThreadErr = -1002,
|
||||
StreamingStopped = -1003,
|
||||
StreamingExitCalled = -1004,
|
||||
Other = -9999,
|
||||
}
|
||||
|
||||
impl HackrfError {
|
||||
pub fn from_id(id: i32) -> Result<()> {
|
||||
Err(match id {
|
||||
0 | 1 => return Ok(()),
|
||||
-2 => HackrfError::InvalidParam,
|
||||
-5 => HackrfError::NotFound,
|
||||
-6 => HackrfError::Busy,
|
||||
-11 => HackrfError::NoMem,
|
||||
-1000 => HackrfError::Libusb,
|
||||
-1001 => HackrfError::Thread,
|
||||
-1002 => HackrfError::StreamingThreadErr,
|
||||
-1003 => HackrfError::StreamingStopped,
|
||||
-1004 => HackrfError::StreamingExitCalled,
|
||||
-9999 => HackrfError::Other,
|
||||
_ => HackrfError::Other,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HackrfError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_fmt(format_args!("{self:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for HackrfError {}
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
#![allow(improper_ctypes)]
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_uchar, c_uint, c_ulonglong, c_void};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct HackrfDevice;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct HackrfTransfer {
|
||||
pub device: *mut HackrfDevice,
|
||||
pub buffer: *mut c_uchar,
|
||||
pub buffer_length: c_int,
|
||||
pub valid_length: c_int,
|
||||
pub rx_ctx: *mut c_void,
|
||||
pub tx_ctx: *mut c_void,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[repr(C)]
|
||||
pub struct SerialNumber {
|
||||
pub part_id: [c_uint; 2],
|
||||
pub serial_no: [c_uint; 4],
|
||||
}
|
||||
|
||||
#[link(name = "hackrf")]
|
||||
extern "C" {
|
||||
pub fn hackrf_init() -> c_int;
|
||||
pub fn hackrf_exit() -> c_int;
|
||||
|
||||
pub fn hackrf_open(device: *mut *mut HackrfDevice) -> c_int;
|
||||
pub fn hackrf_close(device: *mut HackrfDevice) -> c_int;
|
||||
|
||||
pub fn hackrf_start_rx(
|
||||
device: *mut HackrfDevice,
|
||||
callback: extern "C" fn(*mut HackrfTransfer) -> c_int,
|
||||
rx_ctx: *mut c_void,
|
||||
) -> c_int;
|
||||
pub fn hackrf_stop_rx(device: *mut HackrfDevice) -> c_int;
|
||||
|
||||
pub fn hackrf_start_tx(
|
||||
device: *mut HackrfDevice,
|
||||
callback: extern "C" fn(*mut HackrfTransfer) -> c_int,
|
||||
tx_ctx: *mut c_void,
|
||||
) -> c_int;
|
||||
pub fn hackrf_stop_tx(device: *mut HackrfDevice) -> c_int;
|
||||
|
||||
pub fn hackrf_is_streaming(device: *mut HackrfDevice) -> c_int;
|
||||
|
||||
pub fn hackrf_set_baseband_filter_bandwidth(
|
||||
device: *mut HackrfDevice,
|
||||
bandwidth_hz: c_uint,
|
||||
) -> c_int;
|
||||
|
||||
pub fn hackrf_board_id_read(device: *mut HackrfDevice, value: *mut c_uchar) -> c_int;
|
||||
pub fn hackrf_version_string_read(
|
||||
device: *mut HackrfDevice,
|
||||
version: *mut c_char,
|
||||
length: c_uchar,
|
||||
) -> c_int;
|
||||
pub fn hackrf_board_partid_serialno_read(
|
||||
device: *mut HackrfDevice,
|
||||
read_partid_serialno: *mut SerialNumber,
|
||||
) -> c_int;
|
||||
|
||||
pub fn hackrf_set_freq(device: *mut HackrfDevice, freq_hz: c_ulonglong) -> c_int;
|
||||
pub fn hackrf_set_freq_explicit(
|
||||
device: *mut HackrfDevice,
|
||||
if_freq_hz: c_ulonglong,
|
||||
lo_freq_hz: c_ulonglong,
|
||||
path: c_uint,
|
||||
) -> c_int;
|
||||
|
||||
pub fn hackrf_set_sample_rate_manual(
|
||||
device: *mut HackrfDevice,
|
||||
freq_hz: c_uint,
|
||||
divider: c_uint,
|
||||
) -> c_int;
|
||||
pub fn hackrf_set_sample_rate(device: *mut HackrfDevice, freq_hz: c_double) -> c_int;
|
||||
|
||||
pub fn hackrf_set_amp_enable(device: *mut HackrfDevice, value: c_uchar) -> c_int;
|
||||
|
||||
pub fn hackrf_set_lna_gain(device: *mut HackrfDevice, value: c_uint) -> c_int;
|
||||
pub fn hackrf_set_vga_gain(device: *mut HackrfDevice, value: c_uint) -> c_int;
|
||||
pub fn hackrf_set_txvga_gain(device: *mut HackrfDevice, value: c_uint) -> c_int;
|
||||
|
||||
pub fn hackrf_set_antenna_enable(device: *mut HackrfDevice, value: c_uchar) -> c_int;
|
||||
|
||||
pub fn hackrf_error_name(errcode: c_int) -> *const c_char;
|
||||
pub fn hackrf_board_id_name(hackrf_board_id: c_uchar) -> *const c_char;
|
||||
pub fn hackrf_filter_path_name(path: c_uint) -> *const c_char;
|
||||
|
||||
pub fn hackrf_compute_baseband_filter_bw_round_down_lt(bandwidth_hz: c_uint) -> c_uint;
|
||||
pub fn hackrf_compute_baseband_filter_bw(bandwidth_hz: c_uint) -> c_uint;
|
||||
}
|
||||
Vendored
+210
@@ -0,0 +1,210 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::c_void,
|
||||
mem, ptr, slice,
|
||||
sync::{
|
||||
atomic::{AtomicPtr, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
mod enums;
|
||||
pub mod error;
|
||||
pub mod ffi;
|
||||
pub use enums::DeviceType;
|
||||
mod transfer;
|
||||
pub mod util;
|
||||
|
||||
use error::{HackrfError, Result};
|
||||
use ffi::SerialNumber;
|
||||
use transfer::{rx_callback, tx_callback, ReceiveCallback, TransferContext, TransmitCallback};
|
||||
|
||||
pub mod exports {
|
||||
pub use num_complex;
|
||||
}
|
||||
|
||||
static DEVICE_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// A HackRf device.
|
||||
#[derive(Clone)]
|
||||
pub struct HackRf {
|
||||
inner: Arc<HackRfInner>,
|
||||
}
|
||||
|
||||
struct HackRfInner {
|
||||
device: *mut ffi::HackrfDevice,
|
||||
user_data: AtomicPtr<c_void>,
|
||||
}
|
||||
|
||||
impl HackRf {
|
||||
/// Connects to a HackRF device.
|
||||
pub fn open() -> Result<HackRf> {
|
||||
if DEVICE_COUNT.fetch_add(1, Ordering::Relaxed) == 0 {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_init())? }
|
||||
}
|
||||
|
||||
let mut device = std::ptr::null_mut();
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_open(&mut device))? }
|
||||
|
||||
Ok(Self {
|
||||
inner: Arc::new(HackRfInner {
|
||||
device,
|
||||
user_data: AtomicPtr::new(ptr::null_mut()),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the internial representation of the HackRF device. This can be used
|
||||
/// with unsafe FFI functions if needed.
|
||||
#[inline(always)]
|
||||
pub fn device(&self) -> *mut ffi::HackrfDevice {
|
||||
self.inner.device
|
||||
}
|
||||
|
||||
/// Gets the device serial number.
|
||||
pub fn get_serial_number(&self) -> Result<SerialNumber> {
|
||||
let mut serial_number = SerialNumber::default();
|
||||
unsafe {
|
||||
HackrfError::from_id(ffi::hackrf_board_partid_serialno_read(
|
||||
self.device(),
|
||||
&mut serial_number,
|
||||
))?
|
||||
}
|
||||
Ok(serial_number)
|
||||
}
|
||||
|
||||
/// Read hackrf_board_id from a device and convert it to a DeviceType.
|
||||
pub fn get_device_type(&self) -> Result<DeviceType> {
|
||||
let mut value = 0;
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_board_id_read(self.device(), &mut value)) }?;
|
||||
Ok(DeviceType::from_id(value))
|
||||
}
|
||||
|
||||
/// Read HackRF firmware version as a string.
|
||||
pub fn version(&self) -> String {
|
||||
let mut version = vec![0; 32];
|
||||
|
||||
unsafe {
|
||||
ffi::hackrf_version_string_read(
|
||||
self.device(),
|
||||
version.as_mut_ptr(),
|
||||
version.len() as u8,
|
||||
);
|
||||
}
|
||||
|
||||
let end = version
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(version.len());
|
||||
|
||||
// Use from_raw_parts to handle both c_char=i8 (x86) and c_char=u8 (ARM)
|
||||
let version = unsafe { slice::from_raw_parts(version.as_ptr() as *const u8, end) };
|
||||
String::from_utf8_lossy(version).into_owned()
|
||||
}
|
||||
|
||||
/// Sets the center frequency in Hz.
|
||||
pub fn set_freq(&self, freq: u64) -> Result<()> {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_set_freq(self.device(), freq)) }
|
||||
}
|
||||
|
||||
/// Sets the sample rate in Hz.
|
||||
pub fn set_sample_rate(&self, sample_rate: u32) -> Result<()> {
|
||||
unsafe {
|
||||
HackrfError::from_id(ffi::hackrf_set_sample_rate_manual(
|
||||
self.device(),
|
||||
sample_rate,
|
||||
1,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the state of the externial amplifier.
|
||||
pub fn set_amp_enable(&self, enable: bool) -> Result<()> {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_set_amp_enable(self.device(), enable as u8)) }
|
||||
}
|
||||
|
||||
/// Low noise amplifier gain.
|
||||
/// Between 0d and 40d in steps of 8dB.
|
||||
pub fn set_lna_gain(&self, gain: u32) -> Result<()> {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_set_lna_gain(self.device(), gain)) }
|
||||
}
|
||||
|
||||
/// Variable gain amplifier. Range 0-62 (step 2dB).
|
||||
pub fn set_rxvga_gain(&self, gain: u32) -> Result<()> {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_set_vga_gain(self.device(), gain)) }
|
||||
}
|
||||
|
||||
/// Transmit variable gain amplifier. Range 0-47 (step 1dB).
|
||||
pub fn set_txvga_gain(&self, gain: u32) -> Result<()> {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_set_txvga_gain(self.device(), gain)) }
|
||||
}
|
||||
|
||||
pub fn set_baseband_filter_bandwidth(&self, bandwidth_hz: u32) -> Result<()> {
|
||||
unsafe {
|
||||
HackrfError::from_id(ffi::hackrf_set_baseband_filter_bandwidth(
|
||||
self.device(),
|
||||
ffi::hackrf_compute_baseband_filter_bw(bandwidth_hz),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts transmitting samples from the device.
|
||||
pub fn start_tx(&self, callback: TransmitCallback, user_data: impl Any) -> Result<()> {
|
||||
let context = TransferContext::new(callback, self.clone(), Box::new(user_data));
|
||||
let callback = Box::leak(Box::new(context)) as *mut _ as *mut _;
|
||||
self.inner.user_data.store(callback, Ordering::Relaxed);
|
||||
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_start_tx(self.device(), tx_callback, callback)) }
|
||||
}
|
||||
|
||||
/// Stops the current transmit operation.
|
||||
pub fn stop_tx(&self) -> Result<()> {
|
||||
let user_data = &self.inner.user_data;
|
||||
let callback = user_data.swap(ptr::null_mut(), Ordering::Relaxed);
|
||||
if !callback.is_null() {
|
||||
let callback = unsafe { Box::from_raw(callback as *mut fn(*mut ffi::HackrfTransfer)) };
|
||||
drop(callback);
|
||||
}
|
||||
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_stop_tx(self.device())) }
|
||||
}
|
||||
|
||||
/// Starts receiving samples from the device.
|
||||
pub fn start_rx(&self, callback: ReceiveCallback, user_data: impl Any + Sync) -> Result<()> {
|
||||
let context = TransferContext::new(callback, self.clone(), Box::new(user_data));
|
||||
let callback = Box::leak(Box::new(context)) as *mut _ as *mut _;
|
||||
self.inner.user_data.store(callback, Ordering::Relaxed);
|
||||
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_start_rx(self.device(), rx_callback, callback)) }
|
||||
}
|
||||
|
||||
/// Stops the current receive operation.
|
||||
pub fn stop_rx(&self) -> Result<()> {
|
||||
let user_data = &self.inner.user_data;
|
||||
let callback = user_data.swap(ptr::null_mut(), Ordering::Relaxed);
|
||||
if !callback.is_null() {
|
||||
let callback = unsafe { Box::from_raw(callback as *mut fn(*mut ffi::HackrfTransfer)) };
|
||||
drop(callback);
|
||||
}
|
||||
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_stop_rx(self.device())) }
|
||||
}
|
||||
|
||||
/// Returns true if the device is currently streaming samples (transmitting or receiving).
|
||||
pub fn is_streaming(&self) -> bool {
|
||||
unsafe { ffi::hackrf_is_streaming(self.device()) != 0 }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for HackRfInner {}
|
||||
unsafe impl Sync for HackRfInner {}
|
||||
|
||||
impl Drop for HackRf {
|
||||
fn drop(&mut self) {
|
||||
let _ = unsafe { HackrfError::from_id(ffi::hackrf_close(self.device())) };
|
||||
|
||||
if DEVICE_COUNT.fetch_sub(1, Ordering::Relaxed) == 1 {
|
||||
let _ = unsafe { HackrfError::from_id(ffi::hackrf_exit()) };
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+54
@@ -0,0 +1,54 @@
|
||||
use std::{any::Any, slice};
|
||||
|
||||
use num_complex::Complex;
|
||||
|
||||
use super::{ffi, HackRf};
|
||||
|
||||
pub type TransmitCallback = fn(hack_rf: &HackRf, samples: &mut [Complex<i8>], user: &dyn Any);
|
||||
pub type ReceiveCallback = fn(hack_rf: &HackRf, samples: &[Complex<i8>], user: &dyn Any);
|
||||
|
||||
pub struct TransferContext<Callback> {
|
||||
callback: Callback,
|
||||
hackrf: HackRf,
|
||||
user_data: Box<dyn Any>,
|
||||
}
|
||||
|
||||
impl<Callback> TransferContext<Callback> {
|
||||
pub(super) fn new(callback: Callback, hackrf: HackRf, user_data: Box<dyn Any>) -> Self {
|
||||
Self {
|
||||
callback,
|
||||
hackrf,
|
||||
user_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) extern "C" fn tx_callback(transfer: *mut ffi::HackrfTransfer) -> i32 {
|
||||
unsafe {
|
||||
let transfer = &mut *transfer;
|
||||
let context = &*(transfer.tx_ctx as *mut TransferContext<TransmitCallback>);
|
||||
|
||||
let buffer = slice::from_raw_parts_mut(
|
||||
transfer.buffer as *mut Complex<i8>,
|
||||
transfer.valid_length as usize / 2,
|
||||
);
|
||||
(context.callback)(&context.hackrf, buffer, &*context.user_data);
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) extern "C" fn rx_callback(transfer: *mut ffi::HackrfTransfer) -> i32 {
|
||||
unsafe {
|
||||
let transfer = &*transfer;
|
||||
let context = &*(transfer.rx_ctx as *mut TransferContext<ReceiveCallback>);
|
||||
|
||||
let buffer = slice::from_raw_parts(
|
||||
transfer.buffer as *const Complex<i8>,
|
||||
transfer.valid_length as usize / 2,
|
||||
);
|
||||
(context.callback)(&context.hackrf, buffer, &*context.user_data);
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
use num_complex::Complex;
|
||||
|
||||
pub trait ToComplexI8 {
|
||||
fn to_i8(self) -> Complex<i8>;
|
||||
}
|
||||
|
||||
pub trait ToComplexF32 {
|
||||
fn to_f32(self) -> Complex<f32>;
|
||||
}
|
||||
|
||||
impl ToComplexI8 for Complex<f32> {
|
||||
fn to_i8(self) -> Complex<i8> {
|
||||
Complex::new((self.re * 127.0) as i8, (self.im * 127.0) as i8)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToComplexF32 for Complex<i8> {
|
||||
fn to_f32(self) -> Complex<f32> {
|
||||
Complex::new(self.re as f32 / 127.0, self.im as f32 / 127.0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user