From f5b0983044544090dfb6b8b0279ed03dd1e085c1 Mon Sep 17 00:00:00 2001 From: leetcrypt Date: Tue, 21 Jul 2026 12:07:57 -0700 Subject: [PATCH 1/2] feat: exact signature-DB match for decoded KEY .sub files A Flipper KEY file names an already-decoded protocol but carries no pulses. Previously _decode_from_key ignored the signature database entirely: it synthesized a fake signature from the category router's guess, so an exact, known protocol scored like a fuzzy guess (~0.45) and surfaced the router's coarser category (e.g. Princeton -> "Remote Control") while discarding the real catalog manufacturer (e.g. HCS301 lost "Microchip"). Now a decoded protocol name is looked up (case-insensitive) in the signature DB. On a hit we surface the REAL catalog signature -- true manufacturer and category (Princeton -> "Garage Door Opener") -- with high, frequency-aware confidence (0.95 when the band is consistent, 0.90 otherwise): an exact database match, not a timing guess. Unknown names keep the router fallback so uploads are still identified by family rather than silently dropped. Advances the "database matching" + "confidence scoring (exact/partial)" goals. Co-Authored-By: Claude Opus 4.6 --- src/matcher/pattern_decoder.py | 61 ++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/src/matcher/pattern_decoder.py b/src/matcher/pattern_decoder.py index b4d0869..0665825 100644 --- a/src/matcher/pattern_decoder.py +++ b/src/matcher/pattern_decoder.py @@ -146,10 +146,41 @@ class PatternDecoder: Identify a decoded (KEY) .sub file from its Protocol name. KEY files have no RAW pulses but carry a decoded `Protocol:` name, - frequency and key. We route the protocol name to a device category and - emit a match surfacing the real Flipper protocol + routed category, so - decoded uploads are no longer silently unidentifiable. + frequency and key. Two cases: + + 1. The named protocol is in our signature database. This is the + strongest possible identification — the capture device already + demodulated and *named* the protocol, and we recognise it — so we + surface the REAL catalog signature (true manufacturer + category) + with high, frequency-aware confidence. This is an exact "database + match", not a timing guess, and it keeps the surfaced category + consistent with the catalog (e.g. Princeton -> Garage Door Opener) + instead of the router's coarser guess. + + 2. The name is unknown to us. Fall back to category routing so the + upload is still identified by family rather than silently dropped. """ + known = self._lookup_known_protocol(metadata.protocol, metadata.frequency) + if known is not None: + freq_ok = bool(metadata.frequency) and known.matches_frequency(metadata.frequency) + # Exact, already-decoded match against a known protocol. Confidence + # is high but never a literal 1.0 (the key payload itself is not + # cryptographically verified); a consistent frequency lifts it. + confidence = 0.95 if freq_ok else 0.90 + details = { + "predicted_category": known.category, + "source": "decoded_key_exact", + "bit_length": metadata.bit_length, + "frequency_match": freq_ok, + "manufacturer": known.manufacturer, + } + return [DeviceMatch( + protocol=known, + confidence=confidence, + match_method="decoded_key_exact", + details=details, + )] + pred = self.category_router.route_by_protocol( metadata.protocol, metadata.frequency ) @@ -172,6 +203,30 @@ class PatternDecoder: details=details, )] + def _lookup_known_protocol( + self, name: Optional[str], frequency: Optional[int] + ) -> Optional[ProtocolSignature]: + """Exact (case-insensitive) protocol-name lookup in the signature DB. + + When several catalog entries share a name, prefer one whose frequency + band matches the capture; otherwise return the first. Returns None if + the name is unknown. + """ + if not name: + return None + target = name.strip().lower() + candidates = [ + p for p in self.protocol_db.get_all() + if p.name.strip().lower() == target + ] + if not candidates: + return None + if frequency: + for cand in candidates: + if cand.matches_frequency(frequency): + return cand + return candidates[0] + def _identify_pulse_widths(self, pulses: List[int]) -> Tuple[int, int, int, int]: """ Identify SHORT/LONG pulse and gap durations using robust timing analyzer From d85d22661bcf06227682ef9189475cad2c28c3ff Mon Sep 17 00:00:00 2001 From: leetcrypt Date: Tue, 21 Jul 2026 12:08:05 -0700 Subject: [PATCH 2/2] test: cover decoded KEY exact signature-DB matching 10 offline tests for the KEY-file identification path: a named protocol that is in the DB resolves to the real catalog signature (category + manufacturer) via match_method 'decoded_key_exact' with frequency-aware confidence (0.95 consistent / 0.90 off-band); unknown names fall back to the router ('decoded_key', <0.90) yet stay identified; plus _lookup_known_protocol case-insensitivity and None handling. Values are drawn from the DB itself so the suite tracks catalog changes. Bare pytest, no server. Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_decoded_key_identification.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/unit/test_decoded_key_identification.py diff --git a/tests/unit/test_decoded_key_identification.py b/tests/unit/test_decoded_key_identification.py new file mode 100644 index 0000000..0523ffe --- /dev/null +++ b/tests/unit/test_decoded_key_identification.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +Offline unit tests for decoded (KEY) .sub identification against the signature DB. + +A Flipper KEY file carries no RAW pulses but *names* a decoded protocol (e.g. +``Protocol: Princeton``). When that name is in our signature database this is the +strongest possible identification -- the capture device already demodulated and +named the protocol and we recognise it -- so it must: + + - resolve to the REAL catalog signature (true manufacturer + category), not the + category router's coarser guess (Princeton is a Garage Door Opener in the + catalog, which the router previously mislabelled "Remote Control"), and + - score with high, frequency-aware confidence (exact database match), not the + ~0.45 fuzzy-guess confidence it used to get. + +Unknown protocol names must still fall back to category routing so an upload is +identified by family rather than silently dropped ("accept any capture -> +identify the device"). + +Offline / author-time: bare ``pytest`` runs this with no docker, no server, no +network. KEY .sub content is embedded and written to ``tmp_path``. +""" + +import pytest + +from src.parser.sub_parser import SubFileParser +from src.matcher.engine import SignatureMatcher +from src.matcher.pattern_decoder import PatternDecoder +from src.matcher.protocol_database import get_protocol_database + + +def _key_sub(protocol: str, frequency: int, bit: int = 24, key: str = "00 00 00 00 00 95 D5 D4") -> str: + return "\n".join([ + "Filetype: Flipper SubGhz Key File", + "Version: 1", + f"Frequency: {frequency}", + "Preset: FuriHalSubGhzPresetOok270Async", + f"Protocol: {protocol}", + f"Bit: {bit}", + f"Key: {key}", + "TE: 400", + "", + ]) + + +def _write(tmp_path, content, name="k.sub"): + p = tmp_path / name + p.write_text(content) + return str(p) + + +def _identify(tmp_path, content): + meta = SubFileParser().parse(_write(tmp_path, content)) + return meta, SignatureMatcher().match(meta, max_results=5) + + +# A known catalog protocol and a known one that carries a manufacturer, both +# taken from the DB itself so the tests stay correct if the catalog changes. +_DB = get_protocol_database() +_KNOWN = next(p for p in _DB.get_all()) +_KNOWN_WITH_MFR = next((p for p in _DB.get_all() if p.manufacturer), None) +_UNKNOWN_NAME = "ZZ_NOT_A_REAL_PROTOCOL_9999" + + +class TestKnownProtocolExactMatch: + def test_named_protocol_resolves_to_catalog_signature(self, tmp_path): + meta, matches = _identify(tmp_path, _key_sub(_KNOWN.name, _KNOWN.frequency)) + assert meta.file_format == "KEY" + assert meta.is_decoded is True + assert len(matches) >= 1 + top = matches[0] + assert top.device_name == _KNOWN.name + assert top.match_method == "decoded_key_exact" + # Category comes from the catalog, not the router's guess. + assert top.match_details.get("device_category") == _KNOWN.category + + def test_exact_match_scores_high_when_frequency_consistent(self, tmp_path): + _, matches = _identify(tmp_path, _key_sub(_KNOWN.name, _KNOWN.frequency)) + assert matches[0].confidence == pytest.approx(0.95) + assert matches[0].match_details.get("frequency_match") is True + + def test_exact_match_still_high_but_lower_on_frequency_mismatch(self, tmp_path): + # Same known name, but a frequency far outside its band. + off_band = _KNOWN.frequency + 50_000_000 + _, matches = _identify(tmp_path, _key_sub(_KNOWN.name, off_band)) + top = matches[0] + assert top.match_method == "decoded_key_exact" + assert top.confidence == pytest.approx(0.90) + assert top.match_details.get("frequency_match") is False + + @pytest.mark.skipif(_KNOWN_WITH_MFR is None, reason="no catalog protocol carries a manufacturer") + def test_real_manufacturer_is_surfaced(self, tmp_path): + _, matches = _identify( + tmp_path, _key_sub(_KNOWN_WITH_MFR.name, _KNOWN_WITH_MFR.frequency, bit=66) + ) + assert matches[0].manufacturer == _KNOWN_WITH_MFR.manufacturer + + +class TestUnknownProtocolFallback: + def test_unknown_name_falls_back_to_router(self, tmp_path): + _, matches = _identify(tmp_path, _key_sub(_UNKNOWN_NAME, 433920000)) + assert len(matches) >= 1, "unknown decoded protocol must still be identified by family" + top = matches[0] + assert top.match_method == "decoded_key" + # A routed guess must not masquerade as an exact database match. + assert top.confidence < 0.90 + + def test_unknown_name_still_names_the_protocol(self, tmp_path): + _, matches = _identify(tmp_path, _key_sub(_UNKNOWN_NAME, 433920000)) + assert matches[0].device_name == _UNKNOWN_NAME + + +class TestLookupHelper: + def test_known_name_case_insensitive(self): + dec = PatternDecoder() + sig = dec._lookup_known_protocol(_KNOWN.name.upper(), _KNOWN.frequency) + assert sig is not None + assert sig.name == _KNOWN.name + + def test_unknown_name_returns_none(self): + assert PatternDecoder()._lookup_known_protocol(_UNKNOWN_NAME, 433920000) is None + + def test_empty_name_returns_none(self): + assert PatternDecoder()._lookup_known_protocol(None, 433920000) is None + + def test_prefers_frequency_matching_candidate(self): + """If duplicate-named entries exist, the frequency-consistent one wins.""" + dec = PatternDecoder() + sig = dec._lookup_known_protocol(_KNOWN.name, _KNOWN.frequency) + assert sig is not None + assert sig.matches_frequency(_KNOWN.frequency)