Initial commit: Phase 1 & Phase 2 infrastructure complete

This commit is contained in:
2026-01-12 11:21:17 -08:00
commit eb225771bc
53 changed files with 13645 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
"""
Device signature matching engine
"""
from .engine import SignatureMatcher, MatchResult
from .strategies import ExactMatcher, PartialMatcher, PatternMatcher, TimingMatcher
__all__ = [
'SignatureMatcher',
'MatchResult',
'ExactMatcher',
'PartialMatcher',
'PatternMatcher',
'TimingMatcher'
]
+120
View File
@@ -0,0 +1,120 @@
"""
Main signature matching engine
"""
from dataclasses import dataclass
from typing import List, Optional, Dict, Any
from loguru import logger
from ..parser.metadata import SignalMetadata
@dataclass
class MatchResult:
"""Result of a signature match"""
device_id: int
device_name: str
manufacturer: str
confidence: float # 0.0 to 1.0
match_method: str # 'exact', 'partial', 'pattern', 'timing'
match_details: Dict[str, Any]
def to_dict(self) -> Dict:
"""Convert to dictionary"""
return {
'device_id': self.device_id,
'device_name': self.device_name,
'manufacturer': self.manufacturer,
'confidence': self.confidence,
'match_method': self.match_method,
'match_details': self.match_details
}
class SignatureMatcher:
"""
Main signature matching engine
Coordinates multiple matching strategies to identify devices
from RF signal metadata
"""
def __init__(self, database):
"""
Initialize matcher with database connection
Args:
database: Database connection for signature queries
"""
self.db = database
self.strategies = []
def add_strategy(self, strategy):
"""Add a matching strategy"""
self.strategies.append(strategy)
def match(self, metadata: SignalMetadata, max_results: int = 10) -> List[MatchResult]:
"""
Match signal metadata against signature database
Args:
metadata: Parsed signal metadata
max_results: Maximum number of results to return
Returns:
List of MatchResult objects sorted by confidence
"""
all_matches = []
# Run all matching strategies
for strategy in self.strategies:
try:
matches = strategy.match(metadata, self.db)
all_matches.extend(matches)
except Exception as e:
logger.error(f"Strategy {strategy.__class__.__name__} failed: {e}")
# Deduplicate and sort
unique_matches = self._deduplicate_matches(all_matches)
sorted_matches = sorted(unique_matches, key=lambda x: x.confidence, reverse=True)
return sorted_matches[:max_results]
def _deduplicate_matches(self, matches: List[MatchResult]) -> List[MatchResult]:
"""
Deduplicate matches, keeping highest confidence for each device
Args:
matches: List of match results
Returns:
Deduplicated list
"""
device_map = {}
for match in matches:
if match.device_id not in device_map:
device_map[match.device_id] = match
else:
# Keep higher confidence match
if match.confidence > device_map[match.device_id].confidence:
device_map[match.device_id] = match
return list(device_map.values())
class MatchStrategy:
"""Base class for matching strategies"""
def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
"""
Match metadata against database
Args:
metadata: Signal metadata
db: Database connection
Returns:
List of MatchResult objects
"""
raise NotImplementedError
+358
View File
@@ -0,0 +1,358 @@
"""
Signature matching strategies
"""
from typing import List
from loguru import logger
from .engine import MatchStrategy, MatchResult
from ..parser.metadata import SignalMetadata
class ExactMatcher(MatchStrategy):
"""
Exact matching strategy: protocol + frequency + bit length
Highest confidence (1.0) for perfect matches
"""
def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
"""Match by exact protocol, frequency, and bit length"""
matches = []
if not metadata.protocol or not metadata.bit_length:
return matches
# Query database for exact matches
query = """
SELECT DISTINCT
d.id,
d.manufacturer,
d.model,
s.protocol,
s.frequency
FROM devices d
JOIN signatures s ON s.device_id = d.id
WHERE s.protocol = %s
AND s.frequency = %s
AND (s.bit_length = %s OR s.bit_length IS NULL)
"""
results = db.execute(query, (
metadata.protocol,
metadata.frequency,
metadata.bit_length
))
for row in results:
matches.append(MatchResult(
device_id=row['id'],
device_name=row['model'],
manufacturer=row['manufacturer'],
confidence=1.0,
match_method='exact',
match_details={
'protocol': row['protocol'],
'frequency': row['frequency'],
'bit_length': metadata.bit_length
}
))
logger.debug(f"ExactMatcher found {len(matches)} matches")
return matches
class PartialMatcher(MatchStrategy):
"""
Partial matching: protocol + frequency only
Confidence: 0.8
"""
def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
"""Match by protocol and frequency only"""
matches = []
if not metadata.protocol:
return matches
query = """
SELECT DISTINCT
d.id,
d.manufacturer,
d.model,
s.protocol,
s.frequency
FROM devices d
JOIN signatures s ON s.device_id = d.id
WHERE s.protocol = %s
AND s.frequency = %s
"""
results = db.execute(query, (metadata.protocol, metadata.frequency))
for row in results:
matches.append(MatchResult(
device_id=row['id'],
device_name=row['model'],
manufacturer=row['manufacturer'],
confidence=0.8,
match_method='partial',
match_details={
'protocol': row['protocol'],
'frequency': row['frequency']
}
))
logger.debug(f"PartialMatcher found {len(matches)} matches")
return matches
class PatternMatcher(MatchStrategy):
"""
Bit pattern matching with masks
Confidence: 0.7-0.9 based on pattern similarity
"""
def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
"""Match by bit pattern similarity"""
matches = []
if not metadata.key_data:
return matches
# Query signatures with bit patterns
query = """
SELECT DISTINCT
d.id,
d.manufacturer,
d.model,
s.bit_pattern,
s.bit_mask,
s.weight,
s.protocol,
s.frequency
FROM devices d
JOIN signatures s ON s.device_id = d.id
WHERE s.bit_pattern IS NOT NULL
AND s.frequency = %s
"""
results = db.execute(query, (metadata.frequency,))
for row in results:
# Apply mask and compare
if row['bit_mask']:
similarity = self._compare_with_mask(
metadata.key_data,
row['bit_pattern'],
row['bit_mask']
)
else:
similarity = self._compare_bytes(
metadata.key_data,
row['bit_pattern']
)
if similarity > 0.5: # Minimum threshold
confidence = similarity * row.get('weight', 1.0) * 0.9
matches.append(MatchResult(
device_id=row['id'],
device_name=row['model'],
manufacturer=row['manufacturer'],
confidence=confidence,
match_method='pattern',
match_details={
'protocol': row['protocol'],
'frequency': row['frequency'],
'similarity': similarity
}
))
logger.debug(f"PatternMatcher found {len(matches)} matches")
return matches
def _compare_with_mask(self, data1: bytes, data2: bytes, mask: bytes) -> float:
"""
Compare two byte arrays with a mask
Args:
data1: First byte array
data2: Second byte array
mask: Mask (1=compare, 0=ignore)
Returns:
Similarity score (0.0 to 1.0)
"""
if not data1 or not data2 or not mask:
return 0.0
# Ensure same length
min_len = min(len(data1), len(data2), len(mask))
matching_bits = 0
total_bits = 0
for i in range(min_len):
mask_byte = mask[i] if i < len(mask) else 0xFF
# Count bits to compare (1s in mask)
bits_to_check = bin(mask_byte).count('1')
total_bits += bits_to_check
# Compare masked bytes
masked1 = data1[i] & mask_byte
masked2 = data2[i] & mask_byte
# Count matching bits
xor_result = masked1 ^ masked2
matching_bits += bits_to_check - bin(xor_result).count('1')
if total_bits == 0:
return 0.0
return matching_bits / total_bits
def _compare_bytes(self, data1: bytes, data2: bytes) -> float:
"""
Compare two byte arrays directly
Returns:
Similarity score (0.0 to 1.0)
"""
if not data1 or not data2:
return 0.0
min_len = min(len(data1), len(data2))
matches = sum(1 for i in range(min_len) if data1[i] == data2[i])
return matches / max(len(data1), len(data2))
class TimingMatcher(MatchStrategy):
"""
Timing pattern matching for RAW signals
Confidence: 0.6-0.8 based on timing similarity
"""
def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
"""Match by timing pattern characteristics"""
matches = []
if not metadata.raw_data or not metadata.avg_pulse_width:
return matches
# Query signatures with timing information
query = """
SELECT DISTINCT
d.id,
d.manufacturer,
d.model,
s.timing_min,
s.timing_max,
s.protocol,
s.frequency
FROM devices d
JOIN signatures s ON s.device_id = d.id
WHERE s.timing_min IS NOT NULL
AND s.frequency = %s
"""
results = db.execute(query, (metadata.frequency,))
avg_pulse = metadata.avg_pulse_width
for row in results:
timing_min = row['timing_min']
timing_max = row['timing_max']
# Check if average pulse width falls within range
if timing_min <= avg_pulse <= timing_max:
# Calculate confidence based on how centered the value is
range_size = timing_max - timing_min
center = (timing_max + timing_min) / 2
distance_from_center = abs(avg_pulse - center)
# Confidence decreases as we move away from center
confidence = 0.8 * (1.0 - (distance_from_center / (range_size / 2)))
confidence = max(0.6, min(0.8, confidence))
matches.append(MatchResult(
device_id=row['id'],
device_name=row['model'],
manufacturer=row['manufacturer'],
confidence=confidence,
match_method='timing',
match_details={
'protocol': row['protocol'],
'frequency': row['frequency'],
'avg_pulse_width': avg_pulse,
'timing_range': (timing_min, timing_max)
}
))
logger.debug(f"TimingMatcher found {len(matches)} matches")
return matches
class FrequencyMatcher(MatchStrategy):
"""
Fuzzy frequency matching (within tolerance)
Confidence: 0.5-0.7 based on frequency proximity
"""
def __init__(self, tolerance_hz: int = 5000):
"""
Initialize frequency matcher
Args:
tolerance_hz: Frequency tolerance in Hz (default 5kHz)
"""
self.tolerance = tolerance_hz
def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
"""Match by frequency proximity"""
matches = []
query = """
SELECT DISTINCT
d.id,
d.manufacturer,
d.model,
s.frequency,
s.protocol
FROM devices d
JOIN signatures s ON s.device_id = d.id
WHERE s.frequency BETWEEN %s AND %s
AND (s.protocol IS NULL OR s.protocol = 'Unknown')
"""
freq_min = metadata.frequency - self.tolerance
freq_max = metadata.frequency + self.tolerance
results = db.execute(query, (freq_min, freq_max))
for row in results:
# Calculate confidence based on frequency difference
freq_diff = abs(row['frequency'] - metadata.frequency)
confidence = 0.7 * (1.0 - (freq_diff / self.tolerance))
confidence = max(0.5, min(0.7, confidence))
matches.append(MatchResult(
device_id=row['id'],
device_name=row['model'],
manufacturer=row['manufacturer'],
confidence=confidence,
match_method='frequency',
match_details={
'frequency': row['frequency'],
'frequency_diff_hz': freq_diff
}
))
logger.debug(f"FrequencyMatcher found {len(matches)} matches")
return matches