Files
misadeksandCopilot App c3fa7c1a9e MS group affiliation G2-G4: runtime talkgroup attach/detach (cl. 16.8.2)
Implements the standalone MS-initiated group attach/detach procedure so a UI
can change talkgroups at runtime instead of only using the config set, strictly
per ETSI TS 100 392-2 V3.10.1.

G2 (TX): on a TNMM-ATTACH DETACH GROUP IDENTITY request (Table 15.1), once
registered, build and send a U-ATTACH/DETACH GROUP IDENTITY PDU (cl. 16.9.3.1)
over the acknowledged basic link: report = not-report-request, amendment vs
detach-all mode, one Group identity uplink per GSSI (class of usage per
cl. 16.10.6 Table 16.32, or detachment reason per cl. 16.10.21). Start T353
(cl. 16.11.1.3, 10 s). Single operation outstanding; rejected if not registered.

G3 (RX): on D-ATTACH/DETACH GROUP IDENTITY ACKNOWLEDGEMENT (cl. 16.9.2.2), stop
T353, reconcile the attached-group set from the SwMI-confirmed downlink elements
(detach-all replaces; amendment adds/removes), and push it to the MLE via
MLE-IDENTITIES so the MAC downlink filter matches (cl. 23.4.1.2.1). On T353
expiry the op is treated as failed (cl. 16.8.5).

G4 (confirm + UI): emit a TNMM-ATTACH DETACH GROUP IDENTITY confirm (Table 15.1,
GTSIs only per cl. 16.8.2) to the user application; document the request/confirm
JSON shapes and add a live talkgroup-switch example to the MS interface README +
reference client.

Tests: +7 mm_ms unit tests (unregistered reject, TX PDU round-trips through the
BS parser + T353 armed, ACK reconcile + MLE-IDENTITIES + confirm, amendment
detach, detach-all replace, second-op-while-pending reject, T353 expiry failure).
196 lib + all integration suites green; no BS air-interface change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-21 01:09:58 +02:00
..

BlueStation MS external-interface message catalog

Schema bluestation-ms-interface-1.

This directory documents and demonstrates the MS (mobile-station) external interface: the message contract a separate user-interface process uses to drive the BlueStation MS RF process (read state, provision the stack, and issue standardized TETRA mobility primitives). It exists so multiple portable-radio UIs can be built on top of the stack.

  • README.md (this file) — the protocol/API documentation.
  • reference-client.py — a minimal, runnable, language-neutral reference client.

Versioning

Frozen at MS_INTERFACE_SCHEMA_VERSION = "bluestation-ms-interface-1" (crates/tetra-entities/src/management/mod.rs), discoverable at runtime via the GetInterfaceVersion command. This is the application schema version for the MS interface; it is independent of the transport WebSocket subprotocol handshake strings (bluestation-control-v1 / bluestation-telemetry-v1), which are shared with the BS and are intentionally NOT bumped (MS-side only; no BS behaviour change).

Transport & encoding

Both planes share the existing in-tree transport (WebSocket + JSON, TLS + argon2). The stack currently connects as a client to the UI's control and telemetry endpoints; the message schema is transport-agnostic, so a stack-hosted listen/server mode could be added later as a pure-additive change without touching Plane A/B messages.

JSON encoding is serde's default externally-tagged enum representation: {"VariantName": { ...fields... }}. Control commands/responses are wrapped as {"Management": { ... }} (Plane B) or the TNMM variant name (Plane A).

Two planes

  • Plane A — TNMM-SAP (STANDARDIZED, ETSI TS 100 392-2 cl. 15.3): requests UI->stack over the control channel; indications/confirms stack->UI over the telemetry channel. Traces verbatim to cl. 15.3.3 (Tables 15.1-15.7) and the value enums in cl. 15.3.4.
  • Plane B — management/provisioning (NON-STANDARD, implementation-defined): runtime-state reads + config read/write/apply. ETSI does not standardize radio programming over the air; this plane is a BlueStation-specific facility. Carried in the Management wrapper variants of ControlCommand/ControlResponse.

Plane B — control channel (UI -> stack), wrapped in ControlCommand::Management

Command Payload When Response
GetState {handle:u32} live/anytime State
GetInterfaceVersion {handle:u32} live/anytime InterfaceVersion
GetConfig {handle:u32} live/anytime Config
SetConfig {handle:u32, toml:String} live (stages to disk) Ack
ApplyConfig {handle:u32} drains + restarts Ack

Responses, wrapped in ControlResponse::Management

  • State {handle:u32, state: MsRuntimeState}
  • InterfaceVersion {handle:u32, version:String}
  • Config {handle:u32, toml:String} — canonical TOML of the active config (secrets redacted)
  • Ack {handle:u32, accepted:bool, restart_required:bool, message:String}
  • Error {handle:u32, message:String}

MsRuntimeState:

registration_state : "Idle" | "Registering" | "Registered" | "Detaching"
service_status     : ServiceStatus (Plane A vocabulary)
own_issi           : u32
home_mcc           : u16
home_mnc           : u16
serving_la         : u16
colour_code        : u8
attached_groups    : [u32]
restart_required   : bool

Apply model (HYBRID)

  • Structural radio params (MCC/MNC, carrier/band/duplex, ISSI, SDR device): SetConfig validates through the exact startup validator and writes the TOML file, sets restart_required=true; it does NOT bounce the process. A later ApplyConfig performs the graceful de-registration drain (U-ITSI DETACH, cl. 16.6.1) and exits with code 75 for an external supervisor to respawn (see example_config/bluestation-ms.service and bluestation-ms-supervisor.sh).
  • Operational TNMM actions (register/deregister, group attach/detach, energy saving): carried on Plane A and applied live.

Secret handling (redact on the wire, preserve on write-back)

  • GetConfig redacts every secret (control/telemetry/brew password) to the sentinel "********" on the wire; plaintext credentials never leave the RF process.
  • SetConfig treats the sentinel as "keep the existing on-disk secret": a secret posted back unchanged (still "********") preserves the live value; a genuinely-new value overwrites it. A GetConfig -> edit-unrelated-field -> SetConfig round-trip therefore never clobbers a credential, and the result re-parses through the same validator (closure preserved).
  • The on-disk TOML remains the canonical plaintext store of the real secrets.

Plane A — TNMM-SAP (STANDARDIZED, cl. 15.3)

Requests (control channel), top-level ControlCommand variants

Command Table / clause Notes
TnmmRegistration {handle, request} 15.5 / 15.3.3.7 initiate ITSI attach + registration
TnmmDeregistration {handle, request} 15.2 / 15.3.3.2 U-ITSI DETACH (reuses shutdown drain)
TnmmAttachDetachGroupIdentity {handle, request} 15.1 / 15.3.3.1 live talkgroup attach/detach (standalone procedure cl. 16.8.2); requires the MS to be registered
TnmmStatus {handle, request} 15.7 / 15.3.3.9 dormant: direct mode / dual watch / energy economy not implemented
TnmmEnergySaving {handle, request} 15.3 / 15.3.3.5 dormant: energy economy cl. 16.7 not implemented

All requests are acknowledged with ControlResponse::TnmmAck {handle, accepted:bool, detail:Option<String>}. The TnmmAck only reports whether the request was accepted for processing (e.g. rejected if the MS is not registered, or a group op is already outstanding). The TNMM result is reported asynchronously via the telemetry-channel indications/confirms (cl. 15.3.2).

TnmmAttachDetachGroupIdentity request shape (Table 15.1 / 15.9)

{"TnmmAttachDetachGroupIdentity": {
  "handle": 31,
  "request": {
    "group_identity_attach_detach_mode": "Amendment"
        | "DetachTheCurrentlyActiveGroupIdentities",
    "group_identity_request": [
      { "gtsi": 300,
        "group_identity_attach_detach_type_identifier": "Attachment",
        "class_of_usage": "ClassOfUsage4",          // Attachment only
        "group_identity_detachment_request": null } , // Detachment only
      { "gtsi": 91,
        "group_identity_attach_detach_type_identifier": "Detachment",
        "class_of_usage": null,
        "group_identity_detachment_request": "UserInitiatedDetachment" }
    ],
    "group_identity_report": null                   // Option<"ReportRequested"|"ReportNotRequested">
  }
}}
  • Amendment (cl. 16.10.17 mode 0) adds/removes only the listed groups; the rest of the attached set is untouched.
  • DetachTheCurrentlyActiveGroupIdentities (mode 1) detaches everything currently attached, then attaches the listed groups — use this to switch talkgroup.
  • gtsi is the full 48-bit GTSI (MNI << 24 | GSSI); a bare GSSI also works (the low 24 bits are taken). class_of_usage maps to the on-air 3-bit value per cl. 16.10.6 (Table 16.32: "Class of usage N" = N-1). Detachment carries the reason per cl. 16.10.21 (only UserInitiatedDetachment is defined at this SAP).

Indications/confirms (telemetry channel), TelemetryEvent variants

Indication Table / clause Emit point
TNMM-REGISTRATION indication 15.5 MM reg-state transitions (accept/reject/T351)
TNMM-SERVICE indication 15.6 in/out of service transitions
TNMM-ATTACH DETACH GROUP IDENTITY confirm 15.1 on the D-ATTACH/DETACH GROUP IDENTITY ACKNOWLEDGEMENT, or on T353 expiry (failure)
TNMM-REPORT indication 15.4 DORMANT — U-ITSI DETACH transfer-result source not yet wired (MM does not observe the TxReporter through LMM-UNITDATA)
(STATUS / ENERGY-SAVING) 15.7 / 15.3 DEFINED but DORMANT — stack cannot truthfully observe

TnmmAttachDetachGroupIdentityConfirm telemetry event shape (Table 15.1):

{"TnmmAttachDetachGroupIdentityConfirm": {
  "group_identity_attach_detach_mode": "Amendment" | "DetachTheCurrentlyActiveGroupIdentities",
  "group_identity_report": null,
  "group_identities": [                       // the SwMI-acknowledged groups (GTSIs only)
    { "gtsi": 300,
      "group_identity_attach_detach_type_identifier": "Attachment",
      "group_identity_lifetime": "AttachmentNeededForNextItsiAttach",
      "class_of_usage": "ClassOfUsage4",
      "group_identity_detachment_reason": null }
  ]
}}

An empty group_identities list means no group was acknowledged (e.g. the operation failed / T353 expired). The confirmed set is also reflected in MsRuntimeState.attached_groups (readable via GetState).

Reject-cause mapping: cl. 15.3.4 is a strict subset of the on-air 16.10.42 cause set; causes with no cl. 15.3.4 enumerant map to None (never fabricated).


wscat (language-neutral) quick example

# Connect to the stack's control channel (subprotocol = bluestation-control-v1),
# authenticated per your transport config.
wscat -s bluestation-control-v1 -c wss://<stack-host>:<port>/

# Discover the interface schema version:
> {"Management":{"GetInterfaceVersion":{"handle":1}}}
< {"Management":{"InterfaceVersion":{"handle":1,"version":"bluestation-ms-interface-1"}}}

# Read runtime state:
> {"Management":{"GetState":{"handle":2}}}
< {"Management":{"State":{"handle":2,"state":{...}}}}

# Read config (secrets show as "********"), edit the TOML, stage it, then apply:
> {"Management":{"GetConfig":{"handle":3}}}
< {"Management":{"Config":{"handle":3,"toml":"config_version = \"0.6\"\n..."}}}
> {"Management":{"SetConfig":{"handle":4,"toml":"<edited toml>"}}}
< {"Management":{"Ack":{"handle":4,"accepted":true,"restart_required":true,"message":"..."}}}
> {"Management":{"ApplyConfig":{"handle":5}}}
< {"Management":{"Ack":{"handle":5,"accepted":true,"restart_required":true,"message":"..."}}}

# Live TNMM registration:
> {"TnmmRegistration":{"handle":6,"request":{...}}}
< {"TnmmAck":{"handle":6,"accepted":true,"detail":null}}

# Live talkgroup switch (detach all + attach GSSI 300) — must be registered first.
# The Ack only means "accepted for processing"; the RESULT arrives on the
# telemetry channel as a TnmmAttachDetachGroupIdentityConfirm.
> {"TnmmAttachDetachGroupIdentity":{"handle":7,"request":{"group_identity_attach_detach_mode":"DetachTheCurrentlyActiveGroupIdentities","group_identity_request":[{"gtsi":300,"group_identity_attach_detach_type_identifier":"Attachment","class_of_usage":"ClassOfUsage4","group_identity_detachment_request":null}],"group_identity_report":null}}}
< {"TnmmAck":{"handle":7,"accepted":true,"detail":null}}
# ... then on the telemetry channel:
< {"TnmmAttachDetachGroupIdentityConfirm":{"group_identity_attach_detach_mode":"DetachTheCurrentlyActiveGroupIdentities","group_identity_report":null,"group_identities":[{"gtsi":300,"group_identity_attach_detach_type_identifier":"Attachment","group_identity_lifetime":"AttachmentNeededForNextItsiAttach","class_of_usage":"ClassOfUsage4","group_identity_detachment_reason":null}]}}