From 1445ff9a8a2c3e9da2e4335c53cba306b96529e2 Mon Sep 17 00:00:00 2001 From: Luke Berndt Date: Sun, 25 Oct 2026 16:51:42 -0400 Subject: [PATCH] Template: trunk-recorder-plugin 0.7.0 - Cargo.toml and the lock on SDK 0.7.0 (builds and tests against the crates.io release). - protocol.md: the tone_pattern topic, the manifest's page / contribute / live_settings, the action / settings messages and action.result, the newer schema hints (linked to the recorder's guide), a current hello. - events.md: tone_pattern, the names in events (unit tag / alpha_tag, a call's sources), multi-site copies and transmission_id. - how-plugins-work.md: Reply and Plugin::settings; settings.md: the newer schema hints. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- docs/events.md | 40 +++++++++++++++++++++++++++++++++++----- docs/how-plugins-work.md | 5 +++++ docs/protocol.md | 39 +++++++++++++++++++++++++++++++++++---- docs/settings.md | 6 ++++++ 6 files changed, 84 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index babe5aa..46b7270 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,9 +183,9 @@ dependencies = [ [[package]] name = "trunk-recorder-plugin" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a7504b42cca72ec6457beb7ac279f3a4fbe16054f86be57eb1e28a70dd6394d" +checksum = "19fa21d5e7cfb4db40a6bbbb616752cd6e45d6b943068ac3502e550e300e8e65" dependencies = [ "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index d1f48a8..b336a20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ authors = ["Your Name "] publish = false [dependencies] -trunk-recorder-plugin = "0.5.0" +trunk-recorder-plugin = "0.7.0" # The settings form is drawn from your config structs' JSON Schema. Keep the # major version the SDK uses (1). schemars = "1" diff --git a/docs/events.md b/docs/events.md index 510d889..cb97932 100644 --- a/docs/events.md +++ b/docs/events.md @@ -12,6 +12,7 @@ you don't use costs everyone something. | `unit` | `unit` | A radio registers, affiliates, … | Busy systems: many per second | | `audio` | `audio` | Live audio of recording calls | About 50 per second per call | | `status` | `status` | Every system's state | Every 5 seconds | +| `tone_pattern` | `tone_pattern` | A call's tones made a pattern: a page, an alert tone, a DTMF or KNOX string, a Morse ID | As heard | The examples below are the JSON on the wire. The Rust SDK hands you the same data as structs; the field names match. Times are Unix seconds. Frequencies @@ -65,10 +66,13 @@ Only recorded calls conclude. Calls the recorder skipped (encrypted, no free recorder, an unknown talkgroup when those aren't recorded) have no files. A system can have several sites, each a control channel the recorder follows. -A call heard on more than one is one call, saved once from the site that heard -it best: `short_name` is the system, and **`site`** is the site the call was -kept from (a system of one site: its short name; none for a conventional -channel's call). +A call heard on more than one is recorded on each and the best copy saved: +`short_name` is the system, and **`site`** is the site the call was kept from +(a system of one site: its short name; none for a conventional channel's call). +A copy the saved one doesn't hold all of is saved too (better a duplicate than +a transmission lost), and copies of one transmission share +**`transmission_id`**. A system with `conversationMode` off makes each +transmission a call of its own. ## `call.start` and `call.end` @@ -100,6 +104,8 @@ it was recorded, come later in `call.concluded`. talkgroups aren't recorded), `no_recorder` (all in use) or `no_source` (no radio covers its frequency). More reasons may be added. - **`units`**: radios heard on the call so far. At `call.end`, all of them. + **`sources`** has the same radios with each one's name (`tag`, as the call + JSON's `srcList` has it) and what its transmission said of itself. - **`patched_talkgroups`**: the talkgroups patched with this one so far, its own included, ascending; empty when it isn't patched. @@ -108,9 +114,12 @@ it was recorded, come later in `call.concluded`. A radio did something on the control channel. ```json -{ "type": "unit", "system": 0, "short_name": "county", "site": "county", "kind": "affiliation", "unit": 1116707, "talkgroup": 101, "time": 1790771551.4 } +{ "type": "unit", "system": 0, "short_name": "county", "site": "county", "kind": "affiliation", "unit": 1116707, "tag": "Engine 12", "talkgroup": 101, "alpha_tag": "Fire Dispatch", "time": 1790771551.4 } ``` +`tag` is the radio's name and `alpha_tag` the talkgroup's, looked up as the +event is sent (left out when there's none). + `kind` is one of: | Kind | Meaning | `talkgroup` | @@ -164,6 +173,27 @@ several sites has several, with the same `index` and `short_name` and its own - **`decode_rate`** is control channel messages decoded per second. A healthy P25 control channel gives about 40. +## `tone_pattern` + +A call's in-band tones made a pattern, as the call JSON's `tone_patterns` will +list it. + +```json +{ + "type": "tone_pattern", "short_name": "county", "site": "county", "call_num": 412, + "talkgroup": 101, "alpha_tag": "Fire Dispatch", "src_tag": "Station 1", "time": 1790771552.0, + "pattern": { "pattern": "two_tone", "tones": [849.63, 1292.85], "lengths": [1.0, 3.04], + "format": "Quick Call II", "pos": 0.4, "len": 4.04, "src": 1116707, "origin": "frame", + "text": "two-tone 849.63 → 1292.85 Hz, 1 + 3.04 s (Quick Call II)", "signals": ["FD_PAGE"] } +} +``` + +`pattern` is `two_tone` (a page), `long`, `hi_low` (a warble), `pulsed`, +`digits` (DTMF; `knox: true` for KNOX), `morse`, `sequence` or `beep`. +`signals` names the system's signals it matches. `decrypted` is set when it was +heard in voice decrypted with the system's keys (only plugins allowed decrypted +calls get those). + ## Order and loss Events of one topic arrive in the order they happened. `call.concluded` comes diff --git a/docs/how-plugins-work.md b/docs/how-plugins-work.md index e03cc44..e448652 100644 --- a/docs/how-plugins-work.md +++ b/docs/how-plugins-work.md @@ -118,6 +118,11 @@ cheap to clone and works from any thread. | `host.info(…)`, `warn`, `error`, `debug` | In the recorder's log, prefixed with the plugin's id. `debug` lines are dropped. | | `host.status(State::Warning, "OpenMHz is down; 12 calls queued")` | Next to the plugin in the plugins list, until the next status. | | `host.call_result(path, Outcome::Ok, "", url)` | What became of a call: `Ok`, `Skipped` (deliberately not handled) or `Failed`. | +| `reply.ok("Sent")`, `reply.fail(why)` | The answer to one of its buttons: `Plugin::action(a, reply)` gets a `Reply` to answer with, now or later from another thread. Dropped unanswered, it says the plugin didn't answer. | + +A plugin with `live_settings` in its manifest gets new settings through +`Plugin::settings(setup)` while it runs; returning `Err` keeps the ones it had +and shows why. > **Never print to stdout.** A plugin's stdout carries the protocol, so > `println!` sends garbage to the recorder. The recorder logs a line it can't diff --git a/docs/protocol.md b/docs/protocol.md index 660f3c0..59b4be8 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -46,10 +46,13 @@ JSON object on stdout and exits 0: | `id` | yes | `[a-z0-9][a-z0-9-]*` | | `name`, `version`, `description` | yes | | | `api` | yes | The plugin API version it speaks. The recorder refuses a higher one than it knows. | -| `subscribe` | yes | Topics: `call.start`, `call.end`, `call.concluded`, `unit`, `audio`, `status`. | +| `subscribe` | yes | Topics: `call.start`, `call.end`, `call.concluded`, `unit`, `audio`, `status`, `tone_pattern`. | | `audio_formats` | no | `["m4a"]` to get M4A with `call.concluded`. | | `config`, `system_config` | no | [Settings schemas](#settings-schemas). Leave them out for none. | | `homepage`, `repository`, `authors`, `license` | no | | +| `page` | no | `{ "title": "…" }`: a page of its own in the recorder's navigation, for settings too many for its card | +| `contribute` | no | Buttons elsewhere in the interface: `[{ "on": "signal", "label", "hint", "append", "item" }]` adds `item` to the settings list `append` from each of a system's signals | +| `live_settings` | no | `true`: it takes new settings as it runs (`settings`, below) instead of being restarted | It must finish within 10 seconds. @@ -63,7 +66,7 @@ Every message has a `type`. The first is always `hello`. { "type": "hello", "api": 1, - "host": { "name": "trunk-pro", "version": "0.4.0" }, + "host": { "name": "trunk-pro", "version": "0.2.10" }, "config": { "server": "https://api.openmhz.com" }, "systems": [ { "index": 0, "short_name": "county", "kind": "p25", "config": { "apiKey": "…" } }, @@ -93,8 +96,27 @@ settings, it sends a `status` of `error` saying why, and exits with status ### Events -`call.concluded`, `call.start`, `call.end`, `unit`, `audio` and `status`. Each -message is the event's fields plus `type`. See [Events](events.md). +`call.concluded`, `call.start`, `call.end`, `unit`, `audio`, `status` and +`tone_pattern`. Each message is the event's fields plus `type`. See +[Events](events.md). + +### `action` + +```json +{ "type": "action", "request": 7, "action": "test", "value": { "name": "Discord", "url": "…" } } +``` + +Someone pressed one of the plugin's buttons (`x-actions` in its settings +schema). `value` is the settings object the button is on, as edited (saved or +not). Answer with `action.result` and the same `request` within 15 seconds; +the answer is shown by the button, in the browser that pressed it. + +### `settings` + +The plugin's settings changed while it runs: the `hello` it would start with +now, with `"type": "settings"`. Only a plugin with `live_settings` is sent +one; others are restarted. A plugin that can't take them reports an `error` +status and keeps the ones it had. ### `shutdown` @@ -142,6 +164,14 @@ next status. (not handled, on purpose) or `failed` (given up on). `message` and `url` are optional. +### `action.result` + +```json +{ "type": "action.result", "request": 7, "ok": true, "message": "Sent a test to Discord" } +``` + +The answer to an `action`, with its `request`. + A line that isn't JSON is logged as it is. ## Exit status @@ -176,6 +206,7 @@ objects. The recorder's settings form understands this subset: | `{"type": "array", "items": {"type": "string"}}` (or number) | List | | `{"type": "array", "items": {"type": "object", …}}` | A list of groups, added and removed one by one | | `{"type": "string", "x-system": true}` | Menu of the recorder's systems (short names); follows renames | +| `x-systems`, `x-signals`, `x-ref`, `x-tones`, `x-title`, `x-summary`, `x-when`, `x-actions` | Systems or signals to tick, items of another list, tones typed in, list items titled and summed up, fields shown only when they apply, buttons: see [the recorder's guide](https://github.com/TrunkRecorder/trunk-recorder-pro/blob/main/docs/plugins/writing-plugins.md#settings-and-the-form) | On any field: `title` (the label; the key if there's none), `description` (help text), `default`. diff --git a/docs/settings.md b/docs/settings.md index ffabe10..455a496 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -83,6 +83,12 @@ for programmers, and isn't shown. | `Vec` of a struct | A list of groups, with Add and Remove buttons. The struct's doc comment names each one ("Stream 1", "Add stream"). | | `String` with `#[schemars(extend("x-system" = true))]` | Menu of the recorder's systems, by short name. When a system is renamed, the recorder changes the setting to match. | +More hints for bigger forms (systems or signals to tick, a list naming another +list's items, typed-in tones, list items titled and summed up, fields shown +only when they apply, buttons): `x-systems`, `x-signals`, `x-ref`, `x-tones`, +`x-title`, `x-summary`, `x-when`, `x-actions`, in [the recorder's +guide](https://github.com/TrunkRecorder/trunk-recorder-pro/blob/main/docs/plugins/writing-plugins.md#settings-and-the-form). + Fields appear in the order you declare them. ### Fields that have to be filled in