Commit Graph
31 Commits
Author SHA1 Message Date
misadeksandCopilot App 63f9114953 docs: link MMI UI in README, fix MS getting-started TX-gain wording
- 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>
2026-08-15 16:51:33 +02:00
misadeksandCopilot App 1e0b01cd8a docs: add MS-mode Getting Started guide (noob-friendly)
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>
2026-08-15 16:45:29 +02:00
misadeksandCopilot App d9b79ad17d MS UI link: fast reconnect (1s) to the control/telemetry WebSocket
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>
2026-08-09 01:36:26 +02:00
misadeksandCopilot App fb0e93e49e Quiet the journal: default RUST_LOG=info + demote per-frame/per-burst logs
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>
2026-08-09 01:31:11 +02:00
misadeksandCopilot App d575cb3771 MS deploy: core isolation guidance + sample UI unit (FIFO alone insufficient)
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>
2026-08-07 00:53:29 +02:00
misadeksandCopilot App cce4d7b31c MS deploy: real-time scheduling + CPU affinity so a co-located UI can't starve RX
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>
2026-08-07 00:40:33 +02:00
misadeksandCopilot App fe35ec11ff MS: reject out-of-range called/destination SSI instead of crashing
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>
2026-08-02 20:15:52 +02:00
misadeksandCopilot App d8ffe6f9c2 MS: forward received external subscriber number to the TN
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>
2026-08-02 18:00:29 +02:00
misadeksandCopilot App eba3872011 MS: surface inbound DTMF from downlink D-INFO as TnccDtmfIndication
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>
2026-08-02 17:04:07 +02:00
misadeksandCopilot App e4e8d39379 MS: ApplyConfig is a no-op when nothing structural is staged
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>
2026-08-02 16:27:11 +02:00
misadeksandCopilot App 4945508975 MS: true live apply for codeplug/operational config changes (no restart)
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>
2026-08-02 16:19:17 +02:00
misadeksandCopilot App 99d70bf04e refactor(ms): drop gateway kind (PABX/PSTN) — no functional purpose
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>
2026-08-02 15:26:22 +02:00
misadeksandCopilot App 30c0194b32 refactor(ms config): make [cell_info] optional for MS; reorganize codeplug
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>
2026-08-02 15:19:53 +02:00
misadeksandCopilot App 8ad3615701 refactor(ms): home_display becomes a toggleable [codeplug.home_display] setting
Replace the flat codeplug home_display_pid with a dedicated
[codeplug.home_display] sub-table carrying enabled (bool) + pid (u8, SDS
protocol identifier cl. 29.4.3.9, default 130). Codeplug data only; surfaced to
the UI via the GetConfig/SetConfig TOML round-trip.

- CfgHomeDisplay { enabled, pid } (+ Default), CfgCodeplug.home_display: Option;
  home_display() accessor; is_empty updated.
- CodeplugSettingsDto.home_display: HomeDisplayDto (enabled/pid, pid defaults 130);
  codeplug_dto_to_cfg / cfg_to_codeplug_settings_dto round-trip.
- Example config [codeplug.home_display] + MS_CONFIG.md docs.
- Tests: settings round-trip + pid-defaulting; config suite 63 green, shipped
  example parses+validates.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-02 15:09:55 +02:00
misadeksandCopilot App 00b960c242 feat(ms): add codeplug home_display_pid setting
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>
2026-08-02 13:29:34 +02:00
misadeksandCopilot App f898f120b7 feat(ms): SDS-TL transport — message reference + delivery/read reports (cl. 29)
- SDS-TL codec (tetra-pdus/cmce/fields/sds_tl.rs): SDS-TRANSFER / SDS-REPORT /
  SDS-ACK / SDS-SHORT-REPORT encode+decode per cl. 29.4 (byte-aligned; no
  storage/forward, no forward-address), PI 0x82 text messaging, delivery-status /
  delivery-report-request / short-report-type per Tables 29.16/29.17/29.23.
- TNSDS SAP (tetra-saps/tnsds): TnsdsMessageRequest/Indication,
  TnsdsReportRequest/Indication, TnsdsCancelRequest, DeliveryReportRequest enum.
- SDS-TL engine (sds_ms.rs): TX SDS-TRANSFER with a message reference + delivery-
  report request (outstanding table); RX transfer -> TnsdsMessageIndication and an
  auto 'received' SDS-REPORT for individually-addressed inbound; RX report/ack/
  short-report -> TnsdsReportIndication (clears tracking, sends SDS-ACK if required);
  UI-driven consumed report; TNSDS-CANCEL; BUSY/CLOSE no longer panic.
- Control: TnsdsSendMessage / TnsdsSendReport / TnsdsCancel (routed to CMCE).
  Telemetry: TnsdsMessageIndication / TnsdsReportIndication. Interface schema
  bumped bluestation-ms-interface-4 -> -5.
- Tests: SDS-TL codec round-trips; sds_ms engine (transfer/report/short-report/
  auto-ack/cancel) decoded via the BS parsers. entities lib 362, pdus 46, saps,
  config green; test_cmce_ms 9; bluestation-bs builds. Docs: MS_MODE.md 3.5/3.7.

Message concatenation (long multi-part, cl. 29.5.14) deferred.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-02 12:17:53 +02:00
misadeksandCopilot App d622ff7ce6 feat(ms): in-call DTMF (U-INFO / DTMF IE)
- 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>
2026-08-02 02:42:08 +02:00
misadeksandCopilot App 847344f710 feat(ms): contacts + PABX/PSTN gateway calls; fix group-call first-transmission silence
Contacts & gateways (codeplug, Plane B):
- CfgContact (name, optional callsign, ISSI or phone target) and CfgGateway
  (PABX/PSTN kind, gateway_issi, optional access-code prefix) in sec_codeplug.rs,
  with DTOs, both-direction converters, validation (24-bit ISSI, dangling-gateway
  refs, dial-digit set, prefix+number <= 24-digit IE limit, unique ids/names) and
  resolve_phone_contact(). Wired into parsing.rs + example config.

External (PABX/PSTN) call origination:
- External subscriber number IE codec per ETSI TS 100 392-2 cl. 14.8.20 / Table
  14.59 (4-bit digits, n <= 24, set 0-9 * # +) in tetra-pdus.
- send_u_setup carries the IE; originate_external_call addresses the gateway ISSI
  (CPTI=SSI) and encloses the dialled digits; handle_tncc_setup_request routes a
  TnccSetupRequest whose external_subscriber_number_called is set. No on-air
  PABX/PSTN flag exists (spec); the distinction is codeplug/UI only.

Group-call first-transmission fix (cl. 14.5.1.4 / 14.8.31):
- resolve_floor_grant() normalises a Granted(0) naming a different subscriber to
  GrantedToOtherUser so the simplex self-echo guard no longer suppresses a remote
  talker's first spurt; telemetry decision points in rd.rs use the resolved grant.

Tests: external-subscriber-number round-trip; CC-MS external-call U-SETUP decoded
via the BS parser; contact/gateway validation + resolve; grant-normalisation
regressions. tetra-config 61, tetra-pdus 38, tetra-entities lib 353 +
test_cmce_ms/test_umac_ms 17; bluestation-bs builds. Docs: MS_MODE.md + MS_CONFIG.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-02 02:00:30 +02:00
misadeksandCopilot App f8b924948f MS: surface floor/talker on fresh incoming group D-SETUP
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>
2026-08-01 21:12:17 +02:00
misadeksandCopilot App 699d2a29b2 MS: suppress simplex voice self-echo while holding the floor
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>
2026-08-01 20:25:51 +02:00
misadeksandCopilot App dddc460c4d MS SDS/status messaging via TNSDS-SAP (cl. 13.3)
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>
2026-08-01 12:07:15 +02:00
misadeksandCopilot App 989b9ea831 MS: de-register when entering manual selection or starting a survey while registered
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>
2026-07-26 12:58:51 +02:00
misadeksandCopilot App b41ac931c0 fix(ms): survey advances off carriers that lock an undecodable signal
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>
2026-07-25 19:40:08 +02:00
misadeksandCopilot App 97412c8822 feat(ms): scan carrier offsets in frequency_list Range mode
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>
2026-07-25 19:29:27 +02:00
misadeksandCopilot App 96a75b4b1d docs(ms): fix frequency_list range to single-bracket [frequency_list.range]
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>
2026-07-25 19:16:51 +02:00
misadeksandCopilot App e13de82c10 MS: UI-driven manual cell survey + register-to-cell (P1-P4)
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>
2026-07-25 18:43:02 +02:00
misadeksandCopilot App 4781f1b345 fix(ms): yield unsynchronized RX loop so offline config servicing runs before sync
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>
2026-07-25 17:54:31 +02:00
misadeksandCopilot App c732045c2a MS: service management config read/staging before synchronization
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>
2026-07-25 17:23:34 +02:00
misadeksandCopilot App 5960b24f70 fix(ms): exit promptly on Ctrl+C when not synchronized to a BS
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>
2026-07-25 02:04:37 +02:00
misadeksandCopilot App 165d70491d docs: reference the tetra-tn-web-ui companion UI repo
Link https://github.com/misadeks/tetra-tn-web-ui (the portable-radio web UI /
codeplug programming / ACELP vocoder client that drives the MS stack) from the
MS-mode README architecture section and the root README.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-24 23:59:12 +02:00
misadeksandCopilot App 3565f395a5 docs: add MS-mode README + config reference
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>
2026-07-24 23:56:56 +02:00