92 lines
3.4 KiB
Python
92 lines
3.4 KiB
Python
#!/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}")
|