Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48ee413c9b | |||
| 84b75ee6b5 | |||
| 295e4c4d4a | |||
| a58d13362a | |||
| 23431c4a7a | |||
| c0de9effc8 | |||
| bd19816e91 | |||
| 5d0bd97954 |
@@ -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
@@ -444,7 +444,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kat"
|
||||
version = "1.1.2"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atty",
|
||||
@@ -482,8 +482,6 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
[[package]]
|
||||
name = "libhackrf"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e640122f67f490129cd3ab1517149b4091eeca0a14de3df7510626df258196b9"
|
||||
dependencies = [
|
||||
"num-complex",
|
||||
]
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kat"
|
||||
version = "1.1.3"
|
||||
version = "1.2.0"
|
||||
edition = "2021"
|
||||
description = "Keyfob Analysis Toolkit - HackRF/RTL-SDR signal capture, decode, and transmit (HackRF only)"
|
||||
authors = ["KAT Team"]
|
||||
@@ -54,6 +54,10 @@ atty = "0.2"
|
||||
[build-dependencies]
|
||||
pkg-config = "0.3"
|
||||
|
||||
[patch.crates-io]
|
||||
# Vendored with ARM fix: c_char is u8 on ARM, so the upstream transmute fails
|
||||
libhackrf = { path = "vendor/libhackrf" }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
|
||||
@@ -17,7 +17,7 @@ A terminal-based RF signal analysis tool for capturing, decoding, and retransmit
|
||||
## Features
|
||||
|
||||
- **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies (HackRF uses AM envelope detection; FM protocols are tagged for display and may decode when signal is strong)
|
||||
- **Multi-protocol decoding** — 14 protocol decoders: Kia V0–V6, Ford V0, Fiat V0, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions
|
||||
- **Multi-protocol decoding** — 18 protocol decoders: Kia V0–V6, Ford V0, Fiat V0/V1, Mazda V0, Mitsubishi V0, Porsche Touareg, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions
|
||||
- **KeeLoq generic fallback** — when a signal doesn’t match any known protocol, KAT tries decoding it as KeeLoq using every manufacturer key in the embedded keystore (Kia V3/V4 and Star Line air formats); successful decodes appear as **Keeloq (keystore name)** in the capture list
|
||||
- **RF modulation metadata** — each protocol tagged as AM, FM, or both (from ProtoPirate); shown in signal detail and exported in .fob
|
||||
- **Rich signal detail** — encoding (PWM/Manchester), RF (AM/FM), encryption, serial, counter, key data, CRC, frequency, and raw level/duration pairs
|
||||
@@ -119,11 +119,12 @@ Press `Enter` on a capture to open the action menu. When using RTL-SDR (receive-
|
||||
|
||||
When exporting to `.fob`, a metadata form collects filename and optional vehicle info:
|
||||
|
||||
- **File** — output filename (extension added by format)
|
||||
- **File** — output filename (extension added by format). For unknown protocol, a unique 8-character hex suffix (e.g. `A1B2C3D4`) is shown in the field and appended to the filename so each export has a distinct file.
|
||||
- **Year** — vehicle model year
|
||||
- **Make** — manufacturer (auto-suggested from protocol)
|
||||
- **Model** — vehicle model
|
||||
- **Region** — region/market
|
||||
- **Command** — button/command label (e.g. Unlock, Lock, Trunk, Panic); used in the default filename for unknown protocol and stored in the .fob vehicle section.
|
||||
- **Notes** — free-form notes
|
||||
|
||||
The exported `.fob` file is a versioned JSON document (v2.0, format `kat-fob`) containing:
|
||||
@@ -153,17 +154,22 @@ The exported `.fob` file is a versioned JSON document (v2.0, format `kat-fob`) c
|
||||
"year": 2023,
|
||||
"make": "Kia",
|
||||
"model": "Sportage",
|
||||
"region": "",
|
||||
"region": "NA",
|
||||
"command": "Lock",
|
||||
"notes": ""
|
||||
},
|
||||
"capture": {
|
||||
"timestamp": "2026-02-07T12:00:00Z",
|
||||
"raw_data_hex": "0x...",
|
||||
"raw_pair_count": 0,
|
||||
"raw_pairs": [{"level": true, "duration_us": 400}, {"level": false, "duration_us": 800}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`rf_modulation` is AM, FM, or AM/FM per protocol (from ProtoPirate). Raw pairs are included when config `include_raw_pairs` is true.
|
||||
- **vehicle.command** — optional; user-editable command label (e.g. Unlock, Lock). Set in the export form or via **i** (capture metadata). Used for unknown-protocol export filenames (`Year_Make_Model_Region_Command_8HEX.fob`).
|
||||
- **rf_modulation** — AM, FM, or AM/FM per protocol (from ProtoPirate). Omitted when unknown.
|
||||
- **capture** — `raw_data_hex`, `raw_pair_count`, and optionally `raw_pairs` when config `include_raw_pairs` is true.
|
||||
|
||||
### VIM-Style Commands
|
||||
|
||||
@@ -213,7 +219,7 @@ include_raw_pairs = true
|
||||
```
|
||||
|
||||
- **import_directory** — directory scanned at startup for .fob and .sub files to import (default `~/.config/KAT/import`). Exports are still saved to **export_directory**.
|
||||
- **research_mode** — when `false` (default), only successfully decoded signals appear in the list; when `true`, unknown (unidentified) signals are also shown.
|
||||
- **research_mode** — when `true` (default), unknown (unidentified) signals are shown in addition to decoded ones; when `false`, only successfully decoded signals appear.
|
||||
- **include_raw_pairs** — when `true`, .fob exports include raw level/duration pairs for replay.
|
||||
|
||||
## Supported Protocols
|
||||
@@ -227,15 +233,19 @@ Protocol behavior and RF modulation (AM/FM) follow the ProtoPirate reference. KA
|
||||
| Kia V2 | Manchester | FM | Fixed Code | 315 / 433.92 MHz |
|
||||
| Kia V3/V4 | PWM | AM/FM | KeeLoq | 315 / 433.92 MHz |
|
||||
| Kia V5 | Manchester | FM | Fixed Code | 433.92 MHz |
|
||||
| Kia V6 | Manchester | FM | Fixed Code | 433.92 MHz |
|
||||
| Ford V0 | Manchester | FM | Fixed Code | 433.92 MHz |
|
||||
| Kia V6 | Manchester | FM | AES-128 | 433.92 MHz |
|
||||
| Ford V0 | Manchester | FM | Rolling Code | 315 / 433.92 MHz |
|
||||
| Fiat V0 | Manchester | FM | Fixed Code | 433.92 MHz |
|
||||
| Fiat V1 (Magneti Marelli) | Manchester | FM | Rolling Code | 433.92 MHz |
|
||||
| Mazda V0 | Pair-based | FM | XOR Deobfuscation | 433.92 MHz |
|
||||
| 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 |
|
||||
| Suzuki | PWM | AM | Rolling Code | 433.92 MHz |
|
||||
| VAG (VW/Audi/Seat/Skoda) | Manchester | AM | AUT64/XTEA | 433.92 / 434.42 MHz (decode + encode) |
|
||||
| VAG (VW/Audi/Seat/Skoda) | Manchester | AM | AUT64/XTEA | 433.92 / 434.42 MHz |
|
||||
| Scher-Khan | PWM | FM | Magic Code | 433.92 MHz |
|
||||
| Star Line | PWM | AM | KeeLoq | 433.92 MHz |
|
||||
| PSA (Peugeot/Citroën) | Manchester | FM | XTEA/XOR | 433.92 MHz |
|
||||
| PSA (Peugeot/Citroën) | Manchester | FM | Modified TEA/XOR | 433.92 MHz |
|
||||
|
||||
**KeeLoq generic fallback:** If no protocol decodes a capture, KAT tries KeeLoq with every keystore manufacturer key (Kia V3/V4 and Star Line bit layouts). On success the protocol is shown as **Keeloq (*keystore name*)** (e.g. Keeloq (Alligator), Keeloq (Pandora_PRO)). See [docs/keeloq_generic.md](docs/keeloq_generic.md).
|
||||
|
||||
@@ -256,6 +266,8 @@ Details (exact model/year scope) are in `src/vuln_db.rs`. The app shows the NVD
|
||||
### Cryptographic modules
|
||||
|
||||
- **KeeLoq** — encrypt/decrypt with normal, secure, FAAC, and magic serial/XOR learning key derivation (keeloq_common, keys). Unknown signals are tried as KeeLoq with every keystore key via **keeloq_generic** (uses keeloq_common only).
|
||||
- **AES-128** — full encrypt/decrypt for Kia V6 (key derived from keystore A/B with XOR masks)
|
||||
- **Modified TEA** — XTEA-like cipher with dynamic key selection for PSA (Peugeot/Citroën)
|
||||
- **AUT64** — 12-round block cipher for VAG type 1/3/4 (aut64)
|
||||
- **Keystore** — manufacturer keys (Kia, VAG, etc.) built in; see `src/keystore/`
|
||||
|
||||
@@ -268,7 +280,7 @@ The **AM/OOK** demodulator turns IQ samples into level/duration pairs for protoc
|
||||
- **Exponential moving average** — magnitude smoothing
|
||||
- **Schmitt trigger hysteresis** — reduces chattering at the decision boundary
|
||||
- **Debounce** — 40µs minimum pulse width to reject noise spikes
|
||||
- **Gap detection** — 20 ms gap treated as end of signal
|
||||
- **Gap detection** — 80 ms gap treated as end of signal (multi-burst keyfob presses stay one capture)
|
||||
|
||||
## IMPORTS folder
|
||||
|
||||
@@ -296,7 +308,8 @@ src/
|
||||
│ ├── aut64.rs # AUT64 block cipher (VAG)
|
||||
│ ├── keys.rs # Key loading (embedded + optional file), KIA/VAG
|
||||
│ ├── kia_v0..kia_v6.rs
|
||||
│ ├── ford_v0.rs, fiat_v0.rs, subaru.rs, suzuki.rs
|
||||
│ ├── ford_v0.rs, fiat_v0.rs, fiat_v1.rs, subaru.rs, suzuki.rs
|
||||
│ ├── mazda_v0.rs, mitsubishi_v0.rs, porsche_touareg.rs
|
||||
│ ├── vag.rs # VAG decoder/encoder (4 sub-types; encode from capture via extra)
|
||||
│ ├── scher_khan.rs, star_line.rs, psa.rs
|
||||
│ └── ...
|
||||
@@ -314,21 +327,20 @@ src/
|
||||
└── status_bar.rs # Status bar
|
||||
```
|
||||
|
||||
## Call for researchers
|
||||
|
||||
**We need your help.** KAT’s protocol decoders and future analysis depend on real-world keyfob captures. If you use KAT for security research, authorized testing, or protocol development, please consider:
|
||||
|
||||
- **Contributing to protocol analysis** — share timing, encoding, or decoder feedback; report bugs or suggest new protocols.
|
||||
- **Contributing captures to the research library** — add your `.fob` files to the **[FOB Research Library](https://github.com/KaraZajac/FOB_Research_Library)** so others can use them for decoder development, CVE validation, and protocol studies.
|
||||
|
||||
The [FOB Research Library](https://github.com/KaraZajac/FOB_Research_Library) is a community collection of KAT `.fob` captures organized by manufacturer. Your submissions (with proper metadata: year, make, model, region, command) help grow test vectors and support the wider keyfob research community.
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
KAT is developed by **Kara Zajac (.leviathan)**. KAT would not be possible without [ProtoPirate](https://protopirate.net/ProtoPirate/ProtoPirate)—the protocol decoders, reference implementations, and community work are the foundation this tool is built on. Truly standing on the shoulders of giants.
|
||||
|
||||
### ProtoPirate Development Team
|
||||
|
||||
RocketGod, MMX, Leeroy, gullradriel, Skorp (thanks for the Weather App inspiration!), Vadim's Radio Driver.
|
||||
|
||||
### Protocol Magic
|
||||
|
||||
L0rdDiakon, YougZ, RocketGod, MMX, DoobTheGoober, Skorp, Slackware, Trikk, Wootini, Li0ard, Leeroy.
|
||||
|
||||
### Reverse Engineering Support
|
||||
|
||||
DoobTheGoober, MMX, NeedNotApply, RocketGod, Slackware, Trikk, Li0ard.
|
||||
KAT is developed by **Kara Zajac (.leviathan)**. KAT would not be possible without [ProtoPirate](https://protopirate.net/ProtoPirate/ProtoPirate)—the protocol decoders, reference implementations, and community work are the foundation this tool is built on. I am truly standing on the shoulders of giants.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ This folder describes how each keyfob protocol supported by KAT works. Each docu
|
||||
| Subaru | `subaru.rs` | [subaru.md](subaru.md) |
|
||||
| VAG | `vag.rs` | [vag.md](vag.md) |
|
||||
| Fiat V0 | `fiat_v0.rs` | [fiat_v0.md](fiat_v0.md) |
|
||||
| Fiat V1 | `fiat_v1.rs` | [fiat_v1.md](fiat_v1.md) |
|
||||
| Mazda V0 | `mazda_v0.rs` | [mazda_v0.md](mazda_v0.md) |
|
||||
| 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) |
|
||||
| Scher-Khan | `scher_khan.rs` | [scher_khan.md](scher_khan.md) |
|
||||
| Star Line | `star_line.rs` | [star_line.md](star_line.md) |
|
||||
|
||||
@@ -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
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Fiat V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/fiat_v0.rs`
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Fiat V1 Protocol (Magneti Marelli BSI / PCF7946)
|
||||
|
||||
**Rust module:** `src/protocols/fiat_v1.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/fiat_v1.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Fiat V1 is the Magneti Marelli BSI keyfob protocol (PCF7946), found on Fiat Panda, Grande Punto, and possibly other Fiat/Lancia/Alfa ~2003-2012. Uses Manchester encoding with auto-detected timing from preamble pulse averaging. Two timing variants: Type A (~260 us, e.g. Panda) and Type B (~100 us, e.g. Grande Punto). This is a different protocol from Fiat V0.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Default | Notes |
|
||||
|-----------|---------|-------|
|
||||
| Short | 260 us | Auto-detected from preamble |
|
||||
| Long | 520 us | 2x detected TE |
|
||||
| Delta | 80 us | Minimum 30 us |
|
||||
| Preamble | 80+ pulses | 50-350 us range |
|
||||
| Min bits | 80 | Max 104 bits |
|
||||
|
||||
### Auto-Detection
|
||||
|
||||
Preamble pulses (50-350 us) are accumulated. After 80+ pulses, `te_detected = te_sum / te_count` becomes the reference TE. Type A/B boundary is at 180 us.
|
||||
|
||||
## Frame Layout (80-104 bits = 10-13 bytes)
|
||||
|
||||
| Bytes | Content |
|
||||
|-------|---------|
|
||||
| 0-1 | Preamble residue (0xFFFF/0xFFFC) |
|
||||
| 2-5 | Serial (32 bits) |
|
||||
| 6 | Button:4 \| Epoch:4 |
|
||||
| 7 | Counter:5 \| Scramble:2 \| Fixed:1 |
|
||||
| 8-12 | Encrypted payload (40 bits) |
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** -- Wait for HIGH pulse in 50-350 us range. Also detect retransmission gaps (>5000 us).
|
||||
2. **Preamble** -- Count pulses, accumulate for TE averaging. On gap >= te_detected * 4, transition to Sync.
|
||||
3. **Sync** -- Expect HIGH sync pulse of te_detected * 4 to te_detected * 12 duration.
|
||||
4. **Data** -- Manchester decode up to 104 bits into 13-byte raw buffer.
|
||||
5. **RetxSync** -- After gap >5000 us, look for sync pulse 400-2800 us (retransmission).
|
||||
|
||||
## Buttons
|
||||
|
||||
| Code | Name |
|
||||
|------|--------|
|
||||
| 0x7 | Lock |
|
||||
| 0xB | Unlock |
|
||||
| 0xD | Trunk |
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (decode-only).
|
||||
|
||||
## Frequencies
|
||||
|
||||
433.92 MHz.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Ford V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/ford_v0.rs`
|
||||
|
||||
+170
@@ -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 V0–V6, 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>© 2024 KAT — Keyfob Analysis Toolkit. Licensed under the terms in LICENSE file. <a href="https://github.com/KaraZajac/KAT" target="_blank">View on GitHub</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# KeeLoq Generic Fallback
|
||||
|
||||
**Rust module:** `src/protocols/keeloq_generic.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v0.rs`
|
||||
|
||||
+6
-2
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V1 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v1.rs`
|
||||
@@ -5,7 +9,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Kia V1 uses Manchester encoding at 800/1600 µs. 57 bits total: 32 serial + 8 button + 12 counter + 4 CRC. Long preamble (~90 long pairs). CRC4 with offset rules (cnt_high 0 vs ≥ 6).
|
||||
Kia V1 uses Manchester encoding at 800/1600 us. 57 bits total: 32 serial + 8 button + 12 counter + 4 CRC. Long preamble (~90 long pairs). CRC4 uses XOR of nibbles over 7 bytes (serial + button + cnt_low + cnt_high) with offset 1.
|
||||
|
||||
## Timing
|
||||
|
||||
@@ -24,7 +28,7 @@ Manchester: symbol duration short or long; bit value from transition direction.
|
||||
- 32 bits: serial
|
||||
- 8 bits: button
|
||||
- 12 bits: counter
|
||||
- 4 bits: CRC4 (checksum with offset rules)
|
||||
- 4 bits: CRC4 (XOR of nibbles, 7 bytes, offset 1)
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V2 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v2.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V3/V4 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v3_v4.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V5 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v5.rs`
|
||||
|
||||
+6
-2
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Kia V6 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/kia_v6.rs`
|
||||
@@ -26,7 +30,7 @@ Manchester (event mapping 0/2/4/6 for level convention). Three 64/64/16-bit segm
|
||||
- Part2: 64 bits (inverted)
|
||||
- Part3: 16 bits (inverted)
|
||||
|
||||
AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH). Serial/button/counter and CRC extracted after decryption.
|
||||
AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH). Serial/button/counter, CRC, and fx_field extracted after decryption. The fx_field is derived from the top 2 bytes of stored_part1_high and stored in `DecodedSignal.extra`.
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
@@ -36,7 +40,7 @@ AES-128 decrypt with key = f(keystore_a, keystore_b, XOR_MASK_LOW, XOR_MASK_HIGH
|
||||
|
||||
## Encoder
|
||||
|
||||
Decode-only in KAT (no encoder in reference).
|
||||
Supported. Ported from ProtoPirate (`ENABLE_EMULATE_FEATURE`). Builds plaintext (fx_field, serial, button, counter, S-box CRC, CRC8), AES-128 encrypts, packs into 3 parts, then Manchester encodes with two-pass preamble (640 pairs + data, gap, 38 pairs + data). Requires fx_field from decoded signal's `extra` field.
|
||||
|
||||
## Frequencies
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Mazda V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/mazda_v0.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/mazda_v0.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Mazda V0 uses a custom pair-based encoding (not standard Manchester). The `level` parameter is ignored; the decoder processes raw duration pairs. Preamble: 13+ short/short pairs followed by a short+long transition into data. Data bits are collected using a prev_state tracker with inverted polarity, then XOR-deobfuscated, checksum-validated, and parsed.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|--------|--------------|
|
||||
| Short | 250 us | +/-100 us |
|
||||
| Long | 500 us | +/-100 us |
|
||||
| Min bits | 64 | |
|
||||
| Completion| 80-105 bits | |
|
||||
|
||||
## Frame Layout (64 bits after deobfuscation)
|
||||
|
||||
Raw data collects into a 14-byte buffer. First byte is discarded (sync); bytes [1..9] form the 8-byte data frame.
|
||||
|
||||
After XOR deobfuscation:
|
||||
- Bytes 0-3: Serial (32 bits)
|
||||
- Byte 4: Button (8 bits)
|
||||
- Bytes 5-6: Counter (16 bits)
|
||||
- Byte 7: Checksum (additive sum of bytes 0-6)
|
||||
|
||||
## Pair-Based Bit Decoding
|
||||
|
||||
| Pair (te_last, duration) | Action |
|
||||
|--------------------------|--------|
|
||||
| Long + Short | Collect bit(0), bit(1), prev_state=1 |
|
||||
| Short + Long | Collect bit(1), prev_state=0 |
|
||||
| Short + Short | Collect bit(prev_state) |
|
||||
| Long + Long | Collect bit(0), bit(1), prev_state=0 |
|
||||
|
||||
Bit collection uses inverted polarity: state_bit=0 stores a 1.
|
||||
|
||||
## XOR Deobfuscation
|
||||
|
||||
1. Compute parity of data[7] (XOR fold to single bit).
|
||||
2. If parity is odd: XOR bytes [0..6] with data[6] as mask.
|
||||
3. If parity is even: XOR bytes [0..5] with data[5] as mask, also XOR data[6] with data[5].
|
||||
4. Bit interleave swap bytes 5 and 6: swap even/odd bit positions between them.
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** -- Wait for short pulse; save and go to PreambleCheck.
|
||||
2. **PreambleSave** -- Save duration, go to PreambleCheck.
|
||||
3. **PreambleCheck** -- Count short+short pairs; on short+long with count >= 13, start data.
|
||||
4. **DataSave** -- Save duration, go to DataCheck.
|
||||
5. **DataCheck** -- Process pair; if valid continue; if invalid, check completion (80-105 bits + checksum).
|
||||
|
||||
## Buttons
|
||||
|
||||
| Code | Name |
|
||||
|------|--------|
|
||||
| 0x10 | Lock |
|
||||
| 0x20 | Unlock |
|
||||
| 0x40 | Trunk |
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (decode-only).
|
||||
|
||||
## Frequencies
|
||||
|
||||
433.92 MHz.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Mitsubishi V0 Protocol
|
||||
|
||||
**Rust module:** `src/protocols/mitsubishi_v0.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/mitsubishi_v0.c`
|
||||
|
||||
## Overview
|
||||
|
||||
Mitsubishi V0 uses PWM encoding at 250/500 us. 96-bit frame (12 bytes). Level-aware: HIGH pulses are saved, LOW pulses complete the pair. After collection, data is unscrambled via bitwise NOT + counter-derived XOR mask.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|--------|--------------|
|
||||
| Short | 250 us | +/-100 us |
|
||||
| Long | 500 us | +/-100 us |
|
||||
| Min bits | 80 | |
|
||||
| Frame | 96 bits| 12 bytes |
|
||||
|
||||
## PWM Bit Encoding
|
||||
|
||||
| Pair (HIGH, LOW) | Bit |
|
||||
|------------------|-----|
|
||||
| Short HIGH + Long LOW | 1 |
|
||||
| Long HIGH + Short LOW | 0 |
|
||||
|
||||
Bits collected MSB-first into a 12-byte buffer.
|
||||
|
||||
## Frame Layout (96 bits)
|
||||
|
||||
After unscrambling:
|
||||
- Bytes 0-3: Serial (32 bits, big-endian)
|
||||
- Bytes 4-5: Counter (16 bits)
|
||||
- Byte 6: Button (8 bits)
|
||||
- Bytes 7-11: Remaining payload
|
||||
|
||||
## Unscramble Algorithm
|
||||
|
||||
1. Bitwise NOT the first 8 bytes.
|
||||
2. Extract counter from bytes [4..5]: `hi = (counter >> 8) & 0xFF`, `lo = counter & 0xFF`.
|
||||
3. Compute masks: `mask1 = (hi & 0xAA) | (lo & 0x55)`, `mask2 = (lo & 0xAA) | (hi & 0x55)`, `mask3 = mask1 ^ mask2`.
|
||||
4. XOR bytes [0..5] with mask3.
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **Reset** -- Wait for HIGH pulse; save duration.
|
||||
2. **DataSave** -- On HIGH: save duration. On LOW (unexpected): reset.
|
||||
3. **DataCheck** -- On LOW: decode pair. If 96 bits collected, unscramble and return. Otherwise continue.
|
||||
|
||||
## Encoder
|
||||
|
||||
Not supported (decode-only).
|
||||
|
||||
## Frequencies
|
||||
|
||||
868.35 MHz.
|
||||
@@ -0,0 +1,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
@@ -1,24 +1,33 @@
|
||||
# PSA (Peugeot/Citroën) Protocol
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# PSA (Peugeot/Citroen) Protocol
|
||||
|
||||
**Rust module:** `src/protocols/psa.rs`
|
||||
**Reference:** `REFERENCES/ProtoPirate/protocols/psa.c`
|
||||
|
||||
## Overview
|
||||
|
||||
PSA uses Manchester at 250/500 µs symbol (125/250 µs sub-symbol for preamble). 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation. TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds an XOR layer. Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule).
|
||||
PSA uses Manchester encoding with dual preamble pattern support. 128 bits total: key1 (64) + validation (16) + key2/rest (48). Modified TEA (XTEA-like) with dynamic key selection (`key[sum & 3]` and `key[(sum >> 11) & 3]`). Two decode modes: mode 0x23 (direct XOR decrypt with checksum validation) and mode 0x36 (TEA with BF1/BF2 key schedules). Brute-force decryption for mode 0x36 is deferred/partial.
|
||||
|
||||
## Timing
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|------------|--------|---------|
|
||||
| Symbol short | 250 µs | ±100 µs |
|
||||
| Symbol long | 500 µs | ±100 µs |
|
||||
| Preamble | 125/250 µs sub-symbols | |
|
||||
|-------------|--------|---------|
|
||||
| Symbol short | 250 us | +/-100 us |
|
||||
| Symbol long | 500 us | +/-100 us |
|
||||
| Preamble P1 | 250 us sub-symbols | Pattern 1 |
|
||||
| Preamble P2 | 125 us sub-symbols | Pattern 2 |
|
||||
| End marker | 1000 us (P1), 500 us (P2) | |
|
||||
| Min bits | 128 | |
|
||||
|
||||
## Encoding
|
||||
## Dual Preamble Patterns
|
||||
|
||||
Manchester; preamble uses 125/250 µs; then 250/500 µs symbols. TEA encrypt; mode 0x23 adds XOR.
|
||||
- **Pattern 1 (250 us):** Preamble uses 250 us sub-symbols; Manchester decode at 250/500 us. Threshold: 70+ transitions.
|
||||
- **Pattern 2 (125 us):** Preamble uses 125 us sub-symbols; Manchester decode at 125/250 us. Threshold: 69+ transitions.
|
||||
|
||||
Pattern type is auto-detected from the first preamble pulse duration.
|
||||
|
||||
## Frame Layout (128 bits)
|
||||
|
||||
@@ -26,18 +35,42 @@ Manchester; preamble uses 125/250 µs; then 250/500 µs symbols. TEA encrypt; mo
|
||||
- validation: 16 bits
|
||||
- key2/rest: 48 bits
|
||||
|
||||
TEA decrypt key1 (and validation); mode 0x23: XOR with BF1 key schedule; mode 0x36: TEA with BF2 key schedule. Serial/button/counter extracted from decrypted key1.
|
||||
## Encryption
|
||||
|
||||
### Modified TEA (XTEA-like)
|
||||
|
||||
Uses dynamic key word selection per round:
|
||||
- Encrypt: `k_idx1 = sum & 3`, then `sum += DELTA`, then `k_idx2 = (sum >> 11) & 3`
|
||||
- Decrypt: `k_idx2 = (sum >> 11) & 3`, then `sum -= DELTA`, then `k_idx1 = sum & 3`
|
||||
- Round function: `(key[k_idx] + sum) ^ (((v >> 5) ^ (v << 4)) + v)`
|
||||
|
||||
### Mode 0x23 (Direct XOR)
|
||||
|
||||
1. Setup byte buffer from key1/key2 (little-endian unpack).
|
||||
2. Calculate checksum over buffer[2..8] (nibble sum * 16).
|
||||
3. Validate: `(checksum ^ key2_high_byte) & 0xF0 == 0`.
|
||||
4. XOR decrypt using `psa_copy_reverse` byte reordering.
|
||||
5. Extract: serial (24-bit), counter (16-bit), button (4-bit), CRC.
|
||||
|
||||
### Mode 0x36 (TEA Brute-Force)
|
||||
|
||||
Direct TEA decrypt attempted with BF1 and BF2 key schedules. Full brute-force (16M iterations per schedule) is available in ProtoPirate but not fully ported to KAT.
|
||||
|
||||
Key schedules:
|
||||
- BF1: `[0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A64]`
|
||||
- BF2: `[0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04]`
|
||||
|
||||
## Decoder Steps
|
||||
|
||||
1. **WaitEdge** — Wait for edge/preamble.
|
||||
2. **CountPattern** — Count preamble pattern (125/250 µs).
|
||||
3. **DecodeManchester** — Manchester decode 128 bits; TEA decrypt; apply mode (0x23 XOR or 0x36); extract fields.
|
||||
4. **End** — End marker (e.g. 1000 µs); return decode.
|
||||
1. **WaitEdge (State0)** -- Detect preamble pattern type from first HIGH pulse (250 us -> Pattern 1, 125 us -> Pattern 2).
|
||||
2. **CountPattern250 (State1)** -- Count 250 us preamble pairs; on long pulse with count > 70, transition to Manchester decode.
|
||||
3. **DecodeManchester250 (State2)** -- Manchester decode at 250/500 us. End on 1000 us marker or 121+ bits.
|
||||
4. **CountPattern125 (State3)** -- Count 125 us preamble pairs; on 250 us pulse with count >= 69, transition to Manchester decode.
|
||||
5. **DecodeManchester125 (State4)** -- Manchester decode at 125/250 us. End on 500 us marker or 121+ bits.
|
||||
|
||||
## Encoder
|
||||
|
||||
Supported; preamble, Manchester 128 bits, TEA (+ XOR for 0x23).
|
||||
Supported (mode 0x23 only). XOR encrypt, then modified TEA encrypt with BF1 key schedule. Preamble: 70 cycles of 125/125 us + 250/250 us sync. Manchester encoded key1 (64 bits) + validation (16 bits). End marker: 1000 us LOW.
|
||||
|
||||
## Frequencies
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Scher-Khan Protocol
|
||||
|
||||
**Rust module:** `src/protocols/scher_khan.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Star Line Protocol
|
||||
|
||||
**Rust module:** `src/protocols/star_line.rs`
|
||||
|
||||
+351
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Subaru Protocol
|
||||
|
||||
**Rust module:** `src/protocols/subaru.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Suzuki Protocol
|
||||
|
||||
**Rust module:** `src/protocols/suzuki.rs`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# VAG (VW/Audi/Seat/Skoda) Protocol
|
||||
|
||||
**Rust module:** `src/protocols/vag.rs`
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 813 KiB |
+17
-26
@@ -1307,10 +1307,9 @@ impl App {
|
||||
.unwrap_or_else(|_| path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// For unknown protocol: Year_Make_Model_Region_Command (user can edit; we append _<8 hex> on save).
|
||||
/// For known protocol: protocol_serial as before.
|
||||
/// Default export filename for .fob: Year_Make_Model_Region_Command (same format for all captures).
|
||||
/// Uses capture metadata when set; fallbacks: make from protocol for known, command from button_name(), else "Unknown".
|
||||
fn default_export_filename(capture: &Capture) -> String {
|
||||
if capture.protocol_name().eq_ignore_ascii_case("unknown") {
|
||||
let year = capture
|
||||
.year
|
||||
.as_deref()
|
||||
@@ -1320,9 +1319,17 @@ impl App {
|
||||
let make = capture
|
||||
.make
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.trim().replace(' ', "_"))
|
||||
.unwrap_or_else(|| {
|
||||
if capture.protocol_name().eq_ignore_ascii_case("unknown") {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
Self::get_make_for_protocol(capture.protocol_name())
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
.replace(' ', "_")
|
||||
}
|
||||
});
|
||||
let model = capture
|
||||
.model
|
||||
.as_deref()
|
||||
@@ -1346,13 +1353,6 @@ impl App {
|
||||
cmd_str.replace(' ', "_")
|
||||
};
|
||||
format!("{}_{}_{}_{}_{}", year, make, model, region, command)
|
||||
} else {
|
||||
format!(
|
||||
"{}_{}",
|
||||
capture.protocol_name().replace(' ', "_").to_lowercase(),
|
||||
capture.serial_hex()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Start .fob export by entering filename input mode
|
||||
@@ -1362,21 +1362,17 @@ impl App {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Pre-fill filename from protocol + serial; for unknown captures include 8-hex suffix so user sees it
|
||||
// Pre-fill filename: Year_Make_Model_Region_Command_8HEX for all .fob exports
|
||||
let capture = self.captures.iter().find(|c| c.id == id);
|
||||
let default_name = capture
|
||||
.map(|c| Self::default_export_filename(c))
|
||||
.unwrap_or_else(|| format!("capture_{}", id));
|
||||
self.export_filename = if capture.map(|c| c.protocol.is_none()).unwrap_or(false) {
|
||||
let suffix_nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64;
|
||||
let suffix = (suffix_nanos.wrapping_add(id as u64 * 2654435761) % 0x100_000_000) as u32;
|
||||
format!("{}_{:08X}", default_name, suffix)
|
||||
} else {
|
||||
default_name.clone()
|
||||
};
|
||||
self.export_filename = format!("{}_{:08X}", default_name, suffix);
|
||||
|
||||
// Pre-fill metadata from capture if set, otherwise make from protocol
|
||||
let make = capture
|
||||
@@ -1449,18 +1445,15 @@ impl App {
|
||||
notes: self.fob_meta_notes.clone(),
|
||||
};
|
||||
|
||||
// Unknown captures or user-edited vehicle-style filenames always get 8-hex suffix to avoid overwrites.
|
||||
let use_hex_suffix = capture.protocol.is_none()
|
||||
|| self.export_filename.ends_with("_Unknown")
|
||||
|| self.export_filename == "Unknown";
|
||||
// All .fob exports use Year_Make_Model_Region_Command_8HEX; append 8-hex if user removed it
|
||||
let already_has_8hex = self.export_filename.len() >= 9
|
||||
&& self.export_filename.as_bytes()[self.export_filename.len() - 9] == b'_'
|
||||
&& self.export_filename[self.export_filename.len() - 8..]
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_hexdigit());
|
||||
let filename = if use_hex_suffix && already_has_8hex {
|
||||
let filename = if already_has_8hex {
|
||||
format!("{}.fob", self.export_filename.trim())
|
||||
} else if use_hex_suffix {
|
||||
} else {
|
||||
let base = self.export_filename.trim();
|
||||
let suffix_nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -1473,8 +1466,6 @@ impl App {
|
||||
} else {
|
||||
format!("{}_{}.fob", base, hex_suffix)
|
||||
}
|
||||
} else {
|
||||
format!("{}.fob", self.export_filename)
|
||||
};
|
||||
let path = export_dir.join(&filename);
|
||||
|
||||
|
||||
+39
-49
@@ -4,9 +4,9 @@
|
||||
//! `fiat_v0.h`. Preamble: count short pulses (HIGH or LOW, 200±100µs); when preamble_count >= 150
|
||||
//! (0x96), accept 800µs LOW gap (gap_threshold 800, te_delta 100) and enter Data. Data: 64 bits
|
||||
//! (serial=data_low, cnt=data_high) then 7 more bits; complete when bit_count > 0x46 with
|
||||
//! btn = (data_low << 1) | 1, 71 bits total. Encoder: differential Manchester, 150 preamble pairs,
|
||||
//! last LOW replaced by 800µs gap; 64 data bits then 6 btn bits (btn_to_send = btn >> 1); end
|
||||
//! marker te_short*8 LOW.
|
||||
//! btn = (uint8_t)data_low, 71 bits total. Encoder: standard Manchester, 150 preamble pairs,
|
||||
//! last LOW replaced by 800µs gap; 64 data bits then 7 endbyte bits (endbyte & 0x7F); end
|
||||
//! marker te_short*4 LOW.
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -294,8 +294,9 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
self.data_high = 0;
|
||||
}
|
||||
|
||||
if self.bit_count > 0x46 {
|
||||
self.btn = ((self.data_low << 1) | 1) as u8;
|
||||
if self.bit_count == 0x47 {
|
||||
// C: endbyte = (uint8_t)data_low (no transform)
|
||||
self.btn = self.data_low as u8;
|
||||
let result = self.parse_data();
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
@@ -304,6 +305,19 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Manchester reset event (gap path) — C extracts at exactly 71 bits
|
||||
if self.bit_count == 0x47 {
|
||||
self.btn = self.data_low as u8;
|
||||
let result = self.parse_data();
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
} else if self.bit_count < 0x40 {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
self.te_last = duration;
|
||||
}
|
||||
@@ -319,80 +333,56 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let cnt = decoded.counter.unwrap_or(0) as u32;
|
||||
let btn = decoded.button.unwrap_or(0).max(button);
|
||||
let endbyte = if button != 0 { button } else { decoded.button.unwrap_or(0) };
|
||||
|
||||
// Ref: data = (cnt<<32)|serial; btn_to_send = btn >> 1 (reverse decoder's (x<<1)|1)
|
||||
// C: data = (hop << 32) | fix; endbyte sent as-is (7 bits, & 0x7F)
|
||||
let data = ((cnt as u64) << 32) | (serial as u64);
|
||||
let btn_to_send = btn >> 1;
|
||||
let te_short = TE_SHORT;
|
||||
|
||||
let mut signal = Vec::with_capacity(1024);
|
||||
let te_short = TE_SHORT;
|
||||
let te_long = TE_LONG;
|
||||
|
||||
for burst in 0..TOTAL_BURSTS {
|
||||
if burst > 0 {
|
||||
signal.push(LevelDuration::new(false, INTER_BURST_GAP));
|
||||
}
|
||||
|
||||
// Preamble: HIGH-LOW pairs; last LOW replaced by gap (ref)
|
||||
for i in 0..PREAMBLE_PAIRS {
|
||||
// Preamble: alternating short pulses; last LOW extended to gap
|
||||
for _ in 0..PREAMBLE_PAIRS {
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
signal.push(LevelDuration::new(
|
||||
false,
|
||||
if i == PREAMBLE_PAIRS - 1 { GAP_US } else { te_short },
|
||||
));
|
||||
signal.push(LevelDuration::new(false, 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
|
||||
let first_bit = (data >> 63) & 1 == 1;
|
||||
if first_bit {
|
||||
signal.push(LevelDuration::new(true, te_long));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
signal.push(LevelDuration::new(false, te_long));
|
||||
}
|
||||
|
||||
let mut prev_bit = first_bit;
|
||||
|
||||
// Remaining 63 data bits
|
||||
for bit in (0..63).rev() {
|
||||
// Standard Manchester encode 64 bits of data (matches C)
|
||||
for bit in (0..64).rev() {
|
||||
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(false, te_short));
|
||||
} else if !prev_bit && curr_bit {
|
||||
signal.push(LevelDuration::new(true, te_long));
|
||||
} else if prev_bit && !curr_bit {
|
||||
signal.push(LevelDuration::new(false, te_long));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
}
|
||||
prev_bit = curr_bit;
|
||||
}
|
||||
|
||||
// 6 btn bits (ref: for bit 5 down to 0)
|
||||
for bit in (0..6).rev() {
|
||||
let curr_bit = (btn_to_send >> bit) & 1 == 1;
|
||||
if !prev_bit && !curr_bit {
|
||||
// Standard Manchester encode 7 bits of endbyte (matches C: endbyte & 0x7F, bits 6..0)
|
||||
let endbyte_masked = endbyte & 0x7F;
|
||||
for bit in (0..7).rev() {
|
||||
let curr_bit = (endbyte_masked >> bit) & 1 == 1;
|
||||
if curr_bit {
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
} else if !prev_bit && curr_bit {
|
||||
signal.push(LevelDuration::new(true, te_long));
|
||||
} else if prev_bit && !curr_bit {
|
||||
signal.push(LevelDuration::new(false, te_long));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
signal.push(LevelDuration::new(true, te_short));
|
||||
}
|
||||
prev_bit = curr_bit;
|
||||
}
|
||||
|
||||
// End marker (ref)
|
||||
if prev_bit {
|
||||
signal.push(LevelDuration::new(false, te_short));
|
||||
}
|
||||
signal.push(LevelDuration::new(false, te_short * 8));
|
||||
// End marker: te_short * 4 LOW (matches C)
|
||||
signal.push(LevelDuration::new(false, te_short * 4));
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
//! Fiat V1 protocol decoder (Magneti Marelli BSI / PCF7946)
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/fiat_v1.c` and
|
||||
//! `fiat_v1.h`. Found on Fiat Panda, Grande Punto, and possibly other Fiat/Lancia/Alfa ~2003-2012.
|
||||
//!
|
||||
//! RF: 433.92 MHz, Manchester encoding.
|
||||
//! Two timing variants with identical frame structure:
|
||||
//! Type A (e.g. Panda): te_short ~260us, te_long ~520us
|
||||
//! Type B (e.g. Grande Punto): te_short ~100us, te_long ~200us
|
||||
//! TE is auto-detected from preamble pulse averaging (boundary at 180us).
|
||||
//!
|
||||
//! Frame layout (103-104 bits = 13 bytes):
|
||||
//! Bytes 0-1: 0xFFFF/0xFFFC preamble residue
|
||||
//! Bytes 2-5: Serial (32 bits)
|
||||
//! Byte 6: [Button:4 | Epoch:4]
|
||||
//! Byte 7: [Counter:5 | Scramble:2 | Fixed:1]
|
||||
//! Bytes 8-12: Encrypted payload (40 bits)
|
||||
//!
|
||||
//! State machine: Reset -> Preamble -> Sync -> Data (-> RetxSync -> Data)
|
||||
//! No encoder (decode-only).
|
||||
//!
|
||||
//! Original C implementation by @lupettohf
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
// Default timing constants (Type A)
|
||||
const TE_SHORT: u32 = 260;
|
||||
#[allow(dead_code)]
|
||||
const TE_LONG: u32 = 520;
|
||||
#[allow(dead_code)]
|
||||
const TE_DELTA: u32 = 80;
|
||||
#[allow(dead_code)]
|
||||
const MIN_COUNT_BIT: usize = 80;
|
||||
|
||||
// Preamble / sync / data constants (from C defines)
|
||||
const PREAMBLE_PULSE_MIN: u32 = 50;
|
||||
const PREAMBLE_PULSE_MAX: u32 = 350;
|
||||
const PREAMBLE_MIN: u16 = 80;
|
||||
const MAX_DATA_BITS: u8 = 104;
|
||||
const MIN_DATA_BITS: u8 = 80;
|
||||
const GAP_TE_MULT: u32 = 4;
|
||||
const SYNC_TE_MIN_MULT: u32 = 4;
|
||||
const SYNC_TE_MAX_MULT: u32 = 12;
|
||||
const RETX_GAP_MIN: u32 = 5000;
|
||||
const RETX_SYNC_MIN: u32 = 400;
|
||||
const RETX_SYNC_MAX: u32 = 2800;
|
||||
#[allow(dead_code)]
|
||||
const TE_TYPE_AB_BOUNDARY: u32 = 180;
|
||||
|
||||
/// Manchester state machine (Flipper-style, same as fiat_v0.rs / common.rs).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0 = 0,
|
||||
Mid1 = 1,
|
||||
Start0 = 2,
|
||||
Start1 = 3,
|
||||
}
|
||||
|
||||
/// Decoder state machine steps.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble,
|
||||
Sync,
|
||||
Data,
|
||||
RetxSync,
|
||||
}
|
||||
|
||||
pub struct FiatV1Decoder {
|
||||
step: DecoderStep,
|
||||
manchester_state: ManchesterState,
|
||||
|
||||
// Preamble tracking
|
||||
preamble_count: u16,
|
||||
|
||||
// Auto-detected TE from preamble averaging
|
||||
te_sum: u32,
|
||||
te_count: u16,
|
||||
te_detected: u32,
|
||||
|
||||
// Data accumulation
|
||||
raw_data: [u8; 13],
|
||||
bit_count: u8,
|
||||
data: u64, // first 64 bits
|
||||
extra_data: u32, // bits 65+
|
||||
|
||||
// Parsed fields
|
||||
serial: u32,
|
||||
btn: u8,
|
||||
cnt: u8,
|
||||
|
||||
te_last: u32,
|
||||
}
|
||||
|
||||
impl FiatV1Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
preamble_count: 0,
|
||||
te_sum: 0,
|
||||
te_count: 0,
|
||||
te_detected: 0,
|
||||
raw_data: [0u8; 13],
|
||||
bit_count: 0,
|
||||
data: 0,
|
||||
extra_data: 0,
|
||||
serial: 0,
|
||||
btn: 0,
|
||||
cnt: 0,
|
||||
te_last: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the Manchester state machine by one event.
|
||||
/// Event encoding: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh.
|
||||
/// Returns `Some(bit)` when a data bit is emitted, `None` otherwise.
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||
let (new_state, emit) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
|
||||
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
|
||||
|
||||
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
|
||||
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
|
||||
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
_ => (ManchesterState::Mid1, false),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
if emit {
|
||||
Some((event & 1) == 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset Manchester state machine (to Mid1, matching C reset).
|
||||
fn manchester_reset(&mut self) {
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
/// Prepare for data reception: clear accumulators, reset Manchester, enter Data step.
|
||||
/// Matches `fiat_marelli_prepare_data` in the C reference.
|
||||
fn prepare_data(&mut self) {
|
||||
self.bit_count = 0;
|
||||
self.extra_data = 0;
|
||||
self.data = 0;
|
||||
self.raw_data = [0u8; 13];
|
||||
self.manchester_reset();
|
||||
self.step = DecoderStep::Data;
|
||||
}
|
||||
|
||||
/// Get the effective te_short (auto-detected or default).
|
||||
fn te_short(&self) -> u32 {
|
||||
if self.te_detected != 0 {
|
||||
self.te_detected
|
||||
} else {
|
||||
TE_SHORT
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the effective te_long (2 * te_short).
|
||||
fn te_long(&self) -> u32 {
|
||||
self.te_short() * 2
|
||||
}
|
||||
|
||||
/// Get the effective te_delta (te_short / 2, minimum 30).
|
||||
fn te_delta(&self) -> u32 {
|
||||
let d = self.te_short() / 2;
|
||||
if d < 30 { 30 } else { d }
|
||||
}
|
||||
|
||||
/// Map button code to name (matches C fiat_marelli_button_name).
|
||||
#[allow(dead_code)]
|
||||
fn button_name(btn: u8) -> &'static str {
|
||||
match btn {
|
||||
0x7 => "Lock",
|
||||
0xB => "Unlock",
|
||||
0xD => "Trunk",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a DecodedSignal from the current state.
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
// Pack extra bytes (bits 65+) into extra field
|
||||
let extra = if self.bit_count > 64 {
|
||||
Some(self.extra_data as u64)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(self.serial),
|
||||
button: Some(self.btn),
|
||||
counter: Some(self.cnt as u16),
|
||||
crc_valid: true, // no CRC defined for this protocol
|
||||
data: self.data,
|
||||
data_count_bit: self.bit_count as usize,
|
||||
encoder_capable: false,
|
||||
extra,
|
||||
protocol_display_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for FiatV1Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Fiat V1"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.preamble_count = 0;
|
||||
self.bit_count = 0;
|
||||
self.extra_data = 0;
|
||||
self.te_last = 0;
|
||||
self.te_sum = 0;
|
||||
self.te_count = 0;
|
||||
self.te_detected = 0;
|
||||
self.data = 0;
|
||||
self.raw_data = [0u8; 13];
|
||||
self.serial = 0;
|
||||
self.btn = 0;
|
||||
self.cnt = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
let te_short = self.te_short();
|
||||
let te_long = self.te_long();
|
||||
let te_delta = self.te_delta();
|
||||
|
||||
match self.step {
|
||||
// =========================================================
|
||||
// Reset: wait for a HIGH pulse in preamble range, or detect
|
||||
// retransmission gap (LOW > 5000us with valid te_detected).
|
||||
// =========================================================
|
||||
DecoderStep::Reset => {
|
||||
if level {
|
||||
if duration >= PREAMBLE_PULSE_MIN && duration <= PREAMBLE_PULSE_MAX {
|
||||
self.step = DecoderStep::Preamble;
|
||||
self.preamble_count = 1;
|
||||
self.te_sum = duration;
|
||||
self.te_count = 1;
|
||||
self.te_last = duration;
|
||||
}
|
||||
} else if duration > RETX_GAP_MIN && self.te_detected != 0 {
|
||||
self.step = DecoderStep::RetxSync;
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Preamble: accumulate pulses in 50-350us range for TE
|
||||
// averaging. When count >= 80 and we see a LOW gap >= 4*TE,
|
||||
// transition to Sync.
|
||||
// =========================================================
|
||||
DecoderStep::Preamble => {
|
||||
if duration >= PREAMBLE_PULSE_MIN && duration <= PREAMBLE_PULSE_MAX {
|
||||
// Pulse in valid preamble range (either HIGH or LOW)
|
||||
self.preamble_count += 1;
|
||||
self.te_sum += duration;
|
||||
self.te_count += 1;
|
||||
self.te_last = duration;
|
||||
} else if !level {
|
||||
// LOW pulse outside preamble range -- check if it's the gap after preamble
|
||||
if self.preamble_count >= PREAMBLE_MIN && self.te_count > 0 {
|
||||
self.te_detected = self.te_sum / (self.te_count as u32);
|
||||
let gap_threshold = self.te_detected * GAP_TE_MULT;
|
||||
|
||||
if duration > gap_threshold {
|
||||
self.step = DecoderStep::Sync;
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
// HIGH pulse outside preamble range
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Sync: expect HIGH sync pulse of te_detected*4 to
|
||||
// te_detected*12 duration. On match, prepare data decoder.
|
||||
// =========================================================
|
||||
DecoderStep::Sync => {
|
||||
let sync_min = self.te_detected * SYNC_TE_MIN_MULT;
|
||||
let sync_max = self.te_detected * SYNC_TE_MAX_MULT;
|
||||
|
||||
if level && duration >= sync_min && duration <= sync_max {
|
||||
self.prepare_data();
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// RetxSync: after retransmission gap (>5000us LOW), look for
|
||||
// HIGH sync pulse in 400-2800us range.
|
||||
// =========================================================
|
||||
DecoderStep::RetxSync => {
|
||||
if level && duration >= RETX_SYNC_MIN && duration <= RETX_SYNC_MAX {
|
||||
self.prepare_data();
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Data: Manchester decode up to 104 bits. Complete frame on
|
||||
// 104 bits reached or gap with >= 80 bits collected.
|
||||
// =========================================================
|
||||
DecoderStep::Data => {
|
||||
let mut event: u8 = 4; // 4 = ManchesterEventReset (invalid)
|
||||
let mut frame_complete = false;
|
||||
|
||||
// Check for short pulse match
|
||||
let diff_short = duration_diff!(duration, te_short);
|
||||
if diff_short < te_delta {
|
||||
// Flipper convention: level=HIGH -> ShortLow(0), level=LOW -> ShortHigh(1)
|
||||
event = if level { 0 } else { 1 };
|
||||
} else {
|
||||
// Check for long pulse match
|
||||
let diff_long = duration_diff!(duration, te_long);
|
||||
if diff_long < te_delta {
|
||||
event = if level { 2 } else { 3 };
|
||||
}
|
||||
}
|
||||
|
||||
if event != 4 {
|
||||
// Valid Manchester event
|
||||
if let Some(data_bit) = self.manchester_advance(event) {
|
||||
let new_bit: u32 = if data_bit { 1 } else { 0 };
|
||||
|
||||
// Store bit into raw_data byte array (MSB-first)
|
||||
if self.bit_count < MAX_DATA_BITS {
|
||||
let byte_idx = (self.bit_count / 8) as usize;
|
||||
let bit_pos = 7 - (self.bit_count % 8);
|
||||
if new_bit != 0 {
|
||||
self.raw_data[byte_idx] |= 1u8 << bit_pos;
|
||||
}
|
||||
}
|
||||
|
||||
// Also accumulate into u64 data / u32 extra_data
|
||||
if self.bit_count < 64 {
|
||||
self.data = (self.data << 1) | (new_bit as u64);
|
||||
} else {
|
||||
self.extra_data = (self.extra_data << 1) | new_bit;
|
||||
}
|
||||
|
||||
self.bit_count += 1;
|
||||
if self.bit_count >= MAX_DATA_BITS {
|
||||
frame_complete = true;
|
||||
}
|
||||
}
|
||||
} else if self.bit_count >= MIN_DATA_BITS {
|
||||
// Gap or invalid pulse but we have enough bits
|
||||
frame_complete = true;
|
||||
} else {
|
||||
// Not enough bits and invalid pulse -- abort
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
|
||||
if frame_complete {
|
||||
// Parse serial, button, counter from raw_data
|
||||
self.serial = ((self.raw_data[2] as u32) << 24)
|
||||
| ((self.raw_data[3] as u32) << 16)
|
||||
| ((self.raw_data[4] as u32) << 8)
|
||||
| (self.raw_data[5] as u32);
|
||||
self.btn = (self.raw_data[6] >> 4) & 0x0F;
|
||||
self.cnt = (self.raw_data[7] >> 3) & 0x1F;
|
||||
|
||||
let result = self.parse_data();
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = duration;
|
||||
return Some(result);
|
||||
}
|
||||
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FiatV1Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
+23
-10
@@ -225,10 +225,26 @@ impl FordV0Decoder {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// BS calculation
|
||||
// Checksum / BS calculation
|
||||
// =========================================================================
|
||||
|
||||
/// Calculate BS = (count_low_byte + bs_magic + (button << 4)) with overflow handling
|
||||
/// Calculate checksum from serial, count, and button (matches psa.c ford_v0_calculate_checksum).
|
||||
/// Sums all bytes of serial + all bytes of count + (button << 4), truncated to u8.
|
||||
fn calculate_checksum(serial: u32, count: u32, button: u8) -> u8 {
|
||||
let sum: u32 = ((count >> 24) & 0xFF)
|
||||
.wrapping_add((count >> 16) & 0xFF)
|
||||
.wrapping_add((count >> 8) & 0xFF)
|
||||
.wrapping_add(count & 0xFF)
|
||||
.wrapping_add((serial >> 24) & 0xFF)
|
||||
.wrapping_add((serial >> 16) & 0xFF)
|
||||
.wrapping_add((serial >> 8) & 0xFF)
|
||||
.wrapping_add(serial & 0xFF)
|
||||
.wrapping_add((button as u32) << 4);
|
||||
(sum & 0xFF) as u8
|
||||
}
|
||||
|
||||
/// Legacy BS calculation (kept for reference; encoder now uses calculate_checksum)
|
||||
#[allow(dead_code)]
|
||||
fn calculate_bs(count: u32, button: u8, bs_magic: u8) -> u8 {
|
||||
let result: u16 = (count as u16 & 0xFF)
|
||||
.wrapping_add(bs_magic as u16)
|
||||
@@ -651,21 +667,18 @@ impl ProtocolDecoder for FordV0Decoder {
|
||||
decoded.counter.unwrap_or(0) as u32
|
||||
}) & 0xFFFFF; // 20-bit
|
||||
|
||||
// Use stored bs_magic (or default to 0x6F for backward compatibility)
|
||||
let bs_magic = if self.bs_magic != 0 { self.bs_magic } else { 0x6F };
|
||||
|
||||
// Calculate BS from count + button + bs_magic (matches C ford_v0_calculate_bs)
|
||||
let bs = Self::calculate_bs(count, button, bs_magic);
|
||||
// Calculate checksum from serial + count + button (matches ford_v0_calculate_checksum in C)
|
||||
let checksum = Self::calculate_checksum(serial, count, button);
|
||||
|
||||
// Extract header byte from the original key1 (first byte)
|
||||
let header_byte = (decoded.data >> 56) as u8;
|
||||
|
||||
// Encode key1 from fields (same count, new button)
|
||||
let new_key1 = Self::encode_ford_v0(header_byte, serial, button, count, bs);
|
||||
let new_key1 = Self::encode_ford_v0(header_byte, serial, button, count, checksum);
|
||||
|
||||
// Calculate CRC for key2
|
||||
let crc = Self::calculate_crc_for_tx(new_key1, bs);
|
||||
let new_key2 = ((bs as u16) << 8) | (crc as u16);
|
||||
let crc = Self::calculate_crc_for_tx(new_key1, checksum);
|
||||
let new_key2 = ((checksum as u16) << 8) | (crc as u16);
|
||||
|
||||
// Build one 6-burst block and repeat TX_REPEAT times (matches reference encoder.repeat = 10)
|
||||
let single = Self::build_upload(new_key1, new_key2);
|
||||
|
||||
+17
-27
@@ -7,7 +7,7 @@
|
||||
//! - Manchester encoding: 800/1600µs timing
|
||||
//! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC)
|
||||
//! - Long preamble of ~90 long pairs
|
||||
//! - CRC4 checksum with offset rules (cnt_high 0 / >= 6)
|
||||
//! - CRC4 checksum (XOR of nibbles + offset 1, 7 bytes including cnt_high)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
@@ -66,13 +66,14 @@ impl KiaV1Decoder {
|
||||
(crc.wrapping_add(offset)) & 0x0F
|
||||
}
|
||||
|
||||
/// Manchester state machine
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, false) => 0, // Short Low
|
||||
(true, true) => 1, // Short High
|
||||
(false, false) => 2, // Long Low
|
||||
(false, true) => 3, // Long High
|
||||
/// Manchester state machine (Flipper convention: level ? ShortLow : ShortHigh)
|
||||
fn manchester_advance(&mut self, is_short: bool, level: bool) -> Option<bool> {
|
||||
// C: event = level ? ManchesterEventShortLow : ManchesterEventShortHigh (inverted)
|
||||
let event = match (is_short, level) {
|
||||
(true, true) => 0, // level=true → ShortLow
|
||||
(true, false) => 1, // level=false → ShortHigh
|
||||
(false, true) => 2, // level=true → LongLow
|
||||
(false, false) => 3, // level=false → LongHigh
|
||||
};
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
@@ -115,15 +116,9 @@ impl KiaV1Decoder {
|
||||
char_data[4] = button;
|
||||
char_data[5] = (counter & 0xFF) as u8;
|
||||
|
||||
let crc = if cnt_high == 0 {
|
||||
let offset = if counter >= 0x098 { button } else { 1 };
|
||||
Self::crc4(&char_data[..6], offset)
|
||||
} else if cnt_high >= 0x6 {
|
||||
// CRC4: always 7 bytes with offset 1 (matches updated ProtoPirate kia_v1.c)
|
||||
char_data[6] = cnt_high as u8;
|
||||
Self::crc4(&char_data, 1)
|
||||
} else {
|
||||
Self::crc4(&char_data[..6], 1)
|
||||
};
|
||||
let crc = Self::crc4(&char_data, 1);
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
@@ -194,8 +189,9 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
|
||||
if self.header_count > 70 {
|
||||
if !level && is_short && duration_diff!(self.te_last, TE_LONG) < TE_DELTA {
|
||||
self.decode_count_bit = 1;
|
||||
self.decode_data = 1; // Add first bit
|
||||
// C: decode_count_bit=1, then add_bit(1) increments to 2
|
||||
self.decode_count_bit = 2;
|
||||
self.decode_data = 1;
|
||||
self.header_count = 0;
|
||||
self.step = DecoderStep::DecodeData;
|
||||
}
|
||||
@@ -218,7 +214,7 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.decode_count_bit >= MIN_COUNT_BIT {
|
||||
if self.decode_count_bit == MIN_COUNT_BIT {
|
||||
let result = self.parse_data();
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
@@ -247,15 +243,9 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
char_data[4] = button;
|
||||
char_data[5] = (counter & 0xFF) as u8;
|
||||
|
||||
let crc = if cnt_high == 0 {
|
||||
let offset = if counter >= 0x098 { button } else { 1 };
|
||||
Self::crc4(&char_data[..6], offset)
|
||||
} else if cnt_high >= 0x6 {
|
||||
// CRC4: always 7 bytes with offset 1 (matches updated ProtoPirate kia_v1.c)
|
||||
char_data[6] = cnt_high;
|
||||
Self::crc4(&char_data, 1)
|
||||
} else {
|
||||
Self::crc4(&char_data[..6], 1)
|
||||
};
|
||||
let crc = Self::crc4(&char_data, 1);
|
||||
|
||||
// Build data
|
||||
let data: u64 = ((serial as u64) << 24) |
|
||||
|
||||
+16
-15
@@ -57,18 +57,17 @@ impl KiaV2Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC4 for Kia V2 (matches kia_v2.c: 6-byte permuted input, XOR nibbles, offset 1)
|
||||
/// CRC4 for Kia V2 (matches kia_v2.c: strip CRC nibble, XOR all remaining nibbles, + offset 1)
|
||||
fn calculate_crc(data: u64) -> u8 {
|
||||
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
||||
let u_var4 = (data & 0xFFFFFFFF) as u32;
|
||||
|
||||
// C code: data_without_crc = data >> 4; then read 6 sequential bytes (bits 4..51)
|
||||
let data_without_crc = data >> 4;
|
||||
let mut bytes = [0u8; 6];
|
||||
bytes[0] = (u_var4 >> 20) as u8;
|
||||
bytes[1] = ((u_var4 >> 28) | ((serial & 0x0F) << 4)) as u8;
|
||||
bytes[2] = (serial >> 4) as u8;
|
||||
bytes[3] = (serial >> 12) as u8;
|
||||
bytes[4] = (u_var4 >> 4) as u8;
|
||||
bytes[5] = (u_var4 >> 12) as u8;
|
||||
bytes[0] = (data_without_crc & 0xFF) as u8;
|
||||
bytes[1] = ((data_without_crc >> 8) & 0xFF) as u8;
|
||||
bytes[2] = ((data_without_crc >> 16) & 0xFF) as u8;
|
||||
bytes[3] = ((data_without_crc >> 24) & 0xFF) as u8;
|
||||
bytes[4] = ((data_without_crc >> 32) & 0xFF) as u8;
|
||||
bytes[5] = ((data_without_crc >> 40) & 0xFF) as u8;
|
||||
|
||||
let mut crc: u8 = 0;
|
||||
for &byte in &bytes {
|
||||
@@ -181,19 +180,21 @@ impl ProtocolDecoder for KiaV2Decoder {
|
||||
self.te_last = duration;
|
||||
self.header_count += 1;
|
||||
} else if is_short && self.header_count >= 100 {
|
||||
// C code: decode_count_bit=1, then add_bit(1) which shifts and increments to 2
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 1;
|
||||
self.step = DecoderStep::CollectRawBits;
|
||||
self.decode_data = 1; // First bit
|
||||
self.decode_count_bit = 2;
|
||||
self.step = DecoderStep::CollectRawBits;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = duration; // C stays in CheckPreamble, updates te_last
|
||||
}
|
||||
} else {
|
||||
if is_long {
|
||||
self.header_count += 1;
|
||||
self.te_last = duration;
|
||||
} else if !is_short {
|
||||
} else if is_short {
|
||||
self.te_last = duration; // C stays in CheckPreamble for short LOW
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
+197
-7
@@ -1,8 +1,8 @@
|
||||
//! Kia V6 protocol decoder (decode-only)
|
||||
//! Kia V6 protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v6.c`.
|
||||
//! Decode logic (Manchester level mapping, 3-part 144-bit frame, AES-128, CRC8, keystore XOR) matches reference.
|
||||
//! No encoder in protopirate.
|
||||
//! Encoder ported from protopirate (ENABLE_EMULATE_FEATURE): AES-128 encrypt, two-pass Manchester.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance)
|
||||
@@ -275,6 +275,181 @@ impl KiaV6Decoder {
|
||||
*data = state;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Forward AES functions for encoder (matches kia_v6.c ENABLE_EMULATE_FEATURE)
|
||||
// =========================================================================
|
||||
|
||||
/// AES forward SubBytes
|
||||
fn aes_subbytes(state: &mut [u8; 16]) {
|
||||
for i in 0..16 {
|
||||
state[i] = AES_SBOX[state[i] as usize];
|
||||
}
|
||||
}
|
||||
|
||||
/// AES forward ShiftRows
|
||||
fn aes_shiftrows(state: &mut [u8; 16]) {
|
||||
let temp = state[1];
|
||||
state[1] = state[5];
|
||||
state[5] = state[9];
|
||||
state[9] = state[13];
|
||||
state[13] = temp;
|
||||
|
||||
let temp = state[2];
|
||||
state[2] = state[10];
|
||||
state[10] = temp;
|
||||
let temp = state[6];
|
||||
state[6] = state[14];
|
||||
state[14] = temp;
|
||||
|
||||
let temp = state[3];
|
||||
state[3] = state[15];
|
||||
state[15] = state[11];
|
||||
state[11] = state[7];
|
||||
state[7] = temp;
|
||||
}
|
||||
|
||||
/// AES forward MixColumns
|
||||
fn aes_mixcolumns(state: &mut [u8; 16]) {
|
||||
for i in 0..4 {
|
||||
let a = state[i * 4];
|
||||
let b = state[i * 4 + 1];
|
||||
let c = state[i * 4 + 2];
|
||||
let d = state[i * 4 + 3];
|
||||
state[i * 4] = Self::gf_mul2(a) ^ Self::gf_mul2(b) ^ b ^ c ^ d;
|
||||
state[i * 4 + 1] = a ^ Self::gf_mul2(b) ^ Self::gf_mul2(c) ^ c ^ d;
|
||||
state[i * 4 + 2] = a ^ b ^ Self::gf_mul2(c) ^ Self::gf_mul2(d) ^ d;
|
||||
state[i * 4 + 3] = Self::gf_mul2(a) ^ a ^ b ^ c ^ Self::gf_mul2(d);
|
||||
}
|
||||
}
|
||||
|
||||
/// AES-128 encrypt
|
||||
fn aes128_encrypt(expanded_key: &[u8; 176], data: &mut [u8; 16]) {
|
||||
let mut state = *data;
|
||||
|
||||
Self::aes_addroundkey(&mut state, &expanded_key[0..16]);
|
||||
|
||||
for round in 1..10 {
|
||||
Self::aes_subbytes(&mut state);
|
||||
Self::aes_shiftrows(&mut state);
|
||||
Self::aes_mixcolumns(&mut state);
|
||||
Self::aes_addroundkey(&mut state, &expanded_key[round * 16..(round + 1) * 16]);
|
||||
}
|
||||
|
||||
Self::aes_subbytes(&mut state);
|
||||
Self::aes_shiftrows(&mut state);
|
||||
Self::aes_addroundkey(&mut state, &expanded_key[160..176]);
|
||||
|
||||
*data = state;
|
||||
}
|
||||
|
||||
/// Encrypt payload for transmission (matches kia_v6.c kia_v6_encrypt_payload)
|
||||
fn encrypt_payload(
|
||||
fx_field: u8,
|
||||
serial: u32,
|
||||
button: u8,
|
||||
counter: u32,
|
||||
) -> (u32, u32, u32, u32, u16) {
|
||||
let mut plain = [0u8; 16];
|
||||
plain[0] = fx_field;
|
||||
plain[4] = ((serial >> 16) & 0xFF) as u8;
|
||||
plain[5] = ((serial >> 8) & 0xFF) as u8;
|
||||
plain[6] = (serial & 0xFF) as u8;
|
||||
plain[7] = button & 0x0F;
|
||||
plain[8] = ((counter >> 24) & 0xFF) as u8;
|
||||
plain[9] = ((counter >> 16) & 0xFF) as u8;
|
||||
plain[10] = ((counter >> 8) & 0xFF) as u8;
|
||||
plain[11] = (counter & 0xFF) as u8;
|
||||
plain[12] = AES_SBOX[(counter & 0xFF) as usize];
|
||||
plain[15] = Self::crc8(&plain[..15], 0xFF, 0x07);
|
||||
|
||||
let aes_key = Self::get_aes_key();
|
||||
let expanded_key = Self::aes_key_expansion(&aes_key);
|
||||
Self::aes128_encrypt(&expanded_key, &mut plain);
|
||||
|
||||
let fx_hi = 0x20 | (fx_field >> 4);
|
||||
let fx_lo = fx_field & 0x0F;
|
||||
let part1_high = ((fx_hi as u32) << 24)
|
||||
| ((fx_lo as u32) << 16)
|
||||
| ((plain[0] as u32) << 8)
|
||||
| (plain[1] as u32);
|
||||
let part1_low = ((plain[2] as u32) << 24)
|
||||
| ((plain[3] as u32) << 16)
|
||||
| ((plain[4] as u32) << 8)
|
||||
| (plain[5] as u32);
|
||||
let part2_high = ((plain[6] as u32) << 24)
|
||||
| ((plain[7] as u32) << 16)
|
||||
| ((plain[8] as u32) << 8)
|
||||
| (plain[9] as u32);
|
||||
let part2_low = ((plain[10] as u32) << 24)
|
||||
| ((plain[11] as u32) << 16)
|
||||
| ((plain[12] as u32) << 8)
|
||||
| (plain[13] as u32);
|
||||
let part3 = ((plain[14] as u16) << 8) | (plain[15] as u16);
|
||||
|
||||
(part1_low, part1_high, part2_low, part2_high, part3)
|
||||
}
|
||||
|
||||
/// Build encoder signal: two-pass Manchester with preambles (matches kia_v6.c)
|
||||
fn build_upload(
|
||||
p1_lo: u32, p1_hi: u32,
|
||||
p2_lo: u32, p2_hi: u32,
|
||||
p3: u16,
|
||||
) -> Vec<LevelDuration> {
|
||||
let mut signal = Vec::with_capacity(2000);
|
||||
|
||||
// Two passes: 640 preamble pairs, then 38 preamble pairs
|
||||
for &preamble_pairs in &[640u32, 38u32] {
|
||||
// Preamble: short/short pairs
|
||||
for _ in 0..preamble_pairs {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// Sync: short LOW, long HIGH, short LOW
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
|
||||
// Part1: bits 60 down to 0 (61 bits), inverted
|
||||
for b in (0..=60).rev() {
|
||||
let word = if b >= 32 { p1_hi } else { p1_lo };
|
||||
let shift = if b >= 32 { b - 32 } else { b };
|
||||
let bit = ((!word) >> shift) & 1 == 1;
|
||||
Self::encode_manchester_bit(&mut signal, bit);
|
||||
}
|
||||
|
||||
// Part2: bits 63 down to 0 (64 bits), inverted
|
||||
for b in (0..=63).rev() {
|
||||
let word = if b >= 32 { p2_hi } else { p2_lo };
|
||||
let shift = if b >= 32 { b - 32 } else { b };
|
||||
let bit = ((!word) >> shift) & 1 == 1;
|
||||
Self::encode_manchester_bit(&mut signal, bit);
|
||||
}
|
||||
|
||||
// Part3: bits 15 down to 0 (16 bits), inverted
|
||||
for b in (0..=15).rev() {
|
||||
let bit = ((!p3) >> b) & 1 == 1;
|
||||
Self::encode_manchester_bit(&mut signal, bit);
|
||||
}
|
||||
|
||||
// Gap between passes
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
}
|
||||
|
||||
signal
|
||||
}
|
||||
|
||||
/// Encode one Manchester bit (matches kia_v6.c kia_v6_encode_manchester_bit)
|
||||
fn encode_manchester_bit(signal: &mut Vec<LevelDuration>, bit: bool) {
|
||||
if bit {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
}
|
||||
|
||||
/// AES-128 key from V6 keystores A+B with XOR_MASK_LOW/HIGH (matches kia_v6.c)
|
||||
fn get_aes_key() -> [u8; 16] {
|
||||
let keystore_a = Self::get_keystore_a();
|
||||
@@ -306,6 +481,13 @@ impl KiaV6Decoder {
|
||||
aes_key
|
||||
}
|
||||
|
||||
/// Extract fx_field from stored_part1_high (matches kia_v6.c fx_field extraction)
|
||||
fn extract_fx_field(&self) -> u8 {
|
||||
let fx_byte0 = ((self.stored_part1_high >> 24) & 0xFF) as u8;
|
||||
let fx_byte1 = ((self.stored_part1_high >> 16) & 0xFF) as u8;
|
||||
((fx_byte0 & 0xF) << 4) | (fx_byte1 & 0xF)
|
||||
}
|
||||
|
||||
/// Decrypt 16-byte block: byte layout matches kia_v6.c; AES-128 then CRC8 check
|
||||
fn decrypt(&self) -> Option<(u32, u8, u32, bool)> {
|
||||
let mut encrypted_data = [0u8; 16];
|
||||
@@ -528,6 +710,7 @@ impl ProtocolDecoder for KiaV6Decoder {
|
||||
if let Some((serial, button, counter, crc_valid)) = self.decrypt() {
|
||||
let key_data = ((self.stored_part1_high as u64) << 32) |
|
||||
(self.stored_part1_low as u64);
|
||||
let fx_field = self.extract_fx_field();
|
||||
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(DecodedSignal {
|
||||
@@ -537,8 +720,8 @@ impl ProtocolDecoder for KiaV6Decoder {
|
||||
crc_valid,
|
||||
data: key_data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: false,
|
||||
extra: None,
|
||||
encoder_capable: true,
|
||||
extra: Some(fx_field as u64),
|
||||
protocol_display_name: None,
|
||||
});
|
||||
}
|
||||
@@ -552,11 +735,18 @@ impl ProtocolDecoder for KiaV6Decoder {
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false // V6 is decode-only
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None // V6 decode-only in protopirate
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0) as u32;
|
||||
let fx_field = decoded.extra.unwrap_or(0) as u8;
|
||||
|
||||
let (p1_lo, p1_hi, p2_lo, p2_hi, p3) =
|
||||
Self::encrypt_payload(fx_field, serial, button, counter);
|
||||
|
||||
Some(Self::build_upload(p1_lo, p1_hi, p2_lo, p2_hi, p3))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
//! Mazda V0 protocol decoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/mazda_v0.c`.
|
||||
//! Original implementation by @lupettohf.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - 433.92 MHz FM, decode-only (no encoder)
|
||||
//! - Pair-based decoding: feed() ignores level, processes raw durations in pairs
|
||||
//! - Preamble: minimum 13 short/short pairs before data starts
|
||||
//! - Data uses a 14-byte buffer with inverted bit polarity
|
||||
//! - XOR deobfuscation with parity-based mask selection + bit interleave swap
|
||||
//! - Additive checksum over first 7 bytes must equal byte 8
|
||||
//! - Field layout: serial (32-bit), button (8-bit), counter (16-bit)
|
||||
//! - Button codes: 0x10=Lock, 0x20=Unlock, 0x40=Trunk
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
|
||||
const PREAMBLE_MIN: u16 = 13;
|
||||
const COMPLETION_MIN: u16 = 80;
|
||||
const COMPLETION_MAX: u16 = 105;
|
||||
const DATA_BUFFER_SIZE: usize = 14;
|
||||
|
||||
/// Decoder states (matches mazda_v0.c MazdaDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
PreambleSave,
|
||||
PreambleCheck,
|
||||
DataSave,
|
||||
DataCheck,
|
||||
}
|
||||
|
||||
/// Mazda V0 protocol decoder
|
||||
pub struct MazdaV0Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
preamble_count: u16,
|
||||
bit_counter: u16,
|
||||
prev_state: u8,
|
||||
data_buffer: [u8; DATA_BUFFER_SIZE],
|
||||
}
|
||||
|
||||
impl MazdaV0Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
preamble_count: 0,
|
||||
bit_counter: 0,
|
||||
prev_state: 0,
|
||||
data_buffer: [0u8; DATA_BUFFER_SIZE],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_short(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_long(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
}
|
||||
|
||||
/// Collect a single bit into the data buffer.
|
||||
/// Inverted polarity: state_bit == 0 means stored bit is 1.
|
||||
fn collect_bit(&mut self, state_bit: u8) {
|
||||
let byte_idx = (self.bit_counter >> 3) as usize;
|
||||
if byte_idx < DATA_BUFFER_SIZE {
|
||||
self.data_buffer[byte_idx] <<= 1;
|
||||
if state_bit == 0 {
|
||||
self.data_buffer[byte_idx] |= 1;
|
||||
}
|
||||
}
|
||||
self.bit_counter += 1;
|
||||
}
|
||||
|
||||
/// Process a duration pair and collect bits.
|
||||
/// Returns true if the pair was valid, false otherwise.
|
||||
fn process_pair(&mut self, dur_first: u32, dur_second: u32) -> bool {
|
||||
let first_short = Self::is_short(dur_first);
|
||||
let first_long = Self::is_long(dur_first);
|
||||
let second_short = Self::is_short(dur_second);
|
||||
let second_long = Self::is_long(dur_second);
|
||||
|
||||
if first_long && second_short {
|
||||
self.collect_bit(0);
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if first_short && second_long {
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if first_short && second_short {
|
||||
let ps = self.prev_state;
|
||||
self.collect_bit(ps);
|
||||
return true;
|
||||
}
|
||||
|
||||
if first_long && second_long {
|
||||
self.collect_bit(0);
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check whether enough bits have been collected and validate the frame.
|
||||
/// On success, returns a DecodedSignal.
|
||||
fn check_completion(&self) -> Option<DecodedSignal> {
|
||||
if self.bit_counter < COMPLETION_MIN || self.bit_counter > COMPLETION_MAX {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Shift buffer by 1 byte (discard sync/header byte)
|
||||
let mut data = [0u8; 8];
|
||||
for i in 0..8 {
|
||||
data[i] = self.data_buffer[i + 1];
|
||||
}
|
||||
|
||||
// XOR deobfuscation
|
||||
Self::xor_deobfuscate(&mut data);
|
||||
|
||||
// Checksum: sum of data[0..7] must equal data[7]
|
||||
let mut checksum: u8 = 0;
|
||||
for i in 0..7 {
|
||||
checksum = checksum.wrapping_add(data[i]);
|
||||
}
|
||||
if checksum != data[7] {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Pack into u64
|
||||
let mut packed: u64 = 0;
|
||||
for i in 0..8 {
|
||||
packed = (packed << 8) | (data[i] as u64);
|
||||
}
|
||||
|
||||
// Parse fields
|
||||
let serial = (packed >> 32) as u32;
|
||||
let btn = ((packed >> 24) & 0xFF) as u8;
|
||||
let cnt = ((packed >> 8) & 0xFFFF) as u16;
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: Some(cnt),
|
||||
crc_valid: true,
|
||||
data: packed,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: false,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Byte parity: XOR-fold to single bit (matches mazda_byte_parity in C)
|
||||
fn byte_parity(mut value: u8) -> u8 {
|
||||
value ^= value >> 4;
|
||||
value ^= value >> 2;
|
||||
value ^= value >> 1;
|
||||
value & 1
|
||||
}
|
||||
|
||||
/// XOR deobfuscation with parity-based mask selection + bit interleave swap
|
||||
/// (matches mazda_xor_deobfuscate in C)
|
||||
fn xor_deobfuscate(data: &mut [u8; 8]) {
|
||||
let parity = Self::byte_parity(data[7]);
|
||||
|
||||
if parity != 0 {
|
||||
let mask = data[6];
|
||||
for i in 0..6 {
|
||||
data[i] ^= mask;
|
||||
}
|
||||
} else {
|
||||
let mask = data[5];
|
||||
for i in 0..5 {
|
||||
data[i] ^= mask;
|
||||
}
|
||||
data[6] ^= mask;
|
||||
}
|
||||
|
||||
// Bit interleave swap between bytes 5 and 6
|
||||
let old5 = data[5];
|
||||
let old6 = data[6];
|
||||
data[5] = (old5 & 0xAA) | (old6 & 0x55);
|
||||
data[6] = (old5 & 0x55) | (old6 & 0xAA);
|
||||
}
|
||||
|
||||
/// Get button name for display
|
||||
#[allow(dead_code)]
|
||||
fn get_button_name(btn: u8) -> &'static str {
|
||||
match btn {
|
||||
0x10 => "Lock",
|
||||
0x20 => "Unlock",
|
||||
0x40 => "Trunk",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for MazdaV0Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Mazda V0"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.preamble_count = 0;
|
||||
self.bit_counter = 0;
|
||||
self.prev_state = 0;
|
||||
self.te_last = 0;
|
||||
self.data_buffer = [0u8; DATA_BUFFER_SIZE];
|
||||
}
|
||||
|
||||
fn feed(&mut self, _level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if Self::is_short(duration) {
|
||||
self.te_last = duration;
|
||||
self.preamble_count = 0;
|
||||
self.step = DecoderStep::PreambleCheck;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::PreambleSave => {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::PreambleCheck;
|
||||
}
|
||||
|
||||
DecoderStep::PreambleCheck => {
|
||||
if Self::is_short(self.te_last) && Self::is_short(duration) {
|
||||
self.preamble_count += 1;
|
||||
self.step = DecoderStep::PreambleSave;
|
||||
} else if Self::is_short(self.te_last)
|
||||
&& Self::is_long(duration)
|
||||
&& self.preamble_count >= PREAMBLE_MIN
|
||||
{
|
||||
// Transition from preamble to data
|
||||
self.bit_counter = 1;
|
||||
self.data_buffer = [0u8; DATA_BUFFER_SIZE];
|
||||
self.collect_bit(1);
|
||||
self.prev_state = 0;
|
||||
self.step = DecoderStep::DataSave;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DataSave => {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::DataCheck;
|
||||
}
|
||||
|
||||
DecoderStep::DataCheck => {
|
||||
if self.process_pair(self.te_last, duration) {
|
||||
self.step = DecoderStep::DataSave;
|
||||
} else {
|
||||
// Pair was invalid - check if we have a complete frame
|
||||
let result = self.check_completion();
|
||||
self.step = DecoderStep::Reset;
|
||||
if result.is_some() {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MazdaV0Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
//! Mitsubishi V0 protocol decoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/mitsubishi_v0.c`.
|
||||
//! Original implementation by @lupettohf.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: Short HIGH + Long LOW = bit 1, Long HIGH + Short LOW = bit 0
|
||||
//! - 96-bit frame (12 bytes), collected MSB-first into byte buffer
|
||||
//! - Level-aware state machine: HIGH pulses are saved, LOW pulses complete the pair
|
||||
//! - Unscramble: NOT first 8 bytes, extract counter from bytes[4..5], compute XOR mask, apply to bytes[0..5]
|
||||
//! - Field layout: serial = bytes[0..3] (32-bit), counter = bytes[4..5] (16-bit), button = byte[6]
|
||||
//! - 868 MHz, FM modulation, decode-only (no encoder)
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
#[allow(dead_code)]
|
||||
const MIN_COUNT_BIT: usize = 80;
|
||||
const BIT_COUNT: usize = 96;
|
||||
const DATA_BYTES: usize = 12;
|
||||
|
||||
/// Decoder states (matches mitsubishi_v0.c MitsubishiDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
DataSave,
|
||||
DataCheck,
|
||||
}
|
||||
|
||||
/// Mitsubishi V0 protocol decoder
|
||||
pub struct MitsubishiV0Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
bit_count: usize,
|
||||
decode_data: [u8; DATA_BYTES],
|
||||
}
|
||||
|
||||
impl MitsubishiV0Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
bit_count: 0,
|
||||
decode_data: [0u8; DATA_BYTES],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_short(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_long(duration: u32) -> bool {
|
||||
duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
}
|
||||
|
||||
fn reset_payload(&mut self) {
|
||||
self.bit_count = 0;
|
||||
self.decode_data = [0u8; DATA_BYTES];
|
||||
}
|
||||
|
||||
/// Collect a HIGH+LOW pair and decode one bit.
|
||||
/// Short HIGH + Long LOW = bit 1; Long HIGH + Short LOW = bit 0.
|
||||
/// Bits are stored MSB-first into the byte buffer.
|
||||
fn collect_pair(&mut self, high: u32, low: u32) -> bool {
|
||||
let bit_value;
|
||||
|
||||
if Self::is_short(high) && Self::is_long(low) {
|
||||
bit_value = true;
|
||||
} else if Self::is_long(high) && Self::is_short(low) {
|
||||
bit_value = false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
let bit_index = self.bit_count;
|
||||
if bit_index < BIT_COUNT {
|
||||
if bit_value {
|
||||
let byte_index = bit_index >> 3;
|
||||
let bit_position = 7 - (bit_index & 0x07);
|
||||
self.decode_data[byte_index] |= 1u8 << bit_position;
|
||||
}
|
||||
self.bit_count += 1;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Unscramble the payload (matches mitsubishi_unscramble_payload in C reference).
|
||||
/// 1. Bitwise NOT first 8 bytes
|
||||
/// 2. Extract counter from bytes[4..5]
|
||||
/// 3. Compute masks from counter and XOR bytes[0..5]
|
||||
fn unscramble_payload(payload: &mut [u8; DATA_BYTES]) {
|
||||
// Step 1: NOT first 8 bytes
|
||||
for i in 0..8 {
|
||||
payload[i] = !payload[i];
|
||||
}
|
||||
|
||||
// Step 2: Extract counter
|
||||
let counter = ((payload[4] as u16) << 8) | (payload[5] as u16);
|
||||
let hi = ((counter >> 8) & 0xFF) as u8;
|
||||
let lo = (counter & 0xFF) as u8;
|
||||
|
||||
// Step 3: Compute masks
|
||||
let mask1 = (hi & 0xAA) | (lo & 0x55);
|
||||
let mask2 = (lo & 0xAA) | (hi & 0x55);
|
||||
let mask3 = mask1 ^ mask2;
|
||||
|
||||
// Step 4: XOR bytes[0..5] with mask3
|
||||
for i in 0..5 {
|
||||
payload[i] ^= mask3;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the unscrambled payload into a DecodedSignal.
|
||||
fn publish_frame(&self) -> DecodedSignal {
|
||||
let mut payload = self.decode_data;
|
||||
Self::unscramble_payload(&mut payload);
|
||||
|
||||
let serial = ((payload[0] as u32) << 24)
|
||||
| ((payload[1] as u32) << 16)
|
||||
| ((payload[2] as u32) << 8)
|
||||
| (payload[3] as u32);
|
||||
|
||||
let counter = ((payload[4] as u16) << 8) | (payload[5] as u16);
|
||||
let button = payload[6];
|
||||
|
||||
// Store first 8 bytes (post-unscramble) as u64 data field
|
||||
let data = ((payload[0] as u64) << 56)
|
||||
| ((payload[1] as u64) << 48)
|
||||
| ((payload[2] as u64) << 40)
|
||||
| ((payload[3] as u64) << 32)
|
||||
| ((payload[4] as u64) << 24)
|
||||
| ((payload[5] as u64) << 16)
|
||||
| ((payload[6] as u64) << 8)
|
||||
| (payload[7] as u64);
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data,
|
||||
data_count_bit: BIT_COUNT,
|
||||
encoder_capable: false,
|
||||
extra: None,
|
||||
protocol_display_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for MitsubishiV0Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Mitsubishi V0"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[868_350_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.reset_payload();
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
// Wait for a HIGH pulse to start
|
||||
if level {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::DataCheck;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DataSave => {
|
||||
if level {
|
||||
// HIGH pulse: save duration and move to DataCheck
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::DataCheck;
|
||||
} else {
|
||||
// LOW pulse without preceding HIGH data check: reset
|
||||
self.step = DecoderStep::Reset;
|
||||
self.reset_payload();
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DataCheck => {
|
||||
if !level {
|
||||
// LOW pulse: complete the HIGH+LOW pair
|
||||
if self.collect_pair(self.te_last, duration) {
|
||||
if self.bit_count >= BIT_COUNT {
|
||||
// Full frame received
|
||||
let result = self.publish_frame();
|
||||
self.reset_payload();
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
} else {
|
||||
self.step = DecoderStep::DataSave;
|
||||
}
|
||||
} else {
|
||||
// Invalid pair: reset
|
||||
self.reset_payload();
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
// Another HIGH pulse while expecting LOW: update te_last
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MitsubishiV0Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,14 @@ mod subaru;
|
||||
mod ford_v0;
|
||||
mod vag;
|
||||
mod fiat_v0;
|
||||
mod fiat_v1;
|
||||
mod suzuki;
|
||||
mod scher_khan;
|
||||
mod star_line;
|
||||
mod psa;
|
||||
mod mazda_v0;
|
||||
mod mitsubishi_v0;
|
||||
mod porsche_touareg;
|
||||
|
||||
pub use common::DecodedSignal;
|
||||
|
||||
@@ -109,11 +113,15 @@ impl ProtocolRegistry {
|
||||
Box::new(ford_v0::FordV0Decoder::new()),
|
||||
Box::new(subaru::SubaruDecoder::new()),
|
||||
Box::new(fiat_v0::FiatV0Decoder::new()),
|
||||
Box::new(fiat_v1::FiatV1Decoder::new()),
|
||||
Box::new(suzuki::SuzukiDecoder::new()),
|
||||
Box::new(scher_khan::ScherKhanDecoder::new()),
|
||||
Box::new(star_line::StarLineDecoder::new()),
|
||||
Box::new(keeloq::KeeloqDecoder::new()),
|
||||
Box::new(psa::PsaDecoder::new()),
|
||||
Box::new(mazda_v0::MazdaV0Decoder::new()),
|
||||
Box::new(mitsubishi_v0::MitsubishiV0Decoder::new()),
|
||||
Box::new(porsche_touareg::PorscheTouaregDecoder::new()),
|
||||
];
|
||||
|
||||
Self { decoders }
|
||||
|
||||
@@ -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
@@ -1,13 +1,14 @@
|
||||
//! PSA (Peugeot/Citroën) protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`.
|
||||
//! Decode/encode logic (Manchester, preamble, TEA, XOR, mode 0x23/0x36) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble)
|
||||
//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation
|
||||
//! - TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds XOR layer
|
||||
//! - Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule)
|
||||
//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48)
|
||||
//! - Modified TEA (XTEA-like) with dynamic key selection (sum&3, (sum>>11)&3)
|
||||
//! - Mode 0x23: direct XOR decrypt with checksum validation
|
||||
//! - Mode 0x36: TEA brute-force with BF1/BF2 key schedules (deferred)
|
||||
//! - Dual preamble: Pattern 1 (250µs) and Pattern 2 (125µs)
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -21,7 +22,6 @@ const MIN_COUNT_BIT: usize = 128;
|
||||
// Internal timing for Manchester sub-symbol detection
|
||||
const TE_SHORT_125: u32 = 125;
|
||||
const TE_LONG_250: u32 = 250;
|
||||
const TE_TOLERANCE_49: u32 = 49;
|
||||
const TE_TOLERANCE_50: u32 = 50;
|
||||
const TE_TOLERANCE_99: u32 = 99;
|
||||
const TE_END_1000: u32 = 1000;
|
||||
@@ -45,13 +45,19 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states (matches protopirate's PsaDecoderState)
|
||||
/// Decoder states (matches protopirate's PsaDecoderState 0-4)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderState {
|
||||
/// State0: Wait for first edge (pattern detection)
|
||||
WaitEdge,
|
||||
CountPattern,
|
||||
DecodeManchester,
|
||||
End,
|
||||
/// State1: Count 250µs preamble patterns (Pattern 1)
|
||||
CountPattern250,
|
||||
/// State2: Manchester decode at 250/500µs (Pattern 1)
|
||||
DecodeManchester250,
|
||||
/// State3: Count 125µs preamble patterns (Pattern 2)
|
||||
CountPattern125,
|
||||
/// State4: Manchester decode at 125/250µs (Pattern 2)
|
||||
DecodeManchester125,
|
||||
}
|
||||
|
||||
/// PSA protocol decoder
|
||||
@@ -140,111 +146,268 @@ impl PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA decrypt (matches psa.c / standard TEA)
|
||||
/// Modified TEA decrypt with dynamic key selection (matches psa.c psa_tea_decrypt)
|
||||
/// Uses XTEA-like key scheduling: key[sum&3] and key[(sum>>11)&3]
|
||||
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
*v1 = v1.wrapping_sub(
|
||||
(v0.wrapping_shl(4).wrapping_add(key[2]))
|
||||
^ (v0.wrapping_add(sum))
|
||||
^ (v0.wrapping_shr(5).wrapping_add(key[3])),
|
||||
);
|
||||
*v0 = v0.wrapping_sub(
|
||||
(v1.wrapping_shl(4).wrapping_add(key[0]))
|
||||
^ (v1.wrapping_add(sum))
|
||||
^ (v1.wrapping_shr(5).wrapping_add(key[1])),
|
||||
);
|
||||
let k_idx2 = ((sum >> 11) & 3) as usize;
|
||||
let temp = key[k_idx2].wrapping_add(sum);
|
||||
sum = sum.wrapping_sub(TEA_DELTA);
|
||||
*v1 = v1.wrapping_sub(
|
||||
temp ^ (v0.wrapping_shr(5) ^ v0.wrapping_shl(4)).wrapping_add(*v0),
|
||||
);
|
||||
let k_idx1 = (sum & 3) as usize;
|
||||
let temp = key[k_idx1].wrapping_add(sum);
|
||||
*v0 = v0.wrapping_sub(
|
||||
temp ^ (v1.wrapping_shr(5) ^ v1.wrapping_shl(4)).wrapping_add(*v1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA encrypt (matches psa.c / standard TEA)
|
||||
/// Modified TEA encrypt with dynamic key selection (matches psa.c psa_tea_encrypt)
|
||||
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let mut sum: u32 = 0;
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
let k_idx1 = (sum & 3) as usize;
|
||||
let temp = key[k_idx1].wrapping_add(sum);
|
||||
sum = sum.wrapping_add(TEA_DELTA);
|
||||
*v0 = v0.wrapping_add(
|
||||
(v1.wrapping_shl(4).wrapping_add(key[0]))
|
||||
^ (v1.wrapping_add(sum))
|
||||
^ (v1.wrapping_shr(5).wrapping_add(key[1])),
|
||||
temp ^ (v1.wrapping_shr(5) ^ v1.wrapping_shl(4)).wrapping_add(*v1),
|
||||
);
|
||||
let k_idx2 = ((sum >> 11) & 3) as usize;
|
||||
let temp = key[k_idx2].wrapping_add(sum);
|
||||
*v1 = v1.wrapping_add(
|
||||
(v0.wrapping_shl(4).wrapping_add(key[2]))
|
||||
^ (v0.wrapping_add(sum))
|
||||
^ (v0.wrapping_shr(5).wrapping_add(key[3])),
|
||||
temp ^ (v0.wrapping_shr(5) ^ v0.wrapping_shl(4)).wrapping_add(*v0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// XOR decrypt for mode 0x23 (matches psa.c)
|
||||
/// XOR decrypt for mode 0x23 (matches psa.c psa_second_stage_xor_decrypt)
|
||||
/// Uses psa_copy_reverse byte reordering then XOR operations
|
||||
fn xor_decrypt(buffer: &mut [u8]) {
|
||||
// psa_copy_reverse: reorder source bytes
|
||||
let temp = [
|
||||
buffer[5], // temp[0] = source[5]
|
||||
buffer[4], // temp[1] = source[4]
|
||||
buffer[3], // temp[2] = source[3]
|
||||
buffer[2], // temp[3] = source[2]
|
||||
buffer[9], // temp[4] = source[9]
|
||||
buffer[8], // temp[5] = source[8]
|
||||
buffer[7], // temp[6] = source[7]
|
||||
buffer[6], // temp[7] = source[6]
|
||||
];
|
||||
buffer[2] = temp[0] ^ temp[6];
|
||||
buffer[3] = temp[2] ^ temp[0];
|
||||
buffer[4] = temp[6] ^ temp[3];
|
||||
buffer[5] = temp[7] ^ temp[1];
|
||||
buffer[6] = temp[3] ^ temp[1];
|
||||
buffer[7] = temp[6] ^ temp[4] ^ temp[5];
|
||||
}
|
||||
|
||||
/// XOR encrypt for mode 0x23 encoding (matches psa.c psa_second_stage_xor_encrypt)
|
||||
fn xor_encrypt(buffer: &mut [u8]) {
|
||||
let e6 = buffer[8];
|
||||
let e7 = buffer[9];
|
||||
let e5 = buffer[7];
|
||||
let e0 = buffer[2];
|
||||
let e1 = buffer[3];
|
||||
let e2 = buffer[4];
|
||||
let e3 = buffer[5];
|
||||
let e4 = buffer[6];
|
||||
let p0 = buffer[2];
|
||||
let p1 = buffer[3];
|
||||
let p2 = buffer[4];
|
||||
let p3 = buffer[5];
|
||||
let p4 = buffer[6];
|
||||
let p5 = buffer[7];
|
||||
|
||||
buffer[2] = e0 ^ e5;
|
||||
buffer[3] = e1 ^ (e0 ^ e5 ^ e6 ^ e7);
|
||||
buffer[4] = e2 ^ e0;
|
||||
buffer[5] = e3 ^ (e0 ^ e5 ^ e6 ^ e7);
|
||||
buffer[6] = e4 ^ e2;
|
||||
buffer[7] = e5 ^ e6 ^ e7;
|
||||
let ne5 = p5 ^ e7 ^ e6;
|
||||
let ne0 = p2 ^ ne5;
|
||||
let ne2 = p4 ^ ne0;
|
||||
let ne4 = p3 ^ ne2;
|
||||
let ne3 = p0 ^ ne5;
|
||||
let ne1 = p1 ^ ne3;
|
||||
|
||||
buffer[2] = ne0;
|
||||
buffer[3] = ne1;
|
||||
buffer[4] = ne2;
|
||||
buffer[5] = ne3;
|
||||
buffer[6] = ne4;
|
||||
buffer[7] = ne5;
|
||||
}
|
||||
|
||||
/// Decrypt key1 + validation: mode 0x23 (TEA+XOR) or 0x36 (TEA, BF2) — matches psa.c
|
||||
/// Calculate checksum over buffer[2..8] (matches psa.c psa_calculate_checksum)
|
||||
fn calculate_checksum(buffer: &[u8]) -> u8 {
|
||||
let mut checksum: u32 = 0;
|
||||
for i in 2..8 {
|
||||
checksum += (buffer[i] & 0xF) as u32 + ((buffer[i] >> 4) & 0xF) as u32;
|
||||
}
|
||||
((checksum.wrapping_mul(0x10)) & 0xFF) as u8
|
||||
}
|
||||
|
||||
/// Check if direct XOR is allowed by key2 high byte (matches psa.c)
|
||||
fn direct_xor_allowed_by_key2(key2_high_byte: u8) -> bool {
|
||||
let lo = key2_high_byte & 0xF;
|
||||
if lo < 3 { return true; }
|
||||
if lo < 7 && (key2_high_byte & 0xC) != 0 { return true; }
|
||||
false
|
||||
}
|
||||
|
||||
/// Setup byte buffer from key1/key2 (matches psa.c psa_setup_byte_buffer)
|
||||
fn setup_byte_buffer(buffer: &mut [u8], key1_low: u32, key1_high: u32, key2_low: u32) {
|
||||
for i in 0..8usize {
|
||||
let shift = i * 8;
|
||||
let byte_val = if shift < 32 {
|
||||
((key1_low >> shift) & 0xFF) as u8
|
||||
} else {
|
||||
((key1_high >> (shift - 32)) & 0xFF) as u8
|
||||
};
|
||||
buffer[7 - i] = byte_val;
|
||||
}
|
||||
buffer[9] = (key2_low & 0xFF) as u8;
|
||||
buffer[8] = ((key2_low >> 8) & 0xFF) as u8;
|
||||
}
|
||||
|
||||
/// Decrypt using the C code's approach: setup_byte_buffer then attempt direct XOR
|
||||
/// with checksum validation; mode 0x36 is marked for brute-force (matches psa.c)
|
||||
fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> {
|
||||
// Try mode 0x23 first (seed byte 0x23)
|
||||
let seed_byte = (self.key1_high >> 24) as u8;
|
||||
// C: key2_low = decode_data_low (the 16-bit validation sits in the low word)
|
||||
let key2_low = self.validation_field as u32;
|
||||
|
||||
if seed_byte >= 0x23 && seed_byte < 0x24 {
|
||||
// Mode 0x23 - TEA + XOR
|
||||
let mut v0 = self.key1_high;
|
||||
let mut v1 = self.key1_low;
|
||||
Self::tea_decrypt(&mut v0, &mut v1, &BF1_KEY_SCHEDULE);
|
||||
let mut buffer = [0u8; 48];
|
||||
Self::setup_byte_buffer(&mut buffer, self.key1_low, self.key1_high, key2_low);
|
||||
|
||||
let mut buffer = [0u8; 10];
|
||||
buffer[0] = (v0 >> 24) as u8;
|
||||
buffer[1] = (v0 >> 16) as u8;
|
||||
buffer[2] = (v0 >> 8) as u8;
|
||||
buffer[3] = (v0 >> 0) as u8;
|
||||
buffer[4] = (v1 >> 24) as u8;
|
||||
buffer[5] = (v1 >> 16) as u8;
|
||||
buffer[6] = (v1 >> 8) as u8;
|
||||
buffer[7] = (v1 >> 0) as u8;
|
||||
buffer[8] = (self.validation_field >> 8) as u8;
|
||||
buffer[9] = (self.validation_field & 0xFF) as u8;
|
||||
let key2_high_byte = buffer[8];
|
||||
|
||||
// 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);
|
||||
|
||||
let serial = ((buffer[2] as u32) << 16)
|
||||
| ((buffer[3] as u32) << 8)
|
||||
let serial = ((buffer[3] as u32) << 8)
|
||||
| ((buffer[2] as u32) << 16)
|
||||
| (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 btn = buffer[8] & 0x0F;
|
||||
|
||||
return Some((serial, btn, counter, crc, 0x23));
|
||||
}
|
||||
}
|
||||
|
||||
if seed_byte >= 0xF3 && seed_byte < 0xF4 {
|
||||
// Mode 0x36 - TEA + different key schedule
|
||||
let mut v0 = self.key1_high;
|
||||
let mut v1 = self.key1_low;
|
||||
Self::tea_decrypt(&mut v0, &mut v1, &BF2_KEY_SCHEDULE);
|
||||
// Mode 0x36 - TEA brute-force path
|
||||
// Try direct TEA decrypt with BF1 key schedule for a quick decode attempt
|
||||
{
|
||||
let mut w0 = ((buffer[3] as u32) << 16)
|
||||
| ((buffer[2] as u32) << 24)
|
||||
| ((buffer[4] as u32) << 8)
|
||||
| (buffer[5] as u32);
|
||||
let mut w1 = ((buffer[7] as u32) << 16)
|
||||
| ((buffer[6] as u32) << 24)
|
||||
| ((buffer[8] as u32) << 8)
|
||||
| (buffer[9] as u32);
|
||||
|
||||
let serial = ((v0 >> 8) & 0xFFFF00) | ((v0 & 0xFF) as u32);
|
||||
let counter = v1 >> 16;
|
||||
let btn = ((v1 >> 8) & 0xF) as u8;
|
||||
let crc = (v1 & 0xFF) as u16;
|
||||
Self::tea_decrypt(&mut w0, &mut w1, &BF1_KEY_SCHEDULE);
|
||||
|
||||
// Check if the TEA CRC validates (sum of bytes)
|
||||
let crc_calc = Self::calculate_tea_crc(w0, w1);
|
||||
if crc_calc == (w1 & 0xFF) as u8 {
|
||||
let mut dec_buffer = [0u8; 48];
|
||||
dec_buffer[2] = ((w0 >> 24) & 0xFF) as u8;
|
||||
dec_buffer[3] = ((w0 >> 16) & 0xFF) as u8;
|
||||
dec_buffer[4] = ((w0 >> 8) & 0xFF) as u8;
|
||||
dec_buffer[5] = (w0 & 0xFF) as u8;
|
||||
dec_buffer[6] = ((w1 >> 24) & 0xFF) as u8;
|
||||
dec_buffer[7] = ((w1 >> 16) & 0xFF) as u8;
|
||||
dec_buffer[8] = ((w1 >> 8) & 0xFF) as u8;
|
||||
dec_buffer[9] = (w1 & 0xFF) as u8;
|
||||
|
||||
let btn = (dec_buffer[5] >> 4) & 0xF;
|
||||
let serial = ((dec_buffer[3] as u32) << 8)
|
||||
| ((dec_buffer[2] as u32) << 16)
|
||||
| (dec_buffer[4] as u32);
|
||||
let counter = ((dec_buffer[7] as u32) << 8)
|
||||
| ((dec_buffer[6] as u32) << 16)
|
||||
| (dec_buffer[8] as u32)
|
||||
| (((dec_buffer[5] as u32) & 0xF) << 24);
|
||||
let crc = dec_buffer[9] as u16;
|
||||
|
||||
return Some((serial, btn, counter, crc, 0x36));
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot decrypt - return raw data
|
||||
// Also try BF2 key schedule directly (XOR-derived keys)
|
||||
{
|
||||
let mut w0 = ((buffer[3] as u32) << 16)
|
||||
| ((buffer[2] as u32) << 24)
|
||||
| ((buffer[4] as u32) << 8)
|
||||
| (buffer[5] as u32);
|
||||
let mut w1 = ((buffer[7] as u32) << 16)
|
||||
| ((buffer[6] as u32) << 24)
|
||||
| ((buffer[8] as u32) << 8)
|
||||
| (buffer[9] as u32);
|
||||
|
||||
Self::tea_decrypt(&mut w0, &mut w1, &BF2_KEY_SCHEDULE);
|
||||
|
||||
let crc_calc = Self::calculate_tea_crc(w0, w1);
|
||||
if crc_calc == (w1 & 0xFF) as u8 {
|
||||
let mut dec_buffer = [0u8; 48];
|
||||
dec_buffer[2] = ((w0 >> 24) & 0xFF) as u8;
|
||||
dec_buffer[3] = ((w0 >> 16) & 0xFF) as u8;
|
||||
dec_buffer[4] = ((w0 >> 8) & 0xFF) as u8;
|
||||
dec_buffer[5] = (w0 & 0xFF) as u8;
|
||||
dec_buffer[6] = ((w1 >> 24) & 0xFF) as u8;
|
||||
dec_buffer[7] = ((w1 >> 16) & 0xFF) as u8;
|
||||
dec_buffer[8] = ((w1 >> 8) & 0xFF) as u8;
|
||||
dec_buffer[9] = (w1 & 0xFF) as u8;
|
||||
|
||||
let btn = (dec_buffer[5] >> 4) & 0xF;
|
||||
let serial = ((dec_buffer[3] as u32) << 8)
|
||||
| ((dec_buffer[2] as u32) << 16)
|
||||
| (dec_buffer[4] as u32);
|
||||
let counter = ((dec_buffer[7] as u32) << 8)
|
||||
| ((dec_buffer[6] as u32) << 16)
|
||||
| (dec_buffer[8] as u32)
|
||||
| (((dec_buffer[5] as u32) & 0xF) << 24);
|
||||
let crc = dec_buffer[9] as u16;
|
||||
|
||||
return Some((serial, btn, counter, crc, 0x36));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Calculate TEA CRC (matches psa.c psa_calculate_tea_crc)
|
||||
fn calculate_tea_crc(v0: u32, v1: u32) -> u8 {
|
||||
let mut crc: u32 = 0;
|
||||
crc += (v0 >> 24) & 0xFF;
|
||||
crc += (v0 >> 16) & 0xFF;
|
||||
crc += (v0 >> 8) & 0xFF;
|
||||
crc += v0 & 0xFF;
|
||||
crc += (v1 >> 24) & 0xFF;
|
||||
crc += (v1 >> 16) & 0xFF;
|
||||
crc += (v1 >> 8) & 0xFF;
|
||||
(crc & 0xFF) as u8
|
||||
}
|
||||
|
||||
fn init_preamble_state(&mut self) {
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.pattern_counter = 0;
|
||||
self.bit_count = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
fn finalize_frame(&mut self) -> Option<DecodedSignal> {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
if self.bit_count >= 80 {
|
||||
// C validation: ((key1_high >> 16) & 0xF) == 0xA
|
||||
if ((self.key1_high >> 16) & 0xF) != 0xA {
|
||||
return None;
|
||||
}
|
||||
let result = self.parse_data();
|
||||
return Some(result);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -317,24 +480,96 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.state {
|
||||
// State0: detect preamble pattern type
|
||||
DecoderState::WaitEdge => {
|
||||
if level && duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_49 {
|
||||
self.state = DecoderState::CountPattern;
|
||||
if !level {
|
||||
return None;
|
||||
}
|
||||
let diff_250 = duration_diff!(duration, TE_SHORT);
|
||||
let diff_125 = duration_diff!(duration, TE_SHORT_125);
|
||||
|
||||
if diff_250 < TE_TOLERANCE_99 {
|
||||
// Pattern 1: 250µs preamble
|
||||
self.init_preamble_state();
|
||||
self.state = DecoderState::CountPattern250;
|
||||
} else if diff_125 < 40 && duration <= 180 {
|
||||
// Pattern 2: 125µs preamble
|
||||
self.init_preamble_state();
|
||||
self.state = DecoderState::CountPattern125;
|
||||
}
|
||||
self.prev_duration = duration;
|
||||
}
|
||||
|
||||
// State1: count 250µs preamble (Pattern 1)
|
||||
DecoderState::CountPattern250 => {
|
||||
if level {
|
||||
return None;
|
||||
}
|
||||
let diff_short = duration_diff!(duration, TE_SHORT);
|
||||
if diff_short < TE_TOLERANCE_99 + 1 {
|
||||
let prev_diff = duration_diff!(self.prev_duration, TE_SHORT);
|
||||
if prev_diff <= TE_TOLERANCE_99 {
|
||||
self.pattern_counter += 1;
|
||||
}
|
||||
self.prev_duration = duration;
|
||||
} else {
|
||||
let diff_long = duration_diff!(duration, TE_LONG);
|
||||
if diff_long < 100 && self.pattern_counter > 0x46 {
|
||||
// Transition to Manchester decode at 250/500µs
|
||||
self.state = DecoderState::DecodeManchester250;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.pattern_counter = 0;
|
||||
self.prev_duration = duration;
|
||||
} else {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
self.pattern_counter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DecoderState::CountPattern => {
|
||||
// State2: Manchester decode at 250/500µs (Pattern 1)
|
||||
DecoderState::DecodeManchester250 => {
|
||||
if self.bit_count >= 121 {
|
||||
return self.finalize_frame();
|
||||
}
|
||||
// Check for end-of-frame marker
|
||||
if level && self.bit_count == 80 && duration >= 800 {
|
||||
let end_diff = duration_diff!(duration, TE_END_1000);
|
||||
if end_diff <= 199 {
|
||||
return self.finalize_frame();
|
||||
}
|
||||
}
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
|
||||
if duration > 10000 {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
self.pattern_counter = 0;
|
||||
return None;
|
||||
}
|
||||
|
||||
if is_short || is_long {
|
||||
if let Some(bit) = self.manchester_advance(is_short, level) {
|
||||
self.add_bit(bit);
|
||||
}
|
||||
}
|
||||
self.prev_duration = duration;
|
||||
}
|
||||
|
||||
// State3: count 125µs preamble (Pattern 2)
|
||||
DecoderState::CountPattern125 => {
|
||||
let diff_125 = duration_diff!(duration, TE_SHORT_125);
|
||||
let diff_250 = duration_diff!(duration, TE_LONG_250);
|
||||
|
||||
if diff_125 < TE_TOLERANCE_50 {
|
||||
self.pattern_counter += 1;
|
||||
self.prev_duration = duration;
|
||||
} else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x46 {
|
||||
// Found end of preamble, start Manchester decoding
|
||||
self.state = DecoderState::DecodeManchester;
|
||||
} else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x45 {
|
||||
// Transition to Manchester decode at 125/250µs
|
||||
self.state = DecoderState::DecodeManchester125;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
@@ -347,23 +582,17 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
DecoderState::DecodeManchester => {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
let is_end = duration > TE_END_1000;
|
||||
|
||||
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);
|
||||
// State4: Manchester decode at 125/250µs (Pattern 2)
|
||||
DecoderState::DecodeManchester125 => {
|
||||
if self.bit_count >= 121 {
|
||||
return self.finalize_frame();
|
||||
}
|
||||
self.state = DecoderState::WaitEdge;
|
||||
return None;
|
||||
let is_short = duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_50;
|
||||
let is_long = duration_diff!(duration, TE_LONG_250) < TE_TOLERANCE_99;
|
||||
let is_end = duration > 500;
|
||||
|
||||
if is_end {
|
||||
return self.finalize_frame();
|
||||
}
|
||||
|
||||
if is_short || is_long {
|
||||
@@ -373,13 +602,8 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
} else {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
}
|
||||
|
||||
self.prev_duration = duration;
|
||||
}
|
||||
|
||||
DecoderState::End => {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
@@ -406,31 +630,8 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
buffer[8] = button & 0x0F;
|
||||
buffer[9] = 0;
|
||||
|
||||
// XOR encrypt
|
||||
{
|
||||
let e6 = buffer[8];
|
||||
let e7 = buffer[9];
|
||||
let p0 = buffer[2];
|
||||
let p1 = buffer[3];
|
||||
let p2 = buffer[4];
|
||||
let p3 = buffer[5];
|
||||
let p4 = buffer[6];
|
||||
let p5 = buffer[7];
|
||||
|
||||
let ne5 = p5 ^ e7 ^ e6;
|
||||
let ne0 = p2 ^ ne5;
|
||||
let ne2 = p4 ^ ne0;
|
||||
let ne4 = p3 ^ ne2;
|
||||
let ne3 = p0 ^ ne5;
|
||||
let ne1 = p1 ^ ne3;
|
||||
|
||||
buffer[2] = ne0;
|
||||
buffer[3] = ne1;
|
||||
buffer[4] = ne2;
|
||||
buffer[5] = ne3;
|
||||
buffer[6] = ne4;
|
||||
buffer[7] = ne5;
|
||||
}
|
||||
// XOR encrypt (matches psa.c psa_second_stage_xor_encrypt)
|
||||
Self::xor_encrypt(&mut buffer);
|
||||
|
||||
// TEA encrypt
|
||||
let mut v0 = ((buffer[0] as u32) << 24)
|
||||
@@ -450,38 +651,42 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(512);
|
||||
|
||||
// Preamble + sync (matches protopirate psa encode)
|
||||
for _ in 0..70 {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT_125));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT_125));
|
||||
}
|
||||
// Preamble: 80 iterations at 250us HIGH+LOW (matches C: te = PSA_TE_LONG_250)
|
||||
for _ in 0..80 {
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
}
|
||||
|
||||
// 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);
|
||||
for bit in (0..64).rev() {
|
||||
if (key1 >> bit) & 1 == 1 {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
}
|
||||
}
|
||||
|
||||
// Validation: 16 bits Manchester encoded
|
||||
// Validation: 16 bits Manchester at 250us
|
||||
for bit in (0..16).rev() {
|
||||
if (validation >> bit) & 1 == 1 {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
}
|
||||
}
|
||||
|
||||
// End marker
|
||||
// End marker: HIGH 1000us + LOW 1000us (matches C)
|
||||
signal.push(LevelDuration::new(true, TE_END_1000));
|
||||
signal.push(LevelDuration::new(false, TE_END_1000));
|
||||
|
||||
Some(signal)
|
||||
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "libhackrf"
|
||||
version = "0.1.1"
|
||||
authors = ["Connor Slade <connor@connorcode.com>"]
|
||||
description = "A modern libhackrf wrapper that supports receiving and transmitting. (Patched for ARM cross-compilation)"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/connorslade/libhackrf-rs"
|
||||
|
||||
[lib]
|
||||
name = "libhackrf"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies.num-complex]
|
||||
version = "0.4.6"
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
pub enum DeviceType {
|
||||
Jellybean = 0,
|
||||
Jawbreaker = 1,
|
||||
/// HackRF One prior to r9
|
||||
Hackrf1Og = 2,
|
||||
Rad1O = 3,
|
||||
Hackrf1R9 = 4,
|
||||
/// Tried detection but did not recognize board
|
||||
Unrecognized = 0xFE,
|
||||
/// detection not yet attempted
|
||||
Undetected = 0xFF,
|
||||
}
|
||||
|
||||
impl DeviceType {
|
||||
pub fn from_id(id: u8) -> Self {
|
||||
match id {
|
||||
0 => DeviceType::Jellybean,
|
||||
1 => DeviceType::Jawbreaker,
|
||||
2 => DeviceType::Hackrf1Og,
|
||||
3 => DeviceType::Rad1O,
|
||||
4 => DeviceType::Hackrf1R9,
|
||||
0xFE => DeviceType::Unrecognized,
|
||||
0xFF => DeviceType::Undetected,
|
||||
_ => DeviceType::Undetected,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, HackrfError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HackrfError {
|
||||
InvalidParam = -2,
|
||||
NotFound = -5,
|
||||
Busy = -6,
|
||||
NoMem = -11,
|
||||
Libusb = -1000,
|
||||
Thread = -1001,
|
||||
StreamingThreadErr = -1002,
|
||||
StreamingStopped = -1003,
|
||||
StreamingExitCalled = -1004,
|
||||
Other = -9999,
|
||||
}
|
||||
|
||||
impl HackrfError {
|
||||
pub fn from_id(id: i32) -> Result<()> {
|
||||
Err(match id {
|
||||
0 | 1 => return Ok(()),
|
||||
-2 => HackrfError::InvalidParam,
|
||||
-5 => HackrfError::NotFound,
|
||||
-6 => HackrfError::Busy,
|
||||
-11 => HackrfError::NoMem,
|
||||
-1000 => HackrfError::Libusb,
|
||||
-1001 => HackrfError::Thread,
|
||||
-1002 => HackrfError::StreamingThreadErr,
|
||||
-1003 => HackrfError::StreamingStopped,
|
||||
-1004 => HackrfError::StreamingExitCalled,
|
||||
-9999 => HackrfError::Other,
|
||||
_ => HackrfError::Other,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HackrfError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_fmt(format_args!("{self:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for HackrfError {}
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
#![allow(improper_ctypes)]
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_uchar, c_uint, c_ulonglong, c_void};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct HackrfDevice;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct HackrfTransfer {
|
||||
pub device: *mut HackrfDevice,
|
||||
pub buffer: *mut c_uchar,
|
||||
pub buffer_length: c_int,
|
||||
pub valid_length: c_int,
|
||||
pub rx_ctx: *mut c_void,
|
||||
pub tx_ctx: *mut c_void,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[repr(C)]
|
||||
pub struct SerialNumber {
|
||||
pub part_id: [c_uint; 2],
|
||||
pub serial_no: [c_uint; 4],
|
||||
}
|
||||
|
||||
#[link(name = "hackrf")]
|
||||
extern "C" {
|
||||
pub fn hackrf_init() -> c_int;
|
||||
pub fn hackrf_exit() -> c_int;
|
||||
|
||||
pub fn hackrf_open(device: *mut *mut HackrfDevice) -> c_int;
|
||||
pub fn hackrf_close(device: *mut HackrfDevice) -> c_int;
|
||||
|
||||
pub fn hackrf_start_rx(
|
||||
device: *mut HackrfDevice,
|
||||
callback: extern "C" fn(*mut HackrfTransfer) -> c_int,
|
||||
rx_ctx: *mut c_void,
|
||||
) -> c_int;
|
||||
pub fn hackrf_stop_rx(device: *mut HackrfDevice) -> c_int;
|
||||
|
||||
pub fn hackrf_start_tx(
|
||||
device: *mut HackrfDevice,
|
||||
callback: extern "C" fn(*mut HackrfTransfer) -> c_int,
|
||||
tx_ctx: *mut c_void,
|
||||
) -> c_int;
|
||||
pub fn hackrf_stop_tx(device: *mut HackrfDevice) -> c_int;
|
||||
|
||||
pub fn hackrf_is_streaming(device: *mut HackrfDevice) -> c_int;
|
||||
|
||||
pub fn hackrf_set_baseband_filter_bandwidth(
|
||||
device: *mut HackrfDevice,
|
||||
bandwidth_hz: c_uint,
|
||||
) -> c_int;
|
||||
|
||||
pub fn hackrf_board_id_read(device: *mut HackrfDevice, value: *mut c_uchar) -> c_int;
|
||||
pub fn hackrf_version_string_read(
|
||||
device: *mut HackrfDevice,
|
||||
version: *mut c_char,
|
||||
length: c_uchar,
|
||||
) -> c_int;
|
||||
pub fn hackrf_board_partid_serialno_read(
|
||||
device: *mut HackrfDevice,
|
||||
read_partid_serialno: *mut SerialNumber,
|
||||
) -> c_int;
|
||||
|
||||
pub fn hackrf_set_freq(device: *mut HackrfDevice, freq_hz: c_ulonglong) -> c_int;
|
||||
pub fn hackrf_set_freq_explicit(
|
||||
device: *mut HackrfDevice,
|
||||
if_freq_hz: c_ulonglong,
|
||||
lo_freq_hz: c_ulonglong,
|
||||
path: c_uint,
|
||||
) -> c_int;
|
||||
|
||||
pub fn hackrf_set_sample_rate_manual(
|
||||
device: *mut HackrfDevice,
|
||||
freq_hz: c_uint,
|
||||
divider: c_uint,
|
||||
) -> c_int;
|
||||
pub fn hackrf_set_sample_rate(device: *mut HackrfDevice, freq_hz: c_double) -> c_int;
|
||||
|
||||
pub fn hackrf_set_amp_enable(device: *mut HackrfDevice, value: c_uchar) -> c_int;
|
||||
|
||||
pub fn hackrf_set_lna_gain(device: *mut HackrfDevice, value: c_uint) -> c_int;
|
||||
pub fn hackrf_set_vga_gain(device: *mut HackrfDevice, value: c_uint) -> c_int;
|
||||
pub fn hackrf_set_txvga_gain(device: *mut HackrfDevice, value: c_uint) -> c_int;
|
||||
|
||||
pub fn hackrf_set_antenna_enable(device: *mut HackrfDevice, value: c_uchar) -> c_int;
|
||||
|
||||
pub fn hackrf_error_name(errcode: c_int) -> *const c_char;
|
||||
pub fn hackrf_board_id_name(hackrf_board_id: c_uchar) -> *const c_char;
|
||||
pub fn hackrf_filter_path_name(path: c_uint) -> *const c_char;
|
||||
|
||||
pub fn hackrf_compute_baseband_filter_bw_round_down_lt(bandwidth_hz: c_uint) -> c_uint;
|
||||
pub fn hackrf_compute_baseband_filter_bw(bandwidth_hz: c_uint) -> c_uint;
|
||||
}
|
||||
Vendored
+210
@@ -0,0 +1,210 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::c_void,
|
||||
mem, ptr, slice,
|
||||
sync::{
|
||||
atomic::{AtomicPtr, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
mod enums;
|
||||
pub mod error;
|
||||
pub mod ffi;
|
||||
pub use enums::DeviceType;
|
||||
mod transfer;
|
||||
pub mod util;
|
||||
|
||||
use error::{HackrfError, Result};
|
||||
use ffi::SerialNumber;
|
||||
use transfer::{rx_callback, tx_callback, ReceiveCallback, TransferContext, TransmitCallback};
|
||||
|
||||
pub mod exports {
|
||||
pub use num_complex;
|
||||
}
|
||||
|
||||
static DEVICE_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// A HackRf device.
|
||||
#[derive(Clone)]
|
||||
pub struct HackRf {
|
||||
inner: Arc<HackRfInner>,
|
||||
}
|
||||
|
||||
struct HackRfInner {
|
||||
device: *mut ffi::HackrfDevice,
|
||||
user_data: AtomicPtr<c_void>,
|
||||
}
|
||||
|
||||
impl HackRf {
|
||||
/// Connects to a HackRF device.
|
||||
pub fn open() -> Result<HackRf> {
|
||||
if DEVICE_COUNT.fetch_add(1, Ordering::Relaxed) == 0 {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_init())? }
|
||||
}
|
||||
|
||||
let mut device = std::ptr::null_mut();
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_open(&mut device))? }
|
||||
|
||||
Ok(Self {
|
||||
inner: Arc::new(HackRfInner {
|
||||
device,
|
||||
user_data: AtomicPtr::new(ptr::null_mut()),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the internial representation of the HackRF device. This can be used
|
||||
/// with unsafe FFI functions if needed.
|
||||
#[inline(always)]
|
||||
pub fn device(&self) -> *mut ffi::HackrfDevice {
|
||||
self.inner.device
|
||||
}
|
||||
|
||||
/// Gets the device serial number.
|
||||
pub fn get_serial_number(&self) -> Result<SerialNumber> {
|
||||
let mut serial_number = SerialNumber::default();
|
||||
unsafe {
|
||||
HackrfError::from_id(ffi::hackrf_board_partid_serialno_read(
|
||||
self.device(),
|
||||
&mut serial_number,
|
||||
))?
|
||||
}
|
||||
Ok(serial_number)
|
||||
}
|
||||
|
||||
/// Read hackrf_board_id from a device and convert it to a DeviceType.
|
||||
pub fn get_device_type(&self) -> Result<DeviceType> {
|
||||
let mut value = 0;
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_board_id_read(self.device(), &mut value)) }?;
|
||||
Ok(DeviceType::from_id(value))
|
||||
}
|
||||
|
||||
/// Read HackRF firmware version as a string.
|
||||
pub fn version(&self) -> String {
|
||||
let mut version = vec![0; 32];
|
||||
|
||||
unsafe {
|
||||
ffi::hackrf_version_string_read(
|
||||
self.device(),
|
||||
version.as_mut_ptr(),
|
||||
version.len() as u8,
|
||||
);
|
||||
}
|
||||
|
||||
let end = version
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(version.len());
|
||||
|
||||
// Use from_raw_parts to handle both c_char=i8 (x86) and c_char=u8 (ARM)
|
||||
let version = unsafe { slice::from_raw_parts(version.as_ptr() as *const u8, end) };
|
||||
String::from_utf8_lossy(version).into_owned()
|
||||
}
|
||||
|
||||
/// Sets the center frequency in Hz.
|
||||
pub fn set_freq(&self, freq: u64) -> Result<()> {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_set_freq(self.device(), freq)) }
|
||||
}
|
||||
|
||||
/// Sets the sample rate in Hz.
|
||||
pub fn set_sample_rate(&self, sample_rate: u32) -> Result<()> {
|
||||
unsafe {
|
||||
HackrfError::from_id(ffi::hackrf_set_sample_rate_manual(
|
||||
self.device(),
|
||||
sample_rate,
|
||||
1,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the state of the externial amplifier.
|
||||
pub fn set_amp_enable(&self, enable: bool) -> Result<()> {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_set_amp_enable(self.device(), enable as u8)) }
|
||||
}
|
||||
|
||||
/// Low noise amplifier gain.
|
||||
/// Between 0d and 40d in steps of 8dB.
|
||||
pub fn set_lna_gain(&self, gain: u32) -> Result<()> {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_set_lna_gain(self.device(), gain)) }
|
||||
}
|
||||
|
||||
/// Variable gain amplifier. Range 0-62 (step 2dB).
|
||||
pub fn set_rxvga_gain(&self, gain: u32) -> Result<()> {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_set_vga_gain(self.device(), gain)) }
|
||||
}
|
||||
|
||||
/// Transmit variable gain amplifier. Range 0-47 (step 1dB).
|
||||
pub fn set_txvga_gain(&self, gain: u32) -> Result<()> {
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_set_txvga_gain(self.device(), gain)) }
|
||||
}
|
||||
|
||||
pub fn set_baseband_filter_bandwidth(&self, bandwidth_hz: u32) -> Result<()> {
|
||||
unsafe {
|
||||
HackrfError::from_id(ffi::hackrf_set_baseband_filter_bandwidth(
|
||||
self.device(),
|
||||
ffi::hackrf_compute_baseband_filter_bw(bandwidth_hz),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts transmitting samples from the device.
|
||||
pub fn start_tx(&self, callback: TransmitCallback, user_data: impl Any) -> Result<()> {
|
||||
let context = TransferContext::new(callback, self.clone(), Box::new(user_data));
|
||||
let callback = Box::leak(Box::new(context)) as *mut _ as *mut _;
|
||||
self.inner.user_data.store(callback, Ordering::Relaxed);
|
||||
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_start_tx(self.device(), tx_callback, callback)) }
|
||||
}
|
||||
|
||||
/// Stops the current transmit operation.
|
||||
pub fn stop_tx(&self) -> Result<()> {
|
||||
let user_data = &self.inner.user_data;
|
||||
let callback = user_data.swap(ptr::null_mut(), Ordering::Relaxed);
|
||||
if !callback.is_null() {
|
||||
let callback = unsafe { Box::from_raw(callback as *mut fn(*mut ffi::HackrfTransfer)) };
|
||||
drop(callback);
|
||||
}
|
||||
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_stop_tx(self.device())) }
|
||||
}
|
||||
|
||||
/// Starts receiving samples from the device.
|
||||
pub fn start_rx(&self, callback: ReceiveCallback, user_data: impl Any + Sync) -> Result<()> {
|
||||
let context = TransferContext::new(callback, self.clone(), Box::new(user_data));
|
||||
let callback = Box::leak(Box::new(context)) as *mut _ as *mut _;
|
||||
self.inner.user_data.store(callback, Ordering::Relaxed);
|
||||
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_start_rx(self.device(), rx_callback, callback)) }
|
||||
}
|
||||
|
||||
/// Stops the current receive operation.
|
||||
pub fn stop_rx(&self) -> Result<()> {
|
||||
let user_data = &self.inner.user_data;
|
||||
let callback = user_data.swap(ptr::null_mut(), Ordering::Relaxed);
|
||||
if !callback.is_null() {
|
||||
let callback = unsafe { Box::from_raw(callback as *mut fn(*mut ffi::HackrfTransfer)) };
|
||||
drop(callback);
|
||||
}
|
||||
|
||||
unsafe { HackrfError::from_id(ffi::hackrf_stop_rx(self.device())) }
|
||||
}
|
||||
|
||||
/// Returns true if the device is currently streaming samples (transmitting or receiving).
|
||||
pub fn is_streaming(&self) -> bool {
|
||||
unsafe { ffi::hackrf_is_streaming(self.device()) != 0 }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for HackRfInner {}
|
||||
unsafe impl Sync for HackRfInner {}
|
||||
|
||||
impl Drop for HackRf {
|
||||
fn drop(&mut self) {
|
||||
let _ = unsafe { HackrfError::from_id(ffi::hackrf_close(self.device())) };
|
||||
|
||||
if DEVICE_COUNT.fetch_sub(1, Ordering::Relaxed) == 1 {
|
||||
let _ = unsafe { HackrfError::from_id(ffi::hackrf_exit()) };
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+54
@@ -0,0 +1,54 @@
|
||||
use std::{any::Any, slice};
|
||||
|
||||
use num_complex::Complex;
|
||||
|
||||
use super::{ffi, HackRf};
|
||||
|
||||
pub type TransmitCallback = fn(hack_rf: &HackRf, samples: &mut [Complex<i8>], user: &dyn Any);
|
||||
pub type ReceiveCallback = fn(hack_rf: &HackRf, samples: &[Complex<i8>], user: &dyn Any);
|
||||
|
||||
pub struct TransferContext<Callback> {
|
||||
callback: Callback,
|
||||
hackrf: HackRf,
|
||||
user_data: Box<dyn Any>,
|
||||
}
|
||||
|
||||
impl<Callback> TransferContext<Callback> {
|
||||
pub(super) fn new(callback: Callback, hackrf: HackRf, user_data: Box<dyn Any>) -> Self {
|
||||
Self {
|
||||
callback,
|
||||
hackrf,
|
||||
user_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) extern "C" fn tx_callback(transfer: *mut ffi::HackrfTransfer) -> i32 {
|
||||
unsafe {
|
||||
let transfer = &mut *transfer;
|
||||
let context = &*(transfer.tx_ctx as *mut TransferContext<TransmitCallback>);
|
||||
|
||||
let buffer = slice::from_raw_parts_mut(
|
||||
transfer.buffer as *mut Complex<i8>,
|
||||
transfer.valid_length as usize / 2,
|
||||
);
|
||||
(context.callback)(&context.hackrf, buffer, &*context.user_data);
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) extern "C" fn rx_callback(transfer: *mut ffi::HackrfTransfer) -> i32 {
|
||||
unsafe {
|
||||
let transfer = &*transfer;
|
||||
let context = &*(transfer.rx_ctx as *mut TransferContext<ReceiveCallback>);
|
||||
|
||||
let buffer = slice::from_raw_parts(
|
||||
transfer.buffer as *const Complex<i8>,
|
||||
transfer.valid_length as usize / 2,
|
||||
);
|
||||
(context.callback)(&context.hackrf, buffer, &*context.user_data);
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
use num_complex::Complex;
|
||||
|
||||
pub trait ToComplexI8 {
|
||||
fn to_i8(self) -> Complex<i8>;
|
||||
}
|
||||
|
||||
pub trait ToComplexF32 {
|
||||
fn to_f32(self) -> Complex<f32>;
|
||||
}
|
||||
|
||||
impl ToComplexI8 for Complex<f32> {
|
||||
fn to_i8(self) -> Complex<i8> {
|
||||
Complex::new((self.re * 127.0) as i8, (self.im * 127.0) as i8)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToComplexF32 for Complex<i8> {
|
||||
fn to_f32(self) -> Complex<f32> {
|
||||
Complex::new(self.re as f32 / 127.0, self.im as f32 / 127.0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user