Version 1.0.0

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