diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs index 358606b..26df369 100644 --- a/src/trx-server/src/rig_task.rs +++ b/src/trx-server/src/rig_task.rs @@ -1400,4 +1400,298 @@ mod tests { assert_eq!(state.reset_seqs.ft2_decode_reset_seq, 7); assert_eq!(state.reset_seqs.wspr_decode_reset_seq, 8); } + + #[test] + fn usb_freq_change_invalidates_ftx_and_wspr_only() { + let mut state = RigState::new_uninitialized(); + state.apply_mode(RigMode::USB); + let prev_freq_hz = state.status.freq.hz; + state.apply_freq(Freq { + hz: prev_freq_hz + 1_500, + }); + + invalidate_main_decoder_windows_on_freq_change(&mut state, prev_freq_hz); + + // No effect on PKT, HF-APRS, CW. + assert_eq!(state.reset_seqs.aprs_decode_reset_seq, 0); + assert_eq!(state.reset_seqs.hf_aprs_decode_reset_seq, 0); + assert_eq!(state.reset_seqs.cw_decode_reset_seq, 0); + // FT8/FT4/FT2/WSPR all bumped. + assert_eq!(state.reset_seqs.ft8_decode_reset_seq, 1); + assert_eq!(state.reset_seqs.ft4_decode_reset_seq, 1); + assert_eq!(state.reset_seqs.ft2_decode_reset_seq, 1); + assert_eq!(state.reset_seqs.wspr_decode_reset_seq, 1); + } + + #[test] + fn cw_freq_change_invalidates_cw_only() { + for mode in [RigMode::CW, RigMode::CWR] { + let mut state = RigState::new_uninitialized(); + state.apply_mode(mode.clone()); + let prev_freq_hz = state.status.freq.hz; + state.apply_freq(Freq { + hz: prev_freq_hz + 100, + }); + + invalidate_main_decoder_windows_on_freq_change(&mut state, prev_freq_hz); + + assert_eq!(state.reset_seqs.cw_decode_reset_seq, 1, "mode={:?}", mode); + assert_eq!(state.reset_seqs.aprs_decode_reset_seq, 0); + assert_eq!(state.reset_seqs.hf_aprs_decode_reset_seq, 0); + assert_eq!(state.reset_seqs.ft8_decode_reset_seq, 0); + assert_eq!(state.reset_seqs.ft4_decode_reset_seq, 0); + assert_eq!(state.reset_seqs.ft2_decode_reset_seq, 0); + assert_eq!(state.reset_seqs.wspr_decode_reset_seq, 0); + } + } + + #[test] + fn am_lsb_fm_freq_change_does_not_touch_main_decoders() { + // Modes the freq-change table intentionally ignores: any change leaves + // every decoder reset_seq untouched. + for mode in [RigMode::AM, RigMode::LSB, RigMode::FM, RigMode::SAM] { + let mut state = RigState::new_uninitialized(); + state.apply_mode(mode.clone()); + state.reset_seqs.cw_decode_reset_seq = 99; + state.reset_seqs.ft8_decode_reset_seq = 99; + let prev_freq_hz = state.status.freq.hz; + state.apply_freq(Freq { + hz: prev_freq_hz + 5_000, + }); + + invalidate_main_decoder_windows_on_freq_change(&mut state, prev_freq_hz); + + assert_eq!(state.reset_seqs.cw_decode_reset_seq, 99, "mode={:?}", mode); + assert_eq!(state.reset_seqs.ft8_decode_reset_seq, 99, "mode={:?}", mode); + } + } + + #[test] + fn map_signal_strength_uses_fm_offset_for_fm_modes() { + // FM-family modes start at -120 dBm at raw=0; +6 dBm per raw step. + for mode in [RigMode::FM, RigMode::WFM, RigMode::AIS, RigMode::VDES] { + assert_eq!(map_signal_strength(&mode, 0), -120.0, "mode={:?}", mode); + assert_eq!(map_signal_strength(&mode, 5), -90.0, "mode={:?}", mode); + assert_eq!(map_signal_strength(&mode, 15), -30.0, "mode={:?}", mode); + } + } + + #[test] + fn map_signal_strength_uses_default_offset_for_ssb_and_cw() { + // Everything else starts at -127 dBm at raw=0. + for mode in [ + RigMode::USB, + RigMode::LSB, + RigMode::CW, + RigMode::CWR, + RigMode::AM, + RigMode::SAM, + RigMode::DIG, + RigMode::PKT, + ] { + assert_eq!(map_signal_strength(&mode, 0), -127.0, "mode={:?}", mode); + assert_eq!(map_signal_strength(&mode, 1), -121.0, "mode={:?}", mode); + assert_eq!(map_signal_strength(&mode, 15), -37.0, "mode={:?}", mode); + } + } + + #[test] + fn lock_state_falls_back_to_status_when_no_control_override() { + let mut state = RigState::new_uninitialized(); + state.control.lock = None; + state.status.lock = Some(true); + assert!(lock_state_from(&state)); + state.status.lock = Some(false); + assert!(!lock_state_from(&state)); + } + + #[test] + fn lock_state_control_override_takes_precedence() { + let mut state = RigState::new_uninitialized(); + state.control.lock = Some(false); + state.status.lock = Some(true); + // Control wins over status. + assert!(!lock_state_from(&state)); + state.control.lock = Some(true); + state.status.lock = Some(false); + assert!(lock_state_from(&state)); + } + + #[test] + fn lock_state_defaults_to_false_when_neither_set() { + let mut state = RigState::new_uninitialized(); + state.control.lock = None; + state.status.lock = None; + assert!(!lock_state_from(&state)); + } + + #[test] + fn tx_meter_parts_returns_quad_of_nones_when_no_tx_status() { + assert_eq!(tx_meter_parts(None), (None, None, None, None)); + } + + #[test] + fn tx_meter_parts_extracts_fields_in_order() { + let tx = RigTxStatus { + power: Some(50), + limit: Some(100), + swr: Some(1.5), + alc: Some(7), + }; + assert_eq!( + tx_meter_parts(Some(&tx)), + (Some(50), Some(100), Some(1.5), Some(7)) + ); + } + + #[test] + fn meters_changed_detects_rx_signal_change() { + let mut a = RigState::new_uninitialized(); + let mut b = a.clone(); + a.status.rx = Some(RigRxStatus { sig: Some(-90.0) }); + b.status.rx = Some(RigRxStatus { sig: Some(-50.0) }); + assert!(meters_changed(&a, &b)); + } + + #[test] + fn meters_changed_detects_tx_power_change() { + let mut a = RigState::new_uninitialized(); + let mut b = a.clone(); + a.status.tx = Some(RigTxStatus { + power: Some(10), + limit: None, + swr: None, + alc: None, + }); + b.status.tx = Some(RigTxStatus { + power: Some(80), + limit: None, + swr: None, + alc: None, + }); + assert!(meters_changed(&a, &b)); + } + + #[test] + fn meters_changed_false_when_unchanged() { + let mut a = RigState::new_uninitialized(); + a.status.rx = Some(RigRxStatus { sig: Some(-70.0) }); + a.status.tx = Some(RigTxStatus { + power: Some(25), + limit: Some(100), + swr: Some(1.2), + alc: None, + }); + let b = a.clone(); + assert!(!meters_changed(&a, &b)); + } + + fn sample_rig_info() -> trx_core::rig::RigInfo { + use trx_core::radio::freq::Band; + use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo}; + RigInfo { + manufacturer: "Test".into(), + model: "Dummy".into(), + revision: "0".into(), + capabilities: RigCapabilities { + min_freq_step_hz: 1, + supported_bands: vec![Band { + low_hz: 7_000_000, + high_hz: 7_200_000, + tx_allowed: true, + }], + supported_modes: vec![RigMode::USB], + num_vfos: 1, + lock: false, + lockable: true, + attenuator: false, + preamp: false, + rit: false, + rpt: false, + split: false, + tx: true, + tx_limit: false, + vfo_switch: false, + filter_controls: false, + signal_meter: true, + }, + access: RigAccessMethod::Tcp { + addr: "127.0.0.1:0".into(), + }, + } + } + + #[test] + fn desired_machine_state_disconnected_when_no_rig_info() { + let state = RigState::new_uninitialized(); + assert!(state.rig_info.is_none()); + assert!(matches!( + desired_machine_state(&state), + RigMachineState::Disconnected + )); + } + + #[test] + fn desired_machine_state_initializing_when_rig_info_but_not_initialized() { + let mut state = RigState::new_uninitialized(); + state.rig_info = Some(sample_rig_info()); + // initialized stays false. + assert!(matches!( + desired_machine_state(&state), + RigMachineState::Initializing { rig_info: Some(_) } + )); + } + + #[test] + fn desired_machine_state_powered_off_when_control_disabled() { + let mut state = RigState::new_uninitialized(); + state.rig_info = Some(sample_rig_info()); + state.initialized = true; + state.control.enabled = Some(false); + assert!(matches!( + desired_machine_state(&state), + RigMachineState::PoweredOff { .. } + )); + } + + #[test] + fn desired_machine_state_ready_when_initialized_and_not_transmitting() { + let mut state = RigState::new_uninitialized(); + state.rig_info = Some(sample_rig_info()); + state.initialized = true; + // Default control.enabled is Some(false) → PoweredOff. Lift it to make + // the ready path observable. + state.control.enabled = Some(true); + state.status.tx_en = false; + assert!(matches!( + desired_machine_state(&state), + RigMachineState::Ready(_) + )); + } + + #[test] + fn desired_machine_state_transmitting_when_tx_en() { + let mut state = RigState::new_uninitialized(); + state.rig_info = Some(sample_rig_info()); + state.initialized = true; + state.control.enabled = Some(true); + state.status.tx_en = true; + assert!(matches!( + desired_machine_state(&state), + RigMachineState::Transmitting(_) + )); + } + + #[test] + fn desired_machine_state_powered_off_is_default_for_initialized_rig() { + // Documents the default-Some(false) behaviour of RigControl::enabled. + let mut state = RigState::new_uninitialized(); + state.rig_info = Some(sample_rig_info()); + state.initialized = true; + // control.enabled stays at its default Some(false). + assert!(matches!( + desired_machine_state(&state), + RigMachineState::PoweredOff { .. } + )); + } }