[feat](trx-rs): make spectrum affordable over a slow link
CI / lint (pull_request) Successful in 2m22s
CI / test (pull_request) Successful in 8m36s
CI / frontend (push) Successful in 3m38s
CI / reuse (push) Successful in 6s
CI / frontend (pull_request) Successful in 4m27s
CI / reuse (pull_request) Successful in 6s
CI / lint (push) Successful in 2m21s
CI / test (push) Successful in 7m48s

Spectrum dominates the server↔client connection, and all three things that
govern its cost were working against a poor link.

**It was polled, one round trip per frame.** The client asked for a frame every
50 ms on a dedicated connection and waited for the reply, so the frame rate was
capped at 1/RTT — on a 200 ms link, five frames a second no matter what was
configured.  Add SubscribeSpectrum alongside the existing SubscribeMeter: the
server pushes frames from a per-rig broadcast that rig_task fills only while
somebody is subscribed.  A server too old to know the command answers with an
error and leaves the connection usable, so the client falls back to polling on
the same connection without reconnecting.

**Bins were JSON floats.** 1024 bins spelled out as decimal text is around
10 KB a frame, ~200 KB/s at full rate — while the very next hop, client to
browser, already sends the same information as base64 i8 in about 1.4 KB.  Bins
now travel base64-encoded whole dBFS, the resolution the display draws at
anyway.  Decoding still accepts the old array form.

**Nothing was tunable.** [sdr].spectrum_fft_size and [sdr].spectrum_interval_ms
replace the compile-time FFT size and cadence; [[remotes]].spectrum_interval_ms
lets the client ask for less.  512 bins at 5 frames/s is roughly 3.5 KB/s
against roughly 200 KB/s before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit was merged in pull request #51.
This commit is contained in:
sjg
2026-08-07 00:15:50 +02:00
co-authored by Claude Opus 5
parent f396e8f235
commit b78c4a4dd4
23 changed files with 894 additions and 21 deletions
+3 -1
View File
@@ -18,4 +18,6 @@ pub use auth::{NoAuthValidator, SimpleTokenValidator, TokenValidator};
pub use codec::{mode_to_string, parse_envelope, parse_mode};
pub use decoders::{DecoderActivation, DecoderDescriptor, DECODER_REGISTRY};
pub use mapping::{client_command_to_rig, rig_command_to_client};
pub use types::{ClientCommand, ClientEnvelope, ClientResponse, MeterUpdate, RigEntry};
pub use types::{
ClientCommand, ClientEnvelope, ClientResponse, MeterUpdate, RigEntry, SpectrumFrame,
};
+1 -1
View File
@@ -102,7 +102,7 @@ macro_rules! define_command_mapping {
define_command_mapping! {
// ── Client-only variants (no RigCommand counterpart) ─────────────
client_only: GetRigs, GetSatPasses, SubscribeMeter;
client_only: GetRigs, GetSatPasses, SubscribeMeter, SubscribeSpectrum;
// ── Unit variants (no payload) ───────────────────────────────────
unit:
+22
View File
@@ -130,6 +130,28 @@ pub enum ClientCommand {
/// newline-delimited `MeterUpdate` JSON frames and no further commands or
/// regular responses are sent. Intended for a dedicated TCP connection.
SubscribeMeter,
/// Subscribe to a per-rig spectrum stream on this connection. Like
/// `SubscribeMeter`, the connection becomes a one-way flow of
/// newline-delimited `SpectrumFrame` JSON and no further commands or
/// regular responses are sent.
///
/// Polling `GetSpectrum` costs a round trip per frame, which caps the frame
/// rate at 1/RTT no matter how often the client asks; the server pushes at
/// its own cadence instead. Clients fall back to polling when the server
/// rejects this command.
SubscribeSpectrum,
}
/// One spectrum frame pushed by the server on a dedicated spectrum stream.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpectrumFrame {
/// Rig identifier this frame belongs to.
pub rig_id: String,
/// The frame itself; bins travel base64-encoded (see `spectrum_wire`).
pub spectrum: trx_core::rig::state::SpectrumData,
/// Virtual-channel RDS state, mirroring what `GetSpectrum` returned.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vchan_rds: Option<Vec<trx_core::rig::state::VchanRdsEntry>>,
}
/// Fast meter sample pushed by the server on a dedicated meter stream.