closer
This commit is contained in:
+50
-39
@@ -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
@@ -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 10–25 ms between button pushes; in-frame gaps are < 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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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 8–55 (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
@@ -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
@@ -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
@@ -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 (380–620µs), long 1000±120 (880–1120µ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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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(""),
|
||||
|
||||
Reference in New Issue
Block a user