diff --git a/CMakeLists.txt b/CMakeLists.txt
index 929c7efe6..11d4f7b47 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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
diff --git a/docs/CONFIGURE.md b/docs/CONFIGURE.md
index 30a047cc9..c5192fb04 100644
--- a/docs/CONFIGURE.md
+++ b/docs/CONFIGURE.md
@@ -116,6 +116,9 @@ Here is a map of the different sections of the *config.json* file:
There is a list of available Plugins [here](./Plugins.md).
+> Plugins are only loaded if they are explicitly listed in the `plugins` array.
+> Plugins are initialized in the order they appear in the config, and callbacks are run in that same order.
+
## Global Configs
@@ -124,15 +127,12 @@ There is a list of available Plugins [here](./Plugins.md).
| ver | ✓ | | number | The version of formatting for the config file. **This should be set to 2**. Trunk Recorder will not start without this set. |
| sources | ✓ | | array of JSON objects
[{}] | An array of JSON formatted [Source Objects](#source-object) that define the different SDRs available. Source Objects are described below. |
| systems | ✓ | | array of JSON objects
[{}] | An array of JSON formatted [System Objects](#system-object) that define the trunking systems that will be recorded. System Objects are described below. |
-| plugins | | | array of JSON objects
[{}] | An array of JSON formatted [Plugin Objects](#plugin-object) that define the different plugins to use. Refer to the [Plugin System](notes/PLUGIN-SYSTEM.md) documentation for more details. |
+| plugins | | | array of JSON objects
[{}] | An array of JSON formatted [Plugin Objects](#plugin-object) that define the plugins to load. Plugins are only loaded when listed here, and they run in the same order they appear in this array. Refer to the [Plugin System](notes/PLUGIN-SYSTEM.md) documentation for more details. |
| defaultMode | | "digital" | **"analog"** or **"digital"** | Default mode to use when a talkgroups is not listed in the **talkgroupsFile**. The options are *digital* or *analog*. The default is *digital*. This argument is global and not system-specific, and only affects `smartnet` trunking systems which can have both analog and digital talkpaths. |
| tempDir | | /dev/shm *(if available)* else current directory | string | The complete path to the directory where individual Transmissions are recorded, prior to be combined into a single file. It is best to use memory based file system for this. |
| archiveFilesOnFailure | | false | **true** / **false** | If a plugin (like the OpenMHz or Broadcastify uploader) fails, should the files be saved locally or removed. If Audio Archive is set to **true** then audio is always archived and overrides this. |
| captureDir | | current directory | string | The complete path to the directory where recordings should be saved. |
| callTimeout | | 3 | number | A Call will stop recording and save if it has not received anything on the control channel, after this many seconds. |
-| uploadServer | | | string | The URL for uploading to OpenMHz. The default is an empty string. See the Config tab for your system in OpenMHz to find what the value should be. |
-| broadcastifyCallsServer | | | string | The URL for uploading to Broadcastify Calls. The default is an empty string. Refer to [Broadcastify's wiki](https://wiki.radioreference.com/index.php/Broadcastify-Calls-API) for the upload URL. |
-| broadcastifySslVerifyDisable | | false | **true** / **false** | Optionally disable SSL verification for Broadcastify uploads, given their apparent habit of letting their SSL certificate expire |
| consoleLog | | true | **true** / **false** | Send logging output to the console |
| logFile | | false | **true** / **false** | Send logging output to a file |
| logDir | | logs/ | string | Where the output logs should be put |
@@ -226,54 +226,47 @@ During the status display, each source will report the running average as well a
## System Object
-| Key | Required | Default Value | Type | Description |
-|--------------------------|:--------:|----------------------------|------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------ |
-| shortName | ✓ | | string | This is a nickname for the system. It is used to help name and organize the recordings from this system. It should be 4-6 letters with no spaces. |
-| type | ✓ | | **"smartnet"**, **"p25"**, **"conventional"**, **"conventionalDMR"** or **"conventionalP25"**, **"conventionalSIGMF"** | The type of radio system. |
-| control_channels | ✓ | | array of numbers;
[496537500, 496437500] | *For trunked systems* The control channel frequencies for the system, in Hz. The frequencies will automatically be cycled through if the system moves to an alternate channel. |
-| channels | ✓ | | array of numbers;
[166725000, 166925000, 167075000, 166850000] | *For conventional systems* The channel frequencies, in Hz, used for the system. The channels get assigned a virtual talkgroup number based upon their position in the array. Squelch levels need to be specified for the Source(s) being used. |
-| channelFile | ✓ | | string | *For conventional systems* The filename for a CSV file that provides information about the conventional channels. The format for the file is described below. Squelch levels need to be specified for the Source(s) being used. *Use channels or channelFile, not both*. |
-| modulation | | "qpsk" | **"qpsk"** or **"fsk4"** | The type of digital modulation that the system uses. You do not need to specify this with **conventionalDMR** systems. |
-| squelch | | -160 | number | Squelch in DB, this needs to be set for all conventional systems. The squelch setting is also used for analog talkgroups in a SmartNet system. I generally use -60 for my rtl-sdr. The closer the squelch is to 0, the stronger the signal has to be to unmute it. |
-| talkgroupsFile | | | string | The filename for a CSV file that provides information about the talkgroups. It determines whether a talkgroup is analog or digital, and what priority it should have. This file should be located in the same directory as the trunk-recorder executable. |
-| apiKey | | | string | *if uploadServer is set* System-specific API key for uploading calls to OpenMHz.com. See the Config tab for your system in OpenMHz to find what the value should be. |
-| openmhzSystemId | | `shortName` | string | *if uploadServer is set* By default, the plugin will upload calls to the `shortName` OpenMHz system. Setting this value will allow uploads to any specific OpenMHz system with its valid API key. This is useful in a multi-site setup where multiple trunk-recorder systems may be aggregating calls to the same OpenMHz feed. |
-| broadcastifyApiKey | | | string | *if broadcastifyCallsServer is set* System-specific API key for Broadcastify Calls |
-| broadcastifySystemId | | | number | *if broadcastifyCallsServer is set* System ID for Broadcastify Calls
(this is an integer, and different from the RadioReference system ID) |
-| broadcastifyAllow | | | array of string/number;
["507*", "12?45", 12345] | *if broadcastifyCallsServer is set* Optional allow-list for Broadcastify uploads, based on the talkgroup ID **as a string**. Supports glob wildcards: `*` (any length) and `?` (single character). If set (non-empty), the talkgroup **must** match at least one entry or the upload is skipped. |
-| broadcastifyDeny | | | array of string/number;
["99*", "12345"] | *if broadcastifyCallsServer is set* Optional deny-list for Broadcastify uploads, based on the talkgroup ID **as a string**. Supports glob wildcards: `*` (any length) and `?` (single character). If set (non-empty), any matching talkgroup is skipped. |
-| uploadScript | | | string | The filename of a script that is called after each call has finished processing. The script is passed the final `.wav` path as the first argument, the call JSON path as the second argument, and the `.m4a` path as the third argument. The `.wav` and JSON files always exist; the `.m4a` file is only created when `compressWav` is enabled. Checkout *encode-upload.sh.sample* as an example. Should probably start with `./` (or `../`). |
-| compressWav | | true | bool | Convert the final call `.wav` file to an `.m4a` file. **This is required for both OpenMHz and Broadcastify!** The `.wav` file is always created first; when `compressWav` is enabled, an additional `.m4a` file is created from that `.wav`. Requires `ffmpeg` to be installed. |
+| Key | Required | Default Value | Type | Description |
+| ---------------------- | :------: | -------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------ |
+| shortName | ✓ | | string | This is a nickname for the system. It is used to help name and organize the recordings from this system. It should be 4-6 letters with no spaces. |
+| type | ✓ | | **"smartnet"**, **"p25"**, **"conventional"**, **"conventionalDMR"** or **"conventionalP25"**, **"conventionalSIGMF"** | The type of radio system. |
+| control_channels | ✓ | | array of numbers;
[496537500, 496437500] | *For trunked systems* The control channel frequencies for the system, in Hz. The frequencies will automatically be cycled through if the system moves to an alternate channel. |
+| channels | ✓ | | array of numbers;
[166725000, 166925000, 167075000, 166850000] | *For conventional systems* The channel frequencies, in Hz, used for the system. The channels get assigned a virtual talkgroup number based upon their position in the array. Squelch levels need to be specified for the Source(s) being used. |
+| channelFile | ✓ | | string | *For conventional systems* The filename for a CSV file that provides information about the conventional channels. The format for the file is described below. Squelch levels need to be specified for the Source(s) being used. *Use channels or channelFile, not both*. |
+| modulation | | "qpsk" | **"qpsk"** or **"fsk4"** | The type of digital modulation that the system uses. You do not need to specify this with **conventionalDMR** systems. |
+| squelch | | -160 | number | Squelch in DB, this needs to be set for all conventional systems. The squelch setting is also used for analog talkgroups in a SmartNet system. I generally use -60 for my rtl-sdr. The closer the squelch is to 0, the stronger the signal has to be to unmute it. |
+| talkgroupsFile | | | string | The filename for a CSV file that provides information about the talkgroups. It determines whether a talkgroup is analog or digital, and what priority it should have. This file should be located in the same directory as the trunk-recorder executable. |
+| uploadScript | | | string | The filename of a script that is called after each call has finished processing. The script is passed the final `.wav` path as the first argument, the call JSON path as the second argument, and the `.m4a` path as the third argument. The `.wav` and JSON files always exist; the `.m4a` file is only created when `compressWav` is enabled. Checkout *encode-upload.sh.sample* as an example. Should probably start with `./` (or `../`). |
+| compressWav | | true | bool | Convert the final call `.wav` file to an `.m4a` file. **This is required for both OpenMHz and Broadcastify!** The `.wav` file is always created first; when `compressWav` is enabled, an additional `.m4a` file is created from that `.wav`. Requires `ffmpeg` to be installed. |
| compressBitrate | | 32k | string | Sets the audio bitrate used when compressWav creates the final .m4a file with ffmpeg (for example 16k, 32k, 48k, or 64k). This setting only applies to the compressed .m4a output and does not affect the original .wav file. Ignored when compressWav is false. |
-| audio_postprocess | | | object | Optional per-system audio cleanup and loudness normalization settings applied when concluding calls. Cleanup filtering and loudnorm are configured independently. See the **Audio Post-Processing** section below for full details. |
-| unitScript | | | string | The filename of a script that runs when a radio (unit) registers (is turned on), affiliates (joins a talk group), deregisters (is turned off), gets an acknowledgment response, transmits, gets a data channel grant, a unit-unit answer request or a Location Registration Response. Passed as parameters: `shortName radioID on\|join\|off\|ackresp\|call\|data\|ans_req\|location`. On joins and transmissions, `talkgroup` is passed as a fourth parameter; on answer requests, the `source` is. On joins and transmissions, `patchedTalkgroups` (comma separated list of talkgroup IDs) is passed as a fifth parameter if the talkgroup is part of a patch on the system. See *examples/unit-script.sh* for a logging example. Note that for paths relative to trunk-recorder, this should start with `./`( or `../`). |
-| audioArchive | | true | **true** / **false** | Should the recorded audio files be kept after successfully uploading them? |
-| transmissionArchive | | false | **true** / **false** | Should each of the individual transmission be kept? These transmission are combined together with other recent ones to form a single call. |
-| callLog | | true | **true** / **false** | Should a json file with the call details be kept after successful uploads? |
-| analogLevels | | 8 | number (1-32) | The amount of amplification that will be applied to the analog audio. |
-| maxDev | | 5000 | number | The maximum deviation for analog channels. If you analog recordings sound good or if you have a completely digital system, then there is no need to touch this. |
-| digitalLevels | | 1 | number (1-16) | The amount of amplification that will be applied to the digital audio. |
-| unitTagsFile | | | string | The filename of a CSV file that provides information about the unit tags. The format for the file is described below. |
-| recordUnknown | | true | **true** / **false** | Record talkgroups if they are not listed in the Talkgroups File. |
-| hideEncrypted | | false | **true** / **false** | Hide encrypted talkgroups log entries |
-| hideUnknownTalkgroups | | false | **true** / **false** | Hide unknown talkgroups log entries |
-| minDuration | | 0
(which is disabled) | number | The minimum call duration in seconds (decimals allowed), calls below this number will have recordings deleted and will not be uploaded. |
-| minTransmissionDuration | | 0
(which is disabled) | number | The minimum transmission duration in seconds (decimals allowed), transmissions below this number will not be added to their corresponding call. |
-| maxDuration | | 0
(which is disabled) | number | The maximum call duration in seconds (decimals allowed), calls above this number will have recordings split into multiple parts. |
-| talkgroupDisplayFormat | | "id" | **"id" "id_tag"** or **"tag_id"** | The display format for talkgroups in the console and log file. (*id_tag* and *tag_id* is only valid if **talkgroupsFile** is specified) |
-| bandplan | | "800_standard" | **"800_standard"**, **"800_reband"**, **"800_splinter"** or **"400_custom"** | *SmartNet only* The SmartNet bandplan that will be used. |
-| bandplanBase | | | number | *400_custom only* The base frequency, specified in Hz. |
-| bandplanHigh | | | number | *SmartNet, 400_custom only* The highest channel in the system, specified in Hz. |
-| bandplanSpacing | | | number | *SmartNet, 400_custom only* The channel spacing, specified in Hz. Typically this is *25000*. |
-| bandplanOffset | | | number | *SmartNet, 400_custom only* The offset used to calculate frequencies. |
-| customFrequencyTableFile | | | string | *P25 only* The filename for a CSV file that provides information about the P25 custom frequency tables. The format for the file is described below. |
-| decodeMDC | | false | **true** / **false** | *Conventional systems only* enable the MDC-1200 signaling decoder. |
-| decodeFSync | | false | **true** / **false** | *Conventional systems only* enable the Fleet Sync signaling decoder. |
-| decodeStar | | false | **true** / **false** | *Conventional systems only* enable the Star signaling decoder. |
-| decodeTPS | | false | **true** / **false** | *Conventional systems only* enable the Motorola Tactical Public Safety (aka FDNY Fireground) signaling decoder. |
-| deemphasisTau | | 0.000750 | number | *Conventional systems only* configure the de-emphasis time constant. 750µs for NFM (default), 75µs for WFM North America, 50µs for WFM most other regions. |
-| enabled | | true | **true** / **false** | control whether a configured system is enabled or disabled |
-| filenameFormat | | | string | A format string that controls the directory structure and filename for recorded calls. When set at the system level it overrides the instance-level `filenameFormat`. See the [Filename Format](#filename-format) section below for full details. |
+| audio_postprocess | | | object | Optional per-system audio cleanup and loudness normalization settings applied when concluding calls. Cleanup filtering and loudnorm are configured independently. See the **Audio Post-Processing** section below for full details. |
+| audioArchive | | true | **true** / **false** | Should the recorded audio files be kept after successfully uploading them? |
+| transmissionArchive | | false | **true** / **false** | Should each of the individual transmission be kept? These transmission are combined together with other recent ones to form a single call. |
+| callLog | | true | **true** / **false** | Should a json file with the call details be kept after successful uploads? |
+| analogLevels | | 8 | number (1-32) | The amount of amplification that will be applied to the analog audio. |
+| maxDev | | 5000 | number | The maximum deviation for analog channels. If you analog recordings sound good or if you have a completely digital system, then there is no need to touch this. |
+| digitalLevels | | 1 | number (1-16) | The amount of amplification that will be applied to the digital audio. |
+| unitTagsFile | | | string | The filename of a CSV file that provides information about the unit tags. The format for the file is described below. |
+| recordUnknown | | true | **true** / **false** | Record talkgroups if they are not listed in the Talkgroups File. |
+| hideEncrypted | | false | **true** / **false** | Hide encrypted talkgroups log entries |
+| hideUnknownTalkgroups | | false | **true** / **false** | Hide unknown talkgroups log entries |
+| minDuration | | 0
(which is disabled) | number | The minimum call duration in seconds (decimals allowed), calls below this number will have recordings deleted and will not be uploaded. |
+| minTransmissionDuration| | 0
(which is disabled) | number | The minimum transmission duration in seconds (decimals allowed), transmissions below this number will not be added to their corresponding call. |
+| maxDuration | | 0
(which is disabled) | number | The maximum call duration in seconds (decimals allowed), calls above this number will have recordings split into multiple parts. |
+| talkgroupDisplayFormat | | "id" | **"id" "id_tag"** or **"tag_id"** | The display format for talkgroups in the console and log file. (*id_tag* and *tag_id* is only valid if **talkgroupsFile** is specified) |
+| bandplan | | "800_standard" | **"800_standard"**, **"800_reband"**, **"800_splinter"** or **"400_custom"** | *SmartNet only* The SmartNet bandplan that will be used. |
+| bandplanBase | | | number | *400_custom only* The base frequency, specified in Hz. |
+| bandplanHigh | | | number | *SmartNet, 400_custom only* The highest channel in the system, specified in Hz. |
+| bandplanSpacing | | | number | *SmartNet, 400_custom only* The channel spacing, specified in Hz. Typically this is *25000*. |
+| bandplanOffset | | | number | *SmartNet, 400_custom only* The offset used to calculate frequencies. |
+| customFrequencyTableFile| | | string | *P25 only* The filename for a CSV file that provides information about the P25 custom frequency tables. The format for the file is described below. |
+| decodeMDC | | false | **true** / **false** | *Conventional systems only* enable the MDC-1200 signaling decoder. |
+| decodeFSync | | false | **true** / **false** | *Conventional systems only* enable the Fleet Sync signaling decoder. |
+| decodeStar | | false | **true** / **false** | *Conventional systems only* enable the Star signaling decoder. |
+| decodeTPS | | false | **true** / **false** | *Conventional systems only* enable the Motorola Tactical Public Safety (aka FDNY Fireground) signaling decoder. |
+| deemphasisTau | | 0.000750 | number | *Conventional systems only* configure the de-emphasis time constant. 750µs for NFM (default), 75µs for WFM North America, 50µs for WFM most other regions. |
+| enabled | | true | **true** / **false** | control whether a configured system is enabled or disabled |
+| filenameFormat | | | string | A format string that controls the directory structure and filename for recorded calls. When set at the system level it overrides the instance-level `filenameFormat`. See the [Filename Format](#filename-format) section below for full details. |
***
@@ -323,17 +316,17 @@ Each system can optionally define an `audio_postprocess` object to control clean
```json
"audio_postprocess": {
-"enabled": false,
-"highpass_hz": 0,
-"lowpass_hz": 0,
-"bandreject_hz": 0,
-"bandreject_width_hz": 0,
-"loudnorm": true,
-"loudnorm_two_pass": true,
-"loudnorm_i": -16.0,
-"loudnorm_tp": -0.1,
-"loudnorm_lra": 11.0,
-"ffmpeg_filter": ""
+ "enabled": false,
+ "highpass_hz": 0,
+ "lowpass_hz": 0,
+ "bandreject_hz": 0,
+ "bandreject_width_hz": 0,
+ "loudnorm": true,
+ "loudnorm_two_pass": true,
+ "loudnorm_i": -16.0,
+ "loudnorm_tp": -0.1,
+ "loudnorm_lra": 11.0,
+ "ffmpeg_filter": ""
}
```
@@ -408,17 +401,17 @@ If that also fails, Trunk Recorder falls back to unfiltered rendering.
```json
"audio_postprocess": {
-"enabled": true,
-"highpass_hz": 200,
-"lowpass_hz": 0,
-"bandreject_hz": 4000,
-"bandreject_width_hz": 180,
-"loudnorm": false,
-"loudnorm_two_pass": true,
-"loudnorm_i": -16.0,
-"loudnorm_tp": -0.1,
-"loudnorm_lra": 11.0,
-"ffmpeg_filter": ""
+ "enabled": true,
+ "highpass_hz": 200,
+ "lowpass_hz": 0,
+ "bandreject_hz": 4000,
+ "bandreject_width_hz": 180,
+ "loudnorm": false,
+ "loudnorm_two_pass": true,
+ "loudnorm_i": -16.0,
+ "loudnorm_tp": -0.1,
+ "loudnorm_lra": 11.0,
+ "ffmpeg_filter": ""
}
```
@@ -426,17 +419,17 @@ If that also fails, Trunk Recorder falls back to unfiltered rendering.
```json
"audio_postprocess": {
-"enabled": false,
-"highpass_hz": 0,
-"lowpass_hz": 0,
-"bandreject_hz": 0,
-"bandreject_width_hz": 0,
-"loudnorm": true,
-"loudnorm_two_pass": true,
-"loudnorm_i": -16.0,
-"loudnorm_tp": -0.1,
-"loudnorm_lra": 11.0,
-"ffmpeg_filter": ""
+ "enabled": false,
+ "highpass_hz": 0,
+ "lowpass_hz": 0,
+ "bandreject_hz": 0,
+ "bandreject_width_hz": 0,
+ "loudnorm": true,
+ "loudnorm_two_pass": true,
+ "loudnorm_i": -16.0,
+ "loudnorm_tp": -0.1,
+ "loudnorm_lra": 11.0,
+ "ffmpeg_filter": ""
}
```
@@ -444,17 +437,17 @@ If that also fails, Trunk Recorder falls back to unfiltered rendering.
```json
"audio_postprocess": {
-"enabled": true,
-"highpass_hz": 0,
-"lowpass_hz": 0,
-"bandreject_hz": 0,
-"bandreject_width_hz": 0,
-"loudnorm": true,
-"loudnorm_two_pass": false,
-"loudnorm_i": -16.0,
-"loudnorm_tp": -0.1,
-"loudnorm_lra": 11.0,
-"ffmpeg_filter": "highpass=f=200,bandreject=f=4000:w=180"
+ "enabled": true,
+ "highpass_hz": 0,
+ "lowpass_hz": 0,
+ "bandreject_hz": 0,
+ "bandreject_width_hz": 0,
+ "loudnorm": true,
+ "loudnorm_two_pass": false,
+ "loudnorm_i": -16.0,
+ "loudnorm_tp": -0.1,
+ "loudnorm_lra": 11.0,
+ "ffmpeg_filter": "highpass=f=200,bandreject=f=4000:w=180"
}
```
@@ -464,17 +457,17 @@ If you include `loudnorm` directly in `ffmpeg_filter`, the built-in loudnorm set
```json
"audio_postprocess": {
-"enabled": true,
-"highpass_hz": 0,
-"lowpass_hz": 0,
-"bandreject_hz": 0,
-"bandreject_width_hz": 0,
-"loudnorm": true,
-"loudnorm_two_pass": true,
-"loudnorm_i": -16.0,
-"loudnorm_tp": -0.1,
-"loudnorm_lra": 11.0,
-"ffmpeg_filter": "highpass=f=200,loudnorm=I=-16:TP=-0.1:LRA=11"
+ "enabled": true,
+ "highpass_hz": 0,
+ "lowpass_hz": 0,
+ "bandreject_hz": 0,
+ "bandreject_width_hz": 0,
+ "loudnorm": true,
+ "loudnorm_two_pass": true,
+ "loudnorm_i": -16.0,
+ "loudnorm_tp": -0.1,
+ "loudnorm_lra": 11.0,
+ "ffmpeg_filter": "highpass=f=200,loudnorm=I=-16:TP=-0.1:LRA=11"
}
```
@@ -610,172 +603,130 @@ Produces:
## Plugin Object
-| Key | Required | Default Value | Type | Description |
-| ------- | :------: | ------------- | -------------------- | ------------------------------------------------------------ |
-| library | ✓ | | string | The filename of the plugin library to load. |
-| name | |plugin_library | string | Display name of the plugin used for identification and logging. |
-| enabled | | true | **true** / **false** | Control whether a configured plugin is enabled or disabled. |
-| | | | | *Additional elements can be added, they will be passed into the `parse_config` method of the plugin.* |
+Each entry in the top-level `plugins` array describes one plugin to load and the settings that should be passed to it.
-##### Rdio Scanner Plugin
+Plugins are only loaded if they are explicitly listed in the `plugins` array. If multiple plugins are configured, they are initialized in the order they appear in the config, and their callbacks are run in that same order.
-**Library:** librdioscanner_uploader.so
+### Standard Plugin Object Keys
-This plugin makes it easy to connect Trunk Recorder with [Rdio Scanner](https://github.com/chuot/rdio-scanner). It uploads recordings and the information about them. The following additional settings are required:
+| Key | Required | Default Value | Type | Description |
+| ------- | :------: | ----------------------------- | -------------------- | ----------- |
+| library | ✓ | | string | The shared library filename for the plugin, such as `libopenmhz_uploader.so`. |
+| name | | derived from library filename | string | Friendly plugin name. If omitted, Trunk Recorder derives the name from the library filename. For example, `libopenmhz_uploader.so` becomes `openmhz_uploader`. |
+| enabled | | true | **true** / **false** | Whether the configured plugin should be loaded. Disabled plugins are skipped. |
-| Key | Required | Default Value | Type | Description |
-| ------- | :------: | ------------- | ------ | ------------------------------------------------------------ |
-| name | | Rdio Scanner | string | Friendly name for this Rdio uploader. Can be used to better differentiate plugins if multiple are used to feed different servers. |
-| server | ✓ | | string | The URL for uploading to Rdio Scanner. The default is an empty string. It should be the same URL as the one you are using to access Rdio Scanner. |
-| systems | ✓ | | array | This is an array of objects, where each is a system that should be passed to Rdio Scanner. More information about what should be in each object is in the following table. |
+### Additional Plugin Settings
-*Rdio Scanner System Object:*
+Plugins may define additional configuration keys beyond `library`, `name`, and `enabled`.
-| Key | Required | Default Value | Type | Description |
-| --------- | :------: | ------------- | ------ | ------------------------------------------------------------ |
-| systemId | ✓ | | number | System ID for Rdio Scanner. |
-| apiKey | ✓ | | string | System-specific API key for uploading calls to Rdio Scanner. See the ApiKey section in the Rdio Scanner administrative dashboard for the value it should be. |
-| shortName | ✓ | | string | This should match the shortName of a system that is defined in the main section of the config file. |
+Any additional keys included in a plugin object are passed to that plugin during configuration parsing. The exact supported settings depend on the plugin you are using, so refer to that plugin's documentation in [Plugins](./Plugins.md) for its plugin-specific options.
+### Plugin Loading Behavior
+A plugin object only affects the plugin it belongs to. Plugin-specific settings are not global unless the plugin explicitly documents them that way.
-##### Example Plugin Object:
+For example, this object:
-```yaml
- {
- "name": "My Rdio Server",
- "library": "librdioscanner_uploader.so",
- "server": "http://127.0.0.1",
- "systems": [{
- "shortName": "test",
- "apiKey": "fakekey",
- "systemId": 411
- }
+```json
+{
+ "name": "rdioscanner_uploader",
+ "library": "librdioscanner_uploader.so",
+ "server": "http://127.0.0.1",
+ "systems": [
+ {
+ "shortName": "county",
+ "apiKey": "example-key",
+ "systemId": 411
+ }
+ ]
+}
```
-##### simplestream Plugin
+tells Trunk Recorder to:
-**Name:** simplestream
-**Library:** libsimplestream.so
+1. load `librdioscanner_uploader.so`
+2. identify the plugin as `rdioscanner_uploader`
+3. pass the additional settings such as `server` and `systems` to that plugin
-This plugin streams uncompressed audio (16 bit Int, 8 or 16 kHz, mono) to UDP or TCP ports in real time as it is being recorded by trunk-recorder. It can be configured to stream audio from all talkgroups and systems being recorded or only specified talkgroups and systems. TGID information can be prepended to the audio data to allow the receiving program to take action based on the TGID. Audio from different Systems should be streamed to different UDP/TCP ports to prevent crosstalk and interleaved audio from talkgroups with the same TGID on different systems.
+### Notes
-This plugin does not, by itself, stream audio to any online services. Because it sends uncompressed PCM audio, it is not bandwidth efficient and is intended mostly to send audio to other programs running on the same computer as trunk-recorder or to other computers on the LAN. The programs receiving PCM audio from this plugin may play it on speakers, compress it and stream it to an online service, etc.
+- Only plugins listed in the `plugins` array are loaded.
+- Disabled plugins are skipped.
+- The order of plugin objects in the `plugins` array matters.
+- If a plugin requires extra settings, those settings belong inside that plugin's object.
+- Older configuration values related to plugin behavior are not enough by themselves to load a plugin. The plugin must still be declared in the top-level `plugins` array.
-**NOTE 1: In order for this plugin to work, the audioStreaming option in the Global Configs section (see above) must be set to true.**
+### Example Plugin Objects
-**NOTE 2: trunk-recorder passes analog audio to this plugin at 16 kHz sample rate and digital audio at 8 kHz sample rate. JSON metadata (if enabled) will contain the sample rate of the audio being sent.**
+#### Minimal example
-| Key | Required | Default Value | Type | Description |
-| ------- | :------: | ------------- | ------ | ------------------------------------------------------------ |
-| streams | ✓ | | array | This is an array of objects, where each is an audio stream that will be sent to a specific IP address and UDP port. More information about what should be in each object is in the following table. |
+```json
+{
+ "plugins": [
+ {
+ "library": "libsimplestream.so"
+ }
+ ]
+}
+```
-*Audio Stream Object:*
+In this example, Trunk Recorder loads `libsimplestream.so`, derives the plugin name from the library filename, and uses the plugin's default behavior unless additional settings are provided.
-| Key | Required | Default Value | Type | Description |
-| --------- | :------: | ------------- | -------------------- | ------------------------------------------------------------ |
-| address | ✓ | | string | IP address to send this audio stream to. Use "127.0.0.1" to send to the same computer that trunk-recorder is running on. |
-| port | ✓ | | number | UDP or TCP port that this stream will send audio to. |
-| TGID | ✓ | | number | Audio from this Talkgroup ID will be sent on this stream. Set to 0 to stream all recorded talkgroups. |
-| sendJSON | | false | **true** / **false** | When set to true, JSON metadata will be prepended to the audio data each time a packet is sent. JSON fields are talkgroup, patched_talkgroups, src, src_tag, freq, audio_sample_rate, short_name, event (set to "audio"). The length of the JSON metadata is prepended to the metadata in long integer format (4 bytes, little endian). If this is set to **true**, the sendTGID field will be ignored. |
-| sendCallStart | | false | **true** / **false** | Only used if sendJSON is set to **true**. When set to true, a JSON message will be sent at the start of each call that includes the following JSON fields: talkgroup, talkgroup_tag, patched_talkgroups, patched_talkgroup_tags, src, src_tag, freq, short_name, event (set to "call_start"). The length of the JSON metadata is prepended to the metadata in long integer format (4 bytes, little endian).
-| sendCallEnd | | false | **true** / **false** | Only used if sendJSON is set to **true**. When set to true, a JSON message will be sent at the end of each call that includes the following JSON fields: talkgroup, patched_talkgroups, freq, short_name, event (set to "call_end"). The length of the JSON metadata is prepended to the metadata in long integer format (4 bytes, little endian).
-| sendTGID | | false | **true** / **false** | Deprecated. Recommend using sendJSON for metadata instead. If sendJSON is set to true, this setting will be ignored. When set to true, the TGID will be prepended in long integer format (4 bytes, little endian) to the audio data each time a packet is sent. |
-| shortName | | | string | shortName of the System that audio should be streamed for. This should match the shortName of a system that is defined in the main section of the config file. When omitted, all Systems will be streamed to the address and port configured. If TGIDs from Systems overlap, JSON metadata should be used to prevent interleaved audio for talkgroups from different Systems with the same TGID.
-| useTCP | | false | **true** / **false** | When set to true, TCP will be used instead of UDP.
+#### Example with explicit name and plugin-specific settings
-###### Plugin Object Example #1:
-This example will stream audio from talkgroup 58914 on system "CountyTrunked" to the local machine on UDP port 9123.
-```yaml
+```json
+{
+ "plugins": [
+ {
+ "name": "rdioscanner_uploader",
+ "library": "librdioscanner_uploader.so",
+ "enabled": true,
+ "server": "http://127.0.0.1",
+ "systems": [
{
- "name":"simplestream",
- "library":"libsimplestream.so",
- "streams":[{
- "TGID":58914,
- "address":"127.0.0.1",
- "port":9123,
- "sendJSON":false,
- "shortName":"CountyTrunked"}
+ "shortName": "test",
+ "apiKey": "fakekey",
+ "systemId": 411
}
+ ]
+ }
+ ]
+}
```
-###### Plugin Object Example #2:
-This example will stream audio from talkgroup 58914 from System CountyTrunked to the local machine on UDP port 9123 and stream audio from talkgroup 58916 from System "StateTrunked" to the local machine on UDP port 9124.
-```yaml
- {
- "name":"simplestream",
- "library":"libsimplestream.so",
- "streams":[{
- "TGID":58914,
- "address":"127.0.0.1",
- "port":9123,
- "sendJSON":false,
- "shortName":"CountyTrunked"},
- {"TGID":58916,
- "address":"127.0.0.1",
- "port":9124,
- "sendJSON":false,
- "shortName":"StateTrunked"}
- ]}
- }
-```
+#### Example with multiple plugins
-###### Plugin Object Example #3:
-This example will stream audio from talkgroups 58914 and 58916 from all Systems to the local machine on the same UDP port 9123. It will prepend the TGID and other JSON metadata to the audio data in each UDP packet so that the receiving program can differentiate the two audio streams (the receiver may decide to only play one depending on priority, mix the two streams, play one left and one right, etc.)
-```yaml
+```json
+{
+ "plugins": [
+ {
+ "library": "librdioscanner_uploader.so",
+ "server": "http://127.0.0.1",
+ "systems": [
{
- "name":"simplestream",
- "library":"libsimplestream.so",
- "streams":[{
- "TGID":58914,
- "address":"127.0.0.1",
- "port":9123,
- "sendJSON":true},
- {"TGID":58916,
- "address":"127.0.0.1",
- "port":9123,
- "sendJSON":true}
- ]}
+ "shortName": "county",
+ "apiKey": "fakekey",
+ "systemId": 411
}
-```
-###### Plugin Object Example #4:
-This example will stream audio from all talkgroups being recorded on System CountyTrunked to the local machine on UDP port 9123. It will prepend the TGID and other JSON metadata to the audio data in each UDP packet so that the receiving program can decide which ones to play or otherwise handle)
-```yaml
+ ]
+ },
+ {
+ "library": "libsimplestream.so",
+ "streams": [
{
- "name":"simplestream",
- "library":"libsimplestream.so",
- "streams":[{
- "TGID":0,
- "address":"127.0.0.1",
- "port":9123,
- "sendJSON":true,
- "shortName":"CountyTrunked"}
+ "TGID": 58914,
+ "address": "127.0.0.1",
+ "port": 9123,
+ "sendTGID": false,
+ "shortName": "CountyTrunked"
}
+ ]
+ }
+ ]
+}
```
-##### Example - Sending Audio to pulseaudio
-pulseaudio is the default sound system on many Linux computers, including the Raspberry Pi. If configured to do so, pulseaudio can accept raw audio via TCP connection using the module-simple-protocol-tcp module. Each TCP connection will show up as a different "application" in the pavucontrol volume mixer.
-An example command to set up pulseaudio to receive 8 kHz digital audio from simplestream on TCP port 9125 (for 16 kHz analog audio, use `rate=16000`):
-```
-pacmd load-module module-simple-protocol-tcp sink=1 playback=true port=9125 format=s16le rate=8000 channels=1
-```
-The matching simplestream config to send audio from talkgroup 58918 to TCP port 9125 would then be something like this:
-```yaml
- {
- "name":"simplestream",
- "library":"libsimplestream.so",
- "streams":[{
- "TGID":58918,
- "address":"127.0.0.1",
- "port":9125,
- "sendJSON":false,
- "shortName":"CountyTrunked",
- "useTCP":true}
- }
-```
-#### Example - Sending Audio to FFMPEG for compression
-Here's an FFMPEG command that takes PCM audio from simplestream via UDP, cleans it up, and outputs ogg/opus to stdout. Note that this will only work if sendTGID and sendJSON are both set to false and only a single talkgroup is fed to ffmpeg over the UDP port, as ffmpeg cannot interpret any metadata.
-`ffmpeg -loglevel warning -f s16le -ar 16000 -ac 1 -i udp://localhost:9125 -af:a adeclick -f:a ogg -c:a libopus -frame_duration:a 20 -vbr:a on -b:a 48000 -application:a voip pipe:1`
+In this example, the Rdio Scanner plugin is initialized first and the simplestream plugin second, because that is the order they appear in the `plugins` array.
## talkgroupsFile
diff --git a/docs/Plugins.md b/docs/Plugins.md
index 93286a77d..c882ce8bf 100644
--- a/docs/Plugins.md
+++ b/docs/Plugins.md
@@ -4,244 +4,148 @@ sidebar_position: 4
---
# Plugins
-Plugins make it easy to customize Trunk Recorder and have it better fit you workflow. There are some built-in Plugins that are included in Trunk Recorder and also ones that the Community has developed.
-The [Built-in Plugins](#built-in-plugins) are compiled and installed when you setup Trunk Recorder. Follow the instructions for the [Community Plugins](#community-plugins) to install them. In order to load a plugin when you start Trunk Recorder, you need to add a **plugins** section to your **config.json** file. A [Plugin JSON Object](#plugin-object) is add for each of the Plugins you wish to load. The Plugin Object tells Trunk Recorder how to load it and its configuration.
+Plugins make it easy to customize Trunk Recorder and better fit it to your workflow. Some plugins are included with Trunk Recorder, and others are developed separately by the community.
+
+To load a plugin, add it to the top-level `plugins` array in your `config.json` file.
```json
{
- Global Configs
-
- "sources": [{ Source Object }, { Source Object }],
- "systems": [{ System Object }, { System Object }],
- "plugins": [{ Plugin Object }]
+ "plugins": [
+ {
+ "name": "simplestream-main",
+ "library": "libsimplestream.so"
+ }
+ ]
}
```
+For general configuration, see [Configure](./Configure.md).
-#### Plugin Object
+## Plugin Object
-| Key | Required | Default Value | Type | Description |
-| ------- | :------: | ------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
-| library | ✓ | | string | the name of the library that contains the plugin. |
-| name | ✓ | | string | the name of the plugin. This name is used to find the `_plugin_new` method that creates a new instance of the plugin. |
-| enabled | | true | **true** / **false** | control whether a configured plugin is enabled or disabled |
-| | | | | *Additional elements can be added, they will be passed into the `parse_config` method of the plugin.* |
+Each entry in the top-level `plugins` array describes one plugin instance to load and the settings that should be passed to it.
+Plugins are only loaded if they are explicitly listed in the `plugins` array. If multiple plugins are configured, they are initialized in the order they appear in the config, and their callbacks are run in that same order.
-## Built-in Plugins
+### Standard Plugin Object Keys
-##### Rdio Scanner Plugin
+| Key | Required | Default Value | Type | Description |
+| ------- | :------: | ----------------------------- | -------------------- | ----------- |
+| library | ✓ | | string | The shared library filename for the plugin, such as `libopenmhz_uploader.so`. |
+| name | ✓ | derived from library filename | string | Friendly name for this plugin instance. This name is used in logging and helps distinguish multiple instances of the same plugin. If omitted, Trunk Recorder currently derives the name from the library filename, but setting it explicitly is recommended. |
+| enabled | | true | **true** / **false** | Whether this configured plugin instance should be loaded. Disabled plugins are skipped. |
-**Name:** rdioscanner_uploader
-**Library:** librdioscanner_uploader.so
+### Additional Plugin Settings
-This plugin makes it easy to connect Trunk Recorder with [Rdio Scanner](https://github.com/chuot/rdio-scanner). It uploads recordings and the information about them. The following additional settings are required:
+Plugins may define additional configuration keys beyond `library`, `name`, and `enabled`.
-| Key | Required | Default Value | Type | Description |
-| ------- | :------: | ------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| server | ✓ | | string | The URL for uploading to Rdio Scanner. The default is an empty string. It should be the same URL as the one you are using to access Rdio Scanner. |
-| systems | ✓ | | array | This is an array of objects, where each is a system that should be passed to Rdio Scanner. More information about what should be in each object is in the following table. |
+Any additional keys included in a plugin object are passed to that plugin during configuration parsing. The exact supported settings depend on the plugin you are using.
-*Rdio Scanner System Object:*
+### Notes
-| Key | Required | Default Value | Type | Description |
-| --------------- | :------: | ------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| systemId | ✓ | | number | System ID for Rdio Scanner. |
-| apiKey | ✓ | | string | System-specific API key for uploading calls to Rdio Scanner. See the ApiKey section in the Rdio Scanner administrative dashboard for the value it should be. |
-| shortName | ✓ | | string | This should match the shortName of a system that is defined in the main section of the config file. |
-| talkgroupAllow | | [] | array | Optional allow-list of talkgroups to upload for this system. If set (non-empty), the talkgroup **must** match at least one pattern to be uploaded. Patterns are glob-style (supports `*` and `?`). |
-| talkgroupDeny | | [] | array | Optional deny-list of talkgroups to block upload for this system. If set (non-empty), any matching talkgroup will be skipped. Patterns are glob-style (supports `*` and `?`). |
+- Each object in the `plugins` array creates a separate plugin instance.
+- The same plugin library may be listed more than once if you need multiple differently configured instances.
+- When using multiple instances of the same plugin, giving each one a unique `name` is strongly recommended.
+- Only plugins listed in the `plugins` array are loaded.
-**Talkgroup filter rules (per-system):**
-- Talkgroup comparisons are done against the numeric talkgroup ID as a string (e.g. `50712`).
-- Patterns are glob-style:
- - `*` matches any number of characters
- - `?` matches a single character
-- If `talkgroupAllow` is provided and non-empty, the talkgroup must match at least one allow pattern (otherwise it is skipped).
-- If `talkgroupDeny` is provided and non-empty, the talkgroup must **not** match any deny pattern (otherwise it is skipped).
-- If both are provided, the allow check is applied first, then the deny check.
+## Included Trunk Recorder Plugins
-**Examples:**
-- Allow only talkgroups starting with `507`:
- - `talkgroupAllow: ["507*"]`
-- Allow only 5-digit talkgroups starting with `12` (uses `?`):
- - `talkgroupAllow: ["12???"]`
-- Block a specific talkgroup:
- - `talkgroupDeny: ["12345"]`
-- Block a prefix/range while allowing others:
- - `talkgroupDeny: ["99*"]`
-- Block a specific “slot” pattern using `?`:
- - `talkgroupDeny: ["507?9"]`
+The following plugins are included with Trunk Recorder. This page provides only a brief summary for each one. For configuration details, supported settings, and examples, open the linked plugin page.
-##### Example Plugin Object:
+### Rdio Scanner
-```json
-{
- "name": "rdioscanner_uploader",
- "library": "librdioscanner_uploader.so",
- "server": "http://127.0.0.1",
- "systems": [
- {
- "shortName": "test",
- "apiKey": "fakekey",
- "systemId": 411,
- "talkgroupAllow": ["507*", "12???"],
- "talkgroupDeny": ["507?9", "12345"]
- }
- ]
-}
-```
+**Plugin name:** `rdioscanner_uploader`
+**Library:** `librdioscanner_uploader.so`
+Uploads recordings and call information to an Rdio Scanner server. Supports per-system configuration and optional talkgroup allow/deny filters.
-##### simplestream Plugin
+More details: [Rdio Scanner Plugin](./plugins/rdio-scanner.md)
-**Name:** simplestream
-**Library:** libsimplestream.so
+### OpenMHz
-This plugin streams uncompressed audio (16 bit Int, 8 kHz, mono) to UDP or TCP ports in real time as it is being recorded by trunk-recorder. It can be configured to stream audio from all talkgroups and systems being recorded or only specified talkgroups and systems. TGID information can be prepended to the audio data to allow the receiving program to take action based on the TGID. Audio from different Systems should be streamed to different UDP/TCP ports to prevent crosstalk and interleaved audio from talkgroups with the same TGID on different systems.
+**Plugin name:** `openmhz_uploader`
+**Library:** `libopenmhz_uploader.so`
-This plugin does not, by itself, stream audio to any online services. Because it sends uncompressed PCM audio, it is not bandwidth efficient and is intended mostly to send audio to other programs running on the same computer as trunk-recorder or to other computers on the LAN. The programs receiving PCM audio from this plugin may play it on speakers, compress it and stream it to an online service, etc.
+Uploads calls to OpenMHz.
-**NOTE 1: In order for this plugin to work, the audioStreaming option in the Global Configs section (see above) must be set to true.**
+More details: [OpenMHz Plugin](./plugins/openmhz.md)
-**NOTE 2: trunk-recorder passes analog audio to this plugin at 16 kHz sample rate and digital audio at 8 kHz sample rate. Since the audio data being streamed doesn't contain the sample rate, analog and digital audio should be configured to be sent to different ports to receivers that are matched to the same sample rate.**
+### Broadcastify Calls
-| Key | Required | Default Value | Type | Description |
-| ------- | :------: | ------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| streams | ✓ | | array | This is an array of objects, where each is an audio stream that will be sent to a specific IP address and UDP port. More information about what should be in each object is in the following table. |
+**Plugin name:** `broadcastify_uploader`
+**Library:** `libbroadcastify_uploader.so`
-*Audio Stream Object:*
+Uploads calls to Broadcastify Calls.
-| Key | Required | Default Value | Type | Description |
-| --------- | :------: | ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| address | ✓ | | string | IP address to send this audio stream to. Use "127.0.0.1" to send to the same computer that trunk-recorder is running on. |
-| port | ✓ | | number | UDP or TCP port that this stream will send audio to. |
-| TGID | ✓ | | number | Audio from this Talkgroup ID will be sent on this stream. Set to 0 to stream all recorded talkgroups. |
-| sendTGID | | false | **true** / **false** | When set to true, the TGID will be prepended in long integer format (4 bytes, little endian) to the audio data each time a packet is sent. |
-| shortName | | | string | shortName of the System that audio should be streamed for. This should match the shortName of a system that is defined in the main section of the config file. When omitted, all Systems will be streamed to the address and port configured. If TGIDs from Systems overlap, each system must be sent to a different port to prevent interleaved audio for talkgroups from different Systems with the same TGID. |
-| useTCP | | false | **true** / **false** | When set to true, TCP will be used instead of UDP. |
+More details: [Broadcastify Calls Plugin](./plugins/broadcastify-calls.md)
-###### Plugin Object Example #1:
-This example will stream audio from talkgroup 58914 on system "CountyTrunked" to the local machine on UDP port 9123.
-```yaml
- {
- "name":"simplestream",
- "library":"libsimplestream.so",
- "streams":[{
- "TGID":58914,
- "address":"127.0.0.1",
- "port":9123,
- "sendTGID":false,
- "shortName":"CountyTrunked"}
- }
-```
+### simplestream
-###### Plugin Object Example #2:
-This example will stream audio from talkgroup 58914 from System CountyTrunked to the local machine on UDP port 9123 and stream audio from talkgroup 58916 from System "StateTrunked" to the local machine on UDP port 9124.
-```yaml
- {
- "name":"simplestream",
- "library":"libsimplestream.so",
- "streams":[{
- "TGID":58914,
- "address":"127.0.0.1",
- "port":9123,
- "sendTGID":false,
- "shortName":"CountyTrunked"},
- {"TGID":58916,
- "address":"127.0.0.1",
- "port":9124,
- "sendTGID":false,
- "shortName":"StateTrunked"}
- ]}
- }
-```
+**Plugin name:** `simplestream`
+**Library:** `libsimplestream.so`
-###### Plugin Object Example #3:
-This example will stream audio from talkgroups 58914 and 58916 from all Systems to the local machine on the same UDP port 9123. It will prepend the TGID to the audio data in each UDP packet so that the receiving program can differentiate the two audio streams (the receiver may decide to only play one depending on priority, mix the two streams, play one left and one right, etc.)
-```yaml
- {
- "name":"simplestream",
- "library":"libsimplestream.so",
- "streams":[{
- "TGID":58914,
- "address":"127.0.0.1",
- "port":9123,
- "sendTGID":true},
- {"TGID":58916,
- "address":"127.0.0.1",
- "port":9123,
- "sendTGID":true}
- ]}
- }
-```
-###### Plugin Object Example #4:
-This example will stream audio from all talkgroups being recorded on System CountyTrunked to the local machine on UDP port 9123. It will prepend the TGID to the audio data in each UDP packet so that the receiving program can decide which ones to play or otherwise handle)
-```yaml
- {
- "name":"simplestream",
- "library":"libsimplestream.so",
- "streams":[{
- "TGID":0,
- "address":"127.0.0.1",
- "port":9123,
- "sendTGID":true,
- "shortName":"CountyTrunked"}
- }
-```
-##### Example - Sending Audio to pulseaudio
-pulseaudio is the default sound system on many Linux computers, including the Raspberry Pi. If configured to do so, pulseaudio can accept raw audio via TCP connection using the module-simple-protocol-tcp module. Each TCP connection will show up as a different "application" in the pavucontrol volume mixer.
+Streams uncompressed PCM audio over UDP or TCP in real time while Trunk Recorder is recording.
-An example command to set up pulseaudio to receive 8 kHz digital audio from simplestream on TCP port 9125 (for 16 kHz analog audio, use `rate=16000`):
-```
-pacmd load-module module-simple-protocol-tcp sink=1 playback=true port=9125 format=s16le rate=8000 channels=1
-```
-The matching simplestream config to send audio from talkgroup 58918 to TCP port 9125 would then be something like this:
-```yaml
- {
- "name":"simplestream",
- "library":"libsimplestream.so",
- "streams":[{
- "TGID":58918,
- "address":"127.0.0.1",
- "port":9125,
- "sendTGID":false,
- "shortName":"CountyTrunked",
- "useTCP":true}
- }
-```
+More details: [simplestream Plugin](./plugins/simplestream.md)
+
+### stat_socket
+
+**Plugin name:** `stat_socket`
+**Library:** `libstat_socket.so`
+
+Provides live status-style output and access to internal Trunk Recorder information useful for live updates or offline analysis.
+
+More details: [stat_socket Plugin](./plugins/stat-socket.md)
+
+### unit_script
+
+**Plugin name:** `unit_script`
+**Library:** `libunit_script.so`
+
+Handles unit event scripting. This plugin is associated with the `unitScript` system setting, but that setting alone does not load the plugin.
+
+More details: [unit_script Plugin](./plugins/unit-script.md)
## Community Plugins
-Community plugins can extend the features of Trunk Recorder and allow customized workflows or analysis.
-> As new plugins are developed, authors are encouraged to add to the below tables by submitting a PR to this document.
-#### External Plugins
+Community plugins can extend the features of Trunk Recorder and allow customized workflows or analysis.
+
+> As new plugins are developed, authors are encouraged to add to the tables below by submitting a PR to this document.
+
Plugins that are built out-of-tree and installed separately from Trunk Recorder:
-| Plugin Name / Link | Description |
-| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
-| [MQTT Status](https://github.com/TrunkRecorder/trunk-recorder-mqtt-status) | Publishes the current status of a Trunk Recorder instance over MQTT |
-| [MQTT Statistics](https://github.com/TrunkRecorder/trunk-recorder-mqtt-statistics) | Publishes statistics about a Trunk Recorder instance over MQTT |
-| [Decode rates logger](https://github.com/rosecitytransit/trunk-recorder-decode-rate) | Logs trunking control channel decode rates to a CSV file, and includes a PHP file that outputs an SVG graph |
-| [Daily call log and live Web page](https://github.com/rosecitytransit/trunk-recorder-daily-log) | Creates a daily log of calls (instead of just individual JSON files) and includes an updating PHP Web page w/audio player |
-| [Prometheus exporter](https://github.com/USA-RedDragon/trunk-recorder-prometheus) | Publishes statistics to a metrics endpoint via HTTP |
-
-#### user_plugins
+
+| Plugin Name / Link | Description |
+| --- | --- |
+| [MQTT Status](https://github.com/TrunkRecorder/trunk-recorder-mqtt-status) | Publishes the current status of a Trunk Recorder instance over MQTT |
+| [MQTT Statistics](https://github.com/TrunkRecorder/trunk-recorder-mqtt-statistics) | Publishes statistics about a Trunk Recorder instance over MQTT |
+| [Decode rates logger](https://github.com/rosecitytransit/trunk-recorder-decode-rate) | Logs trunking control channel decode rates to a CSV file and includes a PHP file that outputs an SVG graph |
+| [Daily call log and live Web page](https://github.com/rosecitytransit/trunk-recorder-daily-log) | Creates a daily log of calls and includes an updating PHP Web page with audio player |
+| [Prometheus exporter](https://github.com/USA-RedDragon/trunk-recorder-prometheus) | Publishes statistics to a metrics endpoint via HTTP |
+
+### user_plugins
+
Plugins that are ready to clone into `/user_plugins` for automatic building and installation:
-| Plugin Name / Link | Description |
-| ---------------------------------------------------------------------- | ----------------------------------- |
+
+| Plugin Name / Link | Description |
+| --- | --- |
| [Placeholder](https://github.com/tr_plugin_developer/my-tr-plugin.git) | Not a real plugin, but it could be. |
## Automatic Building and Development
-As an alternative to developing out-of-tree, or within `/plugins`, user plugins may be staged into a subdirectory of `/user_plugins` to automatically compile as if they were a built-in plugin. Minor changes may be required for exsting user plugins to benefit from automatic building, but this should simplify the process of development and installation, and ensure plugins remain up-to-date with any changes to Trunk Recorder.
-#### Example
-1. Clone the plugin repository
+As an alternative to developing out-of-tree, or within `/plugins`, user plugins may be staged into a subdirectory of `/user_plugins` to automatically compile as if they were a built-in plugin. Minor changes may be required for existing user plugins to benefit from automatic building, but this can simplify development and installation and help keep plugins up to date with changes to Trunk Recorder.
+
+### Example
+
+1. Clone the plugin repository:
+
```bash
cd /user_plugins
git clone https://github.com/tr_plugin_developer/my-tr-plugin.git
```
- or add it as a submodule.
+
+Or add it as a submodule:
+
```bash
cd /user_plugins
git submodule add https://github.com/tr_plugin_developer/my-tr-plugin.git
@@ -249,22 +153,28 @@ git submodule add https://github.com/tr_plugin_developer/my-tr-plugin.git
2. Review plugin requirements, and ensure all dependencies have been met.
-3. Return to your 'build' directory, and resume from the `cmake` step: e.g.
+3. Return to your build directory and resume from the `cmake` step:
+
```bash
cd trunk-build
cmake ../trunk-recorder
```
- Note that the plugin is displayed near the end of the cmake output:
-```
+
+Near the end of the `cmake` output, the plugin should be listed:
+
+```text
-- Added user plugin: my-tr-plugin
```
4. Continue to build and install Trunk Recorder with included plugins:
+
```bash
make
sudo make install
```
- Return to the `cmake` step as you add or remove user plugins.
+
+Return to the `cmake` step as you add or remove user plugins.
### Development Quick-Start
-Any of the built-in plugins in `/plugins` can be directly copied to `/user_plugins` as a template for development. The `rdio_scanner` plugin is a good example of a curl-based uploader, and `stat_socket` shows how many of the internal Trunk Recorder methods can be accessed for live updates or offline analysis. Ensure that instances of the previous plugin name are changed in `CMakeFile.txt` to avoid any conflicts with built-in plugins.
+
+Any of the built-in plugins in `/plugins` can be directly copied to `/user_plugins` as a template for development. The `rdio_scanner` plugin is a good example of a curl-based uploader, and `stat_socket` shows how many internal Trunk Recorder methods can be accessed for live updates or offline analysis. Ensure that instances of the previous plugin name are changed in `CMakeFile.txt` to avoid conflicts with built-in plugins.
\ No newline at end of file
diff --git a/docs/plugins/broadcastify-calls.md b/docs/plugins/broadcastify-calls.md
new file mode 100644
index 000000000..290190184
--- /dev/null
+++ b/docs/plugins/broadcastify-calls.md
@@ -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
+ }
+ ]
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/docs/plugins/openmhz.md b/docs/plugins/openmhz.md
new file mode 100644
index 000000000..aa9314b67
--- /dev/null
+++ b/docs/plugins/openmhz.md
@@ -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 `//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"
+ }
+ ]
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/docs/plugins/rdio-scanner.md b/docs/plugins/rdio-scanner.md
new file mode 100644
index 000000000..343403c86
--- /dev/null
+++ b/docs/plugins/rdio-scanner.md
@@ -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
+ }
+ ]
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/docs/plugins/simplestream.md b/docs/plugins/simplestream.md
new file mode 100644
index 000000000..3488538ae
--- /dev/null
+++ b/docs/plugins/simplestream.md
@@ -0,0 +1,437 @@
+---
+sidebar_label: 'simplestream'
+---
+
+# simplestream Plugin
+
+**Plugin name:** `simplestream`
+**Library:** `libsimplestream.so`
+
+The `simplestream` plugin streams live raw PCM audio over UDP or TCP while Trunk Recorder is actively recording a call. It can also prepend metadata to the stream and optionally emit JSON `call_start` and `call_end` events.
+
+## Build Requirement
+
+This plugin is **not built by default**.
+
+You must enable it in `trunk-recorder/CMakeLists.txt` before building Trunk Recorder.
+
+### Example
+
+```bash
+sed -i 's/```[[:space:]]*#\s*add_subdirectory(plugins\/simplestream)/add_subdirectory(plugins\/simplestream)/' \
+ trunk-recorder/CMakeLists.txt && \
+mkdir -p trunk-recorder/build
+```
+
+After enabling it, rebuild Trunk Recorder as usual.
+
+## Runtime Requirement
+
+`simplestream` receives live audio through the plugin audio callback path. For that to happen, the global `audioStreaming` setting must be enabled in `config.json`.
+
+If `audioStreaming` is not enabled, the plugin can still load, but it will not receive live audio sample callbacks.
+
+### Minimal Runtime Example
+
+```json
+{
+ "audioStreaming": true,
+ "plugins": [
+ {
+ "name": "Live Audio",
+ "library": "libsimplestream.so",
+ "streams": [
+ {
+ "TGID": 58914,
+ "address": "127.0.0.1",
+ "port": 9123
+ }
+ ]
+ }
+ ]
+}
+```
+
+## Plugin Object
+
+| Key | Required | Default Value | Type | Description |
+| --- | :---: | --- | --- | --- |
+| name | ✓ | `simplestream` | string | Friendly name for this plugin instance. This is especially useful when running multiple differently configured instances. |
+| library | ✓ | | string | Must be `libsimplestream.so`. |
+| enabled | | `true` | **true** / **false** | Whether this plugin instance should be loaded. |
+| streams | ✓ | | array | Array of stream objects. Each stream defines where audio or metadata should be sent and what traffic it should match. |
+
+## Stream Object
+
+Each object in `streams` defines one output target.
+
+| Key | Required | Default Value | Type | Description |
+| --- | :---: | --- | --- | --- |
+| TGID | ✓ | | number | Talkgroup to match. Set to `0` to stream all talkgroups. Patched talkgroups are also considered when matching. |
+| address | ✓ | | string | Destination IP address. Used for both UDP and TCP. |
+| port | ✓ | | number | Destination port. Used for both UDP and TCP. |
+| shortName | | `""` | string | Optional system filter. If empty, all systems match. If set, it must match the call's `shortName`. |
+| sendJSON | | `false` | **true** / **false** | When `true`, prepends JSON metadata to each live audio packet. JSON audio metadata includes `src`, `src_tag`, `talkgroup`, `patched_talkgroups`, `freq`, `short_name`, `audio_sample_rate`, and `event`. If this is enabled, it should be preferred over `sendTGID`. |
+| sendCallStart | | `false` | **true** / **false** | Only meaningful when `sendJSON` is enabled. Sends a JSON `call_start` event at the beginning of matching calls. |
+| sendCallEnd | | `false` | **true** / **false** | Only meaningful when `sendJSON` is enabled. Sends a JSON `call_end` event at the end of matching calls. |
+| sendTGID | | `false` | **true** / **false** | Prepends the matched TGID as 4 bytes before each audio payload. This is effectively a legacy metadata mode. If `sendJSON` is enabled, downstream consumers should use the JSON metadata instead. |
+| useTCP | | `false` | **true** / **false** | When `true`, uses TCP instead of UDP for this stream. TCP sockets are connected during plugin startup and closed during plugin shutdown. |
+
+## Audio Format
+
+The plugin sends raw signed 16-bit mono PCM samples from Trunk Recorder's audio callback. Audio is transmitted exactly as provided to the plugin.
+
+Sample rate depends on the recorder providing the audio:
+
+- digital audio is typically `8000`
+- analog audio is typically `16000`
+
+When `sendJSON` is enabled, the plugin includes `audio_sample_rate` in the JSON metadata for each audio packet.
+
+## Packet Format
+
+### Raw Audio Only
+
+When both `sendJSON` and `sendTGID` are `false`, the packet contains only raw PCM audio.
+
+### TGID + Audio
+
+When `sendTGID` is `true`, the packet contains:
+
+1. a 4-byte TGID
+2. the raw PCM audio
+
+### JSON + Audio
+
+When `sendJSON` is `true`, the packet contains:
+
+1. a 4-byte JSON length
+2. the JSON payload
+3. the raw PCM audio
+
+## JSON Event Payloads
+
+### Audio Packet Metadata
+
+When `sendJSON` is enabled, each live audio packet includes a JSON object with:
+
+- `src`
+- `src_tag`
+- `talkgroup`
+- `patched_talkgroups`
+- `freq`
+- `short_name`
+- `audio_sample_rate`
+- `event` = `"audio"`
+
+### call_start Metadata
+
+When both `sendJSON` and `sendCallStart` are enabled, the plugin sends a `call_start` JSON message containing:
+
+- `src`
+- `src_tag`
+- `talkgroup`
+- `talkgroup_tag`
+- `patched_talkgroups`
+- `patched_talkgroup_tags`
+- `freq`
+- `short_name`
+- `event` = `"call_start"`
+
+### call_end Metadata
+
+When both `sendJSON` and `sendCallEnd` are enabled, the plugin sends a `call_end` JSON message containing:
+
+- `talkgroup`
+- `patched_talkgroups`
+- `freq`
+- `short_name`
+- `event` = `"call_end"`
+
+## Matching Behavior
+
+A stream is used when all applicable conditions match:
+
+- `shortName` matches the call's system short name, or `shortName` is empty
+- `TGID` matches the call's talkgroup or one of its patched talkgroups
+- or `TGID` is `0`, which means match everything
+
+If a call source is not immediately available during audio or `call_start` processing, the plugin attempts to use the source from the most recent transmission for that call.
+
+## Transport Behavior
+
+- UDP streams use a shared UDP socket.
+- TCP streams create and connect a socket for each configured stream during plugin startup.
+- TCP sockets are shut down and closed during plugin stop.
+
+## Examples
+
+### Minimal UDP Example
+
+```json
+{
+ "audioStreaming": true,
+ "plugins": [
+ {
+ "name": "County Live Audio",
+ "library": "libsimplestream.so",
+ "streams": [
+ {
+ "TGID": 58914,
+ "address": "127.0.0.1",
+ "port": 9123,
+ "shortName": "CountyTrunked"
+ }
+ ]
+ }
+ ]
+}
+```
+
+### Example with JSON Audio Metadata
+
+```json
+{
+ "audioStreaming": true,
+ "plugins": [
+ {
+ "name": "County JSON Audio",
+ "library": "libsimplestream.so",
+ "streams": [
+ {
+ "TGID": 58914,
+ "address": "127.0.0.1",
+ "port": 9123,
+ "shortName": "CountyTrunked",
+ "sendJSON": true
+ }
+ ]
+ }
+ ]
+}
+```
+
+### Example with call_start and call_end Events
+
+```json
+{
+ "audioStreaming": true,
+ "plugins": [
+ {
+ "name": "County Event Stream",
+ "library": "libsimplestream.so",
+ "streams": [
+ {
+ "TGID": 0,
+ "address": "127.0.0.1",
+ "port": 9123,
+ "shortName": "CountyTrunked",
+ "sendJSON": true,
+ "sendCallStart": true,
+ "sendCallEnd": true
+ }
+ ]
+ }
+ ]
+}
+```
+
+### Example Using TCP
+
+```json
+{
+ "audioStreaming": true,
+ "plugins": [
+ {
+ "name": "County TCP Stream",
+ "library": "libsimplestream.so",
+ "streams": [
+ {
+ "TGID": 58918,
+ "address": "127.0.0.1",
+ "port": 9125,
+ "shortName": "CountyTrunked",
+ "useTCP": true
+ }
+ ]
+ }
+ ]
+}
+```
+
+## PulseAudio Examples
+
+PulseAudio can receive raw PCM audio from `simplestream` over TCP using `module-simple-protocol-tcp`.
+
+This works well when you want Trunk Recorder to send live audio directly into a local or remote Linux audio stack.
+
+### Important Notes
+
+- These examples require `useTCP: true` in the `simplestream` stream object.
+- These examples also require `audioStreaming: true` in the main Trunk Recorder config.
+- When sending directly to PulseAudio, `sendJSON` and `sendTGID` should both be `false`, because PulseAudio expects raw PCM audio only.
+- The PulseAudio sample rate must match the audio being sent:
+ - digital audio: `8000`
+ - analog audio: `16000`
+
+### Example 1: Digital Audio to PulseAudio over TCP
+
+This example sets up PulseAudio to receive 8 kHz mono signed 16-bit PCM on TCP port `9125`.
+
+```bash
+pacmd load-module module-simple-protocol-tcp sink=1 playback=true port=9125 format=s16le rate=8000 channels=1
+```
+
+Matching `simplestream` config:
+
+```json
+{
+ "audioStreaming": true,
+ "plugins": [
+ {
+ "name": "County Digital PulseAudio",
+ "library": "libsimplestream.so",
+ "streams": [
+ {
+ "TGID": 58918,
+ "address": "127.0.0.1",
+ "port": 9125,
+ "shortName": "CountyTrunked",
+ "useTCP": true,
+ "sendJSON": false,
+ "sendTGID": false
+ }
+ ]
+ }
+ ]
+}
+```
+
+### Example 2: Analog Audio to PulseAudio over TCP
+
+This example is the same idea, but for analog audio at 16 kHz.
+
+```bash
+pacmd load-module module-simple-protocol-tcp sink=1 playback=true port=9126 format=s16le rate=16000 channels=1
+```
+
+Matching `simplestream` config:
+
+```json
+{
+ "audioStreaming": true,
+ "plugins": [
+ {
+ "name": "County Analog PulseAudio",
+ "library": "libsimplestream.so",
+ "streams": [
+ {
+ "TGID": 1541,
+ "address": "127.0.0.1",
+ "port": 9126,
+ "shortName": "CountyAnalog",
+ "useTCP": true,
+ "sendJSON": false,
+ "sendTGID": false
+ }
+ ]
+ }
+ ]
+}
+```
+
+### Example 3: Separate PulseAudio Streams for Different Talkgroups
+
+If you want separate PulseAudio applications for different talkgroups, use a different TCP port for each one and load PulseAudio once per port.
+
+PulseAudio:
+
+```bash
+pacmd load-module module-simple-protocol-tcp sink=1 playback=true port=9127 format=s16le rate=8000 channels=1
+pacmd load-module module-simple-protocol-tcp sink=1 playback=true port=9128 format=s16le rate=8000 channels=1
+```
+
+Matching `simplestream` config:
+
+```json
+{
+ "audioStreaming": true,
+ "plugins": [
+ {
+ "name": "County Multi PulseAudio",
+ "library": "libsimplestream.so",
+ "streams": [
+ {
+ "TGID": 58914,
+ "address": "127.0.0.1",
+ "port": 9127,
+ "shortName": "CountyTrunked",
+ "useTCP": true,
+ "sendJSON": false,
+ "sendTGID": false
+ },
+ {
+ "TGID": 58916,
+ "address": "127.0.0.1",
+ "port": 9128,
+ "shortName": "CountyTrunked",
+ "useTCP": true,
+ "sendJSON": false,
+ "sendTGID": false
+ }
+ ]
+ }
+ ]
+}
+```
+
+Each TCP connection should appear as a separate application in PulseAudio.
+
+### Example 4: Stream All Talkgroups from One System to PulseAudio
+
+You can stream all recorded talkgroups from one system by setting `TGID` to `0`.
+
+```bash
+pacmd load-module module-simple-protocol-tcp sink=1 playback=true port=9129 format=s16le rate=8000 channels=1
+```
+
+Matching `simplestream` config:
+
+```json
+{
+ "audioStreaming": true,
+ "plugins": [
+ {
+ "name": "County All TGIDs PulseAudio",
+ "library": "libsimplestream.so",
+ "streams": [
+ {
+ "TGID": 0,
+ "address": "127.0.0.1",
+ "port": 9129,
+ "shortName": "CountyTrunked",
+ "useTCP": true,
+ "sendJSON": false,
+ "sendTGID": false
+ }
+ ]
+ }
+ ]
+}
+```
+
+This is simple, but audio from different calls will all land in the same PulseAudio stream.
+
+## When to Use JSON Instead
+
+If you want downstream software to know the talkgroup, source, patched talkgroups, or call event type, use `sendJSON: true`.
+
+JSON-enabled streams are **not** suitable for direct input into PulseAudio, because PulseAudio expects raw PCM only. JSON mode is better for custom receivers or other software that can parse the metadata before handling the audio.
+
+## Example: Sending Audio to FFmpeg
+
+FFmpeg can also receive raw PCM audio from `simplestream` and transcode it. This only works when `sendJSON` and `sendTGID` are both `false`, because FFmpeg expects raw PCM audio only on the input stream.
+
+```bash
+ffmpeg -loglevel warning -f s16le -ar 16000 -ac 1 -i udp://localhost:9125 -af:a adeclick -f:a ogg -c:a libopus -frame_duration:a 20 -vbr:a on -b:a 48000 -application:a voip pipe:1
+```
\ No newline at end of file
diff --git a/docs/plugins/stat-socket.md b/docs/plugins/stat-socket.md
new file mode 100644
index 000000000..59ea504eb
--- /dev/null
+++ b/docs/plugins/stat-socket.md
@@ -0,0 +1,124 @@
+---
+sidebar_label: 'stat_socket'
+---
+
+# stat_socket Plugin
+
+**Plugin name:** `stat_socket`
+**Library:** `libstat_socket.so`
+
+The `stat_socket` plugin connects to a WebSocket server and streams live Trunk Recorder status and telemetry data. It sends configuration information, system status, recorder status, active calls, system rate updates, and optionally decoded signaling events.
+
+## Plugin Object
+
+| Key | Required | Default Value | Type | Description |
+| --- | :---: | --- | --- | --- |
+| name | ✓ | `stat_socket` | string | Friendly name for this plugin instance. |
+| library | ✓ | | string | Must be `libstat_socket.so`. |
+| enabled | | `true` | **true** / **false** | Whether this plugin instance should be loaded. |
+
+## Plugin-Specific Settings
+
+This plugin currently does **not** define any plugin-specific settings in its `parse_config()` method. Its behavior depends on the main Trunk Recorder configuration instead.
+
+## Main Config Settings Used
+
+The plugin uses the following main configuration values:
+
+| Key | Required | Description |
+| --- | :---: | --- |
+| `statusServer` | ✓ | WebSocket server URL to connect to. If this is empty, the plugin does nothing. |
+| `instanceId` | | Included in outbound status messages. |
+| `instanceKey` | | Included in outbound status messages. |
+| `broadcastSignals` | | When enabled, decoded signaling events are sent over the WebSocket connection. |
+
+## What the Plugin Sends
+
+When connected, the plugin can send several message types over the WebSocket connection.
+
+### Config Message
+
+When the WebSocket connection opens, the plugin sends a `config` message containing:
+
+- source configuration and source recorder counts
+- system configuration and channel/control-channel lists
+- instance-level settings such as `captureDir`, `uploadServer`, `callTimeout`, `instanceId`, and `instanceKey`
+- optional `broadcast_signals` when enabled
+
+### System Messages
+
+The plugin sends:
+
+- `systems` messages containing current system stats
+- `system` messages when an individual system is set up or updated
+- `rates` messages during `system_rates()` updates
+
+### Recorder Messages
+
+The plugin sends:
+
+- `recorders` messages containing recorder stats
+- `recorder` messages when an individual recorder is set up or updated
+
+### Call Messages
+
+The plugin sends:
+
+- `calls_active` messages with active call stats
+- `call_start` messages when a call begins
+
+The current `call_end()` implementation does not send a `call_end` message. It returns immediately when called.
+
+### Signaling Messages
+
+When `broadcastSignals` is enabled, decoded signaling events can be sent as `signaling` messages. These may include:
+
+- `unit_id`
+- attached `call` stats
+- attached `recorder` stats
+- attached `system` stats
+
+## Connection Behavior
+
+- The plugin connects to the configured `statusServer` when it starts.
+- It uses WebSocket++ with the non-TLS ASIO client configuration.
+- If the connection closes or fails, it schedules a reconnect attempt after a delay based on the retry count plus a small random offset.
+- Reconnect attempts are handled during `poll_one()`.
+- The plugin tracks whether config has already been sent for the current connection and resends it after reconnect.
+
+## Requirements
+
+This plugin requires a valid WebSocket server URL in the main `statusServer` setting.
+
+### Example
+
+```json
+{
+ "statusServer": "ws://127.0.0.1:8080",
+ "instanceId": "my-instance",
+ "instanceKey": "my-secret",
+ "broadcastSignals": true,
+ "plugins": [
+ {
+ "name": "Status Socket",
+ "library": "libstat_socket.so"
+ }
+ ]
+}
+```
+
+If `statusServer` is empty, the plugin loads but does not open a connection.
+
+## Minimal Example
+
+```json
+{
+ "statusServer": "ws://127.0.0.1:8080",
+ "plugins": [
+ {
+ "name": "Status Socket",
+ "library": "libstat_socket.so"
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/docs/plugins/unit-script.md b/docs/plugins/unit-script.md
new file mode 100644
index 000000000..52a9482c0
--- /dev/null
+++ b/docs/plugins/unit-script.md
@@ -0,0 +1,208 @@
+---
+sidebar_label: 'unit_script'
+---
+
+# unit_script Plugin
+
+**Plugin name:** `unit_script`
+**Library:** `libunit_script.so`
+
+The `unit_script` plugin runs an external script when certain radio and call events occur. Scripts are configured per system using that system's `unitScript` value, and the plugin looks up the script to run by matching the call or event against the system's `shortName`.
+
+## Plugin Object
+
+| Key | Required | Default Value | Type | Description |
+| --- | :---: | --- | --- | --- |
+| name | ✓ | `unit_script` | string | Friendly name for this plugin instance. |
+| library | ✓ | | string | Must be `libunit_script.so`. |
+| enabled | | `true` | **true** / **false** | Whether this plugin instance should be loaded. |
+| systems | ✓ | | array | Array of system objects. The plugin reads `shortName` and `unitScript` from each system object and builds its per-system script mapping from them. |
+
+## System Settings Used
+
+This plugin does not define its own standalone per-plugin system object. Instead, it reads the following values from each object in the plugin's `systems` array:
+
+| Key | Required | Type | Description |
+| --- | :---: | --- | --- |
+| shortName | ✓ | string | Must match the Trunk Recorder system short name for the events you want this script to handle. |
+| unitScript | ✓ | string | Path to the script or command that should be run for that system. If this is empty, the plugin does nothing for that system. |
+
+## How It Works
+
+During configuration parsing, the plugin reads each system entry, extracts `shortName` and `unitScript`, and stores only the systems that have a non-empty `unitScript`.
+
+When an event occurs, the plugin:
+
+1. finds the configured script for the matching system `shortName`
+2. builds a shell command with event-specific arguments
+3. runs it asynchronously using `system("... &")`
+
+If no script is configured for the matching system, or the source radio ID is `0`, the event is ignored.
+
+## Events and Arguments
+
+The plugin runs the script with positional arguments. The first three are always:
+
+1. system short name
+2. source radio ID
+3. action
+
+Additional arguments depend on the event type.
+
+### Radio Registration
+
+Action: `on`
+
+```text
+