//! 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 (Star Line)")); 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)")); } }