Scripts and added :load function
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
#!/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) -> list[str]:
|
||||
"""Find first HIGH pulse that looks like VAG preamble (300±79 or 500±79)."""
|
||||
lines: list[str] = []
|
||||
for i, (level, duration) in enumerate(pairs):
|
||||
if invert:
|
||||
level = not level
|
||||
if not level:
|
||||
continue
|
||||
ok_300 = (TE_SHORT_12 - REF_RESET_DELTA <= duration <= TE_SHORT_12 + REF_RESET_DELTA)
|
||||
ok_500 = (TE_SHORT - REF_RESET_DELTA <= duration <= TE_SHORT + REF_RESET_DELTA)
|
||||
if ok_300 or ok_500:
|
||||
kind = "300±79 (Type1/2)" if ok_300 else "500±79 (Type3/4)"
|
||||
lines.append(f"First VAG-like HIGH pulse at index {i}: {duration} µs ({kind})")
|
||||
return lines
|
||||
lines.append("No HIGH pulse in the entire file matches 300±79 or 500±79 µs.")
|
||||
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)
|
||||
|
||||
# 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()
|
||||
Reference in New Issue
Block a user