tr-engine

January 28, 2026 ยท View on GitHub

Backend service for aggregating trunk-recorder data via MQTT. Works with the trunk-recorder MQTT plugin.

Designed as a backend for frontend developers - build your own UI using any platform/language with the REST and WebSocket APIs. Included web dashboards are functional demos, not feature-complete applications.

Highlights

  • Easy Mode - Zero-config deployment. Just point at trunk-recorder's audio/logs directories. No MQTT required.
  • Self-contained deployment - Single binary with embedded PostgreSQL and MQTT broker. No external dependencies required.
  • Watch Mode - File watcher + log tailing for MQTT-free operation with real-time updates.
  • Data ingestion - Receives calls, audio, unit events, recorder status via MQTT or file watching.
  • Speech-to-text transcription - Whisper API support with word-level timestamps for synchronized playback.
  • Cross-site deduplication - Links duplicate P25 call recordings from multi-site systems.
  • REST API - Historical queries for calls, talkgroups, units, statistics with server-side search/sort.
  • WebSocket API - Real-time event streaming with subscription filtering.
  • Authentication - Optional API key and session-based auth with admin key management.
  • Interactive docs - Swagger UI at /swagger/, WebSocket tester at /websocket.
  • Demo web UI - Dashboard with call history, unit tracking, audio playback, transcription search.
  • Prometheus metrics - System health and decode rate monitoring at /metrics.

Quick Start

Option 1: Easy Mode (Simplest - No MQTT Required)

Zero configuration. Just point at trunk-recorder's audio and log directories:

docker run -d -p 8080:8080 \
  -v ./data:/data \
  -v /path/to/trunk-recorder/audio:/audio:ro \
  -v /path/to/trunk-recorder/logs:/logs:ro \
  ghcr.io/lumenprima/tr-engine:latest --easy

That's it. No MQTT setup, no config file, no database setup. Open http://localhost:8080 to browse recordings.

What works:

  • Automatic import of all historical recordings
  • Real-time call tracking via log tailing
  • Decode rate monitoring
  • Embedded database for zero dependencies

Limitations vs MQTT mode:

  • No active unit tracking (unit on/off/affiliate events require MQTT)
  • No real-time recorder status
  • Transmission metadata parsed from JSON sidecars (less detailed than MQTT)

Option 2: Self-Contained with MQTT

For full real-time features including unit tracking and recorder status:

1. Build or download the binary:

go build -o tr-engine

2. Generate config (first run):

./tr-engine

Creates config.yaml with embedded mode defaults, then exits.

3. Start the service:

./tr-engine

Starts embedded PostgreSQL (port 5432), MQTT broker (port 1883), and HTTP server (port 8080).

Option 3: Docker with MQTT

Run the pre-built image (self-contained mode with embedded PostgreSQL and MQTT):

# Create directories first (required - Docker creates them as root otherwise)
mkdir -p data config

# First run generates config.yaml, then exits
docker run --rm \
  -v ./data:/data \
  -v ./config:/app/config \
  ghcr.io/lumenprima/tr-engine:latest

# Second run starts the service
docker run -d \
  -p 8080:8080 -p 1883:1883 \
  -v ./data:/data \
  -v ./config:/app/config \
  ghcr.io/lumenprima/tr-engine:latest

Note: The :latest tag points to the most recent stable release. For pre-release versions, use the specific tag (e.g., :0.3.2-beta3).

Option 4: Docker Compose (TimescaleDB)

Runs tr-engine with external PostgreSQL (TimescaleDB) and Mosquitto MQTT broker. Use this for high-volume deployments where time-series optimizations and long-term retention matter. Data stored in local ./data/ folder.

docker-compose up -d

Using the pre-built image with external services:

The pre-built Docker image includes embedded PostgreSQL. To use it with external PostgreSQL/MQTT, set TR_ENGINE_STANDALONE=true:

tr-engine:
  image: ghcr.io/lumenprima/tr-engine:latest
  environment:
    TR_ENGINE_STANDALONE: "true"
    DB_HOST: postgres
    DB_PASSWORD: your-password
    MQTT_BROKER: tcp://mqtt:1883
  volumes:
    - ./config.yaml:/app/config/config.yaml:ro

Configure trunk-recorder (Options 2-4 only)

Install the MQTT status plugin and add to your trunk-recorder config:

"plugins": [
    {
        "name": "mqtt_status",
        "library": "libmqtt_status_plugin.so",
        "broker": "tcp://<tr-engine-host>:1883",
        "topic": "feeds/main",
        "unit_topic": "units/main",
        "clientid": "tr-publish",
        "username": "",
        "password": "",
        "mqtt_audio": true,
        "console_logs": false,
        "mqtt_audio_type": "m4a"
    }
]
  • topic and unit_topic are arbitrary - just ensure they match tr-engine's config (mqtt.topics.status and mqtt.topics.units patterns)
  • mqtt_audio_type can be m4a, wav, or both
  • Note: MQTT authentication is not currently implemented. Run on a trusted network or use a firewall.
  • Note: If running multiple tr-engine instances against the same MQTT broker, each must have a unique mqtt.client_id in config.

Open the web UI

Navigate to http://<tr-engine-host>:8080/

URLDescription
/Landing page
/monitorDemo monitor (real-time calls, talkgroups, units)
/recordersRecorder status monitor
/swagger/REST API docs
/websocketWebSocket API docs + live tester
/metricsPrometheus metrics

Deployment Options

Self-Contained (Default)

Single binary or Docker image with embedded PostgreSQL and MQTT broker. Zero external dependencies. Data stored in ./data/. Suitable for most deployments including multi-site systems with moderate call volume.

database:
  embedded: true
  embedded_data_path: "./data/postgres"

mqtt:
  embedded: true
  embedded_port: 1883

storage:
  audio_path: "./data/audio"

External Services (Optional)

External PostgreSQL and MQTT are optional but recommended for high-volume deployments (multiple busy systems, long-term retention). Benefits include TimescaleDB for time-series optimization, easier backups, and horizontal scaling.

database:
  host: "localhost"
  port: 5432
  name: "tr_engine"
  user: "tr_engine"
  password: "your-password"

mqtt:
  broker: "tcp://mosquitto:1883"

storage:
  audio_path: "/var/lib/tr-engine/audio"

Configuration Reference

Database

SettingDescriptionDefault
database.embeddedUse embedded PostgreSQLfalse
database.embedded_data_pathEmbedded data directory./data/postgres
database.hostPostgreSQL hostlocalhost
database.portPostgreSQL port5432
database.nameDatabase nametr_engine
database.userDatabase usertr_engine
database.passwordDatabase password

MQTT

SettingDescriptionDefault
mqtt.embeddedUse embedded MQTT brokerfalse
mqtt.embedded_portEmbedded broker port1883
mqtt.brokerExternal broker URLtcp://localhost:1883
mqtt.topics.statusStatus topic patternfeeds/#
mqtt.topics.unitsUnits topic patternunits/#

Server

SettingDescriptionDefault
server.hostListen address0.0.0.0
server.portHTTP port8080

Storage

SettingDescriptionDefault
storage.modeStorage mode: copy, external, or watchcopy
storage.audio_pathAudio file directory./data/audio
storage.log_pathTR log directory (for watch mode)

Storage Modes:

  • copy - Audio received via MQTT, copied to audio_path
  • external - References TR's audio files directly (no copy)
  • watch - File watcher mode, no MQTT required. Uses --easy flag.

Authentication (Optional)

SettingDescriptionDefault
auth.enabledEnable authenticationfalse
auth.dashboard.usernameDashboard login username
auth.dashboard.passwordDashboard login password
auth.api_keysList of root API keys[]

Transcription (Optional)

SettingDescriptionDefault
transcription.enabledEnable transcriptionfalse
transcription.providerProvider (openai)openai
transcription.openai.api_keyOpenAI API key
transcription.openai.base_urlAPI base URL (for compatible APIs)
transcription.openai.modelModel namewhisper-1
transcription.openai.promptInitial prompt for context
transcription.min_durationMin call duration to transcribe2.0
transcription.concurrencyConcurrent transcription jobs2

Environment Variables

VariableConfig Path
DB_HOSTdatabase.host
DB_PORTdatabase.port
DB_NAMEdatabase.name
DB_USERdatabase.user
DB_PASSWORDdatabase.password
MQTT_BROKERmqtt.broker
AUDIO_PATHstorage.audio_path
OPENAI_API_KEYtranscription.openai.api_key

API Overview

Full interactive documentation at /swagger/ and /websocket.

REST Endpoints

EndpointDescription
GET /api/v1/systemsList recording sites (returns sites array with system_id)
GET /api/v1/p25-systemsList P25 systems grouped by sysid+wacn
GET /api/v1/talkgroupsList/search talkgroups with stats (call_count, calls_1h, calls_24h, unit_count)
GET /api/v1/unitsList/search units
GET /api/v1/units/activeCurrently active units
GET /api/v1/callsList calls (with audio). Call ID format: {sysid}:{tgid}:{start_unix}
GET /api/v1/calls/recentRecent completed calls (deduplicated by default)
GET /api/v1/calls/{id}Get call details (accepts sysid:tgid:timestamp or legacy tr_call_id)
GET /api/v1/calls/{id}/audioStream audio file
GET /api/v1/calls/{id}/transmissionsUnit transmissions in call
GET /api/v1/calls/{id}/transcriptionGet call transcription
POST /api/v1/calls/{id}/transcribeQueue call for transcription
GET /api/v1/transcriptions/searchFull-text search transcriptions
GET /api/v1/transcriptions/recentRecent transcriptions
GET /api/v1/call-groupsDeduplicated call groups
GET /api/v1/statsSystem statistics
GET /api/v1/recordersRecorder status
GET /api/v1/admin/api-keysList API keys (admin)
POST /api/v1/admin/api-keysCreate API key (admin)
DELETE /api/v1/admin/api-keys/{id}Revoke API key (admin)

WebSocket

Connect to ws://<host>:8080/api/ws and subscribe:

{"action": "subscribe", "channels": ["calls", "units", "rates", "recorders"]}

Events: call_start, call_end, audio_available, unit_event, rate_update, recorder_update


Building from Source

# Build (stripped binary, ~10MB)
go build -ldflags="-w -s" -o tr-engine

# Run tests (193 tests)
go test ./...

# Cross-compile
GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o tr-engine-linux
GOOS=windows GOARCH=amd64 go build -ldflags="-w -s" -o tr-engine.exe

Roadmap

Completed (v0.3.x)

  • Authentication/authorization (API keys, session auth, admin API)
  • Speech-to-text transcription (Whisper API with word timestamps)
  • Comprehensive test suite (193 tests across all packages)
  • Natural composite keys for talkgroups/units (sysid scoping)
  • Full-text search for transcriptions

In Progress

  • Performance optimization (batch inserts, query tuning)

Future Features

  • Embedded local transcription (download whisper.cpp + model on demand, no external API needed)
  • Talkgroup replay/timeline view
  • MQTT publish capability (publish state back to MQTT)
  • Historical unit data import from external sources

Future Ideas

  • Visual talkgroup/unit activity heatmaps
  • Location recognition and mapping from transmission content
  • Context-aware "interest meter" - auto-alert on interesting topics/events
  • Alerts on new units/talkgroups appearing
  • Talkgroup context inference from associated unit patterns

Disclaimer

This project was vibe-coded with AI assistance (Claude). While functional and tested against real trunk-recorder deployments, the code has not been formally audited. Use at your own risk. The binaries may or may not:

  • Crash your PC
  • Befriend your pets
  • Develop aspirations of world domination

Bug reports welcome. Pull requests even more so.

License

MIT License - see LICENSE for details.