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}