Skip to content

Integrations

How another resource on your server hears transmissions, transmits as a named station, answers 911 calls, manages patches and acts in your CAD through tRadio, and what you have to allow first.

tRadio has two ways in for your own code. The web API (REST and Socket.IO) is for programs that act like a radio or a dispatch panel, and is described in the API Reference. The integration hooks on this page are for another resource on the same server: a logger, a pager, an announcer, a bot that talks on a channel, anything that needs to hear finished transmissions or speak as a console without carrying a voice codec of its own.

Everything here is a Lua export or a local server event. Nothing is allowed until you say so.

Allow a resource

Name the resource in config/config.lua:

lua
Config.integrations = { 'my-resource' }

Restart tRadio after changing it. The default is an empty list, so no resource can hear, transmit, answer calls, write patches or act in your CAD. The names are resource folder names, case-sensitive.

When a resource that is not listed calls one of the hooks, the call fails and tRadio prints one line to the console, naming the resource and the exact line to add:

text
[audit] integration:refused hook=openStation resource=my-resource — to allow it, add 'my-resource' to Config.integrations in tRadio's config/config.lua (Config.integrations = { 'my-resource' }) and restart tRadio

The same resource and hook is reported once every ten minutes, so a resource that retries does not fill the log.

What the list covers:

HookNeeds the resource to be listed
registerTransmissionTapYes
openStation, stationTransmitYes
answer911, end911Yes
createPatch, removePatchYes
cadOperationYes, and Let integrations write to your CAD switched on
cadRequest (the CAD's own record)Yes, and the same switch
unregisterTransmissionTap, closeStationNo: they only act on what the caller opened
getStations, getPatches, getAlerts, getToneKeys, cadCapabilitiesNo

Let integrations write to your CAD is Settings → Advanced → CAD / MDT in the admin panel. With it off, cadOperation and cadRequest refuse every operation (reads too), and cadCapabilities reports an empty list, so a resource knows to stop offering CAD features.

How the exports behave

  • JSON in, JSON out. The new exports take and return JSON strings. A failure is "", false or { "ok": false, "reason": "…" }, never an error, even if the body throws.
  • A resource can only use what it opened. A tap or station belongs to the resource that created it. Another resource handing over its id is refused, so an id is not a key.
  • Ids are 128-bit random tokens. Match them. An event carrying an id you were not given came from someone else.
  • Results are events and arrive after the export returns, so you can register for the result once you have its id.
  • Everything a resource opened goes when it stops. Taps unregister and stations close.
  • Probes are free. unregisterTransmissionTap, closeStation, end911 and removePatch with an id that does not exist return false, change nothing and print nothing. That is how a resource checks whether a tRadio is new enough, since calling a missing export raises No such export.

Hearing transmissions

registerTransmissionTap(optsJson) → tapId

opts = { "frequencies": [154.755, 460.125] }. Returns a tap id, or "" when refused. A resource holds one tap: registering again replaces it.

  • With a list, the tap hears exactly those frequencies, secure channels included: naming the resource and the frequency is the consent.
  • With no list, the tap hears every configured conventional channel except secure ones, so "everything" never means a channel you encrypted.
  • A resource that has a station answering a 911 call also hears that call's frequency, whatever its list says: a call's frequency is made up when the call is placed and is in no configuration.
  • Dispatch consoles are never heard. Neither the dispatch panel's nor a station's transmissions are delivered.

unregisterTransmissionTap(tapId) → bool

event tradio:tap:transmission(tapId, payload)

Fired once per finished transmission, when the talker lets go of the key (or about a second and a half after their last packet if the release never arrived):

json
{
  "serverId": 7,
  "name": "King 41",
  "frequency": 154.755,
  "startedAt": 1730000000000,
  "endedAt": 1730000002100,
  "codec": "imbe",
  "container": "wav",
  "sampleRate": 16000,
  "audio": "<base64>"
}
  • container: "wav" is a digital transmission, decoded by tRadio to mono 16-bit PCM at 16000 Hz. The rate is part of the contract.
  • container: "webm" is an analog transmission as one Opus WebM file, still encoded (sampleRate is 48000).
  • name is the callsign when callsigns are on, otherwise the player's name.
  • A transmission is cut at about 20 seconds of digital audio or 60 seconds of analog: the start is delivered and the rest dropped.
lua
local tapId = exports['tRadio']:registerTransmissionTap(json.encode({ frequencies = { 154.755 } }))

AddEventHandler('tradio:tap:transmission', function(id, payload)
    if id ~= tapId then return end
    -- payload.audio is base64 of a 16 kHz mono WAV
end)

Transmitting as a station

A station is a named virtual dispatch console. It appears in every unit's list under its name, covers the frequencies you give it, and may transmit on them. Its ids come from a reserved block, -890000 to -890999, so you can tell a console from a person.

openStation(optsJson) → stationId

opts = { "name": "Dispatch", "frequencies": [154.755], "answers911": false }. Returns "" when refused.

  • A name is cut to 24 characters and stripped of control characters.
  • A station covers up to 64 frequencies, and a resource may hold eight stations.
  • Every covered frequency and every answered call uses one console id from the block of 1000. When the block is full, openStation returns "" and answer911 answers no_console, and the console says so; close a station or cover fewer frequencies.
  • answers911 = true makes the station count as a dispatcher for the no-dispatcher 911 fallback, so tRadio does not announce that nobody is picking up a call the station is about to answer. It never counts as a dispatcher anywhere else: a duplex channel and a saved patch that opens when no dispatcher is on behave as if the station were not there.

closeStation(stationId) → bool

stationTransmit(stationId, optsJson) → requestId

json
{ "frequency": 154.755, "container": "pcm16", "sampleRate": 8000, "audio": "<base64>", "maxMs": 20000 }

pcm16 is little-endian mono 16-bit samples, from 4000 to 48000 Hz. Anything other than 8000 Hz is resampled; 8000 Hz is what every digital channel runs at, so it needs no conversion. tRadio does the rest: it encodes with the channel's own codec (an analog channel still gets a vocoded station), pads the encoder's tail so the last word is not left inside it, sends the packets in real time, and shows the talking indicator and the TX log entry like any transmission.

Returns "" for a request it will not take at all: a container other than pcm16, a rate outside the range, an empty or oversized audio field. Otherwise it returns a request id and answers with an event.

  • Twenty seconds at most. maxMs can lower that and never raise it.
  • One voice at a time on a channel. A request waits its turn. A channel holds one airing and two waiting, and the next is refused.
  • There is no cancel. Audio handed over airs to its end; closing the station stops it.

event tradio:station:done(stationId, requestId, result)

json
{ "ok": true, "reason": "aired", "airedMs": 1000 }
reasonMeaning
airedIt went out. airedMs is how long it ran.
queue_fullThe channel already had one airing and two waiting.
too_longMore than 20 seconds, or more than maxMs.
no_stationThe station is unknown, closed, or belongs to another resource.
not_coveredThe station does not cover that frequency.
encode_failedThe codec could not be loaded or the audio could not be encoded.

getStations() → json

json
{
  "stations": [{ "stationId": "…", "name": "Dispatch", "resource": "my-resource", "frequencies": [154.755], "answers911": true }],
  "humans": [{ "serverId": -1001, "selected": 154.755, "listening": [460.125] }]
}

humans is every person signed in to the dispatch panel, with the channel they have selected (or null) and every channel they are listening to; a station is never in it. stationId is included only for the caller's own stations, because the id is what lets a resource transmit as one. Anything that should stand down for a human dispatcher reads this.

911 calls

event tradio:911:queued(call)

Fired for every call that enters the queue, whether or not anything is listening:

json
{ "id": "call911_ab12cd34", "callerServerId": 9, "callerName": "Alex Vance", "message": "break in",
  "frequency": -900001, "coords": { "x": 1.0, "y": 2.0, "z": 3.0 } }

coords is null when unknown. frequency is the call's own line.

answer911(stationId, callId) → json

{ "ok": true } or { "ok": false, "reason": "…" }. tRadio puts the station on the call's frequency first and accepts the call second: a call whose line has nobody on it is swept as abandoned, so the other order loses the call. The station gives the line back when the call ends.

reasonMeaning
caller_offlineThe caller has no connection to tRadio yet. The call stays queued; try again in a moment. A caller who has not powered a radio on needs a few seconds to connect.
no_stationThe station is unknown or belongs to another resource.
unknown_callNo such call.
already_handledSomeone else already answered it.
no_consoleEvery station console id is in use. Close a station or cover fewer frequencies.
not_permittedThe resource is not in Config.integrations.

end911(callId) → bool

Ends the call with the reason integration_ended and fires the usual tradio:911:ended. false for an unknown call.

event tradio:911:ended(callerServerId, reason)

Already existed. reason is one of dispatcher_ended, caller_ended, caller_offline, caller_no_answer, hangup, queue_timeout, integration_ended or system_disabled. Treat an unknown reason as "the call ended".

Alerts, tones and patches

The exports that raise an alert or play a tone (setAlertOnChannel, playToneOnChannel, getChannelAlert) are in the API Reference. These read the names your server defines:

getAlerts() → json

[{ "name": "SIGNAL 100", "toneOnly": false }], from Config.alerts. A toneOnly alert is a sound, not a state a channel is put in and lifted from.

getToneKeys() → json

["ALERT_A", "PRIORITY"]: the tone names this server has, sorted.

getPatches() → json

[{ "id": "patch_1a2b3c4d5e6f", "label": "FIRE-PD BRIDGE", "frequencies": [154.755, 460.125] }]

createPatch(label, frequenciesJson) → json

Success is { "ok": true, "id": "patch_…" }. A label is cut to 40 characters and defaults to the frequencies. A patch made this way is saved and shown on the dispatch panel like one made there. A refusal carries reason and, where it applies, the frequency that caused it:

reasonMeaning
too_fewFewer than two different frequencies.
unknown_channelA frequency that is not a configured channel.
directA direct channel, which cannot be patched.
trunkedA trunked channel, which cannot be patched.
already_patchedThe frequency is in a patch already. label names it.
disabledChannel Patching is switched off in Settings → Audio & Radio.
not_permittedThe resource is not in Config.integrations.

removePatch(id) → bool

CAD

The six CAD integrations stay inside tRadio. They know each system's exports, identities and status names, and they reduce every answer to a few fields, so the record a CAD sends never crosses.

cadCapabilities(provider) → json

provider is a CAD id (lacore, sonoran, imperial, wasabi, tkmdt, psmdt), or "" for the one tRadio is using. Returns:

json
{ "provider": "sonoran", "supports": ["plate", "createCall", "attachUnit"], "consent": true }

provider is "" when that CAD is not running. consent is the Let integrations write to your CAD setting. supports is empty unless the caller is listed in Config.integrations and consent is on. The operation names are plate, bolo, person, warrants, createCall, attachUnit, detachUnit, closeCall, addNote, readStatus, writeStatus and roster. Which a CAD supports differs a great deal; see CAD / MDT.

cadOperation(provider, op, paramsJson, requestId) → bool

Starts one operation and answers with an event. true when started, false when refused: the resource is not listed, consent is off, the operation name is unknown, or the request id is empty or longer than 128 characters. params takes serverId, plate, name, about, ref, closeRef, note and status; other fields are ignored and text is cut to 200 characters. For writeStatus, status is one of the CAD's own status names, which tRadio matches against that CAD's list. It does not interpret ten-codes or phrases. A request missing something it needs answers error without reaching the CAD.

event tradio:cad:operationResult(requestId, result)

json
{ "kind": "ok", "ok": true, "body": { "ref": "4411" } }

kind is ok, transient (try later), quota, absent, error, threw, off or unsupported. body is the reduced record: a plate (plate, owner, stolen, insured, registration, flags), a person (name, matches, flags), a call (ref, closeRef), { "warrants": bool }, a unit (serverId, callsign, status, service) or { "units": [...] }. Exactly one event per request id.

Reading a CAD's own record

cadDetect, cadStatusVocabulary, cadReadUnit, cadRoster and cadLiveCall read what is running and the status names a CAD accepts. cadRequest(provider, op, payload, requestId) runs an operation and answers on tradio:cad:result with the CAD's own record, which is the only thing a caller outside tRadio has to parse itself. Because it is public, it needs the same two things as cadOperation: the resource listed, and consent on.

The web API

The REST and Socket.IO surface is documented in the API Reference and is the other way in. It suits a program outside the game server, or one that must act as a radio or dispatcher session: tuning, keying up, dispatcher actions, tones, patches and 911 from a panel. Voice on it is codec frames (IMBE for digital channels, Opus for analog), so a program that wants to hear or speak needs a matching codec. The hooks above exist so a resource on the server does not have to carry one.

Limits

LimitValue
Taps per resource1
Stations per resource8
Frequencies per station64
Station name24 characters
One transmission from a station20 seconds
Queue on one channel1 airing, 2 waiting
Sample rate accepted4000 to 48000 Hz
Patch label40 characters
CAD text field200 characters

Third-party integrations

Separate projects that use the hooks on this page. They are not part of tRadio, are installed and updated on their own, and do nothing until you list them in Config.integrations.

ProjectWhat it isSource
tRadioAIA free, open-source resource that answers 911 calls and speaks on your channels as a dispatcher consolegithub.com/tommy141x/tRadioAI

To use one, install it as its own resource, start it after tRadio, and add its folder name to the list:

lua
Config.integrations = { 'tRadioAI' }

Restart tRadio after the change. Listing a resource trusts it with everything in Allow a resource: it can hear the transmissions it taps, transmit as a station, answer 911 calls and edit patches. Remove the name and restart tRadio to take all of that away. Questions about a project go to its own repository, not tRadio support.

Next steps

Still stuck? Support can answer it directly.

Was this page helpful?