# istrain — a passive RF train detector you can build **Is a train blocking the crossing?** istrain answers that by listening — passively, on public railroad radio frequencies — to the transmitters every train carries: the crew/dispatch voice and trackside **defect detectors** on 160–162 MHz, and the **End-of-Train** and **Head-of-Train** telemetry on 457.9375 / 452.9375 MHz. It decodes the End-of-Train brake-pressure and motion data, transcribes the voice, and fuses it all into one live verdict. **Live instance:** [istrain.jhestyr.net](https://istrain.jhestyr.net) It's built on a couple of ~US$30 RTL-SDR dongles, some wire, and an always-on Linux box running Docker. No transmitting, no license needed to receive in the US, no railroad cooperation — the trains announce themselves; you just get set up to hear them. It works at **any North American crossing** because the EOT/HOT frequencies are continent-wide and the voice channel plans are public — you change one config file for your location. This repository is the **complete, working system** — the DSP, the decoders, the web dashboard, and the container definitions — shared so anyone (or any coding agent) can build one from scratch and learn from it. --- ## → Start here: [`APOCALYPSE-EDITION.md`](APOCALYPSE-EDITION.md) The full from-scratch build guide: hardware shopping list, operating-system setup, freeing the dongles from the TV driver, researching *your* crossing's channels, bringing up the container stack step by step, transcription, and the hard-won tuning lessons. It's written to be read straight through by a human **or** handed to an LLM coding agent pointed at this repo. ## What's here | Path | What it is | |---|---| | [`APOCALYPSE-EDITION.md`](APOCALYPSE-EDITION.md) | the complete build guide (read this first) | | `scripts/` | the signal engine + decoders — stdlib Python + NumPy only | | `scripts/iq_hop.py`, `iq_channelize.py` | the retune-in-place 452⇄457 hop and the sub-channel splitter (the mission radio) | | `scripts/eot/` | the two-pass FFSK End-of-Train decoder (drift-tolerant; validates via vendored [PyEOT](https://github.com/ereuter/PyEOT)) | | `scripts/bot-recover.py` | Head-of-Train frame recovery + head/tail join | | `scripts/transcribe-worker.py` | voice clip → comms filter → Whisper → transcript | | `dashboard/` | `serve.py` (the API + static server, stdlib, no framework) + the web UI | | `docker/` | Compose template + Dockerfiles (web / airband / scanhop / worker) | | `config/` | `istrain.conf.example` (your channels go here) + the DVB-blacklist file | ## The idea in three transmitters 1. **Voice (160–162 MHz):** dispatchers, crews, and defect detectors that read out milepost, axle count, and speed in plain English. The most direct "a train just passed here" signal. 2. **End-of-Train (457.9375 MHz):** the last car's telemetry box — unit ID, **brake-pipe pressure**, **motion flag** — a 1200-baud FFSK burst every few seconds. The brake-pressure curve tells you passing vs. dwelling vs. cut-and-standing vs. departing. 3. **Head-of-Train (452.9375 MHz):** the locomotive's half; decode it and join head to tail for a confirmed complete train. Any one can be too weak to read, but a real train lights several bands at once — so istrain correlates across all of them. ## License & credits **GPLv3** — see [`LICENSE`](LICENSE). The EOT decoder validates frames against [PyEOT](https://github.com/ereuter/PyEOT) by Eric Reuter (GPLv3, vendored under `scripts/eot/`); full attribution and the licensing note for forkers are in [`ATTRIBUTION.md`](ATTRIBUTION.md). The communities and tools that made this possible — RTL-SDR Blog, osmocom rtl-sdr, RTLSDR-Airband, faster-whisper / OpenAI Whisper, FFmpeg, NumPy, Docker + Dockge, and the railfan frequency databases — are credited in the dashboard's **With Thanks** panel. *A community project. If you build one, we'd love to hear what you heard.*