Fixed barrier/gate/alarm menu
This commit is contained in:
+95
-5
@@ -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)?;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user