1use crate::error::ProtocolError;
7use crate::types::Band;
8use crate::types::channel::{ChannelMemory, ChannelName, CrossToneType, FlashDigitalSquelch};
9use crate::types::frequency::Frequency;
10use crate::types::mode::{PowerLevel, ShiftDirection, StepSize};
11use crate::types::radio_params::VfoMemoryMode;
12use crate::types::tone::{CtcssMode, DcsCode, ToneCode};
13
14use super::{Command, Response};
15
16const FO_FIELD_COUNT: usize = 21;
18
19pub(crate) const CHANNEL_FIELD_COUNT: usize = 20;
21
22pub(crate) fn serialize_core_write(cmd: &Command) -> Option<String> {
28 match cmd {
29 Command::SetFrequencyFull { band, channel } => {
30 Some(format!("FO {}", format_fo_fields(*band, channel)))
31 }
32 Command::SetFrequency { band, channel } => {
33 Some(format!("FQ {}", format_fo_fields(*band, channel)))
34 }
35 _ => None,
36 }
37}
38
39pub(crate) fn parse_core(mnemonic: &str, payload: &str) -> Option<Result<Response, ProtocolError>> {
43 match mnemonic {
44 "ID" => Some(Ok(Response::RadioId {
45 model: payload.to_owned(),
46 })),
47 "FV" => Some(Ok(Response::FirmwareVersion {
48 version: payload.to_owned(),
49 })),
50 "PS" => Some(parse_bool_field(payload, "PS").map(|on| Response::PowerStatus { on })),
51 "PC" => Some(parse_pc(payload)),
52 "BC" => Some(parse_bc(payload)),
53 "VM" => Some(parse_vm(payload)),
54 "FR" => Some(parse_bool_field(payload, "FR").map(|enabled| Response::FmRadio { enabled })),
55 "FO" => Some(parse_fo_fq(payload, "FO")),
56 "FQ" => Some(parse_fq(payload)),
57 _ => None,
58 }
59}
60
61pub(crate) fn serialize_channel_fields(ch: &ChannelMemory) -> String {
69 let tone_en = (ch.flags_0a_raw >> 7) & 1;
73 let ctcss_en = (ch.flags_0a_raw >> 6) & 1;
74 let dcs_en = (ch.flags_0a_raw >> 5) & 1;
75 let cross_tone = (ch.flags_0a_raw >> 4) & 1;
76 let reverse = (ch.flags_0a_raw >> 3) & 1;
77 let shift_dir = ch.flags_0a_raw & 0x07; format!(
82 "{},{},{:X},0,0,0,0,{},{},{},{},{},{},{:02},{:02},{:03},{},{},{},{:02}",
83 ch.rx_frequency.to_wire_string(), ch.tx_offset.to_wire_string(), u8::from(ch.step_size), tone_en, ctcss_en, dcs_en, cross_tone, reverse, shift_dir, ch.tone_code.index(), ch.ctcss_code.index(), ch.dcs_code.index(), u8::from(ch.cross_tone_combo), ch.urcall.as_str(), u8::from(ch.digital_squelch), ch.data_mode, )
104}
105
106fn format_fo_fields(band: Band, ch: &ChannelMemory) -> String {
108 format!("{},{}", u8::from(band), serialize_channel_fields(ch))
109}
110
111fn parse_bool_field(payload: &str, cmd: &str) -> Result<bool, ProtocolError> {
113 match payload {
114 "0" => Ok(false),
115 "1" => Ok(true),
116 _ => Err(ProtocolError::FieldParse {
117 command: cmd.to_owned(),
118 field: "value".to_owned(),
119 detail: format!("expected 0 or 1, got {payload:?}"),
120 }),
121 }
122}
123
124fn parse_u8_field(s: &str, cmd: &str, field: &str) -> Result<u8, ProtocolError> {
126 s.parse::<u8>().map_err(|_| ProtocolError::FieldParse {
127 command: cmd.to_owned(),
128 field: field.to_owned(),
129 detail: format!("invalid u8: {s:?}"),
130 })
131}
132
133fn parse_hex_u8_field(s: &str, cmd: &str, field: &str) -> Result<u8, ProtocolError> {
138 u8::from_str_radix(s, 16).map_err(|_| ProtocolError::FieldParse {
139 command: cmd.to_owned(),
140 field: field.to_owned(),
141 detail: format!("invalid hex u8: {s:?}"),
142 })
143}
144
145fn parse_pc(payload: &str) -> Result<Response, ProtocolError> {
147 let parts: Vec<&str> = payload.splitn(2, ',').collect();
148 if parts.len() != 2 {
149 return Err(ProtocolError::FieldParse {
150 command: "PC".to_owned(),
151 field: "all".to_owned(),
152 detail: format!("expected band,level, got {payload:?}"),
153 });
154 }
155 let band_val = parse_u8_field(parts[0], "PC", "band")?;
156 let band = Band::try_from(band_val).map_err(|e| ProtocolError::FieldParse {
157 command: "PC".to_owned(),
158 field: "band".to_owned(),
159 detail: e.to_string(),
160 })?;
161 let level_val = parse_u8_field(parts[1], "PC", "level")?;
162 let level = PowerLevel::try_from(level_val).map_err(|e| ProtocolError::FieldParse {
163 command: "PC".to_owned(),
164 field: "level".to_owned(),
165 detail: e.to_string(),
166 })?;
167 Ok(Response::PowerLevel { band, level })
168}
169
170fn parse_vm(payload: &str) -> Result<Response, ProtocolError> {
174 let parts: Vec<&str> = payload.splitn(2, ',').collect();
175 if parts.len() != 2 {
176 return Err(ProtocolError::FieldParse {
177 command: "VM".to_owned(),
178 field: "all".to_owned(),
179 detail: format!("expected band,mode, got {payload:?}"),
180 });
181 }
182 let band_val = parse_u8_field(parts[0], "VM", "band")?;
183 let band = Band::try_from(band_val).map_err(|e| ProtocolError::FieldParse {
184 command: "VM".to_owned(),
185 field: "band".to_owned(),
186 detail: e.to_string(),
187 })?;
188 let mode_raw = parse_u8_field(parts[1], "VM", "mode")?;
189 let mode = VfoMemoryMode::try_from(mode_raw).map_err(|e| ProtocolError::FieldParse {
190 command: "VM".to_owned(),
191 field: "mode".to_owned(),
192 detail: e.to_string(),
193 })?;
194 Ok(Response::VfoMemoryMode { band, mode })
195}
196
197fn parse_bc(payload: &str) -> Result<Response, ProtocolError> {
199 let band_val = parse_u8_field(payload, "BC", "band")?;
200 let band = Band::try_from(band_val).map_err(|e| ProtocolError::FieldParse {
201 command: "BC".to_owned(),
202 field: "band".to_owned(),
203 detail: e.to_string(),
204 })?;
205 Ok(Response::BandResponse { band })
206}
207
208#[allow(clippy::too_many_lines, clippy::similar_names)]
213pub(crate) fn parse_channel_fields(
214 fields: &[&str],
215 cmd: &str,
216) -> Result<ChannelMemory, ProtocolError> {
217 if fields.len() != CHANNEL_FIELD_COUNT {
218 return Err(ProtocolError::FieldCount {
219 command: cmd.to_owned(),
220 expected: CHANNEL_FIELD_COUNT,
221 actual: fields.len(),
222 });
223 }
224
225 let rx_frequency = Frequency::from_wire_string(fields[0])?;
257
258 let tx_offset = Frequency::from_wire_string(fields[1])?;
260
261 let step_val = parse_hex_u8_field(fields[2], cmd, "step_size")?;
263 let step_size = StepSize::try_from(step_val).map_err(|e| ProtocolError::FieldParse {
264 command: cmd.to_owned(),
265 field: "step_size".to_owned(),
266 detail: e.to_string(),
267 })?;
268
269 let _tx_step = parse_u8_field(fields[3], cmd, "tx_step")?;
272 let mode_val = parse_u8_field(fields[4], cmd, "mode")?;
273 let fine_tuning = parse_u8_field(fields[5], cmd, "fine_tuning")?;
274 let fine_step = parse_u8_field(fields[6], cmd, "fine_step")?;
275 let mode_flags_raw = ((mode_val & 0x07) << 4) | ((fine_tuning & 1) << 3) | (fine_step & 0x07);
276
277 let tone_enable = parse_u8_field(fields[7], cmd, "tone_enable")? != 0;
280 let ctcss_enable = parse_u8_field(fields[8], cmd, "ctcss_enable")? != 0;
281 let dcs_enable = parse_u8_field(fields[9], cmd, "dcs_enable")? != 0;
282 let cross_tone = parse_u8_field(fields[10], cmd, "cross_tone")? != 0;
283 let reverse = parse_u8_field(fields[11], cmd, "reverse")? != 0;
284 let shift_val = parse_u8_field(fields[12], cmd, "shift")?;
287 let shift = ShiftDirection::try_from(shift_val).map_err(|e| ProtocolError::FieldParse {
288 command: cmd.to_owned(),
289 field: "shift".to_owned(),
290 detail: e.to_string(),
291 })?;
292
293 let flags_0a_raw = (u8::from(tone_enable) << 7)
295 | (u8::from(ctcss_enable) << 6)
296 | (u8::from(dcs_enable) << 5)
297 | (u8::from(cross_tone) << 4)
298 | (u8::from(reverse) << 3)
299 | (shift_val & 0x07);
300
301 let ctcss_mode = if ctcss_enable {
303 CtcssMode::try_from(1u8).unwrap_or_else(|_| CtcssMode::try_from(0u8).expect("valid"))
304 } else {
305 CtcssMode::try_from(0u8).expect("zero is valid")
306 };
307
308 let tone_val = parse_u8_field(fields[13], cmd, "tone_code")?;
310 let tone_code = ToneCode::new(tone_val).map_err(|e| ProtocolError::FieldParse {
311 command: cmd.to_owned(),
312 field: "tone_code".to_owned(),
313 detail: e.to_string(),
314 })?;
315
316 let ct_code_val = parse_u8_field(fields[14], cmd, "ctcss_code")?;
318 let ctcss_code = ToneCode::new(ct_code_val).map_err(|e| ProtocolError::FieldParse {
319 command: cmd.to_owned(),
320 field: "ctcss_code".to_owned(),
321 detail: e.to_string(),
322 })?;
323
324 let dcs_val = parse_u8_field(fields[15], cmd, "dcs_code")?;
326 let dcs_code = DcsCode::new(dcs_val).map_err(|e| ProtocolError::FieldParse {
327 command: cmd.to_owned(),
328 field: "dcs_code".to_owned(),
329 detail: e.to_string(),
330 })?;
331
332 let ct_val = parse_u8_field(fields[16], cmd, "cross_tone_combo")?;
334 let cross_tone_combo =
335 CrossToneType::try_from(ct_val & 0x03).map_err(|e| ProtocolError::FieldParse {
336 command: cmd.to_owned(),
337 field: "cross_tone_combo".to_owned(),
338 detail: e.to_string(),
339 })?;
340
341 let urcall = ChannelName::new(fields[17]).map_err(|e| ProtocolError::FieldParse {
343 command: cmd.to_owned(),
344 field: "urcall".to_owned(),
345 detail: e.to_string(),
346 })?;
347
348 let ds_val = parse_u8_field(fields[18], cmd, "digital_squelch")?;
350 let digital_squelch =
351 FlashDigitalSquelch::try_from(ds_val & 0x03).map_err(|e| ProtocolError::FieldParse {
352 command: cmd.to_owned(),
353 field: "digital_squelch".to_owned(),
354 detail: e.to_string(),
355 })?;
356
357 let data_mode = if fields.len() > 19 {
359 parse_u8_field(fields[19], cmd, "digital_code")?
360 } else {
361 0
362 };
363
364 Ok(ChannelMemory {
365 rx_frequency,
366 tx_offset,
367 step_size,
368 mode_flags_raw,
369 shift,
370 reverse,
371 tone_enable,
372 ctcss_mode,
373 dcs_enable,
374 cross_tone_reverse: cross_tone,
375 flags_0a_raw,
376 tone_code,
377 ctcss_code,
378 dcs_code,
379 cross_tone_combo,
380 digital_squelch,
381 urcall,
382 data_mode,
383 })
384}
385
386fn parse_fq(payload: &str) -> Result<Response, ProtocolError> {
391 let fields: Vec<&str> = payload.split(',').collect();
392 if fields.len() == 2 {
393 let band_val = parse_u8_field(fields[0], "FQ", "band")?;
395 let band = Band::try_from(band_val).map_err(|e| ProtocolError::FieldParse {
396 command: "FQ".to_owned(),
397 field: "band".to_owned(),
398 detail: e.to_string(),
399 })?;
400 let rx_frequency = Frequency::from_wire_string(fields[1])?;
401 let channel = ChannelMemory {
402 rx_frequency,
403 ..ChannelMemory::default()
404 };
405 return Ok(Response::Frequency { band, channel });
406 }
407 parse_fo_fq(payload, "FQ")
409}
410
411fn parse_fo_fq(payload: &str, cmd: &str) -> Result<Response, ProtocolError> {
413 let fields: Vec<&str> = payload.split(',').collect();
414 if fields.len() != FO_FIELD_COUNT {
415 return Err(ProtocolError::FieldCount {
416 command: cmd.to_owned(),
417 expected: FO_FIELD_COUNT,
418 actual: fields.len(),
419 });
420 }
421
422 let band_val = parse_u8_field(fields[0], cmd, "band")?;
424 let band = Band::try_from(band_val).map_err(|e| ProtocolError::FieldParse {
425 command: cmd.to_owned(),
426 field: "band".to_owned(),
427 detail: e.to_string(),
428 })?;
429
430 let channel = parse_channel_fields(&fields[1..], cmd)?;
432
433 match cmd {
434 "FO" => Ok(Response::FrequencyFull { band, channel }),
435 "FQ" => Ok(Response::Frequency { band, channel }),
436 _ => Err(ProtocolError::UnknownCommand(cmd.to_owned())),
437 }
438}