This commit is contained in:
leviathan
2026-02-13 21:46:04 -05:00
parent 04b1d91d78
commit b306ff0574
134 changed files with 4060 additions and 458 deletions
+50 -39
View File
@@ -282,13 +282,14 @@ impl App {
let vga_gain = storage.config.default_vga_gain;
let amp_enabled = storage.config.default_amp;
// Scan for .fob and .sub files in the export directory
let mut pending_fob_files = crate::export::fob::scan_fob_files(&storage.config.export_directory);
let sub_files = crate::export::flipper::scan_sub_files(&storage.config.export_directory);
pending_fob_files.extend(sub_files);
pending_fob_files.sort();
// Recursively scan import directory for .fob and .sub at startup (separate from export dir)
let pending_fob_files =
crate::export::scan_import_files_recursive(storage.import_dir());
let initial_mode = if !pending_fob_files.is_empty() {
tracing::info!("Found {} file(s) in export dir", pending_fob_files.len());
tracing::info!(
"Found {} importable file(s) in import dir (recursive)",
pending_fob_files.len()
);
InputMode::StartupImport
} else {
InputMode::Normal
@@ -901,47 +902,55 @@ impl App {
/// Import pending .fob and .sub files into captures list.
/// .sub files are decoded with registered protocols after load (no metadata in file).
/// When research_mode is off, only decoded captures are added (same as live capture).
pub fn import_fob_files(&mut self) -> Result<()> {
let files = std::mem::take(&mut self.pending_fob_files);
let mut imported = 0;
let research_mode = self.storage.config.research_mode;
for path in &files {
let is_sub = path.extension().map_or(false, |e| e == "sub");
if is_sub {
match crate::export::flipper::import_sub(path, self.next_capture_id) {
Ok(captures) => {
for mut capture in captures {
capture.id = self.next_capture_id;
match crate::export::flipper::import_sub_raw(path) {
Ok((frequency, raw_pairs)) => {
let pairs: Vec<crate::radio::LevelDuration> = raw_pairs
.iter()
.map(|p| crate::radio::LevelDuration::new(p.level, p.duration_us))
.collect();
let decoded_list =
self.protocols.process_signal_stream(&pairs, frequency);
for (protocol_name, decoded, segment_pairs) in decoded_list {
let raw: Vec<crate::capture::StoredLevelDuration> = segment_pairs
.iter()
.map(|p| crate::capture::StoredLevelDuration {
level: p.level,
duration_us: p.duration_us,
})
.collect();
let mut capture = crate::capture::Capture::from_pairs_with_rf(
self.next_capture_id,
frequency,
raw,
None,
);
self.next_capture_id += 1;
// .sub has no protocol metadata; run decoder to identify signal
if capture.status == crate::capture::CaptureStatus::Unknown
&& !capture.raw_pairs.is_empty()
{
let pairs: Vec<crate::radio::LevelDuration> = capture
.raw_pairs
.iter()
.map(|p| crate::radio::LevelDuration::new(p.level, p.duration_us))
.collect();
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
};
}
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
};
if research_mode || capture.protocol.is_some() {
self.captures.push(capture);
imported += 1;
}
self.captures.push(capture);
imported += 1;
}
}
Err(e) => tracing::warn!("Failed to import {:?}: {}", path, e),
@@ -976,8 +985,10 @@ impl App {
};
}
}
self.captures.push(capture);
imported += 1;
if research_mode || capture.protocol.is_some() {
self.captures.push(capture);
imported += 1;
}
}
Err(e) => tracing::warn!("Failed to import {:?}: {}", path, e),
}
+12 -50
View File
@@ -1,12 +1,14 @@
//! Flipper Zero .sub export format.
//!
//! Read/write files in the Flipper SubGhz RAW format with alternating
//! positive (high) and negative (low) durations in microseconds.
//! Aligned with ProtoPirate raw_file_reader and sub_decode: same file format
//! (Flipper SubGhz RAW File, Protocol RAW, RAW_Data as int32: positive = HIGH,
//! negative = LOW, duration in µs). Import uses streaming decode: feed the whole
//! stream and reset decoders on each decode (like ProtoPirate sub_decode). Ford
use anyhow::{Context, Result};
use std::path::Path;
use crate::capture::{Capture, CaptureStatus, StoredLevelDuration};
use crate::capture::{Capture, StoredLevelDuration};
/// Export a capture to Flipper Zero .sub RAW format
pub fn export_flipper_sub(capture: &Capture, path: &Path) -> Result<()> {
@@ -48,7 +50,8 @@ pub fn export_flipper_sub(capture: &Capture, path: &Path) -> Result<()> {
Ok(())
}
/// Scan a directory for Flipper .sub files (same format we export).
/// Scan a directory for Flipper .sub files (top-level only). Prefer [crate::export::scan_import_files_recursive] for import.
#[allow(dead_code)]
pub fn scan_sub_files(dir: &Path) -> Vec<std::path::PathBuf> {
let mut out = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
@@ -64,38 +67,10 @@ pub fn scan_sub_files(dir: &Path) -> Vec<std::path::PathBuf> {
out
}
/// Gap duration (µs) used to split a .sub stream into separate transmissions.
/// Keyfobs typically use 1025 ms between button pushes; in-frame gaps are &lt; 1 ms.
pub const SUB_INTER_BURST_GAP_US: u32 = 10_000;
/// Split raw level/duration pairs into segments at long gaps (e.g. between keyfob button pushes).
/// Any pulse (HIGH or LOW) with duration >= `gap_threshold_us` starts a new segment; the long pulse is not included in any segment.
pub fn split_raw_pairs_by_gap(
pairs: &[StoredLevelDuration],
gap_threshold_us: u32,
) -> Vec<Vec<StoredLevelDuration>> {
let mut segments = Vec::new();
let mut current = Vec::new();
for p in pairs {
if p.duration_us >= gap_threshold_us {
if !current.is_empty() {
segments.push(std::mem::take(&mut current));
}
} else {
current.push(*p);
}
}
if !current.is_empty() {
segments.push(current);
}
segments
}
/// Parse a Flipper SubGhz RAW .sub file and return one Capture per transmission.
/// The stream is split at long gaps (see `split_raw_pairs_by_gap`) so multiple button pushes become separate captures.
/// Parse a Flipper SubGhz RAW .sub file into frequency and raw pairs (no splitting).
/// Caller runs streaming decode (e.g. [crate::protocols::ProtocolRegistry::process_signal_stream]) on the pairs.
/// Positive values = HIGH, negative = LOW; duration in microseconds.
pub fn import_sub(path: &Path, next_id: u32) -> Result<Vec<Capture>> {
pub fn import_sub_raw(path: &Path) -> Result<(u32, Vec<StoredLevelDuration>)> {
let s = std::fs::read_to_string(path)
.with_context(|| format!("Read .sub file: {:?}", path))?;
@@ -122,6 +97,7 @@ pub fn import_sub(path: &Path, next_id: u32) -> Result<Vec<Capture>> {
let frequency = frequency_hz.unwrap_or(433_920_000);
// Same convention as ProtoPirate raw_file_reader_get_next: positive => HIGH (true), negative => LOW (false)
let raw_pairs: Vec<StoredLevelDuration> = raw_data
.into_iter()
.map(|v| {
@@ -135,19 +111,5 @@ pub fn import_sub(path: &Path, next_id: u32) -> Result<Vec<Capture>> {
anyhow::bail!("No RAW_Data in .sub file");
}
let segments = split_raw_pairs_by_gap(&raw_pairs, SUB_INTER_BURST_GAP_US);
let captures: Vec<Capture> = segments
.into_iter()
.enumerate()
.map(|(i, pairs)| {
let cap = Capture::from_pairs_with_rf(next_id + i as u32, frequency, pairs, None);
Capture {
status: CaptureStatus::Unknown,
..cap
}
})
.collect();
Ok(captures)
Ok((frequency, raw_pairs))
}
+2 -1
View File
@@ -369,7 +369,8 @@ fn import_fob_v1(fob: &FobFileV1, next_id: u32) -> Result<Capture> {
})
}
/// Scan a directory for .fob files and return their paths
/// Scan a directory for .fob files (top-level only). Prefer [crate::export::scan_import_files_recursive] for import.
#[allow(dead_code)]
pub fn scan_fob_files(dir: &Path) -> Vec<std::path::PathBuf> {
if !dir.exists() || !dir.is_dir() {
return Vec::new();
+33
View File
@@ -1,4 +1,37 @@
//! Export formats for captured signals.
//!
//! Import: use [scan_import_files_recursive] to find all .fob and .sub files under a
//! directory (e.g. EXPORTS/FIAT, EXPORTS/KIA) so that manufacturer subfolders are included.
pub mod fob;
pub mod flipper;
use std::path::{Path, PathBuf};
/// Recursively scan a directory for importable files (.fob and .sub), including all subdirectories.
/// Use this so that EXPORTS/FIAT, EXPORTS/KIA, EXPORTS/FORD, etc. are all discovered.
pub fn scan_import_files_recursive(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
if !dir.exists() || !dir.is_dir() {
return files;
}
walk_for_imports(dir, &mut files);
files.sort();
files
}
fn walk_for_imports(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk_for_imports(&path, out);
} else if path.is_file() {
if path.extension().map_or(false, |e| e == "fob" || e == "sub") {
out.push(path);
}
}
}
}
+46 -8
View File
@@ -1,7 +1,9 @@
//! AUT64 block cipher implementation
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/aut64.c`.
//! Encrypt/decrypt, pack/unpack, and all tables match the reference.
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/aut64.c` and `aut64.h`.
//! Encrypt/decrypt, pack/unpack, tables (table_ln, table_un, table_offset, table_sub), and
//! round logic match the reference. Optional key validation matches `aut64_validate_key` when
//! `AUT64_ENABLE_VALIDATIONS` is set in the reference.
//!
//! AUT64 algorithm: 12 rounds, 8-byte block/key size.
//! See: https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf
@@ -11,10 +13,11 @@ 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;
/// Packed key size in bytes (aut64.h: AUT64_PACKED_KEY_SIZE)
#[allow(dead_code)]
pub const AUT64_KEY_STRUCT_PACKED_SIZE: usize = 16;
/// AUT64 key structure
/// AUT64 key structure (aut64.h: struct aut64_key)
#[derive(Debug, Clone)]
pub struct Aut64Key {
pub index: u8,
@@ -197,7 +200,7 @@ fn permute_bits(key: &Aut64Key, byte: u8) -> u8 {
result
}
/// Compute inverse permutation box
/// Compute inverse permutation box (reference: reverse_box). Used for encrypt key.
fn reverse_box(box_in: &[u8], len: usize) -> Vec<u8> {
let mut reversed = vec![0u8; len];
for i in 0..len {
@@ -211,7 +214,41 @@ fn reverse_box(box_in: &[u8], len: usize) -> Vec<u8> {
reversed
}
/// AUT64 encrypt: 12 rounds of the cipher
/// Validate key: key nibbles in 0..16, pbox permutation of 0..7, sbox permutation of 0..15.
/// Matches `aut64_validate_key` when AUT64_ENABLE_VALIDATIONS is set in the reference.
#[allow(dead_code)]
pub fn aut64_validate_key(key: &Aut64Key) -> bool {
for i in 0..AUT64_KEY_SIZE {
if key.key[i] >= AUT64_SBOX_SIZE as u8 {
return false;
}
}
if !box_is_permutation(&key.pbox, AUT64_PBOX_SIZE) {
return false;
}
if !box_is_permutation(&key.sbox, AUT64_SBOX_SIZE) {
return false;
}
true
}
fn box_is_permutation(box_in: &[u8], len: usize) -> bool {
if box_in.len() < len {
return false;
}
let mut seen = vec![false; len];
for &v in &box_in[..len] {
let v = v as usize;
if v >= len || seen[v] {
return false;
}
seen[v] = true;
}
seen.iter().all(|&b| b)
}
/// Encrypt one 8-byte block in place. Matches `aut64_encrypt` in reference.
/// Message buffer must be at least AUT64_BLOCK_SIZE bytes.
pub fn aut64_encrypt(key: &Aut64Key, message: &mut [u8]) {
// Create reverse key for encryption
let mut reverse_key = key.clone();
@@ -229,7 +266,8 @@ pub fn aut64_encrypt(key: &Aut64Key, message: &mut [u8]) {
}
}
/// AUT64 decrypt: 12 rounds of the cipher (reverse order)
/// Decrypt one 8-byte block in place. Matches `aut64_decrypt` in reference.
/// Message buffer must be at least AUT64_BLOCK_SIZE bytes.
pub fn aut64_decrypt(key: &Aut64Key, message: &mut [u8]) {
for i in (0..AUT64_NUM_ROUNDS).rev() {
message[7] = substitute(key, message[7]);
@@ -240,7 +278,7 @@ pub fn aut64_decrypt(key: &Aut64Key, message: &mut [u8]) {
}
}
/// Pack an AUT64 key structure into a 16-byte array
/// Serialize key into 16-byte packed format. Matches `aut64_pack` (when AUT64_PACK_SUPPORT).
#[allow(dead_code)]
pub fn aut64_pack(src: &Aut64Key) -> [u8; AUT64_KEY_STRUCT_PACKED_SIZE] {
let mut dest = [0u8; AUT64_KEY_STRUCT_PACKED_SIZE];
@@ -265,7 +303,7 @@ pub fn aut64_pack(src: &Aut64Key) -> [u8; AUT64_KEY_STRUCT_PACKED_SIZE] {
dest
}
/// Unpack a 16-byte array into an AUT64 key structure (matches aut64_unpack in reference)
/// Deserialize 16-byte packed key into key structure. Matches `aut64_unpack` in reference.
#[allow(dead_code)]
pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
let mut dest = Aut64Key::default();
+100 -6
View File
@@ -1,11 +1,17 @@
//! Common utilities for protocol implementations.
//!
//! The ProtoPirate reference has `REFERENCES/ProtoPirate/protocols/protocols_common.c`, which
//! only provides Flipper preset name mapping (`protopirate_get_short_preset_name`). KAT does not
//! use that; this module holds shared types and helpers used by multiple protocol decoders.
//! Where applicable, algorithms match the reference: e.g. `crc8_kia` matches `kia_crc8` in
//! kia_v0.c (polynomial 0x7F, init 0x00); `add_bit` matches the common shift-left-and-append
//! pattern used in the reference decoders.
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/protocols_common.c`
//! and `protocols_common.h`. The reference provides only Flipper preset name mapping
//! (`protopirate_get_short_preset_name`); we implement the same mapping in `short_preset_name`.
//! This module also holds shared types and helpers used by multiple protocol decoders.
//!
//! **CRC / add_bit**: `crc8_kia` matches `kia_crc8` in kia_v0.c (polynomial 0x7F, init 0x00);
//! `add_bit` matches the common shift-left-and-append pattern used in the reference decoders.
//!
//! **Manchester**: Ford V0 and Fiat V0 each have their own state machine in their protocol
//! modules. This module provides CommonManchesterState / common_manchester_advance for
//! protocols that use the Flipper-style event mapping (0=ShortLow, 1=ShortHigh, 2=LongLow,
//! 3=LongHigh) and want the shared implementation.
/// Decoded signal information
#[derive(Debug, Clone)]
@@ -74,6 +80,94 @@ pub fn add_bit(data: &mut u64, count: &mut usize, bit: bool) {
*count += 1;
}
// =============================================================================
// Preset name mapping (protocols_common.c: protopirate_get_short_preset_name)
// =============================================================================
/// Short preset name for display. Matches `protopirate_get_short_preset_name`.
/// Returns "UNKNOWN" for null/empty or unknown preset names (reference returns the
/// original pointer for unknown; we use "UNKNOWN" to avoid allocation).
#[allow(dead_code)]
#[inline]
pub fn short_preset_name(preset: Option<&str>) -> &'static str {
let p = match preset {
None => return "UNKNOWN",
Some(s) if s.is_empty() => return "UNKNOWN",
Some(s) => s,
};
match p {
"FuriHalSubGhzPresetOok270Async" => "AM270",
"FuriHalSubGhzPresetOok650Async" => "AM650",
"FuriHalSubGhzPreset2FSKDev238Async" => "FM238",
"FuriHalSubGhzPreset2FSKDev12KAsync" => "FM12K",
"FuriHalSubGhzPreset2FSKDev476Async" => "FM476",
"FuriHalSubGhzPresetCustom" => "CUSTOM",
_ => "UNKNOWN",
}
}
// =============================================================================
// Common Manchester state machine (Flipper lib/toolbox/manchester_decoder)
// =============================================================================
// ProtoPirate Ford and Fiat use Flipper's lib/toolbox/manchester_decoder.h:
// ManchesterState (Mid0, Mid1, Start0, Start1), ManchesterEvent (ShortLow,
// ShortHigh, LongLow, LongHigh, Reset), manchester_advance(state, event, &state, &bit).
// This module provides a separate implementation of the same transition table for
// protocols that want the shared behaviour. Ford and Fiat keep their own
// FordV0ManchesterState and FiatV0ManchesterState in their modules (same table, no reuse).
//
// Event encoding: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh.
// Level mapping in ProtoPirate: level ? ShortLow : ShortHigh (and same for long).
/// Manchester decoder states for the common (Flipper-style) differential Manchester decoder.
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CommonManchesterState {
Mid0 = 0,
Mid1 = 1,
Start0 = 2,
Start1 = 3,
}
/// Advance the common Manchester state machine by one event.
/// Returns `(new_state, Some(bit))` when a data bit is emitted, otherwise `(new_state, None)`.
/// Event: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh (level ? ShortLow : ShortHigh for short/long).
#[allow(dead_code)]
#[inline]
pub fn common_manchester_advance(
state: CommonManchesterState,
event: u8,
) -> (CommonManchesterState, Option<bool>) {
use CommonManchesterState::{Mid0, Mid1, Start0, Start1};
let (new_state, emit) = match (state, event) {
(Mid0, 0) => (Mid0, false),
(Mid0, 1) => (Start1, true),
(Mid0, 2) => (Mid0, false),
(Mid0, 3) => (Mid1, true),
(Mid1, 0) => (Start0, true),
(Mid1, 1) => (Mid1, false),
(Mid1, 2) => (Mid0, true),
(Mid1, 3) => (Mid1, false),
(Start0, 0) => (Mid0, false),
(Start0, 1) => (Mid0, false),
(Start0, 2) => (Mid0, false),
(Start0, 3) => (Mid1, false),
(Start1, 0) => (Mid0, false),
(Start1, 1) => (Mid1, false),
(Start1, 2) => (Mid0, false),
(Start1, 3) => (Mid1, false),
_ => (Mid1, false),
};
let bit = if emit { Some((event & 1) == 1) } else { None };
(new_state, bit)
}
/// Button names for common keyfob buttons
#[allow(dead_code)]
pub fn get_button_name(btn: u8) -> &'static str {
+197 -148
View File
@@ -1,12 +1,12 @@
//! Fiat V0 protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/fiat_v0.c` (Flipper).
//! Decode/encode logic (preamble, gap, Manchester, data/btn extraction, upload waveform) matches reference.
//!
//! Protocol characteristics:
//! - Differential Manchester encoding: 200/400µs timing
//! - 64-bit data (cnt:32 | serial:32) + 6-bit button
//! - 150 preamble pairs (count LOW pulses), 800µs gap, 3 bursts
//! Aligned with older ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/fiat_v0.c` and
//! `fiat_v0.h`. Preamble: count short pulses (HIGH or LOW, 200±100µs); when preamble_count >= 150
//! (0x96), accept 800µs LOW gap (gap_threshold 800, te_delta 100) and enter Data. Data: 64 bits
//! (serial=data_low, cnt=data_high) then 7 more bits; complete when bit_count > 0x46 with
//! btn = (data_low << 1) | 1, 71 bits total. Encoder: differential Manchester, 150 preamble pairs,
//! last LOW replaced by 800µs gap; 64 data bits then 6 btn bits (btn_to_send = btn >> 1); end
//! marker te_short*8 LOW.
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
@@ -19,25 +19,19 @@ const TE_DELTA: u32 = 100;
const MIN_COUNT_BIT: usize = 64;
const PREAMBLE_PAIRS: u16 = 150; // 0x96 in reference
const GAP_US: u32 = 800;
const GAP_THRESHOLD: u32 = 800;
const TOTAL_BURSTS: u8 = 3;
const INTER_BURST_GAP: u32 = 25000;
// After long silence, first HIGH can be merged/wrong (e.g. 658µs); accept 100-700µs to enter Preamble.
const RESET_HIGH_MIN_US: u32 = 100;
const RESET_HIGH_MAX_US: u32 = 700;
// Preamble short LOW: ref 200±100; allow 80-320µs so truncated first LOW (e.g. 99µs) still counts.
const PREAMBLE_SHORT_DELTA: u32 = 120;
/// Manchester state machine states (matches Flipper's manchester_decoder.h, same as Ford V0)
/// Fiat V0 Manchester state machine. Matches Flipper manchester_decoder.h (ref uses it).
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
enum FiatV0ManchesterState {
Mid0 = 0,
Mid1 = 1,
Start0 = 2,
Start1 = 3,
}
/// Decoder states (matches protopirate's FiatV0DecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -45,11 +39,10 @@ enum DecoderStep {
Data,
}
/// Fiat V0 protocol decoder
pub struct FiatV0Decoder {
step: DecoderStep,
preamble_count: u16,
manchester_state: ManchesterState,
manchester_state: FiatV0ManchesterState,
data_low: u32,
data_high: u32,
bit_count: u8,
@@ -64,7 +57,7 @@ impl FiatV0Decoder {
Self {
step: DecoderStep::Reset,
preamble_count: 0,
manchester_state: ManchesterState::Mid1,
manchester_state: FiatV0ManchesterState::Mid1,
data_low: 0,
data_high: 0,
bit_count: 0,
@@ -75,31 +68,29 @@ impl FiatV0Decoder {
}
}
/// Manchester state machine (same as Ford V0 / Flipper manchester_decoder).
/// Event: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh.
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
let (new_state, emit) = match (self.manchester_state, event) {
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
(FiatV0ManchesterState::Mid0, 0) => (FiatV0ManchesterState::Mid0, false),
(FiatV0ManchesterState::Mid0, 1) => (FiatV0ManchesterState::Start1, true),
(FiatV0ManchesterState::Mid0, 2) => (FiatV0ManchesterState::Mid0, false),
(FiatV0ManchesterState::Mid0, 3) => (FiatV0ManchesterState::Mid1, true),
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
(FiatV0ManchesterState::Mid1, 0) => (FiatV0ManchesterState::Start0, true),
(FiatV0ManchesterState::Mid1, 1) => (FiatV0ManchesterState::Mid1, false),
(FiatV0ManchesterState::Mid1, 2) => (FiatV0ManchesterState::Mid0, true),
(FiatV0ManchesterState::Mid1, 3) => (FiatV0ManchesterState::Mid1, false),
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
(FiatV0ManchesterState::Start0, 0) => (FiatV0ManchesterState::Mid0, false),
(FiatV0ManchesterState::Start0, 1) => (FiatV0ManchesterState::Mid0, false),
(FiatV0ManchesterState::Start0, 2) => (FiatV0ManchesterState::Mid0, false),
(FiatV0ManchesterState::Start0, 3) => (FiatV0ManchesterState::Mid1, false),
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
(FiatV0ManchesterState::Start1, 0) => (FiatV0ManchesterState::Mid0, false),
(FiatV0ManchesterState::Start1, 1) => (FiatV0ManchesterState::Mid1, false),
(FiatV0ManchesterState::Start1, 2) => (FiatV0ManchesterState::Mid0, false),
(FiatV0ManchesterState::Start1, 3) => (FiatV0ManchesterState::Mid1, false),
_ => (ManchesterState::Mid1, false),
_ => (FiatV0ManchesterState::Mid1, false),
};
self.manchester_state = new_state;
@@ -111,23 +102,7 @@ impl FiatV0Decoder {
}
fn manchester_reset(&mut self) {
self.manchester_state = ManchesterState::Mid1;
}
/// Add bit to accumulator; at 64 bits extract serial/cnt and clear data (bit_count unchanged in reference).
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;
}
self.manchester_state = FiatV0ManchesterState::Mid1;
}
fn parse_data(&self) -> DecodedSignal {
@@ -137,7 +112,7 @@ impl FiatV0Decoder {
serial: Some(self.serial),
button: Some(self.btn),
counter: Some(self.cnt as u16),
crc_valid: true, // No CRC in Fiat V0
crc_valid: true,
data,
data_count_bit: 71,
encoder_capable: true,
@@ -160,7 +135,7 @@ impl ProtocolDecoder for FiatV0Decoder {
}
fn supported_frequencies(&self) -> &[u32] {
&[433_920_000, 433_880_000] // 433.88 MHz common for Fiat keyfobs
&[433_920_000, 433_880_000]
}
fn reset(&mut self) {
@@ -177,15 +152,18 @@ impl ProtocolDecoder for FiatV0Decoder {
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
let diff_short = if duration < TE_SHORT {
TE_SHORT - duration
} else {
duration - TE_SHORT
};
match self.step {
// Reset: wait for HIGH that starts the burst. Ref: short 200µs; live capture often has
// first HIGH 100-700µs (merged/AGC) and first LOW truncated (e.g. 99µs), so accept range.
DecoderStep::Reset => {
if !level {
return None;
}
let in_range = duration >= RESET_HIGH_MIN_US && duration <= RESET_HIGH_MAX_US;
if in_range {
if diff_short < TE_DELTA {
self.data_low = 0;
self.data_high = 0;
self.step = DecoderStep::Preamble;
@@ -196,63 +174,135 @@ impl ProtocolDecoder for FiatV0Decoder {
}
}
// Preamble: only process LOW pulses (reference: if(level) return). Count short LOWs; gap = 800µs LOW.
// Use PREAMBLE_SHORT_DELTA so first LOW after long gap (e.g. 99µs) counts as short.
DecoderStep::Preamble => {
// Ref: only look at LOW for gap; both HIGH and LOW can count as short
if level {
return None;
}
let short_ok = duration_diff!(duration, TE_SHORT) < PREAMBLE_SHORT_DELTA;
let gap_ok = duration_diff!(duration, GAP_US) < TE_DELTA;
if short_ok {
self.preamble_count += 1;
self.te_last = duration;
} else {
if self.preamble_count >= PREAMBLE_PAIRS && gap_ok {
self.step = DecoderStep::Data;
self.preamble_count = 0;
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
// HIGH pulse
if diff_short < TE_DELTA {
self.preamble_count += 1;
self.te_last = duration;
} else {
self.step = DecoderStep::Reset;
}
return None;
}
// LOW pulse
if duration < TE_SHORT {
let diff = TE_SHORT - duration;
if diff < TE_DELTA {
self.preamble_count += 1;
self.te_last = duration;
if self.preamble_count >= PREAMBLE_PAIRS {
let gap_diff = if duration < GAP_THRESHOLD {
GAP_THRESHOLD - duration
} else {
duration - GAP_THRESHOLD
};
if gap_diff < 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;
self.manchester_reset();
return None;
}
}
} else {
self.step = DecoderStep::Reset;
if self.preamble_count >= PREAMBLE_PAIRS {
let gap_diff = if duration < GAP_THRESHOLD {
GAP_THRESHOLD - duration
} else {
duration - GAP_THRESHOLD
};
if gap_diff < 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;
self.manchester_reset();
return None;
}
}
}
} else {
let diff = duration - TE_SHORT;
if diff < TE_DELTA {
self.preamble_count += 1;
self.te_last = duration;
} else {
self.step = DecoderStep::Reset;
}
if self.preamble_count >= PREAMBLE_PAIRS {
let gap_diff = if duration >= 799 {
duration - GAP_THRESHOLD
} else {
GAP_THRESHOLD - duration
};
if gap_diff < 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;
self.manchester_reset();
return None;
}
}
}
}
// Data: Manchester events — short first, then long (matches reference)
DecoderStep::Data => {
let short_diff = duration_diff!(duration, TE_SHORT);
let long_diff = duration_diff!(duration, TE_LONG);
let event = if short_diff < TE_DELTA {
if level { 0 } else { 1 }
} else if long_diff < TE_DELTA {
if level { 2 } else { 3 }
} else {
self.te_last = duration;
if duration > TE_LONG * 3 {
self.step = DecoderStep::Reset;
let mut event = 4u8; // ManchesterEventReset
if duration < TE_SHORT {
let diff = TE_SHORT - duration;
if diff < TE_DELTA {
event = if level { 0 } else { 1 }; // ShortLow : ShortHigh
}
return None;
};
if let Some(bit) = self.manchester_advance(event) {
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 {
let diff = duration - TE_SHORT;
if diff < TE_DELTA {
event = if level { 0 } else { 1 };
} else {
let long_diff = duration_diff!(duration, TE_LONG);
if long_diff < TE_DELTA {
event = if level { 2 } else { 3 }; // LongLow : LongHigh
}
}
}
if event != 4 {
if let Some(data_bit_bool) = self.manchester_advance(event) {
let new_bit = if data_bit_bool { 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;
}
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);
}
}
}
self.te_last = duration;
}
}
@@ -267,89 +317,88 @@ impl ProtocolDecoder for FiatV0Decoder {
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let cnt = decoded.counter.unwrap_or(0) as u32;
let btn = decoded.button.unwrap_or(0).max(button);
// Ref: data = (cnt<<32)|serial; btn_to_send = btn >> 1 (reverse decoder's (x<<1)|1)
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 btn_to_send = btn >> 1;
let mut signal = Vec::with_capacity(1024);
let te_short = TE_SHORT;
let te_long = TE_LONG;
for burst in 0..TOTAL_BURSTS {
if burst > 0 {
signal.push(LevelDuration::new(false, INTER_BURST_GAP));
}
// Preamble: 150 HIGH-LOW pairs; last LOW is gap (matches reference get_upload)
// Preamble: HIGH-LOW pairs; last LOW replaced by gap (ref)
for i in 0..PREAMBLE_PAIRS {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(true, te_short));
signal.push(LevelDuration::new(
false,
if i == PREAMBLE_PAIRS - 1 { GAP_US } else { TE_SHORT },
if i == PREAMBLE_PAIRS - 1 { GAP_US } else { te_short },
));
}
// First bit (bit 63)
// First bit (bit 63) - differential Manchester
let first_bit = (data >> 63) & 1 == 1;
if first_bit {
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(true, te_long));
} else {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_LONG));
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
// Remaining 63 data bits
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));
}
if !prev_bit && !curr_bit {
signal.push(LevelDuration::new(true, te_short));
signal.push(LevelDuration::new(false, te_short));
} else if !prev_bit && curr_bit {
signal.push(LevelDuration::new(true, te_long));
} else if prev_bit && !curr_bit {
signal.push(LevelDuration::new(false, te_long));
} else {
signal.push(LevelDuration::new(false, te_short));
signal.push(LevelDuration::new(true, te_short));
}
prev_bit = curr_bit;
}
// 6 button bits
// 6 btn bits (ref: for bit 5 down to 0)
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));
}
if !prev_bit && !curr_bit {
signal.push(LevelDuration::new(true, te_short));
signal.push(LevelDuration::new(false, te_short));
} else if !prev_bit && curr_bit {
signal.push(LevelDuration::new(true, te_long));
} else if prev_bit && !curr_bit {
signal.push(LevelDuration::new(false, te_long));
} else {
signal.push(LevelDuration::new(false, te_short));
signal.push(LevelDuration::new(true, te_short));
}
prev_bit = curr_bit;
}
// End marker
// End marker (ref)
if prev_bit {
signal.push(LevelDuration::new(false, TE_SHORT));
signal.push(LevelDuration::new(false, te_short));
}
signal.push(LevelDuration::new(false, TE_SHORT * 8));
signal.push(LevelDuration::new(false, te_short * 8));
}
Some(signal)
}
}
impl Default for FiatV0Decoder {
fn default() -> Self {
Self::new()
}
}
+79 -87
View File
@@ -1,16 +1,12 @@
//! Ford V0 protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v0.c` (Flipper).
//! Decode/encode logic (CRC, BS, decode_ford_v0, encode_ford_v0, upload waveform) matches reference.
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v0.c` and `ford_v0.h`.
//! Ford uses Flipper's lib/toolbox/manchester_decoder.h (ManchesterState, ManchesterEvent,
//! manchester_advance). We use a separate FordV0ManchesterState and the same event mapping:
//! level ? ManchesterEventShortLow : ManchesterEventShortHigh (short/long).
//!
//! Protocol characteristics:
//! - Manchester encoding: 250/500µs timing
//! - 80 bits total (64-bit key1 + 16-bit key2)
//! - Matrix-based CRC in GF(2)
//! - BS (byte swap) magic calculation
//! - 6 bursts, 4 preamble pairs, 3500µs gap
//!
//! Timing matches reference: te_delta 100µs, gap tolerance 250µs.
//! Protocol: 250/500µs Manchester, 80 bits (64 key1 + 16 key2), CRC matrix, BS magic,
//! 6 bursts, 4 preamble pairs, 3500µs gap. te_delta 100µs, gap tolerance 250µs.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
@@ -38,9 +34,10 @@ const CRC_MATRIX: [u8; 64] = [
0x23, 0x12, 0x94, 0x84, 0x35, 0xF4, 0x55, 0x84,
];
/// Manchester state machine states (matches Flipper's manchester_decoder.h)
/// Ford V0 Manchester state machine. Transition table matches Flipper's
/// manchester_decoder.h (ProtoPirate ford_v0.c uses it). Separate from Fiat and common.
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
enum FordV0ManchesterState {
Mid0 = 0,
Mid1 = 1,
Start0 = 2,
@@ -57,15 +54,16 @@ enum DecoderStep {
Data,
}
/// Ford V0 protocol decoder
/// Ford V0 protocol decoder (matches SubGhzProtocolDecoderFordV0)
pub struct FordV0Decoder {
step: DecoderStep,
manchester_state: ManchesterState,
decode_data: u64,
manchester_state: FordV0ManchesterState,
/// Two 64-bit shift registers as in C (ford_v0_add_bit); combined = (data_high<<32)|data_low for key1
data_low: u64,
data_high: u64,
bit_count: u8,
header_count: u16,
te_last: u32,
// Decoded results (stored for encoding)
key1: u64,
key2: u16,
serial: u32,
@@ -78,8 +76,9 @@ impl FordV0Decoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
manchester_state: ManchesterState::Mid1,
decode_data: 0,
manchester_state: FordV0ManchesterState::Mid1,
data_low: 0,
data_high: 0,
bit_count: 0,
header_count: 0,
te_last: 0,
@@ -92,27 +91,26 @@ impl FordV0Decoder {
}
}
/// Add a bit to the accumulator.
/// After 64 bits, key1 is extracted and the accumulator resets for key2.
/// Add a bit (matches ford_v0_add_bit in C exactly)
fn add_bit(&mut self, bit: bool) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
let low = self.data_low as u32;
self.data_low = (self.data_low << 1) | (if bit { 1 } else { 0 });
self.data_high = (self.data_high << 1) | ((low >> 31) & 1) as u64;
self.bit_count += 1;
}
/// Check if we've reached a data milestone (64 or 80 bits).
/// At 64 bits: extract key1 (inverted) and reset accumulator.
/// At 80 bits: extract key2 (inverted), decode fields, return true.
/// Process data at 64 and 80 bits (matches ford_v0_process_data)
fn process_data(&mut self) -> bool {
if self.bit_count == 64 {
// First 64 bits → key1 (bit-inverted)
self.key1 = !self.decode_data;
self.decode_data = 0;
let combined = (self.data_high << 32) | self.data_low;
self.key1 = !combined;
self.data_low = 0;
self.data_high = 0;
return false;
}
if self.bit_count == 80 {
// Next 16 bits → key2 (bit-inverted)
let key2_raw = (self.decode_data & 0xFFFF) as u16;
let key2_raw = (self.data_low & 0xFFFF) as u16;
self.key2 = !key2_raw;
// Decode serial, button, counter, bs_magic from key1+key2
@@ -128,42 +126,36 @@ impl FordV0Decoder {
false
}
/// Manchester state machine (matches Flipper's manchester_advance exactly).
///
/// Event values:
/// 0 = ShortLow (short HIGH pulse → transition to LOW)
/// 1 = ShortHigh (short LOW pulse → transition to HIGH)
/// 2 = LongLow (long HIGH pulse → transition to LOW)
/// 3 = LongHigh (long LOW pulse → transition to HIGH)
///
/// Manchester state machine (Flipper manchester_advance; ProtoPirate ford_v0.c feed).
/// Event: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh. Level mapping: level ? 0/2 : 1/3.
/// Returns Some(bit) when a data bit is produced.
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
let (new_state, emit) = match (self.manchester_state, event) {
// State Mid0: currently in middle of a 0-bit (signal is LOW)
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false), // ShortLow: error, stay
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true), // ShortHigh: emit
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false), // LongLow: error
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true), // LongHigh: emit
(FordV0ManchesterState::Mid0, 0) => (FordV0ManchesterState::Mid0, false), // ShortLow: error, stay
(FordV0ManchesterState::Mid0, 1) => (FordV0ManchesterState::Start1, true), // ShortHigh: emit
(FordV0ManchesterState::Mid0, 2) => (FordV0ManchesterState::Mid0, false), // LongLow: error
(FordV0ManchesterState::Mid0, 3) => (FordV0ManchesterState::Mid1, true), // LongHigh: emit
// State Mid1: currently in middle of a 1-bit (signal is HIGH)
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true), // ShortLow: emit
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false), // ShortHigh: error, stay
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true), // LongLow: emit
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false), // LongHigh: error
(FordV0ManchesterState::Mid1, 0) => (FordV0ManchesterState::Start0, true), // ShortLow: emit
(FordV0ManchesterState::Mid1, 1) => (FordV0ManchesterState::Mid1, false), // ShortHigh: error, stay
(FordV0ManchesterState::Mid1, 2) => (FordV0ManchesterState::Mid0, true), // LongLow: emit
(FordV0ManchesterState::Mid1, 3) => (FordV0ManchesterState::Mid1, false), // LongHigh: error
// State Start0: at start of a 0-bit (signal is HIGH, waiting for H→L)
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false), // ShortLow: complete 0
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false), // error → reset
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false), // error
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false), // error
(FordV0ManchesterState::Start0, 0) => (FordV0ManchesterState::Mid0, false), // ShortLow: complete 0
(FordV0ManchesterState::Start0, 1) => (FordV0ManchesterState::Mid0, false), // error → reset
(FordV0ManchesterState::Start0, 2) => (FordV0ManchesterState::Mid0, false), // error
(FordV0ManchesterState::Start0, 3) => (FordV0ManchesterState::Mid1, false), // error
// State Start1: at start of a 1-bit (signal is LOW, waiting for L→H)
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false), // error
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false), // ShortHigh: complete 1
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false), // error
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false), // error
(FordV0ManchesterState::Start1, 0) => (FordV0ManchesterState::Mid0, false), // error
(FordV0ManchesterState::Start1, 1) => (FordV0ManchesterState::Mid1, false), // ShortHigh: complete 1
(FordV0ManchesterState::Start1, 2) => (FordV0ManchesterState::Mid0, false), // error
(FordV0ManchesterState::Start1, 3) => (FordV0ManchesterState::Mid1, false), // error
_ => (ManchesterState::Mid1, false),
_ => (FordV0ManchesterState::Mid1, false),
};
self.manchester_state = new_state;
@@ -483,6 +475,7 @@ impl FordV0Decoder {
}
/// Get button name for Ford V0
#[allow(dead_code)]
fn button_name(btn: u8) -> &'static str {
match btn {
0x01 => "Lock",
@@ -513,11 +506,12 @@ impl ProtocolDecoder for FordV0Decoder {
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.manchester_state = ManchesterState::Mid1;
self.decode_data = 0;
self.te_last = 0;
self.manchester_state = FordV0ManchesterState::Mid1;
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.header_count = 0;
self.te_last = 0;
self.key1 = 0;
self.key2 = 0;
self.serial = 0;
@@ -528,20 +522,30 @@ impl ProtocolDecoder for FordV0Decoder {
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
// ─── Step 1: Reset — wait for short HIGH pulse ───
// C: level && DURATION_DIFF(duration, te_short) < te_delta → Preamble.
// Also allow level && long so we can re-sync when capture starts mid-preamble.
DecoderStep::Reset => {
if level && duration_diff!(duration, TE_SHORT) < TE_DELTA {
self.decode_data = 0;
self.bit_count = 0;
self.header_count = 0;
self.data_low = 0;
self.data_high = 0;
self.step = DecoderStep::Preamble;
self.te_last = duration;
// Reset Manchester state machine
self.manchester_state = ManchesterState::Mid1;
self.header_count = 0;
self.bit_count = 0;
self.manchester_state = FordV0ManchesterState::Mid1;
} else if level && duration_diff!(duration, TE_LONG) < TE_DELTA {
// Alternative: long HIGH (e.g. preamble) → Preamble so next LOW can sync
self.data_low = 0;
self.data_high = 0;
self.step = DecoderStep::Preamble;
self.te_last = duration;
self.header_count = 0;
self.bit_count = 0;
self.manchester_state = FordV0ManchesterState::Mid1;
}
}
// ─── Step 2: Preamble — wait for long LOW after short HIGH ───
// C: !level, long → PreambleCheck; else → Reset
DecoderStep::Preamble => {
if !level {
if duration_diff!(duration, TE_LONG) < TE_DELTA {
@@ -553,17 +557,14 @@ impl ProtocolDecoder for FordV0Decoder {
}
}
// ─── Step 3: PreambleCheck — count preamble pairs or transition to gap ───
// Order matches protopirate ford_v0.c: check LONG first, then SHORT.
// C: level, long → header_count++, Preamble; level, short → Gap; else → Reset
DecoderStep::PreambleCheck => {
if level {
if duration_diff!(duration, TE_LONG) < TE_DELTA {
// Long HIGH: another preamble pair
self.header_count += 1;
self.te_last = duration;
self.step = DecoderStep::Preamble;
} else if duration_diff!(duration, TE_SHORT) < TE_DELTA {
// Short HIGH: end of preamble, transition to gap
self.step = DecoderStep::Gap;
} else {
self.step = DecoderStep::Reset;
@@ -571,12 +572,11 @@ impl ProtocolDecoder for FordV0Decoder {
}
}
// ─── Step 4: Gap — wait for ~3500µs LOW gap ───
// C: !level && DURATION_DIFF(duration, 3500) < 250 → Data; !level && duration > 3750 → Reset
DecoderStep::Gap => {
if !level && duration_diff!(duration, GAP_US) < GAP_TOLERANCE {
// Gap detected, start data collection
// First bit is implicitly 1 (matches protopirate)
self.decode_data = 1;
self.data_low = 1;
self.data_high = 0;
self.bit_count = 1;
self.step = DecoderStep::Data;
} else if !level && duration > GAP_US + GAP_TOLERANCE {
@@ -584,31 +584,22 @@ impl ProtocolDecoder for FordV0Decoder {
}
}
// ─── Step 5: Data — Manchester decode 80 bits ───
// C: DURATION_DIFF(duration, te_short) < te_delta → short event; te_long → long event
DecoderStep::Data => {
// Map level+duration to Manchester event. Order matches protopirate ford_v0.c:
// check SHORT first, then LONG (so when both within TE_DELTA, short wins).
let short_diff = duration_diff!(duration, TE_SHORT);
let long_diff = duration_diff!(duration, TE_LONG);
let event = if short_diff < TE_DELTA {
if level { 0 } else { 1 } // ShortLow / ShortHigh
} else if long_diff < TE_DELTA {
if level { 2 } else { 3 } // LongLow / LongHigh
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 {
self.step = DecoderStep::Reset;
return None;
};
// Advance Manchester state machine
if let Some(data_bit) = self.manchester_advance(event) {
self.add_bit(data_bit);
if self.process_data() {
// 80 bits decoded — check CRC and return result
let crc_ok = Self::verify_crc(self.key1, self.key2);
let _btn_name = Self::button_name(self.button);
let result = DecodedSignal {
serial: Some(self.serial),
button: Some(self.button),
@@ -619,7 +610,8 @@ impl ProtocolDecoder for FordV0Decoder {
encoder_capable: true,
};
self.decode_data = 0;
self.data_low = 0;
self.data_high = 0;
self.bit_count = 0;
self.step = DecoderStep::Reset;
return Some(result);
+35 -19
View File
@@ -1,8 +1,11 @@
//! KeeLoq common encryption/decryption and learning routines
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/keeloq_common.c`.
//! Shared by Kia V3/V4, Star Line, and other KeeLoq-based protocols.
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/keeloq_common.c` and
//! `keeloq_common.h`. Shared by Kia V3/V4, Star Line, and other KeeLoq-based protocols.
//! NLF (Non-Linear Feedback) constant 0x3A5C742E per reference.
//!
//! Learning types and encrypt/decrypt/normal/magic_* match the reference; secure_learning and
//! faac_learning are from other SubGHz docs and are not present in keeloq_common.c.
/// The KeeLoq NLF constant (KEELOQ_NLF in reference)
const KEELOQ_NLF: u32 = 0x3A5C742E;
@@ -12,13 +15,18 @@ fn bit(x: u32, n: u32) -> u32 {
(x >> n) & 1
}
/// g5(x,a,b,c,d,e) = bit(x,a) + bit(x,b)*2 + ... + bit(x,e)*16 (0..31). Matches reference macro.
#[inline]
fn g5(x: u32, a: u32, b: u32, c: u32, d: u32, e: u32) -> u32 {
bit(x, a) | (bit(x, b) << 1) | (bit(x, c) << 2) | (bit(x, d) << 3) | (bit(x, e) << 4)
}
/// KeeLoq decrypt: 528 rounds (matches subghz_protocol_keeloq_common_decrypt).
/// Key bit for round r is key[(15 - r) & 63]. NLF index g5(x, 0, 8, 19, 25, 30).
/// Simple Learning Decrypt. 528 rounds; key bit for round r is key[(15 - r) & 63]; NLF g5(x, 0, 8, 19, 25, 30).
/// Matches `subghz_protocol_keeloq_common_decrypt`.
///
/// * `data` - keeloq-encrypted 32-bit value
/// * `key` - manufacture key (64-bit)
/// * returns 0xBSSSCCCC: B(4bit) key, S(10bit) serial&0x3FF, C(16bit) counter
pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
let mut x = data;
for r in 0..528u32 {
@@ -30,8 +38,12 @@ pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
x
}
/// KeeLoq encrypt: 528 rounds (matches subghz_protocol_keeloq_common_encrypt).
/// Key bit for round r is key[r & 63]. NLF index g5(x, 1, 9, 20, 26, 31).
/// Simple Learning Encrypt. 528 rounds; key bit for round r is key[r & 63]; NLF g5(x, 1, 9, 20, 26, 31).
/// Matches `subghz_protocol_keeloq_common_encrypt`.
///
/// * `data` - 0xBSSSCCCC (B 4bit key, S 10bit serial&0x3FF, C 16bit counter)
/// * `key` - manufacture key (64-bit)
/// * returns keeloq-encrypted 32-bit value
pub fn keeloq_encrypt(data: u32, key: u64) -> u32 {
let mut x = data;
for r in 0..528u32 {
@@ -43,10 +55,11 @@ pub fn keeloq_encrypt(data: u32, key: u64) -> u32 {
x
}
/// Normal learning key derivation (matches subghz_protocol_keeloq_common_normal_learning).
/// @param data - serial number (28-bit, upper bits ignored)
/// @param key - manufacturer key (64-bit)
/// @return derived key for this serial (64-bit)
/// Normal Learning. Matches `subghz_protocol_keeloq_common_normal_learning`.
///
/// * `data` - serial number (28-bit, upper bits ignored)
/// * `key` - manufacture key (64-bit)
/// * returns derived key for this serial (64-bit)
pub fn keeloq_normal_learning(data: u32, key: u64) -> u64 {
let data = data & 0x0FFFFFFF;
let k1 = keeloq_decrypt(data | 0x20000000, key);
@@ -75,8 +88,7 @@ pub fn reverse8(byte: u8) -> u8 {
b
}
/// Secure learning key derivation
/// Derives a 64-bit key from a serial, seed, and manufacturer key
/// Secure learning key derivation (not in keeloq_common.c; from other SubGHz docs).
#[allow(dead_code)]
pub fn keeloq_secure_learning(data: u32, seed: u32, key: u64) -> u64 {
let serial = data & 0x0FFFFFFF;
@@ -85,8 +97,7 @@ pub fn keeloq_secure_learning(data: u32, seed: u32, key: u64) -> u64 {
((k1 as u64) << 32) | (k2 as u64)
}
/// FAAC SLH (Spa) learning key derivation
/// Derives a 64-bit key from a seed and manufacturer key
/// FAAC SLH (Spa) learning (not in keeloq_common.c; from other SubGHz docs).
#[allow(dead_code)]
pub fn keeloq_faac_learning(seed: u32, key: u64) -> u64 {
let hs = (seed >> 16) as u16;
@@ -95,14 +106,16 @@ pub fn keeloq_faac_learning(seed: u32, key: u64) -> u64 {
((keeloq_encrypt(seed, key) as u64) << 32) | (keeloq_encrypt(lsb, key) as u64)
}
/// Magic XOR Type 1 learning key derivation
/// Magic_xor_type1 Learning. Matches `subghz_protocol_keeloq_common_magic_xor_type1_learning`.
/// * `data` - serial (28-bit) * `xor` - magic xor (64-bit) * returns derived key (64-bit)
#[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
/// Magic_serial_type1 Learning. Matches `subghz_protocol_keeloq_common_magic_serial_type1_learning`.
/// * `data` - serial (28-bit) * `man` - magic man (64-bit) * returns derived key (64-bit)
#[allow(dead_code)]
pub fn keeloq_magic_serial_type1_learning(data: u32, man: u64) -> u64 {
(man & 0xFFFFFFFF)
@@ -110,7 +123,9 @@ pub fn keeloq_magic_serial_type1_learning(data: u32, man: u64) -> u64 {
| (((((data & 0xFF).wrapping_add((data >> 8) & 0xFF)) & 0xFF) as u64) << 32)
}
/// Magic Serial Type 2 learning key derivation
/// Magic_serial_type2 Learning. Matches `subghz_protocol_keeloq_common_magic_serial_type2_learning`.
/// Byte-copy of `data` into high 32 bits of result (LE layout as in reference).
/// * `data` - btn+serial (32-bit) * `man` - magic man (64-bit) * returns derived key (64-bit)
#[allow(dead_code)]
pub fn keeloq_magic_serial_type2_learning(data: u32, man: u64) -> u64 {
let p = data.to_le_bytes();
@@ -122,13 +137,14 @@ pub fn keeloq_magic_serial_type2_learning(data: u32, man: u64) -> u64 {
u64::from_le_bytes(m)
}
/// Magic Serial Type 3 learning key derivation
/// Magic_serial_type3 Learning. Matches `subghz_protocol_keeloq_common_magic_serial_type3_learning`.
/// * `data` - serial (24-bit) * `man` - magic man (64-bit) * returns derived key (64-bit)
#[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
/// KeeLoq learning type constants (keeloq_common.h: KEELOQ_LEARNING_*).
#[allow(dead_code)]
pub mod learning_types {
pub const UNKNOWN: u32 = 0;
+17 -8
View File
@@ -1,8 +1,16 @@
//! Key management module for protocol encryption/decryption
//!
//! Aligned with ProtoPirate's keys.c (KIA_KEY1..4, get_kia_mf_key, etc.).
//! Keys are loaded from the embedded keystore blob in `crate::keystore` at startup,
//! matching the standard encrypted + VAG raw keystore data.
//! Keys are loaded from the embedded keystore blob in `crate::keystore` at startup
//! via `load_keystore_from_embedded()`, which parses the blob from
//! `keystore::embedded::KEYSTORE_BLOB` and populates:
//!
//! - **KIA**: type 10 → kia_mf_key, 11 → kia_v6_a_key, 12 → kia_v6_b_key, 13 → kia_v5_key
//! - **Star Line**: type 20 → star_line_mf_key
//! - **VAG**: raw 64 bytes after "VAG " tag = 4 × 16-byte AUT64 packed keys (index in byte 0 of each)
//!
//! VAG protocol looks up keys by `get_vag_key((key_index + 1) as u8)` (index 1, 2, 3).
//! KIA V5 uses `get_kia_v5_key()`, KIA V6 uses `get_kia_v6_keystore_a()` / `get_kia_v6_keystore_b()`.
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
use configparser::ini::Ini;
@@ -10,11 +18,11 @@ 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
/// Key type identifiers; must match type IDs in `keystore::embedded::KEYSTORE_BLOB`.
const KIA_KEY1: u32 = 10; // kia_mf_key (KIA V3/V4)
const KIA_KEY2: u32 = 11; // kia_v6_a_key (KIA V6A)
const KIA_KEY3: u32 = 12; // kia_v6_b_key (KIA V6B)
const KIA_KEY4: u32 = 13; // kia_v5_key (KIA V5)
const STAR_LINE_KEY: u32 = 20; // star_line_mf_key
/// Maximum number of VAG AUT64 keys (embedded blob has 64 bytes = 4 keys)
@@ -182,7 +190,8 @@ pub fn load_vag_keys(path: &str) {
}
/// Load the global keystore from the embedded blob (src/keystore/embedded.rs).
/// Matches ProtoPirate loading from encrypted + VAG keystore; keys are compiled in.
/// Populates KIA (V3/V4, V5, V6A, V6B), Star Line, and VAG AUT64 keys from the blob.
/// VAG raw bytes are 64 bytes = 4 × 16-byte packed keys; each key's `index` is byte 0 (used by VAG lookup).
pub fn load_keystore_from_embedded() {
let blob = crate::keystore::embedded_blob();
let Some(parsed) = crate::keystore::parse_blob(blob) else {
+21 -18
View File
@@ -1,13 +1,12 @@
//! Kia V0 protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v0.c`.
//! Decode/encode logic (CRC8, preamble/sync, field layout) matches reference.
//! Decoder steps: KIADecoderStepReset → CheckPreambula → SaveDuration → CheckDuration.
//! CRC8 polynomial 0x7F, init 0x00; CRC over bits 855 (6 bytes). min_count_bit_for_found = 61.
//!
//! Protocol characteristics:
//! - PWM encoding: short pulse (250µs) = 0, long pulse (500µs) = 1
//! - 61 bits total (1 sync bit + 60 data bits)
//! - Preamble: alternating short pulses; sync: long-long pattern
//! - Data: 60 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC)
//! Protocol: te_short=250µs, te_long=500µs, te_delta=100µs. PWM: short=0, long=1.
//! Preamble: alternating short pulses; sync: long-long; then 61 bits (1 sync + 60 data).
//! Encoder sends 2 bursts, inter-burst gap 25000µs; encode loop sends 59 bits (mask 58..0) per reference.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use super::common::{crc8_kia, add_bit};
@@ -18,6 +17,8 @@ const TE_SHORT: u32 = 250;
const TE_LONG: u32 = 500;
const TE_DELTA: u32 = 100;
const MIN_COUNT_BIT: usize = 61;
const KIA_TOTAL_BURSTS: u8 = 2;
const KIA_INTER_BURST_GAP_US: u32 = 25000;
/// Decoder states (matches protopirate's KiaV0DecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -155,11 +156,15 @@ impl ProtocolDecoder for KiaV0Decoder {
DecoderStep::SaveDuration => {
if level {
if duration >= TE_LONG + TE_DELTA * 2 {
// End of transmission
// End of transmission (matches C: check count, callback, then clear)
let count = self.decode_count_bit;
let data = self.decode_data;
self.step = DecoderStep::Reset;
self.decode_data = 0;
self.decode_count_bit = 0;
if self.decode_count_bit == MIN_COUNT_BIT {
return Some(Self::parse_data(self.decode_data));
if count == MIN_COUNT_BIT {
return Some(Self::parse_data(data));
}
} else {
self.te_last = duration;
@@ -224,25 +229,23 @@ impl ProtocolDecoder for KiaV0Decoder {
let mut signal = Vec::with_capacity(256);
// Generate 2 bursts
for burst in 0..2 {
for burst in 0..KIA_TOTAL_BURSTS {
if burst > 0 {
// Inter-burst gap
signal.push(LevelDuration::new(false, 25000));
signal.push(LevelDuration::new(false, KIA_INTER_BURST_GAP_US));
}
// Preamble: 32 alternating short pulses
// Preamble: 32 alternating short pulses (matches C subghz_protocol_encoder_kia_get_upload)
for i in 0..32 {
let is_high = (i % 2) == 0;
signal.push(LevelDuration::new(is_high, TE_SHORT));
}
// Sync: long-long (matches protopirate kia_v0 encode)
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_LONG));
// Data: 60 bits MSB first (bits 59..0)
for bit_num in 0..60 {
let bit_mask = 1u64 << (59 - bit_num);
// Data: 59 bits, mask 1ULL << (58 - bit_num) per reference
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 };
@@ -250,7 +253,7 @@ impl ProtocolDecoder for KiaV0Decoder {
signal.push(LevelDuration::new(false, duration));
}
// End marker
// End marker: long * 2
signal.push(LevelDuration::new(true, TE_LONG * 2));
}
+14 -21
View File
@@ -1,14 +1,14 @@
//! Kia V5 protocol decoder (decode-only)
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v5.c`.
//! Decode logic (Manchester polarity, mixer decrypt, YEK, preamble) matches reference.
//! No encoder in protopirate.
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v5.c` and `kia_v5.h`.
//! Decode logic matches reference: constants, steps, Manchester event mapping, mixer_decode, YEK.
//! Encoder exists in reference under ENABLE_EMULATE_FEATURE; KAT keeps decode-only.
//!
//! Protocol characteristics:
//! - Manchester encoding: 400/800µs (opposite polarity to V1/V2: level ? ShortHigh : ShortLow)
//! - Manchester encoding: 400/800µs; V5 polarity: level ? ShortHigh : ShortLow (opposite to V1/V2)
//! - 64 data bits + 3-bit CRC (67 bits on air)
//! - Preamble of ~40+ short/long pairs; then 64-bit key then 3-bit CRC
//! - Custom "mixer" decryption with KIA V5 manufacturer key
//! - Preamble: 40+ short/long pairs; then LONG HIGH (sync), SHORT LOW (alignment), then Manchester data
//! - YEK = bit_reverse_64(key); serial/button/counter from YEK; mixer decryption for counter
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use super::keys;
@@ -145,21 +145,14 @@ impl KiaV5Decoder {
yek
}
/// Manchester state machine
/// NOTE: V5 uses OPPOSITE Manchester polarity from V1/V2.
/// In protopirate, V1/V2 use: level ? ShortLow : ShortHigh
/// But V5 uses: level ? ShortHigh : ShortLow
/// To achieve this inversion, we swap the is_high mapping but use V1's
/// standard state table, so that only ONE inversion occurs.
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
// Invert polarity: V5 uses opposite Manchester convention from V1/V2
let is_high = !is_high;
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
/// Manchester state machine (matches kia_v5.c feed: level ? ManchesterEventShortHigh : ManchesterEventShortLow).
/// Event encoding: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh (Flipper manchester_decoder).
fn manchester_advance(&mut self, is_short: bool, level: bool) -> Option<bool> {
let event = match (is_short, level) {
(true, false) => 0, // ShortLow
(true, true) => 1, // ShortHigh
(false, false) => 2, // LongLow
(false, true) => 3, // LongHigh
};
let (new_state, output) = match (self.manchester_state, event) {
+95 -12
View File
@@ -4,10 +4,10 @@
//! Each decoder processes level+duration pairs from the demodulator and optionally supports
//! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64].
//!
//! **Manchester decoding**: Each protocol that uses Manchester has its own state machine and
//! event mapping (no shared global decoder). Polarity and event conventions match the
//! reference per protocol (e.g. Kia V5 uses opposite polarity to V1/V2; Kia V6 level
//! convention; Fiat/Ford/VAG use level ? ShortLow : ShortHigh).
//! **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;
@@ -93,9 +93,9 @@ impl ProtocolRegistry {
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()),
// 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()),
@@ -108,17 +108,100 @@ impl ProtocolRegistry {
}
/// Process level+duration pairs from demodulator
/// Returns decoded signal info if any protocol matches
/// 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)> {
// Reset all decoders
// 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)
self.process_signal_inner(pairs, frequency, true)
}
/// 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();
}
// Feed pairs to all decoders that support this frequency
for pair in pairs {
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 {
// Check frequency support
let freq_supported = decoder
.supported_frequencies()
.iter()
@@ -131,7 +214,7 @@ impl ProtocolRegistry {
continue;
}
if let Some(decoded) = decoder.feed(pair.level, pair.duration_us) {
if let Some(decoded) = decoder.feed(level, duration_us) {
return Some((decoder.name().to_string(), decoded));
}
}
+40 -35
View File
@@ -1,15 +1,18 @@
//! VAG (VW/Audi/Seat/Skoda) protocol decoder/encoder
//!
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/vag.c`.
//! Decode/encode logic (steps, Type 1/2/3/4, AUT64/TEA, dispatch, gap 6000µs) matches reference.
//! Reset/Preamble2/Sync2/Data2 use reference thresholds (79/80, 380-620/880-1120µs).
//! Preamble1/Data1 use TE_DELTA_12 80 (ref 79/80).
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/vag.c` and `vag.h`.
//! Decoder steps (VAGDecoderStepReset/Preamble1/Data1/Preamble2/Sync2A/B/C/Data2), Type 1/2/3/4
//! parse (vag_parse_data, vag_aut64_decrypt, vag_tea_decrypt), dispatch (0x2A/0x1C/0x46 and
//! 0x2B/0x1D/0x47), and encoder (vag_encoder_build_type1/2/3_4) match the reference.
//!
//! Protocol characteristics:
//! - Manchester encoding; 80 bits (key1 64 + key2 16)
//! - Type 1/2: 300/600µs, prefix 0x2F3F (AUT64) / 0x2F1C (TEA), dispatch 0x2A/0x1C/0x46
//! - Type 3/4: 500/1000µs, preamble 41+ pairs, sync 1000µs + 3×750µs, dispatch 0x2B/0x1D/0x47
//! - End-of-data gap 6000µs (accept within 4000µs); keys from keystore (VAG raw 64 bytes)
//! **Timing**: Reference uses VAG_TOL_300 (79) and VAG_TOL_500 (120). Reset/Preamble1 use 300±79/80;
//! Preamble1→Data1 gap 600µs ±79; Data1 short 300±79/80, long 600±79/80; end-of-data gap 6000µs
//! (accept diff < 4000). Preamble2 count 500±80; Sync2A 500/1000µs ±79; Sync2B 750µs ±79;
//! Sync2C 750µs ±79; Data2 short 500±120 (380620µs), long 1000±120 (8801120µs).
//!
//! **Protocol**: Manchester, 80 bits (key1 64 + key2 16). Type 1/2: 300/600µs, prefix 0xAF3F/0xAF1C.
//! Type 3/4: 500µs, 45 preamble pairs, sync 1000+500 then 3×750µs; key1/key2 not inverted.
//! Button names match reference (vag_button_name): Unlock/Lock/Boot.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use super::aut64;
@@ -29,16 +32,18 @@ const TE_SHORT_12: u32 = 300;
const TE_LONG_12: u32 = 600;
const TE_DELTA_12: u32 = 80; // Preamble1/Data1 (ref vag.c 79/80)
// Reference-aligned deltas (vag.c)
const REF_RESET_DELTA: u32 = 79; // Reset: (300-duration)<=79, 500±79
const REF_PREAMBLE_SYNC: u32 = 80; // Preamble2/Sync2: 500±80, 1000±80, 750±80
const REF_SYNC2C_DELTA: u32 = 79; // Sync2C: diff<=79
// Reference-aligned deltas (vag.c VAG_NEAR / VAG_TOL_300 79, VAG_TOL_500 120)
const REF_RESET_DELTA: u32 = 79; // Reset: 300±79, 500±79 for Preamble2
const REF_PREAMBLE_SYNC: u32 = 80; // Preamble2 counting: 500±80
const REF_SYNC2_AB_DELTA: u32 = 79; // Sync2A/Sync2B: 500/1000/750±79 (ref VAG_NEAR(..., 79))
const REF_SYNC2C_DELTA: u32 = 79; // Sync2C: 750±79
const REF_GAP1_DELTA: u32 = 79; // Preamble1→Data1 gap 600µs ±79 (ref check_gap1)
// TEA constants
const TEA_DELTA: u32 = 0x9E3779B9;
const TEA_ROUNDS: usize = 32;
/// TEA key schedule for VAG (matches vag.c vag_tea_key_schedule)
/// TEA key schedule for VAG (vag.c vag_tea_key_schedule; VAG_TEA_DELTA 0x9E3779B9, 32 rounds)
static TEA_KEY_SCHEDULE: [u32; 4] = [0x0B46502D, 0x5E253718, 0x2BF93A19, 0x622C1206];
/// Manchester states
@@ -218,17 +223,17 @@ impl VagDecoder {
}
}
/// Check if dispatch byte matches Type 1/2
/// Type 1/2 dispatch check (vag.c vag_dispatch_type_1_2)
fn dispatch_type_1_2(dispatch: u8) -> bool {
dispatch == 0x2A || dispatch == 0x1C || dispatch == 0x46
}
/// Check if dispatch byte matches Type 3/4
/// Type 3/4 dispatch check (vag.c vag_dispatch_type_3_4)
fn dispatch_type_3_4(dispatch: u8) -> bool {
dispatch == 0x2B || dispatch == 0x1D || dispatch == 0x47
}
/// Validate decrypted button byte
/// Validate decrypted block button (vag.c vag_button_valid)
fn button_valid(dec: &[u8]) -> bool {
let dec_byte = dec[7];
let dec_btn = (dec_byte >> 4) & 0xF;
@@ -241,7 +246,7 @@ impl VagDecoder {
false
}
/// Check if decrypted button matches dispatch byte
/// Decrypted button vs dispatch (vag.c vag_button_matches)
fn button_matches(dec: &[u8], dispatch_byte: u8) -> bool {
let expected_btn = (dispatch_byte >> 4) & 0xF;
let dec_btn = (dec[7] >> 4) & 0xF;
@@ -254,7 +259,7 @@ impl VagDecoder {
false
}
/// Fill decoded fields from decrypted block
/// Fill decoded fields from decrypted block (vag.c vag_fill_from_decrypted)
fn fill_from_decrypted(&mut self, dec: &[u8], dispatch_byte: u8) {
let serial_raw = (dec[0] as u32)
| ((dec[1] as u32) << 8)
@@ -282,7 +287,7 @@ impl VagDecoder {
}
}
/// Parse key1/key2 and decrypt by type (AUT64/TEA, dispatch); matches vag.c vag_parse_data
/// Parse key1/key2 and decrypt by type (vag.c vag_parse_data)
fn parse_data(&mut self) {
self.decrypted = false;
self.serial = 0;
@@ -435,13 +440,13 @@ impl VagDecoder {
}
}
/// Get button name
/// Get button name (matches vag.c vag_button_name: Unlock/Lock/Boot)
#[allow(dead_code)]
fn get_button_name(btn: u8) -> &'static str {
match btn {
1 => "Unlock",
2 => "Lock",
4 => "Trunk",
1 | 0x10 => "Unlock",
2 | 0x20 => "Lock",
4 | 0x40 => "Boot",
_ => "Unknown",
}
}
@@ -692,7 +697,7 @@ impl VagDecoder {
Some(upload)
}
/// Get dispatch byte from button and type
/// Dispatch byte from button and type (vag.c vag_get_dispatch_byte)
fn get_dispatch_byte(btn: u8, vag_type: u8) -> u8 {
if vag_type == 1 || vag_type == 2 {
match btn {
@@ -711,7 +716,7 @@ impl VagDecoder {
}
}
/// Convert button code to byte for encoding
/// Convert button code to byte for encoding (matches vag.c vag_btn_to_byte)
fn btn_to_byte(btn: u8, vag_type: u8) -> u8 {
if vag_type == 1 {
btn
@@ -720,7 +725,7 @@ impl VagDecoder {
1 => 0x10,
2 => 0x20,
4 => 0x40,
_ => 0x20,
_ => btn, // ref default: return btn
}
}
}
@@ -885,20 +890,20 @@ impl ProtocolDecoder for VagDecoder {
return None;
}
// Check for gap (end of preamble)
// Check for gap (end of preamble): 600µs ±79, te_last 300±79 (ref check_gap1)
if self.header_count >= 201 {
let gap_diff = if duration > TE_LONG_12 {
duration - TE_LONG_12
} else {
TE_LONG_12 - duration
};
if gap_diff <= TE_DELTA_12 {
if gap_diff <= REF_GAP1_DELTA {
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.step = DecoderStep::Data1;
return None;
}
@@ -1037,20 +1042,20 @@ impl ProtocolDecoder for VagDecoder {
}
DecoderStep::Sync2A => {
// Matches vag.c: LOW 500±80 and te_last 1000±80 -> Sync2B
// Matches vag.c: LOW 500±79 and te_last 1000±79 -> Sync2B (VAG_NEAR(..., 79))
if !level {
let diff = if duration < TE_SHORT {
TE_SHORT - duration
} else {
duration - TE_SHORT
};
if diff < REF_PREAMBLE_SYNC {
if diff <= REF_SYNC2_AB_DELTA {
let prev_diff = if self.te_last < TE_LONG {
TE_LONG - self.te_last
} else {
self.te_last - TE_LONG
};
if prev_diff < REF_PREAMBLE_SYNC {
if prev_diff <= REF_SYNC2_AB_DELTA {
self.te_last = duration;
self.step = DecoderStep::Sync2B;
return None;
@@ -1061,14 +1066,14 @@ impl ProtocolDecoder for VagDecoder {
}
DecoderStep::Sync2B => {
// Matches vag.c: HIGH 750±80 -> Sync2C
// Matches vag.c: HIGH 750±79 -> Sync2C (VAG_NEAR(duration, 750, 79))
if level {
let diff = if duration < 750 {
750 - duration
} else {
duration - 750
};
if diff < REF_PREAMBLE_SYNC {
if diff <= REF_SYNC2_AB_DELTA {
self.te_last = duration;
self.step = DecoderStep::Sync2C;
return None;
+39 -3
View File
@@ -5,7 +5,8 @@
//! ```text
//! ~/.config/KAT/
//! config.ini — User configuration
//! exports/ — Exported .fob / .sub files
//! exports/ — Exported .fob / .sub files (save location)
//! import/ — Scanned at startup for .fob / .sub to import
//! keystore/ — Protocol encryption keys
//! keystore.ini — Key definitions (hex values)
//! vag.bin — VAG AUT64 binary key file (optional)
@@ -27,6 +28,8 @@ pub struct Config {
// [general]
/// Directory for exporting signals (.fob / .sub files)
pub export_directory: PathBuf,
/// Directory scanned at startup for .fob and .sub files to import (separate from export)
pub import_directory: PathBuf,
/// Maximum captures to keep in memory during a session
pub max_captures: usize,
/// If off, only successfully decoded signals are added to the list. If on, unknown signals are also shown.
@@ -55,6 +58,7 @@ impl Config {
fn default_for(config_dir: &PathBuf) -> Self {
Self {
export_directory: config_dir.join("exports"),
import_directory: config_dir.join("import"),
max_captures: 100,
research_mode: false,
default_frequency: 433_920_000,
@@ -79,6 +83,11 @@ impl Config {
.map(|s| expand_tilde(&s))
.unwrap_or(defaults.export_directory);
let import_directory = ini
.get("general", "import_directory")
.map(|s| expand_tilde(&s))
.unwrap_or(defaults.import_directory);
let max_captures = ini
.getuint("general", "max_captures")
.ok()
@@ -131,6 +140,7 @@ impl Config {
Ok(Self {
export_directory,
import_directory,
max_captures,
research_mode,
default_frequency,
@@ -145,6 +155,7 @@ impl Config {
/// 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 import_str = self.import_directory.to_string_lossy();
let freq_mhz = self.default_frequency as f64 / 1_000_000.0;
let content = format!(
@@ -159,6 +170,10 @@ impl Config {
; Supports ~ for home directory.
export_directory = {export_dir}
; Directory scanned at startup for .fob and .sub files to import (not used for saving).
; Supports ~ for home directory.
import_directory = {import_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.
@@ -192,6 +207,7 @@ include_raw_pairs = {raw_pairs}
"#,
path = path.display(),
export_dir = export_str,
import_dir = import_str,
max_captures = self.max_captures,
research_mode = self.research_mode,
freq_mhz = freq_mhz,
@@ -315,7 +331,21 @@ impl Storage {
);
}
// ── 5. Ensure keystore directory exists ────────────────────────
// ── 5. Ensure import directory exists ────────────────────────────
if !config.import_directory.exists() {
fs::create_dir_all(&config.import_directory).with_context(|| {
format!(
"Failed to create import dir: {:?}",
config.import_directory
)
})?;
tracing::info!(
"Created import directory: {:?}",
config.import_directory
);
}
// ── 6. Ensure keystore directory exists ────────────────────────
let keystore_dir = config_dir.join("keystore");
if !keystore_dir.exists() {
fs::create_dir_all(&keystore_dir).with_context(|| {
@@ -324,9 +354,10 @@ impl Storage {
tracing::info!("Created keystore directory: {:?}", keystore_dir);
}
// ── 6. Log resolved paths ───────────────────────────────────────
// ── 7. Log resolved paths ───────────────────────────────────────
tracing::info!("Config dir: {:?}", config_dir);
tracing::info!("Export dir: {:?}", config.export_directory);
tracing::info!("Import dir: {:?}", config.import_directory);
tracing::info!("Keystore dir: {:?}", keystore_dir);
Ok(Self {
@@ -357,6 +388,11 @@ impl Storage {
&self.config.export_directory
}
/// Get the import directory path (from config, default `~/.config/KAT/import`)
pub fn import_dir(&self) -> &PathBuf {
&self.config.import_directory
}
/// Get the keystore directory path (`~/.config/KAT/keystore`). Kept for optional file-based override.
#[allow(dead_code)]
pub fn keystore_dir(&self) -> PathBuf {
+1 -1
View File
@@ -76,7 +76,7 @@ fn render_table(frame: &mut Frame, area: Rect, app: &App) {
};
let status_text = match capture.status {
CaptureStatus::EncoderCapable => "✓ Encode",
CaptureStatus::EncoderCapable => "Encode",
CaptureStatus::Decoded => "Decoded",
CaptureStatus::Unknown => "Unknown",
};
+4 -1
View File
@@ -187,7 +187,10 @@ fn render_startup_import_prompt(frame: &mut Frame, app: &App) {
let text = vec![
Line::from(""),
Line::from(Span::styled(
format!("Found {} file(s) (.fob / .sub) in export directory.", count),
format!(
"Found {} file(s) (.fob / .sub) in import dir (incl. subfolders).",
count
),
Style::default().fg(Color::Yellow),
)),
Line::from(""),