Fixed barrier/gate/alarm menu

This commit is contained in:
KaraZajac
2026-02-18 21:42:06 -05:00
parent 8ef3b98634
commit 9026de22e5
3 changed files with 337 additions and 5 deletions
+95 -5
View File
@@ -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<SignalAction> {
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)?;
+240
View File
@@ -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<String> {
static BARRIER_SET: OnceLock<HashSet<String>> = OnceLock::new();
BARRIER_SET.get_or_init(|| BARRIER_NAMES.iter().map(|s| (*s).to_string()).collect())
}
fn alarm_set() -> &'static HashSet<String> {
static ALARM_SET: OnceLock<HashSet<String>> = 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"));
}
}
+2
View File
@@ -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;