Review of #141:
- The quiet window is a voice-announcements option, tg_quiet_s
(config.yaml, default 3 s; 0 turns it off), not the MASTER's
GROUP_HANGTIME, which is 15 s in the example yaml and would have kept
announcements retrying through normal pauses until they were dropped.
- Routing keeps a per-TG record of when an over last started or ended
(_tg_last_heard, written on voice headers and terminators only, from
any ingress but the server's own voice, pruned past 2048 TGs / 600 s).
On the shared SYSTEM the slot's flat RX_TGID / RX_TIME only remember
the latest stream, so a hotspot keying TG 91 erased the pause of a QSO
on TG 213 on the same slot; the record does not.
- RoutingUseCases.tg_quiet_for(tg): 0.0 while a stream on the TG is live
anywhere (tg_has_active_conversation), else seconds since the record,
inf if never heard. Offered to plugins as ServerContext.tg_quiet_for
(PluginDmrdSender: granted talkgroups only), next to voice_slot_for_tg,
which is back to develop's.
- The announcer waits before choosing a slot while quiet < tg_quiet_s:
every 5 s while voice is live, else right when it has been quiet long
enough. tg_voice_heard_within and the own-stream list are gone.
Tests: routing (never heard, from the end of a mesh over, live, another
MASTER, TG 91 on the same slot keeps the 213 trace, own voice not
counted), sender (granted TGs only), announcer (waits through a QSO,
a short pause keeps it waiting, 0 turns it off, no tg_quiet_for = as
before). 10 fail on develop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnjAx7G27d9q88j9PFLj11
voice_slot_for_tg() only looked at the announcement MASTER's own slot.
A QSO on the same talkgroup coming in over the mesh (or from another
MASTER) rides the slot's TX leg, which no longer counts as busy since
#121, and between two overs there is no live stream at all. So a
scheduled announcement could start in the pause between overs: the
hotspots that then picked it up dropped the next over (same TG, other
stream) for the length of the announcement.
tg_voice_heard_within() (routing/helpers.py) tells whether a talkgroup
carried group voice in the last N seconds on any MASTER/PEER slot (RX or
TX leg) or OpenBridge stream, ignoring given stream IDs.
voice_slot_for_tg() returns None while the TG was heard within the
announcement MASTER's GROUP_HANGTIME (TG_QUIET_DEFAULT_S = 5 s when
unset), so the announcer retries every 5 s as for a busy slot. The
plugin's own streams of the last minute are ignored, so its previous
announcement does not hold the next one.
Tests: a mesh QSO's pause holds it and the window then frees it; the
window follows GROUP_HANGTIME; an over from a hotspot on another MASTER
holds it; another TG does not; the plugin's own last announcement does
not. The first three fail on develop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnjAx7G27d9q88j9PFLj11
They are now the voice-announcements plugin (previous commit). Removed
from the core:
- VoiceUseCases: scheduled_announcement, scheduled_tts_announcement, the
broadcast queue, target and slot selection, slot marking, legacy
monitor reports and apply_voice_config (~900 lines). What stays:
voice ident, on-demand 999x files and the disconnected prompt, which
answer a radio rather than follow a schedule.
- inject_announcement_ptt and its bootstrap wiring; plugin frames enter
through inject_plugin_dmrd.
- The TTS engine moves into the plugin (plugin/infrastructure), with its
tests; VoiceProvider.ensure_tts_ambe is gone; tts_ambe.py was a dead
stub.
Nothing to change for sysops:
- the plugin is versioned and enabled by default, and still reads
VOICE.ANNOUNCEMENTS / TTS_ANNOUNCEMENTS from adn-voice.yaml;
- without a PLUGINS.send entry the server grants voice-announcements
exactly the TGs and DMR IDs those items use (disabled ones included,
so enabling one needs no restart); an explicit entry wins;
- the manager now always hands voice_slot_for_tg with send_dmrd; both
check the talkgroup grant on every call.
Tests: the announcement tests of the core are replaced by the plugin's
(schedule, per-TG queue, busy slots, QSO cut, hourly, TTS and its
failures, default DMR ID, derived grant) and by routing tests of plugin
voice ported from test_announcement_ptt_inject (OBP fan-out, no
misattribution to a real peer, dynamic/bridged slot choice, no
pre-armed bridge needed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scheduled announcements and TTS announcements of VoiceUseCases, as
an official plugin that speaks through send_dmrd. Same configuration
(VOICE.ANNOUNCEMENTS / TTS_ANNOUNCEMENTS in adn-voice.yaml, followed
every 15 s) and the same behaviour: interval or hourly, one playback
per TG at a time 1.5 s apart, retries while the TG or every slot is
busy, 58 ms per frame, stop on a refused frame, TTS encoded in a
thread. Versioned next to plugins/example.
Core side, found by an end-to-end run of the plugin:
- PluginIngress ends a plugin voice stream that has been silent for a
second without a terminator (frees the slot, monitor END);
- the default max_frames_per_s adds 18 frames/s per group voice TG, so
one stream per TG fits (a voice stream is ~17 frames/s).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>