Initial commit: Phase 1 & Phase 2 infrastructure complete
This commit is contained in:
@@ -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'
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user