Entering manual cell selection or starting a receive-only survey abandons the serving cell, so a registration bound to that cell (cl. 16.4) must be torn down first. MM now drives a clean U-ITSI DETACH de-registration (cl. 16.6.1) when SetCellSelectionMode(manual) or StartCellScan arrives while Registered. The survey is deferred until the detach drains so it actually reaches the air before the MS tunes away; switching to manual keeps the camp so its detach transmits normally. StopCellScan cancels a deferred survey. Adds 4 regression tests (338 lib tests green).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The survey advanced only on a decodable D-MLE-SYNC (SYNC/SYSINFO finalize) or on the PHY scan-dwell heartbeat, which the PHY emits only while UNSYNCHRONIZED. A candidate that demod-locks a signal the MS cannot decode into a D-MLE-SYNC (non-TETRA emitter, or an undecodable/oscillating cell) instead produces only TlmbMonitorInd, so the survey stalled on it forever and, worse, ran those indications through the serving-cell break/reopen machine (toggling out-of-service).
Fix: while surveying, divert TlmbMonitorInd out of the serving-cell path and use it as a bounded per-carrier backstop (SURVEY_MONITOR_TICK_LIMIT). A decodable SYNC resets the backstop so good-but-slow cells are never cut short; the SYNC/SYSINFO and scan-dwell paths still handle decodable and empty carriers. The survey now always advances and completes. Adds two regression tests.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>
The range sub-table maps to range: Option<FrequencyRangeDto> (one range per list), so it must be a single-bracket [frequency_list.range] table, not a [[...]] array of tables. The [[frequency_list.range]] form the docs/example showed fails to parse (invalid type: sequence, expected struct FrequencyRangeDto). Corrected MS_CONFIG.md and example_config/config-ms.toml.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds an operator-driven manual cell-selection feature to MS mode: an
Auto/Manual selection-mode toggle, a receive-only survey of the programmed
[[frequency_list]] candidate carriers that reports each found cell, and a
register-to-cell camp with forced ITSI attach. MS-only; the BS air interface
is unchanged. Strictly per ETSI TS 100 392-2 v3.10.1 (cl. 18.3.4 cell
selection / 18.3.4.6 camp, D-MLE-SYNC 18.4.2.1, D-MLE-SYSINFO 18.4.2.2,
registration cl. 16.4); the survey UX is Plane-B implementation policy while
every per-cell parse/suitability action stays ETSI.
P1 - MLE survey engine (mle_ms.rs): single-pass candidate walk, survey mode +
survey_results/survey_pending, selection-mode gate (Manual suppresses
auto-camp), and 5 internal LMM-SAP primitives (SelectionModeReq, ScanReq,
CampReq, ScanResultInd, ScanCompleteInd). Receive-only: early-return guards
in the SYNC/SYSINFO handlers so the survey never camps/registers or filters by
allowed-network (it observes, it does not select). 8 unit tests.
P2 - MM management commands + telemetry (mm_ms.rs, management, net_control,
net_telemetry): 4 ManagementCommand variants (SetCellSelectionMode,
StartCellScan, StopCellScan, CampOnCell), MsScanResult/MsScanComplete
telemetry events, MsRuntimeState.selection_mode_manual, schema bumped to
bluestation-ms-interface-3 (append-only) with the golden wire-format freeze
test updated. The 4 commands are offline-serviceable (survey works before
registration). 6 unit tests.
P3 - CampOnCell + forced registration: camp_force_register forces an ITSI
attach even when the cell advertises registration-not-required; the carrier is
validated against the candidate set. The active-call guard is a documented
no-op stub (MM has no CMCE call-state signal yet).
P4 - docs: MS_MODE.md feature rows (MLE survey, register-to-cell) + MS_CONFIG.md
note that the survey reuses [[frequency_list]] (no new config keys).
Tests: tetra-entities 332 lib + integration green; bluestation-bs builds.
Software-tested only; not yet hardware-validated.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The offline config pump (drive_offline_control) added previously was never
reached on real hardware: while the MS downlink is unsynchronized the
demodulator stays in Mode::DlUnsynchronized and never yields a slot, so
RxTxDevSoapySdr::rxtx_timeslot's RX loop blocked indefinitely and the
single-threaded run loop never iterated to service the control interface.
While the downlink is not synchronized, the RX loop now yields back to the
caller (returning with no slot) after a short wall-clock window (UNSYNC_YIELD,
20 ms), in addition to the existing cooperative-shutdown break. The run loop
then reaches its no-slot branch and services the offline-safe management subset
(GetConfig/SetConfig/ApplyConfig/GetState/GetInterfaceVersion), so a UI can read
and stage the codeplug before the MS has synced or registered. The demodulator's
correlation state persists across calls, so sync acquisition is unaffected; once
synchronized the loop returns each slot on its own and is byte-identical, and the
mechanism is never installed in BS mode.
Docs updated (MS_MODE.md 3.7, MS_CONFIG.md availability note).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The MS stack is receive-timed: MessageRouter::run_stack_ms only ticks the
entities once the PHY recovers a downlink slot (DL-synchronized). Before the
MS syncs to a base station no tick runs, so MM's control-command path
(poll_control in tick_start) never executes and management GetConfig/SetConfig/
ApplyConfig requests sit unserviced -- the UI sees an empty/absent config.
Add a pre-tick offline-servicing path so config read/staging works as soon as
the control link is up, independent of registration/service state:
- TetraEntityTrait::drive_offline_control (default no-op); MessageRouter calls
it on every run_stack_ms iteration that recovers no slot.
- MmMs overrides it to service only the offline-safe management subset
(GetConfig/SetConfig/ApplyConfig/GetState/GetInterfaceVersion) -- none inject
SAP traffic needing the stack clock or a serving cell. Every other command
(TNMM requests, scan-list toggles) is buffered in deferred_control and
replayed, in arrival order, on the first real tick, so registration and
on-air behaviour are byte-identical to never having run offline.
No config schema change, no secret-handling change (******** redaction/restore
preserved), no on-air/registration behaviour change. ApplyConfig staging/
restart semantics kept as-is.
Tests: 4 new lib tests (offline GetConfig round-trip, offline SetConfig persist
+restart flag, offline secret redaction, TNMM registration deferral-until-tick).
316 lib + all integration tests pass; bluestation-bs builds clean.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
While the MS is unsynchronized the downlink demodulator stays in
Mode::DlUnsynchronized and never yields a demodulated slot, so the SoapySDR
rxtx_timeslot RX loop (while process_rx_block) spun until the MS camped on a
cell. The MS run loop only checks the stop flag between rxtx_timeslot calls, so
a Ctrl+C while not connected to a base station was deferred until a downlink
appeared -- the process appeared to 'await connection' before exiting.
Give RxTxDevSoapySdr a cooperative shutdown flag (the process-wide is_running,
wired only in build_ms_stack) and break out of the RX loop when a shutdown is
requested *and* the downlink is not synchronized. The break is gated on
!dl_synchronized so a shutdown while camped is byte-identical to before: the
loop still returns each slot on its own, letting MM transmit its U-ITSI DETACH
during the bounded drain. No-op in BS mode (no flag installed).
- soapy_dev.rs: run_flag field + set_run_flag(), stop_requested()/dl_synchronized()
helpers, RxDsp::dl_synchronized(), DemodulatorChannel::is_synchronized().
- demodulator.rs: Demodulator::is_synchronized() (Mode::Dl).
- main.rs: wire is_running into the MS PhyMs SDR device.
- docs/MS_MODE.md: document the unconnected-shutdown behaviour.
Build (bluestation-bs) clean; 312 lib + all integration tests pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Document the mobile-station (MS) implementation on this branch:
- docs/MS_MODE.md - architecture overview, per-layer feature matrix with
implementation state (hardware-validated / software-tested / deferred /
out-of-scope), on-air happy-path walkthrough, known limitations, build/run,
and ETSI TS 100 392-2 v3.10.1 clause traceability.
- docs/MS_CONFIG.md - full MS configuration-file reference: every [phy_io],
[net_info], [cell_info], [duplex_table], [ms], codeplug, [control] and
[telemetry] key, keyed to the tetra-config section structs and cited to the
relevant spec clauses, plus a minimal working config.
- README.md - link the new MS docs, the interface doc, and the example config.
Docs only; no code change.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>