kenwood_thd75/protocol/mod.rs
1//! Pure-logic CAT (Computer Aided Transceiver) command codec.
2//!
3//! This module handles serialization and parsing of the TH-D75's serial
4//! command protocol. Commands are ASCII text terminated by carriage return
5//! (`\r`), with parameters separated by commas. The protocol layer has no
6//! async or I/O dependencies — it operates purely on byte slices.
7//!
8//! All 55 CAT commands (53 from the firmware dispatch table, plus 2 extra
9//! mnemonics TY and 0E) are represented as variants of [`Command`] (outgoing) and [`Response`]
10//! (incoming). Use [`serialize`] and [`parse`] to convert between typed
11//! representations and wire format.
12
13pub mod aprs;
14pub mod bluetooth;
15pub mod codec;
16pub mod control;
17pub mod core;
18pub mod dstar;
19pub mod gps;
20pub mod memory;
21pub mod programming;
22pub mod scan;
23pub mod sd;
24pub mod service;
25pub mod tone;
26pub mod user;
27pub mod vfo;
28
29pub use codec::Codec;
30
31use crate::error::ProtocolError;
32#[allow(unused_imports)]
33use crate::types::{
34 AfGainLevel, Band, BeaconMode, CallsignSlot, ChannelMemory, DetectOutputMode, DstarSlot,
35 DvGatewayMode, FilterMode, FilterWidthIndex, FineStep, GpsRadioMode, KeyLockType, Mode,
36 PowerLevel, SMeterReading, ScanResumeMethod, SquelchLevel, StepSize, TncBaud, TncMode,
37 ToneCode, VfoMemoryMode, VoxDelay, VoxGain,
38};
39
40/// A CAT command to send to the radio.
41#[derive(Debug, Clone)]
42#[allow(clippy::module_name_repetitions)]
43pub enum Command {
44 // === Core (FQ, FO, FV, PS, ID, PC, BC, VM, FR) ===
45 /// Get frequency (FQ read).
46 ///
47 /// Returns the current frequency data for the target band.
48 /// Works in any mode.
49 GetFrequency {
50 /// Target band.
51 band: Band,
52 },
53 /// Set frequency (FQ write) -- takes full channel data.
54 ///
55 /// # Hardware note
56 ///
57 /// FQ write may be rejected by the TH-D75 (returns `?`). Prefer
58 /// [`SetFrequencyFull`](Command::SetFrequencyFull) (FO write) for
59 /// reliable VFO frequency changes, or use
60 /// [`Radio::tune_frequency`](crate::radio::Radio::tune_frequency)
61 /// which handles mode switching and uses FO write internally.
62 ///
63 /// # Mode requirement
64 /// Radio must be in VFO mode on the target band.
65 SetFrequency {
66 /// Target band.
67 band: Band,
68 /// Channel memory data.
69 channel: ChannelMemory,
70 },
71 /// Get full frequency and settings (FO read).
72 ///
73 /// Returns full channel data including tone, shift, and step settings.
74 /// Works in any mode.
75 GetFrequencyFull {
76 /// Target band.
77 band: Band,
78 },
79 /// Set full frequency and settings (FO write).
80 ///
81 /// # Mode requirement
82 /// Radio must be in VFO mode on the target band.
83 /// Use [`Radio::tune_frequency`](crate::radio::Radio::tune_frequency)
84 /// for automatic mode handling.
85 SetFrequencyFull {
86 /// Target band.
87 band: Band,
88 /// Channel memory data.
89 channel: ChannelMemory,
90 },
91 /// Get firmware version (FV).
92 GetFirmwareVersion,
93 /// Set firmware version (FV write) -- factory programming command.
94 ///
95 /// Wire format: `FV version\r`.
96 ///
97 /// # Safety
98 ///
99 /// **DANGEROUS FACTORY COMMAND.** This is intended for factory programming
100 /// only. Writing an incorrect firmware version string may brick the radio,
101 /// cause firmware validation failures, or void your warranty. **Do not use
102 /// unless you fully understand the consequences.**
103 SetFirmwareVersion {
104 /// Firmware version string to write.
105 version: String,
106 },
107 /// Get power on/off status (PS read).
108 GetPowerStatus,
109 /// Get radio model ID (ID).
110 GetRadioId,
111 /// Set radio model ID (ID write) -- factory programming command.
112 ///
113 /// Wire format: `ID model\r`.
114 ///
115 /// # Safety
116 ///
117 /// **DANGEROUS FACTORY COMMAND.** This is intended for factory programming
118 /// only. Writing an incorrect model ID may cause the radio to behave as a
119 /// different model, disable features, or brick the device. **Do not use
120 /// unless you fully understand the consequences.**
121 SetRadioId {
122 /// Model identification string to write.
123 model: String,
124 },
125 /// Get beep setting (BE read).
126 ///
127 /// D75 RE: `BE x` (x: 0=off, 1=on).
128 /// Sends bare `BE\r`.
129 ///
130 /// # Mode requirement
131 /// Hardware-verified: returns `N` (not available) in certain modes.
132 /// The beep setting may not be readable depending on the radio's
133 /// current operating state.
134 GetBeep,
135 /// Set beep on/off (BE write).
136 ///
137 /// D75 RE: `BE x` (x: 0=off, 1=on).
138 /// Sends `BE 0\r` or `BE 1\r`.
139 ///
140 /// # D75 firmware note
141 ///
142 /// The D75 `cat_be_handler` is a stub that always returns `?` for writes.
143 /// The beep setting can only be changed via MCP programming mode (direct
144 /// memory writes) or the radio's menu. The read (`BE\r`) works normally.
145 /// The handler copies a model string on bare read but immediately calls
146 /// `cat_send_error_response()` for all other lengths.
147 SetBeep {
148 /// Whether key beep is enabled.
149 enabled: bool,
150 },
151 /// Get power level (PC read).
152 GetPowerLevel {
153 /// Target band.
154 band: Band,
155 },
156 /// Set power level (PC write).
157 SetPowerLevel {
158 /// Target band.
159 band: Band,
160 /// Power level to set.
161 level: PowerLevel,
162 },
163 /// Get the current active band (BC bare read).
164 GetBand,
165 /// Set the active band (BC write).
166 ///
167 /// # Warning
168 /// This is an ACTION command that switches the radio's active band.
169 SetBand {
170 /// Target band.
171 band: Band,
172 },
173 /// Get VFO/Memory mode (VM read).
174 ///
175 /// Mode values: 0 = VFO, 1 = Memory, 2 = Call, 3 = WX.
176 /// Works in any mode. The response is used to update the
177 /// [`Radio`](crate::radio::Radio) cached mode state.
178 GetVfoMemoryMode {
179 /// Target band.
180 band: Band,
181 },
182 /// Set VFO/Memory mode (VM write).
183 ///
184 /// Mode values: 0 = VFO, 1 = Memory, 2 = Call, 3 = WX.
185 /// This is an ACTION command that changes the radio's operating mode.
186 /// Prefer the safe tuning methods which handle mode switching
187 /// automatically.
188 SetVfoMemoryMode {
189 /// Target band.
190 band: Band,
191 /// VFO/Memory mode.
192 mode: VfoMemoryMode,
193 },
194 /// Get FM radio on/off state (FR read).
195 GetFmRadio,
196 /// Set FM radio on/off state (FR write).
197 SetFmRadio {
198 /// Whether FM radio is enabled.
199 enabled: bool,
200 },
201
202 // === VFO (AG, SQ, SM, MD, FS, FT, SH, UP, RA) ===
203 /// Get AF gain level for a band (AG read).
204 ///
205 /// Per KI4LAX CAT reference: `AG` returns gain level 000-099.
206 /// Hardware observation: bare `AG\r` returns global gain. Band-indexed
207 /// `AG band\r` returns `?`. Read is bare only.
208 GetAfGain,
209 /// Set AF gain level (AG write).
210 ///
211 /// Per KI4LAX CAT reference: `AG AAA` (AAA: 000-099, 3-digit zero-padded).
212 /// Sends bare `AG level\r` (no band parameter — firmware rejects band-indexed writes).
213 ///
214 /// **Important: the `band` field is ignored by the firmware.** The AG
215 /// command on the TH-D75 is a global (non-band-specific) control.
216 /// Attempting to send a band-indexed write (`AG band,level`) results
217 /// in a `?` error response from the radio. The `band` field is
218 /// retained in this variant solely for API symmetry with other
219 /// band-indexed commands (e.g., [`Command::SetSquelch`],
220 /// [`Command::SetMode`]) so that callers can use a uniform
221 /// band+value pattern. The serializer discards it.
222 SetAfGain {
223 /// Target band. **Ignored by firmware** — AF gain is a global
224 /// control on the TH-D75. This field exists for API symmetry
225 /// with other band-indexed commands; the serializer discards it
226 /// and sends a bare `AG level\r`.
227 band: Band,
228 /// Gain level (0-99).
229 level: AfGainLevel,
230 },
231 /// Get squelch level (SQ read).
232 GetSquelch {
233 /// Target band.
234 band: Band,
235 },
236 /// Set squelch level (SQ write).
237 ///
238 /// Per KI4LAX CAT reference: `SQ x,yy` (x: band, yy: squelch level 0-6).
239 /// Sends `SQ band,level\r`.
240 SetSquelch {
241 /// Target band.
242 band: Band,
243 /// Squelch level (0-6 on D75).
244 level: SquelchLevel,
245 },
246 /// Get S-meter reading (SM read).
247 GetSmeter {
248 /// Target band.
249 band: Band,
250 },
251 /// Set S-meter value (SM write) -- calibration/test interface.
252 ///
253 /// Wire format: `SM band,level\r` (band 0-1, level is a hex nibble value).
254 ///
255 /// # Warning
256 ///
257 /// This is likely a calibration or test/debug interface. Setting the S-meter
258 /// value directly may interfere with normal signal strength readings. The
259 /// exact behavior and persistence of written values is undocumented.
260 SetSmeter {
261 /// Target band.
262 band: Band,
263 /// S-meter level value.
264 level: SMeterReading,
265 },
266 /// Get operating mode (MD read).
267 GetMode {
268 /// Target band.
269 band: Band,
270 },
271 /// Set operating mode (MD write).
272 ///
273 /// # Mode requirement
274 /// Radio must be in VFO mode on the target band. Setting the
275 /// operating mode (FM/DV/NFM/AM) only applies to the current VFO.
276 SetMode {
277 /// Target band.
278 band: Band,
279 /// Operating mode to set.
280 mode: Mode,
281 },
282 /// Get fine step (FS bare read).
283 ///
284 /// Firmware-verified: FS = Fine Step. Bare `FS\r` returns `FS value`
285 /// (single value, no band). Band-indexed reads are not supported.
286 GetFineStep,
287 /// Set fine step for a band (FS write).
288 ///
289 /// Firmware-verified: `FS band,step\r` (band 0-1, step 0-3).
290 ///
291 /// # Firmware bug (v1.03)
292 ///
293 /// FS write is broken on firmware 1.03 — the radio returns `N`
294 /// (not available) for all write attempts.
295 SetFineStep {
296 /// Target band.
297 band: Band,
298 /// Fine step to set (0-3).
299 step: FineStep,
300 },
301 /// Get function type (FT bare read, no band parameter).
302 ///
303 /// Sends `FT\r` (bare). The radio returns the current function type.
304 GetFunctionType,
305 /// Set fine tune on/off (FT write).
306 ///
307 /// Wire format: `FT band,value\r` (band 0-1, value 0=off, 1=on).
308 ///
309 /// Per Operating Tips section 5.10.6: Fine Tune only works with AM modulation
310 /// and Band B. The write form takes a band parameter unlike the bare read.
311 /// Set fine tune on/off (FT write).
312 ///
313 /// Wire format: `FT value\r` (bare, no band parameter per ARFC-D75 RE).
314 /// ARFC sends `FT 0\r` (off) or `FT 1\r` (on).
315 SetFunctionType {
316 /// Whether fine tune is enabled.
317 enabled: bool,
318 },
319 /// Get filter width by mode index (SH read).
320 ///
321 /// Per Operating Tips §5.10: SSB high-cut 2.2–3.0 kHz (Menu 120),
322 /// CW bandwidth 0.3–2.0 kHz (Menu 121), AM high-cut 3.0–7.5 kHz
323 /// (Menu 122). `mode_index`: 0 = SSB, 1 = CW, 2 = AM.
324 GetFilterWidth {
325 /// Receiver filter mode.
326 mode: FilterMode,
327 },
328 /// Set filter width by mode index (SH write).
329 ///
330 /// Sets the IF receive filter width for the specified mode. The width
331 /// value maps to the filter selection index for that mode (see
332 /// [`GetFilterWidth`](Command::GetFilterWidth) for mode descriptions).
333 SetFilterWidth {
334 /// Receiver filter mode.
335 mode: FilterMode,
336 /// Filter width index (0-4 for SSB/CW, 0-3 for AM).
337 width: FilterWidthIndex,
338 },
339 /// Step frequency up by one increment (UP action).
340 ///
341 /// # Mode requirement
342 /// Radio should be in VFO mode for frequency stepping. In Memory mode,
343 /// this steps through memory channels instead.
344 ///
345 /// # Warning
346 /// This is an ACTION command that changes the radio's active frequency.
347 /// There is no undo -- the previous frequency is not preserved.
348 FrequencyUp {
349 /// Target band.
350 band: Band,
351 },
352 /// Tune frequency down by one step (DW action).
353 ///
354 /// Per KI4LAX CAT reference: DW tunes the current band's frequency
355 /// down by the current step size. This is a write-only action command
356 /// (like UP). The band parameter selects which band to step.
357 FrequencyDown {
358 /// Target band.
359 band: Band,
360 },
361 /// Get attenuator state (RA read).
362 GetAttenuator {
363 /// Target band.
364 band: Band,
365 },
366 /// Set attenuator on/off (RA write).
367 SetAttenuator {
368 /// Target band.
369 band: Band,
370 /// Whether attenuator is enabled.
371 enabled: bool,
372 },
373
374 // === Control (AI, BY, DL, RX, TX, LC, IO, BL, VD, VG, VX) ===
375 /// Set auto-info notification mode (AI write).
376 ///
377 /// The firmware accepts values 0-200 (not just 0/1).
378 /// Values beyond 1 may control notification verbosity or
379 /// filter settings. The exact semantics of values 2-200
380 /// are undocumented.
381 SetAutoInfo {
382 /// Whether auto-info is enabled.
383 enabled: bool,
384 },
385 /// Get busy state (BY read).
386 GetBusy {
387 /// Target band.
388 band: Band,
389 },
390 /// Set busy/squelch state (BY write) -- test/debug interface.
391 ///
392 /// Wire format: `BY band,state\r` (band 0-1, state 0=not busy, 1=busy).
393 ///
394 /// # Warning
395 ///
396 /// This is likely a test or debug interface. Setting the busy state directly
397 /// may interfere with normal squelch operation. Use with caution.
398 SetBusy {
399 /// Target band.
400 band: Band,
401 /// Whether the channel is busy (squelch open).
402 busy: bool,
403 },
404 /// Get dual-band mode (DL read).
405 GetDualBand,
406 /// Set dual-band mode (DL write).
407 SetDualBand {
408 /// Whether dual-band is enabled.
409 enabled: bool,
410 },
411 /// Switch to receive mode (RX action).
412 Receive {
413 /// Target band.
414 band: Band,
415 },
416 /// Key the transmitter (TX action).
417 ///
418 /// # Safety
419 /// **This transmits on air.** Ensure you are authorized to transmit on
420 /// the current frequency, have proper identification, and comply with
421 /// all applicable regulations. Use [`Command::Receive`] to return to receive mode.
422 Transmit {
423 /// Target band.
424 band: Band,
425 },
426 /// Get lock/control settings (LC read).
427 ///
428 /// Returns the primary lock state as a boolean. For reading all
429 /// lock fields, use MCP memory offsets 0x1060–0x1065.
430 GetLock,
431 /// Set lock/control state — simple boolean form (LC write).
432 ///
433 /// Sends `LC 0` or `LC 1`. The `locked` field uses **wire semantics**:
434 /// on the D75 the wire value is inverted (`true` on the wire means
435 /// *unlocked*). The high-level `Radio::set_lock()` method handles
436 /// this inversion so callers can pass logical lock state.
437 ///
438 /// For full lock configuration, use
439 /// [`SetLockFull`](Command::SetLockFull).
440 SetLock {
441 /// Whether key lock is engaged (wire semantics — inverted on D75).
442 locked: bool,
443 },
444 /// Set all lock/control fields (LC 6-field write).
445 ///
446 /// Sends `LC a,b,c,d,e,f` where each field is a control flag:
447 /// - `a`: Key lock (0=off, 1=on) — MCP 0x1060
448 /// - `b`: Key lock type (0=key, 1=PTT, 2=key+PTT) — MCP 0x1061
449 /// - `c`: Lock key A (0=off, 1=on) — MCP 0x1062
450 /// - `d`: Lock key B (0=off, 1=on) — MCP 0x1063
451 /// - `e`: Lock key C (0=off, 1=on) — MCP 0x1064
452 /// - `f`: Lock PTT (0=off, 1=on) — MCP 0x1065
453 SetLockFull {
454 /// Key lock enabled.
455 locked: bool,
456 /// Key lock type (key-only, key+PTT, key+PTT+dial).
457 lock_type: KeyLockType,
458 /// Lock key A.
459 lock_a: bool,
460 /// Lock key B.
461 lock_b: bool,
462 /// Lock key C.
463 lock_c: bool,
464 /// Lock PTT.
465 lock_ptt: bool,
466 },
467 /// Get AF/IF/Detect output mode (IO read).
468 GetIoPort,
469 /// Set AF/IF/Detect output mode (IO write).
470 SetIoPort {
471 /// Output mode (AF/IF/Detect).
472 value: DetectOutputMode,
473 },
474 /// Get battery level (BL read).
475 ///
476 /// Per KI4LAX CAT reference: BL returns battery charge state.
477 /// 0=Empty (Red), 1=1/3 (Yellow), 2=2/3 (Green), 3=Full (Green),
478 /// 4=Charging (observed on hardware when USB power is connected).
479 GetBatteryLevel,
480 /// Set battery level display (BL write).
481 ///
482 /// Wire format: `BL display,level\r` (7 bytes with comma).
483 ///
484 /// # Warning
485 ///
486 /// The exact purpose of this command is unclear. It may control the battery
487 /// display indicator or be a calibration/test interface. The `display` and
488 /// `level` parameter semantics are undocumented.
489 SetBatteryLevel {
490 /// Display parameter (semantics unknown).
491 display: u8,
492 /// Level parameter (semantics unknown).
493 level: u8,
494 },
495 /// Get VOX delay (VD read).
496 ///
497 /// # Mode requirement
498 /// VOX must be enabled (`VX 1`) for VD read/write to succeed.
499 /// Returns `N` (not available) when VOX is off.
500 GetVoxDelay,
501 /// Set VOX delay (VD write).
502 ///
503 /// # Mode requirement
504 /// VOX must be enabled (`VX 1`) for VD writes to succeed.
505 /// Returns `N` (not available) when VOX is off. Enable VOX first
506 /// with [`SetVox`](Command::SetVox), then set the delay, then
507 /// optionally disable VOX again.
508 SetVoxDelay {
509 /// VOX delay (0-30, in 100ms units).
510 delay: VoxDelay,
511 },
512 /// Get VOX gain (VG read).
513 ///
514 /// # Mode requirement
515 /// VOX must be enabled (`VX 1`) for VG read/write to succeed.
516 /// Returns `N` (not available) when VOX is off.
517 GetVoxGain,
518 /// Set VOX gain (VG write).
519 ///
520 /// # Mode requirement
521 /// VOX must be enabled (`VX 1`) for VG writes to succeed.
522 /// Returns `N` (not available) when VOX is off.
523 SetVoxGain {
524 /// VOX gain (0-9).
525 gain: VoxGain,
526 },
527 /// Get VOX state (VX read).
528 GetVox,
529 /// Set VOX on/off (VX write).
530 SetVox {
531 /// Whether VOX is enabled.
532 enabled: bool,
533 },
534
535 // === Memory (ME, MR, 0M) ===
536 /// Get the current memory channel number for a band (MR read).
537 ///
538 /// Hardware-verified: `MR band\r` returns `MR bandCCC` where CCC is
539 /// the 3-digit channel number (no comma separator in the response).
540 /// Example: `MR 0\r` returns `MR 021` meaning band A, channel 21.
541 ///
542 /// This is a READ that queries which channel is active, not an action.
543 GetCurrentChannel {
544 /// Target band.
545 band: Band,
546 },
547 /// Get memory channel data (ME read).
548 GetMemoryChannel {
549 /// Channel number.
550 channel: u16,
551 },
552 /// Set memory channel data (ME write).
553 SetMemoryChannel {
554 /// Channel number.
555 channel: u16,
556 /// Channel memory data.
557 data: ChannelMemory,
558 },
559 /// Recall memory channel — switches the radio's active channel (MR write).
560 ///
561 /// # Mode requirement
562 /// Radio must be in Memory mode on the target band.
563 /// Use [`Radio::tune_channel`](crate::radio::Radio::tune_channel)
564 /// for automatic mode handling.
565 ///
566 /// # Warning
567 /// This is an ACTION command that changes the radio's active channel.
568 /// Format is `MR band,channel`. Despite the "get" in the name of the
569 /// Kenwood documentation, this command changes radio state.
570 RecallMemoryChannel {
571 /// Target band.
572 band: Band,
573 /// Channel number (3-digit, 000-999).
574 channel: u16,
575 },
576 /// Enter MCP programming mode (0M action).
577 ///
578 /// # Safety
579 /// **DANGEROUS:** This puts the radio into programming mode where it
580 /// stops responding to normal CAT commands. The radio must be manually
581 /// restarted to recover. Do not use unless implementing a full MCP
582 /// programming interface.
583 EnterProgrammingMode,
584
585 // === TNC / D-STAR / Clock (TN, DC, RT) ===
586 /// Get TNC mode (TN bare read).
587 ///
588 /// Hardware-verified: bare `TN\r` returns `TN mode,setting`.
589 /// Band-indexed `TN band\r` returns `?` (rejected).
590 ///
591 /// The D75 RE misidentified this as CTCSS tone. On hardware, TN
592 /// returns TNC mode data (e.g., `TN 0,0`).
593 ///
594 /// Valid mode values per firmware validation: 0, 1, 2, 3.
595 /// Mode 3 may correspond to MMDVM or Reflector Terminal mode.
596 GetTncMode,
597 /// Set TNC mode (TN write).
598 ///
599 /// Wire format: `TN mode,setting\r`.
600 ///
601 /// Valid mode values per firmware validation: 0, 1, 2, 3.
602 /// Mode 3 may correspond to MMDVM or Reflector Terminal mode.
603 SetTncMode {
604 /// TNC operating mode (APRS/NAVITRA/KISS/MMDVM).
605 mode: TncMode,
606 /// TNC data speed setting.
607 setting: TncBaud,
608 },
609 /// Get D-STAR callsign data for a slot (DC read).
610 ///
611 /// Hardware-verified: `DC slot\r` where slot is 1-6.
612 /// `DC 0` returns `N` (not available). Slots 1-6 return callsign
613 /// data in format `DC slot,callsign,suffix`.
614 ///
615 /// The D75 RE misidentified this as DCS code. On hardware, DC
616 /// is the D-STAR callsign command.
617 GetDstarCallsign {
618 /// Callsign slot (1-6). Slot 0 returns `N`.
619 slot: DstarSlot,
620 },
621 /// Set D-STAR callsign for a slot (DC write).
622 ///
623 /// Wire format: `DC slot,callsign,suffix\r` where slot is 1-6,
624 /// callsign is 8 characters (space-padded), and suffix is up to
625 /// 4 characters.
626 SetDstarCallsign {
627 /// Callsign slot (1-6).
628 slot: DstarSlot,
629 /// Callsign string (8 chars, space-padded).
630 callsign: String,
631 /// Callsign suffix (up to 4 chars).
632 suffix: String,
633 },
634 /// Get real-time clock (RT bare read).
635 ///
636 /// Hardware-verified: bare `RT\r` returns `RT YYMMDDHHmmss`.
637 /// Band-indexed `RT band\r` returns `?` (rejected).
638 ///
639 /// The D75 RE misidentified this as repeater tone. On hardware, RT
640 /// returns the radio's real-time clock.
641 GetRealTimeClock,
642
643 // === Scan (SR, SF, BS) ===
644 /// Set scan resume mode (SR write-only).
645 ///
646 /// Hardware-verified: bare `SR\r` returns `?` (no read form).
647 /// SR is write-only. Sends `SR mode\r`.
648 ///
649 /// # Safety warning
650 /// On hardware, `SR 0` was observed to reboot the radio. The D75 RE
651 /// identifies this as scan resume, but the behavior may coincide with
652 /// a reset action. Use with caution.
653 SetScanResume {
654 /// Scan resume method.
655 mode: ScanResumeMethod,
656 },
657 /// Get step size for a band (SF read, band-indexed).
658 ///
659 /// Firmware-verified: SF = Step Size. `SF band\r` returns `SF band,step`.
660 /// Both `SF 0` and `SF 1` confirmed working.
661 GetStepSize {
662 /// Target band.
663 band: Band,
664 },
665 /// Set step size for a band (SF write).
666 ///
667 /// Firmware-verified: `SF band,step\r` (band 0-1, step index 0-11).
668 SetStepSize {
669 /// Target band.
670 band: Band,
671 /// Step size to set (0-11).
672 step: StepSize,
673 },
674 /// Get band scope data (BS read).
675 GetBandScope {
676 /// Target band.
677 band: Band,
678 },
679 /// Set band scope configuration (BS write).
680 ///
681 /// Wire format: `BS band,value\r` (band 0-1, value meaning unknown).
682 SetBandScope {
683 /// Target band.
684 band: Band,
685 /// Band scope value (semantics unknown).
686 value: u8,
687 },
688
689 // === APRS (AS, PT, MS) ===
690 /// Get TNC baud rate (AS read).
691 ///
692 /// Returns 0 = 1200 baud, 1 = 9600 baud.
693 GetTncBaud,
694 /// Set TNC baud rate (AS write).
695 ///
696 /// Values: 0 = 1200 baud, 1 = 9600 baud.
697 SetTncBaud {
698 /// Baud rate.
699 rate: TncBaud,
700 },
701
702 // === Serial Info (AE) ===
703 /// Get serial number and model code (AE read).
704 ///
705 /// Despite the AE mnemonic (historically "APRS Extended"), this command
706 /// returns the radio's serial number and model code.
707 GetSerialInfo,
708 /// Get beacon TX control mode (PT read).
709 GetBeaconType,
710 /// Set beacon TX control mode (PT write).
711 SetBeaconType {
712 /// Beacon transmission mode.
713 mode: BeaconMode,
714 },
715 /// Get APRS position source (MS read).
716 GetPositionSource,
717 /// Send message (MS write).
718 SendMessage {
719 /// Message text to send.
720 text: String,
721 },
722
723 // === D-STAR (DS, CS, GW) ===
724 /// Get active D-STAR callsign slot (DS read).
725 GetDstarSlot,
726 /// Set active D-STAR callsign slot (DS write).
727 SetDstarSlot {
728 /// D-STAR memory slot (1-6).
729 slot: DstarSlot,
730 },
731 /// Get the active callsign slot number (CS bare read).
732 ///
733 /// CS returns a slot number (0-10), NOT the callsign text itself.
734 /// The actual callsign text is read via DC (D-STAR callsign) slots 1-6.
735 GetActiveCallsignSlot,
736 /// Set the active callsign slot (CS write).
737 ///
738 /// Selects which callsign slot is active. Format: `CS N` where N is
739 /// the slot number. The callsign text itself is read via DC slots.
740 SetActiveCallsignSlot {
741 /// Callsign slot to select (0-10).
742 slot: CallsignSlot,
743 },
744 /// Get gateway (GW read).
745 GetGateway,
746 /// Set DV Gateway mode (GW write).
747 SetGateway {
748 /// DV Gateway mode (Off or Reflector Terminal).
749 value: DvGatewayMode,
750 },
751
752 // === GPS (GP, GM, GS) ===
753 /// Get GPS configuration (GP read).
754 ///
755 /// Returns GPS enabled and PC output enabled flags.
756 GetGpsConfig,
757 /// Set GPS configuration (GP write).
758 ///
759 /// Sets GPS enabled and PC output enabled flags.
760 SetGpsConfig {
761 /// Whether GPS is enabled.
762 gps_enabled: bool,
763 /// Whether GPS PC output is enabled.
764 pc_output: bool,
765 },
766 /// Get GPS/Radio mode status (GM bare read).
767 ///
768 /// # Warning
769 /// Only use bare `GM\r` (no parameter). Sending `GM 1\r` **reboots the
770 /// radio** into GPS-only mode. This command only supports the bare read.
771 GetGpsMode,
772 /// Get GPS NMEA sentence enable flags (GS read).
773 ///
774 /// Returns 6 boolean flags for GGA, GLL, GSA, GSV, RMC, VTG.
775 GetGpsSentences,
776 /// Set GPS NMEA sentence enable flags (GS write).
777 ///
778 /// Sets 6 boolean flags for GGA, GLL, GSA, GSV, RMC, VTG.
779 #[allow(clippy::struct_excessive_bools)]
780 SetGpsSentences {
781 /// GGA (Global Positioning System Fix Data) enabled.
782 gga: bool,
783 /// GLL (Geographic Position - Latitude/Longitude) enabled.
784 gll: bool,
785 /// GSA (GNSS DOP and Active Satellites) enabled.
786 gsa: bool,
787 /// GSV (GNSS Satellites in View) enabled.
788 gsv: bool,
789 /// RMC (Recommended Minimum Navigation Information) enabled.
790 rmc: bool,
791 /// VTG (Course Over Ground and Ground Speed) enabled.
792 vtg: bool,
793 },
794
795 // === Bluetooth (BT) ===
796 /// Get Bluetooth state (BT read).
797 GetBluetooth,
798 /// Set Bluetooth on/off (BT write).
799 SetBluetooth {
800 /// Whether Bluetooth is enabled.
801 enabled: bool,
802 },
803
804 // === SD (SD) ===
805 /// Query SD card / programming interface status (SD read).
806 ///
807 /// Note: The firmware's SD handler primarily checks for `SD PROGRAM`
808 /// to enter MCP programming mode. The bare `SD` read response (`SD 0/1`)
809 /// appears to indicate programming interface readiness, not SD card
810 /// presence. Do NOT send `SD PROGRAM` — it enters programming mode
811 /// and the radio stops responding to normal CAT commands.
812 GetSdCard,
813
814 // === User (US) ===
815 /// Get user settings (US read).
816 ///
817 /// # Hardware note
818 ///
819 /// US returns `?` on all tested formats on the TH-D75 and may not be
820 /// implemented. Both bare `US` and indexed `US NN` formats were rejected
821 /// during gap probe testing with firmware 1.03.
822 GetUserSettings,
823
824 // === Extra (TY, 0E) ===
825 /// Get radio type/region code (TY read).
826 ///
827 /// Not in the firmware's 53-command dispatch table — likely processed
828 /// by a separate code path. Returns a region string and variant number
829 /// (e.g., `TY K,2` for US region, variant 2).
830 GetRadioType,
831 /// Get MCP status (0E read).
832 ///
833 /// Returns `N` (not available) in normal operating mode. This mnemonic
834 /// appears to be MCP-related. Its full behavior is unknown.
835 GetMcpStatus,
836
837 // === Service Mode (factory calibration/test — requires `0G KENWOOD` first) ===
838 /// Enter factory service mode (0G write).
839 ///
840 /// Wire format: `0G KENWOOD\r`. The radio validates the "KENWOOD"
841 /// password and switches from the standard 53-command CAT table to
842 /// the 34-entry service mode table. Normal commands will not work
843 /// until service mode is exited with [`ExitServiceMode`](Command::ExitServiceMode).
844 ///
845 /// Discovered via Ghidra RE of TH-D75 V1.03 firmware at 0xC006F464.
846 EnterServiceMode,
847
848 /// Exit factory service mode (0G bare).
849 ///
850 /// Wire format: `0G\r`. Exits service mode and restores the standard
851 /// CAT command table. The 0G handler accepts both the bare form (exit)
852 /// and the `0G KENWOOD` form (entry).
853 ///
854 /// Discovered via Ghidra RE of TH-D75 V1.03 firmware at 0xC006F464.
855 ExitServiceMode,
856
857 /// Read factory calibration data (0S read).
858 ///
859 /// Wire format: `0S\r`. Reads 200 bytes of factory calibration data
860 /// (118 bytes from 0x4E000 + 82 bytes from a second address).
861 /// Response is hex-encoded calibration data.
862 ///
863 /// Requires service mode (`0G KENWOOD` first).
864 /// Discovered via Ghidra RE at 0xC006F508.
865 ReadCalibrationData,
866
867 /// Write factory calibration data (0R write).
868 ///
869 /// Wire format: `0R data\r` where data is 400 hex characters encoding
870 /// 200 bytes. Total wire length is 404 bytes (2 mnemonic + 1 space +
871 /// 400 hex + 1 CR). Writes to the same addresses as 0S.
872 ///
873 /// # Safety
874 ///
875 /// **CRITICAL: Can corrupt factory calibration.** Incorrect data will
876 /// desynchronize RF calibration tables. Recovery may require professional
877 /// recalibration with test equipment. Always read calibration first (0S)
878 /// and keep a backup before writing.
879 ///
880 /// Requires service mode (`0G KENWOOD` first).
881 /// Discovered via Ghidra RE at 0xC006F546.
882 WriteCalibrationData {
883 /// 400 hex characters (200 bytes of calibration data).
884 data: String,
885 },
886
887 /// Get service/MCP status (0E service mode read).
888 ///
889 /// Wire format: `0E\r`. Reads 3 bytes from address 0x110 (hardware
890 /// status register). In service mode, 0E returns actual status data
891 /// rather than `N` (not available) as in normal mode.
892 ///
893 /// Requires service mode (`0G KENWOOD` first).
894 /// Discovered via Ghidra RE at 0xC006F4B0.
895 GetServiceStatus,
896
897 /// Service calibration parameter read/write (1A).
898 ///
899 /// Wire format: `1A\r` (read, 3 bytes). Delegates to the firmware's
900 /// command executor for calibration parameter access.
901 ///
902 /// Requires service mode (`0G KENWOOD` first).
903 /// Discovered via Ghidra RE at 0xC006F5D0.
904 ServiceCalibrate1A,
905
906 /// Service calibration parameter read/write (1D).
907 ///
908 /// Wire format: `1D\r` (read, 3 bytes). Same executor-based pattern
909 /// as 1A.
910 ///
911 /// Requires service mode (`0G KENWOOD` first).
912 /// Discovered via Ghidra RE at 0xC006F5E4.
913 ServiceCalibrate1D,
914
915 /// Service calibration parameter read/write (1E).
916 ///
917 /// Wire format: `1E\r` (read, 3 bytes) or `1E XXX\r` (write, 6 bytes
918 /// = 2 mnemonic + 1 space + 3 value). The firmware accepts both forms.
919 ///
920 /// Requires service mode (`0G KENWOOD` first).
921 /// Discovered via Ghidra RE at 0xC006F5F8.
922 ServiceCalibrate1E {
923 /// Optional 3-character value for write. `None` for read.
924 value: Option<String>,
925 },
926
927 /// Service calibration parameter read/write (1N).
928 ///
929 /// Wire format: `1N\r` (read, 3 bytes). Same executor-based pattern
930 /// as 1A.
931 ///
932 /// Requires service mode (`0G KENWOOD` first).
933 /// Discovered via Ghidra RE at 0xC006F6D0.
934 ServiceCalibrate1N,
935
936 /// Service calibration parameter read/write (1V).
937 ///
938 /// Wire format: `1V\r` (read, 3 bytes) or `1V XXX\r` (write, 6 bytes).
939 /// Same dual-mode pattern as 1E.
940 ///
941 /// Requires service mode (`0G KENWOOD` first).
942 /// Discovered via Ghidra RE at 0xC006F740.
943 ServiceCalibrate1V {
944 /// Optional 3-character value for write. `None` for read.
945 value: Option<String>,
946 },
947
948 /// Service calibration single parameter write (1W).
949 ///
950 /// Wire format: `1W X\r` (write only, 5 bytes total).
951 /// Single-character parameter, likely a mode or flag toggle.
952 ///
953 /// Requires service mode (`0G KENWOOD` first).
954 /// Discovered via Ghidra RE at 0xC006F766.
955 ServiceCalibrate1W {
956 /// Single-character parameter value.
957 value: String,
958 },
959
960 /// Write factory callsign/serial number (1I write).
961 ///
962 /// Wire format: `1I XXXXXXXX,YYY\r` (16 bytes total = 2 mnemonic +
963 /// 1 space + 8 hex chars + 1 comma + 3 hex chars + 1 CR). The firmware
964 /// validates all characters are alphanumeric (0-9, A-Z, a-z).
965 ///
966 /// # Safety
967 ///
968 /// **HIGH RISK: Changes the radio's factory serial number / callsign.**
969 /// This may void the warranty and could cause regulatory issues.
970 /// The original values should be backed up before any modification.
971 ///
972 /// Requires service mode (`0G KENWOOD` first).
973 /// Discovered via Ghidra RE at 0xC006F61E.
974 ServiceWriteId {
975 /// 8-character hex identifier (factory callsign part).
976 id: String,
977 /// 3-character hex code.
978 code: String,
979 },
980
981 /// Raw flash memory read/write (1F).
982 ///
983 /// Wire format for write: `1F AAAAAA,data\r` where AAAAAA is a 6-digit
984 /// hex address (max 0x4FFFF) and data is hex-encoded bytes. The firmware
985 /// validates that address + length does not exceed 0x50000. Read form
986 /// is context-dependent on the executor.
987 ///
988 /// # Safety
989 ///
990 /// **CRITICAL: Can brick the radio.** Raw flash writes can overwrite
991 /// boot code, calibration data, or firmware. There is no recovery
992 /// mechanism short of JTAG or factory repair. Never write to flash
993 /// addresses without understanding the memory map.
994 ///
995 /// Requires service mode (`0G KENWOOD` first).
996 /// Discovered via Ghidra RE at 0xC006F780.
997 ServiceFlashRead,
998
999 /// Raw flash memory write (1F write).
1000 ///
1001 /// Wire format: `1F AAAAAA,data\r` where AAAAAA is 6-digit hex address
1002 /// and data is hex-encoded bytes.
1003 ///
1004 /// # Safety
1005 ///
1006 /// **CRITICAL: Can brick the radio.** See [`ServiceFlashRead`](Command::ServiceFlashRead).
1007 ///
1008 /// Requires service mode (`0G KENWOOD` first).
1009 /// Discovered via Ghidra RE at 0xC006F780.
1010 ServiceFlashWrite {
1011 /// 6-digit hex flash address (0x000000 to 0x04FFFF).
1012 address: String,
1013 /// Hex-encoded data bytes to write.
1014 data: String,
1015 },
1016
1017 /// Generic write via executor (0W).
1018 ///
1019 /// Wire format: `0W\r` (3 bytes). Delegates to the firmware's command
1020 /// executor. The exact write operation depends on the executor's
1021 /// internal state.
1022 ///
1023 /// Requires service mode (`0G KENWOOD` first).
1024 /// Discovered via Ghidra RE at 0xC006F5BC.
1025 ServiceWriteConfig,
1026
1027 /// Service mode band selection (0Y).
1028 ///
1029 /// Wire format: `0Y band\r` (5 bytes total). Band is 0 or 1.
1030 /// Band 0 calls `radio_caller_06ef1c()`, band 1 calls
1031 /// `ipc_caller_06eef6()` — different code paths for the two
1032 /// receiver chains.
1033 ///
1034 /// Requires service mode (`0G KENWOOD` first).
1035 /// Discovered via Ghidra RE at 0xC006F4D0.
1036 ServiceBandSelect {
1037 /// Band number (0 or 1).
1038 band: u8,
1039 },
1040
1041 /// Bulk EEPROM/calibration data export (9E read).
1042 ///
1043 /// Wire format: `9E AAAAAA,LL\r` (13 bytes = 2 mnemonic + 1 space +
1044 /// 6-digit hex address + 1 comma + 2-digit hex length + 1 CR).
1045 /// Reads up to 256 bytes from the specified address. Length 0 means 256.
1046 /// Address + length must not exceed 0x50000.
1047 ///
1048 /// Response is 128-byte formatted hex data.
1049 ///
1050 /// Requires service mode (`0G KENWOOD` first).
1051 /// Discovered via Ghidra RE at 0xC006F826.
1052 ServiceReadEeprom {
1053 /// 6-digit hex address (max 0x4FFFF).
1054 address: String,
1055 /// 2-digit hex length (00 = 256 bytes).
1056 length: String,
1057 },
1058
1059 /// Targeted calibration read at specific offset (9R read).
1060 ///
1061 /// Wire format: `9R\r` (3 bytes). Returns 4 bytes of formatted
1062 /// calibration data at the current offset. The offset is determined
1063 /// by internal firmware state.
1064 ///
1065 /// Requires service mode (`0G KENWOOD` first).
1066 /// Discovered via Ghidra RE at 0xC006F8CA.
1067 ServiceReadEepromAddr,
1068
1069 /// Get internal version/variant information (2V).
1070 ///
1071 /// Wire format: `2V XX,YYY\r` (10 bytes = 2 mnemonic + 1 space +
1072 /// 2-digit hex param + 1 comma + 3-digit hex param + 1 CR).
1073 /// Returns internal model code (e.g., EX-5210), build date, hardware
1074 /// revision, and calibration date.
1075 ///
1076 /// Requires service mode (`0G KENWOOD` first).
1077 /// Discovered via Ghidra RE at 0xC006F910.
1078 ServiceGetVersion {
1079 /// 2-digit hex parameter.
1080 param1: String,
1081 /// 3-digit hex parameter.
1082 param2: String,
1083 },
1084
1085 /// Get hardware register / GPIO status (1G read).
1086 ///
1087 /// Wire format: `1G\r` (3 bytes). Returns hex-encoded hardware
1088 /// register values with a comma separator. Used for factory testing
1089 /// of GPIO and peripheral status.
1090 ///
1091 /// Requires service mode (`0G KENWOOD` first).
1092 /// Discovered via Ghidra RE at 0xC006F9A8.
1093 ServiceGetHardware,
1094
1095 /// New calibration command in D75 (1C write).
1096 ///
1097 /// Wire format: `1C XXX\r` (7 bytes = 2 mnemonic + 1 space + 3-digit
1098 /// hex value + 1 CR). Value must be less than 0x100 (256). Not present
1099 /// in the D74 firmware — likely related to the 220 MHz band (D75A)
1100 /// or enhanced DSP.
1101 ///
1102 /// Requires service mode (`0G KENWOOD` first).
1103 /// Discovered via Ghidra RE at 0xC006FA08.
1104 ServiceCalibrateNew {
1105 /// 3-digit hex value (0x000 to 0x0FF).
1106 value: String,
1107 },
1108
1109 /// Dynamic-length hardware configuration (1U).
1110 ///
1111 /// Wire format: `1U\r` (read, 3 bytes) or `1U data\r` (write, dynamic
1112 /// length determined by reading a hardware register). The firmware
1113 /// calls `os_disable_interrupts()` in the error path — this is a
1114 /// low-level hardware configuration command.
1115 ///
1116 /// Requires service mode (`0G KENWOOD` first).
1117 /// Discovered via Ghidra RE at 0xC006F6E4.
1118 ServiceDynamicParam {
1119 /// Optional data for write. `None` for read.
1120 data: Option<String>,
1121 },
1122}
1123
1124/// A parsed response from the radio.
1125#[derive(Debug, Clone)]
1126#[allow(clippy::module_name_repetitions)]
1127pub enum Response {
1128 // === Core ===
1129 /// Frequency response (FQ).
1130 Frequency {
1131 /// Band the frequency is on.
1132 band: Band,
1133 /// Channel memory data.
1134 channel: ChannelMemory,
1135 },
1136 /// Full frequency and settings response (FO).
1137 FrequencyFull {
1138 /// Band the data is for.
1139 band: Band,
1140 /// Channel memory data.
1141 channel: ChannelMemory,
1142 },
1143 /// Firmware version response (FV).
1144 FirmwareVersion {
1145 /// Version string.
1146 version: String,
1147 },
1148 /// Power status response (PS).
1149 PowerStatus {
1150 /// Whether the radio is on.
1151 on: bool,
1152 },
1153 /// Radio model ID response (ID).
1154 RadioId {
1155 /// Model identification string.
1156 model: String,
1157 },
1158 /// Power level response (PC).
1159 PowerLevel {
1160 /// Band the level is for.
1161 band: Band,
1162 /// Current power level.
1163 level: PowerLevel,
1164 },
1165 /// Band response (BC read).
1166 BandResponse {
1167 /// Current active band.
1168 band: Band,
1169 },
1170 /// VFO/Memory mode response (VM).
1171 ///
1172 /// Mode values: 0 = VFO, 1 = Memory, 2 = Call, 3 = WX.
1173 VfoMemoryMode {
1174 /// Band the mode is for.
1175 band: Band,
1176 /// VFO/Memory mode.
1177 mode: VfoMemoryMode,
1178 },
1179 /// FM radio on/off response (FR).
1180 FmRadio {
1181 /// Whether FM radio is enabled.
1182 enabled: bool,
1183 },
1184
1185 // === VFO ===
1186 /// AF gain response (AG).
1187 ///
1188 /// Per KI4LAX CAT reference: gain range 000-099.
1189 AfGain {
1190 /// Gain level (0-99). Global, not per-band.
1191 level: AfGainLevel,
1192 },
1193 /// Squelch level response (SQ).
1194 Squelch {
1195 /// Band the squelch is for.
1196 band: Band,
1197 /// Squelch level (0-6).
1198 level: SquelchLevel,
1199 },
1200 /// S-meter reading response (SM).
1201 Smeter {
1202 /// Band the reading is for.
1203 band: Band,
1204 /// S-meter level (0-5).
1205 level: SMeterReading,
1206 },
1207 /// Operating mode response (MD).
1208 Mode {
1209 /// Band the mode is for.
1210 band: Band,
1211 /// Current operating mode.
1212 mode: Mode,
1213 },
1214 /// Fine step response (FS).
1215 ///
1216 /// Firmware-verified: bare `FS\r` returns `FS value` (single value, no band).
1217 FineStep {
1218 /// Current fine step setting.
1219 step: FineStep,
1220 },
1221 /// Function type response (FT).
1222 FunctionType {
1223 /// Fine tune enabled (0=off, 1=on).
1224 enabled: bool,
1225 },
1226 /// Filter width response (SH).
1227 FilterWidth {
1228 /// Receiver filter mode queried.
1229 mode: FilterMode,
1230 /// Filter width index (0-4 for SSB/CW, 0-3 for AM).
1231 width: FilterWidthIndex,
1232 },
1233 /// Attenuator state response (RA).
1234 Attenuator {
1235 /// Band the state is for.
1236 band: Band,
1237 /// Whether attenuator is enabled.
1238 enabled: bool,
1239 },
1240
1241 // === Control ===
1242 /// Auto-info mode response (AI).
1243 AutoInfo {
1244 /// Whether auto-info is enabled.
1245 enabled: bool,
1246 },
1247 /// Busy state response (BY).
1248 Busy {
1249 /// Band the state is for.
1250 band: Band,
1251 /// Whether the channel is busy.
1252 busy: bool,
1253 },
1254 /// Dual-band mode response (DL).
1255 DualBand {
1256 /// Whether dual-band is enabled.
1257 enabled: bool,
1258 },
1259 /// Frequency down acknowledgement (DW).
1260 FrequencyDown,
1261 /// Beep setting response (BE).
1262 ///
1263 /// D75 RE: `BE x` (x: 0=off, 1=on).
1264 Beep {
1265 /// Whether key beep is enabled.
1266 enabled: bool,
1267 },
1268 /// Key lock state response (LC).
1269 ///
1270 /// The `locked` field uses **wire semantics**: on the D75 the wire
1271 /// value is inverted (`true` = unlocked on wire). `Radio::set_lock()`
1272 /// and `Radio::get_lock()` handle the inversion so callers see
1273 /// logical lock state.
1274 Lock {
1275 /// Whether key lock is engaged (wire semantics — inverted on D75).
1276 locked: bool,
1277 },
1278 /// AF/IF/Detect output mode response (IO).
1279 IoPort {
1280 /// Output mode.
1281 value: DetectOutputMode,
1282 },
1283 /// Battery level response (BL).
1284 ///
1285 /// 0=Empty (Red), 1=1/3 (Yellow), 2=2/3 (Green), 3=Full (Green),
1286 /// 4=Charging (USB power connected).
1287 BatteryLevel {
1288 /// Battery charge level (0–4, where 4 = charging).
1289 level: crate::types::BatteryLevel,
1290 },
1291 /// VOX delay response (VD).
1292 VoxDelay {
1293 /// Current VOX delay (0-30, in 100ms units).
1294 delay: VoxDelay,
1295 },
1296 /// VOX gain response (VG).
1297 VoxGain {
1298 /// Current VOX gain (0-9).
1299 gain: VoxGain,
1300 },
1301 /// VOX state response (VX).
1302 Vox {
1303 /// Whether VOX is enabled.
1304 enabled: bool,
1305 },
1306
1307 // === Memory ===
1308 /// Memory channel data response (ME).
1309 MemoryChannel {
1310 /// Channel number.
1311 channel: u16,
1312 /// Channel memory data.
1313 data: ChannelMemory,
1314 },
1315 /// Memory recall echo response (MR write acknowledgment).
1316 ///
1317 /// When `MR band,channel` is sent as a write/recall, the radio echoes
1318 /// back the band and channel as acknowledgment.
1319 MemoryRecall {
1320 /// Target band.
1321 band: Band,
1322 /// Channel number.
1323 channel: u16,
1324 },
1325 /// Current channel number response (MR read).
1326 ///
1327 /// Hardware-verified: `MR band\r` returns `MR bandCCC` (no comma).
1328 /// Example: `MR 0\r` returns `MR 021` = band A, channel 21.
1329 CurrentChannel {
1330 /// Band queried.
1331 band: Band,
1332 /// Current channel number on that band.
1333 channel: u16,
1334 },
1335 /// Programming mode acknowledgment (0M).
1336 ///
1337 /// The radio enters MCP programming mode and stops responding to
1338 /// normal CAT commands. This response should never actually be
1339 /// received in practice.
1340 ProgrammingMode,
1341
1342 // === TNC / D-STAR / Clock ===
1343 /// TNC mode response (TN).
1344 ///
1345 /// Hardware-verified: bare `TN\r` returns `TN mode,setting`.
1346 /// Example: `TN 0,0`.
1347 ///
1348 /// Valid mode values per firmware validation: 0, 1, 2, 3.
1349 /// Mode 3 may correspond to MMDVM or Reflector Terminal mode.
1350 TncMode {
1351 /// TNC operating mode.
1352 mode: TncMode,
1353 /// TNC data speed setting.
1354 setting: TncBaud,
1355 },
1356 /// D-STAR callsign data response (DC).
1357 ///
1358 /// Hardware-verified: `DC slot\r` returns `DC slot,callsign,suffix`.
1359 /// Example: `DC 1,KQ4NIT ,D75A`.
1360 DstarCallsign {
1361 /// Callsign slot (1-6).
1362 slot: DstarSlot,
1363 /// Callsign string (may be space-padded).
1364 callsign: String,
1365 /// Callsign suffix/module.
1366 suffix: String,
1367 },
1368 /// Real-time clock response (RT).
1369 ///
1370 /// Hardware-verified: bare `RT\r` returns `RT YYMMDDHHmmss`.
1371 /// Example: `RT 240104095700`.
1372 RealTimeClock {
1373 /// Raw datetime string in `YYMMDDHHmmss` format.
1374 datetime: String,
1375 },
1376
1377 // === Scan ===
1378 /// Step size response (SF).
1379 ///
1380 /// Firmware-verified: SF = Step Size. Format: `band,step` where band is 0/1
1381 /// and step is the step size index (0-11).
1382 StepSize {
1383 /// Band the step is for.
1384 band: Band,
1385 /// Current step size.
1386 step: StepSize,
1387 },
1388 /// Band scope data response (BS).
1389 ///
1390 /// BS echoes the band number when queried.
1391 BandScope {
1392 /// Band the scope is for.
1393 band: Band,
1394 },
1395
1396 // === APRS ===
1397 /// TNC baud rate response (AS).
1398 ///
1399 /// Values: 0 = 1200 baud, 1 = 9600 baud.
1400 TncBaud {
1401 /// Baud rate.
1402 rate: TncBaud,
1403 },
1404 /// Serial number and model code response (AE).
1405 ///
1406 /// Despite the AE mnemonic (historically "APRS Extended"), this command
1407 /// returns the radio's serial number and model code.
1408 /// Format: `serial,model_code` (e.g., `C3C10368,K01`).
1409 SerialInfo {
1410 /// Radio serial number.
1411 serial: String,
1412 /// Model code (e.g., "K01").
1413 model_code: String,
1414 },
1415 /// Beacon TX control mode response (PT).
1416 BeaconType {
1417 /// Beacon transmission mode.
1418 mode: BeaconMode,
1419 },
1420 /// APRS position source response (MS read).
1421 PositionSource {
1422 /// Position source index (0-based).
1423 source: u8,
1424 },
1425
1426 // === D-STAR ===
1427 /// Active D-STAR callsign slot response (DS).
1428 DstarSlot {
1429 /// Active D-STAR memory slot (1-6).
1430 slot: DstarSlot,
1431 },
1432 /// Active callsign slot number response (CS).
1433 ///
1434 /// CS returns a slot number, NOT the callsign text. The actual callsign
1435 /// text is accessible via DC (D-STAR callsign) slots 1-6.
1436 ActiveCallsignSlot {
1437 /// Active callsign slot (0-10).
1438 slot: CallsignSlot,
1439 },
1440 /// DV Gateway mode response (GW).
1441 Gateway {
1442 /// DV Gateway mode.
1443 value: DvGatewayMode,
1444 },
1445
1446 // === GPS ===
1447 /// GPS configuration response (GP).
1448 ///
1449 /// Two boolean fields: GPS enabled and PC output enabled.
1450 /// Format: `gps_enabled,pc_output` (e.g., `0,0`).
1451 GpsConfig {
1452 /// Whether GPS is enabled.
1453 gps_enabled: bool,
1454 /// Whether GPS PC output is enabled.
1455 pc_output: bool,
1456 },
1457 /// GPS/Radio mode status response (GM).
1458 ///
1459 /// 0 = Normal transceiver mode, 1 = GPS receiver mode.
1460 /// Firmware-verified: `cat_gm_handler` guard `local_18 < 2`.
1461 GpsMode {
1462 /// GPS/Radio operating mode.
1463 mode: GpsRadioMode,
1464 },
1465 /// GPS NMEA sentence enable flags response (GS).
1466 ///
1467 /// Six boolean fields controlling which NMEA sentences are output:
1468 /// GGA, GLL, GSA, GSV, RMC, VTG.
1469 GpsSentences {
1470 /// GGA (Global Positioning System Fix Data) enabled.
1471 gga: bool,
1472 /// GLL (Geographic Position - Latitude/Longitude) enabled.
1473 gll: bool,
1474 /// GSA (GNSS DOP and Active Satellites) enabled.
1475 gsa: bool,
1476 /// GSV (GNSS Satellites in View) enabled.
1477 gsv: bool,
1478 /// RMC (Recommended Minimum Navigation Information) enabled.
1479 rmc: bool,
1480 /// VTG (Course Over Ground and Ground Speed) enabled.
1481 vtg: bool,
1482 },
1483
1484 // === Bluetooth ===
1485 /// Bluetooth state response (BT).
1486 Bluetooth {
1487 /// Whether Bluetooth is enabled.
1488 enabled: bool,
1489 },
1490
1491 // === SD ===
1492 /// SD card / programming interface status response (SD).
1493 ///
1494 /// The firmware's SD handler primarily checks for `SD PROGRAM` to enter
1495 /// MCP programming mode. The bare `SD` read response (`SD 0/1`) appears
1496 /// to indicate programming interface readiness, not SD card presence.
1497 SdCard {
1498 /// Programming interface readiness flag.
1499 present: bool,
1500 },
1501
1502 // === User ===
1503 /// User settings response (US).
1504 ///
1505 /// Note: US returns `?` on all tested formats on the TH-D75 and may not
1506 /// be implemented. This variant exists for completeness but may never be
1507 /// received from the radio.
1508 UserSettings {
1509 /// User settings value.
1510 value: u8,
1511 },
1512
1513 // === Extra (TY, 0E) ===
1514 /// Radio type/region code response (TY).
1515 ///
1516 /// Returns the radio's region code and hardware variant.
1517 /// Example: `TY K,2` (K = US region, variant 2).
1518 RadioType {
1519 /// Region code string (e.g., "K" for US).
1520 region: String,
1521 /// Hardware variant number.
1522 variant: u8,
1523 },
1524 /// MCP status response (0E).
1525 ///
1526 /// Placeholder — always returns `N` (not available) in normal mode.
1527 McpStatus {
1528 /// Raw status value.
1529 value: String,
1530 },
1531
1532 // === Service Mode ===
1533 /// Service mode entry/exit response (0G).
1534 ServiceMode {
1535 /// Raw response data.
1536 data: String,
1537 },
1538 /// Factory calibration data response (0S).
1539 ServiceCalibrationData {
1540 /// Hex-encoded 200-byte calibration data.
1541 data: String,
1542 },
1543 /// Calibration data write acknowledgment (0R).
1544 ServiceCalibrationWrite {
1545 /// Response data (typically echo).
1546 data: String,
1547 },
1548 /// Individual calibration parameter response (1A, 1D, 1E, 1N, 1V, 1W, 1C, 1U).
1549 ServiceCalibrationParam {
1550 /// The command mnemonic that generated this response.
1551 mnemonic: String,
1552 /// Raw response data.
1553 data: String,
1554 },
1555 /// Write config acknowledgment (0W).
1556 ServiceWriteConfig {
1557 /// Response data.
1558 data: String,
1559 },
1560 /// Band select response (0Y).
1561 ServiceBandSelect {
1562 /// Response data.
1563 data: String,
1564 },
1565 /// Factory ID write acknowledgment (1I).
1566 ServiceWriteId {
1567 /// Response data.
1568 data: String,
1569 },
1570 /// Flash read/write response (1F).
1571 ServiceFlash {
1572 /// Hex-encoded flash data or write acknowledgment.
1573 data: String,
1574 },
1575 /// EEPROM bulk data response (9E).
1576 ServiceEepromData {
1577 /// Hex-encoded EEPROM/calibration data.
1578 data: String,
1579 },
1580 /// EEPROM targeted read response (9R).
1581 ServiceEepromAddr {
1582 /// 4-byte formatted calibration data.
1583 data: String,
1584 },
1585 /// Internal version/variant response (2V).
1586 ServiceVersion {
1587 /// Version/variant information string.
1588 data: String,
1589 },
1590 /// Hardware register/GPIO status response (1G).
1591 ServiceHardware {
1592 /// Hex-encoded register values.
1593 data: String,
1594 },
1595
1596 // === Special ===
1597 /// Write acknowledgment (radio echoes the command).
1598 Ok,
1599 /// Error response (`?\r`).
1600 Error,
1601 /// Not available response (`N\r`) — command not supported in current mode.
1602 NotAvailable,
1603}
1604
1605/// Get the CAT mnemonic for a command (for logging).
1606#[must_use]
1607pub const fn command_name(cmd: &Command) -> &'static str {
1608 match cmd {
1609 Command::GetFrequency { .. } | Command::SetFrequency { .. } => "FQ",
1610 Command::GetFrequencyFull { .. } | Command::SetFrequencyFull { .. } => "FO",
1611 Command::GetFirmwareVersion | Command::SetFirmwareVersion { .. } => "FV",
1612 Command::GetPowerStatus => "PS",
1613 Command::GetRadioId | Command::SetRadioId { .. } => "ID",
1614 Command::GetBeep | Command::SetBeep { .. } => "BE",
1615 Command::GetPowerLevel { .. } | Command::SetPowerLevel { .. } => "PC",
1616 Command::GetBand | Command::SetBand { .. } => "BC",
1617 Command::GetVfoMemoryMode { .. } | Command::SetVfoMemoryMode { .. } => "VM",
1618 Command::GetFmRadio | Command::SetFmRadio { .. } => "FR",
1619 Command::GetAfGain | Command::SetAfGain { .. } => "AG",
1620 Command::GetSquelch { .. } | Command::SetSquelch { .. } => "SQ",
1621 Command::GetSmeter { .. } | Command::SetSmeter { .. } => "SM",
1622 Command::GetMode { .. } | Command::SetMode { .. } => "MD",
1623 Command::GetFineStep | Command::SetFineStep { .. } => "FS",
1624 Command::GetFunctionType | Command::SetFunctionType { .. } => "FT",
1625 Command::GetFilterWidth { .. } | Command::SetFilterWidth { .. } => "SH",
1626 Command::FrequencyUp { .. } => "UP",
1627 Command::FrequencyDown { .. } => "DW",
1628 Command::GetAttenuator { .. } | Command::SetAttenuator { .. } => "RA",
1629 Command::SetAutoInfo { .. } => "AI",
1630 Command::GetBusy { .. } | Command::SetBusy { .. } => "BY",
1631 Command::GetDualBand | Command::SetDualBand { .. } => "DL",
1632 Command::Receive { .. } => "RX",
1633 Command::Transmit { .. } => "TX",
1634 Command::GetLock | Command::SetLock { .. } | Command::SetLockFull { .. } => "LC",
1635 Command::GetIoPort | Command::SetIoPort { .. } => "IO",
1636 Command::GetBatteryLevel | Command::SetBatteryLevel { .. } => "BL",
1637 Command::GetVoxDelay | Command::SetVoxDelay { .. } => "VD",
1638 Command::GetVoxGain | Command::SetVoxGain { .. } => "VG",
1639 Command::GetVox | Command::SetVox { .. } => "VX",
1640 Command::GetCurrentChannel { .. } | Command::RecallMemoryChannel { .. } => "MR",
1641 Command::GetMemoryChannel { .. } | Command::SetMemoryChannel { .. } => "ME",
1642 Command::EnterProgrammingMode => "0M",
1643 Command::GetTncMode | Command::SetTncMode { .. } => "TN",
1644 Command::GetDstarCallsign { .. } | Command::SetDstarCallsign { .. } => "DC",
1645 Command::GetRealTimeClock => "RT",
1646 Command::SetScanResume { .. } => "SR",
1647 Command::GetStepSize { .. } | Command::SetStepSize { .. } => "SF",
1648 Command::GetBandScope { .. } | Command::SetBandScope { .. } => "BS",
1649 Command::GetTncBaud | Command::SetTncBaud { .. } => "AS",
1650 Command::GetSerialInfo => "AE",
1651 Command::GetBeaconType | Command::SetBeaconType { .. } => "PT",
1652 Command::GetPositionSource | Command::SendMessage { .. } => "MS",
1653 Command::GetDstarSlot | Command::SetDstarSlot { .. } => "DS",
1654 Command::GetActiveCallsignSlot | Command::SetActiveCallsignSlot { .. } => "CS",
1655 Command::GetGateway | Command::SetGateway { .. } => "GW",
1656 Command::GetGpsConfig | Command::SetGpsConfig { .. } => "GP",
1657 Command::GetGpsMode => "GM",
1658 Command::GetGpsSentences | Command::SetGpsSentences { .. } => "GS",
1659 Command::GetBluetooth | Command::SetBluetooth { .. } => "BT",
1660 Command::GetSdCard => "SD",
1661 Command::GetUserSettings => "US",
1662 Command::GetRadioType => "TY",
1663 Command::GetMcpStatus | Command::GetServiceStatus => "0E",
1664 // Service mode
1665 Command::EnterServiceMode | Command::ExitServiceMode => "0G",
1666 Command::ReadCalibrationData => "0S",
1667 Command::WriteCalibrationData { .. } => "0R",
1668 Command::ServiceCalibrate1A => "1A",
1669 Command::ServiceCalibrate1D => "1D",
1670 Command::ServiceCalibrate1E { .. } => "1E",
1671 Command::ServiceCalibrate1N => "1N",
1672 Command::ServiceCalibrate1V { .. } => "1V",
1673 Command::ServiceCalibrate1W { .. } => "1W",
1674 Command::ServiceWriteId { .. } => "1I",
1675 Command::ServiceFlashRead | Command::ServiceFlashWrite { .. } => "1F",
1676 Command::ServiceWriteConfig => "0W",
1677 Command::ServiceBandSelect { .. } => "0Y",
1678 Command::ServiceReadEeprom { .. } => "9E",
1679 Command::ServiceReadEepromAddr => "9R",
1680 Command::ServiceGetVersion { .. } => "2V",
1681 Command::ServiceGetHardware => "1G",
1682 Command::ServiceCalibrateNew { .. } => "1C",
1683 Command::ServiceDynamicParam { .. } => "1U",
1684 }
1685}
1686
1687/// Serialize a command to wire format (bytes ending with `\r`).
1688///
1689/// Converts a [`Command`] into the byte sequence expected by the radio's
1690/// CAT protocol. Each serialized command ends with a carriage return.
1691#[must_use]
1692#[allow(clippy::too_many_lines)]
1693pub fn serialize(cmd: &Command) -> Vec<u8> {
1694 let cmd_mnemonic = command_name(cmd);
1695 tracing::debug!(command = %cmd_mnemonic, "serializing command");
1696
1697 // Try core write serialization first (FO write, FQ write)
1698 if let Some(body) = core::serialize_core_write(cmd) {
1699 let mut bytes = body.into_bytes();
1700 bytes.push(b'\r');
1701 tracing::trace!(wire = %String::from_utf8_lossy(&bytes), "serialized wire format");
1702 return bytes;
1703 }
1704
1705 // Try memory write serialization (ME write)
1706 if let Some(body) = memory::serialize_memory_write(cmd) {
1707 let mut bytes = body.into_bytes();
1708 bytes.push(b'\r');
1709 tracing::trace!(wire = %String::from_utf8_lossy(&bytes), "serialized wire format");
1710 return bytes;
1711 }
1712
1713 let body = match cmd {
1714 // Core
1715 Command::GetFrequency { band } => {
1716 format!("FQ {}", u8::from(*band))
1717 }
1718 Command::SetFrequency { .. } => {
1719 // Handled by serialize_core_write above; this arm is unreachable
1720 // but required for exhaustive matching.
1721 unreachable!("SetFrequency handled by core::serialize_core_write")
1722 }
1723 Command::GetFrequencyFull { band } => {
1724 format!("FO {}", u8::from(*band))
1725 }
1726 Command::SetFrequencyFull { .. } => {
1727 unreachable!("SetFrequencyFull handled by core::serialize_core_write")
1728 }
1729 Command::GetFirmwareVersion => "FV".to_owned(),
1730 Command::SetFirmwareVersion { version } => format!("FV {version}"),
1731 Command::GetPowerStatus => "PS".to_owned(),
1732 Command::GetRadioId => "ID".to_owned(),
1733 Command::SetRadioId { model } => format!("ID {model}"),
1734 Command::GetBeep => "BE".to_owned(),
1735 Command::SetBeep { enabled } => format!("BE {}", u8::from(*enabled)),
1736 Command::GetPowerLevel { band } => format!("PC {}", u8::from(*band)),
1737 Command::SetPowerLevel { band, level } => {
1738 format!("PC {},{}", u8::from(*band), u8::from(*level))
1739 }
1740 Command::GetBand => "BC".to_owned(),
1741 Command::SetBand { band } => format!("BC {}", u8::from(*band)),
1742 Command::GetVfoMemoryMode { band } => format!("VM {}", u8::from(*band)),
1743 Command::SetVfoMemoryMode { band, mode } => {
1744 format!("VM {},{}", u8::from(*band), u8::from(*mode))
1745 }
1746 Command::GetFmRadio => "FR".to_owned(),
1747 Command::SetFmRadio { enabled } => format!("FR {}", u8::from(*enabled)),
1748
1749 // VFO
1750 Command::GetAfGain => "AG".to_owned(),
1751 Command::SetAfGain { band: _, level } => {
1752 // D75 firmware AG write handler expects bare `AG AAA\r`.
1753 // Band-indexed `AG band,level` is rejected with `?`.
1754 // Per KI4LAX: 3-digit zero-padded, range 000-099.
1755 format!("AG {:03}", level.as_u8())
1756 }
1757 Command::GetSquelch { band } => format!("SQ {}", u8::from(*band)),
1758 Command::SetSquelch { band, level } => {
1759 format!("SQ {},{}", u8::from(*band), level.as_u8())
1760 }
1761 Command::GetSmeter { band } => format!("SM {}", u8::from(*band)),
1762 Command::SetSmeter { band, level } => {
1763 format!("SM {},{}", u8::from(*band), level.as_u8())
1764 }
1765 Command::GetMode { band } => format!("MD {}", u8::from(*band)),
1766 Command::SetMode { band, mode } => {
1767 format!("MD {},{}", u8::from(*band), u8::from(*mode))
1768 }
1769 Command::GetFineStep => "FS".to_owned(),
1770 Command::SetFineStep { band, step } => {
1771 format!("FS {},{}", u8::from(*band), u8::from(*step))
1772 }
1773 Command::GetFunctionType => "FT".to_owned(),
1774 Command::SetFunctionType { enabled } => {
1775 format!("FT {}", u8::from(*enabled))
1776 }
1777 Command::GetFilterWidth { mode } => format!("SH {}", u8::from(*mode)),
1778 Command::SetFilterWidth { mode, width } => {
1779 format!("SH {},{}", u8::from(*mode), width.as_u8())
1780 }
1781 Command::FrequencyUp { band } => format!("UP {}", u8::from(*band)),
1782 Command::FrequencyDown { band } => format!("DW {}", u8::from(*band)),
1783 Command::GetAttenuator { band } => format!("RA {}", u8::from(*band)),
1784 Command::SetAttenuator { band, enabled } => {
1785 format!("RA {},{}", u8::from(*band), u8::from(*enabled))
1786 }
1787
1788 // Control
1789 Command::SetAutoInfo { enabled } => format!("AI {}", u8::from(*enabled)),
1790 Command::GetBusy { band } => format!("BY {}", u8::from(*band)),
1791 Command::SetBusy { band, busy } => {
1792 format!("BY {},{}", u8::from(*band), u8::from(*busy))
1793 }
1794 Command::GetDualBand => "DL".to_owned(),
1795 Command::SetDualBand { enabled } => format!("DL {}", u8::from(*enabled)),
1796 Command::Receive { band } => format!("RX {}", u8::from(*band)),
1797 Command::Transmit { band } => format!("TX {}", u8::from(*band)),
1798 Command::GetLock => "LC".to_owned(),
1799 Command::SetLock { locked } => format!("LC {}", u8::from(*locked)),
1800 Command::SetLockFull {
1801 locked,
1802 lock_type,
1803 lock_a,
1804 lock_b,
1805 lock_c,
1806 lock_ptt,
1807 } => format!(
1808 "LC {},{},{},{},{},{}",
1809 u8::from(*locked),
1810 u8::from(*lock_type),
1811 u8::from(*lock_a),
1812 u8::from(*lock_b),
1813 u8::from(*lock_c),
1814 u8::from(*lock_ptt),
1815 ),
1816 Command::GetIoPort => "IO".to_owned(),
1817 Command::SetIoPort { value } => format!("IO {}", u8::from(*value)),
1818 Command::GetBatteryLevel => "BL".to_owned(),
1819 Command::SetBatteryLevel { display, level } => format!("BL {display},{level}"),
1820 Command::GetVoxDelay => "VD".to_owned(),
1821 Command::SetVoxDelay { delay } => format!("VD {}", delay.as_u8()),
1822 Command::GetVoxGain => "VG".to_owned(),
1823 Command::SetVoxGain { gain } => format!("VG {}", gain.as_u8()),
1824 Command::GetVox => "VX".to_owned(),
1825 Command::SetVox { enabled } => format!("VX {}", u8::from(*enabled)),
1826
1827 // Memory
1828 Command::GetCurrentChannel { band } => {
1829 format!("MR {}", u8::from(*band))
1830 }
1831 Command::GetMemoryChannel { channel } => {
1832 format!("ME {channel:03}")
1833 }
1834 Command::SetMemoryChannel { .. } => {
1835 unreachable!("SetMemoryChannel handled by memory::serialize_memory_write")
1836 }
1837 Command::RecallMemoryChannel { band, channel } => {
1838 format!("MR {},{channel:03}", u8::from(*band))
1839 }
1840 Command::EnterProgrammingMode => "0M PROGRAM".to_owned(),
1841
1842 // TNC / D-STAR / Clock
1843 Command::GetTncMode => "TN".to_owned(),
1844 Command::SetTncMode { mode, setting } => {
1845 format!("TN {},{}", u8::from(*mode), u8::from(*setting))
1846 }
1847 Command::GetDstarCallsign { slot } => format!("DC {}", slot.as_u8()),
1848 Command::SetDstarCallsign {
1849 slot,
1850 callsign,
1851 suffix,
1852 } => format!("DC {},{callsign},{suffix}", slot.as_u8()),
1853 Command::GetRealTimeClock => "RT".to_owned(),
1854
1855 // Scan
1856 Command::SetScanResume { mode } => format!("SR {}", mode.to_raw()),
1857 Command::GetStepSize { band } => format!("SF {}", u8::from(*band)),
1858 Command::SetStepSize { band, step } => {
1859 format!("SF {},{:X}", u8::from(*band), u8::from(*step))
1860 }
1861 Command::GetBandScope { band } => format!("BS {}", u8::from(*band)),
1862 Command::SetBandScope { band, value } => {
1863 format!("BS {},{}", u8::from(*band), value)
1864 }
1865
1866 // APRS
1867 Command::GetTncBaud => "AS".to_owned(),
1868 Command::SetTncBaud { rate } => format!("AS {}", u8::from(*rate)),
1869 Command::GetSerialInfo => "AE".to_owned(),
1870 Command::GetBeaconType => "PT".to_owned(),
1871 Command::SetBeaconType { mode } => format!("PT {}", u8::from(*mode)),
1872 Command::GetPositionSource => "MS".to_owned(),
1873 Command::SendMessage { text } => format!("MS {text}"),
1874
1875 // D-STAR
1876 Command::GetDstarSlot => "DS".to_owned(),
1877 Command::SetDstarSlot { slot } => format!("DS {}", slot.as_u8()),
1878 Command::GetActiveCallsignSlot => "CS".to_owned(),
1879 Command::SetActiveCallsignSlot { slot } => format!("CS {}", slot.as_u8()),
1880 Command::GetGateway => "GW".to_owned(),
1881 Command::SetGateway { value } => format!("GW {}", u8::from(*value)),
1882
1883 // GPS
1884 Command::GetGpsConfig => "GP".to_owned(),
1885 Command::SetGpsConfig {
1886 gps_enabled,
1887 pc_output,
1888 } => format!("GP {},{}", u8::from(*gps_enabled), u8::from(*pc_output)),
1889 Command::GetGpsMode => "GM".to_owned(),
1890 Command::GetGpsSentences => "GS".to_owned(),
1891 Command::SetGpsSentences {
1892 gga,
1893 gll,
1894 gsa,
1895 gsv,
1896 rmc,
1897 vtg,
1898 } => format!(
1899 "GS {},{},{},{},{},{}",
1900 u8::from(*gga),
1901 u8::from(*gll),
1902 u8::from(*gsa),
1903 u8::from(*gsv),
1904 u8::from(*rmc),
1905 u8::from(*vtg)
1906 ),
1907
1908 // Bluetooth
1909 Command::GetBluetooth => "BT".to_owned(),
1910 Command::SetBluetooth { enabled } => format!("BT {}", u8::from(*enabled)),
1911
1912 // SD
1913 Command::GetSdCard => "SD".to_owned(),
1914
1915 // User
1916 Command::GetUserSettings => "US".to_owned(),
1917
1918 // Extra
1919 Command::GetRadioType => "TY".to_owned(),
1920 Command::GetMcpStatus | Command::GetServiceStatus => "0E".to_owned(),
1921
1922 // Service mode
1923 Command::EnterServiceMode => "0G KENWOOD".to_owned(),
1924 Command::ExitServiceMode => "0G".to_owned(),
1925 Command::ReadCalibrationData => "0S".to_owned(),
1926 Command::WriteCalibrationData { data } => format!("0R {data}"),
1927 Command::ServiceCalibrate1A => "1A".to_owned(),
1928 Command::ServiceCalibrate1D => "1D".to_owned(),
1929 Command::ServiceCalibrate1E { value: None } => "1E".to_owned(),
1930 Command::ServiceCalibrate1E {
1931 value: Some(value), ..
1932 } => format!("1E {value}"),
1933 Command::ServiceCalibrate1N => "1N".to_owned(),
1934 Command::ServiceCalibrate1V { value: None } => "1V".to_owned(),
1935 Command::ServiceCalibrate1V {
1936 value: Some(value), ..
1937 } => format!("1V {value}"),
1938 Command::ServiceCalibrate1W { value } => format!("1W {value}"),
1939 Command::ServiceWriteId { id, code } => format!("1I {id},{code}"),
1940 Command::ServiceFlashRead => "1F".to_owned(),
1941 Command::ServiceFlashWrite { address, data } => format!("1F {address},{data}"),
1942 Command::ServiceWriteConfig => "0W".to_owned(),
1943 Command::ServiceBandSelect { band } => format!("0Y {band}"),
1944 Command::ServiceReadEeprom { address, length } => format!("9E {address},{length}"),
1945 Command::ServiceReadEepromAddr => "9R".to_owned(),
1946 Command::ServiceGetVersion { param1, param2 } => format!("2V {param1},{param2}"),
1947 Command::ServiceGetHardware => "1G".to_owned(),
1948 Command::ServiceCalibrateNew { value } => format!("1C {value}"),
1949 Command::ServiceDynamicParam { data: None } => "1U".to_owned(),
1950 Command::ServiceDynamicParam {
1951 data: Some(data), ..
1952 } => format!("1U {data}"),
1953 };
1954
1955 let mut bytes = body.into_bytes();
1956 bytes.push(b'\r');
1957 tracing::trace!(wire = %String::from_utf8_lossy(&bytes), "serialized wire format");
1958 bytes
1959}
1960
1961/// Parse a response frame (without trailing `\r`) into a typed [`Response`].
1962///
1963/// # Errors
1964///
1965/// Returns [`ProtocolError::UnknownCommand`] if the mnemonic is not
1966/// recognized. Returns [`ProtocolError::FieldParse`] for recognised
1967/// commands whose payload parsing is not yet implemented.
1968pub fn parse(frame: &[u8]) -> Result<Response, ProtocolError> {
1969 // Error response
1970 if frame == b"?" {
1971 tracing::debug!(mnemonic = "?", "parsing error response");
1972 return Ok(Response::Error);
1973 }
1974
1975 // Not-available response
1976 if frame == b"N" {
1977 tracing::debug!(mnemonic = "N", "parsing not-available response");
1978 return Ok(Response::NotAvailable);
1979 }
1980
1981 let frame_str = std::str::from_utf8(frame).map_err(|_| {
1982 tracing::warn!("failed to parse frame as UTF-8");
1983 ProtocolError::MalformedFrame(frame.to_vec())
1984 })?;
1985
1986 // Extract the mnemonic: first 2 characters.
1987 // Special case: "0M" starts with a digit.
1988 if frame_str.len() < 2 {
1989 tracing::warn!(frame = %frame_str, "frame too short to contain mnemonic");
1990 return Err(ProtocolError::MalformedFrame(frame.to_vec()));
1991 }
1992
1993 let mnemonic = &frame_str[..2];
1994 tracing::debug!(mnemonic = %mnemonic, "parsing response");
1995
1996 // The rest of the frame after the mnemonic (may start with a space).
1997 let payload = if frame_str.len() > 2 {
1998 frame_str[2..].trim_start()
1999 } else {
2000 ""
2001 };
2002
2003 // Try each sub-parser in turn.
2004 let result = core::parse_core(mnemonic, payload)
2005 .or_else(|| vfo::parse_vfo(mnemonic, payload))
2006 .or_else(|| control::parse_control(mnemonic, payload))
2007 .or_else(|| memory::parse_memory(mnemonic, payload))
2008 .or_else(|| tone::parse_tone(mnemonic, payload))
2009 .or_else(|| scan::parse_scan(mnemonic, payload))
2010 .or_else(|| aprs::parse_aprs(mnemonic, payload))
2011 .or_else(|| dstar::parse_dstar(mnemonic, payload))
2012 .or_else(|| gps::parse_gps(mnemonic, payload))
2013 .or_else(|| bluetooth::parse_bluetooth(mnemonic, payload))
2014 .or_else(|| sd::parse_sd(mnemonic, payload))
2015 .or_else(|| user::parse_user(mnemonic, payload))
2016 .or_else(|| service::parse_service(mnemonic, payload));
2017
2018 match result {
2019 Some(Ok(response)) => {
2020 tracing::debug!(mnemonic = %mnemonic, "response parsed successfully");
2021 Ok(response)
2022 }
2023 Some(Err(e)) => {
2024 tracing::warn!(mnemonic = %mnemonic, error = %e, "failed to parse response");
2025 Err(e)
2026 }
2027 None => {
2028 tracing::warn!(mnemonic = %mnemonic, "unknown command mnemonic");
2029 Err(ProtocolError::UnknownCommand(mnemonic.to_owned()))
2030 }
2031 }
2032}
2033
2034#[cfg(test)]
2035mod tests {
2036 use super::*;
2037
2038 #[test]
2039 fn parse_error_response() {
2040 let r = parse(b"?").unwrap();
2041 assert!(matches!(r, Response::Error));
2042 }
2043
2044 #[test]
2045 fn parse_unknown_command() {
2046 let r = parse(b"ZZ 123");
2047 assert!(matches!(r, Err(ProtocolError::UnknownCommand(_))));
2048 }
2049
2050 #[test]
2051 fn serialize_returns_cr_terminated() {
2052 let bytes = serialize(&Command::GetRadioId);
2053 assert!(bytes.ends_with(b"\r"));
2054 }
2055
2056 #[test]
2057 fn serialize_get_radio_id() {
2058 let bytes = serialize(&Command::GetRadioId);
2059 assert_eq!(bytes, b"ID\r");
2060 }
2061
2062 #[test]
2063 fn parse_ty_response() {
2064 let r = parse(b"TY K,2").unwrap();
2065 match r {
2066 Response::RadioType { region, variant } => {
2067 assert_eq!(region, "K");
2068 assert_eq!(variant, 2);
2069 }
2070 other => panic!("expected RadioType, got {other:?}"),
2071 }
2072 }
2073
2074 #[test]
2075 fn serialize_get_radio_type() {
2076 let bytes = serialize(&Command::GetRadioType);
2077 assert_eq!(bytes, b"TY\r");
2078 }
2079
2080 #[test]
2081 fn serialize_get_mcp_status() {
2082 let bytes = serialize(&Command::GetMcpStatus);
2083 assert_eq!(bytes, b"0E\r");
2084 }
2085
2086 #[test]
2087 fn serialize_set_dstar_callsign() {
2088 let bytes = serialize(&Command::SetDstarCallsign {
2089 slot: DstarSlot::new(1).unwrap(),
2090 callsign: "KQ4NIT ".to_owned(),
2091 suffix: "D75A".to_owned(),
2092 });
2093 assert_eq!(bytes, b"DC 1,KQ4NIT ,D75A\r");
2094 }
2095
2096 #[test]
2097 fn serialize_set_function_type() {
2098 let bytes = serialize(&Command::SetFunctionType { enabled: true });
2099 assert_eq!(bytes, b"FT 1\r");
2100 }
2101
2102 #[test]
2103 fn serialize_set_smeter() {
2104 let bytes = serialize(&Command::SetSmeter {
2105 band: Band::B,
2106 level: SMeterReading::new(5).unwrap(),
2107 });
2108 assert_eq!(bytes, b"SM 1,5\r");
2109 }
2110
2111 #[test]
2112 fn serialize_set_battery_level() {
2113 let bytes = serialize(&Command::SetBatteryLevel {
2114 display: 0,
2115 level: 3,
2116 });
2117 assert_eq!(bytes, b"BL 0,3\r");
2118 }
2119
2120 #[test]
2121 fn serialize_set_busy() {
2122 let bytes = serialize(&Command::SetBusy {
2123 band: Band::A,
2124 busy: true,
2125 });
2126 assert_eq!(bytes, b"BY 0,1\r");
2127 }
2128
2129 #[test]
2130 fn serialize_set_band_scope() {
2131 let bytes = serialize(&Command::SetBandScope {
2132 band: Band::B,
2133 value: 1,
2134 });
2135 assert_eq!(bytes, b"BS 1,1\r");
2136 }
2137
2138 #[test]
2139 fn serialize_set_firmware_version() {
2140 let bytes = serialize(&Command::SetFirmwareVersion {
2141 version: "1.03".to_owned(),
2142 });
2143 assert_eq!(bytes, b"FV 1.03\r");
2144 }
2145
2146 #[test]
2147 fn serialize_set_radio_id() {
2148 let bytes = serialize(&Command::SetRadioId {
2149 model: "TH-D75".to_owned(),
2150 });
2151 assert_eq!(bytes, b"ID TH-D75\r");
2152 }
2153
2154 #[test]
2155 fn serialize_set_tnc_mode() {
2156 let bytes = serialize(&Command::SetTncMode {
2157 mode: TncMode::Mmdvm,
2158 setting: TncBaud::Bps1200,
2159 });
2160 assert_eq!(bytes, b"TN 3,0\r");
2161 }
2162
2163 #[test]
2164 fn all_mnemonics_recognized() {
2165 // All 55 standard mnemonics + 15 service mode mnemonics.
2166 // SR is write-only but its echo is still recognized by the parser.
2167 let mnemonics = [
2168 "AI", "AG", "BC", "BY", "DL", "DW", "ME", "MR", "PC", "RX", "SQ", "SR", "SH", "TX",
2169 "UP", "VM", "FQ", "FO", "PS", "FV", "BE", "ID", "CS", "TN", "BL", "GP", "GM", "SM",
2170 "RA", "BT", "FS", "FT", "MD", "SF", "VD", "VG", "VX", "IO", "BS", "LC", "GS", "MS",
2171 "PT", "AS", "DC", "DS", "RT", "FR", "US", "GW", "SD", "0M", "AE",
2172 // Extra mnemonics not in main dispatch table
2173 "TY", "0E",
2174 // Service mode mnemonics (20 commands, 15 unique mnemonics after 0E overlap)
2175 "0G", "0S", "0R", "0W", "0Y", "1A", "1D", "1E", "1I", "1N", "1U", "1V", "1W", "1F",
2176 "9E", "9R", "2V", "1G", "1C",
2177 ];
2178 assert_eq!(mnemonics.len(), 74);
2179 for mnemonic in &mnemonics {
2180 let input = format!("{mnemonic} 0");
2181 let result = parse(input.as_bytes());
2182 if let Err(ProtocolError::UnknownCommand(_)) = result {
2183 panic!("Mnemonic '{mnemonic}' not recognized by parser");
2184 }
2185 // Other errors (FieldParse, etc.) are OK — the test only checks recognition
2186 }
2187 }
2188
2189 // === Service mode serialization tests ===
2190
2191 #[test]
2192 fn serialize_enter_service_mode() {
2193 let bytes = serialize(&Command::EnterServiceMode);
2194 assert_eq!(bytes, b"0G KENWOOD\r");
2195 }
2196
2197 #[test]
2198 fn serialize_exit_service_mode() {
2199 let bytes = serialize(&Command::ExitServiceMode);
2200 assert_eq!(bytes, b"0G\r");
2201 }
2202
2203 #[test]
2204 fn serialize_read_calibration_data() {
2205 let bytes = serialize(&Command::ReadCalibrationData);
2206 assert_eq!(bytes, b"0S\r");
2207 }
2208
2209 #[test]
2210 fn serialize_write_calibration_data() {
2211 let bytes = serialize(&Command::WriteCalibrationData {
2212 data: "AABBCCDD".to_owned(),
2213 });
2214 assert_eq!(bytes, b"0R AABBCCDD\r");
2215 }
2216
2217 #[test]
2218 fn serialize_get_service_status() {
2219 let bytes = serialize(&Command::GetServiceStatus);
2220 assert_eq!(bytes, b"0E\r");
2221 }
2222
2223 #[test]
2224 fn serialize_service_calibrate_1a() {
2225 let bytes = serialize(&Command::ServiceCalibrate1A);
2226 assert_eq!(bytes, b"1A\r");
2227 }
2228
2229 #[test]
2230 fn serialize_service_calibrate_1d() {
2231 let bytes = serialize(&Command::ServiceCalibrate1D);
2232 assert_eq!(bytes, b"1D\r");
2233 }
2234
2235 #[test]
2236 fn serialize_service_calibrate_1e_read() {
2237 let bytes = serialize(&Command::ServiceCalibrate1E { value: None });
2238 assert_eq!(bytes, b"1E\r");
2239 }
2240
2241 #[test]
2242 fn serialize_service_calibrate_1e_write() {
2243 let bytes = serialize(&Command::ServiceCalibrate1E {
2244 value: Some("0FF".to_owned()),
2245 });
2246 assert_eq!(bytes, b"1E 0FF\r");
2247 }
2248
2249 #[test]
2250 fn serialize_service_calibrate_1n() {
2251 let bytes = serialize(&Command::ServiceCalibrate1N);
2252 assert_eq!(bytes, b"1N\r");
2253 }
2254
2255 #[test]
2256 fn serialize_service_calibrate_1v_read() {
2257 let bytes = serialize(&Command::ServiceCalibrate1V { value: None });
2258 assert_eq!(bytes, b"1V\r");
2259 }
2260
2261 #[test]
2262 fn serialize_service_calibrate_1v_write() {
2263 let bytes = serialize(&Command::ServiceCalibrate1V {
2264 value: Some("ABC".to_owned()),
2265 });
2266 assert_eq!(bytes, b"1V ABC\r");
2267 }
2268
2269 #[test]
2270 fn serialize_service_calibrate_1w() {
2271 let bytes = serialize(&Command::ServiceCalibrate1W {
2272 value: "5".to_owned(),
2273 });
2274 assert_eq!(bytes, b"1W 5\r");
2275 }
2276
2277 #[test]
2278 fn serialize_service_write_id() {
2279 let bytes = serialize(&Command::ServiceWriteId {
2280 id: "C3C10368".to_owned(),
2281 code: "K01".to_owned(),
2282 });
2283 assert_eq!(bytes, b"1I C3C10368,K01\r");
2284 }
2285
2286 #[test]
2287 fn serialize_service_flash_read() {
2288 let bytes = serialize(&Command::ServiceFlashRead);
2289 assert_eq!(bytes, b"1F\r");
2290 }
2291
2292 #[test]
2293 fn serialize_service_flash_write() {
2294 let bytes = serialize(&Command::ServiceFlashWrite {
2295 address: "04E000".to_owned(),
2296 data: "AABB".to_owned(),
2297 });
2298 assert_eq!(bytes, b"1F 04E000,AABB\r");
2299 }
2300
2301 #[test]
2302 fn serialize_service_write_config() {
2303 let bytes = serialize(&Command::ServiceWriteConfig);
2304 assert_eq!(bytes, b"0W\r");
2305 }
2306
2307 #[test]
2308 fn serialize_service_band_select() {
2309 let bytes = serialize(&Command::ServiceBandSelect { band: 0 });
2310 assert_eq!(bytes, b"0Y 0\r");
2311 }
2312
2313 #[test]
2314 fn serialize_service_read_eeprom() {
2315 let bytes = serialize(&Command::ServiceReadEeprom {
2316 address: "04E000".to_owned(),
2317 length: "80".to_owned(),
2318 });
2319 assert_eq!(bytes, b"9E 04E000,80\r");
2320 }
2321
2322 #[test]
2323 fn serialize_service_read_eeprom_addr() {
2324 let bytes = serialize(&Command::ServiceReadEepromAddr);
2325 assert_eq!(bytes, b"9R\r");
2326 }
2327
2328 #[test]
2329 fn serialize_service_get_version() {
2330 let bytes = serialize(&Command::ServiceGetVersion {
2331 param1: "00".to_owned(),
2332 param2: "000".to_owned(),
2333 });
2334 assert_eq!(bytes, b"2V 00,000\r");
2335 }
2336
2337 #[test]
2338 fn serialize_service_get_hardware() {
2339 let bytes = serialize(&Command::ServiceGetHardware);
2340 assert_eq!(bytes, b"1G\r");
2341 }
2342
2343 #[test]
2344 fn serialize_service_calibrate_new() {
2345 let bytes = serialize(&Command::ServiceCalibrateNew {
2346 value: "0A5".to_owned(),
2347 });
2348 assert_eq!(bytes, b"1C 0A5\r");
2349 }
2350
2351 #[test]
2352 fn serialize_service_dynamic_param_read() {
2353 let bytes = serialize(&Command::ServiceDynamicParam { data: None });
2354 assert_eq!(bytes, b"1U\r");
2355 }
2356
2357 #[test]
2358 fn serialize_service_dynamic_param_write() {
2359 let bytes = serialize(&Command::ServiceDynamicParam {
2360 data: Some("AABB".to_owned()),
2361 });
2362 assert_eq!(bytes, b"1U AABB\r");
2363 }
2364
2365 // === Service mode parse tests ===
2366
2367 #[test]
2368 fn parse_service_mode_response() {
2369 let r = parse(b"0G").unwrap();
2370 assert!(matches!(r, Response::ServiceMode { .. }));
2371 }
2372
2373 #[test]
2374 fn parse_service_calibration_data() {
2375 let r = parse(b"0S AABBCCDD").unwrap();
2376 match r {
2377 Response::ServiceCalibrationData { data } => assert_eq!(data, "AABBCCDD"),
2378 other => panic!("expected ServiceCalibrationData, got {other:?}"),
2379 }
2380 }
2381
2382 #[test]
2383 fn parse_service_calibration_param_1a() {
2384 let r = parse(b"1A 123").unwrap();
2385 match r {
2386 Response::ServiceCalibrationParam { mnemonic, data } => {
2387 assert_eq!(mnemonic, "1A");
2388 assert_eq!(data, "123");
2389 }
2390 other => panic!("expected ServiceCalibrationParam, got {other:?}"),
2391 }
2392 }
2393
2394 #[test]
2395 fn parse_service_version() {
2396 let r = parse(b"2V EX-5210").unwrap();
2397 match r {
2398 Response::ServiceVersion { data } => assert_eq!(data, "EX-5210"),
2399 other => panic!("expected ServiceVersion, got {other:?}"),
2400 }
2401 }
2402
2403 #[test]
2404 fn parse_service_hardware() {
2405 let r = parse(b"1G AA,BB").unwrap();
2406 match r {
2407 Response::ServiceHardware { data } => assert_eq!(data, "AA,BB"),
2408 other => panic!("expected ServiceHardware, got {other:?}"),
2409 }
2410 }
2411
2412 #[test]
2413 fn parse_service_eeprom_data() {
2414 let r = parse(b"9E AABBCCDD").unwrap();
2415 match r {
2416 Response::ServiceEepromData { data } => assert_eq!(data, "AABBCCDD"),
2417 other => panic!("expected ServiceEepromData, got {other:?}"),
2418 }
2419 }
2420
2421 #[test]
2422 fn parse_service_eeprom_addr() {
2423 let r = parse(b"9R 01020304").unwrap();
2424 match r {
2425 Response::ServiceEepromAddr { data } => assert_eq!(data, "01020304"),
2426 other => panic!("expected ServiceEepromAddr, got {other:?}"),
2427 }
2428 }
2429}