protocol work

This commit is contained in:
KaraZajac
2026-02-18 21:24:34 -05:00
parent 05078ba961
commit 8ef3b98634
5 changed files with 127 additions and 393 deletions
+87 -13
View File
@@ -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, 433920000 is assumed.
Same convention as KATs `.sub` import and ProtoPirates 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 V0style). 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 V0style: `--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 VAGs supported range (433.92 / 434.42 MHz, 2% tolerance as in KAT).
- Simulates the VAG decoders **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.
-104
View File
@@ -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 **KATs 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 VAGs 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.
-257
View File
@@ -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
View File
@@ -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<LevelDuration> = 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()
@@ -230,9 +240,12 @@ impl ProtocolRegistry {
.protocol_display_name
.as_deref()
.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
+17 -9
View File
@@ -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 ~280380µ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;