kenwood_thd75/protocol/
memory.rs

1//! Memory commands: ME, MR, 0M.
2//!
3//! Provides serialization of ME write commands and parsing of ME responses.
4//! MR (recall) and 0M (programming mode) are action commands whose responses
5//! are either echoes or absent.
6
7use crate::error::ProtocolError;
8use crate::types::Band;
9
10use super::core::{CHANNEL_FIELD_COUNT, parse_channel_fields, serialize_channel_fields};
11use super::{Command, Response};
12
13/// Serialize a memory write command into its wire-format body (without trailing `\r`).
14///
15/// The ME wire format has 23 comma-separated fields: 1 channel number followed
16/// by 22 data fields. Two ME-specific fields (at positions 14 and 22) are
17/// inserted relative to the 20-field FO layout and serialized as `0`.
18///
19/// Returns `None` if the command is not a memory write command.
20pub(crate) fn serialize_memory_write(cmd: &Command) -> Option<String> {
21    match cmd {
22        Command::SetMemoryChannel { channel, data } => {
23            let fo = serialize_channel_fields(data);
24            // FO serializes 20 comma-separated fields. Split them so we can
25            // insert the two ME-specific extras.
26            let parts: Vec<&str> = fo.split(',').collect();
27            debug_assert_eq!(parts.len(), CHANNEL_FIELD_COUNT);
28
29            // Reconstruct ME layout:
30            //   parts[0..=12]  -> ME fields 1..=13  (freq through x5)
31            //   "0"            -> ME field 14        (ME-specific)
32            //   parts[13..=19] -> ME fields 15..=21  (tt through dm)
33            //   "0"            -> ME field 22        (ME-specific)
34            let me_body: String = parts[..13]
35                .iter()
36                .copied()
37                .chain(std::iter::once("0"))
38                .chain(parts[13..].iter().copied())
39                .chain(std::iter::once("0"))
40                .collect::<Vec<&str>>()
41                .join(",");
42
43            Some(format!("ME {channel:03},{me_body}"))
44        }
45        _ => None,
46    }
47}
48
49/// Parse a memory command response from mnemonic and payload.
50///
51/// Returns `None` if the mnemonic is not a memory command.
52pub(crate) fn parse_memory(
53    mnemonic: &str,
54    payload: &str,
55) -> Option<Result<Response, ProtocolError>> {
56    match mnemonic {
57        "ME" => Some(parse_me(payload)),
58        "MR" => Some(parse_mr(payload)),
59        "0M" => Some(Ok(Response::ProgrammingMode)),
60        _ => None,
61    }
62}
63
64/// Number of comma-separated fields in an ME response (channel + 22 data).
65const ME_FIELD_COUNT: usize = 23;
66
67/// Parse an ME (memory channel) response.
68///
69/// ME responses contain 23 comma-separated fields: 1 channel number followed by
70/// 22 data fields. The ME layout differs from FO by inserting one extra field
71/// at position 14 (between x5 and tone-code) and one extra field at position 22
72/// (after data-mode):
73///
74/// ```text
75/// ME layout (22 data fields after channel):
76///   [ 1.. 13] freq, offset, step, shift, reverse, tone, ctcss, dcs, x1-x5
77///   [14]      ME-specific field (unknown purpose)
78///   [15..=21] tt, cc, ddd, ds, urcall, lo, dm
79///   [22]      ME-specific field (unknown purpose)
80/// ```
81///
82/// We remap these into the 20-field FO order and delegate to
83/// [`parse_channel_fields`].
84fn parse_me(payload: &str) -> Result<Response, ProtocolError> {
85    let fields: Vec<&str> = payload.split(',').collect();
86
87    if fields.len() != ME_FIELD_COUNT {
88        return Err(ProtocolError::FieldCount {
89            command: "ME".to_owned(),
90            expected: ME_FIELD_COUNT,
91            actual: fields.len(),
92        });
93    }
94
95    let channel = fields[0]
96        .parse::<u16>()
97        .map_err(|_| ProtocolError::FieldParse {
98            command: "ME".to_owned(),
99            field: "channel".to_owned(),
100            detail: format!("invalid channel number: {:?}", fields[0]),
101        })?;
102
103    // Remap ME fields to the 20-field FO layout, skipping the two ME-specific
104    // fields at indices 14 and 22.
105    //   fields[1..=13]  -> FO fields 0..=12  (freq through x5, 13 items)
106    //   fields[15..=21] -> FO fields 13..=19 (tt through dm, 7 items)
107    let fo_fields: Vec<&str> = fields[1..=13]
108        .iter()
109        .chain(fields[15..=21].iter())
110        .copied()
111        .collect();
112
113    debug_assert_eq!(fo_fields.len(), CHANNEL_FIELD_COUNT);
114
115    let data = parse_channel_fields(&fo_fields, "ME")?;
116
117    Ok(Response::MemoryChannel { channel, data })
118}
119
120/// Parse an MR response.
121///
122/// Two formats are supported:
123/// - Write acknowledgment: `band,channel` (comma-separated, e.g., `0,021`)
124/// - Read response: `bandCCC` (no comma, e.g., `021` meaning band 0 channel 21)
125///
126/// Hardware-verified: `MR 0\r` returns `MR 021` (read, no comma).
127/// `MR 0,021\r` returns `MR 0,021` (write acknowledgment, with comma).
128fn parse_mr(payload: &str) -> Result<Response, ProtocolError> {
129    if let Some((band_str, ch_str)) = payload.split_once(',') {
130        // Write acknowledgment format: "band,channel"
131        let band_val = band_str
132            .parse::<u8>()
133            .map_err(|_| ProtocolError::FieldParse {
134                command: "MR".to_owned(),
135                field: "band".to_owned(),
136                detail: format!("invalid band: {band_str:?}"),
137            })?;
138
139        let band = Band::try_from(band_val).map_err(|e| ProtocolError::FieldParse {
140            command: "MR".to_owned(),
141            field: "band".to_owned(),
142            detail: e.to_string(),
143        })?;
144
145        let channel = ch_str
146            .parse::<u16>()
147            .map_err(|_| ProtocolError::FieldParse {
148                command: "MR".to_owned(),
149                field: "channel".to_owned(),
150                detail: format!("invalid channel number: {ch_str:?}"),
151            })?;
152
153        Ok(Response::MemoryRecall { band, channel })
154    } else {
155        // Read response format: "bandCCC" (no comma)
156        // First character is the band digit, rest is the channel number.
157        if payload.is_empty() {
158            return Err(ProtocolError::FieldParse {
159                command: "MR".to_owned(),
160                field: "all".to_owned(),
161                detail: "empty MR read payload".to_owned(),
162            });
163        }
164
165        let band_str = &payload[..1];
166        let ch_str = &payload[1..];
167
168        let band_val = band_str
169            .parse::<u8>()
170            .map_err(|_| ProtocolError::FieldParse {
171                command: "MR".to_owned(),
172                field: "band".to_owned(),
173                detail: format!("invalid band: {band_str:?}"),
174            })?;
175
176        let band = Band::try_from(band_val).map_err(|e| ProtocolError::FieldParse {
177            command: "MR".to_owned(),
178            field: "band".to_owned(),
179            detail: e.to_string(),
180        })?;
181
182        let channel = ch_str
183            .parse::<u16>()
184            .map_err(|_| ProtocolError::FieldParse {
185                command: "MR".to_owned(),
186                field: "channel".to_owned(),
187                detail: format!("invalid channel number: {ch_str:?}"),
188            })?;
189
190        Ok(Response::CurrentChannel { band, channel })
191    }
192}