Skip to content

Add P25 Call Alert (TSBK 0x1F) ingest and display#36

Merged
LumenPrima merged 1 commit intotrunk-reporter:masterfrom
hoosierscanner:feature/call-alert
Apr 15, 2026
Merged

Add P25 Call Alert (TSBK 0x1F) ingest and display#36
LumenPrima merged 1 commit intotrunk-reporter:masterfrom
hoosierscanner:feature/call-alert

Conversation

@hoosierscanner
Copy link
Copy Markdown
Contributor

⚠️ HARD DEPENDENCY — DO NOT MERGE UNTIL BOTH UPSTREAM PRs ARE MERGED ⚠️

This PR will not function without the following two PRs being merged first. There is no trunk-recorder code to emit call_alert MQTT messages until they land:

# Repo What it does Status needed
TrunkRecorder/trunk-recorder#1126 trunk-recorder Decodes TSBK opcode 0x1F into a CALL_ALERT message type and dispatches it through the plugin API Must merge first
TrunkRecorder/tr-plugin-mqtt#8 tr-plugin-mqtt Implements unit_call_alert() in mqtt_status plugin — publishes payload to tr/units/{sys_name}/call_alert Must merge second

If either is missing, trunk-recorder silently drops all Call Alert TSBK messages and tr-engine receives nothing on this topic.


What is a P25 Call Alert?

A Call Alert (TSBK opcode 0x1F) is a directed unit-to-unit page — one radio paging another with no voice channel. Common use: CAD dispatcher silently paging an individual officer. No audio. The MQTT message carries source unit and target unit IDs only.

MQTT topic: tr/units/{sys_name}/call_alert

Changes

Ingest (internal/ingest/)

  • router.go — routes tr/units/{sys}/call_alert to the unit_event handler as subtype call_alert
  • messages.go — adds TargetUnit / TargetUnitAlphaTag fields to UnitEventData
  • handler_units.go — upserts source and target units, stores target_unit in metadata_json, publishes SSE as unit_event:call_alert with target_unit and target_unit_alpha_tag
  • router_test.go, handler_units_test.go — full test coverage

API (openapi.yaml)

  • Adds call_alert to the EventType enum
  • Documents target_unit / target_unit_alpha_tag SSE payload fields

Web UI

  • events.html (Live Events) — red bold CALL ALERT badge, Unit A → Unit B detail, dedicated filter checkbox
  • index.html (Event Horizon) — call alert row in rate meter (red gradient), red background streak
  • omnitrunker.html — red CALL ALERT badge, target unit in TG columns, filterable
  • omnitrunker-classic.html — same as omnitrunker
  • irc-radio-live.html — 🚨 CALL ALERT: UnitA → UnitB posted to *status (no talkgroup, so goes to system status channel)

Test plan

🤖 Generated with Claude Code

⚠️  DEPENDS ON TWO UPSTREAM PRs — DO NOT MERGE UNTIL BOTH ARE IN:
    1. TrunkRecorder/trunk-recorder#1126 — decodes TSBK 0x1F into
       CALL_ALERT message type and dispatches via plugin API
    2. TrunkRecorder/tr-plugin-mqtt#8 — implements unit_call_alert()
       in mqtt_status plugin, publishes to tr/units/{sys}/call_alert

Without those two PRs, trunk-recorder will never emit call_alert MQTT
messages and this handler will never fire.

---

Ingest (internal/ingest/):
- router.go: routes tr/units/{sys}/call_alert → unit_event handler
- messages.go: adds TargetUnit/TargetUnitAlphaTag fields to UnitEventData
- handler_units.go: handles call_alert subtype — upserts both source
  and target units, stores target_unit in metadata_json, publishes SSE
  as unit_event:call_alert with target_unit/target_unit_alpha_tag fields
- router_test.go, handler_units_test.go: full test coverage for routing,
  payload parsing, and round-trip

API (openapi.yaml):
- Adds call_alert to EventType enum
- Documents target_unit/target_unit_alpha_tag SSE payload fields

Web UI:
- events.html: red bold CALL ALERT badge, Unit A → Unit B detail,
  dedicated filter checkbox
- index.html (Event Horizon): call alert row in rate meter (red),
  red streak in background canvas
- omnitrunker.html: red CALL ALERT badge, target unit in TG columns,
  Call Alert filter option
- omnitrunker-classic.html: same as omnitrunker
- irc-radio-live.html: 🚨 CALL ALERT: UnitA → UnitB posted to *status
  (no talkgroup affiliation, so it goes to the system status channel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LumenPrima
Copy link
Copy Markdown
Member

Code review

Found 1 issue:

  1. Dedup key does not include TargetUnit, so simultaneous call alerts from the same source unit to different targets within the 10-second window collapse into one event — the second alert is silently dropped from the DB and SSE stream. Since call alerts carry no talkgroup (Tgid is always 0), the key {SystemID, UnitID, EventType=call_alert, Tgid=0} is identical for every alert initiated by the same unit, regardless of who is being paged. TargetUnit should be added to unitDedupKey for this event type.

{
dk := unitDedupKey{
SystemID: identity.SystemID,
UnitID: data.Unit,
EventType: eventType,
Tgid: data.Talkgroup,
}
if _, loaded := p.unitEventDedup.LoadOrStore(dk, time.Now()); loaded {
isDup = true
}
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@LumenPrima LumenPrima merged commit 4e5e945 into trunk-reporter:master Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants