kenwood_thd75/types/
channel.rs

1//! Channel URCALL and memory types for the TH-D75 transceiver.
2//!
3//! This module contains two channel representations:
4//!
5//! - [`ChannelMemory`]: the CAT wire format (FO/ME commands), used by the
6//!   protocol layer for over-the-air command encoding/decoding.
7//! - [`FlashChannel`]: the 40-byte flash memory format (MCP/SD card), used
8//!   by the memory and SD card modules for binary image parsing.
9//!
10//! The two formats differ in field layout at bytes 0x09, 0x0A, 0x0E, and
11//! 0x0F-0x27. The flash format includes `MemoryMode` (8 modes including
12//! LSB/USB/CW/DR), separated D-STAR callsigns, and structured tone/duplex
13//! bit fields.
14//!
15//! # Memory architecture (per User Manual Chapter 8)
16//!
17//! A total of 1101 memory channels are available:
18//!
19//! - **0-999**: standard memory channels (simplex, repeater, or odd-split)
20//! - **L0/U0 through L49/U49**: 100 program scan edge memories (50 pairs)
21//! - **Pri**: 1 priority scan memory channel
22//! - **A1-A10**: weather channels (TH-D75A only)
23//! - **C**: call channels (one per band/mode combination)
24//!
25//! Each channel can store: RX frequency, TX frequency (odd-split),
26//! step size, offset direction, tone/CTCSS/DCS/cross-tone settings,
27//! shift, reverse, lockout, demodulation mode, fine mode, memory name
28//! (up to 16 characters), and D-STAR digital squelch/callsign data.
29//!
30//! Memory channels can be used as simplex/repeater (one frequency +
31//! optional offset) or odd-split (separate TX/RX frequencies for
32//! non-standard repeater offsets). Odd-split channels show "+-" on
33//! the display.
34//!
35//! # Memory groups (per Operating Tips §5.11, User Manual Chapter 8)
36//!
37//! The TH-D75 supports 30 memory groups (GRP-0 through GRP-29). By
38//! default, channels 0-99 belong to GRP-0, 100-199 to GRP-1, and so on
39//! up to 900-999 in GRP-9. Groups 10-29 are empty by default. Each group
40//! can be given a name of up to 16 characters via Menu No. 201.
41//! Groups without any registered channels are skipped during group
42//! switching.
43//!
44//! # Memory recall (per User Manual Chapter 8)
45//!
46//! Menu No. 202 controls the recall method:
47//! - `All Bands`: recall all programmed memory channels.
48//! - `Current Band`: recall only channels with frequencies in the
49//!   current frequency band. This also affects memory scan and group
50//!   link scan.
51//!
52//! # Memory shift (per User Manual Chapter 8)
53//!
54//! Memory channel or call channel contents can be copied to VFO via
55//! `[F]`, `[VFO]`. The entire contents (frequency, mode, tone, etc.)
56//! are transferred. To copy the TX frequency from an odd-split channel,
57//! turn on Reverse first.
58
59use std::fmt;
60
61use crate::error::{ProtocolError, ValidationError};
62use crate::types::dstar::DstarCallsign;
63use crate::types::frequency::Frequency;
64use crate::types::mode::{MemoryMode, ShiftDirection, StepSize};
65use crate::types::tone::{CtcssMode, DcsCode, ToneCode};
66
67/// D-STAR URCALL callsign (up to 8 characters, stored in 24 bytes).
68///
69/// The TH-D75 stores this field in a 24-byte region to accommodate
70/// multi-byte character encodings such as Shift-JIS (Japanese Industrial
71/// Standards) on Japanese-market models. This type validates ASCII-only
72/// content with a maximum of 8 characters.
73///
74/// Despite being labeled "Channel Name" in some documentation, this field
75/// stores the D-STAR "UR" (your) callsign, defaulting to "CQCQCQ" for
76/// general CQ calls. User-assigned channel display names are stored
77/// separately in flash and are only accessible via the MCP programming
78/// interface.
79#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
80pub struct ChannelName(String);
81
82impl ChannelName {
83    /// Creates a new `ChannelName` from a string slice.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`ValidationError::ChannelNameTooLong`] if the callsign
88    /// exceeds 8 characters.
89    pub fn new(name: &str) -> Result<Self, ValidationError> {
90        let len = name.len();
91        if len > 8 {
92            return Err(ValidationError::ChannelNameTooLong { len });
93        }
94        Ok(Self(name.to_owned()))
95    }
96
97    /// Returns the URCALL callsign as a string slice.
98    #[must_use]
99    pub fn as_str(&self) -> &str {
100        &self.0
101    }
102
103    /// Encodes the URCALL callsign as a 24-byte null-padded ASCII array.
104    #[must_use]
105    pub fn to_bytes(&self) -> [u8; 24] {
106        let mut buf = [0u8; 24];
107        let src = self.0.as_bytes();
108        buf[..src.len()].copy_from_slice(src);
109        buf
110    }
111
112    /// Decodes a URCALL callsign from a 24-byte null-padded array.
113    ///
114    /// Scans for the first null byte and takes ASCII characters up to
115    /// that point. If no null byte is found, takes up to 8 characters.
116    #[must_use]
117    pub fn from_bytes(bytes: &[u8; 24]) -> Self {
118        let end = bytes.iter().position(|&b| b == 0).unwrap_or(8).min(8);
119        let s = String::from_utf8_lossy(&bytes[..end]);
120        Self(s.into_owned())
121    }
122}
123
124/// 40-byte internal channel memory structure.
125///
126/// Maps byte-for-byte to the firmware's internal representation at `DAT_c0012634`.
127/// See `thd75/re_output/channel_memory_structure.txt`.
128///
129/// # Channel display names
130///
131/// Channel display names are NOT accessible via the CAT
132/// protocol. The `urcall` field stores the D-STAR URCALL destination callsign
133/// from the ME/FO wire format (typically "CQCQCQ" for D-STAR). Display names
134/// are only accessible via the MCP programming protocol on USB interface 2.
135#[allow(clippy::struct_excessive_bools)]
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct ChannelMemory {
138    /// RX frequency in Hz (byte 0x00, 4 bytes, little-endian).
139    pub rx_frequency: Frequency,
140    /// TX offset or split TX frequency in Hz (byte 0x04, 4 bytes, little-endian).
141    pub tx_offset: Frequency,
142    /// Frequency step size (byte 0x08 high nibble).
143    pub step_size: StepSize,
144    /// Raw byte 0x09 — mode and fine tuning configuration.
145    ///
146    /// Bit layout (from `FlashChannel` RE):
147    /// - bit 7: reserved
148    /// - bits 6:4: operating mode (0=FM, 1=DV, 2=AM, 3=LSB, 4=USB, 5=CW, 6=NFM)
149    /// - bit 3: narrow FM flag
150    /// - bit 2: fine tuning enable
151    /// - bits 1:0: fine step size
152    ///
153    /// In the CAT wire format (FO/ME), byte\[9\] is unpacked into fields \[3\]-\[6\]:
154    /// `tx_step`, `mode`, `fine_tuning`, `fine_step`. The binary packer preserves
155    /// this byte directly for round-trip fidelity.
156    pub mode_flags_raw: u8,
157    /// Shift direction (byte 0x08 low nibble in binary format).
158    ///
159    /// In the binary packer, this field is written to byte 0x08 low nibble.
160    /// In the CAT wire format, shift is at field\[12\] and also encoded in
161    /// `flags_0a_raw` bits 2:0. Both should carry the same value.
162    pub shift: ShiftDirection,
163    /// Reverse mode (derived from `flags_0a_raw` bit 3).
164    pub reverse: bool,
165    /// Tone encode enable (derived from `flags_0a_raw` bit 7).
166    pub tone_enable: bool,
167    /// CTCSS mode (derived from `flags_0a_raw` bit 6).
168    pub ctcss_mode: CtcssMode,
169    /// DCS enable (derived from `flags_0a_raw` bit 5).
170    pub dcs_enable: bool,
171    /// Cross-tone enable (derived from `flags_0a_raw` bit 4).
172    pub cross_tone_reverse: bool,
173    /// Raw byte 0x0A — source of truth for tone/shift configuration.
174    ///
175    /// Hardware-verified bit layout (20 channels, 0 exceptions):
176    /// - bit 7: tone encode enable
177    /// - bit 6: CTCSS enable
178    /// - bit 5: DCS enable
179    /// - bit 4: cross-tone enable
180    /// - bit 3: reverse
181    /// - bits 2:0: shift direction (0=simplex, 1=+, 2=-, 4=split)
182    ///
183    /// The individual bool fields above are convenience accessors that
184    /// MUST be consistent with this byte. The binary packer (`to_bytes`)
185    /// writes this byte directly; `from_bytes` derives the bools from it.
186    pub flags_0a_raw: u8,
187    /// Tone encoder frequency code index (byte 0x0B).
188    pub tone_code: ToneCode,
189    /// CTCSS decoder frequency code index (byte 0x0C).
190    pub ctcss_code: ToneCode,
191    /// DCS code index (byte 0x0D).
192    pub dcs_code: DcsCode,
193    /// Cross-tone combination type (byte 0x0E bits 5:4, range 0-3).
194    ///
195    /// CAT wire field 16. Controls how TX and RX tone types are combined
196    /// when cross-tone mode is enabled (`cross_tone_reverse` / byte 0x0A bit 4).
197    pub cross_tone_combo: CrossToneType,
198    /// Digital squelch mode (byte 0x0E bits 1:0, range 0-2).
199    ///
200    /// CAT wire field 18: 0=Off, 1=Code Squelch, 2=Callsign Squelch.
201    /// Note: channel lockout (ME field 22) is stored separately in MCP
202    /// flags region at offset 0x2000, not here.
203    pub digital_squelch: FlashDigitalSquelch,
204    /// D-STAR URCALL destination callsign (byte 0x0F, 24 bytes).
205    ///
206    /// Stores the D-STAR "UR" (your) callsign, defaulting to "CQCQCQ"
207    /// for general CQ calls. Display names are stored separately in MCP
208    /// at offset 0x10000.
209    pub urcall: ChannelName,
210    /// Digital code (CAT wire field 19, 2 digits).
211    pub data_mode: u8,
212}
213
214impl ChannelMemory {
215    /// Size of the packed binary representation in bytes.
216    pub const BYTE_SIZE: usize = 40;
217
218    /// Serializes the channel memory to a 40-byte packed binary array.
219    #[must_use]
220    pub fn to_bytes(&self) -> [u8; 40] {
221        let mut buf = [0u8; 40];
222
223        // bytes[0..4]: RX frequency, little-endian
224        buf[0..4].copy_from_slice(&self.rx_frequency.to_le_bytes());
225
226        // bytes[4..8]: TX offset, little-endian
227        buf[4..8].copy_from_slice(&self.tx_offset.to_le_bytes());
228
229        // byte 0x08: step_size (high nibble) | shift (low nibble)
230        buf[0x08] = (u8::from(self.step_size) << 4) | u8::from(self.shift);
231
232        // byte 0x09: mode + fine tuning flags (preserved as raw byte)
233        buf[0x09] = self.mode_flags_raw;
234
235        // byte 0x0A: flags_0a_raw (all 8 bits — hardware-verified mapping):
236        //   bit 7 = tone encode, bit 6 = CTCSS, bit 5 = DCS, bit 4 = cross-tone,
237        //   bit 3 = reverse, bits 2:0 = shift direction
238        buf[0x0A] = self.flags_0a_raw;
239
240        // byte 0x0B: tone code index
241        buf[0x0B] = self.tone_code.index();
242
243        // byte 0x0C: CTCSS code index
244        buf[0x0C] = self.ctcss_code.index();
245
246        // byte 0x0D: DCS code index
247        buf[0x0D] = self.dcs_code.index();
248
249        // byte 0x0E: cross_tone_combo (bits 5:4) | data_speed=3 (bits 3:2) | digital_squelch (bits 1:0)
250        buf[0x0E] = ((u8::from(self.cross_tone_combo) & 0x03) << 4)
251            | 0x0C
252            | (u8::from(self.digital_squelch) & 0x03);
253
254        // bytes[0x0F..0x27]: URCALL callsign (24 bytes)
255        buf[0x0F..0x27].copy_from_slice(&self.urcall.to_bytes());
256
257        // byte 0x27: data mode
258        buf[0x27] = self.data_mode;
259
260        buf
261    }
262
263    /// Parses a channel memory from a byte slice (must be >= 40 bytes).
264    ///
265    /// # Errors
266    ///
267    /// Returns [`ProtocolError::FieldParse`] if any field contains an
268    /// invalid value, or if the slice is too short.
269    #[allow(clippy::similar_names, clippy::too_many_lines)]
270    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProtocolError> {
271        if bytes.len() < Self::BYTE_SIZE {
272            return Err(ProtocolError::FieldParse {
273                command: "channel".into(),
274                field: "length".into(),
275                detail: format!(
276                    "expected at least {} bytes, got {}",
277                    Self::BYTE_SIZE,
278                    bytes.len()
279                ),
280            });
281        }
282
283        let rx_frequency = Frequency::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
284        let tx_offset = Frequency::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
285
286        let step_size =
287            StepSize::try_from(bytes[0x08] >> 4).map_err(|e| ProtocolError::FieldParse {
288                command: "channel".into(),
289                field: "step_size".into(),
290                detail: e.to_string(),
291            })?;
292
293        let shift = ShiftDirection::try_from(bytes[0x08] & 0x0F).map_err(|e| {
294            ProtocolError::FieldParse {
295                command: "channel".into(),
296                field: "shift".into(),
297                detail: e.to_string(),
298            }
299        })?;
300
301        // byte 0x09: mode + fine tuning flags (preserved as raw byte)
302        let mode_flags_raw = bytes[0x09];
303
304        // byte 0x0A: all 8 bits (hardware-verified mapping)
305        let flags_0a_raw = bytes[0x0A];
306        let tone_enable = (flags_0a_raw >> 7) & 1 != 0;
307        let ctcss_enable = (flags_0a_raw >> 6) & 1 != 0;
308        let dcs_enable = (flags_0a_raw >> 5) & 1 != 0;
309        let cross_tone_reverse = (flags_0a_raw >> 4) & 1 != 0;
310        let reverse = (flags_0a_raw >> 3) & 1 != 0;
311
312        let ctcss_mode = if ctcss_enable {
313            CtcssMode::try_from(1u8).map_err(|e| ProtocolError::FieldParse {
314                command: "channel".into(),
315                field: "ctcss_mode".into(),
316                detail: e.to_string(),
317            })?
318        } else {
319            CtcssMode::try_from(0u8).map_err(|e| ProtocolError::FieldParse {
320                command: "channel".into(),
321                field: "ctcss_mode".into(),
322                detail: e.to_string(),
323            })?
324        };
325
326        let tone_code = ToneCode::new(bytes[0x0B]).map_err(|e| ProtocolError::FieldParse {
327            command: "channel".into(),
328            field: "tone_code".into(),
329            detail: e.to_string(),
330        })?;
331
332        let ctcss_code = ToneCode::new(bytes[0x0C]).map_err(|e| ProtocolError::FieldParse {
333            command: "channel".into(),
334            field: "ctcss_code".into(),
335            detail: e.to_string(),
336        })?;
337
338        let dcs_code = DcsCode::new(bytes[0x0D]).map_err(|e| ProtocolError::FieldParse {
339            command: "channel".into(),
340            field: "dcs_code".into(),
341            detail: e.to_string(),
342        })?;
343
344        // byte 0x0E: cross_tone_combo (bits 5:4) | digital_squelch (bits 1:0)
345        let cross_tone_combo = CrossToneType::try_from((bytes[0x0E] >> 4) & 0x03).map_err(|e| {
346            ProtocolError::FieldParse {
347                command: "channel".into(),
348                field: "cross_tone_combo".into(),
349                detail: e.to_string(),
350            }
351        })?;
352        let digital_squelch = FlashDigitalSquelch::try_from(bytes[0x0E] & 0x03).map_err(|e| {
353            ProtocolError::FieldParse {
354                command: "channel".into(),
355                field: "digital_squelch".into(),
356                detail: e.to_string(),
357            }
358        })?;
359
360        let mut urcall_arr = [0u8; 24];
361        urcall_arr.copy_from_slice(&bytes[0x0F..0x27]);
362        let urcall = ChannelName::from_bytes(&urcall_arr);
363
364        let data_mode = bytes[0x27];
365
366        Ok(Self {
367            rx_frequency,
368            tx_offset,
369            step_size,
370            mode_flags_raw,
371            shift,
372            reverse,
373            tone_enable,
374            ctcss_mode,
375            dcs_enable,
376            cross_tone_reverse,
377            flags_0a_raw,
378            tone_code,
379            ctcss_code,
380            dcs_code,
381            cross_tone_combo,
382            digital_squelch,
383            urcall,
384            data_mode,
385        })
386    }
387}
388
389// ===========================================================================
390// Flash channel types (MCP / SD card binary format)
391// ===========================================================================
392
393/// Duplex mode as stored in flash memory byte 0x0A bits \[1:0\].
394///
395/// Combined with the split flag (bit 2) to determine the full duplex
396/// configuration.
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
398pub enum FlashDuplex {
399    /// Simplex -- TX on same frequency as RX (value 0).
400    Simplex = 0,
401    /// Plus -- TX frequency = RX + offset (value 1).
402    Plus = 1,
403    /// Minus -- TX frequency = RX - offset (value 2).
404    Minus = 2,
405}
406
407impl FlashDuplex {
408    /// Number of valid flash duplex values (0-2).
409    pub const COUNT: u8 = 3;
410}
411
412impl TryFrom<u8> for FlashDuplex {
413    type Error = ValidationError;
414
415    fn try_from(value: u8) -> Result<Self, Self::Error> {
416        match value {
417            0 => Ok(Self::Simplex),
418            1 => Ok(Self::Plus),
419            2 => Ok(Self::Minus),
420            _ => Err(ValidationError::ShiftOutOfRange(value)),
421        }
422    }
423}
424
425impl From<FlashDuplex> for u8 {
426    fn from(d: FlashDuplex) -> Self {
427        d as Self
428    }
429}
430
431/// Cross-tone type as stored in flash memory byte 0x0E bits \[5:4\].
432///
433/// Determines how different tone/DCS codes are applied to TX vs RX
434/// when cross-tone mode is active.
435///
436/// Per User Manual Chapter 10: cross tone allows separate signaling
437/// types for TX and RX when accessing a repeater that uses different
438/// encode/decode signaling. Activated by pressing `[TONE]` 4 times.
439///
440/// | Value | Encode (TX) | Decode (RX) | Display icon |
441/// |-------|-------------|-------------|--------------|
442/// | 0 | DCS | Off | D/O |
443/// | 1 | Tone | DCS | T/D |
444/// | 2 | DCS | CTCSS | D/C |
445/// | 3 | Tone | CTCSS | T/C |
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
447pub enum CrossToneType {
448    /// DCS encode (TX), Off decode (RX). Display: D/O (value 0).
449    DcsOff = 0,
450    /// Tone encode (TX), DCS decode (RX). Display: T/D (value 1).
451    ToneDcs = 1,
452    /// DCS encode (TX), CTCSS decode (RX). Display: D/C (value 2).
453    DcsCtcss = 2,
454    /// Tone encode (TX), CTCSS decode (RX). Display: T/C (value 3).
455    ToneCtcss = 3,
456}
457
458impl CrossToneType {
459    /// Number of valid cross-tone type values (0-3).
460    pub const COUNT: u8 = 4;
461}
462
463impl TryFrom<u8> for CrossToneType {
464    type Error = ValidationError;
465
466    fn try_from(value: u8) -> Result<Self, Self::Error> {
467        match value {
468            0 => Ok(Self::DcsOff),
469            1 => Ok(Self::ToneDcs),
470            2 => Ok(Self::DcsCtcss),
471            3 => Ok(Self::ToneCtcss),
472            _ => Err(ValidationError::CrossToneTypeOutOfRange(value)),
473        }
474    }
475}
476
477impl From<CrossToneType> for u8 {
478    fn from(ct: CrossToneType) -> Self {
479        ct as Self
480    }
481}
482
483/// Flash digital squelch mode at byte 0x0E bits \[1:0\].
484///
485/// Controls whether D-STAR digital squelch is active per-channel.
486/// This is the per-channel flash encoding; the system-level
487/// [`DigitalSquelch`](crate::types::dstar::DigitalSquelch) config is separate.
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
489pub enum FlashDigitalSquelch {
490    /// Digital squelch off (value 0).
491    Off = 0,
492    /// Code squelch -- match digital code (value 1).
493    Code = 1,
494    /// Callsign squelch -- match source callsign (value 2).
495    Callsign = 2,
496}
497
498impl FlashDigitalSquelch {
499    /// Number of valid flash digital squelch values (0-2).
500    pub const COUNT: u8 = 3;
501}
502
503impl TryFrom<u8> for FlashDigitalSquelch {
504    type Error = ValidationError;
505
506    fn try_from(value: u8) -> Result<Self, Self::Error> {
507        match value {
508            0 => Ok(Self::Off),
509            1 => Ok(Self::Code),
510            2 => Ok(Self::Callsign),
511            _ => Err(ValidationError::FlashDigitalSquelchOutOfRange(value)),
512        }
513    }
514}
515
516impl From<FlashDigitalSquelch> for u8 {
517    fn from(ds: FlashDigitalSquelch) -> Self {
518        ds as Self
519    }
520}
521
522/// Fine tuning step size stored at byte 0x09 bits \[1:0\].
523///
524/// Used in conjunction with the fine-mode flag (byte 0x09 bit 2) for
525/// sub-kHz frequency adjustment.
526///
527/// Per User Manual Chapter 12: fine tuning is available only on Band B
528/// in LSB, USB, CW, or AM modes. It does not work on Band A, or in
529/// FM/DV modes. Activated via `[F]`, `[MHz]` -> On. While fine tuning
530/// is active, the 100 Hz digit appears on the display, and step size,
531/// MHz mode, and MHz scan are disabled. Turning fine tuning off does
532/// not change the current frequency, but the next frequency change
533/// uses the normal step size. The fine step can be set independently
534/// per frequency band.
535#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
536pub enum FineStep {
537    /// 20 Hz fine step (value 0).
538    Hz20 = 0,
539    /// 100 Hz fine step (value 1).
540    Hz100 = 1,
541    /// 500 Hz fine step (value 2).
542    Hz500 = 2,
543    /// 1000 Hz fine step (value 3).
544    Hz1000 = 3,
545}
546
547impl FineStep {
548    /// Number of valid fine step values (0-3).
549    pub const COUNT: u8 = 4;
550}
551
552impl TryFrom<u8> for FineStep {
553    type Error = ValidationError;
554
555    fn try_from(value: u8) -> Result<Self, Self::Error> {
556        match value {
557            0 => Ok(Self::Hz20),
558            1 => Ok(Self::Hz100),
559            2 => Ok(Self::Hz500),
560            3 => Ok(Self::Hz1000),
561            _ => Err(ValidationError::FineStepOutOfRange(value)),
562        }
563    }
564}
565
566impl From<FineStep> for u8 {
567    fn from(fs: FineStep) -> Self {
568        fs as Self
569    }
570}
571
572impl fmt::Display for FineStep {
573    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574        match self {
575            Self::Hz20 => f.write_str("20 Hz"),
576            Self::Hz100 => f.write_str("100 Hz"),
577            Self::Hz500 => f.write_str("500 Hz"),
578            Self::Hz1000 => f.write_str("1000 Hz"),
579        }
580    }
581}
582
583/// 40-byte flash memory channel structure.
584///
585/// Maps byte-for-byte to the MCP programming memory and `.d75` file format.
586/// Field layout derived from firmware analysis.
587///
588/// This struct represents the **flash encoding**, which differs from the
589/// CAT wire format ([`ChannelMemory`]) in several ways:
590///
591/// - **Mode** (byte 0x09 bits \[6:4\]): 8 modes including HF (LSB/USB/CW)
592///   and DR, vs 4 modes in CAT.
593/// - **Tone/duplex** (byte 0x0A): structured bit fields for tone, CTCSS,
594///   DTCS, cross-tone, split, and duplex direction.
595/// - **D-STAR callsigns** (bytes 0x0F-0x26): three separate 8-byte fields
596///   (UR, RPT1, RPT2) instead of one 24-byte blob.
597/// - **Cross-tone / digital squelch** (byte 0x0E): cross-tone type and
598///   per-channel digital squelch mode.
599///
600/// The per-field offsets documented on each struct member below are the
601/// complete byte map, correlated against MCP memory dumps from hardware.
602#[allow(clippy::struct_excessive_bools)]
603#[derive(Debug, Clone, PartialEq, Eq)]
604pub struct FlashChannel {
605    /// RX frequency in Hz (offset 0x00, 4 bytes, little-endian).
606    pub rx_frequency: Frequency,
607    /// TX offset or split TX frequency in Hz (offset 0x04, 4 bytes, little-endian).
608    pub tx_offset: Frequency,
609    /// Frequency step size (offset 0x08 bits \[7:4\]).
610    pub step_size: StepSize,
611    /// Raw low nibble of byte 0x08 (split tune step / unknown bit 0).
612    pub byte08_low: u8,
613    /// Operating mode (offset 0x09 bits \[6:4\]).
614    pub mode: MemoryMode,
615    /// Narrow FM flag (offset 0x09 bit 3).
616    pub narrow: bool,
617    /// Fine tuning mode enabled (offset 0x09 bit 2).
618    pub fine_mode: bool,
619    /// Fine tuning step size (offset 0x09 bits \[1:0\]).
620    pub fine_step: FineStep,
621    /// Raw bit 7 of byte 0x09 (unknown / reserved).
622    pub byte09_bit7: bool,
623    /// Tone encode enable (offset 0x0A bit 7).
624    pub tone_enabled: bool,
625    /// CTCSS encode+decode enable (offset 0x0A bit 6).
626    pub ctcss_enabled: bool,
627    /// DTCS (DCS) enable (offset 0x0A bit 5).
628    pub dtcs_enabled: bool,
629    /// Cross-tone mode enable (offset 0x0A bit 4).
630    pub cross_tone: bool,
631    /// Raw bit 3 of byte 0x0A (unknown / reserved).
632    pub byte0a_bit3: bool,
633    /// Odd split flag (offset 0x0A bit 2). When set, `tx_offset` is an
634    /// absolute TX frequency rather than a repeater offset.
635    pub split: bool,
636    /// Duplex direction (offset 0x0A bits \[1:0\]).
637    pub duplex: FlashDuplex,
638    /// CTCSS TX tone index (offset 0x0B, 0-49).
639    pub tone_code: ToneCode,
640    /// CTCSS RX tone index (offset 0x0C bits \[5:0\]).
641    pub ctcss_code: ToneCode,
642    /// Raw high bits of byte 0x0C (bits \[7:6\], unknown).
643    pub byte0c_high: u8,
644    /// DCS code index (offset 0x0D bits \[6:0\]).
645    pub dcs_code: DcsCode,
646    /// Raw bit 7 of byte 0x0D (unknown / reserved).
647    pub byte0d_bit7: bool,
648    /// Cross-tone type (offset 0x0E bits \[5:4\]).
649    pub cross_tone_type: CrossToneType,
650    /// Digital squelch mode (offset 0x0E bits \[1:0\]).
651    pub digital_squelch: FlashDigitalSquelch,
652    /// Raw bits of byte 0x0E that are not cross-tone or digital squelch
653    /// (bits \[7:6\] and \[3:2\]).
654    pub byte0e_reserved: u8,
655    /// D-STAR UR callsign (offset 0x0F, 8 bytes, space-padded).
656    pub ur_call: DstarCallsign,
657    /// D-STAR RPT1 callsign (offset 0x17, 8 bytes, space-padded).
658    pub rpt1: DstarCallsign,
659    /// D-STAR RPT2 callsign (offset 0x1F, 8 bytes, space-padded).
660    pub rpt2: DstarCallsign,
661    /// D-STAR DV code (offset 0x27 bits \[6:0\], 0-127).
662    pub dv_code: u8,
663    /// Raw bit 7 of byte 0x27 (unknown / reserved).
664    pub byte27_bit7: bool,
665}
666
667impl FlashChannel {
668    /// Size of the packed binary representation in bytes.
669    pub const BYTE_SIZE: usize = 40;
670
671    /// Parses a flash channel from a byte slice (must be >= 40 bytes).
672    ///
673    /// # Errors
674    ///
675    /// Returns [`ProtocolError::FieldParse`] if any field contains an
676    /// invalid value, or if the slice is too short.
677    #[allow(clippy::similar_names, clippy::too_many_lines)]
678    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProtocolError> {
679        if bytes.len() < Self::BYTE_SIZE {
680            return Err(ProtocolError::FieldParse {
681                command: "flash_channel".into(),
682                field: "length".into(),
683                detail: format!(
684                    "expected at least {} bytes, got {}",
685                    Self::BYTE_SIZE,
686                    bytes.len()
687                ),
688            });
689        }
690
691        let rx_frequency = Frequency::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
692        let tx_offset = Frequency::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
693
694        // Byte 0x08: step_size [7:4] | low nibble [3:0]
695        let step_size =
696            StepSize::try_from(bytes[0x08] >> 4).map_err(|e| ProtocolError::FieldParse {
697                command: "flash_channel".into(),
698                field: "step_size".into(),
699                detail: e.to_string(),
700            })?;
701        let byte08_low = bytes[0x08] & 0x0F;
702
703        // Byte 0x09: unknown[7] | mode[6:4] | narrow[3] | fine_mode[2] | fine_step[1:0]
704        let byte09 = bytes[0x09];
705        let byte09_bit7 = (byte09 >> 7) & 1 != 0;
706        let mode =
707            MemoryMode::try_from((byte09 >> 4) & 0x07).map_err(|e| ProtocolError::FieldParse {
708                command: "flash_channel".into(),
709                field: "mode".into(),
710                detail: e.to_string(),
711            })?;
712        let narrow = (byte09 >> 3) & 1 != 0;
713        let fine_mode = (byte09 >> 2) & 1 != 0;
714        let fine_step =
715            FineStep::try_from(byte09 & 0x03).map_err(|e| ProtocolError::FieldParse {
716                command: "flash_channel".into(),
717                field: "fine_step".into(),
718                detail: e.to_string(),
719            })?;
720
721        // Byte 0x0A: tone[7] | ctcss[6] | dtcs[5] | cross[4] | unk[3] | split[2] | duplex[1:0]
722        let byte0a = bytes[0x0A];
723        let tone_enabled = (byte0a >> 7) & 1 != 0;
724        let ctcss_enabled = (byte0a >> 6) & 1 != 0;
725        let dtcs_enabled = (byte0a >> 5) & 1 != 0;
726        let cross_tone = (byte0a >> 4) & 1 != 0;
727        let byte0a_bit3 = (byte0a >> 3) & 1 != 0;
728        let split = (byte0a >> 2) & 1 != 0;
729        let duplex =
730            FlashDuplex::try_from(byte0a & 0x03).map_err(|e| ProtocolError::FieldParse {
731                command: "flash_channel".into(),
732                field: "duplex".into(),
733                detail: e.to_string(),
734            })?;
735
736        // Byte 0x0B: CTCSS TX tone index
737        let tone_code = ToneCode::new(bytes[0x0B]).map_err(|e| ProtocolError::FieldParse {
738            command: "flash_channel".into(),
739            field: "tone_code".into(),
740            detail: e.to_string(),
741        })?;
742
743        // Byte 0x0C: unknown[7:6] | CTCSS RX index[5:0]
744        let byte0c_high = (bytes[0x0C] >> 6) & 0x03;
745        let ctcss_code =
746            ToneCode::new(bytes[0x0C] & 0x3F).map_err(|e| ProtocolError::FieldParse {
747                command: "flash_channel".into(),
748                field: "ctcss_code".into(),
749                detail: e.to_string(),
750            })?;
751
752        // Byte 0x0D: unknown[7] | DCS code[6:0]
753        let byte0d_bit7 = (bytes[0x0D] >> 7) & 1 != 0;
754        let dcs_code = DcsCode::new(bytes[0x0D] & 0x7F).map_err(|e| ProtocolError::FieldParse {
755            command: "flash_channel".into(),
756            field: "dcs_code".into(),
757            detail: e.to_string(),
758        })?;
759
760        // Byte 0x0E: reserved[7:6] | cross_type[5:4] | reserved[3:2] | digital_squelch[1:0]
761        let byte0e = bytes[0x0E];
762        let byte0e_reserved = (byte0e & 0xC0) | (byte0e & 0x0C);
763        let cross_tone_type = CrossToneType::try_from((byte0e >> 4) & 0x03).map_err(|e| {
764            ProtocolError::FieldParse {
765                command: "flash_channel".into(),
766                field: "cross_tone_type".into(),
767                detail: e.to_string(),
768            }
769        })?;
770        let digital_squelch = FlashDigitalSquelch::try_from(byte0e & 0x03).map_err(|e| {
771            ProtocolError::FieldParse {
772                command: "flash_channel".into(),
773                field: "digital_squelch".into(),
774                detail: e.to_string(),
775            }
776        })?;
777
778        // Bytes 0x0F-0x16: UR callsign (8 bytes)
779        let mut ur_arr = [0u8; 8];
780        ur_arr.copy_from_slice(&bytes[0x0F..0x17]);
781        let ur_call = DstarCallsign::from_wire_bytes(&ur_arr);
782
783        // Bytes 0x17-0x1E: RPT1 callsign (8 bytes)
784        let mut rpt1_arr = [0u8; 8];
785        rpt1_arr.copy_from_slice(&bytes[0x17..0x1F]);
786        let rpt1 = DstarCallsign::from_wire_bytes(&rpt1_arr);
787
788        // Bytes 0x1F-0x26: RPT2 callsign (8 bytes)
789        let mut rpt2_arr = [0u8; 8];
790        rpt2_arr.copy_from_slice(&bytes[0x1F..0x27]);
791        let rpt2 = DstarCallsign::from_wire_bytes(&rpt2_arr);
792
793        // Byte 0x27: unknown[7] | DV code[6:0]
794        let byte27_bit7 = (bytes[0x27] >> 7) & 1 != 0;
795        let dv_code = bytes[0x27] & 0x7F;
796
797        Ok(Self {
798            rx_frequency,
799            tx_offset,
800            step_size,
801            byte08_low,
802            mode,
803            narrow,
804            fine_mode,
805            fine_step,
806            byte09_bit7,
807            tone_enabled,
808            ctcss_enabled,
809            dtcs_enabled,
810            cross_tone,
811            byte0a_bit3,
812            split,
813            duplex,
814            tone_code,
815            ctcss_code,
816            byte0c_high,
817            dcs_code,
818            byte0d_bit7,
819            cross_tone_type,
820            digital_squelch,
821            byte0e_reserved,
822            ur_call,
823            rpt1,
824            rpt2,
825            dv_code,
826            byte27_bit7,
827        })
828    }
829
830    /// Serializes the flash channel to a 40-byte packed binary array.
831    #[must_use]
832    pub fn to_bytes(&self) -> [u8; 40] {
833        let mut buf = [0u8; 40];
834
835        // bytes[0..4]: RX frequency, little-endian
836        buf[0..4].copy_from_slice(&self.rx_frequency.to_le_bytes());
837
838        // bytes[4..8]: TX offset, little-endian
839        buf[4..8].copy_from_slice(&self.tx_offset.to_le_bytes());
840
841        // byte 0x08: step_size[7:4] | low[3:0]
842        buf[0x08] = (u8::from(self.step_size) << 4) | (self.byte08_low & 0x0F);
843
844        // byte 0x09: bit7 | mode[6:4] | narrow[3] | fine_mode[2] | fine_step[1:0]
845        buf[0x09] = (u8::from(self.byte09_bit7) << 7)
846            | (u8::from(self.mode) << 4)
847            | (u8::from(self.narrow) << 3)
848            | (u8::from(self.fine_mode) << 2)
849            | u8::from(self.fine_step);
850
851        // byte 0x0A: tone[7] | ctcss[6] | dtcs[5] | cross[4] | bit3 | split[2] | duplex[1:0]
852        buf[0x0A] = (u8::from(self.tone_enabled) << 7)
853            | (u8::from(self.ctcss_enabled) << 6)
854            | (u8::from(self.dtcs_enabled) << 5)
855            | (u8::from(self.cross_tone) << 4)
856            | (u8::from(self.byte0a_bit3) << 3)
857            | (u8::from(self.split) << 2)
858            | u8::from(self.duplex);
859
860        // byte 0x0B: tone code index
861        buf[0x0B] = self.tone_code.index();
862
863        // byte 0x0C: high[7:6] | ctcss_code[5:0]
864        buf[0x0C] = (self.byte0c_high << 6) | (self.ctcss_code.index() & 0x3F);
865
866        // byte 0x0D: bit7 | dcs_code[6:0]
867        buf[0x0D] = (u8::from(self.byte0d_bit7) << 7) | (self.dcs_code.index() & 0x7F);
868
869        // byte 0x0E: reserved[7:6] | cross_type[5:4] | reserved[3:2] | digital_squelch[1:0]
870        buf[0x0E] = (self.byte0e_reserved & 0xCC)
871            | (u8::from(self.cross_tone_type) << 4)
872            | u8::from(self.digital_squelch);
873
874        // bytes 0x0F-0x16: UR callsign
875        buf[0x0F..0x17].copy_from_slice(&self.ur_call.to_wire_bytes());
876
877        // bytes 0x17-0x1E: RPT1 callsign
878        buf[0x17..0x1F].copy_from_slice(&self.rpt1.to_wire_bytes());
879
880        // bytes 0x1F-0x26: RPT2 callsign
881        buf[0x1F..0x27].copy_from_slice(&self.rpt2.to_wire_bytes());
882
883        // byte 0x27: bit7 | dv_code[6:0]
884        buf[0x27] = (u8::from(self.byte27_bit7) << 7) | (self.dv_code & 0x7F);
885
886        buf
887    }
888}
889
890impl Default for FlashChannel {
891    fn default() -> Self {
892        Self {
893            rx_frequency: Frequency::new(0),
894            tx_offset: Frequency::new(0),
895            step_size: StepSize::Hz5000,
896            byte08_low: 0,
897            mode: MemoryMode::Fm,
898            narrow: false,
899            fine_mode: false,
900            fine_step: FineStep::Hz20,
901            byte09_bit7: false,
902            tone_enabled: false,
903            ctcss_enabled: false,
904            dtcs_enabled: false,
905            cross_tone: false,
906            byte0a_bit3: false,
907            split: false,
908            duplex: FlashDuplex::Simplex,
909            // Safety: 0 is always a valid index for ToneCode (0..=49)
910            tone_code: ToneCode::new(0).expect("0 is valid tone code"),
911            ctcss_code: ToneCode::new(0).expect("0 is valid tone code"),
912            byte0c_high: 0,
913            // Safety: 0 is always a valid index for DcsCode (0..=103)
914            dcs_code: DcsCode::new(0).expect("0 is valid DCS code"),
915            byte0d_bit7: false,
916            cross_tone_type: CrossToneType::DcsOff,
917            digital_squelch: FlashDigitalSquelch::Off,
918            byte0e_reserved: 0,
919            ur_call: DstarCallsign::default(),
920            rpt1: DstarCallsign::default(),
921            rpt2: DstarCallsign::default(),
922            dv_code: 0,
923            byte27_bit7: false,
924        }
925    }
926}
927
928impl Default for ChannelMemory {
929    fn default() -> Self {
930        Self {
931            rx_frequency: Frequency::new(0),
932            tx_offset: Frequency::new(0),
933            step_size: StepSize::Hz5000,
934            mode_flags_raw: 0,
935            shift: ShiftDirection::SIMPLEX,
936            reverse: false,
937            tone_enable: false,
938            ctcss_mode: CtcssMode::Off,
939            dcs_enable: false,
940            cross_tone_reverse: false,
941            flags_0a_raw: 0,
942            // Safety: 0 is always a valid index for ToneCode (0..=49)
943            tone_code: ToneCode::new(0).expect("0 is valid tone code"),
944            // Safety: 0 is always a valid index for ToneCode (0..=49)
945            ctcss_code: ToneCode::new(0).expect("0 is valid tone code"),
946            // Safety: 0 is always a valid index for DcsCode (0..=103)
947            dcs_code: DcsCode::new(0).expect("0 is valid DCS code"),
948            cross_tone_combo: CrossToneType::DcsOff,
949            digital_squelch: FlashDigitalSquelch::Off,
950            urcall: ChannelName::default(),
951            data_mode: 0,
952        }
953    }
954}
955
956#[cfg(test)]
957mod tests {
958    use super::*;
959    use crate::error::ValidationError;
960    use crate::types::frequency::Frequency;
961    use crate::types::mode::{ShiftDirection, StepSize};
962    use crate::types::tone::{CtcssMode, DcsCode, ToneCode};
963
964    #[test]
965    fn channel_name_valid() {
966        let name = ChannelName::new("RPT1").unwrap();
967        assert_eq!(name.as_str(), "RPT1");
968    }
969
970    #[test]
971    fn channel_name_empty() {
972        let name = ChannelName::new("").unwrap();
973        assert_eq!(name.as_str(), "");
974    }
975
976    #[test]
977    fn channel_name_max_length() {
978        let name = ChannelName::new("12345678").unwrap();
979        assert_eq!(name.as_str(), "12345678");
980    }
981
982    #[test]
983    fn channel_name_too_long() {
984        let err = ChannelName::new("123456789").unwrap_err();
985        assert!(matches!(
986            err,
987            ValidationError::ChannelNameTooLong { len: 9 }
988        ));
989    }
990
991    #[test]
992    fn channel_name_to_bytes_padded() {
993        let name = ChannelName::new("RPT1").unwrap();
994        let bytes = name.to_bytes();
995        assert_eq!(bytes.len(), 24);
996        assert_eq!(&bytes[..4], b"RPT1");
997        assert!(bytes[4..].iter().all(|&b| b == 0));
998    }
999
1000    #[test]
1001    fn channel_name_from_bytes() {
1002        let mut bytes = [0u8; 24];
1003        bytes[..4].copy_from_slice(b"RPT1");
1004        let name = ChannelName::from_bytes(&bytes);
1005        assert_eq!(name.as_str(), "RPT1");
1006    }
1007
1008    // --- ChannelMemory tests ---
1009
1010    #[test]
1011    fn channel_memory_byte_layout_size() {
1012        assert_eq!(ChannelMemory::BYTE_SIZE, 40);
1013    }
1014
1015    #[test]
1016    fn channel_memory_round_trip_simplex_vhf() {
1017        // flags_0a_raw must be consistent with individual fields for round-trip
1018        // tone=bit7, shift=bits2:0=1 → flags_0a_raw = 0x81
1019        let ch = ChannelMemory {
1020            rx_frequency: Frequency::new(145_000_000),
1021            tx_offset: Frequency::new(600_000),
1022            step_size: StepSize::Hz12500,
1023            mode_flags_raw: 0,
1024            shift: ShiftDirection::UP,
1025            reverse: false,
1026            tone_enable: true,
1027            ctcss_mode: CtcssMode::Off,
1028            dcs_enable: false,
1029            cross_tone_reverse: false,
1030            flags_0a_raw: 0x81, // tone(bit7) + shift+(bit0)
1031            tone_code: ToneCode::new(8).unwrap(),
1032            ctcss_code: ToneCode::new(8).unwrap(),
1033            dcs_code: DcsCode::new(0).unwrap(),
1034            cross_tone_combo: CrossToneType::DcsOff,
1035            digital_squelch: FlashDigitalSquelch::Off,
1036            urcall: ChannelName::new("").unwrap(),
1037            data_mode: 0,
1038        };
1039        let bytes = ch.to_bytes();
1040        assert_eq!(bytes.len(), 40);
1041        let parsed = ChannelMemory::from_bytes(&bytes).unwrap();
1042        assert_eq!(parsed, ch);
1043    }
1044
1045    #[test]
1046    fn channel_memory_byte08_packing() {
1047        let ch = ChannelMemory {
1048            step_size: StepSize::Hz12500, // index 5
1049            shift: ShiftDirection::UP,    // 1
1050            ..ChannelMemory::default()
1051        };
1052        let bytes = ch.to_bytes();
1053        assert_eq!(bytes[0x08], 0x51); // 5 << 4 | 1
1054    }
1055
1056    #[test]
1057    fn channel_memory_byte09_packing() {
1058        // byte[9] is now zeroed in to_bytes (mode/fine not individually modeled)
1059        let ch = ChannelMemory {
1060            reverse: true,
1061            tone_enable: true,
1062            ctcss_mode: CtcssMode::On,
1063            ..ChannelMemory::default()
1064        };
1065        let bytes = ch.to_bytes();
1066        assert_eq!(bytes[0x09], 0x00);
1067    }
1068
1069    #[test]
1070    fn channel_memory_byte0a_packing() {
1071        // byte[0x0A] is stored directly from flags_0a_raw (hardware-verified)
1072        // tone=bit7, ctcss=bit6, dcs=bit5, cross=bit4, reverse=bit3, shift=bits2:0
1073        let ch = ChannelMemory {
1074            dcs_enable: true,
1075            cross_tone_reverse: true,
1076            flags_0a_raw: 0xB0, // dcs(bit5) + cross(bit4) + tone(bit7)... actually just set directly
1077            ..ChannelMemory::default()
1078        };
1079        let bytes = ch.to_bytes();
1080        assert_eq!(bytes[0x0A], 0xB0);
1081    }
1082
1083    #[test]
1084    fn channel_memory_unknown_bits_passthrough() {
1085        let ch = ChannelMemory {
1086            dcs_enable: false,
1087            cross_tone_reverse: false,
1088            flags_0a_raw: 0x2B,
1089            ..ChannelMemory::default()
1090        };
1091        let bytes = ch.to_bytes();
1092        assert_eq!(bytes[0x0A], 0x2B);
1093        let parsed = ChannelMemory::from_bytes(&bytes).unwrap();
1094        assert_eq!(parsed.flags_0a_raw, 0x2B);
1095    }
1096
1097    #[test]
1098    fn channel_memory_byte0e_packing() {
1099        let ch = ChannelMemory {
1100            cross_tone_combo: CrossToneType::ToneDcs,
1101            digital_squelch: FlashDigitalSquelch::Code,
1102            ..ChannelMemory::default()
1103        };
1104        let bytes = ch.to_bytes();
1105        assert_eq!(bytes[0x0E], 0x1D); // (1<<4) | 0x0C | 1
1106    }
1107
1108    #[test]
1109    fn channel_memory_frequency_le_bytes() {
1110        let ch = ChannelMemory {
1111            rx_frequency: Frequency::new(145_000_000),
1112            tx_offset: Frequency::new(600_000),
1113            ..ChannelMemory::default()
1114        };
1115        let bytes = ch.to_bytes();
1116        let rx = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1117        assert_eq!(rx, 145_000_000);
1118        let tx = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1119        assert_eq!(tx, 600_000);
1120    }
1121
1122    // --- FlashChannel tests ---
1123
1124    #[test]
1125    fn flash_channel_byte_size() {
1126        assert_eq!(FlashChannel::BYTE_SIZE, 40);
1127    }
1128
1129    #[test]
1130    fn flash_channel_default_round_trip() {
1131        let ch = FlashChannel::default();
1132        let bytes = ch.to_bytes();
1133        let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1134        assert_eq!(parsed, ch);
1135    }
1136
1137    #[test]
1138    fn flash_channel_mode_encoding() {
1139        use crate::types::mode::MemoryMode;
1140        // Flash mode encoding: FM=0, DV=1, AM=2, LSB=3, USB=4, CW=5, NFM=6, DR=7
1141        for (raw, expected) in [
1142            (0, MemoryMode::Fm),
1143            (1, MemoryMode::Dv),
1144            (2, MemoryMode::Am),
1145            (3, MemoryMode::Lsb),
1146            (4, MemoryMode::Usb),
1147            (5, MemoryMode::Cw),
1148            (6, MemoryMode::Nfm),
1149            (7, MemoryMode::Dr),
1150        ] {
1151            let ch = FlashChannel {
1152                mode: expected,
1153                ..FlashChannel::default()
1154            };
1155            let bytes = ch.to_bytes();
1156            // Mode is at byte 0x09 bits [6:4]
1157            assert_eq!(
1158                (bytes[0x09] >> 4) & 0x07,
1159                raw,
1160                "mode {expected} should encode as {raw}"
1161            );
1162            let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1163            assert_eq!(parsed.mode, expected);
1164        }
1165    }
1166
1167    #[test]
1168    fn flash_channel_mode_matches_cat() {
1169        // CAT MD and flash memory use the same encoding for all 8 modes (0-7).
1170        use crate::types::mode::{MemoryMode, Mode};
1171        assert_eq!(u8::from(MemoryMode::Am), u8::from(Mode::Am));
1172        assert_eq!(u8::from(MemoryMode::Nfm), u8::from(Mode::Nfm));
1173        assert_eq!(u8::from(MemoryMode::Fm), u8::from(Mode::Fm));
1174        assert_eq!(u8::from(MemoryMode::Dr), u8::from(Mode::Dr));
1175    }
1176
1177    #[test]
1178    fn flash_channel_byte09_packing() {
1179        let ch = FlashChannel {
1180            mode: MemoryMode::Am,        // 2 -> bits [6:4] = 0b010
1181            narrow: true,                // bit 3
1182            fine_mode: true,             // bit 2
1183            fine_step: FineStep::Hz1000, // bits [1:0] = 3
1184            byte09_bit7: false,
1185            ..FlashChannel::default()
1186        };
1187        let bytes = ch.to_bytes();
1188        // Expected: 0b0_010_1_1_11 = 0x2F
1189        assert_eq!(bytes[0x09], 0x2F);
1190        let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1191        assert_eq!(parsed.mode, MemoryMode::Am);
1192        assert!(parsed.narrow);
1193        assert!(parsed.fine_mode);
1194        assert_eq!(parsed.fine_step, FineStep::Hz1000);
1195    }
1196
1197    #[test]
1198    fn flash_channel_byte0a_tone_duplex() {
1199        let ch = FlashChannel {
1200            tone_enabled: true,         // bit 7
1201            ctcss_enabled: false,       // bit 6
1202            dtcs_enabled: true,         // bit 5
1203            cross_tone: false,          // bit 4
1204            byte0a_bit3: false,         // bit 3
1205            split: true,                // bit 2
1206            duplex: FlashDuplex::Minus, // bits [1:0] = 2
1207            ..FlashChannel::default()
1208        };
1209        let bytes = ch.to_bytes();
1210        // Expected: 0b1_0_1_0_0_1_10 = 0xA6
1211        assert_eq!(bytes[0x0A], 0xA6);
1212        let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1213        assert!(parsed.tone_enabled);
1214        assert!(!parsed.ctcss_enabled);
1215        assert!(parsed.dtcs_enabled);
1216        assert!(!parsed.cross_tone);
1217        assert!(parsed.split);
1218        assert_eq!(parsed.duplex, FlashDuplex::Minus);
1219    }
1220
1221    #[test]
1222    fn flash_channel_dstar_callsigns() {
1223        use crate::types::dstar::DstarCallsign;
1224        let ch = FlashChannel {
1225            ur_call: DstarCallsign::new("CQCQCQ").unwrap(),
1226            rpt1: DstarCallsign::new("W4BFB B").unwrap(),
1227            rpt2: DstarCallsign::new("W4BFB G").unwrap(),
1228            ..FlashChannel::default()
1229        };
1230        let bytes = ch.to_bytes();
1231        // UR at 0x0F-0x16 (8 bytes, space-padded)
1232        assert_eq!(&bytes[0x0F..0x17], b"CQCQCQ  ");
1233        // RPT1 at 0x17-0x1E
1234        assert_eq!(&bytes[0x17..0x1F], b"W4BFB B ");
1235        // RPT2 at 0x1F-0x26
1236        assert_eq!(&bytes[0x1F..0x27], b"W4BFB G ");
1237
1238        let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1239        assert_eq!(parsed.ur_call.as_str(), "CQCQCQ");
1240        assert_eq!(parsed.rpt1.as_str(), "W4BFB B");
1241        assert_eq!(parsed.rpt2.as_str(), "W4BFB G");
1242    }
1243
1244    #[test]
1245    fn flash_channel_byte0e_cross_tone_digital_squelch() {
1246        let ch = FlashChannel {
1247            cross_tone_type: CrossToneType::DcsCtcss, // bits [5:4] = 2
1248            digital_squelch: FlashDigitalSquelch::Callsign, // bits [1:0] = 2
1249            byte0e_reserved: 0,
1250            ..FlashChannel::default()
1251        };
1252        let bytes = ch.to_bytes();
1253        // Expected: 0b00_10_00_10 = 0x22
1254        assert_eq!(bytes[0x0E], 0x22);
1255        let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1256        assert_eq!(parsed.cross_tone_type, CrossToneType::DcsCtcss);
1257        assert_eq!(parsed.digital_squelch, FlashDigitalSquelch::Callsign);
1258    }
1259
1260    #[test]
1261    fn flash_channel_dv_code() {
1262        let ch = FlashChannel {
1263            dv_code: 42,
1264            byte27_bit7: true,
1265            ..FlashChannel::default()
1266        };
1267        let bytes = ch.to_bytes();
1268        assert_eq!(bytes[0x27], 0x80 | 0x2A);
1269        let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1270        assert_eq!(parsed.dv_code, 42);
1271        assert!(parsed.byte27_bit7);
1272    }
1273
1274    #[test]
1275    fn flash_channel_full_round_trip() {
1276        use crate::types::dstar::DstarCallsign;
1277        let ch = FlashChannel {
1278            rx_frequency: Frequency::new(146_520_000),
1279            tx_offset: Frequency::new(600_000),
1280            step_size: StepSize::Hz12500,
1281            byte08_low: 0x03,
1282            mode: MemoryMode::Dv,
1283            narrow: false,
1284            fine_mode: true,
1285            fine_step: FineStep::Hz100,
1286            byte09_bit7: false,
1287            tone_enabled: true,
1288            ctcss_enabled: true,
1289            dtcs_enabled: false,
1290            cross_tone: false,
1291            byte0a_bit3: false,
1292            split: false,
1293            duplex: FlashDuplex::Plus,
1294            tone_code: ToneCode::new(8).unwrap(),
1295            ctcss_code: ToneCode::new(12).unwrap(),
1296            byte0c_high: 0,
1297            dcs_code: DcsCode::new(5).unwrap(),
1298            byte0d_bit7: false,
1299            cross_tone_type: CrossToneType::ToneDcs,
1300            digital_squelch: FlashDigitalSquelch::Code,
1301            byte0e_reserved: 0,
1302            ur_call: DstarCallsign::new("CQCQCQ").unwrap(),
1303            rpt1: DstarCallsign::new("W4BFB B").unwrap(),
1304            rpt2: DstarCallsign::new("W4BFB G").unwrap(),
1305            dv_code: 99,
1306            byte27_bit7: false,
1307        };
1308        let bytes = ch.to_bytes();
1309        assert_eq!(bytes.len(), 40);
1310        let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1311        assert_eq!(parsed, ch);
1312    }
1313
1314    #[test]
1315    fn flash_channel_too_short() {
1316        let bytes = [0u8; 39];
1317        let err = FlashChannel::from_bytes(&bytes);
1318        assert!(err.is_err());
1319    }
1320
1321    #[test]
1322    fn flash_channel_reserved_bits_preserved() {
1323        let ch = FlashChannel {
1324            byte0c_high: 0x03,
1325            byte0d_bit7: true,
1326            byte0e_reserved: 0xCC, // bits [7:6] and [3:2]
1327            byte0a_bit3: true,
1328            byte09_bit7: true,
1329            ..FlashChannel::default()
1330        };
1331        let bytes = ch.to_bytes();
1332        let parsed = FlashChannel::from_bytes(&bytes).unwrap();
1333        assert_eq!(parsed.byte0c_high, 0x03);
1334        assert!(parsed.byte0d_bit7);
1335        assert_eq!(parsed.byte0e_reserved, 0xCC);
1336        assert!(parsed.byte0a_bit3);
1337        assert!(parsed.byte09_bit7);
1338    }
1339
1340    #[test]
1341    fn flash_fine_step_display() {
1342        assert_eq!(FineStep::Hz20.to_string(), "20 Hz");
1343        assert_eq!(FineStep::Hz100.to_string(), "100 Hz");
1344        assert_eq!(FineStep::Hz500.to_string(), "500 Hz");
1345        assert_eq!(FineStep::Hz1000.to_string(), "1000 Hz");
1346    }
1347
1348    #[test]
1349    fn flash_duplex_round_trip() {
1350        for i in 0u8..FlashDuplex::COUNT {
1351            let d = FlashDuplex::try_from(i).unwrap();
1352            assert_eq!(u8::from(d), i);
1353        }
1354        assert!(FlashDuplex::try_from(FlashDuplex::COUNT).is_err());
1355    }
1356
1357    #[test]
1358    fn cross_tone_type_round_trip() {
1359        for i in 0u8..CrossToneType::COUNT {
1360            let ct = CrossToneType::try_from(i).unwrap();
1361            assert_eq!(u8::from(ct), i);
1362        }
1363        assert!(CrossToneType::try_from(CrossToneType::COUNT).is_err());
1364    }
1365
1366    #[test]
1367    fn flash_digital_squelch_round_trip() {
1368        for i in 0u8..FlashDigitalSquelch::COUNT {
1369            let ds = FlashDigitalSquelch::try_from(i).unwrap();
1370            assert_eq!(u8::from(ds), i);
1371        }
1372        assert!(FlashDigitalSquelch::try_from(FlashDigitalSquelch::COUNT).is_err());
1373    }
1374}