[feat](trx-rs): add FT4 decoder support

Reuse the existing ft8_lib C library (FTX_PROTOCOL_FT4) and FT8
decoder infrastructure to add FT4 decoding across the full stack.

Changes:
- trx-ft8: add protocol param to ft8_decoder_create; add Ft8Decoder::new_ft4()
- trx-core: DecodedMessage::Ft4 variant, AUDIO_MSG_FT4_DECODE (0x14),
  ft4_decode_enabled/ft4_decode_reset_seq state, SetFt4DecodeEnabled/
  ResetFt4Decoder commands, protocol mapping
- trx-server: DecoderHistories::ft4, run_ft4_decoder (7.5s slots via
  now*2/15), run_background_ft4_decoder, history push/replay, decoder
  task spawn
- trx-frontend-http: ft4_history in FrontendRuntimeContext,
  toggle/clear endpoints, /ft4.js route, bookmark/scheduler/background
  decode support, DecodeHistoryPayload ft4 field
- web: ft4.js plugin (7.5s period timer, reuses FT8 CSS/map infra),
  FT4 subtab in index.html, app.js dispatch (onServerFt4/Batch,
  restoreFt4History), decode-history-worker HISTORY_GROUP_KEYS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-03-14 18:50:08 +01:00
parent a1b9b7f762
commit 8eae376c56
25 changed files with 676 additions and 15 deletions
+2
View File
@@ -62,6 +62,8 @@ pub const AUDIO_MSG_VCHAN_DESTROYED: u8 = 0x12;
/// Client → server: update the audio filter bandwidth of an existing virtual channel.
/// Payload: JSON `{"uuid": "<uuid>", "bandwidth_hz": <u32>}`.
pub const AUDIO_MSG_VCHAN_BW: u8 = 0x13;
/// Server → client: FT4 decoded message (JSON `DecodedMessage::Ft4`).
pub const AUDIO_MSG_FT4_DECODE: u8 = 0x14;
/// Maximum payload size for normal messages (1 MB).
const MAX_PAYLOAD_SIZE: u32 = 1_048_576;
+2
View File
@@ -22,6 +22,8 @@ pub enum DecodedMessage {
Cw(CwEvent),
#[serde(rename = "ft8")]
Ft8(Ft8Message),
#[serde(rename = "ft4")]
Ft4(Ft8Message),
#[serde(rename = "wspr")]
Wspr(WsprMessage),
}
+2
View File
@@ -28,11 +28,13 @@ pub enum RigCommand {
SetCwWpm(u32),
SetCwToneHz(u32),
SetFt8DecodeEnabled(bool),
SetFt4DecodeEnabled(bool),
SetWsprDecodeEnabled(bool),
ResetAprsDecoder,
ResetHfAprsDecoder,
ResetCwDecoder,
ResetFt8Decoder,
ResetFt4Decoder,
ResetWsprDecoder,
SetBandwidth(u32),
SetFirTaps(u32),
@@ -509,12 +509,14 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box<dyn RigCommandHandler> {
| RigCommand::SetCwWpm(_)
| RigCommand::SetCwToneHz(_)
| RigCommand::SetFt8DecodeEnabled(_)
| RigCommand::SetFt4DecodeEnabled(_)
| RigCommand::SetWsprDecodeEnabled(_)
| RigCommand::SetHfAprsDecodeEnabled(_)
| RigCommand::ResetHfAprsDecoder
| RigCommand::ResetAprsDecoder
| RigCommand::ResetCwDecoder
| RigCommand::ResetFt8Decoder
| RigCommand::ResetFt4Decoder
| RigCommand::ResetWsprDecoder
| RigCommand::SetBandwidth(_)
| RigCommand::SetFirTaps(_)
+11
View File
@@ -38,6 +38,8 @@ pub struct RigState {
#[serde(default)]
pub ft8_decode_enabled: bool,
#[serde(default)]
pub ft4_decode_enabled: bool,
#[serde(default)]
pub wspr_decode_enabled: bool,
#[serde(default)]
pub cw_auto: bool,
@@ -66,6 +68,8 @@ pub struct RigState {
#[serde(default, skip_serializing)]
pub ft8_decode_reset_seq: u64,
#[serde(default, skip_serializing)]
pub ft4_decode_reset_seq: u64,
#[serde(default, skip_serializing)]
pub wspr_decode_reset_seq: u64,
}
@@ -143,6 +147,7 @@ impl RigState {
hf_aprs_decode_enabled: false,
cw_decode_enabled: true,
ft8_decode_enabled: false,
ft4_decode_enabled: false,
wspr_decode_enabled: false,
cw_auto: true,
cw_wpm: 15,
@@ -154,6 +159,7 @@ impl RigState {
hf_aprs_decode_reset_seq: 0,
cw_decode_reset_seq: 0,
ft8_decode_reset_seq: 0,
ft4_decode_reset_seq: 0,
wspr_decode_reset_seq: 0,
}
}
@@ -210,6 +216,7 @@ impl RigState {
cw_wpm: snapshot.cw_wpm,
cw_tone_hz: snapshot.cw_tone_hz,
ft8_decode_enabled: snapshot.ft8_decode_enabled,
ft4_decode_enabled: snapshot.ft4_decode_enabled,
wspr_decode_enabled: snapshot.wspr_decode_enabled,
filter: snapshot.filter,
spectrum: None, // spectrum flows through /api/spectrum, not persistent state
@@ -218,6 +225,7 @@ impl RigState {
hf_aprs_decode_reset_seq: 0,
cw_decode_reset_seq: 0,
ft8_decode_reset_seq: 0,
ft4_decode_reset_seq: 0,
wspr_decode_reset_seq: 0,
}
}
@@ -252,6 +260,7 @@ impl RigState {
cw_wpm: self.cw_wpm,
cw_tone_hz: self.cw_tone_hz,
ft8_decode_enabled: self.ft8_decode_enabled,
ft4_decode_enabled: self.ft4_decode_enabled,
wspr_decode_enabled: self.wspr_decode_enabled,
filter: self.filter.clone(),
spectrum: self.spectrum.clone(),
@@ -416,6 +425,8 @@ pub struct RigSnapshot {
#[serde(default)]
pub ft8_decode_enabled: bool,
#[serde(default)]
pub ft4_decode_enabled: bool,
#[serde(default)]
pub wspr_decode_enabled: bool,
#[serde(default)]
pub cw_auto: bool,