protocol work
This commit is contained in:
+87
-13
@@ -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
|
```bash
|
||||||
python scripts/analyze_sub_vag.py path/to/file.sub
|
# PWM decode (short→0, long→1), default 250/500 µs
|
||||||
python scripts/analyze_sub_vag.py "IMPORTS/VOLKSWAGEN AUDI/Test_55_unlock_and_55_lock_suran.sub"
|
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:**
|
### Options summary
|
||||||
- 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.
|
|
||||||
|
|
||||||
**Why a file might not show up at all:**
|
| Option | Default | Description |
|
||||||
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.
|
|--------|---------|-------------|
|
||||||
|
| `--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.
|
||||||
|
|||||||
@@ -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<StoredLevelDuration>)`.
|
|
||||||
- **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.
|
|
||||||
@@ -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 <path/to/file.sub>")
|
|
||||||
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()
|
|
||||||
+20
-7
@@ -4,6 +4,13 @@
|
|||||||
//! Each decoder processes level+duration pairs from the demodulator and optionally supports
|
//! Each decoder processes level+duration pairs from the demodulator and optionally supports
|
||||||
//! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64].
|
//! 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
|
//! **Manchester decoding**: Ford, Fiat, and common each have separate Manchester state machines
|
||||||
//! (FordV0ManchesterState, FiatV0ManchesterState, CommonManchesterState in common.rs). They are
|
//! (FordV0ManchesterState, FiatV0ManchesterState, CommonManchesterState in common.rs). They are
|
||||||
//! not reused across protocols. Event conventions match the reference per protocol (e.g. Kia V5
|
//! 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_v3_v4::KiaV3V4Decoder::new()),
|
||||||
Box::new(kia_v5::KiaV5Decoder::new()),
|
Box::new(kia_v5::KiaV5Decoder::new()),
|
||||||
Box::new(kia_v6::KiaV6Decoder::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(ford_v0::FordV0Decoder::new()),
|
||||||
Box::new(subaru::SubaruDecoder::new()),
|
Box::new(subaru::SubaruDecoder::new()),
|
||||||
Box::new(vag::VagDecoder::new()),
|
|
||||||
Box::new(fiat_v0::FiatV0Decoder::new()),
|
Box::new(fiat_v0::FiatV0Decoder::new()),
|
||||||
Box::new(suzuki::SuzukiDecoder::new()),
|
Box::new(suzuki::SuzukiDecoder::new()),
|
||||||
Box::new(scher_khan::ScherKhanDecoder::new()),
|
Box::new(scher_khan::ScherKhanDecoder::new()),
|
||||||
@@ -160,7 +167,9 @@ impl ProtocolRegistry {
|
|||||||
let level = if invert_level { !pair.level } else { pair.level };
|
let level = if invert_level { !pair.level } else { pair.level };
|
||||||
let duration_us = pair.duration_us;
|
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 {
|
for decoder in &mut self.decoders {
|
||||||
let freq_supported = decoder
|
let freq_supported = decoder
|
||||||
.supported_frequencies()
|
.supported_frequencies()
|
||||||
@@ -177,11 +186,10 @@ impl ProtocolRegistry {
|
|||||||
.protocol_display_name
|
.protocol_display_name
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or_else(|| decoder.name());
|
.unwrap_or_else(|| decoder.name());
|
||||||
hit = Some((name.to_string(), decoded));
|
hits.push((name.to_string(), decoded));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((name, decoded)) = hit {
|
if let Some((name, decoded)) = hits.into_iter().next() {
|
||||||
let segment: Vec<LevelDuration> = pairs[segment_start..=i]
|
let segment: Vec<LevelDuration> = pairs[segment_start..=i]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| LevelDuration::new(p.level, p.duration_us))
|
.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 level = if invert_level { !pair.level } else { pair.level };
|
||||||
let duration_us = pair.duration_us;
|
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 {
|
for decoder in &mut self.decoders {
|
||||||
let freq_supported = decoder
|
let freq_supported = decoder
|
||||||
.supported_frequencies()
|
.supported_frequencies()
|
||||||
@@ -230,9 +240,12 @@ impl ProtocolRegistry {
|
|||||||
.protocol_display_name
|
.protocol_display_name
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or_else(|| decoder.name());
|
.unwrap_or_else(|| decoder.name());
|
||||||
return Some((name.to_string(), decoded));
|
hits.push((name.to_string(), decoded));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some((name, decoded)) = hits.into_iter().next() {
|
||||||
|
return Some((name.to_string(), decoded));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
|
|||||||
+17
-9
@@ -18,6 +18,7 @@ use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
|||||||
use super::aut64;
|
use super::aut64;
|
||||||
use super::keys;
|
use super::keys;
|
||||||
use crate::radio::demodulator::LevelDuration;
|
use crate::radio::demodulator::LevelDuration;
|
||||||
|
use tracing;
|
||||||
|
|
||||||
// Type 3/4 timing (used as default for ProtocolTiming)
|
// Type 3/4 timing (used as default for ProtocolTiming)
|
||||||
const TE_SHORT: u32 = 500;
|
const TE_SHORT: u32 = 500;
|
||||||
@@ -30,7 +31,8 @@ const MIN_COUNT_BIT: usize = 80;
|
|||||||
// Type 1/2 timing
|
// Type 1/2 timing
|
||||||
const TE_SHORT_12: u32 = 300;
|
const TE_SHORT_12: u32 = 300;
|
||||||
const TE_LONG_12: u32 = 600;
|
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)
|
// 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
|
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_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_SYNC2C_DELTA: u32 = 79; // Sync2C: 750±79
|
||||||
const REF_GAP1_DELTA: u32 = 79; // Preamble1→Data1 gap 600µs ±79 (ref check_gap1)
|
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
|
// TEA constants
|
||||||
const TEA_DELTA: u32 = 0x9E3779B9;
|
const TEA_DELTA: u32 = 0x9E3779B9;
|
||||||
@@ -842,9 +846,9 @@ impl ProtocolDecoder for VagDecoder {
|
|||||||
if !level {
|
if !level {
|
||||||
return None;
|
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 duration < TE_SHORT_12 {
|
||||||
if (TE_SHORT_12 - duration) > REF_RESET_DELTA {
|
if (TE_SHORT_12 - duration) > REF_PREAMBLE1_TOL {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// init_pattern1
|
// init_pattern1
|
||||||
@@ -857,8 +861,8 @@ impl ProtocolDecoder for VagDecoder {
|
|||||||
self.vag_type = VagType::Unknown;
|
self.vag_type = VagType::Unknown;
|
||||||
self.te_last = duration;
|
self.te_last = duration;
|
||||||
self.manchester_advance(ManchesterEvent::Reset);
|
self.manchester_advance(ManchesterEvent::Reset);
|
||||||
} else if duration.wrapping_sub(TE_SHORT_12) <= REF_RESET_DELTA {
|
} else if duration.wrapping_sub(TE_SHORT_12) <= REF_PREAMBLE1_TOL {
|
||||||
// Fall-through to init_pattern1 in ref (duration 300..380)
|
// Fall-through to init_pattern1 in ref (duration 300..300+tol)
|
||||||
self.step = DecoderStep::Preamble1;
|
self.step = DecoderStep::Preamble1;
|
||||||
self.data_low = 0;
|
self.data_low = 0;
|
||||||
self.data_high = 0;
|
self.data_high = 0;
|
||||||
@@ -900,14 +904,14 @@ impl ProtocolDecoder for VagDecoder {
|
|||||||
TE_SHORT_12 - duration
|
TE_SHORT_12 - duration
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reference: (300-duration)<=79 or (duration-300)<80 -> count pair (check_preamble1_prev)
|
// Reference: (300-duration) or (duration-300) within tol -> count pair. Use REF_PREAMBLE1_TOL for real-world jitter.
|
||||||
if te_diff < TE_DELTA_12 {
|
if te_diff <= REF_PREAMBLE1_TOL {
|
||||||
let prev_diff = if self.te_last > TE_SHORT_12 {
|
let prev_diff = if self.te_last > TE_SHORT_12 {
|
||||||
self.te_last - TE_SHORT_12
|
self.te_last - TE_SHORT_12
|
||||||
} else {
|
} else {
|
||||||
TE_SHORT_12 - self.te_last
|
TE_SHORT_12 - self.te_last
|
||||||
};
|
};
|
||||||
if prev_diff <= REF_RESET_DELTA {
|
if prev_diff <= REF_PREAMBLE1_TOL {
|
||||||
self.te_last = duration;
|
self.te_last = duration;
|
||||||
self.header_count += 1;
|
self.header_count += 1;
|
||||||
return None;
|
return None;
|
||||||
@@ -930,7 +934,7 @@ impl ProtocolDecoder for VagDecoder {
|
|||||||
} else {
|
} else {
|
||||||
TE_SHORT_12 - self.te_last
|
TE_SHORT_12 - self.te_last
|
||||||
};
|
};
|
||||||
if prev_diff <= REF_RESET_DELTA {
|
if prev_diff <= REF_PREAMBLE1_TOL {
|
||||||
self.step = DecoderStep::Data1;
|
self.step = DecoderStep::Data1;
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -1005,6 +1009,10 @@ impl ProtocolDecoder for VagDecoder {
|
|||||||
self.data_count_bit = 80;
|
self.data_count_bit = 80;
|
||||||
|
|
||||||
self.parse_data();
|
self.parse_data();
|
||||||
|
tracing::debug!(
|
||||||
|
"VAG Data1 decode: 80 bits, decrypted={} (report regardless of key)",
|
||||||
|
self.decrypted
|
||||||
|
);
|
||||||
|
|
||||||
let result = self.build_decoded_signal();
|
let result = self.build_decoded_signal();
|
||||||
self.data_low = 0;
|
self.data_low = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user