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}