- README: replace the old tetra-tn-web-ui link with the new
misadeks/tetra-bluestation-mmi operator UI, and add a link to the new
MS_GETTING_STARTED.md.
- MS_GETTING_STARTED.md: correct the TX-gain note — the example defaults are low
(not zero / not 'no power'); drop the IT-PA specifics.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A step-by-step, copy-paste getting-started guide for running the stack in
Mobile Station mode, mirroring the MMI UI's GETTING_STARTED style: install
tools (Rust + SoapySDR + driver), clone (no submodules / no codec tables here),
build bluestation-bs, configure the codeplug (with the tx_gain=0 -> can't
register gotcha), connect the tetra-bluestation-mmi operator UI (stack connects
out; control 9102 / telemetry 9101), Pi/systemd deploy, and troubleshooting.
Links the new MMI UI (misadeks/tetra-bluestation-mmi); does not reference the
old web UI. README left untouched.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The control and telemetry workers already try to connect immediately when they
start, but they only retried every 15s when the UI was unavailable — so if the
operator UI started after (or restarted independently of) the radio, it took up
to 15s to (re)connect. Cut RECONNECT_DELAY 15s -> 1s in both workers so the
radio reconnects within ~1s of the UI appearing.
To keep the journal clean under the faster cadence, log the connection failure
once per down-period at WARN, then quietly at DEBUG until it reconnects (added a
`down_logged` flag reset on a successful connect). The first attempt is still
immediate on worker start.
- net_control/worker.rs, net_telemetry/worker.rs: RECONNECT_DELAY 15s->1s;
warn-once-then-debug reconnect logging.
- docs/MS_CONFIG.md: document the immediate-connect + ~1s retry behaviour.
Logging/timing of the UI link only; no protocol/stack behavior changed.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
journald was rate-limiting bluestation-bs ("Suppressed ~6,200 messages" every
30s) because the stack logged a per-frame/per-burst firehose at debug/info by
default. Logging-only change; no protocol/timing/DSP behavior touched.
- debug.rs get_default_stdout_filter(): now honors RUST_LOG (full override, e.g.
RUST_LOG=debug|trace or per-module) and otherwise defaults to a quiet `info`.
Removed the hardcoded per-module `=debug` directives (umac/mle/llc/cmce/mm/...)
that forced the flood; kept messagerouter/bitbuffer=warn.
- Demoted the highest-rate lines so steady-state is ~silent and `debug` stays
readable:
- every entity's per-message `rx_prim: {:?}` dump: debug -> trace (phy/lmac/
umac/mle/mm/cmce/llc/sndcp, MS + BS).
- per-frame PDU decode dumps (MacSync/MacSysinfo/MacResource/AccessAssign/
AccessAssignFr18/DMleSync/DMleSysinfo `<- {:?}`): debug -> trace.
- per-frame `rx_broadcast_sysinfo: Updated TdmaTime`: info -> debug.
- per-burst PhyMs "queued uplink burst", Modulator(Ul) "pending burst vs TX
window", Modulator "uplink burst active part emitted (SN0)": info -> debug.
- Kept at INFO (low-rate, useful): startup/SDR-open/freqs, retune, downlink
synchronized, registration/cell-(re)selection/call/floor events, all warnings.
- example_config/bluestation-ms.service: Environment=RUST_LOG=info (documented
how to bump). Supervisor: export RUST_LOG="${RUST_LOG:-info}".
- docs/MS_CONFIG.md: new "Logging verbosity" section.
Acceptance: default config -> registered idle emits only occasional lines, no
"Suppressed N messages"; RUST_LOG=debug/trace restores full detail.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Field finding: with CPUSchedulingPolicy=fifo/prio 73 active and get_throttled=0x0,
the MS still flapped (serving-cell downlink failure / MLE-BREAK) whenever the
local Slint UI ran. The SXceiver (driver=sx / SoapySX) delivers samples over an
I2S/ALSA DMA path, and hard IRQ/softirq/kernel-DMA work preempts even a
SCHED_FIFO userspace thread — so real-time priority is necessary but not
sufficient; the fix is CPU isolation (keep the UI and routine IRQs off the
radio's cores).
- example_config/bluestation-ms-ui.service: sample unit for a co-located
operator UI, pinned to cores 0-1 (CPUAffinity=0 1, Nice=5) so it cannot
contend with the radio on cores 2-3.
- docs/MS_CONFIG.md: new "When FIFO priority alone is not enough" subsection —
the fast taskset confirmation test, kernel cmdline isolation
(isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3 irqaffinity=0,1), and steering the
I2S/DMA IRQ via /proc/irq/<n>/smp_affinity.
Deployment guidance only; not yet field-verified.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The MS is receive-timed: if the SDR RX pipeline is starved of CPU (e.g. a Slint
UI / desktop compositor sharing the Pi), samples are dropped ("Lost N samples,
skipping ..."), the demodulator loses sync, and the MLE declares a serving-cell
downlink failure (MLE-BREAK). The radio then flaps in/out of service and never
completes registration (T351 timeouts) even though RSSI is healthy and bursts
CRC-pass. Symptom: the MS connects fine until the UI is started.
Give the stack real-time priority so its RX thread preempts the UI, and pin it
to dedicated cores:
- example_config/bluestation-ms.service: add CPUSchedulingPolicy=fifo,
CPUSchedulingPriority=73, IOSchedulingClass=realtime, Nice=-10, LimitRTPRIO=99,
LimitMEMLOCK=infinity, CPUAffinity=2 3, and KillSignal=SIGINT. Comments explain
tuning (isolcpus, pinning the UI to the other cores) and that this is a
scheduling, not RF, problem.
- example_config/bluestation-ms-supervisor.sh: launch the binary under
`chrt -f $RT_PRIO` (default 73) and, when CPU_AFFINITY is set, `taskset -c`,
each best-effort with graceful fallback when the tool/privilege is missing.
- docs/MS_CONFIG.md: new "Real-time scheduling" section documenting the failure
signature and the fix (incl. UI-side guidance and vcgencmd get_throttled).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A TNCC-SETUP (and the TNSDS status / SDS-TL send paths) took a called/
destination SSI straight from the TN and wrote it into the 24-bit Called party
SSI element (cl. 14.8.28). A value > 0xFF_FFFF (e.g. 58555588) overflowed
BitBuffer::write_bits, which PANICS ("value exceeds num_bits 24") — taking the
whole radio down (exit 101, systemd restart loop) mid U-SETUP serialisation.
Per ETSI TS 100 392-2 cl. 7 an SSI/GSSI is a 24-bit identity (1..=16777215).
Such values are now validated at the CC/SDS TN boundary and refused with a
negative ack carrying the reason, so the TN is told the request is impossible
instead of the stack crashing.
- tncc_sap.rs: handle_tncc_setup_request rejects called_party_ssi outside
1..=0xFF_FFFF (covers individual/group/external origination) -> Err ->
TnccAck{accepted:false}.
- cmce_ms.rs: add MAX_SSI + ssi_out_of_range(); guard TnsdsUnitdata,
TnsdsStatus, TnsdsSendMessage and TnsdsSendReport -> negative TnsdsAck when
the destination SSI is out of range (these U-SDS-DATA/U-STATUS paths had the
same latent panic; to_bitbuf's Err guard does not catch write_bits panics).
- tests: cmce_ms_tncc_setup_out_of_range_ssi_rejected_without_panic and
cmce_ms_tnsds_status_out_of_range_ssi_rejected_without_panic — negative ack,
no PDU emitted, no panic.
- docs: MS_MODE.md notes the TN-request address validation.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The MS decoded the External subscriber number IE (cl. 14.8.20) off-air but
dropped it on the inbound path: every TnccSetupIndication / TnccTxIndication in
rx_d_* hardcoded external_subscriber_number* = None, so an external caller's or
talker's number (PABX/PSTN) never reached the TN even though the uplink path
already encodes it in U-SETUP.
- pdu.rs (cc_ms): add decode_external_subscriber_number() helper wrapping
external_subscriber_number::decode over the Option<Type3FieldGeneric>.
- rd.rs: populate external_subscriber_number_calling from the D-SETUP ESN in the
TNCC-SETUP indication, and external_subscriber_number from the source PDU's
ESN in the TNCC-TX indications synthesized from D-SETUP (group re-broadcast +
late-entry) and from D-TX-GRANTED / D-TX-INTERRUPT. D-TX-CEASED/WAIT/CONTINUE
carry no ESN field, so they stay None (correct).
- Made rx_d_tx_granted pub(in ...cc_ms) for unit testing (like rx_d_setup).
- Test: rx_downlink_external_subscriber_number_forwarded_to_tn feeds a D-SETUP
and a D-TX-GRANTED carrying an ESN and asserts both are surfaced to the TN;
added a d_tx_granted() test helper.
- docs: MS_MODE.md documents the inbound ESN -> TN direction.
No wire-schema change (reuses existing tncc indication fields).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Previously the MS only handled the uplink DTMF direction (TnccDtmf request ->
U-INFO). A DTMF type-3 element received in a downlink D-INFO (cl. 14.8.19,
Table 14.58) was parsed but silently dropped: TnccDtmfIndication was defined
but never constructed, and rx_d_info ignored pdu.dtmf. This wires the inbound
path to the TN.
- dtmf.rs (tetra-pdus): add DTMF_TYPE_NOT_SUPPORTED (0b010) and
DTMF_TYPE_NOT_SUBSCRIBED (0b011) type constants (Table 14.58).
- pdu.rs (cc_ms): tncc_dtmf_indication_from_ie() maps a decoded DTMF element to
a TNCC-DTMF indication (Table 11.3): tone-start -> delimiter=Dtmf + digits,
tone-end -> delimiter=ToneEnd, not-supported/not-subscribed -> dtmf_result;
reserved/unrecognised types return None (dropped). Plus dtmf_nibble_to_digit.
- rx_d_info (cc_ms/routes/rd.rs): after the notify indication, if pdu.dtmf is
present, emit TelemetryEvent::TnccDtmfIndication for the call. Made rx_d_info
pub(in ...cc_ms) so it is unit-testable like rx_d_setup.
- events.rs / codec.rs: new TnccDtmfIndication telemetry variant; added to the
bitcode/JSON round-trip and JSON schema-freeze golden tests.
- Interface schema bumped bluestation-ms-interface-5 -> -6 (new outbound
telemetry event); net_control golden + version-guard tests updated.
- Test: rx_d_info_dtmf_surfaces_tncc_dtmf_indication covers tone-start digits,
tone-end, not-supported result, and no-DTMF (no indication).
- docs: MS_MODE.md documents the downlink DTMF -> TnccDtmfIndication direction
and the schema bump.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The UI issues SetConfig followed by ApplyConfig unconditionally. After the
live-apply change, a codeplug/operational-only SetConfig applies live and
leaves restart_required = false — but ApplyConfig still bounced the radio
(exit 75 -> supervisor respawn), so the stack restarted anyway.
Fix: handle_apply_config now returns a success no-op when self.restart_required
is false — it does not set restart_requested/is_running and does not begin
de-registration. ApplyConfig only performs the drain-and-restart when a
structural change is actually staged. This makes the UI's "Save then Apply"
flow correct for codeplug-only edits (they already took effect live).
- mm_ms.rs: early no-op branch in handle_apply_config; updated the existing
apply-restart test to stage a structural change first; added
test_management_apply_config_noop_when_nothing_staged (registered MS, nothing
staged -> ApplyConfig accepted, no restart requested, loop keeps running, no
de-registration).
- docs: MS_MODE.md / MS_CONFIG.md note the ApplyConfig no-op semantics.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Plane B SetConfig now applies codeplug/operational-only changes live by
hot-swapping the whole StackConfig into the shared config, so contacts,
gateways, talkgroups, scan lists, home-display, folders, networks and
frequency lists take effect without bouncing the stack. Structural changes
(stack_mode / phy_io / net_info / cell_info / ms / duplex_table / control /
telemetry / brew) still stage to disk and require an ApplyConfig restart.
- config.rs: SharedConfig.cfg is now Arc<RwLock<Arc<StackConfig>>>; config()
reads via the lock and clones the inner Arc (signature unchanged, all call
sites untouched); added apply_config_live() that validates then atomically
swaps the inner Arc so every SharedConfig clone observes it on the next read.
- parsing.rs: is_operational_only_change() classifies a change by clearing the
codeplug on both configs and comparing the remainder as parsed toml::Value
(order-independent, so HashMap-backed fields like SoapySDR gains don't cause
false negatives). Tests for the classifier + live-swap-observed-by-clones.
- mm_ms.rs: handle_set_config validates + writes to disk, then classifies:
operational-only -> apply_config_live + ack restart_required=false; structural
-> stage + restart_required=true. Updated existing SetConfig tests to send a
structural change where a restart is asserted; added a codeplug-only live-apply
test (add contact -> accepted, no restart, running config reflects it live).
- docs: MS_MODE.md / MS_CONFIG.md apply-semantics sections rewritten to describe
the real live/structural split (and that the UI must send the current codeplug
schema shape; unknown keys are silently ignored).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
TETRA has no on-air PABX/PSTN distinction; both are ordinary external-number
calls routed by the SwMI on the gateway subscriber + digits. The kind field
(and CfgGatewayKind enum) was a cosmetic UI label only, so remove it to keep the
codeplug lean. A gateway is now just id + name + gateway_issi + optional prefix.
Updated CfgGateway/GatewayDto/converters, example config, MS_CONFIG.md, and the
contacts/PSTN UI prompt. Config suite 64 green.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Analysis: in MS mode the stack reads nothing functional from [cell_info] — RF is
learned over the air (SYNC/SYSINFO, cl. 18.4.2), location_area is only a
pre-registration seed (overwritten by the registration confirm), and colour_code
just fills a cosmetic state-snapshot field. So [cell_info] is not needed for MS.
- Parser: root cell_info is now Option (serde default). BS mode still requires
it (existing main_carrier/freq_band guard); MS may omit the whole section and
the serializer drops it from canonical MS output.
- Reorganize example_config/config-ms.toml: drop [cell_info]; group ALL codeplug
sections into one numbered region (settings -> networks -> frequencies ->
carrier overrides -> folders -> talkgroups -> scanlists -> gateways -> contacts).
- net_info stays a shared top-level home-network section (used by both BS and MS);
documented its relationship to the codeplug [[network]] allowed-networks list.
- Tests: MS config without any [cell_info] parses/validates; MS round-trip omits
[cell_info]. Config suite 64 green (shipped BS + MS examples parse+validate).
- Docs: MS_CONFIG.md net_info/cell_info sections rewritten.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add a scalar [codeplug] settings table with home_display_pid (8-bit SDS
protocol identifier, ETSI TS 100 392-2 cl. 29.4.3.9) for a 'home mode display'
status/text message. Codeplug data only (Plane B) — surfaced to the UI via the
existing GetConfig/SetConfig TOML round-trip; no on-air behaviour change.
- CfgCodeplug.home_display_pid: Option<u8> + is_empty + home_display_pid()
accessor; CodeplugSettingsDto ([codeplug] table) + codeplug_dto_to_cfg /
cfg_to_codeplug_settings_dto; parsing.rs root wiring.
- Example config [codeplug] section + MS_CONFIG.md docs.
- Test: settings round-trip via the DTO; config suite 62 green, shipped
example config parses+validates.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- DTMF information-element codec per ETSI TS 100 392-2 cl. 14.8.19 (Tables
14.56-14.58): 3-bit DTMF type + n<=254 4-bit digits (0-9 * # A-D), tone-start
vs tone-end, encode/decode + tests (tetra-pdus).
- CC-MS handle_tncc_dtmf: builds the DTMF IE and sends a U-INFO on an active
individual/duplex call, carried like the floor PDUs (FACCH-stolen from the TCH
half-slot once on a traffic channel, keyed on own ISSI; call correlated by the
Call identifier IE). Refused for group/unknown calls. UInfo gains UplinkPdu.
- ControlCommand::TnccDtmf { handle, call_identifier, request } + worker routing
to CMCE + cmce_ms dispatch with tncc_ack.
- Tests: DTMF IE round-trip; CC-MS tone-start/tone-end U-INFO decoded via the BS
UInfo parser; group/unknown-call rejection. tetra-entities lib 356, tetra-pdus
DTMF 4; bluestation-bs builds. Docs: MS_MODE.md 3.5.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
When a group call is announced to a listening MS with the transmission-grant element already reading 'granted to another user' (ETSI TS 100 392-2 cl. 14.8.31; identical semantics to D-TX-GRANTED), the MS is immediately receiving that party's speech (cl. 14.5.1.4). rx_d_setup raised only the one-shot TNCC-SETUP indication and never a TNCC-TX indication, so the UI (which derives the current talker + 'receiving' state from TNCC-TX) showed the floor as free with no talker until the next D-TX-GRANTED -- which the SwMI need not send while the same party keeps talking after our late entry.
The fresh-call group path now emits a TNCC-TX indication with the calling-party address as the transmitting party and the grant-derived transmission status, mirroring the existing re-broadcast path. Gated on grant == GrantedToOtherUser so an announcement with no active talker (NotGranted) does not fabricate one. Added fresh_group_dsetup_surfaces_talker_and_switches_uplane_on and fresh_group_dsetup_without_talker_raises_no_tx_indication. Updated docs/MS_MODE.md 3.5. 348 lib tests green; bluestation-bs builds.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
On a simplex circuit-mode call the transmitting party's receive U-plane is inactive (ETSI TS 100 392-2 cl. 14.5.1.4): while the MS holds the floor it transmits on the traffic channel and does not listen. The serving cell repeats the talker's own speech on the downlink (group talk-back); forwarding it to the UI echoed the operator's own voice back to them (observed on-air in ms.log call=11: 11 downlink speech frames forwarded while the MS held the floor).
rx_downlink_traffic now drops downlink TCH/S frames for a simplex call when tx_grant_state == GrantedSelf. Duplex calls are full-duplex and keep receiving the far end while transmitting.
Rewrote downlink_speech_gated_on_uplane_switch to grant to a listener (GrantedToOtherUser) so it still exercises U-plane on/off gating without colliding with the new suppression; added simplex_talkback_suppressed_while_holding_floor and duplex_still_receives_while_transmitting. Updated docs/MS_MODE.md 3.6. 346 lib tests green; bluestation-bs builds. No BS air-interface change.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Implements Short Data Service + status messaging for MS mode: Type-4 text and pre-coded status codes, RX and TX, over the standardized TNSDS-SAP (ETSI TS 100 392-2 v3.10.1 cl. 13.3). MS-only; no BS air-interface change.
- tetra-saps: new tnsds module (TNSDS-UNITDATA/STATUS request+indication structs, Tables 13.1/13.3); wire derives on SdsUserData.
- sds_ms.rs: rx_sds_data/rx_status emit TNSDS indications to the UI telemetry SAP; new send_u_sds_data/send_u_status build U-SDS-DATA (cl. 14.7.2.8) / U-STATUS (cl. 14.7.2.7) uplink via LCMC-SAP (acknowledged for ISSI, unacknowledged for GSSI).
- net_control/net_telemetry: TnsdsUnitdata/TnsdsStatus commands + TnsdsAck response, TnsdsUnitdataIndication/TnsdsStatusIndication events; worker routes them to CMCE; cmce_ms dispatch.
- schema bumped bluestation-ms-interface-3 -> -4; golden wire-format freeze test updated with TNSDS lines.
- Tests: 6 sds_ms unit tests round-tripping U-SDS-DATA/U-STATUS through the BS parsers and asserting exactly-one RX indication. 344 lib + 17 integration green; bluestation-bs builds.
- Docs: MS_MODE.md SDS/TNSDS rows -> software-tested; MS_CONFIG.md no new keys.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>