Version 1.0.0
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/REFERENCES
|
||||
Generated
+1469
File diff suppressed because it is too large
Load Diff
+56
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! Export formats for captured signals.
|
||||
|
||||
pub mod fob;
|
||||
pub mod flipper;
|
||||
+362
@@ -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()?;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user