# Talkgroup Model A **talkgroup** is the fundamental addressing unit of the PTT Platform. Every voice transmission is addressed to a talkgroup. Devices receive audio by affiliating with a talkgroup. --- ## Talkgroup Types ### Group (type: `G`) A shared channel that any affiliated device can transmit to and receive from. This is the standard PTT "channel" model. - Multiple devices can be affiliated simultaneously. - Any affiliated device can key up and transmit. - Audio is forwarded to all devices in the room via `socket.to(grpRoom(gid)).emit(...)`. - Scan affiliations are also served: `socket.to("scan-" + gid).emit(...)`. ### Broadcast (type: `B`) A one-to-many channel where only devices listed in the `broadcast-auth.json` allow-list for that group can transmit. Used for base stations, dispatch positions, or automated announcement sources. - Any device can affiliate and receive. - Only devices in the broadcast allow-list for the group have `targetTxEnabled = true`. - The `PTT_MAX_VOICE_SEQ` interrupt does not apply to type `B` talkgroups (broadcast sources can hold the channel). - The allow-list is keyed by group id; values are arrays of DNIs authorized to transmit. ### Private / individual (type: `P`) A point-to-point channel between two devices. The wire value is always `P` for private calls (DNI-addressed or backend-assigned). - **DNI-addressed**: The caller affiliates to a target DNI string (16-digit decimal). The server resolves the target's live socket via the `dev:` room and initiates a `ptt_call_setup` handshake. No backend lookup is required. - **Backend-assigned**: The server has a backend-assigned `talkgroup_id` for the device and routes by that integer; `tgType` is still `P`. Private calls require a `ptt_call_setup` handshake before transmission can begin. The target device must accept the call (it can reject with `status_code: 400`). --- ## Affiliation A device expresses its current active talkgroup by sending the `affiliate` event: ```javascript // Preferred 3-arg shape (new clients): socket.emit('affiliate', targetTGID, sourceTGLabel, sourceTGPublicKey) // Legacy 4-arg shape (old clients — second arg is deprecated and ignored by server): socket.emit('affiliate', targetTGID, _deprecatedSourceTGID, sourceTGLabel, sourceTGPublicKey) ``` On the server: 1. `normalizeAffiliatePayload(args)` normalizes all arg shapes; caller identity is always `socket.data.dni`. 2. `dniMod.parseAddressTarget(targetTGID)` determines whether the target is a device DNI or a group id. 3. Group type is resolved locally via the `groupTier` module — **no backend call is made** at affiliation time. 4. `socket.leaveAll()` — leave all previous rooms. 5. `socket.join("grp:" + gid)` — join the new group room (for group/broadcast targets). 6. `socket.join("dev:" + socket.data.dni)` — re-join own device room (always). 7. `rejoinScanRooms(socket)` — restore scan room memberships. 8. Emit `update_target` back to the client with talkgroup metadata. The active talkgroup is persisted in `socket.data.targetTGID`. --- ## Scan Affiliation A device can listen to additional talkgroups without making them its active transmit target. This is used for scanning. ```javascript socket.emit('affiliatescan', scannedTGID) socket.emit('deaffiliatescan', scannedTGID) ``` On the server: - `affiliatescan` joins the `"scan-" + tgid` room. - `deaffiliatescan` leaves the `"scan-" + tgid` room. - The scan list is persisted in `socket.data.scanListTGIDs[]`. - On every `leaveAll()` (affiliation change), `rejoinScanRooms()` is called to restore scan membership. Scan rooms receive the same `voice_data` events as the primary room. --- ## Device Identity and Routing (DNI) Each device is identified by its **Device Numeric Identity (DNI)** — a 16-digit decimal number derived from the device public key. The DNI serves as the device's own-address in the platform: - `socket.data.dni` is the device's DNI, set at connect time and used as the authoritative caller identity. - After authentication, the device is placed in room `dev:`. - `update_talkgroup { talkgroup_id: , talkgroup_label: }` is emitted after authentication to give the client its own routable address. The legacy integer `myTGID` (populated from backend device records) is still maintained in the client for backward compatibility with backend-assigned private talkgroups, but DNI-addressed routing (`dev:` rooms) is the primary mechanism for device-to-device calls in current deployments. --- ## Private Call Setup (ptt_call_setup) When device A (DNI `1234567890123456`) wants to call device B (DNI `9876543210987654`): ``` Device A affiliates to targetTGID = "9876543210987654" (DNI string) Server: 1. parseAddressTarget("9876543210987654") → { isDevice: true, dni: "9876543210987654" } 2. Lookup room "dev:9876543210987654" → get socket(s) for device B 3. targetSocket.emit('ptt_call_setup', targetTGID, callerDni, sourceTGLabel, sourcePubKey, callerPubKeyHash, null, callback) Device B's callback response: { status_code: 200, status_message: "ok", publicKey: } or { status_code: 400, status_message: "rejected" } If accepted: Device A's targetTGID is set to the peer DNI string socket.data.targetTGType = 'P' Voice goes to device B via socket.to("dev:" + targetDni).emit(...) ``` The `ptt_call_setup` event payload uses `callerDni` (the server-authoritative authenticated DNI) as the second argument, replacing the legacy `sourceTGID` integer used in older versions. Device A uses device B's public key (returned in the callback) to encrypt the AES session key in the voice envelope. --- ## Group Join (group_join) Devices join group rooms explicitly with the `group_join` event: ```javascript socket.emit('group_join', { id: }) ``` On the server: - Validates `id` is an integer in range `[0, 65535]` (reserved ranges 60000–65535 return `RESERVED_RANGE` error). - Joins `grp:` room. - Emits `group_info` with the group's metadata (type, label, DNS fallback status, tier info). - If `PTT_DNS_DOMAIN` is set and the group tier indicates DNS consultation, resolves a TXT record for additional metadata. --- ## Room Event Notifications When a device joins or leaves a numeric group or `grp:` room, the Socket.IO adapter fires `join-room` and `leave-room` events. The server emits `room_event` to everyone in the room: ```javascript io.to(room).emit('room_event', Number(groupId), "join"|"leave", socketId, roomSize) ``` `room_event` is only emitted for numeric-looking rooms and `grp:` rooms, not for `dev:` or `scan-` rooms. This allows clients to display the count of active listeners on a channel.