Protocol updates

This commit is contained in:
leviathan
2026-02-11 21:59:30 -05:00
parent 0767850a6f
commit 9e2565c1fc
30 changed files with 772 additions and 507 deletions
+7 -6
View File
@@ -1,9 +1,9 @@
//! AUT64 block cipher implementation
//!
//! Ported from protopirate's aut64.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/aut64.c`.
//! Encrypt/decrypt, pack/unpack, and all tables match the reference.
//!
//! AUT64 algorithm: 12 rounds, 8-byte block/key size
//! Based on: Reference AUT64 implementation
//! AUT64 algorithm: 12 rounds, 8-byte block/key size.
//! See: https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf
pub const AUT64_NUM_ROUNDS: usize = 12;
@@ -265,7 +265,7 @@ pub fn aut64_pack(src: &Aut64Key) -> [u8; AUT64_KEY_STRUCT_PACKED_SIZE] {
dest
}
/// Unpack a 16-byte array into an AUT64 key structure
/// Unpack a 16-byte array into an AUT64 key structure (matches aut64_unpack in reference)
#[allow(dead_code)]
pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
let mut dest = Aut64Key::default();
@@ -276,9 +276,10 @@ pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
dest.key[i * 2 + 1] = src[i + 1] & 0xF;
}
let pbox: u32 = ((src[5] as u32) << 16) | ((src[6] as u32) << 8) | src[7] as u32;
let mut pbox: u32 = (u32::from(src[5]) << 16) | (u32::from(src[6]) << 8) | u32::from(src[7]);
for i in (0..dest.pbox.len()).rev() {
dest.pbox[i] = ((pbox >> ((dest.pbox.len() - 1 - i) * 3)) & 0x7) as u8;
dest.pbox[i] = (pbox & 0x7) as u8;
pbox >>= 3;
}
for i in 0..(dest.sbox.len() / 2) {
+13 -6
View File
@@ -1,4 +1,11 @@
//! 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.
/// Decoded signal information
#[derive(Debug, Clone)]
@@ -34,12 +41,12 @@ impl DecodedSignal {
}
}
/// CRC8 calculation with custom polynomial
///
/// CRC8 calculation with custom polynomial (MSB-first, shift-left style).
///
/// # Arguments
/// * `data` - Data bytes to calculate CRC over
/// * `poly` - CRC polynomial
/// * `init` - Initial CRC value
/// * `poly` - CRC polynomial (e.g. 0x7F for Kia)
/// * `init` - Initial CRC value (e.g. 0x00)
pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
let mut crc = init;
for &byte in data {
@@ -55,12 +62,12 @@ pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
crc
}
/// CRC8 for Kia protocol (polynomial 0x7F, init 0x00)
/// CRC8 for Kia protocol (matches kia_v0.c kia_crc8: polynomial 0x7F, init 0x00)
pub fn crc8_kia(data: &[u8]) -> u8 {
crc8(data, 0x7F, 0x00)
}
/// Add a bit to the decoder's data accumulator
/// Add a bit to the decoder's data accumulator (shift-left, LSB last; matches reference add_bit pattern)
#[inline]
pub fn add_bit(data: &mut u64, count: &mut usize, bit: bool) {
*data = (*data << 1) | (bit as u64);
+82 -62
View File
@@ -1,11 +1,12 @@
//! Fiat V0 protocol decoder/encoder
//!
//! Ported from protopirate's fiat_v0.c
//! 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, 800µs gap, 3 bursts
//! - 150 preamble pairs (count LOW pulses), 800µs gap, 3 bursts
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
@@ -16,21 +17,21 @@ const TE_LONG: u32 = 400;
const TE_DELTA: u32 = 100;
#[allow(dead_code)]
const MIN_COUNT_BIT: usize = 64;
const PREAMBLE_PAIRS: u16 = 150;
const PREAMBLE_PAIRS: u16 = 150; // 0x96 in reference
const GAP_US: u32 = 800;
const TOTAL_BURSTS: u8 = 3;
const INTER_BURST_GAP: u32 = 25000;
/// Manchester decoder states
/// Manchester state machine states (matches Flipper's manchester_decoder.h, same as Ford V0)
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
Mid1,
Start0,
Start1,
Mid0 = 0,
Mid1 = 1,
Start0 = 2,
Start1 = 3,
}
/// Decoder states
/// Decoder states (matches protopirate's FiatV0DecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -68,37 +69,46 @@ impl FiatV0Decoder {
}
}
/// Manchester advance - returns decoded bit or None
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
let event = match (is_short, is_high) {
(true, true) => 0,
(true, false) => 1,
(false, true) => 2,
(false, false) => 3,
};
/// 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),
let (new_state, output) = match (self.manchester_state, event) {
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => {
(ManchesterState::Start1, None)
}
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => {
(ManchesterState::Start0, None)
}
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
_ => (ManchesterState::Mid1, None),
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
(ManchesterState::Mid1, 3) => (ManchesterState::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),
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
_ => (ManchesterState::Mid1, false),
};
self.manchester_state = new_state;
output
if emit {
Some((event & 1) == 1)
} else {
None
}
}
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;
@@ -162,6 +172,7 @@ impl ProtocolDecoder for FiatV0Decoder {
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
// Reset: wait for short HIGH (matches reference)
DecoderStep::Reset => {
if !level {
return None;
@@ -177,50 +188,60 @@ impl ProtocolDecoder for FiatV0Decoder {
}
}
// Preamble: only process LOW pulses (reference: if(level) return). Count short LOWs; gap = 800µs LOW.
DecoderStep::Preamble => {
// Count short pulses in preamble, look for gap
if duration_diff!(duration, TE_SHORT) < TE_DELTA {
if level {
return None;
}
let short_ok = duration_diff!(duration, TE_SHORT) < TE_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 {
// Check for gap
if duration_diff!(duration, GAP_US) < TE_DELTA {
} 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;
self.te_last = duration;
return None;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
// Data: Manchester events — short first, then long (matches reference)
DecoderStep::Data => {
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
let short_diff = duration_diff!(duration, TE_SHORT);
let long_diff = duration_diff!(duration, TE_LONG);
if is_short || is_long {
if let Some(bit) = self.manchester_advance(is_short, level) {
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);
}
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;
}
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 if duration > TE_LONG * 3 {
// End of signal
self.step = DecoderStep::Reset;
}
self.te_last = duration;
@@ -249,14 +270,13 @@ impl ProtocolDecoder for FiatV0Decoder {
signal.push(LevelDuration::new(false, INTER_BURST_GAP));
}
// Preamble
// Preamble: 150 HIGH-LOW pairs; last LOW is gap (matches reference get_upload)
for i in 0..PREAMBLE_PAIRS {
signal.push(LevelDuration::new(true, TE_SHORT));
if i < PREAMBLE_PAIRS - 1 {
signal.push(LevelDuration::new(false, TE_SHORT));
} else {
signal.push(LevelDuration::new(false, GAP_US));
}
signal.push(LevelDuration::new(
false,
if i == PREAMBLE_PAIRS - 1 { GAP_US } else { TE_SHORT },
));
}
// First bit (bit 63)
+13 -17
View File
@@ -1,6 +1,7 @@
//! Ford V0 protocol decoder/encoder
//!
//! Ported from protopirate's ford_v0.c
//! 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.
//!
//! Protocol characteristics:
//! - Manchester encoding: 250/500µs timing
@@ -8,6 +9,9 @@
//! - Matrix-based CRC in GF(2)
//! - BS (byte swap) magic calculation
//! - 6 bursts, 4 preamble pairs, 3500µs gap
//!
//! HackRF-specific: `TE_DELTA` (200µs) and `GAP_TOLERANCE` (1500µs) are wider than reference
//! (100µs / 250µs) for software demodulator tolerance.
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
@@ -551,18 +555,16 @@ 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.
DecoderStep::PreambleCheck => {
if level {
let short_diff = duration_diff!(duration, TE_SHORT);
let long_diff = duration_diff!(duration, TE_LONG);
if long_diff < TE_DELTA && long_diff <= short_diff {
// Long HIGH (closer to TE_LONG): another preamble pair
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 short_diff < TE_DELTA {
// Short HIGH (closer to TE_SHORT): end of preamble, transition to gap
} 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;
@@ -585,18 +587,12 @@ impl ProtocolDecoder for FordV0Decoder {
// ─── Step 5: Data — Manchester decode 80 bits ───
DecoderStep::Data => {
// Map level+duration to Manchester event using NEAREST-MATCH.
// With TE_DELTA=200, SHORT(250) and LONG(500) ranges overlap at 300450µs.
// First-match would always pick SHORT for overlapping durations, causing
// bit errors and CRC failure. Nearest-match picks the closer timing.
//
// Tie-break favors LONG (strict < for short_diff) because asymmetric
// demodulation compresses LOWs towards the midpoint (375µs) — these are
// actually LONG pulses that got shortened by threshold bias.
// 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 && short_diff < long_diff {
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
+41 -47
View File
@@ -1,63 +1,57 @@
//! KeeLoq common encryption/decryption routines
//! 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.
//! Based on the NLF (Non-Linear Feedback) function with constant 0x3A5C742E.
//! NLF (Non-Linear Feedback) constant 0x3A5C742E per reference.
/// The KeeLoq NLF constant
/// The KeeLoq NLF constant (KEELOQ_NLF in reference)
const KEELOQ_NLF: u32 = 0x3A5C742E;
/// KeeLoq decrypt: 528 rounds of the KeeLoq cipher (decrypt direction)
#[inline]
fn bit(x: u32, n: u32) -> u32 {
(x >> n) & 1
}
#[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).
pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
let mut block = data;
let mut tkey = key;
for _ in 0..528 {
let lutkey = ((block >> 0) & 1)
| ((block >> 7) & 2)
| ((block >> 17) & 4)
| ((block >> 22) & 8)
| ((block >> 26) & 16);
let lsb = ((block >> 31)
^ ((block >> 15) & 1)
^ ((KEELOQ_NLF >> lutkey) & 1)
^ (((tkey >> 15) & 1) as u32)) as u32;
block = ((block & 0x7FFFFFFF) << 1) | lsb;
tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63);
let mut x = data;
for r in 0..528u32 {
let key_bit = ((key >> ((15 - r) & 63)) & 1) as u32;
let new_lsb = bit(x, 31) ^ bit(x, 15) ^ key_bit
^ bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30));
x = (x << 1) ^ new_lsb;
}
block
x
}
/// KeeLoq encrypt: 528 rounds of the KeeLoq cipher (encrypt direction)
/// 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).
pub fn keeloq_encrypt(data: u32, key: u64) -> u32 {
let mut block = data;
let mut tkey = key;
for _ in 0..528 {
let lutkey = ((block >> 1) & 1)
| ((block >> 8) & 2)
| ((block >> 18) & 4)
| ((block >> 23) & 8)
| ((block >> 27) & 16);
let msb = ((block >> 0)
^ ((block >> 16) & 1)
^ ((KEELOQ_NLF >> lutkey) & 1)
^ (((tkey >> 0) & 1) as u32)) as u32;
block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31);
tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63);
let mut x = data;
for r in 0..528u32 {
let key_bit = ((key >> (r & 63)) & 1) as u32;
let new_msb = bit(x, 0) ^ bit(x, 16) ^ key_bit
^ bit(KEELOQ_NLF, g5(x, 1, 9, 20, 26, 31));
x = (x >> 1) ^ (new_msb << 31);
}
block
x
}
/// Normal learning key derivation
/// Derives a 64-bit key from a 32-bit fix code and a 64-bit manufacturer key
pub fn keeloq_normal_learning(fix: u32, manufacturer_key: u64) -> u64 {
let serial_low = fix & 0xFFFF;
let serial_high = (fix >> 16) & 0xFFFF;
let key_low = keeloq_decrypt(serial_low as u32 | 0x20000000, manufacturer_key);
let key_high = keeloq_decrypt(serial_high as u32 | 0x60000000, manufacturer_key);
((key_high as u64) << 32) | (key_low as u64)
/// 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)
pub fn keeloq_normal_learning(data: u32, key: u64) -> u64 {
let data = data & 0x0FFFFFFF;
let k1 = keeloq_decrypt(data | 0x20000000, key);
let k2 = keeloq_decrypt(data | 0x60000000, key);
((k2 as u64) << 32) | (k1 as u64)
}
/// Reverse the bits in a 64-bit key (for protocols that store data MSB-first)
+29 -19
View File
@@ -1,16 +1,8 @@
//! Key management module for protocol encryption/decryption
//!
//! Ported from protopirate's keys.c
//!
//! Manages manufacturer keys used by various protocols:
//! - KIA V3/V4: kia_mf_key (manufacturer key for KeeLoq)
//! - KIA V5: kia_v5_key (custom mixer cipher key)
//! - KIA V6: kia_v6_a_key, kia_v6_b_key (AES-128 XOR mask keys)
//! - Star Line: star_line_mf_key (manufacturer key for KeeLoq)
//! - VAG: AUT64 keys loaded from keystore files
//!
//! Keys are loaded from `~/.config/KAT/keystore/keystore.ini` at startup.
//! See [`load_keystore_from_dir`] for the file format.
//! 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.
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
use configparser::ini::Ini;
@@ -25,8 +17,8 @@ const KIA_KEY3: u32 = 12; // kia_v6_b_key
const KIA_KEY4: u32 = 13; // kia_v5_key
const STAR_LINE_KEY: u32 = 20; // star_line_mf_key
/// Maximum number of VAG AUT64 keys
const VAG_KEYS_COUNT: usize = 3;
/// Maximum number of VAG AUT64 keys (embedded blob has 64 bytes = 4 keys)
const MAX_VAG_KEYS: usize = 4;
/// Global key store - thread-safe access to loaded keys
pub struct KeyStore {
@@ -81,22 +73,20 @@ impl KeyStore {
}
}
/// Load VAG AUT64 keys from raw binary data
/// The data should contain packed AUT64 key structures (16 bytes each)
/// Load VAG AUT64 keys from raw binary data (16 bytes per key; up to MAX_VAG_KEYS)
pub fn load_vag_keys_from_data(&mut self, data: &[u8]) {
if self.vag_keys_loaded {
return;
}
self.vag_keys.clear();
let n = (data.len() / AUT64_KEY_STRUCT_PACKED_SIZE).min(MAX_VAG_KEYS);
for i in 0..VAG_KEYS_COUNT {
for i in 0..n {
let offset = i * AUT64_KEY_STRUCT_PACKED_SIZE;
if offset + AUT64_KEY_STRUCT_PACKED_SIZE > data.len() {
error!("VAG key data too short for key {}", i);
break;
}
let key = aut64::aut64_unpack(&data[offset..offset + AUT64_KEY_STRUCT_PACKED_SIZE]);
self.vag_keys.push(key);
}
@@ -179,7 +169,7 @@ pub fn get_keystore_mut() -> std::sync::RwLockWriteGuard<'static, KeyStore> {
global_keystore().write().unwrap()
}
/// Initialize the global keystore with KIA keys
/// Initialize the global keystore with KIA keys (matches protopirate_keys_load pattern)
pub fn load_keys(kia_entries: &[(u32, u64)]) {
let mut store = get_keystore_mut();
store.load_kia_keys(kia_entries);
@@ -191,6 +181,26 @@ pub fn load_vag_keys(path: &str) {
store.load_vag_keys_from_file(path);
}
/// Load the global keystore from the embedded blob (src/keystore/embedded.rs).
/// Matches ProtoPirate loading from encrypted + VAG keystore; keys are compiled in.
pub fn load_keystore_from_embedded() {
let blob = crate::keystore::embedded_blob();
let Some(parsed) = crate::keystore::parse_blob(blob) else {
error!("Failed to parse embedded keystore blob");
return;
};
let mut store = get_keystore_mut();
store.load_kia_keys(&parsed.entries);
if !parsed.vag_bytes.is_empty() {
store.load_vag_keys_from_data(&parsed.vag_bytes);
}
info!(
"Keystore loaded from embedded blob ({} entries, {} VAG keys)",
parsed.entries.len(),
store.vag_keys.len()
);
}
// =============================================================================
// Keystore file loading from ~/.config/KAT/keystore/
// =============================================================================
+19 -13
View File
@@ -1,13 +1,13 @@
//! Kia V0 protocol decoder
//! 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.
//!
//! Ported from protopirate's kia_v0.c
//!
//! Protocol characteristics:
//! - PWM encoding: short pulse (250µs) = 0, long pulse (500µs) = 1
//! - 61 bits total
//! - Preamble: alternating short pulses
//! - Sync: long-long pattern
//! - Data: 59 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC)
//! - 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)
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use super::common::{crc8_kia, add_bit};
@@ -19,7 +19,7 @@ const TE_LONG: u32 = 500;
const TE_DELTA: u32 = 100;
const MIN_COUNT_BIT: usize = 61;
/// Decoder states
/// Decoder states (matches protopirate's KiaV0DecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -48,7 +48,7 @@ impl KiaV0Decoder {
}
}
/// Calculate CRC for Kia data packet
/// CRC8 for Kia data packet (matches kia_v0.c kia_crc8: polynomial 0x7F, init 0x00)
fn calculate_crc(data: u64) -> u8 {
let crc_data = [
((data >> 48) & 0xFF) as u8,
@@ -236,13 +236,13 @@ impl ProtocolDecoder for KiaV0Decoder {
signal.push(LevelDuration::new(is_high, TE_SHORT));
}
// Sync: long-long
// Sync: long-long (matches protopirate kia_v0 encode)
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_LONG));
// Data: 59 bits (MSB first)
for bit_num in 0..59 {
let bit_mask = 1u64 << (58 - bit_num);
// Data: 60 bits MSB first (bits 59..0)
for bit_num in 0..60 {
let bit_mask = 1u64 << (59 - bit_num);
let bit = (data & bit_mask) != 0;
let duration = if bit { TE_LONG } else { TE_SHORT };
@@ -257,3 +257,9 @@ impl ProtocolDecoder for KiaV0Decoder {
Some(signal)
}
}
impl Default for KiaV0Decoder {
fn default() -> Self {
Self::new()
}
}
+19 -12
View File
@@ -1,12 +1,13 @@
//! Kia V1 protocol decoder
//! Kia V1 protocol decoder/encoder
//!
//! Ported from protopirate's kia_v1.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v1.c`.
//! Decode/encode logic (Manchester, CRC4, preamble, field layout) matches reference.
//!
//! Protocol characteristics:
//! - Manchester encoding: 800/1600µs timing
//! - 57 bits total
//! - Long preamble of ~90 pulses
//! - CRC4 checksum
//! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC)
//! - Long preamble of ~90 long pairs
//! - CRC4 checksum with offset rules (cnt_high 0 / >= 6)
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
@@ -17,7 +18,7 @@ const TE_LONG: u32 = 1600;
const TE_DELTA: u32 = 200;
const MIN_COUNT_BIT: usize = 57;
/// Manchester states
/// Manchester decoder states (matches protopirate kia_v1 Manchester state machine)
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
@@ -26,7 +27,7 @@ enum ManchesterState {
Start1,
}
/// Decoder states
/// Decoder states (matches protopirate's KiaV1DecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -56,7 +57,7 @@ impl KiaV1Decoder {
}
}
/// CRC4 calculation for Kia V1
/// CRC4 for Kia V1 (matches kia_v1.c: XOR nibbles + offset)
fn crc4(bytes: &[u8], offset: u8) -> u8 {
let mut crc: u8 = 0;
for &byte in bytes {
@@ -97,7 +98,7 @@ impl KiaV1Decoder {
fn parse_data(&self) -> DecodedSignal {
let data = self.decode_data;
// Extract fields per kia_v1.c
// Field layout matches kia_v1.c: serial(32) | button(8) | cnt_low(8) | cnt_high(4) | crc(4)
let serial = (data >> 24) as u32;
let button = ((data >> 16) & 0xFF) as u8;
let cnt_low = ((data >> 8) & 0xFF) as u16;
@@ -263,7 +264,7 @@ impl ProtocolDecoder for KiaV1Decoder {
let mut signal = Vec::with_capacity(600);
// Generate 3 bursts
// Generate 3 bursts (matches protopirate kia_v1 encode)
for burst in 0..3 {
if burst > 0 {
signal.push(LevelDuration::new(false, 25000));
@@ -275,10 +276,10 @@ impl ProtocolDecoder for KiaV1Decoder {
signal.push(LevelDuration::new(true, TE_LONG));
}
// Short gap
// Short gap before data
signal.push(LevelDuration::new(false, TE_SHORT));
// Data: Manchester encoded, MSB first
// Data: 57 bits Manchester encoded, MSB first
for bit_num in (1..MIN_COUNT_BIT).rev() {
let bit = ((data >> (bit_num - 1)) & 1) == 1;
if bit {
@@ -294,3 +295,9 @@ impl ProtocolDecoder for KiaV1Decoder {
Some(signal)
}
}
impl Default for KiaV1Decoder {
fn default() -> Self {
Self::new()
}
}
+20 -13
View File
@@ -1,12 +1,13 @@
//! Kia V2 protocol decoder
//! Kia V2 protocol decoder/encoder
//!
//! Ported from protopirate's kia_v2.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v2.c`.
//! Decode/encode logic (Manchester, CRC4, preamble, byte-swapped counter) matches reference.
//!
//! Protocol characteristics:
//! - Manchester encoding: 500/1000µs timing
//! - 53 bits total
//! - Long preamble of 252+ pairs
//! - CRC4 checksum
//! - 53 bits total (32 serial + 4 button + 12 counter + 4 CRC, plus start bit)
//! - Long preamble of 252 long pairs
//! - CRC4 checksum (XOR nibbles + offset 1)
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
@@ -17,7 +18,7 @@ const TE_LONG: u32 = 1000;
const TE_DELTA: u32 = 150;
const MIN_COUNT_BIT: usize = 53;
/// Manchester states
/// Manchester decoder states (matches protopirate kia_v2 Manchester state machine)
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
@@ -26,7 +27,7 @@ enum ManchesterState {
Start1,
}
/// Decoder states
/// Decoder states (matches protopirate's KiaV2DecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -56,7 +57,7 @@ impl KiaV2Decoder {
}
}
/// Calculate CRC for Kia V2
/// CRC4 for Kia V2 (matches kia_v2.c: 6-byte permuted input, XOR nibbles, offset 1)
fn calculate_crc(data: u64) -> u8 {
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
let u_var4 = (data & 0xFFFFFFFF) as u32;
@@ -105,10 +106,10 @@ impl KiaV2Decoder {
output
}
/// Parse decoded data
/// Parse decoded data (field layout matches kia_v2.c)
fn parse_data(&self) -> DecodedSignal {
let data = self.decode_data;
// serial(32) | button(4) | counter_swapped(12) | crc(4); counter byte-swapped in stream
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
let button = ((data >> 16) & 0x0F) as u8;
@@ -246,7 +247,7 @@ impl ProtocolDecoder for KiaV2Decoder {
let mut signal = Vec::with_capacity(700);
// Generate 2 bursts
// Generate 2 bursts (matches protopirate kia_v2 encode)
for _burst in 0..2 {
// Preamble: 252 long pairs
for _ in 0..252 {
@@ -254,10 +255,10 @@ impl ProtocolDecoder for KiaV2Decoder {
signal.push(LevelDuration::new(true, TE_LONG));
}
// Short gap
// Short gap before data
signal.push(LevelDuration::new(false, TE_SHORT));
// Data: Manchester encoded, MSB first
// Data: 53 bits Manchester encoded, MSB first
for bit_num in (1..MIN_COUNT_BIT).rev() {
let bit = ((new_data >> (bit_num - 1)) & 1) == 1;
if bit {
@@ -273,3 +274,9 @@ impl ProtocolDecoder for KiaV2Decoder {
Some(signal)
}
}
impl Default for KiaV2Decoder {
fn default() -> Self {
Self::new()
}
}
+25 -60
View File
@@ -1,15 +1,16 @@
//! Kia V3/V4 protocol decoder
//! Kia V3/V4 protocol decoder/encoder
//!
//! Ported from protopirate's kia_v3_v4.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v3_v4.c`.
//! Decode/encode logic (PWM, preamble, sync polarity, KeeLoq, CRC4, byte order) matches reference.
//!
//! Protocol characteristics:
//! - PWM encoding: 400/800µs timing
//! - 68 bits total
//! - Short preamble of 16 pairs
//! - KeeLoq encryption (requires manufacturer key)
//! - V3 and V4 differ only in sync polarity
//! - PWM encoding: 400/800µs (short=0, long=1)
//! - 68 bits total (8 bytes encrypted + 4 bits CRC)
//! - Short preamble of 16 pairs; sync 1200µs (V4: long HIGH, V3: long LOW)
//! - KeeLoq encryption (KIA manufacturer key); V3/V4 differ only in sync polarity
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt};
use super::keys;
use crate::radio::demodulator::LevelDuration;
use crate::duration_diff;
@@ -23,7 +24,7 @@ const INTER_BURST_GAP_US: u32 = 10000;
const PREAMBLE_PAIRS: usize = 16;
const TOTAL_BURSTS: usize = 3;
/// Decoder states
/// Decoder states (matches protopirate's KiaV3V4DecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -53,7 +54,7 @@ impl KiaV3V4Decoder {
}
}
/// Reverse bits in a byte
/// Reverse bits in a byte (matches kia_v3_v4.c byte order for KeeLoq payload)
fn reverse8(byte: u8) -> u8 {
let mut byte = byte;
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
@@ -76,7 +77,7 @@ impl KiaV3V4Decoder {
}
}
/// CRC4 calculation
/// CRC4 for Kia V3/V4 (matches kia_v3_v4.c: XOR nibbles over first 8 bytes)
fn calculate_crc(bytes: &[u8]) -> u8 {
let mut crc: u8 = 0;
for &byte in bytes.iter().take(8) {
@@ -85,61 +86,18 @@ impl KiaV3V4Decoder {
crc & 0x0F
}
/// KeeLoq decrypt
fn keeloq_decrypt(data: u32, key: u64) -> u32 {
let mut block = data;
let mut tkey = key;
for _ in 0..528 {
let lutkey = ((block >> 0) & 1) |
((block >> 7) & 2) |
((block >> 17) & 4) |
((block >> 22) & 8) |
((block >> 26) & 16);
let lsb = ((block >> 31) ^
((block >> 15) & 1) ^
((0x3A5C742E_u32 >> lutkey) & 1) ^
(((tkey >> 15) & 1) as u32)) as u32;
block = ((block & 0x7FFFFFFF) << 1) | lsb;
tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63);
}
block
}
/// KeeLoq encrypt
fn keeloq_encrypt(data: u32, key: u64) -> u32 {
let mut block = data;
let mut tkey = key;
for _ in 0..528 {
let lutkey = ((block >> 1) & 1) |
((block >> 8) & 2) |
((block >> 18) & 4) |
((block >> 23) & 8) |
((block >> 27) & 16);
let msb = ((block >> 0) ^
((block >> 16) & 1) ^
((0x3A5C742E_u32 >> lutkey) & 1) ^
(((tkey >> 0) & 1) as u32)) as u32;
block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31);
tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63);
}
block
}
/// Get manufacturer key (placeholder - in real use, this would be loaded from config)
/// KIA manufacturer key from keystore (type 10)
fn get_mf_key() -> u64 {
keys::get_keystore().get_kia_mf_key()
}
/// Process the collected buffer and validate
/// Process collected 68 bits: decrypt KeeLoq block, validate button/serial (matches kia_v3_v4.c)
fn process_buffer(&self) -> Option<DecodedSignal> {
if self.raw_bit_count < 68 {
return None;
}
let mut b = self.raw_bits;
// V3 sync means data is inverted
if self.is_v3_sync {
let num_bytes = ((self.raw_bit_count + 7) / 8) as usize;
@@ -164,7 +122,7 @@ impl KiaV3V4Decoder {
let our_serial_lsb = (serial & 0xFF) as u8;
let mf_key = Self::get_mf_key();
let decrypted = Self::keeloq_decrypt(encrypted, mf_key);
let decrypted = keeloq_decrypt(encrypted, mf_key);
let dec_btn = ((decrypted >> 28) & 0x0F) as u8;
let dec_serial_lsb = ((decrypted >> 16) & 0xFF) as u8;
@@ -313,7 +271,7 @@ impl ProtocolDecoder for KiaV3V4Decoder {
(((button & 0x0F) as u32) << 28);
let mf_key = Self::get_mf_key();
let encrypted = Self::keeloq_encrypt(plaintext, mf_key);
let encrypted = keeloq_encrypt(plaintext, mf_key);
// Build raw bytes
let mut raw_bytes = [0u8; 9];
@@ -343,18 +301,19 @@ impl ProtocolDecoder for KiaV3V4Decoder {
let mut signal = Vec::with_capacity(600);
// 3 bursts with 10ms gap (matches protopirate kia_v3_v4 encode)
for burst in 0..TOTAL_BURSTS {
if burst > 0 {
signal.push(LevelDuration::new(false, INTER_BURST_GAP_US));
}
// Preamble
// Preamble: 16 short pairs
for _ in 0..PREAMBLE_PAIRS {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
// Sync pulse
// Sync: V4 = long HIGH, V3 = long LOW
if version == 0 {
// V4: long HIGH, short LOW
signal.push(LevelDuration::new(true, SYNC_DURATION));
@@ -365,7 +324,7 @@ impl ProtocolDecoder for KiaV3V4Decoder {
signal.push(LevelDuration::new(false, SYNC_DURATION));
}
// Data bits
// Data: 68 bits PWM (8 bytes + 4-bit CRC), MSB first per byte
for byte_idx in 0..9 {
let bits_in_byte = if byte_idx == 8 { 4 } else { 8 };
for bit_idx in (8 - bits_in_byte..8).rev() {
@@ -384,3 +343,9 @@ impl ProtocolDecoder for KiaV3V4Decoder {
Some(signal)
}
}
impl Default for KiaV3V4Decoder {
fn default() -> Self {
Self::new()
}
}
+22 -15
View File
@@ -1,13 +1,14 @@
//! Kia V5 protocol decoder
//! Kia V5 protocol decoder (decode-only)
//!
//! Ported from protopirate's kia_v5.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v5.c`.
//! Decode logic (Manchester polarity, mixer decrypt, YEK, preamble) matches reference.
//! No encoder in protopirate.
//!
//! Protocol characteristics:
//! - Manchester encoding: 400/800µs timing
//! - 64 bits total (+3 bit CRC)
//! - Preamble of ~40+ short pairs
//! - Custom "mixer" encryption
//! - Decode-only (no encoder)
//! - Manchester encoding: 400/800µs (opposite polarity to V1/V2: level ? ShortHigh : ShortLow)
//! - 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
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use super::keys;
@@ -19,7 +20,7 @@ const TE_LONG: u32 = 800;
const TE_DELTA: u32 = 150;
const MIN_COUNT_BIT: usize = 64;
/// Manchester states
/// Manchester decoder states (V5 uses opposite polarity to V1/V2; see manchester_advance)
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
@@ -28,7 +29,7 @@ enum ManchesterState {
Start1,
}
/// Decoder states
/// Decoder states (matches protopirate's KiaV5DecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -60,12 +61,12 @@ impl KiaV5Decoder {
}
}
/// Get encryption key from global keystore
/// KIA V5 manufacturer key from keystore (type 13)
fn get_v5_key() -> u64 {
keys::get_keystore().get_kia_v5_key()
}
/// Custom mixer decryption
/// Mixer decryption (matches kia_v5.c custom cipher)
fn mixer_decode(encrypted: u32) -> u16 {
let mut s0 = (encrypted & 0xFF) as u8;
let mut s1 = ((encrypted >> 8) & 0xFF) as u8;
@@ -128,7 +129,7 @@ impl KiaV5Decoder {
((s0 as u16) + ((s1 as u16) << 8)) & 0xFFFF
}
/// Reverse bits in 64-bit value
/// YEK: reverse bit order per byte (matches kia_v5.c for key derivation)
fn compute_yek(key: u64) -> u64 {
let mut yek: u64 = 0;
for i in 0..8 {
@@ -180,7 +181,7 @@ impl KiaV5Decoder {
output
}
/// Parse decoded data
/// Parse 64-bit key: YEK then serial/button from high bits, mixer_decode for counter (matches kia_v5.c)
fn parse_data(&self) -> Option<DecodedSignal> {
if self.bit_count < MIN_COUNT_BIT as u8 {
return None;
@@ -188,7 +189,7 @@ impl KiaV5Decoder {
let key = self.saved_key;
let yek = Self::compute_yek(key);
// serial(28) + button(4) in high 32 bits; low 32 bits encrypted counter
let serial = ((yek >> 32) & 0x0FFFFFFF) as u32;
let button = ((yek >> 60) & 0x0F) as u8;
let encrypted = (yek & 0xFFFFFFFF) as u32;
@@ -316,6 +317,12 @@ impl ProtocolDecoder for KiaV5Decoder {
}
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
None // V5 doesn't support encoding
None // V5 decode-only in protopirate
}
}
impl Default for KiaV5Decoder {
fn default() -> Self {
Self::new()
}
}
+23 -17
View File
@@ -1,13 +1,14 @@
//! Kia V6 protocol decoder
//! Kia V6 protocol decoder (decode-only)
//!
//! Ported from protopirate's kia_v6.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v6.c`.
//! Decode logic (Manchester level mapping, 3-part 144-bit frame, AES-128, CRC8, keystore XOR) matches reference.
//! No encoder in protopirate.
//!
//! Protocol characteristics:
//! - Manchester encoding: 200/400µs timing
//! - 144 bits total (split into 3 parts)
//! - Long preamble of 600+ pairs
//! - AES-128 encryption
//! - Decode-only (no encoder)
//! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance)
//! - 144 bits total: part1 (64) + part2 (64) + part3 (16), each part inverted on store
//! - Long preamble of 601 pairs; sync bits 1,1,0,1 then data
//! - AES-128 decryption with key derived from KIA V6 A/B keystores (types 11/12) and XOR masks
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use super::keys;
@@ -65,7 +66,7 @@ const AES_SBOX_INV: [u8; 256] = [
const AES_RCON: [u8; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
/// Manchester states
/// Manchester decoder states (event mapping 0/2/4/6 matches protopirate kia_v6 level convention)
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
@@ -74,7 +75,7 @@ enum ManchesterState {
Start1,
}
/// Decoder states
/// Decoder states (matches protopirate's KiaV6DecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -119,17 +120,17 @@ impl KiaV6Decoder {
}
}
/// Get keystore A from global keystore
/// KIA V6 keystore A from keystore (type 11)
fn get_keystore_a() -> u64 {
keys::get_keystore().get_kia_v6_keystore_a()
}
/// Get keystore B from global keystore
/// KIA V6 keystore B from keystore (type 12)
fn get_keystore_b() -> u64 {
keys::get_keystore().get_kia_v6_keystore_b()
}
/// CRC8 calculation
/// CRC8 for V6 (matches kia_v6.c: init 0xFF, polynomial 0x07, over first 15 bytes)
fn crc8(data: &[u8], init: u8, polynomial: u8) -> u8 {
let mut crc = init;
for &byte in data {
@@ -274,7 +275,7 @@ impl KiaV6Decoder {
*data = state;
}
/// Get AES key from keystores
/// AES-128 key from V6 keystores A+B with XOR_MASK_LOW/HIGH (matches kia_v6.c)
fn get_aes_key() -> [u8; 16] {
let keystore_a = Self::get_keystore_a();
let keystore_a_hi = ((keystore_a >> 32) & 0xFFFFFFFF) as u32;
@@ -305,7 +306,7 @@ impl KiaV6Decoder {
aes_key
}
/// Decrypt the stored data
/// Decrypt 16-byte block: byte layout matches kia_v6.c; AES-128 then CRC8 check
fn decrypt(&self) -> Option<(u32, u8, u32, bool)> {
let mut encrypted_data = [0u8; 16];
@@ -380,9 +381,8 @@ impl KiaV6Decoder {
output
}
/// Add initial sync bits
/// Add initial sync bits (1,1,0,1 — matches kia_v6.c)
fn add_sync_bits(&mut self) {
// Add 1, 1, 0, 1 as initial bits
for bit in [true, true, false, true] {
let carry = self.data_part1_low >> 31;
self.data_part1_low = (self.data_part1_low << 1) | (bit as u32);
@@ -554,6 +554,12 @@ impl ProtocolDecoder for KiaV6Decoder {
}
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
None
None // V6 decode-only in protopirate
}
}
impl Default for KiaV6Decoder {
fn default() -> Self {
Self::new()
}
}
+3 -2
View File
@@ -1,7 +1,8 @@
//! Protocol decoders and encoders for various keyfob systems.
//!
//! This module implements decoders for various keyfob protocols, ported from
//! protopirate. Each protocol processes level+duration pairs from the demodulator.
//! Protocols are aligned with the ProtoPirate reference (`REFERENCES/ProtoPirate/protocols/`).
//! Each decoder processes level+duration pairs from the demodulator and optionally supports
//! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64].
mod common;
pub mod keeloq_common;
+25 -23
View File
@@ -1,12 +1,13 @@
//! PSA (Peugeot/Citroen) protocol decoder/encoder
//! PSA (Peugeot/Citroën) protocol decoder/encoder
//!
//! Ported from protopirate's psa.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`.
//! Decode/encode logic (Manchester, preamble, TEA, XOR, mode 0x23/0x36) matches reference.
//!
//! Protocol characteristics:
//! - Manchester encoding: 125/250µs bit timing, 250/500µs symbol timing
//! - 128 bits total (key1: 64 bits, key2: 16 bits, validation: 48 bits)
//! - TEA and XOR encryption schemes
//! - Two modes: 0x23 and 0x36
//! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble)
//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation
//! - TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds XOR layer
//! - Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule)
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
@@ -35,7 +36,7 @@ const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A
// Brute-force constants for mode 0x36
const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04];
/// Manchester decoder states
/// Manchester decoder states (matches protopirate psa.c Manchester state machine)
#[derive(Debug, Clone, Copy, PartialEq)]
enum ManchesterState {
Mid0,
@@ -44,16 +45,12 @@ enum ManchesterState {
Start1,
}
/// Decoder states
/// Decoder states (matches protopirate's PsaDecoderState)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderState {
/// Waiting for first edge
WaitEdge,
/// Counting preamble pattern
CountPattern,
/// Decoding Manchester data
DecodeManchester,
/// Found end of data
End,
}
@@ -94,7 +91,7 @@ impl PsaDecoder {
}
}
/// Manchester advance
/// Manchester state machine (matches psa.c event mapping)
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
let event = match (is_short, is_high) {
(true, true) => 0,
@@ -143,7 +140,7 @@ impl PsaDecoder {
}
}
/// TEA decrypt
/// TEA decrypt (matches psa.c / standard TEA)
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
for _ in 0..TEA_ROUNDS {
@@ -161,7 +158,7 @@ impl PsaDecoder {
}
}
/// TEA encrypt
/// TEA encrypt (matches psa.c / standard TEA)
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
let mut sum: u32 = 0;
for _ in 0..TEA_ROUNDS {
@@ -179,7 +176,7 @@ impl PsaDecoder {
}
}
/// XOR decrypt (mode 0x23)
/// XOR decrypt for mode 0x23 (matches psa.c)
fn xor_decrypt(buffer: &mut [u8]) {
let e6 = buffer[8];
let e7 = buffer[9];
@@ -198,8 +195,9 @@ impl PsaDecoder {
buffer[7] = e5 ^ e6 ^ e7;
}
/// Decrypt key1 + validation: mode 0x23 (TEA+XOR) or 0x36 (TEA, BF2) — matches psa.c
fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> {
// Try mode 0x23 first
// Try mode 0x23 first (seed byte 0x23)
let seed_byte = (self.key1_high >> 24) as u8;
if seed_byte >= 0x23 && seed_byte < 0x24 {
@@ -250,8 +248,9 @@ impl PsaDecoder {
None
}
/// Build DecodedSignal from key1 + validation; decrypt yields serial/button/counter (matches psa.c)
fn parse_data(&self) -> DecodedSignal {
// Combine into 128-bit data (store lower 64)
// Store key1 as 64-bit data for display/replay
let data = ((self.key1_high as u64) << 32) | (self.key1_low as u64);
if let Some((serial, btn, counter, _crc, _mode)) = self.try_decrypt() {
@@ -445,20 +444,17 @@ impl ProtocolDecoder for PsaDecoder {
let key1_low = v1;
let validation = ((buffer[8] as u16) << 8) | (buffer[9] as u16);
// Build signal
let mut signal = Vec::with_capacity(512);
// Preamble: alternating 125µs pulses
// Preamble + sync (matches protopirate psa encode)
for _ in 0..70 {
signal.push(LevelDuration::new(true, TE_SHORT_125));
signal.push(LevelDuration::new(false, TE_SHORT_125));
}
// Sync
signal.push(LevelDuration::new(true, TE_LONG_250));
signal.push(LevelDuration::new(false, TE_LONG_250));
// Key1: 64 bits Manchester encoded
// Key1: 64 bits Manchester, then validation 16 bits
let key1 = ((key1_high as u64) << 32) | (key1_low as u64);
for bit in (0..64).rev() {
if (key1 >> bit) & 1 == 1 {
@@ -487,3 +483,9 @@ impl ProtocolDecoder for PsaDecoder {
Some(signal)
}
}
impl Default for PsaDecoder {
fn default() -> Self {
Self::new()
}
}
+17 -10
View File
@@ -1,14 +1,14 @@
//! Scher-Khan protocol decoder
//! Scher-Khan protocol decoder (decode-only)
//!
//! Ported from protopirate's scher_khan.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/scher_khan.c`.
//! Decode logic (PWM preamble/sync, short=0/long=1, variable bit count) matches reference.
//! No encoder in protopirate.
//!
//! Protocol characteristics:
//! - PWM encoding: 750/1100µs timing
//! - Variable bit count (35, 51, 57, 63, 64, 81, 82)
//! - Decode-only (no encoder)
//! - PWM encoding: 750µs = 0, 1100µs = 1; preamble uses 2× short then alternating
//! - Variable bit count (35, 51, 57, 63, 64, 81, 82); only 51-bit format parsed for serial/button/counter
//!
//! References:
//! - https://phreakerclub.com/72
//! References: https://phreakerclub.com/72
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
@@ -19,7 +19,7 @@ const TE_LONG: u32 = 1100;
const TE_DELTA: u32 = 160;
const MIN_COUNT_BIT: usize = 35;
/// Decoder states
/// Decoder states (matches protopirate's ScherKhanDecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -48,10 +48,11 @@ impl ScherKhanDecoder {
}
}
/// Parse payload by bit count; 51-bit format yields serial/button/counter (matches scher_khan.c)
fn parse_data(data: u64, bit_count: usize) -> DecodedSignal {
let (serial, btn, cnt) = match bit_count {
51 => {
// MAGIC CODE, Dynamic
// 51-bit "MAGIC CODE" / Dynamic format: serial(28) | button(4) | counter(16) — matches reference
let serial =
((data >> 24) & 0xFFFFFF0) as u32 | ((data >> 20) & 0x0F) as u32;
let btn = ((data >> 24) & 0x0F) as u8;
@@ -196,6 +197,12 @@ impl ProtocolDecoder for ScherKhanDecoder {
}
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
None
None // Scher-Khan decode-only in protopirate
}
}
impl Default for ScherKhanDecoder {
fn default() -> Self {
Self::new()
}
}
+17 -9
View File
@@ -1,12 +1,13 @@
//! Star Line protocol decoder/encoder
//!
//! Ported from protopirate's star_line.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/star_line.c`.
//! Decode/encode logic (PWM header, fix/hop split, KeeLoq simple/normal learning) matches reference.
//!
//! Protocol characteristics:
//! - PWM encoding: 250/500µs timing
//! - 64 bits total
//! - PWM encoding: 250µs = 0, 500µs = 1
//! - 64 bits total: key_fix (32) + key_hop (32), sent MSB-first (reversed on air)
//! - Header: 6 pairs of 1000µs HIGH + 1000µs LOW
//! - KeeLoq encryption (requires manufacturer key)
//! - KeeLoq: fix = serial(24) + button(8); hop encrypted with MF key or normal-learning derived key
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key};
use super::keys;
@@ -20,7 +21,7 @@ const TE_DELTA: u32 = 120;
const MIN_COUNT_BIT: usize = 64;
const HEADER_DURATION: u32 = 1000; // te_long * 2
/// Decoder states
/// Decoder states (matches protopirate's StarLineDecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -49,13 +50,14 @@ impl StarLineDecoder {
}
}
/// Get manufacturer key from global keystore
/// Star Line manufacturer key from keystore (type 20)
fn get_mf_key() -> u64 {
keys::get_keystore().get_star_line_mf_key()
}
/// Parse 64-bit payload: reverse_key then fix(32)/hop(32); KeeLoq decrypt (simple then normal learning) — matches star_line.c
fn parse_data(data: u64) -> DecodedSignal {
// Data is stored MSB-first in the air, reverse to get fix|hop
// Data is MSB-first on air; reverse to get fix|hop
let reversed = reverse_key(data, MIN_COUNT_BIT);
let key_fix = (reversed >> 32) as u32;
let key_hop = (reversed & 0xFFFFFFFF) as u32;
@@ -246,13 +248,13 @@ impl ProtocolDecoder for StarLineDecoder {
let mut signal = Vec::with_capacity(256);
// Header: 6 pairs of LONG*2 HIGH + LONG*2 LOW
// Header: 6 pairs 1000µs HIGH + 1000µs LOW (matches protopirate star_line encode)
for _ in 0..6 {
signal.push(LevelDuration::new(true, HEADER_DURATION));
signal.push(LevelDuration::new(false, HEADER_DURATION));
}
// Data: 64 bits, MSB first
// Data: 64 bits PWM, MSB first (1=long, 0=short)
for bit in (0..64).rev() {
if (data >> bit) & 1 == 1 {
// Bit 1: LONG HIGH + LONG LOW
@@ -268,3 +270,9 @@ impl ProtocolDecoder for StarLineDecoder {
Some(signal)
}
}
impl Default for StarLineDecoder {
fn default() -> Self {
Self::new()
}
}
+20 -32
View File
@@ -1,13 +1,13 @@
//! Subaru protocol decoder
//! Subaru protocol decoder/encoder
//!
//! Ported from protopirate's subaru.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/subaru.c`.
//! Decode/encode logic (PWM preamble/gap/sync, short=1/long=0, counter decode) matches reference.
//!
//! Protocol characteristics:
//! - PWM encoding: short HIGH (800µs) = 1, long HIGH (1600µs) = 0
//! - 64 bits total
//! - Long preamble of 1600µs pulses
//! - Gap and sync pattern
//! - Complex counter encoding
//! - PWM encoding: 800µs HIGH = 1, 1600µs HIGH = 0; LOW is 800µs after each bit
//! - 64 bits total (8 bytes MSB first: button(4)+serial(24)+counter-related)
//! - Preamble: 79 full 1600µs pairs + 80th HIGH only; then gap 2800µs, sync 2800µs HIGH + 1600µs LOW
//! - Complex counter encoding (decode_counter) from bytes 47
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use crate::radio::demodulator::LevelDuration;
@@ -22,7 +22,7 @@ const MIN_COUNT_BIT: usize = 64;
const GAP_US: u32 = 2800;
const SYNC_US: u32 = 2800;
/// Decoder states
/// Decoder states (matches protopirate's SubaruDecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -67,7 +67,7 @@ impl SubaruDecoder {
}
}
/// Decode the counter from the complex Subaru encoding
/// Decode 16-bit counter from bytes 47 (matches subaru.c complex encoding)
fn decode_counter(kb: &[u8; 8]) -> u16 {
let mut lo: u8 = 0;
if (kb[4] & 0x40) == 0 { lo |= 0x01; }
@@ -115,9 +115,7 @@ impl SubaruDecoder {
((hi as u16) << 8) | (lo as u16)
}
/// Add a level+duration to the signal, merging with the previous entry
/// if it has the same level. This prevents consecutive same-level pulses
/// which would silently merge during HackRF transmission.
/// Append level+duration, merging with previous if same level (prevents HackRF from merging consecutive same-level pulses)
fn add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
if let Some(last) = signal.last_mut() {
if last.level == level {
@@ -128,15 +126,13 @@ impl SubaruDecoder {
signal.push(LevelDuration::new(level, duration));
}
/// Process the decoded data
/// Build DecodedSignal from 8-byte buffer: serial(bytes 13), button(byte0 low nibble), counter(decode_counter) — matches subaru.c
fn process_data(&self) -> Option<DecodedSignal> {
if self.bit_count < 64 {
return None;
}
let b = &self.data;
// Build 64-bit key
let key = ((b[0] as u64) << 56) | ((b[1] as u64) << 48) |
((b[2] as u64) << 40) | ((b[3] as u64) << 32) |
((b[4] as u64) << 24) | ((b[5] as u64) << 16) |
@@ -297,37 +293,24 @@ impl ProtocolDecoder for SubaruDecoder {
let key = decoded.data;
let mut signal = Vec::with_capacity(512);
// Generate 3 bursts.
//
// IMPORTANT: Uses add_level() to merge adjacent same-level pulses.
// The HackRF transmitter generates IQ samples sequentially from the
// LevelDuration list, so consecutive same-level pairs silently merge
// and corrupt timing. For example, the last preamble LOW (1600µs) +
// gap LOW (2800µs) would become a single 4400µs LOW without merging.
// 3 bursts; add_level() merges same-level pulses for correct HackRF timing (matches protopirate subaru encode)
for burst in 0..3 {
if burst > 0 {
// Inter-burst silence
Self::add_level(&mut signal, false, 25000);
}
// Preamble: 79 full pairs + 80th HIGH only.
// The gap LOW replaces the 80th preamble LOW.
// Preamble: 79 full 1600µs pairs + 80th HIGH only; gap replaces 80th LOW
for i in 0..80 {
Self::add_level(&mut signal, true, TE_LONG);
if i < 79 {
Self::add_level(&mut signal, false, TE_LONG);
}
}
// Gap (replaces the 80th preamble LOW)
Self::add_level(&mut signal, false, GAP_US);
// Sync
Self::add_level(&mut signal, true, SYNC_US);
Self::add_level(&mut signal, false, TE_LONG);
// Data: 64 bits (MSB first)
// Short HIGH = 1, Long HIGH = 0
// Data: 64 bits MSB first; short HIGH = 1, long HIGH = 0; LOW = 800µs after each
for bit in (0..64).rev() {
if (key >> bit) & 1 == 1 {
Self::add_level(&mut signal, true, TE_SHORT);
@@ -337,10 +320,15 @@ impl ProtocolDecoder for SubaruDecoder {
Self::add_level(&mut signal, false, TE_SHORT);
}
// End-of-burst gap (extends the last data LOW)
Self::add_level(&mut signal, false, TE_LONG * 2);
}
Some(signal)
}
}
impl Default for SubaruDecoder {
fn default() -> Self {
Self::new()
}
}
+17 -12
View File
@@ -1,12 +1,13 @@
//! Suzuki protocol decoder/encoder
//!
//! Ported from protopirate's suzuki.c
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/suzuki.c`.
//! Decode/encode logic (preamble count 350, gap 2000µs, short=0/long=1, field layout) matches reference.
//!
//! Protocol characteristics:
//! - PWM encoding: 250/500µs timing
//! - 64 bits total
//! - 350 preamble pairs
//! - 2000µs gap between transmissions
//! - PWM encoding: 250µs HIGH = 0, 500µs HIGH = 1; LOW 250µs after each bit
//! - 64 bits total; preamble: 300+ short LOW pulses then long HIGH starts data
//! - 350 preamble pairs (SHORT HIGH / SHORT LOW); 2000µs gap at end
//! - Field layout: serial = (data_high&0xFFF)<<16 | data_low>>16; btn = (data_low>>12)&0xF; cnt = (data_high<<4)>>16
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
@@ -20,7 +21,7 @@ const PREAMBLE_COUNT: u16 = 350;
const GAP_TIME: u32 = 2000;
const GAP_DELTA: u32 = 399;
/// Decoder states
/// Decoder states (matches protopirate's SuzukiDecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
@@ -53,10 +54,11 @@ impl SuzukiDecoder {
self.decode_count_bit += 1;
}
/// Parse 64-bit data (matches suzuki.c: serial, btn, cnt layout)
fn parse_data(data: u64) -> DecodedSignal {
let data_high = (data >> 32) as u32;
let data_low = data as u32;
// Reference: instance->generic.serial = ((data_high & 0xFFF) << 16) | (data_low >> 16); etc.
let serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
let btn = ((data_low >> 12) & 0xF) as u8;
let cnt = ((data_high << 4) >> 16) as u16;
@@ -178,14 +180,12 @@ impl ProtocolDecoder for SuzukiDecoder {
let mut signal = Vec::with_capacity(1024);
// Preamble: SHORT HIGH / SHORT LOW pairs
// Preamble + data + gap (matches subghz_protocol_encoder_suzuki_get_upload in suzuki.c)
for _ in 0..PREAMBLE_COUNT {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
// Data: 64 bits, MSB first
// SHORT HIGH (~250µs) = 0, LONG HIGH (~500µs) = 1
// Data: 64 bits MSB first; SHORT HIGH = 0, LONG HIGH = 1; LOW = 250µs after each
for bit in (0..64).rev() {
if (data >> bit) & 1 == 1 {
signal.push(LevelDuration::new(true, TE_LONG));
@@ -195,9 +195,14 @@ impl ProtocolDecoder for SuzukiDecoder {
signal.push(LevelDuration::new(false, TE_SHORT));
}
// End gap
signal.push(LevelDuration::new(false, GAP_TIME));
Some(signal)
}
}
impl Default for SuzukiDecoder {
fn default() -> Self {
Self::new()
}
}
+27 -23
View File
@@ -1,17 +1,15 @@
//! VAG (VW/Audi/Seat/Skoda) protocol decoder/encoder
//!
//! Ported from protopirate's vag.c
//! 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.
//! Intentional differences for HackRF: TE_DELTA 150 (ref 80), TE_DELTA_12 120 (ref 79);
//! Preamble2/Sync2C accept shorter LOW pulses for asymmetric software demodulation.
//!
//! Protocol characteristics:
//! - Manchester encoding
//! - Type 1/2: 300/600µs timing (te_short=300), 80 bits, AUT64 or TEA encryption
//! - Type 3/4: 500/1000µs timing (te_short=500), 80 bits, AUT64 encryption
//! - 433.92 MHz and 434.42 MHz
//! - Four sub-types:
//! Type 1: AUT64 encrypted (300µs Manchester, dispatch 0x2A/0x1C/0x46)
//! Type 2: TEA encrypted (300µs Manchester, dispatch 0x2A/0x1C/0x46)
//! Type 3: AUT64 encrypted (500µs Manchester, auto-detect key)
//! Type 4: AUT64 encrypted (500µs Manchester, key 2)
//! - 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)
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
use super::aut64;
@@ -34,7 +32,7 @@ const TE_DELTA_12: u32 = 120; // Wider tolerance for HackRF (was 79)
const TEA_DELTA: u32 = 0x9E3779B9;
const TEA_ROUNDS: usize = 32;
/// TEA key schedule for VAG
/// TEA key schedule for VAG (matches vag.c vag_tea_key_schedule)
static TEA_KEY_SCHEDULE: [u32; 4] = [0x0B46502D, 0x5E253718, 0x2BF93A19, 0x622C1206];
/// Manchester states
@@ -56,17 +54,17 @@ enum ManchesterEvent {
Reset,
}
/// Decoder states
/// Decoder states (matches protopirate's VAGDecoderStep)
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
Preamble1, // Type 1/2: ~300µs preamble
Data1, // Type 1/2: Manchester data
Preamble2, // Type 3/4: ~500µs preamble
Sync2A, // Type 3/4: sync pattern step A
Sync2B, // Type 3/4: sync pattern step B (750µs)
Sync2C, // Type 3/4: sync pattern step C (750µs)
Data2, // Type 3/4: Manchester data
Preamble1,
Data1,
Preamble2,
Sync2A,
Sync2B,
Sync2C,
Data2,
}
/// VAG sub-type
@@ -182,7 +180,7 @@ impl VagDecoder {
self.bit_count += 1;
}
/// TEA decrypt
/// TEA decrypt (matches vag.c vag_tea_decrypt)
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key_schedule: &[u32; 4]) {
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS as u32);
for _ in 0..TEA_ROUNDS {
@@ -198,7 +196,7 @@ impl VagDecoder {
}
}
/// TEA encrypt
/// TEA encrypt (matches vag.c vag_tea_encrypt)
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key_schedule: &[u32; 4]) {
let mut sum: u32 = 0;
for _ in 0..TEA_ROUNDS {
@@ -278,7 +276,7 @@ impl VagDecoder {
}
}
/// Parse the collected data and attempt decryption
/// Parse key1/key2 and decrypt by type (AUT64/TEA, dispatch); matches vag.c vag_parse_data
fn parse_data(&mut self) {
self.decrypted = false;
self.serial = 0;
@@ -950,7 +948,7 @@ impl ProtocolDecoder for VagDecoder {
}
}
// Check for gap (end of data)
// End-of-data gap: 6000µs, accept within 4000µs (matches vag.c check_gap1_data)
if !level {
let gap_diff = if duration > 6000 {
duration - 6000
@@ -1145,3 +1143,9 @@ impl ProtocolDecoder for VagDecoder {
self.encode_signal(decoded)
}
}
impl Default for VagDecoder {
fn default() -> Self {
Self::new()
}
}