istrain-public: from-scratch build of a passive RF train detector
Public community release. The complete working system — DSP + decoders (scripts/), web dashboard + API (dashboard/), container stack (docker/), config templates (config/) — plus APOCALYPSE-EDITION.md, the full build guide for a human or a coding agent. GPLv3 (PyEOT dependency). Scrubbed of all secrets, internal IPs, hostnames, and location detail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
# hot_explore.py — Head-of-Train (HOT, 452.9375) reverse-engineering explorer. STANDALONE / offline.
|
||||
#
|
||||
# STATUS (2026-06-26): structure cracked, fields NOT decoded. There is no public HOT bit-map (see
|
||||
# ../../EOT-HOT-PROTOCOL.md) — this is pure RE off our own bot_clips/*.s16. What works so far:
|
||||
# - reuse the proven EOT FFSK front-end (eot_scan: 1200-baud, mark 1200 / space 1800 Hz)
|
||||
# - a strong HOT burst shows a ~440-bit alternating PREAMBLE (spec: 456) then a 64-bit REPEAT PERIOD
|
||||
# (spec: a 64-bit block sent 3x). Majority-voting the 3 internal copies -> a CLEAN 64-bit block.
|
||||
# - artifact test: different sessions give DIFFERENT clean blocks (real frames, not a stuck carrier).
|
||||
# NEXT (open): pin the 24-bit frame-sync word, locate the 17-bit addressed-EOT-ID in the 30-bit payload
|
||||
# (validate against a known EOT unit heard concurrently), recover the HOT BCH(33) polynomial.
|
||||
#
|
||||
# Usage: python3 hot_explore.py ~/istrain/bot_clips/bot_*.s16 (defaults to a strong-clip sample)
|
||||
import sys, os, glob, numpy as np
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # eot_scan lives next door
|
||||
import eot_scan as E
|
||||
|
||||
def best_bits(path):
|
||||
"""Demod FFSK to raw bits; pick the tone pair giving the longest alternating preamble (= bit-sync)."""
|
||||
x, fr = E.load(path); W = max(8, int(round(fr / E.BAUD)))
|
||||
best = None
|
||||
for flo, fhi in E.TONE_PAIRS:
|
||||
bs = E.bitstring(E.discriminator(x, fr, flo, fhi, W), fr)
|
||||
run = mx = 0
|
||||
for i in range(1, len(bs)):
|
||||
if bs[i] != bs[i-1]: run += 1; mx = max(mx, run)
|
||||
else: run = 0
|
||||
if best is None or mx > best[0]: best = (mx, bs)
|
||||
return best
|
||||
|
||||
def preamble_end(bs):
|
||||
run = mx = end = 0
|
||||
for i in range(1, len(bs)):
|
||||
if bs[i] != bs[i-1]:
|
||||
run += 1
|
||||
if run > mx: mx = run; end = i + 1
|
||||
else: run = 0
|
||||
return end
|
||||
|
||||
def vote(bits):
|
||||
"""Find (period, offset) maximizing 3-way agreement among 3 consecutive blocks, then majority-vote."""
|
||||
a = np.array([int(c) for c in bits]); best = None
|
||||
for p in range(60, 69):
|
||||
for off in range(0, min(len(a) - 3*p, 30)) if len(a) >= 3*p else []:
|
||||
b1, b2, b3 = a[off:off+p], a[off+p:off+2*p], a[off+2*p:off+3*p]
|
||||
agree = np.sum(b1 == b2) + np.sum(b1 == b3) + np.sum(b2 == b3)
|
||||
if best is None or agree > best[0]: best = (agree, p, off, (b1, b2, b3))
|
||||
if best is None: return None
|
||||
agree, p, off, (b1, b2, b3) = best
|
||||
cons = ((b1.astype(int) + b2 + b3) >= 2).astype(int)
|
||||
errs = [int(np.sum(b != cons)) for b in (b1, b2, b3)]
|
||||
return p, off, ''.join(map(str, cons.tolist())), errs, agree, 3 * p
|
||||
|
||||
def explore(path):
|
||||
mx, bs = best_bits(path)
|
||||
body = bs[preamble_end(bs):]
|
||||
if len(body) < 3 * 60:
|
||||
print(f"{os.path.basename(path):30s} preamble={mx:3d}b (body too short: {len(body)}b — likely not a frame)")
|
||||
return None
|
||||
r = vote(body)
|
||||
if not r: return None
|
||||
p, off, cons, errs, agree, maxa = r
|
||||
print(f"{os.path.basename(path):30s} preamble={mx:3d}b period={p} off={off} "
|
||||
f"3way={agree}/{maxa}={agree/maxa:.0%} vote-err={errs} block={cons}")
|
||||
return cons
|
||||
|
||||
if __name__ == '__main__':
|
||||
paths = sys.argv[1:] or sorted(glob.glob(os.path.expanduser("~/istrain/bot_clips/bot_20260625_225*.s16")))[:4]
|
||||
blocks = [b for b in (explore(p) for p in paths) if b]
|
||||
if len(blocks) > 1:
|
||||
print("\nartifact test — distinct consensus blocks (same everywhere = artifact; varying = real):",
|
||||
len(set(blocks)))
|
||||
Reference in New Issue
Block a user