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 <noreply@anthropic.com>
This commit is contained in:
@@ -146,10 +146,41 @@ class PatternDecoder:
|
|||||||
Identify a decoded (KEY) .sub file from its Protocol name.
|
Identify a decoded (KEY) .sub file from its Protocol name.
|
||||||
|
|
||||||
KEY files have no RAW pulses but carry a decoded `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
|
frequency and key. Two cases:
|
||||||
emit a match surfacing the real Flipper protocol + routed category, so
|
|
||||||
decoded uploads are no longer silently unidentifiable.
|
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(
|
pred = self.category_router.route_by_protocol(
|
||||||
metadata.protocol, metadata.frequency
|
metadata.protocol, metadata.frequency
|
||||||
)
|
)
|
||||||
@@ -172,6 +203,30 @@ class PatternDecoder:
|
|||||||
details=details,
|
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]:
|
def _identify_pulse_widths(self, pulses: List[int]) -> Tuple[int, int, int, int]:
|
||||||
"""
|
"""
|
||||||
Identify SHORT/LONG pulse and gap durations using robust timing analyzer
|
Identify SHORT/LONG pulse and gap durations using robust timing analyzer
|
||||||
|
|||||||
Reference in New Issue
Block a user