""" RAW Signal Data Parser Parses RAW_Data from Flipper Zero .sub files to extract timing signatures for protocol identification. Author: GigLez Team Date: January 2026 """ import re import logging from typing import List, Dict, Optional, Tuple from dataclasses import dataclass import statistics logger = logging.getLogger(__name__) @dataclass class TimingSignature: """Timing signature extracted from RAW data""" pulses: List[int] # All pulse durations (absolute values) high_pulses: List[int] # RF-on pulse durations low_pulses: List[int] # RF-off pulse durations mean_high: float mean_low: float std_high: float std_low: float short_pulse: int # Likely short pulse width long_pulse: int # Likely long pulse width gap: Optional[int] # Gap between transmissions pulse_ratio: float # long / short ratio encoding_type: str # PWM, PPM, Manchester, etc. total_pulses: int duration_ms: float class RAWParser: """ Parser for Flipper Zero RAW_Data format RAW_Data format: space-separated integers - Positive: RF on duration (microseconds) - Negative: RF off duration (microseconds) Example: "2980 -240 520 -980 520 -980 980 -520 ..." """ # Encoding detection thresholds PWM_RATIO_MIN = 1.5 # Long/Short ratio > 1.5 suggests PWM PPM_RATIO_MIN = 2.5 # Long/Short ratio > 2.5 suggests PPM MANCHESTER_RATIO_MAX = 1.3 # Long/Short ratio < 1.3 suggests Manchester def __init__(self): """Initialize RAW parser""" pass def parse(self, raw_data: str) -> Optional[TimingSignature]: """ Parse RAW_Data string to extract timing signature Args: raw_data: RAW_Data string from .sub file Returns: TimingSignature or None if parsing fails """ try: # Parse integers from string values = self._parse_raw_string(raw_data) if not values or len(values) < 10: logger.warning(f"Insufficient RAW data: {len(values) if values else 0} values") return None # Separate high and low pulses high_pulses = [v for v in values if v > 0] low_pulses = [abs(v) for v in values if v < 0] if not high_pulses or not low_pulses: logger.warning("No high or low pulses found") return None # Calculate statistics mean_high = statistics.mean(high_pulses) mean_low = statistics.mean(low_pulses) std_high = statistics.stdev(high_pulses) if len(high_pulses) > 1 else 0 std_low = statistics.stdev(low_pulses) if len(low_pulses) > 1 else 0 # Identify short and long pulses (cluster analysis) short_pulse, long_pulse = self._identify_pulse_widths(high_pulses) # Identify gap (longest low pulse, if significantly longer) gap = self._identify_gap(low_pulses) # Calculate pulse ratio pulse_ratio = long_pulse / short_pulse if short_pulse > 0 else 1.0 # Detect encoding type encoding_type = self._detect_encoding(pulse_ratio, high_pulses, low_pulses) # Calculate total duration duration_ms = sum(abs(v) for v in values) / 1000.0 signature = TimingSignature( pulses=[abs(v) for v in values], high_pulses=high_pulses, low_pulses=low_pulses, mean_high=mean_high, mean_low=mean_low, std_high=std_high, std_low=std_low, short_pulse=short_pulse, long_pulse=long_pulse, gap=gap, pulse_ratio=pulse_ratio, encoding_type=encoding_type, total_pulses=len(values), duration_ms=duration_ms ) logger.debug(f"Parsed RAW signal: {short_pulse}µs/{long_pulse}µs, " f"ratio={pulse_ratio:.2f}, encoding={encoding_type}") return signature except Exception as e: logger.error(f"Failed to parse RAW data: {e}") return None def _parse_raw_string(self, raw_data: str) -> List[int]: """Parse RAW_Data string to list of integers""" # Remove any non-numeric characters except spaces and minus signs cleaned = re.sub(r'[^0-9\s\-]', '', raw_data) # Split and convert to integers values = [] for token in cleaned.split(): try: values.append(int(token)) except ValueError: continue return values def _identify_pulse_widths(self, pulses: List[int]) -> Tuple[int, int]: """ Identify short and long pulse widths using clustering Uses simple percentile-based clustering: - Short pulse: 25th percentile - Long pulse: 75th percentile Args: pulses: List of pulse durations Returns: (short_pulse, long_pulse) in microseconds """ if not pulses: return (0, 0) sorted_pulses = sorted(pulses) # Use percentiles for robust estimation short_idx = len(sorted_pulses) // 4 # 25th percentile long_idx = (3 * len(sorted_pulses)) // 4 # 75th percentile short_pulse = sorted_pulses[short_idx] long_pulse = sorted_pulses[long_idx] # Ensure they're different if short_pulse == long_pulse: # Fall back to min/max short_pulse = min(pulses) long_pulse = max(pulses) return (short_pulse, long_pulse) def _identify_gap(self, low_pulses: List[int]) -> Optional[int]: """ Identify gap between transmissions Gap is typically the longest low pulse, if significantly longer than median low pulse. Args: low_pulses: List of RF-off durations Returns: Gap duration in microseconds or None """ if not low_pulses or len(low_pulses) < 3: return None median = statistics.median(low_pulses) max_pulse = max(low_pulses) # Gap is significantly longer than median (3x threshold) if max_pulse > median * 3: return max_pulse return None def _detect_encoding(self, pulse_ratio: float, high_pulses: List[int], low_pulses: List[int]) -> str: """ Detect encoding type from timing characteristics Encoding types: - PWM (Pulse Width Modulation): Different pulse widths (1.5 < ratio < 2.5) - PPM (Pulse Position Modulation): Very different pulse widths (ratio > 2.5) - Manchester: Similar pulse widths (ratio < 1.3) - OOK (On-Off Keying): Generic fallback Args: pulse_ratio: Long pulse / Short pulse ratio high_pulses: RF-on pulses low_pulses: RF-off pulses Returns: Encoding type string """ # Manchester encoding: transitions at every bit if pulse_ratio < self.MANCHESTER_RATIO_MAX: # Check for consistent timing if statistics.stdev(high_pulses) < statistics.mean(high_pulses) * 0.3: return "Manchester" # PPM: position encoding if pulse_ratio > self.PPM_RATIO_MIN: return "PPM" # PWM: width encoding if pulse_ratio > self.PWM_RATIO_MIN: return "PWM" # Default to OOK return "OOK" def extract_te(self, signature: TimingSignature) -> int: """ Extract TE (Timing Element) - the base timing unit For many protocols, TE is the GCD of pulse widths. Simpler approach: use short pulse as TE. Args: signature: Timing signature Returns: TE in microseconds """ return signature.short_pulse def parse_raw_data(raw_data: str) -> Optional[TimingSignature]: """ Convenience function to parse RAW data Args: raw_data: RAW_Data string from .sub file Returns: TimingSignature or None """ parser = RAWParser() return parser.parse(raw_data) if __name__ == '__main__': # Test the parser logging.basicConfig(level=logging.DEBUG) print("\n" + "="*60) print("RAW DATA PARSER TEST") print("="*60) # Test 1: PWM signal test_pwm = "2980 -240 520 -980 520 -980 980 -520 520 -980 980 -520 520 -3000" print("\n1. PWM Signal Test") print(f" Input: {test_pwm[:50]}...") signature = parse_raw_data(test_pwm) if signature: print(f" Short pulse: {signature.short_pulse}µs") print(f" Long pulse: {signature.long_pulse}µs") print(f" Pulse ratio: {signature.pulse_ratio:.2f}") print(f" Encoding: {signature.encoding_type}") print(f" Gap: {signature.gap}µs" if signature.gap else " Gap: None") print(f" Total pulses: {signature.total_pulses}") print(f" Duration: {signature.duration_ms:.1f}ms") # Test 2: Manchester-like signal test_manchester = "500 -500 500 -500 1000 -500 500 -1000 500 -500" print("\n2. Manchester-like Signal Test") print(f" Input: {test_manchester}") signature = parse_raw_data(test_manchester) if signature: print(f" Short pulse: {signature.short_pulse}µs") print(f" Long pulse: {signature.long_pulse}µs") print(f" Pulse ratio: {signature.pulse_ratio:.2f}") print(f" Encoding: {signature.encoding_type}") # Test 3: Real Flipper Zero capture test_real = "7960 -3980 396 -796 400 -392 400 -792 400 -396 796 -396 400 -792 796 -396" print("\n3. Real Flipper Capture Test") print(f" Input: {test_real}") signature = parse_raw_data(test_real) if signature: print(f" Short pulse: {signature.short_pulse}µs") print(f" Long pulse: {signature.long_pulse}µs") print(f" Pulse ratio: {signature.pulse_ratio:.2f}") print(f" Encoding: {signature.encoding_type}") print(f" Mean high: {signature.mean_high:.0f}µs") print(f" Mean low: {signature.mean_low:.0f}µs") print("\n" + "="*60)