- op25-container/Dockerfile: clone gr310 then check out 5b58335 (OP25_COMMIT
build arg). That is the gr310 merge tk_drb_conv was verified against for
#29 and the first carrying PR #296's NBFM noise/voice squelch. Changing
the RUN line also busts CI's cached unpinned clone.
- Keys verified from op25_nbfm.py at that commit: nbfm_squelch_mode
(power|noise|voice), nbfm_noise_squelch_db, nbfm_squelch_threshold, read
from the channel dict.
- op25-container: ConfigGenerator.nbfm_squelch_mode (enum, default noise) and
nbfm_noise_squelch_db, written onto the analog channel.
- edge-node: system config squelch_mode / squelch_db / squelch_threshold ->
generate payload (validated, noise when absent); /api/listen and the
dashboard's analog pane expose mode and level.
#29's CI test of tk_drb_conv against the real tk_p25.py is not done here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
op25-container
- generate-config takes p25_mode "conventional" + conv_channels [{frequency,
name, nac}]. It builds fixed fsk4 channels (up to 2 on one non-tunable SDR,
DC spike kept off the channels) with no control channel to hunt. Two
channels send audio to UDP 23456/23457, and audio.py -2 + amplify(2) keeps
levels.
- tk_drb_conv.py (a tk_p25.rx_ctl subclass, copied into OP25 apps) reports
voice / NAC / encryption (LC service options + LDU2 ESS algid/keyid) for
conventional channels and applies the per-channel NAC filter, which stock
tk_p25 does not. Analog uses it too.
- Trunked P25 config is unchanged when p25_mode is absent.
drb-edge-node
- radio_config: system kind from type + config.mode (missing = trunked).
- conventional.py normalizes the console: a distinctive LC TGID, else the
channel's last TGID (30 s), else a synthetic per-channel id (freq/100 Hz).
Encrypted rows are never talkers. With exactly one conventional channel,
orphan audio is attributed to it at close, which fixes analog recording
(#20). The synthetic id and the real TGID on one channel never split.
- Health/dashboard: P25 conventional listen tab (/api/listen p25conv), and
conventional decode + encryption checks. The checkin carries radio_channels.
- Tests: edge 296 pass; op25-container 18 (new CI job). Reviewed by
drb-audio-review (findings fixed; simultaneous two-channel mixdown filed
as #26) and drb-qa-review.
Not verified on SDR hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /api/health: ordered checklist (password, C2 approval, MQTT, SDR, source,
OP25, decoding, audio, Icecast) with what's wrong and what to do; pure
build_checks() so every state (incl. Toughny's unapproved node) is tested
- /api/listen: pick P25 control channels / one analog freq / a cached C2
system, validated in MHz, stored as a local override; OP25 restarted with
the start failure reason surfaced
- /api/config/system no longer writes a raw blob OP25 can't read
- OP25 resumes after reboot on a locally chosen system, not only C2-assigned
- op25-container: analog (NBFM) config generation fixed (AttributeError +
wrong field names), /op25/log tail, start refuses without a config,
status detects a dead multi_rx ("crashed") -- node-26#12
- index.html rebuilt on UI_REDESIGN tokens; scanner drops DMR, shows System key
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a per-node secondary_sdr_mode config field (none|adsb|ais|op25_2),
applied the same way as hardware_preset/ppm_override so it survives system
reassignment. op25-container gets a GET /devices endpoint that counts
connected SDRs via lsusb; the edge-node checkin now reports sdr_count and
secondary_sdr_mode up to the server, the first node-initiated hardware
report (everything else was C2 pushing config down). Tracked as node-26#9.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The source password had a `hackme` fallback in five places -- entrypoint.sh,
docker-compose.yml, setup.sh's prompt default, .env.example, edge-node's
config.py -- plus op25-container's os.getenv default and two README rows. Any
node whose operator pressed Enter through setup.sh is running a credential
that is written down in this repo.
That matters more than the usual default-password case because Icecast binds
all interfaces and the SOURCE password is write access: it does not just let a
LAN neighbour listen, it lets them PUSH audio into the stream the frontend and
mobile clients play as live radio. Injecting fake traffic into a public-safety
feed is the failure worth preventing.
Approach: remove every fallback rather than change them to a better default.
- icecast/entrypoint.sh refuses to start if either password is empty, and says
how to generate one. This is the single hard gate; everything else is
defence in depth behind it.
- docker-compose.yml uses ${VAR:?message} so a missing value stops the stack
at compose time with a readable error instead of becoming an empty string.
- setup.sh GENERATES a random password when the operator presses Enter, via
openssl rand -base64 24 with a /dev/urandom fallback. Pressing Enter now
gives you a random password rather than a known one, which is the actual
behaviour change -- a prompt default nobody types over is not a default, it
is the value.
- .env.example ships the keys empty with the generation command in a comment,
and README.md now marks both as required with no default.
Client suite: 185 passed.
Note this does NOT rotate anything already deployed. node-002's .env still has
whatever it was set up with; that is an operational step, tracked in the issue.
Closeslogan/node-26#3
Both :8001 (FastAPI control API) and :8081 (OP25's HTTP terminal) listened on
0.0.0.0 with no authentication, on a container that is privileged with /dev
mounted and network_mode: host. Nodes get deployed to third-party sites, so
that exposed start/stop/retune to anyone on the host's LAN.
All three containers share the host network namespace, so edge-node still
reaches both over 127.0.0.1 unchanged. OP25_DEBUG_EXPOSE=true restores the
old 0.0.0.0 binding and logs a loud warning; it is off by default.
Confirmed against boatbod/op25 gr310 that the terminal's http:<host>:<port>
string is honoured as a real bind address (http_server.py splits it and hands
the host to create_server), so no flag was invented.
Also reorder models.py so IcecastConfig precedes ConfigGenerator, which
annotates a field with it. That only worked because python:slim-trixie is
currently Python 3.14, where PEP 649 defers annotation evaluation; on 3.13 or
earlier the same file is a hard NameError at import.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>