#!/usr/bin/env python3 """Rigorous comparison of two SBUS captures (old remote vs our firmware). Usage: .venv/bin/python tools/compare.py captures/a.npz captures/b.npz """ import sys import numpy as np def extract(path): d = np.load(path) v = d["volts"] srate = float(d["srate"]) idle = np.median(v) p10, p90 = np.percentile(v, [10, 90]) active = p10 if abs(p10 - idle) > abs(p90 - idle) else p90 thr = (idle + active) / 2 phys_hi = v > thr # physical high (pulse) # plateau levels: median of samples well inside each state lvl_hi = np.median(v[phys_hi]) lvl_lo = np.median(v[~phys_hi]) # runs sig = phys_hi.astype(np.int8) edges = np.flatnonzero(np.diff(sig)) + 1 starts = np.concatenate(([0], edges)) ends = np.concatenate((edges, [len(sig)])) levels = sig[starts] dur_us = (ends - starts) * 1e6 / srate # burst envelope: first to last physical-high sample hi_idx = np.flatnonzero(phys_hi) envelope_us = (hi_idx[-1] - hi_idx[0] + 1) * 1e6 / srate # inner runs (drop leading/trailing idle) runs = [(int(l), float(du)) for l, du in zip(levels[1:-1], dur_us[1:-1])] # UART logic: logic1 == idle state; idle here is physical low stream = [] for l, du in runs: n = max(1, round(du / 10.0)) stream.extend([1 - l] * n) # physical high -> logic 0 stream.extend([1] * 24) frames = [] i = 0 while i + 12 <= len(stream): if stream[i] == 1: i += 1 continue byte = sum(b << k for k, b in enumerate(stream[i + 1 : i + 9])) frames.append(byte) i += 12 # bit clock estimate: envelope should be 299 bits (last stop bits merge w/ idle) n_units = round(envelope_us / 10.0) bit_us = envelope_us / n_units return { "lvl_hi": lvl_hi, "lvl_lo": lvl_lo, "runs": runs, "frames": frames, "envelope_us": envelope_us, "bit_us": bit_us, "vmin": float(v.min()), "vmax": float(v.max()), } a_path, b_path = sys.argv[1], sys.argv[2] A, B = extract(a_path), extract(b_path) print(f"{'':24s} {'A: ' + a_path:>28s} {'B: ' + b_path:>28s}") print(f"{'bytes decoded':24s} {len(A['frames']):>28d} {len(B['frames']):>28d}") ha = " ".join(f"{x:02X}" for x in A["frames"]) hb = " ".join(f"{x:02X}" for x in B["frames"]) print(f"frames identical: {A['frames'] == B['frames']}") print(" A:", ha) print(" B:", hb) print(f"{'run count':24s} {len(A['runs']):>28d} {len(B['runs']):>28d}") qa = [round(du / 10) for _, du in A["runs"]] qb = [round(du / 10) for _, du in B["runs"]] la = [l for l, _ in A["runs"]] lb = [l for l, _ in B["runs"]] print(f"quantized run pattern identical: {qa == qb and la == lb}") print(f"{'envelope, us':24s} {A['envelope_us']:>28.2f} {B['envelope_us']:>28.2f}") print(f"{'bit time, us':24s} {A['bit_us']:>28.4f} {B['bit_us']:>28.4f}") print(f"{'-> baud':24s} {1e6/A['bit_us']:>28.1f} {1e6/B['bit_us']:>28.1f}") print(f"{'high plateau, V':24s} {A['lvl_hi']:>28.3f} {B['lvl_hi']:>28.3f}") print(f"{'low plateau, V':24s} {A['lvl_lo']:>28.3f} {B['lvl_lo']:>28.3f}") print(f"{'abs min/max, V':24s} {A['vmin']:>14.2f}/{A['vmax']:>12.2f} {B['vmin']:>14.2f}/{B['vmax']:>12.2f}") # worst run deviation from ideal 10us grid da = max(abs(du - 10 * round(du / 10)) for _, du in A["runs"]) db = max(abs(du - 10 * round(du / 10)) for _, du in B["runs"]) print(f"{'worst grid dev, us':24s} {da:>28.2f} {db:>28.2f}")