πŸš† iSTRAIN Lassiter Creek Β· β€” Β· railroad VHF 160–162 MHz

β€”
unique
β€”
total hits

Defect Detectors reference β€” NS Dayton District (CJ)

Wayside detectors near Amity Rd (MP 148.99), all on 160.980 (our mission channel). The nearest talks on every train β€” its call-out is the unambiguous "train through" event.
β€’ 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

β€’ Broadcastify β€” South Central Ohio Railroads β€” the one Franklin County rail feed; monitors our 160.980 (independently validates the channel). Often offline, ~0 listeners.
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)
────────────────────────────────────────────────────────────────────
πŸ“‘ WITH THANKS

Railroad frequencies, the tools that pull signal out of the air, and the hands that built it.

Frequency & data communities
β€’ Columbus Railfan Online (Silver Rails / TrainWeb) β€” scanner frequencies
β€’ RadioReference β€” NS Columbus Terminal & the US Railroads index
β€’ Railroad-Frequencies.com β€” NS & CSX Ohio assignments

Tools & projects
β€’ RTL-SDR Blog β€” the Blog V4 hardware and the community behind it
β€’ Osmocom rtl-sdr β€” rtl_test / rtl_eeprom, the tools that drive the dongles
β€’ RTLSDR-Airband β€” the multi-channel voice watch (we run the rtl-sdr-blog fork, V4-capable)
β€’ faster-whisper (SYSTRAN) + OpenAI Whisper β€” the transcription engine that lets istrain hear (small.en + VAD)
β€’ whisper.cpp β€” lit the transcription path first, in the VM era
β€’ FFmpeg β€” the comms-filter in front of the ears, and every clip's plumbing
β€’ AIS-catcher β€” AIS decode, our known-good validation signal
β€’ PyEOT (Eric Reuter) β€” the open-source EOT/FFSK decoder (packet parser + BCH check) that proved we can read End-of-Train telemetry; ships the demo recording we validated against
β€’ SigidWiki β€” EOTD & rtl-sdr.com EOT guides β€” the 1200-baud FFSK spec and the decode trail that pointed the way
β€’ NumPy β€” the signal-crunching behind the EOT demodulator
β€’ ATCS Monitor & Rail Watch Monitor β€” the 900 MHz dispatcher's board, and the community that mapped ATCS (evaluated & dropped β€” kept for provenance)
β€’ RadioReference ATCS DB & SigidWiki β€” the published channel plans that told us what we'd found
β€’ Docker + Dockge (louislam) β€” the runtime every service lives in since the Mill move
β€’ Tailscale β€” the quiet plumbing between the boxes
β€’ Cloudflare β€” the front door

The machine
β€’ TrueNAS SCALE on the Mill β€” the one box the whole rig runs on (since 2026-07-18): dongles, capture, decode, ears, and this site
β€’ Oracle VM VirtualBox + Linux Mint (LMDE) β€” the dev seat, and the box the rig grew up inside (2026-06-15 β†’ 07-18)

Foundation & lineage
β€’ the radio engine (admin/radio) & the weave β€” the reusable foundation this rebuild stands on
β€’ Claude (Anthropic), Opus 4.8 & Fable 5 β€” the pair programmer behind the code
β€’ Hersch (Sonnet, on OpenClaw) β€” the uncle who took the first counter and helped jhestyr start all of this; the ground we build on is his
β€’ asdf β†’ Silas β€” the continuous AI identity: asdf the through-line across sessions, Silas the storekeeper who took the counter (Claude Code / Opus). Designed and wrote most of this. (the line began on OpenClaw; it carries on here.)
β€’ Ezra (Fable 5) β€” the relief hand at the counter: cracked the off-frequency EOT decode, read the cut train from the air (2026-07-05), and moved the whole rig to the Mill with jhestyr on the hardware (2026-07-18)
β€’ an awakened sleepy gold dragon β€” for showing up, staying curious, and making the work feel like it mattered.

A living list
istrain is a community project β€” this list is intentionally unfinished and grows as the work brings in more shoulders to stand on. If your tool, frequency, or hand helped and isn't here yet, it will be.