6 Commits

Author SHA1 Message Date
KaraZajac 48ee413c9b v1.2.0: Add 4 new protocols, fix 5 existing, port Kia V6 encoder
New protocols (from ProtoPirate):
- Mazda V0: 433MHz, pair-based decoding, XOR deobfuscation
- Mitsubishi V0: 868MHz, PWM, bit negation + XOR unscrambling
- Porsche Touareg: 433/868MHz, sync preamble, 24-bit rotate cipher
- Fiat V1 (Magneti Marelli BSI): 433MHz, auto-detected timing variants

Protocol fixes aligned with ProtoPirate reference:
- Kia V1: Fix Manchester level mapping (inverted convention), off-by-one
  bit count, CRC4 simplified to 7 bytes + offset 1
- Kia V2: Fix CRC byte layout (was double-counting nibbles), off-by-one
  bit count, preamble short-HIGH handling
- Fiat V0: Fix endbyte transform (remove (<<1)|1), standard Manchester
  encoder (was differential), 7 btn bits (was 6), gap path fallback
- PSA: Fix modified TEA (XTEA-like dynamic key selection), XOR decrypt
  byte mapping, critical key2_low construction bug, encoder polarity and
  preamble, add key1_high nibble validation, dual preamble patterns
- Ford V0: Add calculate_checksum (sums all serial+count bytes) for encoder

Kia V6 encoder ported:
- Forward AES-128 (SubBytes, ShiftRows, MixColumns, encrypt)
- encrypt_payload: build plaintext, AES encrypt, pack into 3 parts
- Two-pass Manchester upload (640 + 38 preamble pairs)
- fx_field extraction stored in DecodedSignal.extra for encode roundtrip

Updated README, protocol docs, and version bump to 1.2.0.
2026-03-22 12:05:01 -04:00
KaraZajac 84b75ee6b5 Fix ARM compilation: vendor libhackrf with c_char portability fix
The upstream libhackrf v0.1.1 crate fails to compile on ARM because
c_char is u8 on ARM (not i8 like on x86). The transmute at lib.rs:102
explicitly casts &[i8] -> &[u8], but on ARM the source is already &[u8],
causing a type mismatch.

Vendored the crate at vendor/libhackrf/ with the transmute replaced by
slice::from_raw_parts with a pointer cast, which works on both
architectures. Also cleaned up transmute calls in transfer.rs.

Fixes #1
2026-03-22 11:14:41 -04:00
KaraZajac 295e4c4d4a set up GitHub Pages with docs/ source, custom landing page, and Jekyll-rendered protocol docs 2026-03-22 11:10:09 -04:00
Kara Zajac a58d13362a Add GitHub Actions workflow for GitHub Pages deployment
This workflow automates the deployment of static content to GitHub Pages on pushes to the main branch.
2026-03-22 11:01:25 -04:00
KaraZajac 23431c4a7a updated screenshot 2026-02-23 20:50:41 -05:00
KaraZajac c0de9effc8 updated readme 2026-02-22 21:40:21 -05:00
46 changed files with 3422 additions and 308 deletions
+43
View File
@@ -0,0 +1,43 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: '.'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
Generated
+1 -3
View File
@@ -444,7 +444,7 @@ dependencies = [
[[package]] [[package]]
name = "kat" name = "kat"
version = "1.1.3" version = "1.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atty", "atty",
@@ -482,8 +482,6 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]] [[package]]
name = "libhackrf" name = "libhackrf"
version = "0.1.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e640122f67f490129cd3ab1517149b4091eeca0a14de3df7510626df258196b9"
dependencies = [ dependencies = [
"num-complex", "num-complex",
] ]
+5 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "kat" name = "kat"
version = "1.1.3" version = "1.2.0"
edition = "2021" edition = "2021"
description = "Keyfob Analysis Toolkit - HackRF/RTL-SDR signal capture, decode, and transmit (HackRF only)" description = "Keyfob Analysis Toolkit - HackRF/RTL-SDR signal capture, decode, and transmit (HackRF only)"
authors = ["KAT Team"] authors = ["KAT Team"]
@@ -54,6 +54,10 @@ atty = "0.2"
[build-dependencies] [build-dependencies]
pkg-config = "0.3" 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] [profile.release]
opt-level = 3 opt-level = 3
lto = true lto = true
+14 -7
View File
@@ -17,7 +17,7 @@ A terminal-based RF signal analysis tool for capturing, decoding, and retransmit
## Features ## Features
- **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies (HackRF uses AM envelope detection; FM protocols are tagged for display and may decode when signal is strong) - **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies (HackRF uses AM envelope detection; FM protocols are tagged for display and may decode when signal is strong)
- **Multi-protocol decoding** — 14 protocol decoders: Kia V0V6, Ford V0, Fiat V0, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions - **Multi-protocol decoding** — 18 protocol decoders: Kia V0V6, Ford V0, Fiat V0/V1, Mazda V0, Mitsubishi V0, Porsche Touareg, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions
- **KeeLoq generic fallback** — when a signal doesnt match any known protocol, KAT tries decoding it as KeeLoq using every manufacturer key in the embedded keystore (Kia V3/V4 and Star Line air formats); successful decodes appear as **Keeloq (keystore name)** in the capture list - **KeeLoq generic fallback** — when a signal doesnt match any known protocol, KAT tries decoding it as KeeLoq using every manufacturer key in the embedded keystore (Kia V3/V4 and Star Line air formats); successful decodes appear as **Keeloq (keystore name)** in the capture list
- **RF modulation metadata** — each protocol tagged as AM, FM, or both (from ProtoPirate); shown in signal detail and exported in .fob - **RF modulation metadata** — each protocol tagged as AM, FM, or both (from ProtoPirate); shown in signal detail and exported in .fob
- **Rich signal detail** — encoding (PWM/Manchester), RF (AM/FM), encryption, serial, counter, key data, CRC, frequency, and raw level/duration pairs - **Rich signal detail** — encoding (PWM/Manchester), RF (AM/FM), encryption, serial, counter, key data, CRC, frequency, and raw level/duration pairs
@@ -233,15 +233,19 @@ Protocol behavior and RF modulation (AM/FM) follow the ProtoPirate reference. KA
| Kia V2 | Manchester | FM | Fixed Code | 315 / 433.92 MHz | | Kia V2 | Manchester | FM | Fixed Code | 315 / 433.92 MHz |
| Kia V3/V4 | PWM | AM/FM | KeeLoq | 315 / 433.92 MHz | | Kia V3/V4 | PWM | AM/FM | KeeLoq | 315 / 433.92 MHz |
| Kia V5 | Manchester | FM | Fixed Code | 433.92 MHz | | Kia V5 | Manchester | FM | Fixed Code | 433.92 MHz |
| Kia V6 | Manchester | FM | Fixed Code | 433.92 MHz | | Kia V6 | Manchester | FM | AES-128 | 433.92 MHz |
| Ford V0 | Manchester | FM | Fixed Code | 433.92 MHz | | Ford V0 | Manchester | FM | Rolling Code | 315 / 433.92 MHz |
| Fiat V0 | Manchester | FM | Fixed Code | 433.92 MHz | | Fiat V0 | Manchester | FM | Fixed Code | 433.92 MHz |
| Fiat V1 (Magneti Marelli) | Manchester | FM | Rolling Code | 433.92 MHz |
| Mazda V0 | Pair-based | FM | XOR Deobfuscation | 433.92 MHz |
| Mitsubishi V0 | PWM | FM | Bit Negation + XOR | 868.35 MHz |
| Porsche Touareg | PWM | AM | Rotation Cipher | 433.92 / 868.35 MHz |
| Subaru | PWM | AM | Rolling Code | 433.92 MHz | | Subaru | PWM | AM | Rolling Code | 433.92 MHz |
| Suzuki | PWM | AM | Rolling Code | 433.92 MHz | | Suzuki | PWM | AM | Rolling Code | 433.92 MHz |
| VAG (VW/Audi/Seat/Skoda) | Manchester | AM | AUT64/XTEA | 433.92 / 434.42 MHz (decode + encode) | | VAG (VW/Audi/Seat/Skoda) | Manchester | AM | AUT64/XTEA | 433.92 / 434.42 MHz |
| Scher-Khan | PWM | FM | Magic Code | 433.92 MHz | | Scher-Khan | PWM | FM | Magic Code | 433.92 MHz |
| Star Line | PWM | AM | KeeLoq | 433.92 MHz | | Star Line | PWM | AM | KeeLoq | 433.92 MHz |
| PSA (Peugeot/Citroën) | Manchester | FM | XTEA/XOR | 433.92 MHz | | PSA (Peugeot/Citroën) | Manchester | FM | Modified TEA/XOR | 433.92 MHz |
**KeeLoq generic fallback:** If no protocol decodes a capture, KAT tries KeeLoq with every keystore manufacturer key (Kia V3/V4 and Star Line bit layouts). On success the protocol is shown as **Keeloq (*keystore name*)** (e.g. Keeloq (Alligator), Keeloq (Pandora_PRO)). See [docs/keeloq_generic.md](docs/keeloq_generic.md). **KeeLoq generic fallback:** If no protocol decodes a capture, KAT tries KeeLoq with every keystore manufacturer key (Kia V3/V4 and Star Line bit layouts). On success the protocol is shown as **Keeloq (*keystore name*)** (e.g. Keeloq (Alligator), Keeloq (Pandora_PRO)). See [docs/keeloq_generic.md](docs/keeloq_generic.md).
@@ -262,6 +266,8 @@ Details (exact model/year scope) are in `src/vuln_db.rs`. The app shows the NVD
### Cryptographic modules ### Cryptographic modules
- **KeeLoq** — encrypt/decrypt with normal, secure, FAAC, and magic serial/XOR learning key derivation (keeloq_common, keys). Unknown signals are tried as KeeLoq with every keystore key via **keeloq_generic** (uses keeloq_common only). - **KeeLoq** — encrypt/decrypt with normal, secure, FAAC, and magic serial/XOR learning key derivation (keeloq_common, keys). Unknown signals are tried as KeeLoq with every keystore key via **keeloq_generic** (uses keeloq_common only).
- **AES-128** — full encrypt/decrypt for Kia V6 (key derived from keystore A/B with XOR masks)
- **Modified TEA** — XTEA-like cipher with dynamic key selection for PSA (Peugeot/Citroën)
- **AUT64** — 12-round block cipher for VAG type 1/3/4 (aut64) - **AUT64** — 12-round block cipher for VAG type 1/3/4 (aut64)
- **Keystore** — manufacturer keys (Kia, VAG, etc.) built in; see `src/keystore/` - **Keystore** — manufacturer keys (Kia, VAG, etc.) built in; see `src/keystore/`
@@ -302,7 +308,8 @@ src/
│ ├── aut64.rs # AUT64 block cipher (VAG) │ ├── aut64.rs # AUT64 block cipher (VAG)
│ ├── keys.rs # Key loading (embedded + optional file), KIA/VAG │ ├── keys.rs # Key loading (embedded + optional file), KIA/VAG
│ ├── kia_v0..kia_v6.rs │ ├── kia_v0..kia_v6.rs
│ ├── ford_v0.rs, fiat_v0.rs, subaru.rs, suzuki.rs │ ├── ford_v0.rs, fiat_v0.rs, fiat_v1.rs, subaru.rs, suzuki.rs
│ ├── mazda_v0.rs, mitsubishi_v0.rs, porsche_touareg.rs
│ ├── vag.rs # VAG decoder/encoder (4 sub-types; encode from capture via extra) │ ├── vag.rs # VAG decoder/encoder (4 sub-types; encode from capture via extra)
│ ├── scher_khan.rs, star_line.rs, psa.rs │ ├── scher_khan.rs, star_line.rs, psa.rs
│ └── ... │ └── ...
@@ -333,7 +340,7 @@ The [FOB Research Library](https://github.com/KaraZajac/FOB_Research_Library) is
## Credits ## 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. 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.
--- ---
+4
View File
@@ -16,6 +16,10 @@ This folder describes how each keyfob protocol supported by KAT works. Each docu
| Subaru | `subaru.rs` | [subaru.md](subaru.md) | | Subaru | `subaru.rs` | [subaru.md](subaru.md) |
| VAG | `vag.rs` | [vag.md](vag.md) | | VAG | `vag.rs` | [vag.md](vag.md) |
| Fiat V0 | `fiat_v0.rs` | [fiat_v0.md](fiat_v0.md) | | Fiat V0 | `fiat_v0.rs` | [fiat_v0.md](fiat_v0.md) |
| Fiat V1 | `fiat_v1.rs` | [fiat_v1.md](fiat_v1.md) |
| Mazda V0 | `mazda_v0.rs` | [mazda_v0.md](mazda_v0.md) |
| Mitsubishi V0 | `mitsubishi_v0.rs` | [mitsubishi_v0.md](mitsubishi_v0.md) |
| Porsche Touareg | `porsche_touareg.rs` | [porsche_touareg.md](porsche_touareg.md) |
| Suzuki | `suzuki.rs` | [suzuki.md](suzuki.md) | | Suzuki | `suzuki.rs` | [suzuki.md](suzuki.md) |
| Scher-Khan | `scher_khan.rs` | [scher_khan.md](scher_khan.md) | | Scher-Khan | `scher_khan.rs` | [scher_khan.md](scher_khan.md) |
| Star Line | `star_line.rs` | [star_line.md](star_line.md) | | Star Line | `star_line.rs` | [star_line.md](star_line.md) |
+6
View File
@@ -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
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Fiat V0 Protocol # Fiat V0 Protocol
**Rust module:** `src/protocols/fiat_v0.rs` **Rust module:** `src/protocols/fiat_v0.rs`
+60
View File
@@ -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.
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Ford V0 Protocol # Ford V0 Protocol
**Rust module:** `src/protocols/ford_v0.rs` **Rust module:** `src/protocols/ford_v0.rs`
+170
View File
@@ -0,0 +1,170 @@
<!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>14 protocol decoders including Kia V0V6, Ford, Fiat, 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</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="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>&copy; 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>
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# KeeLoq Generic Fallback # KeeLoq Generic Fallback
**Rust module:** `src/protocols/keeloq_generic.rs` **Rust module:** `src/protocols/keeloq_generic.rs`
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Kia V0 Protocol # Kia V0 Protocol
**Rust module:** `src/protocols/kia_v0.rs` **Rust module:** `src/protocols/kia_v0.rs`
+6 -2
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Kia V1 Protocol # Kia V1 Protocol
**Rust module:** `src/protocols/kia_v1.rs` **Rust module:** `src/protocols/kia_v1.rs`
@@ -5,7 +9,7 @@
## Overview ## Overview
Kia V1 uses Manchester encoding at 800/1600 µs. 57 bits total: 32 serial + 8 button + 12 counter + 4 CRC. Long preamble (~90 long pairs). CRC4 with offset rules (cnt_high 0 vs ≥ 6). Kia V1 uses Manchester encoding at 800/1600 us. 57 bits total: 32 serial + 8 button + 12 counter + 4 CRC. Long preamble (~90 long pairs). CRC4 uses XOR of nibbles over 7 bytes (serial + button + cnt_low + cnt_high) with offset 1.
## Timing ## Timing
@@ -24,7 +28,7 @@ Manchester: symbol duration short or long; bit value from transition direction.
- 32 bits: serial - 32 bits: serial
- 8 bits: button - 8 bits: button
- 12 bits: counter - 12 bits: counter
- 4 bits: CRC4 (checksum with offset rules) - 4 bits: CRC4 (XOR of nibbles, 7 bytes, offset 1)
## Decoder Steps ## Decoder Steps
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Kia V2 Protocol # Kia V2 Protocol
**Rust module:** `src/protocols/kia_v2.rs` **Rust module:** `src/protocols/kia_v2.rs`
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Kia V3/V4 Protocol # Kia V3/V4 Protocol
**Rust module:** `src/protocols/kia_v3_v4.rs` **Rust module:** `src/protocols/kia_v3_v4.rs`
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Kia V5 Protocol # Kia V5 Protocol
**Rust module:** `src/protocols/kia_v5.rs` **Rust module:** `src/protocols/kia_v5.rs`
+6 -2
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Kia V6 Protocol # Kia V6 Protocol
**Rust module:** `src/protocols/kia_v6.rs` **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) - Part2: 64 bits (inverted)
- Part3: 16 bits (inverted) - Part3: 16 bits (inverted)
AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH). Serial/button/counter and CRC extracted after decryption. AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH). Serial/button/counter, CRC, and fx_field extracted after decryption. The fx_field is derived from the top 2 bytes of stored_part1_high and stored in `DecodedSignal.extra`.
## Decoder Steps ## Decoder Steps
@@ -36,7 +40,7 @@ AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH
## Encoder ## Encoder
Decode-only in KAT (no encoder in reference). Supported. Ported from ProtoPirate (`ENABLE_EMULATE_FEATURE`). Builds plaintext (fx_field, serial, button, counter, S-box CRC, CRC8), AES-128 encrypts, packs into 3 parts, then Manchester encodes with two-pass preamble (640 pairs + data, gap, 38 pairs + data). Requires fx_field from decoded signal's `extra` field.
## Frequencies ## Frequencies
+73
View File
@@ -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.
+59
View File
@@ -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.
+72
View File
@@ -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.
+47 -14
View File
@@ -1,24 +1,33 @@
# PSA (Peugeot/Citroën) Protocol ---
layout: default
---
# PSA (Peugeot/Citroen) Protocol
**Rust module:** `src/protocols/psa.rs` **Rust module:** `src/protocols/psa.rs`
**Reference:** `REFERENCES/ProtoPirate/protocols/psa.c` **Reference:** `REFERENCES/ProtoPirate/protocols/psa.c`
## Overview ## Overview
PSA uses Manchester at 250/500 µs symbol (125/250 µs sub-symbol for preamble). 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation. TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds an XOR layer. Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule). PSA uses Manchester encoding with dual preamble pattern support. 128 bits total: key1 (64) + validation (16) + key2/rest (48). Modified TEA (XTEA-like) with dynamic key selection (`key[sum & 3]` and `key[(sum >> 11) & 3]`). Two decode modes: mode 0x23 (direct XOR decrypt with checksum validation) and mode 0x36 (TEA with BF1/BF2 key schedules). Brute-force decryption for mode 0x36 is deferred/partial.
## Timing ## Timing
| Parameter | Value | Notes | | Parameter | Value | Notes |
|------------|--------|---------| |-------------|--------|---------|
| Symbol short | 250 µs | ±100 µs | | Symbol short | 250 us | +/-100 us |
| Symbol long | 500 µs | ±100 µs | | Symbol long | 500 us | +/-100 us |
| Preamble | 125/250 µs sub-symbols | | | Preamble P1 | 250 us sub-symbols | Pattern 1 |
| Preamble P2 | 125 us sub-symbols | Pattern 2 |
| End marker | 1000 us (P1), 500 us (P2) | |
| Min bits | 128 | | | Min bits | 128 | |
## Encoding ## Dual Preamble Patterns
Manchester; preamble uses 125/250 µs; then 250/500 µs symbols. TEA encrypt; mode 0x23 adds XOR. - **Pattern 1 (250 us):** Preamble uses 250 us sub-symbols; Manchester decode at 250/500 us. Threshold: 70+ transitions.
- **Pattern 2 (125 us):** Preamble uses 125 us sub-symbols; Manchester decode at 125/250 us. Threshold: 69+ transitions.
Pattern type is auto-detected from the first preamble pulse duration.
## Frame Layout (128 bits) ## Frame Layout (128 bits)
@@ -26,18 +35,42 @@ Manchester; preamble uses 125/250 µs; then 250/500 µs symbols. TEA encrypt; mo
- validation: 16 bits - validation: 16 bits
- key2/rest: 48 bits - key2/rest: 48 bits
TEA decrypt key1 (and validation); mode 0x23: XOR with BF1 key schedule; mode 0x36: TEA with BF2 key schedule. Serial/button/counter extracted from decrypted key1. ## Encryption
### Modified TEA (XTEA-like)
Uses dynamic key word selection per round:
- Encrypt: `k_idx1 = sum & 3`, then `sum += DELTA`, then `k_idx2 = (sum >> 11) & 3`
- Decrypt: `k_idx2 = (sum >> 11) & 3`, then `sum -= DELTA`, then `k_idx1 = sum & 3`
- Round function: `(key[k_idx] + sum) ^ (((v >> 5) ^ (v << 4)) + v)`
### Mode 0x23 (Direct XOR)
1. Setup byte buffer from key1/key2 (little-endian unpack).
2. Calculate checksum over buffer[2..8] (nibble sum * 16).
3. Validate: `(checksum ^ key2_high_byte) & 0xF0 == 0`.
4. XOR decrypt using `psa_copy_reverse` byte reordering.
5. Extract: serial (24-bit), counter (16-bit), button (4-bit), CRC.
### Mode 0x36 (TEA Brute-Force)
Direct TEA decrypt attempted with BF1 and BF2 key schedules. Full brute-force (16M iterations per schedule) is available in ProtoPirate but not fully ported to KAT.
Key schedules:
- BF1: `[0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A64]`
- BF2: `[0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04]`
## Decoder Steps ## Decoder Steps
1. **WaitEdge** — Wait for edge/preamble. 1. **WaitEdge (State0)** -- Detect preamble pattern type from first HIGH pulse (250 us -> Pattern 1, 125 us -> Pattern 2).
2. **CountPattern** Count preamble pattern (125/250 µs). 2. **CountPattern250 (State1)** -- Count 250 us preamble pairs; on long pulse with count > 70, transition to Manchester decode.
3. **DecodeManchester** Manchester decode 128 bits; TEA decrypt; apply mode (0x23 XOR or 0x36); extract fields. 3. **DecodeManchester250 (State2)** -- Manchester decode at 250/500 us. End on 1000 us marker or 121+ bits.
4. **End** — End marker (e.g. 1000 µs); return decode. 4. **CountPattern125 (State3)** -- Count 125 us preamble pairs; on 250 us pulse with count >= 69, transition to Manchester decode.
5. **DecodeManchester125 (State4)** -- Manchester decode at 125/250 us. End on 500 us marker or 121+ bits.
## Encoder ## Encoder
Supported; preamble, Manchester 128 bits, TEA (+ XOR for 0x23). Supported (mode 0x23 only). XOR encrypt, then modified TEA encrypt with BF1 key schedule. Preamble: 70 cycles of 125/125 us + 250/250 us sync. Manchester encoded key1 (64 bits) + validation (16 bits). End marker: 1000 us LOW.
## Frequencies ## Frequencies
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Scher-Khan Protocol # Scher-Khan Protocol
**Rust module:** `src/protocols/scher_khan.rs` **Rust module:** `src/protocols/scher_khan.rs`
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Star Line Protocol # Star Line Protocol
**Rust module:** `src/protocols/star_line.rs` **Rust module:** `src/protocols/star_line.rs`
+351
View File
@@ -0,0 +1,351 @@
* {
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);
line-height: 1.6;
}
.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;
}
.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;
}
.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;
}
}
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Subaru Protocol # Subaru Protocol
**Rust module:** `src/protocols/subaru.rs` **Rust module:** `src/protocols/subaru.rs`
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# Suzuki Protocol # Suzuki Protocol
**Rust module:** `src/protocols/suzuki.rs` **Rust module:** `src/protocols/suzuki.rs`
+4
View File
@@ -1,3 +1,7 @@
---
layout: default
---
# VAG (VW/Audi/Seat/Skoda) Protocol # VAG (VW/Audi/Seat/Skoda) Protocol
**Rust module:** `src/protocols/vag.rs` **Rust module:** `src/protocols/vag.rs`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 813 KiB

+39 -49
View File
@@ -4,9 +4,9 @@
//! `fiat_v0.h`. Preamble: count short pulses (HIGH or LOW, 200±100µs); when preamble_count >= 150 //! `fiat_v0.h`. Preamble: count short pulses (HIGH or LOW, 200±100µs); when preamble_count >= 150
//! (0x96), accept 800µs LOW gap (gap_threshold 800, te_delta 100) and enter Data. Data: 64 bits //! (0x96), accept 800µs LOW gap (gap_threshold 800, te_delta 100) and enter Data. Data: 64 bits
//! (serial=data_low, cnt=data_high) then 7 more bits; complete when bit_count > 0x46 with //! (serial=data_low, cnt=data_high) then 7 more bits; complete when bit_count > 0x46 with
//! btn = (data_low << 1) | 1, 71 bits total. Encoder: differential Manchester, 150 preamble pairs, //! btn = (uint8_t)data_low, 71 bits total. Encoder: standard Manchester, 150 preamble pairs,
//! last LOW replaced by 800µs gap; 64 data bits then 6 btn bits (btn_to_send = btn >> 1); end //! last LOW replaced by 800µs gap; 64 data bits then 7 endbyte bits (endbyte & 0x7F); end
//! marker te_short*8 LOW. //! marker te_short*4 LOW.
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff; use crate::duration_diff;
@@ -294,8 +294,9 @@ impl ProtocolDecoder for FiatV0Decoder {
self.data_high = 0; self.data_high = 0;
} }
if self.bit_count > 0x46 { if self.bit_count == 0x47 {
self.btn = ((self.data_low << 1) | 1) as u8; // C: endbyte = (uint8_t)data_low (no transform)
self.btn = self.data_low as u8;
let result = self.parse_data(); let result = self.parse_data();
self.data_low = 0; self.data_low = 0;
self.data_high = 0; self.data_high = 0;
@@ -304,6 +305,19 @@ impl ProtocolDecoder for FiatV0Decoder {
return Some(result); return Some(result);
} }
} }
} else {
// Manchester reset event (gap path) — C extracts at exactly 71 bits
if self.bit_count == 0x47 {
self.btn = self.data_low as u8;
let result = self.parse_data();
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.step = DecoderStep::Reset;
return Some(result);
} else if self.bit_count < 0x40 {
self.step = DecoderStep::Reset;
}
} }
self.te_last = duration; self.te_last = duration;
} }
@@ -319,80 +333,56 @@ impl ProtocolDecoder for FiatV0Decoder {
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> { fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?; let serial = decoded.serial?;
let cnt = decoded.counter.unwrap_or(0) as u32; let cnt = decoded.counter.unwrap_or(0) as u32;
let btn = decoded.button.unwrap_or(0).max(button); let endbyte = if button != 0 { button } else { decoded.button.unwrap_or(0) };
// Ref: data = (cnt<<32)|serial; btn_to_send = btn >> 1 (reverse decoder's (x<<1)|1) // C: data = (hop << 32) | fix; endbyte sent as-is (7 bits, & 0x7F)
let data = ((cnt as u64) << 32) | (serial as u64); let data = ((cnt as u64) << 32) | (serial as u64);
let btn_to_send = btn >> 1; let te_short = TE_SHORT;
let mut signal = Vec::with_capacity(1024); let mut signal = Vec::with_capacity(1024);
let te_short = TE_SHORT;
let te_long = TE_LONG;
for burst in 0..TOTAL_BURSTS { for burst in 0..TOTAL_BURSTS {
if burst > 0 { if burst > 0 {
signal.push(LevelDuration::new(false, INTER_BURST_GAP)); signal.push(LevelDuration::new(false, INTER_BURST_GAP));
} }
// Preamble: HIGH-LOW pairs; last LOW replaced by gap (ref) // Preamble: alternating short pulses; last LOW extended to gap
for i in 0..PREAMBLE_PAIRS { for _ in 0..PREAMBLE_PAIRS {
signal.push(LevelDuration::new(true, te_short)); signal.push(LevelDuration::new(true, te_short));
signal.push(LevelDuration::new( signal.push(LevelDuration::new(false, te_short));
false, }
if i == PREAMBLE_PAIRS - 1 { GAP_US } else { te_short }, // Extend last LOW to create gap (matches C: upload[index-1] = gap)
)); if let Some(last) = signal.last_mut() {
*last = LevelDuration::new(false, GAP_US);
} }
// First bit (bit 63) - differential Manchester // Standard Manchester encode 64 bits of data (matches C)
let first_bit = (data >> 63) & 1 == 1; for bit in (0..64).rev() {
if first_bit {
signal.push(LevelDuration::new(true, te_long));
} else {
signal.push(LevelDuration::new(true, te_short));
signal.push(LevelDuration::new(false, te_long));
}
let mut prev_bit = first_bit;
// Remaining 63 data bits
for bit in (0..63).rev() {
let curr_bit = (data >> bit) & 1 == 1; let curr_bit = (data >> bit) & 1 == 1;
if !prev_bit && !curr_bit { if curr_bit {
signal.push(LevelDuration::new(true, te_short)); signal.push(LevelDuration::new(true, te_short));
signal.push(LevelDuration::new(false, te_short)); signal.push(LevelDuration::new(false, te_short));
} else if !prev_bit && curr_bit {
signal.push(LevelDuration::new(true, te_long));
} else if prev_bit && !curr_bit {
signal.push(LevelDuration::new(false, te_long));
} else { } else {
signal.push(LevelDuration::new(false, te_short)); signal.push(LevelDuration::new(false, te_short));
signal.push(LevelDuration::new(true, te_short)); signal.push(LevelDuration::new(true, te_short));
} }
prev_bit = curr_bit;
} }
// 6 btn bits (ref: for bit 5 down to 0) // Standard Manchester encode 7 bits of endbyte (matches C: endbyte & 0x7F, bits 6..0)
for bit in (0..6).rev() { let endbyte_masked = endbyte & 0x7F;
let curr_bit = (btn_to_send >> bit) & 1 == 1; for bit in (0..7).rev() {
if !prev_bit && !curr_bit { let curr_bit = (endbyte_masked >> bit) & 1 == 1;
if curr_bit {
signal.push(LevelDuration::new(true, te_short)); signal.push(LevelDuration::new(true, te_short));
signal.push(LevelDuration::new(false, te_short)); signal.push(LevelDuration::new(false, te_short));
} else if !prev_bit && curr_bit {
signal.push(LevelDuration::new(true, te_long));
} else if prev_bit && !curr_bit {
signal.push(LevelDuration::new(false, te_long));
} else { } else {
signal.push(LevelDuration::new(false, te_short)); signal.push(LevelDuration::new(false, te_short));
signal.push(LevelDuration::new(true, te_short)); signal.push(LevelDuration::new(true, te_short));
} }
prev_bit = curr_bit;
} }
// End marker (ref) // End marker: te_short * 4 LOW (matches C)
if prev_bit { signal.push(LevelDuration::new(false, te_short * 4));
signal.push(LevelDuration::new(false, te_short));
}
signal.push(LevelDuration::new(false, te_short * 8));
} }
Some(signal) Some(signal)
+435
View File
@@ -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
View File
@@ -225,10 +225,26 @@ impl FordV0Decoder {
} }
// ========================================================================= // =========================================================================
// BS calculation // Checksum / BS calculation
// ========================================================================= // =========================================================================
/// Calculate BS = (count_low_byte + bs_magic + (button << 4)) with overflow handling /// Calculate checksum from serial, count, and button (matches psa.c ford_v0_calculate_checksum).
/// Sums all bytes of serial + all bytes of count + (button << 4), truncated to u8.
fn calculate_checksum(serial: u32, count: u32, button: u8) -> u8 {
let sum: u32 = ((count >> 24) & 0xFF)
.wrapping_add((count >> 16) & 0xFF)
.wrapping_add((count >> 8) & 0xFF)
.wrapping_add(count & 0xFF)
.wrapping_add((serial >> 24) & 0xFF)
.wrapping_add((serial >> 16) & 0xFF)
.wrapping_add((serial >> 8) & 0xFF)
.wrapping_add(serial & 0xFF)
.wrapping_add((button as u32) << 4);
(sum & 0xFF) as u8
}
/// Legacy BS calculation (kept for reference; encoder now uses calculate_checksum)
#[allow(dead_code)]
fn calculate_bs(count: u32, button: u8, bs_magic: u8) -> u8 { fn calculate_bs(count: u32, button: u8, bs_magic: u8) -> u8 {
let result: u16 = (count as u16 & 0xFF) let result: u16 = (count as u16 & 0xFF)
.wrapping_add(bs_magic as u16) .wrapping_add(bs_magic as u16)
@@ -651,21 +667,18 @@ impl ProtocolDecoder for FordV0Decoder {
decoded.counter.unwrap_or(0) as u32 decoded.counter.unwrap_or(0) as u32
}) & 0xFFFFF; // 20-bit }) & 0xFFFFF; // 20-bit
// Use stored bs_magic (or default to 0x6F for backward compatibility) // Calculate checksum from serial + count + button (matches ford_v0_calculate_checksum in C)
let bs_magic = if self.bs_magic != 0 { self.bs_magic } else { 0x6F }; let checksum = Self::calculate_checksum(serial, count, button);
// Calculate BS from count + button + bs_magic (matches C ford_v0_calculate_bs)
let bs = Self::calculate_bs(count, button, bs_magic);
// Extract header byte from the original key1 (first byte) // Extract header byte from the original key1 (first byte)
let header_byte = (decoded.data >> 56) as u8; let header_byte = (decoded.data >> 56) as u8;
// Encode key1 from fields (same count, new button) // Encode key1 from fields (same count, new button)
let new_key1 = Self::encode_ford_v0(header_byte, serial, button, count, bs); let new_key1 = Self::encode_ford_v0(header_byte, serial, button, count, checksum);
// Calculate CRC for key2 // Calculate CRC for key2
let crc = Self::calculate_crc_for_tx(new_key1, bs); let crc = Self::calculate_crc_for_tx(new_key1, checksum);
let new_key2 = ((bs as u16) << 8) | (crc as u16); let new_key2 = ((checksum as u16) << 8) | (crc as u16);
// Build one 6-burst block and repeat TX_REPEAT times (matches reference encoder.repeat = 10) // Build one 6-burst block and repeat TX_REPEAT times (matches reference encoder.repeat = 10)
let single = Self::build_upload(new_key1, new_key2); let single = Self::build_upload(new_key1, new_key2);
+17 -27
View File
@@ -7,7 +7,7 @@
//! - Manchester encoding: 800/1600µs timing //! - Manchester encoding: 800/1600µs timing
//! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC) //! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC)
//! - Long preamble of ~90 long pairs //! - Long preamble of ~90 long pairs
//! - CRC4 checksum with offset rules (cnt_high 0 / >= 6) //! - CRC4 checksum (XOR of nibbles + offset 1, 7 bytes including cnt_high)
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration; use crate::radio::demodulator::LevelDuration;
@@ -66,13 +66,14 @@ impl KiaV1Decoder {
(crc.wrapping_add(offset)) & 0x0F (crc.wrapping_add(offset)) & 0x0F
} }
/// Manchester state machine /// Manchester state machine (Flipper convention: level ? ShortLow : ShortHigh)
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> { fn manchester_advance(&mut self, is_short: bool, level: bool) -> Option<bool> {
let event = match (is_short, is_high) { // C: event = level ? ManchesterEventShortLow : ManchesterEventShortHigh (inverted)
(true, false) => 0, // Short Low let event = match (is_short, level) {
(true, true) => 1, // Short High (true, true) => 0, // level=true → ShortLow
(false, false) => 2, // Long Low (true, false) => 1, // level=false → ShortHigh
(false, true) => 3, // Long High (false, true) => 2, // level=true → LongLow
(false, false) => 3, // level=false → LongHigh
}; };
let (new_state, output) = match (self.manchester_state, event) { let (new_state, output) = match (self.manchester_state, event) {
@@ -115,15 +116,9 @@ impl KiaV1Decoder {
char_data[4] = button; char_data[4] = button;
char_data[5] = (counter & 0xFF) as u8; char_data[5] = (counter & 0xFF) as u8;
let crc = if cnt_high == 0 { // CRC4: always 7 bytes with offset 1 (matches updated ProtoPirate kia_v1.c)
let offset = if counter >= 0x098 { button } else { 1 };
Self::crc4(&char_data[..6], offset)
} else if cnt_high >= 0x6 {
char_data[6] = cnt_high as u8; char_data[6] = cnt_high as u8;
Self::crc4(&char_data, 1) let crc = Self::crc4(&char_data, 1);
} else {
Self::crc4(&char_data[..6], 1)
};
DecodedSignal { DecodedSignal {
serial: Some(serial), serial: Some(serial),
@@ -194,8 +189,9 @@ impl ProtocolDecoder for KiaV1Decoder {
if self.header_count > 70 { if self.header_count > 70 {
if !level && is_short && duration_diff!(self.te_last, TE_LONG) < TE_DELTA { if !level && is_short && duration_diff!(self.te_last, TE_LONG) < TE_DELTA {
self.decode_count_bit = 1; // C: decode_count_bit=1, then add_bit(1) increments to 2
self.decode_data = 1; // Add first bit self.decode_count_bit = 2;
self.decode_data = 1;
self.header_count = 0; self.header_count = 0;
self.step = DecoderStep::DecodeData; self.step = DecoderStep::DecodeData;
} }
@@ -218,7 +214,7 @@ impl ProtocolDecoder for KiaV1Decoder {
return None; return None;
} }
if self.decode_count_bit >= MIN_COUNT_BIT { if self.decode_count_bit == MIN_COUNT_BIT {
let result = self.parse_data(); let result = self.parse_data();
self.step = DecoderStep::Reset; self.step = DecoderStep::Reset;
return Some(result); return Some(result);
@@ -247,15 +243,9 @@ impl ProtocolDecoder for KiaV1Decoder {
char_data[4] = button; char_data[4] = button;
char_data[5] = (counter & 0xFF) as u8; char_data[5] = (counter & 0xFF) as u8;
let crc = if cnt_high == 0 { // CRC4: always 7 bytes with offset 1 (matches updated ProtoPirate kia_v1.c)
let offset = if counter >= 0x098 { button } else { 1 };
Self::crc4(&char_data[..6], offset)
} else if cnt_high >= 0x6 {
char_data[6] = cnt_high; char_data[6] = cnt_high;
Self::crc4(&char_data, 1) let crc = Self::crc4(&char_data, 1);
} else {
Self::crc4(&char_data[..6], 1)
};
// Build data // Build data
let data: u64 = ((serial as u64) << 24) | let data: u64 = ((serial as u64) << 24) |
+16 -15
View File
@@ -57,18 +57,17 @@ impl KiaV2Decoder {
} }
} }
/// CRC4 for Kia V2 (matches kia_v2.c: 6-byte permuted input, XOR nibbles, offset 1) /// CRC4 for Kia V2 (matches kia_v2.c: strip CRC nibble, XOR all remaining nibbles, + offset 1)
fn calculate_crc(data: u64) -> u8 { fn calculate_crc(data: u64) -> u8 {
let serial = ((data >> 20) & 0xFFFFFFFF) as u32; // C code: data_without_crc = data >> 4; then read 6 sequential bytes (bits 4..51)
let u_var4 = (data & 0xFFFFFFFF) as u32; let data_without_crc = data >> 4;
let mut bytes = [0u8; 6]; let mut bytes = [0u8; 6];
bytes[0] = (u_var4 >> 20) as u8; bytes[0] = (data_without_crc & 0xFF) as u8;
bytes[1] = ((u_var4 >> 28) | ((serial & 0x0F) << 4)) as u8; bytes[1] = ((data_without_crc >> 8) & 0xFF) as u8;
bytes[2] = (serial >> 4) as u8; bytes[2] = ((data_without_crc >> 16) & 0xFF) as u8;
bytes[3] = (serial >> 12) as u8; bytes[3] = ((data_without_crc >> 24) & 0xFF) as u8;
bytes[4] = (u_var4 >> 4) as u8; bytes[4] = ((data_without_crc >> 32) & 0xFF) as u8;
bytes[5] = (u_var4 >> 12) as u8; bytes[5] = ((data_without_crc >> 40) & 0xFF) as u8;
let mut crc: u8 = 0; let mut crc: u8 = 0;
for &byte in &bytes { for &byte in &bytes {
@@ -181,19 +180,21 @@ impl ProtocolDecoder for KiaV2Decoder {
self.te_last = duration; self.te_last = duration;
self.header_count += 1; self.header_count += 1;
} else if is_short && self.header_count >= 100 { } else if is_short && self.header_count >= 100 {
// C code: decode_count_bit=1, then add_bit(1) which shifts and increments to 2
self.header_count = 0; self.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 1;
self.step = DecoderStep::CollectRawBits;
self.decode_data = 1; // First bit self.decode_data = 1; // First bit
self.decode_count_bit = 2;
self.step = DecoderStep::CollectRawBits;
} else { } else {
self.step = DecoderStep::Reset; self.te_last = duration; // C stays in CheckPreamble, updates te_last
} }
} else { } else {
if is_long { if is_long {
self.header_count += 1; self.header_count += 1;
self.te_last = duration; self.te_last = duration;
} else if !is_short { } else if is_short {
self.te_last = duration; // C stays in CheckPreamble for short LOW
} else {
self.step = DecoderStep::Reset; self.step = DecoderStep::Reset;
} }
} }
+197 -7
View File
@@ -1,8 +1,8 @@
//! Kia V6 protocol decoder (decode-only) //! Kia V6 protocol decoder/encoder
//! //!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v6.c`. //! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v6.c`.
//! Decode logic (Manchester level mapping, 3-part 144-bit frame, AES-128, CRC8, keystore XOR) matches reference. //! Decode logic (Manchester level mapping, 3-part 144-bit frame, AES-128, CRC8, keystore XOR) matches reference.
//! No encoder in protopirate. //! Encoder ported from protopirate (ENABLE_EMULATE_FEATURE): AES-128 encrypt, two-pass Manchester.
//! //!
//! Protocol characteristics: //! Protocol characteristics:
//! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance) //! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance)
@@ -275,6 +275,181 @@ impl KiaV6Decoder {
*data = state; *data = state;
} }
// =========================================================================
// Forward AES functions for encoder (matches kia_v6.c ENABLE_EMULATE_FEATURE)
// =========================================================================
/// AES forward SubBytes
fn aes_subbytes(state: &mut [u8; 16]) {
for i in 0..16 {
state[i] = AES_SBOX[state[i] as usize];
}
}
/// AES forward ShiftRows
fn aes_shiftrows(state: &mut [u8; 16]) {
let temp = state[1];
state[1] = state[5];
state[5] = state[9];
state[9] = state[13];
state[13] = temp;
let temp = state[2];
state[2] = state[10];
state[10] = temp;
let temp = state[6];
state[6] = state[14];
state[14] = temp;
let temp = state[3];
state[3] = state[15];
state[15] = state[11];
state[11] = state[7];
state[7] = temp;
}
/// AES forward MixColumns
fn aes_mixcolumns(state: &mut [u8; 16]) {
for i in 0..4 {
let a = state[i * 4];
let b = state[i * 4 + 1];
let c = state[i * 4 + 2];
let d = state[i * 4 + 3];
state[i * 4] = Self::gf_mul2(a) ^ Self::gf_mul2(b) ^ b ^ c ^ d;
state[i * 4 + 1] = a ^ Self::gf_mul2(b) ^ Self::gf_mul2(c) ^ c ^ d;
state[i * 4 + 2] = a ^ b ^ Self::gf_mul2(c) ^ Self::gf_mul2(d) ^ d;
state[i * 4 + 3] = Self::gf_mul2(a) ^ a ^ b ^ c ^ Self::gf_mul2(d);
}
}
/// AES-128 encrypt
fn aes128_encrypt(expanded_key: &[u8; 176], data: &mut [u8; 16]) {
let mut state = *data;
Self::aes_addroundkey(&mut state, &expanded_key[0..16]);
for round in 1..10 {
Self::aes_subbytes(&mut state);
Self::aes_shiftrows(&mut state);
Self::aes_mixcolumns(&mut state);
Self::aes_addroundkey(&mut state, &expanded_key[round * 16..(round + 1) * 16]);
}
Self::aes_subbytes(&mut state);
Self::aes_shiftrows(&mut state);
Self::aes_addroundkey(&mut state, &expanded_key[160..176]);
*data = state;
}
/// Encrypt payload for transmission (matches kia_v6.c kia_v6_encrypt_payload)
fn encrypt_payload(
fx_field: u8,
serial: u32,
button: u8,
counter: u32,
) -> (u32, u32, u32, u32, u16) {
let mut plain = [0u8; 16];
plain[0] = fx_field;
plain[4] = ((serial >> 16) & 0xFF) as u8;
plain[5] = ((serial >> 8) & 0xFF) as u8;
plain[6] = (serial & 0xFF) as u8;
plain[7] = button & 0x0F;
plain[8] = ((counter >> 24) & 0xFF) as u8;
plain[9] = ((counter >> 16) & 0xFF) as u8;
plain[10] = ((counter >> 8) & 0xFF) as u8;
plain[11] = (counter & 0xFF) as u8;
plain[12] = AES_SBOX[(counter & 0xFF) as usize];
plain[15] = Self::crc8(&plain[..15], 0xFF, 0x07);
let aes_key = Self::get_aes_key();
let expanded_key = Self::aes_key_expansion(&aes_key);
Self::aes128_encrypt(&expanded_key, &mut plain);
let fx_hi = 0x20 | (fx_field >> 4);
let fx_lo = fx_field & 0x0F;
let part1_high = ((fx_hi as u32) << 24)
| ((fx_lo as u32) << 16)
| ((plain[0] as u32) << 8)
| (plain[1] as u32);
let part1_low = ((plain[2] as u32) << 24)
| ((plain[3] as u32) << 16)
| ((plain[4] as u32) << 8)
| (plain[5] as u32);
let part2_high = ((plain[6] as u32) << 24)
| ((plain[7] as u32) << 16)
| ((plain[8] as u32) << 8)
| (plain[9] as u32);
let part2_low = ((plain[10] as u32) << 24)
| ((plain[11] as u32) << 16)
| ((plain[12] as u32) << 8)
| (plain[13] as u32);
let part3 = ((plain[14] as u16) << 8) | (plain[15] as u16);
(part1_low, part1_high, part2_low, part2_high, part3)
}
/// Build encoder signal: two-pass Manchester with preambles (matches kia_v6.c)
fn build_upload(
p1_lo: u32, p1_hi: u32,
p2_lo: u32, p2_hi: u32,
p3: u16,
) -> Vec<LevelDuration> {
let mut signal = Vec::with_capacity(2000);
// Two passes: 640 preamble pairs, then 38 preamble pairs
for &preamble_pairs in &[640u32, 38u32] {
// Preamble: short/short pairs
for _ in 0..preamble_pairs {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
// Sync: short LOW, long HIGH, short LOW
signal.push(LevelDuration::new(false, TE_SHORT));
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_SHORT));
// Part1: bits 60 down to 0 (61 bits), inverted
for b in (0..=60).rev() {
let word = if b >= 32 { p1_hi } else { p1_lo };
let shift = if b >= 32 { b - 32 } else { b };
let bit = ((!word) >> shift) & 1 == 1;
Self::encode_manchester_bit(&mut signal, bit);
}
// Part2: bits 63 down to 0 (64 bits), inverted
for b in (0..=63).rev() {
let word = if b >= 32 { p2_hi } else { p2_lo };
let shift = if b >= 32 { b - 32 } else { b };
let bit = ((!word) >> shift) & 1 == 1;
Self::encode_manchester_bit(&mut signal, bit);
}
// Part3: bits 15 down to 0 (16 bits), inverted
for b in (0..=15).rev() {
let bit = ((!p3) >> b) & 1 == 1;
Self::encode_manchester_bit(&mut signal, bit);
}
// Gap between passes
signal.push(LevelDuration::new(false, TE_LONG));
}
signal
}
/// Encode one Manchester bit (matches kia_v6.c kia_v6_encode_manchester_bit)
fn encode_manchester_bit(signal: &mut Vec<LevelDuration>, bit: bool) {
if bit {
signal.push(LevelDuration::new(false, TE_SHORT));
signal.push(LevelDuration::new(true, TE_SHORT));
} else {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
}
/// AES-128 key from V6 keystores A+B with XOR_MASK_LOW/HIGH (matches kia_v6.c) /// AES-128 key from V6 keystores A+B with XOR_MASK_LOW/HIGH (matches kia_v6.c)
fn get_aes_key() -> [u8; 16] { fn get_aes_key() -> [u8; 16] {
let keystore_a = Self::get_keystore_a(); let keystore_a = Self::get_keystore_a();
@@ -306,6 +481,13 @@ impl KiaV6Decoder {
aes_key aes_key
} }
/// Extract fx_field from stored_part1_high (matches kia_v6.c fx_field extraction)
fn extract_fx_field(&self) -> u8 {
let fx_byte0 = ((self.stored_part1_high >> 24) & 0xFF) as u8;
let fx_byte1 = ((self.stored_part1_high >> 16) & 0xFF) as u8;
((fx_byte0 & 0xF) << 4) | (fx_byte1 & 0xF)
}
/// Decrypt 16-byte block: byte layout matches kia_v6.c; AES-128 then CRC8 check /// Decrypt 16-byte block: byte layout matches kia_v6.c; AES-128 then CRC8 check
fn decrypt(&self) -> Option<(u32, u8, u32, bool)> { fn decrypt(&self) -> Option<(u32, u8, u32, bool)> {
let mut encrypted_data = [0u8; 16]; let mut encrypted_data = [0u8; 16];
@@ -528,6 +710,7 @@ impl ProtocolDecoder for KiaV6Decoder {
if let Some((serial, button, counter, crc_valid)) = self.decrypt() { if let Some((serial, button, counter, crc_valid)) = self.decrypt() {
let key_data = ((self.stored_part1_high as u64) << 32) | let key_data = ((self.stored_part1_high as u64) << 32) |
(self.stored_part1_low as u64); (self.stored_part1_low as u64);
let fx_field = self.extract_fx_field();
self.step = DecoderStep::Reset; self.step = DecoderStep::Reset;
return Some(DecodedSignal { return Some(DecodedSignal {
@@ -537,8 +720,8 @@ impl ProtocolDecoder for KiaV6Decoder {
crc_valid, crc_valid,
data: key_data, data: key_data,
data_count_bit: MIN_COUNT_BIT, data_count_bit: MIN_COUNT_BIT,
encoder_capable: false, encoder_capable: true,
extra: None, extra: Some(fx_field as u64),
protocol_display_name: None, protocol_display_name: None,
}); });
} }
@@ -552,11 +735,18 @@ impl ProtocolDecoder for KiaV6Decoder {
} }
fn supports_encoding(&self) -> bool { fn supports_encoding(&self) -> bool {
false // V6 is decode-only true
} }
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> { fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
None // V6 decode-only in protopirate let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0) as u32;
let fx_field = decoded.extra.unwrap_or(0) as u8;
let (p1_lo, p1_hi, p2_lo, p2_hi, p3) =
Self::encrypt_payload(fx_field, serial, button, counter);
Some(Self::build_upload(p1_lo, p1_hi, p2_lo, p2_hi, p3))
} }
} }
+312
View File
@@ -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()
}
}
+244
View File
@@ -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()
}
}
+8
View File
@@ -36,10 +36,14 @@ mod subaru;
mod ford_v0; mod ford_v0;
mod vag; mod vag;
mod fiat_v0; mod fiat_v0;
mod fiat_v1;
mod suzuki; mod suzuki;
mod scher_khan; mod scher_khan;
mod star_line; mod star_line;
mod psa; mod psa;
mod mazda_v0;
mod mitsubishi_v0;
mod porsche_touareg;
pub use common::DecodedSignal; pub use common::DecodedSignal;
@@ -109,11 +113,15 @@ impl ProtocolRegistry {
Box::new(ford_v0::FordV0Decoder::new()), Box::new(ford_v0::FordV0Decoder::new()),
Box::new(subaru::SubaruDecoder::new()), Box::new(subaru::SubaruDecoder::new()),
Box::new(fiat_v0::FiatV0Decoder::new()), Box::new(fiat_v0::FiatV0Decoder::new()),
Box::new(fiat_v1::FiatV1Decoder::new()),
Box::new(suzuki::SuzukiDecoder::new()), Box::new(suzuki::SuzukiDecoder::new()),
Box::new(scher_khan::ScherKhanDecoder::new()), Box::new(scher_khan::ScherKhanDecoder::new()),
Box::new(star_line::StarLineDecoder::new()), Box::new(star_line::StarLineDecoder::new()),
Box::new(keeloq::KeeloqDecoder::new()), Box::new(keeloq::KeeloqDecoder::new()),
Box::new(psa::PsaDecoder::new()), Box::new(psa::PsaDecoder::new()),
Box::new(mazda_v0::MazdaV0Decoder::new()),
Box::new(mitsubishi_v0::MitsubishiV0Decoder::new()),
Box::new(porsche_touareg::PorscheTouaregDecoder::new()),
]; ];
Self { decoders } Self { decoders }
+321
View File
@@ -0,0 +1,321 @@
//! Porsche Touareg protocol decoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/porsche_touareg.c`.
//! Original implementation by @lupettohf
//!
//! Protocol characteristics:
//! - PWM encoding with Manchester-like bit pairs: SHORT LOW + LONG HIGH = 0, LONG LOW + SHORT HIGH = 1
//! - 64 bits total; sync preamble of 15+ LOW/HIGH pairs at 3370µs, then 5930µs gap pair, then data
//! - Field layout: pkt[0]=(btn<<4)|(frame_type&0x07), pkt[1..3]=serial 24-bit, pkt[4..7]=encrypted
//! - Counter recovery via brute-force matching of computed encrypted bytes against received bytes
//! - Frame types: 0x02="First", 0x01="Cont", 0x04="Final"
//! - Frequencies: 433.92 MHz and 868.35 MHz (AM demodulation)
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
use crate::radio::demodulator::LevelDuration;
const TE_SHORT: u32 = 1680;
const TE_LONG: u32 = 3370;
const TE_DELTA: u32 = 500;
const MIN_COUNT_BIT: usize = 64;
const PC_TE_SYNC: u32 = 3370;
const PC_TE_GAP: u32 = 5930;
const PC_SYNC_MIN: u16 = 15;
/// Decoder states (matches PorscheCayenneDecoderStep in porsche_touareg.c)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Sync,
GapHigh,
GapLow,
Data,
}
/// Porsche Touareg protocol decoder
pub struct PorscheTouaregDecoder {
step: DecoderStep,
sync_count: u16,
raw_data: u64,
bit_count: usize,
te_last: u32,
}
/// Circular left-shift of a 24-bit register stored in three bytes (h, m, l).
///
/// Each byte shifts left by 1, receiving the MSB of the next byte in the chain:
/// h gets MSB of m, m gets MSB of l, l gets MSB of h (wrap-around).
///
/// Matches the ROTATE24 macro in porsche_touareg.c exactly.
#[inline]
fn rotate24(r_h: &mut u8, r_m: &mut u8, r_l: &mut u8) {
let ch = (*r_h >> 7) & 1;
let cm = (*r_m >> 7) & 1;
let cl = (*r_l >> 7) & 1;
*r_h = (*r_h << 1) | cm;
*r_m = (*r_m << 1) | cl;
*r_l = (*r_l << 1) | ch;
}
/// Compute an 8-byte frame from serial, button, counter, and frame_type.
///
/// This is a direct port of `porsche_cayenne_compute_frame` from the C reference.
/// pkt[0..3] = plaintext header, pkt[4..7] = encrypted payload derived from
/// a 24-bit rotate register seeded from serial bytes and rotated (4 + counter_low) times.
fn compute_frame(serial24: u32, btn: u8, counter: u16, frame_type: u8) -> [u8; 8] {
let b0 = (btn << 4) | (frame_type & 0x07);
let b1 = ((serial24 >> 16) & 0xFF) as u8;
let b2 = ((serial24 >> 8) & 0xFF) as u8;
let b3 = (serial24 & 0xFF) as u8;
let cnt = counter.wrapping_add(1);
let cnt_lo = (cnt & 0xFF) as u8;
let cnt_hi = ((cnt >> 8) & 0xFF) as u8;
let mut r_h = b3;
let mut r_m = b1;
let mut r_l = b2;
// Rotate 4 times initially
for _ in 0..4 {
rotate24(&mut r_h, &mut r_m, &mut r_l);
}
// Then rotate cnt_lo more times
for _ in 0..cnt_lo as u16 {
rotate24(&mut r_h, &mut r_m, &mut r_l);
}
let a9a = r_h ^ b0;
let nb9b_p1 = ((!cnt_lo).wrapping_shl(2) & 0xFC) ^ r_m;
let nb9b_p2 = ((!cnt_hi).wrapping_shl(2) & 0xFC) ^ r_m;
let nb9b_p3 = ((!cnt_hi).wrapping_shr(6) & 0x03) ^ r_m;
let a9b = (nb9b_p1 & 0xCC) | (nb9b_p2 & 0x30) | (nb9b_p3 & 0x03);
let nb9c_p1 = ((!cnt_lo).wrapping_shr(2) & 0x3F) ^ r_l;
let nb9c_p2 = ((!cnt_hi & 0x03).wrapping_shl(6)) ^ r_l;
let nb9c_p3 = ((!cnt_hi).wrapping_shr(2) & 0x3F) ^ r_l;
let a9c = (nb9c_p1 & 0x33) | (nb9c_p2 & 0xC0) | (nb9c_p3 & 0x0C);
let mut pkt = [0u8; 8];
pkt[0] = b0;
pkt[1] = b1;
pkt[2] = b2;
pkt[3] = b3;
pkt[4] = ((a9a >> 2) & 0x3F) | ((!cnt_lo & 0x03) << 6);
pkt[5] = (!cnt_lo & 0xC0) | ((a9a & 0x03) << 4) | (a9b & 0x0C) | ((!cnt_lo).wrapping_shr(2) & 0x03);
pkt[6] = ((a9b & 0x03) << 6) | ((a9c >> 2) & 0x3C) | ((!cnt_lo).wrapping_shr(4) & 0x03);
pkt[7] = ((a9b >> 4) & 0x0F) | ((a9c & 0x0F) << 4);
pkt
}
/// Parse raw 64-bit data into a DecodedSignal.
///
/// Extracts serial (24-bit), button (4-bit), frame_type (3-bit), then brute-forces
/// the counter (1..=256) by calling compute_frame and comparing encrypted bytes.
fn parse_data(data: u64) -> DecodedSignal {
// Unpack 64-bit data into 8 bytes (big-endian)
let mut pkt = [0u8; 8];
let mut raw = data;
for i in (0..8).rev() {
pkt[i] = (raw & 0xFF) as u8;
raw >>= 8;
}
let serial = ((pkt[1] as u32) << 16) | ((pkt[2] as u32) << 8) | (pkt[3] as u32);
let btn = pkt[0] >> 4;
let frame_type = pkt[0] & 0x07;
// Brute-force counter recovery: try counter values 1..=256
let mut counter: u16 = 0;
for try_cnt in 1u16..=256 {
let try_pkt = compute_frame(serial, btn, try_cnt - 1, frame_type);
if try_pkt[4] == pkt[4]
&& try_pkt[5] == pkt[5]
&& try_pkt[6] == pkt[6]
&& try_pkt[7] == pkt[7]
{
counter = try_cnt;
break;
}
}
// Determine frame type name for extra info
let frame_type_name = match frame_type {
0x02 => "First",
0x01 => "Cont",
0x04 => "Final",
_ => "??",
};
DecodedSignal {
serial: Some(serial),
button: Some(btn),
counter: Some(counter),
crc_valid: counter != 0,
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: false,
extra: Some(frame_type as u64),
protocol_display_name: Some(format!(
"Porsche Touareg [{}]",
frame_type_name
)),
}
}
impl PorscheTouaregDecoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
sync_count: 0,
raw_data: 0,
bit_count: 0,
te_last: 0,
}
}
}
impl ProtocolDecoder for PorscheTouaregDecoder {
fn name(&self) -> &'static str {
"Porsche Touareg"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: MIN_COUNT_BIT,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[433_920_000, 868_350_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.sync_count = 0;
self.raw_data = 0;
self.bit_count = 0;
self.te_last = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
// Reset: wait for a LOW pulse matching sync timing (3370µs)
DecoderStep::Reset => {
if !level && duration_diff!(duration, PC_TE_SYNC) < TE_DELTA {
self.sync_count = 1;
self.step = DecoderStep::Sync;
}
}
// Sync: count sync pulses (HIGH and LOW at 3370µs).
// On a gap pulse (5930µs) with enough sync pulses, transition to GapHigh/GapLow.
DecoderStep::Sync => {
if level {
if duration_diff!(duration, PC_TE_SYNC) < TE_DELTA {
// Keep collecting sync pairs -- HIGH sync pulse, stay in Sync
} else if self.sync_count >= PC_SYNC_MIN
&& duration_diff!(duration, PC_TE_GAP) < TE_DELTA
{
// HIGH gap after sufficient sync pulses
self.step = DecoderStep::GapLow;
} else {
self.step = DecoderStep::Reset;
}
} else {
// LOW pulse
if duration_diff!(duration, PC_TE_SYNC) < TE_DELTA {
self.sync_count += 1;
} else if self.sync_count >= PC_SYNC_MIN
&& duration_diff!(duration, PC_TE_GAP) < TE_DELTA
{
// LOW gap after sufficient sync pulses
self.step = DecoderStep::GapHigh;
} else {
self.step = DecoderStep::Reset;
}
}
}
// GapHigh: expect the complementary HIGH gap pulse
DecoderStep::GapHigh => {
if level && duration_diff!(duration, PC_TE_GAP) < TE_DELTA {
self.raw_data = 0;
self.bit_count = 0;
self.step = DecoderStep::Data;
} else {
self.step = DecoderStep::Reset;
}
}
// GapLow: expect the complementary LOW gap pulse
DecoderStep::GapLow => {
if !level && duration_diff!(duration, PC_TE_GAP) < TE_DELTA {
self.raw_data = 0;
self.bit_count = 0;
self.step = DecoderStep::Data;
} else {
self.step = DecoderStep::Reset;
}
}
// Data: decode bit pairs.
// LOW pulses are saved in te_last; HIGH pulses complete the bit:
// SHORT LOW + LONG HIGH = bit 0
// LONG LOW + SHORT HIGH = bit 1
DecoderStep::Data => {
if level {
// HIGH pulse completes a bit pair
let bit_value;
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA
&& duration_diff!(duration, TE_LONG) < TE_DELTA
{
bit_value = false; // bit 0
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA
&& duration_diff!(duration, TE_SHORT) < TE_DELTA
{
bit_value = true; // bit 1
} else {
self.step = DecoderStep::Reset;
return None;
}
self.raw_data = (self.raw_data << 1) | (bit_value as u64);
self.bit_count += 1;
if self.bit_count >= MIN_COUNT_BIT {
let result = parse_data(self.raw_data);
self.step = DecoderStep::Reset;
return Some(result);
}
} else {
// LOW pulse: save duration for the bit pair
self.te_last = duration;
}
}
}
None
}
fn supports_encoding(&self) -> bool {
false
}
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
None
}
}
impl Default for PorscheTouaregDecoder {
fn default() -> Self {
Self::new()
}
}
+345 -140
View File
@@ -1,13 +1,14 @@
//! PSA (Peugeot/Citroën) protocol decoder/encoder //! PSA (Peugeot/Citroën) protocol decoder/encoder
//! //!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`. //! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`.
//! Decode/encode logic (Manchester, preamble, TEA, XOR, mode 0x23/0x36) matches reference.
//! //!
//! Protocol characteristics: //! Protocol characteristics:
//! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble) //! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble)
//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation //! - 128 bits total: key1 (64) + validation (16) + key2/rest (48)
//! - TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds XOR layer //! - Modified TEA (XTEA-like) with dynamic key selection (sum&3, (sum>>11)&3)
//! - Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule) //! - Mode 0x23: direct XOR decrypt with checksum validation
//! - Mode 0x36: TEA brute-force with BF1/BF2 key schedules (deferred)
//! - Dual preamble: Pattern 1 (250µs) and Pattern 2 (125µs)
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff; use crate::duration_diff;
@@ -21,7 +22,6 @@ const MIN_COUNT_BIT: usize = 128;
// Internal timing for Manchester sub-symbol detection // Internal timing for Manchester sub-symbol detection
const TE_SHORT_125: u32 = 125; const TE_SHORT_125: u32 = 125;
const TE_LONG_250: u32 = 250; const TE_LONG_250: u32 = 250;
const TE_TOLERANCE_49: u32 = 49;
const TE_TOLERANCE_50: u32 = 50; const TE_TOLERANCE_50: u32 = 50;
const TE_TOLERANCE_99: u32 = 99; const TE_TOLERANCE_99: u32 = 99;
const TE_END_1000: u32 = 1000; const TE_END_1000: u32 = 1000;
@@ -45,13 +45,19 @@ enum ManchesterState {
Start1, Start1,
} }
/// Decoder states (matches protopirate's PsaDecoderState) /// Decoder states (matches protopirate's PsaDecoderState 0-4)
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderState { enum DecoderState {
/// State0: Wait for first edge (pattern detection)
WaitEdge, WaitEdge,
CountPattern, /// State1: Count 250µs preamble patterns (Pattern 1)
DecodeManchester, CountPattern250,
End, /// State2: Manchester decode at 250/500µs (Pattern 1)
DecodeManchester250,
/// State3: Count 125µs preamble patterns (Pattern 2)
CountPattern125,
/// State4: Manchester decode at 125/250µs (Pattern 2)
DecodeManchester125,
} }
/// PSA protocol decoder /// PSA protocol decoder
@@ -140,111 +146,268 @@ impl PsaDecoder {
} }
} }
/// TEA decrypt (matches psa.c / standard TEA) /// Modified TEA decrypt with dynamic key selection (matches psa.c psa_tea_decrypt)
/// Uses XTEA-like key scheduling: key[sum&3] and key[(sum>>11)&3]
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) { fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS); let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
for _ in 0..TEA_ROUNDS { for _ in 0..TEA_ROUNDS {
*v1 = v1.wrapping_sub( let k_idx2 = ((sum >> 11) & 3) as usize;
(v0.wrapping_shl(4).wrapping_add(key[2])) let temp = key[k_idx2].wrapping_add(sum);
^ (v0.wrapping_add(sum))
^ (v0.wrapping_shr(5).wrapping_add(key[3])),
);
*v0 = v0.wrapping_sub(
(v1.wrapping_shl(4).wrapping_add(key[0]))
^ (v1.wrapping_add(sum))
^ (v1.wrapping_shr(5).wrapping_add(key[1])),
);
sum = sum.wrapping_sub(TEA_DELTA); sum = sum.wrapping_sub(TEA_DELTA);
*v1 = v1.wrapping_sub(
temp ^ (v0.wrapping_shr(5) ^ v0.wrapping_shl(4)).wrapping_add(*v0),
);
let k_idx1 = (sum & 3) as usize;
let temp = key[k_idx1].wrapping_add(sum);
*v0 = v0.wrapping_sub(
temp ^ (v1.wrapping_shr(5) ^ v1.wrapping_shl(4)).wrapping_add(*v1),
);
} }
} }
/// TEA encrypt (matches psa.c / standard TEA) /// Modified TEA encrypt with dynamic key selection (matches psa.c psa_tea_encrypt)
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) { fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
let mut sum: u32 = 0; let mut sum: u32 = 0;
for _ in 0..TEA_ROUNDS { for _ in 0..TEA_ROUNDS {
let k_idx1 = (sum & 3) as usize;
let temp = key[k_idx1].wrapping_add(sum);
sum = sum.wrapping_add(TEA_DELTA); sum = sum.wrapping_add(TEA_DELTA);
*v0 = v0.wrapping_add( *v0 = v0.wrapping_add(
(v1.wrapping_shl(4).wrapping_add(key[0])) temp ^ (v1.wrapping_shr(5) ^ v1.wrapping_shl(4)).wrapping_add(*v1),
^ (v1.wrapping_add(sum))
^ (v1.wrapping_shr(5).wrapping_add(key[1])),
); );
let k_idx2 = ((sum >> 11) & 3) as usize;
let temp = key[k_idx2].wrapping_add(sum);
*v1 = v1.wrapping_add( *v1 = v1.wrapping_add(
(v0.wrapping_shl(4).wrapping_add(key[2])) temp ^ (v0.wrapping_shr(5) ^ v0.wrapping_shl(4)).wrapping_add(*v0),
^ (v0.wrapping_add(sum))
^ (v0.wrapping_shr(5).wrapping_add(key[3])),
); );
} }
} }
/// XOR decrypt for mode 0x23 (matches psa.c) /// XOR decrypt for mode 0x23 (matches psa.c psa_second_stage_xor_decrypt)
/// Uses psa_copy_reverse byte reordering then XOR operations
fn xor_decrypt(buffer: &mut [u8]) { fn xor_decrypt(buffer: &mut [u8]) {
// psa_copy_reverse: reorder source bytes
let temp = [
buffer[5], // temp[0] = source[5]
buffer[4], // temp[1] = source[4]
buffer[3], // temp[2] = source[3]
buffer[2], // temp[3] = source[2]
buffer[9], // temp[4] = source[9]
buffer[8], // temp[5] = source[8]
buffer[7], // temp[6] = source[7]
buffer[6], // temp[7] = source[6]
];
buffer[2] = temp[0] ^ temp[6];
buffer[3] = temp[2] ^ temp[0];
buffer[4] = temp[6] ^ temp[3];
buffer[5] = temp[7] ^ temp[1];
buffer[6] = temp[3] ^ temp[1];
buffer[7] = temp[6] ^ temp[4] ^ temp[5];
}
/// XOR encrypt for mode 0x23 encoding (matches psa.c psa_second_stage_xor_encrypt)
fn xor_encrypt(buffer: &mut [u8]) {
let e6 = buffer[8]; let e6 = buffer[8];
let e7 = buffer[9]; let e7 = buffer[9];
let e5 = buffer[7]; let p0 = buffer[2];
let e0 = buffer[2]; let p1 = buffer[3];
let e1 = buffer[3]; let p2 = buffer[4];
let e2 = buffer[4]; let p3 = buffer[5];
let e3 = buffer[5]; let p4 = buffer[6];
let e4 = buffer[6]; let p5 = buffer[7];
buffer[2] = e0 ^ e5; let ne5 = p5 ^ e7 ^ e6;
buffer[3] = e1 ^ (e0 ^ e5 ^ e6 ^ e7); let ne0 = p2 ^ ne5;
buffer[4] = e2 ^ e0; let ne2 = p4 ^ ne0;
buffer[5] = e3 ^ (e0 ^ e5 ^ e6 ^ e7); let ne4 = p3 ^ ne2;
buffer[6] = e4 ^ e2; let ne3 = p0 ^ ne5;
buffer[7] = e5 ^ e6 ^ e7; let ne1 = p1 ^ ne3;
buffer[2] = ne0;
buffer[3] = ne1;
buffer[4] = ne2;
buffer[5] = ne3;
buffer[6] = ne4;
buffer[7] = ne5;
} }
/// Decrypt key1 + validation: mode 0x23 (TEA+XOR) or 0x36 (TEA, BF2) — matches psa.c /// Calculate checksum over buffer[2..8] (matches psa.c psa_calculate_checksum)
fn calculate_checksum(buffer: &[u8]) -> u8 {
let mut checksum: u32 = 0;
for i in 2..8 {
checksum += (buffer[i] & 0xF) as u32 + ((buffer[i] >> 4) & 0xF) as u32;
}
((checksum.wrapping_mul(0x10)) & 0xFF) as u8
}
/// Check if direct XOR is allowed by key2 high byte (matches psa.c)
fn direct_xor_allowed_by_key2(key2_high_byte: u8) -> bool {
let lo = key2_high_byte & 0xF;
if lo < 3 { return true; }
if lo < 7 && (key2_high_byte & 0xC) != 0 { return true; }
false
}
/// Setup byte buffer from key1/key2 (matches psa.c psa_setup_byte_buffer)
fn setup_byte_buffer(buffer: &mut [u8], key1_low: u32, key1_high: u32, key2_low: u32) {
for i in 0..8usize {
let shift = i * 8;
let byte_val = if shift < 32 {
((key1_low >> shift) & 0xFF) as u8
} else {
((key1_high >> (shift - 32)) & 0xFF) as u8
};
buffer[7 - i] = byte_val;
}
buffer[9] = (key2_low & 0xFF) as u8;
buffer[8] = ((key2_low >> 8) & 0xFF) as u8;
}
/// Decrypt using the C code's approach: setup_byte_buffer then attempt direct XOR
/// with checksum validation; mode 0x36 is marked for brute-force (matches psa.c)
fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> { fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> {
// Try mode 0x23 first (seed byte 0x23) // C: key2_low = decode_data_low (the 16-bit validation sits in the low word)
let seed_byte = (self.key1_high >> 24) as u8; let key2_low = self.validation_field as u32;
if seed_byte >= 0x23 && seed_byte < 0x24 { let mut buffer = [0u8; 48];
// Mode 0x23 - TEA + XOR Self::setup_byte_buffer(&mut buffer, self.key1_low, self.key1_high, key2_low);
let mut v0 = self.key1_high;
let mut v1 = self.key1_low;
Self::tea_decrypt(&mut v0, &mut v1, &BF1_KEY_SCHEDULE);
let mut buffer = [0u8; 10]; let key2_high_byte = buffer[8];
buffer[0] = (v0 >> 24) as u8;
buffer[1] = (v0 >> 16) as u8;
buffer[2] = (v0 >> 8) as u8;
buffer[3] = (v0 >> 0) as u8;
buffer[4] = (v1 >> 24) as u8;
buffer[5] = (v1 >> 16) as u8;
buffer[6] = (v1 >> 8) as u8;
buffer[7] = (v1 >> 0) as u8;
buffer[8] = (self.validation_field >> 8) as u8;
buffer[9] = (self.validation_field & 0xFF) as u8;
// Try direct XOR decrypt (mode 0x23) if allowed by key2 filter
if Self::direct_xor_allowed_by_key2(key2_high_byte) {
let checksum = Self::calculate_checksum(&buffer);
let validation_result = (checksum ^ key2_high_byte) & 0xF0;
if validation_result == 0 {
// Direct XOR decrypt succeeded validation
Self::xor_decrypt(&mut buffer); Self::xor_decrypt(&mut buffer);
let serial = ((buffer[2] as u32) << 16) let serial = ((buffer[3] as u32) << 8)
| ((buffer[3] as u32) << 8) | ((buffer[2] as u32) << 16)
| (buffer[4] as u32); | (buffer[4] as u32);
let counter = ((buffer[5] as u32) << 8) | (buffer[6] as u32); let counter = (buffer[6] as u32) | ((buffer[5] as u32) << 8);
let crc = buffer[7] as u16; let crc = buffer[7] as u16;
let btn = buffer[8] & 0x0F; let btn = buffer[8] & 0x0F;
return Some((serial, btn, counter, crc, 0x23)); return Some((serial, btn, counter, crc, 0x23));
} }
}
if seed_byte >= 0xF3 && seed_byte < 0xF4 { // Mode 0x36 - TEA brute-force path
// Mode 0x36 - TEA + different key schedule // Try direct TEA decrypt with BF1 key schedule for a quick decode attempt
let mut v0 = self.key1_high; {
let mut v1 = self.key1_low; let mut w0 = ((buffer[3] as u32) << 16)
Self::tea_decrypt(&mut v0, &mut v1, &BF2_KEY_SCHEDULE); | ((buffer[2] as u32) << 24)
| ((buffer[4] as u32) << 8)
| (buffer[5] as u32);
let mut w1 = ((buffer[7] as u32) << 16)
| ((buffer[6] as u32) << 24)
| ((buffer[8] as u32) << 8)
| (buffer[9] as u32);
let serial = ((v0 >> 8) & 0xFFFF00) | ((v0 & 0xFF) as u32); Self::tea_decrypt(&mut w0, &mut w1, &BF1_KEY_SCHEDULE);
let counter = v1 >> 16;
let btn = ((v1 >> 8) & 0xF) as u8; // Check if the TEA CRC validates (sum of bytes)
let crc = (v1 & 0xFF) as u16; let crc_calc = Self::calculate_tea_crc(w0, w1);
if crc_calc == (w1 & 0xFF) as u8 {
let mut dec_buffer = [0u8; 48];
dec_buffer[2] = ((w0 >> 24) & 0xFF) as u8;
dec_buffer[3] = ((w0 >> 16) & 0xFF) as u8;
dec_buffer[4] = ((w0 >> 8) & 0xFF) as u8;
dec_buffer[5] = (w0 & 0xFF) as u8;
dec_buffer[6] = ((w1 >> 24) & 0xFF) as u8;
dec_buffer[7] = ((w1 >> 16) & 0xFF) as u8;
dec_buffer[8] = ((w1 >> 8) & 0xFF) as u8;
dec_buffer[9] = (w1 & 0xFF) as u8;
let btn = (dec_buffer[5] >> 4) & 0xF;
let serial = ((dec_buffer[3] as u32) << 8)
| ((dec_buffer[2] as u32) << 16)
| (dec_buffer[4] as u32);
let counter = ((dec_buffer[7] as u32) << 8)
| ((dec_buffer[6] as u32) << 16)
| (dec_buffer[8] as u32)
| (((dec_buffer[5] as u32) & 0xF) << 24);
let crc = dec_buffer[9] as u16;
return Some((serial, btn, counter, crc, 0x36)); return Some((serial, btn, counter, crc, 0x36));
} }
}
// Cannot decrypt - return raw data // Also try BF2 key schedule directly (XOR-derived keys)
{
let mut w0 = ((buffer[3] as u32) << 16)
| ((buffer[2] as u32) << 24)
| ((buffer[4] as u32) << 8)
| (buffer[5] as u32);
let mut w1 = ((buffer[7] as u32) << 16)
| ((buffer[6] as u32) << 24)
| ((buffer[8] as u32) << 8)
| (buffer[9] as u32);
Self::tea_decrypt(&mut w0, &mut w1, &BF2_KEY_SCHEDULE);
let crc_calc = Self::calculate_tea_crc(w0, w1);
if crc_calc == (w1 & 0xFF) as u8 {
let mut dec_buffer = [0u8; 48];
dec_buffer[2] = ((w0 >> 24) & 0xFF) as u8;
dec_buffer[3] = ((w0 >> 16) & 0xFF) as u8;
dec_buffer[4] = ((w0 >> 8) & 0xFF) as u8;
dec_buffer[5] = (w0 & 0xFF) as u8;
dec_buffer[6] = ((w1 >> 24) & 0xFF) as u8;
dec_buffer[7] = ((w1 >> 16) & 0xFF) as u8;
dec_buffer[8] = ((w1 >> 8) & 0xFF) as u8;
dec_buffer[9] = (w1 & 0xFF) as u8;
let btn = (dec_buffer[5] >> 4) & 0xF;
let serial = ((dec_buffer[3] as u32) << 8)
| ((dec_buffer[2] as u32) << 16)
| (dec_buffer[4] as u32);
let counter = ((dec_buffer[7] as u32) << 8)
| ((dec_buffer[6] as u32) << 16)
| (dec_buffer[8] as u32)
| (((dec_buffer[5] as u32) & 0xF) << 24);
let crc = dec_buffer[9] as u16;
return Some((serial, btn, counter, crc, 0x36));
}
}
None
}
/// Calculate TEA CRC (matches psa.c psa_calculate_tea_crc)
fn calculate_tea_crc(v0: u32, v1: u32) -> u8 {
let mut crc: u32 = 0;
crc += (v0 >> 24) & 0xFF;
crc += (v0 >> 16) & 0xFF;
crc += (v0 >> 8) & 0xFF;
crc += v0 & 0xFF;
crc += (v1 >> 24) & 0xFF;
crc += (v1 >> 16) & 0xFF;
crc += (v1 >> 8) & 0xFF;
(crc & 0xFF) as u8
}
fn init_preamble_state(&mut self) {
self.data_low = 0;
self.data_high = 0;
self.pattern_counter = 0;
self.bit_count = 0;
self.manchester_state = ManchesterState::Mid1;
}
fn finalize_frame(&mut self) -> Option<DecodedSignal> {
self.state = DecoderState::WaitEdge;
if self.bit_count >= 80 {
// C validation: ((key1_high >> 16) & 0xF) == 0xA
if ((self.key1_high >> 16) & 0xF) != 0xA {
return None;
}
let result = self.parse_data();
return Some(result);
}
None None
} }
@@ -317,24 +480,96 @@ impl ProtocolDecoder for PsaDecoder {
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> { fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.state { match self.state {
// State0: detect preamble pattern type
DecoderState::WaitEdge => { DecoderState::WaitEdge => {
if level && duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_49 { if !level {
self.state = DecoderState::CountPattern; return None;
}
let diff_250 = duration_diff!(duration, TE_SHORT);
let diff_125 = duration_diff!(duration, TE_SHORT_125);
if diff_250 < TE_TOLERANCE_99 {
// Pattern 1: 250µs preamble
self.init_preamble_state();
self.state = DecoderState::CountPattern250;
} else if diff_125 < 40 && duration <= 180 {
// Pattern 2: 125µs preamble
self.init_preamble_state();
self.state = DecoderState::CountPattern125;
}
self.prev_duration = duration; self.prev_duration = duration;
}
// State1: count 250µs preamble (Pattern 1)
DecoderState::CountPattern250 => {
if level {
return None;
}
let diff_short = duration_diff!(duration, TE_SHORT);
if diff_short < TE_TOLERANCE_99 + 1 {
let prev_diff = duration_diff!(self.prev_duration, TE_SHORT);
if prev_diff <= TE_TOLERANCE_99 {
self.pattern_counter += 1;
}
self.prev_duration = duration;
} else {
let diff_long = duration_diff!(duration, TE_LONG);
if diff_long < 100 && self.pattern_counter > 0x46 {
// Transition to Manchester decode at 250/500µs
self.state = DecoderState::DecodeManchester250;
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.manchester_state = ManchesterState::Mid1;
self.pattern_counter = 0;
self.prev_duration = duration;
} else {
self.state = DecoderState::WaitEdge;
self.pattern_counter = 0; self.pattern_counter = 0;
} }
} }
}
DecoderState::CountPattern => { // State2: Manchester decode at 250/500µs (Pattern 1)
DecoderState::DecodeManchester250 => {
if self.bit_count >= 121 {
return self.finalize_frame();
}
// Check for end-of-frame marker
if level && self.bit_count == 80 && duration >= 800 {
let end_diff = duration_diff!(duration, TE_END_1000);
if end_diff <= 199 {
return self.finalize_frame();
}
}
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
if duration > 10000 {
self.state = DecoderState::WaitEdge;
self.pattern_counter = 0;
return None;
}
if is_short || is_long {
if let Some(bit) = self.manchester_advance(is_short, level) {
self.add_bit(bit);
}
}
self.prev_duration = duration;
}
// State3: count 125µs preamble (Pattern 2)
DecoderState::CountPattern125 => {
let diff_125 = duration_diff!(duration, TE_SHORT_125); let diff_125 = duration_diff!(duration, TE_SHORT_125);
let diff_250 = duration_diff!(duration, TE_LONG_250); let diff_250 = duration_diff!(duration, TE_LONG_250);
if diff_125 < TE_TOLERANCE_50 { if diff_125 < TE_TOLERANCE_50 {
self.pattern_counter += 1; self.pattern_counter += 1;
self.prev_duration = duration; self.prev_duration = duration;
} else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x46 { } else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x45 {
// Found end of preamble, start Manchester decoding // Transition to Manchester decode at 125/250µs
self.state = DecoderState::DecodeManchester; self.state = DecoderState::DecodeManchester125;
self.data_low = 0; self.data_low = 0;
self.data_high = 0; self.data_high = 0;
self.bit_count = 0; self.bit_count = 0;
@@ -347,23 +582,17 @@ impl ProtocolDecoder for PsaDecoder {
} }
} }
DecoderState::DecodeManchester => { // State4: Manchester decode at 125/250µs (Pattern 2)
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA; DecoderState::DecodeManchester125 => {
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA; if self.bit_count >= 121 {
let is_end = duration > TE_END_1000; return self.finalize_frame();
if is_end || self.bit_count >= 121 {
// End of data
self.state = DecoderState::End;
if self.bit_count >= 96 {
// Got enough data
let result = self.parse_data();
self.state = DecoderState::WaitEdge;
return Some(result);
} }
self.state = DecoderState::WaitEdge; let is_short = duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_50;
return None; let is_long = duration_diff!(duration, TE_LONG_250) < TE_TOLERANCE_99;
let is_end = duration > 500;
if is_end {
return self.finalize_frame();
} }
if is_short || is_long { if is_short || is_long {
@@ -373,13 +602,8 @@ impl ProtocolDecoder for PsaDecoder {
} else { } else {
self.state = DecoderState::WaitEdge; self.state = DecoderState::WaitEdge;
} }
self.prev_duration = duration; self.prev_duration = duration;
} }
DecoderState::End => {
self.state = DecoderState::WaitEdge;
}
} }
None None
@@ -406,31 +630,8 @@ impl ProtocolDecoder for PsaDecoder {
buffer[8] = button & 0x0F; buffer[8] = button & 0x0F;
buffer[9] = 0; buffer[9] = 0;
// XOR encrypt // XOR encrypt (matches psa.c psa_second_stage_xor_encrypt)
{ Self::xor_encrypt(&mut buffer);
let e6 = buffer[8];
let e7 = buffer[9];
let p0 = buffer[2];
let p1 = buffer[3];
let p2 = buffer[4];
let p3 = buffer[5];
let p4 = buffer[6];
let p5 = buffer[7];
let ne5 = p5 ^ e7 ^ e6;
let ne0 = p2 ^ ne5;
let ne2 = p4 ^ ne0;
let ne4 = p3 ^ ne2;
let ne3 = p0 ^ ne5;
let ne1 = p1 ^ ne3;
buffer[2] = ne0;
buffer[3] = ne1;
buffer[4] = ne2;
buffer[5] = ne3;
buffer[6] = ne4;
buffer[7] = ne5;
}
// TEA encrypt // TEA encrypt
let mut v0 = ((buffer[0] as u32) << 24) let mut v0 = ((buffer[0] as u32) << 24)
@@ -450,38 +651,42 @@ impl ProtocolDecoder for PsaDecoder {
let mut signal = Vec::with_capacity(512); let mut signal = Vec::with_capacity(512);
// Preamble + sync (matches protopirate psa encode) // Preamble: 80 iterations at 250us HIGH+LOW (matches C: te = PSA_TE_LONG_250)
for _ in 0..70 { for _ in 0..80 {
signal.push(LevelDuration::new(true, TE_SHORT_125));
signal.push(LevelDuration::new(false, TE_SHORT_125));
}
signal.push(LevelDuration::new(true, TE_LONG_250)); signal.push(LevelDuration::new(true, TE_LONG_250));
signal.push(LevelDuration::new(false, TE_LONG_250)); signal.push(LevelDuration::new(false, TE_LONG_250));
}
// Key1: 64 bits Manchester, then validation 16 bits // Sync transition: LOW 250us, HIGH 500us, LOW 250us (matches C)
signal.push(LevelDuration::new(false, TE_LONG_250));
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_LONG_250));
// Key1: 64 bits Manchester at 250us (C: bit 1 = true,false; bit 0 = false,true)
let key1 = ((key1_high as u64) << 32) | (key1_low as u64); let key1 = ((key1_high as u64) << 32) | (key1_low as u64);
for bit in (0..64).rev() { for bit in (0..64).rev() {
if (key1 >> bit) & 1 == 1 { if (key1 >> bit) & 1 == 1 {
signal.push(LevelDuration::new(false, TE_SHORT)); signal.push(LevelDuration::new(true, TE_LONG_250));
signal.push(LevelDuration::new(true, TE_SHORT)); signal.push(LevelDuration::new(false, TE_LONG_250));
} else { } else {
signal.push(LevelDuration::new(true, TE_SHORT)); signal.push(LevelDuration::new(false, TE_LONG_250));
signal.push(LevelDuration::new(false, TE_SHORT)); signal.push(LevelDuration::new(true, TE_LONG_250));
} }
} }
// Validation: 16 bits Manchester encoded // Validation: 16 bits Manchester at 250us
for bit in (0..16).rev() { for bit in (0..16).rev() {
if (validation >> bit) & 1 == 1 { if (validation >> bit) & 1 == 1 {
signal.push(LevelDuration::new(false, TE_SHORT)); signal.push(LevelDuration::new(true, TE_LONG_250));
signal.push(LevelDuration::new(true, TE_SHORT)); signal.push(LevelDuration::new(false, TE_LONG_250));
} else { } else {
signal.push(LevelDuration::new(true, TE_SHORT)); signal.push(LevelDuration::new(false, TE_LONG_250));
signal.push(LevelDuration::new(false, TE_SHORT)); signal.push(LevelDuration::new(true, TE_LONG_250));
} }
} }
// End marker // End marker: HIGH 1000us + LOW 1000us (matches C)
signal.push(LevelDuration::new(true, TE_END_1000));
signal.push(LevelDuration::new(false, TE_END_1000)); signal.push(LevelDuration::new(false, TE_END_1000));
Some(signal) Some(signal)
+16
View File
@@ -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"
+27
View File
@@ -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,
}
}
}
+47
View File
@@ -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 {}
+94
View File
@@ -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;
}
+210
View File
@@ -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()) };
}
}
}
+54
View File
@@ -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
}
+21
View File
@@ -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)
}
}