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}