#!/usr/bin/env python3 """Self-contained decoder for the Uster 868 MHz 2-FSK bursts. python3 decode.py [symbol_rate_bd] Reads interleaved uint8 IQ (I,Q, offset binary, centre 127.5) at 2.048 MS/s, finds bursts, demodulates each as 2-FSK, recovers a clock from the alternating preamble, and prints every bit stream with the position of the 0x2DD4 sync word. Needs: numpy, scipy. No other project state. """ import sys import numpy as np from scipy.signal import firwin, lfilter FS = 2.048e6 SYNC = "0010110111010100" # 0x2D 0xD4, Silicon Labs Si4x3x sync def load(path): raw = np.fromfile(path, dtype=np.uint8).astype(np.float32) iq = (raw - 127.5) / 127.5 return iq[0::2] + 1j * iq[1::2] def events(x, thr_frac=0.25, min_ms=2.0, k=20): ps = np.convolve(np.abs(x) ** 2, np.ones(k) / k, mode="same") base, peak = np.percentile(ps, 10), np.percentile(ps, 99.9) on = ps > base + thr_frac * (peak - base) d = np.diff(on.astype(np.int8)) st, en = np.where(d == 1)[0], np.where(d == -1)[0] if len(en) < len(st): en = np.append(en, len(ps) - 1) return [(s, e) for s, e in zip(st, en) if (e - s) / FS * 1e3 >= min_ms] def fsk_bits(x, s, e, rate, lpf=20e3): """Discriminate, low-pass, slice, sample at `rate`. Returns (bits, carrier_offset_hz).""" seg = x[s + 100:e - 100].astype(np.complex64) if len(seg) < 2000: return None, None ph = np.unwrap(np.angle(seg)) a = np.polyfit(np.arange(len(ph)), ph, 1)[0] f_off = a * FS / (2 * np.pi) y = seg * np.exp(-1j * a * np.arange(len(seg))) inst = np.diff(np.unwrap(np.angle(y))) * FS / (2 * np.pi) f = lfilter(firwin(101, lpf / (FS / 2)), 1, inst) bb = f > 0 edges = np.where(np.diff(bb.astype(np.int8)) != 0)[0] if len(edges) < 10: return None, f_off period = FS / rate idx = np.arange(edges[0] + period / 2, len(bb) - 1, period).astype(int) return "".join(map(str, bb[idx].astype(int))), f_off def main(): if len(sys.argv) < 3: print(__doc__) sys.exit(1) path, fc = sys.argv[1], float(sys.argv[2]) rate = float(sys.argv[3]) if len(sys.argv) > 3 else 22720.0 x = load(path) print(f"{path}: {len(x)} samples = {len(x)/FS:.2f} s at {FS/1e6:.3f} MS/s, centre {fc} MHz") ev = events(x) print(f"bursts found: {len(ev)}\n") hits = 0 for i, (s, e) in enumerate(ev): bits, f_off = fsk_bits(x, s, e, rate) if bits is None: continue k = bits.find(SYNC) if k < 0: print(f"{i:3d} t={s/FS:7.3f}s {fc + f_off/1e6:10.5f} MHz " f"dur={(e-s)/FS*1e3:5.1f} ms no sync ({len(bits)} bits)") continue hits += 1 after = bits[k + len(SYNC):] print(f"{i:3d} t={s/FS:7.3f}s {fc + f_off/1e6:10.5f} MHz dur={(e-s)/FS*1e3:5.1f} ms " f"preamble={k:3d} sync@{k} bits={len(bits)}") print(f" post-sync: {after[:112]}") print(f"\n{hits}/{len(ev)} bursts carried the 0x2DD4 sync at {rate:.0f} Bd") print("Try other rates if the payload lengths disagree, e.g. 22680, 22973, 23120, 23640.") if __name__ == "__main__": main()