kenwood_thd75/radio/freq.rs
1//! Core radio methods: frequency, mode, power, squelch, S-meter, TX/RX, firmware, power status, ID,
2//! band control, VFO/memory mode, FM radio, fine step, function type, and filter width.
3//!
4//! # Band capabilities (per Operating Tips §5.9, §5.10)
5//!
6//! - **Band A**: 144 / 220 (A only) / 430 MHz amateur operation
7//! - **Band B**: 0.1-524 MHz wideband receive, all modes (FM, NFM, AM, LSB, USB, CW, DV, DR)
8//! - **TH-D75A TX ranges**: 144-148 MHz, 222-225 MHz, 430-450 MHz
9//! - **TH-D75E TX ranges**: 144-146 MHz, 430-440 MHz
10//!
11//! # IF signal output (per Operating Tips §5.10)
12//!
13//! Menu No. 102 enables IF (Intermediate Frequency) signal output via the USB
14//! port: 12 kHz center frequency, 15 kHz bandwidth. This is intended for
15//! SSB/CW/AM demodulation by a PC application. Single Band mode is required
16//! for IF/Detect output. A band scope can be driven via a third-party PC
17//! application using the BS command.
18//!
19//! # FQ vs FO
20//!
21//! The D75 has two frequency-related command pairs:
22//!
23//! - **FQ** (read-only): returns the current frequency and step size for a band. Writes are
24//! rejected by the firmware — use FO for frequency changes.
25//! - **FO** (read/write): returns or sets the full channel configuration for a band, including
26//! frequency, offset, tone mode, CTCSS/DCS codes, shift direction, and more. This is the
27//! primary command for tuning the radio via CAT.
28//!
29//! # VFO mode requirement
30//!
31//! Most write commands in this module (FO write, MD write, SQ write, FS write, etc.) require the
32//! target band to be in VFO mode. If the band is in Memory, Call, or WX mode, the radio returns
33//! `?` and the write is silently rejected. Use [`set_vfo_memory_mode`](Radio::set_vfo_memory_mode)
34//! to switch to VFO mode first, or use the safe `tune_frequency()` API which handles mode
35//! management automatically.
36//!
37//! # Tone and offset configuration
38//!
39//! CTCSS tone, DCS code, tone mode, and repeater offset are not configured through dedicated
40//! commands. Instead, they are fields within the [`ChannelMemory`] struct passed to
41//! [`set_frequency_full`](Radio::set_frequency_full) (FO write). Read the current state with
42//! [`get_frequency_full`](Radio::get_frequency_full), modify the desired fields, and write it back.
43
44use crate::error::{Error, ProtocolError};
45use crate::protocol::{Command, Response};
46use crate::transport::Transport;
47use crate::types::{
48 Band, ChannelMemory, FilterMode, FilterWidthIndex, FineStep, Mode, PowerLevel, SMeterReading,
49 SquelchLevel, VfoMemoryMode,
50};
51
52use super::Radio;
53
54impl<T: Transport> Radio<T> {
55 /// Read the current frequency data for the given band (FQ read).
56 ///
57 /// # Errors
58 ///
59 /// Returns an error if the command fails or the response is unexpected.
60 pub async fn get_frequency(&mut self, band: Band) -> Result<ChannelMemory, Error> {
61 tracing::debug!(?band, "reading frequency data");
62 let response = self.execute(Command::GetFrequency { band }).await?;
63 match response {
64 Response::Frequency { channel, .. } => Ok(channel),
65 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
66 expected: "Frequency".into(),
67 actual: format!("{other:?}").into_bytes(),
68 })),
69 }
70 }
71
72 /// Read the full frequency and settings for the given band (FO read).
73 ///
74 /// # Errors
75 ///
76 /// Returns an error if the command fails or the response is unexpected.
77 pub async fn get_frequency_full(&mut self, band: Band) -> Result<ChannelMemory, Error> {
78 tracing::debug!(?band, "reading full frequency data");
79 let response = self.execute(Command::GetFrequencyFull { band }).await?;
80 match response {
81 Response::FrequencyFull { channel, .. } => Ok(channel),
82 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
83 expected: "FrequencyFull".into(),
84 actual: format!("{other:?}").into_bytes(),
85 })),
86 }
87 }
88
89 /// Write full frequency and settings for the given band (FO write).
90 ///
91 /// Sends the full channel configuration (frequency, offset, tone mode, CTCSS/DCS codes,
92 /// shift direction, and other fields) to the radio for the specified band.
93 ///
94 /// # VFO mode requirement
95 ///
96 /// The target band **must** be in VFO mode (`VM band,0`). If the band is in Memory, Call,
97 /// or WX mode, the radio returns `?` and the write is silently rejected. Use
98 /// [`set_vfo_memory_mode`](Self::set_vfo_memory_mode) to switch to VFO first, or prefer
99 /// `tune_frequency()` which handles mode management safely.
100 ///
101 /// # Wire format
102 ///
103 /// `FO band,freq,step,shift,reverse,tone_status,ctcss_status,dcs_status,tone_freq,ctcss_freq,dcs_code,offset,...\r`
104 ///
105 /// The full FO command encodes all 21 fields of the [`ChannelMemory`] struct as
106 /// comma-separated values.
107 ///
108 /// # Errors
109 ///
110 /// Returns an error if the command fails or the response is unexpected.
111 pub async fn set_frequency_full(
112 &mut self,
113 band: Band,
114 channel: &ChannelMemory,
115 ) -> Result<(), Error> {
116 tracing::debug!(?band, "writing full frequency data");
117 let response = self
118 .execute(Command::SetFrequencyFull {
119 band,
120 channel: channel.clone(),
121 })
122 .await?;
123 match response {
124 Response::FrequencyFull { .. } => Ok(()),
125 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
126 expected: "FrequencyFull".into(),
127 actual: format!("{other:?}").into_bytes(),
128 })),
129 }
130 }
131
132 /// Get the operating mode for the given band (MD read).
133 ///
134 /// # Errors
135 ///
136 /// Returns an error if the command fails or the response is unexpected.
137 pub async fn get_mode(&mut self, band: Band) -> Result<Mode, Error> {
138 tracing::debug!(?band, "reading operating mode");
139 let response = self.execute(Command::GetMode { band }).await?;
140 match response {
141 Response::Mode { mode, .. } => Ok(mode),
142 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
143 expected: "Mode".into(),
144 actual: format!("{other:?}").into_bytes(),
145 })),
146 }
147 }
148
149 /// Set the operating mode for the given band (MD write).
150 ///
151 /// # Band restrictions
152 ///
153 /// SSB (LSB/USB), CW, and AM modes are only available on Band B. Attempting to set these
154 /// modes on Band A will return `?`. FM, NFM, DV, and DR modes are available on both bands.
155 ///
156 /// See the [`Mode`] type for valid values. Note that the MD command uses a different
157 /// encoding than FO/ME commands — the [`Mode`] type handles this mapping internally.
158 ///
159 /// # Errors
160 ///
161 /// Returns an error if the command fails or the response is unexpected.
162 pub async fn set_mode(&mut self, band: Band, mode: Mode) -> Result<(), Error> {
163 tracing::debug!(?band, ?mode, "setting operating mode");
164 let response = self.execute(Command::SetMode { band, mode }).await?;
165 match response {
166 Response::Mode { .. } => Ok(()),
167 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
168 expected: "Mode".into(),
169 actual: format!("{other:?}").into_bytes(),
170 })),
171 }
172 }
173
174 /// Get the power level for the given band (PC read).
175 ///
176 /// # Errors
177 ///
178 /// Returns an error if the command fails or the response is unexpected.
179 pub async fn get_power_level(&mut self, band: Band) -> Result<PowerLevel, Error> {
180 tracing::debug!(?band, "reading power level");
181 let response = self.execute(Command::GetPowerLevel { band }).await?;
182 match response {
183 Response::PowerLevel { level, .. } => Ok(level),
184 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
185 expected: "PowerLevel".into(),
186 actual: format!("{other:?}").into_bytes(),
187 })),
188 }
189 }
190
191 /// Set the power level for the given band (PC write).
192 ///
193 /// # Errors
194 ///
195 /// Returns an error if the command fails or the response is unexpected.
196 pub async fn set_power_level(&mut self, band: Band, level: PowerLevel) -> Result<(), Error> {
197 tracing::debug!(?band, ?level, "setting power level");
198 let response = self.execute(Command::SetPowerLevel { band, level }).await?;
199 match response {
200 Response::PowerLevel { .. } => Ok(()),
201 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
202 expected: "PowerLevel".into(),
203 actual: format!("{other:?}").into_bytes(),
204 })),
205 }
206 }
207
208 /// Get the squelch level for the given band (SQ read).
209 ///
210 /// # Errors
211 ///
212 /// Returns an error if the command fails or the response is unexpected.
213 pub async fn get_squelch(&mut self, band: Band) -> Result<SquelchLevel, Error> {
214 tracing::debug!(?band, "reading squelch level");
215 let response = self.execute(Command::GetSquelch { band }).await?;
216 match response {
217 Response::Squelch { level, .. } => Ok(level),
218 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
219 expected: "Squelch".into(),
220 actual: format!("{other:?}").into_bytes(),
221 })),
222 }
223 }
224
225 /// Set the squelch level for the given band (SQ write).
226 ///
227 /// # Valid range
228 ///
229 /// `level` must be 0 through 6 on the TH-D75. Values outside this range cause the radio
230 /// to return `?` and the write is rejected. Level 0 means squelch is fully open (all signals
231 /// pass); level 6 is the tightest squelch setting.
232 ///
233 /// # Wire format
234 ///
235 /// `SQ band,level\r` where band is 0 (A) or 1 (B) and level is a single digit 0-6.
236 ///
237 /// # Errors
238 ///
239 /// Returns an error if the command fails or the response is unexpected.
240 pub async fn set_squelch(&mut self, band: Band, level: SquelchLevel) -> Result<(), Error> {
241 tracing::debug!(?band, ?level, "setting squelch level");
242 let response = self.execute(Command::SetSquelch { band, level }).await?;
243 match response {
244 Response::Squelch { .. } => Ok(()),
245 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
246 expected: "Squelch".into(),
247 actual: format!("{other:?}").into_bytes(),
248 })),
249 }
250 }
251
252 /// Get the S-meter reading for the given band (SM read).
253 ///
254 /// Returns an instantaneous signal strength measurement as a raw value 0-5. This is a
255 /// read-only, point-in-time snapshot — the value changes continuously as signal conditions
256 /// vary.
257 ///
258 /// # Value mapping
259 ///
260 /// The raw values map to approximate S-meter readings:
261 ///
262 /// | Raw | S-meter |
263 /// |-----|---------|
264 /// | 0 | S0 (no signal) |
265 /// | 1 | S1 |
266 /// | 2 | S3 |
267 /// | 3 | S5 |
268 /// | 4 | S7 |
269 /// | 5 | S9 (full scale) |
270 ///
271 /// # Polling warning
272 ///
273 /// Do not poll SM continuously — the firmware returns spurious spikes on Band B. Instead,
274 /// use AI mode ([`set_auto_info`](Self::set_auto_info)) with the BY (busy) signal as a
275 /// gate: read SM once when squelch opens, and treat it as zero when squelch is closed.
276 ///
277 /// # Wire format
278 ///
279 /// `SM band\r` returns `SM band,level\r`.
280 ///
281 /// # Errors
282 ///
283 /// Returns an error if the command fails or the response is unexpected.
284 pub async fn get_smeter(&mut self, band: Band) -> Result<SMeterReading, Error> {
285 tracing::debug!(?band, "reading S-meter");
286 let response = self.execute(Command::GetSmeter { band }).await?;
287 match response {
288 Response::Smeter { level, .. } => Ok(level),
289 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
290 expected: "Smeter".into(),
291 actual: format!("{other:?}").into_bytes(),
292 })),
293 }
294 }
295
296 /// Get the busy state for the given band (BY read).
297 ///
298 /// "Busy" means the squelch is open — a signal strong enough to exceed the current squelch
299 /// threshold is present on the channel. Returns `true` when the squelch is open (signal
300 /// present), `false` when closed (no signal or signal below threshold).
301 ///
302 /// # Wire format
303 ///
304 /// `BY band\r` returns `BY band,state\r` where state is 0 (not busy / squelch closed) or
305 /// 1 (busy / squelch open).
306 ///
307 /// # Errors
308 ///
309 /// Returns an error if the command fails or the response is unexpected.
310 pub async fn get_busy(&mut self, band: Band) -> Result<bool, Error> {
311 tracing::debug!(?band, "reading busy state");
312 let response = self.execute(Command::GetBusy { band }).await?;
313 match response {
314 Response::Busy { busy, .. } => Ok(busy),
315 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
316 expected: "Busy".into(),
317 actual: format!("{other:?}").into_bytes(),
318 })),
319 }
320 }
321
322 /// Switch the given band to transmit mode (TX action).
323 ///
324 /// # RF emission warning
325 ///
326 /// **This keys the transmitter and causes RF emission on the currently tuned frequency.**
327 /// The radio will transmit continuously until [`receive`](Self::receive) is called. Ensure
328 /// you are authorized to transmit on the current frequency before calling this method.
329 /// Unauthorized transmission is a violation of radio regulations (e.g., FCC Part 97 in the
330 /// US).
331 ///
332 /// Always call [`receive`](Self::receive) when done to return to receive mode. If your
333 /// program panics or is interrupted while transmitting, the radio will continue to transmit
334 /// until manually stopped or the timeout (if any) expires.
335 ///
336 /// # Wire format
337 ///
338 /// `TX band\r` where band is 0 (A) or 1 (B). Returns `OK\r` on success.
339 ///
340 /// # Errors
341 ///
342 /// Returns an error if the command fails or the response is unexpected.
343 pub async fn transmit(&mut self, band: Band) -> Result<(), Error> {
344 tracing::info!(?band, "keying transmitter");
345 let response = self.execute(Command::Transmit { band }).await?;
346 match response {
347 Response::Ok => Ok(()),
348 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
349 expected: "Ok".into(),
350 actual: format!("{other:?}").into_bytes(),
351 })),
352 }
353 }
354
355 /// Switch the given band to receive mode (RX action).
356 ///
357 /// Stops transmitting and returns the radio to receive mode. This is the counterpart to
358 /// [`transmit`](Self::transmit) and **must** be called after transmitting to stop RF
359 /// emission.
360 ///
361 /// # Wire format
362 ///
363 /// `RX band\r` where band is 0 (A) or 1 (B). Returns `OK\r` on success.
364 ///
365 /// # Errors
366 ///
367 /// Returns an error if the command fails or the response is unexpected.
368 pub async fn receive(&mut self, band: Band) -> Result<(), Error> {
369 tracing::info!(?band, "returning to receive");
370 let response = self.execute(Command::Receive { band }).await?;
371 match response {
372 Response::Ok => Ok(()),
373 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
374 expected: "Ok".into(),
375 actual: format!("{other:?}").into_bytes(),
376 })),
377 }
378 }
379
380 /// Get the firmware version string (FV read).
381 ///
382 /// # Errors
383 ///
384 /// Returns an error if the command fails or the response is unexpected.
385 pub async fn get_firmware_version(&mut self) -> Result<String, Error> {
386 tracing::debug!("reading firmware version");
387 let response = self.execute(Command::GetFirmwareVersion).await?;
388 match response {
389 Response::FirmwareVersion { version } => Ok(version),
390 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
391 expected: "FirmwareVersion".into(),
392 actual: format!("{other:?}").into_bytes(),
393 })),
394 }
395 }
396
397 /// Get the power on/off status (PS read).
398 ///
399 /// # Errors
400 ///
401 /// Returns an error if the command fails or the response is unexpected.
402 pub async fn get_power_status(&mut self) -> Result<bool, Error> {
403 tracing::debug!("reading power status");
404 let response = self.execute(Command::GetPowerStatus).await?;
405 match response {
406 Response::PowerStatus { on } => Ok(on),
407 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
408 expected: "PowerStatus".into(),
409 actual: format!("{other:?}").into_bytes(),
410 })),
411 }
412 }
413
414 /// Get the radio model identification string (ID read).
415 ///
416 /// # Errors
417 ///
418 /// Returns an error if the command fails or the response is unexpected.
419 pub async fn get_radio_id(&mut self) -> Result<String, Error> {
420 tracing::debug!("reading radio ID");
421 let response = self.execute(Command::GetRadioId).await?;
422 match response {
423 Response::RadioId { model } => Ok(model),
424 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
425 expected: "RadioId".into(),
426 actual: format!("{other:?}").into_bytes(),
427 })),
428 }
429 }
430
431 /// Get the current active band (BC read).
432 ///
433 /// # Errors
434 ///
435 /// Returns an error if the command fails or the response is unexpected.
436 pub async fn get_band(&mut self) -> Result<Band, Error> {
437 tracing::debug!("reading active band");
438 let response = self.execute(Command::GetBand).await?;
439 match response {
440 Response::BandResponse { band } => Ok(band),
441 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
442 expected: "BandResponse".into(),
443 actual: format!("{other:?}").into_bytes(),
444 })),
445 }
446 }
447
448 /// Set the active band (BC write).
449 ///
450 /// # Warning
451 /// This is an ACTION command that switches the radio's active band.
452 ///
453 /// # Errors
454 ///
455 /// Returns an error if the command fails or the response is unexpected.
456 pub async fn set_band(&mut self, band: Band) -> Result<(), Error> {
457 tracing::info!(?band, "setting active band");
458 let response = self.execute(Command::SetBand { band }).await?;
459 match response {
460 Response::BandResponse { .. } => Ok(()),
461 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
462 expected: "BandResponse".into(),
463 actual: format!("{other:?}").into_bytes(),
464 })),
465 }
466 }
467
468 /// Get the VFO/Memory mode for a band (VM read).
469 ///
470 /// Returns a mode index: 0 = VFO, 1 = Memory, 2 = Call, 3 = WX.
471 ///
472 /// # Errors
473 ///
474 /// Returns an error if the command fails or the response is unexpected.
475 pub async fn get_vfo_memory_mode(&mut self, band: Band) -> Result<VfoMemoryMode, Error> {
476 tracing::debug!(?band, "reading VFO/Memory mode");
477 let response = self.execute(Command::GetVfoMemoryMode { band }).await?;
478 match response {
479 Response::VfoMemoryMode { mode, .. } => Ok(mode),
480 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
481 expected: "VfoMemoryMode".into(),
482 actual: format!("{other:?}").into_bytes(),
483 })),
484 }
485 }
486
487 /// Set the VFO/Memory mode for a band (VM write).
488 ///
489 /// Mode values: 0 = VFO, 1 = Memory, 2 = Call, 3 = WX.
490 ///
491 /// # Errors
492 ///
493 /// Returns an error if the command fails or the response is unexpected.
494 pub async fn set_vfo_memory_mode(
495 &mut self,
496 band: Band,
497 mode: VfoMemoryMode,
498 ) -> Result<(), Error> {
499 tracing::info!(?band, ?mode, "setting VFO/Memory mode");
500 let response = self
501 .execute(Command::SetVfoMemoryMode { band, mode })
502 .await?;
503 match response {
504 Response::VfoMemoryMode { .. } => Ok(()),
505 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
506 expected: "VfoMemoryMode".into(),
507 actual: format!("{other:?}").into_bytes(),
508 })),
509 }
510 }
511
512 /// Get the current memory channel number for a band (MR read).
513 ///
514 /// Hardware-verified: `MR band\r` returns `MR bandCCC` where CCC is
515 /// the channel number. This is a read that queries which channel is
516 /// active, not an action that changes the channel.
517 ///
518 /// # Errors
519 ///
520 /// Returns an error if the command fails or the response is unexpected.
521 pub async fn get_current_channel(&mut self, band: Band) -> Result<u16, Error> {
522 tracing::debug!(?band, "reading current memory channel");
523 let response = self.execute(Command::GetCurrentChannel { band }).await?;
524 match response {
525 Response::CurrentChannel { channel, .. } => Ok(channel),
526 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
527 expected: "CurrentChannel".into(),
528 actual: format!("{other:?}").into_bytes(),
529 })),
530 }
531 }
532
533 /// Recall a memory channel on the given band (MR action).
534 ///
535 /// This is an ACTION command that switches the radio's active channel.
536 /// The previous channel selection is not preserved.
537 ///
538 /// # Errors
539 ///
540 /// Returns an error if the command fails or the response is unexpected.
541 pub async fn recall_channel(&mut self, band: Band, channel: u16) -> Result<(), Error> {
542 tracing::info!(?band, channel, "recalling memory channel");
543 let response = self
544 .execute(Command::RecallMemoryChannel { band, channel })
545 .await?;
546 match response {
547 Response::MemoryRecall { .. } => Ok(()),
548 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
549 expected: "MemoryRecall".into(),
550 actual: format!("{other:?}").into_bytes(),
551 })),
552 }
553 }
554
555 /// Step the frequency up by one increment on the given band (UP action).
556 ///
557 /// This is an ACTION command that changes the radio's active frequency.
558 /// There is no undo — the previous frequency is not preserved.
559 ///
560 /// # Errors
561 ///
562 /// Returns an error if the command fails or the response is unexpected.
563 pub async fn frequency_up(&mut self, band: Band) -> Result<(), Error> {
564 tracing::info!(?band, "stepping frequency up");
565 let response = self.execute(Command::FrequencyUp { band }).await?;
566 match response {
567 Response::Ok => Ok(()),
568 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
569 expected: "Ok".into(),
570 actual: format!("{other:?}").into_bytes(),
571 })),
572 }
573 }
574
575 /// Get the FM broadcast radio on/off state (FR read).
576 ///
577 /// # Errors
578 ///
579 /// Returns an error if the command fails or the response is unexpected.
580 pub async fn get_fm_radio(&mut self) -> Result<bool, Error> {
581 tracing::debug!("reading FM radio state");
582 let response = self.execute(Command::GetFmRadio).await?;
583 match response {
584 Response::FmRadio { enabled } => Ok(enabled),
585 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
586 expected: "FmRadio".into(),
587 actual: format!("{other:?}").into_bytes(),
588 })),
589 }
590 }
591
592 /// Set the FM broadcast radio on/off state (FR write).
593 ///
594 /// This controls the **broadcast FM receiver** (76-108 MHz), not amateur FM mode. This is
595 /// the same as the "FM Radio" menu item on the radio — it tunes to commercial broadcast
596 /// stations.
597 ///
598 /// # Side effects
599 ///
600 /// Enabling the FM broadcast receiver takes over the display and audio output. The radio's
601 /// normal amateur band display is replaced with the broadcast FM frequency. Normal band
602 /// receive audio is muted while the FM broadcast receiver is active. Disable it to return
603 /// to normal amateur radio operation.
604 ///
605 /// # Errors
606 ///
607 /// Returns an error if the command fails or the response is unexpected.
608 pub async fn set_fm_radio(&mut self, enabled: bool) -> Result<(), Error> {
609 tracing::info!(enabled, "setting FM radio state");
610 let response = self.execute(Command::SetFmRadio { enabled }).await?;
611 match response {
612 Response::FmRadio { .. } => Ok(()),
613 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
614 expected: "FmRadio".into(),
615 actual: format!("{other:?}").into_bytes(),
616 })),
617 }
618 }
619
620 /// Get the fine step setting (FS bare read).
621 ///
622 /// Firmware-verified: FS = Fine Step. Bare `FS\r` returns a single value (0-3).
623 /// No band parameter — the radio returns a global fine step setting.
624 ///
625 /// # Errors
626 ///
627 /// Returns an error if the command fails or the response is unexpected.
628 pub async fn get_fine_step(&mut self) -> Result<FineStep, Error> {
629 tracing::debug!("reading fine step");
630 let response = self.execute(Command::GetFineStep).await?;
631 match response {
632 Response::FineStep { step } => Ok(step),
633 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
634 expected: "FineStep".into(),
635 actual: format!("{other:?}").into_bytes(),
636 })),
637 }
638 }
639
640 /// Set the fine step for a band (FS write).
641 ///
642 /// Firmware-verified: `FS band,step\r` (band 0-1, step 0-3).
643 ///
644 /// # Firmware bug (v1.03)
645 ///
646 /// FS write is broken on firmware 1.03 — the radio returns `N`
647 /// (not available) for all write attempts.
648 ///
649 /// # Errors
650 ///
651 /// Returns an error if the command fails or the response is unexpected.
652 pub async fn set_fine_step(&mut self, band: Band, step: FineStep) -> Result<(), Error> {
653 tracing::info!(?band, ?step, "setting fine step");
654 let response = self.execute(Command::SetFineStep { band, step }).await?;
655 match response {
656 Response::FineStep { .. } => Ok(()),
657 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
658 expected: "FineStep".into(),
659 actual: format!("{other:?}").into_bytes(),
660 })),
661 }
662 }
663
664 /// Get the function type value (FT read).
665 ///
666 /// # Errors
667 ///
668 /// Returns an error if the command fails or the response is unexpected.
669 pub async fn get_function_type(&mut self) -> Result<bool, Error> {
670 tracing::debug!("reading function type (fine tune)");
671 let response = self.execute(Command::GetFunctionType).await?;
672 match response {
673 Response::FunctionType { enabled } => Ok(enabled),
674 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
675 expected: "FunctionType".into(),
676 actual: format!("{other:?}").into_bytes(),
677 })),
678 }
679 }
680
681 /// Set fine tune on/off (FT write).
682 ///
683 /// Per Operating Tips section 5.10.6: Fine Tune only works with AM modulation
684 /// and Band B.
685 ///
686 /// # Wire format
687 ///
688 /// `FT value\r` where value is 0 (off) or 1 (on). This is a global toggle
689 /// (no band parameter). Confirmed by ARFC-D75 decompilation and firmware
690 /// handler analysis (accepts only 5-byte commands: `FT N\r`).
691 ///
692 /// # Errors
693 ///
694 /// Returns an error if the command fails or the response is unexpected.
695 pub async fn set_function_type(&mut self, enabled: bool) -> Result<(), Error> {
696 tracing::info!(enabled, "setting fine tune (FT)");
697 let response = self.execute(Command::SetFunctionType { enabled }).await?;
698 match response {
699 Response::FunctionType { .. } => Ok(()),
700 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
701 expected: "FunctionType".into(),
702 actual: format!("{other:?}").into_bytes(),
703 })),
704 }
705 }
706
707 /// Set the S-meter value for a band (SM write) -- calibration/test interface.
708 ///
709 /// # Warning
710 ///
711 /// This is likely a calibration or test/debug interface. Setting the S-meter
712 /// value directly may interfere with normal signal strength readings. The
713 /// exact behavior and persistence of written values is undocumented.
714 ///
715 /// # Wire format
716 ///
717 /// `SM band,level\r` where band is 0 (A) or 1 (B) and level is a hex nibble value.
718 ///
719 /// # Errors
720 ///
721 /// Returns an error if the command fails or the response is unexpected.
722 pub async fn set_smeter(&mut self, band: Band, level: SMeterReading) -> Result<(), Error> {
723 tracing::info!(?band, ?level, "setting S-meter (SM write, calibration)");
724 let response = self.execute(Command::SetSmeter { band, level }).await?;
725 match response {
726 Response::Smeter { .. } => Ok(()),
727 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
728 expected: "Smeter".into(),
729 actual: format!("{other:?}").into_bytes(),
730 })),
731 }
732 }
733
734 /// Set the busy/squelch state for a band (BY write) -- test/debug interface.
735 ///
736 /// # Warning
737 ///
738 /// This is likely a test or debug interface. Setting the busy state directly
739 /// may interfere with normal squelch operation. Use with caution.
740 ///
741 /// # Wire format
742 ///
743 /// `BY band,state\r` where band is 0 (A) or 1 (B) and state is 0 (not busy)
744 /// or 1 (busy).
745 ///
746 /// # Errors
747 ///
748 /// Returns an error if the command fails or the response is unexpected.
749 pub async fn set_busy(&mut self, band: Band, busy: bool) -> Result<(), Error> {
750 tracing::info!(?band, busy, "setting busy state (BY write, test/debug)");
751 let response = self.execute(Command::SetBusy { band, busy }).await?;
752 match response {
753 Response::Busy { .. } => Ok(()),
754 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
755 expected: "Busy".into(),
756 actual: format!("{other:?}").into_bytes(),
757 })),
758 }
759 }
760
761 /// Set the firmware version string (FV write) -- factory programming command.
762 ///
763 /// # Safety
764 ///
765 /// **DANGEROUS FACTORY COMMAND.** This is intended for factory programming
766 /// only. Writing an incorrect firmware version string may brick the radio,
767 /// cause firmware validation failures, or void your warranty. **Do not use
768 /// unless you fully understand the consequences.**
769 ///
770 /// # Wire format
771 ///
772 /// `FV version\r`.
773 ///
774 /// # Errors
775 ///
776 /// Returns an error if the command fails or the response is unexpected.
777 pub async fn set_firmware_version(&mut self, version: &str) -> Result<(), Error> {
778 tracing::warn!(version, "setting firmware version (FACTORY COMMAND)");
779 let response = self
780 .execute(Command::SetFirmwareVersion {
781 version: version.to_owned(),
782 })
783 .await?;
784 match response {
785 Response::FirmwareVersion { .. } => Ok(()),
786 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
787 expected: "FirmwareVersion".into(),
788 actual: format!("{other:?}").into_bytes(),
789 })),
790 }
791 }
792
793 /// Set the radio model identification string (ID write) -- factory programming command.
794 ///
795 /// # Safety
796 ///
797 /// **DANGEROUS FACTORY COMMAND.** This is intended for factory programming
798 /// only. Writing an incorrect model ID may cause the radio to behave as a
799 /// different model, disable features, or brick the device. **Do not use
800 /// unless you fully understand the consequences.**
801 ///
802 /// # Wire format
803 ///
804 /// `ID model\r`.
805 ///
806 /// # Errors
807 ///
808 /// Returns an error if the command fails or the response is unexpected.
809 pub async fn set_radio_id(&mut self, model: &str) -> Result<(), Error> {
810 tracing::warn!(model, "setting radio model ID (FACTORY COMMAND)");
811 let response = self
812 .execute(Command::SetRadioId {
813 model: model.to_owned(),
814 })
815 .await?;
816 match response {
817 Response::RadioId { .. } => Ok(()),
818 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
819 expected: "RadioId".into(),
820 actual: format!("{other:?}").into_bytes(),
821 })),
822 }
823 }
824
825 /// Get the filter width for a given mode index (SH read).
826 ///
827 /// `mode_index`: 0 = SSB, 1 = CW, 2 = AM.
828 ///
829 /// # Errors
830 ///
831 /// Returns an error if the command fails or the response is unexpected.
832 pub async fn get_filter_width(&mut self, mode: FilterMode) -> Result<FilterWidthIndex, Error> {
833 tracing::debug!(?mode, "reading filter width");
834 let response = self.execute(Command::GetFilterWidth { mode }).await?;
835 match response {
836 Response::FilterWidth { width, .. } => Ok(width),
837 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
838 expected: "FilterWidth".into(),
839 actual: format!("{other:?}").into_bytes(),
840 })),
841 }
842 }
843
844 /// Set the filter width for a given mode index (SH write).
845 ///
846 /// `mode_index`: 0 = SSB, 1 = CW, 2 = AM. The width value selects
847 /// from the available filter options for that mode (per Operating
848 /// Tips §5.10.1–§5.10.3).
849 ///
850 /// # Errors
851 ///
852 /// Returns an error if the command fails or the response is unexpected.
853 pub async fn set_filter_width(
854 &mut self,
855 mode: FilterMode,
856 width: FilterWidthIndex,
857 ) -> Result<(), Error> {
858 tracing::info!(?mode, ?width, "setting filter width");
859 let response = self
860 .execute(Command::SetFilterWidth { mode, width })
861 .await?;
862 match response {
863 Response::FilterWidth { .. } => Ok(()),
864 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
865 expected: "FilterWidth".into(),
866 actual: format!("{other:?}").into_bytes(),
867 })),
868 }
869 }
870}