This commit is contained in:
KaraZajac
2026-02-17 19:24:03 -05:00
parent b1295a9b86
commit 5944cfa741
49 changed files with 807 additions and 449 deletions
+308 -308
View File
@@ -1,308 +1,308 @@
//! Protocol decoders and encoders for various keyfob systems.
//!
//! Protocols are aligned with the ProtoPirate reference (`REFERENCES/ProtoPirate/protocols/`).
//! Each decoder processes level+duration pairs from the demodulator and optionally supports
//! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64].
//!
//! **Manchester decoding**: Ford, Fiat, and common each have separate Manchester state machines
//! (FordV0ManchesterState, FiatV0ManchesterState, CommonManchesterState in common.rs). They are
//! not reused across protocols. Event conventions match the reference per protocol (e.g. Kia V5
//! opposite polarity; Fiat/Ford/common use Flipper-style: level ? ShortLow : ShortHigh).
mod common;
pub mod keeloq_common;
mod keeloq_generic;
#[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 (Ford before Subaru so 250/500µs Ford keyfobs decode as Ford)
Box::new(ford_v0::FordV0Decoder::new()),
Box::new(subaru::SubaruDecoder::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.
/// Tries normal polarity first, then inverted polarity (so OOK captures where
/// carrier-on is recorded as LOW can still decode as Fiat/Ford etc.).
pub fn process_signal(&mut self, pairs: &[LevelDuration], frequency: u32) -> Option<(String, DecodedSignal)> {
// Try normal polarity first
if let Some(result) = self.process_signal_inner(pairs, frequency, false) {
return Some(result);
}
// Try inverted polarity (capture LOW = RF HIGH)
if let Some(result) = self.process_signal_inner(pairs, frequency, true) {
return Some(result);
}
// No known protocol: try KeeLoq generic (uses keeloq_common with every keystore key)
keeloq_generic::try_decode(pairs, frequency)
}
/// ProtoPirate-style streaming decode: feed the whole stream, on each decode record and reset decoders, continue.
/// Returns one entry per decode: (protocol name, decoded signal, pairs that produced it).
/// Tries normal polarity first; if no decodes, runs again with inverted polarity.
pub fn process_signal_stream(
&mut self,
pairs: &[LevelDuration],
frequency: u32,
) -> Vec<(String, DecodedSignal, Vec<LevelDuration>)> {
let with_normal = self.process_signal_stream_inner(pairs, frequency, false);
if !with_normal.is_empty() {
return with_normal;
}
self.process_signal_stream_inner(pairs, frequency, true)
}
/// Inner streaming decode with optional level inversion.
fn process_signal_stream_inner(
&mut self,
pairs: &[LevelDuration],
frequency: u32,
invert_level: bool,
) -> Vec<(String, DecodedSignal, Vec<LevelDuration>)> {
let mut out = Vec::new();
let mut segment_start = 0_usize;
for decoder in &mut self.decoders {
decoder.reset();
}
for (i, pair) in pairs.iter().enumerate() {
let level = if invert_level { !pair.level } else { pair.level };
let duration_us = pair.duration_us;
let mut hit = None;
for decoder in &mut self.decoders {
let freq_supported = decoder
.supported_frequencies()
.iter()
.any(|&f| {
let diff = if f > frequency { f - frequency } else { frequency - f };
diff < (f / 50)
});
if !freq_supported {
continue;
}
if let Some(decoded) = decoder.feed(level, duration_us) {
hit = Some((decoder.name().to_string(), decoded));
break;
}
}
if let Some((name, decoded)) = hit {
let segment: Vec<LevelDuration> = pairs[segment_start..=i]
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
out.push((name, decoded, segment));
for d in &mut self.decoders {
d.reset();
}
segment_start = i + 1;
}
}
out
}
/// Inner decode: feed pairs (with optional level flip) to decoders that support this frequency.
fn process_signal_inner(
&mut self,
pairs: &[LevelDuration],
frequency: u32,
invert_level: bool,
) -> Option<(String, DecodedSignal)> {
for decoder in &mut self.decoders {
decoder.reset();
}
for pair in pairs {
let level = if invert_level { !pair.level } else { pair.level };
let duration_us = pair.duration_us;
for decoder in &mut self.decoders {
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(level, 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
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::export::flipper::import_sub_raw;
use crate::radio::LevelDuration;
use std::path::Path;
#[test]
fn ford_v0_decodes_imports_ford_unlock_sub() {
let path = Path::new("IMPORTS/FORD/3_unlock_ford.sub");
if !path.exists() {
eprintln!("Skip: {:?} not found (run from crate root)", path);
return;
}
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
let pairs: Vec<LevelDuration> = raw_pairs
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
let mut reg = ProtocolRegistry::new();
let results = reg.process_signal_stream(&pairs, freq);
let ford_decodes: Vec<_> = results.iter().filter(|(name, _, _)| *name == "Ford V0").collect();
assert!(
!ford_decodes.is_empty(),
"expected at least one Ford V0 decode from 3_unlock_ford.sub, got: {:?}",
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
);
}
}
//! Protocol decoders and encoders for various keyfob systems.
//!
//! Protocols are aligned with the ProtoPirate reference (`REFERENCES/ProtoPirate/protocols/`).
//! Each decoder processes level+duration pairs from the demodulator and optionally supports
//! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64].
//!
//! **Manchester decoding**: Ford, Fiat, and common each have separate Manchester state machines
//! (FordV0ManchesterState, FiatV0ManchesterState, CommonManchesterState in common.rs). They are
//! not reused across protocols. Event conventions match the reference per protocol (e.g. Kia V5
//! opposite polarity; Fiat/Ford/common use Flipper-style: level ? ShortLow : ShortHigh).
mod common;
pub mod keeloq_common;
mod keeloq_generic;
#[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 (Ford before Subaru so 250/500µs Ford keyfobs decode as Ford)
Box::new(ford_v0::FordV0Decoder::new()),
Box::new(subaru::SubaruDecoder::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.
/// Tries normal polarity first, then inverted polarity (so OOK captures where
/// carrier-on is recorded as LOW can still decode as Fiat/Ford etc.).
pub fn process_signal(&mut self, pairs: &[LevelDuration], frequency: u32) -> Option<(String, DecodedSignal)> {
// Try normal polarity first
if let Some(result) = self.process_signal_inner(pairs, frequency, false) {
return Some(result);
}
// Try inverted polarity (capture LOW = RF HIGH)
if let Some(result) = self.process_signal_inner(pairs, frequency, true) {
return Some(result);
}
// No known protocol: try KeeLoq generic (uses keeloq_common with every keystore key)
keeloq_generic::try_decode(pairs, frequency)
}
/// ProtoPirate-style streaming decode: feed the whole stream, on each decode record and reset decoders, continue.
/// Returns one entry per decode: (protocol name, decoded signal, pairs that produced it).
/// Tries normal polarity first; if no decodes, runs again with inverted polarity.
pub fn process_signal_stream(
&mut self,
pairs: &[LevelDuration],
frequency: u32,
) -> Vec<(String, DecodedSignal, Vec<LevelDuration>)> {
let with_normal = self.process_signal_stream_inner(pairs, frequency, false);
if !with_normal.is_empty() {
return with_normal;
}
self.process_signal_stream_inner(pairs, frequency, true)
}
/// Inner streaming decode with optional level inversion.
fn process_signal_stream_inner(
&mut self,
pairs: &[LevelDuration],
frequency: u32,
invert_level: bool,
) -> Vec<(String, DecodedSignal, Vec<LevelDuration>)> {
let mut out = Vec::new();
let mut segment_start = 0_usize;
for decoder in &mut self.decoders {
decoder.reset();
}
for (i, pair) in pairs.iter().enumerate() {
let level = if invert_level { !pair.level } else { pair.level };
let duration_us = pair.duration_us;
let mut hit = None;
for decoder in &mut self.decoders {
let freq_supported = decoder
.supported_frequencies()
.iter()
.any(|&f| {
let diff = if f > frequency { f - frequency } else { frequency - f };
diff < (f / 50)
});
if !freq_supported {
continue;
}
if let Some(decoded) = decoder.feed(level, duration_us) {
hit = Some((decoder.name().to_string(), decoded));
break;
}
}
if let Some((name, decoded)) = hit {
let segment: Vec<LevelDuration> = pairs[segment_start..=i]
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
out.push((name, decoded, segment));
for d in &mut self.decoders {
d.reset();
}
segment_start = i + 1;
}
}
out
}
/// Inner decode: feed pairs (with optional level flip) to decoders that support this frequency.
fn process_signal_inner(
&mut self,
pairs: &[LevelDuration],
frequency: u32,
invert_level: bool,
) -> Option<(String, DecodedSignal)> {
for decoder in &mut self.decoders {
decoder.reset();
}
for pair in pairs {
let level = if invert_level { !pair.level } else { pair.level };
let duration_us = pair.duration_us;
for decoder in &mut self.decoders {
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(level, 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
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::export::flipper::import_sub_raw;
use crate::radio::LevelDuration;
use std::path::Path;
#[test]
fn ford_v0_decodes_imports_ford_unlock_sub() {
let path = Path::new("IMPORTS/FORD/3_unlock_ford.sub");
if !path.exists() {
eprintln!("Skip: {:?} not found (run from crate root)", path);
return;
}
let (freq, raw_pairs) = import_sub_raw(path).unwrap();
let pairs: Vec<LevelDuration> = raw_pairs
.iter()
.map(|p| LevelDuration::new(p.level, p.duration_us))
.collect();
let mut reg = ProtocolRegistry::new();
let results = reg.process_signal_stream(&pairs, freq);
let ford_decodes: Vec<_> = results.iter().filter(|(name, _, _)| *name == "Ford V0").collect();
assert!(
!ford_decodes.is_empty(),
"expected at least one Ford V0 decode from 3_unlock_ford.sub, got: {:?}",
results.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>()
);
}
}
+10 -8
View File
@@ -899,14 +899,14 @@ impl ProtocolDecoder for VagDecoder {
TE_SHORT_12 - duration
};
// Reference: (300-duration)<=79 or (duration-300)<80 -> count pair (check_preamble1_prev)
if te_diff < TE_DELTA_12 {
// Check previous pulse
let prev_diff = if self.te_last > TE_SHORT_12 {
self.te_last - TE_SHORT_12
} else {
TE_SHORT_12 - self.te_last
};
if prev_diff <= TE_DELTA_12 {
if prev_diff <= REF_RESET_DELTA {
self.te_last = duration;
self.header_count += 1;
return None;
@@ -915,12 +915,13 @@ impl ProtocolDecoder for VagDecoder {
return None;
}
// Check for gap (end of preamble): 600µs ±79, te_last 300±79 (ref check_gap1)
// Duration not near 300: ref checks for 600µs gap (Preamble1->Data1), then reset
// ref: set step=Reset; if header_count>=201 then duration=|duration-600|; if duration<=79 and te_last 300±79 -> Data1
if self.header_count >= 201 {
let gap_diff = if duration > TE_LONG_12 {
duration - TE_LONG_12
} else {
let gap_diff = if duration < TE_LONG_12 {
TE_LONG_12 - duration
} else {
duration - TE_LONG_12
};
if gap_diff <= REF_GAP1_DELTA {
let prev_diff = if self.te_last > TE_SHORT_12 {
@@ -952,9 +953,10 @@ impl ProtocolDecoder for VagDecoder {
TE_LONG_12 - duration
};
let event = if short_diff <= TE_DELTA_12 {
// Reference Data1: short 300±79 (221..380), long 600±79 (521..680)
let event = if short_diff <= REF_RESET_DELTA {
Some(if level { ManchesterEvent::ShortLow } else { ManchesterEvent::ShortHigh })
} else if long_diff <= TE_DELTA_12 {
} else if long_diff <= REF_RESET_DELTA {
Some(if level { ManchesterEvent::LongLow } else { ManchesterEvent::LongHigh })
} else {
None