kenwood_thd75/radio/
tuning.rs

1//! Safe high-level tuning APIs with automatic mode management.
2//!
3//! These methods handle VFO/Memory mode switching automatically, so callers
4//! do not need to worry about the radio being in the wrong mode. They are
5//! the recommended way to change frequencies and channels.
6
7use crate::error::{Error, ProtocolError};
8use crate::transport::Transport;
9use crate::types::{Band, Frequency, Mode, StepSize};
10
11use super::{Radio, RadioMode};
12
13impl<T: Transport> Radio<T> {
14    /// Tune a band to a specific frequency.
15    ///
16    /// Automatically switches to VFO mode if needed, sets the frequency,
17    /// and verifies the change. This is the safe way to change frequencies.
18    ///
19    /// # Errors
20    ///
21    /// Returns an error if the mode switch, frequency set, or verification
22    /// read fails.
23    pub async fn tune_frequency(&mut self, band: Band, freq: Frequency) -> Result<(), Error> {
24        tracing::info!(?band, hz = freq.as_hz(), "tuning to frequency");
25
26        // Ensure VFO mode.
27        self.ensure_mode(band, RadioMode::Vfo).await?;
28
29        // Read current channel data so we can preserve settings (step, shift, etc.)
30        let mut channel = self.get_frequency_full(band).await?;
31        channel.rx_frequency = freq;
32
33        // Write the updated frequency.
34        self.set_frequency_full(band, &channel).await?;
35
36        // Verify.
37        let readback = self.get_frequency(band).await?;
38        if readback.rx_frequency != freq {
39            tracing::warn!(
40                expected = freq.as_hz(),
41                actual = readback.rx_frequency.as_hz(),
42                "frequency readback mismatch"
43            );
44        }
45
46        Ok(())
47    }
48
49    /// Tune a band to a memory channel by number.
50    ///
51    /// Automatically switches to memory mode if needed and recalls
52    /// the channel. Verifies the channel is populated by reading it
53    /// first.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`Error::RadioError`] if the channel number is out of range
58    /// or the channel is empty. Returns transport/protocol errors on
59    /// communication failure.
60    pub async fn tune_channel(&mut self, band: Band, channel: u16) -> Result<(), Error> {
61        tracing::info!(?band, channel, "tuning to memory channel");
62
63        // Verify the channel exists and is populated by trying to read it.
64        let ch_data = self.read_channel(channel).await?;
65        if ch_data.rx_frequency.as_hz() == 0 {
66            tracing::warn!(channel, "channel appears empty (frequency is 0 Hz)");
67        }
68
69        // Ensure memory mode.
70        self.ensure_mode(band, RadioMode::Memory).await?;
71
72        // Recall the channel.
73        self.recall_channel(band, channel).await?;
74
75        Ok(())
76    }
77
78    /// Find a memory channel number by its display name.
79    ///
80    /// Searches all channel names for a match and returns the channel
81    /// number. Does **not** tune the radio to that channel (the USB
82    /// connection is reset by MCP programming mode before recall could
83    /// happen). The caller should reconnect and use
84    /// [`Radio::tune_channel`](Radio::tune_channel) with the returned
85    /// channel number.
86    ///
87    /// # Errors
88    ///
89    /// Returns [`Error::Protocol`] with [`ProtocolError::UnexpectedResponse`]
90    /// if no channel with the given name is found. Returns transport/protocol
91    /// errors on communication failure.
92    ///
93    /// # Warning
94    ///
95    /// This method enters MCP programming mode to read channel names.
96    /// After returning, the USB connection will have been reset by the
97    /// radio. The `Radio` instance should be dropped and a fresh
98    /// connection established.
99    pub async fn find_channel_by_name(&mut self, band: Band, name: &str) -> Result<u16, Error> {
100        tracing::info!(?band, name, "searching for channel by name");
101
102        // Read all channel names via programming mode.
103        let names = self.read_channel_names().await?;
104
105        // Find a matching channel (skip empty names).
106        let found = names
107            .iter()
108            .enumerate()
109            .find(|(_, n)| !n.is_empty() && n.as_str() == name);
110
111        let (channel_num, _) = found.ok_or_else(|| {
112            Error::Protocol(ProtocolError::UnexpectedResponse {
113                expected: format!("channel named {name:?}"),
114                actual: b"no matching channel found".to_vec(),
115            })
116        })?;
117
118        let channel = u16::try_from(channel_num).map_err(|_| {
119            Error::Protocol(ProtocolError::FieldParse {
120                command: "find_channel_by_name".into(),
121                field: "channel".into(),
122                detail: format!("channel index {channel_num} exceeds u16 range"),
123            })
124        })?;
125
126        tracing::info!(channel, name, "found channel by name");
127
128        // Note: After read_channel_names() returns, the USB connection has
129        // been reset. The caller needs to reconnect. We cannot recall the
130        // channel here because the transport is dead. Return the channel
131        // number so the caller can reconnect and use tune_channel().
132        Ok(channel)
133    }
134
135    /// Quick-tune: set frequency, operating mode, and step size in one call.
136    ///
137    /// Switches to VFO mode if needed, then sets the frequency, operating
138    /// mode, and step size. This is a convenience method that combines
139    /// [`tune_frequency`](Self::tune_frequency), [`set_mode`](Self::set_mode),
140    /// and [`set_step_size`](Self::set_step_size).
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if any of the individual operations fail.
145    pub async fn quick_tune(
146        &mut self,
147        band: Band,
148        freq_hz: u32,
149        mode: Mode,
150        step: StepSize,
151    ) -> Result<(), Error> {
152        tracing::info!(?band, freq_hz, ?mode, ?step, "quick-tuning band");
153
154        // Set frequency (handles VFO mode switch internally).
155        self.tune_frequency(band, Frequency::new(freq_hz)).await?;
156
157        // Set operating mode.
158        self.set_mode(band, mode).await?;
159
160        // Set step size.
161        self.set_step_size(band, step).await?;
162
163        Ok(())
164    }
165
166    /// Ensure a band is in the specified mode, switching if necessary.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if querying or setting the mode fails.
171    async fn ensure_mode(&mut self, band: Band, target: RadioMode) -> Result<(), Error> {
172        // Check cached mode first.
173        let current = self.get_cached_mode(band);
174        if current == Some(target) {
175            tracing::debug!(?band, ?target, "already in target mode");
176            return Ok(());
177        }
178
179        // If unknown, query the radio.
180        if current.is_none() {
181            let vfo_mode = self.get_vfo_memory_mode(band).await?;
182            let actual = RadioMode::from_vfo_mode(vfo_mode);
183            if actual == target {
184                tracing::debug!(?band, ?target, "queried mode matches target");
185                return Ok(());
186            }
187        }
188
189        // Switch mode.
190        tracing::info!(?band, ?target, "switching band mode");
191        self.set_vfo_memory_mode(band, target.as_vfo_mode()).await?;
192
193        Ok(())
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::transport::MockTransport;
201
202    /// A typical FO response for Band A at 145.000 MHz.
203    /// Field layout verified against real D75 hardware (see `probes/fo_field_map.rs`).
204    const FO_RESPONSE_145: &[u8] =
205        b"FO 0,0145000000,0000600000,0,0,0,0,0,0,0,0,0,0,2,08,08,000,0,CQCQCQ,0,00\r";
206
207    /// FO write command for 146.520 MHz (preserving other fields from
208    /// `FO_RESPONSE_145` except the RX frequency).
209    const FO_WRITE_146520: &[u8] =
210        b"FO 0,0146520000,0000600000,0,0,0,0,0,0,0,0,0,0,2,08,08,000,0,CQCQCQ,0,00\r";
211
212    /// FO response echoed after writing 146.520 MHz.
213    const FO_RESPONSE_146520: &[u8] =
214        b"FO 0,0146520000,0000600000,0,0,0,0,0,0,0,0,0,0,2,08,08,000,0,CQCQCQ,0,00\r";
215
216    /// FQ short response for Band A at 146.520 MHz.
217    const FQ_RESPONSE_146520: &[u8] = b"FQ 0,0146520000\r";
218
219    #[tokio::test]
220    async fn tune_frequency_already_in_vfo_mode() {
221        let mut mock = MockTransport::new();
222        // ensure_mode: query VM -> already VFO (0)
223        mock.expect(b"VM 0\r", b"VM 0,0\r");
224        // get_frequency_full: read current FO
225        mock.expect(b"FO 0\r", FO_RESPONSE_145);
226        // set_frequency_full: write new frequency
227        mock.expect(FO_WRITE_146520, FO_RESPONSE_146520);
228        // get_frequency: verify readback
229        mock.expect(b"FQ 0\r", FQ_RESPONSE_146520);
230
231        let mut radio = Radio::connect(mock).await.unwrap();
232        radio
233            .tune_frequency(Band::A, Frequency::new(146_520_000))
234            .await
235            .unwrap();
236    }
237
238    #[tokio::test]
239    async fn tune_channel_switches_to_memory_mode() {
240        let mut mock = MockTransport::new();
241        // read_channel: ME read to verify channel is populated
242        mock.expect(
243            b"ME 021\r",
244            b"ME 021,0146520000,0000600000,5,0,0,0,0,0,0,0,0,0,0,0,08,08,000,0,,0,00,0\r",
245        );
246        // ensure_mode: query VM -> VFO (0), need to switch
247        mock.expect(b"VM 0\r", b"VM 0,0\r");
248        // ensure_mode: switch to memory mode (1)
249        mock.expect(b"VM 0,1\r", b"VM 0,1\r");
250        // recall_channel: MR action
251        mock.expect(b"MR 0,021\r", b"MR 0,021\r");
252
253        let mut radio = Radio::connect(mock).await.unwrap();
254        radio.tune_channel(Band::A, 21).await.unwrap();
255    }
256
257    #[tokio::test]
258    async fn tune_channel_already_in_memory_mode() {
259        let mut mock = MockTransport::new();
260        // read_channel: ME read to verify channel is populated
261        mock.expect(
262            b"ME 005\r",
263            b"ME 005,0440000000,0005000000,5,2,0,0,0,0,0,0,0,0,0,0,08,08,000,0,,0,00,0\r",
264        );
265        // ensure_mode: query VM -> already Memory (1)
266        mock.expect(b"VM 0\r", b"VM 0,1\r");
267        // recall_channel: MR action
268        mock.expect(b"MR 0,005\r", b"MR 0,005\r");
269
270        let mut radio = Radio::connect(mock).await.unwrap();
271        radio.tune_channel(Band::A, 5).await.unwrap();
272    }
273
274    #[tokio::test]
275    async fn quick_tune_sets_freq_mode_step() {
276        let mut mock = MockTransport::new();
277        // tune_frequency internals:
278        //   ensure_mode: query VM -> already VFO (0)
279        mock.expect(b"VM 0\r", b"VM 0,0\r");
280        //   get_frequency_full: FO read
281        mock.expect(b"FO 0\r", FO_RESPONSE_145);
282        //   set_frequency_full: FO write (146.520 MHz)
283        mock.expect(FO_WRITE_146520, FO_RESPONSE_146520);
284        //   get_frequency: verify readback
285        mock.expect(b"FQ 0\r", FQ_RESPONSE_146520);
286        // set_mode: MD write (FM = 0)
287        mock.expect(b"MD 0,0\r", b"MD 0,0\r");
288        // set_step_size: SF write (Hz5000 = 0x0)
289        mock.expect(b"SF 0,0\r", b"SF 0,0\r");
290
291        let mut radio = Radio::connect(mock).await.unwrap();
292        radio
293            .quick_tune(Band::A, 146_520_000, Mode::Fm, StepSize::Hz5000)
294            .await
295            .unwrap();
296    }
297}