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}