Files
istrain-public/scripts/eot/README.md
T
istrain 559caead36 istrain-public: from-scratch build of a passive RF train detector
Public community release. The complete working system — DSP + decoders (scripts/),
web dashboard + API (dashboard/), container stack (docker/), config templates (config/) —
plus APOCALYPSE-EDITION.md, the full build guide for a human or a coding agent. GPLv3
(PyEOT dependency). Scrubbed of all secrets, internal IPs, hostnames, and location detail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 23:19:16 -04:00

75 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# istrain — EOT decode & reap
Stage A captures EOT-band bursts (457.9375 MHz, dongle 1001) with `vumon --clips`; Stage B decodes them here.
## What's here
- **`eot_scan.py`** — the decoder, TWO passes (2026-07-05): (1) fast/exact — whole file, fixed tone
pairs, exact sync (proven against `PyEOT/demo3eot.wav`, 3 valid packets); (2) adaptive/fuzzy
escalation for off-frequency emitters (OBSERVATIONS **O-11**) — burst located by FM-quieting, tone
pair built on the burst's spectral centroid, sync matched with ≤2 bit errors (BCH still gates).
Packets carry `dq: exact|fuzzy`; the card layer corroborates fuzzy singletons before trusting them.
`scan_file(path)` returns majority-vote-ordered packets (`[]` = not EOT). numpy required.
- **`../reaper.py`** — the scheduled janitor, now **generic/multi-band** (lives in `scripts/`, not here). EOT is wired as a band; BOT/mid are ready slots. See below.
- **`PyEOT/`** — vendored parser + BCH + demo recording by **Eric Reuter**, GPL (see `PyEOT/LICENSE`).
Source: <https://github.com/ereuter/PyEOT>. We use its packet parser and the reference wav.
## Decode a file by hand
```
python3 eot_scan.py PyEOT/demo3eot.wav # self-test → 3 packets (unit/pressure/motion…)
python3 eot_scan.py ~/istrain/eot_clips/eot_*.s16 # our captured bursts
```
## The reaper (scheduled cleanup) — `../reaper.py`
Generic janitor, split out of EOT (2026-06-23) so it can serve BOT/mid later. Per wired band it decodes each
finished clip (EOT → `~/istrain/eot_clips/`):
- **real** → move the recording to `<band>_clips/confirmed/`, enrich its detection in the band's log
- **noise** → delete the clip **and** remove its detection hit from the log (panel only shows real trains)
- **oversized (>32 MB)** → quarantine to `<band>_clips/oversized/`, never load (OOM guard)
Only scans clips older than 60s (never one mid-capture). `--dry-run` previews; `--band NAME` runs one band.
BOT/mid are commented placeholders in `reaper.py` (no decoder yet → nothing to reap). The EOT *decoder*
(`eot_scan.py`) stays separate, so it can be paused/iterated independently of the cleanup.
## Deploy the schedule (systemd --user, every 15 min)
```
cp ../../systemd/istrain-eot-reap.{service,timer} ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now istrain-eot-reap.timer
systemctl --user list-timers istrain-eot-reap.timer # confirm it's scheduled
```
## DPU decode plan (2026-06-24) — the plan-for-a-plan
Mid-train DPU rides UHF 452/457 (NOT ~900 — see `radio/research/railroad-eot-atcs.md` 06-24 correction),
1200-baud FFSK, same modulation family as EOT. Channel = lead loco road-number last digit:
452.925 (3/7) · 452.950 (2/6) · 457.925 (1/5/9) · 457.950 (0/4/8). So most of the chain is **already built**.
**We can't write this yet — we have no DPU sample.** This is the staged plan to execute once a live burst
is in hand (the rooftop antenna is the gate, same as EOT). Stages 12 are shared with EOT; only stage 3 is new:
1. **Isolate the sub-channel (the one genuinely-new RF step).****BUILT 2026-06-24**
`../iq_channelize.py`: reads `rtl_sdr` CU8 IQ, NCO-mixes + 151-tap FIR-LPFs + decimates each sub-channel
(center + ±12.5 kHz), presence = in-channel power over an adaptive floor, emits a discriminated s16 clip
per burst. **Routing + isolation proven on synthetic IQ** (`--selftest`); FM-demod audio can't do this
(±12.5 kHz becomes a DC bias, not a separable tone). Opt-in via `HOP_IQ=1` on scan-hop (FM path stays the
fallback). This same channelizer is what lets detection *label* a burst DPU-vs-HOT — distinct-ID and decode
are one job. **Unvalidated against a real DPU burst** until the antenna's up — same gate as live EOT.
2. **FFSK demod → bits.** REUSE `eot_scan.py`'s tone discriminator + 1200-baud clock recovery unchanged —
DPU is the same Bell-202-style FFSK as EOT.
3. **DPU frame parse (the new code).** DPU packets are a **different frame format** than EOT (control vs
telemetry — why railfans run SoftDPU separate from SoftEOT). Need to source the DPU packet layout
(SoftDPU / open references) and write a parser + its error-check. PyEOT's BCH is EOT-specific; verify
whether DPU uses the same FEC before reusing it.
**Validation:** no DPU reference recording ships with PyEOT (its `demo3eot.wav` is EOT only). First real
captured DPU burst becomes our reference; until then, stage 3 is unverifiable — don't trust it blind.
**For the mission, none of this is required** — detection already counts DPU via the 452/457 legs
(`../correlate.py`). Decode is the "what is the lead unit telling the mid-consist" luxury, not the commute signal.
## Status (2026-06-23)
Decoder proven on the reference. **No real EOT received yet** on the desk 6" whip — a range limit, not a
code one; EOT waits on the rooftop **DPD EOTD vertical** (tuned 452+457, the chosen feed for 1001 — `../../ANTENNA-UPGRADE.md`). Reaper split to the generic
`../reaper.py` (2026-06-23) and is currently **PAUSED** (timer stopped) to preserve a capture flurry for
study — resume with `systemctl --user start istrain-eot-reap.timer`. (Promote-candidate: `eot_scan.py` is
generic enough for the radio engine; living here in istrain until it earns the move.)