Version 1.0.0
This commit is contained in:
+999
@@ -0,0 +1,999 @@
|
||||
//! Application state management.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
use crate::capture::{ButtonCommand, Capture};
|
||||
use crate::protocols::ProtocolRegistry;
|
||||
use crate::radio::HackRfController;
|
||||
use crate::storage::Storage;
|
||||
|
||||
/// Input mode for the application
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InputMode {
|
||||
/// Normal navigation mode
|
||||
Normal,
|
||||
/// Command input mode (after pressing :)
|
||||
Command,
|
||||
/// Signal action popup menu
|
||||
SignalMenu,
|
||||
/// Tab bar - selecting which radio setting
|
||||
SettingsSelect,
|
||||
/// Editing a radio setting value
|
||||
SettingsEdit,
|
||||
/// Startup prompt: found .fob files, import? (y/n)
|
||||
StartupImport,
|
||||
/// Fob export metadata: editing year field
|
||||
FobMetaYear,
|
||||
/// Fob export metadata: editing make field
|
||||
FobMetaMake,
|
||||
/// Fob export metadata: editing model field
|
||||
FobMetaModel,
|
||||
/// Fob export metadata: editing region field
|
||||
FobMetaRegion,
|
||||
/// Fob export metadata: editing notes field
|
||||
FobMetaNotes,
|
||||
}
|
||||
|
||||
/// Items available in the signal action menu
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SignalAction {
|
||||
Lock,
|
||||
Unlock,
|
||||
Trunk,
|
||||
Panic,
|
||||
ExportFob,
|
||||
ExportFlipper,
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl SignalAction {
|
||||
pub const ALL: [SignalAction; 7] = [
|
||||
SignalAction::Lock,
|
||||
SignalAction::Unlock,
|
||||
SignalAction::Trunk,
|
||||
SignalAction::Panic,
|
||||
SignalAction::ExportFob,
|
||||
SignalAction::ExportFlipper,
|
||||
SignalAction::Delete,
|
||||
];
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
SignalAction::Lock => "TX Lock",
|
||||
SignalAction::Unlock => "TX Unlock",
|
||||
SignalAction::Trunk => "TX Trunk",
|
||||
SignalAction::Panic => "TX Panic",
|
||||
SignalAction::ExportFob => "Export .fob",
|
||||
SignalAction::ExportFlipper => "Export .sub (Flipper)",
|
||||
SignalAction::Delete => "Delete Signal",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Radio settings selectable via Tab
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SettingsField {
|
||||
Freq,
|
||||
Lna,
|
||||
Vga,
|
||||
Amp,
|
||||
}
|
||||
|
||||
impl SettingsField {
|
||||
pub const ALL: [SettingsField; 4] = [
|
||||
SettingsField::Freq,
|
||||
SettingsField::Lna,
|
||||
SettingsField::Vga,
|
||||
SettingsField::Amp,
|
||||
];
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
SettingsField::Freq => "Freq",
|
||||
SettingsField::Lna => "LNA",
|
||||
SettingsField::Vga => "VGA",
|
||||
SettingsField::Amp => "AMP",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Common keyfob frequencies (Hz)
|
||||
pub const PRESET_FREQUENCIES: [(u32, &str); 9] = [
|
||||
(300_000_000, "300.00 MHz"),
|
||||
(303_875_000, "303.875 MHz"),
|
||||
(310_000_000, "310.00 MHz"),
|
||||
(315_000_000, "315.00 MHz"),
|
||||
(318_000_000, "318.00 MHz"),
|
||||
(390_000_000, "390.00 MHz"),
|
||||
(433_920_000, "433.92 MHz"),
|
||||
(868_350_000, "868.35 MHz"),
|
||||
(915_000_000, "915.00 MHz"),
|
||||
];
|
||||
|
||||
/// LNA gain steps (dB)
|
||||
pub const LNA_STEPS: [u32; 6] = [0, 8, 16, 24, 32, 40];
|
||||
|
||||
/// VGA gain steps (dB, subset for menu)
|
||||
pub const VGA_STEPS: [u32; 8] = [0, 8, 16, 20, 24, 32, 40, 62];
|
||||
|
||||
/// Radio state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RadioState {
|
||||
/// Not connected
|
||||
Disconnected,
|
||||
/// Connected but idle
|
||||
Idle,
|
||||
/// Receiving signals
|
||||
Receiving,
|
||||
/// Transmitting
|
||||
#[allow(dead_code)]
|
||||
Transmitting,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RadioState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RadioState::Disconnected => write!(f, "DISCONNECTED"),
|
||||
RadioState::Idle => write!(f, "IDLE"),
|
||||
RadioState::Receiving => write!(f, "RX"),
|
||||
RadioState::Transmitting => write!(f, "TX"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Events from the radio subsystem
|
||||
pub enum RadioEvent {
|
||||
/// New signal captured
|
||||
SignalCaptured(Capture),
|
||||
/// Error occurred
|
||||
Error(String),
|
||||
/// State changed
|
||||
#[allow(dead_code)]
|
||||
StateChanged(RadioState),
|
||||
}
|
||||
|
||||
/// Main application state
|
||||
pub struct App {
|
||||
/// Current input mode
|
||||
pub input_mode: InputMode,
|
||||
/// Command input buffer
|
||||
pub command_input: String,
|
||||
/// List of captures
|
||||
pub captures: Vec<Capture>,
|
||||
/// Currently selected capture index
|
||||
pub selected_capture: Option<usize>,
|
||||
/// Scroll offset for captures list
|
||||
pub scroll_offset: usize,
|
||||
/// Current frequency in Hz
|
||||
pub frequency: u32,
|
||||
/// LNA gain (0-40 dB, 8 dB steps)
|
||||
pub lna_gain: u32,
|
||||
/// VGA gain (0-62 dB, 2 dB steps)
|
||||
pub vga_gain: u32,
|
||||
/// Amplifier enabled
|
||||
pub amp_enabled: bool,
|
||||
/// Radio state
|
||||
pub radio_state: RadioState,
|
||||
/// Last error message
|
||||
pub last_error: Option<String>,
|
||||
/// Last status message
|
||||
pub status_message: Option<String>,
|
||||
|
||||
// -- Signal action menu state --
|
||||
/// Currently selected signal menu item index
|
||||
pub signal_menu_index: usize,
|
||||
|
||||
// -- Settings menu state --
|
||||
/// Currently selected settings field
|
||||
pub settings_field_index: usize,
|
||||
/// Currently selected value index within the settings field editor
|
||||
pub settings_value_index: usize,
|
||||
|
||||
/// Next capture ID
|
||||
next_capture_id: u32,
|
||||
/// Storage manager
|
||||
pub storage: Storage,
|
||||
/// Protocol registry
|
||||
protocols: ProtocolRegistry,
|
||||
/// HackRF controller (optional - may not be connected)
|
||||
hackrf: Option<HackRfController>,
|
||||
/// Channel for radio events
|
||||
radio_event_rx: Receiver<RadioEvent>,
|
||||
/// Sender for radio events (cloned to radio thread)
|
||||
#[allow(dead_code)]
|
||||
radio_event_tx: Sender<RadioEvent>,
|
||||
|
||||
// -- Startup import state --
|
||||
/// .fob files found on startup in export_dir
|
||||
pub pending_fob_files: Vec<std::path::PathBuf>,
|
||||
|
||||
// -- .fob export metadata state --
|
||||
/// Capture ID being exported (set before entering FobMeta modes)
|
||||
pub export_capture_id: Option<u32>,
|
||||
/// Year input buffer
|
||||
pub fob_meta_year: String,
|
||||
/// Make input buffer
|
||||
pub fob_meta_make: String,
|
||||
/// Model input buffer
|
||||
pub fob_meta_model: String,
|
||||
/// Region input buffer (e.g. NA, EU, APAC, etc.)
|
||||
pub fob_meta_region: String,
|
||||
/// Notes input buffer
|
||||
pub fob_meta_notes: String,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Create a new application instance
|
||||
pub fn new() -> Result<Self> {
|
||||
let storage = Storage::new()?;
|
||||
let protocols = ProtocolRegistry::new();
|
||||
let (radio_event_tx, radio_event_rx) = mpsc::channel();
|
||||
|
||||
// Try to initialize HackRF
|
||||
let hackrf = match HackRfController::new(radio_event_tx.clone()) {
|
||||
Ok(h) => {
|
||||
tracing::info!("HackRF initialized successfully");
|
||||
Some(h)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to initialize HackRF: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let radio_state = if hackrf.is_some() {
|
||||
RadioState::Idle
|
||||
} else {
|
||||
RadioState::Disconnected
|
||||
};
|
||||
|
||||
// Captures start empty — they are in-memory only and discarded on exit.
|
||||
// The user is offered the chance to import .fob files from their exports folder.
|
||||
let captures: Vec<Capture> = Vec::new();
|
||||
let next_capture_id = 1u32;
|
||||
|
||||
// Use config defaults for radio settings
|
||||
let frequency = storage.config.default_frequency;
|
||||
let lna_gain = storage.config.default_lna_gain;
|
||||
let vga_gain = storage.config.default_vga_gain;
|
||||
let amp_enabled = storage.config.default_amp;
|
||||
|
||||
// Scan for .fob files in the export directory
|
||||
let pending_fob_files = crate::export::fob::scan_fob_files(&storage.config.export_directory);
|
||||
let initial_mode = if !pending_fob_files.is_empty() {
|
||||
tracing::info!("Found {} .fob files in export dir", pending_fob_files.len());
|
||||
InputMode::StartupImport
|
||||
} else {
|
||||
InputMode::Normal
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
input_mode: initial_mode,
|
||||
command_input: String::new(),
|
||||
captures,
|
||||
selected_capture: None,
|
||||
scroll_offset: 0,
|
||||
frequency,
|
||||
lna_gain,
|
||||
vga_gain,
|
||||
amp_enabled,
|
||||
radio_state,
|
||||
last_error: None,
|
||||
status_message: None,
|
||||
signal_menu_index: 0,
|
||||
settings_field_index: 0,
|
||||
settings_value_index: 0,
|
||||
next_capture_id,
|
||||
storage,
|
||||
protocols,
|
||||
hackrf,
|
||||
radio_event_rx,
|
||||
radio_event_tx,
|
||||
pending_fob_files,
|
||||
export_capture_id: None,
|
||||
fob_meta_year: String::new(),
|
||||
fob_meta_make: String::new(),
|
||||
fob_meta_model: String::new(),
|
||||
fob_meta_region: String::new(),
|
||||
fob_meta_notes: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the frequency in MHz
|
||||
pub fn frequency_mhz(&self) -> f64 {
|
||||
self.frequency as f64 / 1_000_000.0
|
||||
}
|
||||
|
||||
/// Select the next capture in the list
|
||||
pub fn next_capture(&mut self) {
|
||||
if self.captures.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.selected_capture = Some(match self.selected_capture {
|
||||
Some(i) => (i + 1).min(self.captures.len() - 1),
|
||||
None => 0,
|
||||
});
|
||||
// Update scroll to keep selection visible
|
||||
self.ensure_selection_visible();
|
||||
}
|
||||
|
||||
/// Select the previous capture in the list
|
||||
pub fn previous_capture(&mut self) {
|
||||
if self.captures.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.selected_capture = Some(match self.selected_capture {
|
||||
Some(i) => i.saturating_sub(1),
|
||||
None => 0,
|
||||
});
|
||||
// Update scroll to keep selection visible
|
||||
self.ensure_selection_visible();
|
||||
}
|
||||
|
||||
/// Ensure the selected capture is visible in the scroll view
|
||||
fn ensure_selection_visible(&mut self) {
|
||||
if let Some(selected) = self.selected_capture {
|
||||
// Assume visible area is about 15 items (will be adjusted by UI)
|
||||
let visible_rows = 15;
|
||||
|
||||
if selected < self.scroll_offset {
|
||||
self.scroll_offset = selected;
|
||||
} else if selected >= self.scroll_offset + visible_rows {
|
||||
self.scroll_offset = selected.saturating_sub(visible_rows - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle receiving state
|
||||
pub fn toggle_receiving(&mut self) -> Result<()> {
|
||||
// Clear any previous error when user takes action
|
||||
self.last_error = None;
|
||||
|
||||
match self.radio_state {
|
||||
RadioState::Disconnected => {
|
||||
self.last_error = Some("HackRF not connected".to_string());
|
||||
}
|
||||
RadioState::Idle => {
|
||||
if let Some(ref mut hackrf) = self.hackrf {
|
||||
hackrf.start_receiving(self.frequency)?;
|
||||
self.radio_state = RadioState::Receiving;
|
||||
self.status_message = Some(format!("Receiving on {:.2} MHz", self.frequency_mhz()));
|
||||
}
|
||||
}
|
||||
RadioState::Receiving => {
|
||||
if let Some(ref mut hackrf) = self.hackrf {
|
||||
hackrf.stop_receiving()?;
|
||||
self.radio_state = RadioState::Idle;
|
||||
self.status_message = Some("Stopped receiving".to_string());
|
||||
}
|
||||
}
|
||||
RadioState::Transmitting => {
|
||||
self.last_error = Some("Cannot change state while transmitting".to_string());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a command
|
||||
pub fn execute_command(&mut self, command: &str) -> Result<()> {
|
||||
let parts: Vec<&str> = command.trim().split_whitespace().collect();
|
||||
if parts.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.last_error = None;
|
||||
self.status_message = None;
|
||||
|
||||
match parts[0] {
|
||||
"q" | "quit" => {
|
||||
// Will be handled by main loop
|
||||
std::process::exit(0);
|
||||
}
|
||||
"freq" => {
|
||||
if parts.len() < 2 {
|
||||
self.last_error = Some("Usage: :freq <MHz>".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
match parts[1].parse::<f64>() {
|
||||
Ok(mhz) => {
|
||||
let hz = (mhz * 1_000_000.0) as u32;
|
||||
self.set_frequency(hz)?;
|
||||
}
|
||||
Err(_) => {
|
||||
self.last_error = Some("Invalid frequency".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
"unlock" => self.transmit_command(parts.get(1), ButtonCommand::Unlock)?,
|
||||
"lock" => self.transmit_command(parts.get(1), ButtonCommand::Lock)?,
|
||||
"trunk" => self.transmit_command(parts.get(1), ButtonCommand::Trunk)?,
|
||||
"panic" => self.transmit_command(parts.get(1), ButtonCommand::Panic)?,
|
||||
"delete" => {
|
||||
if parts.len() < 2 {
|
||||
self.last_error = Some("Usage: :delete <ID> or :delete all".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
if parts[1].eq_ignore_ascii_case("all") {
|
||||
self.delete_all_captures()?;
|
||||
} else {
|
||||
self.delete_capture(parts[1])?;
|
||||
}
|
||||
}
|
||||
"lna" => {
|
||||
if parts.len() < 2 {
|
||||
self.last_error = Some("Usage: :lna <0-40>".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
match parts[1].parse::<u32>() {
|
||||
Ok(gain) => self.set_lna_gain(gain)?,
|
||||
Err(_) => {
|
||||
self.last_error = Some("Invalid LNA gain value".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
"vga" => {
|
||||
if parts.len() < 2 {
|
||||
self.last_error = Some("Usage: :vga <0-62>".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
match parts[1].parse::<u32>() {
|
||||
Ok(gain) => self.set_vga_gain(gain)?,
|
||||
Err(_) => {
|
||||
self.last_error = Some("Invalid VGA gain value".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
"amp" => {
|
||||
if parts.len() < 2 {
|
||||
// Toggle if no argument
|
||||
self.toggle_amp()?;
|
||||
} else {
|
||||
match parts[1].to_lowercase().as_str() {
|
||||
"on" | "1" | "true" => self.set_amp(true)?,
|
||||
"off" | "0" | "false" => self.set_amp(false)?,
|
||||
_ => {
|
||||
self.last_error = Some("Usage: :amp [on|off]".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.last_error = Some(format!("Unknown command: {}", parts[0]));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the receive frequency
|
||||
fn set_frequency(&mut self, hz: u32) -> Result<()> {
|
||||
// Validate frequency range (common keyfob frequencies)
|
||||
if hz < 300_000_000 || hz > 928_000_000 {
|
||||
self.last_error = Some("Frequency must be between 300-928 MHz".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.frequency = hz;
|
||||
|
||||
if let Some(ref mut hackrf) = self.hackrf {
|
||||
if self.radio_state == RadioState::Receiving {
|
||||
hackrf.set_frequency(hz)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.status_message = Some(format!("Frequency set to {:.2} MHz", hz as f64 / 1_000_000.0));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the LNA gain
|
||||
fn set_lna_gain(&mut self, gain: u32) -> Result<()> {
|
||||
// LNA gain is 0-40 dB in 8 dB steps
|
||||
if gain > 40 {
|
||||
self.last_error = Some("LNA gain must be 0-40 dB".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Round to nearest 8 dB step
|
||||
let gain = (gain / 8) * 8;
|
||||
self.lna_gain = gain;
|
||||
|
||||
if let Some(ref mut hackrf) = self.hackrf {
|
||||
hackrf.set_lna_gain(gain)?;
|
||||
}
|
||||
|
||||
self.status_message = Some(format!("LNA gain set to {} dB", gain));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the VGA gain
|
||||
fn set_vga_gain(&mut self, gain: u32) -> Result<()> {
|
||||
// VGA gain is 0-62 dB in 2 dB steps
|
||||
if gain > 62 {
|
||||
self.last_error = Some("VGA gain must be 0-62 dB".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Round to nearest 2 dB step
|
||||
let gain = (gain / 2) * 2;
|
||||
self.vga_gain = gain;
|
||||
|
||||
if let Some(ref mut hackrf) = self.hackrf {
|
||||
hackrf.set_vga_gain(gain)?;
|
||||
}
|
||||
|
||||
self.status_message = Some(format!("VGA gain set to {} dB", gain));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Toggle amplifier
|
||||
fn toggle_amp(&mut self) -> Result<()> {
|
||||
self.set_amp(!self.amp_enabled)
|
||||
}
|
||||
|
||||
/// Set amplifier state
|
||||
fn set_amp(&mut self, enabled: bool) -> Result<()> {
|
||||
self.amp_enabled = enabled;
|
||||
|
||||
if let Some(ref mut hackrf) = self.hackrf {
|
||||
hackrf.set_amp_enable(enabled)?;
|
||||
}
|
||||
|
||||
self.status_message = Some(format!("Amp {}", if enabled { "enabled" } else { "disabled" }));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transmit a command for a capture
|
||||
fn transmit_command(&mut self, id_str: Option<&&str>, command: ButtonCommand) -> Result<()> {
|
||||
use crate::protocols::DecodedSignal;
|
||||
|
||||
let id_str = match id_str {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
self.last_error = Some(format!("Usage: :{:?} <ID>", command).to_lowercase());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let id: u32 = match id_str.parse() {
|
||||
Ok(i) => i,
|
||||
Err(_) => {
|
||||
self.last_error = Some("Invalid capture ID".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let capture = match self.captures.iter().find(|c| c.id == id) {
|
||||
Some(c) => c.clone(),
|
||||
None => {
|
||||
self.last_error = Some(format!("Capture {} not found", id));
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if capture.protocol.is_none() {
|
||||
self.last_error = Some("Cannot transmit: unknown protocol".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let protocol_name = capture.protocol.as_ref().unwrap();
|
||||
let protocol = match self.protocols.get(protocol_name) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
self.last_error = Some(format!("Protocol {} not supported for encoding", protocol_name));
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if !protocol.supports_encoding() {
|
||||
self.last_error = Some(format!("Protocol {} does not support encoding", protocol_name));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create a DecodedSignal from the capture
|
||||
let decoded = DecodedSignal {
|
||||
serial: capture.serial,
|
||||
button: capture.button,
|
||||
counter: capture.counter,
|
||||
crc_valid: capture.crc_valid,
|
||||
data: capture.data,
|
||||
data_count_bit: capture.data_count_bit,
|
||||
encoder_capable: true,
|
||||
};
|
||||
|
||||
// Generate the signal with the new button
|
||||
let button_code = command.code();
|
||||
let signal = match protocol.encode(&decoded, button_code) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
self.last_error = Some("Failed to encode signal".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Transmit
|
||||
if let Some(ref mut hackrf) = self.hackrf {
|
||||
hackrf.transmit(&signal, capture.frequency)?;
|
||||
self.status_message = Some(format!("Transmitted {:?} for capture {}", command, id));
|
||||
} else {
|
||||
self.last_error = Some("HackRF not connected".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a capture (in-memory only — captures are not persisted)
|
||||
fn delete_capture(&mut self, id_str: &str) -> Result<()> {
|
||||
let id: u32 = match id_str.parse() {
|
||||
Ok(i) => i,
|
||||
Err(_) => {
|
||||
self.last_error = Some("Invalid capture ID".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let idx = match self.captures.iter().position(|c| c.id == id) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
self.last_error = Some(format!("Capture {} not found", id));
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
self.captures.remove(idx);
|
||||
|
||||
// Adjust selection
|
||||
if let Some(sel) = self.selected_capture {
|
||||
if sel >= self.captures.len() && !self.captures.is_empty() {
|
||||
self.selected_capture = Some(self.captures.len() - 1);
|
||||
} else if self.captures.is_empty() {
|
||||
self.selected_capture = None;
|
||||
}
|
||||
}
|
||||
|
||||
self.status_message = Some(format!("Deleted capture {}", id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all captures (in-memory only)
|
||||
fn delete_all_captures(&mut self) -> Result<()> {
|
||||
let count = self.captures.len();
|
||||
|
||||
if count == 0 {
|
||||
self.status_message = Some("No captures to delete".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Clear the list
|
||||
self.captures.clear();
|
||||
self.selected_capture = None;
|
||||
self.scroll_offset = 0;
|
||||
|
||||
self.status_message = Some(format!("Deleted all {} captures", count));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process pending radio events
|
||||
pub fn process_radio_events(&mut self) -> Result<()> {
|
||||
while let Ok(event) = self.radio_event_rx.try_recv() {
|
||||
match event {
|
||||
RadioEvent::SignalCaptured(mut capture) => {
|
||||
// Assign ID
|
||||
capture.id = self.next_capture_id;
|
||||
self.next_capture_id += 1;
|
||||
|
||||
// Convert stored pairs to the format protocols expect
|
||||
let pairs: Vec<crate::radio::LevelDuration> = capture.raw_pairs
|
||||
.iter()
|
||||
.map(|p| crate::radio::LevelDuration::new(p.level, p.duration_us))
|
||||
.collect();
|
||||
|
||||
// Try to decode with registered protocols
|
||||
if let Some((protocol_name, decoded)) = self.protocols.process_signal(&pairs, capture.frequency) {
|
||||
capture.protocol = Some(protocol_name);
|
||||
capture.serial = decoded.serial;
|
||||
capture.button = decoded.button;
|
||||
capture.counter = decoded.counter;
|
||||
capture.crc_valid = decoded.crc_valid;
|
||||
capture.data = decoded.data;
|
||||
capture.data_count_bit = decoded.data_count_bit;
|
||||
capture.status = if decoded.encoder_capable {
|
||||
crate::capture::CaptureStatus::EncoderCapable
|
||||
} else {
|
||||
crate::capture::CaptureStatus::Decoded
|
||||
};
|
||||
}
|
||||
|
||||
// Captures are in-memory only — no auto-save to disk.
|
||||
// Use Export (.fob / .sub) to persist a signal.
|
||||
self.captures.push(capture);
|
||||
|
||||
// Auto-select and scroll to new capture
|
||||
let new_idx = self.captures.len() - 1;
|
||||
self.selected_capture = Some(new_idx);
|
||||
self.ensure_selection_visible();
|
||||
|
||||
self.status_message = Some("New signal captured".to_string());
|
||||
}
|
||||
RadioEvent::Error(e) => {
|
||||
self.last_error = Some(e);
|
||||
}
|
||||
RadioEvent::StateChanged(state) => {
|
||||
self.radio_state = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// -- Signal Action Menu helpers --
|
||||
|
||||
/// Execute the currently selected signal action
|
||||
pub fn execute_signal_action(&mut self) -> Result<()> {
|
||||
let action = SignalAction::ALL[self.signal_menu_index];
|
||||
let capture_id = match self.selected_capture {
|
||||
Some(idx) if idx < self.captures.len() => self.captures[idx].id,
|
||||
_ => {
|
||||
self.last_error = Some("No capture selected".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
match action {
|
||||
SignalAction::Lock => {
|
||||
let id_str = capture_id.to_string();
|
||||
self.transmit_command(Some(&&*id_str.as_str()), ButtonCommand::Lock)?;
|
||||
}
|
||||
SignalAction::Unlock => {
|
||||
let id_str = capture_id.to_string();
|
||||
self.transmit_command(Some(&&*id_str.as_str()), ButtonCommand::Unlock)?;
|
||||
}
|
||||
SignalAction::Trunk => {
|
||||
let id_str = capture_id.to_string();
|
||||
self.transmit_command(Some(&&*id_str.as_str()), ButtonCommand::Trunk)?;
|
||||
}
|
||||
SignalAction::Panic => {
|
||||
let id_str = capture_id.to_string();
|
||||
self.transmit_command(Some(&&*id_str.as_str()), ButtonCommand::Panic)?;
|
||||
}
|
||||
SignalAction::ExportFob => {
|
||||
self.export_fob(capture_id)?;
|
||||
}
|
||||
SignalAction::ExportFlipper => {
|
||||
self.export_flipper(capture_id)?;
|
||||
}
|
||||
SignalAction::Delete => {
|
||||
let id_str = capture_id.to_string();
|
||||
self.delete_capture(&id_str)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start .fob export by entering metadata input mode
|
||||
pub fn export_fob(&mut self, id: u32) -> Result<()> {
|
||||
if !self.captures.iter().any(|c| c.id == id) {
|
||||
self.last_error = Some(format!("Capture {} not found", id));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Pre-fill make from protocol
|
||||
let make = self.captures.iter().find(|c| c.id == id).map(|c| {
|
||||
Self::get_make_for_protocol(c.protocol_name()).to_string()
|
||||
}).unwrap_or_default();
|
||||
|
||||
self.export_capture_id = Some(id);
|
||||
self.fob_meta_year = String::new();
|
||||
self.fob_meta_make = make;
|
||||
self.fob_meta_model = String::new();
|
||||
self.fob_meta_region = String::new();
|
||||
self.fob_meta_notes = String::new();
|
||||
self.input_mode = InputMode::FobMetaYear;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Complete the .fob export with collected metadata
|
||||
pub fn complete_fob_export(&mut self) -> Result<()> {
|
||||
let id = match self.export_capture_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
self.last_error = Some("No capture selected for export".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let capture = match self.captures.iter().find(|c| c.id == id) {
|
||||
Some(c) => c.clone(),
|
||||
None => {
|
||||
self.last_error = Some(format!("Capture {} not found", id));
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let export_dir = self.storage.export_dir().clone();
|
||||
if !export_dir.exists() {
|
||||
std::fs::create_dir_all(&export_dir)?;
|
||||
}
|
||||
|
||||
let metadata = crate::export::fob::FobMetadata {
|
||||
year: self.fob_meta_year.parse::<u32>().ok(),
|
||||
make: self.fob_meta_make.clone(),
|
||||
model: self.fob_meta_model.clone(),
|
||||
region: self.fob_meta_region.clone(),
|
||||
notes: self.fob_meta_notes.clone(),
|
||||
};
|
||||
|
||||
let filename = format!(
|
||||
"{}_{}.fob",
|
||||
capture.protocol_name().replace(' ', "_").to_lowercase(),
|
||||
capture.serial_hex()
|
||||
);
|
||||
let path = export_dir.join(&filename);
|
||||
|
||||
crate::export::fob::export_fob(
|
||||
&capture,
|
||||
&path,
|
||||
self.storage.config.include_raw_pairs,
|
||||
Some(&metadata),
|
||||
)?;
|
||||
|
||||
self.export_capture_id = None;
|
||||
self.status_message = Some(format!("Exported to {}", filename));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Import pending .fob files into captures list
|
||||
pub fn import_fob_files(&mut self) -> Result<()> {
|
||||
let files = std::mem::take(&mut self.pending_fob_files);
|
||||
let mut imported = 0;
|
||||
|
||||
for path in &files {
|
||||
match crate::export::fob::import_fob(path, self.next_capture_id) {
|
||||
Ok(capture) => {
|
||||
self.next_capture_id += 1;
|
||||
self.captures.push(capture);
|
||||
imported += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to import {:?}: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if imported > 0 {
|
||||
self.selected_capture = Some(0);
|
||||
self.status_message = Some(format!("Imported {} .fob file(s)", imported));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Skip .fob import and start blank
|
||||
pub fn skip_fob_import(&mut self) {
|
||||
self.pending_fob_files.clear();
|
||||
self.status_message = Some("Starting with no imported signals".to_string());
|
||||
}
|
||||
|
||||
/// Export capture as Flipper .sub file
|
||||
pub fn export_flipper(&mut self, id: u32) -> Result<()> {
|
||||
let capture = match self.captures.iter().find(|c| c.id == id) {
|
||||
Some(c) => c.clone(),
|
||||
None => {
|
||||
self.last_error = Some(format!("Capture {} not found", id));
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let export_dir = self.storage.export_dir().clone();
|
||||
if !export_dir.exists() {
|
||||
std::fs::create_dir_all(&export_dir)?;
|
||||
}
|
||||
|
||||
let filename = format!(
|
||||
"{}_{}.sub",
|
||||
capture.protocol_name().replace(' ', "_").to_lowercase(),
|
||||
capture.serial_hex()
|
||||
);
|
||||
let path = export_dir.join(&filename);
|
||||
|
||||
crate::export::flipper::export_flipper_sub(&capture, &path)?;
|
||||
self.status_message = Some(format!("Exported to {}", filename));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// -- Settings Menu helpers --
|
||||
|
||||
/// Get the current value index for the active settings field
|
||||
pub fn current_settings_value_index(&self) -> usize {
|
||||
let field = SettingsField::ALL[self.settings_field_index];
|
||||
match field {
|
||||
SettingsField::Freq => {
|
||||
PRESET_FREQUENCIES.iter().position(|(f, _)| *f == self.frequency).unwrap_or(0)
|
||||
}
|
||||
SettingsField::Lna => {
|
||||
LNA_STEPS.iter().position(|&g| g == self.lna_gain).unwrap_or(0)
|
||||
}
|
||||
SettingsField::Vga => {
|
||||
VGA_STEPS.iter().position(|&g| g == self.vga_gain).unwrap_or(0)
|
||||
}
|
||||
SettingsField::Amp => {
|
||||
if self.amp_enabled { 0 } else { 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of values for the active settings field
|
||||
pub fn settings_value_count(&self) -> usize {
|
||||
let field = SettingsField::ALL[self.settings_field_index];
|
||||
match field {
|
||||
SettingsField::Freq => PRESET_FREQUENCIES.len(),
|
||||
SettingsField::Lna => LNA_STEPS.len(),
|
||||
SettingsField::Vga => VGA_STEPS.len(),
|
||||
SettingsField::Amp => 2, // ON / OFF
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the selected settings value
|
||||
pub fn apply_settings_value(&mut self) -> Result<()> {
|
||||
let field = SettingsField::ALL[self.settings_field_index];
|
||||
match field {
|
||||
SettingsField::Freq => {
|
||||
if self.settings_value_index < PRESET_FREQUENCIES.len() {
|
||||
let (hz, _) = PRESET_FREQUENCIES[self.settings_value_index];
|
||||
self.set_frequency(hz)?;
|
||||
}
|
||||
}
|
||||
SettingsField::Lna => {
|
||||
if self.settings_value_index < LNA_STEPS.len() {
|
||||
self.set_lna_gain(LNA_STEPS[self.settings_value_index])?;
|
||||
}
|
||||
}
|
||||
SettingsField::Vga => {
|
||||
if self.settings_value_index < VGA_STEPS.len() {
|
||||
self.set_vga_gain(VGA_STEPS[self.settings_value_index])?;
|
||||
}
|
||||
}
|
||||
SettingsField::Amp => {
|
||||
self.set_amp(self.settings_value_index == 0)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the make for a protocol name
|
||||
pub fn get_make_for_protocol(protocol: &str) -> &'static str {
|
||||
match protocol {
|
||||
p if p.starts_with("Kia") => "Kia/Hyundai",
|
||||
p if p.starts_with("Ford") => "Ford",
|
||||
p if p.starts_with("Fiat") => "Fiat",
|
||||
"Subaru" => "Subaru",
|
||||
"Suzuki" => "Suzuki",
|
||||
"VAG" | "VW" => "VW/Audi/Seat/Skoda",
|
||||
"PSA" => "Peugeot/Citroen",
|
||||
"Star Line" => "Star Line",
|
||||
"Scher-Khan" => "Scher-Khan",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a demo capture (for testing without HackRF)
|
||||
#[allow(dead_code)]
|
||||
pub fn add_demo_capture(&mut self) {
|
||||
let capture = Capture {
|
||||
id: self.next_capture_id,
|
||||
timestamp: chrono::Utc::now(),
|
||||
frequency: 433_920_000,
|
||||
protocol: Some("Ford V0".to_string()),
|
||||
serial: Some(0x1A2B3C4D),
|
||||
button: Some(0x01),
|
||||
counter: Some(1234),
|
||||
crc_valid: true,
|
||||
data: 0x5A2B3C4D00001234,
|
||||
data_count_bit: 64,
|
||||
raw_pairs: vec![],
|
||||
status: crate::capture::CaptureStatus::EncoderCapable,
|
||||
};
|
||||
self.next_capture_id += 1;
|
||||
self.captures.push(capture);
|
||||
}
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
//! Capture data structures for storing decoded signals.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Status of a captured signal
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CaptureStatus {
|
||||
/// Signal decoded but protocol unknown
|
||||
Unknown,
|
||||
/// Signal decoded with known protocol
|
||||
Decoded,
|
||||
/// Signal can be re-encoded for transmission
|
||||
EncoderCapable,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CaptureStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CaptureStatus::Unknown => write!(f, "Unknown"),
|
||||
CaptureStatus::Decoded => write!(f, "Decoded"),
|
||||
CaptureStatus::EncoderCapable => write!(f, "Encode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Level+duration pair for storage (serializable version)
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct StoredLevelDuration {
|
||||
pub level: bool,
|
||||
pub duration_us: u32,
|
||||
}
|
||||
|
||||
/// A captured keyfob signal
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Capture {
|
||||
/// Unique identifier
|
||||
pub id: u32,
|
||||
/// When the signal was captured
|
||||
pub timestamp: DateTime<Utc>,
|
||||
/// Frequency in Hz
|
||||
pub frequency: u32,
|
||||
/// Protocol name if identified
|
||||
pub protocol: Option<String>,
|
||||
/// Serial number / key identifier (32-bit)
|
||||
pub serial: Option<u32>,
|
||||
/// Button code
|
||||
pub button: Option<u8>,
|
||||
/// Rolling counter value
|
||||
pub counter: Option<u16>,
|
||||
/// Whether CRC validation passed
|
||||
pub crc_valid: bool,
|
||||
/// Raw 64-bit data value
|
||||
pub data: u64,
|
||||
/// Number of valid bits in data
|
||||
pub data_count_bit: usize,
|
||||
/// Raw level+duration pairs
|
||||
pub raw_pairs: Vec<StoredLevelDuration>,
|
||||
/// Current status
|
||||
pub status: CaptureStatus,
|
||||
}
|
||||
|
||||
/// Modulation type used by protocol
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModulationType {
|
||||
Unknown,
|
||||
Pwm,
|
||||
Manchester,
|
||||
#[allow(dead_code)]
|
||||
DifferentialManchester,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ModulationType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ModulationType::Unknown => write!(f, "Unknown"),
|
||||
ModulationType::Pwm => write!(f, "PWM"),
|
||||
ModulationType::Manchester => write!(f, "Manchester"),
|
||||
ModulationType::DifferentialManchester => write!(f, "Diff. Manchester"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Capture {
|
||||
/// Create a new capture from level+duration pairs
|
||||
pub fn from_pairs(id: u32, frequency: u32, pairs: Vec<StoredLevelDuration>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
timestamp: Utc::now(),
|
||||
frequency,
|
||||
protocol: None,
|
||||
serial: None,
|
||||
button: None,
|
||||
counter: None,
|
||||
crc_valid: false,
|
||||
data: 0,
|
||||
data_count_bit: 0,
|
||||
raw_pairs: pairs,
|
||||
status: CaptureStatus::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the serial as a hex string
|
||||
pub fn serial_hex(&self) -> String {
|
||||
match self.serial {
|
||||
Some(s) => format!("{:07X}", s),
|
||||
None => "-".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the frequency in MHz as a string
|
||||
pub fn frequency_mhz(&self) -> String {
|
||||
format!("{:.2}MHz", self.frequency as f64 / 1_000_000.0)
|
||||
}
|
||||
|
||||
/// Get the protocol name or "Unknown"
|
||||
pub fn protocol_name(&self) -> &str {
|
||||
self.protocol.as_deref().unwrap_or("Unknown")
|
||||
}
|
||||
|
||||
/// Get CRC status as a string
|
||||
pub fn crc_status(&self) -> &str {
|
||||
if self.protocol.is_none() {
|
||||
"-"
|
||||
} else if self.crc_valid {
|
||||
"OK"
|
||||
} else {
|
||||
"FAIL"
|
||||
}
|
||||
}
|
||||
|
||||
/// Get button name
|
||||
pub fn button_name(&self) -> &str {
|
||||
match self.button {
|
||||
Some(0x01) => "Lock",
|
||||
Some(0x02) => "Unlock",
|
||||
Some(0x03) => "Lk+Un",
|
||||
Some(0x04) => "Trunk",
|
||||
Some(0x08) => "Panic",
|
||||
Some(_) => "Other",
|
||||
None => "-",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get data as hex string
|
||||
pub fn data_hex(&self) -> String {
|
||||
if self.data_count_bit > 0 {
|
||||
let bytes = (self.data_count_bit + 7) / 8;
|
||||
format!("{:0width$X}", self.data, width = bytes * 2)
|
||||
} else {
|
||||
"-".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the modulation type based on the protocol
|
||||
pub fn modulation(&self) -> ModulationType {
|
||||
match self.protocol_name() {
|
||||
// Manchester-encoded protocols
|
||||
p if p.starts_with("Kia V1") => ModulationType::Manchester,
|
||||
p if p.starts_with("Kia V2") => ModulationType::Manchester,
|
||||
p if p.starts_with("Kia V5") => ModulationType::Manchester,
|
||||
p if p.starts_with("Kia V6") => ModulationType::Manchester,
|
||||
"Ford V0" => ModulationType::Manchester,
|
||||
"Fiat V0" => ModulationType::Manchester,
|
||||
"PSA" => ModulationType::Manchester,
|
||||
"VAG" => ModulationType::Manchester,
|
||||
// PWM-encoded protocols
|
||||
p if p.starts_with("Kia V0") => ModulationType::Pwm,
|
||||
p if p.starts_with("Kia V3") => ModulationType::Pwm,
|
||||
p if p.starts_with("Kia V4") => ModulationType::Pwm,
|
||||
"Subaru" => ModulationType::Pwm,
|
||||
"Suzuki" => ModulationType::Pwm,
|
||||
"Star Line" => ModulationType::Pwm,
|
||||
"Scher-Khan" => ModulationType::Pwm,
|
||||
// Unknown
|
||||
_ => ModulationType::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the encryption/encoding type based on the protocol
|
||||
pub fn encryption_type(&self) -> &'static str {
|
||||
match self.protocol_name() {
|
||||
p if p.starts_with("Kia V3") || p.starts_with("Kia V4") => "KeeLoq",
|
||||
"Star Line" => "KeeLoq",
|
||||
"PSA" => "XTEA/XOR",
|
||||
"VAG" => "AUT64/XTEA",
|
||||
"Scher-Khan" => "Magic Code",
|
||||
"Subaru" | "Suzuki" => "Rolling Code",
|
||||
p if p.starts_with("Ford") => "Fixed Code",
|
||||
p if p.starts_with("Fiat") => "Fixed Code",
|
||||
p if p.starts_with("Kia V0") => "Fixed Code",
|
||||
p if p.starts_with("Kia V1") || p.starts_with("Kia V2") => "Fixed Code",
|
||||
p if p.starts_with("Kia V5") || p.starts_with("Kia V6") => "Fixed Code",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the counter as a formatted string
|
||||
pub fn counter_str(&self) -> String {
|
||||
match self.counter {
|
||||
Some(c) => format!("{:04X}", c),
|
||||
None => "-".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the timestamp formatted for display
|
||||
pub fn timestamp_short(&self) -> String {
|
||||
self.timestamp.format("%H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
/// Get full timestamp for detail display
|
||||
pub fn timestamp_full(&self) -> String {
|
||||
self.timestamp.format("%Y-%m-%d %H:%M:%S UTC").to_string()
|
||||
}
|
||||
|
||||
/// Get button code as hex
|
||||
pub fn button_hex(&self) -> String {
|
||||
match self.button {
|
||||
Some(b) => format!("0x{:02X}", b),
|
||||
None => "-".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get data bits description
|
||||
pub fn data_bits_str(&self) -> String {
|
||||
if self.data_count_bit > 0 {
|
||||
format!("{} bits", self.data_count_bit)
|
||||
} else {
|
||||
"-".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this capture has raw signal data for replay
|
||||
pub fn has_raw_data(&self) -> bool {
|
||||
!self.raw_pairs.is_empty()
|
||||
}
|
||||
|
||||
/// Number of raw signal transitions
|
||||
pub fn raw_pair_count(&self) -> usize {
|
||||
self.raw_pairs.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Button types for keyfob commands
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ButtonCommand {
|
||||
Unlock,
|
||||
Lock,
|
||||
Trunk,
|
||||
Panic,
|
||||
}
|
||||
|
||||
impl ButtonCommand {
|
||||
/// Get the button code for this command
|
||||
pub fn code(&self) -> u8 {
|
||||
match self {
|
||||
ButtonCommand::Unlock => 0x02,
|
||||
ButtonCommand::Lock => 0x01,
|
||||
ButtonCommand::Trunk => 0x04,
|
||||
ButtonCommand::Panic => 0x08,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//! Flipper Zero .sub export format.
|
||||
//!
|
||||
//! Outputs files in the Flipper SubGhz RAW format with alternating
|
||||
//! positive (high) and negative (low) durations in microseconds.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::capture::Capture;
|
||||
|
||||
/// Export a capture to Flipper Zero .sub RAW format
|
||||
pub fn export_flipper_sub(capture: &Capture, path: &Path) -> Result<()> {
|
||||
if capture.raw_pairs.is_empty() {
|
||||
return Err(anyhow::anyhow!("No raw signal data to export"));
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
// Header
|
||||
lines.push("Filetype: Flipper SubGhz RAW File".to_string());
|
||||
lines.push("Version: 1".to_string());
|
||||
lines.push(format!("Frequency: {}", capture.frequency));
|
||||
lines.push("Preset: FuriHalSubGhzPresetOok270Async".to_string());
|
||||
lines.push("Protocol: RAW".to_string());
|
||||
|
||||
// Convert raw_pairs to alternating +/- durations
|
||||
// Flipper format: positive values = HIGH, negative values = LOW
|
||||
let mut raw_data = Vec::new();
|
||||
for pair in &capture.raw_pairs {
|
||||
let duration = pair.duration_us as i64;
|
||||
if pair.level {
|
||||
raw_data.push(duration);
|
||||
} else {
|
||||
raw_data.push(-duration);
|
||||
}
|
||||
}
|
||||
|
||||
// Write RAW_Data lines (max ~512 values per line for readability)
|
||||
const MAX_PER_LINE: usize = 512;
|
||||
for chunk in raw_data.chunks(MAX_PER_LINE) {
|
||||
let values: Vec<String> = chunk.iter().map(|v| v.to_string()).collect();
|
||||
lines.push(format!("RAW_Data: {}", values.join(" ")));
|
||||
}
|
||||
|
||||
let content = lines.join("\n") + "\n";
|
||||
std::fs::write(path, content)?;
|
||||
tracing::info!("Exported Flipper .sub to {:?}", path);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
//! .fob export/import format - rich JSON metadata for captured keyfob signals.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::capture::{Capture, CaptureStatus, StoredLevelDuration};
|
||||
|
||||
/// User-provided metadata for .fob export
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FobMetadata {
|
||||
pub year: Option<u32>,
|
||||
pub make: String,
|
||||
pub model: String,
|
||||
pub region: String,
|
||||
pub notes: String,
|
||||
}
|
||||
|
||||
/// Top-level .fob file structure
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FobFile {
|
||||
pub version: String,
|
||||
pub format: String,
|
||||
pub signal: FobSignalInfo,
|
||||
pub vehicle: FobVehicleInfo,
|
||||
pub capture: FobCapture,
|
||||
}
|
||||
|
||||
/// Signal-level metadata (derived from protocol)
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FobSignalInfo {
|
||||
pub protocol: String,
|
||||
pub frequency: u32,
|
||||
pub frequency_mhz: String,
|
||||
pub modulation: String,
|
||||
pub encryption: String,
|
||||
pub data_bits: usize,
|
||||
pub data_hex: String,
|
||||
pub serial: String,
|
||||
pub key: String,
|
||||
#[serde(default)]
|
||||
pub button: Option<u8>,
|
||||
pub button_name: String,
|
||||
#[serde(default)]
|
||||
pub counter: Option<u16>,
|
||||
pub crc_valid: bool,
|
||||
pub encoder_capable: bool,
|
||||
}
|
||||
|
||||
/// Vehicle metadata (user-provided + auto-detected)
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FobVehicleInfo {
|
||||
#[serde(default)]
|
||||
pub year: Option<u32>,
|
||||
pub make: String,
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub region: Option<String>,
|
||||
#[serde(default)]
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
/// Capture data within a .fob file (timing + raw data)
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FobCapture {
|
||||
pub timestamp: String,
|
||||
/// Raw data value (hex string) for signal reconstruction
|
||||
#[serde(default)]
|
||||
pub raw_data_hex: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub raw_pairs: Option<Vec<FobPair>>,
|
||||
#[serde(default)]
|
||||
pub raw_pair_count: usize,
|
||||
}
|
||||
|
||||
/// A single level+duration pair in the .fob file
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FobPair {
|
||||
pub level: bool,
|
||||
pub duration_us: u32,
|
||||
}
|
||||
|
||||
/// Export a capture to .fob format with optional user metadata
|
||||
pub fn export_fob(
|
||||
capture: &Capture,
|
||||
path: &Path,
|
||||
include_raw: bool,
|
||||
metadata: Option<&FobMetadata>,
|
||||
) -> Result<()> {
|
||||
let protocol_name = capture.protocol_name().to_string();
|
||||
let make = metadata
|
||||
.map(|m| m.make.clone())
|
||||
.filter(|m| !m.is_empty())
|
||||
.unwrap_or_else(|| App::get_make_for_protocol(&protocol_name).to_string());
|
||||
let model = metadata.and_then(|m| {
|
||||
if m.model.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(m.model.clone())
|
||||
}
|
||||
});
|
||||
let year = metadata.and_then(|m| m.year);
|
||||
let region = metadata.and_then(|m| {
|
||||
if m.region.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(m.region.clone())
|
||||
}
|
||||
});
|
||||
let notes = metadata.and_then(|m| {
|
||||
if m.notes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(m.notes.clone())
|
||||
}
|
||||
});
|
||||
|
||||
let raw_pairs = if include_raw && !capture.raw_pairs.is_empty() {
|
||||
Some(
|
||||
capture
|
||||
.raw_pairs
|
||||
.iter()
|
||||
.map(|p| FobPair {
|
||||
level: p.level,
|
||||
duration_us: p.duration_us,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let fob = FobFile {
|
||||
version: "2.0".to_string(),
|
||||
format: "kat-fob".to_string(),
|
||||
signal: FobSignalInfo {
|
||||
protocol: protocol_name.clone(),
|
||||
frequency: capture.frequency,
|
||||
frequency_mhz: capture.frequency_mhz(),
|
||||
modulation: capture.modulation().to_string(),
|
||||
encryption: capture.encryption_type().to_string(),
|
||||
data_bits: capture.data_count_bit,
|
||||
data_hex: capture.data_hex(),
|
||||
serial: capture.serial_hex(),
|
||||
key: capture.data_hex(),
|
||||
button: capture.button,
|
||||
button_name: capture.button_name().to_string(),
|
||||
counter: capture.counter,
|
||||
crc_valid: capture.crc_valid,
|
||||
encoder_capable: capture.status == CaptureStatus::EncoderCapable,
|
||||
},
|
||||
vehicle: FobVehicleInfo {
|
||||
year,
|
||||
make,
|
||||
model,
|
||||
region,
|
||||
notes,
|
||||
},
|
||||
capture: FobCapture {
|
||||
timestamp: capture.timestamp.to_rfc3339(),
|
||||
raw_data_hex: Some(capture.data_hex()),
|
||||
raw_pair_count: capture.raw_pairs.len(),
|
||||
raw_pairs,
|
||||
},
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&fob)?;
|
||||
std::fs::write(path, json)?;
|
||||
tracing::info!("Exported .fob v2 to {:?}", path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Import a .fob file and return a Capture (supports v1 and v2 formats)
|
||||
pub fn import_fob(path: &Path, next_id: u32) -> Result<Capture> {
|
||||
let content =
|
||||
std::fs::read_to_string(path).with_context(|| format!("Failed to read {:?}", path))?;
|
||||
|
||||
// Try v2 format first
|
||||
if let Ok(fob) = serde_json::from_str::<FobFile>(&content) {
|
||||
return import_fob_v2(&fob, next_id);
|
||||
}
|
||||
|
||||
// Fall back to v1 format
|
||||
let fob: FobFileV1 =
|
||||
serde_json::from_str(&content).with_context(|| format!("Failed to parse {:?}", path))?;
|
||||
import_fob_v1(&fob, next_id)
|
||||
}
|
||||
|
||||
// --- V1 compatibility types ---
|
||||
|
||||
/// Legacy v1 .fob file structure
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct FobFileV1 {
|
||||
#[allow(dead_code)]
|
||||
pub version: String,
|
||||
#[allow(dead_code)]
|
||||
pub format: String,
|
||||
pub capture: FobCaptureV1,
|
||||
}
|
||||
|
||||
/// Legacy v1 capture data
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct FobCaptureV1 {
|
||||
pub timestamp: String,
|
||||
pub frequency: u32,
|
||||
pub protocol: String,
|
||||
#[serde(default)]
|
||||
pub year: Option<u32>,
|
||||
#[allow(dead_code)]
|
||||
pub make: String,
|
||||
#[serde(default)]
|
||||
#[allow(dead_code)]
|
||||
pub model: Option<String>,
|
||||
pub serial: String,
|
||||
pub key: String,
|
||||
#[serde(default)]
|
||||
pub button: Option<u8>,
|
||||
#[allow(dead_code)]
|
||||
pub button_name: String,
|
||||
#[serde(default)]
|
||||
pub counter: Option<u16>,
|
||||
#[allow(dead_code)]
|
||||
pub encryption: String,
|
||||
pub crc_valid: bool,
|
||||
pub data_bits: usize,
|
||||
#[serde(default)]
|
||||
pub data_hex: Option<String>,
|
||||
#[serde(default)]
|
||||
pub raw_pairs: Option<Vec<FobPair>>,
|
||||
}
|
||||
|
||||
fn import_fob_v2(fob: &FobFile, next_id: u32) -> Result<Capture> {
|
||||
let sig = &fob.signal;
|
||||
let cap = &fob.capture;
|
||||
|
||||
// Parse serial from hex string
|
||||
let serial = u32::from_str_radix(sig.serial.trim_start_matches("0x"), 16).ok();
|
||||
|
||||
// Parse data from hex string
|
||||
let data = cap
|
||||
.raw_data_hex
|
||||
.as_deref()
|
||||
.or(Some(sig.data_hex.as_str()))
|
||||
.and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Parse timestamp
|
||||
let timestamp = chrono::DateTime::parse_from_rfc3339(&cap.timestamp)
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc))
|
||||
.unwrap_or_else(|_| chrono::Utc::now());
|
||||
|
||||
// Reconstruct raw pairs if present
|
||||
let raw_pairs: Vec<StoredLevelDuration> = cap
|
||||
.raw_pairs
|
||||
.as_ref()
|
||||
.map(|pairs| {
|
||||
pairs
|
||||
.iter()
|
||||
.map(|p| StoredLevelDuration {
|
||||
level: p.level,
|
||||
duration_us: p.duration_us,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let protocol = if sig.protocol == "Unknown" {
|
||||
None
|
||||
} else {
|
||||
Some(sig.protocol.clone())
|
||||
};
|
||||
|
||||
let status = if sig.encoder_capable && !raw_pairs.is_empty() {
|
||||
CaptureStatus::EncoderCapable
|
||||
} else if protocol.is_some() {
|
||||
CaptureStatus::Decoded
|
||||
} else {
|
||||
CaptureStatus::Unknown
|
||||
};
|
||||
|
||||
Ok(Capture {
|
||||
id: next_id,
|
||||
timestamp,
|
||||
frequency: sig.frequency,
|
||||
protocol,
|
||||
serial,
|
||||
button: sig.button,
|
||||
counter: sig.counter,
|
||||
crc_valid: sig.crc_valid,
|
||||
data,
|
||||
data_count_bit: sig.data_bits,
|
||||
raw_pairs,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
fn import_fob_v1(fob: &FobFileV1, next_id: u32) -> Result<Capture> {
|
||||
let cap = &fob.capture;
|
||||
|
||||
// Parse serial from hex string
|
||||
let serial = u32::from_str_radix(cap.serial.trim_start_matches("0x"), 16).ok();
|
||||
|
||||
// Parse data from hex string
|
||||
let data = cap
|
||||
.data_hex
|
||||
.as_deref()
|
||||
.or(Some(cap.key.as_str()))
|
||||
.and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Parse timestamp
|
||||
let timestamp = chrono::DateTime::parse_from_rfc3339(&cap.timestamp)
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc))
|
||||
.unwrap_or_else(|_| chrono::Utc::now());
|
||||
|
||||
// Reconstruct raw pairs if present
|
||||
let raw_pairs: Vec<StoredLevelDuration> = cap
|
||||
.raw_pairs
|
||||
.as_ref()
|
||||
.map(|pairs| {
|
||||
pairs
|
||||
.iter()
|
||||
.map(|p| StoredLevelDuration {
|
||||
level: p.level,
|
||||
duration_us: p.duration_us,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let protocol = if cap.protocol == "Unknown" {
|
||||
None
|
||||
} else {
|
||||
Some(cap.protocol.clone())
|
||||
};
|
||||
|
||||
let status = if protocol.is_some() && !raw_pairs.is_empty() {
|
||||
CaptureStatus::EncoderCapable
|
||||
} else if protocol.is_some() {
|
||||
CaptureStatus::Decoded
|
||||
} else {
|
||||
CaptureStatus::Unknown
|
||||
};
|
||||
|
||||
Ok(Capture {
|
||||
id: next_id,
|
||||
timestamp,
|
||||
frequency: cap.frequency,
|
||||
protocol,
|
||||
serial,
|
||||
button: cap.button,
|
||||
counter: cap.counter,
|
||||
crc_valid: cap.crc_valid,
|
||||
data,
|
||||
data_count_bit: cap.data_bits,
|
||||
raw_pairs,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
/// Scan a directory for .fob files and return their paths
|
||||
pub fn scan_fob_files(dir: &Path) -> Vec<std::path::PathBuf> {
|
||||
if !dir.exists() || !dir.is_dir() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.extension().map_or(false, |e| e == "fob") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
files
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! Export formats for captured signals.
|
||||
|
||||
pub mod fob;
|
||||
pub mod flipper;
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
//! KAT - Keyfob Analysis Toolkit
|
||||
//!
|
||||
//! A terminal UI application for capturing, decoding, and transmitting
|
||||
//! keyfob signals using HackRF.
|
||||
|
||||
mod app;
|
||||
mod capture;
|
||||
mod export;
|
||||
mod protocols;
|
||||
mod radio;
|
||||
mod storage;
|
||||
mod ui;
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use std::io::{self, Write};
|
||||
use std::panic;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use app::{App, InputMode, SignalAction, SettingsField};
|
||||
use ui::draw_ui;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Restore the terminal to normal state (for panic handler)
|
||||
fn restore_terminal_panic() {
|
||||
// Disable raw mode first
|
||||
let _ = disable_raw_mode();
|
||||
|
||||
// Write escape sequences directly to stdout
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
// Leave alternate screen: ESC [ ? 1049 l
|
||||
let _ = stdout.write_all(b"\x1b[?1049l");
|
||||
|
||||
// Show cursor: ESC [ ? 25 h
|
||||
let _ = stdout.write_all(b"\x1b[?25h");
|
||||
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Check if we have a TTY first
|
||||
if !atty::is(atty::Stream::Stdout) {
|
||||
eprintln!("Error: KAT requires a terminal (TTY) to run.");
|
||||
eprintln!("Please run this program in a real terminal, not via a script or IDE runner.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Set up panic hook to restore terminal on panic
|
||||
let default_panic = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
restore_terminal_panic();
|
||||
default_panic(panic_info);
|
||||
}));
|
||||
|
||||
// Initialize logging to a file (not stdout, which would corrupt TUI)
|
||||
let log_file = crate::storage::resolve_config_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from(".").join("KAT"))
|
||||
.join("kat.log");
|
||||
|
||||
// Create log directory if needed
|
||||
if let Some(parent) = log_file.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
// Set up file-based logging
|
||||
if let Ok(file) = std::fs::File::create(&log_file) {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "kat=info".into()),
|
||||
)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_target(false)
|
||||
.with_writer(std::sync::Mutex::new(file))
|
||||
.with_ansi(false)
|
||||
)
|
||||
.init();
|
||||
}
|
||||
|
||||
tracing::info!("Starting KAT v{}", VERSION);
|
||||
|
||||
// Setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Create app and run
|
||||
let mut app = App::new()?;
|
||||
let res = run_app(&mut terminal, &mut app);
|
||||
|
||||
// Restore terminal properly using the terminal's backend
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
eprintln!("Error: {err:?}");
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| draw_ui(f, app))?;
|
||||
|
||||
if event::poll(std::time::Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match app.input_mode {
|
||||
InputMode::Normal => match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char(':') => {
|
||||
app.input_mode = InputMode::Command;
|
||||
app.command_input.clear();
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Down => {
|
||||
app.next_capture();
|
||||
}
|
||||
KeyCode::Char('k') | KeyCode::Up => {
|
||||
app.previous_capture();
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
app.toggle_receiving()?;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Open signal action menu if a capture is selected
|
||||
if app.selected_capture.is_some() && !app.captures.is_empty() {
|
||||
app.input_mode = InputMode::SignalMenu;
|
||||
app.signal_menu_index = 0;
|
||||
}
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
// Open settings selector
|
||||
app.input_mode = InputMode::SettingsSelect;
|
||||
app.settings_field_index = 0;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
InputMode::Command => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let command = app.command_input.clone();
|
||||
app.execute_command(&command)?;
|
||||
app.command_input.clear();
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.command_input.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.command_input.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.command_input.clear();
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
InputMode::SignalMenu => match key.code {
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
if app.signal_menu_index > 0 {
|
||||
app.signal_menu_index -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
if app.signal_menu_index < SignalAction::ALL.len() - 1 {
|
||||
app.signal_menu_index += 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
app.execute_signal_action()?;
|
||||
// Only return to Normal if the action didn't change
|
||||
// input mode (e.g. ExportFob sets FobMetaYear)
|
||||
if app.input_mode == InputMode::SignalMenu {
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
InputMode::SettingsSelect => match key.code {
|
||||
KeyCode::Left | KeyCode::Char('h') => {
|
||||
if app.settings_field_index > 0 {
|
||||
app.settings_field_index -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Right | KeyCode::Char('l') => {
|
||||
if app.settings_field_index < SettingsField::ALL.len() - 1 {
|
||||
app.settings_field_index += 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
// Cycle through fields
|
||||
app.settings_field_index =
|
||||
(app.settings_field_index + 1) % SettingsField::ALL.len();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Enter edit mode for this field
|
||||
app.settings_value_index = app.current_settings_value_index();
|
||||
app.input_mode = InputMode::SettingsEdit;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
InputMode::SettingsEdit => match key.code {
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
if app.settings_value_index > 0 {
|
||||
app.settings_value_index -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
let max = app.settings_value_count();
|
||||
if app.settings_value_index < max - 1 {
|
||||
app.settings_value_index += 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
app.apply_settings_value()?;
|
||||
app.input_mode = InputMode::SettingsSelect;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.input_mode = InputMode::SettingsSelect;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// Startup: found .fob files, y/n to import
|
||||
InputMode::StartupImport => match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||
app.import_fob_files()?;
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||
app.skip_fob_import();
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Year
|
||||
InputMode::FobMetaYear => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.input_mode = InputMode::FobMetaMake;
|
||||
}
|
||||
KeyCode::Char(c) if c.is_ascii_digit() => {
|
||||
if app.fob_meta_year.len() < 4 {
|
||||
app.fob_meta_year.push(c);
|
||||
}
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.fob_meta_year.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.export_capture_id = None;
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Make
|
||||
InputMode::FobMetaMake => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.input_mode = InputMode::FobMetaModel;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.fob_meta_make.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.fob_meta_make.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.export_capture_id = None;
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Model -> Region
|
||||
InputMode::FobMetaModel => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.input_mode = InputMode::FobMetaRegion;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.fob_meta_model.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.fob_meta_model.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.export_capture_id = None;
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Region -> Notes
|
||||
InputMode::FobMetaRegion => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.input_mode = InputMode::FobMetaNotes;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.fob_meta_region.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.fob_meta_region.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.export_capture_id = None;
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// .fob export metadata: Notes -> Export
|
||||
InputMode::FobMetaNotes => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.complete_fob_export()?;
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.fob_meta_notes.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.fob_meta_notes.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.export_capture_id = None;
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process any pending radio events
|
||||
app.process_radio_events()?;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
//! AUT64 block cipher implementation
|
||||
//!
|
||||
//! Ported from protopirate's aut64.c
|
||||
//!
|
||||
//! AUT64 algorithm: 12 rounds, 8-byte block/key size
|
||||
//! Based on: Reference AUT64 implementation
|
||||
//! See: https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf
|
||||
|
||||
pub const AUT64_NUM_ROUNDS: usize = 12;
|
||||
pub const AUT64_BLOCK_SIZE: usize = 8;
|
||||
pub const AUT64_KEY_SIZE: usize = 8;
|
||||
pub const AUT64_PBOX_SIZE: usize = 8;
|
||||
pub const AUT64_SBOX_SIZE: usize = 16;
|
||||
#[allow(dead_code)]
|
||||
pub const AUT64_KEY_STRUCT_PACKED_SIZE: usize = 16;
|
||||
|
||||
/// AUT64 key structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Aut64Key {
|
||||
pub index: u8,
|
||||
pub key: [u8; AUT64_KEY_SIZE],
|
||||
pub pbox: [u8; AUT64_PBOX_SIZE],
|
||||
pub sbox: [u8; AUT64_SBOX_SIZE],
|
||||
}
|
||||
|
||||
impl Default for Aut64Key {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
index: 0,
|
||||
key: [0u8; AUT64_KEY_SIZE],
|
||||
pbox: [0u8; AUT64_PBOX_SIZE],
|
||||
sbox: [0u8; AUT64_SBOX_SIZE],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Round-dependent upper-nibble lookup table
|
||||
static TABLE_LN: [[u8; 8]; AUT64_NUM_ROUNDS] = [
|
||||
[0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3], // Round 0
|
||||
[0x5, 0x4, 0x7, 0x6, 0x1, 0x0, 0x3, 0x2], // Round 1
|
||||
[0x6, 0x7, 0x4, 0x5, 0x2, 0x3, 0x0, 0x1], // Round 2
|
||||
[0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0], // Round 3
|
||||
[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7], // Round 4
|
||||
[0x1, 0x0, 0x3, 0x2, 0x5, 0x4, 0x7, 0x6], // Round 5
|
||||
[0x2, 0x3, 0x0, 0x1, 0x6, 0x7, 0x4, 0x5], // Round 6
|
||||
[0x3, 0x2, 0x1, 0x0, 0x7, 0x6, 0x5, 0x4], // Round 7
|
||||
[0x5, 0x4, 0x7, 0x6, 0x1, 0x0, 0x3, 0x2], // Round 8
|
||||
[0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3], // Round 9
|
||||
[0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0], // Round 10
|
||||
[0x6, 0x7, 0x4, 0x5, 0x2, 0x3, 0x0, 0x1], // Round 11
|
||||
];
|
||||
|
||||
/// Round-dependent lower-nibble lookup table
|
||||
static TABLE_UN: [[u8; 8]; AUT64_NUM_ROUNDS] = [
|
||||
[0x1, 0x0, 0x3, 0x2, 0x5, 0x4, 0x7, 0x6], // Round 0
|
||||
[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7], // Round 1
|
||||
[0x3, 0x2, 0x1, 0x0, 0x7, 0x6, 0x5, 0x4], // Round 2
|
||||
[0x2, 0x3, 0x0, 0x1, 0x6, 0x7, 0x4, 0x5], // Round 3
|
||||
[0x5, 0x4, 0x7, 0x6, 0x1, 0x0, 0x3, 0x2], // Round 4
|
||||
[0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3], // Round 5
|
||||
[0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0], // Round 6
|
||||
[0x6, 0x7, 0x4, 0x5, 0x2, 0x3, 0x0, 0x1], // Round 7
|
||||
[0x3, 0x2, 0x1, 0x0, 0x7, 0x6, 0x5, 0x4], // Round 8
|
||||
[0x2, 0x3, 0x0, 0x1, 0x6, 0x7, 0x4, 0x5], // Round 9
|
||||
[0x1, 0x0, 0x3, 0x2, 0x5, 0x4, 0x7, 0x6], // Round 10
|
||||
[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7], // Round 11
|
||||
];
|
||||
|
||||
/// GF(2^4) multiplication table (nibble offset table)
|
||||
#[rustfmt::skip]
|
||||
static TABLE_OFFSET: [u8; 256] = [
|
||||
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 0
|
||||
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, // 1
|
||||
0x0, 0x2, 0x4, 0x6, 0x8, 0xA, 0xC, 0xE, 0x3, 0x1, 0x7, 0x5, 0xB, 0x9, 0xF, 0xD, // 2
|
||||
0x0, 0x3, 0x6, 0x5, 0xC, 0xF, 0xA, 0x9, 0xB, 0x8, 0xD, 0xE, 0x7, 0x4, 0x1, 0x2, // 3
|
||||
0x0, 0x4, 0x8, 0xC, 0x3, 0x7, 0xB, 0xF, 0x6, 0x2, 0xE, 0xA, 0x5, 0x1, 0xD, 0x9, // 4
|
||||
0x0, 0x5, 0xA, 0xF, 0x7, 0x2, 0xD, 0x8, 0xE, 0xB, 0x4, 0x1, 0x9, 0xC, 0x3, 0x6, // 5
|
||||
0x0, 0x6, 0xC, 0xA, 0xB, 0xD, 0x7, 0x1, 0x5, 0x3, 0x9, 0xF, 0xE, 0x8, 0x2, 0x4, // 6
|
||||
0x0, 0x7, 0xE, 0x9, 0xF, 0x8, 0x1, 0x6, 0xD, 0xA, 0x3, 0x4, 0x2, 0x5, 0xC, 0xB, // 7
|
||||
0x0, 0x8, 0x3, 0xB, 0x6, 0xE, 0x5, 0xD, 0xC, 0x4, 0xF, 0x7, 0xA, 0x2, 0x9, 0x1, // 8
|
||||
0x0, 0x9, 0x1, 0x8, 0x2, 0xB, 0x3, 0xA, 0x4, 0xD, 0x5, 0xC, 0x6, 0xF, 0x7, 0xE, // 9
|
||||
0x0, 0xA, 0x7, 0xD, 0xE, 0x4, 0x9, 0x3, 0xF, 0x5, 0x8, 0x2, 0x1, 0xB, 0x6, 0xC, // A
|
||||
0x0, 0xB, 0x5, 0xE, 0xA, 0x1, 0xF, 0x4, 0x7, 0xC, 0x2, 0x9, 0xD, 0x6, 0x8, 0x3, // B
|
||||
0x0, 0xC, 0xB, 0x7, 0x5, 0x9, 0xE, 0x2, 0xA, 0x6, 0x1, 0xD, 0xF, 0x3, 0x4, 0x8, // C
|
||||
0x0, 0xD, 0x9, 0x4, 0x1, 0xC, 0x8, 0x5, 0x2, 0xF, 0xB, 0x6, 0x3, 0xE, 0xA, 0x7, // D
|
||||
0x0, 0xE, 0xF, 0x1, 0xD, 0x3, 0x2, 0xC, 0x9, 0x7, 0x6, 0x8, 0x4, 0xA, 0xB, 0x5, // E
|
||||
0x0, 0xF, 0xD, 0x2, 0x9, 0x6, 0x4, 0xB, 0x1, 0xE, 0xC, 0x3, 0x8, 0x7, 0x5, 0xA, // F
|
||||
];
|
||||
|
||||
/// S-box substitution table
|
||||
static TABLE_SUB: [u8; 16] = [
|
||||
0x0, 0x1, 0x9, 0xE, 0xD, 0xB, 0x7, 0x6,
|
||||
0xF, 0x2, 0xC, 0x5, 0xA, 0x4, 0x3, 0x8,
|
||||
];
|
||||
|
||||
/// Key nibble operation: apply key-dependent GF offset
|
||||
fn key_nibble(key: &Aut64Key, nibble: u8, table: &[u8; 8], iteration: usize) -> u8 {
|
||||
let key_value = key.key[table[iteration] as usize];
|
||||
let offset = ((key_value as usize) << 4) | (nibble as usize);
|
||||
TABLE_OFFSET[offset]
|
||||
}
|
||||
|
||||
/// Compute round key from state
|
||||
fn round_key(key: &Aut64Key, state: &[u8], round_n: usize) -> u8 {
|
||||
let mut result_hi: u8 = 0;
|
||||
let mut result_lo: u8 = 0;
|
||||
|
||||
for i in 0..(AUT64_BLOCK_SIZE - 1) {
|
||||
result_hi ^= key_nibble(key, state[i] >> 4, &TABLE_UN[round_n], i);
|
||||
result_lo ^= key_nibble(key, state[i] & 0x0F, &TABLE_LN[round_n], i);
|
||||
}
|
||||
|
||||
(result_hi << 4) | result_lo
|
||||
}
|
||||
|
||||
/// Final byte nibble for key schedule
|
||||
fn final_byte_nibble(key: &Aut64Key, table: &[u8; 8]) -> u8 {
|
||||
let key_value = key.key[table[AUT64_BLOCK_SIZE - 1] as usize];
|
||||
TABLE_SUB[key_value as usize] << 4
|
||||
}
|
||||
|
||||
/// Encrypt final byte nibble (inverse S-box lookup through offset table)
|
||||
fn encrypt_final_byte_nibble(key: &Aut64Key, nibble: u8, table: &[u8; 8]) -> u8 {
|
||||
let offset = final_byte_nibble(key, table) as usize;
|
||||
|
||||
for i in 0u8..16 {
|
||||
if TABLE_OFFSET[offset + i as usize] == nibble {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
0 // Should not reach here for valid inputs
|
||||
}
|
||||
|
||||
/// Encrypt compress: compute encrypted output byte for a round
|
||||
fn encrypt_compress(key: &Aut64Key, state: &[u8], round_n: usize) -> u8 {
|
||||
let round_k = round_key(key, state, round_n);
|
||||
let mut result_hi = round_k >> 4;
|
||||
let mut result_lo = round_k & 0x0F;
|
||||
|
||||
result_hi ^= encrypt_final_byte_nibble(key, state[AUT64_BLOCK_SIZE - 1] >> 4, &TABLE_UN[round_n]);
|
||||
result_lo ^= encrypt_final_byte_nibble(key, state[AUT64_BLOCK_SIZE - 1] & 0x0F, &TABLE_LN[round_n]);
|
||||
|
||||
(result_hi << 4) | result_lo
|
||||
}
|
||||
|
||||
/// Decrypt final byte nibble (forward S-box through offset table)
|
||||
fn decrypt_final_byte_nibble(key: &Aut64Key, nibble: u8, table: &[u8; 8], result: u8) -> u8 {
|
||||
let offset = final_byte_nibble(key, table) as usize;
|
||||
TABLE_OFFSET[(result ^ nibble) as usize + offset]
|
||||
}
|
||||
|
||||
/// Decrypt compress: compute decrypted output byte for a round
|
||||
fn decrypt_compress(key: &Aut64Key, state: &[u8], round_n: usize) -> u8 {
|
||||
let round_k = round_key(key, state, round_n);
|
||||
let result_hi = round_k >> 4;
|
||||
let result_lo = round_k & 0x0F;
|
||||
|
||||
let hi = decrypt_final_byte_nibble(
|
||||
key,
|
||||
state[AUT64_BLOCK_SIZE - 1] >> 4,
|
||||
&TABLE_UN[round_n],
|
||||
result_hi,
|
||||
);
|
||||
let lo = decrypt_final_byte_nibble(
|
||||
key,
|
||||
state[AUT64_BLOCK_SIZE - 1] & 0x0F,
|
||||
&TABLE_LN[round_n],
|
||||
result_lo,
|
||||
);
|
||||
|
||||
(hi << 4) | lo
|
||||
}
|
||||
|
||||
/// S-box substitution on a full byte (applies S-box to each nibble independently)
|
||||
fn substitute(key: &Aut64Key, byte: u8) -> u8 {
|
||||
(key.sbox[(byte >> 4) as usize] << 4) | key.sbox[(byte & 0x0F) as usize]
|
||||
}
|
||||
|
||||
/// Byte-level permutation using P-box
|
||||
fn permute_bytes(key: &Aut64Key, state: &mut [u8]) {
|
||||
let mut result = [0u8; AUT64_PBOX_SIZE];
|
||||
for i in 0..AUT64_PBOX_SIZE {
|
||||
result[key.pbox[i] as usize] = state[i];
|
||||
}
|
||||
state[..AUT64_PBOX_SIZE].copy_from_slice(&result);
|
||||
}
|
||||
|
||||
/// Bit-level permutation using P-box
|
||||
fn permute_bits(key: &Aut64Key, byte: u8) -> u8 {
|
||||
let mut result: u8 = 0;
|
||||
for i in 0..8 {
|
||||
if byte & (1 << i) != 0 {
|
||||
result |= 1 << key.pbox[i];
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Compute inverse permutation box
|
||||
fn reverse_box(box_in: &[u8], len: usize) -> Vec<u8> {
|
||||
let mut reversed = vec![0u8; len];
|
||||
for i in 0..len {
|
||||
for j in 0..len {
|
||||
if box_in[j] == i as u8 {
|
||||
reversed[i] = j as u8;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
reversed
|
||||
}
|
||||
|
||||
/// AUT64 encrypt: 12 rounds of the cipher
|
||||
pub fn aut64_encrypt(key: &Aut64Key, message: &mut [u8]) {
|
||||
// Create reverse key for encryption
|
||||
let mut reverse_key = key.clone();
|
||||
let rev_pbox = reverse_box(&key.pbox, AUT64_PBOX_SIZE);
|
||||
let rev_sbox = reverse_box(&key.sbox, AUT64_SBOX_SIZE);
|
||||
reverse_key.pbox.copy_from_slice(&rev_pbox);
|
||||
reverse_key.sbox.copy_from_slice(&rev_sbox);
|
||||
|
||||
for i in 0..AUT64_NUM_ROUNDS {
|
||||
permute_bytes(&reverse_key, message);
|
||||
message[7] = encrypt_compress(&reverse_key, message, i);
|
||||
message[7] = substitute(&reverse_key, message[7]);
|
||||
message[7] = permute_bits(&reverse_key, message[7]);
|
||||
message[7] = substitute(&reverse_key, message[7]);
|
||||
}
|
||||
}
|
||||
|
||||
/// AUT64 decrypt: 12 rounds of the cipher (reverse order)
|
||||
pub fn aut64_decrypt(key: &Aut64Key, message: &mut [u8]) {
|
||||
for i in (0..AUT64_NUM_ROUNDS).rev() {
|
||||
message[7] = substitute(key, message[7]);
|
||||
message[7] = permute_bits(key, message[7]);
|
||||
message[7] = substitute(key, message[7]);
|
||||
message[7] = decrypt_compress(key, message, i);
|
||||
permute_bytes(key, message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pack an AUT64 key structure into a 16-byte array
|
||||
#[allow(dead_code)]
|
||||
pub fn aut64_pack(src: &Aut64Key) -> [u8; AUT64_KEY_STRUCT_PACKED_SIZE] {
|
||||
let mut dest = [0u8; AUT64_KEY_STRUCT_PACKED_SIZE];
|
||||
dest[0] = src.index;
|
||||
|
||||
for i in 0..(src.key.len() / 2) {
|
||||
dest[i + 1] = (src.key[i * 2] << 4) | src.key[i * 2 + 1];
|
||||
}
|
||||
|
||||
let mut pbox: u32 = 0;
|
||||
for i in 0..src.pbox.len() {
|
||||
pbox = (pbox << 3) | src.pbox[i] as u32;
|
||||
}
|
||||
dest[5] = (pbox >> 16) as u8;
|
||||
dest[6] = ((pbox >> 8) & 0xFF) as u8;
|
||||
dest[7] = (pbox & 0xFF) as u8;
|
||||
|
||||
for i in 0..(src.sbox.len() / 2) {
|
||||
dest[i + 8] = (src.sbox[i * 2] << 4) | src.sbox[i * 2 + 1];
|
||||
}
|
||||
|
||||
dest
|
||||
}
|
||||
|
||||
/// Unpack a 16-byte array into an AUT64 key structure
|
||||
#[allow(dead_code)]
|
||||
pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
|
||||
let mut dest = Aut64Key::default();
|
||||
dest.index = src[0];
|
||||
|
||||
for i in 0..(dest.key.len() / 2) {
|
||||
dest.key[i * 2] = src[i + 1] >> 4;
|
||||
dest.key[i * 2 + 1] = src[i + 1] & 0xF;
|
||||
}
|
||||
|
||||
let pbox: u32 = ((src[5] as u32) << 16) | ((src[6] as u32) << 8) | src[7] as u32;
|
||||
for i in (0..dest.pbox.len()).rev() {
|
||||
dest.pbox[i] = ((pbox >> ((dest.pbox.len() - 1 - i) * 3)) & 0x7) as u8;
|
||||
}
|
||||
|
||||
for i in 0..(dest.sbox.len() / 2) {
|
||||
dest.sbox[i * 2] = src[i + 8] >> 4;
|
||||
dest.sbox[i * 2 + 1] = src[i + 8] & 0xF;
|
||||
}
|
||||
|
||||
dest
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//! Common utilities for protocol implementations.
|
||||
|
||||
/// Decoded signal information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DecodedSignal {
|
||||
/// Serial number / device ID
|
||||
pub serial: Option<u32>,
|
||||
/// Button code
|
||||
pub button: Option<u8>,
|
||||
/// Rolling counter
|
||||
pub counter: Option<u16>,
|
||||
/// CRC is valid
|
||||
pub crc_valid: bool,
|
||||
/// Raw data (up to 64 bits)
|
||||
pub data: u64,
|
||||
/// Number of bits in data
|
||||
pub data_count_bit: usize,
|
||||
/// Whether encoding is supported
|
||||
pub encoder_capable: bool,
|
||||
}
|
||||
|
||||
impl DecodedSignal {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(data: u64, bit_count: usize) -> Self {
|
||||
Self {
|
||||
serial: None,
|
||||
button: None,
|
||||
counter: None,
|
||||
crc_valid: false,
|
||||
data,
|
||||
data_count_bit: bit_count,
|
||||
encoder_capable: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC8 calculation with custom polynomial
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - Data bytes to calculate CRC over
|
||||
/// * `poly` - CRC polynomial
|
||||
/// * `init` - Initial CRC value
|
||||
pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
|
||||
let mut crc = init;
|
||||
for &byte in data {
|
||||
crc ^= byte;
|
||||
for _ in 0..8 {
|
||||
if (crc & 0x80) != 0 {
|
||||
crc = (crc << 1) ^ poly;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
/// CRC8 for Kia protocol (polynomial 0x7F, init 0x00)
|
||||
pub fn crc8_kia(data: &[u8]) -> u8 {
|
||||
crc8(data, 0x7F, 0x00)
|
||||
}
|
||||
|
||||
/// Add a bit to the decoder's data accumulator
|
||||
#[inline]
|
||||
pub fn add_bit(data: &mut u64, count: &mut usize, bit: bool) {
|
||||
*data = (*data << 1) | (bit as u64);
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
/// Button names for common keyfob buttons
|
||||
#[allow(dead_code)]
|
||||
pub fn get_button_name(btn: u8) -> &'static str {
|
||||
match btn {
|
||||
0x01 => "Lock",
|
||||
0x02 => "Unlock",
|
||||
0x03 => "Lock+Unlock",
|
||||
0x04 => "Trunk",
|
||||
0x08 => "Panic",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Button code constants
|
||||
#[allow(dead_code)]
|
||||
pub mod buttons {
|
||||
pub const LOCK: u8 = 0x01;
|
||||
pub const UNLOCK: u8 = 0x02;
|
||||
pub const TRUNK: u8 = 0x04;
|
||||
pub const PANIC: u8 = 0x08;
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
//! Fiat V0 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's fiat_v0.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Differential Manchester encoding: 200/400µs timing
|
||||
//! - 64-bit data (cnt:32 | serial:32) + 6-bit button
|
||||
//! - 150 preamble pairs, 800µs gap, 3 bursts
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 200;
|
||||
const TE_LONG: u32 = 400;
|
||||
const TE_DELTA: u32 = 100;
|
||||
#[allow(dead_code)]
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
const PREAMBLE_PAIRS: u16 = 150;
|
||||
const GAP_US: u32 = 800;
|
||||
const TOTAL_BURSTS: u8 = 3;
|
||||
const INTER_BURST_GAP: u32 = 25000;
|
||||
|
||||
/// Manchester decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
Mid1,
|
||||
Start0,
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble,
|
||||
Data,
|
||||
}
|
||||
|
||||
/// Fiat V0 protocol decoder
|
||||
pub struct FiatV0Decoder {
|
||||
step: DecoderStep,
|
||||
preamble_count: u16,
|
||||
manchester_state: ManchesterState,
|
||||
data_low: u32,
|
||||
data_high: u32,
|
||||
bit_count: u8,
|
||||
cnt: u32,
|
||||
serial: u32,
|
||||
btn: u8,
|
||||
te_last: u32,
|
||||
}
|
||||
|
||||
impl FiatV0Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
preamble_count: 0,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
data_low: 0,
|
||||
data_high: 0,
|
||||
bit_count: 0,
|
||||
cnt: 0,
|
||||
serial: 0,
|
||||
btn: 0,
|
||||
te_last: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Manchester advance - returns decoded bit or None
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, true) => 0,
|
||||
(true, false) => 1,
|
||||
(false, true) => 2,
|
||||
(false, false) => 3,
|
||||
};
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => {
|
||||
(ManchesterState::Start1, None)
|
||||
}
|
||||
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => {
|
||||
(ManchesterState::Start0, None)
|
||||
}
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
|
||||
_ => (ManchesterState::Mid1, None),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
output
|
||||
}
|
||||
|
||||
fn manchester_reset(&mut self) {
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
fn add_manchester_bit(&mut self, bit: bool) {
|
||||
let new_bit = if bit { 1u32 } else { 0u32 };
|
||||
let carry = (self.data_low >> 31) & 1;
|
||||
self.data_low = (self.data_low << 1) | new_bit;
|
||||
self.data_high = (self.data_high << 1) | carry;
|
||||
self.bit_count += 1;
|
||||
|
||||
if self.bit_count == 0x40 {
|
||||
self.serial = self.data_low;
|
||||
self.cnt = self.data_high;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
let data = ((self.cnt as u64) << 32) | (self.serial as u64);
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(self.serial),
|
||||
button: Some(self.btn),
|
||||
counter: Some(self.cnt as u16),
|
||||
crc_valid: true, // No CRC in Fiat V0
|
||||
data,
|
||||
data_count_bit: 71,
|
||||
encoder_capable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for FiatV0Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Fiat V0"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.preamble_count = 0;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.cnt = 0;
|
||||
self.serial = 0;
|
||||
self.btn = 0;
|
||||
self.te_last = 0;
|
||||
self.manchester_reset();
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if !level {
|
||||
return None;
|
||||
}
|
||||
if duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.step = DecoderStep::Preamble;
|
||||
self.te_last = duration;
|
||||
self.preamble_count = 0;
|
||||
self.bit_count = 0;
|
||||
self.manchester_reset();
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Preamble => {
|
||||
// Count short pulses in preamble, look for gap
|
||||
if duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||
self.preamble_count += 1;
|
||||
self.te_last = duration;
|
||||
} else if self.preamble_count >= PREAMBLE_PAIRS {
|
||||
// Check for gap
|
||||
if duration_diff!(duration, GAP_US) < TE_DELTA {
|
||||
self.step = DecoderStep::Data;
|
||||
self.preamble_count = 0;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.te_last = duration;
|
||||
return None;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Data => {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
|
||||
if is_short || is_long {
|
||||
if let Some(bit) = self.manchester_advance(is_short, level) {
|
||||
self.add_manchester_bit(bit);
|
||||
|
||||
if self.bit_count > 0x46 {
|
||||
self.btn = ((self.data_low << 1) | 1) as u8;
|
||||
let result = self.parse_data();
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
} else if duration > TE_LONG * 3 {
|
||||
// End of signal
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let cnt = decoded.counter.unwrap_or(0) as u32;
|
||||
|
||||
let data = ((cnt as u64) << 32) | (serial as u64);
|
||||
// Reverse the decoder's btn fix: decoder does (x << 1) | 1
|
||||
let btn_to_send = button >> 1;
|
||||
|
||||
let mut signal = Vec::with_capacity(1024);
|
||||
|
||||
for burst in 0..TOTAL_BURSTS {
|
||||
if burst > 0 {
|
||||
signal.push(LevelDuration::new(false, INTER_BURST_GAP));
|
||||
}
|
||||
|
||||
// Preamble
|
||||
for i in 0..PREAMBLE_PAIRS {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
if i < PREAMBLE_PAIRS - 1 {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, GAP_US));
|
||||
}
|
||||
}
|
||||
|
||||
// First bit (bit 63)
|
||||
let first_bit = (data >> 63) & 1 == 1;
|
||||
if first_bit {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
}
|
||||
|
||||
let mut prev_bit = first_bit;
|
||||
|
||||
// Remaining 63 data bits using differential Manchester
|
||||
for bit in (0..63).rev() {
|
||||
let curr_bit = (data >> bit) & 1 == 1;
|
||||
match (prev_bit, curr_bit) {
|
||||
(false, false) => {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
(false, true) => {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
(true, false) => {
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
}
|
||||
(true, true) => {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
}
|
||||
}
|
||||
prev_bit = curr_bit;
|
||||
}
|
||||
|
||||
// 6 button bits
|
||||
for bit in (0..6).rev() {
|
||||
let curr_bit = (btn_to_send >> bit) & 1 == 1;
|
||||
match (prev_bit, curr_bit) {
|
||||
(false, false) => {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
(false, true) => {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
(true, false) => {
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
}
|
||||
(true, true) => {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
}
|
||||
}
|
||||
prev_bit = curr_bit;
|
||||
}
|
||||
|
||||
// End marker
|
||||
if prev_bit {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
signal.push(LevelDuration::new(false, TE_SHORT * 8));
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
//! Ford V0 protocol decoder
|
||||
//!
|
||||
//! Ported from protopirate's ford_v0.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 250/500µs timing
|
||||
//! - 64 bits total
|
||||
//! - Matrix-based CRC
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
|
||||
/// Manchester decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
Mid1,
|
||||
Start0,
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
CheckPreamble,
|
||||
SaveDuration,
|
||||
CheckDuration,
|
||||
}
|
||||
|
||||
/// Ford V0 protocol decoder
|
||||
pub struct FordV0Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
header_count: u16,
|
||||
decode_data: u64,
|
||||
decode_count_bit: usize,
|
||||
manchester_state: ManchesterState,
|
||||
}
|
||||
|
||||
impl FordV0Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
header_count: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Manchester decode: advance state machine
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, true) => 0, // Short High
|
||||
(true, false) => 1, // Short Low
|
||||
(false, true) => 2, // Long High
|
||||
(false, false) => 3, // Long Low
|
||||
};
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
// From Mid0 or Mid1
|
||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
|
||||
(ManchesterState::Start1, None),
|
||||
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) =>
|
||||
(ManchesterState::Start0, None),
|
||||
|
||||
// From Start1
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
|
||||
|
||||
// From Start0
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
|
||||
|
||||
// Reset on invalid transitions
|
||||
_ => (ManchesterState::Mid1, None),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
output
|
||||
}
|
||||
|
||||
/// CRC matrix for Ford
|
||||
const CRC_MATRIX: [[u8; 4]; 8] = [
|
||||
[0x0C, 0xBB, 0x51, 0x25],
|
||||
[0x18, 0xB1, 0x62, 0xCB],
|
||||
[0x30, 0xA7, 0x44, 0x57],
|
||||
[0x60, 0x89, 0x88, 0xAE],
|
||||
[0xC0, 0xD6, 0xD4, 0x97],
|
||||
[0x25, 0x49, 0x6D, 0xE1],
|
||||
[0x4A, 0x92, 0xDA, 0x03],
|
||||
[0x94, 0xE1, 0x71, 0x06],
|
||||
];
|
||||
|
||||
/// Calculate Ford CRC
|
||||
fn calculate_crc(data: u64) -> u8 {
|
||||
let mut crc = 0u8;
|
||||
|
||||
for byte_idx in 0..7 {
|
||||
let byte = ((data >> (56 - byte_idx * 8)) & 0xFF) as u8;
|
||||
for bit in 0..8 {
|
||||
if (byte >> (7 - bit)) & 1 == 1 {
|
||||
crc ^= Self::CRC_MATRIX[bit][byte_idx % 4];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crc
|
||||
}
|
||||
|
||||
/// Parse decoded data
|
||||
fn parse_data(data: u64) -> DecodedSignal {
|
||||
// Ford V0 format:
|
||||
// Bits 60-63: Prefix (0x5)
|
||||
// Bits 32-59: Serial (28 bits)
|
||||
// Bits 28-31: Button (4 bits)
|
||||
// Bits 16-27: Counter (12 bits)
|
||||
// Bits 8-15: Encrypted data
|
||||
// Bits 0-7: CRC
|
||||
|
||||
let serial = ((data >> 32) & 0x0FFFFFFF) as u32;
|
||||
let button = ((data >> 28) & 0x0F) as u8;
|
||||
let counter = ((data >> 16) & 0x0FFF) as u16;
|
||||
let received_crc = (data & 0xFF) as u8;
|
||||
let calculated_crc = Self::calculate_crc(data);
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: received_crc == calculated_crc,
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for FordV0Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Ford V0"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000] // 315 MHz (US) and 433.92 MHz (EU)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && is_short {
|
||||
self.step = DecoderStep::CheckPreamble;
|
||||
self.header_count = 1;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckPreamble => {
|
||||
if is_short {
|
||||
self.header_count += 1;
|
||||
if self.header_count > 20 && !level {
|
||||
// Enough preamble, start looking for data
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
} else if is_long {
|
||||
if self.header_count > 10 {
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
|
||||
// Process this long pulse
|
||||
if let Some(bit) = self.manchester_advance(false, level) {
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::SaveDuration => {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::CheckDuration;
|
||||
}
|
||||
|
||||
DecoderStep::CheckDuration => {
|
||||
let last_short = duration_diff!(self.te_last, TE_SHORT) < TE_DELTA;
|
||||
let last_long = duration_diff!(self.te_last, TE_LONG) < TE_DELTA;
|
||||
|
||||
// Check for end of transmission
|
||||
if duration > TE_LONG * 3 {
|
||||
if self.decode_count_bit >= MIN_COUNT_BIT {
|
||||
let result = Self::parse_data(self.decode_data);
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Manchester decode
|
||||
if last_short {
|
||||
if let Some(bit) = self.manchester_advance(true, !level) {
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
} else if last_long {
|
||||
if let Some(bit) = self.manchester_advance(false, !level) {
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if is_short || is_long {
|
||||
if let Some(bit) = self.manchester_advance(is_short, level) {
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
|
||||
// Check if we have enough bits
|
||||
if self.decode_count_bit >= MIN_COUNT_BIT {
|
||||
let result = Self::parse_data(self.decode_data);
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0);
|
||||
|
||||
// Build data packet
|
||||
let mut data: u64 = 0;
|
||||
data |= 0x5 << 60; // Prefix
|
||||
data |= ((serial as u64) & 0x0FFFFFFF) << 32;
|
||||
data |= ((button as u64) & 0x0F) << 28;
|
||||
data |= ((counter as u64) & 0x0FFF) << 16;
|
||||
data |= ((decoded.data >> 8) & 0xFF) << 8; // Keep encrypted byte
|
||||
data |= Self::calculate_crc(data) as u64;
|
||||
|
||||
let mut signal = Vec::with_capacity(256);
|
||||
|
||||
// Preamble
|
||||
for _ in 0..30 {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// Sync
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
|
||||
// Data: Manchester encoded, 64 bits MSB first
|
||||
for bit_num in (0..64).rev() {
|
||||
let bit = (data >> bit_num) & 1 == 1;
|
||||
if bit {
|
||||
// Manchester 1: low-high
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
} else {
|
||||
// Manchester 0: high-low
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
}
|
||||
|
||||
// End
|
||||
signal.push(LevelDuration::new(false, TE_LONG * 4));
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
//! KeeLoq common encryption/decryption routines
|
||||
//!
|
||||
//! Shared by Kia V3/V4, Star Line, and other KeeLoq-based protocols.
|
||||
//! Based on the NLF (Non-Linear Feedback) function with constant 0x3A5C742E.
|
||||
|
||||
/// The KeeLoq NLF constant
|
||||
const KEELOQ_NLF: u32 = 0x3A5C742E;
|
||||
|
||||
/// KeeLoq decrypt: 528 rounds of the KeeLoq cipher (decrypt direction)
|
||||
pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 0) & 1)
|
||||
| ((block >> 7) & 2)
|
||||
| ((block >> 17) & 4)
|
||||
| ((block >> 22) & 8)
|
||||
| ((block >> 26) & 16);
|
||||
let lsb = ((block >> 31)
|
||||
^ ((block >> 15) & 1)
|
||||
^ ((KEELOQ_NLF >> lutkey) & 1)
|
||||
^ (((tkey >> 15) & 1) as u32)) as u32;
|
||||
block = ((block & 0x7FFFFFFF) << 1) | lsb;
|
||||
tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63);
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
/// KeeLoq encrypt: 528 rounds of the KeeLoq cipher (encrypt direction)
|
||||
pub fn keeloq_encrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 1) & 1)
|
||||
| ((block >> 8) & 2)
|
||||
| ((block >> 18) & 4)
|
||||
| ((block >> 23) & 8)
|
||||
| ((block >> 27) & 16);
|
||||
let msb = ((block >> 0)
|
||||
^ ((block >> 16) & 1)
|
||||
^ ((KEELOQ_NLF >> lutkey) & 1)
|
||||
^ (((tkey >> 0) & 1) as u32)) as u32;
|
||||
block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31);
|
||||
tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63);
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
/// Normal learning key derivation
|
||||
/// Derives a 64-bit key from a 32-bit fix code and a 64-bit manufacturer key
|
||||
pub fn keeloq_normal_learning(fix: u32, manufacturer_key: u64) -> u64 {
|
||||
let serial_low = fix & 0xFFFF;
|
||||
let serial_high = (fix >> 16) & 0xFFFF;
|
||||
|
||||
let key_low = keeloq_decrypt(serial_low as u32 | 0x20000000, manufacturer_key);
|
||||
let key_high = keeloq_decrypt(serial_high as u32 | 0x60000000, manufacturer_key);
|
||||
|
||||
((key_high as u64) << 32) | (key_low as u64)
|
||||
}
|
||||
|
||||
/// Reverse the bits in a 64-bit key (for protocols that store data MSB-first)
|
||||
pub fn reverse_key(key: u64, bit_count: usize) -> u64 {
|
||||
let mut result: u64 = 0;
|
||||
for i in 0..bit_count {
|
||||
if (key >> i) & 1 == 1 {
|
||||
result |= 1 << (bit_count - 1 - i);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Reverse bits in a byte
|
||||
#[allow(dead_code)]
|
||||
pub fn reverse8(byte: u8) -> u8 {
|
||||
let mut b = byte;
|
||||
b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
|
||||
b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
|
||||
b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
|
||||
b
|
||||
}
|
||||
|
||||
/// Secure learning key derivation
|
||||
/// Derives a 64-bit key from a serial, seed, and manufacturer key
|
||||
#[allow(dead_code)]
|
||||
pub fn keeloq_secure_learning(data: u32, seed: u32, key: u64) -> u64 {
|
||||
let serial = data & 0x0FFFFFFF;
|
||||
let k1 = keeloq_decrypt(serial, key);
|
||||
let k2 = keeloq_decrypt(seed, key);
|
||||
((k1 as u64) << 32) | (k2 as u64)
|
||||
}
|
||||
|
||||
/// FAAC SLH (Spa) learning key derivation
|
||||
/// Derives a 64-bit key from a seed and manufacturer key
|
||||
#[allow(dead_code)]
|
||||
pub fn keeloq_faac_learning(seed: u32, key: u64) -> u64 {
|
||||
let hs = (seed >> 16) as u16;
|
||||
let ending: u16 = 0x544D;
|
||||
let lsb = ((hs as u32) << 16) | (ending as u32);
|
||||
((keeloq_encrypt(seed, key) as u64) << 32) | (keeloq_encrypt(lsb, key) as u64)
|
||||
}
|
||||
|
||||
/// Magic XOR Type 1 learning key derivation
|
||||
#[allow(dead_code)]
|
||||
pub fn keeloq_magic_xor_type1_learning(data: u32, xor: u64) -> u64 {
|
||||
let serial = data & 0x0FFFFFFF;
|
||||
(((serial as u64) << 32) | (serial as u64)) ^ xor
|
||||
}
|
||||
|
||||
/// Magic Serial Type 1 learning key derivation
|
||||
#[allow(dead_code)]
|
||||
pub fn keeloq_magic_serial_type1_learning(data: u32, man: u64) -> u64 {
|
||||
(man & 0xFFFFFFFF)
|
||||
| ((data as u64) << 40)
|
||||
| (((((data & 0xFF).wrapping_add((data >> 8) & 0xFF)) & 0xFF) as u64) << 32)
|
||||
}
|
||||
|
||||
/// Magic Serial Type 2 learning key derivation
|
||||
#[allow(dead_code)]
|
||||
pub fn keeloq_magic_serial_type2_learning(data: u32, man: u64) -> u64 {
|
||||
let p = data.to_le_bytes();
|
||||
let mut m = man.to_le_bytes();
|
||||
m[7] = p[0];
|
||||
m[6] = p[1];
|
||||
m[5] = p[2];
|
||||
m[4] = p[3];
|
||||
u64::from_le_bytes(m)
|
||||
}
|
||||
|
||||
/// Magic Serial Type 3 learning key derivation
|
||||
#[allow(dead_code)]
|
||||
pub fn keeloq_magic_serial_type3_learning(data: u32, man: u64) -> u64 {
|
||||
(man & 0xFFFFFFFFFF000000) | ((data & 0xFFFFFF) as u64)
|
||||
}
|
||||
|
||||
/// KeeLoq learning type constants
|
||||
#[allow(dead_code)]
|
||||
pub mod learning_types {
|
||||
pub const UNKNOWN: u32 = 0;
|
||||
pub const SIMPLE: u32 = 1;
|
||||
pub const NORMAL: u32 = 2;
|
||||
// pub const SECURE: u32 = 3;
|
||||
pub const MAGIC_XOR_TYPE_1: u32 = 4;
|
||||
// pub const FAAC: u32 = 5;
|
||||
pub const MAGIC_SERIAL_TYPE_1: u32 = 6;
|
||||
pub const MAGIC_SERIAL_TYPE_2: u32 = 7;
|
||||
pub const MAGIC_SERIAL_TYPE_3: u32 = 8;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Key management module for protocol encryption/decryption
|
||||
//!
|
||||
//! Ported from protopirate's keys.c
|
||||
//!
|
||||
//! Manages manufacturer keys used by various protocols:
|
||||
//! - KIA V3/V4: kia_mf_key (manufacturer key for KeeLoq)
|
||||
//! - KIA V5: kia_v5_key (custom mixer cipher key)
|
||||
//! - KIA V6: kia_v6_a_key, kia_v6_b_key (AES-128 XOR mask keys)
|
||||
//! - VAG: AUT64 keys loaded from keystore files
|
||||
|
||||
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
|
||||
use std::path::Path;
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
use tracing::{info, warn, error};
|
||||
|
||||
/// Key type identifiers (matches protopirate's keystore types)
|
||||
const KIA_KEY1: u32 = 10; // kia_mf_key
|
||||
const KIA_KEY2: u32 = 11; // kia_v6_a_key
|
||||
const KIA_KEY3: u32 = 12; // kia_v6_b_key
|
||||
const KIA_KEY4: u32 = 13; // kia_v5_key
|
||||
|
||||
/// Maximum number of VAG AUT64 keys
|
||||
const VAG_KEYS_COUNT: usize = 3;
|
||||
|
||||
/// Global key store - thread-safe access to loaded keys
|
||||
pub struct KeyStore {
|
||||
/// KIA manufacturer key (for KeeLoq-based V3/V4)
|
||||
pub kia_mf_key: u64,
|
||||
/// KIA V6 AES key A
|
||||
pub kia_v6_a_key: u64,
|
||||
/// KIA V6 AES key B
|
||||
pub kia_v6_b_key: u64,
|
||||
/// KIA V5 mixer key
|
||||
pub kia_v5_key: u64,
|
||||
/// VAG AUT64 keys
|
||||
pub vag_keys: Vec<Aut64Key>,
|
||||
/// Whether VAG keys have been loaded
|
||||
pub vag_keys_loaded: bool,
|
||||
}
|
||||
|
||||
impl Default for KeyStore {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kia_mf_key: 0,
|
||||
kia_v6_a_key: 0,
|
||||
kia_v6_b_key: 0,
|
||||
kia_v5_key: 0,
|
||||
vag_keys: Vec::new(),
|
||||
vag_keys_loaded: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStore {
|
||||
/// Create a new empty key store
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Load KIA keys from a key entries list
|
||||
/// Each entry is (type_id, key_value)
|
||||
pub fn load_kia_keys(&mut self, entries: &[(u32, u64)]) {
|
||||
for &(key_type, key_value) in entries {
|
||||
match key_type {
|
||||
KIA_KEY1 => self.kia_mf_key = key_value,
|
||||
KIA_KEY2 => self.kia_v6_a_key = key_value,
|
||||
KIA_KEY3 => self.kia_v6_b_key = key_value,
|
||||
KIA_KEY4 => self.kia_v5_key = key_value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load VAG AUT64 keys from raw binary data
|
||||
/// The data should contain packed AUT64 key structures (16 bytes each)
|
||||
pub fn load_vag_keys_from_data(&mut self, data: &[u8]) {
|
||||
if self.vag_keys_loaded {
|
||||
return;
|
||||
}
|
||||
|
||||
self.vag_keys.clear();
|
||||
|
||||
for i in 0..VAG_KEYS_COUNT {
|
||||
let offset = i * AUT64_KEY_STRUCT_PACKED_SIZE;
|
||||
if offset + AUT64_KEY_STRUCT_PACKED_SIZE > data.len() {
|
||||
error!("VAG key data too short for key {}", i);
|
||||
break;
|
||||
}
|
||||
|
||||
let key = aut64::aut64_unpack(&data[offset..offset + AUT64_KEY_STRUCT_PACKED_SIZE]);
|
||||
self.vag_keys.push(key);
|
||||
}
|
||||
|
||||
self.vag_keys_loaded = true;
|
||||
info!("Loaded {} VAG keys", self.vag_keys.len());
|
||||
}
|
||||
|
||||
/// Load VAG AUT64 keys from a file path
|
||||
pub fn load_vag_keys_from_file(&mut self, path: &str) {
|
||||
if self.vag_keys_loaded {
|
||||
return;
|
||||
}
|
||||
|
||||
let file_path = Path::new(path);
|
||||
if !file_path.exists() {
|
||||
warn!("VAG key file not found: {}", path);
|
||||
return;
|
||||
}
|
||||
|
||||
match std::fs::read(file_path) {
|
||||
Ok(data) => {
|
||||
self.load_vag_keys_from_data(&data);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to read VAG key file {}: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a VAG AUT64 key by its internal index field
|
||||
pub fn get_vag_key(&self, index: u8) -> Option<&Aut64Key> {
|
||||
self.vag_keys.iter().find(|k| k.index == index)
|
||||
}
|
||||
|
||||
/// Get a VAG AUT64 key by array position (0-based)
|
||||
pub fn get_vag_key_by_position(&self, position: usize) -> Option<&Aut64Key> {
|
||||
self.vag_keys.get(position)
|
||||
}
|
||||
|
||||
/// Get the KIA manufacturer key
|
||||
pub fn get_kia_mf_key(&self) -> u64 {
|
||||
self.kia_mf_key
|
||||
}
|
||||
|
||||
/// Get the KIA V6 AES key A
|
||||
pub fn get_kia_v6_keystore_a(&self) -> u64 {
|
||||
self.kia_v6_a_key
|
||||
}
|
||||
|
||||
/// Get the KIA V6 AES key B
|
||||
pub fn get_kia_v6_keystore_b(&self) -> u64 {
|
||||
self.kia_v6_b_key
|
||||
}
|
||||
|
||||
/// Get the KIA V5 mixer key
|
||||
pub fn get_kia_v5_key(&self) -> u64 {
|
||||
self.kia_v5_key
|
||||
}
|
||||
}
|
||||
|
||||
/// Global singleton keystore
|
||||
fn global_keystore() -> &'static RwLock<KeyStore> {
|
||||
static GLOBAL_KEYSTORE: OnceLock<RwLock<KeyStore>> = OnceLock::new();
|
||||
GLOBAL_KEYSTORE.get_or_init(|| RwLock::new(KeyStore::new()))
|
||||
}
|
||||
|
||||
/// Get a read reference to the global keystore
|
||||
pub fn get_keystore() -> std::sync::RwLockReadGuard<'static, KeyStore> {
|
||||
global_keystore().read().unwrap()
|
||||
}
|
||||
|
||||
/// Get a write reference to the global keystore
|
||||
pub fn get_keystore_mut() -> std::sync::RwLockWriteGuard<'static, KeyStore> {
|
||||
global_keystore().write().unwrap()
|
||||
}
|
||||
|
||||
/// Initialize the global keystore with KIA keys
|
||||
pub fn load_keys(kia_entries: &[(u32, u64)]) {
|
||||
let mut store = get_keystore_mut();
|
||||
store.load_kia_keys(kia_entries);
|
||||
}
|
||||
|
||||
/// Initialize VAG keys from file
|
||||
pub fn load_vag_keys(path: &str) {
|
||||
let mut store = get_keystore_mut();
|
||||
store.load_vag_keys_from_file(path);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
//! Kia V0 protocol decoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v0.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: short pulse (250µs) = 0, long pulse (500µs) = 1
|
||||
//! - 61 bits total
|
||||
//! - Preamble: alternating short pulses
|
||||
//! - Sync: long-long pattern
|
||||
//! - Data: 59 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::common::{crc8_kia, add_bit};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 61;
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
CheckPreamble,
|
||||
SaveDuration,
|
||||
CheckDuration,
|
||||
}
|
||||
|
||||
/// Kia V0 protocol decoder
|
||||
pub struct KiaV0Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
header_count: u16,
|
||||
decode_data: u64,
|
||||
decode_count_bit: usize,
|
||||
}
|
||||
|
||||
impl KiaV0Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
header_count: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate CRC for Kia data packet
|
||||
fn calculate_crc(data: u64) -> u8 {
|
||||
let crc_data = [
|
||||
((data >> 48) & 0xFF) as u8,
|
||||
((data >> 40) & 0xFF) as u8,
|
||||
((data >> 32) & 0xFF) as u8,
|
||||
((data >> 24) & 0xFF) as u8,
|
||||
((data >> 16) & 0xFF) as u8,
|
||||
((data >> 8) & 0xFF) as u8,
|
||||
];
|
||||
crc8_kia(&crc_data)
|
||||
}
|
||||
|
||||
/// Verify CRC of received data
|
||||
fn verify_crc(data: u64) -> bool {
|
||||
let received_crc = (data & 0xFF) as u8;
|
||||
let calculated_crc = Self::calculate_crc(data);
|
||||
received_crc == calculated_crc
|
||||
}
|
||||
|
||||
/// Extract fields from decoded data
|
||||
fn parse_data(data: u64) -> DecodedSignal {
|
||||
let serial = ((data >> 12) & 0x0FFFFFFF) as u32;
|
||||
let button = ((data >> 8) & 0x0F) as u8;
|
||||
let counter = ((data >> 40) & 0xFFFF) as u16;
|
||||
let crc_valid = Self::verify_crc(data);
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid,
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for KiaV0Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Kia V0"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000] // 433.92 MHz
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||
self.step = DecoderStep::CheckPreamble;
|
||||
self.te_last = duration;
|
||||
self.header_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckPreamble => {
|
||||
if level {
|
||||
if duration_diff!(duration, TE_SHORT) < TE_DELTA ||
|
||||
duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else if duration_diff!(duration, TE_SHORT) < TE_DELTA &&
|
||||
duration_diff!(self.te_last, TE_SHORT) < TE_DELTA {
|
||||
// Short-short pair in preamble
|
||||
self.header_count += 1;
|
||||
} else if duration_diff!(duration, TE_LONG) < TE_DELTA &&
|
||||
duration_diff!(self.te_last, TE_LONG) < TE_DELTA {
|
||||
// Long-long sync pattern
|
||||
if self.header_count > 15 {
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 1;
|
||||
// Add first bit (the sync is also a '1' bit)
|
||||
add_bit(&mut self.decode_data, &mut self.decode_count_bit, true);
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::SaveDuration => {
|
||||
if level {
|
||||
if duration >= TE_LONG + TE_DELTA * 2 {
|
||||
// End of transmission
|
||||
self.step = DecoderStep::Reset;
|
||||
|
||||
if self.decode_count_bit == MIN_COUNT_BIT {
|
||||
return Some(Self::parse_data(self.decode_data));
|
||||
}
|
||||
} else {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::CheckDuration;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckDuration => {
|
||||
if !level {
|
||||
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA &&
|
||||
duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||
// Short-short = bit 0
|
||||
add_bit(&mut self.decode_data, &mut self.decode_count_bit, false);
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA &&
|
||||
duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||
// Long-long = bit 1
|
||||
add_bit(&mut self.decode_data, &mut self.decode_count_bit, true);
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0);
|
||||
|
||||
// Build data packet
|
||||
let mut data: u64 = 0;
|
||||
|
||||
// Bits 56-59: Preserve from original (usually 0xF)
|
||||
data |= decoded.data & 0x0F00000000000000;
|
||||
|
||||
// Bits 40-55: Counter (16 bits)
|
||||
data |= ((counter as u64) & 0xFFFF) << 40;
|
||||
|
||||
// Bits 12-39: Serial (28 bits)
|
||||
data |= ((serial as u64) & 0x0FFFFFFF) << 12;
|
||||
|
||||
// Bits 8-11: Button (4 bits)
|
||||
data |= ((button as u64) & 0x0F) << 8;
|
||||
|
||||
// Bits 0-7: CRC
|
||||
let crc = Self::calculate_crc(data);
|
||||
data |= crc as u64;
|
||||
|
||||
let mut signal = Vec::with_capacity(256);
|
||||
|
||||
// Generate 2 bursts
|
||||
for burst in 0..2 {
|
||||
if burst > 0 {
|
||||
// Inter-burst gap
|
||||
signal.push(LevelDuration::new(false, 25000));
|
||||
}
|
||||
|
||||
// Preamble: 32 alternating short pulses
|
||||
for i in 0..32 {
|
||||
let is_high = (i % 2) == 0;
|
||||
signal.push(LevelDuration::new(is_high, TE_SHORT));
|
||||
}
|
||||
|
||||
// Sync: long-long
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
|
||||
// Data: 59 bits (MSB first)
|
||||
for bit_num in 0..59 {
|
||||
let bit_mask = 1u64 << (58 - bit_num);
|
||||
let bit = (data & bit_mask) != 0;
|
||||
let duration = if bit { TE_LONG } else { TE_SHORT };
|
||||
|
||||
signal.push(LevelDuration::new(true, duration));
|
||||
signal.push(LevelDuration::new(false, duration));
|
||||
}
|
||||
|
||||
// End marker
|
||||
signal.push(LevelDuration::new(true, TE_LONG * 2));
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
//! Kia V1 protocol decoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v1.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 800/1600µs timing
|
||||
//! - 57 bits total
|
||||
//! - Long preamble of ~90 pulses
|
||||
//! - CRC4 checksum
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 800;
|
||||
const TE_LONG: u32 = 1600;
|
||||
const TE_DELTA: u32 = 200;
|
||||
const MIN_COUNT_BIT: usize = 57;
|
||||
|
||||
/// Manchester states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
Mid1,
|
||||
Start0,
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
CheckPreamble,
|
||||
DecodeData,
|
||||
}
|
||||
|
||||
/// Kia V1 protocol decoder
|
||||
pub struct KiaV1Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
header_count: u16,
|
||||
decode_data: u64,
|
||||
decode_count_bit: usize,
|
||||
manchester_state: ManchesterState,
|
||||
}
|
||||
|
||||
impl KiaV1Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
header_count: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC4 calculation for Kia V1
|
||||
fn crc4(bytes: &[u8], offset: u8) -> u8 {
|
||||
let mut crc: u8 = 0;
|
||||
for &byte in bytes {
|
||||
crc ^= (byte & 0x0F) ^ (byte >> 4);
|
||||
}
|
||||
(crc.wrapping_add(offset)) & 0x0F
|
||||
}
|
||||
|
||||
/// Manchester state machine
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, false) => 0, // Short Low
|
||||
(true, true) => 1, // Short High
|
||||
(false, false) => 2, // Long Low
|
||||
(false, true) => 3, // Long High
|
||||
};
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
|
||||
(ManchesterState::Start0, None),
|
||||
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) =>
|
||||
(ManchesterState::Start1, None),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid1, Some(true)),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Start0, Some(true)),
|
||||
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, Some(false)),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Start1, Some(false)),
|
||||
|
||||
_ => (ManchesterState::Mid1, None),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
output
|
||||
}
|
||||
|
||||
/// Parse decoded data
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
let data = self.decode_data;
|
||||
|
||||
// Extract fields per kia_v1.c
|
||||
let serial = (data >> 24) as u32;
|
||||
let button = ((data >> 16) & 0xFF) as u8;
|
||||
let cnt_low = ((data >> 8) & 0xFF) as u16;
|
||||
let cnt_high = ((data >> 4) & 0x0F) as u16;
|
||||
let counter = (cnt_high << 8) | cnt_low;
|
||||
let received_crc = (data & 0x0F) as u8;
|
||||
|
||||
// Calculate CRC
|
||||
let mut char_data = [0u8; 7];
|
||||
char_data[0] = ((serial >> 24) & 0xFF) as u8;
|
||||
char_data[1] = ((serial >> 16) & 0xFF) as u8;
|
||||
char_data[2] = ((serial >> 8) & 0xFF) as u8;
|
||||
char_data[3] = (serial & 0xFF) as u8;
|
||||
char_data[4] = button;
|
||||
char_data[5] = (counter & 0xFF) as u8;
|
||||
|
||||
let crc = if cnt_high == 0 {
|
||||
let offset = if counter >= 0x098 { button } else { 1 };
|
||||
Self::crc4(&char_data[..6], offset)
|
||||
} else if cnt_high >= 0x6 {
|
||||
char_data[6] = cnt_high as u8;
|
||||
Self::crc4(&char_data, 1)
|
||||
} else {
|
||||
Self::crc4(&char_data[..6], 1)
|
||||
};
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: received_crc == crc,
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for KiaV1Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Kia V1"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && is_long {
|
||||
self.step = DecoderStep::CheckPreamble;
|
||||
self.te_last = duration;
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckPreamble => {
|
||||
if !level {
|
||||
if is_long && duration_diff!(self.te_last, TE_LONG) < TE_DELTA {
|
||||
self.header_count += 1;
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
if self.header_count > 70 {
|
||||
if !level && is_short && duration_diff!(self.te_last, TE_LONG) < TE_DELTA {
|
||||
self.decode_count_bit = 1;
|
||||
self.decode_data = 1; // Add first bit
|
||||
self.header_count = 0;
|
||||
self.step = DecoderStep::DecodeData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DecodeData => {
|
||||
if is_short {
|
||||
if let Some(bit) = self.manchester_advance(true, level) {
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
} else if is_long {
|
||||
if let Some(bit) = self.manchester_advance(false, level) {
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.decode_count_bit >= MIN_COUNT_BIT {
|
||||
let result = self.parse_data();
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0);
|
||||
|
||||
// Calculate CRC
|
||||
let cnt_high = ((counter >> 8) & 0x0F) as u8;
|
||||
let mut char_data = [0u8; 7];
|
||||
char_data[0] = ((serial >> 24) & 0xFF) as u8;
|
||||
char_data[1] = ((serial >> 16) & 0xFF) as u8;
|
||||
char_data[2] = ((serial >> 8) & 0xFF) as u8;
|
||||
char_data[3] = (serial & 0xFF) as u8;
|
||||
char_data[4] = button;
|
||||
char_data[5] = (counter & 0xFF) as u8;
|
||||
|
||||
let crc = if cnt_high == 0 {
|
||||
let offset = if counter >= 0x098 { button } else { 1 };
|
||||
Self::crc4(&char_data[..6], offset)
|
||||
} else if cnt_high >= 0x6 {
|
||||
char_data[6] = cnt_high;
|
||||
Self::crc4(&char_data, 1)
|
||||
} else {
|
||||
Self::crc4(&char_data[..6], 1)
|
||||
};
|
||||
|
||||
// Build data
|
||||
let data: u64 = ((serial as u64) << 24) |
|
||||
((button as u64) << 16) |
|
||||
(((counter & 0xFF) as u64) << 8) |
|
||||
((cnt_high as u64) << 4) |
|
||||
(crc as u64);
|
||||
|
||||
let mut signal = Vec::with_capacity(600);
|
||||
|
||||
// Generate 3 bursts
|
||||
for burst in 0..3 {
|
||||
if burst > 0 {
|
||||
signal.push(LevelDuration::new(false, 25000));
|
||||
}
|
||||
|
||||
// Preamble: 90 long pairs
|
||||
for _ in 0..90 {
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
|
||||
// Short gap
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
|
||||
// Data: Manchester encoded, MSB first
|
||||
for bit_num in (1..MIN_COUNT_BIT).rev() {
|
||||
let bit = ((data >> (bit_num - 1)) & 1) == 1;
|
||||
if bit {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
//! Kia V2 protocol decoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v2.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 500/1000µs timing
|
||||
//! - 53 bits total
|
||||
//! - Long preamble of 252+ pairs
|
||||
//! - CRC4 checksum
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 500;
|
||||
const TE_LONG: u32 = 1000;
|
||||
const TE_DELTA: u32 = 150;
|
||||
const MIN_COUNT_BIT: usize = 53;
|
||||
|
||||
/// Manchester states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
Mid1,
|
||||
Start0,
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
CheckPreamble,
|
||||
CollectRawBits,
|
||||
}
|
||||
|
||||
/// Kia V2 protocol decoder
|
||||
pub struct KiaV2Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
header_count: u16,
|
||||
decode_data: u64,
|
||||
decode_count_bit: usize,
|
||||
manchester_state: ManchesterState,
|
||||
}
|
||||
|
||||
impl KiaV2Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
header_count: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate CRC for Kia V2
|
||||
fn calculate_crc(data: u64) -> u8 {
|
||||
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
||||
let u_var4 = (data & 0xFFFFFFFF) as u32;
|
||||
|
||||
let mut bytes = [0u8; 6];
|
||||
bytes[0] = (u_var4 >> 20) as u8;
|
||||
bytes[1] = ((u_var4 >> 28) | ((serial & 0x0F) << 4)) as u8;
|
||||
bytes[2] = (serial >> 4) as u8;
|
||||
bytes[3] = (serial >> 12) as u8;
|
||||
bytes[4] = (u_var4 >> 4) as u8;
|
||||
bytes[5] = (u_var4 >> 12) as u8;
|
||||
|
||||
let mut crc: u8 = 0;
|
||||
for &byte in &bytes {
|
||||
crc ^= (byte & 0x0F) ^ (byte >> 4);
|
||||
}
|
||||
|
||||
(crc.wrapping_add(1)) & 0x0F
|
||||
}
|
||||
|
||||
/// Manchester state machine
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, false) => 0, // Short Low
|
||||
(true, true) => 1, // Short High
|
||||
(false, false) => 2, // Long Low
|
||||
(false, true) => 3, // Long High
|
||||
};
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
|
||||
(ManchesterState::Start0, None),
|
||||
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) =>
|
||||
(ManchesterState::Start1, None),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid1, Some(true)),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Start0, Some(true)),
|
||||
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, Some(false)),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Start1, Some(false)),
|
||||
|
||||
_ => (ManchesterState::Mid1, None),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
output
|
||||
}
|
||||
|
||||
/// Parse decoded data
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
let data = self.decode_data;
|
||||
|
||||
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
||||
let button = ((data >> 16) & 0x0F) as u8;
|
||||
|
||||
// Counter has byte-swapped format
|
||||
let raw_count = ((data >> 4) & 0xFFF) as u16;
|
||||
let counter = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
|
||||
|
||||
let received_crc = (data & 0x0F) as u8;
|
||||
let calculated_crc = Self::calculate_crc(data);
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: received_crc == calculated_crc,
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for KiaV2Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Kia V2"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && is_long {
|
||||
self.step = DecoderStep::CheckPreamble;
|
||||
self.te_last = duration;
|
||||
self.header_count = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckPreamble => {
|
||||
if level {
|
||||
if is_long {
|
||||
self.te_last = duration;
|
||||
self.header_count += 1;
|
||||
} else if is_short && self.header_count >= 100 {
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 1;
|
||||
self.step = DecoderStep::CollectRawBits;
|
||||
self.decode_data = 1; // First bit
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
if is_long {
|
||||
self.header_count += 1;
|
||||
self.te_last = duration;
|
||||
} else if !is_short {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CollectRawBits => {
|
||||
if is_short {
|
||||
if let Some(bit) = self.manchester_advance(true, level) {
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
} else if is_long {
|
||||
if let Some(bit) = self.manchester_advance(false, level) {
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.decode_count_bit >= MIN_COUNT_BIT {
|
||||
let result = self.parse_data();
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0);
|
||||
|
||||
// Reconstruct data in V2 format
|
||||
let u_var6 = ((counter & 0xFF) as u32) << 8 |
|
||||
((button & 0x0F) as u32) << 16 |
|
||||
(((counter >> 4) & 0xF0) as u32);
|
||||
|
||||
let mut new_data: u64 = 1u64 << 52; // Start bit
|
||||
new_data |= ((serial as u64) << 20) & 0xFFFFFFFFF00000;
|
||||
new_data |= u_var6 as u64;
|
||||
|
||||
// Calculate and apply CRC
|
||||
let crc = Self::calculate_crc(new_data);
|
||||
new_data = (new_data & !0x0F) | (crc as u64);
|
||||
|
||||
let mut signal = Vec::with_capacity(700);
|
||||
|
||||
// Generate 2 bursts
|
||||
for _burst in 0..2 {
|
||||
// Preamble: 252 long pairs
|
||||
for _ in 0..252 {
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
|
||||
// Short gap
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
|
||||
// Data: Manchester encoded, MSB first
|
||||
for bit_num in (1..MIN_COUNT_BIT).rev() {
|
||||
let bit = ((new_data >> (bit_num - 1)) & 1) == 1;
|
||||
if bit {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
//! Kia V3/V4 protocol decoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v3_v4.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 400/800µs timing
|
||||
//! - 68 bits total
|
||||
//! - Short preamble of 16 pairs
|
||||
//! - KeeLoq encryption (requires manufacturer key)
|
||||
//! - V3 and V4 differ only in sync polarity
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 400;
|
||||
const TE_LONG: u32 = 800;
|
||||
const TE_DELTA: u32 = 150;
|
||||
const MIN_COUNT_BIT: usize = 68;
|
||||
const SYNC_DURATION: u32 = 1200;
|
||||
const INTER_BURST_GAP_US: u32 = 10000;
|
||||
const PREAMBLE_PAIRS: usize = 16;
|
||||
const TOTAL_BURSTS: usize = 3;
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
CheckPreamble,
|
||||
CollectRawBits,
|
||||
}
|
||||
|
||||
/// Kia V3/V4 protocol decoder
|
||||
pub struct KiaV3V4Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
header_count: u16,
|
||||
raw_bits: [u8; 32],
|
||||
raw_bit_count: u16,
|
||||
is_v3_sync: bool,
|
||||
}
|
||||
|
||||
impl KiaV3V4Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
header_count: 0,
|
||||
raw_bits: [0; 32],
|
||||
raw_bit_count: 0,
|
||||
is_v3_sync: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverse bits in a byte
|
||||
fn reverse8(byte: u8) -> u8 {
|
||||
let mut byte = byte;
|
||||
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
|
||||
byte = (byte & 0xCC) >> 2 | (byte & 0x33) << 2;
|
||||
byte = (byte & 0xAA) >> 1 | (byte & 0x55) << 1;
|
||||
byte
|
||||
}
|
||||
|
||||
/// Add a raw bit to the buffer
|
||||
fn add_raw_bit(&mut self, bit: bool) {
|
||||
if self.raw_bit_count < 256 {
|
||||
let byte_idx = (self.raw_bit_count / 8) as usize;
|
||||
let bit_idx = 7 - (self.raw_bit_count % 8);
|
||||
if bit {
|
||||
self.raw_bits[byte_idx] |= 1 << bit_idx;
|
||||
} else {
|
||||
self.raw_bits[byte_idx] &= !(1 << bit_idx);
|
||||
}
|
||||
self.raw_bit_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC4 calculation
|
||||
fn calculate_crc(bytes: &[u8]) -> u8 {
|
||||
let mut crc: u8 = 0;
|
||||
for &byte in bytes.iter().take(8) {
|
||||
crc ^= (byte & 0x0F) ^ (byte >> 4);
|
||||
}
|
||||
crc & 0x0F
|
||||
}
|
||||
|
||||
/// KeeLoq decrypt
|
||||
fn keeloq_decrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 0) & 1) |
|
||||
((block >> 7) & 2) |
|
||||
((block >> 17) & 4) |
|
||||
((block >> 22) & 8) |
|
||||
((block >> 26) & 16);
|
||||
let lsb = ((block >> 31) ^
|
||||
((block >> 15) & 1) ^
|
||||
((0x3A5C742E_u32 >> lutkey) & 1) ^
|
||||
(((tkey >> 15) & 1) as u32)) as u32;
|
||||
block = ((block & 0x7FFFFFFF) << 1) | lsb;
|
||||
tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63);
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
/// KeeLoq encrypt
|
||||
fn keeloq_encrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 1) & 1) |
|
||||
((block >> 8) & 2) |
|
||||
((block >> 18) & 4) |
|
||||
((block >> 23) & 8) |
|
||||
((block >> 27) & 16);
|
||||
let msb = ((block >> 0) ^
|
||||
((block >> 16) & 1) ^
|
||||
((0x3A5C742E_u32 >> lutkey) & 1) ^
|
||||
(((tkey >> 0) & 1) as u32)) as u32;
|
||||
block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31);
|
||||
tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63);
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
/// Get manufacturer key (placeholder - in real use, this would be loaded from config)
|
||||
fn get_mf_key() -> u64 {
|
||||
// This is a placeholder - actual key should be loaded from secure storage
|
||||
0x0000000000000000
|
||||
}
|
||||
|
||||
/// Process the collected buffer and validate
|
||||
fn process_buffer(&self) -> Option<DecodedSignal> {
|
||||
if self.raw_bit_count < 68 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut b = self.raw_bits;
|
||||
|
||||
// V3 sync means data is inverted
|
||||
if self.is_v3_sync {
|
||||
let num_bytes = ((self.raw_bit_count + 7) / 8) as usize;
|
||||
for i in 0..num_bytes {
|
||||
b[i] = !b[i];
|
||||
}
|
||||
}
|
||||
|
||||
let _crc = (b[8] >> 4) & 0x0F;
|
||||
|
||||
let encrypted = ((Self::reverse8(b[3]) as u32) << 24) |
|
||||
((Self::reverse8(b[2]) as u32) << 16) |
|
||||
((Self::reverse8(b[1]) as u32) << 8) |
|
||||
(Self::reverse8(b[0]) as u32);
|
||||
|
||||
let serial = ((Self::reverse8(b[7] & 0xF0) as u32) << 24) |
|
||||
((Self::reverse8(b[6]) as u32) << 16) |
|
||||
((Self::reverse8(b[5]) as u32) << 8) |
|
||||
(Self::reverse8(b[4]) as u32);
|
||||
|
||||
let button = (Self::reverse8(b[7]) & 0xF0) >> 4;
|
||||
let our_serial_lsb = (serial & 0xFF) as u8;
|
||||
|
||||
let mf_key = Self::get_mf_key();
|
||||
let decrypted = Self::keeloq_decrypt(encrypted, mf_key);
|
||||
let dec_btn = ((decrypted >> 28) & 0x0F) as u8;
|
||||
let dec_serial_lsb = ((decrypted >> 16) & 0xFF) as u8;
|
||||
|
||||
// Validate decryption (may fail if key is wrong)
|
||||
let crc_valid = if mf_key != 0 {
|
||||
dec_btn == button && dec_serial_lsb == our_serial_lsb
|
||||
} else {
|
||||
// Can't validate without key
|
||||
true
|
||||
};
|
||||
|
||||
let counter = (decrypted & 0xFFFF) as u16;
|
||||
|
||||
// Build key data
|
||||
let key_data = ((b[0] as u64) << 56) |
|
||||
((b[1] as u64) << 48) |
|
||||
((b[2] as u64) << 40) |
|
||||
((b[3] as u64) << 32) |
|
||||
((b[4] as u64) << 24) |
|
||||
((b[5] as u64) << 16) |
|
||||
((b[6] as u64) << 8) |
|
||||
(b[7] as u64);
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid,
|
||||
data: key_data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for KiaV3V4Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Kia V3/V4"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[315_000_000, 433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.header_count = 0;
|
||||
self.raw_bits = [0; 32];
|
||||
self.raw_bit_count = 0;
|
||||
self.is_v3_sync = false;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
let is_sync = duration > 1000 && duration < 1500;
|
||||
let is_very_long = duration > 1500;
|
||||
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && is_short {
|
||||
self.step = DecoderStep::CheckPreamble;
|
||||
self.te_last = duration;
|
||||
self.header_count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckPreamble => {
|
||||
if level {
|
||||
if is_short {
|
||||
self.te_last = duration;
|
||||
} else if is_sync && self.header_count >= 8 {
|
||||
// V4 sync: long HIGH
|
||||
self.step = DecoderStep::CollectRawBits;
|
||||
self.raw_bit_count = 0;
|
||||
self.is_v3_sync = false;
|
||||
self.raw_bits = [0; 32];
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
if is_sync && self.header_count >= 8 {
|
||||
// V3 sync: long LOW
|
||||
self.step = DecoderStep::CollectRawBits;
|
||||
self.raw_bit_count = 0;
|
||||
self.is_v3_sync = true;
|
||||
self.raw_bits = [0; 32];
|
||||
} else if is_short && duration_diff!(self.te_last, TE_SHORT) < TE_DELTA {
|
||||
self.header_count += 1;
|
||||
} else if is_very_long {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CollectRawBits => {
|
||||
if level {
|
||||
if is_sync || is_very_long {
|
||||
// End of data
|
||||
let result = self.process_buffer();
|
||||
self.step = DecoderStep::Reset;
|
||||
return result;
|
||||
} else if is_short {
|
||||
self.add_raw_bit(false);
|
||||
} else if is_long {
|
||||
self.add_raw_bit(true);
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
if is_sync || is_very_long {
|
||||
let result = self.process_buffer();
|
||||
self.step = DecoderStep::Reset;
|
||||
return result;
|
||||
}
|
||||
// LOW durations don't carry data in PWM
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0);
|
||||
|
||||
// Build plaintext for encryption
|
||||
let plaintext = (counter as u32) |
|
||||
((serial & 0xFF) << 16) |
|
||||
(0x1 << 24) |
|
||||
(((button & 0x0F) as u32) << 28);
|
||||
|
||||
let mf_key = Self::get_mf_key();
|
||||
let encrypted = Self::keeloq_encrypt(plaintext, mf_key);
|
||||
|
||||
// Build raw bytes
|
||||
let mut raw_bytes = [0u8; 9];
|
||||
raw_bytes[0] = Self::reverse8((encrypted >> 0) as u8);
|
||||
raw_bytes[1] = Self::reverse8((encrypted >> 8) as u8);
|
||||
raw_bytes[2] = Self::reverse8((encrypted >> 16) as u8);
|
||||
raw_bytes[3] = Self::reverse8((encrypted >> 24) as u8);
|
||||
|
||||
let serial_btn = (serial & 0x0FFFFFFF) | (((button & 0x0F) as u32) << 28);
|
||||
raw_bytes[4] = Self::reverse8((serial_btn >> 0) as u8);
|
||||
raw_bytes[5] = Self::reverse8((serial_btn >> 8) as u8);
|
||||
raw_bytes[6] = Self::reverse8((serial_btn >> 16) as u8);
|
||||
raw_bytes[7] = Self::reverse8((serial_btn >> 24) as u8);
|
||||
|
||||
let crc = Self::calculate_crc(&raw_bytes);
|
||||
raw_bytes[8] = crc << 4;
|
||||
|
||||
// Use V4 encoding by default
|
||||
let version = 0;
|
||||
|
||||
if version == 1 {
|
||||
// V3: invert data
|
||||
for byte in raw_bytes.iter_mut() {
|
||||
*byte = !*byte;
|
||||
}
|
||||
}
|
||||
|
||||
let mut signal = Vec::with_capacity(600);
|
||||
|
||||
for burst in 0..TOTAL_BURSTS {
|
||||
if burst > 0 {
|
||||
signal.push(LevelDuration::new(false, INTER_BURST_GAP_US));
|
||||
}
|
||||
|
||||
// Preamble
|
||||
for _ in 0..PREAMBLE_PAIRS {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// Sync pulse
|
||||
if version == 0 {
|
||||
// V4: long HIGH, short LOW
|
||||
signal.push(LevelDuration::new(true, SYNC_DURATION));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
} else {
|
||||
// V3: short HIGH, long LOW
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, SYNC_DURATION));
|
||||
}
|
||||
|
||||
// Data bits
|
||||
for byte_idx in 0..9 {
|
||||
let bits_in_byte = if byte_idx == 8 { 4 } else { 8 };
|
||||
for bit_idx in (8 - bits_in_byte..8).rev() {
|
||||
let bit = (raw_bytes[byte_idx] >> bit_idx) & 1 != 0;
|
||||
if bit {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
//! Kia V5 protocol decoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v5.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 400/800µs timing
|
||||
//! - 64 bits total (+3 bit CRC)
|
||||
//! - Preamble of ~40+ short pairs
|
||||
//! - Custom "mixer" encryption
|
||||
//! - Decode-only (no encoder)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 400;
|
||||
const TE_LONG: u32 = 800;
|
||||
const TE_DELTA: u32 = 150;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
|
||||
/// Manchester states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
Mid1,
|
||||
Start0,
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
CheckPreamble,
|
||||
Data,
|
||||
}
|
||||
|
||||
/// Kia V5 protocol decoder
|
||||
pub struct KiaV5Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
header_count: u16,
|
||||
bit_count: u8,
|
||||
decoded_data: u64,
|
||||
saved_key: u64,
|
||||
manchester_state: ManchesterState,
|
||||
}
|
||||
|
||||
impl KiaV5Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
header_count: 0,
|
||||
bit_count: 0,
|
||||
decoded_data: 0,
|
||||
saved_key: 0,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get encryption key (placeholder)
|
||||
fn get_v5_key() -> u64 {
|
||||
0x0000000000000000
|
||||
}
|
||||
|
||||
/// Custom mixer decryption
|
||||
fn mixer_decode(encrypted: u32) -> u16 {
|
||||
let mut s0 = (encrypted & 0xFF) as u8;
|
||||
let mut s1 = ((encrypted >> 8) & 0xFF) as u8;
|
||||
let mut s2 = ((encrypted >> 16) & 0xFF) as u8;
|
||||
let mut s3 = ((encrypted >> 24) & 0xFF) as u8;
|
||||
|
||||
let key = Self::get_v5_key();
|
||||
let mut keystore_bytes = [0u8; 8];
|
||||
for i in 0..8 {
|
||||
keystore_bytes[i] = ((key >> ((7 - i) * 8)) & 0xFF) as u8;
|
||||
}
|
||||
|
||||
let mut round_index: usize = 1;
|
||||
for _ in 0..18 {
|
||||
let mut r = keystore_bytes[round_index];
|
||||
let mut steps = 8;
|
||||
while steps > 0 {
|
||||
let base = if (s3 & 0x40) == 0 {
|
||||
if (s3 & 0x02) == 0 { 0x74 } else { 0x2E }
|
||||
} else {
|
||||
if (s3 & 0x02) == 0 { 0x3A } else { 0x5C }
|
||||
};
|
||||
|
||||
let mut base = base;
|
||||
if s2 & 0x08 != 0 {
|
||||
base = ((base >> 4) & 0x0F) | ((base & 0x0F) << 4);
|
||||
}
|
||||
if s1 & 0x01 != 0 {
|
||||
base = (base & 0x3F) << 2;
|
||||
}
|
||||
if s0 & 0x01 != 0 {
|
||||
base = base << 1;
|
||||
}
|
||||
|
||||
let temp = (s3 ^ s1) & 0xFF;
|
||||
s3 = (s3 & 0x7F) << 1;
|
||||
if s2 & 0x80 != 0 {
|
||||
s3 |= 0x01;
|
||||
}
|
||||
s2 = (s2 & 0x7F) << 1;
|
||||
if s1 & 0x80 != 0 {
|
||||
s2 |= 0x01;
|
||||
}
|
||||
s1 = (s1 & 0x7F) << 1;
|
||||
if s0 & 0x80 != 0 {
|
||||
s1 |= 0x01;
|
||||
}
|
||||
s0 = (s0 & 0x7F) << 1;
|
||||
|
||||
let chk = (base ^ (r ^ temp)) & 0xFF;
|
||||
if chk & 0x80 != 0 {
|
||||
s0 |= 0x01;
|
||||
}
|
||||
r = (r & 0x7F) << 1;
|
||||
steps -= 1;
|
||||
}
|
||||
round_index = (round_index.wrapping_sub(1)) & 0x7;
|
||||
}
|
||||
|
||||
((s0 as u16) + ((s1 as u16) << 8)) & 0xFFFF
|
||||
}
|
||||
|
||||
/// Reverse bits in 64-bit value
|
||||
fn compute_yek(key: u64) -> u64 {
|
||||
let mut yek: u64 = 0;
|
||||
for i in 0..8 {
|
||||
let byte = ((key >> (i * 8)) & 0xFF) as u8;
|
||||
let mut reversed: u8 = 0;
|
||||
for b in 0..8 {
|
||||
if byte & (1 << b) != 0 {
|
||||
reversed |= 1 << (7 - b);
|
||||
}
|
||||
}
|
||||
yek |= (reversed as u64) << ((7 - i) * 8);
|
||||
}
|
||||
yek
|
||||
}
|
||||
|
||||
/// Manchester state machine
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, true) => 0, // Short High
|
||||
(true, false) => 1, // Short Low
|
||||
(false, true) => 2, // Long High
|
||||
(false, false) => 3, // Long Low
|
||||
};
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) =>
|
||||
(ManchesterState::Start0, None),
|
||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
|
||||
(ManchesterState::Start1, None),
|
||||
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
|
||||
|
||||
_ => (ManchesterState::Mid1, None),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
output
|
||||
}
|
||||
|
||||
/// Parse decoded data
|
||||
fn parse_data(&self) -> Option<DecodedSignal> {
|
||||
if self.bit_count < MIN_COUNT_BIT as u8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let key = self.saved_key;
|
||||
let yek = Self::compute_yek(key);
|
||||
|
||||
let serial = ((yek >> 32) & 0x0FFFFFFF) as u32;
|
||||
let button = ((yek >> 60) & 0x0F) as u8;
|
||||
let encrypted = (yek & 0xFFFFFFFF) as u32;
|
||||
let counter = Self::mixer_decode(encrypted);
|
||||
|
||||
let _crc = (self.decoded_data & 0x07) as u8;
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: true, // V5 doesn't have a standard CRC validation
|
||||
data: key,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: false, // V5 is decode-only
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for KiaV5Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Kia V5"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.header_count = 0;
|
||||
self.bit_count = 0;
|
||||
self.decoded_data = 0;
|
||||
self.saved_key = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && is_short {
|
||||
self.step = DecoderStep::CheckPreamble;
|
||||
self.te_last = duration;
|
||||
self.header_count = 1;
|
||||
self.bit_count = 0;
|
||||
self.decoded_data = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckPreamble => {
|
||||
if level {
|
||||
if is_long {
|
||||
if self.header_count > 40 {
|
||||
self.step = DecoderStep::Data;
|
||||
self.bit_count = 0;
|
||||
self.decoded_data = 0;
|
||||
self.saved_key = 0;
|
||||
self.header_count = 0;
|
||||
} else {
|
||||
self.te_last = duration;
|
||||
}
|
||||
} else if is_short {
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
if (is_short && duration_diff!(self.te_last, TE_SHORT) < TE_DELTA) ||
|
||||
(is_long && duration_diff!(self.te_last, TE_SHORT) < TE_DELTA) ||
|
||||
(duration_diff!(self.te_last, TE_LONG) < TE_DELTA) {
|
||||
self.header_count += 1;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::Data => {
|
||||
if !is_short && !is_long {
|
||||
// End of data - try to parse
|
||||
if self.bit_count >= MIN_COUNT_BIT as u8 {
|
||||
let result = self.parse_data();
|
||||
self.step = DecoderStep::Reset;
|
||||
return result;
|
||||
}
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.bit_count <= 66 {
|
||||
if let Some(bit) = self.manchester_advance(is_short, level) {
|
||||
self.decoded_data = (self.decoded_data << 1) | (bit as u64);
|
||||
self.bit_count += 1;
|
||||
|
||||
if self.bit_count == 64 {
|
||||
self.saved_key = self.decoded_data;
|
||||
self.decoded_data = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.te_last = duration;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false // V5 is decode-only in protopirate
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None // V5 doesn't support encoding
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
//! Kia V6 protocol decoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v6.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 200/400µs timing
|
||||
//! - 144 bits total (split into 3 parts)
|
||||
//! - Long preamble of 600+ pairs
|
||||
//! - AES-128 encryption
|
||||
//! - Decode-only (no encoder)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 200;
|
||||
const TE_LONG: u32 = 400;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 144;
|
||||
const PREAMBLE_COUNT: u16 = 601;
|
||||
|
||||
const XOR_MASK_LOW: u32 = 0x84AF25FB;
|
||||
const XOR_MASK_HIGH: u32 = 0x638766AB;
|
||||
|
||||
/// AES S-box
|
||||
const AES_SBOX: [u8; 256] = [
|
||||
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
|
||||
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
|
||||
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
|
||||
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
|
||||
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
|
||||
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
|
||||
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
|
||||
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
|
||||
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
|
||||
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
|
||||
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
|
||||
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
|
||||
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
|
||||
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
|
||||
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16,
|
||||
];
|
||||
|
||||
/// AES inverse S-box
|
||||
const AES_SBOX_INV: [u8; 256] = [
|
||||
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
|
||||
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
|
||||
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
|
||||
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
|
||||
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
|
||||
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
|
||||
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
|
||||
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
|
||||
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
|
||||
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
|
||||
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
|
||||
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
|
||||
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
|
||||
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
|
||||
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
|
||||
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d,
|
||||
];
|
||||
|
||||
const AES_RCON: [u8; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
|
||||
|
||||
/// Manchester states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
Mid1,
|
||||
Start0,
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
WaitFirstHigh,
|
||||
WaitLongHigh,
|
||||
Data,
|
||||
}
|
||||
|
||||
/// Kia V6 protocol decoder
|
||||
pub struct KiaV6Decoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
header_count: u16,
|
||||
manchester_state: ManchesterState,
|
||||
|
||||
data_part1_low: u32,
|
||||
data_part1_high: u32,
|
||||
stored_part1_low: u32,
|
||||
stored_part1_high: u32,
|
||||
stored_part2_low: u32,
|
||||
stored_part2_high: u32,
|
||||
data_part3: u16,
|
||||
|
||||
bit_count: u8,
|
||||
}
|
||||
|
||||
impl KiaV6Decoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
header_count: 0,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
data_part1_low: 0,
|
||||
data_part1_high: 0,
|
||||
stored_part1_low: 0,
|
||||
stored_part1_high: 0,
|
||||
stored_part2_low: 0,
|
||||
stored_part2_high: 0,
|
||||
data_part3: 0,
|
||||
bit_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get keystore A (placeholder)
|
||||
fn get_keystore_a() -> u64 {
|
||||
0x0000000000000000
|
||||
}
|
||||
|
||||
/// Get keystore B (placeholder)
|
||||
fn get_keystore_b() -> u64 {
|
||||
0x0000000000000000
|
||||
}
|
||||
|
||||
/// CRC8 calculation
|
||||
fn crc8(data: &[u8], init: u8, polynomial: u8) -> u8 {
|
||||
let mut crc = init;
|
||||
for &byte in data {
|
||||
crc ^= byte;
|
||||
for _ in 0..8 {
|
||||
let b = crc << 1;
|
||||
if (crc & 0x80) != 0 {
|
||||
crc = b ^ polynomial;
|
||||
} else {
|
||||
crc = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
/// GF(2^8) multiply by 2
|
||||
fn gf_mul2(x: u8) -> u8 {
|
||||
((x >> 7).wrapping_mul(0x1b)) ^ (x << 1)
|
||||
}
|
||||
|
||||
/// AES inverse SubBytes
|
||||
fn aes_subbytes_inv(state: &mut [u8; 16]) {
|
||||
for i in 0..16 {
|
||||
state[i] = AES_SBOX_INV[state[i] as usize];
|
||||
}
|
||||
}
|
||||
|
||||
/// AES inverse ShiftRows
|
||||
fn aes_shiftrows_inv(state: &mut [u8; 16]) {
|
||||
let temp = state[13];
|
||||
state[13] = state[9];
|
||||
state[9] = state[5];
|
||||
state[5] = state[1];
|
||||
state[1] = temp;
|
||||
|
||||
let temp = state[2];
|
||||
state[2] = state[10];
|
||||
state[10] = temp;
|
||||
let temp = state[6];
|
||||
state[6] = state[14];
|
||||
state[14] = temp;
|
||||
|
||||
let temp = state[3];
|
||||
state[3] = state[7];
|
||||
state[7] = state[11];
|
||||
state[11] = state[15];
|
||||
state[15] = temp;
|
||||
}
|
||||
|
||||
/// AES inverse MixColumns
|
||||
fn aes_mixcolumns_inv(state: &mut [u8; 16]) {
|
||||
for i in 0..4 {
|
||||
let a = state[i * 4];
|
||||
let b = state[i * 4 + 1];
|
||||
let c = state[i * 4 + 2];
|
||||
let d = state[i * 4 + 3];
|
||||
|
||||
let a2 = Self::gf_mul2(a);
|
||||
let a4 = Self::gf_mul2(a2);
|
||||
let a8 = Self::gf_mul2(a4);
|
||||
let b2 = Self::gf_mul2(b);
|
||||
let b4 = Self::gf_mul2(b2);
|
||||
let b8 = Self::gf_mul2(b4);
|
||||
let c2 = Self::gf_mul2(c);
|
||||
let c4 = Self::gf_mul2(c2);
|
||||
let c8 = Self::gf_mul2(c4);
|
||||
let d2 = Self::gf_mul2(d);
|
||||
let d4 = Self::gf_mul2(d2);
|
||||
let d8 = Self::gf_mul2(d4);
|
||||
|
||||
state[i * 4] = (a8 ^ a4 ^ a2) ^ (b8 ^ b2 ^ b) ^ (c8 ^ c4 ^ c) ^ (d8 ^ d);
|
||||
state[i * 4 + 1] = (a8 ^ a) ^ (b8 ^ b4 ^ b2) ^ (c8 ^ c2 ^ c) ^ (d8 ^ d4 ^ d);
|
||||
state[i * 4 + 2] = (a8 ^ a4 ^ a) ^ (b8 ^ b) ^ (c8 ^ c4 ^ c2) ^ (d8 ^ d2 ^ d);
|
||||
state[i * 4 + 3] = (a8 ^ a2 ^ a) ^ (b8 ^ b4 ^ b) ^ (c8 ^ c) ^ (d8 ^ d4 ^ d2);
|
||||
}
|
||||
}
|
||||
|
||||
/// AES AddRoundKey
|
||||
fn aes_addroundkey(state: &mut [u8; 16], round_key: &[u8]) {
|
||||
for i in 0..16 {
|
||||
state[i] ^= round_key[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// AES key expansion
|
||||
fn aes_key_expansion(key: &[u8; 16]) -> [u8; 176] {
|
||||
let mut round_keys = [0u8; 176];
|
||||
round_keys[..16].copy_from_slice(key);
|
||||
|
||||
for i in 4..44 {
|
||||
let prev_word_idx = (i - 1) * 4;
|
||||
let mut b0 = round_keys[prev_word_idx];
|
||||
let mut b1 = round_keys[prev_word_idx + 1];
|
||||
let mut b2 = round_keys[prev_word_idx + 2];
|
||||
let mut b3 = round_keys[prev_word_idx + 3];
|
||||
|
||||
if (i % 4) == 0 {
|
||||
let new_b0 = AES_SBOX[b1 as usize] ^ AES_RCON[(i / 4) - 1];
|
||||
let new_b1 = AES_SBOX[b2 as usize];
|
||||
let new_b2 = AES_SBOX[b3 as usize];
|
||||
let new_b3 = AES_SBOX[b0 as usize];
|
||||
b0 = new_b0;
|
||||
b1 = new_b1;
|
||||
b2 = new_b2;
|
||||
b3 = new_b3;
|
||||
}
|
||||
|
||||
let back_word_idx = (i - 4) * 4;
|
||||
b0 ^= round_keys[back_word_idx];
|
||||
b1 ^= round_keys[back_word_idx + 1];
|
||||
b2 ^= round_keys[back_word_idx + 2];
|
||||
b3 ^= round_keys[back_word_idx + 3];
|
||||
|
||||
let curr_word_idx = i * 4;
|
||||
round_keys[curr_word_idx] = b0;
|
||||
round_keys[curr_word_idx + 1] = b1;
|
||||
round_keys[curr_word_idx + 2] = b2;
|
||||
round_keys[curr_word_idx + 3] = b3;
|
||||
}
|
||||
|
||||
round_keys
|
||||
}
|
||||
|
||||
/// AES-128 decrypt
|
||||
fn aes128_decrypt(expanded_key: &[u8; 176], data: &mut [u8; 16]) {
|
||||
let mut state = *data;
|
||||
|
||||
Self::aes_addroundkey(&mut state, &expanded_key[160..176]);
|
||||
|
||||
for round in (1..10).rev() {
|
||||
Self::aes_shiftrows_inv(&mut state);
|
||||
Self::aes_subbytes_inv(&mut state);
|
||||
Self::aes_addroundkey(&mut state, &expanded_key[round * 16..(round + 1) * 16]);
|
||||
Self::aes_mixcolumns_inv(&mut state);
|
||||
}
|
||||
|
||||
Self::aes_shiftrows_inv(&mut state);
|
||||
Self::aes_subbytes_inv(&mut state);
|
||||
Self::aes_addroundkey(&mut state, &expanded_key[0..16]);
|
||||
|
||||
*data = state;
|
||||
}
|
||||
|
||||
/// Get AES key from keystores
|
||||
fn get_aes_key() -> [u8; 16] {
|
||||
let keystore_a = Self::get_keystore_a();
|
||||
let keystore_a_hi = ((keystore_a >> 32) & 0xFFFFFFFF) as u32;
|
||||
let keystore_a_lo = (keystore_a & 0xFFFFFFFF) as u32;
|
||||
|
||||
let u_var15_a = keystore_a_lo ^ XOR_MASK_LOW;
|
||||
let u_var5_a = XOR_MASK_HIGH ^ keystore_a_hi;
|
||||
|
||||
let val64_a = ((u_var5_a as u64) << 32) | (u_var15_a as u64);
|
||||
|
||||
let keystore_b = Self::get_keystore_b();
|
||||
let keystore_b_hi = ((keystore_b >> 32) & 0xFFFFFFFF) as u32;
|
||||
let keystore_b_lo = (keystore_b & 0xFFFFFFFF) as u32;
|
||||
|
||||
let u_var15_b = keystore_b_lo ^ XOR_MASK_LOW;
|
||||
let u_var5_b = XOR_MASK_HIGH ^ keystore_b_hi;
|
||||
|
||||
let val64_b = ((u_var5_b as u64) << 32) | (u_var15_b as u64);
|
||||
|
||||
let mut aes_key = [0u8; 16];
|
||||
for i in 0..8 {
|
||||
aes_key[i] = ((val64_a >> (56 - i * 8)) & 0xFF) as u8;
|
||||
}
|
||||
for i in 0..8 {
|
||||
aes_key[i + 8] = ((val64_b >> (56 - i * 8)) & 0xFF) as u8;
|
||||
}
|
||||
|
||||
aes_key
|
||||
}
|
||||
|
||||
/// Decrypt the stored data
|
||||
fn decrypt(&self) -> Option<(u32, u8, u32, bool)> {
|
||||
let mut encrypted_data = [0u8; 16];
|
||||
|
||||
encrypted_data[0] = ((self.stored_part1_high >> 8) & 0xFF) as u8;
|
||||
encrypted_data[1] = (self.stored_part1_high & 0xFF) as u8;
|
||||
encrypted_data[2] = ((self.stored_part1_low >> 24) & 0xFF) as u8;
|
||||
encrypted_data[3] = ((self.stored_part1_low >> 16) & 0xFF) as u8;
|
||||
encrypted_data[4] = ((self.stored_part1_low >> 8) & 0xFF) as u8;
|
||||
encrypted_data[5] = (self.stored_part1_low & 0xFF) as u8;
|
||||
encrypted_data[6] = ((self.stored_part2_high >> 24) & 0xFF) as u8;
|
||||
encrypted_data[7] = ((self.stored_part2_high >> 16) & 0xFF) as u8;
|
||||
encrypted_data[8] = ((self.stored_part2_high >> 8) & 0xFF) as u8;
|
||||
encrypted_data[9] = (self.stored_part2_high & 0xFF) as u8;
|
||||
encrypted_data[10] = ((self.stored_part2_low >> 24) & 0xFF) as u8;
|
||||
encrypted_data[11] = ((self.stored_part2_low >> 16) & 0xFF) as u8;
|
||||
encrypted_data[12] = ((self.stored_part2_low >> 8) & 0xFF) as u8;
|
||||
encrypted_data[13] = (self.stored_part2_low & 0xFF) as u8;
|
||||
encrypted_data[14] = ((self.data_part3 >> 8) & 0xFF) as u8;
|
||||
encrypted_data[15] = (self.data_part3 & 0xFF) as u8;
|
||||
|
||||
let aes_key = Self::get_aes_key();
|
||||
let expanded_key = Self::aes_key_expansion(&aes_key);
|
||||
|
||||
Self::aes128_decrypt(&expanded_key, &mut encrypted_data);
|
||||
|
||||
let decrypted = &encrypted_data;
|
||||
let calculated_crc = Self::crc8(&decrypted[..15], 0xFF, 0x07);
|
||||
let stored_crc = decrypted[15];
|
||||
let crc_valid = (calculated_crc ^ stored_crc) < 2;
|
||||
|
||||
// Serial: bytes 4-6 as 24-bit big-endian
|
||||
let serial = ((decrypted[4] as u32) << 16) | ((decrypted[5] as u32) << 8) | (decrypted[6] as u32);
|
||||
let button = decrypted[7];
|
||||
let counter = ((decrypted[8] as u32) << 24) |
|
||||
((decrypted[9] as u32) << 16) |
|
||||
((decrypted[10] as u32) << 8) |
|
||||
(decrypted[11] as u32);
|
||||
|
||||
Some((serial, button, counter, crc_valid))
|
||||
}
|
||||
|
||||
/// Manchester state machine
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, true) => 0, // Short High
|
||||
(true, false) => 2, // Short Low
|
||||
(false, true) => 6, // Long High
|
||||
(false, false) => 4, // Long Low
|
||||
};
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 2) | (ManchesterState::Mid1, 2) =>
|
||||
(ManchesterState::Start0, None),
|
||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
|
||||
(ManchesterState::Start1, None),
|
||||
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Mid1, Some(true)),
|
||||
(ManchesterState::Start1, 4) => (ManchesterState::Start0, Some(true)),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
|
||||
(ManchesterState::Start0, 6) => (ManchesterState::Start1, Some(false)),
|
||||
|
||||
_ => (ManchesterState::Mid1, None),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
output
|
||||
}
|
||||
|
||||
/// Add initial sync bits
|
||||
fn add_sync_bits(&mut self) {
|
||||
// Add 1, 1, 0, 1 as initial bits
|
||||
for bit in [true, true, false, true] {
|
||||
let carry = self.data_part1_low >> 31;
|
||||
self.data_part1_low = (self.data_part1_low << 1) | (bit as u32);
|
||||
self.data_part1_high = (self.data_part1_high << 1) | carry;
|
||||
self.bit_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for KiaV6Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Kia V6"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.header_count = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.data_part1_low = 0;
|
||||
self.data_part1_high = 0;
|
||||
self.stored_part1_low = 0;
|
||||
self.stored_part1_high = 0;
|
||||
self.stored_part2_low = 0;
|
||||
self.stored_part2_high = 0;
|
||||
self.data_part3 = 0;
|
||||
self.bit_count = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && is_short {
|
||||
self.step = DecoderStep::WaitFirstHigh;
|
||||
self.te_last = duration;
|
||||
self.header_count = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::WaitFirstHigh => {
|
||||
if level {
|
||||
return None;
|
||||
}
|
||||
|
||||
let diff_short = duration_diff!(duration, TE_SHORT);
|
||||
let diff_long = duration_diff!(duration, TE_LONG);
|
||||
|
||||
if diff_long < TE_DELTA && diff_long < diff_short {
|
||||
if self.header_count >= PREAMBLE_COUNT {
|
||||
self.header_count = 0;
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::WaitLongHigh;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if diff_short >= TE_DELTA && diff_long >= TE_DELTA {
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA {
|
||||
self.te_last = duration;
|
||||
self.header_count += 1;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::WaitLongHigh => {
|
||||
if !level {
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
let diff_long = duration_diff!(duration, TE_LONG);
|
||||
let diff_short = duration_diff!(duration, TE_SHORT);
|
||||
|
||||
if diff_long >= TE_DELTA && diff_short >= TE_DELTA {
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
if duration_diff!(self.te_last, TE_LONG) >= TE_DELTA {
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
self.data_part1_low = 0;
|
||||
self.data_part1_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.add_sync_bits();
|
||||
self.step = DecoderStep::Data;
|
||||
}
|
||||
|
||||
DecoderStep::Data => {
|
||||
if !is_short && !is_long {
|
||||
self.step = DecoderStep::Reset;
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(bit) = self.manchester_advance(is_short, level) {
|
||||
let carry = self.data_part1_low >> 31;
|
||||
self.data_part1_low = (self.data_part1_low << 1) | (bit as u32);
|
||||
self.data_part1_high = (self.data_part1_high << 1) | carry;
|
||||
self.bit_count += 1;
|
||||
|
||||
if self.bit_count == 64 {
|
||||
self.stored_part1_low = !self.data_part1_low;
|
||||
self.stored_part1_high = !self.data_part1_high;
|
||||
self.data_part1_low = 0;
|
||||
self.data_part1_high = 0;
|
||||
} else if self.bit_count == 128 {
|
||||
self.stored_part2_low = !self.data_part1_low;
|
||||
self.stored_part2_high = !self.data_part1_high;
|
||||
self.data_part1_low = 0;
|
||||
self.data_part1_high = 0;
|
||||
}
|
||||
}
|
||||
|
||||
self.te_last = duration;
|
||||
|
||||
if self.bit_count as usize == MIN_COUNT_BIT {
|
||||
self.data_part3 = !(self.data_part1_low as u16);
|
||||
|
||||
if let Some((serial, button, counter, crc_valid)) = self.decrypt() {
|
||||
let key_data = ((self.stored_part1_high as u64) << 32) |
|
||||
(self.stored_part1_low as u64);
|
||||
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some((counter & 0xFFFF) as u16), // V6 has 32-bit counter but we only store 16
|
||||
crc_valid,
|
||||
data: key_data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: false,
|
||||
});
|
||||
}
|
||||
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false // V6 is decode-only
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//! Protocol decoders and encoders for various keyfob systems.
|
||||
//!
|
||||
//! This module implements decoders for various keyfob protocols, ported from
|
||||
//! protopirate. Each protocol processes level+duration pairs from the demodulator.
|
||||
|
||||
mod common;
|
||||
pub mod keeloq_common;
|
||||
#[allow(dead_code)]
|
||||
pub mod aut64;
|
||||
#[allow(dead_code)]
|
||||
pub mod keys;
|
||||
mod kia_v0;
|
||||
mod kia_v1;
|
||||
mod kia_v2;
|
||||
mod kia_v3_v4;
|
||||
mod kia_v5;
|
||||
mod kia_v6;
|
||||
mod subaru;
|
||||
mod ford_v0;
|
||||
mod vag;
|
||||
mod fiat_v0;
|
||||
mod suzuki;
|
||||
mod scher_khan;
|
||||
mod star_line;
|
||||
mod psa;
|
||||
|
||||
pub use common::DecodedSignal;
|
||||
|
||||
use crate::capture::Capture;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
/// Protocol timing constants
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ProtocolTiming {
|
||||
/// Short pulse duration in µs
|
||||
pub te_short: u32,
|
||||
/// Long pulse duration in µs
|
||||
pub te_long: u32,
|
||||
/// Tolerance for timing matching in µs
|
||||
pub te_delta: u32,
|
||||
/// Minimum bit count for valid decode
|
||||
pub min_count_bit: usize,
|
||||
}
|
||||
|
||||
/// Trait for protocol decoders
|
||||
///
|
||||
/// Each protocol implements a state machine that processes level+duration pairs.
|
||||
pub trait ProtocolDecoder: Send + Sync {
|
||||
/// Get the protocol name
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Get timing constants
|
||||
#[allow(dead_code)]
|
||||
fn timing(&self) -> ProtocolTiming;
|
||||
|
||||
/// Get supported frequencies in Hz
|
||||
fn supported_frequencies(&self) -> &[u32];
|
||||
|
||||
/// Reset the decoder state machine
|
||||
fn reset(&mut self);
|
||||
|
||||
/// Feed a level+duration pair to the decoder
|
||||
/// Returns Some(DecodedSignal) when a complete valid signal is decoded
|
||||
fn feed(&mut self, level: bool, duration_us: u32) -> Option<DecodedSignal>;
|
||||
|
||||
/// Check if this protocol supports encoding
|
||||
fn supports_encoding(&self) -> bool;
|
||||
|
||||
/// Encode a signal with the given button command
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>>;
|
||||
}
|
||||
|
||||
/// Registry of all supported protocols
|
||||
pub struct ProtocolRegistry {
|
||||
decoders: Vec<Box<dyn ProtocolDecoder>>,
|
||||
}
|
||||
|
||||
impl ProtocolRegistry {
|
||||
/// Create a new protocol registry with all built-in protocols
|
||||
pub fn new() -> Self {
|
||||
let decoders: Vec<Box<dyn ProtocolDecoder>> = vec![
|
||||
// Kia protocols
|
||||
Box::new(kia_v0::KiaV0Decoder::new()),
|
||||
Box::new(kia_v1::KiaV1Decoder::new()),
|
||||
Box::new(kia_v2::KiaV2Decoder::new()),
|
||||
Box::new(kia_v3_v4::KiaV3V4Decoder::new()),
|
||||
Box::new(kia_v5::KiaV5Decoder::new()),
|
||||
Box::new(kia_v6::KiaV6Decoder::new()),
|
||||
// Other protocols
|
||||
Box::new(subaru::SubaruDecoder::new()),
|
||||
Box::new(ford_v0::FordV0Decoder::new()),
|
||||
Box::new(vag::VagDecoder::new()),
|
||||
Box::new(fiat_v0::FiatV0Decoder::new()),
|
||||
Box::new(suzuki::SuzukiDecoder::new()),
|
||||
Box::new(scher_khan::ScherKhanDecoder::new()),
|
||||
Box::new(star_line::StarLineDecoder::new()),
|
||||
Box::new(psa::PsaDecoder::new()),
|
||||
];
|
||||
|
||||
Self { decoders }
|
||||
}
|
||||
|
||||
/// Process level+duration pairs from demodulator
|
||||
/// Returns decoded signal info if any protocol matches
|
||||
pub fn process_signal(&mut self, pairs: &[LevelDuration], frequency: u32) -> Option<(String, DecodedSignal)> {
|
||||
// Reset all decoders
|
||||
for decoder in &mut self.decoders {
|
||||
decoder.reset();
|
||||
}
|
||||
|
||||
// Feed pairs to all decoders that support this frequency
|
||||
for pair in pairs {
|
||||
for decoder in &mut self.decoders {
|
||||
// Check frequency support
|
||||
let freq_supported = decoder
|
||||
.supported_frequencies()
|
||||
.iter()
|
||||
.any(|&f| {
|
||||
let diff = if f > frequency { f - frequency } else { frequency - f };
|
||||
diff < (f / 50) // 2% tolerance
|
||||
});
|
||||
|
||||
if !freq_supported {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(decoded) = decoder.feed(pair.level, pair.duration_us) {
|
||||
return Some((decoder.name().to_string(), decoded));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Try to decode a capture (for compatibility with old interface)
|
||||
#[allow(dead_code)]
|
||||
pub fn try_decode(&mut self, capture: &Capture) -> Option<(String, DecodedSignal)> {
|
||||
// Convert raw pairs to LevelDuration and process
|
||||
if capture.raw_pairs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let pairs: Vec<LevelDuration> = capture.raw_pairs
|
||||
.iter()
|
||||
.map(|p| LevelDuration::new(p.level, p.duration_us))
|
||||
.collect();
|
||||
|
||||
self.process_signal(&pairs, capture.frequency)
|
||||
}
|
||||
|
||||
/// Get a decoder by name
|
||||
pub fn get(&self, name: &str) -> Option<&dyn ProtocolDecoder> {
|
||||
self.decoders
|
||||
.iter()
|
||||
.find(|d| d.name().eq_ignore_ascii_case(name))
|
||||
.map(|d| d.as_ref())
|
||||
}
|
||||
|
||||
/// List all protocol names
|
||||
#[allow(dead_code)]
|
||||
pub fn list_protocols(&self) -> Vec<&'static str> {
|
||||
self.decoders.iter().map(|d| d.name()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProtocolRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper macro for duration comparison (matches protopirate's DURATION_DIFF)
|
||||
#[macro_export]
|
||||
macro_rules! duration_diff {
|
||||
($actual:expr, $expected:expr) => {
|
||||
if $actual > $expected {
|
||||
$actual - $expected
|
||||
} else {
|
||||
$expected - $actual
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
//! PSA (Peugeot/Citroen) protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's psa.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 125/250µs bit timing, 250/500µs symbol timing
|
||||
//! - 128 bits total (key1: 64 bits, key2: 16 bits, validation: 48 bits)
|
||||
//! - TEA and XOR encryption schemes
|
||||
//! - Two modes: 0x23 and 0x36
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 128;
|
||||
|
||||
// Internal timing for Manchester sub-symbol detection
|
||||
const TE_SHORT_125: u32 = 125;
|
||||
const TE_LONG_250: u32 = 250;
|
||||
const TE_TOLERANCE_49: u32 = 49;
|
||||
const TE_TOLERANCE_50: u32 = 50;
|
||||
const TE_TOLERANCE_99: u32 = 99;
|
||||
const TE_END_1000: u32 = 1000;
|
||||
|
||||
// TEA constants
|
||||
const TEA_DELTA: u32 = 0x9E3779B9;
|
||||
const TEA_ROUNDS: u32 = 32;
|
||||
|
||||
// Brute-force constants for mode 0x23
|
||||
const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A64];
|
||||
|
||||
// Brute-force constants for mode 0x36
|
||||
const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04];
|
||||
|
||||
/// Manchester decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
Mid1,
|
||||
Start0,
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderState {
|
||||
/// Waiting for first edge
|
||||
WaitEdge,
|
||||
/// Counting preamble pattern
|
||||
CountPattern,
|
||||
/// Decoding Manchester data
|
||||
DecodeManchester,
|
||||
/// Found end of data
|
||||
End,
|
||||
}
|
||||
|
||||
/// PSA protocol decoder
|
||||
pub struct PsaDecoder {
|
||||
state: DecoderState,
|
||||
prev_duration: u32,
|
||||
manchester_state: ManchesterState,
|
||||
pattern_counter: u16,
|
||||
data_low: u32,
|
||||
data_high: u32,
|
||||
bit_count: u8,
|
||||
// Decoded fields
|
||||
key1_low: u32,
|
||||
key1_high: u32,
|
||||
validation_field: u16,
|
||||
key2_low: u32,
|
||||
key2_high: u32,
|
||||
seed: u32,
|
||||
}
|
||||
|
||||
impl PsaDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: DecoderState::WaitEdge,
|
||||
prev_duration: 0,
|
||||
manchester_state: ManchesterState::Mid1,
|
||||
pattern_counter: 0,
|
||||
data_low: 0,
|
||||
data_high: 0,
|
||||
bit_count: 0,
|
||||
key1_low: 0,
|
||||
key1_high: 0,
|
||||
validation_field: 0,
|
||||
key2_low: 0,
|
||||
key2_high: 0,
|
||||
seed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Manchester advance
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, true) => 0,
|
||||
(true, false) => 1,
|
||||
(false, true) => 2,
|
||||
(false, false) => 3,
|
||||
};
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => {
|
||||
(ManchesterState::Start1, None)
|
||||
}
|
||||
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => {
|
||||
(ManchesterState::Start0, None)
|
||||
}
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
|
||||
_ => (ManchesterState::Mid1, None),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
output
|
||||
}
|
||||
|
||||
fn add_bit(&mut self, bit: bool) {
|
||||
let new_bit = if bit { 1u32 } else { 0u32 };
|
||||
let carry = (self.data_low >> 31) & 1;
|
||||
self.data_low = (self.data_low << 1) | new_bit;
|
||||
self.data_high = (self.data_high << 1) | carry;
|
||||
self.bit_count += 1;
|
||||
|
||||
// Extract key1 at 64 bits
|
||||
if self.bit_count == 64 {
|
||||
self.key1_low = self.data_low;
|
||||
self.key1_high = self.data_high;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
}
|
||||
// Extract validation at 80 bits (16 more)
|
||||
else if self.bit_count == 80 {
|
||||
self.validation_field = self.data_low as u16;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA decrypt
|
||||
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
*v1 = v1.wrapping_sub(
|
||||
(v0.wrapping_shl(4).wrapping_add(key[2]))
|
||||
^ (v0.wrapping_add(sum))
|
||||
^ (v0.wrapping_shr(5).wrapping_add(key[3])),
|
||||
);
|
||||
*v0 = v0.wrapping_sub(
|
||||
(v1.wrapping_shl(4).wrapping_add(key[0]))
|
||||
^ (v1.wrapping_add(sum))
|
||||
^ (v1.wrapping_shr(5).wrapping_add(key[1])),
|
||||
);
|
||||
sum = sum.wrapping_sub(TEA_DELTA);
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA encrypt
|
||||
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let mut sum: u32 = 0;
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
sum = sum.wrapping_add(TEA_DELTA);
|
||||
*v0 = v0.wrapping_add(
|
||||
(v1.wrapping_shl(4).wrapping_add(key[0]))
|
||||
^ (v1.wrapping_add(sum))
|
||||
^ (v1.wrapping_shr(5).wrapping_add(key[1])),
|
||||
);
|
||||
*v1 = v1.wrapping_add(
|
||||
(v0.wrapping_shl(4).wrapping_add(key[2]))
|
||||
^ (v0.wrapping_add(sum))
|
||||
^ (v0.wrapping_shr(5).wrapping_add(key[3])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// XOR decrypt (mode 0x23)
|
||||
fn xor_decrypt(buffer: &mut [u8]) {
|
||||
let e6 = buffer[8];
|
||||
let e7 = buffer[9];
|
||||
let e5 = buffer[7];
|
||||
let e0 = buffer[2];
|
||||
let e1 = buffer[3];
|
||||
let e2 = buffer[4];
|
||||
let e3 = buffer[5];
|
||||
let e4 = buffer[6];
|
||||
|
||||
buffer[2] = e0 ^ e5;
|
||||
buffer[3] = e1 ^ (e0 ^ e5 ^ e6 ^ e7);
|
||||
buffer[4] = e2 ^ e0;
|
||||
buffer[5] = e3 ^ (e0 ^ e5 ^ e6 ^ e7);
|
||||
buffer[6] = e4 ^ e2;
|
||||
buffer[7] = e5 ^ e6 ^ e7;
|
||||
}
|
||||
|
||||
fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> {
|
||||
// Try mode 0x23 first
|
||||
let seed_byte = (self.key1_high >> 24) as u8;
|
||||
|
||||
if seed_byte >= 0x23 && seed_byte < 0x24 {
|
||||
// Mode 0x23 - TEA + XOR
|
||||
let mut v0 = self.key1_high;
|
||||
let mut v1 = self.key1_low;
|
||||
Self::tea_decrypt(&mut v0, &mut v1, &BF1_KEY_SCHEDULE);
|
||||
|
||||
let mut buffer = [0u8; 10];
|
||||
buffer[0] = (v0 >> 24) as u8;
|
||||
buffer[1] = (v0 >> 16) as u8;
|
||||
buffer[2] = (v0 >> 8) as u8;
|
||||
buffer[3] = (v0 >> 0) as u8;
|
||||
buffer[4] = (v1 >> 24) as u8;
|
||||
buffer[5] = (v1 >> 16) as u8;
|
||||
buffer[6] = (v1 >> 8) as u8;
|
||||
buffer[7] = (v1 >> 0) as u8;
|
||||
buffer[8] = (self.validation_field >> 8) as u8;
|
||||
buffer[9] = (self.validation_field & 0xFF) as u8;
|
||||
|
||||
Self::xor_decrypt(&mut buffer);
|
||||
|
||||
let serial = ((buffer[2] as u32) << 16)
|
||||
| ((buffer[3] as u32) << 8)
|
||||
| (buffer[4] as u32);
|
||||
let counter = ((buffer[5] as u32) << 8) | (buffer[6] as u32);
|
||||
let crc = buffer[7] as u16;
|
||||
let btn = buffer[8] & 0x0F;
|
||||
|
||||
return Some((serial, btn, counter, crc, 0x23));
|
||||
}
|
||||
|
||||
if seed_byte >= 0xF3 && seed_byte < 0xF4 {
|
||||
// Mode 0x36 - TEA + different key schedule
|
||||
let mut v0 = self.key1_high;
|
||||
let mut v1 = self.key1_low;
|
||||
Self::tea_decrypt(&mut v0, &mut v1, &BF2_KEY_SCHEDULE);
|
||||
|
||||
let serial = ((v0 >> 8) & 0xFFFF00) | ((v0 & 0xFF) as u32);
|
||||
let counter = v1 >> 16;
|
||||
let btn = ((v1 >> 8) & 0xF) as u8;
|
||||
let crc = (v1 & 0xFF) as u16;
|
||||
|
||||
return Some((serial, btn, counter, crc, 0x36));
|
||||
}
|
||||
|
||||
// Cannot decrypt - return raw data
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
// Combine into 128-bit data (store lower 64)
|
||||
let data = ((self.key1_high as u64) << 32) | (self.key1_low as u64);
|
||||
|
||||
if let Some((serial, btn, counter, _crc, _mode)) = self.try_decrypt() {
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: Some(counter as u16),
|
||||
crc_valid: true,
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
}
|
||||
} else {
|
||||
DecodedSignal {
|
||||
serial: None,
|
||||
button: None,
|
||||
counter: None,
|
||||
crc_valid: false,
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for PsaDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"PSA"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
self.prev_duration = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.pattern_counter = 0;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.key1_low = 0;
|
||||
self.key1_high = 0;
|
||||
self.validation_field = 0;
|
||||
self.key2_low = 0;
|
||||
self.key2_high = 0;
|
||||
self.seed = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.state {
|
||||
DecoderState::WaitEdge => {
|
||||
if level && duration_diff!(duration, TE_SHORT_125) < TE_TOLERANCE_49 {
|
||||
self.state = DecoderState::CountPattern;
|
||||
self.prev_duration = duration;
|
||||
self.pattern_counter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderState::CountPattern => {
|
||||
let diff_125 = duration_diff!(duration, TE_SHORT_125);
|
||||
let diff_250 = duration_diff!(duration, TE_LONG_250);
|
||||
|
||||
if diff_125 < TE_TOLERANCE_50 {
|
||||
self.pattern_counter += 1;
|
||||
self.prev_duration = duration;
|
||||
} else if diff_250 < TE_TOLERANCE_99 && self.pattern_counter >= 0x46 {
|
||||
// Found end of preamble, start Manchester decoding
|
||||
self.state = DecoderState::DecodeManchester;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
self.prev_duration = duration;
|
||||
} else if self.pattern_counter < 2 {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
} else {
|
||||
self.prev_duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderState::DecodeManchester => {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
let is_end = duration > TE_END_1000;
|
||||
|
||||
if is_end || self.bit_count >= 121 {
|
||||
// End of data
|
||||
self.state = DecoderState::End;
|
||||
|
||||
if self.bit_count >= 96 {
|
||||
// Got enough data
|
||||
let result = self.parse_data();
|
||||
self.state = DecoderState::WaitEdge;
|
||||
return Some(result);
|
||||
}
|
||||
self.state = DecoderState::WaitEdge;
|
||||
return None;
|
||||
}
|
||||
|
||||
if is_short || is_long {
|
||||
if let Some(bit) = self.manchester_advance(is_short, level) {
|
||||
self.add_bit(bit);
|
||||
}
|
||||
} else {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
}
|
||||
|
||||
self.prev_duration = duration;
|
||||
}
|
||||
|
||||
DecoderState::End => {
|
||||
self.state = DecoderState::WaitEdge;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0).wrapping_add(1) as u32;
|
||||
|
||||
// Build plaintext buffer for mode 0x23
|
||||
let mut buffer = [0u8; 10];
|
||||
buffer[0] = 0x23;
|
||||
buffer[1] = 0x00;
|
||||
buffer[2] = (serial >> 16) as u8;
|
||||
buffer[3] = (serial >> 8) as u8;
|
||||
buffer[4] = serial as u8;
|
||||
buffer[5] = (counter >> 8) as u8;
|
||||
buffer[6] = counter as u8;
|
||||
buffer[7] = 0; // CRC placeholder
|
||||
buffer[8] = button & 0x0F;
|
||||
buffer[9] = 0;
|
||||
|
||||
// XOR encrypt
|
||||
{
|
||||
let e6 = buffer[8];
|
||||
let e7 = buffer[9];
|
||||
let p0 = buffer[2];
|
||||
let p1 = buffer[3];
|
||||
let p2 = buffer[4];
|
||||
let p3 = buffer[5];
|
||||
let p4 = buffer[6];
|
||||
let p5 = buffer[7];
|
||||
|
||||
let ne5 = p5 ^ e7 ^ e6;
|
||||
let ne0 = p2 ^ ne5;
|
||||
let ne2 = p4 ^ ne0;
|
||||
let ne4 = p3 ^ ne2;
|
||||
let ne3 = p0 ^ ne5;
|
||||
let ne1 = p1 ^ ne3;
|
||||
|
||||
buffer[2] = ne0;
|
||||
buffer[3] = ne1;
|
||||
buffer[4] = ne2;
|
||||
buffer[5] = ne3;
|
||||
buffer[6] = ne4;
|
||||
buffer[7] = ne5;
|
||||
}
|
||||
|
||||
// TEA encrypt
|
||||
let mut v0 = ((buffer[0] as u32) << 24)
|
||||
| ((buffer[1] as u32) << 16)
|
||||
| ((buffer[2] as u32) << 8)
|
||||
| (buffer[3] as u32);
|
||||
let mut v1 = ((buffer[4] as u32) << 24)
|
||||
| ((buffer[5] as u32) << 16)
|
||||
| ((buffer[6] as u32) << 8)
|
||||
| (buffer[7] as u32);
|
||||
|
||||
Self::tea_encrypt(&mut v0, &mut v1, &BF1_KEY_SCHEDULE);
|
||||
|
||||
let key1_high = v0;
|
||||
let key1_low = v1;
|
||||
let validation = ((buffer[8] as u16) << 8) | (buffer[9] as u16);
|
||||
|
||||
// Build signal
|
||||
let mut signal = Vec::with_capacity(512);
|
||||
|
||||
// Preamble: alternating 125µs pulses
|
||||
for _ in 0..70 {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT_125));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT_125));
|
||||
}
|
||||
|
||||
// Sync
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
|
||||
// Key1: 64 bits Manchester encoded
|
||||
let key1 = ((key1_high as u64) << 32) | (key1_low as u64);
|
||||
for bit in (0..64).rev() {
|
||||
if (key1 >> bit) & 1 == 1 {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
}
|
||||
|
||||
// Validation: 16 bits Manchester encoded
|
||||
for bit in (0..16).rev() {
|
||||
if (validation >> bit) & 1 == 1 {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
}
|
||||
|
||||
// End marker
|
||||
signal.push(LevelDuration::new(false, TE_END_1000));
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
//! Scher-Khan protocol decoder
|
||||
//!
|
||||
//! Ported from protopirate's scher_khan.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 750/1100µs timing
|
||||
//! - Variable bit count (35, 51, 57, 63, 64, 81, 82)
|
||||
//! - Decode-only (no encoder)
|
||||
//!
|
||||
//! References:
|
||||
//! - https://phreakerclub.com/72
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 750;
|
||||
const TE_LONG: u32 = 1100;
|
||||
const TE_DELTA: u32 = 160;
|
||||
const MIN_COUNT_BIT: usize = 35;
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
CheckPreamble,
|
||||
SaveDuration,
|
||||
CheckDuration,
|
||||
}
|
||||
|
||||
/// Scher-Khan protocol decoder
|
||||
pub struct ScherKhanDecoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
header_count: u16,
|
||||
decode_data: u64,
|
||||
decode_count_bit: usize,
|
||||
}
|
||||
|
||||
impl ScherKhanDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
header_count: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_data(data: u64, bit_count: usize) -> DecodedSignal {
|
||||
let (serial, btn, cnt) = match bit_count {
|
||||
51 => {
|
||||
// MAGIC CODE, Dynamic
|
||||
let serial =
|
||||
((data >> 24) & 0xFFFFFF0) as u32 | ((data >> 20) & 0x0F) as u32;
|
||||
let btn = ((data >> 24) & 0x0F) as u8;
|
||||
let cnt = (data & 0xFFFF) as u16;
|
||||
(Some(serial), Some(btn), Some(cnt))
|
||||
}
|
||||
_ => (None, None, None),
|
||||
};
|
||||
|
||||
DecodedSignal {
|
||||
serial,
|
||||
button: btn,
|
||||
counter: cnt,
|
||||
crc_valid: true,
|
||||
data,
|
||||
data_count_bit: bit_count,
|
||||
encoder_capable: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for ScherKhanDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Scher-Khan"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && duration_diff!(duration, TE_SHORT * 2) < TE_DELTA {
|
||||
self.step = DecoderStep::CheckPreamble;
|
||||
self.te_last = duration;
|
||||
self.header_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckPreamble => {
|
||||
if level {
|
||||
if duration_diff!(duration, TE_SHORT * 2) < TE_DELTA
|
||||
|| duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
{
|
||||
self.te_last = duration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else if duration_diff!(duration, TE_SHORT * 2) < TE_DELTA
|
||||
|| duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
{
|
||||
if duration_diff!(self.te_last, TE_SHORT * 2) < TE_DELTA {
|
||||
self.header_count += 1;
|
||||
} else if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA {
|
||||
// Found start bit
|
||||
if self.header_count >= 2 {
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 1;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::SaveDuration => {
|
||||
if level {
|
||||
if duration >= (TE_DELTA * 2 + TE_LONG) {
|
||||
// Found stop bit
|
||||
self.step = DecoderStep::Reset;
|
||||
if self.decode_count_bit >= MIN_COUNT_BIT {
|
||||
let result =
|
||||
Self::parse_data(self.decode_data, self.decode_count_bit);
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
return Some(result);
|
||||
}
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
} else {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::CheckDuration;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckDuration => {
|
||||
if !level {
|
||||
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA
|
||||
&& duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
{
|
||||
// Bit 0
|
||||
self.decode_data = (self.decode_data << 1) | 0;
|
||||
self.decode_count_bit += 1;
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA
|
||||
&& duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
{
|
||||
// Bit 1
|
||||
self.decode_data = (self.decode_data << 1) | 1;
|
||||
self.decode_count_bit += 1;
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
//! Star Line protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's star_line.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 250/500µs timing
|
||||
//! - 64 bits total
|
||||
//! - Header: 6 pairs of 1000µs HIGH + 1000µs LOW
|
||||
//! - KeeLoq encryption (requires manufacturer key)
|
||||
|
||||
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key};
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 120;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
const HEADER_DURATION: u32 = 1000; // te_long * 2
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
CheckPreamble,
|
||||
SaveDuration,
|
||||
CheckDuration,
|
||||
}
|
||||
|
||||
/// Star Line protocol decoder
|
||||
pub struct StarLineDecoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
header_count: u16,
|
||||
decode_data: u64,
|
||||
decode_count_bit: usize,
|
||||
}
|
||||
|
||||
impl StarLineDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
header_count: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get manufacturer key (placeholder)
|
||||
fn get_mf_key() -> u64 {
|
||||
0x0000000000000000
|
||||
}
|
||||
|
||||
fn parse_data(data: u64) -> DecodedSignal {
|
||||
// Data is stored MSB-first in the air, reverse to get fix|hop
|
||||
let reversed = reverse_key(data, MIN_COUNT_BIT);
|
||||
let key_fix = (reversed >> 32) as u32;
|
||||
let key_hop = (reversed & 0xFFFFFFFF) as u32;
|
||||
|
||||
let serial = key_fix & 0x00FFFFFF;
|
||||
let btn = (key_fix >> 24) as u8;
|
||||
|
||||
// Attempt KeeLoq decryption
|
||||
let mf_key = Self::get_mf_key();
|
||||
let counter = if mf_key != 0 {
|
||||
// Try simple learning first
|
||||
let decrypt = keeloq_decrypt(key_hop, mf_key);
|
||||
let dec_btn = (decrypt >> 24) as u8;
|
||||
let dec_serial_lsb = ((decrypt >> 16) & 0xFF) as u8;
|
||||
let serial_lsb = (serial & 0xFF) as u8;
|
||||
|
||||
if dec_btn == btn && dec_serial_lsb == serial_lsb {
|
||||
Some((decrypt & 0xFFFF) as u16)
|
||||
} else {
|
||||
// Try normal learning
|
||||
let man_key = keeloq_normal_learning(key_fix, mf_key);
|
||||
let decrypt = keeloq_decrypt(key_hop, man_key);
|
||||
let dec_btn = (decrypt >> 24) as u8;
|
||||
let dec_serial_lsb = ((decrypt >> 16) & 0xFF) as u8;
|
||||
|
||||
if dec_btn == btn && dec_serial_lsb == serial_lsb {
|
||||
Some((decrypt & 0xFFFF) as u16)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: counter.or(Some(0)),
|
||||
crc_valid: counter.is_some() || mf_key == 0,
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for StarLineDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Star Line"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level {
|
||||
if duration_diff!(duration, HEADER_DURATION) < TE_DELTA * 2 {
|
||||
self.step = DecoderStep::CheckPreamble;
|
||||
self.header_count += 1;
|
||||
} else if self.header_count > 4 {
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::CheckDuration;
|
||||
}
|
||||
} else {
|
||||
self.header_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckPreamble => {
|
||||
if !level && duration_diff!(duration, HEADER_DURATION) < TE_DELTA * 2 {
|
||||
// Found preamble pair
|
||||
self.step = DecoderStep::Reset;
|
||||
} else {
|
||||
self.header_count = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::SaveDuration => {
|
||||
if level {
|
||||
if duration >= (TE_LONG + TE_DELTA) {
|
||||
// End of data - check if we have enough bits
|
||||
self.step = DecoderStep::Reset;
|
||||
if self.decode_count_bit >= MIN_COUNT_BIT
|
||||
&& self.decode_count_bit <= MIN_COUNT_BIT + 2
|
||||
{
|
||||
let result = Self::parse_data(self.decode_data);
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.header_count = 0;
|
||||
return Some(result);
|
||||
}
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.header_count = 0;
|
||||
} else {
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::CheckDuration;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckDuration => {
|
||||
if !level {
|
||||
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA
|
||||
&& duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
{
|
||||
// Bit 0: short HIGH + short LOW
|
||||
if self.decode_count_bit < MIN_COUNT_BIT {
|
||||
self.decode_data = (self.decode_data << 1) | 0;
|
||||
self.decode_count_bit += 1;
|
||||
} else {
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA
|
||||
&& duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
{
|
||||
// Bit 1: long HIGH + long LOW
|
||||
if self.decode_count_bit < MIN_COUNT_BIT {
|
||||
self.decode_data = (self.decode_data << 1) | 1;
|
||||
self.decode_count_bit += 1;
|
||||
} else {
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let serial = decoded.serial?;
|
||||
let counter = decoded.counter.unwrap_or(0).wrapping_add(1);
|
||||
|
||||
let fix = ((button as u32) << 24) | (serial & 0x00FFFFFF);
|
||||
let plaintext = ((button as u32) << 24)
|
||||
| (((serial & 0xFF) as u32) << 16)
|
||||
| (counter as u32);
|
||||
|
||||
let mf_key = Self::get_mf_key();
|
||||
let hop = if mf_key != 0 {
|
||||
keeloq_encrypt(plaintext, mf_key)
|
||||
} else {
|
||||
// Without a key, replay the original hop
|
||||
let reversed = reverse_key(decoded.data, MIN_COUNT_BIT);
|
||||
(reversed & 0xFFFFFFFF) as u32
|
||||
};
|
||||
|
||||
let yek = ((fix as u64) << 32) | (hop as u64);
|
||||
let data = reverse_key(yek, MIN_COUNT_BIT);
|
||||
|
||||
let mut signal = Vec::with_capacity(256);
|
||||
|
||||
// Header: 6 pairs of LONG*2 HIGH + LONG*2 LOW
|
||||
for _ in 0..6 {
|
||||
signal.push(LevelDuration::new(true, HEADER_DURATION));
|
||||
signal.push(LevelDuration::new(false, HEADER_DURATION));
|
||||
}
|
||||
|
||||
// Data: 64 bits, MSB first
|
||||
for bit in (0..64).rev() {
|
||||
if (data >> bit) & 1 == 1 {
|
||||
// Bit 1: LONG HIGH + LONG LOW
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
} else {
|
||||
// Bit 0: SHORT HIGH + SHORT LOW
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
//! Subaru protocol decoder
|
||||
//!
|
||||
//! Ported from protopirate's subaru.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: short HIGH (800µs) = 1, long HIGH (1600µs) = 0
|
||||
//! - 64 bits total
|
||||
//! - Long preamble of 1600µs pulses
|
||||
//! - Gap and sync pattern
|
||||
//! - Complex counter encoding
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
|
||||
const TE_SHORT: u32 = 800;
|
||||
const TE_LONG: u32 = 1600;
|
||||
const TE_DELTA: u32 = 200;
|
||||
#[allow(dead_code)]
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
|
||||
const GAP_US: u32 = 2800;
|
||||
const SYNC_US: u32 = 2800;
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
CheckPreamble,
|
||||
FoundGap,
|
||||
FoundSync,
|
||||
SaveDuration,
|
||||
CheckDuration,
|
||||
}
|
||||
|
||||
/// Subaru protocol decoder
|
||||
pub struct SubaruDecoder {
|
||||
step: DecoderStep,
|
||||
te_last: u32,
|
||||
header_count: u16,
|
||||
data: [u8; 8],
|
||||
bit_count: usize,
|
||||
}
|
||||
|
||||
impl SubaruDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
te_last: 0,
|
||||
header_count: 0,
|
||||
data: [0u8; 8],
|
||||
bit_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a bit to the data buffer
|
||||
fn add_bit(&mut self, bit: bool) {
|
||||
if self.bit_count < 64 {
|
||||
let byte_idx = self.bit_count / 8;
|
||||
let bit_idx = 7 - (self.bit_count % 8);
|
||||
if bit {
|
||||
self.data[byte_idx] |= 1 << bit_idx;
|
||||
} else {
|
||||
self.data[byte_idx] &= !(1 << bit_idx);
|
||||
}
|
||||
self.bit_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the counter from the complex Subaru encoding
|
||||
fn decode_counter(kb: &[u8; 8]) -> u16 {
|
||||
let mut lo: u8 = 0;
|
||||
if (kb[4] & 0x40) == 0 { lo |= 0x01; }
|
||||
if (kb[4] & 0x80) == 0 { lo |= 0x02; }
|
||||
if (kb[5] & 0x01) == 0 { lo |= 0x04; }
|
||||
if (kb[5] & 0x02) == 0 { lo |= 0x08; }
|
||||
if (kb[6] & 0x01) == 0 { lo |= 0x10; }
|
||||
if (kb[6] & 0x02) == 0 { lo |= 0x20; }
|
||||
if (kb[5] & 0x40) == 0 { lo |= 0x40; }
|
||||
if (kb[5] & 0x80) == 0 { lo |= 0x80; }
|
||||
|
||||
let mut reg_sh1 = (kb[7] << 4) & 0xF0;
|
||||
if kb[5] & 0x04 != 0 { reg_sh1 |= 0x04; }
|
||||
if kb[5] & 0x08 != 0 { reg_sh1 |= 0x08; }
|
||||
if kb[6] & 0x80 != 0 { reg_sh1 |= 0x02; }
|
||||
if kb[6] & 0x40 != 0 { reg_sh1 |= 0x01; }
|
||||
|
||||
let reg_sh2 = ((kb[6] << 2) & 0xF0) | ((kb[7] >> 4) & 0x0F);
|
||||
|
||||
let mut ser0 = kb[3];
|
||||
let mut ser1 = kb[1];
|
||||
let mut ser2 = kb[2];
|
||||
|
||||
let total_rot = 4 + lo;
|
||||
for _ in 0..total_rot {
|
||||
let t_bit = (ser0 >> 7) & 1;
|
||||
ser0 = ((ser0 << 1) & 0xFE) | ((ser1 >> 7) & 1);
|
||||
ser1 = ((ser1 << 1) & 0xFE) | ((ser2 >> 7) & 1);
|
||||
ser2 = ((ser2 << 1) & 0xFE) | t_bit;
|
||||
}
|
||||
|
||||
let t1 = ser1 ^ reg_sh1;
|
||||
let t2 = ser2 ^ reg_sh2;
|
||||
|
||||
let mut hi: u8 = 0;
|
||||
if (t1 & 0x10) == 0 { hi |= 0x04; }
|
||||
if (t1 & 0x20) == 0 { hi |= 0x08; }
|
||||
if (t2 & 0x80) == 0 { hi |= 0x02; }
|
||||
if (t2 & 0x40) == 0 { hi |= 0x01; }
|
||||
if (t1 & 0x01) == 0 { hi |= 0x40; }
|
||||
if (t1 & 0x02) == 0 { hi |= 0x80; }
|
||||
if (t2 & 0x08) == 0 { hi |= 0x20; }
|
||||
if (t2 & 0x04) == 0 { hi |= 0x10; }
|
||||
|
||||
((hi as u16) << 8) | (lo as u16)
|
||||
}
|
||||
|
||||
/// Process the decoded data
|
||||
fn process_data(&self) -> Option<DecodedSignal> {
|
||||
if self.bit_count < 64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let b = &self.data;
|
||||
|
||||
// Build 64-bit key
|
||||
let key = ((b[0] as u64) << 56) | ((b[1] as u64) << 48) |
|
||||
((b[2] as u64) << 40) | ((b[3] as u64) << 32) |
|
||||
((b[4] as u64) << 24) | ((b[5] as u64) << 16) |
|
||||
((b[6] as u64) << 8) | (b[7] as u64);
|
||||
|
||||
let serial = ((b[1] as u32) << 16) | ((b[2] as u32) << 8) | (b[3] as u32);
|
||||
let button = b[0] & 0x0F;
|
||||
let counter = Self::decode_counter(&self.data);
|
||||
|
||||
Some(DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: true, // Subaru doesn't use CRC
|
||||
data: key,
|
||||
data_count_bit: 64,
|
||||
encoder_capable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for SubaruDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Subaru"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.te_last = 0;
|
||||
self.header_count = 0;
|
||||
self.data = [0u8; 8];
|
||||
self.bit_count = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if level && duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||
self.step = DecoderStep::CheckPreamble;
|
||||
self.te_last = duration;
|
||||
self.header_count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckPreamble => {
|
||||
if !level {
|
||||
if duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||
self.header_count += 1;
|
||||
} else if duration > 2000 && duration < 3500 {
|
||||
// Gap detected
|
||||
if self.header_count > 20 {
|
||||
self.step = DecoderStep::FoundGap;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
if duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||
self.te_last = duration;
|
||||
self.header_count += 1;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::FoundGap => {
|
||||
if level && duration > 2000 && duration < 3500 {
|
||||
self.step = DecoderStep::FoundSync;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::FoundSync => {
|
||||
if !level && duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
self.bit_count = 0;
|
||||
self.data = [0u8; 8];
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::SaveDuration => {
|
||||
if level {
|
||||
if duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||
// Short HIGH = bit 1
|
||||
self.add_bit(true);
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::CheckDuration;
|
||||
} else if duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||
// Long HIGH = bit 0
|
||||
self.add_bit(false);
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::CheckDuration;
|
||||
} else if duration > 3000 {
|
||||
// End of transmission
|
||||
if self.bit_count >= 64 {
|
||||
let result = self.process_data();
|
||||
self.step = DecoderStep::Reset;
|
||||
return result;
|
||||
}
|
||||
self.step = DecoderStep::Reset;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::CheckDuration => {
|
||||
if !level {
|
||||
if duration_diff!(duration, TE_SHORT) < TE_DELTA ||
|
||||
duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||
self.step = DecoderStep::SaveDuration;
|
||||
} else if duration > 3000 {
|
||||
// Gap - end of packet
|
||||
if self.bit_count >= 64 {
|
||||
let result = self.process_data();
|
||||
self.step = DecoderStep::Reset;
|
||||
return result;
|
||||
}
|
||||
self.step = DecoderStep::Reset;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let key = decoded.data;
|
||||
let mut signal = Vec::with_capacity(512);
|
||||
|
||||
// Generate 3 bursts
|
||||
for burst in 0..3 {
|
||||
if burst > 0 {
|
||||
signal.push(LevelDuration::new(false, 25000));
|
||||
}
|
||||
|
||||
// Preamble: 80 long HIGH/LOW pairs
|
||||
for _ in 0..80 {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
}
|
||||
|
||||
// Gap
|
||||
signal.push(LevelDuration::new(false, GAP_US));
|
||||
|
||||
// Sync
|
||||
signal.push(LevelDuration::new(true, SYNC_US));
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
|
||||
// Data: 64 bits (MSB first)
|
||||
// Short HIGH = 1, Long HIGH = 0
|
||||
for bit in (0..64).rev() {
|
||||
if (key >> bit) & 1 == 1 {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// End marker
|
||||
signal.push(LevelDuration::new(false, TE_LONG * 2));
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Suzuki protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's suzuki.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 250/500µs timing
|
||||
//! - 64 bits total
|
||||
//! - 350 preamble pairs
|
||||
//! - 2000µs gap between transmissions
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const TE_SHORT: u32 = 250;
|
||||
const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 99;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
const PREAMBLE_COUNT: u16 = 350;
|
||||
const GAP_TIME: u32 = 2000;
|
||||
const GAP_DELTA: u32 = 399;
|
||||
|
||||
/// Decoder states
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
CountPreamble,
|
||||
DecodeData,
|
||||
}
|
||||
|
||||
/// Suzuki protocol decoder
|
||||
pub struct SuzukiDecoder {
|
||||
step: DecoderStep,
|
||||
header_count: u16,
|
||||
decode_data: u64,
|
||||
decode_count_bit: usize,
|
||||
te_last: u32,
|
||||
}
|
||||
|
||||
impl SuzukiDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: DecoderStep::Reset,
|
||||
header_count: 0,
|
||||
decode_data: 0,
|
||||
decode_count_bit: 0,
|
||||
te_last: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_bit(&mut self, bit: u8) {
|
||||
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
|
||||
fn parse_data(data: u64) -> DecodedSignal {
|
||||
let data_high = (data >> 32) as u32;
|
||||
let data_low = data as u32;
|
||||
|
||||
let serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
|
||||
let btn = ((data_low >> 12) & 0xF) as u8;
|
||||
let cnt = ((data_high << 4) >> 16) as u16;
|
||||
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: Some(cnt),
|
||||
crc_valid: true, // CRC checked via structure
|
||||
data,
|
||||
data_count_bit: MIN_COUNT_BIT,
|
||||
encoder_capable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for SuzukiDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Suzuki"
|
||||
}
|
||||
|
||||
fn timing(&self) -> ProtocolTiming {
|
||||
ProtocolTiming {
|
||||
te_short: TE_SHORT,
|
||||
te_long: TE_LONG,
|
||||
te_delta: TE_DELTA,
|
||||
min_count_bit: MIN_COUNT_BIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_frequencies(&self) -> &[u32] {
|
||||
&[433_920_000]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.step = DecoderStep::Reset;
|
||||
self.header_count = 0;
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.te_last = 0;
|
||||
}
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
DecoderStep::Reset => {
|
||||
if !level {
|
||||
return None;
|
||||
}
|
||||
if duration_diff!(duration, TE_SHORT) > TE_DELTA {
|
||||
return None;
|
||||
}
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.step = DecoderStep::CountPreamble;
|
||||
self.header_count = 0;
|
||||
}
|
||||
|
||||
DecoderStep::CountPreamble => {
|
||||
if level {
|
||||
// HIGH pulse
|
||||
if self.header_count >= 300 {
|
||||
if duration_diff!(duration, TE_LONG) <= TE_DELTA {
|
||||
self.step = DecoderStep::DecodeData;
|
||||
self.add_bit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// LOW pulse
|
||||
if duration_diff!(duration, TE_SHORT) <= TE_DELTA {
|
||||
self.te_last = duration;
|
||||
self.header_count += 1;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DecoderStep::DecodeData => {
|
||||
if level {
|
||||
// HIGH pulse - determines bit value
|
||||
let diff_long = duration_diff!(duration, TE_LONG);
|
||||
let diff_short = duration_diff!(duration, TE_SHORT);
|
||||
|
||||
if diff_long <= TE_DELTA {
|
||||
self.add_bit(1);
|
||||
} else if diff_short <= TE_DELTA {
|
||||
self.add_bit(0);
|
||||
}
|
||||
} else {
|
||||
// LOW pulse - check for gap (end of transmission)
|
||||
let diff_gap = duration_diff!(duration, GAP_TIME);
|
||||
|
||||
if diff_gap <= GAP_DELTA {
|
||||
if self.decode_count_bit == MIN_COUNT_BIT {
|
||||
let result = Self::parse_data(self.decode_data);
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
|
||||
self.decode_data = 0;
|
||||
self.decode_count_bit = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_encoding(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn encode(&self, decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
let data = decoded.data;
|
||||
|
||||
let mut signal = Vec::with_capacity(1024);
|
||||
|
||||
// Preamble: SHORT HIGH / SHORT LOW pairs
|
||||
for _ in 0..PREAMBLE_COUNT {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// Data: 64 bits, MSB first
|
||||
// SHORT HIGH (~250µs) = 0, LONG HIGH (~500µs) = 1
|
||||
for bit in (0..64).rev() {
|
||||
if (data >> bit) & 1 == 1 {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
}
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// End gap
|
||||
signal.push(LevelDuration::new(false, GAP_TIME));
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,194 @@
|
||||
//! AM/OOK demodulator for extracting level+duration pairs from raw IQ samples.
|
||||
//!
|
||||
//! This demodulator converts raw IQ samples into a stream of (level, duration_us) pairs
|
||||
//! that can be processed by protocol decoders, similar to how the Flipper Zero SubGHz
|
||||
//! system works.
|
||||
|
||||
/// A single level+duration pair representing one segment of the signal
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LevelDuration {
|
||||
/// Signal level (true = high, false = low)
|
||||
pub level: bool,
|
||||
/// Duration in microseconds
|
||||
pub duration_us: u32,
|
||||
}
|
||||
|
||||
impl LevelDuration {
|
||||
pub fn new(level: bool, duration_us: u32) -> Self {
|
||||
Self { level, duration_us }
|
||||
}
|
||||
}
|
||||
|
||||
/// Demodulator for processing raw IQ samples into level+duration pairs
|
||||
pub struct Demodulator {
|
||||
/// Sample rate in Hz
|
||||
#[allow(dead_code)]
|
||||
sample_rate: u32,
|
||||
/// Samples per microsecond
|
||||
samples_per_us: f64,
|
||||
/// Current threshold for high/low detection
|
||||
threshold: f32,
|
||||
/// Adaptive threshold - high level estimate
|
||||
high_level: f32,
|
||||
/// Adaptive threshold - low level estimate
|
||||
low_level: f32,
|
||||
/// Current signal state (high or low)
|
||||
current_level: bool,
|
||||
/// Sample count at current level
|
||||
level_sample_count: u64,
|
||||
/// Accumulated level+duration pairs
|
||||
pairs: Vec<LevelDuration>,
|
||||
/// Total samples processed
|
||||
total_samples: u64,
|
||||
/// Minimum duration to consider valid (in µs)
|
||||
min_duration_us: u32,
|
||||
/// Maximum gap before considering signal complete (in µs)
|
||||
max_gap_us: u32,
|
||||
/// Samples since last edge
|
||||
samples_since_edge: u64,
|
||||
}
|
||||
|
||||
impl Demodulator {
|
||||
/// Create a new demodulator
|
||||
pub fn new(sample_rate: u32) -> Self {
|
||||
Self {
|
||||
sample_rate,
|
||||
samples_per_us: sample_rate as f64 / 1_000_000.0,
|
||||
threshold: 0.15,
|
||||
high_level: 0.3,
|
||||
low_level: 0.05,
|
||||
current_level: false,
|
||||
level_sample_count: 0,
|
||||
pairs: Vec::with_capacity(2048),
|
||||
total_samples: 0,
|
||||
min_duration_us: 50, // Minimum 50µs pulse
|
||||
max_gap_us: 10_000, // 10ms gap = end of signal
|
||||
samples_since_edge: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Process raw IQ samples and return level+duration pairs if signal complete
|
||||
///
|
||||
/// Returns None if still accumulating, Some(pairs) when a complete signal is detected
|
||||
pub fn process_samples(&mut self, samples: &[i8]) -> Option<Vec<LevelDuration>> {
|
||||
// Process each IQ sample pair
|
||||
for chunk in samples.chunks(2) {
|
||||
if chunk.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate magnitude (AM envelope detection)
|
||||
let i = chunk[0] as f32 / 128.0;
|
||||
let q = chunk[1] as f32 / 128.0;
|
||||
let magnitude = (i * i + q * q).sqrt();
|
||||
|
||||
// Update adaptive threshold
|
||||
self.update_threshold(magnitude);
|
||||
|
||||
// Detect level
|
||||
let is_high = magnitude > self.threshold;
|
||||
|
||||
// Check for level change
|
||||
if is_high != self.current_level && self.level_sample_count > 0 {
|
||||
// Calculate duration of the previous level
|
||||
let duration_us = (self.level_sample_count as f64 / self.samples_per_us) as u32;
|
||||
|
||||
// Only record if above minimum duration (noise filtering)
|
||||
if duration_us >= self.min_duration_us {
|
||||
self.pairs.push(LevelDuration::new(self.current_level, duration_us));
|
||||
self.samples_since_edge = 0;
|
||||
}
|
||||
|
||||
self.current_level = is_high;
|
||||
self.level_sample_count = 1;
|
||||
} else {
|
||||
self.level_sample_count += 1;
|
||||
self.samples_since_edge += 1;
|
||||
}
|
||||
|
||||
self.total_samples += 1;
|
||||
}
|
||||
|
||||
// Check if we have a complete signal (long gap detected)
|
||||
let gap_samples = (self.max_gap_us as f64 * self.samples_per_us) as u64;
|
||||
|
||||
if !self.pairs.is_empty() && self.samples_since_edge > gap_samples {
|
||||
// Add the final level duration
|
||||
let duration_us = (self.level_sample_count as f64 / self.samples_per_us) as u32;
|
||||
if duration_us >= self.min_duration_us {
|
||||
self.pairs.push(LevelDuration::new(self.current_level, duration_us));
|
||||
}
|
||||
|
||||
// Return the pairs and reset
|
||||
let result = std::mem::take(&mut self.pairs);
|
||||
self.reset_state();
|
||||
|
||||
if result.len() >= 10 {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Limit buffer size
|
||||
if self.pairs.len() > 4096 {
|
||||
self.reset_state();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Update adaptive threshold based on signal levels
|
||||
fn update_threshold(&mut self, magnitude: f32) {
|
||||
const ALPHA: f32 = 0.001; // Slow adaptation
|
||||
|
||||
if magnitude > self.threshold {
|
||||
// Update high level estimate
|
||||
self.high_level = self.high_level * (1.0 - ALPHA) + magnitude * ALPHA;
|
||||
} else {
|
||||
// Update low level estimate
|
||||
self.low_level = self.low_level * (1.0 - ALPHA) + magnitude * ALPHA;
|
||||
}
|
||||
|
||||
// Threshold is midpoint between low and high
|
||||
self.threshold = (self.low_level + self.high_level) / 2.0;
|
||||
|
||||
// Ensure reasonable bounds
|
||||
self.threshold = self.threshold.max(0.05).min(0.5);
|
||||
}
|
||||
|
||||
/// Reset the demodulator state
|
||||
fn reset_state(&mut self) {
|
||||
self.pairs.clear();
|
||||
self.level_sample_count = 0;
|
||||
self.samples_since_edge = 0;
|
||||
self.current_level = false;
|
||||
}
|
||||
|
||||
/// Reset completely (including threshold adaptation)
|
||||
#[allow(dead_code)]
|
||||
pub fn reset(&mut self) {
|
||||
self.reset_state();
|
||||
self.threshold = 0.15;
|
||||
self.high_level = 0.3;
|
||||
self.low_level = 0.05;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: duration_diff macro is defined in protocols/mod.rs
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_demodulator_creation() {
|
||||
let demod = Demodulator::new(2_000_000);
|
||||
assert_eq!(demod.sample_rate, 2_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_level_duration() {
|
||||
let ld = LevelDuration::new(true, 500);
|
||||
assert!(ld.level);
|
||||
assert_eq!(ld.duration_us, 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
//! HackRF device control.
|
||||
//!
|
||||
//! This module provides a high-level interface for controlling HackRF devices
|
||||
//! using the `libhackrf` crate. Falls back to demo mode at runtime if no
|
||||
//! HackRF hardware is detected.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
};
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use crate::app::RadioEvent;
|
||||
use crate::capture::Capture;
|
||||
|
||||
use super::demodulator::Demodulator;
|
||||
use super::demodulator::LevelDuration;
|
||||
|
||||
/// Sample rate for HackRF (2 MHz is good for keyfob signals)
|
||||
const SAMPLE_RATE: u32 = 2_000_000;
|
||||
|
||||
/// HackRF controller for receiving and transmitting signals
|
||||
pub struct HackRfController {
|
||||
/// Event sender for notifying the app
|
||||
event_tx: Sender<RadioEvent>,
|
||||
/// Whether we're currently receiving
|
||||
receiving: Arc<AtomicBool>,
|
||||
/// Receiver thread handle
|
||||
rx_thread: Option<JoinHandle<()>>,
|
||||
/// Current frequency
|
||||
frequency: Arc<Mutex<u32>>,
|
||||
/// Demodulator for processing samples
|
||||
demodulator: Arc<Mutex<Demodulator>>,
|
||||
/// Whether HackRF is available
|
||||
hackrf_available: bool,
|
||||
}
|
||||
|
||||
impl HackRfController {
|
||||
/// Create a new HackRF controller
|
||||
pub fn new(event_tx: Sender<RadioEvent>) -> Result<Self> {
|
||||
let demodulator = Demodulator::new(SAMPLE_RATE);
|
||||
|
||||
// Check if HackRF is available
|
||||
let hackrf_available = check_hackrf_available();
|
||||
|
||||
if hackrf_available {
|
||||
tracing::info!("HackRF device detected");
|
||||
} else {
|
||||
tracing::warn!("HackRF not detected - running in demo mode");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
event_tx,
|
||||
receiving: Arc::new(AtomicBool::new(false)),
|
||||
rx_thread: None,
|
||||
frequency: Arc::new(Mutex::new(433_920_000)),
|
||||
demodulator: Arc::new(Mutex::new(demodulator)),
|
||||
hackrf_available,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if HackRF is available
|
||||
#[allow(dead_code)]
|
||||
pub fn is_available(&self) -> bool {
|
||||
self.hackrf_available
|
||||
}
|
||||
|
||||
/// Start receiving at the specified frequency
|
||||
pub fn start_receiving(&mut self, frequency: u32) -> Result<()> {
|
||||
if self.receiving.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
*self.frequency.lock().unwrap() = frequency;
|
||||
self.receiving.store(true, Ordering::SeqCst);
|
||||
|
||||
let receiving = self.receiving.clone();
|
||||
let event_tx = self.event_tx.clone();
|
||||
let freq = self.frequency.clone();
|
||||
let demodulator = self.demodulator.clone();
|
||||
let hackrf_available = self.hackrf_available;
|
||||
|
||||
self.rx_thread = Some(thread::spawn(move || {
|
||||
if hackrf_available {
|
||||
if let Err(e) =
|
||||
run_receiver_hackrf(receiving.clone(), event_tx.clone(), freq, demodulator)
|
||||
{
|
||||
let _ = event_tx.send(RadioEvent::Error(format!("Receiver error: {}", e)));
|
||||
}
|
||||
} else {
|
||||
run_demo_receiver(receiving, event_tx, freq);
|
||||
}
|
||||
}));
|
||||
|
||||
tracing::info!("Started receiving at {} Hz", frequency);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop receiving
|
||||
pub fn stop_receiving(&mut self) -> Result<()> {
|
||||
self.receiving.store(false, Ordering::SeqCst);
|
||||
|
||||
if let Some(handle) = self.rx_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
|
||||
tracing::info!("Stopped receiving");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the receive frequency
|
||||
pub fn set_frequency(&mut self, frequency: u32) -> Result<()> {
|
||||
*self.frequency.lock().unwrap() = frequency;
|
||||
tracing::info!("Set frequency to {} Hz", frequency);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transmit a signal
|
||||
pub fn transmit(&mut self, signal: &[LevelDuration], frequency: u32) -> Result<()> {
|
||||
if !self.hackrf_available {
|
||||
tracing::warn!("HackRF not available - simulating transmission");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Stop receiving first if we are
|
||||
let was_receiving = self.receiving.load(Ordering::SeqCst);
|
||||
if was_receiving {
|
||||
self.stop_receiving()?;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Transmitting {} level/duration pairs at {} Hz",
|
||||
signal.len(),
|
||||
frequency
|
||||
);
|
||||
|
||||
transmit_signal_hackrf(signal, frequency)?;
|
||||
|
||||
// Resume receiving if we were before
|
||||
if was_receiving {
|
||||
let freq = *self.frequency.lock().unwrap();
|
||||
self.start_receiving(freq)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set LNA gain (0-40 dB, 8 dB steps)
|
||||
pub fn set_lna_gain(&mut self, gain: u32) -> Result<()> {
|
||||
tracing::info!("Set LNA gain to {} dB", gain);
|
||||
// Note: gain changes take effect on next start_receiving
|
||||
// For now, just log - actual application happens in run_receiver_hackrf
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set VGA gain (0-62 dB, 2 dB steps)
|
||||
pub fn set_vga_gain(&mut self, gain: u32) -> Result<()> {
|
||||
tracing::info!("Set VGA gain to {} dB", gain);
|
||||
// Note: gain changes take effect on next start_receiving
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enable/disable the RF amplifier
|
||||
pub fn set_amp_enable(&mut self, enabled: bool) -> Result<()> {
|
||||
tracing::info!("Set amp enable to {}", enabled);
|
||||
// Note: amp changes take effect on next start_receiving
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HackRfController {
|
||||
fn drop(&mut self) {
|
||||
self.receiving.store(false, Ordering::SeqCst);
|
||||
if let Some(handle) = self.rx_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if HackRF is available
|
||||
fn check_hackrf_available() -> bool {
|
||||
// Try to open a HackRF device
|
||||
match libhackrf::HackRf::open() {
|
||||
Ok(_) => {
|
||||
tracing::debug!("HackRF opened successfully");
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!("HackRF not available: {:?}", e);
|
||||
// Fallback: check via hackrf_info command
|
||||
match std::process::Command::new("hackrf_info")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
{
|
||||
Ok(status) => status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a demo receiver (no actual HackRF)
|
||||
fn run_demo_receiver(
|
||||
receiving: Arc<AtomicBool>,
|
||||
_event_tx: Sender<RadioEvent>,
|
||||
_frequency: Arc<Mutex<u32>>,
|
||||
) {
|
||||
tracing::info!("Demo receiver thread started (no HackRF)");
|
||||
|
||||
while receiving.load(Ordering::SeqCst) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
tracing::info!("Demo receiver thread stopped");
|
||||
}
|
||||
|
||||
/// Shared state for RX callback (libhackrf requires fn pointers, not closures)
|
||||
struct RxState {
|
||||
receiving: Arc<AtomicBool>,
|
||||
event_tx: Sender<RadioEvent>,
|
||||
frequency: Arc<Mutex<u32>>,
|
||||
demodulator: Arc<Mutex<Demodulator>>,
|
||||
capture_id: std::sync::atomic::AtomicU32,
|
||||
}
|
||||
|
||||
/// RX callback function for libhackrf
|
||||
fn rx_callback(
|
||||
_hackrf: &libhackrf::HackRf,
|
||||
buffer: &[num_complex::Complex<i8>],
|
||||
user_data: &dyn std::any::Any,
|
||||
) {
|
||||
use crate::capture::StoredLevelDuration;
|
||||
|
||||
// Downcast user_data to our state
|
||||
let state = match user_data.downcast_ref::<RxState>() {
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if !state.receiving.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_freq = *state.frequency.lock().unwrap();
|
||||
|
||||
// Convert Complex<i8> samples to i8 pairs for demodulator
|
||||
let samples: Vec<i8> = buffer.iter()
|
||||
.flat_map(|c| [c.re, c.im])
|
||||
.collect();
|
||||
|
||||
// Process through demodulator
|
||||
if let Ok(mut demod) = state.demodulator.lock() {
|
||||
if let Some(pairs) = demod.process_samples(&samples) {
|
||||
// Convert to storable format
|
||||
let stored_pairs: Vec<StoredLevelDuration> = pairs
|
||||
.iter()
|
||||
.map(|p| StoredLevelDuration { level: p.level, duration_us: p.duration_us })
|
||||
.collect();
|
||||
|
||||
let id = state.capture_id.fetch_add(1, Ordering::SeqCst);
|
||||
let capture = Capture::from_pairs(id, current_freq, stored_pairs);
|
||||
let _ = state.event_tx.send(RadioEvent::SignalCaptured(capture));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the receiver loop with actual HackRF using libhackrf
|
||||
fn run_receiver_hackrf(
|
||||
receiving: Arc<AtomicBool>,
|
||||
event_tx: Sender<RadioEvent>,
|
||||
frequency: Arc<Mutex<u32>>,
|
||||
demodulator: Arc<Mutex<Demodulator>>,
|
||||
) -> Result<()> {
|
||||
use anyhow::Context;
|
||||
|
||||
tracing::info!("HackRF receiver thread starting...");
|
||||
|
||||
// Open HackRF device
|
||||
let hackrf = libhackrf::HackRf::open()
|
||||
.context("Failed to open HackRF device")?;
|
||||
|
||||
let freq = *frequency.lock().unwrap();
|
||||
tracing::info!("Configuring HackRF: freq={} Hz, sample_rate={} Hz", freq, SAMPLE_RATE);
|
||||
|
||||
// Configure HackRF
|
||||
hackrf.set_sample_rate(SAMPLE_RATE)
|
||||
.context("Failed to set sample rate")?;
|
||||
|
||||
hackrf.set_freq(freq as u64)
|
||||
.context("Failed to set frequency")?;
|
||||
|
||||
hackrf.set_lna_gain(32)
|
||||
.context("Failed to set LNA gain")?;
|
||||
|
||||
hackrf.set_rxvga_gain(20)
|
||||
.context("Failed to set RXVGA gain")?;
|
||||
|
||||
hackrf.set_amp_enable(true)
|
||||
.context("Failed to enable amp")?;
|
||||
|
||||
tracing::info!("HackRF configured, starting RX...");
|
||||
|
||||
// Create state for callback
|
||||
let state = RxState {
|
||||
receiving: receiving.clone(),
|
||||
event_tx: event_tx.clone(),
|
||||
frequency: frequency.clone(),
|
||||
demodulator,
|
||||
capture_id: std::sync::atomic::AtomicU32::new(0),
|
||||
};
|
||||
|
||||
// Start receiving
|
||||
hackrf.start_rx(rx_callback, state)
|
||||
.context("Failed to start RX")?;
|
||||
|
||||
// Wait until receiving is stopped
|
||||
while receiving.load(Ordering::SeqCst) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
// Stop receiving
|
||||
hackrf.stop_rx().context("Failed to stop RX")?;
|
||||
|
||||
tracing::info!("HackRF receiver thread stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shared state for TX callback
|
||||
struct TxState {
|
||||
samples: Vec<(i8, i8)>,
|
||||
sample_index: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
|
||||
/// TX callback function for libhackrf
|
||||
fn tx_callback(
|
||||
_hackrf: &libhackrf::HackRf,
|
||||
buffer: &mut [num_complex::Complex<i8>],
|
||||
user_data: &dyn std::any::Any,
|
||||
) {
|
||||
use num_complex::Complex;
|
||||
|
||||
// Downcast user_data to our state
|
||||
let state = match user_data.downcast_ref::<TxState>() {
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let total = state.samples.len();
|
||||
|
||||
for sample in buffer.iter_mut() {
|
||||
let idx = state.sample_index.fetch_add(1, Ordering::SeqCst);
|
||||
if idx < total {
|
||||
let (i, q) = state.samples[idx];
|
||||
*sample = Complex::new(i, q);
|
||||
} else {
|
||||
*sample = Complex::new(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transmit a signal via HackRF
|
||||
fn transmit_signal_hackrf(signal: &[LevelDuration], frequency: u32) -> Result<()> {
|
||||
use anyhow::Context;
|
||||
|
||||
tracing::info!("Starting HackRF transmission at maximum power...");
|
||||
|
||||
// Open HackRF device
|
||||
let hackrf = libhackrf::HackRf::open()
|
||||
.context("Failed to open HackRF device")?;
|
||||
|
||||
// Configure for TX with MAXIMUM POWER
|
||||
hackrf.set_sample_rate(SAMPLE_RATE)
|
||||
.context("Failed to set sample rate")?;
|
||||
|
||||
hackrf.set_freq(frequency as u64)
|
||||
.context("Failed to set frequency")?;
|
||||
|
||||
// Set TX VGA gain to maximum (47 dB is the max for HackRF)
|
||||
hackrf.set_txvga_gain(47)
|
||||
.context("Failed to set TXVGA gain")?;
|
||||
|
||||
// Enable the RF amplifier for +14dB additional gain
|
||||
hackrf.set_amp_enable(true)
|
||||
.context("Failed to enable amp")?;
|
||||
|
||||
// Generate TX samples
|
||||
let tx_samples = generate_tx_samples(signal, SAMPLE_RATE);
|
||||
let total_samples = tx_samples.len();
|
||||
|
||||
tracing::debug!("Generated {} TX samples", total_samples);
|
||||
|
||||
// Create state for callback
|
||||
let state = TxState {
|
||||
samples: tx_samples,
|
||||
sample_index: std::sync::atomic::AtomicUsize::new(0),
|
||||
};
|
||||
|
||||
// Start transmitting
|
||||
hackrf.start_tx(tx_callback, state)
|
||||
.context("Failed to start TX")?;
|
||||
|
||||
// Wait for transmission to complete (check sample_index through a loop)
|
||||
// We can't easily check completion with this API, so just wait based on expected time
|
||||
let duration_us: u32 = signal.iter().map(|s| s.duration_us).sum();
|
||||
let wait_ms = (duration_us / 1000).max(100);
|
||||
std::thread::sleep(std::time::Duration::from_millis(wait_ms as u64 + 100));
|
||||
|
||||
// Stop transmitting
|
||||
hackrf.stop_tx().context("Failed to stop TX")?;
|
||||
|
||||
tracing::info!("Transmission complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate TX samples from level/duration pairs
|
||||
fn generate_tx_samples(signal: &[LevelDuration], sample_rate: u32) -> Vec<(i8, i8)> {
|
||||
let mut samples = Vec::new();
|
||||
let samples_per_us = sample_rate as f64 / 1_000_000.0;
|
||||
|
||||
for ld in signal {
|
||||
let num_samples = (ld.duration_us as f64 * samples_per_us) as usize;
|
||||
let value: i8 = if ld.level { 127 } else { 0 };
|
||||
|
||||
// IQ samples
|
||||
for _ in 0..num_samples {
|
||||
samples.push((value, 0)); // I, Q (Q=0 for OOK)
|
||||
}
|
||||
}
|
||||
|
||||
samples
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! Radio subsystem for HackRF control.
|
||||
|
||||
pub mod demodulator;
|
||||
mod hackrf;
|
||||
mod modulator;
|
||||
|
||||
pub use demodulator::LevelDuration;
|
||||
pub use hackrf::HackRfController;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use demodulator::Demodulator;
|
||||
#[allow(unused_imports)]
|
||||
pub use modulator::Modulator;
|
||||
@@ -0,0 +1,147 @@
|
||||
//! Signal modulator for generating TX waveforms.
|
||||
|
||||
use super::demodulator::LevelDuration;
|
||||
|
||||
/// Modulator for generating transmission waveforms
|
||||
#[allow(dead_code)]
|
||||
pub struct Modulator {
|
||||
/// Time element in microseconds (base timing unit)
|
||||
pub te: u32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Modulator {
|
||||
/// Create a new modulator with the given time element
|
||||
pub fn new(te: u32) -> Self {
|
||||
Self { te }
|
||||
}
|
||||
|
||||
/// Generate a preamble (alternating pattern)
|
||||
pub fn generate_preamble(&self, count: usize) -> Vec<LevelDuration> {
|
||||
let mut result = Vec::with_capacity(count * 2);
|
||||
for _ in 0..count {
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Generate a sync pattern
|
||||
pub fn generate_sync(&self, high_te: u32, low_te: u32) -> Vec<LevelDuration> {
|
||||
vec![
|
||||
LevelDuration::new(true, self.te * high_te),
|
||||
LevelDuration::new(false, self.te * low_te),
|
||||
]
|
||||
}
|
||||
|
||||
/// Encode data using PWM (Pulse Width Modulation)
|
||||
/// bit 0: short high, long low
|
||||
/// bit 1: long high, short low
|
||||
pub fn encode_pwm(&self, data: &[u8], bit_count: usize) -> Vec<LevelDuration> {
|
||||
let mut result = Vec::with_capacity(bit_count * 2);
|
||||
|
||||
for i in 0..bit_count {
|
||||
let byte_idx = i / 8;
|
||||
let bit_idx = 7 - (i % 8);
|
||||
let bit = (data[byte_idx] >> bit_idx) & 1;
|
||||
|
||||
if bit == 0 {
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
result.push(LevelDuration::new(false, self.te * 3));
|
||||
} else {
|
||||
result.push(LevelDuration::new(true, self.te * 3));
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Encode data using Manchester encoding
|
||||
/// bit 0: high then low
|
||||
/// bit 1: low then high
|
||||
pub fn encode_manchester(&self, data: &[u8], bit_count: usize) -> Vec<LevelDuration> {
|
||||
let mut result = Vec::with_capacity(bit_count * 2);
|
||||
|
||||
for i in 0..bit_count {
|
||||
let byte_idx = i / 8;
|
||||
let bit_idx = 7 - (i % 8);
|
||||
let bit = (data[byte_idx] >> bit_idx) & 1;
|
||||
|
||||
if bit == 0 {
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
} else {
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Encode data using inverted Manchester encoding
|
||||
/// bit 0: low then high
|
||||
/// bit 1: high then low
|
||||
pub fn encode_manchester_inverted(&self, data: &[u8], bit_count: usize) -> Vec<LevelDuration> {
|
||||
let mut result = Vec::with_capacity(bit_count * 2);
|
||||
|
||||
for i in 0..bit_count {
|
||||
let byte_idx = i / 8;
|
||||
let bit_idx = 7 - (i % 8);
|
||||
let bit = (data[byte_idx] >> bit_idx) & 1;
|
||||
|
||||
if bit == 0 {
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
} else {
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Generate a trailer (final low period)
|
||||
pub fn generate_trailer(&self, te_count: u32) -> Vec<LevelDuration> {
|
||||
vec![LevelDuration::new(false, self.te * te_count)]
|
||||
}
|
||||
|
||||
/// Combine multiple signal parts into one
|
||||
pub fn combine(parts: Vec<Vec<LevelDuration>>) -> Vec<LevelDuration> {
|
||||
parts.into_iter().flatten().collect()
|
||||
}
|
||||
|
||||
/// Repeat a signal pattern multiple times
|
||||
pub fn repeat(signal: &[LevelDuration], count: usize) -> Vec<LevelDuration> {
|
||||
let mut result = Vec::with_capacity(signal.len() * count);
|
||||
for _ in 0..count {
|
||||
result.extend_from_slice(signal);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pwm_encoding() {
|
||||
let mod_ = Modulator::new(400);
|
||||
let data = vec![0b10101010];
|
||||
let encoded = mod_.encode_pwm(&data, 8);
|
||||
|
||||
assert_eq!(encoded.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manchester_encoding() {
|
||||
let mod_ = Modulator::new(400);
|
||||
let data = vec![0b10101010];
|
||||
let encoded = mod_.encode_manchester(&data, 8);
|
||||
|
||||
assert_eq!(encoded.len(), 16);
|
||||
}
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
//! Storage management for configuration and exports.
|
||||
//!
|
||||
//! All application data lives under `~/.config/KAT/`:
|
||||
//!
|
||||
//! ```text
|
||||
//! ~/.config/KAT/
|
||||
//! config.ini — User configuration
|
||||
//! exports/ — Exported .fob / .sub files
|
||||
//! ```
|
||||
//!
|
||||
//! Captures are **in-memory only** and are discarded when KAT exits.
|
||||
//! Only explicitly exported signals (.fob / .sub) persist between runs.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use configparser::ini::Ini;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Application configuration loaded from `~/.config/KAT/config.ini`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
// [general]
|
||||
/// Directory for exporting signals (.fob / .sub files)
|
||||
pub export_directory: PathBuf,
|
||||
/// Maximum captures to keep in memory during a session
|
||||
pub max_captures: usize,
|
||||
|
||||
// [radio]
|
||||
/// Default frequency in Hz
|
||||
pub default_frequency: u32,
|
||||
/// Default LNA gain (0-40 dB, 8 dB steps)
|
||||
pub default_lna_gain: u32,
|
||||
/// Default VGA gain (0-62 dB, 2 dB steps)
|
||||
pub default_vga_gain: u32,
|
||||
/// Default amplifier state
|
||||
pub default_amp: bool,
|
||||
|
||||
// [export]
|
||||
/// Default export format (fob or sub)
|
||||
pub default_export_format: String,
|
||||
/// Include raw level/duration pairs in exports
|
||||
pub include_raw_pairs: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Build the default config, using the given config_dir as the base.
|
||||
/// This keeps everything under `~/.config/KAT/` by default.
|
||||
fn default_for(config_dir: &PathBuf) -> Self {
|
||||
Self {
|
||||
export_directory: config_dir.join("exports"),
|
||||
max_captures: 100,
|
||||
default_frequency: 433_920_000,
|
||||
default_lna_gain: 24,
|
||||
default_vga_gain: 20,
|
||||
default_amp: false,
|
||||
default_export_format: "fob".to_string(),
|
||||
include_raw_pairs: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load config from an INI file, falling back to defaults for missing keys.
|
||||
fn load_from_ini(path: &std::path::Path, config_dir: &PathBuf) -> Result<Self> {
|
||||
let mut ini = Ini::new();
|
||||
ini.load(path)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?;
|
||||
|
||||
let defaults = Config::default_for(config_dir);
|
||||
|
||||
let export_directory = ini
|
||||
.get("general", "export_directory")
|
||||
.map(|s| expand_tilde(&s))
|
||||
.unwrap_or(defaults.export_directory);
|
||||
|
||||
let max_captures = ini
|
||||
.getuint("general", "max_captures")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v as usize)
|
||||
.unwrap_or(defaults.max_captures);
|
||||
|
||||
let default_frequency = ini
|
||||
.getuint("radio", "default_frequency")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(defaults.default_frequency);
|
||||
|
||||
let default_lna_gain = ini
|
||||
.getuint("radio", "default_lna_gain")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(defaults.default_lna_gain);
|
||||
|
||||
let default_vga_gain = ini
|
||||
.getuint("radio", "default_vga_gain")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(defaults.default_vga_gain);
|
||||
|
||||
let default_amp = ini
|
||||
.getbool("radio", "default_amp")
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(defaults.default_amp);
|
||||
|
||||
let default_export_format = ini
|
||||
.get("export", "default_format")
|
||||
.unwrap_or(defaults.default_export_format);
|
||||
|
||||
let include_raw_pairs = ini
|
||||
.getbool("export", "include_raw_pairs")
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(defaults.include_raw_pairs);
|
||||
|
||||
Ok(Self {
|
||||
export_directory,
|
||||
max_captures,
|
||||
default_frequency,
|
||||
default_lna_gain,
|
||||
default_vga_gain,
|
||||
default_amp,
|
||||
default_export_format,
|
||||
include_raw_pairs,
|
||||
})
|
||||
}
|
||||
|
||||
/// Save config to an INI-style file with comments explaining each field.
|
||||
fn save_to_ini(&self, path: &std::path::Path) -> Result<()> {
|
||||
let export_str = self.export_directory.to_string_lossy();
|
||||
let freq_mhz = self.default_frequency as f64 / 1_000_000.0;
|
||||
|
||||
let content = format!(
|
||||
r#"; KAT — Keyfob Analysis Toolkit configuration
|
||||
; Location: {path}
|
||||
;
|
||||
; Edit this file to change default settings.
|
||||
; Lines starting with ; or # are comments.
|
||||
|
||||
[general]
|
||||
; Directory where .fob and .sub exports are saved.
|
||||
; Supports ~ for home directory.
|
||||
export_directory = {export_dir}
|
||||
|
||||
; Maximum number of captures to keep in memory per session.
|
||||
; Captures are NOT persisted between runs — only exported
|
||||
; signals (.fob / .sub) survive in the exports folder.
|
||||
max_captures = {max_captures}
|
||||
|
||||
[radio]
|
||||
; Default receive frequency in Hz ({freq_mhz:.2} MHz)
|
||||
; Common keyfob frequencies: 315000000, 433920000, 868350000
|
||||
default_frequency = {frequency}
|
||||
|
||||
; Default LNA gain in dB (0, 8, 16, 24, 32, 40)
|
||||
default_lna_gain = {lna}
|
||||
|
||||
; Default VGA gain in dB (0-62, even numbers)
|
||||
default_vga_gain = {vga}
|
||||
|
||||
; Enable RF amplifier by default (true/false)
|
||||
default_amp = {amp}
|
||||
|
||||
[export]
|
||||
; Default export format: fob (JSON metadata) or sub (Flipper Zero)
|
||||
default_format = {export_fmt}
|
||||
|
||||
; Include raw signal level/duration pairs in .fob exports.
|
||||
; Enables signal replay but increases file size.
|
||||
include_raw_pairs = {raw_pairs}
|
||||
"#,
|
||||
path = path.display(),
|
||||
export_dir = export_str,
|
||||
max_captures = self.max_captures,
|
||||
freq_mhz = freq_mhz,
|
||||
frequency = self.default_frequency,
|
||||
lna = self.default_lna_gain,
|
||||
vga = self.default_vga_gain,
|
||||
amp = self.default_amp,
|
||||
export_fmt = self.default_export_format,
|
||||
raw_pairs = self.include_raw_pairs,
|
||||
);
|
||||
|
||||
fs::write(path, content)
|
||||
.with_context(|| format!("Failed to write config to {:?}", path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback Default (without knowing config_dir). Only used if something goes
|
||||
/// very wrong and we need a Config without a Storage instance.
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let fallback = resolve_config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from(".").join("KAT"));
|
||||
Config::default_for(&fallback)
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand `~` at the start of a path to the user's home directory.
|
||||
fn expand_tilde(s: &str) -> PathBuf {
|
||||
if s.starts_with("~/") {
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
return home.join(&s[2..]);
|
||||
}
|
||||
}
|
||||
PathBuf::from(s)
|
||||
}
|
||||
|
||||
/// Resolve the KAT config directory to `~/.config/KAT/` regardless of OS.
|
||||
pub fn resolve_config_dir() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".config").join("KAT"))
|
||||
}
|
||||
|
||||
// ─── Storage ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Storage manager for configuration and exports.
|
||||
///
|
||||
/// On construction it ensures the directory tree exists:
|
||||
///
|
||||
/// ```text
|
||||
/// ~/.config/KAT/
|
||||
/// config.ini
|
||||
/// exports/
|
||||
/// ```
|
||||
///
|
||||
/// Captures are in-memory only — they are discarded on exit.
|
||||
pub struct Storage {
|
||||
/// Base config directory (~/.config/KAT)
|
||||
config_dir: PathBuf,
|
||||
/// Configuration
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
/// Create a new storage manager.
|
||||
///
|
||||
/// 1. Resolves the config directory (`~/.config/KAT`).
|
||||
/// 2. Creates it if missing.
|
||||
/// 3. Loads `config.ini` if it exists, otherwise writes a default one.
|
||||
/// 4. Creates the export directory if missing.
|
||||
pub fn new() -> Result<Self> {
|
||||
// ── 1. Resolve base path ─────────────────────────────────────────
|
||||
let config_dir = resolve_config_dir()
|
||||
.context("Could not determine home directory (is $HOME set?)")?;
|
||||
|
||||
let config_path = config_dir.join("config.ini");
|
||||
|
||||
// ── 2. Ensure directory tree exists ──────────────────────────────
|
||||
if !config_dir.exists() {
|
||||
fs::create_dir_all(&config_dir)
|
||||
.with_context(|| format!("Failed to create config dir: {:?}", config_dir))?;
|
||||
tracing::info!("Created config directory: {:?}", config_dir);
|
||||
}
|
||||
|
||||
// ── 3. Load or create config.ini ─────────────────────────────────
|
||||
let config = if config_path.exists() {
|
||||
tracing::info!("Loading config from {:?}", config_path);
|
||||
match Config::load_from_ini(&config_path, &config_dir) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to parse config.ini, using defaults: {}",
|
||||
e
|
||||
);
|
||||
Config::default_for(&config_dir)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::info!(
|
||||
"No config.ini found — creating default at {:?}",
|
||||
config_path
|
||||
);
|
||||
let config = Config::default_for(&config_dir);
|
||||
if let Err(e) = config.save_to_ini(&config_path) {
|
||||
tracing::warn!("Could not write default config.ini: {}", e);
|
||||
}
|
||||
config
|
||||
};
|
||||
|
||||
// ── 4. Ensure export directory exists ────────────────────────────
|
||||
if !config.export_directory.exists() {
|
||||
fs::create_dir_all(&config.export_directory).with_context(|| {
|
||||
format!(
|
||||
"Failed to create export dir: {:?}",
|
||||
config.export_directory
|
||||
)
|
||||
})?;
|
||||
tracing::info!(
|
||||
"Created export directory: {:?}",
|
||||
config.export_directory
|
||||
);
|
||||
}
|
||||
|
||||
// ── 5. Log resolved paths ───────────────────────────────────────
|
||||
tracing::info!("Config dir: {:?}", config_dir);
|
||||
tracing::info!("Export dir: {:?}", config.export_directory);
|
||||
|
||||
Ok(Self {
|
||||
config_dir,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Save the current configuration back to `config.ini`.
|
||||
#[allow(dead_code)]
|
||||
pub fn save_config(&self) -> Result<()> {
|
||||
let config_path = self.config_dir.join("config.ini");
|
||||
self.config.save_to_ini(&config_path)?;
|
||||
tracing::info!("Saved config to {:?}", config_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ─── Path accessors ──────────────────────────────────────────────────
|
||||
|
||||
/// Get the config directory path (`~/.config/KAT`)
|
||||
#[allow(dead_code)]
|
||||
pub fn config_dir(&self) -> &PathBuf {
|
||||
&self.config_dir
|
||||
}
|
||||
|
||||
/// Get the export directory path (from config, default `~/.config/KAT/exports`)
|
||||
pub fn export_dir(&self) -> &PathBuf {
|
||||
&self.config.export_directory
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
//! Captures list widget with detail panel.
|
||||
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState, Wrap},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::capture::CaptureStatus;
|
||||
|
||||
/// Render the captures area: table + detail panel
|
||||
pub fn render_captures_list(frame: &mut Frame, area: Rect, app: &App) {
|
||||
// Split vertically: table on top, detail panel on bottom
|
||||
let has_selection = app
|
||||
.selected_capture
|
||||
.map(|i| i < app.captures.len())
|
||||
.unwrap_or(false);
|
||||
|
||||
let chunks = if has_selection {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(6), // Table (flexible, takes remaining)
|
||||
Constraint::Length(12), // Detail panel (fixed height)
|
||||
])
|
||||
.split(area)
|
||||
} else {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(6)])
|
||||
.split(area)
|
||||
};
|
||||
|
||||
render_table(frame, chunks[0], app);
|
||||
|
||||
if has_selection && chunks.len() > 1 {
|
||||
render_detail_panel(frame, chunks[1], app);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the compact signal table
|
||||
fn render_table(frame: &mut Frame, area: Rect, app: &App) {
|
||||
let header_cells = [
|
||||
"ID", "Time", "Protocol", "Freq", "Serial", "Btn", "Cnt", "Mod", "CRC", "Status",
|
||||
]
|
||||
.iter()
|
||||
.map(|h| Cell::from(*h).style(Style::default().add_modifier(Modifier::BOLD)));
|
||||
|
||||
let header = Row::new(header_cells).style(Style::default()).height(1);
|
||||
|
||||
let rows = app.captures.iter().map(|capture| {
|
||||
let status_style = match capture.status {
|
||||
CaptureStatus::Unknown => Style::default().fg(Color::DarkGray),
|
||||
CaptureStatus::Decoded => Style::default().fg(Color::Yellow),
|
||||
CaptureStatus::EncoderCapable => Style::default().fg(Color::Green),
|
||||
};
|
||||
|
||||
let crc_style = if capture.protocol.is_none() {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
} else if capture.crc_valid {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
};
|
||||
|
||||
let mod_style = match capture.modulation() {
|
||||
crate::capture::ModulationType::Pwm => Style::default().fg(Color::Magenta),
|
||||
crate::capture::ModulationType::Manchester => Style::default().fg(Color::Cyan),
|
||||
crate::capture::ModulationType::DifferentialManchester => {
|
||||
Style::default().fg(Color::Blue)
|
||||
}
|
||||
crate::capture::ModulationType::Unknown => Style::default().fg(Color::DarkGray),
|
||||
};
|
||||
|
||||
let status_text = match capture.status {
|
||||
CaptureStatus::EncoderCapable => "✓ Encode",
|
||||
CaptureStatus::Decoded => "Decoded",
|
||||
CaptureStatus::Unknown => "Unknown",
|
||||
};
|
||||
|
||||
Row::new(vec![
|
||||
Cell::from(format!("{:02}", capture.id)),
|
||||
Cell::from(capture.timestamp_short()),
|
||||
Cell::from(capture.protocol_name().to_string()),
|
||||
Cell::from(capture.frequency_mhz()),
|
||||
Cell::from(capture.serial_hex()),
|
||||
Cell::from(capture.button_name().to_string()),
|
||||
Cell::from(capture.counter_str()),
|
||||
Cell::from(capture.modulation().to_string()).style(mod_style),
|
||||
Cell::from(capture.crc_status()).style(crc_style),
|
||||
Cell::from(status_text).style(status_style),
|
||||
])
|
||||
.height(1)
|
||||
});
|
||||
|
||||
let widths = [
|
||||
Constraint::Length(4), // ID
|
||||
Constraint::Length(9), // Time
|
||||
Constraint::Length(10), // Protocol
|
||||
Constraint::Length(11), // Freq
|
||||
Constraint::Length(9), // Serial
|
||||
Constraint::Length(6), // Btn
|
||||
Constraint::Length(6), // Cnt
|
||||
Constraint::Length(7), // Mod
|
||||
Constraint::Length(5), // CRC
|
||||
Constraint::Length(10), // Status
|
||||
];
|
||||
|
||||
let table = Table::new(rows, widths)
|
||||
.header(header)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Captures "),
|
||||
)
|
||||
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||
|
||||
let mut state = TableState::default();
|
||||
state.select(app.selected_capture);
|
||||
|
||||
// Apply scroll offset if needed
|
||||
if app.scroll_offset > 0 && app.selected_capture.is_some() {
|
||||
*state.offset_mut() = app.scroll_offset;
|
||||
}
|
||||
|
||||
frame.render_stateful_widget(table, area, &mut state);
|
||||
}
|
||||
|
||||
/// Render the detail panel for the selected signal
|
||||
fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) {
|
||||
let capture = match app.selected_capture {
|
||||
Some(idx) if idx < app.captures.len() => &app.captures[idx],
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let label_style = Style::default()
|
||||
.fg(Color::DarkGray)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let value_style = Style::default().fg(Color::White);
|
||||
let accent_style = Style::default().fg(Color::Cyan);
|
||||
let good_style = Style::default().fg(Color::Green);
|
||||
let bad_style = Style::default().fg(Color::Red);
|
||||
|
||||
// Build detail content in two columns
|
||||
let make = App::get_make_for_protocol(capture.protocol_name());
|
||||
|
||||
// --- Left column lines ---
|
||||
let mut left_lines = Vec::new();
|
||||
|
||||
// Row 1: Protocol + Make
|
||||
left_lines.push(Line::from(vec![
|
||||
Span::styled(" Protocol: ", label_style),
|
||||
Span::styled(capture.protocol_name(), accent_style),
|
||||
Span::styled(" Make: ", label_style),
|
||||
Span::styled(make, value_style),
|
||||
]));
|
||||
|
||||
// Row 2: Frequency + Modulation + Encryption
|
||||
left_lines.push(Line::from(vec![
|
||||
Span::styled(" Freq: ", label_style),
|
||||
Span::styled(capture.frequency_mhz(), value_style),
|
||||
Span::styled(" Mod: ", label_style),
|
||||
Span::styled(capture.modulation().to_string(), value_style),
|
||||
Span::styled(" Enc: ", label_style),
|
||||
Span::styled(capture.encryption_type(), value_style),
|
||||
]));
|
||||
|
||||
// Row 3: Full Serial + Button
|
||||
left_lines.push(Line::from(vec![
|
||||
Span::styled(" Serial: ", label_style),
|
||||
Span::styled(format!("0x{}", capture.serial_hex()), accent_style),
|
||||
Span::styled(" Btn: ", label_style),
|
||||
Span::styled(
|
||||
format!("{} ({})", capture.button_name(), capture.button_hex()),
|
||||
value_style,
|
||||
),
|
||||
]));
|
||||
|
||||
// Row 4: Counter + CRC
|
||||
let crc_span = if capture.protocol.is_none() {
|
||||
Span::styled("-", Style::default().fg(Color::DarkGray))
|
||||
} else if capture.crc_valid {
|
||||
Span::styled("OK ✓", good_style)
|
||||
} else {
|
||||
Span::styled("FAIL ✗", bad_style)
|
||||
};
|
||||
|
||||
left_lines.push(Line::from(vec![
|
||||
Span::styled(" Counter: ", label_style),
|
||||
Span::styled(capture.counter_str(), value_style),
|
||||
Span::styled(" CRC: ", label_style),
|
||||
crc_span,
|
||||
Span::styled(" Status: ", label_style),
|
||||
Span::styled(capture.status.to_string(), value_style),
|
||||
]));
|
||||
|
||||
// Row 5: Full data/key hex
|
||||
left_lines.push(Line::from(vec![
|
||||
Span::styled(" Key/Data: ", label_style),
|
||||
Span::styled(
|
||||
format!("0x{}", capture.data_hex()),
|
||||
Style::default().fg(Color::Yellow),
|
||||
),
|
||||
Span::styled(
|
||||
format!(" ({})", capture.data_bits_str()),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
),
|
||||
]));
|
||||
|
||||
// Row 6: Timestamp + Raw data info
|
||||
let raw_info = if capture.has_raw_data() {
|
||||
format!("✓ {} transitions", capture.raw_pair_count())
|
||||
} else {
|
||||
"None".to_string()
|
||||
};
|
||||
let raw_style = if capture.has_raw_data() {
|
||||
good_style
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
|
||||
left_lines.push(Line::from(vec![
|
||||
Span::styled(" Captured: ", label_style),
|
||||
Span::styled(capture.timestamp_full(), value_style),
|
||||
]));
|
||||
|
||||
left_lines.push(Line::from(vec![
|
||||
Span::styled(" Raw Data: ", label_style),
|
||||
Span::styled(raw_info, raw_style),
|
||||
]));
|
||||
|
||||
// Build the title
|
||||
let title = format!(
|
||||
" Signal #{:02} — {} ",
|
||||
capture.id,
|
||||
capture.protocol_name()
|
||||
);
|
||||
|
||||
let border_style = match capture.status {
|
||||
CaptureStatus::EncoderCapable => Style::default().fg(Color::Green),
|
||||
CaptureStatus::Decoded => Style::default().fg(Color::Yellow),
|
||||
CaptureStatus::Unknown => Style::default().fg(Color::DarkGray),
|
||||
};
|
||||
|
||||
let detail = Paragraph::new(left_lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style)
|
||||
.title(title),
|
||||
)
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
frame.render_widget(detail, area);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Command input widget.
|
||||
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::{App, InputMode};
|
||||
|
||||
/// Render the command input line
|
||||
pub fn render_command_line(frame: &mut Frame, area: Rect, app: &App) {
|
||||
let (input_text, mode_text, mode_style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
String::new(),
|
||||
"NORMAL",
|
||||
Style::default().fg(Color::Green),
|
||||
),
|
||||
InputMode::Command => (
|
||||
format!(":{}", app.command_input),
|
||||
"COMMAND",
|
||||
Style::default().fg(Color::Yellow),
|
||||
),
|
||||
InputMode::SignalMenu => (
|
||||
String::new(),
|
||||
"SIGNAL",
|
||||
Style::default().fg(Color::Cyan),
|
||||
),
|
||||
InputMode::SettingsSelect => (
|
||||
String::new(),
|
||||
"SETTINGS",
|
||||
Style::default().fg(Color::Cyan),
|
||||
),
|
||||
InputMode::SettingsEdit => (
|
||||
String::new(),
|
||||
"EDIT",
|
||||
Style::default().fg(Color::Green),
|
||||
),
|
||||
InputMode::StartupImport => (
|
||||
String::new(),
|
||||
"IMPORT",
|
||||
Style::default().fg(Color::Yellow),
|
||||
),
|
||||
InputMode::FobMetaYear
|
||||
| InputMode::FobMetaMake
|
||||
| InputMode::FobMetaModel
|
||||
| InputMode::FobMetaRegion
|
||||
| InputMode::FobMetaNotes => (
|
||||
String::new(),
|
||||
"EXPORT",
|
||||
Style::default().fg(Color::Green),
|
||||
),
|
||||
};
|
||||
|
||||
let input_line = Line::from(vec![
|
||||
Span::styled(
|
||||
format!(" {} ", mode_text),
|
||||
mode_style.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw(input_text),
|
||||
Span::styled(
|
||||
if app.input_mode == InputMode::Command {
|
||||
"█"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
Style::default(),
|
||||
),
|
||||
]);
|
||||
|
||||
let input = Paragraph::new(input_line).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("input"),
|
||||
);
|
||||
|
||||
frame.render_widget(input, area);
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
//! Main UI layout.
|
||||
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::{App, InputMode, RadioState};
|
||||
|
||||
use super::captures_list::render_captures_list;
|
||||
use super::command::render_command_line;
|
||||
use super::settings_menu::{render_settings_dropdown, render_settings_tabs};
|
||||
use super::signal_menu::render_signal_menu;
|
||||
use super::status_bar::render_status_bar;
|
||||
|
||||
use crate::app::InputMode as IM;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Draw the entire UI
|
||||
pub fn draw_ui(frame: &mut Frame, app: &App) {
|
||||
let show_settings = matches!(app.input_mode, IM::SettingsSelect | IM::SettingsEdit);
|
||||
let show_command = app.input_mode == IM::Command;
|
||||
|
||||
let mut constraints = vec![Constraint::Length(3)]; // Header
|
||||
|
||||
if show_settings {
|
||||
constraints.push(Constraint::Length(3)); // Settings tabs
|
||||
}
|
||||
|
||||
constraints.push(Constraint::Min(8)); // Captures list
|
||||
constraints.push(Constraint::Length(3)); // Status bar
|
||||
|
||||
if show_command {
|
||||
constraints.push(Constraint::Length(3)); // Command input
|
||||
}
|
||||
|
||||
constraints.push(Constraint::Length(1)); // Help bar
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(constraints)
|
||||
.split(frame.area());
|
||||
|
||||
let mut idx = 0;
|
||||
render_header(frame, chunks[idx], app);
|
||||
idx += 1;
|
||||
|
||||
if show_settings {
|
||||
render_settings_tabs(frame, chunks[idx], app);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
render_captures_list(frame, chunks[idx], app);
|
||||
idx += 1;
|
||||
|
||||
render_status_bar(frame, chunks[idx], app);
|
||||
idx += 1;
|
||||
|
||||
if show_command {
|
||||
render_command_line(frame, chunks[idx], app);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
render_help_bar(frame, chunks[idx], app);
|
||||
|
||||
// Overlay widgets (rendered on top of everything else)
|
||||
if app.input_mode == InputMode::SignalMenu {
|
||||
render_signal_menu(frame, app);
|
||||
}
|
||||
|
||||
if app.input_mode == InputMode::SettingsEdit {
|
||||
render_settings_dropdown(frame, app);
|
||||
}
|
||||
|
||||
if app.input_mode == InputMode::StartupImport {
|
||||
render_startup_import_prompt(frame, app);
|
||||
}
|
||||
|
||||
if matches!(
|
||||
app.input_mode,
|
||||
InputMode::FobMetaYear
|
||||
| InputMode::FobMetaMake
|
||||
| InputMode::FobMetaModel
|
||||
| InputMode::FobMetaRegion
|
||||
| InputMode::FobMetaNotes
|
||||
) {
|
||||
render_fob_metadata_form(frame, app);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the header with title and radio status
|
||||
fn render_header(frame: &mut Frame, area: Rect, app: &App) {
|
||||
let (status_symbol, status_style) = match app.radio_state {
|
||||
RadioState::Disconnected => ("○", Style::default().fg(Color::DarkGray)),
|
||||
RadioState::Idle => ("○", Style::default().fg(Color::Yellow)),
|
||||
RadioState::Receiving => ("●", Style::default().fg(Color::Green)),
|
||||
RadioState::Transmitting => ("●", Style::default().fg(Color::Red)),
|
||||
};
|
||||
|
||||
let title = format!("KAT v{}", VERSION);
|
||||
|
||||
// Build radio info string with all settings
|
||||
let amp_str = if app.amp_enabled { "ON" } else { "OFF" };
|
||||
let radio_info = format!(
|
||||
"{} {} | {:.2} MHz | LNA:{} VGA:{} AMP:{}",
|
||||
status_symbol,
|
||||
app.radio_state,
|
||||
app.frequency_mhz(),
|
||||
app.lna_gain,
|
||||
app.vga_gain,
|
||||
amp_str
|
||||
);
|
||||
|
||||
// Calculate padding for right-alignment
|
||||
let padding = area
|
||||
.width
|
||||
.saturating_sub(title.len() as u16 + radio_info.len() as u16 + 4);
|
||||
|
||||
let header_line = Line::from(vec![
|
||||
Span::styled(title, Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" ".repeat(padding as usize)),
|
||||
Span::styled(radio_info, status_style),
|
||||
]);
|
||||
|
||||
let header = Paragraph::new(header_line).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default()),
|
||||
);
|
||||
|
||||
frame.render_widget(header, area);
|
||||
}
|
||||
|
||||
/// Render the context-sensitive help bar
|
||||
fn render_help_bar(frame: &mut Frame, area: Rect, app: &App) {
|
||||
let help_text = match app.input_mode {
|
||||
InputMode::Normal => {
|
||||
"Enter: Actions | Tab: Settings | r: RX Toggle | :: Command | q: Quit"
|
||||
}
|
||||
InputMode::Command => "Enter: Execute | Esc: Cancel",
|
||||
InputMode::SignalMenu => "Up/Down: Navigate | Enter: Select | Esc: Close",
|
||||
InputMode::SettingsSelect => "Left/Right: Select | Tab: Cycle | Enter: Edit | Esc: Back",
|
||||
InputMode::SettingsEdit => "Up/Down: Change Value | Enter: Apply | Esc: Cancel",
|
||||
InputMode::StartupImport => "y: Import | n: Skip",
|
||||
InputMode::FobMetaYear
|
||||
| InputMode::FobMetaMake
|
||||
| InputMode::FobMetaModel
|
||||
| InputMode::FobMetaRegion => "Enter: Next Field | Esc: Cancel Export",
|
||||
InputMode::FobMetaNotes => "Enter: Save & Export | Esc: Cancel Export",
|
||||
};
|
||||
|
||||
let help = Paragraph::new(Line::from(Span::styled(
|
||||
format!(" {}", help_text),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
|
||||
frame.render_widget(help, area);
|
||||
}
|
||||
|
||||
/// Center a rect of given width/height in the given area
|
||||
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
|
||||
let x = area.x + area.width.saturating_sub(width) / 2;
|
||||
let y = area.y + area.height.saturating_sub(height) / 2;
|
||||
Rect::new(x, y, width.min(area.width), height.min(area.height))
|
||||
}
|
||||
|
||||
/// Render the startup import prompt overlay
|
||||
fn render_startup_import_prompt(frame: &mut Frame, app: &App) {
|
||||
let count = app.pending_fob_files.len();
|
||||
let area = frame.area();
|
||||
let popup = centered_rect(50, 7, area);
|
||||
|
||||
frame.render_widget(Clear, popup);
|
||||
|
||||
let text = vec![
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
format!("Found {} .fob file(s) in export directory.", count),
|
||||
Style::default().fg(Color::Yellow),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
"Import them? (y/n)",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
];
|
||||
|
||||
let block = Block::default()
|
||||
.title(" Import Saved Signals ")
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan));
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(block)
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
frame.render_widget(paragraph, popup);
|
||||
}
|
||||
|
||||
/// Render the .fob export metadata form overlay with signal summary
|
||||
fn render_fob_metadata_form(frame: &mut Frame, app: &App) {
|
||||
let area = frame.area();
|
||||
let popup = centered_rect(62, 19, area);
|
||||
|
||||
frame.render_widget(Clear, popup);
|
||||
|
||||
let active_style = Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let inactive_style = Style::default().fg(Color::DarkGray);
|
||||
let done_style = Style::default().fg(Color::Green);
|
||||
let value_style = Style::default().fg(Color::White);
|
||||
let dim_style = Style::default().fg(Color::DarkGray);
|
||||
let accent_style = Style::default().fg(Color::Yellow);
|
||||
let cursor = Span::styled(
|
||||
"_",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::RAPID_BLINK),
|
||||
);
|
||||
|
||||
// Determine which field is active
|
||||
let field_modes = [
|
||||
InputMode::FobMetaYear,
|
||||
InputMode::FobMetaMake,
|
||||
InputMode::FobMetaModel,
|
||||
InputMode::FobMetaRegion,
|
||||
InputMode::FobMetaNotes,
|
||||
];
|
||||
let current_idx = field_modes
|
||||
.iter()
|
||||
.position(|m| *m == app.input_mode)
|
||||
.unwrap_or(0);
|
||||
|
||||
let style_for = |idx: usize| -> Style {
|
||||
if idx == current_idx {
|
||||
active_style
|
||||
} else if idx < current_idx {
|
||||
done_style
|
||||
} else {
|
||||
inactive_style
|
||||
}
|
||||
};
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
// --- Signal summary section ---
|
||||
if let Some(capture) = app
|
||||
.export_capture_id
|
||||
.and_then(|id| app.captures.iter().find(|c| c.id == id))
|
||||
{
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" Signal: ", dim_style),
|
||||
Span::styled(
|
||||
format!(
|
||||
"#{:02} {} | {} | {} | 0x{}",
|
||||
capture.id,
|
||||
capture.protocol_name(),
|
||||
capture.frequency_mhz(),
|
||||
capture.modulation(),
|
||||
capture.serial_hex(),
|
||||
),
|
||||
accent_style,
|
||||
),
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" Key: ", dim_style),
|
||||
Span::styled(
|
||||
format!("0x{} ({})", capture.data_hex(), capture.encryption_type()),
|
||||
accent_style,
|
||||
),
|
||||
]));
|
||||
}
|
||||
|
||||
lines.push(Line::from(Span::styled(
|
||||
" ──────────────────────────────────────────────────────",
|
||||
dim_style,
|
||||
)));
|
||||
|
||||
// --- Form fields ---
|
||||
struct FormField<'a> {
|
||||
label: &'a str,
|
||||
value: &'a str,
|
||||
placeholder: &'a str,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
let fields = [
|
||||
FormField {
|
||||
label: " Year: ",
|
||||
value: &app.fob_meta_year,
|
||||
placeholder: "(e.g. 2024)",
|
||||
idx: 0,
|
||||
},
|
||||
FormField {
|
||||
label: " Make: ",
|
||||
value: &app.fob_meta_make,
|
||||
placeholder: "(auto-detected from protocol)",
|
||||
idx: 1,
|
||||
},
|
||||
FormField {
|
||||
label: " Model: ",
|
||||
value: &app.fob_meta_model,
|
||||
placeholder: "(e.g. Sportage, F-150)",
|
||||
idx: 2,
|
||||
},
|
||||
FormField {
|
||||
label: " Region: ",
|
||||
value: &app.fob_meta_region,
|
||||
placeholder: "(e.g. NA, EU, APAC, MEA)",
|
||||
idx: 3,
|
||||
},
|
||||
FormField {
|
||||
label: " Notes: ",
|
||||
value: &app.fob_meta_notes,
|
||||
placeholder: "(optional — color, trim, VIN, etc.)",
|
||||
idx: 4,
|
||||
},
|
||||
];
|
||||
|
||||
for field in &fields {
|
||||
let label_s = style_for(field.idx);
|
||||
let display_val = if field.value.is_empty() {
|
||||
field.placeholder
|
||||
} else {
|
||||
field.value
|
||||
};
|
||||
|
||||
let val_s = if field.value.is_empty() && field.idx != current_idx {
|
||||
dim_style
|
||||
} else {
|
||||
value_style
|
||||
};
|
||||
|
||||
let mut spans = vec![
|
||||
Span::styled(field.label, label_s),
|
||||
Span::styled(display_val.to_string(), val_s),
|
||||
];
|
||||
|
||||
// Show cursor on active field
|
||||
if field.idx == current_idx {
|
||||
spans.push(cursor.clone());
|
||||
}
|
||||
|
||||
// Show checkmark for completed fields with values
|
||||
if field.idx < current_idx && !field.value.is_empty() {
|
||||
spans.push(Span::styled(" ✓", done_style));
|
||||
}
|
||||
|
||||
lines.push(Line::from(spans));
|
||||
}
|
||||
|
||||
lines.push(Line::from(""));
|
||||
|
||||
// Progress indicator
|
||||
let total_fields = fields.len();
|
||||
let progress = format!(
|
||||
" Step {}/{}",
|
||||
current_idx + 1,
|
||||
total_fields,
|
||||
);
|
||||
let hint = if current_idx == total_fields - 1 {
|
||||
" Enter: Save & Export | Esc: Cancel"
|
||||
} else {
|
||||
" Enter: Next | Esc: Cancel"
|
||||
};
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(progress, accent_style),
|
||||
Span::styled(" ", dim_style),
|
||||
Span::styled(hint, dim_style),
|
||||
]));
|
||||
|
||||
let block = Block::default()
|
||||
.title(" Export .fob — Vehicle Details ")
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Green));
|
||||
|
||||
let paragraph = Paragraph::new(lines).block(block);
|
||||
|
||||
frame.render_widget(paragraph, popup);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//! Terminal UI components.
|
||||
|
||||
mod captures_list;
|
||||
mod command;
|
||||
mod layout;
|
||||
pub mod signal_menu;
|
||||
pub mod settings_menu;
|
||||
mod status_bar;
|
||||
|
||||
pub use layout::draw_ui;
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Radio settings inline editor triggered by Tab.
|
||||
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Clear, List, ListItem, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::{App, InputMode, SettingsField, PRESET_FREQUENCIES, LNA_STEPS, VGA_STEPS};
|
||||
|
||||
/// Render the settings selector tabs in the header area
|
||||
pub fn render_settings_tabs(frame: &mut Frame, area: Rect, app: &App) {
|
||||
let mut spans = Vec::new();
|
||||
spans.push(Span::styled(" Settings: ", Style::default().fg(Color::DarkGray)));
|
||||
|
||||
for (i, field) in SettingsField::ALL.iter().enumerate() {
|
||||
let is_selected = app.input_mode == InputMode::SettingsSelect
|
||||
&& i == app.settings_field_index;
|
||||
let is_editing = app.input_mode == InputMode::SettingsEdit
|
||||
&& i == app.settings_field_index;
|
||||
|
||||
let style = if is_editing {
|
||||
Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD)
|
||||
} else if is_selected {
|
||||
Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
|
||||
let value = match field {
|
||||
SettingsField::Freq => format!("{:.2}MHz", app.frequency as f64 / 1_000_000.0),
|
||||
SettingsField::Lna => format!("{}dB", app.lna_gain),
|
||||
SettingsField::Vga => format!("{}dB", app.vga_gain),
|
||||
SettingsField::Amp => if app.amp_enabled { "ON".to_string() } else { "OFF".to_string() },
|
||||
};
|
||||
|
||||
spans.push(Span::styled(
|
||||
format!(" [{}:{}] ", field.label(), value),
|
||||
style,
|
||||
));
|
||||
}
|
||||
|
||||
let line = Line::from(spans);
|
||||
let widget = Paragraph::new(line).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.title(" Radio Settings (Tab) "),
|
||||
);
|
||||
|
||||
frame.render_widget(widget, area);
|
||||
}
|
||||
|
||||
/// Render the settings value dropdown when in SettingsEdit mode
|
||||
pub fn render_settings_dropdown(frame: &mut Frame, app: &App) {
|
||||
if app.input_mode != InputMode::SettingsEdit {
|
||||
return;
|
||||
}
|
||||
|
||||
let area = frame.area();
|
||||
let field = SettingsField::ALL[app.settings_field_index];
|
||||
|
||||
let values: Vec<String> = match field {
|
||||
SettingsField::Freq => PRESET_FREQUENCIES
|
||||
.iter()
|
||||
.map(|(_, label)| label.to_string())
|
||||
.collect(),
|
||||
SettingsField::Lna => LNA_STEPS.iter().map(|g| format!("{} dB", g)).collect(),
|
||||
SettingsField::Vga => VGA_STEPS.iter().map(|g| format!("{} dB", g)).collect(),
|
||||
SettingsField::Amp => vec!["ON".to_string(), "OFF".to_string()],
|
||||
};
|
||||
|
||||
let menu_width = 22u16;
|
||||
let menu_height = (values.len() as u16) + 2; // items + borders
|
||||
|
||||
// Position: below the header, near the field
|
||||
let x_offset = 12 + (app.settings_field_index as u16) * 16;
|
||||
let x = x_offset.min(area.width.saturating_sub(menu_width));
|
||||
let y = 3u16; // Below header
|
||||
|
||||
let menu_area = Rect::new(
|
||||
x,
|
||||
y,
|
||||
menu_width.min(area.width.saturating_sub(x)),
|
||||
menu_height.min(area.height.saturating_sub(y)),
|
||||
);
|
||||
|
||||
frame.render_widget(Clear, menu_area);
|
||||
|
||||
let items: Vec<ListItem> = values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, val)| {
|
||||
let style = if i == app.settings_value_index {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
|
||||
let prefix = if i == app.settings_value_index {
|
||||
"> "
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
|
||||
ListItem::new(Line::from(Span::styled(
|
||||
format!("{}{}", prefix, val),
|
||||
style,
|
||||
)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items).block(
|
||||
Block::default()
|
||||
.title(format!(" {} ", field.label()))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Green)),
|
||||
);
|
||||
|
||||
frame.render_widget(list, menu_area);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Signal action popup menu rendered as a centered overlay.
|
||||
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Clear, List, ListItem},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::{App, SignalAction};
|
||||
|
||||
/// Render the signal action popup menu as a centered overlay
|
||||
pub fn render_signal_menu(frame: &mut Frame, app: &App) {
|
||||
let area = frame.area();
|
||||
|
||||
// Get selected capture info for the header
|
||||
let (capture_info, freq_info) = if let Some(idx) = app.selected_capture {
|
||||
if idx < app.captures.len() {
|
||||
let c = &app.captures[idx];
|
||||
(
|
||||
format!("#{:02} {} | 0x{}", c.id, c.protocol_name(), c.serial_hex()),
|
||||
format!("{} | {}", c.frequency_mhz(), c.modulation()),
|
||||
)
|
||||
} else {
|
||||
("No capture".to_string(), String::new())
|
||||
}
|
||||
} else {
|
||||
("No capture".to_string(), String::new())
|
||||
};
|
||||
|
||||
// Menu dimensions - wider to show more info
|
||||
let menu_width = 38u16;
|
||||
let extra_lines = if freq_info.is_empty() { 0u16 } else { 2u16 };
|
||||
let menu_height = (SignalAction::ALL.len() as u16) + 4 + extra_lines;
|
||||
|
||||
// Center the menu
|
||||
let x = area.width.saturating_sub(menu_width) / 2;
|
||||
let y = area.height.saturating_sub(menu_height) / 2;
|
||||
let menu_area = Rect::new(x, y, menu_width.min(area.width), menu_height.min(area.height));
|
||||
|
||||
// Clear the area behind the popup
|
||||
frame.render_widget(Clear, menu_area);
|
||||
|
||||
// Build list items
|
||||
let mut items: Vec<ListItem> = Vec::new();
|
||||
|
||||
// Add signal info lines at the top if we have capture data
|
||||
if !freq_info.is_empty() {
|
||||
items.push(ListItem::new(Line::from(Span::styled(
|
||||
format!(" {}", freq_info),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
))));
|
||||
items.push(ListItem::new(Line::from(Span::raw(""))));
|
||||
}
|
||||
|
||||
// Add action items
|
||||
for (i, action) in SignalAction::ALL.iter().enumerate() {
|
||||
let style = if i == app.signal_menu_index {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
match action {
|
||||
SignalAction::Delete => Style::default().fg(Color::Red),
|
||||
SignalAction::ExportFob | SignalAction::ExportFlipper => {
|
||||
Style::default().fg(Color::Green)
|
||||
}
|
||||
_ => Style::default().fg(Color::White),
|
||||
}
|
||||
};
|
||||
|
||||
let prefix = if i == app.signal_menu_index {
|
||||
" > "
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
|
||||
items.push(ListItem::new(Line::from(Span::styled(
|
||||
format!("{}{}", prefix, action.label()),
|
||||
style,
|
||||
))));
|
||||
}
|
||||
|
||||
let list = List::new(items).block(
|
||||
Block::default()
|
||||
.title(format!(" {} ", capture_info))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan)),
|
||||
);
|
||||
|
||||
frame.render_widget(list, menu_area);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//! Status bar widget.
|
||||
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
/// Render the status bar with messages and errors
|
||||
pub fn render_status_bar(frame: &mut Frame, area: Rect, app: &App) {
|
||||
let (message, style) = if let Some(ref error) = app.last_error {
|
||||
(
|
||||
format!("Error: {}", error),
|
||||
Style::default().fg(Color::Red),
|
||||
)
|
||||
} else if let Some(ref status) = app.status_message {
|
||||
(status.clone(), Style::default().fg(Color::Green))
|
||||
} else {
|
||||
(
|
||||
format!("Captures: {}", app.captures.len()),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)
|
||||
};
|
||||
|
||||
let status_line = Line::from(vec![Span::styled(message, style)]);
|
||||
|
||||
let status = Paragraph::new(status_line).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("status"),
|
||||
);
|
||||
|
||||
frame.render_widget(status, area);
|
||||
}
|
||||
Reference in New Issue
Block a user