From 9026de22e588b7fb65a60f70bc34ef176dea9a97 Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Wed, 18 Feb 2026 21:42:06 -0500 Subject: [PATCH] Fixed barrier/gate/alarm menu --- src/app.rs | 100 ++++++++++++- src/protocols/keeloq_barriers.rs | 240 +++++++++++++++++++++++++++++++ src/protocols/mod.rs | 2 + 3 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 src/protocols/keeloq_barriers.rs diff --git a/src/app.rs b/src/app.rs index 8984dc4..116d8df 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,7 +7,7 @@ use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::Arc; use crate::capture::{ButtonCommand, Capture}; -use crate::protocols::ProtocolRegistry; +use crate::protocols::{is_keeloq_non_car, ProtocolRegistry}; use crate::radio::{HackRfController, LevelDuration, RtlSdrController}; use crate::storage::Storage; @@ -151,6 +151,7 @@ pub enum ExportFormat { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SignalAction { Replay, + SendNextCode, Lock, Unlock, Trunk, @@ -161,6 +162,7 @@ pub enum SignalAction { } impl SignalAction { + /// All actions (car keyfob menu). Barrier/alarm menu is built separately to include SendNextCode only there. pub const ALL: [SignalAction; 8] = [ SignalAction::Replay, SignalAction::Lock, @@ -175,6 +177,7 @@ impl SignalAction { pub fn label(&self) -> &'static str { match self { SignalAction::Replay => "Replay", + SignalAction::SendNextCode => "Send next code", SignalAction::Lock => "TX Lock", SignalAction::Unlock => "TX Unlock", SignalAction::Trunk => "TX Trunk", @@ -884,6 +887,76 @@ impl App { Ok(()) } + /// Transmit the next KeeLoq rolling code for a barrier/alarm capture (same button, counter+1). + pub fn transmit_next_code(&mut self, id: u32) -> Result<()> { + use crate::protocols::DecodedSignal; + + if let Some(ref radio) = self.radio { + if !radio.supports_tx() { + self.last_error = Some("Transmit not available – RTL-SDR is receive-only".to_string()); + return Ok(()); + } + } else { + self.last_error = Some("No radio device connected".to_string()); + return Ok(()); + } + + let capture = match self.captures.iter().find(|c| c.id == id) { + Some(c) => c.clone(), + None => { + self.last_error = Some(format!("Capture {} not found", id)); + return Ok(()); + } + }; + + if capture.protocol.is_none() { + self.last_error = Some("Cannot transmit: unknown protocol".to_string()); + return Ok(()); + } + + let protocol_name = capture.protocol.as_ref().unwrap(); + let protocol = match self.protocols.get(protocol_name) { + Some(p) => p, + None => { + self.last_error = Some(format!("Protocol {} not supported for encoding", protocol_name)); + return Ok(()); + } + }; + + if !protocol.supports_encoding() { + self.last_error = Some("Protocol does not support encoding".to_string()); + return Ok(()); + } + + let button = capture.button.unwrap_or(0); + let decoded = DecodedSignal { + serial: capture.serial, + button: capture.button, + counter: capture.counter, + crc_valid: capture.crc_valid, + data: capture.data, + data_count_bit: capture.data_count_bit, + encoder_capable: true, + extra: capture.data_extra, + protocol_display_name: None, + }; + + let signal = match protocol.encode(&decoded, button) { + Some(s) => s, + None => { + self.last_error = Some("Failed to encode next code".to_string()); + return Ok(()); + } + }; + + if let Some(ref mut radio) = self.radio { + radio.transmit(&signal, capture.frequency)?; + self.status_message = Some(format!("Sent next code for capture {} (button {})", id, button)); + } + + Ok(()) + } + /// Delete the currently selected capture (if any). No-op if none selected or list empty. pub fn delete_selected_capture(&mut self) -> Result<()> { let id = match self.selected_capture { @@ -1027,14 +1100,17 @@ impl App { // -- Signal Action Menu helpers -- /// Signal actions shown in the menu. With HackRF: Replay always; Lock/Unlock/Trunk/Panic only when - /// the selected capture is encoder-capable (unknown or decoded-only signals get Replay only). - /// Without TX (e.g. RTL-SDR): only export and delete. + /// the selected capture is encoder-capable and not a barrier/gate/garage or alarm (KeeLoq barrier + /// and alarm protocols get Replay + export + delete only). Without TX (e.g. RTL-SDR): only export and delete. pub fn available_signal_actions(&self) -> Vec { let has_tx = self.radio.as_ref().map_or(false, |r| r.supports_tx()); - let encoder_capable = self + let selected = self .selected_capture - .and_then(|idx| self.captures.get(idx)) + .and_then(|idx| self.captures.get(idx)); + let encoder_capable = selected .map_or(false, |c| c.status == crate::capture::CaptureStatus::EncoderCapable); + let is_non_car_keeloq = selected + .map_or(false, |c| is_keeloq_non_car(c.protocol_name())); if !has_tx { return SignalAction::ALL @@ -1049,6 +1125,17 @@ impl App { .collect(); } + // Barrier/gate/garage or alarm: Replay, Send next code (encoder next rolling code), export + delete + if encoder_capable && is_non_car_keeloq { + return vec![ + SignalAction::Replay, + SignalAction::SendNextCode, + SignalAction::ExportFob, + SignalAction::ExportFlipper, + SignalAction::Delete, + ]; + } + if encoder_capable { SignalAction::ALL.to_vec() } else { @@ -1088,6 +1175,9 @@ impl App { SignalAction::Replay => { self.replay_capture(capture_id)?; } + SignalAction::SendNextCode => { + self.transmit_next_code(capture_id)?; + } SignalAction::Lock => { let id_str = capture_id.to_string(); self.transmit_command(Some(&&*id_str.as_str()), ButtonCommand::Lock)?; diff --git a/src/protocols/keeloq_barriers.rs b/src/protocols/keeloq_barriers.rs new file mode 100644 index 0000000..79237fd --- /dev/null +++ b/src/protocols/keeloq_barriers.rs @@ -0,0 +1,240 @@ +//! KeeLoq barrier/gate/garage and alarm manufacturer names. +//! When a decode matches one of these, the signal action menu shows only Replay (and export/delete), +//! not TX Lock/Unlock/Trunk/Panic (those are for car keyfobs). + +use std::collections::HashSet; +use std::sync::OnceLock; + +fn normalize(s: &str) -> String { + s.chars() + .filter(|c| c.is_ascii_alphanumeric()) + .flat_map(|c| c.to_lowercase()) + .collect() +} + +/// Barrier/gate/garage door manufacturers (normalized names). +/// Covers Flipper/ProtoPirate-style names and common variants (e.g. NICE_MHOUSE, Guard_RF-311A). +const BARRIER_NAMES: &[&str] = &[ + "airforce", + "allmatic", + "alutechat4n", + "ansonic", + "aprimatic", + "beninca", + "benincavaaes128", + "bett", + "bft", + "cameatomo", + "cameatomotop44rbn", + "camespace", + "camestatic", + "cameweetwin", + "chamberlaincode", + "clemsa", + "comunello", + "deamio", + "dickertmahs", + "doitrand", + "doorhan", + "dooya", + "dtmneo", + "ecostar", + "elmespoland", + "elplast", + "faacrctx", + "faacslh", + "faacslhspa", + "feron", + "gatetx", + "gbidi", + "geniusbravo", + "geniusbravoecho", + "gibidi", + "gsn", + "guardrf311a", + "gangqi", + "hay21", + "hollarm", + "holtek", + "holtekht12x", + "honeywell", + "honeywellwdb", + "hormannbisecur", + "hormannhsm", + "ido", + "intertechnov3", + "ironlogic", + "jcmtech", + "jollymotors", + "kingatesstylo4k", + "legrand", + "linear", + "lineardelta3", + "magellan", + "marantec", + "marantec24", + "mastercode", + "megacode", + "merlin", + "monarch", + "motorline", + "mutancode", + "mutancomutancode", + "neroradio", + "nerosketch", + "niceflo", + "niceflorsone", + "nicemhouse", + "nicesmilo", + "novoferm", + "pecninin", + "pecnin", + "phoenixv2", + "powersmart", + "prastel", + "princeton", + "reversrb2", + "roger", + "rosh", + "rossi", + "sea", + "secplusv1v2", + "smc5326", + "somfykeytis", + "somfytelis", + "steelmate", + "stilmatic", +]; + +/// Alarm (car alarm / aftermarket) manufacturers (normalized names). +/// Same menu behaviour as barriers: Replay + export + delete only. +const ALARM_NAMES: &[&str] = &[ + "a2a4", + "sla2a4", + "a6a9", + "sla6a9", + "alligator", + "alligators275", + "aps1100", + "aps2550", + "aps1100aps2550", + "b6b9", + "slb6b9dop", + "cenmaxst5", + "cenmaxst7", + "cenmax", + "cfm", + "faraon", + "harpoon", + "jaguar", + "kgd", + "leopard", + "mongoose", + "panteraclk", + "panteraxsjaguar", + "partisanrx", + "pro1", + "pro2", + "pandorapro2", + "reff", + "sheriff", + "tomahawk9010", + "tomahawkzx35", + "tomahawktz9030", + "tz9030", + "starline", + "zx730", + "zx750", + "zx755", + "zx930", + "zx940", + "zx1070", + "zx1090", + "zx7307501055", +]; + +fn barrier_set() -> &'static HashSet { + static BARRIER_SET: OnceLock> = OnceLock::new(); + BARRIER_SET.get_or_init(|| BARRIER_NAMES.iter().map(|s| (*s).to_string()).collect()) +} + +fn alarm_set() -> &'static HashSet { + static ALARM_SET: OnceLock> = OnceLock::new(); + ALARM_SET.get_or_init(|| ALARM_NAMES.iter().map(|s| (*s).to_string()).collect()) +} + +/// Returns true if the protocol string is KeeLoq with a barrier/gate/garage door manufacturer. +/// Protocol is typically "KeeLoq (ManufacturerName)" e.g. "KeeLoq (BFT)" or "KeeLoq (NICE_MHOUSE)". +pub fn is_keeloq_barrier(protocol: &str) -> bool { + let protocol = protocol.trim(); + let open = protocol.find(" ("); + let Some(open_idx) = open else { return false }; + if !protocol[..open_idx].eq_ignore_ascii_case("keeloq") { + return false; + } + let rest = protocol[open_idx + 2..].trim_start(); + let close = rest.rfind(')'); + let Some(close_idx) = close else { return false }; + let inner = rest[..close_idx].trim(); + if inner.is_empty() { + return false; + } + barrier_set().contains(&normalize(inner)) +} + +/// Returns true if the protocol string is KeeLoq with an alarm (car alarm / aftermarket) manufacturer. +/// Same menu behaviour as barriers: Replay + export + delete only, no TX Lock/Unlock/Trunk/Panic. +pub fn is_keeloq_alarm(protocol: &str) -> bool { + let protocol = protocol.trim(); + let open = protocol.find(" ("); + let Some(open_idx) = open else { return false }; + if !protocol[..open_idx].eq_ignore_ascii_case("keeloq") { + return false; + } + let rest = protocol[open_idx + 2..].trim_start(); + let close = rest.rfind(')'); + let Some(close_idx) = close else { return false }; + let inner = rest[..close_idx].trim(); + if inner.is_empty() { + return false; + } + alarm_set().contains(&normalize(inner)) +} + +/// True if this KeeLoq protocol should hide car-style TX actions (barrier/gate or alarm). +pub fn is_keeloq_non_car(protocol: &str) -> bool { + is_keeloq_barrier(protocol) || is_keeloq_alarm(protocol) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn barrier_detection() { + assert!(is_keeloq_barrier("KeeLoq (BFT)")); + assert!(is_keeloq_barrier("KeeLoq (DoorHan)")); + assert!(is_keeloq_barrier("KeeLoq (NICE_MHOUSE)")); + assert!(is_keeloq_barrier("KeeLoq (Guard_RF-311A)")); + assert!(is_keeloq_barrier("KeeLoq (Stilmatic)")); + assert!(is_keeloq_barrier("KeeLoq (Motorline)")); + assert!(!is_keeloq_barrier("KeeLoq (KIAV5)")); + assert!(!is_keeloq_barrier("KeeLoq (Star Line)")); + assert!(!is_keeloq_barrier("Ford V0")); + assert!(!is_keeloq_barrier("Unknown")); + } + + #[test] + fn alarm_detection() { + assert!(is_keeloq_alarm("KeeLoq (Star Line)")); + assert!(is_keeloq_alarm("KeeLoq (Pantera_CLK)")); + assert!(is_keeloq_alarm("KeeLoq (Sheriff)")); + assert!(is_keeloq_alarm("KeeLoq (Alligator_S-275)")); + assert!(is_keeloq_alarm("KeeLoq (Harpoon)")); + assert!(is_keeloq_alarm("KeeLoq (Partisan_RX)")); + assert!(is_keeloq_alarm("KeeLoq (Cenmax_St-7)")); + assert!(is_keeloq_alarm("KeeLoq (Reff)")); + assert!(!is_keeloq_alarm("KeeLoq (KIAV5)")); + assert!(!is_keeloq_alarm("Ford V0")); + } +} diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index f0a6c1c..76c4fcd 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -19,6 +19,8 @@ mod common; pub mod keeloq_common; mod keeloq; +mod keeloq_barriers; +pub use keeloq_barriers::is_keeloq_non_car; mod keeloq_generic; #[allow(dead_code)] pub mod aut64;