Sal's Kewl Korner Documentation / Sal's Kewl Dispatch Discord

Sal's Kewl Dispatch — owner's manual

A trunked two-way radio for FiveM with a fire/EMS tone-out pager, 911 call-taking, CAD integration, and a hosted browser dispatch console with two-way voice. This manual covers installation, configuration, daily use and troubleshooting for server owners.

documents resource version 2.9.0

Overview

Sal's Kewl Dispatch is two products that form one communications system:

  • The in-game radio (sals_kewldispatch) — a trunked radio resource for your FiveM server. Talkgroups with codeplug-style labels, job-driven personalities, server-enforced single-transmitter arbitration, PTT IDs on every display, a full-keypad portable on foot and a control head in vehicles, a fire/EMS tone-out pager, 911 call-taking, and an in-game admin configuration window. A one-time purchase that runs entirely on your server, forever.
  • The hosted dispatch console — a browser-based, multi-channel dispatch position with two-way voice, delivered as a hosted service on a subscription. Dispatchers work from any browser; no copy of the game is required.

The two are sold separately — all pricing is on the main site. The radio never depends on the console: with no dispatch subscription, everything in-game works forever on your own server, including 911 (see 911 call-taking).

Eight chassis ship, drawn entirely in CSS from four invented manufacturers — Valcon, Corvex, Nordvik and Bulwark — plus an unbranded compact. Nothing is streamed, and no photograph, texture, logo or model file from any real manufacturer is included or referenced.

Requirements & voice

Works with qb-core, qbx_core, es_extended, or standalone (everyone gets the catch-all personality). Detection is automatic; there is nothing to configure. Optional integrations (inventories, phones, CAD) are detected at runtime — none of them are hard dependencies.

Voice backends

The radio does not ship its own voice transport. Pick one in Config.Voice.Backend (the default 'auto' detects a running backend):

BackendConfig valueNotes
pma-voice'pma'Most common. Carries one talkgroup at a time; scan stops on the active talkgroup like a real single-receiver scanner and returns to your selected talkgroup after a configurable hang time (Config.Voice.ScanFollow). Selected traffic always wins, and keying up always transmits on the selected talkgroup.
Native Mumble'native'Uses FiveM's Mumble natives directly. Hears every scanned talkgroup simultaneously. Do not run alongside pma-voice.
SaltyChat'salty'Routing only; the SaltyChat plugin keeps its own PTT.
None'none'State, UI and arbitration with no player audio. Browser consoles are still audible.

pma-voice users: this resource owns the push-to-talk key and drives pma-voice's +radiotalk / -radiotalk on grant and release. Have players unbind pma-voice's own radio key (Settings → Key Bindings → FiveM) or they will key up twice and bypass arbitration. The radio warns any player whose pma key is still set — once on spawn and again if they press it.

Installation

  1. Drop the folder into your resources/ directory as sals_kewldispatch.
  2. Add ensure sals_kewldispatch to server.cfg.
  3. Grant yourself the admin panel (below).
  4. Open config.lua and set your talkgroups and personalities — or start the server and configure everything live with /radioadmin.

Admin access

Two routes; either one is enough.

Identifier list (simplest). Join the server and run /radioid — it prints your identifiers to the server console and tells you which line to paste:

Config.Admin = {
    Identifiers = {
        'license:1a2b3c4d5e6f7890...',
    },
}

Matching is case-insensitive and the license: prefix is optional. Restart the resource after editing.

ACE (better once you have an admin team). In server.cfg:

add_ace group.admin sals_kewldispatch.admin allow
add_principal identifier.license:1a2b3c4d5e6f7890... group.admin

If /radioadmin does nothing, check the server console: a denial prints the player's actual identifiers and the exact add_ace line to run.

Concepts

TalkgroupA logical channel: a stable id, a display label (what the radio screen shows), a colour, and flags for encryption, emergency eligibility and dispatch monitoring.
ZoneAn ordered bank of talkgroups — one position set of the channel knob.
PersonalityThe codeplug a player is handed, chosen by job. It decides which zones exist, what may be scanned, where emergency goes, and which features are unlocked.
Alias / PTT IDThe unit ID other radios display when you key up. The agency prefix always comes from the personality, so a civilian cannot transmit as LSPD even with a custom suffix.
Scan vs monitorScan is one radio hunting for activity across a permitted list. Monitor is a set of talkgroups heard permanently and simultaneously — what a dispatch console sits on. Both are validated server-side against the codeplug.

Transmit arbitration

One unit holds a talkgroup at a time, enforced in the audio path, not just on the display: the server grants the channel and only the granted unit's voice is routed to it.

  • A denied key-up gets the bonk and TG BUSY <holder> on the display.
  • Config.Tx.MaxSeconds is a repeater-style time-out timer, so a stuck mic cannot hold a talkgroup hostage.
  • Config.Tx.QueueRequests auto-grants to the next waiting unit on release.
  • A unit in emergency preempts whoever is holding the talkgroup.

Emergency

Hold the orange button (or the emergency key) for Config.Emergency.HoldMs. The radio forces itself to the personality's emergency talkgroup, powers on if it was off, alarms on everyone's display, and hands the emergency unit a hot mic. It re-announces until cleared and auto-clears as a safety net. Dispatch consoles can clear it remotely.

Player guide

Keybinds

All rebindable in Settings → Key Bindings. Defaults:

KeyAction
Numpad *Push to talk
F3Show / hide the radio
F4Grab / release the mouse
. (hold)Emergency
Page Up / Page DownChannel up / down
HomeNext zone

Commands

/radioToggle the radio (the radio item's own name works too)
/radioguideInteractive tour that spotlights and explains every control on the chassis actually on screen. Nothing pressed while the guide is open transmits.
/radiokeysKey setup panel (see below)
/radioalias <suffix>Set your personal alias suffix (the agency prefix stays fixed)
/radiomicRe-open FiveM's microphone permission prompt
/radiomovetop-left | top-right | bottom-left | bottom-right | center | reset — reposition the radio from chat if it is ever stranded
/radiounfocusEscape hatch that releases NUI focus if anything strands it
/pagerToggle the pager (its item name works too, e.g. /pspager)
/911, /911textCall — or text — 911 (see 911 call-taking)
/radioid, /radioadmin, /radioreload, /radiodiag, /cadtestAdmin: identifiers, config window, re-issue codeplugs, diagnostics, CAD test

Two states, deliberately separate

F3 shows the radio as a plain HUD — click-through, holding no focus, so the player keeps complete control of their ped. F4 grabs the mouse for clicking, dragging and resizing; the radio picks up a rim light so the state is obvious, and Esc releases it. Position, scale and the keypad's expanded state persist per player, separately for the portable and the mobile head.

The keypad

The portable's keypad is functional: digits enter a unit ID, # sends a directed page to the matching unit, * backspaces. Matching is on the digits inside an alias, so 402 finds LSPD 402-ADAM. The KEYPAD bar collapses it to the short limited-keypad radio, remembered per player.

Instant recall

MENU → RECALL lists the last few transmissions on the selected talkgroup and replays them through the radio speaker. Live traffic outranks the recorder, exactly like the recall button on a real portable. Recordings live in server memory only — nothing is written to disk, and a radio can only replay the talkgroup it is currently tuned to. Configure with Config.Recall; requires the voice bridge.

Key setup modes

Config.Keys.Mode decides who owns the bindings:

'default'Classic FiveM key mappings, changed in the pause menu — which accepts mouse side buttons. /radiokeys is a read-only view (FiveM's binding store cannot be written from script).
'managed'The radio owns the bindings and captures them in /radiokeys: click, press the key, done. Layout-independent, persists across sessions, works on production-mode servers. Keyboard only.

For joystick buttons — or mouse buttons in managed mode — map the button to F13F24 in your peripheral software (G HUB, Synapse, JoyToKey, reWASD, Stream Deck…) and bind that.

Radio & pager items

Config.Access.RequireItem gates the radio behind an inventory item (default on); a personality can override with its own requireItemfalse for issued kit, or a specific item or list. Picking the item up hands you a radio without relogging (re-checked every Config.Access.RecheckSeconds, default 15; /radio re-checks immediately), and Config.Access.RevokeOnLoss decides whether losing it takes the radio back.

Using the item opens the radio, and the item's name works as a command. When somebody cannot use the radio they are told why — the missing item by name, or that their job has no radio assigned. Set Config.Access.ExplainDenials = false for a flat refusal (the right call if the item is contraband).

ox_inventory routes item use through the item definition: point the item's client.event at sals_kewldispatch:client:radioUse (the pager's is sals_kewldispatch:client:pagerUse).

Fire/EMS pager

A two-tone voice pager beside the radio — the device a volunteer firefighter wears off shift. Everything lives under Config.Pager; set Config.Pager.Enabled = false and nothing else changes.

How a page happens

  1. Dispatch opens the console's PAGER TONE-OUT window, checks one or more tone sets (frequency pairs from Config.Pager.ToneSets, or custom pairs when AllowCustomTones is on), checks the target talkgroups, types an announcement, and sends.
  2. The tones sound over the air — every radio and console on those talkgroups hears them.
  3. Every on-duty pager subscribed to a matching tone set (within MatchToleranceHz, like a real reed decoder) alerts, shows the announcement, and opens its speaker on the paged talkgroup for AlertAudioSeconds so the voice announcement that follows is heard. REPLAY on the pager replays it afterwards (requires Config.Recall.Enabled).

Who gets a pager is Config.Pager.Personalities — resolved from the same job names as the radio but a separate table on purpose, so the fire job's radio and pager are configured independently. A pager never transmits. The pager's DUTY switch is pager-local — it silences that pager and never touches the framework's duty flag.

The pspager item

Item definitions belong to your inventory system — add one there (optional; the item gate is off by default). A ready-made 128×128 icon (pspager.png) ships beside fxmanifest.lua — copy it into your inventory's images folder.

qb-coreqb-core/shared/items.lua:

['pspager'] = {
    name = 'pspager', label = 'Public Safety Pager', weight = 220,
    type = 'item', image = 'pspager.png', unique = true, useable = true,
    shouldClose = true, combinable = nil,
    description = 'Two-tone fire/EMS voice pager',
},

ox_inventoryox_inventory/data/items.lua (the client.event line is what makes using it open the pager; if you run ox alongside qb-core, define the item here only):

['pspager'] = {
    label = 'Public Safety Pager',
    weight = 220,
    stack = false,
    close = true,
    description = 'Two-tone fire/EMS voice pager',
    client = { event = 'sals_kewldispatch:client:pagerUse' },
},

Hosted dispatch console

The browser console is a real position on the trunked system — it registers with the same engine a handheld does, competes in the same arbitration, and shows up in everyone's PTT ID. It is delivered as a hosted service: your game server connects out to hosted dispatch, dispatchers open their community's console page over HTTPS in any browser, and each signs in with their own named account (Discord or password) — their own PTT ID, their own audit trail, revocable on their own.

Nothing inbound ever touches your game port — no reverse proxy, no port forward, no certificate to manage. How many positions may be signed on at once comes from your dispatch plan.

Linking your server

Your tenant id and key come with your dispatch account. Put them in server.cfg — pasting a key there turns the link on by itself:

set sals_dispatch_tenant "ten_..."
set sals_dispatch_key    "your-key"

Use set, never setr — a replicated convar is sent to every connecting client, and your key must not be. Treat the key like your rcon password. The resource audits its own posture loudly at startup and refuses the link on a published or sample key.

The position

The console is a grid of talkgroup modules, all live at once, and two speakers: SELECT (the module your PTT goes to, at full level) and UNSELECT (everything else you monitor, mixed under it). Each module has SEL (talk on it), MON (hear it), XMIT (instant transmit without moving your SELECT), its own fader, mute, live PTT ID, listener count and level meter. Click a module's listener count to see exactly who would hear traffic on it right now, tagged with why (SEL / SCAN / MON).

Also on the position: the emergency queue with remote clear, unit roster, text paging, the pager tone-out window, a traffic log, and instant recall with WAV export. Which modules a position lays out comes from its personality; an account's agency can only ever narrow that list, never grant a module the codeplug does not carry.

Voice

The console carries voice in both directions — G.711 at 8 kHz riding the same transport as everything else (about 10 kB/s per talking unit):

DirectionNotes
Console → gamePlayed through each radio's own speaker. Works on every voice backend, including 'none'.
Game → consoleThe player's client bridges their mic alongside game voice. Needs the mic permission (/radiomic re-asks).
Console → consoleSame relay.

The relay refuses any frame from a unit that does not hold the talkgroup grant — the single-transmitter rule governs bridged audio exactly as it governs game audio. Sign-on attempts are throttled per address, sessions are pinned to the address that opened them, and the account credential is re-checked on every request.

The radio never dims. With no link configured, the handhelds, mobiles and everything in-game work forever, entirely on your own server.

911 call-taking

A player dials 911 in game and reaches a dispatcher at the browser console. It behaves like a telephone, not like a radio: two parties, full duplex, no keying, created when somebody dials. A call is never in anybody's codeplug and cannot be reached by tuning to it — the two systems meet in exactly one place, PATCH, where a dispatcher deliberately puts the caller's voice onto a talkgroup.

911 ships off. Point Config.Calls.Services at the personalities that should answer, set Config.Calls.Fallback.Talkgroup, then either set Config.Calls.Enabled = true or put set sals_dispatch_calls "true" in server.cfg.

It works with your phone

Automatic detection for lb-phone, qb-phone, npwd, Roadphone, gksphone, qs-smartphone and yseries. Anything else is described in config — export and event names, no Lua edit (Config.Calls.Phone.Custom). Export names are tried as candidate lists and the one that answered is printed at boot, so a phone that renames an export between versions keeps working. With no phone resource at all, /911 works and numbers are synthesized stably per character.

ANI and ALI

ANI is who is calling — number, mobile or fixed line, subscriber name when the phone can resolve one, and a repeat-caller flag. ALI is where they are: a fixed line (Config.Calls.Landlines) reports its registered address exactly; a mobile draws a position with an uncertainty radius that tightens over the first seconds of the call, the street name withheld until the fix is tight enough, and REBID asks for a fresh fix. Config.Calls.ALI.Phase is 'one', 'two' (default) or 'exact'.

At the console

Three sidebar panels: CALL QUEUE (oldest first, ring timer), ACTIVE CALL (ANI, ALI, hold, transfer, conference, patch, rebid, notes, disposition) and CALL HISTORY, including abandoned calls. The telephone leg has its own fader and is deliberately not coloured like radio traffic; keying the radio PTT mutes the phone leg — one mouth.

Text-to-911

/911text opens a call that carries text instead of audio, with the same queue, location and disposition — for players with no microphone, players who refused the permission prompt, and players who cannot speak right now.

Without a dispatch subscription

911 still works. The call is logged, sals_kewldispatch:call:fallback fires for your own CAD, and Config.Calls.Fallback pages or tones the emergency talkgroup. A player dialling 911 cannot tell the difference from the game side.

A call opens the caller's microphone for the duration of the call — the only place this resource does that — and releases it when the call ends. A caller who refused the permission prompt shows as CALLER HAS NO MICROPHONE on the call card and is told so on their own screen; /radiomic is the way back.

CAD integration

Send 911 calls, radio emergencies and unit positions to the CAD your server already runs, and let the CAD send calls and assignments back. CDE CAD ships as a driver; anything else is either a config block or one event handler in your own resource. Everything is off by default, capability by capability.

The rule the integration obeys: the CAD is an accessory to a call, never a step in one. Nothing in the 911 or radio path waits on your CAD — a CAD that is down has exactly one symptom: the CAD does not have the data.

Setting it up (CDE)

Credentials go in server.cfg — the same convars CDE's own resource uses, so if you already run it there is nothing to do here:

set CDE_CAD_API_URL       https://your-cad.example
set CDE_CAD_API_KEY       fvm_...              # Admin Panel > FiveM Settings
set CDE_CAD_COMMUNITY_ID  000000000000000000   # your Discord guild id
set CDE_CAD_SERVER_NAME   "Your Server"

Then turn on the parts you want in Config.CAD (Push.Calls, Push.Panic, Push.Units, Push.Location, Pull.Dispatch…). /cadtest is the command to run first — it prints the posture, fires one payload of each enabled kind, and the 911 test is a full round trip that cleans up after itself.

If your CAD ships its own phone bridge, Config.CAD.Pull.Calls.Ownership settles who owns 911: 'we-own' (recommended) or 'cad-owns'. The resource warns at boot when it sees a second 911 owner running.

For a CAD nobody has written a driver for: Provider = 'event' emits everything as events for your own resource, and Provider = 'custom' posts the same payloads as JSON to endpoints you name.

Admin panel & live config

/radioadmin opens the in-game configuration window. Three tabs: Talkgroups (add, edit, reorder, delete — deleting one tells you which personalities reference it and cleans them up), Personalities (jobs, zones, channel order, scan lists, feature flags, emergency talkgroup) and Settings (nine sections down a rail — arbitration, emergency, audio, voice routing, interface, alias format, access, logging). A moon/sun button switches the panel between light and dark, remembered per machine.

Saving writes data/overrides.json and re-issues every radio live — units keep their current zone/channel wherever the new codeplug still has it. config.lua is never rewritten; delete overrides.json to fall back to it. The last Config.Admin.BackupCount saves are kept, and Roll Back restores the most recent. Everything from the panel is re-validated server side.

Talkgroup order matters. The array index determines the voice channel number. On a live server, append new talkgroups at the end rather than inserting in the middle, or you will re-map voice channels for everyone connected.

Audio

Voice coloring is a GTA audio submix applied to radio traffic. Presets in Config.Audio.Presets:

PresetCharacter
p25300–3000 Hz passband, zero analog fudge, a light ring-mod blend for the vocoded edge.
p25_lightSame idea, dialled back.
analogClassic FM grit and distortion.
offUntouched voice.

Signaling tones — talk-permit beep, bonk, courtesy tone, squelch clicks, emergency warble, page alert, keypress clicks — are synthesised in the UI; no audio files ship with the resource. Frequencies and lengths are in Config.Audio.Tones, and Config.Audio.DigitalArtifact is the short quantised-noise burst when a transmission opens.

Exports & events

The commonly used server exports:

exports['sals_kewldispatch']:GetTalkgroupRoster('LSPD1')  -- who is listening
exports['sals_kewldispatch']:IsTalkgroupBusy('LSPD1')
exports['sals_kewldispatch']:SendPageToTalkgroup('LSPD1', 'CAD', 'Signal 100')

-- 911
exports['sals_kewldispatch']:StartCall({ src = source, number = '911' })
exports['sals_kewldispatch']:EndCall(callId, 'reason')

-- pager
exports['sals_kewldispatch']:SendToneOut(
    { 'FIRE1', 'EMS1' },
    { 'STATION1', { a = 607.0, b = 1032.0 } },
    'Structure fire, Vinewood Blvd', 'CAD')
exports['sals_kewldispatch']:SendPagerText(src, 'Respond to station', 'CHIEF')

-- jobs (standalone servers, or to override the framework)
exports['sals_kewldispatch']:SetPlayerJob(src, 'police', 4, true)
exports['sals_kewldispatch']:RefreshPlayer(src)

Three events fire for every 911 call, whatever else is configured:

sals_kewldispatch:call:determined   -- somebody established what the call is
sals_kewldispatch:call:fallback     -- nobody took it; the units are being paged
sals_kewldispatch:call:closed       -- it ended, with disposition and notes

StartCall accepts nature, priority, location, a landline block for a known place, and meta — a callout script that already knows what the call is should say so. On qb/qbx/esx the framework outranks SetPlayerJob: the next real job or duty event clears the override. The full export list is in the README that ships with the resource.

Troubleshooting

SymptomFix
Anything at all /radiodiag first. It prints the running version, which framework / inventory / phone / CAD adapter resolved and by which export, the voice backend, the link posture and the pma-voice key check.
A fix you just uploaded changes nothing FXServer caches the manifest. After copying files, run refresh, restart the resource, and confirm the version line /radiodiag prints matches the CHANGELOG before testing.
Players key up twice / bypass arbitration pma-voice's own radio key is still bound — have them unbind it in Settings → Key Bindings → FiveM. The radio warns them in chat, naming the key.
A caller or unit is silent on the console They refused FiveM's microphone prompt. /radiomic re-asks. The call card says CALLER HAS NO MICROPHONE when this is the reason.
/radioadmin does nothing Check the server console — a denial prints the player's identifiers and the exact add_ace line to run.
The radio is stuck somewhere the mouse can't reach /radiomove reset. Saved positions are clamped to the screen on load, so a resolution change cannot strand it permanently.
The game stops taking input /radiounfocus releases NUI focus if anything stranded it.
A player says they "have no radio" With Config.Access.ExplainDenials on (the default) the refusal names the missing item or the job problem. Check their job is in a personality and they hold the required item.

Support

Support, release news and coupon codes are on Discord. Purchases are handled by Tebex, who own the checkout, billing support and refunds. The resource ships with its own README and CHANGELOG — the CHANGELOG calls out breaking config changes explicitly, and /radiodiag prints the version you are actually running.

In-game radio and dispatch console are separate products — all pricing is on the main site.