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}