Version 1.0.0

This commit is contained in:
leviathan
2026-02-07 17:35:27 -05:00
parent c8bff9afd7
commit 4339895b41
43 changed files with 12218 additions and 3 deletions
+2
View File
@@ -0,0 +1,2 @@
/target
/REFERENCES
Generated
+1469
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
[package]
name = "kat"
version = "1.0.0"
edition = "2021"
description = "Keyfob Analysis Toolkit - HackRF signal capture, decode, and transmit tool"
authors = ["KAT Team"]
license = "BSD-3-Clause"
[dependencies]
# TUI
ratatui = "0.29"
crossterm = "0.28"
# HackRF library
libhackrf = "0.1"
# Signal processing
num-complex = "0.4"
# Async runtime
tokio = { version = "1.43", features = ["full"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
# INI config
configparser = "3"
# Config directory
dirs = "5.0"
# Time handling
chrono = { version = "0.4", features = ["serde"] }
# Error handling
anyhow = "1.0"
thiserror = "2.0"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Bit manipulation
bitvec = "1.0"
# TTY detection
atty = "0.2"
[build-dependencies]
pkg-config = "0.3"
[profile.release]
opt-level = 3
lto = true
+13
View File
@@ -0,0 +1,13 @@
Copyright (c) 2026 leviathan.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
YOU ACKNOWLEDGE THAT THIS SOFTWARE IS NOT DESIGNED, LICENSED OR INTENDED FOR USE IN THE DESIGN, CONSTRUCTION, OPERATION OR MAINTENANCE OF ANY MILITARY FACILITY
+246 -2
View File
@@ -1,3 +1,247 @@
# KAT
# KAT — Keyfob Analysis Toolkit
Keyfob Analysis Toolkit (KAT)
A terminal-based RF signal analysis tool for capturing, decoding, and retransmitting automotive keyfob signals using HackRF One. Built in Rust with a real-time TUI powered by `ratatui`.
---
## Features
- **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies
- **Multi-protocol decoding** — 14 protocol decoders covering Kia, Ford, Fiat, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, and Star Line
- **Rich signal detail** — modulation type, encryption method, serial, counter, key data, CRC, frequency, and raw level/duration pairs
- **Signal retransmission** — transmit Lock, Unlock, Trunk, and Panic commands from decoded captures
- **Export formats** — `.fob` (rich JSON with vehicle metadata, signal info, and capture data) and `.sub` (Flipper Zero compatible)
- **Import support** — load `.fob` files with automatic v1/v2 format detection
- **Persistent storage** — automatic capture saving to `~/.config/kat/captures/`
- **INI configuration** — human-readable config at `~/.config/kat/config.ini` (auto-created with comments on first run)
- **VIM-style command line** — `:freq`, `:lock`, `:unlock`, `:save`, `:load`, `:delete`, and more
- **Interactive TUI** — captures list with detail panel, signal action menu, radio settings menu, and fob export form
## Requirements
- **HackRF One** (or compatible SDR)
- **Rust 1.75+** (for building from source)
- **libhackrf** — HackRF C library and headers
### Installing Dependencies
**macOS:**
```bash
brew install hackrf
```
**Debian / Ubuntu:**
```bash
sudo apt install libhackrf-dev pkg-config
```
**Fedora:**
```bash
sudo dnf install hackrf-devel pkg-config
```
**Arch Linux:**
```bash
sudo pacman -S hackrf
```
## Building
```bash
git clone <repo-url> && cd prometheus
cargo build --release
```
The binary is placed at `target/release/kat`.
## Usage
```bash
./target/release/kat
```
KAT starts in an interactive terminal UI. If a HackRF device is not connected, the application runs in demo/offline mode so you can still view, import, and export captures.
### Keyboard Controls
| Key | Action |
|---|---|
| `j` / `k` or Arrow Up / Down | Navigate captures list |
| `Enter` | Open signal action menu on selected capture |
| `Tab` | Open radio settings menu (Frequency, LNA, VGA, AMP) |
| `r` | Toggle receive mode (start/stop RX) |
| `:` | Enter VIM-style command mode |
| `Esc` | Close menu / cancel current action |
| `q` | Quit |
### Signal Action Menu
Press `Enter` on a capture to open the action menu:
| Action | Description |
|---|---|
| TX Lock | Transmit lock command |
| TX Unlock | Transmit unlock command |
| TX Trunk | Transmit trunk release command |
| TX Panic | Transmit panic alarm command |
| Export .fob | Export signal with full vehicle + signal metadata |
| Export .sub | Export in Flipper Zero SubGHz format |
| Delete | Remove capture from the list |
### Fob Export
When exporting to `.fob`, a 6-step metadata form collects:
1. **Year** — vehicle model year
2. **Make** — manufacturer (auto-suggested from protocol)
3. **Model** — vehicle model
4. **Color** — vehicle color
5. **Trim** — trim level / package
6. **Notes** — free-form notes
The exported `.fob` file is a versioned JSON document (v2.0) containing:
```json
{
"version": "2.0",
"format": "KAT Fob Signal",
"signal": {
"protocol": "Kia V3/V4",
"modulation": "PWM",
"encryption": "KeeLoq",
"frequency_mhz": 433.92,
"serial": "0x1A2B3C",
"key": "0xDEADBEEF...",
"button": 1,
"counter": 1234,
"encoder_capable": true
},
"vehicle": {
"year": "2023",
"make": "Kia",
"model": "Sportage",
"color": "White",
"trim": "EX",
"notes": ""
},
"capture": {
"timestamp": "2026-02-07T12:00:00Z",
"raw_pairs": [[true, 400], [false, 800]]
}
}
```
### VIM-Style Commands
| Command | Description |
|---|---|
| `:freq <MHz>` | Set receive frequency (e.g. `:freq 433.92`) |
| `:lock <ID>` | Transmit lock signal for capture ID |
| `:unlock <ID>` | Transmit unlock signal for capture ID |
| `:trunk <ID>` | Transmit trunk release signal |
| `:panic <ID>` | Transmit panic alarm signal |
| `:save <ID>` | Save capture to file |
| `:delete <ID>` | Delete capture from list |
| `:load <file>` | Import capture from `.fob` or `.sub` file |
| `:q` | Quit application |
## Configuration
On first launch, KAT creates the following directory structure:
```
~/.config/kat/
├── config.ini # Application settings (auto-generated with comments)
├── captures/ # Persistent capture storage
└── exports/ # Default export directory for .fob / .sub files
```
The `config.ini` file is a commented INI file with the following settings:
```ini
[radio]
frequency = 433920000 # Default receive frequency in Hz
lna_gain = 32 # LNA gain (0-40 dB, step 8)
vga_gain = 40 # VGA gain (0-62 dB, step 2)
amp_enable = true # RF amplifier on/off
[storage]
export_directory = ~/.config/kat/exports # Where .fob/.sub files are saved
```
## Supported Protocols
| Protocol | Encoding | Encryption | Frequency |
|---|---|---|---|
| Kia V0 | PWM | Fixed Code | 433.92 MHz |
| Kia V1 | Manchester | Rolling Code | 433.92 MHz |
| Kia V2 | PWM | Rolling Code | 433.92 MHz |
| Kia V3/V4 | PWM | KeeLoq | 433.92 MHz |
| Kia V5 | PWM | Custom Mixer | 433.92 MHz |
| Kia V6 | PWM | AES-128 | 433.92 MHz |
| Ford V0 | PWM | Fixed Code | 433.92 MHz |
| Subaru | PWM | Rolling Code | 433.92 MHz |
| Suzuki | Manchester | Rolling Code | 433.92 MHz |
| Fiat V0 | Diff. Manchester | Rolling Code | 433.92 MHz |
| VAG (VW/Audi/Seat/Skoda) | Manchester | AUT64 / TEA | 433.92 / 434.42 MHz |
| Scher-Khan | PWM | Magic Code | 433.92 MHz |
| Star Line | PWM | KeeLoq | 433.92 MHz |
| PSA (Peugeot/Citroen) | PWM | Rolling Code | 433.92 MHz |
### Cryptographic Modules
- **KeeLoq** — full encrypt/decrypt with normal, secure, FAAC, and magic serial/XOR learning key derivation
- **AUT64** — 12-round block cipher for VAG type 1/3/4 signals
- **Key Store** — global thread-safe key management for manufacturer keys (KIA, VAG)
## Project Structure
```
src/
├── main.rs # Entry point, event loop, key handling
├── app.rs # Application state, radio events, signal actions
├── capture.rs # Capture data structure, modulation/encryption helpers
├── storage.rs # Config management, capture persistence, INI read/write
├── export/
│ ├── fob.rs # .fob JSON export/import (v1 + v2 format support)
│ └── flipper.rs # Flipper Zero .sub export
├── protocols/
│ ├── mod.rs # Protocol registry, decoder trait, duration_diff macro
│ ├── common.rs # Shared CRC, bit helpers, button codes
│ ├── keeloq_common.rs # KeeLoq cipher + learning key algorithms
│ ├── aut64.rs # AUT64 block cipher implementation
│ ├── keys.rs # Global key store (KIA, VAG key management)
│ ├── kia_v0.rs # Kia V0 decoder
│ ├── kia_v1.rs # Kia V1 decoder (Manchester)
│ ├── kia_v2.rs # Kia V2 decoder
│ ├── kia_v3_v4.rs # Kia V3/V4 decoder (KeeLoq)
│ ├── kia_v5.rs # Kia V5 decoder (mixer cipher)
│ ├── kia_v6.rs # Kia V6 decoder (AES-128)
│ ├── ford_v0.rs # Ford V0 decoder
│ ├── subaru.rs # Subaru decoder
│ ├── suzuki.rs # Suzuki decoder
│ ├── fiat_v0.rs # Fiat V0 decoder (diff. Manchester)
│ ├── vag.rs # VAG decoder/encoder (4 sub-types)
│ ├── scher_khan.rs # Scher-Khan decoder
│ ├── star_line.rs # Star Line decoder
│ └── psa.rs # PSA decoder
├── radio/
│ ├── hackrf.rs # HackRF One device control (RX/TX)
│ ├── demodulator.rs # AM/OOK demodulator (IQ -> level/duration pairs)
│ └── modulator.rs # Signal modulator (level/duration -> TX waveform)
└── ui/
├── layout.rs # Main TUI layout, fob metadata form overlay
├── captures_list.rs # Captures table + signal detail panel
├── signal_menu.rs # Signal action popup menu
├── settings_menu.rs # Radio settings popup menu
├── command.rs # VIM-style command line renderer
└── status_bar.rs # Bottom status bar (radio state, frequency, gains)
```
## License
BSD-3-Clause
+21
View File
@@ -0,0 +1,21 @@
fn main() {
// Use pkg-config to find libhackrf and emit the correct linker search paths.
// This is needed because the libhackrf Rust crate uses #[link(name = "hackrf")]
// but doesn't ship a build script to locate the system library.
match pkg_config::Config::new()
.atleast_version("0.5")
.probe("libhackrf")
{
Ok(lib) => {
for path in &lib.link_paths {
println!("cargo:rustc-link-search=native={}", path.display());
}
}
Err(_) => {
// Fallback: try common Homebrew / system paths
println!("cargo:rustc-link-search=native=/usr/local/lib");
println!("cargo:rustc-link-search=native=/opt/homebrew/lib");
println!("cargo:rustc-link-search=native=/usr/lib");
}
}
}
+999
View File
@@ -0,0 +1,999 @@
//! Application state management.
use anyhow::Result;
use std::sync::mpsc::{self, Receiver, Sender};
use crate::capture::{ButtonCommand, Capture};
use crate::protocols::ProtocolRegistry;
use crate::radio::HackRfController;
use crate::storage::Storage;
/// Input mode for the application
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputMode {
/// Normal navigation mode
Normal,
/// Command input mode (after pressing :)
Command,
/// Signal action popup menu
SignalMenu,
/// Tab bar - selecting which radio setting
SettingsSelect,
/// Editing a radio setting value
SettingsEdit,
/// Startup prompt: found .fob files, import? (y/n)
StartupImport,
/// Fob export metadata: editing year field
FobMetaYear,
/// Fob export metadata: editing make field
FobMetaMake,
/// Fob export metadata: editing model field
FobMetaModel,
/// Fob export metadata: editing region field
FobMetaRegion,
/// Fob export metadata: editing notes field
FobMetaNotes,
}
/// Items available in the signal action menu
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignalAction {
Lock,
Unlock,
Trunk,
Panic,
ExportFob,
ExportFlipper,
Delete,
}
impl SignalAction {
pub const ALL: [SignalAction; 7] = [
SignalAction::Lock,
SignalAction::Unlock,
SignalAction::Trunk,
SignalAction::Panic,
SignalAction::ExportFob,
SignalAction::ExportFlipper,
SignalAction::Delete,
];
pub fn label(&self) -> &'static str {
match self {
SignalAction::Lock => "TX Lock",
SignalAction::Unlock => "TX Unlock",
SignalAction::Trunk => "TX Trunk",
SignalAction::Panic => "TX Panic",
SignalAction::ExportFob => "Export .fob",
SignalAction::ExportFlipper => "Export .sub (Flipper)",
SignalAction::Delete => "Delete Signal",
}
}
}
/// Radio settings selectable via Tab
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingsField {
Freq,
Lna,
Vga,
Amp,
}
impl SettingsField {
pub const ALL: [SettingsField; 4] = [
SettingsField::Freq,
SettingsField::Lna,
SettingsField::Vga,
SettingsField::Amp,
];
pub fn label(&self) -> &'static str {
match self {
SettingsField::Freq => "Freq",
SettingsField::Lna => "LNA",
SettingsField::Vga => "VGA",
SettingsField::Amp => "AMP",
}
}
}
/// Common keyfob frequencies (Hz)
pub const PRESET_FREQUENCIES: [(u32, &str); 9] = [
(300_000_000, "300.00 MHz"),
(303_875_000, "303.875 MHz"),
(310_000_000, "310.00 MHz"),
(315_000_000, "315.00 MHz"),
(318_000_000, "318.00 MHz"),
(390_000_000, "390.00 MHz"),
(433_920_000, "433.92 MHz"),
(868_350_000, "868.35 MHz"),
(915_000_000, "915.00 MHz"),
];
/// LNA gain steps (dB)
pub const LNA_STEPS: [u32; 6] = [0, 8, 16, 24, 32, 40];
/// VGA gain steps (dB, subset for menu)
pub const VGA_STEPS: [u32; 8] = [0, 8, 16, 20, 24, 32, 40, 62];
/// Radio state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RadioState {
/// Not connected
Disconnected,
/// Connected but idle
Idle,
/// Receiving signals
Receiving,
/// Transmitting
#[allow(dead_code)]
Transmitting,
}
impl std::fmt::Display for RadioState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RadioState::Disconnected => write!(f, "DISCONNECTED"),
RadioState::Idle => write!(f, "IDLE"),
RadioState::Receiving => write!(f, "RX"),
RadioState::Transmitting => write!(f, "TX"),
}
}
}
/// Events from the radio subsystem
pub enum RadioEvent {
/// New signal captured
SignalCaptured(Capture),
/// Error occurred
Error(String),
/// State changed
#[allow(dead_code)]
StateChanged(RadioState),
}
/// Main application state
pub struct App {
/// Current input mode
pub input_mode: InputMode,
/// Command input buffer
pub command_input: String,
/// List of captures
pub captures: Vec<Capture>,
/// Currently selected capture index
pub selected_capture: Option<usize>,
/// Scroll offset for captures list
pub scroll_offset: usize,
/// Current frequency in Hz
pub frequency: u32,
/// LNA gain (0-40 dB, 8 dB steps)
pub lna_gain: u32,
/// VGA gain (0-62 dB, 2 dB steps)
pub vga_gain: u32,
/// Amplifier enabled
pub amp_enabled: bool,
/// Radio state
pub radio_state: RadioState,
/// Last error message
pub last_error: Option<String>,
/// Last status message
pub status_message: Option<String>,
// -- Signal action menu state --
/// Currently selected signal menu item index
pub signal_menu_index: usize,
// -- Settings menu state --
/// Currently selected settings field
pub settings_field_index: usize,
/// Currently selected value index within the settings field editor
pub settings_value_index: usize,
/// Next capture ID
next_capture_id: u32,
/// Storage manager
pub storage: Storage,
/// Protocol registry
protocols: ProtocolRegistry,
/// HackRF controller (optional - may not be connected)
hackrf: Option<HackRfController>,
/// Channel for radio events
radio_event_rx: Receiver<RadioEvent>,
/// Sender for radio events (cloned to radio thread)
#[allow(dead_code)]
radio_event_tx: Sender<RadioEvent>,
// -- Startup import state --
/// .fob files found on startup in export_dir
pub pending_fob_files: Vec<std::path::PathBuf>,
// -- .fob export metadata state --
/// Capture ID being exported (set before entering FobMeta modes)
pub export_capture_id: Option<u32>,
/// Year input buffer
pub fob_meta_year: String,
/// Make input buffer
pub fob_meta_make: String,
/// Model input buffer
pub fob_meta_model: String,
/// Region input buffer (e.g. NA, EU, APAC, etc.)
pub fob_meta_region: String,
/// Notes input buffer
pub fob_meta_notes: String,
}
impl App {
/// Create a new application instance
pub fn new() -> Result<Self> {
let storage = Storage::new()?;
let protocols = ProtocolRegistry::new();
let (radio_event_tx, radio_event_rx) = mpsc::channel();
// Try to initialize HackRF
let hackrf = match HackRfController::new(radio_event_tx.clone()) {
Ok(h) => {
tracing::info!("HackRF initialized successfully");
Some(h)
}
Err(e) => {
tracing::warn!("Failed to initialize HackRF: {}", e);
None
}
};
let radio_state = if hackrf.is_some() {
RadioState::Idle
} else {
RadioState::Disconnected
};
// Captures start empty — they are in-memory only and discarded on exit.
// The user is offered the chance to import .fob files from their exports folder.
let captures: Vec<Capture> = Vec::new();
let next_capture_id = 1u32;
// Use config defaults for radio settings
let frequency = storage.config.default_frequency;
let lna_gain = storage.config.default_lna_gain;
let vga_gain = storage.config.default_vga_gain;
let amp_enabled = storage.config.default_amp;
// Scan for .fob files in the export directory
let pending_fob_files = crate::export::fob::scan_fob_files(&storage.config.export_directory);
let initial_mode = if !pending_fob_files.is_empty() {
tracing::info!("Found {} .fob files in export dir", pending_fob_files.len());
InputMode::StartupImport
} else {
InputMode::Normal
};
Ok(Self {
input_mode: initial_mode,
command_input: String::new(),
captures,
selected_capture: None,
scroll_offset: 0,
frequency,
lna_gain,
vga_gain,
amp_enabled,
radio_state,
last_error: None,
status_message: None,
signal_menu_index: 0,
settings_field_index: 0,
settings_value_index: 0,
next_capture_id,
storage,
protocols,
hackrf,
radio_event_rx,
radio_event_tx,
pending_fob_files,
export_capture_id: None,
fob_meta_year: String::new(),
fob_meta_make: String::new(),
fob_meta_model: String::new(),
fob_meta_region: String::new(),
fob_meta_notes: String::new(),
})
}
/// Get the frequency in MHz
pub fn frequency_mhz(&self) -> f64 {
self.frequency as f64 / 1_000_000.0
}
/// Select the next capture in the list
pub fn next_capture(&mut self) {
if self.captures.is_empty() {
return;
}
self.selected_capture = Some(match self.selected_capture {
Some(i) => (i + 1).min(self.captures.len() - 1),
None => 0,
});
// Update scroll to keep selection visible
self.ensure_selection_visible();
}
/// Select the previous capture in the list
pub fn previous_capture(&mut self) {
if self.captures.is_empty() {
return;
}
self.selected_capture = Some(match self.selected_capture {
Some(i) => i.saturating_sub(1),
None => 0,
});
// Update scroll to keep selection visible
self.ensure_selection_visible();
}
/// Ensure the selected capture is visible in the scroll view
fn ensure_selection_visible(&mut self) {
if let Some(selected) = self.selected_capture {
// Assume visible area is about 15 items (will be adjusted by UI)
let visible_rows = 15;
if selected < self.scroll_offset {
self.scroll_offset = selected;
} else if selected >= self.scroll_offset + visible_rows {
self.scroll_offset = selected.saturating_sub(visible_rows - 1);
}
}
}
/// Toggle receiving state
pub fn toggle_receiving(&mut self) -> Result<()> {
// Clear any previous error when user takes action
self.last_error = None;
match self.radio_state {
RadioState::Disconnected => {
self.last_error = Some("HackRF not connected".to_string());
}
RadioState::Idle => {
if let Some(ref mut hackrf) = self.hackrf {
hackrf.start_receiving(self.frequency)?;
self.radio_state = RadioState::Receiving;
self.status_message = Some(format!("Receiving on {:.2} MHz", self.frequency_mhz()));
}
}
RadioState::Receiving => {
if let Some(ref mut hackrf) = self.hackrf {
hackrf.stop_receiving()?;
self.radio_state = RadioState::Idle;
self.status_message = Some("Stopped receiving".to_string());
}
}
RadioState::Transmitting => {
self.last_error = Some("Cannot change state while transmitting".to_string());
}
}
Ok(())
}
/// Execute a command
pub fn execute_command(&mut self, command: &str) -> Result<()> {
let parts: Vec<&str> = command.trim().split_whitespace().collect();
if parts.is_empty() {
return Ok(());
}
self.last_error = None;
self.status_message = None;
match parts[0] {
"q" | "quit" => {
// Will be handled by main loop
std::process::exit(0);
}
"freq" => {
if parts.len() < 2 {
self.last_error = Some("Usage: :freq <MHz>".to_string());
return Ok(());
}
match parts[1].parse::<f64>() {
Ok(mhz) => {
let hz = (mhz * 1_000_000.0) as u32;
self.set_frequency(hz)?;
}
Err(_) => {
self.last_error = Some("Invalid frequency".to_string());
}
}
}
"unlock" => self.transmit_command(parts.get(1), ButtonCommand::Unlock)?,
"lock" => self.transmit_command(parts.get(1), ButtonCommand::Lock)?,
"trunk" => self.transmit_command(parts.get(1), ButtonCommand::Trunk)?,
"panic" => self.transmit_command(parts.get(1), ButtonCommand::Panic)?,
"delete" => {
if parts.len() < 2 {
self.last_error = Some("Usage: :delete <ID> or :delete all".to_string());
return Ok(());
}
if parts[1].eq_ignore_ascii_case("all") {
self.delete_all_captures()?;
} else {
self.delete_capture(parts[1])?;
}
}
"lna" => {
if parts.len() < 2 {
self.last_error = Some("Usage: :lna <0-40>".to_string());
return Ok(());
}
match parts[1].parse::<u32>() {
Ok(gain) => self.set_lna_gain(gain)?,
Err(_) => {
self.last_error = Some("Invalid LNA gain value".to_string());
}
}
}
"vga" => {
if parts.len() < 2 {
self.last_error = Some("Usage: :vga <0-62>".to_string());
return Ok(());
}
match parts[1].parse::<u32>() {
Ok(gain) => self.set_vga_gain(gain)?,
Err(_) => {
self.last_error = Some("Invalid VGA gain value".to_string());
}
}
}
"amp" => {
if parts.len() < 2 {
// Toggle if no argument
self.toggle_amp()?;
} else {
match parts[1].to_lowercase().as_str() {
"on" | "1" | "true" => self.set_amp(true)?,
"off" | "0" | "false" => self.set_amp(false)?,
_ => {
self.last_error = Some("Usage: :amp [on|off]".to_string());
}
}
}
}
_ => {
self.last_error = Some(format!("Unknown command: {}", parts[0]));
}
}
Ok(())
}
/// Set the receive frequency
fn set_frequency(&mut self, hz: u32) -> Result<()> {
// Validate frequency range (common keyfob frequencies)
if hz < 300_000_000 || hz > 928_000_000 {
self.last_error = Some("Frequency must be between 300-928 MHz".to_string());
return Ok(());
}
self.frequency = hz;
if let Some(ref mut hackrf) = self.hackrf {
if self.radio_state == RadioState::Receiving {
hackrf.set_frequency(hz)?;
}
}
self.status_message = Some(format!("Frequency set to {:.2} MHz", hz as f64 / 1_000_000.0));
Ok(())
}
/// Set the LNA gain
fn set_lna_gain(&mut self, gain: u32) -> Result<()> {
// LNA gain is 0-40 dB in 8 dB steps
if gain > 40 {
self.last_error = Some("LNA gain must be 0-40 dB".to_string());
return Ok(());
}
// Round to nearest 8 dB step
let gain = (gain / 8) * 8;
self.lna_gain = gain;
if let Some(ref mut hackrf) = self.hackrf {
hackrf.set_lna_gain(gain)?;
}
self.status_message = Some(format!("LNA gain set to {} dB", gain));
Ok(())
}
/// Set the VGA gain
fn set_vga_gain(&mut self, gain: u32) -> Result<()> {
// VGA gain is 0-62 dB in 2 dB steps
if gain > 62 {
self.last_error = Some("VGA gain must be 0-62 dB".to_string());
return Ok(());
}
// Round to nearest 2 dB step
let gain = (gain / 2) * 2;
self.vga_gain = gain;
if let Some(ref mut hackrf) = self.hackrf {
hackrf.set_vga_gain(gain)?;
}
self.status_message = Some(format!("VGA gain set to {} dB", gain));
Ok(())
}
/// Toggle amplifier
fn toggle_amp(&mut self) -> Result<()> {
self.set_amp(!self.amp_enabled)
}
/// Set amplifier state
fn set_amp(&mut self, enabled: bool) -> Result<()> {
self.amp_enabled = enabled;
if let Some(ref mut hackrf) = self.hackrf {
hackrf.set_amp_enable(enabled)?;
}
self.status_message = Some(format!("Amp {}", if enabled { "enabled" } else { "disabled" }));
Ok(())
}
/// Transmit a command for a capture
fn transmit_command(&mut self, id_str: Option<&&str>, command: ButtonCommand) -> Result<()> {
use crate::protocols::DecodedSignal;
let id_str = match id_str {
Some(s) => s,
None => {
self.last_error = Some(format!("Usage: :{:?} <ID>", command).to_lowercase());
return Ok(());
}
};
let id: u32 = match id_str.parse() {
Ok(i) => i,
Err(_) => {
self.last_error = Some("Invalid capture ID".to_string());
return Ok(());
}
};
let capture = match self.captures.iter().find(|c| c.id == id) {
Some(c) => c.clone(),
None => {
self.last_error = Some(format!("Capture {} not found", id));
return Ok(());
}
};
if capture.protocol.is_none() {
self.last_error = Some("Cannot transmit: unknown protocol".to_string());
return Ok(());
}
let protocol_name = capture.protocol.as_ref().unwrap();
let protocol = match self.protocols.get(protocol_name) {
Some(p) => p,
None => {
self.last_error = Some(format!("Protocol {} not supported for encoding", protocol_name));
return Ok(());
}
};
if !protocol.supports_encoding() {
self.last_error = Some(format!("Protocol {} does not support encoding", protocol_name));
return Ok(());
}
// Create a DecodedSignal from the capture
let decoded = DecodedSignal {
serial: capture.serial,
button: capture.button,
counter: capture.counter,
crc_valid: capture.crc_valid,
data: capture.data,
data_count_bit: capture.data_count_bit,
encoder_capable: true,
};
// Generate the signal with the new button
let button_code = command.code();
let signal = match protocol.encode(&decoded, button_code) {
Some(s) => s,
None => {
self.last_error = Some("Failed to encode signal".to_string());
return Ok(());
}
};
// Transmit
if let Some(ref mut hackrf) = self.hackrf {
hackrf.transmit(&signal, capture.frequency)?;
self.status_message = Some(format!("Transmitted {:?} for capture {}", command, id));
} else {
self.last_error = Some("HackRF not connected".to_string());
}
Ok(())
}
/// Delete a capture (in-memory only — captures are not persisted)
fn delete_capture(&mut self, id_str: &str) -> Result<()> {
let id: u32 = match id_str.parse() {
Ok(i) => i,
Err(_) => {
self.last_error = Some("Invalid capture ID".to_string());
return Ok(());
}
};
let idx = match self.captures.iter().position(|c| c.id == id) {
Some(i) => i,
None => {
self.last_error = Some(format!("Capture {} not found", id));
return Ok(());
}
};
self.captures.remove(idx);
// Adjust selection
if let Some(sel) = self.selected_capture {
if sel >= self.captures.len() && !self.captures.is_empty() {
self.selected_capture = Some(self.captures.len() - 1);
} else if self.captures.is_empty() {
self.selected_capture = None;
}
}
self.status_message = Some(format!("Deleted capture {}", id));
Ok(())
}
/// Delete all captures (in-memory only)
fn delete_all_captures(&mut self) -> Result<()> {
let count = self.captures.len();
if count == 0 {
self.status_message = Some("No captures to delete".to_string());
return Ok(());
}
// Clear the list
self.captures.clear();
self.selected_capture = None;
self.scroll_offset = 0;
self.status_message = Some(format!("Deleted all {} captures", count));
Ok(())
}
/// Process pending radio events
pub fn process_radio_events(&mut self) -> Result<()> {
while let Ok(event) = self.radio_event_rx.try_recv() {
match event {
RadioEvent::SignalCaptured(mut capture) => {
// Assign ID
capture.id = self.next_capture_id;
self.next_capture_id += 1;
// Convert stored pairs to the format protocols expect
let pairs: Vec<crate::radio::LevelDuration> = capture.raw_pairs
.iter()
.map(|p| crate::radio::LevelDuration::new(p.level, p.duration_us))
.collect();
// Try to decode with registered protocols
if let Some((protocol_name, decoded)) = self.protocols.process_signal(&pairs, capture.frequency) {
capture.protocol = Some(protocol_name);
capture.serial = decoded.serial;
capture.button = decoded.button;
capture.counter = decoded.counter;
capture.crc_valid = decoded.crc_valid;
capture.data = decoded.data;
capture.data_count_bit = decoded.data_count_bit;
capture.status = if decoded.encoder_capable {
crate::capture::CaptureStatus::EncoderCapable
} else {
crate::capture::CaptureStatus::Decoded
};
}
// Captures are in-memory only — no auto-save to disk.
// Use Export (.fob / .sub) to persist a signal.
self.captures.push(capture);
// Auto-select and scroll to new capture
let new_idx = self.captures.len() - 1;
self.selected_capture = Some(new_idx);
self.ensure_selection_visible();
self.status_message = Some("New signal captured".to_string());
}
RadioEvent::Error(e) => {
self.last_error = Some(e);
}
RadioEvent::StateChanged(state) => {
self.radio_state = state;
}
}
}
Ok(())
}
// -- Signal Action Menu helpers --
/// Execute the currently selected signal action
pub fn execute_signal_action(&mut self) -> Result<()> {
let action = SignalAction::ALL[self.signal_menu_index];
let capture_id = match self.selected_capture {
Some(idx) if idx < self.captures.len() => self.captures[idx].id,
_ => {
self.last_error = Some("No capture selected".to_string());
return Ok(());
}
};
match action {
SignalAction::Lock => {
let id_str = capture_id.to_string();
self.transmit_command(Some(&&*id_str.as_str()), ButtonCommand::Lock)?;
}
SignalAction::Unlock => {
let id_str = capture_id.to_string();
self.transmit_command(Some(&&*id_str.as_str()), ButtonCommand::Unlock)?;
}
SignalAction::Trunk => {
let id_str = capture_id.to_string();
self.transmit_command(Some(&&*id_str.as_str()), ButtonCommand::Trunk)?;
}
SignalAction::Panic => {
let id_str = capture_id.to_string();
self.transmit_command(Some(&&*id_str.as_str()), ButtonCommand::Panic)?;
}
SignalAction::ExportFob => {
self.export_fob(capture_id)?;
}
SignalAction::ExportFlipper => {
self.export_flipper(capture_id)?;
}
SignalAction::Delete => {
let id_str = capture_id.to_string();
self.delete_capture(&id_str)?;
}
}
Ok(())
}
/// Start .fob export by entering metadata input mode
pub fn export_fob(&mut self, id: u32) -> Result<()> {
if !self.captures.iter().any(|c| c.id == id) {
self.last_error = Some(format!("Capture {} not found", id));
return Ok(());
}
// Pre-fill make from protocol
let make = self.captures.iter().find(|c| c.id == id).map(|c| {
Self::get_make_for_protocol(c.protocol_name()).to_string()
}).unwrap_or_default();
self.export_capture_id = Some(id);
self.fob_meta_year = String::new();
self.fob_meta_make = make;
self.fob_meta_model = String::new();
self.fob_meta_region = String::new();
self.fob_meta_notes = String::new();
self.input_mode = InputMode::FobMetaYear;
Ok(())
}
/// Complete the .fob export with collected metadata
pub fn complete_fob_export(&mut self) -> Result<()> {
let id = match self.export_capture_id {
Some(id) => id,
None => {
self.last_error = Some("No capture selected for export".to_string());
return Ok(());
}
};
let capture = match self.captures.iter().find(|c| c.id == id) {
Some(c) => c.clone(),
None => {
self.last_error = Some(format!("Capture {} not found", id));
return Ok(());
}
};
let export_dir = self.storage.export_dir().clone();
if !export_dir.exists() {
std::fs::create_dir_all(&export_dir)?;
}
let metadata = crate::export::fob::FobMetadata {
year: self.fob_meta_year.parse::<u32>().ok(),
make: self.fob_meta_make.clone(),
model: self.fob_meta_model.clone(),
region: self.fob_meta_region.clone(),
notes: self.fob_meta_notes.clone(),
};
let filename = format!(
"{}_{}.fob",
capture.protocol_name().replace(' ', "_").to_lowercase(),
capture.serial_hex()
);
let path = export_dir.join(&filename);
crate::export::fob::export_fob(
&capture,
&path,
self.storage.config.include_raw_pairs,
Some(&metadata),
)?;
self.export_capture_id = None;
self.status_message = Some(format!("Exported to {}", filename));
Ok(())
}
/// Import pending .fob files into captures list
pub fn import_fob_files(&mut self) -> Result<()> {
let files = std::mem::take(&mut self.pending_fob_files);
let mut imported = 0;
for path in &files {
match crate::export::fob::import_fob(path, self.next_capture_id) {
Ok(capture) => {
self.next_capture_id += 1;
self.captures.push(capture);
imported += 1;
}
Err(e) => {
tracing::warn!("Failed to import {:?}: {}", path, e);
}
}
}
if imported > 0 {
self.selected_capture = Some(0);
self.status_message = Some(format!("Imported {} .fob file(s)", imported));
}
Ok(())
}
/// Skip .fob import and start blank
pub fn skip_fob_import(&mut self) {
self.pending_fob_files.clear();
self.status_message = Some("Starting with no imported signals".to_string());
}
/// Export capture as Flipper .sub file
pub fn export_flipper(&mut self, id: u32) -> Result<()> {
let capture = match self.captures.iter().find(|c| c.id == id) {
Some(c) => c.clone(),
None => {
self.last_error = Some(format!("Capture {} not found", id));
return Ok(());
}
};
let export_dir = self.storage.export_dir().clone();
if !export_dir.exists() {
std::fs::create_dir_all(&export_dir)?;
}
let filename = format!(
"{}_{}.sub",
capture.protocol_name().replace(' ', "_").to_lowercase(),
capture.serial_hex()
);
let path = export_dir.join(&filename);
crate::export::flipper::export_flipper_sub(&capture, &path)?;
self.status_message = Some(format!("Exported to {}", filename));
Ok(())
}
// -- Settings Menu helpers --
/// Get the current value index for the active settings field
pub fn current_settings_value_index(&self) -> usize {
let field = SettingsField::ALL[self.settings_field_index];
match field {
SettingsField::Freq => {
PRESET_FREQUENCIES.iter().position(|(f, _)| *f == self.frequency).unwrap_or(0)
}
SettingsField::Lna => {
LNA_STEPS.iter().position(|&g| g == self.lna_gain).unwrap_or(0)
}
SettingsField::Vga => {
VGA_STEPS.iter().position(|&g| g == self.vga_gain).unwrap_or(0)
}
SettingsField::Amp => {
if self.amp_enabled { 0 } else { 1 }
}
}
}
/// Get the number of values for the active settings field
pub fn settings_value_count(&self) -> usize {
let field = SettingsField::ALL[self.settings_field_index];
match field {
SettingsField::Freq => PRESET_FREQUENCIES.len(),
SettingsField::Lna => LNA_STEPS.len(),
SettingsField::Vga => VGA_STEPS.len(),
SettingsField::Amp => 2, // ON / OFF
}
}
/// Apply the selected settings value
pub fn apply_settings_value(&mut self) -> Result<()> {
let field = SettingsField::ALL[self.settings_field_index];
match field {
SettingsField::Freq => {
if self.settings_value_index < PRESET_FREQUENCIES.len() {
let (hz, _) = PRESET_FREQUENCIES[self.settings_value_index];
self.set_frequency(hz)?;
}
}
SettingsField::Lna => {
if self.settings_value_index < LNA_STEPS.len() {
self.set_lna_gain(LNA_STEPS[self.settings_value_index])?;
}
}
SettingsField::Vga => {
if self.settings_value_index < VGA_STEPS.len() {
self.set_vga_gain(VGA_STEPS[self.settings_value_index])?;
}
}
SettingsField::Amp => {
self.set_amp(self.settings_value_index == 0)?;
}
}
Ok(())
}
/// Get the make for a protocol name
pub fn get_make_for_protocol(protocol: &str) -> &'static str {
match protocol {
p if p.starts_with("Kia") => "Kia/Hyundai",
p if p.starts_with("Ford") => "Ford",
p if p.starts_with("Fiat") => "Fiat",
"Subaru" => "Subaru",
"Suzuki" => "Suzuki",
"VAG" | "VW" => "VW/Audi/Seat/Skoda",
"PSA" => "Peugeot/Citroen",
"Star Line" => "Star Line",
"Scher-Khan" => "Scher-Khan",
_ => "Unknown",
}
}
/// Add a demo capture (for testing without HackRF)
#[allow(dead_code)]
pub fn add_demo_capture(&mut self) {
let capture = Capture {
id: self.next_capture_id,
timestamp: chrono::Utc::now(),
frequency: 433_920_000,
protocol: Some("Ford V0".to_string()),
serial: Some(0x1A2B3C4D),
button: Some(0x01),
counter: Some(1234),
crc_valid: true,
data: 0x5A2B3C4D00001234,
data_count_bit: 64,
raw_pairs: vec![],
status: crate::capture::CaptureStatus::EncoderCapable,
};
self.next_capture_id += 1;
self.captures.push(capture);
}
}
+263
View File
@@ -0,0 +1,263 @@
//! Capture data structures for storing decoded signals.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// Status of a captured signal
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CaptureStatus {
/// Signal decoded but protocol unknown
Unknown,
/// Signal decoded with known protocol
Decoded,
/// Signal can be re-encoded for transmission
EncoderCapable,
}
impl std::fmt::Display for CaptureStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CaptureStatus::Unknown => write!(f, "Unknown"),
CaptureStatus::Decoded => write!(f, "Decoded"),
CaptureStatus::EncoderCapable => write!(f, "Encode"),
}
}
}
/// Level+duration pair for storage (serializable version)
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct StoredLevelDuration {
pub level: bool,
pub duration_us: u32,
}
/// A captured keyfob signal
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Capture {
/// Unique identifier
pub id: u32,
/// When the signal was captured
pub timestamp: DateTime<Utc>,
/// Frequency in Hz
pub frequency: u32,
/// Protocol name if identified
pub protocol: Option<String>,
/// Serial number / key identifier (32-bit)
pub serial: Option<u32>,
/// Button code
pub button: Option<u8>,
/// Rolling counter value
pub counter: Option<u16>,
/// Whether CRC validation passed
pub crc_valid: bool,
/// Raw 64-bit data value
pub data: u64,
/// Number of valid bits in data
pub data_count_bit: usize,
/// Raw level+duration pairs
pub raw_pairs: Vec<StoredLevelDuration>,
/// Current status
pub status: CaptureStatus,
}
/// Modulation type used by protocol
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModulationType {
Unknown,
Pwm,
Manchester,
#[allow(dead_code)]
DifferentialManchester,
}
impl std::fmt::Display for ModulationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModulationType::Unknown => write!(f, "Unknown"),
ModulationType::Pwm => write!(f, "PWM"),
ModulationType::Manchester => write!(f, "Manchester"),
ModulationType::DifferentialManchester => write!(f, "Diff. Manchester"),
}
}
}
impl Capture {
/// Create a new capture from level+duration pairs
pub fn from_pairs(id: u32, frequency: u32, pairs: Vec<StoredLevelDuration>) -> Self {
Self {
id,
timestamp: Utc::now(),
frequency,
protocol: None,
serial: None,
button: None,
counter: None,
crc_valid: false,
data: 0,
data_count_bit: 0,
raw_pairs: pairs,
status: CaptureStatus::Unknown,
}
}
/// Get the serial as a hex string
pub fn serial_hex(&self) -> String {
match self.serial {
Some(s) => format!("{:07X}", s),
None => "-".to_string(),
}
}
/// Get the frequency in MHz as a string
pub fn frequency_mhz(&self) -> String {
format!("{:.2}MHz", self.frequency as f64 / 1_000_000.0)
}
/// Get the protocol name or "Unknown"
pub fn protocol_name(&self) -> &str {
self.protocol.as_deref().unwrap_or("Unknown")
}
/// Get CRC status as a string
pub fn crc_status(&self) -> &str {
if self.protocol.is_none() {
"-"
} else if self.crc_valid {
"OK"
} else {
"FAIL"
}
}
/// Get button name
pub fn button_name(&self) -> &str {
match self.button {
Some(0x01) => "Lock",
Some(0x02) => "Unlock",
Some(0x03) => "Lk+Un",
Some(0x04) => "Trunk",
Some(0x08) => "Panic",
Some(_) => "Other",
None => "-",
}
}
/// Get data as hex string
pub fn data_hex(&self) -> String {
if self.data_count_bit > 0 {
let bytes = (self.data_count_bit + 7) / 8;
format!("{:0width$X}", self.data, width = bytes * 2)
} else {
"-".to_string()
}
}
/// Get the modulation type based on the protocol
pub fn modulation(&self) -> ModulationType {
match self.protocol_name() {
// Manchester-encoded protocols
p if p.starts_with("Kia V1") => ModulationType::Manchester,
p if p.starts_with("Kia V2") => ModulationType::Manchester,
p if p.starts_with("Kia V5") => ModulationType::Manchester,
p if p.starts_with("Kia V6") => ModulationType::Manchester,
"Ford V0" => ModulationType::Manchester,
"Fiat V0" => ModulationType::Manchester,
"PSA" => ModulationType::Manchester,
"VAG" => ModulationType::Manchester,
// PWM-encoded protocols
p if p.starts_with("Kia V0") => ModulationType::Pwm,
p if p.starts_with("Kia V3") => ModulationType::Pwm,
p if p.starts_with("Kia V4") => ModulationType::Pwm,
"Subaru" => ModulationType::Pwm,
"Suzuki" => ModulationType::Pwm,
"Star Line" => ModulationType::Pwm,
"Scher-Khan" => ModulationType::Pwm,
// Unknown
_ => ModulationType::Unknown,
}
}
/// Get the encryption/encoding type based on the protocol
pub fn encryption_type(&self) -> &'static str {
match self.protocol_name() {
p if p.starts_with("Kia V3") || p.starts_with("Kia V4") => "KeeLoq",
"Star Line" => "KeeLoq",
"PSA" => "XTEA/XOR",
"VAG" => "AUT64/XTEA",
"Scher-Khan" => "Magic Code",
"Subaru" | "Suzuki" => "Rolling Code",
p if p.starts_with("Ford") => "Fixed Code",
p if p.starts_with("Fiat") => "Fixed Code",
p if p.starts_with("Kia V0") => "Fixed Code",
p if p.starts_with("Kia V1") || p.starts_with("Kia V2") => "Fixed Code",
p if p.starts_with("Kia V5") || p.starts_with("Kia V6") => "Fixed Code",
_ => "Unknown",
}
}
/// Get the counter as a formatted string
pub fn counter_str(&self) -> String {
match self.counter {
Some(c) => format!("{:04X}", c),
None => "-".to_string(),
}
}
/// Get the timestamp formatted for display
pub fn timestamp_short(&self) -> String {
self.timestamp.format("%H:%M:%S").to_string()
}
/// Get full timestamp for detail display
pub fn timestamp_full(&self) -> String {
self.timestamp.format("%Y-%m-%d %H:%M:%S UTC").to_string()
}
/// Get button code as hex
pub fn button_hex(&self) -> String {
match self.button {
Some(b) => format!("0x{:02X}", b),
None => "-".to_string(),
}
}
/// Get data bits description
pub fn data_bits_str(&self) -> String {
if self.data_count_bit > 0 {
format!("{} bits", self.data_count_bit)
} else {
"-".to_string()
}
}
/// Whether this capture has raw signal data for replay
pub fn has_raw_data(&self) -> bool {
!self.raw_pairs.is_empty()
}
/// Number of raw signal transitions
pub fn raw_pair_count(&self) -> usize {
self.raw_pairs.len()
}
}
/// Button types for keyfob commands
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonCommand {
Unlock,
Lock,
Trunk,
Panic,
}
impl ButtonCommand {
/// Get the button code for this command
pub fn code(&self) -> u8 {
match self {
ButtonCommand::Unlock => 0x02,
ButtonCommand::Lock => 0x01,
ButtonCommand::Trunk => 0x04,
ButtonCommand::Panic => 0x08,
}
}
}
+49
View File
@@ -0,0 +1,49 @@
//! Flipper Zero .sub export format.
//!
//! Outputs files in the Flipper SubGhz RAW format with alternating
//! positive (high) and negative (low) durations in microseconds.
use anyhow::Result;
use std::path::Path;
use crate::capture::Capture;
/// Export a capture to Flipper Zero .sub RAW format
pub fn export_flipper_sub(capture: &Capture, path: &Path) -> Result<()> {
if capture.raw_pairs.is_empty() {
return Err(anyhow::anyhow!("No raw signal data to export"));
}
let mut lines = Vec::new();
// Header
lines.push("Filetype: Flipper SubGhz RAW File".to_string());
lines.push("Version: 1".to_string());
lines.push(format!("Frequency: {}", capture.frequency));
lines.push("Preset: FuriHalSubGhzPresetOok270Async".to_string());
lines.push("Protocol: RAW".to_string());
// Convert raw_pairs to alternating +/- durations
// Flipper format: positive values = HIGH, negative values = LOW
let mut raw_data = Vec::new();
for pair in &capture.raw_pairs {
let duration = pair.duration_us as i64;
if pair.level {
raw_data.push(duration);
} else {
raw_data.push(-duration);
}
}
// Write RAW_Data lines (max ~512 values per line for readability)
const MAX_PER_LINE: usize = 512;
for chunk in raw_data.chunks(MAX_PER_LINE) {
let values: Vec<String> = chunk.iter().map(|v| v.to_string()).collect();
lines.push(format!("RAW_Data: {}", values.join(" ")));
}
let content = lines.join("\n") + "\n";
std::fs::write(path, content)?;
tracing::info!("Exported Flipper .sub to {:?}", path);
Ok(())
}
+380
View File
@@ -0,0 +1,380 @@
//! .fob export/import format - rich JSON metadata for captured keyfob signals.
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::app::App;
use crate::capture::{Capture, CaptureStatus, StoredLevelDuration};
/// User-provided metadata for .fob export
#[derive(Debug, Clone, Default)]
pub struct FobMetadata {
pub year: Option<u32>,
pub make: String,
pub model: String,
pub region: String,
pub notes: String,
}
/// Top-level .fob file structure
#[derive(Serialize, Deserialize)]
pub struct FobFile {
pub version: String,
pub format: String,
pub signal: FobSignalInfo,
pub vehicle: FobVehicleInfo,
pub capture: FobCapture,
}
/// Signal-level metadata (derived from protocol)
#[derive(Serialize, Deserialize)]
pub struct FobSignalInfo {
pub protocol: String,
pub frequency: u32,
pub frequency_mhz: String,
pub modulation: String,
pub encryption: String,
pub data_bits: usize,
pub data_hex: String,
pub serial: String,
pub key: String,
#[serde(default)]
pub button: Option<u8>,
pub button_name: String,
#[serde(default)]
pub counter: Option<u16>,
pub crc_valid: bool,
pub encoder_capable: bool,
}
/// Vehicle metadata (user-provided + auto-detected)
#[derive(Serialize, Deserialize)]
pub struct FobVehicleInfo {
#[serde(default)]
pub year: Option<u32>,
pub make: String,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub region: Option<String>,
#[serde(default)]
pub notes: Option<String>,
}
/// Capture data within a .fob file (timing + raw data)
#[derive(Serialize, Deserialize)]
pub struct FobCapture {
pub timestamp: String,
/// Raw data value (hex string) for signal reconstruction
#[serde(default)]
pub raw_data_hex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub raw_pairs: Option<Vec<FobPair>>,
#[serde(default)]
pub raw_pair_count: usize,
}
/// A single level+duration pair in the .fob file
#[derive(Serialize, Deserialize)]
pub struct FobPair {
pub level: bool,
pub duration_us: u32,
}
/// Export a capture to .fob format with optional user metadata
pub fn export_fob(
capture: &Capture,
path: &Path,
include_raw: bool,
metadata: Option<&FobMetadata>,
) -> Result<()> {
let protocol_name = capture.protocol_name().to_string();
let make = metadata
.map(|m| m.make.clone())
.filter(|m| !m.is_empty())
.unwrap_or_else(|| App::get_make_for_protocol(&protocol_name).to_string());
let model = metadata.and_then(|m| {
if m.model.is_empty() {
None
} else {
Some(m.model.clone())
}
});
let year = metadata.and_then(|m| m.year);
let region = metadata.and_then(|m| {
if m.region.is_empty() {
None
} else {
Some(m.region.clone())
}
});
let notes = metadata.and_then(|m| {
if m.notes.is_empty() {
None
} else {
Some(m.notes.clone())
}
});
let raw_pairs = if include_raw && !capture.raw_pairs.is_empty() {
Some(
capture
.raw_pairs
.iter()
.map(|p| FobPair {
level: p.level,
duration_us: p.duration_us,
})
.collect(),
)
} else {
None
};
let fob = FobFile {
version: "2.0".to_string(),
format: "kat-fob".to_string(),
signal: FobSignalInfo {
protocol: protocol_name.clone(),
frequency: capture.frequency,
frequency_mhz: capture.frequency_mhz(),
modulation: capture.modulation().to_string(),
encryption: capture.encryption_type().to_string(),
data_bits: capture.data_count_bit,
data_hex: capture.data_hex(),
serial: capture.serial_hex(),
key: capture.data_hex(),
button: capture.button,
button_name: capture.button_name().to_string(),
counter: capture.counter,
crc_valid: capture.crc_valid,
encoder_capable: capture.status == CaptureStatus::EncoderCapable,
},
vehicle: FobVehicleInfo {
year,
make,
model,
region,
notes,
},
capture: FobCapture {
timestamp: capture.timestamp.to_rfc3339(),
raw_data_hex: Some(capture.data_hex()),
raw_pair_count: capture.raw_pairs.len(),
raw_pairs,
},
};
let json = serde_json::to_string_pretty(&fob)?;
std::fs::write(path, json)?;
tracing::info!("Exported .fob v2 to {:?}", path);
Ok(())
}
/// Import a .fob file and return a Capture (supports v1 and v2 formats)
pub fn import_fob(path: &Path, next_id: u32) -> Result<Capture> {
let content =
std::fs::read_to_string(path).with_context(|| format!("Failed to read {:?}", path))?;
// Try v2 format first
if let Ok(fob) = serde_json::from_str::<FobFile>(&content) {
return import_fob_v2(&fob, next_id);
}
// Fall back to v1 format
let fob: FobFileV1 =
serde_json::from_str(&content).with_context(|| format!("Failed to parse {:?}", path))?;
import_fob_v1(&fob, next_id)
}
// --- V1 compatibility types ---
/// Legacy v1 .fob file structure
#[derive(Serialize, Deserialize)]
struct FobFileV1 {
#[allow(dead_code)]
pub version: String,
#[allow(dead_code)]
pub format: String,
pub capture: FobCaptureV1,
}
/// Legacy v1 capture data
#[derive(Serialize, Deserialize)]
struct FobCaptureV1 {
pub timestamp: String,
pub frequency: u32,
pub protocol: String,
#[serde(default)]
pub year: Option<u32>,
#[allow(dead_code)]
pub make: String,
#[serde(default)]
#[allow(dead_code)]
pub model: Option<String>,
pub serial: String,
pub key: String,
#[serde(default)]
pub button: Option<u8>,
#[allow(dead_code)]
pub button_name: String,
#[serde(default)]
pub counter: Option<u16>,
#[allow(dead_code)]
pub encryption: String,
pub crc_valid: bool,
pub data_bits: usize,
#[serde(default)]
pub data_hex: Option<String>,
#[serde(default)]
pub raw_pairs: Option<Vec<FobPair>>,
}
fn import_fob_v2(fob: &FobFile, next_id: u32) -> Result<Capture> {
let sig = &fob.signal;
let cap = &fob.capture;
// Parse serial from hex string
let serial = u32::from_str_radix(sig.serial.trim_start_matches("0x"), 16).ok();
// Parse data from hex string
let data = cap
.raw_data_hex
.as_deref()
.or(Some(sig.data_hex.as_str()))
.and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok())
.unwrap_or(0);
// Parse timestamp
let timestamp = chrono::DateTime::parse_from_rfc3339(&cap.timestamp)
.map(|dt| dt.with_timezone(&chrono::Utc))
.unwrap_or_else(|_| chrono::Utc::now());
// Reconstruct raw pairs if present
let raw_pairs: Vec<StoredLevelDuration> = cap
.raw_pairs
.as_ref()
.map(|pairs| {
pairs
.iter()
.map(|p| StoredLevelDuration {
level: p.level,
duration_us: p.duration_us,
})
.collect()
})
.unwrap_or_default();
let protocol = if sig.protocol == "Unknown" {
None
} else {
Some(sig.protocol.clone())
};
let status = if sig.encoder_capable && !raw_pairs.is_empty() {
CaptureStatus::EncoderCapable
} else if protocol.is_some() {
CaptureStatus::Decoded
} else {
CaptureStatus::Unknown
};
Ok(Capture {
id: next_id,
timestamp,
frequency: sig.frequency,
protocol,
serial,
button: sig.button,
counter: sig.counter,
crc_valid: sig.crc_valid,
data,
data_count_bit: sig.data_bits,
raw_pairs,
status,
})
}
fn import_fob_v1(fob: &FobFileV1, next_id: u32) -> Result<Capture> {
let cap = &fob.capture;
// Parse serial from hex string
let serial = u32::from_str_radix(cap.serial.trim_start_matches("0x"), 16).ok();
// Parse data from hex string
let data = cap
.data_hex
.as_deref()
.or(Some(cap.key.as_str()))
.and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok())
.unwrap_or(0);
// Parse timestamp
let timestamp = chrono::DateTime::parse_from_rfc3339(&cap.timestamp)
.map(|dt| dt.with_timezone(&chrono::Utc))
.unwrap_or_else(|_| chrono::Utc::now());
// Reconstruct raw pairs if present
let raw_pairs: Vec<StoredLevelDuration> = cap
.raw_pairs
.as_ref()
.map(|pairs| {
pairs
.iter()
.map(|p| StoredLevelDuration {
level: p.level,
duration_us: p.duration_us,
})
.collect()
})
.unwrap_or_default();
let protocol = if cap.protocol == "Unknown" {
None
} else {
Some(cap.protocol.clone())
};
let status = if protocol.is_some() && !raw_pairs.is_empty() {
CaptureStatus::EncoderCapable
} else if protocol.is_some() {
CaptureStatus::Decoded
} else {
CaptureStatus::Unknown
};
Ok(Capture {
id: next_id,
timestamp,
frequency: cap.frequency,
protocol,
serial,
button: cap.button,
counter: cap.counter,
crc_valid: cap.crc_valid,
data,
data_count_bit: cap.data_bits,
raw_pairs,
status,
})
}
/// Scan a directory for .fob files and return their paths
pub fn scan_fob_files(dir: &Path) -> Vec<std::path::PathBuf> {
if !dir.exists() || !dir.is_dir() {
return Vec::new();
}
let mut files = Vec::new();
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |e| e == "fob") {
files.push(path);
}
}
}
files.sort();
files
}
+4
View File
@@ -0,0 +1,4 @@
//! Export formats for captured signals.
pub mod fob;
pub mod flipper;
+362
View File
@@ -0,0 +1,362 @@
//! KAT - Keyfob Analysis Toolkit
//!
//! A terminal UI application for capturing, decoding, and transmitting
//! keyfob signals using HackRF.
mod app;
mod capture;
mod export;
mod protocols;
mod radio;
mod storage;
mod ui;
use anyhow::Result;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io::{self, Write};
use std::panic;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use app::{App, InputMode, SignalAction, SettingsField};
use ui::draw_ui;
const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Restore the terminal to normal state (for panic handler)
fn restore_terminal_panic() {
// Disable raw mode first
let _ = disable_raw_mode();
// Write escape sequences directly to stdout
let mut stdout = io::stdout();
// Leave alternate screen: ESC [ ? 1049 l
let _ = stdout.write_all(b"\x1b[?1049l");
// Show cursor: ESC [ ? 25 h
let _ = stdout.write_all(b"\x1b[?25h");
let _ = stdout.flush();
}
fn main() -> Result<()> {
// Check if we have a TTY first
if !atty::is(atty::Stream::Stdout) {
eprintln!("Error: KAT requires a terminal (TTY) to run.");
eprintln!("Please run this program in a real terminal, not via a script or IDE runner.");
std::process::exit(1);
}
// Set up panic hook to restore terminal on panic
let default_panic = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
restore_terminal_panic();
default_panic(panic_info);
}));
// Initialize logging to a file (not stdout, which would corrupt TUI)
let log_file = crate::storage::resolve_config_dir()
.unwrap_or_else(|| std::path::PathBuf::from(".").join("KAT"))
.join("kat.log");
// Create log directory if needed
if let Some(parent) = log_file.parent() {
let _ = std::fs::create_dir_all(parent);
}
// Set up file-based logging
if let Ok(file) = std::fs::File::create(&log_file) {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "kat=info".into()),
)
.with(
tracing_subscriber::fmt::layer()
.with_target(false)
.with_writer(std::sync::Mutex::new(file))
.with_ansi(false)
)
.init();
}
tracing::info!("Starting KAT v{}", VERSION);
// Setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// Create app and run
let mut app = App::new()?;
let res = run_app(&mut terminal, &mut app);
// Restore terminal properly using the terminal's backend
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen
)?;
terminal.show_cursor()?;
if let Err(err) = res {
eprintln!("Error: {err:?}");
return Err(err);
}
Ok(())
}
fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
loop {
terminal.draw(|f| draw_ui(f, app))?;
if event::poll(std::time::Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match app.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char(':') => {
app.input_mode = InputMode::Command;
app.command_input.clear();
}
KeyCode::Char('j') | KeyCode::Down => {
app.next_capture();
}
KeyCode::Char('k') | KeyCode::Up => {
app.previous_capture();
}
KeyCode::Char('r') => {
app.toggle_receiving()?;
}
KeyCode::Enter => {
// Open signal action menu if a capture is selected
if app.selected_capture.is_some() && !app.captures.is_empty() {
app.input_mode = InputMode::SignalMenu;
app.signal_menu_index = 0;
}
}
KeyCode::Tab => {
// Open settings selector
app.input_mode = InputMode::SettingsSelect;
app.settings_field_index = 0;
}
_ => {}
},
InputMode::Command => match key.code {
KeyCode::Enter => {
let command = app.command_input.clone();
app.execute_command(&command)?;
app.command_input.clear();
app.input_mode = InputMode::Normal;
}
KeyCode::Char(c) => {
app.command_input.push(c);
}
KeyCode::Backspace => {
app.command_input.pop();
}
KeyCode::Esc => {
app.command_input.clear();
app.input_mode = InputMode::Normal;
}
_ => {}
},
InputMode::SignalMenu => match key.code {
KeyCode::Up | KeyCode::Char('k') => {
if app.signal_menu_index > 0 {
app.signal_menu_index -= 1;
}
}
KeyCode::Down | KeyCode::Char('j') => {
if app.signal_menu_index < SignalAction::ALL.len() - 1 {
app.signal_menu_index += 1;
}
}
KeyCode::Enter => {
app.execute_signal_action()?;
// Only return to Normal if the action didn't change
// input mode (e.g. ExportFob sets FobMetaYear)
if app.input_mode == InputMode::SignalMenu {
app.input_mode = InputMode::Normal;
}
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
_ => {}
},
InputMode::SettingsSelect => match key.code {
KeyCode::Left | KeyCode::Char('h') => {
if app.settings_field_index > 0 {
app.settings_field_index -= 1;
}
}
KeyCode::Right | KeyCode::Char('l') => {
if app.settings_field_index < SettingsField::ALL.len() - 1 {
app.settings_field_index += 1;
}
}
KeyCode::Tab => {
// Cycle through fields
app.settings_field_index =
(app.settings_field_index + 1) % SettingsField::ALL.len();
}
KeyCode::Enter => {
// Enter edit mode for this field
app.settings_value_index = app.current_settings_value_index();
app.input_mode = InputMode::SettingsEdit;
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
_ => {}
},
InputMode::SettingsEdit => match key.code {
KeyCode::Up | KeyCode::Char('k') => {
if app.settings_value_index > 0 {
app.settings_value_index -= 1;
}
}
KeyCode::Down | KeyCode::Char('j') => {
let max = app.settings_value_count();
if app.settings_value_index < max - 1 {
app.settings_value_index += 1;
}
}
KeyCode::Enter => {
app.apply_settings_value()?;
app.input_mode = InputMode::SettingsSelect;
}
KeyCode::Esc => {
app.input_mode = InputMode::SettingsSelect;
}
_ => {}
},
// Startup: found .fob files, y/n to import
InputMode::StartupImport => match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
app.import_fob_files()?;
app.input_mode = InputMode::Normal;
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
app.skip_fob_import();
app.input_mode = InputMode::Normal;
}
_ => {}
},
// .fob export metadata: Year
InputMode::FobMetaYear => match key.code {
KeyCode::Enter => {
app.input_mode = InputMode::FobMetaMake;
}
KeyCode::Char(c) if c.is_ascii_digit() => {
if app.fob_meta_year.len() < 4 {
app.fob_meta_year.push(c);
}
}
KeyCode::Backspace => {
app.fob_meta_year.pop();
}
KeyCode::Esc => {
app.export_capture_id = None;
app.input_mode = InputMode::Normal;
}
_ => {}
},
// .fob export metadata: Make
InputMode::FobMetaMake => match key.code {
KeyCode::Enter => {
app.input_mode = InputMode::FobMetaModel;
}
KeyCode::Char(c) => {
app.fob_meta_make.push(c);
}
KeyCode::Backspace => {
app.fob_meta_make.pop();
}
KeyCode::Esc => {
app.export_capture_id = None;
app.input_mode = InputMode::Normal;
}
_ => {}
},
// .fob export metadata: Model -> Region
InputMode::FobMetaModel => match key.code {
KeyCode::Enter => {
app.input_mode = InputMode::FobMetaRegion;
}
KeyCode::Char(c) => {
app.fob_meta_model.push(c);
}
KeyCode::Backspace => {
app.fob_meta_model.pop();
}
KeyCode::Esc => {
app.export_capture_id = None;
app.input_mode = InputMode::Normal;
}
_ => {}
},
// .fob export metadata: Region -> Notes
InputMode::FobMetaRegion => match key.code {
KeyCode::Enter => {
app.input_mode = InputMode::FobMetaNotes;
}
KeyCode::Char(c) => {
app.fob_meta_region.push(c);
}
KeyCode::Backspace => {
app.fob_meta_region.pop();
}
KeyCode::Esc => {
app.export_capture_id = None;
app.input_mode = InputMode::Normal;
}
_ => {}
},
// .fob export metadata: Notes -> Export
InputMode::FobMetaNotes => match key.code {
KeyCode::Enter => {
app.complete_fob_export()?;
app.input_mode = InputMode::Normal;
}
KeyCode::Char(c) => {
app.fob_meta_notes.push(c);
}
KeyCode::Backspace => {
app.fob_meta_notes.pop();
}
KeyCode::Esc => {
app.export_capture_id = None;
app.input_mode = InputMode::Normal;
}
_ => {}
},
}
}
}
}
// Process any pending radio events
app.process_radio_events()?;
}
}
+290
View File
@@ -0,0 +1,290 @@
//! AUT64 block cipher implementation
//!
//! Ported from protopirate's aut64.c
//!
//! AUT64 algorithm: 12 rounds, 8-byte block/key size
//! Based on: Reference AUT64 implementation
//! See: https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf
pub const AUT64_NUM_ROUNDS: usize = 12;
pub const AUT64_BLOCK_SIZE: usize = 8;
pub const AUT64_KEY_SIZE: usize = 8;
pub const AUT64_PBOX_SIZE: usize = 8;
pub const AUT64_SBOX_SIZE: usize = 16;
#[allow(dead_code)]
pub const AUT64_KEY_STRUCT_PACKED_SIZE: usize = 16;
/// AUT64 key structure
#[derive(Debug, Clone)]
pub struct Aut64Key {
pub index: u8,
pub key: [u8; AUT64_KEY_SIZE],
pub pbox: [u8; AUT64_PBOX_SIZE],
pub sbox: [u8; AUT64_SBOX_SIZE],
}
impl Default for Aut64Key {
fn default() -> Self {
Self {
index: 0,
key: [0u8; AUT64_KEY_SIZE],
pbox: [0u8; AUT64_PBOX_SIZE],
sbox: [0u8; AUT64_SBOX_SIZE],
}
}
}
/// Round-dependent upper-nibble lookup table
static TABLE_LN: [[u8; 8]; AUT64_NUM_ROUNDS] = [
[0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3], // Round 0
[0x5, 0x4, 0x7, 0x6, 0x1, 0x0, 0x3, 0x2], // Round 1
[0x6, 0x7, 0x4, 0x5, 0x2, 0x3, 0x0, 0x1], // Round 2
[0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0], // Round 3
[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7], // Round 4
[0x1, 0x0, 0x3, 0x2, 0x5, 0x4, 0x7, 0x6], // Round 5
[0x2, 0x3, 0x0, 0x1, 0x6, 0x7, 0x4, 0x5], // Round 6
[0x3, 0x2, 0x1, 0x0, 0x7, 0x6, 0x5, 0x4], // Round 7
[0x5, 0x4, 0x7, 0x6, 0x1, 0x0, 0x3, 0x2], // Round 8
[0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3], // Round 9
[0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0], // Round 10
[0x6, 0x7, 0x4, 0x5, 0x2, 0x3, 0x0, 0x1], // Round 11
];
/// Round-dependent lower-nibble lookup table
static TABLE_UN: [[u8; 8]; AUT64_NUM_ROUNDS] = [
[0x1, 0x0, 0x3, 0x2, 0x5, 0x4, 0x7, 0x6], // Round 0
[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7], // Round 1
[0x3, 0x2, 0x1, 0x0, 0x7, 0x6, 0x5, 0x4], // Round 2
[0x2, 0x3, 0x0, 0x1, 0x6, 0x7, 0x4, 0x5], // Round 3
[0x5, 0x4, 0x7, 0x6, 0x1, 0x0, 0x3, 0x2], // Round 4
[0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3], // Round 5
[0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0], // Round 6
[0x6, 0x7, 0x4, 0x5, 0x2, 0x3, 0x0, 0x1], // Round 7
[0x3, 0x2, 0x1, 0x0, 0x7, 0x6, 0x5, 0x4], // Round 8
[0x2, 0x3, 0x0, 0x1, 0x6, 0x7, 0x4, 0x5], // Round 9
[0x1, 0x0, 0x3, 0x2, 0x5, 0x4, 0x7, 0x6], // Round 10
[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7], // Round 11
];
/// GF(2^4) multiplication table (nibble offset table)
#[rustfmt::skip]
static TABLE_OFFSET: [u8; 256] = [
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 0
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, // 1
0x0, 0x2, 0x4, 0x6, 0x8, 0xA, 0xC, 0xE, 0x3, 0x1, 0x7, 0x5, 0xB, 0x9, 0xF, 0xD, // 2
0x0, 0x3, 0x6, 0x5, 0xC, 0xF, 0xA, 0x9, 0xB, 0x8, 0xD, 0xE, 0x7, 0x4, 0x1, 0x2, // 3
0x0, 0x4, 0x8, 0xC, 0x3, 0x7, 0xB, 0xF, 0x6, 0x2, 0xE, 0xA, 0x5, 0x1, 0xD, 0x9, // 4
0x0, 0x5, 0xA, 0xF, 0x7, 0x2, 0xD, 0x8, 0xE, 0xB, 0x4, 0x1, 0x9, 0xC, 0x3, 0x6, // 5
0x0, 0x6, 0xC, 0xA, 0xB, 0xD, 0x7, 0x1, 0x5, 0x3, 0x9, 0xF, 0xE, 0x8, 0x2, 0x4, // 6
0x0, 0x7, 0xE, 0x9, 0xF, 0x8, 0x1, 0x6, 0xD, 0xA, 0x3, 0x4, 0x2, 0x5, 0xC, 0xB, // 7
0x0, 0x8, 0x3, 0xB, 0x6, 0xE, 0x5, 0xD, 0xC, 0x4, 0xF, 0x7, 0xA, 0x2, 0x9, 0x1, // 8
0x0, 0x9, 0x1, 0x8, 0x2, 0xB, 0x3, 0xA, 0x4, 0xD, 0x5, 0xC, 0x6, 0xF, 0x7, 0xE, // 9
0x0, 0xA, 0x7, 0xD, 0xE, 0x4, 0x9, 0x3, 0xF, 0x5, 0x8, 0x2, 0x1, 0xB, 0x6, 0xC, // A
0x0, 0xB, 0x5, 0xE, 0xA, 0x1, 0xF, 0x4, 0x7, 0xC, 0x2, 0x9, 0xD, 0x6, 0x8, 0x3, // B
0x0, 0xC, 0xB, 0x7, 0x5, 0x9, 0xE, 0x2, 0xA, 0x6, 0x1, 0xD, 0xF, 0x3, 0x4, 0x8, // C
0x0, 0xD, 0x9, 0x4, 0x1, 0xC, 0x8, 0x5, 0x2, 0xF, 0xB, 0x6, 0x3, 0xE, 0xA, 0x7, // D
0x0, 0xE, 0xF, 0x1, 0xD, 0x3, 0x2, 0xC, 0x9, 0x7, 0x6, 0x8, 0x4, 0xA, 0xB, 0x5, // E
0x0, 0xF, 0xD, 0x2, 0x9, 0x6, 0x4, 0xB, 0x1, 0xE, 0xC, 0x3, 0x8, 0x7, 0x5, 0xA, // F
];
/// S-box substitution table
static TABLE_SUB: [u8; 16] = [
0x0, 0x1, 0x9, 0xE, 0xD, 0xB, 0x7, 0x6,
0xF, 0x2, 0xC, 0x5, 0xA, 0x4, 0x3, 0x8,
];
/// Key nibble operation: apply key-dependent GF offset
fn key_nibble(key: &Aut64Key, nibble: u8, table: &[u8; 8], iteration: usize) -> u8 {
let key_value = key.key[table[iteration] as usize];
let offset = ((key_value as usize) << 4) | (nibble as usize);
TABLE_OFFSET[offset]
}
/// Compute round key from state
fn round_key(key: &Aut64Key, state: &[u8], round_n: usize) -> u8 {
let mut result_hi: u8 = 0;
let mut result_lo: u8 = 0;
for i in 0..(AUT64_BLOCK_SIZE - 1) {
result_hi ^= key_nibble(key, state[i] >> 4, &TABLE_UN[round_n], i);
result_lo ^= key_nibble(key, state[i] & 0x0F, &TABLE_LN[round_n], i);
}
(result_hi << 4) | result_lo
}
/// Final byte nibble for key schedule
fn final_byte_nibble(key: &Aut64Key, table: &[u8; 8]) -> u8 {
let key_value = key.key[table[AUT64_BLOCK_SIZE - 1] as usize];
TABLE_SUB[key_value as usize] << 4
}
/// Encrypt final byte nibble (inverse S-box lookup through offset table)
fn encrypt_final_byte_nibble(key: &Aut64Key, nibble: u8, table: &[u8; 8]) -> u8 {
let offset = final_byte_nibble(key, table) as usize;
for i in 0u8..16 {
if TABLE_OFFSET[offset + i as usize] == nibble {
return i;
}
}
0 // Should not reach here for valid inputs
}
/// Encrypt compress: compute encrypted output byte for a round
fn encrypt_compress(key: &Aut64Key, state: &[u8], round_n: usize) -> u8 {
let round_k = round_key(key, state, round_n);
let mut result_hi = round_k >> 4;
let mut result_lo = round_k & 0x0F;
result_hi ^= encrypt_final_byte_nibble(key, state[AUT64_BLOCK_SIZE - 1] >> 4, &TABLE_UN[round_n]);
result_lo ^= encrypt_final_byte_nibble(key, state[AUT64_BLOCK_SIZE - 1] & 0x0F, &TABLE_LN[round_n]);
(result_hi << 4) | result_lo
}
/// Decrypt final byte nibble (forward S-box through offset table)
fn decrypt_final_byte_nibble(key: &Aut64Key, nibble: u8, table: &[u8; 8], result: u8) -> u8 {
let offset = final_byte_nibble(key, table) as usize;
TABLE_OFFSET[(result ^ nibble) as usize + offset]
}
/// Decrypt compress: compute decrypted output byte for a round
fn decrypt_compress(key: &Aut64Key, state: &[u8], round_n: usize) -> u8 {
let round_k = round_key(key, state, round_n);
let result_hi = round_k >> 4;
let result_lo = round_k & 0x0F;
let hi = decrypt_final_byte_nibble(
key,
state[AUT64_BLOCK_SIZE - 1] >> 4,
&TABLE_UN[round_n],
result_hi,
);
let lo = decrypt_final_byte_nibble(
key,
state[AUT64_BLOCK_SIZE - 1] & 0x0F,
&TABLE_LN[round_n],
result_lo,
);
(hi << 4) | lo
}
/// S-box substitution on a full byte (applies S-box to each nibble independently)
fn substitute(key: &Aut64Key, byte: u8) -> u8 {
(key.sbox[(byte >> 4) as usize] << 4) | key.sbox[(byte & 0x0F) as usize]
}
/// Byte-level permutation using P-box
fn permute_bytes(key: &Aut64Key, state: &mut [u8]) {
let mut result = [0u8; AUT64_PBOX_SIZE];
for i in 0..AUT64_PBOX_SIZE {
result[key.pbox[i] as usize] = state[i];
}
state[..AUT64_PBOX_SIZE].copy_from_slice(&result);
}
/// Bit-level permutation using P-box
fn permute_bits(key: &Aut64Key, byte: u8) -> u8 {
let mut result: u8 = 0;
for i in 0..8 {
if byte & (1 << i) != 0 {
result |= 1 << key.pbox[i];
}
}
result
}
/// Compute inverse permutation box
fn reverse_box(box_in: &[u8], len: usize) -> Vec<u8> {
let mut reversed = vec![0u8; len];
for i in 0..len {
for j in 0..len {
if box_in[j] == i as u8 {
reversed[i] = j as u8;
break;
}
}
}
reversed
}
/// AUT64 encrypt: 12 rounds of the cipher
pub fn aut64_encrypt(key: &Aut64Key, message: &mut [u8]) {
// Create reverse key for encryption
let mut reverse_key = key.clone();
let rev_pbox = reverse_box(&key.pbox, AUT64_PBOX_SIZE);
let rev_sbox = reverse_box(&key.sbox, AUT64_SBOX_SIZE);
reverse_key.pbox.copy_from_slice(&rev_pbox);
reverse_key.sbox.copy_from_slice(&rev_sbox);
for i in 0..AUT64_NUM_ROUNDS {
permute_bytes(&reverse_key, message);
message[7] = encrypt_compress(&reverse_key, message, i);
message[7] = substitute(&reverse_key, message[7]);
message[7] = permute_bits(&reverse_key, message[7]);
message[7] = substitute(&reverse_key, message[7]);
}
}
/// AUT64 decrypt: 12 rounds of the cipher (reverse order)
pub fn aut64_decrypt(key: &Aut64Key, message: &mut [u8]) {
for i in (0..AUT64_NUM_ROUNDS).rev() {
message[7] = substitute(key, message[7]);
message[7] = permute_bits(key, message[7]);
message[7] = substitute(key, message[7]);
message[7] = decrypt_compress(key, message, i);
permute_bytes(key, message);
}
}
/// Pack an AUT64 key structure into a 16-byte array
#[allow(dead_code)]
pub fn aut64_pack(src: &Aut64Key) -> [u8; AUT64_KEY_STRUCT_PACKED_SIZE] {
let mut dest = [0u8; AUT64_KEY_STRUCT_PACKED_SIZE];
dest[0] = src.index;
for i in 0..(src.key.len() / 2) {
dest[i + 1] = (src.key[i * 2] << 4) | src.key[i * 2 + 1];
}
let mut pbox: u32 = 0;
for i in 0..src.pbox.len() {
pbox = (pbox << 3) | src.pbox[i] as u32;
}
dest[5] = (pbox >> 16) as u8;
dest[6] = ((pbox >> 8) & 0xFF) as u8;
dest[7] = (pbox & 0xFF) as u8;
for i in 0..(src.sbox.len() / 2) {
dest[i + 8] = (src.sbox[i * 2] << 4) | src.sbox[i * 2 + 1];
}
dest
}
/// Unpack a 16-byte array into an AUT64 key structure
#[allow(dead_code)]
pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
let mut dest = Aut64Key::default();
dest.index = src[0];
for i in 0..(dest.key.len() / 2) {
dest.key[i * 2] = src[i + 1] >> 4;
dest.key[i * 2 + 1] = src[i + 1] & 0xF;
}
let pbox: u32 = ((src[5] as u32) << 16) | ((src[6] as u32) << 8) | src[7] as u32;
for i in (0..dest.pbox.len()).rev() {
dest.pbox[i] = ((pbox >> ((dest.pbox.len() - 1 - i) * 3)) & 0x7) as u8;
}
for i in 0..(dest.sbox.len() / 2) {
dest.sbox[i * 2] = src[i + 8] >> 4;
dest.sbox[i * 2 + 1] = src[i + 8] & 0xF;
}
dest
}
+90
View File
@@ -0,0 +1,90 @@
//! Common utilities for protocol implementations.
/// Decoded signal information
#[derive(Debug, Clone)]
pub struct DecodedSignal {
/// Serial number / device ID
pub serial: Option<u32>,
/// Button code
pub button: Option<u8>,
/// Rolling counter
pub counter: Option<u16>,
/// CRC is valid
pub crc_valid: bool,
/// Raw data (up to 64 bits)
pub data: u64,
/// Number of bits in data
pub data_count_bit: usize,
/// Whether encoding is supported
pub encoder_capable: bool,
}
impl DecodedSignal {
#[allow(dead_code)]
pub fn new(data: u64, bit_count: usize) -> Self {
Self {
serial: None,
button: None,
counter: None,
crc_valid: false,
data,
data_count_bit: bit_count,
encoder_capable: false,
}
}
}
/// CRC8 calculation with custom polynomial
///
/// # Arguments
/// * `data` - Data bytes to calculate CRC over
/// * `poly` - CRC polynomial
/// * `init` - Initial CRC value
pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
let mut crc = init;
for &byte in data {
crc ^= byte;
for _ in 0..8 {
if (crc & 0x80) != 0 {
crc = (crc << 1) ^ poly;
} else {
crc <<= 1;
}
}
}
crc
}
/// CRC8 for Kia protocol (polynomial 0x7F, init 0x00)
pub fn crc8_kia(data: &[u8]) -> u8 {
crc8(data, 0x7F, 0x00)
}
/// Add a bit to the decoder's data accumulator
#[inline]
pub fn add_bit(data: &mut u64, count: &mut usize, bit: bool) {
*data = (*data << 1) | (bit as u64);
*count += 1;
}
/// Button names for common keyfob buttons
#[allow(dead_code)]
pub fn get_button_name(btn: u8) -> &'static str {
match btn {
0x01 => "Lock",
0x02 => "Unlock",
0x03 => "Lock+Unlock",
0x04 => "Trunk",
0x08 => "Panic",
_ => "Unknown",
}
}
/// Button code constants
#[allow(dead_code)]
pub mod buttons {
pub const LOCK: u8 = 0x01;
pub const UNLOCK: u8 = 0x02;
pub const TRUNK: u8 = 0x04;
pub const PANIC: u8 = 0x08;
}
+326
View File
@@ -0,0 +1,326 @@
//! Fiat V0 protocol decoder/encoder
//!
//! Ported from protopirate's fiat_v0.c
//!
//! Protocol characteristics:
//! - Differential Manchester encoding: 200/400µs timing
//! - 64-bit data (cnt:32 | serial:32) + 6-bit button
//! - 150 preamble pairs, 800µs gap, 3 bursts
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
use crate::radio::demodulator::LevelDuration;
const TE_SHORT: u32 = 200;
const TE_LONG: u32 = 400;
const TE_DELTA: u32 = 100;
#[allow(dead_code)]
const MIN_COUNT_BIT: usize = 64;
const PREAMBLE_PAIRS: u16 = 150;
const GAP_US: u32 = 800;
const TOTAL_BURSTS: u8 = 3;
const INTER_BURST_GAP: u32 = 25000;
/// Manchester decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
Mid1,
Start0,
Start1,
}
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Preamble,
Data,
}
/// Fiat V0 protocol decoder
pub struct FiatV0Decoder {
step: DecoderStep,
preamble_count: u16,
manchester_state: ManchesterState,
data_low: u32,
data_high: u32,
bit_count: u8,
cnt: u32,
serial: u32,
btn: u8,
te_last: u32,
}
impl FiatV0Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
preamble_count: 0,
manchester_state: ManchesterState::Mid1,
data_low: 0,
data_high: 0,
bit_count: 0,
cnt: 0,
serial: 0,
btn: 0,
te_last: 0,
}
}
/// Manchester advance - returns decoded bit or None
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
let event = match (is_short, is_high) {
(true, true) => 0,
(true, false) => 1,
(false, true) => 2,
(false, false) => 3,
};
let (new_state, output) = match (self.manchester_state, event) {
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => {
(ManchesterState::Start1, None)
}
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => {
(ManchesterState::Start0, None)
}
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
_ => (ManchesterState::Mid1, None),
};
self.manchester_state = new_state;
output
}
fn manchester_reset(&mut self) {
self.manchester_state = ManchesterState::Mid1;
}
fn add_manchester_bit(&mut self, bit: bool) {
let new_bit = if bit { 1u32 } else { 0u32 };
let carry = (self.data_low >> 31) & 1;
self.data_low = (self.data_low << 1) | new_bit;
self.data_high = (self.data_high << 1) | carry;
self.bit_count += 1;
if self.bit_count == 0x40 {
self.serial = self.data_low;
self.cnt = self.data_high;
self.data_low = 0;
self.data_high = 0;
}
}
fn parse_data(&self) -> DecodedSignal {
let data = ((self.cnt as u64) << 32) | (self.serial as u64);
DecodedSignal {
serial: Some(self.serial),
button: Some(self.btn),
counter: Some(self.cnt as u16),
crc_valid: true, // No CRC in Fiat V0
data,
data_count_bit: 71,
encoder_capable: true,
}
}
}
impl ProtocolDecoder for FiatV0Decoder {
fn name(&self) -> &'static str {
"Fiat 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.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.cnt = 0;
self.serial = 0;
self.btn = 0;
self.te_last = 0;
self.manchester_reset();
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if !level {
return None;
}
if duration_diff!(duration, TE_SHORT) < TE_DELTA {
self.data_low = 0;
self.data_high = 0;
self.step = DecoderStep::Preamble;
self.te_last = duration;
self.preamble_count = 0;
self.bit_count = 0;
self.manchester_reset();
}
}
DecoderStep::Preamble => {
// Count short pulses in preamble, look for gap
if duration_diff!(duration, TE_SHORT) < TE_DELTA {
self.preamble_count += 1;
self.te_last = duration;
} else if self.preamble_count >= PREAMBLE_PAIRS {
// Check for gap
if duration_diff!(duration, GAP_US) < TE_DELTA {
self.step = DecoderStep::Data;
self.preamble_count = 0;
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.te_last = duration;
return None;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::Data => {
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
if is_short || is_long {
if let Some(bit) = self.manchester_advance(is_short, level) {
self.add_manchester_bit(bit);
if self.bit_count > 0x46 {
self.btn = ((self.data_low << 1) | 1) 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 duration > TE_LONG * 3 {
// End of signal
self.step = DecoderStep::Reset;
}
self.te_last = duration;
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let cnt = decoded.counter.unwrap_or(0) as u32;
let data = ((cnt as u64) << 32) | (serial as u64);
// Reverse the decoder's btn fix: decoder does (x << 1) | 1
let btn_to_send = button >> 1;
let mut signal = Vec::with_capacity(1024);
for burst in 0..TOTAL_BURSTS {
if burst > 0 {
signal.push(LevelDuration::new(false, INTER_BURST_GAP));
}
// Preamble
for i in 0..PREAMBLE_PAIRS {
signal.push(LevelDuration::new(true, TE_SHORT));
if i < PREAMBLE_PAIRS - 1 {
signal.push(LevelDuration::new(false, TE_SHORT));
} else {
signal.push(LevelDuration::new(false, GAP_US));
}
}
// First bit (bit 63)
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 using differential Manchester
for bit in (0..63).rev() {
let curr_bit = (data >> bit) & 1 == 1;
match (prev_bit, curr_bit) {
(false, false) => {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
(false, true) => {
signal.push(LevelDuration::new(true, TE_LONG));
}
(true, false) => {
signal.push(LevelDuration::new(false, TE_LONG));
}
(true, true) => {
signal.push(LevelDuration::new(false, TE_SHORT));
signal.push(LevelDuration::new(true, TE_SHORT));
}
}
prev_bit = curr_bit;
}
// 6 button bits
for bit in (0..6).rev() {
let curr_bit = (btn_to_send >> bit) & 1 == 1;
match (prev_bit, curr_bit) {
(false, false) => {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
(false, true) => {
signal.push(LevelDuration::new(true, TE_LONG));
}
(true, false) => {
signal.push(LevelDuration::new(false, TE_LONG));
}
(true, true) => {
signal.push(LevelDuration::new(false, TE_SHORT));
signal.push(LevelDuration::new(true, TE_SHORT));
}
}
prev_bit = curr_bit;
}
// End marker
if prev_bit {
signal.push(LevelDuration::new(false, TE_SHORT));
}
signal.push(LevelDuration::new(false, TE_SHORT * 8));
}
Some(signal)
}
}
+320
View File
@@ -0,0 +1,320 @@
//! Ford V0 protocol decoder
//!
//! Ported from protopirate's ford_v0.c
//!
//! Protocol characteristics:
//! - Manchester encoding: 250/500µs timing
//! - 64 bits total
//! - Matrix-based CRC
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 250;
const TE_LONG: u32 = 500;
const TE_DELTA: u32 = 100;
const MIN_COUNT_BIT: usize = 64;
/// Manchester decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
Mid1,
Start0,
Start1,
}
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CheckPreamble,
SaveDuration,
CheckDuration,
}
/// Ford V0 protocol decoder
pub struct FordV0Decoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
decode_data: u64,
decode_count_bit: usize,
manchester_state: ManchesterState,
}
impl FordV0Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
decode_data: 0,
decode_count_bit: 0,
manchester_state: ManchesterState::Mid1,
}
}
/// Manchester decode: advance state machine
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
let event = match (is_short, is_high) {
(true, true) => 0, // Short High
(true, false) => 1, // Short Low
(false, true) => 2, // Long High
(false, false) => 3, // Long Low
};
let (new_state, output) = match (self.manchester_state, event) {
// From Mid0 or Mid1
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
(ManchesterState::Start1, None),
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) =>
(ManchesterState::Start0, None),
// From Start1
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
// From Start0
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
// Reset on invalid transitions
_ => (ManchesterState::Mid1, None),
};
self.manchester_state = new_state;
output
}
/// CRC matrix for Ford
const CRC_MATRIX: [[u8; 4]; 8] = [
[0x0C, 0xBB, 0x51, 0x25],
[0x18, 0xB1, 0x62, 0xCB],
[0x30, 0xA7, 0x44, 0x57],
[0x60, 0x89, 0x88, 0xAE],
[0xC0, 0xD6, 0xD4, 0x97],
[0x25, 0x49, 0x6D, 0xE1],
[0x4A, 0x92, 0xDA, 0x03],
[0x94, 0xE1, 0x71, 0x06],
];
/// Calculate Ford CRC
fn calculate_crc(data: u64) -> u8 {
let mut crc = 0u8;
for byte_idx in 0..7 {
let byte = ((data >> (56 - byte_idx * 8)) & 0xFF) as u8;
for bit in 0..8 {
if (byte >> (7 - bit)) & 1 == 1 {
crc ^= Self::CRC_MATRIX[bit][byte_idx % 4];
}
}
}
crc
}
/// Parse decoded data
fn parse_data(data: u64) -> DecodedSignal {
// Ford V0 format:
// Bits 60-63: Prefix (0x5)
// Bits 32-59: Serial (28 bits)
// Bits 28-31: Button (4 bits)
// Bits 16-27: Counter (12 bits)
// Bits 8-15: Encrypted data
// Bits 0-7: CRC
let serial = ((data >> 32) & 0x0FFFFFFF) as u32;
let button = ((data >> 28) & 0x0F) as u8;
let counter = ((data >> 16) & 0x0FFF) as u16;
let received_crc = (data & 0xFF) as u8;
let calculated_crc = Self::calculate_crc(data);
DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid: received_crc == calculated_crc,
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
}
}
}
impl ProtocolDecoder for FordV0Decoder {
fn name(&self) -> &'static str {
"Ford 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] {
&[315_000_000, 433_920_000] // 315 MHz (US) and 433.92 MHz (EU)
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.te_last = 0;
self.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
self.manchester_state = ManchesterState::Mid1;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
match self.step {
DecoderStep::Reset => {
if level && is_short {
self.step = DecoderStep::CheckPreamble;
self.header_count = 1;
self.manchester_state = ManchesterState::Mid1;
}
}
DecoderStep::CheckPreamble => {
if is_short {
self.header_count += 1;
if self.header_count > 20 && !level {
// Enough preamble, start looking for data
self.step = DecoderStep::SaveDuration;
self.decode_data = 0;
self.decode_count_bit = 0;
self.manchester_state = ManchesterState::Mid1;
}
} else if is_long {
if self.header_count > 10 {
self.step = DecoderStep::SaveDuration;
self.decode_data = 0;
self.decode_count_bit = 0;
self.manchester_state = ManchesterState::Mid1;
// Process this long pulse
if let Some(bit) = self.manchester_advance(false, level) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::SaveDuration => {
self.te_last = duration;
self.step = DecoderStep::CheckDuration;
}
DecoderStep::CheckDuration => {
let last_short = duration_diff!(self.te_last, TE_SHORT) < TE_DELTA;
let last_long = duration_diff!(self.te_last, TE_LONG) < TE_DELTA;
// Check for end of transmission
if duration > TE_LONG * 3 {
if self.decode_count_bit >= MIN_COUNT_BIT {
let result = Self::parse_data(self.decode_data);
self.step = DecoderStep::Reset;
return Some(result);
}
self.step = DecoderStep::Reset;
return None;
}
// Manchester decode
if last_short {
if let Some(bit) = self.manchester_advance(true, !level) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
} else if last_long {
if let Some(bit) = self.manchester_advance(false, !level) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
}
if is_short || is_long {
if let Some(bit) = self.manchester_advance(is_short, level) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
self.step = DecoderStep::SaveDuration;
} else {
self.step = DecoderStep::Reset;
}
// Check if we have enough bits
if self.decode_count_bit >= MIN_COUNT_BIT {
let result = Self::parse_data(self.decode_data);
self.step = DecoderStep::Reset;
return Some(result);
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0);
// Build data packet
let mut data: u64 = 0;
data |= 0x5 << 60; // Prefix
data |= ((serial as u64) & 0x0FFFFFFF) << 32;
data |= ((button as u64) & 0x0F) << 28;
data |= ((counter as u64) & 0x0FFF) << 16;
data |= ((decoded.data >> 8) & 0xFF) << 8; // Keep encrypted byte
data |= Self::calculate_crc(data) as u64;
let mut signal = Vec::with_capacity(256);
// Preamble
for _ in 0..30 {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
// Sync
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_LONG));
// Data: Manchester encoded, 64 bits MSB first
for bit_num in (0..64).rev() {
let bit = (data >> bit_num) & 1 == 1;
if bit {
// Manchester 1: low-high
signal.push(LevelDuration::new(false, TE_SHORT));
signal.push(LevelDuration::new(true, TE_SHORT));
} else {
// Manchester 0: high-low
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
}
// End
signal.push(LevelDuration::new(false, TE_LONG * 4));
Some(signal)
}
}
+149
View File
@@ -0,0 +1,149 @@
//! KeeLoq common encryption/decryption routines
//!
//! Shared by Kia V3/V4, Star Line, and other KeeLoq-based protocols.
//! Based on the NLF (Non-Linear Feedback) function with constant 0x3A5C742E.
/// The KeeLoq NLF constant
const KEELOQ_NLF: u32 = 0x3A5C742E;
/// KeeLoq decrypt: 528 rounds of the KeeLoq cipher (decrypt direction)
pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
let mut block = data;
let mut tkey = key;
for _ in 0..528 {
let lutkey = ((block >> 0) & 1)
| ((block >> 7) & 2)
| ((block >> 17) & 4)
| ((block >> 22) & 8)
| ((block >> 26) & 16);
let lsb = ((block >> 31)
^ ((block >> 15) & 1)
^ ((KEELOQ_NLF >> lutkey) & 1)
^ (((tkey >> 15) & 1) as u32)) as u32;
block = ((block & 0x7FFFFFFF) << 1) | lsb;
tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63);
}
block
}
/// KeeLoq encrypt: 528 rounds of the KeeLoq cipher (encrypt direction)
pub fn keeloq_encrypt(data: u32, key: u64) -> u32 {
let mut block = data;
let mut tkey = key;
for _ in 0..528 {
let lutkey = ((block >> 1) & 1)
| ((block >> 8) & 2)
| ((block >> 18) & 4)
| ((block >> 23) & 8)
| ((block >> 27) & 16);
let msb = ((block >> 0)
^ ((block >> 16) & 1)
^ ((KEELOQ_NLF >> lutkey) & 1)
^ (((tkey >> 0) & 1) as u32)) as u32;
block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31);
tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63);
}
block
}
/// Normal learning key derivation
/// Derives a 64-bit key from a 32-bit fix code and a 64-bit manufacturer key
pub fn keeloq_normal_learning(fix: u32, manufacturer_key: u64) -> u64 {
let serial_low = fix & 0xFFFF;
let serial_high = (fix >> 16) & 0xFFFF;
let key_low = keeloq_decrypt(serial_low as u32 | 0x20000000, manufacturer_key);
let key_high = keeloq_decrypt(serial_high as u32 | 0x60000000, manufacturer_key);
((key_high as u64) << 32) | (key_low as u64)
}
/// Reverse the bits in a 64-bit key (for protocols that store data MSB-first)
pub fn reverse_key(key: u64, bit_count: usize) -> u64 {
let mut result: u64 = 0;
for i in 0..bit_count {
if (key >> i) & 1 == 1 {
result |= 1 << (bit_count - 1 - i);
}
}
result
}
/// Reverse bits in a byte
#[allow(dead_code)]
pub fn reverse8(byte: u8) -> u8 {
let mut b = byte;
b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
b
}
/// Secure learning key derivation
/// Derives a 64-bit key from a serial, seed, and manufacturer key
#[allow(dead_code)]
pub fn keeloq_secure_learning(data: u32, seed: u32, key: u64) -> u64 {
let serial = data & 0x0FFFFFFF;
let k1 = keeloq_decrypt(serial, key);
let k2 = keeloq_decrypt(seed, key);
((k1 as u64) << 32) | (k2 as u64)
}
/// FAAC SLH (Spa) learning key derivation
/// Derives a 64-bit key from a seed and manufacturer key
#[allow(dead_code)]
pub fn keeloq_faac_learning(seed: u32, key: u64) -> u64 {
let hs = (seed >> 16) as u16;
let ending: u16 = 0x544D;
let lsb = ((hs as u32) << 16) | (ending as u32);
((keeloq_encrypt(seed, key) as u64) << 32) | (keeloq_encrypt(lsb, key) as u64)
}
/// Magic XOR Type 1 learning key derivation
#[allow(dead_code)]
pub fn keeloq_magic_xor_type1_learning(data: u32, xor: u64) -> u64 {
let serial = data & 0x0FFFFFFF;
(((serial as u64) << 32) | (serial as u64)) ^ xor
}
/// Magic Serial Type 1 learning key derivation
#[allow(dead_code)]
pub fn keeloq_magic_serial_type1_learning(data: u32, man: u64) -> u64 {
(man & 0xFFFFFFFF)
| ((data as u64) << 40)
| (((((data & 0xFF).wrapping_add((data >> 8) & 0xFF)) & 0xFF) as u64) << 32)
}
/// Magic Serial Type 2 learning key derivation
#[allow(dead_code)]
pub fn keeloq_magic_serial_type2_learning(data: u32, man: u64) -> u64 {
let p = data.to_le_bytes();
let mut m = man.to_le_bytes();
m[7] = p[0];
m[6] = p[1];
m[5] = p[2];
m[4] = p[3];
u64::from_le_bytes(m)
}
/// Magic Serial Type 3 learning key derivation
#[allow(dead_code)]
pub fn keeloq_magic_serial_type3_learning(data: u32, man: u64) -> u64 {
(man & 0xFFFFFFFFFF000000) | ((data & 0xFFFFFF) as u64)
}
/// KeeLoq learning type constants
#[allow(dead_code)]
pub mod learning_types {
pub const UNKNOWN: u32 = 0;
pub const SIMPLE: u32 = 1;
pub const NORMAL: u32 = 2;
// pub const SECURE: u32 = 3;
pub const MAGIC_XOR_TYPE_1: u32 = 4;
// pub const FAAC: u32 = 5;
pub const MAGIC_SERIAL_TYPE_1: u32 = 6;
pub const MAGIC_SERIAL_TYPE_2: u32 = 7;
pub const MAGIC_SERIAL_TYPE_3: u32 = 8;
}
+177
View File
@@ -0,0 +1,177 @@
//! Key management module for protocol encryption/decryption
//!
//! Ported from protopirate's keys.c
//!
//! Manages manufacturer keys used by various protocols:
//! - KIA V3/V4: kia_mf_key (manufacturer key for KeeLoq)
//! - KIA V5: kia_v5_key (custom mixer cipher key)
//! - KIA V6: kia_v6_a_key, kia_v6_b_key (AES-128 XOR mask keys)
//! - VAG: AUT64 keys loaded from keystore files
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
use std::path::Path;
use std::sync::{OnceLock, RwLock};
use tracing::{info, warn, error};
/// Key type identifiers (matches protopirate's keystore types)
const KIA_KEY1: u32 = 10; // kia_mf_key
const KIA_KEY2: u32 = 11; // kia_v6_a_key
const KIA_KEY3: u32 = 12; // kia_v6_b_key
const KIA_KEY4: u32 = 13; // kia_v5_key
/// Maximum number of VAG AUT64 keys
const VAG_KEYS_COUNT: usize = 3;
/// Global key store - thread-safe access to loaded keys
pub struct KeyStore {
/// KIA manufacturer key (for KeeLoq-based V3/V4)
pub kia_mf_key: u64,
/// KIA V6 AES key A
pub kia_v6_a_key: u64,
/// KIA V6 AES key B
pub kia_v6_b_key: u64,
/// KIA V5 mixer key
pub kia_v5_key: u64,
/// VAG AUT64 keys
pub vag_keys: Vec<Aut64Key>,
/// Whether VAG keys have been loaded
pub vag_keys_loaded: bool,
}
impl Default for KeyStore {
fn default() -> Self {
Self {
kia_mf_key: 0,
kia_v6_a_key: 0,
kia_v6_b_key: 0,
kia_v5_key: 0,
vag_keys: Vec::new(),
vag_keys_loaded: false,
}
}
}
impl KeyStore {
/// Create a new empty key store
pub fn new() -> Self {
Self::default()
}
/// Load KIA keys from a key entries list
/// Each entry is (type_id, key_value)
pub fn load_kia_keys(&mut self, entries: &[(u32, u64)]) {
for &(key_type, key_value) in entries {
match key_type {
KIA_KEY1 => self.kia_mf_key = key_value,
KIA_KEY2 => self.kia_v6_a_key = key_value,
KIA_KEY3 => self.kia_v6_b_key = key_value,
KIA_KEY4 => self.kia_v5_key = key_value,
_ => {}
}
}
}
/// Load VAG AUT64 keys from raw binary data
/// The data should contain packed AUT64 key structures (16 bytes each)
pub fn load_vag_keys_from_data(&mut self, data: &[u8]) {
if self.vag_keys_loaded {
return;
}
self.vag_keys.clear();
for i in 0..VAG_KEYS_COUNT {
let offset = i * AUT64_KEY_STRUCT_PACKED_SIZE;
if offset + AUT64_KEY_STRUCT_PACKED_SIZE > data.len() {
error!("VAG key data too short for key {}", i);
break;
}
let key = aut64::aut64_unpack(&data[offset..offset + AUT64_KEY_STRUCT_PACKED_SIZE]);
self.vag_keys.push(key);
}
self.vag_keys_loaded = true;
info!("Loaded {} VAG keys", self.vag_keys.len());
}
/// Load VAG AUT64 keys from a file path
pub fn load_vag_keys_from_file(&mut self, path: &str) {
if self.vag_keys_loaded {
return;
}
let file_path = Path::new(path);
if !file_path.exists() {
warn!("VAG key file not found: {}", path);
return;
}
match std::fs::read(file_path) {
Ok(data) => {
self.load_vag_keys_from_data(&data);
}
Err(e) => {
error!("Failed to read VAG key file {}: {}", path, e);
}
}
}
/// Get a VAG AUT64 key by its internal index field
pub fn get_vag_key(&self, index: u8) -> Option<&Aut64Key> {
self.vag_keys.iter().find(|k| k.index == index)
}
/// Get a VAG AUT64 key by array position (0-based)
pub fn get_vag_key_by_position(&self, position: usize) -> Option<&Aut64Key> {
self.vag_keys.get(position)
}
/// Get the KIA manufacturer key
pub fn get_kia_mf_key(&self) -> u64 {
self.kia_mf_key
}
/// Get the KIA V6 AES key A
pub fn get_kia_v6_keystore_a(&self) -> u64 {
self.kia_v6_a_key
}
/// Get the KIA V6 AES key B
pub fn get_kia_v6_keystore_b(&self) -> u64 {
self.kia_v6_b_key
}
/// Get the KIA V5 mixer key
pub fn get_kia_v5_key(&self) -> u64 {
self.kia_v5_key
}
}
/// Global singleton keystore
fn global_keystore() -> &'static RwLock<KeyStore> {
static GLOBAL_KEYSTORE: OnceLock<RwLock<KeyStore>> = OnceLock::new();
GLOBAL_KEYSTORE.get_or_init(|| RwLock::new(KeyStore::new()))
}
/// Get a read reference to the global keystore
pub fn get_keystore() -> std::sync::RwLockReadGuard<'static, KeyStore> {
global_keystore().read().unwrap()
}
/// Get a write reference to the global keystore
pub fn get_keystore_mut() -> std::sync::RwLockWriteGuard<'static, KeyStore> {
global_keystore().write().unwrap()
}
/// Initialize the global keystore with KIA keys
pub fn load_keys(kia_entries: &[(u32, u64)]) {
let mut store = get_keystore_mut();
store.load_kia_keys(kia_entries);
}
/// Initialize VAG keys from file
pub fn load_vag_keys(path: &str) {
let mut store = get_keystore_mut();
store.load_vag_keys_from_file(path);
}
+259
View File
@@ -0,0 +1,259 @@
//! Kia V0 protocol decoder
//!
//! Ported from protopirate's kia_v0.c
//!
//! Protocol characteristics:
//! - PWM encoding: short pulse (250µs) = 0, long pulse (500µs) = 1
//! - 61 bits total
//! - Preamble: alternating short pulses
//! - Sync: long-long pattern
//! - Data: 59 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC)
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use super::common::{crc8_kia, add_bit};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 250;
const TE_LONG: u32 = 500;
const TE_DELTA: u32 = 100;
const MIN_COUNT_BIT: usize = 61;
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CheckPreamble,
SaveDuration,
CheckDuration,
}
/// Kia V0 protocol decoder
pub struct KiaV0Decoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
decode_data: u64,
decode_count_bit: usize,
}
impl KiaV0Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
decode_data: 0,
decode_count_bit: 0,
}
}
/// Calculate CRC for Kia data packet
fn calculate_crc(data: u64) -> u8 {
let crc_data = [
((data >> 48) & 0xFF) as u8,
((data >> 40) & 0xFF) as u8,
((data >> 32) & 0xFF) as u8,
((data >> 24) & 0xFF) as u8,
((data >> 16) & 0xFF) as u8,
((data >> 8) & 0xFF) as u8,
];
crc8_kia(&crc_data)
}
/// Verify CRC of received data
fn verify_crc(data: u64) -> bool {
let received_crc = (data & 0xFF) as u8;
let calculated_crc = Self::calculate_crc(data);
received_crc == calculated_crc
}
/// Extract fields from decoded data
fn parse_data(data: u64) -> DecodedSignal {
let serial = ((data >> 12) & 0x0FFFFFFF) as u32;
let button = ((data >> 8) & 0x0F) as u8;
let counter = ((data >> 40) & 0xFFFF) as u16;
let crc_valid = Self::verify_crc(data);
DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid,
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
}
}
}
impl ProtocolDecoder for KiaV0Decoder {
fn name(&self) -> &'static str {
"Kia 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] // 433.92 MHz
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.te_last = 0;
self.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if level && duration_diff!(duration, TE_SHORT) < TE_DELTA {
self.step = DecoderStep::CheckPreamble;
self.te_last = duration;
self.header_count = 0;
}
}
DecoderStep::CheckPreamble => {
if level {
if duration_diff!(duration, TE_SHORT) < TE_DELTA ||
duration_diff!(duration, TE_LONG) < TE_DELTA {
self.te_last = duration;
} else {
self.step = DecoderStep::Reset;
}
} else if duration_diff!(duration, TE_SHORT) < TE_DELTA &&
duration_diff!(self.te_last, TE_SHORT) < TE_DELTA {
// Short-short pair in preamble
self.header_count += 1;
} else if duration_diff!(duration, TE_LONG) < TE_DELTA &&
duration_diff!(self.te_last, TE_LONG) < TE_DELTA {
// Long-long sync pattern
if self.header_count > 15 {
self.step = DecoderStep::SaveDuration;
self.decode_data = 0;
self.decode_count_bit = 1;
// Add first bit (the sync is also a '1' bit)
add_bit(&mut self.decode_data, &mut self.decode_count_bit, true);
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::SaveDuration => {
if level {
if duration >= TE_LONG + TE_DELTA * 2 {
// End of transmission
self.step = DecoderStep::Reset;
if self.decode_count_bit == MIN_COUNT_BIT {
return Some(Self::parse_data(self.decode_data));
}
} else {
self.te_last = duration;
self.step = DecoderStep::CheckDuration;
}
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::CheckDuration => {
if !level {
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA &&
duration_diff!(duration, TE_SHORT) < TE_DELTA {
// Short-short = bit 0
add_bit(&mut self.decode_data, &mut self.decode_count_bit, false);
self.step = DecoderStep::SaveDuration;
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA &&
duration_diff!(duration, TE_LONG) < TE_DELTA {
// Long-long = bit 1
add_bit(&mut self.decode_data, &mut self.decode_count_bit, true);
self.step = DecoderStep::SaveDuration;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0);
// Build data packet
let mut data: u64 = 0;
// Bits 56-59: Preserve from original (usually 0xF)
data |= decoded.data & 0x0F00000000000000;
// Bits 40-55: Counter (16 bits)
data |= ((counter as u64) & 0xFFFF) << 40;
// Bits 12-39: Serial (28 bits)
data |= ((serial as u64) & 0x0FFFFFFF) << 12;
// Bits 8-11: Button (4 bits)
data |= ((button as u64) & 0x0F) << 8;
// Bits 0-7: CRC
let crc = Self::calculate_crc(data);
data |= crc as u64;
let mut signal = Vec::with_capacity(256);
// Generate 2 bursts
for burst in 0..2 {
if burst > 0 {
// Inter-burst gap
signal.push(LevelDuration::new(false, 25000));
}
// Preamble: 32 alternating short pulses
for i in 0..32 {
let is_high = (i % 2) == 0;
signal.push(LevelDuration::new(is_high, TE_SHORT));
}
// Sync: long-long
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_LONG));
// Data: 59 bits (MSB first)
for bit_num in 0..59 {
let bit_mask = 1u64 << (58 - bit_num);
let bit = (data & bit_mask) != 0;
let duration = if bit { TE_LONG } else { TE_SHORT };
signal.push(LevelDuration::new(true, duration));
signal.push(LevelDuration::new(false, duration));
}
// End marker
signal.push(LevelDuration::new(true, TE_LONG * 2));
}
Some(signal)
}
}
+296
View File
@@ -0,0 +1,296 @@
//! Kia V1 protocol decoder
//!
//! Ported from protopirate's kia_v1.c
//!
//! Protocol characteristics:
//! - Manchester encoding: 800/1600µs timing
//! - 57 bits total
//! - Long preamble of ~90 pulses
//! - CRC4 checksum
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 800;
const TE_LONG: u32 = 1600;
const TE_DELTA: u32 = 200;
const MIN_COUNT_BIT: usize = 57;
/// Manchester states
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
Mid1,
Start0,
Start1,
}
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CheckPreamble,
DecodeData,
}
/// Kia V1 protocol decoder
pub struct KiaV1Decoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
decode_data: u64,
decode_count_bit: usize,
manchester_state: ManchesterState,
}
impl KiaV1Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
decode_data: 0,
decode_count_bit: 0,
manchester_state: ManchesterState::Mid1,
}
}
/// CRC4 calculation for Kia V1
fn crc4(bytes: &[u8], offset: u8) -> u8 {
let mut crc: u8 = 0;
for &byte in bytes {
crc ^= (byte & 0x0F) ^ (byte >> 4);
}
(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
};
let (new_state, output) = match (self.manchester_state, event) {
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
(ManchesterState::Start0, None),
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) =>
(ManchesterState::Start1, None),
(ManchesterState::Start1, 0) => (ManchesterState::Mid1, Some(true)),
(ManchesterState::Start1, 2) => (ManchesterState::Start0, Some(true)),
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, Some(false)),
(ManchesterState::Start0, 3) => (ManchesterState::Start1, Some(false)),
_ => (ManchesterState::Mid1, None),
};
self.manchester_state = new_state;
output
}
/// Parse decoded data
fn parse_data(&self) -> DecodedSignal {
let data = self.decode_data;
// Extract fields per kia_v1.c
let serial = (data >> 24) as u32;
let button = ((data >> 16) & 0xFF) as u8;
let cnt_low = ((data >> 8) & 0xFF) as u16;
let cnt_high = ((data >> 4) & 0x0F) as u16;
let counter = (cnt_high << 8) | cnt_low;
let received_crc = (data & 0x0F) as u8;
// Calculate CRC
let mut char_data = [0u8; 7];
char_data[0] = ((serial >> 24) & 0xFF) as u8;
char_data[1] = ((serial >> 16) & 0xFF) as u8;
char_data[2] = ((serial >> 8) & 0xFF) as u8;
char_data[3] = (serial & 0xFF) as u8;
char_data[4] = button;
char_data[5] = (counter & 0xFF) as u8;
let crc = if cnt_high == 0 {
let offset = if counter >= 0x098 { button } else { 1 };
Self::crc4(&char_data[..6], offset)
} else if cnt_high >= 0x6 {
char_data[6] = cnt_high as u8;
Self::crc4(&char_data, 1)
} else {
Self::crc4(&char_data[..6], 1)
};
DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid: received_crc == crc,
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
}
}
}
impl ProtocolDecoder for KiaV1Decoder {
fn name(&self) -> &'static str {
"Kia 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] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.te_last = 0;
self.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
self.manchester_state = ManchesterState::Mid1;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
match self.step {
DecoderStep::Reset => {
if level && is_long {
self.step = DecoderStep::CheckPreamble;
self.te_last = duration;
self.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
self.manchester_state = ManchesterState::Mid1;
}
}
DecoderStep::CheckPreamble => {
if !level {
if is_long && duration_diff!(self.te_last, TE_LONG) < TE_DELTA {
self.header_count += 1;
self.te_last = duration;
} else {
self.step = DecoderStep::Reset;
}
}
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
self.header_count = 0;
self.step = DecoderStep::DecodeData;
}
}
}
DecoderStep::DecodeData => {
if is_short {
if let Some(bit) = self.manchester_advance(true, level) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
} else if is_long {
if let Some(bit) = self.manchester_advance(false, level) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
} else {
self.step = DecoderStep::Reset;
return None;
}
if self.decode_count_bit >= MIN_COUNT_BIT {
let result = self.parse_data();
self.step = DecoderStep::Reset;
return Some(result);
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0);
// Calculate CRC
let cnt_high = ((counter >> 8) & 0x0F) as u8;
let mut char_data = [0u8; 7];
char_data[0] = ((serial >> 24) & 0xFF) as u8;
char_data[1] = ((serial >> 16) & 0xFF) as u8;
char_data[2] = ((serial >> 8) & 0xFF) as u8;
char_data[3] = (serial & 0xFF) as u8;
char_data[4] = button;
char_data[5] = (counter & 0xFF) as u8;
let crc = if cnt_high == 0 {
let offset = if counter >= 0x098 { button } else { 1 };
Self::crc4(&char_data[..6], offset)
} else if cnt_high >= 0x6 {
char_data[6] = cnt_high;
Self::crc4(&char_data, 1)
} else {
Self::crc4(&char_data[..6], 1)
};
// Build data
let data: u64 = ((serial as u64) << 24) |
((button as u64) << 16) |
(((counter & 0xFF) as u64) << 8) |
((cnt_high as u64) << 4) |
(crc as u64);
let mut signal = Vec::with_capacity(600);
// Generate 3 bursts
for burst in 0..3 {
if burst > 0 {
signal.push(LevelDuration::new(false, 25000));
}
// Preamble: 90 long pairs
for _ in 0..90 {
signal.push(LevelDuration::new(false, TE_LONG));
signal.push(LevelDuration::new(true, TE_LONG));
}
// Short gap
signal.push(LevelDuration::new(false, TE_SHORT));
// Data: Manchester encoded, MSB first
for bit_num in (1..MIN_COUNT_BIT).rev() {
let bit = ((data >> (bit_num - 1)) & 1) == 1;
if bit {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
} else {
signal.push(LevelDuration::new(false, TE_SHORT));
signal.push(LevelDuration::new(true, TE_SHORT));
}
}
}
Some(signal)
}
}
+275
View File
@@ -0,0 +1,275 @@
//! Kia V2 protocol decoder
//!
//! Ported from protopirate's kia_v2.c
//!
//! Protocol characteristics:
//! - Manchester encoding: 500/1000µs timing
//! - 53 bits total
//! - Long preamble of 252+ pairs
//! - CRC4 checksum
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 500;
const TE_LONG: u32 = 1000;
const TE_DELTA: u32 = 150;
const MIN_COUNT_BIT: usize = 53;
/// Manchester states
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
Mid1,
Start0,
Start1,
}
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CheckPreamble,
CollectRawBits,
}
/// Kia V2 protocol decoder
pub struct KiaV2Decoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
decode_data: u64,
decode_count_bit: usize,
manchester_state: ManchesterState,
}
impl KiaV2Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
decode_data: 0,
decode_count_bit: 0,
manchester_state: ManchesterState::Mid1,
}
}
/// Calculate CRC for Kia V2
fn calculate_crc(data: u64) -> u8 {
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
let u_var4 = (data & 0xFFFFFFFF) as u32;
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;
let mut crc: u8 = 0;
for &byte in &bytes {
crc ^= (byte & 0x0F) ^ (byte >> 4);
}
(crc.wrapping_add(1)) & 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
};
let (new_state, output) = match (self.manchester_state, event) {
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
(ManchesterState::Start0, None),
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) =>
(ManchesterState::Start1, None),
(ManchesterState::Start1, 0) => (ManchesterState::Mid1, Some(true)),
(ManchesterState::Start1, 2) => (ManchesterState::Start0, Some(true)),
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, Some(false)),
(ManchesterState::Start0, 3) => (ManchesterState::Start1, Some(false)),
_ => (ManchesterState::Mid1, None),
};
self.manchester_state = new_state;
output
}
/// Parse decoded data
fn parse_data(&self) -> DecodedSignal {
let data = self.decode_data;
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
let button = ((data >> 16) & 0x0F) as u8;
// Counter has byte-swapped format
let raw_count = ((data >> 4) & 0xFFF) as u16;
let counter = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
let received_crc = (data & 0x0F) as u8;
let calculated_crc = Self::calculate_crc(data);
DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid: received_crc == calculated_crc,
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
}
}
}
impl ProtocolDecoder for KiaV2Decoder {
fn name(&self) -> &'static str {
"Kia V2"
}
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] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.te_last = 0;
self.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
self.manchester_state = ManchesterState::Mid1;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
match self.step {
DecoderStep::Reset => {
if level && is_long {
self.step = DecoderStep::CheckPreamble;
self.te_last = duration;
self.header_count = 0;
self.manchester_state = ManchesterState::Mid1;
}
}
DecoderStep::CheckPreamble => {
if level {
if is_long {
self.te_last = duration;
self.header_count += 1;
} else if is_short && self.header_count >= 100 {
self.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 1;
self.step = DecoderStep::CollectRawBits;
self.decode_data = 1; // First bit
} else {
self.step = DecoderStep::Reset;
}
} else {
if is_long {
self.header_count += 1;
self.te_last = duration;
} else if !is_short {
self.step = DecoderStep::Reset;
}
}
}
DecoderStep::CollectRawBits => {
if is_short {
if let Some(bit) = self.manchester_advance(true, level) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
} else if is_long {
if let Some(bit) = self.manchester_advance(false, level) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
} else {
self.step = DecoderStep::Reset;
return None;
}
if self.decode_count_bit >= MIN_COUNT_BIT {
let result = self.parse_data();
self.step = DecoderStep::Reset;
return Some(result);
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0);
// Reconstruct data in V2 format
let u_var6 = ((counter & 0xFF) as u32) << 8 |
((button & 0x0F) as u32) << 16 |
(((counter >> 4) & 0xF0) as u32);
let mut new_data: u64 = 1u64 << 52; // Start bit
new_data |= ((serial as u64) << 20) & 0xFFFFFFFFF00000;
new_data |= u_var6 as u64;
// Calculate and apply CRC
let crc = Self::calculate_crc(new_data);
new_data = (new_data & !0x0F) | (crc as u64);
let mut signal = Vec::with_capacity(700);
// Generate 2 bursts
for _burst in 0..2 {
// Preamble: 252 long pairs
for _ in 0..252 {
signal.push(LevelDuration::new(false, TE_LONG));
signal.push(LevelDuration::new(true, TE_LONG));
}
// Short gap
signal.push(LevelDuration::new(false, TE_SHORT));
// Data: Manchester encoded, MSB first
for bit_num in (1..MIN_COUNT_BIT).rev() {
let bit = ((new_data >> (bit_num - 1)) & 1) == 1;
if bit {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
} else {
signal.push(LevelDuration::new(false, TE_SHORT));
signal.push(LevelDuration::new(true, TE_SHORT));
}
}
}
Some(signal)
}
}
+386
View File
@@ -0,0 +1,386 @@
//! Kia V3/V4 protocol decoder
//!
//! Ported from protopirate's kia_v3_v4.c
//!
//! Protocol characteristics:
//! - PWM encoding: 400/800µs timing
//! - 68 bits total
//! - Short preamble of 16 pairs
//! - KeeLoq encryption (requires manufacturer key)
//! - V3 and V4 differ only in sync polarity
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 400;
const TE_LONG: u32 = 800;
const TE_DELTA: u32 = 150;
const MIN_COUNT_BIT: usize = 68;
const SYNC_DURATION: u32 = 1200;
const INTER_BURST_GAP_US: u32 = 10000;
const PREAMBLE_PAIRS: usize = 16;
const TOTAL_BURSTS: usize = 3;
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CheckPreamble,
CollectRawBits,
}
/// Kia V3/V4 protocol decoder
pub struct KiaV3V4Decoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
raw_bits: [u8; 32],
raw_bit_count: u16,
is_v3_sync: bool,
}
impl KiaV3V4Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
raw_bits: [0; 32],
raw_bit_count: 0,
is_v3_sync: false,
}
}
/// Reverse bits in a byte
fn reverse8(byte: u8) -> u8 {
let mut byte = byte;
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
byte = (byte & 0xCC) >> 2 | (byte & 0x33) << 2;
byte = (byte & 0xAA) >> 1 | (byte & 0x55) << 1;
byte
}
/// Add a raw bit to the buffer
fn add_raw_bit(&mut self, bit: bool) {
if self.raw_bit_count < 256 {
let byte_idx = (self.raw_bit_count / 8) as usize;
let bit_idx = 7 - (self.raw_bit_count % 8);
if bit {
self.raw_bits[byte_idx] |= 1 << bit_idx;
} else {
self.raw_bits[byte_idx] &= !(1 << bit_idx);
}
self.raw_bit_count += 1;
}
}
/// CRC4 calculation
fn calculate_crc(bytes: &[u8]) -> u8 {
let mut crc: u8 = 0;
for &byte in bytes.iter().take(8) {
crc ^= (byte & 0x0F) ^ (byte >> 4);
}
crc & 0x0F
}
/// KeeLoq decrypt
fn keeloq_decrypt(data: u32, key: u64) -> u32 {
let mut block = data;
let mut tkey = key;
for _ in 0..528 {
let lutkey = ((block >> 0) & 1) |
((block >> 7) & 2) |
((block >> 17) & 4) |
((block >> 22) & 8) |
((block >> 26) & 16);
let lsb = ((block >> 31) ^
((block >> 15) & 1) ^
((0x3A5C742E_u32 >> lutkey) & 1) ^
(((tkey >> 15) & 1) as u32)) as u32;
block = ((block & 0x7FFFFFFF) << 1) | lsb;
tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63);
}
block
}
/// KeeLoq encrypt
fn keeloq_encrypt(data: u32, key: u64) -> u32 {
let mut block = data;
let mut tkey = key;
for _ in 0..528 {
let lutkey = ((block >> 1) & 1) |
((block >> 8) & 2) |
((block >> 18) & 4) |
((block >> 23) & 8) |
((block >> 27) & 16);
let msb = ((block >> 0) ^
((block >> 16) & 1) ^
((0x3A5C742E_u32 >> lutkey) & 1) ^
(((tkey >> 0) & 1) as u32)) as u32;
block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31);
tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63);
}
block
}
/// Get manufacturer key (placeholder - in real use, this would be loaded from config)
fn get_mf_key() -> u64 {
// This is a placeholder - actual key should be loaded from secure storage
0x0000000000000000
}
/// Process the collected buffer and validate
fn process_buffer(&self) -> Option<DecodedSignal> {
if self.raw_bit_count < 68 {
return None;
}
let mut b = self.raw_bits;
// V3 sync means data is inverted
if self.is_v3_sync {
let num_bytes = ((self.raw_bit_count + 7) / 8) as usize;
for i in 0..num_bytes {
b[i] = !b[i];
}
}
let _crc = (b[8] >> 4) & 0x0F;
let encrypted = ((Self::reverse8(b[3]) as u32) << 24) |
((Self::reverse8(b[2]) as u32) << 16) |
((Self::reverse8(b[1]) as u32) << 8) |
(Self::reverse8(b[0]) as u32);
let serial = ((Self::reverse8(b[7] & 0xF0) as u32) << 24) |
((Self::reverse8(b[6]) as u32) << 16) |
((Self::reverse8(b[5]) as u32) << 8) |
(Self::reverse8(b[4]) as u32);
let button = (Self::reverse8(b[7]) & 0xF0) >> 4;
let our_serial_lsb = (serial & 0xFF) as u8;
let mf_key = Self::get_mf_key();
let decrypted = Self::keeloq_decrypt(encrypted, mf_key);
let dec_btn = ((decrypted >> 28) & 0x0F) as u8;
let dec_serial_lsb = ((decrypted >> 16) & 0xFF) as u8;
// Validate decryption (may fail if key is wrong)
let crc_valid = if mf_key != 0 {
dec_btn == button && dec_serial_lsb == our_serial_lsb
} else {
// Can't validate without key
true
};
let counter = (decrypted & 0xFFFF) as u16;
// Build key data
let key_data = ((b[0] as u64) << 56) |
((b[1] as u64) << 48) |
((b[2] as u64) << 40) |
((b[3] as u64) << 32) |
((b[4] as u64) << 24) |
((b[5] as u64) << 16) |
((b[6] as u64) << 8) |
(b[7] as u64);
Some(DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid,
data: key_data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
})
}
}
impl ProtocolDecoder for KiaV3V4Decoder {
fn name(&self) -> &'static str {
"Kia V3/V4"
}
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] {
&[315_000_000, 433_920_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.te_last = 0;
self.header_count = 0;
self.raw_bits = [0; 32];
self.raw_bit_count = 0;
self.is_v3_sync = false;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
let is_sync = duration > 1000 && duration < 1500;
let is_very_long = duration > 1500;
match self.step {
DecoderStep::Reset => {
if level && is_short {
self.step = DecoderStep::CheckPreamble;
self.te_last = duration;
self.header_count = 1;
}
}
DecoderStep::CheckPreamble => {
if level {
if is_short {
self.te_last = duration;
} else if is_sync && self.header_count >= 8 {
// V4 sync: long HIGH
self.step = DecoderStep::CollectRawBits;
self.raw_bit_count = 0;
self.is_v3_sync = false;
self.raw_bits = [0; 32];
} else {
self.step = DecoderStep::Reset;
}
} else {
if is_sync && self.header_count >= 8 {
// V3 sync: long LOW
self.step = DecoderStep::CollectRawBits;
self.raw_bit_count = 0;
self.is_v3_sync = true;
self.raw_bits = [0; 32];
} else if is_short && duration_diff!(self.te_last, TE_SHORT) < TE_DELTA {
self.header_count += 1;
} else if is_very_long {
self.step = DecoderStep::Reset;
}
}
}
DecoderStep::CollectRawBits => {
if level {
if is_sync || is_very_long {
// End of data
let result = self.process_buffer();
self.step = DecoderStep::Reset;
return result;
} else if is_short {
self.add_raw_bit(false);
} else if is_long {
self.add_raw_bit(true);
} else {
self.step = DecoderStep::Reset;
}
} else {
if is_sync || is_very_long {
let result = self.process_buffer();
self.step = DecoderStep::Reset;
return result;
}
// LOW durations don't carry data in PWM
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0);
// Build plaintext for encryption
let plaintext = (counter as u32) |
((serial & 0xFF) << 16) |
(0x1 << 24) |
(((button & 0x0F) as u32) << 28);
let mf_key = Self::get_mf_key();
let encrypted = Self::keeloq_encrypt(plaintext, mf_key);
// Build raw bytes
let mut raw_bytes = [0u8; 9];
raw_bytes[0] = Self::reverse8((encrypted >> 0) as u8);
raw_bytes[1] = Self::reverse8((encrypted >> 8) as u8);
raw_bytes[2] = Self::reverse8((encrypted >> 16) as u8);
raw_bytes[3] = Self::reverse8((encrypted >> 24) as u8);
let serial_btn = (serial & 0x0FFFFFFF) | (((button & 0x0F) as u32) << 28);
raw_bytes[4] = Self::reverse8((serial_btn >> 0) as u8);
raw_bytes[5] = Self::reverse8((serial_btn >> 8) as u8);
raw_bytes[6] = Self::reverse8((serial_btn >> 16) as u8);
raw_bytes[7] = Self::reverse8((serial_btn >> 24) as u8);
let crc = Self::calculate_crc(&raw_bytes);
raw_bytes[8] = crc << 4;
// Use V4 encoding by default
let version = 0;
if version == 1 {
// V3: invert data
for byte in raw_bytes.iter_mut() {
*byte = !*byte;
}
}
let mut signal = Vec::with_capacity(600);
for burst in 0..TOTAL_BURSTS {
if burst > 0 {
signal.push(LevelDuration::new(false, INTER_BURST_GAP_US));
}
// Preamble
for _ in 0..PREAMBLE_PAIRS {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
// Sync pulse
if version == 0 {
// V4: long HIGH, short LOW
signal.push(LevelDuration::new(true, SYNC_DURATION));
signal.push(LevelDuration::new(false, TE_SHORT));
} else {
// V3: short HIGH, long LOW
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, SYNC_DURATION));
}
// Data bits
for byte_idx in 0..9 {
let bits_in_byte = if byte_idx == 8 { 4 } else { 8 };
for bit_idx in (8 - bits_in_byte..8).rev() {
let bit = (raw_bytes[byte_idx] >> bit_idx) & 1 != 0;
if bit {
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_SHORT));
} else {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_LONG));
}
}
}
}
Some(signal)
}
}
+312
View File
@@ -0,0 +1,312 @@
//! Kia V5 protocol decoder
//!
//! Ported from protopirate's kia_v5.c
//!
//! Protocol characteristics:
//! - Manchester encoding: 400/800µs timing
//! - 64 bits total (+3 bit CRC)
//! - Preamble of ~40+ short pairs
//! - Custom "mixer" encryption
//! - Decode-only (no encoder)
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 400;
const TE_LONG: u32 = 800;
const TE_DELTA: u32 = 150;
const MIN_COUNT_BIT: usize = 64;
/// Manchester states
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
Mid1,
Start0,
Start1,
}
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CheckPreamble,
Data,
}
/// Kia V5 protocol decoder
pub struct KiaV5Decoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
bit_count: u8,
decoded_data: u64,
saved_key: u64,
manchester_state: ManchesterState,
}
impl KiaV5Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
bit_count: 0,
decoded_data: 0,
saved_key: 0,
manchester_state: ManchesterState::Mid1,
}
}
/// Get encryption key (placeholder)
fn get_v5_key() -> u64 {
0x0000000000000000
}
/// Custom mixer decryption
fn mixer_decode(encrypted: u32) -> u16 {
let mut s0 = (encrypted & 0xFF) as u8;
let mut s1 = ((encrypted >> 8) & 0xFF) as u8;
let mut s2 = ((encrypted >> 16) & 0xFF) as u8;
let mut s3 = ((encrypted >> 24) & 0xFF) as u8;
let key = Self::get_v5_key();
let mut keystore_bytes = [0u8; 8];
for i in 0..8 {
keystore_bytes[i] = ((key >> ((7 - i) * 8)) & 0xFF) as u8;
}
let mut round_index: usize = 1;
for _ in 0..18 {
let mut r = keystore_bytes[round_index];
let mut steps = 8;
while steps > 0 {
let base = if (s3 & 0x40) == 0 {
if (s3 & 0x02) == 0 { 0x74 } else { 0x2E }
} else {
if (s3 & 0x02) == 0 { 0x3A } else { 0x5C }
};
let mut base = base;
if s2 & 0x08 != 0 {
base = ((base >> 4) & 0x0F) | ((base & 0x0F) << 4);
}
if s1 & 0x01 != 0 {
base = (base & 0x3F) << 2;
}
if s0 & 0x01 != 0 {
base = base << 1;
}
let temp = (s3 ^ s1) & 0xFF;
s3 = (s3 & 0x7F) << 1;
if s2 & 0x80 != 0 {
s3 |= 0x01;
}
s2 = (s2 & 0x7F) << 1;
if s1 & 0x80 != 0 {
s2 |= 0x01;
}
s1 = (s1 & 0x7F) << 1;
if s0 & 0x80 != 0 {
s1 |= 0x01;
}
s0 = (s0 & 0x7F) << 1;
let chk = (base ^ (r ^ temp)) & 0xFF;
if chk & 0x80 != 0 {
s0 |= 0x01;
}
r = (r & 0x7F) << 1;
steps -= 1;
}
round_index = (round_index.wrapping_sub(1)) & 0x7;
}
((s0 as u16) + ((s1 as u16) << 8)) & 0xFFFF
}
/// Reverse bits in 64-bit value
fn compute_yek(key: u64) -> u64 {
let mut yek: u64 = 0;
for i in 0..8 {
let byte = ((key >> (i * 8)) & 0xFF) as u8;
let mut reversed: u8 = 0;
for b in 0..8 {
if byte & (1 << b) != 0 {
reversed |= 1 << (7 - b);
}
}
yek |= (reversed as u64) << ((7 - i) * 8);
}
yek
}
/// Manchester state machine
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
let event = match (is_short, is_high) {
(true, true) => 0, // Short High
(true, false) => 1, // Short Low
(false, true) => 2, // Long High
(false, false) => 3, // Long Low
};
let (new_state, output) = match (self.manchester_state, event) {
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) =>
(ManchesterState::Start0, None),
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
(ManchesterState::Start1, None),
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
_ => (ManchesterState::Mid1, None),
};
self.manchester_state = new_state;
output
}
/// Parse decoded data
fn parse_data(&self) -> Option<DecodedSignal> {
if self.bit_count < MIN_COUNT_BIT as u8 {
return None;
}
let key = self.saved_key;
let yek = Self::compute_yek(key);
let serial = ((yek >> 32) & 0x0FFFFFFF) as u32;
let button = ((yek >> 60) & 0x0F) as u8;
let encrypted = (yek & 0xFFFFFFFF) as u32;
let counter = Self::mixer_decode(encrypted);
let _crc = (self.decoded_data & 0x07) as u8;
Some(DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid: true, // V5 doesn't have a standard CRC validation
data: key,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: false, // V5 is decode-only
})
}
}
impl ProtocolDecoder for KiaV5Decoder {
fn name(&self) -> &'static str {
"Kia V5"
}
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.te_last = 0;
self.header_count = 0;
self.bit_count = 0;
self.decoded_data = 0;
self.saved_key = 0;
self.manchester_state = ManchesterState::Mid1;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
match self.step {
DecoderStep::Reset => {
if level && is_short {
self.step = DecoderStep::CheckPreamble;
self.te_last = duration;
self.header_count = 1;
self.bit_count = 0;
self.decoded_data = 0;
self.manchester_state = ManchesterState::Mid1;
}
}
DecoderStep::CheckPreamble => {
if level {
if is_long {
if self.header_count > 40 {
self.step = DecoderStep::Data;
self.bit_count = 0;
self.decoded_data = 0;
self.saved_key = 0;
self.header_count = 0;
} else {
self.te_last = duration;
}
} else if is_short {
self.te_last = duration;
} else {
self.step = DecoderStep::Reset;
}
} else {
if (is_short && duration_diff!(self.te_last, TE_SHORT) < TE_DELTA) ||
(is_long && duration_diff!(self.te_last, TE_SHORT) < TE_DELTA) ||
(duration_diff!(self.te_last, TE_LONG) < TE_DELTA) {
self.header_count += 1;
} else {
self.step = DecoderStep::Reset;
}
self.te_last = duration;
}
}
DecoderStep::Data => {
if !is_short && !is_long {
// End of data - try to parse
if self.bit_count >= MIN_COUNT_BIT as u8 {
let result = self.parse_data();
self.step = DecoderStep::Reset;
return result;
}
self.step = DecoderStep::Reset;
return None;
}
if self.bit_count <= 66 {
if let Some(bit) = self.manchester_advance(is_short, level) {
self.decoded_data = (self.decoded_data << 1) | (bit as u64);
self.bit_count += 1;
if self.bit_count == 64 {
self.saved_key = self.decoded_data;
self.decoded_data = 0;
}
}
}
self.te_last = duration;
}
}
None
}
fn supports_encoding(&self) -> bool {
false // V5 is decode-only in protopirate
}
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
None // V5 doesn't support encoding
}
}
+553
View File
@@ -0,0 +1,553 @@
//! Kia V6 protocol decoder
//!
//! Ported from protopirate's kia_v6.c
//!
//! Protocol characteristics:
//! - Manchester encoding: 200/400µs timing
//! - 144 bits total (split into 3 parts)
//! - Long preamble of 600+ pairs
//! - AES-128 encryption
//! - Decode-only (no encoder)
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 200;
const TE_LONG: u32 = 400;
const TE_DELTA: u32 = 100;
const MIN_COUNT_BIT: usize = 144;
const PREAMBLE_COUNT: u16 = 601;
const XOR_MASK_LOW: u32 = 0x84AF25FB;
const XOR_MASK_HIGH: u32 = 0x638766AB;
/// AES S-box
const AES_SBOX: [u8; 256] = [
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16,
];
/// AES inverse S-box
const AES_SBOX_INV: [u8; 256] = [
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d,
];
const AES_RCON: [u8; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
/// Manchester states
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
Mid1,
Start0,
Start1,
}
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
WaitFirstHigh,
WaitLongHigh,
Data,
}
/// Kia V6 protocol decoder
pub struct KiaV6Decoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
manchester_state: ManchesterState,
data_part1_low: u32,
data_part1_high: u32,
stored_part1_low: u32,
stored_part1_high: u32,
stored_part2_low: u32,
stored_part2_high: u32,
data_part3: u16,
bit_count: u8,
}
impl KiaV6Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
manchester_state: ManchesterState::Mid1,
data_part1_low: 0,
data_part1_high: 0,
stored_part1_low: 0,
stored_part1_high: 0,
stored_part2_low: 0,
stored_part2_high: 0,
data_part3: 0,
bit_count: 0,
}
}
/// Get keystore A (placeholder)
fn get_keystore_a() -> u64 {
0x0000000000000000
}
/// Get keystore B (placeholder)
fn get_keystore_b() -> u64 {
0x0000000000000000
}
/// CRC8 calculation
fn crc8(data: &[u8], init: u8, polynomial: u8) -> u8 {
let mut crc = init;
for &byte in data {
crc ^= byte;
for _ in 0..8 {
let b = crc << 1;
if (crc & 0x80) != 0 {
crc = b ^ polynomial;
} else {
crc = b;
}
}
}
crc
}
/// GF(2^8) multiply by 2
fn gf_mul2(x: u8) -> u8 {
((x >> 7).wrapping_mul(0x1b)) ^ (x << 1)
}
/// AES inverse SubBytes
fn aes_subbytes_inv(state: &mut [u8; 16]) {
for i in 0..16 {
state[i] = AES_SBOX_INV[state[i] as usize];
}
}
/// AES inverse ShiftRows
fn aes_shiftrows_inv(state: &mut [u8; 16]) {
let temp = state[13];
state[13] = state[9];
state[9] = state[5];
state[5] = state[1];
state[1] = 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[7];
state[7] = state[11];
state[11] = state[15];
state[15] = temp;
}
/// AES inverse MixColumns
fn aes_mixcolumns_inv(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];
let a2 = Self::gf_mul2(a);
let a4 = Self::gf_mul2(a2);
let a8 = Self::gf_mul2(a4);
let b2 = Self::gf_mul2(b);
let b4 = Self::gf_mul2(b2);
let b8 = Self::gf_mul2(b4);
let c2 = Self::gf_mul2(c);
let c4 = Self::gf_mul2(c2);
let c8 = Self::gf_mul2(c4);
let d2 = Self::gf_mul2(d);
let d4 = Self::gf_mul2(d2);
let d8 = Self::gf_mul2(d4);
state[i * 4] = (a8 ^ a4 ^ a2) ^ (b8 ^ b2 ^ b) ^ (c8 ^ c4 ^ c) ^ (d8 ^ d);
state[i * 4 + 1] = (a8 ^ a) ^ (b8 ^ b4 ^ b2) ^ (c8 ^ c2 ^ c) ^ (d8 ^ d4 ^ d);
state[i * 4 + 2] = (a8 ^ a4 ^ a) ^ (b8 ^ b) ^ (c8 ^ c4 ^ c2) ^ (d8 ^ d2 ^ d);
state[i * 4 + 3] = (a8 ^ a2 ^ a) ^ (b8 ^ b4 ^ b) ^ (c8 ^ c) ^ (d8 ^ d4 ^ d2);
}
}
/// AES AddRoundKey
fn aes_addroundkey(state: &mut [u8; 16], round_key: &[u8]) {
for i in 0..16 {
state[i] ^= round_key[i];
}
}
/// AES key expansion
fn aes_key_expansion(key: &[u8; 16]) -> [u8; 176] {
let mut round_keys = [0u8; 176];
round_keys[..16].copy_from_slice(key);
for i in 4..44 {
let prev_word_idx = (i - 1) * 4;
let mut b0 = round_keys[prev_word_idx];
let mut b1 = round_keys[prev_word_idx + 1];
let mut b2 = round_keys[prev_word_idx + 2];
let mut b3 = round_keys[prev_word_idx + 3];
if (i % 4) == 0 {
let new_b0 = AES_SBOX[b1 as usize] ^ AES_RCON[(i / 4) - 1];
let new_b1 = AES_SBOX[b2 as usize];
let new_b2 = AES_SBOX[b3 as usize];
let new_b3 = AES_SBOX[b0 as usize];
b0 = new_b0;
b1 = new_b1;
b2 = new_b2;
b3 = new_b3;
}
let back_word_idx = (i - 4) * 4;
b0 ^= round_keys[back_word_idx];
b1 ^= round_keys[back_word_idx + 1];
b2 ^= round_keys[back_word_idx + 2];
b3 ^= round_keys[back_word_idx + 3];
let curr_word_idx = i * 4;
round_keys[curr_word_idx] = b0;
round_keys[curr_word_idx + 1] = b1;
round_keys[curr_word_idx + 2] = b2;
round_keys[curr_word_idx + 3] = b3;
}
round_keys
}
/// AES-128 decrypt
fn aes128_decrypt(expanded_key: &[u8; 176], data: &mut [u8; 16]) {
let mut state = *data;
Self::aes_addroundkey(&mut state, &expanded_key[160..176]);
for round in (1..10).rev() {
Self::aes_shiftrows_inv(&mut state);
Self::aes_subbytes_inv(&mut state);
Self::aes_addroundkey(&mut state, &expanded_key[round * 16..(round + 1) * 16]);
Self::aes_mixcolumns_inv(&mut state);
}
Self::aes_shiftrows_inv(&mut state);
Self::aes_subbytes_inv(&mut state);
Self::aes_addroundkey(&mut state, &expanded_key[0..16]);
*data = state;
}
/// Get AES key from keystores
fn get_aes_key() -> [u8; 16] {
let keystore_a = Self::get_keystore_a();
let keystore_a_hi = ((keystore_a >> 32) & 0xFFFFFFFF) as u32;
let keystore_a_lo = (keystore_a & 0xFFFFFFFF) as u32;
let u_var15_a = keystore_a_lo ^ XOR_MASK_LOW;
let u_var5_a = XOR_MASK_HIGH ^ keystore_a_hi;
let val64_a = ((u_var5_a as u64) << 32) | (u_var15_a as u64);
let keystore_b = Self::get_keystore_b();
let keystore_b_hi = ((keystore_b >> 32) & 0xFFFFFFFF) as u32;
let keystore_b_lo = (keystore_b & 0xFFFFFFFF) as u32;
let u_var15_b = keystore_b_lo ^ XOR_MASK_LOW;
let u_var5_b = XOR_MASK_HIGH ^ keystore_b_hi;
let val64_b = ((u_var5_b as u64) << 32) | (u_var15_b as u64);
let mut aes_key = [0u8; 16];
for i in 0..8 {
aes_key[i] = ((val64_a >> (56 - i * 8)) & 0xFF) as u8;
}
for i in 0..8 {
aes_key[i + 8] = ((val64_b >> (56 - i * 8)) & 0xFF) as u8;
}
aes_key
}
/// Decrypt the stored data
fn decrypt(&self) -> Option<(u32, u8, u32, bool)> {
let mut encrypted_data = [0u8; 16];
encrypted_data[0] = ((self.stored_part1_high >> 8) & 0xFF) as u8;
encrypted_data[1] = (self.stored_part1_high & 0xFF) as u8;
encrypted_data[2] = ((self.stored_part1_low >> 24) & 0xFF) as u8;
encrypted_data[3] = ((self.stored_part1_low >> 16) & 0xFF) as u8;
encrypted_data[4] = ((self.stored_part1_low >> 8) & 0xFF) as u8;
encrypted_data[5] = (self.stored_part1_low & 0xFF) as u8;
encrypted_data[6] = ((self.stored_part2_high >> 24) & 0xFF) as u8;
encrypted_data[7] = ((self.stored_part2_high >> 16) & 0xFF) as u8;
encrypted_data[8] = ((self.stored_part2_high >> 8) & 0xFF) as u8;
encrypted_data[9] = (self.stored_part2_high & 0xFF) as u8;
encrypted_data[10] = ((self.stored_part2_low >> 24) & 0xFF) as u8;
encrypted_data[11] = ((self.stored_part2_low >> 16) & 0xFF) as u8;
encrypted_data[12] = ((self.stored_part2_low >> 8) & 0xFF) as u8;
encrypted_data[13] = (self.stored_part2_low & 0xFF) as u8;
encrypted_data[14] = ((self.data_part3 >> 8) & 0xFF) as u8;
encrypted_data[15] = (self.data_part3 & 0xFF) as u8;
let aes_key = Self::get_aes_key();
let expanded_key = Self::aes_key_expansion(&aes_key);
Self::aes128_decrypt(&expanded_key, &mut encrypted_data);
let decrypted = &encrypted_data;
let calculated_crc = Self::crc8(&decrypted[..15], 0xFF, 0x07);
let stored_crc = decrypted[15];
let crc_valid = (calculated_crc ^ stored_crc) < 2;
// Serial: bytes 4-6 as 24-bit big-endian
let serial = ((decrypted[4] as u32) << 16) | ((decrypted[5] as u32) << 8) | (decrypted[6] as u32);
let button = decrypted[7];
let counter = ((decrypted[8] as u32) << 24) |
((decrypted[9] as u32) << 16) |
((decrypted[10] as u32) << 8) |
(decrypted[11] as u32);
Some((serial, button, counter, crc_valid))
}
/// Manchester state machine
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
let event = match (is_short, is_high) {
(true, true) => 0, // Short High
(true, false) => 2, // Short Low
(false, true) => 6, // Long High
(false, false) => 4, // Long Low
};
let (new_state, output) = match (self.manchester_state, event) {
(ManchesterState::Mid0, 2) | (ManchesterState::Mid1, 2) =>
(ManchesterState::Start0, None),
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
(ManchesterState::Start1, None),
(ManchesterState::Start1, 2) => (ManchesterState::Mid1, Some(true)),
(ManchesterState::Start1, 4) => (ManchesterState::Start0, Some(true)),
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
(ManchesterState::Start0, 6) => (ManchesterState::Start1, Some(false)),
_ => (ManchesterState::Mid1, None),
};
self.manchester_state = new_state;
output
}
/// Add initial sync bits
fn add_sync_bits(&mut self) {
// Add 1, 1, 0, 1 as initial bits
for bit in [true, true, false, true] {
let carry = self.data_part1_low >> 31;
self.data_part1_low = (self.data_part1_low << 1) | (bit as u32);
self.data_part1_high = (self.data_part1_high << 1) | carry;
self.bit_count += 1;
}
}
}
impl ProtocolDecoder for KiaV6Decoder {
fn name(&self) -> &'static str {
"Kia V6"
}
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.te_last = 0;
self.header_count = 0;
self.manchester_state = ManchesterState::Mid1;
self.data_part1_low = 0;
self.data_part1_high = 0;
self.stored_part1_low = 0;
self.stored_part1_high = 0;
self.stored_part2_low = 0;
self.stored_part2_high = 0;
self.data_part3 = 0;
self.bit_count = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
match self.step {
DecoderStep::Reset => {
if level && is_short {
self.step = DecoderStep::WaitFirstHigh;
self.te_last = duration;
self.header_count = 0;
self.manchester_state = ManchesterState::Mid1;
}
}
DecoderStep::WaitFirstHigh => {
if level {
return None;
}
let diff_short = duration_diff!(duration, TE_SHORT);
let diff_long = duration_diff!(duration, TE_LONG);
if diff_long < TE_DELTA && diff_long < diff_short {
if self.header_count >= PREAMBLE_COUNT {
self.header_count = 0;
self.te_last = duration;
self.step = DecoderStep::WaitLongHigh;
return None;
}
}
if diff_short >= TE_DELTA && diff_long >= TE_DELTA {
self.step = DecoderStep::Reset;
return None;
}
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA {
self.te_last = duration;
self.header_count += 1;
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::WaitLongHigh => {
if !level {
self.step = DecoderStep::Reset;
return None;
}
let diff_long = duration_diff!(duration, TE_LONG);
let diff_short = duration_diff!(duration, TE_SHORT);
if diff_long >= TE_DELTA && diff_short >= TE_DELTA {
self.step = DecoderStep::Reset;
return None;
}
if duration_diff!(self.te_last, TE_LONG) >= TE_DELTA {
self.step = DecoderStep::Reset;
return None;
}
self.data_part1_low = 0;
self.data_part1_high = 0;
self.bit_count = 0;
self.add_sync_bits();
self.step = DecoderStep::Data;
}
DecoderStep::Data => {
if !is_short && !is_long {
self.step = DecoderStep::Reset;
return None;
}
if let Some(bit) = self.manchester_advance(is_short, level) {
let carry = self.data_part1_low >> 31;
self.data_part1_low = (self.data_part1_low << 1) | (bit as u32);
self.data_part1_high = (self.data_part1_high << 1) | carry;
self.bit_count += 1;
if self.bit_count == 64 {
self.stored_part1_low = !self.data_part1_low;
self.stored_part1_high = !self.data_part1_high;
self.data_part1_low = 0;
self.data_part1_high = 0;
} else if self.bit_count == 128 {
self.stored_part2_low = !self.data_part1_low;
self.stored_part2_high = !self.data_part1_high;
self.data_part1_low = 0;
self.data_part1_high = 0;
}
}
self.te_last = duration;
if self.bit_count as usize == MIN_COUNT_BIT {
self.data_part3 = !(self.data_part1_low as u16);
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);
self.step = DecoderStep::Reset;
return Some(DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some((counter & 0xFFFF) as u16), // V6 has 32-bit counter but we only store 16
crc_valid,
data: key_data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: false,
});
}
self.step = DecoderStep::Reset;
}
}
}
None
}
fn supports_encoding(&self) -> bool {
false // V6 is decode-only
}
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
None
}
}
+184
View File
@@ -0,0 +1,184 @@
//! Protocol decoders and encoders for various keyfob systems.
//!
//! This module implements decoders for various keyfob protocols, ported from
//! protopirate. Each protocol processes level+duration pairs from the demodulator.
mod common;
pub mod keeloq_common;
#[allow(dead_code)]
pub mod aut64;
#[allow(dead_code)]
pub mod keys;
mod kia_v0;
mod kia_v1;
mod kia_v2;
mod kia_v3_v4;
mod kia_v5;
mod kia_v6;
mod subaru;
mod ford_v0;
mod vag;
mod fiat_v0;
mod suzuki;
mod scher_khan;
mod star_line;
mod psa;
pub use common::DecodedSignal;
use crate::capture::Capture;
use crate::radio::demodulator::LevelDuration;
/// Protocol timing constants
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct ProtocolTiming {
/// Short pulse duration in µs
pub te_short: u32,
/// Long pulse duration in µs
pub te_long: u32,
/// Tolerance for timing matching in µs
pub te_delta: u32,
/// Minimum bit count for valid decode
pub min_count_bit: usize,
}
/// Trait for protocol decoders
///
/// Each protocol implements a state machine that processes level+duration pairs.
pub trait ProtocolDecoder: Send + Sync {
/// Get the protocol name
fn name(&self) -> &'static str;
/// Get timing constants
#[allow(dead_code)]
fn timing(&self) -> ProtocolTiming;
/// Get supported frequencies in Hz
fn supported_frequencies(&self) -> &[u32];
/// Reset the decoder state machine
fn reset(&mut self);
/// Feed a level+duration pair to the decoder
/// Returns Some(DecodedSignal) when a complete valid signal is decoded
fn feed(&mut self, level: bool, duration_us: u32) -> Option<DecodedSignal>;
/// Check if this protocol supports encoding
fn supports_encoding(&self) -> bool;
/// Encode a signal with the given button command
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>>;
}
/// Registry of all supported protocols
pub struct ProtocolRegistry {
decoders: Vec<Box<dyn ProtocolDecoder>>,
}
impl ProtocolRegistry {
/// Create a new protocol registry with all built-in protocols
pub fn new() -> Self {
let decoders: Vec<Box<dyn ProtocolDecoder>> = vec![
// Kia protocols
Box::new(kia_v0::KiaV0Decoder::new()),
Box::new(kia_v1::KiaV1Decoder::new()),
Box::new(kia_v2::KiaV2Decoder::new()),
Box::new(kia_v3_v4::KiaV3V4Decoder::new()),
Box::new(kia_v5::KiaV5Decoder::new()),
Box::new(kia_v6::KiaV6Decoder::new()),
// Other protocols
Box::new(subaru::SubaruDecoder::new()),
Box::new(ford_v0::FordV0Decoder::new()),
Box::new(vag::VagDecoder::new()),
Box::new(fiat_v0::FiatV0Decoder::new()),
Box::new(suzuki::SuzukiDecoder::new()),
Box::new(scher_khan::ScherKhanDecoder::new()),
Box::new(star_line::StarLineDecoder::new()),
Box::new(psa::PsaDecoder::new()),
];
Self { decoders }
}
/// Process level+duration pairs from demodulator
/// Returns decoded signal info if any protocol matches
pub fn process_signal(&mut self, pairs: &[LevelDuration], frequency: u32) -> Option<(String, DecodedSignal)> {
// Reset all decoders
for decoder in &mut self.decoders {
decoder.reset();
}
// Feed pairs to all decoders that support this frequency
for pair in pairs {
for decoder in &mut self.decoders {
// Check frequency support
let freq_supported = decoder
.supported_frequencies()
.iter()
.any(|&f| {
let diff = if f > frequency { f - frequency } else { frequency - f };
diff < (f / 50) // 2% tolerance
});
if !freq_supported {
continue;
}
if let Some(decoded) = decoder.feed(pair.level, pair.duration_us) {
return Some((decoder.name().to_string(), decoded));
}
}
}
None
}
/// Try to decode a capture (for compatibility with old interface)
#[allow(dead_code)]
pub fn try_decode(&mut self, capture: &Capture) -> Option<(String, DecodedSignal)> {
// Convert raw pairs to LevelDuration and process
if capture.raw_pairs.is_empty() {
return None;
}
let pairs: Vec<LevelDuration> = capture.raw_pairs
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
self.process_signal(&pairs, capture.frequency)
}
/// Get a decoder by name
pub fn get(&self, name: &str) -> Option<&dyn ProtocolDecoder> {
self.decoders
.iter()
.find(|d| d.name().eq_ignore_ascii_case(name))
.map(|d| d.as_ref())
}
/// List all protocol names
#[allow(dead_code)]
pub fn list_protocols(&self) -> Vec<&'static str> {
self.decoders.iter().map(|d| d.name()).collect()
}
}
impl Default for ProtocolRegistry {
fn default() -> Self {
Self::new()
}
}
/// Helper macro for duration comparison (matches protopirate's DURATION_DIFF)
#[macro_export]
macro_rules! duration_diff {
($actual:expr, $expected:expr) => {
if $actual > $expected {
$actual - $expected
} else {
$expected - $actual
}
};
}
+489
View File
@@ -0,0 +1,489 @@
//! PSA (Peugeot/Citroen) protocol decoder/encoder
//!
//! Ported from protopirate's psa.c
//!
//! Protocol characteristics:
//! - Manchester encoding: 125/250µs bit timing, 250/500µs symbol timing
//! - 128 bits total (key1: 64 bits, key2: 16 bits, validation: 48 bits)
//! - TEA and XOR encryption schemes
//! - Two modes: 0x23 and 0x36
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 = 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;
// TEA constants
const TEA_DELTA: u32 = 0x9E3779B9;
const TEA_ROUNDS: u32 = 32;
// Brute-force constants for mode 0x23
const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A64];
// Brute-force constants for mode 0x36
const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04];
/// Manchester decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
Mid1,
Start0,
Start1,
}
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderState {
/// Waiting for first edge
WaitEdge,
/// Counting preamble pattern
CountPattern,
/// Decoding Manchester data
DecodeManchester,
/// Found end of data
End,
}
/// PSA protocol decoder
pub struct PsaDecoder {
state: DecoderState,
prev_duration: u32,
manchester_state: ManchesterState,
pattern_counter: u16,
data_low: u32,
data_high: u32,
bit_count: u8,
// Decoded fields
key1_low: u32,
key1_high: u32,
validation_field: u16,
key2_low: u32,
key2_high: u32,
seed: u32,
}
impl PsaDecoder {
pub fn new() -> Self {
Self {
state: DecoderState::WaitEdge,
prev_duration: 0,
manchester_state: ManchesterState::Mid1,
pattern_counter: 0,
data_low: 0,
data_high: 0,
bit_count: 0,
key1_low: 0,
key1_high: 0,
validation_field: 0,
key2_low: 0,
key2_high: 0,
seed: 0,
}
}
/// Manchester advance
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
let event = match (is_short, is_high) {
(true, true) => 0,
(true, false) => 1,
(false, true) => 2,
(false, false) => 3,
};
let (new_state, output) = match (self.manchester_state, event) {
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => {
(ManchesterState::Start1, None)
}
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => {
(ManchesterState::Start0, None)
}
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
_ => (ManchesterState::Mid1, None),
};
self.manchester_state = new_state;
output
}
fn add_bit(&mut self, bit: bool) {
let new_bit = if bit { 1u32 } else { 0u32 };
let carry = (self.data_low >> 31) & 1;
self.data_low = (self.data_low << 1) | new_bit;
self.data_high = (self.data_high << 1) | carry;
self.bit_count += 1;
// Extract key1 at 64 bits
if self.bit_count == 64 {
self.key1_low = self.data_low;
self.key1_high = self.data_high;
self.data_low = 0;
self.data_high = 0;
}
// Extract validation at 80 bits (16 more)
else if self.bit_count == 80 {
self.validation_field = self.data_low as u16;
self.data_low = 0;
self.data_high = 0;
}
}
/// TEA decrypt
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])),
);
sum = sum.wrapping_sub(TEA_DELTA);
}
}
/// TEA encrypt
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
let mut sum: u32 = 0;
for _ in 0..TEA_ROUNDS {
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])),
);
*v1 = v1.wrapping_add(
(v0.wrapping_shl(4).wrapping_add(key[2]))
^ (v0.wrapping_add(sum))
^ (v0.wrapping_shr(5).wrapping_add(key[3])),
);
}
}
/// XOR decrypt (mode 0x23)
fn xor_decrypt(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];
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;
}
fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> {
// Try mode 0x23 first
let seed_byte = (self.key1_high >> 24) as u8;
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; 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;
Self::xor_decrypt(&mut buffer);
let serial = ((buffer[2] as u32) << 16)
| ((buffer[3] as u32) << 8)
| (buffer[4] as u32);
let counter = ((buffer[5] as u32) << 8) | (buffer[6] as u32);
let crc = buffer[7] as u16;
let btn = buffer[8] & 0x0F;
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);
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;
return Some((serial, btn, counter, crc, 0x36));
}
// Cannot decrypt - return raw data
None
}
fn parse_data(&self) -> DecodedSignal {
// Combine into 128-bit data (store lower 64)
let data = ((self.key1_high as u64) << 32) | (self.key1_low as u64);
if let Some((serial, btn, counter, _crc, _mode)) = self.try_decrypt() {
DecodedSignal {
serial: Some(serial),
button: Some(btn),
counter: Some(counter as u16),
crc_valid: true,
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
}
} else {
DecodedSignal {
serial: None,
button: None,
counter: None,
crc_valid: false,
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: false,
}
}
}
}
impl ProtocolDecoder for PsaDecoder {
fn name(&self) -> &'static str {
"PSA"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: MIN_COUNT_BIT,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[433_920_000]
}
fn reset(&mut self) {
self.state = DecoderState::WaitEdge;
self.prev_duration = 0;
self.manchester_state = ManchesterState::Mid1;
self.pattern_counter = 0;
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.key1_low = 0;
self.key1_high = 0;
self.validation_field = 0;
self.key2_low = 0;
self.key2_high = 0;
self.seed = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.state {
DecoderState::WaitEdge => {
if level && duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_49 {
self.state = DecoderState::CountPattern;
self.prev_duration = duration;
self.pattern_counter = 0;
}
}
DecoderState::CountPattern => {
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;
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.manchester_state = ManchesterState::Mid1;
self.prev_duration = duration;
} else if self.pattern_counter < 2 {
self.state = DecoderState::WaitEdge;
} else {
self.prev_duration = duration;
}
}
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);
}
self.state = DecoderState::WaitEdge;
return None;
}
if is_short || is_long {
if let Some(bit) = self.manchester_advance(is_short, level) {
self.add_bit(bit);
}
} else {
self.state = DecoderState::WaitEdge;
}
self.prev_duration = duration;
}
DecoderState::End => {
self.state = DecoderState::WaitEdge;
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0).wrapping_add(1) as u32;
// Build plaintext buffer for mode 0x23
let mut buffer = [0u8; 10];
buffer[0] = 0x23;
buffer[1] = 0x00;
buffer[2] = (serial >> 16) as u8;
buffer[3] = (serial >> 8) as u8;
buffer[4] = serial as u8;
buffer[5] = (counter >> 8) as u8;
buffer[6] = counter as u8;
buffer[7] = 0; // CRC placeholder
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;
}
// TEA encrypt
let mut v0 = ((buffer[0] as u32) << 24)
| ((buffer[1] as u32) << 16)
| ((buffer[2] as u32) << 8)
| (buffer[3] as u32);
let mut v1 = ((buffer[4] as u32) << 24)
| ((buffer[5] as u32) << 16)
| ((buffer[6] as u32) << 8)
| (buffer[7] as u32);
Self::tea_encrypt(&mut v0, &mut v1, &BF1_KEY_SCHEDULE);
let key1_high = v0;
let key1_low = v1;
let validation = ((buffer[8] as u16) << 8) | (buffer[9] as u16);
// Build signal
let mut signal = Vec::with_capacity(512);
// Preamble: alternating 125µs pulses
for _ in 0..70 {
signal.push(LevelDuration::new(true, TE_SHORT_125));
signal.push(LevelDuration::new(false, TE_SHORT_125));
}
// Sync
signal.push(LevelDuration::new(true, TE_LONG_250));
signal.push(LevelDuration::new(false, TE_LONG_250));
// Key1: 64 bits Manchester encoded
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));
} else {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
}
// Validation: 16 bits Manchester encoded
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));
} else {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
}
// End marker
signal.push(LevelDuration::new(false, TE_END_1000));
Some(signal)
}
}
+201
View File
@@ -0,0 +1,201 @@
//! Scher-Khan protocol decoder
//!
//! Ported from protopirate's scher_khan.c
//!
//! Protocol characteristics:
//! - PWM encoding: 750/1100µs timing
//! - Variable bit count (35, 51, 57, 63, 64, 81, 82)
//! - Decode-only (no encoder)
//!
//! References:
//! - https://phreakerclub.com/72
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
use crate::radio::demodulator::LevelDuration;
const TE_SHORT: u32 = 750;
const TE_LONG: u32 = 1100;
const TE_DELTA: u32 = 160;
const MIN_COUNT_BIT: usize = 35;
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CheckPreamble,
SaveDuration,
CheckDuration,
}
/// Scher-Khan protocol decoder
pub struct ScherKhanDecoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
decode_data: u64,
decode_count_bit: usize,
}
impl ScherKhanDecoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
decode_data: 0,
decode_count_bit: 0,
}
}
fn parse_data(data: u64, bit_count: usize) -> DecodedSignal {
let (serial, btn, cnt) = match bit_count {
51 => {
// MAGIC CODE, Dynamic
let serial =
((data >> 24) & 0xFFFFFF0) as u32 | ((data >> 20) & 0x0F) as u32;
let btn = ((data >> 24) & 0x0F) as u8;
let cnt = (data & 0xFFFF) as u16;
(Some(serial), Some(btn), Some(cnt))
}
_ => (None, None, None),
};
DecodedSignal {
serial,
button: btn,
counter: cnt,
crc_valid: true,
data,
data_count_bit: bit_count,
encoder_capable: false,
}
}
}
impl ProtocolDecoder for ScherKhanDecoder {
fn name(&self) -> &'static str {
"Scher-Khan"
}
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.te_last = 0;
self.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if level && duration_diff!(duration, TE_SHORT * 2) < TE_DELTA {
self.step = DecoderStep::CheckPreamble;
self.te_last = duration;
self.header_count = 0;
}
}
DecoderStep::CheckPreamble => {
if level {
if duration_diff!(duration, TE_SHORT * 2) < TE_DELTA
|| duration_diff!(duration, TE_SHORT) < TE_DELTA
{
self.te_last = duration;
} else {
self.step = DecoderStep::Reset;
}
} else if duration_diff!(duration, TE_SHORT * 2) < TE_DELTA
|| duration_diff!(duration, TE_SHORT) < TE_DELTA
{
if duration_diff!(self.te_last, TE_SHORT * 2) < TE_DELTA {
self.header_count += 1;
} else if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA {
// Found start bit
if self.header_count >= 2 {
self.step = DecoderStep::SaveDuration;
self.decode_data = 0;
self.decode_count_bit = 1;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::SaveDuration => {
if level {
if duration >= (TE_DELTA * 2 + TE_LONG) {
// Found stop bit
self.step = DecoderStep::Reset;
if self.decode_count_bit >= MIN_COUNT_BIT {
let result =
Self::parse_data(self.decode_data, self.decode_count_bit);
self.decode_data = 0;
self.decode_count_bit = 0;
return Some(result);
}
self.decode_data = 0;
self.decode_count_bit = 0;
} else {
self.te_last = duration;
self.step = DecoderStep::CheckDuration;
}
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::CheckDuration => {
if !level {
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA
&& duration_diff!(duration, TE_SHORT) < TE_DELTA
{
// Bit 0
self.decode_data = (self.decode_data << 1) | 0;
self.decode_count_bit += 1;
self.step = DecoderStep::SaveDuration;
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA
&& duration_diff!(duration, TE_LONG) < TE_DELTA
{
// Bit 1
self.decode_data = (self.decode_data << 1) | 1;
self.decode_count_bit += 1;
self.step = DecoderStep::SaveDuration;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
}
None
}
fn supports_encoding(&self) -> bool {
false
}
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
None
}
}
+269
View File
@@ -0,0 +1,269 @@
//! Star Line protocol decoder/encoder
//!
//! Ported from protopirate's star_line.c
//!
//! Protocol characteristics:
//! - PWM encoding: 250/500µs timing
//! - 64 bits total
//! - Header: 6 pairs of 1000µs HIGH + 1000µs LOW
//! - KeeLoq encryption (requires manufacturer key)
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key};
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 = 120;
const MIN_COUNT_BIT: usize = 64;
const HEADER_DURATION: u32 = 1000; // te_long * 2
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CheckPreamble,
SaveDuration,
CheckDuration,
}
/// Star Line protocol decoder
pub struct StarLineDecoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
decode_data: u64,
decode_count_bit: usize,
}
impl StarLineDecoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
decode_data: 0,
decode_count_bit: 0,
}
}
/// Get manufacturer key (placeholder)
fn get_mf_key() -> u64 {
0x0000000000000000
}
fn parse_data(data: u64) -> DecodedSignal {
// Data is stored MSB-first in the air, reverse to get fix|hop
let reversed = reverse_key(data, MIN_COUNT_BIT);
let key_fix = (reversed >> 32) as u32;
let key_hop = (reversed & 0xFFFFFFFF) as u32;
let serial = key_fix & 0x00FFFFFF;
let btn = (key_fix >> 24) as u8;
// Attempt KeeLoq decryption
let mf_key = Self::get_mf_key();
let counter = if mf_key != 0 {
// Try simple learning first
let decrypt = keeloq_decrypt(key_hop, mf_key);
let dec_btn = (decrypt >> 24) as u8;
let dec_serial_lsb = ((decrypt >> 16) & 0xFF) as u8;
let serial_lsb = (serial & 0xFF) as u8;
if dec_btn == btn && dec_serial_lsb == serial_lsb {
Some((decrypt & 0xFFFF) as u16)
} else {
// Try normal learning
let man_key = keeloq_normal_learning(key_fix, mf_key);
let decrypt = keeloq_decrypt(key_hop, man_key);
let dec_btn = (decrypt >> 24) as u8;
let dec_serial_lsb = ((decrypt >> 16) & 0xFF) as u8;
if dec_btn == btn && dec_serial_lsb == serial_lsb {
Some((decrypt & 0xFFFF) as u16)
} else {
None
}
}
} else {
None
};
DecodedSignal {
serial: Some(serial),
button: Some(btn),
counter: counter.or(Some(0)),
crc_valid: counter.is_some() || mf_key == 0,
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
}
}
}
impl ProtocolDecoder for StarLineDecoder {
fn name(&self) -> &'static str {
"Star Line"
}
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.te_last = 0;
self.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if level {
if duration_diff!(duration, HEADER_DURATION) < TE_DELTA * 2 {
self.step = DecoderStep::CheckPreamble;
self.header_count += 1;
} else if self.header_count > 4 {
self.decode_data = 0;
self.decode_count_bit = 0;
self.te_last = duration;
self.step = DecoderStep::CheckDuration;
}
} else {
self.header_count = 0;
}
}
DecoderStep::CheckPreamble => {
if !level && duration_diff!(duration, HEADER_DURATION) < TE_DELTA * 2 {
// Found preamble pair
self.step = DecoderStep::Reset;
} else {
self.header_count = 0;
self.step = DecoderStep::Reset;
}
}
DecoderStep::SaveDuration => {
if level {
if duration >= (TE_LONG + TE_DELTA) {
// End of data - check if we have enough bits
self.step = DecoderStep::Reset;
if self.decode_count_bit >= MIN_COUNT_BIT
&& self.decode_count_bit <= MIN_COUNT_BIT + 2
{
let result = Self::parse_data(self.decode_data);
self.decode_data = 0;
self.decode_count_bit = 0;
self.header_count = 0;
return Some(result);
}
self.decode_data = 0;
self.decode_count_bit = 0;
self.header_count = 0;
} else {
self.te_last = duration;
self.step = DecoderStep::CheckDuration;
}
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::CheckDuration => {
if !level {
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA
&& duration_diff!(duration, TE_SHORT) < TE_DELTA
{
// Bit 0: short HIGH + short LOW
if self.decode_count_bit < MIN_COUNT_BIT {
self.decode_data = (self.decode_data << 1) | 0;
self.decode_count_bit += 1;
} else {
self.decode_count_bit += 1;
}
self.step = DecoderStep::SaveDuration;
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA
&& duration_diff!(duration, TE_LONG) < TE_DELTA
{
// Bit 1: long HIGH + long LOW
if self.decode_count_bit < MIN_COUNT_BIT {
self.decode_data = (self.decode_data << 1) | 1;
self.decode_count_bit += 1;
} else {
self.decode_count_bit += 1;
}
self.step = DecoderStep::SaveDuration;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0).wrapping_add(1);
let fix = ((button as u32) << 24) | (serial & 0x00FFFFFF);
let plaintext = ((button as u32) << 24)
| (((serial & 0xFF) as u32) << 16)
| (counter as u32);
let mf_key = Self::get_mf_key();
let hop = if mf_key != 0 {
keeloq_encrypt(plaintext, mf_key)
} else {
// Without a key, replay the original hop
let reversed = reverse_key(decoded.data, MIN_COUNT_BIT);
(reversed & 0xFFFFFFFF) as u32
};
let yek = ((fix as u64) << 32) | (hop as u64);
let data = reverse_key(yek, MIN_COUNT_BIT);
let mut signal = Vec::with_capacity(256);
// Header: 6 pairs of LONG*2 HIGH + LONG*2 LOW
for _ in 0..6 {
signal.push(LevelDuration::new(true, HEADER_DURATION));
signal.push(LevelDuration::new(false, HEADER_DURATION));
}
// Data: 64 bits, MSB first
for bit in (0..64).rev() {
if (data >> bit) & 1 == 1 {
// Bit 1: LONG HIGH + LONG LOW
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_LONG));
} else {
// Bit 0: SHORT HIGH + SHORT LOW
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
}
Some(signal)
}
}
+323
View File
@@ -0,0 +1,323 @@
//! Subaru protocol decoder
//!
//! Ported from protopirate's subaru.c
//!
//! Protocol characteristics:
//! - PWM encoding: short HIGH (800µs) = 1, long HIGH (1600µs) = 0
//! - 64 bits total
//! - Long preamble of 1600µs pulses
//! - Gap and sync pattern
//! - Complex counter encoding
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
const TE_SHORT: u32 = 800;
const TE_LONG: u32 = 1600;
const TE_DELTA: u32 = 200;
#[allow(dead_code)]
const MIN_COUNT_BIT: usize = 64;
const GAP_US: u32 = 2800;
const SYNC_US: u32 = 2800;
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CheckPreamble,
FoundGap,
FoundSync,
SaveDuration,
CheckDuration,
}
/// Subaru protocol decoder
pub struct SubaruDecoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
data: [u8; 8],
bit_count: usize,
}
impl SubaruDecoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
data: [0u8; 8],
bit_count: 0,
}
}
/// Add a bit to the data buffer
fn add_bit(&mut self, bit: bool) {
if self.bit_count < 64 {
let byte_idx = self.bit_count / 8;
let bit_idx = 7 - (self.bit_count % 8);
if bit {
self.data[byte_idx] |= 1 << bit_idx;
} else {
self.data[byte_idx] &= !(1 << bit_idx);
}
self.bit_count += 1;
}
}
/// Decode the counter from the complex Subaru encoding
fn decode_counter(kb: &[u8; 8]) -> u16 {
let mut lo: u8 = 0;
if (kb[4] & 0x40) == 0 { lo |= 0x01; }
if (kb[4] & 0x80) == 0 { lo |= 0x02; }
if (kb[5] & 0x01) == 0 { lo |= 0x04; }
if (kb[5] & 0x02) == 0 { lo |= 0x08; }
if (kb[6] & 0x01) == 0 { lo |= 0x10; }
if (kb[6] & 0x02) == 0 { lo |= 0x20; }
if (kb[5] & 0x40) == 0 { lo |= 0x40; }
if (kb[5] & 0x80) == 0 { lo |= 0x80; }
let mut reg_sh1 = (kb[7] << 4) & 0xF0;
if kb[5] & 0x04 != 0 { reg_sh1 |= 0x04; }
if kb[5] & 0x08 != 0 { reg_sh1 |= 0x08; }
if kb[6] & 0x80 != 0 { reg_sh1 |= 0x02; }
if kb[6] & 0x40 != 0 { reg_sh1 |= 0x01; }
let reg_sh2 = ((kb[6] << 2) & 0xF0) | ((kb[7] >> 4) & 0x0F);
let mut ser0 = kb[3];
let mut ser1 = kb[1];
let mut ser2 = kb[2];
let total_rot = 4 + lo;
for _ in 0..total_rot {
let t_bit = (ser0 >> 7) & 1;
ser0 = ((ser0 << 1) & 0xFE) | ((ser1 >> 7) & 1);
ser1 = ((ser1 << 1) & 0xFE) | ((ser2 >> 7) & 1);
ser2 = ((ser2 << 1) & 0xFE) | t_bit;
}
let t1 = ser1 ^ reg_sh1;
let t2 = ser2 ^ reg_sh2;
let mut hi: u8 = 0;
if (t1 & 0x10) == 0 { hi |= 0x04; }
if (t1 & 0x20) == 0 { hi |= 0x08; }
if (t2 & 0x80) == 0 { hi |= 0x02; }
if (t2 & 0x40) == 0 { hi |= 0x01; }
if (t1 & 0x01) == 0 { hi |= 0x40; }
if (t1 & 0x02) == 0 { hi |= 0x80; }
if (t2 & 0x08) == 0 { hi |= 0x20; }
if (t2 & 0x04) == 0 { hi |= 0x10; }
((hi as u16) << 8) | (lo as u16)
}
/// Process the decoded data
fn process_data(&self) -> Option<DecodedSignal> {
if self.bit_count < 64 {
return None;
}
let b = &self.data;
// Build 64-bit key
let key = ((b[0] as u64) << 56) | ((b[1] as u64) << 48) |
((b[2] as u64) << 40) | ((b[3] as u64) << 32) |
((b[4] as u64) << 24) | ((b[5] as u64) << 16) |
((b[6] as u64) << 8) | (b[7] as u64);
let serial = ((b[1] as u32) << 16) | ((b[2] as u32) << 8) | (b[3] as u32);
let button = b[0] & 0x0F;
let counter = Self::decode_counter(&self.data);
Some(DecodedSignal {
serial: Some(serial),
button: Some(button),
counter: Some(counter),
crc_valid: true, // Subaru doesn't use CRC
data: key,
data_count_bit: 64,
encoder_capable: true,
})
}
}
impl ProtocolDecoder for SubaruDecoder {
fn name(&self) -> &'static str {
"Subaru"
}
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.te_last = 0;
self.header_count = 0;
self.data = [0u8; 8];
self.bit_count = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if level && duration_diff!(duration, TE_LONG) < TE_DELTA {
self.step = DecoderStep::CheckPreamble;
self.te_last = duration;
self.header_count = 1;
}
}
DecoderStep::CheckPreamble => {
if !level {
if duration_diff!(duration, TE_LONG) < TE_DELTA {
self.header_count += 1;
} else if duration > 2000 && duration < 3500 {
// Gap detected
if self.header_count > 20 {
self.step = DecoderStep::FoundGap;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
} else {
if duration_diff!(duration, TE_LONG) < TE_DELTA {
self.te_last = duration;
self.header_count += 1;
} else {
self.step = DecoderStep::Reset;
}
}
}
DecoderStep::FoundGap => {
if level && duration > 2000 && duration < 3500 {
self.step = DecoderStep::FoundSync;
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::FoundSync => {
if !level && duration_diff!(duration, TE_LONG) < TE_DELTA {
self.step = DecoderStep::SaveDuration;
self.bit_count = 0;
self.data = [0u8; 8];
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::SaveDuration => {
if level {
if duration_diff!(duration, TE_SHORT) < TE_DELTA {
// Short HIGH = bit 1
self.add_bit(true);
self.te_last = duration;
self.step = DecoderStep::CheckDuration;
} else if duration_diff!(duration, TE_LONG) < TE_DELTA {
// Long HIGH = bit 0
self.add_bit(false);
self.te_last = duration;
self.step = DecoderStep::CheckDuration;
} else if duration > 3000 {
// End of transmission
if self.bit_count >= 64 {
let result = self.process_data();
self.step = DecoderStep::Reset;
return result;
}
self.step = DecoderStep::Reset;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::CheckDuration => {
if !level {
if duration_diff!(duration, TE_SHORT) < TE_DELTA ||
duration_diff!(duration, TE_LONG) < TE_DELTA {
self.step = DecoderStep::SaveDuration;
} else if duration > 3000 {
// Gap - end of packet
if self.bit_count >= 64 {
let result = self.process_data();
self.step = DecoderStep::Reset;
return result;
}
self.step = DecoderStep::Reset;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
let key = decoded.data;
let mut signal = Vec::with_capacity(512);
// Generate 3 bursts
for burst in 0..3 {
if burst > 0 {
signal.push(LevelDuration::new(false, 25000));
}
// Preamble: 80 long HIGH/LOW pairs
for _ in 0..80 {
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_LONG));
}
// Gap
signal.push(LevelDuration::new(false, GAP_US));
// Sync
signal.push(LevelDuration::new(true, SYNC_US));
signal.push(LevelDuration::new(false, TE_LONG));
// Data: 64 bits (MSB first)
// Short HIGH = 1, Long HIGH = 0
for bit in (0..64).rev() {
if (key >> bit) & 1 == 1 {
signal.push(LevelDuration::new(true, TE_SHORT));
} else {
signal.push(LevelDuration::new(true, TE_LONG));
}
signal.push(LevelDuration::new(false, TE_SHORT));
}
// End marker
signal.push(LevelDuration::new(false, TE_LONG * 2));
}
Some(signal)
}
}
+203
View File
@@ -0,0 +1,203 @@
//! Suzuki protocol decoder/encoder
//!
//! Ported from protopirate's suzuki.c
//!
//! Protocol characteristics:
//! - PWM encoding: 250/500µs timing
//! - 64 bits total
//! - 350 preamble pairs
//! - 2000µs gap between transmissions
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 = 99;
const MIN_COUNT_BIT: usize = 64;
const PREAMBLE_COUNT: u16 = 350;
const GAP_TIME: u32 = 2000;
const GAP_DELTA: u32 = 399;
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CountPreamble,
DecodeData,
}
/// Suzuki protocol decoder
pub struct SuzukiDecoder {
step: DecoderStep,
header_count: u16,
decode_data: u64,
decode_count_bit: usize,
te_last: u32,
}
impl SuzukiDecoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
header_count: 0,
decode_data: 0,
decode_count_bit: 0,
te_last: 0,
}
}
fn add_bit(&mut self, bit: u8) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
fn parse_data(data: u64) -> DecodedSignal {
let data_high = (data >> 32) as u32;
let data_low = data as u32;
let serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
let btn = ((data_low >> 12) & 0xF) as u8;
let cnt = ((data_high << 4) >> 16) as u16;
DecodedSignal {
serial: Some(serial),
button: Some(btn),
counter: Some(cnt),
crc_valid: true, // CRC checked via structure
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
}
}
}
impl ProtocolDecoder for SuzukiDecoder {
fn name(&self) -> &'static str {
"Suzuki"
}
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.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
self.te_last = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if !level {
return None;
}
if duration_diff!(duration, TE_SHORT) > TE_DELTA {
return None;
}
self.decode_data = 0;
self.decode_count_bit = 0;
self.step = DecoderStep::CountPreamble;
self.header_count = 0;
}
DecoderStep::CountPreamble => {
if level {
// HIGH pulse
if self.header_count >= 300 {
if duration_diff!(duration, TE_LONG) <= TE_DELTA {
self.step = DecoderStep::DecodeData;
self.add_bit(1);
}
}
} else {
// LOW pulse
if duration_diff!(duration, TE_SHORT) <= TE_DELTA {
self.te_last = duration;
self.header_count += 1;
} else {
self.step = DecoderStep::Reset;
}
}
}
DecoderStep::DecodeData => {
if level {
// HIGH pulse - determines bit value
let diff_long = duration_diff!(duration, TE_LONG);
let diff_short = duration_diff!(duration, TE_SHORT);
if diff_long <= TE_DELTA {
self.add_bit(1);
} else if diff_short <= TE_DELTA {
self.add_bit(0);
}
} else {
// LOW pulse - check for gap (end of transmission)
let diff_gap = duration_diff!(duration, GAP_TIME);
if diff_gap <= GAP_DELTA {
if self.decode_count_bit == MIN_COUNT_BIT {
let result = Self::parse_data(self.decode_data);
self.decode_data = 0;
self.decode_count_bit = 0;
self.step = DecoderStep::Reset;
return Some(result);
}
self.decode_data = 0;
self.decode_count_bit = 0;
self.step = DecoderStep::Reset;
}
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
let data = decoded.data;
let mut signal = Vec::with_capacity(1024);
// Preamble: SHORT HIGH / SHORT LOW pairs
for _ in 0..PREAMBLE_COUNT {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
// Data: 64 bits, MSB first
// SHORT HIGH (~250µs) = 0, LONG HIGH (~500µs) = 1
for bit in (0..64).rev() {
if (data >> bit) & 1 == 1 {
signal.push(LevelDuration::new(true, TE_LONG));
} else {
signal.push(LevelDuration::new(true, TE_SHORT));
}
signal.push(LevelDuration::new(false, TE_SHORT));
}
// End gap
signal.push(LevelDuration::new(false, GAP_TIME));
Some(signal)
}
}
+1137
View File
File diff suppressed because it is too large Load Diff
+194
View File
@@ -0,0 +1,194 @@
//! AM/OOK demodulator for extracting level+duration pairs from raw IQ samples.
//!
//! This demodulator converts raw IQ samples into a stream of (level, duration_us) pairs
//! that can be processed by protocol decoders, similar to how the Flipper Zero SubGHz
//! system works.
/// A single level+duration pair representing one segment of the signal
#[derive(Debug, Clone, Copy)]
pub struct LevelDuration {
/// Signal level (true = high, false = low)
pub level: bool,
/// Duration in microseconds
pub duration_us: u32,
}
impl LevelDuration {
pub fn new(level: bool, duration_us: u32) -> Self {
Self { level, duration_us }
}
}
/// Demodulator for processing raw IQ samples into level+duration pairs
pub struct Demodulator {
/// Sample rate in Hz
#[allow(dead_code)]
sample_rate: u32,
/// Samples per microsecond
samples_per_us: f64,
/// Current threshold for high/low detection
threshold: f32,
/// Adaptive threshold - high level estimate
high_level: f32,
/// Adaptive threshold - low level estimate
low_level: f32,
/// Current signal state (high or low)
current_level: bool,
/// Sample count at current level
level_sample_count: u64,
/// Accumulated level+duration pairs
pairs: Vec<LevelDuration>,
/// Total samples processed
total_samples: u64,
/// Minimum duration to consider valid (in µs)
min_duration_us: u32,
/// Maximum gap before considering signal complete (in µs)
max_gap_us: u32,
/// Samples since last edge
samples_since_edge: u64,
}
impl Demodulator {
/// Create a new demodulator
pub fn new(sample_rate: u32) -> Self {
Self {
sample_rate,
samples_per_us: sample_rate as f64 / 1_000_000.0,
threshold: 0.15,
high_level: 0.3,
low_level: 0.05,
current_level: false,
level_sample_count: 0,
pairs: Vec::with_capacity(2048),
total_samples: 0,
min_duration_us: 50, // Minimum 50µs pulse
max_gap_us: 10_000, // 10ms gap = end of signal
samples_since_edge: 0,
}
}
/// Process raw IQ samples and return level+duration pairs if signal complete
///
/// Returns None if still accumulating, Some(pairs) when a complete signal is detected
pub fn process_samples(&mut self, samples: &[i8]) -> Option<Vec<LevelDuration>> {
// Process each IQ sample pair
for chunk in samples.chunks(2) {
if chunk.len() < 2 {
continue;
}
// Calculate magnitude (AM envelope detection)
let i = chunk[0] as f32 / 128.0;
let q = chunk[1] as f32 / 128.0;
let magnitude = (i * i + q * q).sqrt();
// Update adaptive threshold
self.update_threshold(magnitude);
// Detect level
let is_high = magnitude > self.threshold;
// Check for level change
if is_high != self.current_level && self.level_sample_count > 0 {
// Calculate duration of the previous level
let duration_us = (self.level_sample_count as f64 / self.samples_per_us) as u32;
// Only record if above minimum duration (noise filtering)
if duration_us >= self.min_duration_us {
self.pairs.push(LevelDuration::new(self.current_level, duration_us));
self.samples_since_edge = 0;
}
self.current_level = is_high;
self.level_sample_count = 1;
} else {
self.level_sample_count += 1;
self.samples_since_edge += 1;
}
self.total_samples += 1;
}
// Check if we have a complete signal (long gap detected)
let gap_samples = (self.max_gap_us as f64 * self.samples_per_us) as u64;
if !self.pairs.is_empty() && self.samples_since_edge > gap_samples {
// Add the final level duration
let duration_us = (self.level_sample_count as f64 / self.samples_per_us) as u32;
if duration_us >= self.min_duration_us {
self.pairs.push(LevelDuration::new(self.current_level, duration_us));
}
// Return the pairs and reset
let result = std::mem::take(&mut self.pairs);
self.reset_state();
if result.len() >= 10 {
return Some(result);
}
}
// Limit buffer size
if self.pairs.len() > 4096 {
self.reset_state();
}
None
}
/// Update adaptive threshold based on signal levels
fn update_threshold(&mut self, magnitude: f32) {
const ALPHA: f32 = 0.001; // Slow adaptation
if magnitude > self.threshold {
// Update high level estimate
self.high_level = self.high_level * (1.0 - ALPHA) + magnitude * ALPHA;
} else {
// Update low level estimate
self.low_level = self.low_level * (1.0 - ALPHA) + magnitude * ALPHA;
}
// Threshold is midpoint between low and high
self.threshold = (self.low_level + self.high_level) / 2.0;
// Ensure reasonable bounds
self.threshold = self.threshold.max(0.05).min(0.5);
}
/// Reset the demodulator state
fn reset_state(&mut self) {
self.pairs.clear();
self.level_sample_count = 0;
self.samples_since_edge = 0;
self.current_level = false;
}
/// Reset completely (including threshold adaptation)
#[allow(dead_code)]
pub fn reset(&mut self) {
self.reset_state();
self.threshold = 0.15;
self.high_level = 0.3;
self.low_level = 0.05;
}
}
// Note: duration_diff macro is defined in protocols/mod.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_demodulator_creation() {
let demod = Demodulator::new(2_000_000);
assert_eq!(demod.sample_rate, 2_000_000);
}
#[test]
fn test_level_duration() {
let ld = LevelDuration::new(true, 500);
assert!(ld.level);
assert_eq!(ld.duration_us, 500);
}
}
+434
View File
@@ -0,0 +1,434 @@
//! HackRF device control.
//!
//! This module provides a high-level interface for controlling HackRF devices
//! using the `libhackrf` crate. Falls back to demo mode at runtime if no
//! HackRF hardware is detected.
use anyhow::Result;
use std::sync::mpsc::Sender;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
};
use std::thread::{self, JoinHandle};
use crate::app::RadioEvent;
use crate::capture::Capture;
use super::demodulator::Demodulator;
use super::demodulator::LevelDuration;
/// Sample rate for HackRF (2 MHz is good for keyfob signals)
const SAMPLE_RATE: u32 = 2_000_000;
/// HackRF controller for receiving and transmitting signals
pub struct HackRfController {
/// Event sender for notifying the app
event_tx: Sender<RadioEvent>,
/// Whether we're currently receiving
receiving: Arc<AtomicBool>,
/// Receiver thread handle
rx_thread: Option<JoinHandle<()>>,
/// Current frequency
frequency: Arc<Mutex<u32>>,
/// Demodulator for processing samples
demodulator: Arc<Mutex<Demodulator>>,
/// Whether HackRF is available
hackrf_available: bool,
}
impl HackRfController {
/// Create a new HackRF controller
pub fn new(event_tx: Sender<RadioEvent>) -> Result<Self> {
let demodulator = Demodulator::new(SAMPLE_RATE);
// Check if HackRF is available
let hackrf_available = check_hackrf_available();
if hackrf_available {
tracing::info!("HackRF device detected");
} else {
tracing::warn!("HackRF not detected - running in demo mode");
}
Ok(Self {
event_tx,
receiving: Arc::new(AtomicBool::new(false)),
rx_thread: None,
frequency: Arc::new(Mutex::new(433_920_000)),
demodulator: Arc::new(Mutex::new(demodulator)),
hackrf_available,
})
}
/// Check if HackRF is available
#[allow(dead_code)]
pub fn is_available(&self) -> bool {
self.hackrf_available
}
/// Start receiving at the specified frequency
pub fn start_receiving(&mut self, frequency: u32) -> Result<()> {
if self.receiving.load(Ordering::SeqCst) {
return Ok(());
}
*self.frequency.lock().unwrap() = frequency;
self.receiving.store(true, Ordering::SeqCst);
let receiving = self.receiving.clone();
let event_tx = self.event_tx.clone();
let freq = self.frequency.clone();
let demodulator = self.demodulator.clone();
let hackrf_available = self.hackrf_available;
self.rx_thread = Some(thread::spawn(move || {
if hackrf_available {
if let Err(e) =
run_receiver_hackrf(receiving.clone(), event_tx.clone(), freq, demodulator)
{
let _ = event_tx.send(RadioEvent::Error(format!("Receiver error: {}", e)));
}
} else {
run_demo_receiver(receiving, event_tx, freq);
}
}));
tracing::info!("Started receiving at {} Hz", frequency);
Ok(())
}
/// Stop receiving
pub fn stop_receiving(&mut self) -> Result<()> {
self.receiving.store(false, Ordering::SeqCst);
if let Some(handle) = self.rx_thread.take() {
let _ = handle.join();
}
tracing::info!("Stopped receiving");
Ok(())
}
/// Set the receive frequency
pub fn set_frequency(&mut self, frequency: u32) -> Result<()> {
*self.frequency.lock().unwrap() = frequency;
tracing::info!("Set frequency to {} Hz", frequency);
Ok(())
}
/// Transmit a signal
pub fn transmit(&mut self, signal: &[LevelDuration], frequency: u32) -> Result<()> {
if !self.hackrf_available {
tracing::warn!("HackRF not available - simulating transmission");
return Ok(());
}
// Stop receiving first if we are
let was_receiving = self.receiving.load(Ordering::SeqCst);
if was_receiving {
self.stop_receiving()?;
}
tracing::info!(
"Transmitting {} level/duration pairs at {} Hz",
signal.len(),
frequency
);
transmit_signal_hackrf(signal, frequency)?;
// Resume receiving if we were before
if was_receiving {
let freq = *self.frequency.lock().unwrap();
self.start_receiving(freq)?;
}
Ok(())
}
/// Set LNA gain (0-40 dB, 8 dB steps)
pub fn set_lna_gain(&mut self, gain: u32) -> Result<()> {
tracing::info!("Set LNA gain to {} dB", gain);
// Note: gain changes take effect on next start_receiving
// For now, just log - actual application happens in run_receiver_hackrf
Ok(())
}
/// Set VGA gain (0-62 dB, 2 dB steps)
pub fn set_vga_gain(&mut self, gain: u32) -> Result<()> {
tracing::info!("Set VGA gain to {} dB", gain);
// Note: gain changes take effect on next start_receiving
Ok(())
}
/// Enable/disable the RF amplifier
pub fn set_amp_enable(&mut self, enabled: bool) -> Result<()> {
tracing::info!("Set amp enable to {}", enabled);
// Note: amp changes take effect on next start_receiving
Ok(())
}
}
impl Drop for HackRfController {
fn drop(&mut self) {
self.receiving.store(false, Ordering::SeqCst);
if let Some(handle) = self.rx_thread.take() {
let _ = handle.join();
}
}
}
/// Check if HackRF is available
fn check_hackrf_available() -> bool {
// Try to open a HackRF device
match libhackrf::HackRf::open() {
Ok(_) => {
tracing::debug!("HackRF opened successfully");
true
}
Err(e) => {
tracing::debug!("HackRF not available: {:?}", e);
// Fallback: check via hackrf_info command
match std::process::Command::new("hackrf_info")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
{
Ok(status) => status.success(),
Err(_) => false,
}
}
}
}
/// Run a demo receiver (no actual HackRF)
fn run_demo_receiver(
receiving: Arc<AtomicBool>,
_event_tx: Sender<RadioEvent>,
_frequency: Arc<Mutex<u32>>,
) {
tracing::info!("Demo receiver thread started (no HackRF)");
while receiving.load(Ordering::SeqCst) {
std::thread::sleep(std::time::Duration::from_millis(100));
}
tracing::info!("Demo receiver thread stopped");
}
/// Shared state for RX callback (libhackrf requires fn pointers, not closures)
struct RxState {
receiving: Arc<AtomicBool>,
event_tx: Sender<RadioEvent>,
frequency: Arc<Mutex<u32>>,
demodulator: Arc<Mutex<Demodulator>>,
capture_id: std::sync::atomic::AtomicU32,
}
/// RX callback function for libhackrf
fn rx_callback(
_hackrf: &libhackrf::HackRf,
buffer: &[num_complex::Complex<i8>],
user_data: &dyn std::any::Any,
) {
use crate::capture::StoredLevelDuration;
// Downcast user_data to our state
let state = match user_data.downcast_ref::<RxState>() {
Some(s) => s,
None => return,
};
if !state.receiving.load(Ordering::SeqCst) {
return;
}
let current_freq = *state.frequency.lock().unwrap();
// Convert Complex<i8> samples to i8 pairs for demodulator
let samples: Vec<i8> = buffer.iter()
.flat_map(|c| [c.re, c.im])
.collect();
// Process through demodulator
if let Ok(mut demod) = state.demodulator.lock() {
if let Some(pairs) = demod.process_samples(&samples) {
// Convert to storable format
let stored_pairs: Vec<StoredLevelDuration> = pairs
.iter()
.map(|p| StoredLevelDuration { level: p.level, duration_us: p.duration_us })
.collect();
let id = state.capture_id.fetch_add(1, Ordering::SeqCst);
let capture = Capture::from_pairs(id, current_freq, stored_pairs);
let _ = state.event_tx.send(RadioEvent::SignalCaptured(capture));
}
}
}
/// Run the receiver loop with actual HackRF using libhackrf
fn run_receiver_hackrf(
receiving: Arc<AtomicBool>,
event_tx: Sender<RadioEvent>,
frequency: Arc<Mutex<u32>>,
demodulator: Arc<Mutex<Demodulator>>,
) -> Result<()> {
use anyhow::Context;
tracing::info!("HackRF receiver thread starting...");
// Open HackRF device
let hackrf = libhackrf::HackRf::open()
.context("Failed to open HackRF device")?;
let freq = *frequency.lock().unwrap();
tracing::info!("Configuring HackRF: freq={} Hz, sample_rate={} Hz", freq, SAMPLE_RATE);
// Configure HackRF
hackrf.set_sample_rate(SAMPLE_RATE)
.context("Failed to set sample rate")?;
hackrf.set_freq(freq as u64)
.context("Failed to set frequency")?;
hackrf.set_lna_gain(32)
.context("Failed to set LNA gain")?;
hackrf.set_rxvga_gain(20)
.context("Failed to set RXVGA gain")?;
hackrf.set_amp_enable(true)
.context("Failed to enable amp")?;
tracing::info!("HackRF configured, starting RX...");
// Create state for callback
let state = RxState {
receiving: receiving.clone(),
event_tx: event_tx.clone(),
frequency: frequency.clone(),
demodulator,
capture_id: std::sync::atomic::AtomicU32::new(0),
};
// Start receiving
hackrf.start_rx(rx_callback, state)
.context("Failed to start RX")?;
// Wait until receiving is stopped
while receiving.load(Ordering::SeqCst) {
std::thread::sleep(std::time::Duration::from_millis(100));
}
// Stop receiving
hackrf.stop_rx().context("Failed to stop RX")?;
tracing::info!("HackRF receiver thread stopped");
Ok(())
}
/// Shared state for TX callback
struct TxState {
samples: Vec<(i8, i8)>,
sample_index: std::sync::atomic::AtomicUsize,
}
/// TX callback function for libhackrf
fn tx_callback(
_hackrf: &libhackrf::HackRf,
buffer: &mut [num_complex::Complex<i8>],
user_data: &dyn std::any::Any,
) {
use num_complex::Complex;
// Downcast user_data to our state
let state = match user_data.downcast_ref::<TxState>() {
Some(s) => s,
None => return,
};
let total = state.samples.len();
for sample in buffer.iter_mut() {
let idx = state.sample_index.fetch_add(1, Ordering::SeqCst);
if idx < total {
let (i, q) = state.samples[idx];
*sample = Complex::new(i, q);
} else {
*sample = Complex::new(0, 0);
}
}
}
/// Transmit a signal via HackRF
fn transmit_signal_hackrf(signal: &[LevelDuration], frequency: u32) -> Result<()> {
use anyhow::Context;
tracing::info!("Starting HackRF transmission at maximum power...");
// Open HackRF device
let hackrf = libhackrf::HackRf::open()
.context("Failed to open HackRF device")?;
// Configure for TX with MAXIMUM POWER
hackrf.set_sample_rate(SAMPLE_RATE)
.context("Failed to set sample rate")?;
hackrf.set_freq(frequency as u64)
.context("Failed to set frequency")?;
// Set TX VGA gain to maximum (47 dB is the max for HackRF)
hackrf.set_txvga_gain(47)
.context("Failed to set TXVGA gain")?;
// Enable the RF amplifier for +14dB additional gain
hackrf.set_amp_enable(true)
.context("Failed to enable amp")?;
// Generate TX samples
let tx_samples = generate_tx_samples(signal, SAMPLE_RATE);
let total_samples = tx_samples.len();
tracing::debug!("Generated {} TX samples", total_samples);
// Create state for callback
let state = TxState {
samples: tx_samples,
sample_index: std::sync::atomic::AtomicUsize::new(0),
};
// Start transmitting
hackrf.start_tx(tx_callback, state)
.context("Failed to start TX")?;
// Wait for transmission to complete (check sample_index through a loop)
// We can't easily check completion with this API, so just wait based on expected time
let duration_us: u32 = signal.iter().map(|s| s.duration_us).sum();
let wait_ms = (duration_us / 1000).max(100);
std::thread::sleep(std::time::Duration::from_millis(wait_ms as u64 + 100));
// Stop transmitting
hackrf.stop_tx().context("Failed to stop TX")?;
tracing::info!("Transmission complete");
Ok(())
}
/// Generate TX samples from level/duration pairs
fn generate_tx_samples(signal: &[LevelDuration], sample_rate: u32) -> Vec<(i8, i8)> {
let mut samples = Vec::new();
let samples_per_us = sample_rate as f64 / 1_000_000.0;
for ld in signal {
let num_samples = (ld.duration_us as f64 * samples_per_us) as usize;
let value: i8 = if ld.level { 127 } else { 0 };
// IQ samples
for _ in 0..num_samples {
samples.push((value, 0)); // I, Q (Q=0 for OOK)
}
}
samples
}
+13
View File
@@ -0,0 +1,13 @@
//! Radio subsystem for HackRF control.
pub mod demodulator;
mod hackrf;
mod modulator;
pub use demodulator::LevelDuration;
pub use hackrf::HackRfController;
#[allow(unused_imports)]
pub use demodulator::Demodulator;
#[allow(unused_imports)]
pub use modulator::Modulator;
+147
View File
@@ -0,0 +1,147 @@
//! Signal modulator for generating TX waveforms.
use super::demodulator::LevelDuration;
/// Modulator for generating transmission waveforms
#[allow(dead_code)]
pub struct Modulator {
/// Time element in microseconds (base timing unit)
pub te: u32,
}
#[allow(dead_code)]
impl Modulator {
/// Create a new modulator with the given time element
pub fn new(te: u32) -> Self {
Self { te }
}
/// Generate a preamble (alternating pattern)
pub fn generate_preamble(&self, count: usize) -> Vec<LevelDuration> {
let mut result = Vec::with_capacity(count * 2);
for _ in 0..count {
result.push(LevelDuration::new(true, self.te));
result.push(LevelDuration::new(false, self.te));
}
result
}
/// Generate a sync pattern
pub fn generate_sync(&self, high_te: u32, low_te: u32) -> Vec<LevelDuration> {
vec![
LevelDuration::new(true, self.te * high_te),
LevelDuration::new(false, self.te * low_te),
]
}
/// Encode data using PWM (Pulse Width Modulation)
/// bit 0: short high, long low
/// bit 1: long high, short low
pub fn encode_pwm(&self, data: &[u8], bit_count: usize) -> Vec<LevelDuration> {
let mut result = Vec::with_capacity(bit_count * 2);
for i in 0..bit_count {
let byte_idx = i / 8;
let bit_idx = 7 - (i % 8);
let bit = (data[byte_idx] >> bit_idx) & 1;
if bit == 0 {
result.push(LevelDuration::new(true, self.te));
result.push(LevelDuration::new(false, self.te * 3));
} else {
result.push(LevelDuration::new(true, self.te * 3));
result.push(LevelDuration::new(false, self.te));
}
}
result
}
/// Encode data using Manchester encoding
/// bit 0: high then low
/// bit 1: low then high
pub fn encode_manchester(&self, data: &[u8], bit_count: usize) -> Vec<LevelDuration> {
let mut result = Vec::with_capacity(bit_count * 2);
for i in 0..bit_count {
let byte_idx = i / 8;
let bit_idx = 7 - (i % 8);
let bit = (data[byte_idx] >> bit_idx) & 1;
if bit == 0 {
result.push(LevelDuration::new(true, self.te));
result.push(LevelDuration::new(false, self.te));
} else {
result.push(LevelDuration::new(false, self.te));
result.push(LevelDuration::new(true, self.te));
}
}
result
}
/// Encode data using inverted Manchester encoding
/// bit 0: low then high
/// bit 1: high then low
pub fn encode_manchester_inverted(&self, data: &[u8], bit_count: usize) -> Vec<LevelDuration> {
let mut result = Vec::with_capacity(bit_count * 2);
for i in 0..bit_count {
let byte_idx = i / 8;
let bit_idx = 7 - (i % 8);
let bit = (data[byte_idx] >> bit_idx) & 1;
if bit == 0 {
result.push(LevelDuration::new(false, self.te));
result.push(LevelDuration::new(true, self.te));
} else {
result.push(LevelDuration::new(true, self.te));
result.push(LevelDuration::new(false, self.te));
}
}
result
}
/// Generate a trailer (final low period)
pub fn generate_trailer(&self, te_count: u32) -> Vec<LevelDuration> {
vec![LevelDuration::new(false, self.te * te_count)]
}
/// Combine multiple signal parts into one
pub fn combine(parts: Vec<Vec<LevelDuration>>) -> Vec<LevelDuration> {
parts.into_iter().flatten().collect()
}
/// Repeat a signal pattern multiple times
pub fn repeat(signal: &[LevelDuration], count: usize) -> Vec<LevelDuration> {
let mut result = Vec::with_capacity(signal.len() * count);
for _ in 0..count {
result.extend_from_slice(signal);
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pwm_encoding() {
let mod_ = Modulator::new(400);
let data = vec![0b10101010];
let encoded = mod_.encode_pwm(&data, 8);
assert_eq!(encoded.len(), 16);
}
#[test]
fn test_manchester_encoding() {
let mod_ = Modulator::new(400);
let data = vec![0b10101010];
let encoded = mod_.encode_manchester(&data, 8);
assert_eq!(encoded.len(), 16);
}
}
+331
View File
@@ -0,0 +1,331 @@
//! Storage management for configuration and exports.
//!
//! All application data lives under `~/.config/KAT/`:
//!
//! ```text
//! ~/.config/KAT/
//! config.ini — User configuration
//! exports/ — Exported .fob / .sub files
//! ```
//!
//! Captures are **in-memory only** and are discarded when KAT exits.
//! Only explicitly exported signals (.fob / .sub) persist between runs.
use anyhow::{Context, Result};
use configparser::ini::Ini;
use std::fs;
use std::path::PathBuf;
// ─── Config ──────────────────────────────────────────────────────────────────
/// Application configuration loaded from `~/.config/KAT/config.ini`
#[derive(Debug, Clone)]
pub struct Config {
// [general]
/// Directory for exporting signals (.fob / .sub files)
pub export_directory: PathBuf,
/// Maximum captures to keep in memory during a session
pub max_captures: usize,
// [radio]
/// Default frequency in Hz
pub default_frequency: u32,
/// Default LNA gain (0-40 dB, 8 dB steps)
pub default_lna_gain: u32,
/// Default VGA gain (0-62 dB, 2 dB steps)
pub default_vga_gain: u32,
/// Default amplifier state
pub default_amp: bool,
// [export]
/// Default export format (fob or sub)
pub default_export_format: String,
/// Include raw level/duration pairs in exports
pub include_raw_pairs: bool,
}
impl Config {
/// Build the default config, using the given config_dir as the base.
/// This keeps everything under `~/.config/KAT/` by default.
fn default_for(config_dir: &PathBuf) -> Self {
Self {
export_directory: config_dir.join("exports"),
max_captures: 100,
default_frequency: 433_920_000,
default_lna_gain: 24,
default_vga_gain: 20,
default_amp: false,
default_export_format: "fob".to_string(),
include_raw_pairs: true,
}
}
/// Load config from an INI file, falling back to defaults for missing keys.
fn load_from_ini(path: &std::path::Path, config_dir: &PathBuf) -> Result<Self> {
let mut ini = Ini::new();
ini.load(path)
.map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?;
let defaults = Config::default_for(config_dir);
let export_directory = ini
.get("general", "export_directory")
.map(|s| expand_tilde(&s))
.unwrap_or(defaults.export_directory);
let max_captures = ini
.getuint("general", "max_captures")
.ok()
.flatten()
.map(|v| v as usize)
.unwrap_or(defaults.max_captures);
let default_frequency = ini
.getuint("radio", "default_frequency")
.ok()
.flatten()
.map(|v| v as u32)
.unwrap_or(defaults.default_frequency);
let default_lna_gain = ini
.getuint("radio", "default_lna_gain")
.ok()
.flatten()
.map(|v| v as u32)
.unwrap_or(defaults.default_lna_gain);
let default_vga_gain = ini
.getuint("radio", "default_vga_gain")
.ok()
.flatten()
.map(|v| v as u32)
.unwrap_or(defaults.default_vga_gain);
let default_amp = ini
.getbool("radio", "default_amp")
.ok()
.flatten()
.unwrap_or(defaults.default_amp);
let default_export_format = ini
.get("export", "default_format")
.unwrap_or(defaults.default_export_format);
let include_raw_pairs = ini
.getbool("export", "include_raw_pairs")
.ok()
.flatten()
.unwrap_or(defaults.include_raw_pairs);
Ok(Self {
export_directory,
max_captures,
default_frequency,
default_lna_gain,
default_vga_gain,
default_amp,
default_export_format,
include_raw_pairs,
})
}
/// Save config to an INI-style file with comments explaining each field.
fn save_to_ini(&self, path: &std::path::Path) -> Result<()> {
let export_str = self.export_directory.to_string_lossy();
let freq_mhz = self.default_frequency as f64 / 1_000_000.0;
let content = format!(
r#"; KAT — Keyfob Analysis Toolkit configuration
; Location: {path}
;
; Edit this file to change default settings.
; Lines starting with ; or # are comments.
[general]
; Directory where .fob and .sub exports are saved.
; Supports ~ for home directory.
export_directory = {export_dir}
; Maximum number of captures to keep in memory per session.
; Captures are NOT persisted between runs — only exported
; signals (.fob / .sub) survive in the exports folder.
max_captures = {max_captures}
[radio]
; Default receive frequency in Hz ({freq_mhz:.2} MHz)
; Common keyfob frequencies: 315000000, 433920000, 868350000
default_frequency = {frequency}
; Default LNA gain in dB (0, 8, 16, 24, 32, 40)
default_lna_gain = {lna}
; Default VGA gain in dB (0-62, even numbers)
default_vga_gain = {vga}
; Enable RF amplifier by default (true/false)
default_amp = {amp}
[export]
; Default export format: fob (JSON metadata) or sub (Flipper Zero)
default_format = {export_fmt}
; Include raw signal level/duration pairs in .fob exports.
; Enables signal replay but increases file size.
include_raw_pairs = {raw_pairs}
"#,
path = path.display(),
export_dir = export_str,
max_captures = self.max_captures,
freq_mhz = freq_mhz,
frequency = self.default_frequency,
lna = self.default_lna_gain,
vga = self.default_vga_gain,
amp = self.default_amp,
export_fmt = self.default_export_format,
raw_pairs = self.include_raw_pairs,
);
fs::write(path, content)
.with_context(|| format!("Failed to write config to {:?}", path))?;
Ok(())
}
}
/// Fallback Default (without knowing config_dir). Only used if something goes
/// very wrong and we need a Config without a Storage instance.
impl Default for Config {
fn default() -> Self {
let fallback = resolve_config_dir()
.unwrap_or_else(|| PathBuf::from(".").join("KAT"));
Config::default_for(&fallback)
}
}
/// Expand `~` at the start of a path to the user's home directory.
fn expand_tilde(s: &str) -> PathBuf {
if s.starts_with("~/") {
if let Some(home) = dirs::home_dir() {
return home.join(&s[2..]);
}
}
PathBuf::from(s)
}
/// Resolve the KAT config directory to `~/.config/KAT/` regardless of OS.
pub fn resolve_config_dir() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".config").join("KAT"))
}
// ─── Storage ─────────────────────────────────────────────────────────────────
/// Storage manager for configuration and exports.
///
/// On construction it ensures the directory tree exists:
///
/// ```text
/// ~/.config/KAT/
/// config.ini
/// exports/
/// ```
///
/// Captures are in-memory only — they are discarded on exit.
pub struct Storage {
/// Base config directory (~/.config/KAT)
config_dir: PathBuf,
/// Configuration
pub config: Config,
}
impl Storage {
/// Create a new storage manager.
///
/// 1. Resolves the config directory (`~/.config/KAT`).
/// 2. Creates it if missing.
/// 3. Loads `config.ini` if it exists, otherwise writes a default one.
/// 4. Creates the export directory if missing.
pub fn new() -> Result<Self> {
// ── 1. Resolve base path ─────────────────────────────────────────
let config_dir = resolve_config_dir()
.context("Could not determine home directory (is $HOME set?)")?;
let config_path = config_dir.join("config.ini");
// ── 2. Ensure directory tree exists ──────────────────────────────
if !config_dir.exists() {
fs::create_dir_all(&config_dir)
.with_context(|| format!("Failed to create config dir: {:?}", config_dir))?;
tracing::info!("Created config directory: {:?}", config_dir);
}
// ── 3. Load or create config.ini ─────────────────────────────────
let config = if config_path.exists() {
tracing::info!("Loading config from {:?}", config_path);
match Config::load_from_ini(&config_path, &config_dir) {
Ok(cfg) => cfg,
Err(e) => {
tracing::warn!(
"Failed to parse config.ini, using defaults: {}",
e
);
Config::default_for(&config_dir)
}
}
} else {
tracing::info!(
"No config.ini found — creating default at {:?}",
config_path
);
let config = Config::default_for(&config_dir);
if let Err(e) = config.save_to_ini(&config_path) {
tracing::warn!("Could not write default config.ini: {}", e);
}
config
};
// ── 4. Ensure export directory exists ────────────────────────────
if !config.export_directory.exists() {
fs::create_dir_all(&config.export_directory).with_context(|| {
format!(
"Failed to create export dir: {:?}",
config.export_directory
)
})?;
tracing::info!(
"Created export directory: {:?}",
config.export_directory
);
}
// ── 5. Log resolved paths ───────────────────────────────────────
tracing::info!("Config dir: {:?}", config_dir);
tracing::info!("Export dir: {:?}", config.export_directory);
Ok(Self {
config_dir,
config,
})
}
/// Save the current configuration back to `config.ini`.
#[allow(dead_code)]
pub fn save_config(&self) -> Result<()> {
let config_path = self.config_dir.join("config.ini");
self.config.save_to_ini(&config_path)?;
tracing::info!("Saved config to {:?}", config_path);
Ok(())
}
// ─── Path accessors ──────────────────────────────────────────────────
/// Get the config directory path (`~/.config/KAT`)
#[allow(dead_code)]
pub fn config_dir(&self) -> &PathBuf {
&self.config_dir
}
/// Get the export directory path (from config, default `~/.config/KAT/exports`)
pub fn export_dir(&self) -> &PathBuf {
&self.config.export_directory
}
}
+258
View File
@@ -0,0 +1,258 @@
//! Captures list widget with detail panel.
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState, Wrap},
Frame,
};
use crate::app::App;
use crate::capture::CaptureStatus;
/// Render the captures area: table + detail panel
pub fn render_captures_list(frame: &mut Frame, area: Rect, app: &App) {
// Split vertically: table on top, detail panel on bottom
let has_selection = app
.selected_capture
.map(|i| i < app.captures.len())
.unwrap_or(false);
let chunks = if has_selection {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(6), // Table (flexible, takes remaining)
Constraint::Length(12), // Detail panel (fixed height)
])
.split(area)
} else {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(6)])
.split(area)
};
render_table(frame, chunks[0], app);
if has_selection && chunks.len() > 1 {
render_detail_panel(frame, chunks[1], app);
}
}
/// Render the compact signal table
fn render_table(frame: &mut Frame, area: Rect, app: &App) {
let header_cells = [
"ID", "Time", "Protocol", "Freq", "Serial", "Btn", "Cnt", "Mod", "CRC", "Status",
]
.iter()
.map(|h| Cell::from(*h).style(Style::default().add_modifier(Modifier::BOLD)));
let header = Row::new(header_cells).style(Style::default()).height(1);
let rows = app.captures.iter().map(|capture| {
let status_style = match capture.status {
CaptureStatus::Unknown => Style::default().fg(Color::DarkGray),
CaptureStatus::Decoded => Style::default().fg(Color::Yellow),
CaptureStatus::EncoderCapable => Style::default().fg(Color::Green),
};
let crc_style = if capture.protocol.is_none() {
Style::default().fg(Color::DarkGray)
} else if capture.crc_valid {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::Red)
};
let mod_style = match capture.modulation() {
crate::capture::ModulationType::Pwm => Style::default().fg(Color::Magenta),
crate::capture::ModulationType::Manchester => Style::default().fg(Color::Cyan),
crate::capture::ModulationType::DifferentialManchester => {
Style::default().fg(Color::Blue)
}
crate::capture::ModulationType::Unknown => Style::default().fg(Color::DarkGray),
};
let status_text = match capture.status {
CaptureStatus::EncoderCapable => "✓ Encode",
CaptureStatus::Decoded => "Decoded",
CaptureStatus::Unknown => "Unknown",
};
Row::new(vec![
Cell::from(format!("{:02}", capture.id)),
Cell::from(capture.timestamp_short()),
Cell::from(capture.protocol_name().to_string()),
Cell::from(capture.frequency_mhz()),
Cell::from(capture.serial_hex()),
Cell::from(capture.button_name().to_string()),
Cell::from(capture.counter_str()),
Cell::from(capture.modulation().to_string()).style(mod_style),
Cell::from(capture.crc_status()).style(crc_style),
Cell::from(status_text).style(status_style),
])
.height(1)
});
let widths = [
Constraint::Length(4), // ID
Constraint::Length(9), // Time
Constraint::Length(10), // Protocol
Constraint::Length(11), // Freq
Constraint::Length(9), // Serial
Constraint::Length(6), // Btn
Constraint::Length(6), // Cnt
Constraint::Length(7), // Mod
Constraint::Length(5), // CRC
Constraint::Length(10), // Status
];
let table = Table::new(rows, widths)
.header(header)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Captures "),
)
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED));
let mut state = TableState::default();
state.select(app.selected_capture);
// Apply scroll offset if needed
if app.scroll_offset > 0 && app.selected_capture.is_some() {
*state.offset_mut() = app.scroll_offset;
}
frame.render_stateful_widget(table, area, &mut state);
}
/// Render the detail panel for the selected signal
fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) {
let capture = match app.selected_capture {
Some(idx) if idx < app.captures.len() => &app.captures[idx],
_ => return,
};
let label_style = Style::default()
.fg(Color::DarkGray)
.add_modifier(Modifier::BOLD);
let value_style = Style::default().fg(Color::White);
let accent_style = Style::default().fg(Color::Cyan);
let good_style = Style::default().fg(Color::Green);
let bad_style = Style::default().fg(Color::Red);
// Build detail content in two columns
let make = App::get_make_for_protocol(capture.protocol_name());
// --- Left column lines ---
let mut left_lines = Vec::new();
// Row 1: Protocol + Make
left_lines.push(Line::from(vec![
Span::styled(" Protocol: ", label_style),
Span::styled(capture.protocol_name(), accent_style),
Span::styled(" Make: ", label_style),
Span::styled(make, value_style),
]));
// Row 2: Frequency + Modulation + Encryption
left_lines.push(Line::from(vec![
Span::styled(" Freq: ", label_style),
Span::styled(capture.frequency_mhz(), value_style),
Span::styled(" Mod: ", label_style),
Span::styled(capture.modulation().to_string(), value_style),
Span::styled(" Enc: ", label_style),
Span::styled(capture.encryption_type(), value_style),
]));
// Row 3: Full Serial + Button
left_lines.push(Line::from(vec![
Span::styled(" Serial: ", label_style),
Span::styled(format!("0x{}", capture.serial_hex()), accent_style),
Span::styled(" Btn: ", label_style),
Span::styled(
format!("{} ({})", capture.button_name(), capture.button_hex()),
value_style,
),
]));
// Row 4: Counter + CRC
let crc_span = if capture.protocol.is_none() {
Span::styled("-", Style::default().fg(Color::DarkGray))
} else if capture.crc_valid {
Span::styled("OK ✓", good_style)
} else {
Span::styled("FAIL ✗", bad_style)
};
left_lines.push(Line::from(vec![
Span::styled(" Counter: ", label_style),
Span::styled(capture.counter_str(), value_style),
Span::styled(" CRC: ", label_style),
crc_span,
Span::styled(" Status: ", label_style),
Span::styled(capture.status.to_string(), value_style),
]));
// Row 5: Full data/key hex
left_lines.push(Line::from(vec![
Span::styled(" Key/Data: ", label_style),
Span::styled(
format!("0x{}", capture.data_hex()),
Style::default().fg(Color::Yellow),
),
Span::styled(
format!(" ({})", capture.data_bits_str()),
Style::default().fg(Color::DarkGray),
),
]));
// Row 6: Timestamp + Raw data info
let raw_info = if capture.has_raw_data() {
format!("{} transitions", capture.raw_pair_count())
} else {
"None".to_string()
};
let raw_style = if capture.has_raw_data() {
good_style
} else {
Style::default().fg(Color::DarkGray)
};
left_lines.push(Line::from(vec![
Span::styled(" Captured: ", label_style),
Span::styled(capture.timestamp_full(), value_style),
]));
left_lines.push(Line::from(vec![
Span::styled(" Raw Data: ", label_style),
Span::styled(raw_info, raw_style),
]));
// Build the title
let title = format!(
" Signal #{:02}{} ",
capture.id,
capture.protocol_name()
);
let border_style = match capture.status {
CaptureStatus::EncoderCapable => Style::default().fg(Color::Green),
CaptureStatus::Decoded => Style::default().fg(Color::Yellow),
CaptureStatus::Unknown => Style::default().fg(Color::DarkGray),
};
let detail = Paragraph::new(left_lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.title(title),
)
.wrap(Wrap { trim: false });
frame.render_widget(detail, area);
}
+81
View File
@@ -0,0 +1,81 @@
//! Command input widget.
use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
use crate::app::{App, InputMode};
/// Render the command input line
pub fn render_command_line(frame: &mut Frame, area: Rect, app: &App) {
let (input_text, mode_text, mode_style) = match app.input_mode {
InputMode::Normal => (
String::new(),
"NORMAL",
Style::default().fg(Color::Green),
),
InputMode::Command => (
format!(":{}", app.command_input),
"COMMAND",
Style::default().fg(Color::Yellow),
),
InputMode::SignalMenu => (
String::new(),
"SIGNAL",
Style::default().fg(Color::Cyan),
),
InputMode::SettingsSelect => (
String::new(),
"SETTINGS",
Style::default().fg(Color::Cyan),
),
InputMode::SettingsEdit => (
String::new(),
"EDIT",
Style::default().fg(Color::Green),
),
InputMode::StartupImport => (
String::new(),
"IMPORT",
Style::default().fg(Color::Yellow),
),
InputMode::FobMetaYear
| InputMode::FobMetaMake
| InputMode::FobMetaModel
| InputMode::FobMetaRegion
| InputMode::FobMetaNotes => (
String::new(),
"EXPORT",
Style::default().fg(Color::Green),
),
};
let input_line = Line::from(vec![
Span::styled(
format!(" {} ", mode_text),
mode_style.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw(input_text),
Span::styled(
if app.input_mode == InputMode::Command {
""
} else {
""
},
Style::default(),
),
]);
let input = Paragraph::new(input_line).block(
Block::default()
.borders(Borders::ALL)
.title("input"),
);
frame.render_widget(input, area);
}
+388
View File
@@ -0,0 +1,388 @@
//! Main UI layout.
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame,
};
use crate::app::{App, InputMode, RadioState};
use super::captures_list::render_captures_list;
use super::command::render_command_line;
use super::settings_menu::{render_settings_dropdown, render_settings_tabs};
use super::signal_menu::render_signal_menu;
use super::status_bar::render_status_bar;
use crate::app::InputMode as IM;
const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Draw the entire UI
pub fn draw_ui(frame: &mut Frame, app: &App) {
let show_settings = matches!(app.input_mode, IM::SettingsSelect | IM::SettingsEdit);
let show_command = app.input_mode == IM::Command;
let mut constraints = vec![Constraint::Length(3)]; // Header
if show_settings {
constraints.push(Constraint::Length(3)); // Settings tabs
}
constraints.push(Constraint::Min(8)); // Captures list
constraints.push(Constraint::Length(3)); // Status bar
if show_command {
constraints.push(Constraint::Length(3)); // Command input
}
constraints.push(Constraint::Length(1)); // Help bar
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(frame.area());
let mut idx = 0;
render_header(frame, chunks[idx], app);
idx += 1;
if show_settings {
render_settings_tabs(frame, chunks[idx], app);
idx += 1;
}
render_captures_list(frame, chunks[idx], app);
idx += 1;
render_status_bar(frame, chunks[idx], app);
idx += 1;
if show_command {
render_command_line(frame, chunks[idx], app);
idx += 1;
}
render_help_bar(frame, chunks[idx], app);
// Overlay widgets (rendered on top of everything else)
if app.input_mode == InputMode::SignalMenu {
render_signal_menu(frame, app);
}
if app.input_mode == InputMode::SettingsEdit {
render_settings_dropdown(frame, app);
}
if app.input_mode == InputMode::StartupImport {
render_startup_import_prompt(frame, app);
}
if matches!(
app.input_mode,
InputMode::FobMetaYear
| InputMode::FobMetaMake
| InputMode::FobMetaModel
| InputMode::FobMetaRegion
| InputMode::FobMetaNotes
) {
render_fob_metadata_form(frame, app);
}
}
/// Render the header with title and radio status
fn render_header(frame: &mut Frame, area: Rect, app: &App) {
let (status_symbol, status_style) = match app.radio_state {
RadioState::Disconnected => ("", Style::default().fg(Color::DarkGray)),
RadioState::Idle => ("", Style::default().fg(Color::Yellow)),
RadioState::Receiving => ("", Style::default().fg(Color::Green)),
RadioState::Transmitting => ("", Style::default().fg(Color::Red)),
};
let title = format!("KAT v{}", VERSION);
// Build radio info string with all settings
let amp_str = if app.amp_enabled { "ON" } else { "OFF" };
let radio_info = format!(
"{} {} | {:.2} MHz | LNA:{} VGA:{} AMP:{}",
status_symbol,
app.radio_state,
app.frequency_mhz(),
app.lna_gain,
app.vga_gain,
amp_str
);
// Calculate padding for right-alignment
let padding = area
.width
.saturating_sub(title.len() as u16 + radio_info.len() as u16 + 4);
let header_line = Line::from(vec![
Span::styled(title, Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" ".repeat(padding as usize)),
Span::styled(radio_info, status_style),
]);
let header = Paragraph::new(header_line).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default()),
);
frame.render_widget(header, area);
}
/// Render the context-sensitive help bar
fn render_help_bar(frame: &mut Frame, area: Rect, app: &App) {
let help_text = match app.input_mode {
InputMode::Normal => {
"Enter: Actions | Tab: Settings | r: RX Toggle | :: Command | q: Quit"
}
InputMode::Command => "Enter: Execute | Esc: Cancel",
InputMode::SignalMenu => "Up/Down: Navigate | Enter: Select | Esc: Close",
InputMode::SettingsSelect => "Left/Right: Select | Tab: Cycle | Enter: Edit | Esc: Back",
InputMode::SettingsEdit => "Up/Down: Change Value | Enter: Apply | Esc: Cancel",
InputMode::StartupImport => "y: Import | n: Skip",
InputMode::FobMetaYear
| InputMode::FobMetaMake
| InputMode::FobMetaModel
| InputMode::FobMetaRegion => "Enter: Next Field | Esc: Cancel Export",
InputMode::FobMetaNotes => "Enter: Save & Export | Esc: Cancel Export",
};
let help = Paragraph::new(Line::from(Span::styled(
format!(" {}", help_text),
Style::default().fg(Color::DarkGray),
)));
frame.render_widget(help, area);
}
/// Center a rect of given width/height in the given area
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let x = area.x + area.width.saturating_sub(width) / 2;
let y = area.y + area.height.saturating_sub(height) / 2;
Rect::new(x, y, width.min(area.width), height.min(area.height))
}
/// Render the startup import prompt overlay
fn render_startup_import_prompt(frame: &mut Frame, app: &App) {
let count = app.pending_fob_files.len();
let area = frame.area();
let popup = centered_rect(50, 7, area);
frame.render_widget(Clear, popup);
let text = vec![
Line::from(""),
Line::from(Span::styled(
format!("Found {} .fob file(s) in export directory.", count),
Style::default().fg(Color::Yellow),
)),
Line::from(""),
Line::from(Span::styled(
"Import them? (y/n)",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)),
];
let block = Block::default()
.title(" Import Saved Signals ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan));
let paragraph = Paragraph::new(text)
.block(block)
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
frame.render_widget(paragraph, popup);
}
/// Render the .fob export metadata form overlay with signal summary
fn render_fob_metadata_form(frame: &mut Frame, app: &App) {
let area = frame.area();
let popup = centered_rect(62, 19, area);
frame.render_widget(Clear, popup);
let active_style = Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
let inactive_style = Style::default().fg(Color::DarkGray);
let done_style = Style::default().fg(Color::Green);
let value_style = Style::default().fg(Color::White);
let dim_style = Style::default().fg(Color::DarkGray);
let accent_style = Style::default().fg(Color::Yellow);
let cursor = Span::styled(
"_",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::RAPID_BLINK),
);
// Determine which field is active
let field_modes = [
InputMode::FobMetaYear,
InputMode::FobMetaMake,
InputMode::FobMetaModel,
InputMode::FobMetaRegion,
InputMode::FobMetaNotes,
];
let current_idx = field_modes
.iter()
.position(|m| *m == app.input_mode)
.unwrap_or(0);
let style_for = |idx: usize| -> Style {
if idx == current_idx {
active_style
} else if idx < current_idx {
done_style
} else {
inactive_style
}
};
let mut lines = Vec::new();
// --- Signal summary section ---
if let Some(capture) = app
.export_capture_id
.and_then(|id| app.captures.iter().find(|c| c.id == id))
{
lines.push(Line::from(vec![
Span::styled(" Signal: ", dim_style),
Span::styled(
format!(
"#{:02} {} | {} | {} | 0x{}",
capture.id,
capture.protocol_name(),
capture.frequency_mhz(),
capture.modulation(),
capture.serial_hex(),
),
accent_style,
),
]));
lines.push(Line::from(vec![
Span::styled(" Key: ", dim_style),
Span::styled(
format!("0x{} ({})", capture.data_hex(), capture.encryption_type()),
accent_style,
),
]));
}
lines.push(Line::from(Span::styled(
" ──────────────────────────────────────────────────────",
dim_style,
)));
// --- Form fields ---
struct FormField<'a> {
label: &'a str,
value: &'a str,
placeholder: &'a str,
idx: usize,
}
let fields = [
FormField {
label: " Year: ",
value: &app.fob_meta_year,
placeholder: "(e.g. 2024)",
idx: 0,
},
FormField {
label: " Make: ",
value: &app.fob_meta_make,
placeholder: "(auto-detected from protocol)",
idx: 1,
},
FormField {
label: " Model: ",
value: &app.fob_meta_model,
placeholder: "(e.g. Sportage, F-150)",
idx: 2,
},
FormField {
label: " Region: ",
value: &app.fob_meta_region,
placeholder: "(e.g. NA, EU, APAC, MEA)",
idx: 3,
},
FormField {
label: " Notes: ",
value: &app.fob_meta_notes,
placeholder: "(optional — color, trim, VIN, etc.)",
idx: 4,
},
];
for field in &fields {
let label_s = style_for(field.idx);
let display_val = if field.value.is_empty() {
field.placeholder
} else {
field.value
};
let val_s = if field.value.is_empty() && field.idx != current_idx {
dim_style
} else {
value_style
};
let mut spans = vec![
Span::styled(field.label, label_s),
Span::styled(display_val.to_string(), val_s),
];
// Show cursor on active field
if field.idx == current_idx {
spans.push(cursor.clone());
}
// Show checkmark for completed fields with values
if field.idx < current_idx && !field.value.is_empty() {
spans.push(Span::styled("", done_style));
}
lines.push(Line::from(spans));
}
lines.push(Line::from(""));
// Progress indicator
let total_fields = fields.len();
let progress = format!(
" Step {}/{}",
current_idx + 1,
total_fields,
);
let hint = if current_idx == total_fields - 1 {
" Enter: Save & Export | Esc: Cancel"
} else {
" Enter: Next | Esc: Cancel"
};
lines.push(Line::from(vec![
Span::styled(progress, accent_style),
Span::styled(" ", dim_style),
Span::styled(hint, dim_style),
]));
let block = Block::default()
.title(" Export .fob — Vehicle Details ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Green));
let paragraph = Paragraph::new(lines).block(block);
frame.render_widget(paragraph, popup);
}
+10
View File
@@ -0,0 +1,10 @@
//! Terminal UI components.
mod captures_list;
mod command;
mod layout;
pub mod signal_menu;
pub mod settings_menu;
mod status_bar;
pub use layout::draw_ui;
+126
View File
@@ -0,0 +1,126 @@
//! Radio settings inline editor triggered by Tab.
use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph},
Frame,
};
use crate::app::{App, InputMode, SettingsField, PRESET_FREQUENCIES, LNA_STEPS, VGA_STEPS};
/// Render the settings selector tabs in the header area
pub fn render_settings_tabs(frame: &mut Frame, area: Rect, app: &App) {
let mut spans = Vec::new();
spans.push(Span::styled(" Settings: ", Style::default().fg(Color::DarkGray)));
for (i, field) in SettingsField::ALL.iter().enumerate() {
let is_selected = app.input_mode == InputMode::SettingsSelect
&& i == app.settings_field_index;
let is_editing = app.input_mode == InputMode::SettingsEdit
&& i == app.settings_field_index;
let style = if is_editing {
Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD)
} else if is_selected {
Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
};
let value = match field {
SettingsField::Freq => format!("{:.2}MHz", app.frequency as f64 / 1_000_000.0),
SettingsField::Lna => format!("{}dB", app.lna_gain),
SettingsField::Vga => format!("{}dB", app.vga_gain),
SettingsField::Amp => if app.amp_enabled { "ON".to_string() } else { "OFF".to_string() },
};
spans.push(Span::styled(
format!(" [{}:{}] ", field.label(), value),
style,
));
}
let line = Line::from(spans);
let widget = Paragraph::new(line).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan))
.title(" Radio Settings (Tab) "),
);
frame.render_widget(widget, area);
}
/// Render the settings value dropdown when in SettingsEdit mode
pub fn render_settings_dropdown(frame: &mut Frame, app: &App) {
if app.input_mode != InputMode::SettingsEdit {
return;
}
let area = frame.area();
let field = SettingsField::ALL[app.settings_field_index];
let values: Vec<String> = match field {
SettingsField::Freq => PRESET_FREQUENCIES
.iter()
.map(|(_, label)| label.to_string())
.collect(),
SettingsField::Lna => LNA_STEPS.iter().map(|g| format!("{} dB", g)).collect(),
SettingsField::Vga => VGA_STEPS.iter().map(|g| format!("{} dB", g)).collect(),
SettingsField::Amp => vec!["ON".to_string(), "OFF".to_string()],
};
let menu_width = 22u16;
let menu_height = (values.len() as u16) + 2; // items + borders
// Position: below the header, near the field
let x_offset = 12 + (app.settings_field_index as u16) * 16;
let x = x_offset.min(area.width.saturating_sub(menu_width));
let y = 3u16; // Below header
let menu_area = Rect::new(
x,
y,
menu_width.min(area.width.saturating_sub(x)),
menu_height.min(area.height.saturating_sub(y)),
);
frame.render_widget(Clear, menu_area);
let items: Vec<ListItem> = values
.iter()
.enumerate()
.map(|(i, val)| {
let style = if i == app.settings_value_index {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
};
let prefix = if i == app.settings_value_index {
"> "
} else {
" "
};
ListItem::new(Line::from(Span::styled(
format!("{}{}", prefix, val),
style,
)))
})
.collect();
let list = List::new(items).block(
Block::default()
.title(format!(" {} ", field.label()))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Green)),
);
frame.render_widget(list, menu_area);
}
+94
View File
@@ -0,0 +1,94 @@
//! Signal action popup menu rendered as a centered overlay.
use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem},
Frame,
};
use crate::app::{App, SignalAction};
/// Render the signal action popup menu as a centered overlay
pub fn render_signal_menu(frame: &mut Frame, app: &App) {
let area = frame.area();
// Get selected capture info for the header
let (capture_info, freq_info) = if let Some(idx) = app.selected_capture {
if idx < app.captures.len() {
let c = &app.captures[idx];
(
format!("#{:02} {} | 0x{}", c.id, c.protocol_name(), c.serial_hex()),
format!("{} | {}", c.frequency_mhz(), c.modulation()),
)
} else {
("No capture".to_string(), String::new())
}
} else {
("No capture".to_string(), String::new())
};
// Menu dimensions - wider to show more info
let menu_width = 38u16;
let extra_lines = if freq_info.is_empty() { 0u16 } else { 2u16 };
let menu_height = (SignalAction::ALL.len() as u16) + 4 + extra_lines;
// Center the menu
let x = area.width.saturating_sub(menu_width) / 2;
let y = area.height.saturating_sub(menu_height) / 2;
let menu_area = Rect::new(x, y, menu_width.min(area.width), menu_height.min(area.height));
// Clear the area behind the popup
frame.render_widget(Clear, menu_area);
// Build list items
let mut items: Vec<ListItem> = Vec::new();
// Add signal info lines at the top if we have capture data
if !freq_info.is_empty() {
items.push(ListItem::new(Line::from(Span::styled(
format!(" {}", freq_info),
Style::default().fg(Color::DarkGray),
))));
items.push(ListItem::new(Line::from(Span::raw(""))));
}
// Add action items
for (i, action) in SignalAction::ALL.iter().enumerate() {
let style = if i == app.signal_menu_index {
Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
match action {
SignalAction::Delete => Style::default().fg(Color::Red),
SignalAction::ExportFob | SignalAction::ExportFlipper => {
Style::default().fg(Color::Green)
}
_ => Style::default().fg(Color::White),
}
};
let prefix = if i == app.signal_menu_index {
" > "
} else {
" "
};
items.push(ListItem::new(Line::from(Span::styled(
format!("{}{}", prefix, action.label()),
style,
))));
}
let list = List::new(items).block(
Block::default()
.title(format!(" {} ", capture_info))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)),
);
frame.render_widget(list, menu_area);
}
+38
View File
@@ -0,0 +1,38 @@
//! Status bar widget.
use ratatui::{
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
use crate::app::App;
/// Render the status bar with messages and errors
pub fn render_status_bar(frame: &mut Frame, area: Rect, app: &App) {
let (message, style) = if let Some(ref error) = app.last_error {
(
format!("Error: {}", error),
Style::default().fg(Color::Red),
)
} else if let Some(ref status) = app.status_message {
(status.clone(), Style::default().fg(Color::Green))
} else {
(
format!("Captures: {}", app.captures.len()),
Style::default().fg(Color::DarkGray),
)
};
let status_line = Line::from(vec![Span::styled(message, style)]);
let status = Paragraph::new(status_line).block(
Block::default()
.borders(Borders::ALL)
.title("status"),
);
frame.render_widget(status, area);
}