kenwood_thd75/radio/system.rs
1//! System-level radio methods: battery level, beep, lock, dual-band, frequency step, bluetooth, attenuator, auto-info.
2
3use crate::error::{Error, ProtocolError};
4use crate::protocol::{Command, Response};
5use crate::transport::Transport;
6use crate::types::{Band, BatteryLevel, ChannelMemory, DetectOutputMode, KeyLockType};
7
8use super::Radio;
9
10impl<T: Transport> Radio<T> {
11 /// Get beep setting (BE read).
12 ///
13 /// D75 RE: `BE x` (x: 0=off, 1=on).
14 ///
15 /// # Errors
16 ///
17 /// Returns an error if the command fails or the response is unexpected.
18 pub async fn get_beep(&mut self) -> Result<bool, Error> {
19 tracing::debug!("reading beep setting");
20 let response = self.execute(Command::GetBeep).await?;
21 match response {
22 Response::Beep { enabled } => Ok(enabled),
23 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
24 expected: "Beep".into(),
25 actual: format!("{other:?}").into_bytes(),
26 })),
27 }
28 }
29
30 /// Set beep on/off (BE write).
31 ///
32 /// D75 RE: `BE x` (x: 0=off, 1=on).
33 ///
34 /// # D75 firmware bug
35 ///
36 /// **The CAT `BE` write command is a firmware stub on the TH-D75.** It always returns `?`
37 /// regardless of the value sent. The read (`get_beep`) works, but writes are silently
38 /// ignored by the firmware.
39 ///
40 /// Use [`set_beep_via_mcp`](Self::set_beep_via_mcp) instead, which writes directly to
41 /// the verified MCP memory offset (`0x1071`) and actually changes the setting.
42 ///
43 /// # Errors
44 ///
45 /// Returns an error if the command fails or the response is unexpected.
46 pub async fn set_beep(&mut self, enabled: bool) -> Result<(), Error> {
47 tracing::debug!(enabled, "setting beep");
48 let response = self.execute(Command::SetBeep { enabled }).await?;
49 match response {
50 Response::Beep { .. } => Ok(()),
51 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
52 expected: "Beep".into(),
53 actual: format!("{other:?}").into_bytes(),
54 })),
55 }
56 }
57
58 /// Get the battery charge level (BL read).
59 ///
60 /// Returns 0=Empty (Red), 1=1/3 (Yellow), 2=2/3 (Green), 3=Full (Green),
61 /// 4=Charging (USB power connected). Read-only.
62 ///
63 /// # Errors
64 ///
65 /// Returns an error if the command fails or the response is unexpected.
66 pub async fn get_battery_level(&mut self) -> Result<BatteryLevel, Error> {
67 tracing::debug!("reading battery level");
68 let response = self.execute(Command::GetBatteryLevel).await?;
69 match response {
70 Response::BatteryLevel { level } => Ok(level),
71 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
72 expected: "BatteryLevel".into(),
73 actual: format!("{other:?}").into_bytes(),
74 })),
75 }
76 }
77
78 /// Set battery level display (BL write).
79 ///
80 /// # Warning
81 ///
82 /// The exact purpose of this command is unclear. It may control the battery
83 /// display indicator or be a calibration/test interface. The `display` and
84 /// `level` parameter semantics are undocumented.
85 ///
86 /// # Wire format
87 ///
88 /// `BL display,level\r` (7 bytes with comma).
89 ///
90 /// # Errors
91 ///
92 /// Returns an error if the command fails or the response is unexpected.
93 pub async fn set_battery_level(&mut self, bl_display: u8, level: u8) -> Result<(), Error> {
94 tracing::info!(
95 bl_display,
96 level,
97 "setting battery level display (BL write)"
98 );
99 let response = self
100 .execute(Command::SetBatteryLevel {
101 display: bl_display,
102 level,
103 })
104 .await?;
105 match response {
106 Response::BatteryLevel { .. } => Ok(()),
107 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
108 expected: "BatteryLevel".into(),
109 actual: format!("{other:?}").into_bytes(),
110 })),
111 }
112 }
113
114 /// Get the key lock state (LC read).
115 ///
116 /// On the TH-D75, the LC wire value is inverted: `LC 0` means locked,
117 /// `LC 1` means unlocked. This method inverts the response so that
118 /// `true` means locked (logical meaning). The MCP offset for the lock
119 /// setting is `0x1060`.
120 ///
121 /// # Errors
122 ///
123 /// Returns an error if the command fails or the response is unexpected.
124 pub async fn get_lock(&mut self) -> Result<bool, Error> {
125 tracing::debug!("reading key lock state");
126 let response = self.execute(Command::GetLock).await?;
127 match response {
128 // Wire value is inverted: 0 = locked, 1 = unlocked.
129 Response::Lock { locked } => Ok(!locked),
130 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
131 expected: "Lock".into(),
132 actual: format!("{other:?}").into_bytes(),
133 })),
134 }
135 }
136
137 /// Set the key lock state (LC write).
138 ///
139 /// Accepts logical meaning: `true` = locked, `false` = unlocked.
140 /// Inverts before sending on the wire (`LC 0` = locked, `LC 1` =
141 /// unlocked on the D75).
142 ///
143 /// # Errors
144 ///
145 /// Returns an error if the command fails or the response is unexpected.
146 pub async fn set_lock(&mut self, locked: bool) -> Result<(), Error> {
147 tracing::info!(locked, "setting key lock");
148 // Invert: logical true (locked) → wire 0, logical false → wire 1.
149 let response = self.execute(Command::SetLock { locked: !locked }).await?;
150 match response {
151 Response::Lock { .. } => Ok(()),
152 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
153 expected: "Lock".into(),
154 actual: format!("{other:?}").into_bytes(),
155 })),
156 }
157 }
158
159 /// Set all lock/control fields (LC 6-field write).
160 ///
161 /// Sends the full `LC a,b,c,d,e,f` format to configure all lock parameters at once.
162 ///
163 /// # Parameters
164 ///
165 /// - `locked`: master lock enable (`true` = locked, `false` = unlocked). Inverted
166 /// before sending on the wire (D75: `0` = locked, `1` = unlocked).
167 /// - `lock_type`: what to lock — key only, key+PTT, or key+PTT+dial.
168 /// - `lock_a`: lock Band A controls (`true` = locked).
169 /// - `lock_b`: lock Band B controls (`true` = locked).
170 /// - `lock_c`: lock Band C controls (`true` = locked).
171 /// - `lock_ptt`: lock the PTT button (`true` = locked, prevents transmission).
172 ///
173 /// # Errors
174 ///
175 /// Returns an error if the command fails or the response is unexpected.
176 #[allow(clippy::fn_params_excessive_bools)]
177 pub async fn set_lock_full(
178 &mut self,
179 locked: bool,
180 lock_type: KeyLockType,
181 lock_a: bool,
182 lock_b: bool,
183 lock_c: bool,
184 lock_ptt: bool,
185 ) -> Result<(), Error> {
186 tracing::info!(
187 locked,
188 ?lock_type,
189 lock_a,
190 lock_b,
191 lock_c,
192 lock_ptt,
193 "setting full lock configuration"
194 );
195 // Invert master lock: logical true (locked) → wire 0.
196 let response = self
197 .execute(Command::SetLockFull {
198 locked: !locked,
199 lock_type,
200 lock_a,
201 lock_b,
202 lock_c,
203 lock_ptt,
204 })
205 .await?;
206 match response {
207 Response::Lock { .. } => Ok(()),
208 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
209 expected: "Lock".into(),
210 actual: format!("{other:?}").into_bytes(),
211 })),
212 }
213 }
214
215 /// Get the dual-band enabled state (DL read).
216 ///
217 /// On the TH-D75, the DL wire value is inverted: `DL 0` means dual
218 /// band enabled, `DL 1` means single band. This method inverts the
219 /// response so that `true` means dual band is enabled (logical meaning).
220 ///
221 /// # Errors
222 ///
223 /// Returns an error if the command fails or the response is unexpected.
224 pub async fn get_dual_band(&mut self) -> Result<bool, Error> {
225 tracing::debug!("reading dual-band state");
226 let response = self.execute(Command::GetDualBand).await?;
227 match response {
228 // Wire value is inverted: 0 = dual band, 1 = single band.
229 Response::DualBand { enabled } => Ok(!enabled),
230 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
231 expected: "DualBand".into(),
232 actual: format!("{other:?}").into_bytes(),
233 })),
234 }
235 }
236
237 /// Set the dual-band enabled state (DL write).
238 ///
239 /// Accepts logical meaning: `true` = dual band enabled, `false` =
240 /// single band. Inverts before sending on the wire (`DL 0` = dual
241 /// band, `DL 1` = single band on the D75).
242 ///
243 /// # Errors
244 ///
245 /// Returns an error if the command fails or the response is unexpected.
246 pub async fn set_dual_band(&mut self, enabled: bool) -> Result<(), Error> {
247 tracing::debug!(enabled, "setting dual-band state");
248 // Invert: logical true (dual band) → wire 0, logical false → wire 1.
249 let response = self
250 .execute(Command::SetDualBand { enabled: !enabled })
251 .await?;
252 match response {
253 Response::DualBand { .. } => Ok(()),
254 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
255 expected: "DualBand".into(),
256 actual: format!("{other:?}").into_bytes(),
257 })),
258 }
259 }
260
261 /// Step frequency down on the given band (DW action), then read back the
262 /// resulting frequency.
263 ///
264 /// Sends `DW` to step down, then issues `FQ` to read back the new
265 /// frequency. Returns the post-step [`ChannelMemory`].
266 ///
267 /// Per KI4LAX CAT reference: DW tunes the current band's frequency
268 /// down by the current step size. Counterpart to [`frequency_up`](super::Radio::frequency_up).
269 ///
270 /// # VFO mode requirement
271 ///
272 /// The target band must be in VFO mode for this command to take effect. In Memory mode,
273 /// the command may be ignored or return an error.
274 ///
275 /// # Step size
276 ///
277 /// The frequency moves by the band's current step size (see
278 /// [`get_step_size`](super::Radio::get_step_size) /
279 /// [`set_step_size`](super::Radio::set_step_size)). The step size varies by
280 /// band and mode — for example, 25 kHz for FM, 1 kHz for SSB.
281 ///
282 /// # Wire format
283 ///
284 /// `DW band\r` where band is 0 (A) or 1 (B). Despite the mnemonic suggesting "Dual Watch",
285 /// on the D75 this is strictly frequency-down.
286 ///
287 /// # Errors
288 ///
289 /// Returns an error if the command fails or the response is unexpected.
290 ///
291 /// [`ChannelMemory`]: crate::types::ChannelMemory
292 pub async fn frequency_down(&mut self, band: Band) -> Result<ChannelMemory, Error> {
293 tracing::debug!(?band, "stepping frequency down");
294 let response = self.execute(Command::FrequencyDown { band }).await?;
295 // The radio echoes either `DW\r` (parsed as FrequencyDown) or a bare
296 // OK depending on firmware version and AI mode state.
297 match response {
298 Response::FrequencyDown | Response::Ok => {}
299 other => {
300 return Err(Error::Protocol(ProtocolError::UnexpectedResponse {
301 expected: "FrequencyDown".into(),
302 actual: format!("{other:?}").into_bytes(),
303 }));
304 }
305 }
306 self.get_frequency(band).await
307 }
308
309 /// Step frequency down on the given band (DW action) without reading
310 /// back the result.
311 ///
312 /// This is the fire-and-forget variant of [`frequency_down`](Self::frequency_down).
313 /// Use this when you do not need the resulting frequency (saves one
314 /// round-trip).
315 ///
316 /// # Errors
317 ///
318 /// Returns an error if the command fails or the response is unexpected.
319 pub async fn frequency_down_blind(&mut self, band: Band) -> Result<(), Error> {
320 tracing::debug!(?band, "stepping frequency down (blind)");
321 let response = self.execute(Command::FrequencyDown { band }).await?;
322 match response {
323 Response::FrequencyDown | Response::Ok => Ok(()),
324 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
325 expected: "FrequencyDown".into(),
326 actual: format!("{other:?}").into_bytes(),
327 })),
328 }
329 }
330
331 /// Get the Bluetooth enabled state (BT read).
332 ///
333 /// # Errors
334 ///
335 /// Returns an error if the command fails or the response is unexpected.
336 pub async fn get_bluetooth(&mut self) -> Result<bool, Error> {
337 tracing::debug!("reading Bluetooth state");
338 let response = self.execute(Command::GetBluetooth).await?;
339 match response {
340 Response::Bluetooth { enabled } => Ok(enabled),
341 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
342 expected: "Bluetooth".into(),
343 actual: format!("{other:?}").into_bytes(),
344 })),
345 }
346 }
347
348 /// Set the Bluetooth enabled state (BT write).
349 ///
350 /// # Errors
351 ///
352 /// Returns an error if the command fails or the response is unexpected.
353 pub async fn set_bluetooth(&mut self, enabled: bool) -> Result<(), Error> {
354 tracing::info!(enabled, "setting Bluetooth state");
355 let response = self.execute(Command::SetBluetooth { enabled }).await?;
356 match response {
357 Response::Bluetooth { .. } => Ok(()),
358 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
359 expected: "Bluetooth".into(),
360 actual: format!("{other:?}").into_bytes(),
361 })),
362 }
363 }
364
365 /// Get the attenuator state for the given band (RA read).
366 ///
367 /// # Errors
368 ///
369 /// Returns an error if the command fails or the response is unexpected.
370 pub async fn get_attenuator(&mut self, band: Band) -> Result<bool, Error> {
371 tracing::debug!(?band, "reading attenuator state");
372 let response = self.execute(Command::GetAttenuator { band }).await?;
373 match response {
374 Response::Attenuator { enabled, .. } => Ok(enabled),
375 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
376 expected: "Attenuator".into(),
377 actual: format!("{other:?}").into_bytes(),
378 })),
379 }
380 }
381
382 /// Set the attenuator state for the given band (RA write).
383 ///
384 /// # Errors
385 ///
386 /// Returns an error if the command fails or the response is unexpected.
387 pub async fn set_attenuator(&mut self, band: Band, enabled: bool) -> Result<(), Error> {
388 tracing::debug!(?band, enabled, "setting attenuator state");
389 let response = self
390 .execute(Command::SetAttenuator { band, enabled })
391 .await?;
392 match response {
393 Response::Attenuator { .. } => Ok(()),
394 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
395 expected: "Attenuator".into(),
396 actual: format!("{other:?}").into_bytes(),
397 })),
398 }
399 }
400
401 /// Set the auto-info mode (AI write). This is a write-only command.
402 ///
403 /// When enabled (`AI 1`), the radio pushes unsolicited status updates over the serial
404 /// connection whenever internal state changes. This includes frequency changes (FQ),
405 /// mode changes (MD), squelch changes (SQ), and busy state transitions (BY). Without
406 /// AI mode, the only way to detect changes is to poll each command individually.
407 ///
408 /// Unsolicited frames pushed by the radio are delivered through the broadcast channel
409 /// returned by [`subscribe`](Self::subscribe). The `execute()` method routes solicited
410 /// responses (matching the sent command's mnemonic) to the caller and unsolicited frames
411 /// to the broadcast channel.
412 ///
413 /// This command is write-only — there is no `AI` read form. To check the current state,
414 /// you must track it in your application after calling this method.
415 ///
416 /// # Wire format
417 ///
418 /// `AI 0\r` (disable) or `AI 1\r` (enable).
419 ///
420 /// # Errors
421 ///
422 /// Returns an error if the command fails or the response is unexpected.
423 pub async fn set_auto_info(&mut self, enabled: bool) -> Result<(), Error> {
424 tracing::info!(enabled, "setting auto-info mode");
425 let response = self.execute(Command::SetAutoInfo { enabled }).await?;
426 match response {
427 Response::AutoInfo { .. } => Ok(()),
428 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
429 expected: "AutoInfo".into(),
430 actual: format!("{other:?}").into_bytes(),
431 })),
432 }
433 }
434
435 /// Get the radio type/region code (TY read).
436 ///
437 /// Returns a tuple of (region code, variant number). For example,
438 /// `("K", 2)` indicates a US-region radio, hardware variant 2.
439 ///
440 /// This command is not in the firmware's 53-command dispatch table.
441 ///
442 /// # Errors
443 ///
444 /// Returns an error if the command fails or the response is unexpected.
445 pub async fn get_radio_type(&mut self) -> Result<(String, u8), Error> {
446 tracing::debug!("reading radio type/region");
447 let response = self.execute(Command::GetRadioType).await?;
448 match response {
449 Response::RadioType { region, variant } => Ok((region, variant)),
450 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
451 expected: "RadioType".into(),
452 actual: format!("{other:?}").into_bytes(),
453 })),
454 }
455 }
456
457 /// Get the USB Out select state (IO read).
458 ///
459 /// Per KI4LAX CAT reference and Operating Tips §5.10.5:
460 /// 0 = AF (audio frequency output), 1 = IF (12 kHz centered IF signal
461 /// for SSB/CW/AM, 15 kHz bandwidth), 2 = Detect (pre-detection signal).
462 ///
463 /// Menu 102 (USB Out Select) controls this. IF/Detect output is only
464 /// available when in Single Band mode on Band B.
465 ///
466 /// # Errors
467 ///
468 /// Returns an error if the command fails or the response is unexpected.
469 pub async fn get_io_port(&mut self) -> Result<DetectOutputMode, Error> {
470 tracing::debug!("reading I/O port state");
471 let response = self.execute(Command::GetIoPort).await?;
472 match response {
473 Response::IoPort { value } => Ok(value),
474 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
475 expected: "IoPort".into(),
476 actual: format!("{other:?}").into_bytes(),
477 })),
478 }
479 }
480
481 /// Set the USB Out select state (IO write).
482 ///
483 /// See [`get_io_port`](Self::get_io_port) for value meanings.
484 ///
485 /// # Errors
486 ///
487 /// Returns an error if the command fails or the response is unexpected.
488 pub async fn set_io_port(&mut self, value: DetectOutputMode) -> Result<(), Error> {
489 tracing::debug!(?value, "setting I/O port output mode");
490 let response = self.execute(Command::SetIoPort { value }).await?;
491 match response {
492 Response::IoPort { .. } => Ok(()),
493 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
494 expected: "IoPort".into(),
495 actual: format!("{other:?}").into_bytes(),
496 })),
497 }
498 }
499
500 /// Query SD card / programming interface status (SD read).
501 ///
502 /// The firmware's SD handler primarily checks for `SD PROGRAM` to enter
503 /// MCP programming mode. The bare `SD` read response indicates
504 /// programming interface readiness, not SD card presence.
505 ///
506 /// # Errors
507 ///
508 /// Returns an error if the command fails or the response is unexpected.
509 pub async fn get_sd_status(&mut self) -> Result<bool, Error> {
510 tracing::debug!("reading SD/programming interface status");
511 let response = self.execute(Command::GetSdCard).await?;
512 match response {
513 Response::SdCard { present } => Ok(present),
514 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
515 expected: "SdCard".into(),
516 actual: format!("{other:?}").into_bytes(),
517 })),
518 }
519 }
520
521 /// Get MCP status (0E read).
522 ///
523 /// Returns `N` (not available) in normal operating mode. This mnemonic
524 /// appears to be MCP-related.
525 ///
526 /// # Errors
527 ///
528 /// Returns an error if the command fails or the response is unexpected.
529 pub async fn get_mcp_status(&mut self) -> Result<String, Error> {
530 tracing::debug!("reading MCP status");
531 let response = self.execute(Command::GetMcpStatus).await?;
532 match response {
533 Response::McpStatus { value } => Ok(value),
534 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
535 expected: "McpStatus".into(),
536 actual: format!("{other:?}").into_bytes(),
537 })),
538 }
539 }
540
541 // -----------------------------------------------------------------------
542 // MCP-based setting writes (for settings where CAT writes are rejected)
543 //
544 // The TH-D75 firmware rejects CAT write commands for several settings
545 // (BE, BL, DW return `?` for all write formats). These methods bypass
546 // CAT entirely and write directly to the verified MCP memory offsets.
547 //
548 // Each method enters MCP programming mode, reads the containing page,
549 // modifies the target byte, writes the page back, and exits. The USB
550 // connection does not survive the MCP transition — after calling any of
551 // these methods, drop the Radio and reconnect.
552 // -----------------------------------------------------------------------
553
554 /// Set key beep on/off via MCP memory write.
555 ///
556 /// The CAT `BE` command is a firmware stub on the TH-D75 — it always
557 /// returns `?` for writes. This method writes directly to the verified
558 /// MCP offset (`0x1071`) instead.
559 ///
560 /// # Connection lifetime
561 ///
562 /// This enters MCP programming mode. The USB connection drops after
563 /// exit. The `Radio` instance should be dropped and a fresh connection
564 /// established for subsequent CAT commands.
565 ///
566 /// # Errors
567 ///
568 /// Returns an error if entering programming mode, reading the page,
569 /// writing the page, or exiting programming mode fails.
570 pub async fn set_beep_via_mcp(&mut self, enabled: bool) -> Result<(), Error> {
571 const OFFSET: usize = 0x1071;
572 // 0x1071 / 256 = 0x10 = 16, fits in u16.
573 #[allow(clippy::cast_possible_truncation)]
574 const PAGE: u16 = (OFFSET / 256) as u16;
575 const BYTE_INDEX: usize = OFFSET % 256;
576
577 tracing::info!(enabled, offset = OFFSET, "setting key beep via MCP");
578 self.modify_memory_page(PAGE, |data| {
579 data[BYTE_INDEX] = u8::from(enabled);
580 })
581 .await
582 }
583
584 /// Set beep volume level via MCP memory write.
585 ///
586 /// The CAT `BE` command only supports on/off — volume level must be
587 /// set via MCP. Writes directly to verified MCP offset (`0x1072`).
588 /// Volume range is 0–7 (per Menu 915 in the Operating Tips §5.6.1).
589 ///
590 /// # Connection lifetime
591 ///
592 /// This enters MCP programming mode. The USB connection drops after
593 /// exit. The `Radio` instance should be dropped and a fresh connection
594 /// established for subsequent CAT commands.
595 ///
596 /// # Errors
597 ///
598 /// Returns an error if entering programming mode, reading the page,
599 /// writing the page, or exiting programming mode fails.
600 pub async fn set_beep_volume_via_mcp(&mut self, volume: u8) -> Result<(), Error> {
601 const OFFSET: usize = 0x1072;
602 #[allow(clippy::cast_possible_truncation)]
603 const PAGE: u16 = (OFFSET / 256) as u16;
604 const BYTE_INDEX: usize = OFFSET % 256;
605
606 if volume > 7 {
607 return Err(Error::Validation(
608 crate::error::ValidationError::SettingOutOfRange {
609 name: "beep volume",
610 value: volume,
611 detail: "must be 0-7",
612 },
613 ));
614 }
615
616 tracing::info!(volume, offset = OFFSET, "setting beep volume via MCP");
617 self.modify_memory_page(PAGE, |data| {
618 data[BYTE_INDEX] = volume;
619 })
620 .await
621 }
622
623 /// Set VOX enabled on/off via MCP memory write.
624 ///
625 /// Writes directly to the verified MCP offset (`0x101B`). This
626 /// provides an alternative to CAT for modes where CAT writes are
627 /// rejected.
628 ///
629 /// # Connection lifetime
630 ///
631 /// This enters MCP programming mode. The USB connection drops after
632 /// exit. The `Radio` instance should be dropped and a fresh connection
633 /// established for subsequent CAT commands.
634 ///
635 /// # Errors
636 ///
637 /// Returns an error if entering programming mode, reading the page,
638 /// writing the page, or exiting programming mode fails.
639 pub async fn set_vox_via_mcp(&mut self, enabled: bool) -> Result<(), Error> {
640 const OFFSET: usize = 0x101B;
641 // 0x101B / 256 = 0x10 = 16, fits in u16.
642 #[allow(clippy::cast_possible_truncation)]
643 const PAGE: u16 = (OFFSET / 256) as u16;
644 const BYTE_INDEX: usize = OFFSET % 256;
645
646 tracing::info!(enabled, offset = OFFSET, "setting VOX enable via MCP");
647 self.modify_memory_page(PAGE, |data| {
648 data[BYTE_INDEX] = u8::from(enabled);
649 })
650 .await
651 }
652
653 /// Set lock on/off via MCP memory write.
654 ///
655 /// Writes directly to the verified MCP offset (`0x1060`). This
656 /// provides an alternative to CAT for modes where CAT writes are
657 /// rejected.
658 ///
659 /// # Connection lifetime
660 ///
661 /// This enters MCP programming mode. The USB connection drops after
662 /// exit. The `Radio` instance should be dropped and a fresh connection
663 /// established for subsequent CAT commands.
664 ///
665 /// # Errors
666 ///
667 /// Returns an error if entering programming mode, reading the page,
668 /// writing the page, or exiting programming mode fails.
669 pub async fn set_lock_via_mcp(&mut self, locked: bool) -> Result<(), Error> {
670 const OFFSET: usize = 0x1060;
671 // 0x1060 / 256 = 0x10 = 16, fits in u16.
672 #[allow(clippy::cast_possible_truncation)]
673 const PAGE: u16 = (OFFSET / 256) as u16;
674 const BYTE_INDEX: usize = OFFSET % 256;
675
676 tracing::info!(locked, offset = OFFSET, "setting lock via MCP");
677 self.modify_memory_page(PAGE, |data| {
678 data[BYTE_INDEX] = u8::from(locked);
679 })
680 .await
681 }
682
683 /// Set Bluetooth on/off via MCP memory write.
684 ///
685 /// Writes directly to the verified MCP offset (`0x1078`). This
686 /// provides an alternative to CAT for modes where CAT writes are
687 /// rejected.
688 ///
689 /// # Connection lifetime
690 ///
691 /// This enters MCP programming mode. The USB connection drops after
692 /// exit. The `Radio` instance should be dropped and a fresh connection
693 /// established for subsequent CAT commands.
694 ///
695 /// # Errors
696 ///
697 /// Returns an error if entering programming mode, reading the page,
698 /// writing the page, or exiting programming mode fails.
699 pub async fn set_bluetooth_via_mcp(&mut self, enabled: bool) -> Result<(), Error> {
700 const OFFSET: usize = 0x1078;
701 // 0x1078 / 256 = 0x10 = 16, fits in u16.
702 #[allow(clippy::cast_possible_truncation)]
703 const PAGE: u16 = (OFFSET / 256) as u16;
704 const BYTE_INDEX: usize = OFFSET % 256;
705
706 tracing::info!(enabled, offset = OFFSET, "setting Bluetooth via MCP");
707 self.modify_memory_page(PAGE, |data| {
708 data[BYTE_INDEX] = u8::from(enabled);
709 })
710 .await
711 }
712}