Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
41d935d
initial version
robotastic Jan 7, 2026
ae9fc28
Added support for saving updates to a config.json file
robotastic Mar 7, 2026
4f3ed32
refactor(plugin-manager): decouple uploader config from System
TheGreatCodeholio Apr 6, 2026
9dc1fd4
refactor(call-concluder): process call artifacts in temp before final…
TheGreatCodeholio Apr 6, 2026
68e423f
Merge branch 'TrunkRecorder:master' into plugin-refactor
TheGreatCodeholio Apr 6, 2026
746eda7
Merge remote-tracking branch 'upstream/dev/plugin-config' into plugin…
TheGreatCodeholio Apr 6, 2026
82910e1
feat(plugins): add shared JSON context and whisper_transcribe example…
TheGreatCodeholio Apr 7, 2026
555fa1b
add extra logging to call_concluder
TheGreatCodeholio Apr 7, 2026
662b420
fix plugin override signatures
TheGreatCodeholio Apr 7, 2026
6fa78fa
fix plugin override signatures
TheGreatCodeholio Apr 7, 2026
80a0183
fix(plugins): restore RDIO and OpenMHz call_end uploads after API change
TheGreatCodeholio Apr 7, 2026
f9906a0
refactor(plugins): add blocking/deferred call_end phases with separat…
TheGreatCodeholio Apr 7, 2026
8272abc
fix warning for len for is_invalid pass for ffmpeg
TheGreatCodeholio Apr 7, 2026
2f8bfba
add blocking plugin run before deferred, and rewrite json
TheGreatCodeholio Apr 8, 2026
80580cb
Merge remote-tracking branch 'upstream/master' into plugin-refactor
TheGreatCodeholio Apr 8, 2026
79807b7
remove whisper plugin.
TheGreatCodeholio Apr 9, 2026
6f01241
fix(plugins): deferred plugins skipped after blocking retry, failed p…
TheGreatCodeholio Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ list(APPEND trunk_recorder_sources
trunk-recorder/sources/iq_file_source.cc
trunk-recorder/csv_helper.cc
trunk-recorder/config.cc
trunk-recorder/config_service.cc
trunk-recorder/setup_systems.cc
trunk-recorder/monitor_systems.cc
trunk-recorder/talkgroup.cc
Expand Down
431 changes: 191 additions & 240 deletions docs/CONFIGURE.md

Large diffs are not rendered by default.

300 changes: 105 additions & 195 deletions docs/Plugins.md

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions docs/plugins/broadcastify-calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
sidebar_label: 'Broadcastify Calls'
---

# Broadcastify Calls Plugin

**Plugin name:** `broadcastify_uploader`
**Library:** `libbroadcastify_uploader.so`

The Broadcastify Calls plugin uploads completed calls to a Broadcastify Calls server. It runs on `call_end`, uploads call metadata first, and then uploads the converted audio file if the metadata step succeeds. It skips encrypted calls and supports per-system API credentials plus optional talkgroup allow/deny filters.

## Requirements

Broadcastify Calls requires audio uploads in `.m4a` format with AAC audio.

This plugin uploads the converted call audio file, not the raw WAV file. Because of that, `compressWav` must be enabled on the system so Trunk Recorder creates the converted `.m4a` file before the plugin runs.

## Plugin Object

| Key | Required | Default Value | Type | Description |
| --- | :---: | --- | --- | --- |
| name | ✓ | `broadcastify_uploader` | string | Friendly name for this plugin instance. This is used in logging. If set to exactly `broadcastify_uploader`, log messages display `Broadcastify`. Otherwise the configured name is used. |
| library | ✓ | | string | Must be `libbroadcastify_uploader.so`. |
| enabled | | `true` | **true** / **false** | Whether this plugin instance should be loaded. General plugin loading behavior is handled by the plugin manager. |
| broadcastifyCallsServer | ✓ | | string | Broadcastify Calls server URL. The plugin validates that this is a parseable HTTP or HTTPS URL. Metadata uploads are sent to this URL directly. |
| broadcastifySslVerifyDisable | | `false` | **true** / **false** | When true, disables SSL certificate and host verification for Broadcastify uploads. |
| broadcastifyOTA | | `true` | **true** / **false** | Enables upload of the OTA alias as `srcId_alias` when available from the first entry in `transmission_source_list`. |
| systems | ✓ | | array | Array of system objects. The plugin scans these for systems with a `broadcastifyApiKey` and uses them to match completed calls by `shortName`. If no usable systems are configured, the plugin returns an error during configuration parsing. |

## Broadcastify System Object

Each object in `systems` describes one Trunk Recorder system that should be uploaded to Broadcastify Calls.

| Key | Required | Default Value | Type | Description |
| --- | :---: | --- | --- | --- |
| shortName | ✓ | | string | Must match the `shortName` of a system defined in the main Trunk Recorder config. The plugin uses this to match a completed call to the correct Broadcastify upload settings. |
| broadcastifyApiKey | ✓ | | string | API key used for uploads for this system. Systems without an API key are ignored by the plugin. The key is partially redacted in logs. |
| broadcastifySystemId | ✓ | | number | Broadcastify system ID to send with uploads. If this is `0`, uploads for the system are skipped. |
| broadcastifyAllow | | `[]` | array of string/number | Optional allow-list of talkgroups. If this list is non-empty, the talkgroup must match at least one pattern or the upload is skipped. Patterns use glob-style matching with `*` and `?`. Numeric values are accepted and converted to strings. |
| broadcastifyDeny | | `[]` | array of string/number | Optional deny-list of talkgroups. If a talkgroup matches any deny pattern, the upload is skipped. Patterns use glob-style matching with `*` and `?`. Numeric values are accepted and converted to strings. |


## Talkgroup Filter Rules

Talkgroup filters are evaluated per configured Broadcastify system.

- Talkgroups are compared as strings, for example `50712`.
- `*` matches any number of characters.
- `?` matches a single character.
- If `broadcastifyAllow` is non-empty, the talkgroup must match at least one allow pattern.
- If `broadcastifyDeny` is non-empty, the talkgroup must not match any deny pattern.
- If both are present, allow is checked first, then deny.

## What the Plugin Uploads

The plugin uploads in two steps.

### 1. Metadata Upload

It sends a multipart metadata request containing:

- `metadata` as `call_meta.json` from `call_info.call_json.dump()`
- `callDuration`
- `systemId`
- `apiKey`
- optionally `srcId_alias` when `broadcastifyOTA` is enabled and an OTA alias is present in the first transmission source entry

### 2. Audio Upload

If the metadata upload succeeds and the server returns a success response with an audio upload URL, the plugin then uploads the converted m4a as audio with content type `audio/aac`.

The plugin always uploads m4a files as required by Broadcastify Calls, it requires `compressWav` enabled.

## Behavior Notes

- If a completed call's `shortName` does not match a configured Broadcastify system, the upload is skipped and treated as a non-error.
- Encrypted calls are skipped and treated as a non-error.
- If talkgroup filters reject the call, the upload is skipped and an informational log message is written.
- If the matched system has no API key or system ID, the upload is skipped and treated as a non-error.
- Metadata upload must return HTTP `200` before audio upload is attempted.
- If the metadata response starts with:
- `1 SKIPPED...` the upload is logged as skipped and treated as a non-error.
- `1 REJECTED...` the upload is logged as rejected and treated as a non-error.
- any other non-zero code is treated as a retryable error.
- Audio upload errors are treated as retryable failures.
- The plugin uses shared cURL DNS caching with a 300 second TTL to reduce DNS lookups.

## Example

```json
{
"plugins": [
{
"name": "Broadcastify Main",
"library": "libbroadcastify_uploader.so",
"broadcastifyCallsServer": "https://api.broadcastify.com/call-upload",
"broadcastifySslVerifyDisable": false,
"broadcastifyOTA": true,
"systems": [
{
"shortName": "county",
"broadcastifyApiKey": "fakekey",
"broadcastifySystemId": 411,
"broadcastifyAllow": ["507*", "12???"],
"broadcastifyDeny": ["507?9", "12345"]
}
]
}
]
}
```

## Minimal Example

```json
{
"plugins": [
{
"name": "Broadcastify Main",
"library": "libbroadcastify_uploader.so",
"broadcastifyCallsServer": "https://api.broadcastify.com/call-upload",
"systems": [
{
"shortName": "county",
"broadcastifyApiKey": "fakekey",
"broadcastifySystemId": 411
}
]
}
]
}
```
101 changes: 101 additions & 0 deletions docs/plugins/openmhz.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
sidebar_label: 'OpenMHz'
---

# OpenMHz Plugin

**Plugin name:** `openmhz_uploader`
**Library:** `libopenmhz_uploader.so`

The OpenMHz plugin uploads completed calls to an OpenMHz server. It runs on `call_end` and uploads the converted call audio file plus call metadata to the configured OpenMHz system upload endpoint.

## Plugin Object

| Key | Required | Default Value | Type | Description |
| --- | :---: | --- | --- | --- |
| name | ✓ | `openmhz_uploader` | string | Friendly name for this plugin instance. This is used in logging. If set to exactly `openmhz_uploader`, log messages display `OpenMHz`. Otherwise the configured name is used. |
| library | ✓ | | string | Must be `libopenmhz_uploader.so`. |
| enabled | | `true` | **true** / **false** | Whether this plugin instance should be loaded. General plugin loading behavior is handled by the plugin manager. |
| uploadServer | ✓ | | string | Base OpenMHz server URL. The plugin validates that this is a parseable HTTP or HTTPS URL. Uploads are sent to `/<openmhzSystemId>/upload` on this server. |
| systems | ✓ | | array | Array of system objects. The plugin scans these for systems with an `apiKey` and uses them to match completed calls by `shortName`. If no usable systems are configured, the plugin returns an error during configuration parsing. |

## OpenMHz System Object

Each object in `systems` describes one Trunk Recorder system that should be uploaded to OpenMHz.

| Key | Required | Default Value | Type | Description |
| --- | :---: | --- | --- | --- |
| shortName | ✓ | | string | Must match the `shortName` of a system defined in the main Trunk Recorder config. The plugin uses this to match a completed call to the correct OpenMHz upload settings. |
| apiKey | ✓ | | string | API key used for uploads for this system. Systems without an API key are ignored by the plugin. The key is partially redacted in logs. |
| openmhzSystemId | | `shortName` | string | OpenMHz system ID to upload into. If omitted, the plugin uses the system `shortName`.|

## What the Plugin Uploads

On successful `call_end`, the plugin uploads the converted call audio file and the following metadata fields:

- `freq`
- `error_count`
- `spike_count`
- `start_time`
- `stop_time`
- `call_length`
- `talkgroup_num`
- `emergency`
- `api_key`
- `patch_list`
- `source_list`

The plugin always uploads `call_info.converted`, so this plugin expects the converted/compressed call artifact to exist.

## Behavior Notes

- If a completed call's `shortName` does not match a configured OpenMHz system with an API key, the upload is skipped and treated as a non-error.
- If the server returns HTTP `200`, the upload is logged as a success.
- Certain server-side responses are treated as configuration or policy issues and do **not** go to the retry queue:
- `API Keys do not match` → logged as `Invalid API Key`
- `ShortName does not exist` → logged as `Invalid System Name`
- `Error, invalid filename` → logged as `Invalid Filename`
- `Talkgroup does not exist` → logged as `Skipped: System Ignoring Unknown Talkgroups`
- Other failures are logged as upload errors and returned as retryable failures.
- The plugin uses shared cURL DNS caching with a 300 second TTL to reduce DNS lookups.

## Example

```json
{
"plugins": [
{
"name": "OpenMHz Main",
"library": "libopenmhz_uploader.so",
"uploadServer": "https://api.openmhz.com",
"systems": [
{
"shortName": "county",
"apiKey": "fakekey",
"openmhzSystemId": "county-main"
}
]
}
]
}
```

## Minimal Example

```json
{
"plugins": [
{
"name": "OpenMHz Main",
"library": "libopenmhz_uploader.so",
"uploadServer": "https://api.openmhz.com",
"systems": [
{
"shortName": "county",
"apiKey": "fakekey"
}
]
}
]
}
```
114 changes: 114 additions & 0 deletions docs/plugins/rdio-scanner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
sidebar_label: 'Rdio Scanner'
---

# Rdio Scanner Plugin

**Plugin name:** `rdioscanner_uploader`
**Library:** `librdioscanner_uploader.so`

The Rdio Scanner plugin uploads completed calls to an Rdio Scanner server using that server's `/api/call-upload` endpoint. It runs on `call_end`, skips encrypted calls, and supports per-system API credentials and optional talkgroup allow/deny filters.

## Plugin Object

| Key | Required | Default Value | Type | Description |
| --- | :---: | --- | --- | --- |
| name | ✓ | `rdioscanner_uploader` | string | Friendly name for this plugin instance. This is used in logging. If set to exactly `rdioscanner_uploader`, log messages display `Rdio Scanner`. Otherwise the configured name is used. |
| library | ✓ | | string | Must be `librdioscanner_uploader.so`. |
| enabled | | `true` | **true** / **false** | Whether this plugin instance should be loaded. General plugin behavior is handled by the plugin manager. |
| server | ✓ | | string | Base URL for the Rdio Scanner server. The plugin validates that this is a parseable HTTP or HTTPS URL. Uploads are sent to `/api/call-upload` on this server. |
| systems | ✓ | | array | Array of Rdio Scanner system objects. At least one valid system with an `apiKey` must be configured or the plugin returns an error during configuration parsing. |

## Rdio Scanner System Object

Each object in `systems` describes one Trunk Recorder system that should be uploaded to Rdio Scanner.

| Key | Required | Default Value | Type | Description |
| --- | :---: | --- | --- | --- |
| shortName | ✓ | | string | Must match the `shortName` of a system defined in the main Trunk Recorder config. The plugin uses this to match a completed call to the correct Rdio Scanner upload settings.|
| apiKey | ✓ | | string | API key used for uploads for this system. If a system has no API key configured, uploads for that system are skipped. The key is partially redacted in logs. |
| systemId | ✓ | | number | Rdio Scanner system ID to send with uploads.|
| talkgroupAllow | | `[]` | array of string/number | Optional allow-list of talkgroups. If this list is non-empty, the talkgroup must match at least one pattern or the upload is skipped. Patterns use glob-style matching with `*` and `?`. Numeric values are accepted and converted to strings. |
| talkgroupDeny | | `[]` | array of string/number | Optional deny-list of talkgroups. If a talkgroup matches any deny pattern, the upload is skipped. Patterns use glob-style matching with `*` and `?`. Numeric values are accepted and converted to strings. |

## Talkgroup Filter Rules

Talkgroup filters are evaluated per configured Rdio Scanner system.

- Talkgroups are compared as strings, for example `50712`.
- `*` matches any number of characters.
- `?` matches a single character.
- If `talkgroupAllow` is non-empty, the talkgroup must match at least one allow pattern.
- If `talkgroupDeny` is non-empty, the talkgroup must not match any deny pattern.
- If both are present, allow is checked first, then deny.

## What the Plugin Uploads

On successful `call_end`, the plugin uploads the call audio plus metadata fields including:

- audio file
- audio filename
- audio type
- date/time
- frequencies
- primary frequency
- API key
- patches
- talkgroup
- talkgroup group / label / tag / name
- sources
- system ID
- system label (`shortName`)

The plugin uses the converted file when `compress_wav` is enabled for the call, otherwise it uploads the WAV file. It sets the logical audio type to `audio/mp4` for converted audio and `audio/wav` for WAV uploads.

## Behavior Notes

- Encrypted calls are skipped and treated as a non-error.
- If the matched system is missing or has no API key, the upload is skipped and treated as a non-error.
- If talkgroup filters reject the call, the upload is skipped and an informational log message is written.
- HTTP 2xx responses are considered successful. HTTP `202` is logged as an accepted upload.
- The plugin uses shared cURL DNS caching with a 300 second TTL to reduce DNS lookups.
## Example

```json
{
"plugins": [
{
"name": "Rdio Main",
"library": "librdioscanner_uploader.so",
"server": "http://127.0.0.1",
"systems": [
{
"shortName": "county",
"apiKey": "fakekey",
"systemId": 411,
"talkgroupAllow": ["507*", "12???"],
"talkgroupDeny": ["507?9", "12345"]
}
]
}
]
}
```

## Minimal Example

```json
{
"plugins": [
{
"name": "Rdio Main",
"library": "librdioscanner_uploader.so",
"server": "http://127.0.0.1",
"systems": [
{
"shortName": "county",
"apiKey": "fakekey",
"systemId": 411
}
]
}
]
}
```
Loading
Loading