diff --git a/scripts/README.md b/scripts/README.md index 6793aa5..2d86307 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,20 +1,94 @@ -# Scripts +# KAT scripts -## analyze_sub_vag.py +## demod_sub_to_bits.py -Analyzes a Flipper `.sub` file against the VAG decoder logic in `src/protocols/vag.rs` to see why a signal does not decode as VAG (or even appear as Unknown). +Demodulate Flipper SubGhz RAW `.sub` files into a stream of `1` and `0` bits using duration-based on-off keying (OOK). The script parses level+duration pulses, classifies each as **short** or **long** against configurable timing, then outputs bits according to the chosen encoding. + +### Input format + +The script reads Flipper-style `.sub` files: + +- **RAW_Data:** space-separated signed integers (one per pulse). +- **Positive value** = HIGH level, **negative** = LOW; **magnitude** = duration in microseconds. +- Optional **Frequency:** line (Hz). If missing, 433 920 000 is assumed. + +Same convention as KAT’s `.sub` import and ProtoPirate’s raw file reader. + +### Encoding modes + +| Mode | Description | +|-------------|-------------| +| **pwm** | One bit per pulse: short duration → `0`, long duration → `1`. Unmatched durations are skipped. Default. | +| **manchester** | Short/long pulses fed into a Manchester state machine (Ford V0–style). Outputs decoded data bits. Long gaps reset state. | +| **raw** | No duration decoding: one character per pulse, `1` = HIGH, `0` = LOW. | + +### Timing parameters + +Pulses are classified using nominal short/long durations and a tolerance: + +- **--te-short** — Nominal short duration (µs). Default: 250. +- **--te-long** — Nominal long duration (µs). Default: 500. +- **--te-delta** — Tolerance (µs): a pulse is “short” if `|duration - te_short| ≤ te_delta`, “long” if `|duration - te_long| ≤ te_delta`. Default: 100. + +Examples: + +- Ford V0–style: `--te-short 250 --te-long 500 --te-delta 100` +- VAG/VW 500 µs: `--te-short 500 --te-long 1000 --te-delta 120` + +### Usage -**Usage:** ```bash -python scripts/analyze_sub_vag.py path/to/file.sub -python scripts/analyze_sub_vag.py "IMPORTS/VOLKSWAGEN AUDI/Test_55_unlock_and_55_lock_suran.sub" +# PWM decode (short→0, long→1), default 250/500 µs +python3 scripts/demod_sub_to_bits.py path/to/file.sub + +# PWM with VAG-like timing +python3 scripts/demod_sub_to_bits.py path/to/file.sub --te-short 500 --te-long 1000 --te-delta 120 + +# Manchester decode (e.g. Ford) +python3 scripts/demod_sub_to_bits.py path/to/file.sub --encoding manchester --te-short 250 --te-long 500 + +# Wrap output to 80 characters per line +python3 scripts/demod_sub_to_bits.py path/to/file.sub --encoding pwm --wrap 80 + +# Only decode HIGH pulses (or --pulse-level low) +python3 scripts/demod_sub_to_bits.py path/to/file.sub --encoding pwm --pulse-level high + +# Raw level stream (no duration decoding) +python3 scripts/demod_sub_to_bits.py path/to/file.sub --encoding raw + +# Print level:duration for each pulse (no bits) +python3 scripts/demod_sub_to_bits.py path/to/file.sub --with-durations ``` -**What it does:** -- Parses the .sub file (Frequency + RAW_Data: positive = HIGH, negative = LOW, duration in µs). -- Checks whether the file frequency is within VAG’s supported range (433.92 / 434.42 MHz, 2% tolerance as in KAT). -- Simulates the VAG decoder’s **Reset** step: the first HIGH pulse must be 300±79 µs (Type 1/2) or 500±79 µs (Type 3/4). Reports the first few pulses and why they pass or fail. -- Scans the full stream for the first HIGH pulse that matches 300±79 or 500±79 (VAG-like preamble), for both normal and inverted polarity. +### Options summary -**Why a file might not show up at all:** -KAT only creates a capture (including “Unknown”) when some decoder returns a result. The VAG decoder only leaves the Reset state when it sees a HIGH pulse of 300±79 or 500±79 µs. If the stream starts with other data (e.g. 133 µs HIGH), the decoder never leaves Reset and never emits a decode, so no capture is created for that stream. +| Option | Default | Description | +|--------|---------|-------------| +| `--encoding` | pwm | `raw`, `pwm`, or `manchester` | +| `--te-short` | 250 | Nominal short pulse (µs) | +| `--te-long` | 500 | Nominal long pulse (µs) | +| `--te-delta` | 100 | Timing tolerance (µs) | +| `--pulse-level` | both | For PWM: `high`, `low`, or `both` | +| `--gap-us` | 10000 | Manchester: gap (µs) that resets state | +| `--wrap` | 0 | Wrap bit string every N characters (0 = no wrap) | +| `--with-durations` | off | Print `level:duration` instead of bits | + +### Output + +- Decoded bit string is printed to **stdout** (e.g. `1011001...` or wrapped lines). +- A single comment line (pulse count, frequency, encoding, timing) is printed to **stderr**. + +### Requirements + +- Python 3.9+ (for `list[tuple[...]]` type hints; can be relaxed if needed). +- No extra dependencies; uses only the standard library. + +### Inspecting raw pulses (e.g. for VAG) + +To see the first pulses of a `.sub` file (level and duration in µs) without decoding: + +```bash +python3 scripts/demod_sub_to_bits.py path/to/file.sub --encoding raw --with-durations +``` + +VAG Type 3/4 expects: first pulse **HIGH** ~500 µs, then LOW ~500 µs, repeated (preamble); after ≥41 such pairs, HIGH ~1000 µs then LOW ~500 µs (sync), then 3×750 µs, then data. If the first pulse is LOW or durations are outside 500±79/80, the decoder will not lock. diff --git a/scripts/SUB_DECODE_PROTOPIRATE_VS_KAT.md b/scripts/SUB_DECODE_PROTOPIRATE_VS_KAT.md deleted file mode 100644 index d441b8e..0000000 --- a/scripts/SUB_DECODE_PROTOPIRATE_VS_KAT.md +++ /dev/null @@ -1,104 +0,0 @@ -# Sub Decode: ProtoPirate vs KAT - -Comparison of how **ProtoPirate** (REFERENCES/protopirate) and **KAT** decode Flipper `.sub` (RAW) files. - -## File format - -Both use the same Flipper SubGhz RAW format: - -- **Filetype:** `Flipper SubGhz RAW File` -- **Protocol:** `RAW` -- **Frequency:** one value (Hz) -- **RAW_Data:** space-separated **int32** values: - - **Positive** = HIGH level - - **Negative** = LOW level - - **Magnitude** = duration in **microseconds** - -So parsing and (level, duration) stream content are the same. - ---- - -## ProtoPirate sub decode (REFERENCES/protopirate) - -### Where it lives - -- **Scene:** `scenes/protopirate_scene_sub_decode.c` -- **Raw reader:** `helpers/raw_file_reader.c` / `raw_file_reader.h` - -### Flow - -1. **Open file** - `raw_file_reader_open()` uses FlipperFormat to open the file, checks header "Flipper SubGhz RAW File" and Protocol "RAW". Does **not** read Frequency in the reader; the scene reads that separately. - -2. **Read metadata (scene)** - In `DecodeStateReadHeader` / `DecodeStateStartingWorker` the scene opens the file again with FlipperFormat and reads: - - **Frequency** (default 433920000 if missing) - - **Preset** (e.g. AM650, FM238) and maps it to the SubGhz preset used for the receiver. - -3. **Feed stream** - In `DecodeStateDecodingRaw`: - - Loop: `raw_file_reader_get_next(ctx->raw_reader, &level, &duration)` to get the next (level, duration). - - For each pair: **`subghz_receiver_decode(app->txrx->receiver, level, duration)`**. - - Reads in chunks of **128** samples per tick (`SAMPLES_TO_READ_PER_TICK`) for UI responsiveness. - -4. **On decode** - When the Flipper receiver reports a decode (`protopirate_sub_decode_receiver_callback`): - - Add the decode to history. - - **`subghz_receiver_reset(receiver)`** so all decoders are reset. - - Continue feeding the **same** file from the next sample. - -So: one continuous stream from the file, (level, duration) in order; on each decode → record → reset receiver → keep going. **No** polarity inversion in this path. **No** sliding window or multiple start positions. - -### Raw file reader details - -- **`raw_file_reader_get_next()`** (raw_file_reader.c): - Reads next int32 from buffer; if buffer is empty, loads next chunk via `flipper_format_read_int32(..., "RAW_Data", ...)`. - `level = (value >= 0)`, `duration = abs(value)`. Same convention as KAT. - ---- - -## KAT sub import (src/app.rs + protocols/mod.rs + export/flipper.rs) - -### Where it lives - -- **Import:** `src/export/flipper.rs` → `import_sub_raw(path)` → returns `(frequency, Vec)`. -- **Decode:** `src/protocols/mod.rs` → `process_signal_stream()` / `process_signal_stream_inner()`. -- **Use:** `src/app.rs` → when loading a .sub file, calls `import_sub_raw` then `protocols.process_signal_stream(&pairs, frequency)`. - -### Flow - -1. **Parse file** - `import_sub_raw()`: - - Reads whole file as text. - - Parses **Frequency** (default 433_920_000 if missing). - - Parses all **RAW_Data** lines into one list of (level, duration) with the same rule: positive ⇒ HIGH, negative ⇒ LOW, duration = abs(value) µs. - -2. **Decode stream** - `process_signal_stream(pairs, frequency)`: - - Tries **normal polarity** first: `process_signal_stream_inner(pairs, frequency, false)`. - - If that returns **no** decodes, tries **inverted polarity**: `process_signal_stream_inner(pairs, frequency, true)` (flip level for every pair). - - Inner loop: for each (level, duration) in order, for each decoder that supports the file frequency, call **`decoder.feed(level, duration_us)`**. If any decoder returns a decode: - - Record (protocol name, decoded signal, segment of pairs). - - **Reset all decoders** and set `segment_start` to next index. - - Continue from the next pair. - -So: one pass over the in-memory stream; on each decode → record → reset all decoders → continue. **Difference:** KAT also runs a **second** pass with **inverted polarity** if the first pass finds nothing. - ---- - -## Differences summary - -| Aspect | ProtoPirate | KAT | -|--------|-------------|-----| -| **File format** | Same (Flipper RAW, positive=HIGH, negative=LOW, µs) | Same | -| **Stream order** | Same (sequential level/duration from file) | Same | -| **Frequency** | Read from file, used for preset/receiver | Read from file, used for decoder filter (2% tolerance) | -| **Decode loop** | One (level, duration) at a time → `subghz_receiver_decode()` | One (level, duration) at a time → `decoder.feed()` for each decoder | -| **On decode** | Add to history, **subghz_receiver_reset()**, continue same file | Append to list, **reset all decoders**, continue same stream | -| **Polarity** | Single polarity (as in file) | Tries **normal**, then **inverted** if no decodes | -| **Sliding window / multiple starts** | No | No | -| **“No protocol” result** | Shows “No ProtoPirate protocol detected” (no “Unknown” capture) | No capture if no decoder ever returns (same idea) | - -So the **sub decode strategy is the same**: single stream, reset-after-each-decode, no sliding window. The only functional difference is **KAT’s extra inverted-polarity pass** when the normal pass finds no decodes. - -For a file like the VAG Suran .sub: the first HIGH pulse is 133 µs, so VAG’s Reset condition (300±79 or 500±79 µs) is never met. In both codebases the decoder would stay in Reset and never emit a decode; neither would create a capture from that stream. ProtoPirate would show “No ProtoPirate protocol detected”; KAT would add no capture. So for that case the behavior is aligned; fixing it would require something like trying decode from multiple start indices (sliding window) or trimming to the first VAG-like preamble, in either codebase. diff --git a/scripts/analyze_sub_vag.py b/scripts/analyze_sub_vag.py deleted file mode 100644 index 1faaa78..0000000 --- a/scripts/analyze_sub_vag.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -""" -Analyze a Flipper .sub file against the VAG decoder logic (vag.rs) to determine -why the signal does not decode as VAG (or even as Unknown). - -Usage: - python scripts/analyze_sub_vag.py path/to/file.sub - python scripts/analyze_sub_vag.py IMPORTS/Test_55_unlock_and_55_lock_suran.sub - -Reads the .sub file (Frequency + RAW_Data: positive=HIGH, negative=LOW, µs), -then: - 1. Checks if frequency is in VAG's supported range (433.92 / 434.42 MHz, 2% tolerance). - 2. Simulates the VAG decoder state machine (Reset -> Preamble1/2 -> ...) and reports - the first pulse index where the decoder fails and why. - 3. Scans for the first occurrence of a VAG-like preamble (HIGH 300±79 or 500±79 µs) - so you can see if the frame appears later in the stream. - 4. Tries both normal and inverted polarity (KAT tries inverted if normal yields no decode). -""" - -import re -import sys -from pathlib import Path - -# --- Timing constants from vag.rs (Type 1/2 vs Type 3/4) --- -TE_SHORT_12 = 300 # Type 1/2 short -TE_LONG_12 = 600 # Type 1/2 long -TE_SHORT = 500 # Type 3/4 short -TE_LONG = 1000 # Type 3/4 long -REF_RESET_DELTA = 79 -REF_PREAMBLE_SYNC = 80 -REF_GAP1_DELTA = 79 - -# VAG supported frequencies (Hz) from vag.rs -VAG_FREQS = [433_920_000, 434_420_000] - -# KAT frequency tolerance: diff < (f / 50) => 2% -def frequency_supported(file_hz: int) -> tuple[bool, str]: - for f in VAG_FREQS: - diff = abs(f - file_hz) - if diff < (f / 50): - return True, f"Frequency {file_hz} Hz is within 2% of VAG supported {f} Hz" - return False, f"Frequency {file_hz} Hz is NOT in VAG supported list {VAG_FREQS} (2% tolerance)" - - -def parse_sub(path: Path) -> tuple[int, list[tuple[bool, int]]]: - """Parse .sub file; return (frequency_hz, list of (level, duration_us)).""" - text = path.read_text() - frequency_hz = 433_920_000 - raw_data: list[int] = [] - - for line in text.splitlines(): - line = line.strip() - if line.startswith("Frequency:"): - frequency_hz = int(line.split(":", 1)[1].strip()) - elif line.startswith("RAW_Data:"): - rest = line.split(":", 1)[1].strip() - for word in rest.split(): - raw_data.append(int(word)) - - # positive => HIGH (True), negative => LOW (False); duration in µs - pairs: list[tuple[bool, int]] = [] - for v in raw_data: - duration_us = abs(v) - level = v >= 0 - pairs.append((level, duration_us)) - - return frequency_hz, pairs - - -def analyze_reset_and_first_steps(pairs: list[tuple[bool, int]], invert: bool) -> list[str]: - """ - Simulate VAG decoder from Reset: what does it expect, and at which index do we fail? - Returns list of report lines. - """ - lines: list[str] = [] - count = 0 - for i, (level, duration) in enumerate(pairs): - if invert: - level = not level - if count >= 20: - break - if i == 0: - lines.append("") - lines.append("--- Step-by-step from start (first 20 pulses) ---") - lines.append("Reset expects: first pulse must be HIGH, duration 300±79 µs (Type1/2) or 500±79 µs (Type3/4).") - # Reset: only look at HIGH pulses - if not level: - lines.append(f" [{i}] LOW {duration:5} µs -> Reset ignores LOW (stays in Reset)") - count += 1 - continue - # HIGH pulse - count += 1 - if duration < TE_SHORT_12: - diff = TE_SHORT_12 - duration - if diff <= REF_RESET_DELTA: - lines.append(f" [{i}] HIGH {duration:5} µs -> 300-duration={diff}<=79 -> would enter Preamble1 (Type1/2)") - else: - lines.append(f" [{i}] HIGH {duration:5} µs -> 300-duration={diff}>79 -> REJECT (stays Reset)") - elif duration - TE_SHORT_12 <= REF_RESET_DELTA: - lines.append(f" [{i}] HIGH {duration:5} µs -> duration-300<={duration - TE_SHORT_12}<=79 -> would enter Preamble1 (Type1/2)") - else: - if TE_SHORT - REF_RESET_DELTA <= duration <= TE_SHORT + REF_RESET_DELTA: - lines.append(f" [{i}] HIGH {duration:5} µs -> 500±79 -> would enter Preamble2 (Type3/4)") - else: - lines.append(f" [{i}] HIGH {duration:5} µs -> not 300±79 nor 500±79 -> REJECT (stays Reset)") - return lines - - -def find_first_vag_like_pulse( - pairs: list[tuple[bool, int]], invert: bool, tolerance_300: int = 79, tolerance_500: int = 79 -) -> list[str]: - """Find first HIGH pulse that looks like VAG preamble (300±tolerance or 500±tolerance).""" - lines: list[str] = [] - for i, (level, duration) in enumerate(pairs): - if invert: - level = not level - if not level: - continue - ok_300 = (TE_SHORT_12 - tolerance_300 <= duration <= TE_SHORT_12 + tolerance_300) - ok_500 = (TE_SHORT - tolerance_500 <= duration <= TE_SHORT + tolerance_500) - if ok_300 or ok_500: - kind = f"300±{tolerance_300} (Type1/2)" if ok_300 else f"500±{tolerance_500} (Type3/4)" - lines.append(f"First VAG-like HIGH pulse at index {i}: {duration} µs ({kind})") - return lines - lines.append(f"No HIGH pulse matches 300±{tolerance_300} or 500±{tolerance_500} µs.") - return lines - - -def scan_vag_preamble_with_tolerances(pairs: list[tuple[bool, int]], invert: bool) -> list[str]: - """ - Scan entire file for HIGH pulses that could be VAG preamble (300 or 500 µs) - with multiple tolerances. Reports counts and first few indices for each. - """ - lines: list[str] = [] - # Tolerances to try: current (79), then relaxed - tolerances = [79, 100, 120, 150, 200] - for tol in tolerances: - indices_300: list[int] = [] - indices_500: list[int] = [] - for i, (level, duration) in enumerate(pairs): - if invert: - level = not level - if not level: - continue - if TE_SHORT_12 - tol <= duration <= TE_SHORT_12 + tol: - indices_300.append(i) - if TE_SHORT - tol <= duration <= TE_SHORT + tol: - indices_500.append(i) - lines.append(f" Tolerance ±{tol} µs: {len(indices_300)} pulses near 300 µs, {len(indices_500)} near 500 µs") - if indices_300: - first_few = indices_300[:5] - lines.append(f" First 300±{tol} at indices: {first_few}") - if indices_500: - first_few = indices_500[:5] - lines.append(f" First 500±{tol} at indices: {first_few}") - return lines - - -def histogram_high_pulses(pairs: list[tuple[bool, int]], invert: bool, buckets: list[tuple[int, int]]) -> list[str]: - """Count HIGH pulse durations in buckets (center, half_width) -> (min, max) µs.""" - lines: list[str] = [] - counts: list[tuple[str, int]] = [] - for center, half in buckets: - lo, hi = center - half, center + half - n = sum(1 for level, d in pairs if (level if not invert else not level) and lo <= d <= hi) - counts.append((f"{center}±{half}", n)) - for label, n in counts: - lines.append(f" {label} µs: {n} HIGH pulses") - return lines - - -def main() -> None: - if len(sys.argv) < 2: - print("Usage: python analyze_sub_vag.py ") - sys.exit(1) - path = Path(sys.argv[1]) - if not path.exists(): - print(f"File not found: {path}") - sys.exit(1) - - print("=" * 60) - print("VAG decoder analysis for:", path) - print("=" * 60) - - freq_hz, pairs = parse_sub(path) - print(f"\nParsed: frequency = {freq_hz} Hz ({freq_hz/1e6:.2f} MHz), {len(pairs)} level/duration pairs.") - - ok, msg = frequency_supported(freq_hz) - print(f"\nFrequency check: {msg}") - if not ok: - print(" -> VAG decoder is never tried for this file (frequency filter in KAT).") - - # First few pulses - print("\nFirst 10 pulses (level, duration_us):") - for i, (level, dur) in enumerate(pairs[:10]): - lvl = "HIGH " if level else "LOW " - print(f" [{i}] {lvl} {dur} µs") - - # Why decoder fails from start - report = analyze_reset_and_first_steps(pairs, invert=False) - for line in report: - print(line) - - # Find first VAG-like pulse in stream - print("\n--- Search for VAG preamble in full stream ---") - for line in find_first_vag_like_pulse(pairs, invert=False): - print(line) - print("\nWith INVERTED polarity (KAT tries this if normal fails):") - for line in find_first_vag_like_pulse(pairs, invert=True): - print(line) - - # Scan entire file with multiple tolerances (is there any VAG preamble at all?) - print("\n--- VAG preamble scan: counts at different tolerances ---") - print("Normal polarity:") - for line in scan_vag_preamble_with_tolerances(pairs, invert=False): - print(line) - print("Inverted polarity:") - for line in scan_vag_preamble_with_tolerances(pairs, invert=True): - print(line) - - # Histogram of HIGH pulse durations (VAG uses 300, 500, 600, 1000 µs) - print("\n--- Histogram of HIGH pulse durations (µs) ---") - buckets = [(300, 79), (500, 79), (600, 79), (1000, 79), (300, 150), (500, 150)] - print("Normal polarity:") - for line in histogram_high_pulses(pairs, invert=False, buckets=buckets): - print(line) - print("Inverted polarity:") - for line in histogram_high_pulses(pairs, invert=True, buckets=buckets): - print(line) - - # Summary - print("\n--- Summary (why no decode / no Unknown) ---") - if not ok: - print("1. Frequency is outside VAG supported range -> VAG decoder is skipped.") - else: - first_high = next((i for i, (l, d) in enumerate(pairs) if l), None) - if first_high is None: - print("1. No HIGH pulse in file (invalid or empty?).") - else: - _, first_high_dur = pairs[first_high] - if first_high_dur < TE_SHORT_12 - REF_RESET_DELTA or ( - first_high_dur > TE_SHORT_12 + REF_RESET_DELTA - and not (TE_SHORT - REF_RESET_DELTA <= first_high_dur <= TE_SHORT + REF_RESET_DELTA) - ): - print(f"1. First HIGH pulse is at index {first_high} with duration {first_high_dur} µs.") - print(" VAG Reset requires first HIGH to be 300±79 µs (Type1/2) or 500±79 µs (Type3/4).") - print(" So the decoder never leaves Reset and never produces a decode (or Unknown).") - print("2. KAT only creates a capture (including Unknown) when a decoder consumes the stream") - print(" and returns a result. VAG never returns because it stays in Reset.") - print("3. If the VAG frame appears later in the file, the decoder would need to be run from") - print(" that position (sliding window). Currently KAT feeds the stream from the start only.") - - -if __name__ == "__main__": - main() diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 0a7b7a4..f0a6c1c 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -4,6 +4,13 @@ //! Each decoder processes level+duration pairs from the demodulator and optionally supports //! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64]. //! +//! **Decoder selection (vs ProtoPirate)** +//! ProtoPirate calls `subghz_receiver_decode(receiver, level, duration)` for each pulse; the +//! Flipper SDK receiver (not in REFERENCES) feeds all registered decoders. There is no +//! preamble-based decoder selection in the scene—only the decoder's own feed() logic (e.g. VAG +//! Reset/Preamble1/Preamble2). We do the same: feed every pulse to all decoders that support the +//! file frequency; whoever returns a valid frame is reported. No extra preamble filtering. +//! //! **Manchester decoding**: Ford, Fiat, and common each have separate Manchester state machines //! (FordV0ManchesterState, FiatV0ManchesterState, CommonManchesterState in common.rs). They are //! not reused across protocols. Event conventions match the reference per protocol (e.g. Kia V5 @@ -95,10 +102,10 @@ impl ProtocolRegistry { Box::new(kia_v3_v4::KiaV3V4Decoder::new()), Box::new(kia_v5::KiaV5Decoder::new()), Box::new(kia_v6::KiaV6Decoder::new()), - // Other protocols (Ford before Subaru so 250/500µs Ford keyfobs decode as Ford) + // VAG before Ford/Subaru so 500/1000µs VAG streams decode as VAG (ProtoPirate order has VAG after Ford/Subaru but Flipper likely feeds all decoders; KAT uses first-match so VAG must be tried earlier) + Box::new(vag::VagDecoder::new()), Box::new(ford_v0::FordV0Decoder::new()), Box::new(subaru::SubaruDecoder::new()), - Box::new(vag::VagDecoder::new()), Box::new(fiat_v0::FiatV0Decoder::new()), Box::new(suzuki::SuzukiDecoder::new()), Box::new(scher_khan::ScherKhanDecoder::new()), @@ -160,7 +167,9 @@ impl ProtocolRegistry { let level = if invert_level { !pair.level } else { pair.level }; let duration_us = pair.duration_us; - let mut hit = None; + // Feed this pulse to all decoders that support this frequency (Flipper-style). + // Whoever actually produces a valid frame is reported; decoder order no longer decides. + let mut hits: Vec<(String, DecodedSignal)> = Vec::new(); for decoder in &mut self.decoders { let freq_supported = decoder .supported_frequencies() @@ -177,11 +186,10 @@ impl ProtocolRegistry { .protocol_display_name .as_deref() .unwrap_or_else(|| decoder.name()); - hit = Some((name.to_string(), decoded)); - break; + hits.push((name.to_string(), decoded)); } } - if let Some((name, decoded)) = hit { + if let Some((name, decoded)) = hits.into_iter().next() { let segment: Vec = pairs[segment_start..=i] .iter() .map(|p| LevelDuration::new(p.level, p.duration_us)) @@ -212,6 +220,8 @@ impl ProtocolRegistry { let level = if invert_level { !pair.level } else { pair.level }; let duration_us = pair.duration_us; + // Feed this pulse to all decoders; report first valid frame (Flipper-style). + let mut hits: Vec<(String, DecodedSignal)> = Vec::new(); for decoder in &mut self.decoders { let freq_supported = decoder .supported_frequencies() @@ -227,12 +237,15 @@ impl ProtocolRegistry { if let Some(decoded) = decoder.feed(level, duration_us) { let name = decoded - .protocol_display_name - .as_deref() - .unwrap_or_else(|| decoder.name()); - return Some((name.to_string(), decoded)); + .protocol_display_name + .as_deref() + .unwrap_or_else(|| decoder.name()); + hits.push((name.to_string(), decoded)); } } + if let Some((name, decoded)) = hits.into_iter().next() { + return Some((name.to_string(), decoded)); + } } None diff --git a/src/protocols/vag.rs b/src/protocols/vag.rs index 79da54d..79dce86 100644 --- a/src/protocols/vag.rs +++ b/src/protocols/vag.rs @@ -18,6 +18,7 @@ use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use super::aut64; use super::keys; use crate::radio::demodulator::LevelDuration; +use tracing; // Type 3/4 timing (used as default for ProtocolTiming) const TE_SHORT: u32 = 500; @@ -30,7 +31,8 @@ const MIN_COUNT_BIT: usize = 80; // Type 1/2 timing const TE_SHORT_12: u32 = 300; const TE_LONG_12: u32 = 600; -const TE_DELTA_12: u32 = 80; // Preamble1/Data1 (ref vag.c 79/80) +#[allow(dead_code)] +const TE_DELTA_12: u32 = 80; // Preamble1/Data1 (ref vag.c 79/80); preamble now uses REF_PREAMBLE1_TOL // Reference-aligned deltas (vag.c VAG_NEAR / VAG_TOL_300 79, VAG_TOL_500 120) const REF_RESET_DELTA: u32 = 79; // Reset: 300±79, 500±79 for Preamble2 @@ -38,6 +40,8 @@ const REF_PREAMBLE_SYNC: u32 = 80; // Preamble2 counting: 500±80 const REF_SYNC2_AB_DELTA: u32 = 79; // Sync2A/Sync2B: 500/1000/750±79 (ref VAG_NEAR(..., 79)) const REF_SYNC2C_DELTA: u32 = 79; // Sync2C: 750±79 const REF_GAP1_DELTA: u32 = 79; // Preamble1→Data1 gap 600µs ±79 (ref check_gap1) +// Real-world Type 1/2: preamble often ~280–380µs; ref uses 79/80 +const REF_PREAMBLE1_TOL: u32 = 100; // 300±100 for Type 1/2 preamble lock/count // TEA constants const TEA_DELTA: u32 = 0x9E3779B9; @@ -842,9 +846,9 @@ impl ProtocolDecoder for VagDecoder { if !level { return None; } - // Matches vag.c: duration < 300 and (300-duration)<=79 -> Preamble1; else (duration-300)<=79 -> Preamble1; else (duration-300)>79 and 500±79 -> Preamble2 + // Matches vag.c: duration < 300 and (300-duration)<=tol -> Preamble1; else (duration-300)<=tol -> Preamble1; else 500±79 -> Preamble2. Use REF_PREAMBLE1_TOL for Type 1/2 lock. if duration < TE_SHORT_12 { - if (TE_SHORT_12 - duration) > REF_RESET_DELTA { + if (TE_SHORT_12 - duration) > REF_PREAMBLE1_TOL { return None; } // init_pattern1 @@ -857,8 +861,8 @@ impl ProtocolDecoder for VagDecoder { self.vag_type = VagType::Unknown; self.te_last = duration; self.manchester_advance(ManchesterEvent::Reset); - } else if duration.wrapping_sub(TE_SHORT_12) <= REF_RESET_DELTA { - // Fall-through to init_pattern1 in ref (duration 300..380) + } else if duration.wrapping_sub(TE_SHORT_12) <= REF_PREAMBLE1_TOL { + // Fall-through to init_pattern1 in ref (duration 300..300+tol) self.step = DecoderStep::Preamble1; self.data_low = 0; self.data_high = 0; @@ -900,14 +904,14 @@ impl ProtocolDecoder for VagDecoder { TE_SHORT_12 - duration }; - // Reference: (300-duration)<=79 or (duration-300)<80 -> count pair (check_preamble1_prev) - if te_diff < TE_DELTA_12 { + // Reference: (300-duration) or (duration-300) within tol -> count pair. Use REF_PREAMBLE1_TOL for real-world jitter. + if te_diff <= REF_PREAMBLE1_TOL { let prev_diff = if self.te_last > TE_SHORT_12 { self.te_last - TE_SHORT_12 } else { TE_SHORT_12 - self.te_last }; - if prev_diff <= REF_RESET_DELTA { + if prev_diff <= REF_PREAMBLE1_TOL { self.te_last = duration; self.header_count += 1; return None; @@ -930,7 +934,7 @@ impl ProtocolDecoder for VagDecoder { } else { TE_SHORT_12 - self.te_last }; - if prev_diff <= REF_RESET_DELTA { + if prev_diff <= REF_PREAMBLE1_TOL { self.step = DecoderStep::Data1; return None; } @@ -1005,6 +1009,10 @@ impl ProtocolDecoder for VagDecoder { self.data_count_bit = 80; self.parse_data(); + tracing::debug!( + "VAG Data1 decode: 80 bits, decrypted={} (report regardless of key)", + self.decrypted + ); let result = self.build_decoded_signal(); self.data_low = 0;