559caead36
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>
75 lines
5.2 KiB
Markdown
75 lines
5.2 KiB
Markdown
# 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 1–2 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.)
|