This commit is contained in:
leviathan
2026-02-15 14:04:56 -05:00
parent 15b44a572e
commit fd545311c7
5 changed files with 114 additions and 27 deletions
+44 -13
View File
@@ -770,6 +770,16 @@ impl App {
Ok(())
}
/// True if a capture with the same protocol, data, serial, and button already exists.
fn capture_duplicate_of_existing(&self, capture: &Capture) -> bool {
self.captures.iter().any(|c| {
c.protocol == capture.protocol
&& c.data == capture.data
&& c.serial == capture.serial
&& c.button == capture.button
})
}
/// Process pending radio events
pub fn process_radio_events(&mut self) -> Result<()> {
while let Ok(event) = self.radio_event_rx.try_recv() {
@@ -801,18 +811,24 @@ impl App {
// When research_mode is off, only add successfully decoded signals.
let show = self.storage.config.research_mode || capture.protocol.is_some();
if show {
capture.id = self.next_capture_id;
self.next_capture_id += 1;
// Captures are in-memory only — no auto-save to disk.
// Use Export (.fob / .sub) to persist a signal.
self.captures.push(capture);
// Same IQ is fed to AM and FM demodulators; both can emit for one keypress.
// Skip if we already have this exact signal (protocol + data + serial + button).
if self.capture_duplicate_of_existing(&capture) {
self.status_message = Some("Duplicate signal ignored".to_string());
} else {
capture.id = self.next_capture_id;
self.next_capture_id += 1;
// 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();
// 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());
self.status_message = Some("New signal captured".to_string());
}
}
// When research_mode is off and decode failed, the signal is dropped (not shown).
}
@@ -979,7 +995,20 @@ impl App {
.collect();
let decoded_list =
self.protocols.process_signal_stream(&pairs, frequency);
// Deduplicate: same signal can decode at multiple stream positions (e.g. Ford V0 across bursts)
let mut seen: std::collections::HashSet<(String, u64, Option<u32>, Option<u8>)> =
std::collections::HashSet::new();
for (protocol_name, decoded, segment_pairs) in decoded_list {
let key = (
protocol_name.clone(),
decoded.data,
decoded.serial,
decoded.button,
);
if seen.contains(&key) {
continue;
}
seen.insert(key);
let raw: Vec<crate::capture::StoredLevelDuration> = segment_pairs
.iter()
.map(|p| crate::capture::StoredLevelDuration {
@@ -993,7 +1022,6 @@ impl App {
raw,
None,
);
self.next_capture_id += 1;
capture.protocol = Some(protocol_name);
capture.serial = decoded.serial;
capture.button = decoded.button;
@@ -1008,8 +1036,11 @@ impl App {
crate::capture::CaptureStatus::Decoded
};
if research_mode || capture.protocol.is_some() {
self.captures.push(capture);
imported += 1;
if !self.capture_duplicate_of_existing(&capture) {
self.next_capture_id += 1;
self.captures.push(capture);
imported += 1;
}
}
}
}
+26 -13
View File
@@ -14,9 +14,12 @@ use crate::duration_diff;
const TE_SHORT: u32 = 250;
const TE_LONG: u32 = 500;
const TE_DELTA: u32 = 100; // ref ford_v0.c
/// Timing tolerance (µs). Ref ford_v0.c uses 100; real-world captures (e.g. IMPORTS/FORD/3_unlock_ford.sub)
/// can have preamble "long" pulses ~387397µs (103113µs from 500), so we use 120 to decode them.
const TE_DELTA: u32 = 120;
const MIN_COUNT_BIT: usize = 64;
const TOTAL_BURSTS: u8 = 6;
const TX_REPEAT: usize = 1;
const PREAMBLE_PAIRS: usize = 4;
const GAP_US: u32 = 3500;
const GAP_TOLERANCE: u32 = 250; // ref: DURATION_DIFF(duration, gap_threshold) < 250
@@ -584,12 +587,17 @@ impl ProtocolDecoder for FordV0Decoder {
}
}
// C: DURATION_DIFF(duration, te_short) < te_delta → short event; te_long → long event
// C: DURATION_DIFF(duration, te_short) < te_delta → short event; te_long → long event.
// Real-world .sub captures (e.g. IMPORTS/FORD/3_unlock_ford.sub) have inter-burst gaps
// (~51 ms) between the 6 repeats; skip those so we keep collecting bits across bursts.
DecoderStep::Data => {
let event = if duration_diff!(duration, TE_SHORT) < TE_DELTA {
if level { 0 } else { 1 }
} else if duration_diff!(duration, TE_LONG) < TE_DELTA {
if level { 2 } else { 3 }
} else if duration >= 5_000 {
// Inter-burst or long gap: skip without resetting so next burst continues the bit stream
return None;
} else {
self.step = DecoderStep::Reset;
return None;
@@ -633,32 +641,37 @@ impl ProtocolDecoder for FordV0Decoder {
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
// Use the stored counter from last decode (or from decoded signal)
// Increment counter by 1 for the new transmission
let base_counter = if self.counter != 0 {
// Use the same counter as the decoded signal (no increment).
// Reference ford_v0.c encoder uses instance->count from the loaded file as-is;
// replay and different-button TX both use that count so the vehicle accepts it.
let count = (if self.counter != 0 {
self.counter
} else {
decoded.counter.unwrap_or(0) as u32
};
let new_counter = base_counter.wrapping_add(1) & 0xFFFFF; // 20-bit counter
}) & 0xFFFFF; // 20-bit
// Use stored bs_magic (or default to 0x6F for backward compatibility)
let bs_magic = if self.bs_magic != 0 { self.bs_magic } else { 0x6F };
// Calculate BS for the new counter value
let bs = Self::calculate_bs(new_counter, button, bs_magic);
// Calculate BS from count + button + bs_magic (matches C ford_v0_calculate_bs)
let bs = Self::calculate_bs(count, button, bs_magic);
// Extract header byte from the original key1 (first byte)
let header_byte = (decoded.data >> 56) as u8;
// Encode new key1 from fields
let new_key1 = Self::encode_ford_v0(header_byte, serial, button, new_counter, bs);
// Encode key1 from fields (same count, new button)
let new_key1 = Self::encode_ford_v0(header_byte, serial, button, count, bs);
// Calculate CRC for key2
let crc = Self::calculate_crc_for_tx(new_key1, bs);
let new_key2 = ((bs as u16) << 8) | (crc as u16);
// Build the signal upload
Some(Self::build_upload(new_key1, new_key2))
// Build one 6-burst block and repeat TX_REPEAT times (matches reference encoder.repeat = 10)
let single = Self::build_upload(new_key1, new_key2);
let mut signal = Vec::with_capacity(single.len() * TX_REPEAT);
for _ in 0..TX_REPEAT {
signal.extend_from_slice(&single);
}
Some(signal)
}
}
+30
View File
@@ -271,3 +271,33 @@ macro_rules! duration_diff {
}
};
}
#[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<_>>()
);
}
}