1b9b33fa3f
Adds a rules-based device category router that classifies a signal by frequency band + timing ratio + pulse count BEFORE the per-protocol scoring loop, restricting the candidate set. This fixes the "everything matches a weather sensor with 69-76% false confidence" problem. - src/matcher/category_router.py: frequency-band + timing routing - pattern_decoder.py: category filter, category-mismatch penalty, post-match spread penalty (surfaces low-discrimination cases) - protocol_database.py: garage door / doorbell / fan controller entries - scripts/benchmark_phase0.py: real-world-shaped benchmark Benchmark gate: top-3 accuracy 0% -> 67% (target >=30%). 52/52 unit tests pass. NOTE: benchmark .sub files are synthetic-from-DB-params, so 67% is an upper bound pending real Flipper capture validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
754 lines
21 KiB
Python
754 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Protocol Database for Pattern-Based Decoder
|
|
|
|
Contains timing signatures and patterns for known RF protocols.
|
|
Extracted from Flipper Zero firmware and RTL_433 protocol definitions.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Optional, List, Dict, Tuple
|
|
from enum import Enum
|
|
|
|
|
|
class Modulation(Enum):
|
|
"""Signal modulation types"""
|
|
OOK = "OOK" # On-Off Keying
|
|
FSK = "FSK" # Frequency Shift Keying
|
|
ASK = "ASK" # Amplitude Shift Keying
|
|
|
|
|
|
class Encoding(Enum):
|
|
"""Pulse encoding schemes"""
|
|
PWM = "PWM" # Pulse Width Modulation (SHORT=0, LONG=1)
|
|
PPM = "PPM" # Pulse Position Modulation
|
|
MANCHESTER = "Manchester"
|
|
DIFFERENTIAL_MANCHESTER = "Differential Manchester"
|
|
|
|
|
|
@dataclass
|
|
class ProtocolSignature:
|
|
"""Signature for a known RF protocol"""
|
|
|
|
# Identification
|
|
name: str
|
|
category: str # Weather, Remote, Door, Sensor, etc.
|
|
manufacturer: Optional[str] = None
|
|
model: Optional[str] = None
|
|
|
|
# Frequency
|
|
frequency: int = 433920000 # Default 433.92 MHz
|
|
frequency_tolerance: int = 100000 # ±100 kHz
|
|
|
|
# Modulation
|
|
modulation: Modulation = Modulation.OOK
|
|
encoding: Encoding = Encoding.PWM
|
|
|
|
# Timing (microseconds)
|
|
short_pulse_us: int = 500
|
|
long_pulse_us: int = 1000
|
|
timing_tolerance: float = 0.2 # ±20%
|
|
|
|
# Patterns
|
|
preamble_pattern: Optional[str] = None # Binary pattern (e.g., "101010")
|
|
sync_pattern: Optional[str] = None
|
|
|
|
# Data
|
|
min_bits: int = 24
|
|
max_bits: int = 64
|
|
typical_pulse_count: int = 100
|
|
|
|
# Confidence thresholds
|
|
min_confidence: float = 0.6
|
|
|
|
def matches_timing(self, short_us: int, long_us: int) -> bool:
|
|
"""Check if observed timing matches this protocol"""
|
|
short_min = self.short_pulse_us * (1 - self.timing_tolerance)
|
|
short_max = self.short_pulse_us * (1 + self.timing_tolerance)
|
|
long_min = self.long_pulse_us * (1 - self.timing_tolerance)
|
|
long_max = self.long_pulse_us * (1 + self.timing_tolerance)
|
|
|
|
return (short_min <= short_us <= short_max and
|
|
long_min <= long_us <= long_max)
|
|
|
|
def matches_frequency(self, freq: int) -> bool:
|
|
"""Check if frequency matches this protocol"""
|
|
return abs(freq - self.frequency) <= self.frequency_tolerance
|
|
|
|
|
|
# Known Protocol Signatures
|
|
# Extracted from Flipper Zero firmware and RTL_433 protocol definitions
|
|
|
|
WEATHER_SENSORS = [
|
|
ProtocolSignature(
|
|
name="Oregon Scientific v2.1",
|
|
category="Weather Sensor",
|
|
manufacturer="Oregon Scientific",
|
|
short_pulse_us=488,
|
|
long_pulse_us=976,
|
|
encoding=Encoding.MANCHESTER,
|
|
preamble_pattern="1010" * 8, # 32-bit preamble
|
|
sync_pattern="1000",
|
|
min_bits=64,
|
|
max_bits=128,
|
|
typical_pulse_count=200,
|
|
),
|
|
ProtocolSignature(
|
|
name="Oregon Scientific v3.0",
|
|
category="Weather Sensor",
|
|
manufacturer="Oregon Scientific",
|
|
short_pulse_us=500,
|
|
long_pulse_us=1000,
|
|
encoding=Encoding.MANCHESTER,
|
|
preamble_pattern="1010" * 12,
|
|
sync_pattern="1000",
|
|
min_bits=64,
|
|
max_bits=128,
|
|
typical_pulse_count=250,
|
|
),
|
|
ProtocolSignature(
|
|
name="Acurite Tower Sensor",
|
|
category="Weather Sensor",
|
|
manufacturer="Acurite",
|
|
short_pulse_us=220,
|
|
long_pulse_us=440,
|
|
encoding=Encoding.PWM,
|
|
preamble_pattern=None,
|
|
sync_pattern="10",
|
|
min_bits=56,
|
|
max_bits=64,
|
|
typical_pulse_count=130,
|
|
),
|
|
ProtocolSignature(
|
|
name="Acurite 5n1 Weather Station",
|
|
category="Weather Sensor",
|
|
manufacturer="Acurite",
|
|
short_pulse_us=220,
|
|
long_pulse_us=440,
|
|
encoding=Encoding.PWM,
|
|
min_bits=64,
|
|
max_bits=80,
|
|
typical_pulse_count=160,
|
|
),
|
|
ProtocolSignature(
|
|
name="LaCrosse TX141TH-Bv2",
|
|
category="Weather Sensor",
|
|
manufacturer="LaCrosse",
|
|
short_pulse_us=500,
|
|
long_pulse_us=1000,
|
|
encoding=Encoding.PWM,
|
|
preamble_pattern="10" * 4,
|
|
min_bits=40,
|
|
max_bits=48,
|
|
typical_pulse_count=100,
|
|
),
|
|
ProtocolSignature(
|
|
name="Nexus Temperature/Humidity",
|
|
category="Weather Sensor",
|
|
manufacturer="Nexus",
|
|
short_pulse_us=500,
|
|
long_pulse_us=1000,
|
|
encoding=Encoding.PWM,
|
|
preamble_pattern="1" * 8,
|
|
min_bits=36,
|
|
max_bits=40,
|
|
typical_pulse_count=90,
|
|
),
|
|
ProtocolSignature(
|
|
name="Ambient Weather F007TH",
|
|
category="Weather Sensor",
|
|
manufacturer="Ambient Weather",
|
|
short_pulse_us=500,
|
|
long_pulse_us=1000,
|
|
encoding=Encoding.PWM,
|
|
min_bits=64,
|
|
max_bits=72,
|
|
typical_pulse_count=150,
|
|
),
|
|
]
|
|
|
|
GARAGE_DOOR_OPENERS = [
|
|
ProtocolSignature(
|
|
name="Princeton",
|
|
category="Garage Door Opener",
|
|
manufacturer=None,
|
|
short_pulse_us=400,
|
|
long_pulse_us=1200,
|
|
encoding=Encoding.PWM,
|
|
preamble_pattern="1" * 4,
|
|
sync_pattern="10",
|
|
min_bits=24,
|
|
max_bits=32,
|
|
typical_pulse_count=60,
|
|
),
|
|
ProtocolSignature(
|
|
name="Chamberlain/LiftMaster 315MHz",
|
|
category="Garage Door Opener",
|
|
manufacturer="Chamberlain",
|
|
short_pulse_us=300,
|
|
long_pulse_us=900,
|
|
encoding=Encoding.PWM,
|
|
min_bits=32,
|
|
max_bits=40,
|
|
typical_pulse_count=80,
|
|
frequency=315000000,
|
|
),
|
|
ProtocolSignature(
|
|
name="LiftMaster 433MHz",
|
|
category="Garage Door Opener",
|
|
manufacturer="LiftMaster",
|
|
# Fixed-code LiftMaster (pre-Security+) at 433 MHz
|
|
# Uses ~350µs SHORT, ~1050µs LONG (3:1 ratio like Princeton but 40-bit)
|
|
short_pulse_us=350,
|
|
long_pulse_us=1050,
|
|
encoding=Encoding.PWM,
|
|
min_bits=32,
|
|
max_bits=40,
|
|
typical_pulse_count=90,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="LiftMaster Security+ 2.0",
|
|
category="Garage Door Opener",
|
|
manufacturer="LiftMaster",
|
|
# Security+ 2.0 uses rolling code — we can only detect the family by
|
|
# its distinctive 390 MHz carrier and ~300µs pulse width
|
|
short_pulse_us=300,
|
|
long_pulse_us=600,
|
|
encoding=Encoding.PWM,
|
|
min_bits=40,
|
|
max_bits=66,
|
|
typical_pulse_count=100,
|
|
frequency=390000000,
|
|
frequency_tolerance=2000000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Linear MegaCode",
|
|
category="Garage Door Opener",
|
|
manufacturer="Linear",
|
|
short_pulse_us=250,
|
|
long_pulse_us=500,
|
|
encoding=Encoding.PWM,
|
|
min_bits=32,
|
|
max_bits=32,
|
|
typical_pulse_count=70,
|
|
frequency=318000000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Marantec D302 / D304",
|
|
category="Garage Door Opener",
|
|
manufacturer="Marantec",
|
|
short_pulse_us=1000,
|
|
long_pulse_us=2000,
|
|
encoding=Encoding.PWM,
|
|
min_bits=12,
|
|
max_bits=16,
|
|
typical_pulse_count=35,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="FAAC XT2 / XT4",
|
|
category="Garage Door Opener",
|
|
manufacturer="FAAC",
|
|
short_pulse_us=500,
|
|
long_pulse_us=1500,
|
|
encoding=Encoding.PWM,
|
|
min_bits=40,
|
|
max_bits=64,
|
|
typical_pulse_count=110,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Came TOP432 / BRC802",
|
|
category="Garage Door Opener",
|
|
manufacturer="Came",
|
|
short_pulse_us=500,
|
|
long_pulse_us=1000,
|
|
encoding=Encoding.PWM,
|
|
min_bits=12,
|
|
max_bits=24,
|
|
typical_pulse_count=55,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="BFT Mitto Rolling Code",
|
|
category="Garage Door Opener",
|
|
manufacturer="BFT",
|
|
short_pulse_us=500,
|
|
long_pulse_us=1500,
|
|
encoding=Encoding.PWM,
|
|
min_bits=52,
|
|
max_bits=64,
|
|
typical_pulse_count=125,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="CAME-АТЛАС Gate Remote",
|
|
category="Garage Door Opener",
|
|
manufacturer="Came",
|
|
short_pulse_us=320,
|
|
long_pulse_us=960,
|
|
encoding=Encoding.PWM,
|
|
min_bits=24,
|
|
max_bits=24,
|
|
typical_pulse_count=55,
|
|
frequency=433920000,
|
|
),
|
|
]
|
|
|
|
DOORBELLS = [
|
|
ProtocolSignature(
|
|
name="Honeywell Doorbell",
|
|
category="Doorbell",
|
|
manufacturer="Honeywell",
|
|
short_pulse_us=175,
|
|
long_pulse_us=340,
|
|
encoding=Encoding.PWM,
|
|
min_bits=48,
|
|
max_bits=48,
|
|
typical_pulse_count=100,
|
|
),
|
|
ProtocolSignature(
|
|
name="Byron Doorbell",
|
|
category="Doorbell",
|
|
manufacturer="Byron",
|
|
# Byron WE-series: Princeton-like OOK, 300-400us SHORT, 3:1 ratio
|
|
short_pulse_us=350,
|
|
long_pulse_us=1050,
|
|
encoding=Encoding.PWM,
|
|
preamble_pattern="1" * 4,
|
|
sync_pattern="10000", # Long sync gap
|
|
min_bits=24,
|
|
max_bits=32,
|
|
typical_pulse_count=60,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="GE Doorbell",
|
|
category="Doorbell",
|
|
manufacturer="GE",
|
|
# GE wireless doorbell 433 MHz, ~250us SHORT, 2:1 ratio
|
|
short_pulse_us=250,
|
|
long_pulse_us=500,
|
|
encoding=Encoding.PWM,
|
|
min_bits=24,
|
|
max_bits=48,
|
|
typical_pulse_count=80,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Heidemann Doorbell",
|
|
category="Doorbell",
|
|
manufacturer="Heidemann",
|
|
short_pulse_us=400,
|
|
long_pulse_us=1200,
|
|
encoding=Encoding.PWM,
|
|
min_bits=12,
|
|
max_bits=24,
|
|
typical_pulse_count=40,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Elro DB286A Doorbell",
|
|
category="Doorbell",
|
|
manufacturer="Elro",
|
|
short_pulse_us=300,
|
|
long_pulse_us=900,
|
|
encoding=Encoding.PWM,
|
|
preamble_pattern="1" * 4,
|
|
min_bits=24,
|
|
max_bits=24,
|
|
typical_pulse_count=50,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Blyss Doorbell",
|
|
category="Doorbell",
|
|
manufacturer="Blyss",
|
|
short_pulse_us=500,
|
|
long_pulse_us=1500,
|
|
encoding=Encoding.PWM,
|
|
min_bits=32,
|
|
max_bits=40,
|
|
typical_pulse_count=80,
|
|
frequency=433920000,
|
|
),
|
|
]
|
|
|
|
TIRE_PRESSURE = [
|
|
ProtocolSignature(
|
|
name="Toyota TPMS",
|
|
category="TPMS",
|
|
manufacturer="Toyota",
|
|
short_pulse_us=50,
|
|
long_pulse_us=100,
|
|
encoding=Encoding.MANCHESTER,
|
|
min_bits=64,
|
|
max_bits=80,
|
|
typical_pulse_count=160,
|
|
frequency=315000000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Schrader TPMS",
|
|
category="TPMS",
|
|
manufacturer="Schrader",
|
|
short_pulse_us=50,
|
|
long_pulse_us=100,
|
|
encoding=Encoding.MANCHESTER,
|
|
min_bits=64,
|
|
max_bits=80,
|
|
typical_pulse_count=160,
|
|
frequency=433920000,
|
|
),
|
|
]
|
|
|
|
SECURITY_SENSORS = [
|
|
ProtocolSignature(
|
|
name="Magellan",
|
|
category="Security Sensor",
|
|
manufacturer="Paradox",
|
|
short_pulse_us=250,
|
|
long_pulse_us=500,
|
|
encoding=Encoding.PWM,
|
|
min_bits=32,
|
|
max_bits=48,
|
|
typical_pulse_count=80,
|
|
frequency=433920000,
|
|
),
|
|
]
|
|
|
|
REMOTE_CONTROLS = [
|
|
ProtocolSignature(
|
|
name="PT2262",
|
|
category="Remote Control",
|
|
manufacturer=None,
|
|
short_pulse_us=350,
|
|
long_pulse_us=1050,
|
|
encoding=Encoding.PWM,
|
|
preamble_pattern="1" * 4,
|
|
min_bits=24,
|
|
max_bits=24,
|
|
typical_pulse_count=50,
|
|
),
|
|
ProtocolSignature(
|
|
name="PT2260",
|
|
category="Remote Control",
|
|
manufacturer=None,
|
|
short_pulse_us=300,
|
|
long_pulse_us=900,
|
|
encoding=Encoding.PWM,
|
|
min_bits=24,
|
|
max_bits=24,
|
|
typical_pulse_count=50,
|
|
),
|
|
ProtocolSignature(
|
|
name="EV1527",
|
|
category="Remote Control",
|
|
manufacturer=None,
|
|
short_pulse_us=300,
|
|
long_pulse_us=900,
|
|
encoding=Encoding.PWM,
|
|
min_bits=24,
|
|
max_bits=24,
|
|
typical_pulse_count=50,
|
|
),
|
|
ProtocolSignature(
|
|
name="Generic Remote SC226x EV1527",
|
|
category="Remote Control",
|
|
manufacturer=None,
|
|
short_pulse_us=320,
|
|
long_pulse_us=960,
|
|
encoding=Encoding.PWM,
|
|
min_bits=24,
|
|
max_bits=24,
|
|
typical_pulse_count=52,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="HCS301 Rolling Code",
|
|
category="Remote Control",
|
|
manufacturer="Microchip",
|
|
short_pulse_us=400,
|
|
long_pulse_us=800,
|
|
encoding=Encoding.PWM,
|
|
min_bits=66,
|
|
max_bits=66,
|
|
typical_pulse_count=140,
|
|
),
|
|
ProtocolSignature(
|
|
name="RGB LED Remote Controller",
|
|
category="Remote Control",
|
|
manufacturer=None,
|
|
# Most 433 MHz RGB LED remotes use EV1527-like encoding
|
|
short_pulse_us=300,
|
|
long_pulse_us=900,
|
|
encoding=Encoding.PWM,
|
|
min_bits=24,
|
|
max_bits=24,
|
|
typical_pulse_count=52,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Nice Flor-S Rolling Code",
|
|
category="Remote Control",
|
|
manufacturer="Nice",
|
|
short_pulse_us=500,
|
|
long_pulse_us=1000,
|
|
encoding=Encoding.PWM,
|
|
min_bits=52,
|
|
max_bits=56,
|
|
typical_pulse_count=115,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="FAAC SLH Rolling Code",
|
|
category="Remote Control",
|
|
manufacturer="FAAC",
|
|
short_pulse_us=500,
|
|
long_pulse_us=1500,
|
|
encoding=Encoding.PWM,
|
|
min_bits=40,
|
|
max_bits=64,
|
|
typical_pulse_count=110,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Somfy RTS",
|
|
category="Remote Control",
|
|
manufacturer="Somfy",
|
|
# Somfy RTS uses Manchester at 433.42 MHz
|
|
short_pulse_us=604,
|
|
long_pulse_us=1208,
|
|
encoding=Encoding.MANCHESTER,
|
|
min_bits=56,
|
|
max_bits=56,
|
|
typical_pulse_count=120,
|
|
frequency=433420000,
|
|
frequency_tolerance=50000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Holtek HT12X Remote",
|
|
category="Remote Control",
|
|
manufacturer="Holtek",
|
|
short_pulse_us=300,
|
|
long_pulse_us=900,
|
|
encoding=Encoding.PWM,
|
|
preamble_pattern="1" * 36, # Long burst sync
|
|
min_bits=12,
|
|
max_bits=12,
|
|
typical_pulse_count=52,
|
|
frequency=433920000,
|
|
),
|
|
]
|
|
|
|
FAN_CONTROLLERS = [
|
|
ProtocolSignature(
|
|
name="Hampton Bay Ceiling Fan Remote",
|
|
category="Fan Controller",
|
|
manufacturer="Hampton Bay",
|
|
# Most common: 303 MHz or 433 MHz, OOK, 12-bit dip-switch code
|
|
short_pulse_us=300,
|
|
long_pulse_us=900,
|
|
encoding=Encoding.PWM,
|
|
min_bits=12,
|
|
max_bits=24,
|
|
typical_pulse_count=45,
|
|
frequency=303900000,
|
|
frequency_tolerance=200000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Hampton Bay Ceiling Fan Remote 433MHz",
|
|
category="Fan Controller",
|
|
manufacturer="Hampton Bay",
|
|
short_pulse_us=320,
|
|
long_pulse_us=960,
|
|
encoding=Encoding.PWM,
|
|
min_bits=12,
|
|
max_bits=24,
|
|
typical_pulse_count=45,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Hunter Fan Remote",
|
|
category="Fan Controller",
|
|
manufacturer="Hunter",
|
|
short_pulse_us=250,
|
|
long_pulse_us=750,
|
|
encoding=Encoding.PWM,
|
|
min_bits=16,
|
|
max_bits=32,
|
|
typical_pulse_count=55,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Harbor Breeze Ceiling Fan Remote",
|
|
category="Fan Controller",
|
|
manufacturer="Harbor Breeze",
|
|
short_pulse_us=300,
|
|
long_pulse_us=900,
|
|
encoding=Encoding.PWM,
|
|
min_bits=12,
|
|
max_bits=24,
|
|
typical_pulse_count=50,
|
|
frequency=433920000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Westinghouse Fan Remote",
|
|
category="Fan Controller",
|
|
manufacturer="Westinghouse",
|
|
short_pulse_us=330,
|
|
long_pulse_us=990,
|
|
encoding=Encoding.PWM,
|
|
min_bits=16,
|
|
max_bits=24,
|
|
typical_pulse_count=48,
|
|
frequency=303900000,
|
|
frequency_tolerance=200000,
|
|
),
|
|
ProtocolSignature(
|
|
name="Generic Ceiling Fan Remote (EV1527)",
|
|
category="Fan Controller",
|
|
manufacturer=None,
|
|
# Many cheap ceiling fans use EV1527 at 433 MHz
|
|
short_pulse_us=300,
|
|
long_pulse_us=900,
|
|
encoding=Encoding.PWM,
|
|
min_bits=24,
|
|
max_bits=24,
|
|
typical_pulse_count=52,
|
|
frequency=433920000,
|
|
),
|
|
]
|
|
|
|
|
|
# Import RTL_433 protocols
|
|
try:
|
|
from src.matcher.rtl433_protocols_imported import RTL433_PROTOCOLS
|
|
except ImportError:
|
|
print("Warning: RTL_433 protocols not imported yet. Run scripts/import_rtl433_protocols.py")
|
|
RTL433_PROTOCOLS = []
|
|
|
|
# Compile all protocols into single list
|
|
ALL_PROTOCOLS = (
|
|
WEATHER_SENSORS +
|
|
GARAGE_DOOR_OPENERS +
|
|
DOORBELLS +
|
|
TIRE_PRESSURE +
|
|
SECURITY_SENSORS +
|
|
REMOTE_CONTROLS +
|
|
FAN_CONTROLLERS +
|
|
RTL433_PROTOCOLS # Imported from RTL_433 database
|
|
)
|
|
|
|
|
|
class ProtocolDatabase:
|
|
"""Database of known RF protocol signatures"""
|
|
|
|
def __init__(self):
|
|
self.protocols = ALL_PROTOCOLS
|
|
self._by_category: Dict[str, List[ProtocolSignature]] = {}
|
|
self._by_frequency: Dict[int, List[ProtocolSignature]] = {}
|
|
self._index_protocols()
|
|
|
|
def _index_protocols(self):
|
|
"""Build indexes for fast lookup"""
|
|
for proto in self.protocols:
|
|
# Index by category
|
|
if proto.category not in self._by_category:
|
|
self._by_category[proto.category] = []
|
|
self._by_category[proto.category].append(proto)
|
|
|
|
# Index by frequency (rounded to MHz)
|
|
freq_mhz = round(proto.frequency / 1_000_000)
|
|
if freq_mhz not in self._by_frequency:
|
|
self._by_frequency[freq_mhz] = []
|
|
self._by_frequency[freq_mhz].append(proto)
|
|
|
|
def find_by_timing(
|
|
self,
|
|
short_us: int,
|
|
long_us: int,
|
|
frequency: Optional[int] = None
|
|
) -> List[ProtocolSignature]:
|
|
"""Find protocols matching timing characteristics"""
|
|
matches = []
|
|
|
|
candidates = self.protocols
|
|
if frequency:
|
|
freq_mhz = round(frequency / 1_000_000)
|
|
candidates = self._by_frequency.get(freq_mhz, self.protocols)
|
|
|
|
for proto in candidates:
|
|
if proto.matches_timing(short_us, long_us):
|
|
if not frequency or proto.matches_frequency(frequency):
|
|
matches.append(proto)
|
|
|
|
return matches
|
|
|
|
def find_by_category(self, category: str) -> List[ProtocolSignature]:
|
|
"""Find all protocols in a category"""
|
|
return self._by_category.get(category, [])
|
|
|
|
def find_by_frequency(self, frequency: int) -> List[ProtocolSignature]:
|
|
"""Find protocols near a frequency"""
|
|
matches = []
|
|
for proto in self.protocols:
|
|
if proto.matches_frequency(frequency):
|
|
matches.append(proto)
|
|
return matches
|
|
|
|
def get_all(self) -> List[ProtocolSignature]:
|
|
"""Get all protocols"""
|
|
return self.protocols
|
|
|
|
def get_statistics(self) -> Dict:
|
|
"""Get database statistics"""
|
|
return {
|
|
"total_protocols": len(self.protocols),
|
|
"categories": list(self._by_category.keys()),
|
|
"by_category": {
|
|
cat: len(protos)
|
|
for cat, protos in self._by_category.items()
|
|
},
|
|
"frequency_bands": list(set(
|
|
round(p.frequency / 1_000_000) for p in self.protocols
|
|
)),
|
|
}
|
|
|
|
|
|
# Global instance
|
|
_database: Optional[ProtocolDatabase] = None
|
|
|
|
|
|
def get_protocol_database() -> ProtocolDatabase:
|
|
"""Get singleton protocol database instance"""
|
|
global _database
|
|
if _database is None:
|
|
_database = ProtocolDatabase()
|
|
return _database
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# Test protocol database
|
|
db = get_protocol_database()
|
|
|
|
print("=== Protocol Database Statistics ===")
|
|
stats = db.get_statistics()
|
|
print(f"Total protocols: {stats['total_protocols']}")
|
|
print(f"Categories: {', '.join(stats['categories'])}")
|
|
print()
|
|
print("Protocols by category:")
|
|
for cat, count in stats['by_category'].items():
|
|
print(f" {cat}: {count}")
|
|
print()
|
|
print(f"Frequency bands: {', '.join(str(f) + ' MHz' for f in sorted(stats['frequency_bands']))}")
|
|
print()
|
|
|
|
# Test timing match
|
|
print("=== Testing Timing Match ===")
|
|
print("Looking for protocols with SHORT=500us, LONG=1000us @ 433.92 MHz")
|
|
matches = db.find_by_timing(500, 1000, 433920000)
|
|
print(f"Found {len(matches)} matches:")
|
|
for proto in matches:
|
|
print(f" - {proto.name} ({proto.manufacturer or 'Unknown'}) - {proto.category}")
|