kenwood_thd75/radio/
audio.rs

1//! Audio control methods.
2//!
3//! Controls AF (Audio Frequency) gain (band-indexed) and VOX (Voice-Operated
4//! Exchange) settings for hands-free transmit.
5//!
6//! # D75 tone commands
7//!
8//! The D75 firmware RE originally identified TN, DC, and RT as tone commands.
9//! Hardware testing revealed their actual functions:
10//! - **TN**: TNC mode (not CTCSS tone)
11//! - **DC**: D-STAR callsign slots (not DCS code)
12//! - **RT**: Real-time clock (not repeater tone)
13//!
14//! CTCSS tone and DCS code are instead configured through the FO (full
15//! frequency/offset) command's channel data fields.
16
17use crate::error::{Error, ProtocolError};
18use crate::protocol::{Command, Response};
19use crate::transport::Transport;
20use crate::types::{AfGainLevel, Band, DstarSlot, TncBaud, TncMode, VoxDelay, VoxGain};
21
22use super::Radio;
23
24impl<T: Transport> Radio<T> {
25    /// Get the AF gain level (AG read).
26    ///
27    /// D75 RE: bare `AG\r` returns global gain level. Band-indexed read
28    /// returns `?`, so this is a global query.
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if the command fails or the response is unexpected.
33    pub async fn get_af_gain(&mut self) -> Result<AfGainLevel, Error> {
34        tracing::debug!("reading AF gain");
35        let response = self.execute(Command::GetAfGain).await?;
36        match response {
37            Response::AfGain { level } => Ok(level),
38            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
39                expected: "AfGain".into(),
40                actual: format!("{other:?}").into_bytes(),
41            })),
42        }
43    }
44
45    /// Set the AF gain level (AG write).
46    ///
47    /// # Get/set asymmetry
48    ///
49    /// The get and set commands have different wire formats on the D75:
50    /// - **Read** (`AG\r`): bare command, returns a global gain level. Band-indexed read
51    ///   (`AG 0\r`) returns `?`.
52    /// - **Write** (`AG NNN\r`): bare 3-digit zero-padded value (e.g., `AG 015\r`). Despite
53    ///   the `band` parameter in this method's signature, the wire format is bare (no band
54    ///   index) — the value applies globally.
55    ///
56    /// # Valid range
57    ///
58    /// `level` must be 0 through 99. The wire format zero-pads to 3 digits (e.g., `AG 005\r`).
59    /// Values outside 0-99 may be rejected or cause unexpected behavior.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the command fails or the response is unexpected.
64    pub async fn set_af_gain(&mut self, band: Band, level: AfGainLevel) -> Result<(), Error> {
65        tracing::debug!(?band, ?level, "setting AF gain");
66        let response = self.execute(Command::SetAfGain { band, level }).await?;
67        match response {
68            Response::AfGain { .. } => Ok(()),
69            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
70                expected: "AfGain".into(),
71                actual: format!("{other:?}").into_bytes(),
72            })),
73        }
74    }
75
76    /// Get the TNC mode (TN bare read).
77    ///
78    /// Hardware-verified: bare `TN\r` returns `TN mode,setting`.
79    /// Returns `(mode, setting)`.
80    ///
81    /// Valid mode values per firmware validation: 0, 1, 2, 3.
82    /// Mode 3 may correspond to MMDVM or Reflector Terminal mode.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error if the command fails or the response is unexpected.
87    pub async fn get_tnc_mode(&mut self) -> Result<(TncMode, TncBaud), Error> {
88        tracing::debug!("reading TNC mode");
89        let response = self.execute(Command::GetTncMode).await?;
90        match response {
91            Response::TncMode { mode, setting } => Ok((mode, setting)),
92            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
93                expected: "TncMode".into(),
94                actual: format!("{other:?}").into_bytes(),
95            })),
96        }
97    }
98
99    /// Set the TNC mode (TN write).
100    ///
101    /// Valid mode values per firmware validation: 0, 1, 2, 3.
102    /// Mode 3 may correspond to MMDVM or Reflector Terminal mode.
103    ///
104    /// # Wire format
105    ///
106    /// `TN mode,setting\r`.
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if the command fails or the response is unexpected.
111    pub async fn set_tnc_mode(&mut self, mode: TncMode, setting: TncBaud) -> Result<(), Error> {
112        tracing::info!(?mode, ?setting, "setting TNC mode");
113        let response = self.execute(Command::SetTncMode { mode, setting }).await?;
114        match response {
115            Response::TncMode { .. } => Ok(()),
116            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
117                expected: "TncMode".into(),
118                actual: format!("{other:?}").into_bytes(),
119            })),
120        }
121    }
122
123    /// Get D-STAR callsign data for a slot (DC read).
124    ///
125    /// Hardware-verified: `DC slot\r` where slot is 1-6.
126    /// Returns `(callsign, suffix)`.
127    ///
128    /// Note: This method lives in `audio.rs` rather than `dstar.rs` because
129    /// it was discovered during audio subsystem hardware probing. The `DC`
130    /// mnemonic is overloaded on the D75 (DCS code, not D-STAR callsign
131    /// as on D74).
132    ///
133    /// # Errors
134    ///
135    /// Returns an error if the command fails or the response is unexpected.
136    pub async fn get_dstar_callsign(&mut self, slot: DstarSlot) -> Result<(String, String), Error> {
137        tracing::debug!(?slot, "reading D-STAR callsign");
138        let response = self.execute(Command::GetDstarCallsign { slot }).await?;
139        match response {
140            Response::DstarCallsign {
141                callsign, suffix, ..
142            } => Ok((callsign, suffix)),
143            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
144                expected: "DstarCallsign".into(),
145                actual: format!("{other:?}").into_bytes(),
146            })),
147        }
148    }
149
150    /// Set D-STAR callsign data for a slot (DC write).
151    ///
152    /// Writes callsign and suffix data to one of the 6 D-STAR callsign slots.
153    ///
154    /// # Wire format
155    ///
156    /// `DC slot,callsign,suffix\r` where slot is 1-6, callsign is 8 characters
157    /// (space-padded), and suffix is up to 4 characters.
158    ///
159    /// # Parameters
160    ///
161    /// - `slot`: Callsign slot number (1-6).
162    /// - `callsign`: Callsign string (8 characters, space-padded to length).
163    /// - `suffix`: Callsign suffix (up to 4 characters, e.g., "D75A").
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if the command fails or the response is unexpected.
168    pub async fn set_dstar_callsign(
169        &mut self,
170        slot: DstarSlot,
171        callsign: &str,
172        suffix: &str,
173    ) -> Result<(), Error> {
174        tracing::info!(?slot, callsign, suffix, "setting D-STAR callsign");
175        let response = self
176            .execute(Command::SetDstarCallsign {
177                slot,
178                callsign: callsign.to_owned(),
179                suffix: suffix.to_owned(),
180            })
181            .await?;
182        match response {
183            Response::DstarCallsign { .. } => Ok(()),
184            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
185                expected: "DstarCallsign".into(),
186                actual: format!("{other:?}").into_bytes(),
187            })),
188        }
189    }
190
191    /// Get the real-time clock (RT bare read).
192    ///
193    /// Note: This method lives in `audio.rs` rather than `system.rs` because
194    /// `RT` is overloaded on the D75 (repeater tone vs real-time clock on D74).
195    /// It was discovered during audio subsystem probing.
196    ///
197    /// Hardware-verified: bare `RT\r` returns `RT YYMMDDHHmmss`.
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if the command fails or the response is unexpected.
202    pub async fn get_real_time_clock(&mut self) -> Result<String, Error> {
203        tracing::debug!("reading real-time clock");
204        let response = self.execute(Command::GetRealTimeClock).await?;
205        match response {
206            Response::RealTimeClock { datetime } => Ok(datetime),
207            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
208                expected: "RealTimeClock".into(),
209                actual: format!("{other:?}").into_bytes(),
210            })),
211        }
212    }
213
214    /// Get the VOX (Voice-Operated Exchange/Transmit) enabled state (VX read).
215    ///
216    /// VOX allows hands-free transmit operation. When enabled, the radio automatically keys
217    /// the transmitter when it detects audio input from the microphone, and returns to receive
218    /// after a configurable delay when audio stops.
219    ///
220    /// VOX must be enabled before [`get_vox_gain`](Self::get_vox_gain) or
221    /// [`get_vox_delay`](Self::get_vox_delay) will succeed — those commands return `N`
222    /// (not available) when VOX is disabled.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if the command fails or the response is unexpected.
227    pub async fn get_vox(&mut self) -> Result<bool, Error> {
228        tracing::debug!("reading VOX state");
229        let response = self.execute(Command::GetVox).await?;
230        match response {
231            Response::Vox { enabled } => Ok(enabled),
232            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
233                expected: "Vox".into(),
234                actual: format!("{other:?}").into_bytes(),
235            })),
236        }
237    }
238
239    /// Set the VOX (Voice-Operated Exchange/Transmit) enabled state (VX write).
240    ///
241    /// See [`get_vox`](Self::get_vox) for a description of VOX operation. Enabling VOX
242    /// (`true`) unlocks the [`set_vox_gain`](Self::set_vox_gain) and
243    /// [`set_vox_delay`](Self::set_vox_delay) commands. Disabling VOX (`false`) causes
244    /// those commands to return `N` (not available).
245    ///
246    /// # Errors
247    ///
248    /// Returns an error if the command fails or the response is unexpected.
249    pub async fn set_vox(&mut self, enabled: bool) -> Result<(), Error> {
250        tracing::debug!(enabled, "setting VOX state");
251        let response = self.execute(Command::SetVox { enabled }).await?;
252        match response {
253            Response::Vox { .. } => Ok(()),
254            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
255                expected: "Vox".into(),
256                actual: format!("{other:?}").into_bytes(),
257            })),
258        }
259    }
260
261    /// Get the VOX gain level (VG read).
262    ///
263    /// # Mode requirement
264    /// VOX must be enabled (`VX 1`) for VG read to succeed.
265    /// Returns `N` (not available) when VOX is off.
266    ///
267    /// # Errors
268    ///
269    /// Returns an error if the command fails or the response is unexpected.
270    pub async fn get_vox_gain(&mut self) -> Result<VoxGain, Error> {
271        tracing::debug!("reading VOX gain");
272        let response = self.execute(Command::GetVoxGain).await?;
273        match response {
274            Response::VoxGain { gain } => Ok(gain),
275            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
276                expected: "VoxGain".into(),
277                actual: format!("{other:?}").into_bytes(),
278            })),
279        }
280    }
281
282    /// Set the VOX gain level (VG write).
283    ///
284    /// # Mode requirement
285    /// VOX must be enabled (`VX 1`) for VG write to succeed.
286    /// Returns `N` (not available) when VOX is off.
287    ///
288    /// # Errors
289    ///
290    /// Returns an error if the command fails or the response is unexpected.
291    pub async fn set_vox_gain(&mut self, gain: VoxGain) -> Result<(), Error> {
292        tracing::debug!(?gain, "setting VOX gain");
293        let response = self.execute(Command::SetVoxGain { gain }).await?;
294        match response {
295            Response::VoxGain { .. } => Ok(()),
296            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
297                expected: "VoxGain".into(),
298                actual: format!("{other:?}").into_bytes(),
299            })),
300        }
301    }
302
303    /// Get the VOX delay value (VD read).
304    ///
305    /// # Mode requirement
306    /// VOX must be enabled (`VX 1`) for VD read to succeed.
307    /// Returns `N` (not available) when VOX is off.
308    ///
309    /// # Errors
310    ///
311    /// Returns an error if the command fails or the response is unexpected.
312    pub async fn get_vox_delay(&mut self) -> Result<VoxDelay, Error> {
313        tracing::debug!("reading VOX delay");
314        let response = self.execute(Command::GetVoxDelay).await?;
315        match response {
316            Response::VoxDelay { delay } => Ok(delay),
317            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
318                expected: "VoxDelay".into(),
319                actual: format!("{other:?}").into_bytes(),
320            })),
321        }
322    }
323
324    /// Set the VOX delay value (VD write).
325    ///
326    /// # Mode requirement
327    /// VOX must be enabled (`VX 1`) for VD write to succeed.
328    /// Returns `N` (not available) when VOX is off.
329    ///
330    /// # Errors
331    ///
332    /// Returns an error if the command fails or the response is unexpected.
333    pub async fn set_vox_delay(&mut self, delay: VoxDelay) -> Result<(), Error> {
334        tracing::debug!(?delay, "setting VOX delay");
335        let response = self.execute(Command::SetVoxDelay { delay }).await?;
336        match response {
337            Response::VoxDelay { .. } => Ok(()),
338            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
339                expected: "VoxDelay".into(),
340                actual: format!("{other:?}").into_bytes(),
341            })),
342        }
343    }
344}