mirror of
https://github.com/misadeks/tetra-bluestation.git
synced 2026-09-17 00:44:18 +00:00
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>
386 lines
15 KiB
Markdown
386 lines
15 KiB
Markdown
# TETRA BlueStation — MS Mode Configuration Reference
|
||
|
||
Companion to [`MS_MODE.md`](./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`](../example_config/config-ms.toml).
|
||
|
||
The config file is TOML. It is passed as the single CLI argument:
|
||
|
||
```bash
|
||
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:
|
||
|
||
1. scans the `[[frequency_list]]` downlink carriers,
|
||
2. selects the strongest suitable cell whose network is allowed,
|
||
3. camps on it, and
|
||
4. **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_mixer` appropriately 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. |
|
||
|
||
```toml
|
||
[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. |
|
||
|
||
```toml
|
||
[[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 as `MsScanResult` telemetry) and
|
||
> register-to-cell (`CampOnCell`) surveys / camps this **same** combined candidate set —
|
||
> **no new config keys are introduced** for that feature. A `CampOnCell` carrier 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). |
|
||
|
||
```toml
|
||
[[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. |
|
||
|
||
```toml
|
||
[[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`](../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, `GetConfig` redacts every secret to `"********"`, and `SetConfig`
|
||
> treats 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):
|
||
`SetConfig` validates through the exact startup validator and stages the new TOML to
|
||
disk with `restart_required = true`; a later `ApplyConfig` performs 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`, and `ApplyConfig` are
|
||
> 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. See
|
||
> [`MS_MODE.md` §3.7 "Offline config servicing"](./MS_MODE.md#37-external-interface-tnmm--management).
|
||
|
||
---
|
||
|
||
## Minimal MS config
|
||
|
||
```toml
|
||
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
|
||
```
|