Version 1.0.0

This commit is contained in:
leviathan
2026-02-07 17:35:27 -05:00
parent c8bff9afd7
commit 4339895b41
43 changed files with 12218 additions and 3 deletions
+269
View File
@@ -0,0 +1,269 @@
//! Star Line protocol decoder/encoder
//!
//! Ported from protopirate's star_line.c
//!
//! Protocol characteristics:
//! - PWM encoding: 250/500µs timing
//! - 64 bits total
//! - Header: 6 pairs of 1000µs HIGH + 1000µs LOW
//! - KeeLoq encryption (requires manufacturer key)
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key};
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
use crate::duration_diff;
use crate::radio::demodulator::LevelDuration;
const TE_SHORT: u32 = 250;
const TE_LONG: u32 = 500;
const TE_DELTA: u32 = 120;
const MIN_COUNT_BIT: usize = 64;
const HEADER_DURATION: u32 = 1000; // te_long * 2
/// Decoder states
#[derive(Debug, Clone, Copy, PartialEq)]
enum DecoderStep {
Reset,
CheckPreamble,
SaveDuration,
CheckDuration,
}
/// Star Line protocol decoder
pub struct StarLineDecoder {
step: DecoderStep,
te_last: u32,
header_count: u16,
decode_data: u64,
decode_count_bit: usize,
}
impl StarLineDecoder {
pub fn new() -> Self {
Self {
step: DecoderStep::Reset,
te_last: 0,
header_count: 0,
decode_data: 0,
decode_count_bit: 0,
}
}
/// Get manufacturer key (placeholder)
fn get_mf_key() -> u64 {
0x0000000000000000
}
fn parse_data(data: u64) -> DecodedSignal {
// Data is stored MSB-first in the 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;
let serial = key_fix & 0x00FFFFFF;
let btn = (key_fix >> 24) as u8;
// Attempt KeeLoq decryption
let mf_key = Self::get_mf_key();
let counter = if mf_key != 0 {
// Try simple learning first
let decrypt = keeloq_decrypt(key_hop, mf_key);
let dec_btn = (decrypt >> 24) as u8;
let dec_serial_lsb = ((decrypt >> 16) & 0xFF) as u8;
let serial_lsb = (serial & 0xFF) as u8;
if dec_btn == btn && dec_serial_lsb == serial_lsb {
Some((decrypt & 0xFFFF) as u16)
} else {
// Try normal learning
let man_key = keeloq_normal_learning(key_fix, mf_key);
let decrypt = keeloq_decrypt(key_hop, man_key);
let dec_btn = (decrypt >> 24) as u8;
let dec_serial_lsb = ((decrypt >> 16) & 0xFF) as u8;
if dec_btn == btn && dec_serial_lsb == serial_lsb {
Some((decrypt & 0xFFFF) as u16)
} else {
None
}
}
} else {
None
};
DecodedSignal {
serial: Some(serial),
button: Some(btn),
counter: counter.or(Some(0)),
crc_valid: counter.is_some() || mf_key == 0,
data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
}
}
}
impl ProtocolDecoder for StarLineDecoder {
fn name(&self) -> &'static str {
"Star Line"
}
fn timing(&self) -> ProtocolTiming {
ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: MIN_COUNT_BIT,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[433_920_000]
}
fn reset(&mut self) {
self.step = DecoderStep::Reset;
self.te_last = 0;
self.header_count = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
}
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
match self.step {
DecoderStep::Reset => {
if level {
if duration_diff!(duration, HEADER_DURATION) < TE_DELTA * 2 {
self.step = DecoderStep::CheckPreamble;
self.header_count += 1;
} else if self.header_count > 4 {
self.decode_data = 0;
self.decode_count_bit = 0;
self.te_last = duration;
self.step = DecoderStep::CheckDuration;
}
} else {
self.header_count = 0;
}
}
DecoderStep::CheckPreamble => {
if !level && duration_diff!(duration, HEADER_DURATION) < TE_DELTA * 2 {
// Found preamble pair
self.step = DecoderStep::Reset;
} else {
self.header_count = 0;
self.step = DecoderStep::Reset;
}
}
DecoderStep::SaveDuration => {
if level {
if duration >= (TE_LONG + TE_DELTA) {
// End of data - check if we have enough bits
self.step = DecoderStep::Reset;
if self.decode_count_bit >= MIN_COUNT_BIT
&& self.decode_count_bit <= MIN_COUNT_BIT + 2
{
let result = Self::parse_data(self.decode_data);
self.decode_data = 0;
self.decode_count_bit = 0;
self.header_count = 0;
return Some(result);
}
self.decode_data = 0;
self.decode_count_bit = 0;
self.header_count = 0;
} else {
self.te_last = duration;
self.step = DecoderStep::CheckDuration;
}
} else {
self.step = DecoderStep::Reset;
}
}
DecoderStep::CheckDuration => {
if !level {
if duration_diff!(self.te_last, TE_SHORT) < TE_DELTA
&& duration_diff!(duration, TE_SHORT) < TE_DELTA
{
// Bit 0: short HIGH + short LOW
if self.decode_count_bit < MIN_COUNT_BIT {
self.decode_data = (self.decode_data << 1) | 0;
self.decode_count_bit += 1;
} else {
self.decode_count_bit += 1;
}
self.step = DecoderStep::SaveDuration;
} else if duration_diff!(self.te_last, TE_LONG) < TE_DELTA
&& duration_diff!(duration, TE_LONG) < TE_DELTA
{
// Bit 1: long HIGH + long LOW
if self.decode_count_bit < MIN_COUNT_BIT {
self.decode_data = (self.decode_data << 1) | 1;
self.decode_count_bit += 1;
} else {
self.decode_count_bit += 1;
}
self.step = DecoderStep::SaveDuration;
} else {
self.step = DecoderStep::Reset;
}
} else {
self.step = DecoderStep::Reset;
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0).wrapping_add(1);
let fix = ((button as u32) << 24) | (serial & 0x00FFFFFF);
let plaintext = ((button as u32) << 24)
| (((serial & 0xFF) as u32) << 16)
| (counter as u32);
let mf_key = Self::get_mf_key();
let hop = if mf_key != 0 {
keeloq_encrypt(plaintext, mf_key)
} else {
// Without a key, replay the original hop
let reversed = reverse_key(decoded.data, MIN_COUNT_BIT);
(reversed & 0xFFFFFFFF) as u32
};
let yek = ((fix as u64) << 32) | (hop as u64);
let data = reverse_key(yek, MIN_COUNT_BIT);
let mut signal = Vec::with_capacity(256);
// Header: 6 pairs of LONG*2 HIGH + LONG*2 LOW
for _ in 0..6 {
signal.push(LevelDuration::new(true, HEADER_DURATION));
signal.push(LevelDuration::new(false, HEADER_DURATION));
}
// Data: 64 bits, MSB first
for bit in (0..64).rev() {
if (data >> bit) & 1 == 1 {
// Bit 1: LONG HIGH + LONG LOW
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_LONG));
} else {
// Bit 0: SHORT HIGH + SHORT LOW
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
}
Some(signal)
}
}