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

β€”
unique
β€”
total hits

Dispatch / Yard rail-voice captures β€” the "AIS" for trains

Airband is watching 5 channels on 1002. Captured transmissions (squelch-gated clips β†’ whisper transcripts) will list here newest-first: channel, time, and the milepost / axle-count / speed read-outs from detector call-outs. β˜… 160.980 NS Dayton District is the mission channel.
Live clips appear in the Captures panel (one per transmission, playable); transcription (whisper) still pending.
scan status: service istrain-airband β€” live status & controls in the Supervisor panel below

Defect Detectors reference β€” your subdivision (example)

Wayside detectors near your crossing (example: MP 148.99), all on your mission channel (example 160.980). 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 β–Έ meaning
mission: is a train blocking YOUR crossing? (set your channels in config/istrain.conf)
legend:  β˜… TCXO (precise)   [nnnn] = dongle serial   ─▢ data path
updated 2026-06-28: voice live (1002); 1001 WEIGHTED hop 452/457 (clock-step-proof); EOT decoded LIVE (ID+brake+motion); HOT head-end ADDRESS cracked β†’ head+tail join; MID standing carrier ID'd = 457.925 EOT booster, carded. ATCS dropped.
updated 2026-07-05: EOT decode is TWO-PASS β€” off-frequency emitters (Β±800 Hz, ~100 ms bursts) now decode
  via adaptive tones + fuzzy sync (O-11) β†’ dwelling trains identified w/ brake psi: held ~60 = stopped
  solid Β· bled to 0 + head polling = CUT & standing (crossing possibly open). Booster floor subtracted
  from contacts & correlate (O-9).
updated 2026-07-18: THE WHOLE RIG MOVED TO THE MILL β€” both dongles + capture + decode + transcription
  + this site now run as one Docker (Dockge) stack on a TrueNAS box; the old VM runs zero radio
  services; data is born where it's served. Voice transcription is LIVE (not planned β€” see below).
────────────────────────────────────────────────────────────────────

PRIMARY β–‘β–‘ RAIL VHF VOICE β€” 160–162 MHz Β· AAR Β· NBFM, unencrypted β–‘β–‘
 β”” defect detectors ("MP 123, no defects, 45 axles, 60 mph") + dispatcher
    ─▢ RTLSDR-Airband (multi-ch) ─▢ ffmpeg comms-filter ─▢ faster-whisper (small.en + VAD,
       LIVE on the Mill since 07-10) ─▢ transcripts.jsonl β€” "a train is moving through," in words
    β–Έ the most direct, gettable "is MY crossing blocked" signal
    β–Έ proven 2026-06-16: same band as NOAA 162.550 β€” RX confirmed here
    β–Έ CONFIRMED channel (2026-06-18): 160.980 (ch58) NS Dayton District, MP 148.99;
      detectors CJ 144.7 / 157.8 announce here. Plan + sources β†’ CHANNELS.md
    β–Έ first intelligible speech 2026-07-10, the night the tuned antenna went in:
      detector CJ at MP 146.4, transcribed (O-22)
    stick: [1002 β˜…] Blog V4 β€” Airband demods the 5 pinned channels at once (160.86–161.19 @ 1.024 MS/s)
    antenna: tuned railroad-band vertical (upgraded 07-10 β€” β‰ˆ2.4Γ— the desk whip's real content;
             office placement, rooftop mast = the standing next lever)

SECONDARY β–‘β–‘ EOT-SYSTEM UHF β€” 1001 WEIGHTED HOP (452 ⇄ 457) Β· 1200-baud FFSK β–‘β–‘
 β”” a train is a chain of RF emitters front-to-back; 1001 hops to watch both ends:
    β–Έ 457.9375  EOT/FRED β€” the REAR unit; DECODED live: ID Β· brake Β· MOTION (rolling/stopped) ─▢ eot.jsonl + clips
                decode is TWO-PASS (07-05): exact fast path + adaptive/fuzzy escalation for the
                off-frequency short-burst emitters that used to read "strong but undecodable" (O-11)
    β–Έ 452.9375  BOT / head-end (HOT) β€” the FRONT unit; frame RECOVERED + addressed-unit DECODED ─▢ bot.jsonl
    β–Έ hop: iq_hop.py β€” retune-in-place, BOT 10s / EOT 10s (rebalanced 07-07: the head = the join + the EARLY WARNING; was 7/14 EOT-favored before the HOT decoder existed)
    β–Έ EOT decode LIVE 2026-06-27 β€” real trains identified; brake-release + motion=1 = departed (crossing cleared)
    β–Έ HOT address CRACKED 2026-06-28 from our own air (invert Β· sync 0110101010 Β· bit-49) β†’ head↔tail JOIN:
      a HOT frame addressing a card's own EOT = confirmed complete consist. Command/type + BCH still open.
    stick: [1001] generic β€” drifts, ppm-calibrate for a clean FSK decode; 450 band clean (no filtering)
    antenna: ~6" vertical β€” ΒΌΞ» β‰ˆ 6.4" @ 457, well-matched
    β–Έ MID / DPU β€” Β±12.5k off both centers (452.925/.950, 457.925/.950), watched in-hop β†’ midtrain.jsonl
 found β–‘ a STANDING carrier on 457.925 = an FCC railroad EOT SIGNAL-BOOSTER (fixed wayside repeater, not a
         train); carded as a STANDING SIGNAL. A booster here β‡’ marginal head↔tail RF β€” likely why our range is short.

SYNTHESIS β–‘β–‘ the mission is DETECT a train, not decode one signal β–‘β–‘
 β”” a train lights several bands at once β€” co-firing in the same 15-min slot = a candidate,
    even when each is too weak to decode:  160.980 voice + 452 BOT + 457 EOT (+ Β±12.5k DPU,
    booster-floor subtracted so the fixed carrier can't fake a band β€” O-9)
    β–Έ correlate panel = week heatmap (one row/band, aligned bins) β€” read DOWN a column
    β–Έ BOT/mid/EOT strips below = the RECENT ZOOM (1-min bins, last 6 h) β€” correlate's magnifier
    β–Έ live meter = bouncing dBFS bar for whichever band the hop is on (level.json)

MEANING β–‘β–‘ the AIR CURVE is the state β€” psi tells the story silence can't (O-10 Β· O-11) β–‘β–‘
 β”” from decoded EOT frames, per train:
    psi ~85–90 + moving            = PASSING through
    psi drops to a HELD ~55–65 + motion 0 = DWELLING (brakes set β€” possibly blocking)
    psi bleeds to 0 AND HOLDS + head still polling = CUT & STANDING β€” crews cut the crossing
       (eyes-confirmed 07-05: ~11 h split, crossing open; never read the bleed as a departure)
    psi RECHARGES toward ~88 + motion 0β†’1 = DEPARTED (the roll-out is the event time)

sanity β–‘ NOAA 162.550 (Columbus) β€” the proven known-good RX test on this band
         (the per-radio toggle is parked since the Mill move; control relay TBD)
config ░ radio→task is swappable, not hardcoded — reassign per band
dropped β–‘ ATCS (900 MHz): LOS-limited + FCC-retired Sep 2025 β†’ encrypted PTC;
          our 900 "carriers" were a dongle spur. Why β†’ radio/sdr/atcs/tuning-log.md
────────────────────────────────────────────────────────────────────
why voice first: unencrypted, easy at 160 MHz, and it says a train is
moving through your crossing in plain words β€” the direct commute signal.

System Map the istrain dashboard & the services behind it

iSTRAIN β€” SYSTEM MAP        the dashboard & the services behind it
how the pieces talk Β· keep current as it changes
legend:  ─▢ request / data    β—‚ produced by    β–£ = a container in the Dockge stack `istrain`
updated 2026-07-18 (the Mill cutover): ONE BOX β€” everything below is a container on the Mill
  (a TrueNAS SCALE machine running Dockge); both dongles on its USB; every log AUTHORED where
  it's served (the VM-era push pipeline is retired); istrain.jhestyr.net serves this stack.
────────────────────────────────────────────────────────────────────

BROWSER β–Έ istrain.jhestyr.net   (Cloudflare ─▢ the Mill's β–£ web container)
 β”‚
 └─▢ β–£ web    serve.py β€” stdlib Python, no framework
       β”‚      (keeps a jsonl PARSE CACHE keyed on file mtime/size + a 4 s TTL on the card
       β”‚       compute β€” polls are ~free; the logs only change every ~20-40 s)
       β”œ static  index.html Β· main.js Β· style.css Β· panels/strip.js (TWO grids: aligned week/15-min
       β”‚         for correlate Β· RECENT1M = 6h/1-min zoom for the band strips)
       β”‚         panels/{trains,live,supervisor,correlate,bot,midtrain,eot,checklist}
       β”œ GET  /api/version    β—‚ dashboard/VERSION (stamped by mill/deploy.sh)  ─▢ header build stamp
       β”œ GET  /api/trains     β—‚ eot.jsonl + bot.jsonl (cards: tail+head βœ“join, βš“parked, β—‰standing,
       β”‚                        air-curve states: passing/dwelling/CUT&standing/departed) ─▢ Trains
       β”œ GET  /api/istrain    β—‚ strength-gated isTRAIN? verdict (+ dwelling-join fallback) ─▢ Banner
       β”œ GET  /api/level      β—‚ level.json   (live dBFS)                 ─▢ Live meter (2Γ—/s)
       β”œ GET  /api/heard      β—‚ heard.jsonl  (voice detections)          ─▢ Correlate (voice row)
       β”œ GET  /api/eot?hours=6      β—‚ eot.jsonl      ─▢ EOT zoom strip   (no ?hours = week ─▢ Correlate)
       β”œ GET  /api/bot?hours=6      β—‚ bot.jsonl      ─▢ BOT zoom strip   (ditto)
       β”œ GET  /api/midtrain?hours=6 β—‚ midtrain.jsonl ─▢ Mid-train zoom strip (Β±12.5k DPU + booster)
       β”œ GET  /api/correlate  β—‚ scripts/correlate.py (recent-window why; booster-floor aware) ─▢ Correlate
       β”œ GET  /api/supervisor β—‚ docker state via supervisor-probe (host cron, 1/min) + DATA-FLOW
       β”‚                        liveness: hop = level.json freshness Β· voice = last-capture age
       β”” POST /api/noaa Β· /api/supervisor/{ctl,reset}  (parked since the move β€” buttons decline
                                                        gracefully; a docker control relay is TBD)

DONGLES β–‘ USB by serial, both on the Mill through a hub Β· dvb driver blacklisted on the host
 β”œ 1001 β–Έ UHF hop β€” 452(BOT) ⇄ 457(EOT)    β”” 1002 β˜… β–Έ rail voice 160–162 / NOAA

CAPTURE β–‘ voice + the UHF hop run at once β€” separate dongles, separate containers
 β”œ β–£ airband  rtl_airband v5.2.0 (rtl-sdr-blog fork, V4-capable) -c istrain.conf
 β”‚            ─▢ 5 NFM channels on 1002 ─▢ captures/*.mp3 ─▢ heard.jsonl
 β”” β–£ scanhop  rtl_tcp + iq_hop.py: retune-in-place hop on 1001 (BOT 10s ⇄ EOT 10s, monotonic dwell β€” step-proof)
              β”œ 452 ─▢ bot.jsonl + bot_clips/*.s16  (presence + clip)
              β”œ 457 ─▢ eot.jsonl + eot_clips/*.s16  (presence + clip)
              β”” both ─▢ level.json                  (live meter + the hop's liveness signal)

DECODE β–‘ containers Β· no radio Β· CPU on saved clips Β· NEVER cull
 β”œ β–£ eot-decode   ─▢ eot-decode.py β†’ eot_scan (TWO-PASS: exact + adaptive/fuzzy for off-freq
 β”‚                   emitters, O-11) β†’ unitΒ·brakeΒ·motion (+dq provenance) β†’ enrich eot.jsonl
 β”œ β–£ bot-recover  ─▢ bot-recover.py: BOT clips β†’ recover HOT frame + DECODE addressed unit β†’ enrich bot.jsonl
 β”” β–£ transcribe-worker ─▢ voice clips >5 KB ─▢ ffmpeg comms-filter ─▢ the whisper stack
       (faster-whisper small.en + VAD, a sibling Dockge stack, localhost) ─▢ sidecar .txt per clip;
       real speech only ─▢ transcripts.jsonl (durable)

HOUSEKEEPING β–‘ β–£ trim β€” 24 h voice-audio prune (ingest-gated; heard.jsonl KEPT)
              β–‘ supervisor-probe β€” host cron, 1/min: docker truth into the snapshot
DATA β–‘ born on the Mill's ZFS datasets β€” jsonl logs = keep forever Β· clips accumulate
       (keep-everything standing policy) Β· the VM-era corpus is archived alongside

build vs operate β–‘ the hands build the code; the radios are operated by services β€”
  never fired ad hoc. deploy = mill/deploy.sh: rsync ─▢ build ─▢ up ─▢ smoke, idempotent.
history β–‘ the VM-capture / NAS-web era (2026-06-15 β†’ 07-18) is preserved in MIGRATION.md;
  the VM units stay on disk, disabled β€” the standing rollback.
────────────────────────────────────────────────────────────────────
πŸ“‘ 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
β€’ built on a reusable radio-capture engine and iterated with an AI pair-programmer (Claude, Anthropic)
β€’ a community project β€” the credits above are the shoulders it stands on; if your tool, frequency, or hand helped and isn’t here yet, it will be.

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.