Range lists enumerated only the nominal 25 kHz raster (offset 0). Add an optional offsets key to [frequency_list.range] so each carrier is also probed at the ETSI-legal offsets (0, +6250, -6250, +12500 Hz; EN 300 392-2 D-MLE-SYNC Offset field). candidate_frequencies() expands carrier x offset (deduped); validation rejects any offset outside the four legal values. Absent/empty offsets = [0], preserving prior behaviour. List mode already supports any offset via absolute Hz. Docs + example config updated. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
15 KiB
TETRA BlueStation — MS Mode Configuration Reference
Companion to MS_MODE.md. This documents every configuration key an MS
(stack_mode = "Ms") reads. A complete, commented starting point ships as
../example_config/config-ms.toml.
The config file is TOML. It is passed as the single CLI argument:
bluestation-bs config-ms.toml
Sources of truth: the parser and section structs live in
crates/tetra-config/src/bluestation/ (parsing.rs, sec_*.rs); validation is
StackConfig::validate(). Where a value maps to an on-air element, the ETSI clause is
noted.
Radio-programming model (read this first)
An MS is programmed by downlink, exactly like a real portable radio. It does not hard-code a serving carrier or duplex spacing. Instead it:
- scans the
[[frequency_list]]downlink carriers, - selects the strongest suitable cell whose network is allowed,
- camps on it, and
- derives its uplink carrier and duplex spacing from that cell's own D-MLE-SYNC / SYSINFO at camp time (EN 300 392-2 cl. 18.4.2.2).
Consequently an MS config has no fixed tx_freq/rx_freq traffic plan and no
[cell_info] RF block — only an initial scan seed and non-RF identity. The uplink
frequency stays unset until the MS camps.
Top-level keys
| Key | Type | Required | Description |
|---|---|---|---|
config_version |
string | yes | Config schema version (e.g. "0.7"). |
stack_mode |
string | yes | "Bs", "Ms", or "Mon". Must be "Ms" for MS mode. |
debug_log |
string (path) | no | If set, writes a verbose trace log to this path. Files grow fast and add load; leave unset for normal runs. |
[phy_io] — PHY back end
| Key | Type | Description |
|---|---|---|
backend |
string | RF back end. Currently "SoapySdr". |
[phy_io.soapysdr] — SoapySDR device & tuning
For an MS there is no fixed tx_freq/rx_freq. The initial RX (downlink) center is
seeded from the first [[frequency_list]] scan candidate, and the MLE scan/cell-select
engine retunes it at runtime. The uplink (TX) stays unset until the MS camps and derives
it from the cell's SYSINFO. (You may still set tx_freq = <dl_hz> to force a specific
startup RX carrier, but it is not required.)
| Key | Type | Default | Description |
|---|---|---|---|
device |
string | — | SoapySDR device args (e.g. "driver=sx"). Check SoapySDRUtil --find. |
sample_rate |
int (Hz) | — | SDR sample rate. SXceiver defaults to 600000. |
ppm_err |
int | 0 |
Tuning-error correction for your SDR's reference. |
rx_antenna |
string | — | RX antenna port name (e.g. "RX"). |
tx_antenna |
string | — | TX antenna port name (e.g. "TX"). |
rx_gain_lna |
float (dB) | — | LNA gain for receiving the BS downlink. Reduce if the front end overloads near the BS. |
rx_gain_pga |
float (dB) | — | PGA gain (downlink RX). |
tx_gain_dac |
float (dB) | — | Uplink TX DAC gain. Keep low until you know your MS RF chain; raise for on-air uplink. |
tx_gain_mixer |
float (dB) | — | Uplink TX mixer gain. (For the IT PA, mixer MAX ≈ 21.) |
dl_input_file |
string (path) | — | Debug: read DL RF samples from a file instead of the SDR. |
ul_rx_file |
string (path) | — | Debug: save received RF samples to a file. |
Uplink TX power matters. In early bring-up the single most common cause of "the BS never acked" was insufficient TX output power, not timing. Once uplink is enabled, set
tx_gain_dac/tx_gain_mixerappropriately for your PA.
[net_info] — home network
The MCC/MNC of the network this MS belongs to (used for cell suitability and MM addressing).
| Key | Type | Bits | Description |
|---|---|---|---|
mcc |
int | 10-bit | Mobile Country Code. |
mnc |
int | 14-bit | Mobile Network Code. |
[cell_info] — non-RF cell identity
For a radio-style MS the RF band/carrier/duplex parameters are learned over the air, so this block keeps only non-RF identity fields.
| Key | Type | Description |
|---|---|---|
location_area |
int | Location Area identifier (LA). |
colour_code |
int | Colour code, distinguishes adjacent cells on the same frequency. |
[duplex_table] — duplex-spacing overrides (optional)
A programmed radio may override the standardized duplex spacing for specific 3-bit duplex indices (TS 100 392-15 cl. 6). Unlisted indices use the ETSI defaults for the operating band. Index 7 has no ETSI default, so networks that use it must program it here.
| Key | Type | Description |
|---|---|---|
overrides |
array of [index, spacing_hz] |
Each entry maps a duplex index (0–7) to a spacing in Hz. |
[duplex_table]
overrides = [[7, 9400000]] # index 7 -> 9.4 MHz spacing
[ms] — MS identity & affiliation (required for stack_mode = "Ms")
| Key | Type | Range | Default | Description |
|---|---|---|---|---|
issi |
int | 1..=16777215 (24-bit) | — (required) | Own Individual Short Subscriber Identity — the MS's address. Change this to your MS's ISSI. |
subscriber_class |
int | 1..=16 | 1 |
Subscriber class; checked against the cell's advertised subscriber-class bitmask (D-MLE-SYSINFO, cl. 18.4.2.2). |
attach_groups |
array of int | 24-bit GSSIs | [] |
Group identities to attach to once registered (cl. 16). Empty = receive-only, no group affiliation. |
Transmit parameters such as class-of-MS / power class are not modelled as config keys; the MS sends a truthful minimal class-of-MS element where the spec requires one.
Codeplug (Plane B — non-standard management structure)
The codeplug is BlueStation-specific data (ETSI does not standardize over-the-air radio programming). Every value still maps to a real air-interface element and is validated against its ETSI range. The radio model: a list of talkgroups (organized into folders), a list of allowed networks, a set of downlink frequency lists to scan, optional carrier overrides, and scan lists.
[[folder]] — UI grouping of talkgroups
| Key | Type | Description |
|---|---|---|
id |
string | Unique folder id (referenced by talkgroup.folder). |
name |
string | Display name. |
order |
int | Sort position (ascending; ties broken by name). |
[[talkgroup]] — user-selectable groups
| Key | Type | Description |
|---|---|---|
gssi |
int (24-bit) | Group Short Subscriber Identity. |
name |
string | Display name. |
folder |
string | Folder id this group lives under (optional). |
class_of_usage |
int (3-bit) | Group identity class of usage (cl. 16.10.6). Optional. |
order |
int | Sort position within the folder. |
[[network]] — allowed networks
A cell is only suitable if its MCC/MNC is listed here. If no [[network]] is programmed,
only the home MCC/MNC from [net_info] is allowed.
| Key | Type | Description |
|---|---|---|
mcc |
int | Mobile Country Code. |
mnc |
int | Mobile Network Code. |
name |
string | Display name (optional). |
priority |
int | Preference order; lower is preferred first (optional). |
[[carrier_override]] — per-carrier camp pinning (optional)
Pin extra camp parameters to one specific downlink frequency, applied when the scanner
lands on it. Program the frequency by explicit band+carrier (+offset) or by
absolute dl_freq.
| Key | Type | Description |
|---|---|---|
name |
string | Unique label. |
band |
int | Frequency band (100 MHz increments). Use with carrier. |
carrier |
int (12-bit) | Main carrier number. |
freq_offset |
int (Hz) | Offset from the 25 kHz carrier: 0, 6250, -6250, or 12500. |
dl_freq |
int (Hz) | Absolute downlink frequency (alternative to band+carrier). |
colour_code |
int | Only camp on a cell with this colour code (optional). |
duplex_index |
int | Duplex-spacing index hint (else derived from SYSINFO) (optional). |
custom_duplex_spacing |
int (Hz) | Per-carrier custom duplex spacing (optional). |
rx_only |
bool | Receive-only: never transmit (uplink/registration suppressed) here. |
[[carrier_override]]
name = "BS-1"
band = 4
carrier = 1593 # 439.825 MHz DL
freq_offset = 0
colour_code = 1
duplex_index = 7
custom_duplex_spacing = 9400000
rx_only = true
[[frequency_list]] — downlink carriers to scan
The candidate downlink carriers the MS scans to find a serving cell. Define one or more
named lists; the radio scans all lists combined into a single candidate set
(duplicates removed) and camps on the best suitable cell. With no [[frequency_list]],
the MS does not scan.
Reused by the UI-driven manual carrier survey. The manual cell survey (
SetCellSelectionMode→StartCellScan, results asMsScanResulttelemetry) and register-to-cell (CampOnCell) surveys / camps this same combined candidate set — no new config keys are introduced for that feature. ACampOnCellcarrier must be a member of this set.
| Key | Type | Description |
|---|---|---|
name |
string | Unique label. |
mode |
string | "List" (explicit frequencies) or "Range" (enumerated carrier range). |
frequencies |
array of int (Hz) | Downlink frequencies for a List list (a single entry = "one fixed channel"). |
dwell_ms |
int (ms) | Per-candidate dwell time while scanning. |
For a Range list, add a nested single-bracket sub-table [frequency_list.range]
(one range per list — not a [[...]] array of tables):
| Key | Type | Description |
|---|---|---|
band |
int | Frequency band. |
start_carrier |
int | First carrier number. |
stop_carrier |
int | Last carrier number (inclusive). |
step |
int | Step in carrier units (multiples of 25 kHz), ≥ 1. |
offsets |
array of int (Hz) | Optional. Carrier offsets to probe for each enumerated carrier. TETRA permits only four (D-MLE-SYNC "Offset" field): 0, 6250, -6250, 12500. Omitted/empty = [0] (nominal 25 kHz raster only). A range with offsets = [0, 6250] scans each carrier at both its nominal and +6.25 kHz frequency (duplicates removed). |
[[frequency_list]]
name = "primary"
mode = "List"
frequencies = [439825000, 439850000]
dwell_ms = 800
# [[frequency_list]]
# name = "band4-sweep"
# mode = "Range"
# dwell_ms = 800
# [frequency_list.range]
# band = 4
# start_carrier = 1500
# stop_carrier = 1700
# step = 1
# offsets = [0, 6250, 12500] # optional: also probe +6.25 / +12.5 kHz carriers
[[scanlist]] — named talkgroup scan/affiliation sets (optional)
A scan list is a set of talkgroups the radio monitors together. "Activating" a scan list
means the MS affiliates to those GSSIs via the standalone group attach/detach
procedure (cl. 16.8.2); deactivating detaches the groups no other active list still
needs. active here is the programmed default — the management UI can toggle a scan
list live (ManagementCommand::ActivateScanlist), so the running state may differ.
The MS's desired affiliation set = [ms].attach_groups ∪ the GSSIs of every active
scan list.
| Key | Type | Description |
|---|---|---|
name |
string | Unique label. |
talkgroups |
array of int (GSSI) | Members; each must reference a programmed [[talkgroup]]. |
active |
bool | Programmed default activation state at start-up. |
order |
int | Menu sort position. |
[[scanlist]]
name = "Patrol"
talkgroups = [101, 102]
active = true
order = 1
[control] — UI → stack command channel (optional)
The inbound control endpoint the stack connects to (WebSocket + JSON, optional TLS +
HTTP Basic auth). Carries Plane A (TNMM requests) and Plane B (management) commands. See
../examples/ms-interface/README.md.
| Key | Type | Description |
|---|---|---|
host |
string | Control server hostname/IP. |
port |
int | Control server port. |
use_tls |
bool | Use TLS (wss://). Default false. |
ca_cert |
string (path) | DER-encoded CA cert for self-signed TLS. Requires use_tls = true. |
username |
string | HTTP Basic auth username (must be paired with password). |
password |
string | HTTP Basic auth password. |
[telemetry] — stack → UI event channel (optional)
The outbound telemetry endpoint (stack→UI events: TNMM indications, MsSpeechFrame
voice, state changes). Same connection/auth fields as [control].
| Key | Type | Description |
|---|---|---|
host |
string | Telemetry server hostname/IP. |
port |
int | Telemetry server port. |
use_tls |
bool | Use TLS (wss://). Default false. |
ca_cert |
string (path) | DER-encoded CA cert for self-signed TLS. Requires use_tls = true. |
username |
string | HTTP Basic auth username (paired with password). |
password |
string | HTTP Basic auth password. |
Secrets. The on-disk TOML is the canonical plaintext store of credentials. Over the management interface,
GetConfigredacts every secret to"********", andSetConfigtreats the sentinel as "keep the existing value" — so a config round-trip never clobbers a credential. (Redaction is for logs/wire only.)
[brew](BrandMeister/Brew connectivity) is a BS-side feature and is not used by an MS; omit it from an MS config.
Applying config changes at runtime (hybrid model)
Via the management interface (Plane B):
- Structural radio params (MCC/MNC, carrier/band/duplex, ISSI, SDR device):
SetConfigvalidates through the exact startup validator and stages the new TOML to disk withrestart_required = true; a laterApplyConfigperforms the graceful de-registration drain (U-ITSI DETACH, cl. 16.6.1) and exits for a supervisor to respawn with the new config. - Operational TNMM actions (register/deregister, group attach/detach, energy saving, scan-list activation) apply live — no restart.
Availability before registration.
GetConfig,SetConfig, andApplyConfigare serviced as soon as the control link is up — independent of registration or service state (including out of service, and before the MS has ever synced to a base station). The stack is receive-timed, so while unsynchronized it services this config/state subset off a dedicated pre-tick path; any other command received meanwhile is buffered and replayed on the first real tick, leaving registration / on-air behaviour unchanged. The MS PHY cooperatively yields the receive loop (~20 ms) while it has no downlink lock so this path actually runs before the radio finds a base station. SeeMS_MODE.md§3.7 "Offline config servicing".
Minimal MS config
config_version = "0.7"
stack_mode = "Ms"
[phy_io]
backend = "SoapySdr"
[phy_io.soapysdr]
device = "driver=sx"
sample_rate = 600000
ppm_err = 0
rx_antenna = "RX"
tx_antenna = "TX"
rx_gain_lna = 48.0
rx_gain_pga = 8.0
tx_gain_dac = 0.0
tx_gain_mixer = 0.0
[net_info]
mcc = 901
mnc = 9999
[cell_info]
location_area = 1
colour_code = 1
[duplex_table]
overrides = [[7, 9400000]]
[ms]
issi = 1000001
subscriber_class = 1
attach_groups = []
[[frequency_list]]
name = "primary"
mode = "List"
frequencies = [439825000]
dwell_ms = 800