compose: opt-in trunk-recorder service + supervisor and uploadScript (node-26#45)
robotastic/trunk-recorder:5.2.1 pinned by digest, behind the trunk-recorder compose profile, host network, /dev/bus/usb. run.sh restarts TR when the edge-node rewrites config.json and reports state for health; drb-upload.sh hands each finished call (wav, then json) to recordings/tr-inbox. README carries the GPL-3.0 source note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fa3eac3b28
commit
c9f162a508
@@ -108,6 +108,23 @@ UPLOAD_SPOOL_MAX_AGE_HOURS=72
|
||||
UPLOAD_RETRY_MIN_SECONDS=15
|
||||
UPLOAD_RETRY_MAX_SECONDS=600
|
||||
|
||||
# --- Recorder backend (node-26#45) -------------------------------------------
|
||||
# op25 (default): OP25 + PulseAudio recorder, one call at a time.
|
||||
# trunk-recorder: Trunk Recorder records every talkgroup at once, with call
|
||||
# boundaries from the decoder. Set BOTH lines below to switch; OP25 is then
|
||||
# never started (it would hold the SDR), and the live browser stream (Icecast)
|
||||
# goes quiet — Discord live audio keeps working.
|
||||
RECORDER_BACKEND=op25
|
||||
# COMPOSE_PROFILES=trunk-recorder
|
||||
# Trunk Recorder SDR settings (only read with RECORDER_BACKEND=trunk-recorder).
|
||||
# TR_SAMPLE_RATE=2400000
|
||||
# TR_GAIN=34
|
||||
# TR_CENTER_HZ= # override the computed centre frequency
|
||||
# TR_DIGITAL_RECORDERS=4 # simultaneous voice channels on a trunked system
|
||||
# TR_MIN_DURATION=1.0 # seconds; shorter calls are dropped by TR
|
||||
# TR_SQUELCH_DB=-50 # conventional channels; absolute dB, tune per site
|
||||
# TR_LIVE_PORT=9123 # local UDP port for simplestream -> Discord
|
||||
|
||||
# OP25 container (usually no need to change)
|
||||
OP25_API_URL=http://localhost:8001
|
||||
OP25_TERMINAL_URL=http://localhost:8081
|
||||
|
||||
@@ -51,6 +51,35 @@ services:
|
||||
volumes:
|
||||
- /dev:/dev
|
||||
|
||||
# Trunk Recorder (node-26#45) — the alternative recorder. Opt-in per node:
|
||||
# it only starts when the node's .env has COMPOSE_PROFILES=trunk-recorder,
|
||||
# and the edge-node only uses it with RECORDER_BACKEND=trunk-recorder. Set
|
||||
# both or neither. Upstream image, pinned by tag AND digest (the digest is
|
||||
# the multi-arch index for 5.2.1 on Docker Hub, amd64/arm64/arm/v7); bump
|
||||
# both together. GPL-3.0: we ship it unmodified as a separate program —
|
||||
# see trunk-recorder/README.md for the source offer.
|
||||
#
|
||||
# The edge-node writes ./configs/trunk-recorder/config.json; run.sh (our
|
||||
# supervisor, replacing the image's CMD) restarts TR when it changes. It
|
||||
# shares OP25's dongle slot, so op25 is never started on such a node.
|
||||
trunk-recorder:
|
||||
image: robotastic/trunk-recorder:5.2.1@sha256:c3e425858bfca16c609d8612808a83f06b0f376d950d11733d7c25b2a28f8866
|
||||
profiles: ["trunk-recorder"]
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
devices:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
# TR stages each transmission in tempDir (/dev/shm); Docker's 64 MB
|
||||
# default is tight with several recorders running at once.
|
||||
shm_size: 256m
|
||||
entrypoint: ["/bin/sh", "/drb/run.sh"]
|
||||
environment:
|
||||
TZ: ${TZ:-UTC}
|
||||
volumes:
|
||||
- ./configs/trunk-recorder:/app
|
||||
- ./recordings:/recordings
|
||||
- ./trunk-recorder:/drb:ro
|
||||
|
||||
edge-node:
|
||||
image: ${IMAGE_REGISTRY:-git.vpn.cusano.net}/${DOCKER_ORG:-logan}/${DOCKER_REPO:-node-26}/edge-node:${DRB_TAG_EDGE_NODE:-latest}
|
||||
build: ./drb-edge-node
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Trunk Recorder on a DRB node (node-26#45)
|
||||
|
||||
Opt-in alternative to the OP25 recorder. On a node, set both in `.env`, then
|
||||
`docker compose up -d`:
|
||||
|
||||
RECORDER_BACKEND=trunk-recorder
|
||||
COMPOSE_PROFILES=trunk-recorder
|
||||
|
||||
Set both back (`RECORDER_BACKEND=op25`, remove `COMPOSE_PROFILES`) to return
|
||||
to OP25.
|
||||
|
||||
- `run.sh`: supervisor that replaces the image's CMD. It restarts TR when the
|
||||
edge-node rewrites `configs/trunk-recorder/config.json`, and writes
|
||||
`drb-status.json` for the dashboard health check.
|
||||
- `drb-upload.sh`: TR's `uploadScript`. It copies each finished call (WAV +
|
||||
JSON) into `recordings/tr-inbox`, where the edge-node uploads it through the
|
||||
normal spool.
|
||||
|
||||
## License (GPL-3.0)
|
||||
|
||||
Trunk Recorder is GPL-3.0. DRB runs the unmodified upstream image
|
||||
`robotastic/trunk-recorder:5.2.1` (digest pinned in `docker-compose.yml`) as a
|
||||
separate program. It talks to DRB only through files, a script argument list
|
||||
and a local UDP socket. That makes it mere aggregation, the same position we
|
||||
already hold for OP25.
|
||||
|
||||
Anyone we distribute a node image or device to must still be able to get the
|
||||
corresponding source. Upstream source for this exact version is at
|
||||
<https://github.com/TrunkRecorder/trunk-recorder/tree/v5.2.1>. If we ever ship
|
||||
the TR image ourselves (mirrored to our registry, preinstalled on a sold
|
||||
device), we must also offer that source directly, in writing, for three years,
|
||||
or ship it alongside. Pointing to upstream alone is not enough at that point.
|
||||
Any patch to TR itself makes DRB's copy a modified version, which must then be
|
||||
published under GPL-3.0.
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Trunk Recorder uploadScript (node-26#45). TR runs this after every call as
|
||||
# drb-upload.sh <call.wav> <call.json> <call.m4a>
|
||||
# (run_upload_script_argv, call_concluder.cc v5.2.1; the m4a only exists with
|
||||
# compressWav, which DRB leaves off). TR deletes the wav/json as soon as this
|
||||
# exits (audioArchive/callLog false), so it copies them into the edge-node's
|
||||
# inbox first. Order matters: the WAV lands first and the JSON last, so the
|
||||
# JSON's presence tells the edge-node the WAV is complete. Exit non-zero = TR
|
||||
# marks the call failed.
|
||||
set -u
|
||||
WAV="${1:?wav path}"
|
||||
JSON="${2:?json path}"
|
||||
INBOX="${DRB_TR_INBOX:-/recordings/tr-inbox}"
|
||||
TMP="$INBOX/.incoming"
|
||||
|
||||
[ -f "$WAV" ] && [ -f "$JSON" ] || { echo "drb-upload: missing $WAV or $JSON" >&2; exit 1; }
|
||||
mkdir -p "$TMP" || exit 1
|
||||
|
||||
stem="$(basename "$JSON" .json)"
|
||||
# Hard link when on the same filesystem (no copy, TR's unlink leaves ours);
|
||||
# copy otherwise.
|
||||
put() { ln -f "$1" "$2" 2>/dev/null || cp -f "$1" "$2"; }
|
||||
|
||||
put "$WAV" "$TMP/$stem.wav" || exit 1
|
||||
put "$JSON" "$TMP/$stem.json" || exit 1
|
||||
mv -f "$TMP/$stem.wav" "$INBOX/$stem.wav" || exit 1
|
||||
mv -f "$TMP/$stem.json" "$INBOX/$stem.json" || exit 1
|
||||
exit 0
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/bin/sh
|
||||
# Supervisor for Trunk Recorder inside its container (node-26#45).
|
||||
#
|
||||
# The edge-node writes /app/config.json from the C2 system config but has no
|
||||
# Docker access, so it cannot restart this container. This loop does it:
|
||||
# - waits for config.json, then runs trunk-recorder on it
|
||||
# - restarts it when config.json changes or /app/restart-request appears
|
||||
# - keeps it stopped while /app/stop-request exists (dashboard Stop)
|
||||
# - restarts it after a crash, backing off 5 s -> 60 s (`restarts` counts
|
||||
# crashes only, not config restarts)
|
||||
# - writes /app/drb-status.json every few seconds for the edge-node's health
|
||||
# check (state, pid, started_at, restarts, last_exit, epoch)
|
||||
set -u
|
||||
APP="${DRB_TR_APP:-/app}"
|
||||
CFG="$APP/config.json"
|
||||
STATUS="$APP/drb-status.json"
|
||||
RESTART="$APP/restart-request"
|
||||
STOP="$APP/stop-request"
|
||||
REC="${DRB_RECORDINGS:-/recordings}"
|
||||
mkdir -p "$APP/logs" "$REC/tr-capture" "$REC/tr-inbox"
|
||||
|
||||
pid=""
|
||||
started=0
|
||||
restarts=0
|
||||
last_exit=null
|
||||
backoff=5
|
||||
cfgsum=""
|
||||
|
||||
write_status() {
|
||||
printf '{"state":"%s","pid":%s,"started_at":%s,"restarts":%s,"last_exit":%s,"epoch":%s}\n' \
|
||||
"$1" "${pid:-null}" "$started" "$restarts" "$last_exit" "$(date +%s)" > "$STATUS.tmp" \
|
||||
&& mv -f "$STATUS.tmp" "$STATUS"
|
||||
}
|
||||
|
||||
sum() { md5sum "$CFG" 2>/dev/null | cut -d' ' -f1; }
|
||||
|
||||
stop_tr() {
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
kill -TERM "$pid"
|
||||
i=0
|
||||
while kill -0 "$pid" 2>/dev/null && [ "$i" -lt 30 ]; do sleep 0.5; i=$((i + 1)); done
|
||||
kill -KILL "$pid" 2>/dev/null
|
||||
wait "$pid" 2>/dev/null
|
||||
fi
|
||||
pid=""
|
||||
}
|
||||
|
||||
trap 'stop_tr; write_status stopped; exit 0' TERM INT
|
||||
|
||||
nap() { sleep "$1" & wait $!; }
|
||||
|
||||
while :; do
|
||||
if [ -f "$STOP" ]; then
|
||||
[ -n "$pid" ] && { echo "drb: stop requested"; stop_tr; }
|
||||
write_status stopped; nap 2; continue
|
||||
fi
|
||||
if [ ! -f "$CFG" ]; then
|
||||
write_status waiting_for_config; nap 5; continue
|
||||
fi
|
||||
if [ -z "$pid" ]; then
|
||||
rm -f "$RESTART"
|
||||
cfgsum="$(sum)"
|
||||
echo "drb: starting trunk-recorder"
|
||||
trunk-recorder --config="$CFG" &
|
||||
pid=$!
|
||||
started="$(date +%s)"
|
||||
fi
|
||||
write_status running
|
||||
nap 5
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
wait "$pid" 2>/dev/null
|
||||
last_exit=$?
|
||||
pid=""
|
||||
restarts=$((restarts + 1))
|
||||
# A run that lasted a while resets the backoff.
|
||||
[ $(( $(date +%s) - started )) -gt 120 ] && backoff=5
|
||||
echo "drb: trunk-recorder exited ($last_exit); restarting in ${backoff}s"
|
||||
write_status crashed
|
||||
nap "$backoff"
|
||||
backoff=$((backoff * 2)); [ "$backoff" -gt 60 ] && backoff=60
|
||||
continue
|
||||
fi
|
||||
if [ -f "$RESTART" ] || [ "$(sum)" != "$cfgsum" ]; then
|
||||
echo "drb: config changed or restart requested — restarting trunk-recorder"
|
||||
stop_tr
|
||||
fi
|
||||
done
|
||||
Reference in New Issue
Block a user