Defect Detectors reference β NS Dayton District (CJ)
β’ CJ 146.4 β β nearest, ~2.6 mi east (New Rome OH). HBD-DED Β· talks every train (Talk-On-Defect-Only: No) β routine call-out on westbounds Β· record
β’ CJ 144.7 β ~4.3 mi east, high-car (over-height) β defect-only
β’ CJ 157.8 β ~8.8 mi west, warns eastbounds β type TBD
Source: defectdetector.net β static records (location/type/freq/audio), pullable per detector by id. One-time import, not a live poller.
Community Feeds live audio β when up
Their rooftop hears more than our desk whip β "they catch it, we don't" = range, not broken. Someday: we become the local contributor.
Signal Flow how it fits together β the air βΈ the dongles βΈ meaning
iSTRAIN β SIGNAL FLOW the air βΈ the dongles βΈ the answer as of 2026-08-30
mission: is a train blocking the NS Amity Rd crossing (Galloway, OH)?
legend: [serial] = dongle β
= TCXO (precise clock) ββΆ data path
THE ANSWER COMES FROM TELEMETRY. Voice confirms it; voice never decides it.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1 βΈ TELEMETRY β UHF, the train's own devices talking to each other (1200-baud FFSK)
dongle [1001] hops between the two ends of every train, retuning in place:
457.9375 EOT / tail dwell 14 s ββ DECODED live: unit ID Β· brake-pipe psi Β· moving/stopped ββΆ eot.jsonl
452.9375 BOT / head dwell 7 s ββ head-end frame recovered + unit address decoded ββΆ bot.jsonl
Β±12.5 kHz MID / DPU heard inside both dwells: mid-train locomotives ββΆ midtrain.jsonl
a head frame that addresses a card's own EOT = one confirmed complete train (headβtail join)
also on 457.925: a fixed wayside EOT BOOSTER β a repeater, not a train; its floor is subtracted everywhere
antenna: ΒΌ-wave whip at the band (all UHF is presence + decode; no audio)
2 βΈ VOICE β VHF rail radio, 160β162 MHz, unencrypted narrow FM
dongle [1002 β
] demodulates five channels at once (160.86β161.19 MHz); squelch-gated clips
160.980 NS Dayton District road β the dispatcher and the defect detectors
(CJ at MP 144.7 / 157.8 talks to every passing train: "no defects, 45 axles")
160.920 Β· 161.190 Β· 160.860 Β· 161.085 β the neighbouring NS / CSX channels
clips ββΆ captures/*.mp3 ββΆ heard.jsonl (when each clip was heard, on which channel)
clips > 5 KB ββΆ ffmpeg comms filter ββΆ faster-whisper (small.en + VAD) ββΆ transcripts.jsonl (the words)
antenna: tuned railroad-band vertical, indoors (a rooftop mast is the next range lever)
3 βΈ THE BANNER β /api/istrain, the one line most visitors read
decoded EOT frames become one card per train. The AIR CURVE is the state:
psi ~85β90, moving PASSING through
psi held ~55β65, stopped DWELLING β brakes set; may be on the crossing
psi bled to 0 and held, head still polling CUT & STANDING β crews split the train; crossing likely open
psi recharging, motion 0β1 DEPARTED β the roll-out is the event time
a stopped train's PLACE comes from geometry, not loudness: each end's signal CRESTS as it passes
the antenna (β the crossing). Head past its crest + tail still at its crest + brakes held + strong
signal = a train SPANNING the crossing β YES. Both ends at crest, air held β YES. Otherwise MAYBE.
voice/speech only ever corroborate (a burst of clips while a train is carded upgrades the detail).
4 βΈ CORRELATE β for the reader who wants the evidence behind the line
one row per band, 15-minute bins, one week: read DOWN a column. BOT + MID + EOT lit together in one
bin is a train even when nothing decoded; voice and speech lit in the SAME bin confirm it.
Voice alone is noise. The BOT / MID / EOT strips beneath are the same data at 1-minute bins, last 6 h.
not used: ATCS 900 MHz (retired to encrypted PTC in 2025; our "carriers" were a dongle spur) Β· AIS (inland)
the record: every finding is numbered in OBSERVATIONS.md (O-1 β¦ O-35) Β· protocol notes EOT-HOT-PROTOCOL.md Β·
channel plan CHANNELS.md Β· the data DATA.md
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
System Map the istrain dashboard & the services behind it
iSTRAIN β SYSTEM MAP the site and the services behind it as of 2026-08-30
ONE BOX: everything below is a container in the Dockge stack `istrain` on the Mill (TrueNAS SCALE).
Both dongles are on its USB; every log is written where it is served.
legend: β£ container ββΆ data β read from
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
BROWSER βΈ istrain.jhestyr.net ββ Cloudflare tunnel βββΆ β£ web serve.py :3456 (stdlib Python, no framework)
reads the jsonl logs through caches β whole-file (eot / bot / mid) and time-window tails (voice) β
both incremental on append; the card compute is cached 10 s. The banner path never loads the voice log.
GET /api/istrain the verdict + headline ββΆ the banner (and the Vestaboard's crossing corner)
GET /api/trains one card per train: state Β· crossing Β· headβtail join Β· per-band channels
GET /api/eot | /api/bot | /api/midtrain ?hours=6 = the strips Β· no ?hours = the week for correlate
GET /api/heard voice detections (this call also ingests new clips into heard.jsonl)
GET /api/correlate the 15-minute-band read (scripts/correlate.py)
GET /api/level the live dBFS meter, 2Γ/s β level.json (also the hop's heartbeat)
GET /api/transcripts Β· /api/captures Β· /api/groundtruth Β· /api/errors Β· /api/supervisor Β· /api/version
DONGLES [1001] UHF hop 452 β 457 [1002 β
] rail voice 160β162 (USB by serial; dvb driver blacklisted)
CAPTURE β£ scanhop rtl_tcp + iq_hop.py on 1001 β BOT 7 s β EOT 14 s dwells, MID inside both
ββΆ eot.jsonl + eot_clips/ Β· bot.jsonl + bot_clips/ Β· midtrain.jsonl Β· level.json
β£ airband rtl_airband on 1002 β five channels at once ββΆ captures/*.mp3 (squelch-gated)
DECODE β£ eot-decode eot_clips ββΆ unit Β· brake psi Β· motion (two-pass: exact, then adaptive/fuzzy) ββΆ enriches eot.jsonl
β£ bot-recover bot_clips ββΆ head-end frame + unit address ββΆ enriches bot.jsonl
β£ worker captures > 5 KB ββΆ ffmpeg ββΆ β£ istrain-whisper :9000 (faster-whisper, its own stack) ββΆ transcripts.jsonl
no container deletes a clip β the corpus is kept for re-decoding
KEEPING β£ trim hourly: ingests any unlogged clip (via /api/heard), then prunes audio older than 24 h
host cron, 1/min (TrueNAS, outside the stack):
supervisor-probe.py container + data-flow liveness ββΆ /api/supervisor; state edges ββΆ errors.jsonl
web-watchdog.py restarts web when stuck connections pile up on :3456 (the 07-28 outage's tell)
DATA /data on ZFS β jsonl logs kept forever Β· clips accumulate Β· backed up nightly (the Mill's Duty #1)
DEPLOY mill/deploy.sh from the dev VM β rsync ββΆ build ββΆ up ββΆ restart web ββΆ smoke + warm-up
before any verdict change: scripts/verdict-agreement.py (banner and cards must agree on the archive)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ