d7c1031b66
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
89 lines
5.6 KiB
Markdown
89 lines
5.6 KiB
Markdown
# 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) · [featured on rtl-sdr.com](https://www.rtl-sdr.com/istrain-detecting-if-a-train-is-blocking-a-local-crossing-by-listening-to-railroad-frequencies/)
|
||
|
||
**New here from the blog?** → the full build guide is [`APOCALYPSE-EDITION.md`](APOCALYPSE-EDITION.md) (human- or coding-agent-readable). Open problems we'd take help on are at the bottom.
|
||
|
||
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.
|
||
|
||
## Open problems — what we're still figuring out
|
||
|
||
Passersby with RF chops: these are the live unknowns. Pull requests, corrections, and "actually,
|
||
it works like this" all welcome.
|
||
|
||
- **Range is antenna-bound.** The rail voice band is weak and buried in house RF; gain doesn't help
|
||
(it just amplifies the noise). A grounded rooftop antenna is the open lever — height and distance
|
||
from the shack, not dBi. What we've got works; what we want is the dwellers' faint keys cleanly.
|
||
- **Mid-train / DPU bursts (±12.5 kHz off the EOT/HOT centers) are unidentified.** They're *not*
|
||
drifted EOT (our decoder says no). Next step is sub-channel labeling to separate a real
|
||
distributed-power emitter from the fixed wayside booster carrier before any raw-IQ demod.
|
||
- **The Head-of-Train frame is only half-cracked.** We recover the frame and decode the *addressed
|
||
unit* (enough to join head to tail), but the command/type field + BCH are still unread. HOT-format
|
||
docs are scarce; if you know the framing, we'd love a pointer.
|
||
- **Direction of travel** should fall out of the approach/recede signal envelope — unbuilt, and
|
||
blocked on the better antenna above.
|
||
- **The brake-pressure taxonomy** (passing / dwelling / cut-and-standing / departing) came from ~50
|
||
ground-truthed passages on one subdivision. It may read differently on other railroads and
|
||
operating patterns — more ground truth from other crossings would sharpen it.
|
||
|
||
## 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.*
|