diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 98c1ee6..f5d8b7b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -929,13 +929,18 @@ function applyRigList(activeRigId, rigIds, displayNames) { aboutList.textContent = lastRigIds.length ? lastRigIds.join(", ") : "--"; } if (typeof activeRigId === "string" && activeRigId.length > 0) { - lastActiveRigId = activeRigId; + // Only adopt the server's active rig when this tab has no selection yet + // (first load). Otherwise keep the per-tab choice so other tabs' switches + // do not override ours. + if (!lastActiveRigId) { + lastActiveRigId = activeRigId; + } const aboutActive = document.getElementById("about-active-rig"); - if (aboutActive) aboutActive.textContent = activeRigId; + if (aboutActive) aboutActive.textContent = lastActiveRigId; } const disableSwitch = lastRigIds.length === 0 || !authRole || authRole === "rx"; - populateRigPicker(headerRigSwitchSelect, lastRigIds, activeRigId, disableSwitch); - updateRigSubtitle(activeRigId); + populateRigPicker(headerRigSwitchSelect, lastRigIds, lastActiveRigId, disableSwitch); + updateRigSubtitle(lastActiveRigId); if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId); if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId); updateMapRigFilter(); @@ -2750,11 +2755,9 @@ function render(update) { `trx-server hosted by ${safeCallsign}`; } } - // Detect rig switch and reset stale decoder state from the previous rig. - if (typeof update.active_rig_id === "string" && update.active_rig_id.length > 0 && update.active_rig_id !== lastActiveRigId) { - resetDecoderStateOnRigSwitch(); - } - updateRigSubtitle(update.active_rig_id); + // Note: rig switch decoder reset is now handled in switchRigFromSelect() + // so that other tabs' switches don't reset our state. + updateRigSubtitle(lastActiveRigId); if (ownerSubtitle) { if (ownerCallsign) { const safeOwner = escapeMapHtml(ownerCallsign); @@ -3298,6 +3301,11 @@ function scheduleUiFrameJob(key, job) { window.trxScheduleUiFrameJob = scheduleUiFrameJob; async function postPath(path) { + // Auto-append rig_id so each tab targets its own rig. + if (lastActiveRigId) { + const sep = path.includes("?") ? "&" : "?"; + path = `${path}${sep}rig_id=${encodeURIComponent(lastActiveRigId)}`; + } const resp = await fetch(path, { method: "POST" }); if (authEnabled && resp.status === 401) { // Not authenticated - return to login @@ -3341,6 +3349,12 @@ async function switchRigFromSelect(selectEl) { return; } selectEl.disabled = true; + // Set per-tab rig immediately so subsequent commands target the new rig. + const prevRig = lastActiveRigId; + lastActiveRigId = selectEl.value; + if (prevRig && prevRig !== lastActiveRigId) { + resetDecoderStateOnRigSwitch(); + } showHint("Switching rig…"); try { await postPath(`/select_rig?rig_id=${encodeURIComponent(selectEl.value)}`); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 4a4797c..09a06d4 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -769,6 +769,7 @@ pub async fn spectrum( #[post("/toggle_power")] pub async fn toggle_power( + query: web::Query, state: web::Data>, rig_tx: web::Data>, ) -> Result { @@ -778,33 +779,37 @@ pub async fn toggle_power( } else { RigCommand::PowerOff }; - send_command(&rig_tx, cmd).await + send_command(&rig_tx, cmd, query.into_inner().rig_id).await } #[post("/toggle_vfo")] pub async fn toggle_vfo( + query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::ToggleVfo).await + send_command(&rig_tx, RigCommand::ToggleVfo, query.into_inner().rig_id).await } #[post("/lock")] pub async fn lock_panel( + query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::Lock).await + send_command(&rig_tx, RigCommand::Lock, query.into_inner().rig_id).await } #[post("/unlock")] pub async fn unlock_panel( + query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::Unlock).await + send_command(&rig_tx, RigCommand::Unlock, query.into_inner().rig_id).await } #[derive(serde::Deserialize)] pub struct FreqQuery { pub hz: u64, + pub rig_id: Option, } #[post("/set_freq")] @@ -812,7 +817,8 @@ pub async fn set_freq( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetFreq(Freq { hz: query.hz })).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetFreq(Freq { hz: q.hz }), q.rig_id).await } #[post("/set_center_freq")] @@ -820,12 +826,19 @@ pub async fn set_center_freq( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetCenterFreq(Freq { hz: query.hz })).await + let q = query.into_inner(); + send_command( + &rig_tx, + RigCommand::SetCenterFreq(Freq { hz: q.hz }), + q.rig_id, + ) + .await } #[derive(serde::Deserialize)] pub struct ModeQuery { pub mode: String, + pub rig_id: Option, } #[post("/set_mode")] @@ -833,13 +846,15 @@ pub async fn set_mode( query: web::Query, rig_tx: web::Data>, ) -> Result { - let mode = parse_mode(&query.mode); - send_command(&rig_tx, RigCommand::SetMode(mode)).await + let q = query.into_inner(); + let mode = parse_mode(&q.mode); + send_command(&rig_tx, RigCommand::SetMode(mode), q.rig_id).await } #[derive(serde::Deserialize)] pub struct PttQuery { pub ptt: String, + pub rig_id: Option, } #[post("/set_ptt")] @@ -847,19 +862,21 @@ pub async fn set_ptt( query: web::Query, rig_tx: web::Data>, ) -> Result { - let ptt = match query.ptt.to_ascii_lowercase().as_str() { + let q = query.into_inner(); + let ptt = match q.ptt.to_ascii_lowercase().as_str() { "1" | "true" | "on" => Ok(true), "0" | "false" | "off" => Ok(false), other => Err(actix_web::error::ErrorBadRequest(format!( "invalid ptt parameter: {other}" ))), }?; - send_command(&rig_tx, RigCommand::SetPtt(ptt)).await + send_command(&rig_tx, RigCommand::SetPtt(ptt), q.rig_id).await } #[derive(serde::Deserialize)] pub struct TxLimitQuery { pub limit: u8, + pub rig_id: Option, } #[post("/set_tx_limit")] @@ -867,12 +884,14 @@ pub async fn set_tx_limit( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetTxLimit(query.limit)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetTxLimit(q.limit), q.rig_id).await } #[derive(serde::Deserialize)] pub struct BandwidthQuery { pub hz: u32, + pub rig_id: Option, } #[post("/set_bandwidth")] @@ -880,12 +899,14 @@ pub async fn set_bandwidth( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetBandwidth(query.hz)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetBandwidth(q.hz), q.rig_id).await } #[derive(serde::Deserialize)] pub struct SdrGainQuery { pub db: f64, + pub rig_id: Option, } #[post("/set_sdr_gain")] @@ -893,12 +914,14 @@ pub async fn set_sdr_gain( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetSdrGain(query.db)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetSdrGain(q.db), q.rig_id).await } #[derive(serde::Deserialize)] pub struct SdrLnaGainQuery { pub db: f64, + pub rig_id: Option, } #[post("/set_sdr_lna_gain")] @@ -906,12 +929,14 @@ pub async fn set_sdr_lna_gain( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetSdrLnaGain(query.db)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetSdrLnaGain(q.db), q.rig_id).await } #[derive(serde::Deserialize)] pub struct SdrAgcQuery { pub enabled: bool, + pub rig_id: Option, } #[post("/set_sdr_agc")] @@ -919,13 +944,15 @@ pub async fn set_sdr_agc( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetSdrAgc(query.enabled)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetSdrAgc(q.enabled), q.rig_id).await } #[derive(serde::Deserialize)] pub struct SdrSquelchQuery { pub enabled: bool, pub threshold_db: f64, + pub rig_id: Option, } #[post("/set_sdr_squelch")] @@ -933,12 +960,14 @@ pub async fn set_sdr_squelch( query: web::Query, rig_tx: web::Data>, ) -> Result { + let q = query.into_inner(); send_command( &rig_tx, RigCommand::SetSdrSquelch { - enabled: query.enabled, - threshold_db: query.threshold_db, + enabled: q.enabled, + threshold_db: q.threshold_db, }, + q.rig_id, ) .await } @@ -946,6 +975,7 @@ pub async fn set_sdr_squelch( #[derive(serde::Deserialize)] pub struct WfmDeemphasisQuery { pub us: u32, + pub rig_id: Option, } #[post("/set_wfm_deemphasis")] @@ -953,12 +983,14 @@ pub async fn set_wfm_deemphasis( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetWfmDeemphasis(query.us)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetWfmDeemphasis(q.us), q.rig_id).await } #[derive(serde::Deserialize)] pub struct WfmStereoQuery { pub enabled: bool, + pub rig_id: Option, } #[post("/set_wfm_stereo")] @@ -966,12 +998,14 @@ pub async fn set_wfm_stereo( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetWfmStereo(query.enabled)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetWfmStereo(q.enabled), q.rig_id).await } #[derive(serde::Deserialize)] pub struct WfmDenoiseQuery { pub level: WfmDenoiseLevel, + pub rig_id: Option, } #[post("/set_wfm_denoise")] @@ -979,39 +1013,59 @@ pub async fn set_wfm_denoise( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetWfmDenoise(query.level)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetWfmDenoise(q.level), q.rig_id).await } #[post("/toggle_aprs_decode")] pub async fn toggle_aprs_decode( + query: web::Query, state: web::Data>, rig_tx: web::Data>, ) -> Result { let enabled = state.get_ref().borrow().aprs_decode_enabled; - send_command(&rig_tx, RigCommand::SetAprsDecodeEnabled(!enabled)).await + send_command( + &rig_tx, + RigCommand::SetAprsDecodeEnabled(!enabled), + query.into_inner().rig_id, + ) + .await } #[post("/toggle_hf_aprs_decode")] pub async fn toggle_hf_aprs_decode( + query: web::Query, state: web::Data>, rig_tx: web::Data>, ) -> Result { let enabled = state.get_ref().borrow().hf_aprs_decode_enabled; - send_command(&rig_tx, RigCommand::SetHfAprsDecodeEnabled(!enabled)).await + send_command( + &rig_tx, + RigCommand::SetHfAprsDecodeEnabled(!enabled), + query.into_inner().rig_id, + ) + .await } #[post("/toggle_cw_decode")] pub async fn toggle_cw_decode( + query: web::Query, state: web::Data>, rig_tx: web::Data>, ) -> Result { let enabled = state.get_ref().borrow().cw_decode_enabled; - send_command(&rig_tx, RigCommand::SetCwDecodeEnabled(!enabled)).await + send_command( + &rig_tx, + RigCommand::SetCwDecodeEnabled(!enabled), + query.into_inner().rig_id, + ) + .await } #[derive(serde::Deserialize)] pub struct CwAutoQuery { pub enabled: bool, + pub rig_id: Option, } #[post("/set_cw_auto")] @@ -1019,12 +1073,14 @@ pub async fn set_cw_auto( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetCwAuto(query.enabled)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetCwAuto(q.enabled), q.rig_id).await } #[derive(serde::Deserialize)] pub struct CwWpmQuery { pub wpm: u32, + pub rig_id: Option, } #[post("/set_cw_wpm")] @@ -1032,12 +1088,14 @@ pub async fn set_cw_wpm( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetCwWpm(query.wpm)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetCwWpm(q.wpm), q.rig_id).await } #[derive(serde::Deserialize)] pub struct CwToneQuery { pub tone_hz: u32, + pub rig_id: Option, } #[post("/set_cw_tone")] @@ -1045,97 +1103,158 @@ pub async fn set_cw_tone( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetCwToneHz(query.tone_hz)).await + let q = query.into_inner(); + send_command(&rig_tx, RigCommand::SetCwToneHz(q.tone_hz), q.rig_id).await } #[post("/toggle_ft8_decode")] pub async fn toggle_ft8_decode( + query: web::Query, state: web::Data>, rig_tx: web::Data>, ) -> Result { let enabled = state.get_ref().borrow().ft8_decode_enabled; - send_command(&rig_tx, RigCommand::SetFt8DecodeEnabled(!enabled)).await + send_command( + &rig_tx, + RigCommand::SetFt8DecodeEnabled(!enabled), + query.into_inner().rig_id, + ) + .await } #[post("/toggle_ft4_decode")] pub async fn toggle_ft4_decode( + query: web::Query, state: web::Data>, rig_tx: web::Data>, ) -> Result { let enabled = state.get_ref().borrow().ft4_decode_enabled; - send_command(&rig_tx, RigCommand::SetFt4DecodeEnabled(!enabled)).await + send_command( + &rig_tx, + RigCommand::SetFt4DecodeEnabled(!enabled), + query.into_inner().rig_id, + ) + .await } #[post("/toggle_ft2_decode")] pub async fn toggle_ft2_decode( + query: web::Query, state: web::Data>, rig_tx: web::Data>, ) -> Result { let enabled = state.get_ref().borrow().ft2_decode_enabled; - send_command(&rig_tx, RigCommand::SetFt2DecodeEnabled(!enabled)).await + send_command( + &rig_tx, + RigCommand::SetFt2DecodeEnabled(!enabled), + query.into_inner().rig_id, + ) + .await } #[post("/toggle_wspr_decode")] pub async fn toggle_wspr_decode( + query: web::Query, state: web::Data>, rig_tx: web::Data>, ) -> Result { let enabled = state.get_ref().borrow().wspr_decode_enabled; - send_command(&rig_tx, RigCommand::SetWsprDecodeEnabled(!enabled)).await + send_command( + &rig_tx, + RigCommand::SetWsprDecodeEnabled(!enabled), + query.into_inner().rig_id, + ) + .await } #[post("/clear_ft8_decode")] pub async fn clear_ft8_decode( + query: web::Query, context: web::Data>, rig_tx: web::Data>, ) -> Result { crate::server::audio::clear_ft8_history(context.get_ref()); - send_command(&rig_tx, RigCommand::ResetFt8Decoder).await + send_command( + &rig_tx, + RigCommand::ResetFt8Decoder, + query.into_inner().rig_id, + ) + .await } #[post("/clear_ft4_decode")] pub async fn clear_ft4_decode( + query: web::Query, context: web::Data>, rig_tx: web::Data>, ) -> Result { crate::server::audio::clear_ft4_history(context.get_ref()); - send_command(&rig_tx, RigCommand::ResetFt4Decoder).await + send_command( + &rig_tx, + RigCommand::ResetFt4Decoder, + query.into_inner().rig_id, + ) + .await } #[post("/clear_ft2_decode")] pub async fn clear_ft2_decode( + query: web::Query, context: web::Data>, rig_tx: web::Data>, ) -> Result { crate::server::audio::clear_ft2_history(context.get_ref()); - send_command(&rig_tx, RigCommand::ResetFt2Decoder).await + send_command( + &rig_tx, + RigCommand::ResetFt2Decoder, + query.into_inner().rig_id, + ) + .await } #[post("/clear_wspr_decode")] pub async fn clear_wspr_decode( + query: web::Query, context: web::Data>, rig_tx: web::Data>, ) -> Result { crate::server::audio::clear_wspr_history(context.get_ref()); - send_command(&rig_tx, RigCommand::ResetWsprDecoder).await + send_command( + &rig_tx, + RigCommand::ResetWsprDecoder, + query.into_inner().rig_id, + ) + .await } #[post("/clear_aprs_decode")] pub async fn clear_aprs_decode( + query: web::Query, context: web::Data>, rig_tx: web::Data>, ) -> Result { crate::server::audio::clear_aprs_history(context.get_ref()); - send_command(&rig_tx, RigCommand::ResetAprsDecoder).await + send_command( + &rig_tx, + RigCommand::ResetAprsDecoder, + query.into_inner().rig_id, + ) + .await } #[post("/clear_hf_aprs_decode")] pub async fn clear_hf_aprs_decode( + query: web::Query, context: web::Data>, rig_tx: web::Data>, ) -> Result { crate::server::audio::clear_hf_aprs_history(context.get_ref()); - send_command(&rig_tx, RigCommand::ResetHfAprsDecoder).await + send_command( + &rig_tx, + RigCommand::ResetHfAprsDecoder, + query.into_inner().rig_id, + ) + .await } #[post("/clear_ais_decode")] @@ -1156,11 +1275,17 @@ pub async fn clear_vdes_decode( #[post("/clear_cw_decode")] pub async fn clear_cw_decode( + query: web::Query, context: web::Data>, rig_tx: web::Data>, ) -> Result { crate::server::audio::clear_cw_history(context.get_ref()); - send_command(&rig_tx, RigCommand::ResetCwDecoder).await + send_command( + &rig_tx, + RigCommand::ResetCwDecoder, + query.into_inner().rig_id, + ) + .await } // ============================================================================ @@ -1916,16 +2041,23 @@ async fn vchan_js() -> impl Responder { .body(status::VCHAN_JS) } +/// Generic query extractor for endpoints that only need the optional rig_id. +#[derive(serde::Deserialize)] +pub struct RigIdQuery { + pub rig_id: Option, +} + async fn send_command( rig_tx: &mpsc::Sender, cmd: RigCommand, + rig_id: Option, ) -> Result { let (resp_tx, resp_rx) = oneshot::channel(); rig_tx .send(RigRequest { cmd, respond_to: resp_tx, - rig_id_override: None, + rig_id_override: rig_id, }) .await .map_err(|e| {