Files
KAT/src/capture.rs
T

333 lines
11 KiB
Rust

//! 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,
/// Protocol-specific extra for encoding (e.g. VAG vag_type + key_idx)
#[serde(default)]
pub data_extra: Option<u64>,
/// Raw level+duration pairs
pub raw_pairs: Vec<StoredLevelDuration>,
/// Current status
pub status: CaptureStatus,
/// Which demodulator path produced this capture (AM or FM). None if unknown/imported.
#[serde(default)]
pub received_rf: Option<RfModulation>,
}
/// Modulation type used by protocol (encoding: PWM vs Manchester)
#[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"),
}
}
}
/// RF modulation (carrier): AM/OOK vs FM/2FSK. From ProtoPirate SubGhzProtocolFlag_AM / _FM.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RfModulation {
AM,
FM,
/// Protocol used with both AM and FM (e.g. Kia V3/V4)
Both,
Unknown,
}
impl std::fmt::Display for RfModulation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RfModulation::AM => write!(f, "AM"),
RfModulation::FM => write!(f, "FM"),
RfModulation::Both => write!(f, "AM/FM"),
RfModulation::Unknown => write!(f, "—"),
}
}
}
impl Capture {
/// Create a new capture from level+duration pairs (received_rf = None).
#[allow(dead_code)] // public API for tests / code that doesn't have receive path
pub fn from_pairs(id: u32, frequency: u32, pairs: Vec<StoredLevelDuration>) -> Self {
Self::from_pairs_with_rf(id, frequency, pairs, None)
}
/// Create a new capture from level+duration pairs and optional receive path (AM/FM).
pub fn from_pairs_with_rf(
id: u32,
frequency: u32,
pairs: Vec<StoredLevelDuration>,
received_rf: Option<RfModulation>,
) -> Self {
Self {
id,
timestamp: Utc::now(),
frequency,
protocol: None,
serial: None,
button: None,
counter: None,
crc_valid: false,
data: 0,
data_count_bit: 0,
data_extra: None,
raw_pairs: pairs,
status: CaptureStatus::Unknown,
received_rf,
}
}
/// 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,
p if p.starts_with("Keeloq (") => ModulationType::Pwm,
"Scher-Khan" => ModulationType::Pwm,
// Unknown
_ => ModulationType::Unknown,
}
}
/// Get the RF modulation (AM/FM) for this protocol. From ProtoPirate SubGhzProtocolFlag_AM / _FM.
/// KAT's demodulator is AM/OOK only; FM protocols may still decode if the signal is strong.
pub fn rf_modulation(&self) -> RfModulation {
match self.protocol_name() {
// FM only (ProtoPirate SubGhzProtocolFlag_FM)
p if p.starts_with("Kia V0") => RfModulation::FM,
p if p.starts_with("Kia V2") => RfModulation::FM,
p if p.starts_with("Kia V5") => RfModulation::FM,
p if p.starts_with("Kia V6") => RfModulation::FM,
"Scher-Khan" => RfModulation::FM,
"PSA" => RfModulation::FM,
"Fiat V0" => RfModulation::FM,
"Ford V0" => RfModulation::FM,
// AM only (SubGhzProtocolFlag_AM)
p if p.starts_with("Kia V1") => RfModulation::AM,
"VAG" => RfModulation::AM,
"Subaru" => RfModulation::AM,
"Suzuki" => RfModulation::AM,
"Star Line" => RfModulation::AM,
p if p.starts_with("Keeloq (") => RfModulation::AM,
// Both AM and FM (Kia V3/V4)
p if p.starts_with("Kia V3") || p.starts_with("Kia V4") => RfModulation::Both,
_ => RfModulation::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",
p if p.starts_with("Keeloq (") => "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,
}
}
}