1use heapless::Vec as HVec;
19
20use crate::{
21 DeviceMessageEncodeError, DeviceMessageParseError, ErrorCode, Info, InfoParseError,
22 MAX_OTA_PAYLOAD, Modulation, ModulationEncodeError, ModulationParseError, commands,
23};
24
25pub const TYPE_OK: u8 = 0x80;
28pub const TYPE_ERR: u8 = 0x81;
29pub const TYPE_RX: u8 = 0xC0;
30pub const TYPE_TX_DONE: u8 = 0xC1;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[cfg_attr(feature = "defmt", derive(defmt::Format))]
37#[repr(u8)]
38pub enum SetConfigResultCode {
39 Applied = 0,
41 AlreadyMatched = 1,
43 LockedMismatch = 2,
45}
46
47impl SetConfigResultCode {
48 pub const fn as_u8(self) -> u8 {
49 self as u8
50 }
51 pub const fn from_u8(v: u8) -> Option<Self> {
52 Some(match v {
53 0 => Self::Applied,
54 1 => Self::AlreadyMatched,
55 2 => Self::LockedMismatch,
56 _ => return None,
57 })
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[cfg_attr(feature = "defmt", derive(defmt::Format))]
64#[repr(u8)]
65pub enum Owner {
66 None = 0,
68 Mine = 1,
70 Other = 2,
72}
73
74impl Owner {
75 pub const fn as_u8(self) -> u8 {
76 self as u8
77 }
78 pub const fn from_u8(v: u8) -> Option<Self> {
79 Some(match v {
80 0 => Self::None,
81 1 => Self::Mine,
82 2 => Self::Other,
83 _ => return None,
84 })
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94#[cfg_attr(feature = "defmt", derive(defmt::Format))]
95pub struct SetConfigResult {
96 pub result: SetConfigResultCode,
97 pub owner: Owner,
98 pub current: Modulation,
99}
100
101impl SetConfigResult {
102 pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
103 if buf.len() < 2 {
104 return Err(DeviceMessageEncodeError::BufferTooSmall);
105 }
106 buf[0] = self.result.as_u8();
107 buf[1] = self.owner.as_u8();
108 let rest = self.current.encode(&mut buf[2..]).map_err(|e| match e {
109 ModulationEncodeError::BufferTooSmall => DeviceMessageEncodeError::BufferTooSmall,
110 ModulationEncodeError::SyncWordTooLong => DeviceMessageEncodeError::SyncWordTooLong,
111 })?;
112 Ok(2 + rest)
113 }
114
115 pub fn decode(buf: &[u8]) -> Result<Self, DeviceMessageParseError> {
116 if buf.len() < 2 {
117 return Err(DeviceMessageParseError::TooShort);
118 }
119 let result =
120 SetConfigResultCode::from_u8(buf[0]).ok_or(DeviceMessageParseError::InvalidField)?;
121 let owner = Owner::from_u8(buf[1]).ok_or(DeviceMessageParseError::InvalidField)?;
122 let current = Modulation::decode(&buf[2..]).map_err(DeviceMessageParseError::from)?;
123 Ok(Self {
124 result,
125 owner,
126 current,
127 })
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133#[cfg_attr(feature = "defmt", derive(defmt::Format))]
134pub enum OkPayload {
135 Empty,
137 Info(Info),
139 SetConfig(SetConfigResult),
141}
142
143impl OkPayload {
144 pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
145 match self {
146 Self::Empty => Ok(0),
147 Self::Info(info) => info.encode(buf).map_err(|e| match e {
148 InfoParseError::TooShort | InfoParseError::InvalidField => {
149 DeviceMessageEncodeError::InvalidField
150 }
151 InfoParseError::BufferTooSmall => DeviceMessageEncodeError::BufferTooSmall,
152 }),
153 Self::SetConfig(r) => r.encode(buf),
154 }
155 }
156
157 pub fn parse_for(cmd_type: u8, payload: &[u8]) -> Result<Self, DeviceMessageParseError> {
162 match cmd_type {
163 commands::TYPE_PING
164 | commands::TYPE_TX
165 | commands::TYPE_RX_START
166 | commands::TYPE_RX_STOP => {
167 if !payload.is_empty() {
168 return Err(DeviceMessageParseError::WrongLength);
169 }
170 Ok(Self::Empty)
171 }
172 commands::TYPE_GET_INFO => Info::decode(payload)
173 .map(Self::Info)
174 .map_err(DeviceMessageParseError::from),
175 commands::TYPE_SET_CONFIG => SetConfigResult::decode(payload).map(Self::SetConfig),
176 _ => Err(DeviceMessageParseError::UnknownContext),
177 }
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186#[cfg_attr(feature = "defmt", derive(defmt::Format))]
187#[repr(u8)]
188pub enum RxOrigin {
189 Ota = 0,
190 LocalLoopback = 1,
191}
192
193impl RxOrigin {
194 pub const fn as_u8(self) -> u8 {
195 self as u8
196 }
197 pub const fn from_u8(v: u8) -> Option<Self> {
198 Some(match v {
199 0 => Self::Ota,
200 1 => Self::LocalLoopback,
201 _ => return None,
202 })
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
209#[cfg_attr(feature = "defmt", derive(defmt::Format))]
210pub struct RxPayload {
211 pub rssi_tenths_dbm: i16,
213 pub snr_tenths_db: i16,
215 pub freq_err_hz: i32,
217 pub timestamp_us: u64,
219 pub crc_valid: bool,
221 pub packets_dropped: u16,
224 pub origin: RxOrigin,
225 pub data: HVec<u8, MAX_OTA_PAYLOAD>,
226}
227
228impl RxPayload {
229 pub const METADATA_SIZE: usize = 20;
231
232 pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
233 let total = Self::METADATA_SIZE + self.data.len();
234 if buf.len() < total {
235 return Err(DeviceMessageEncodeError::BufferTooSmall);
236 }
237 if self.data.len() > MAX_OTA_PAYLOAD {
238 return Err(DeviceMessageEncodeError::PayloadTooLarge);
239 }
240 buf[0..2].copy_from_slice(&self.rssi_tenths_dbm.to_le_bytes());
241 buf[2..4].copy_from_slice(&self.snr_tenths_db.to_le_bytes());
242 buf[4..8].copy_from_slice(&self.freq_err_hz.to_le_bytes());
243 buf[8..16].copy_from_slice(&self.timestamp_us.to_le_bytes());
244 buf[16] = u8::from(self.crc_valid);
245 buf[17..19].copy_from_slice(&self.packets_dropped.to_le_bytes());
246 buf[19] = self.origin.as_u8();
247 buf[20..total].copy_from_slice(&self.data);
248 Ok(total)
249 }
250
251 pub fn decode(buf: &[u8]) -> Result<Self, DeviceMessageParseError> {
252 if buf.len() < Self::METADATA_SIZE {
253 return Err(DeviceMessageParseError::TooShort);
254 }
255 let n = buf.len() - Self::METADATA_SIZE;
256 if n > MAX_OTA_PAYLOAD {
257 return Err(DeviceMessageParseError::WrongLength);
258 }
259 let rssi_tenths_dbm = i16::from_le_bytes([buf[0], buf[1]]);
260 let snr_tenths_db = i16::from_le_bytes([buf[2], buf[3]]);
261 let freq_err_hz = i32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
262 let timestamp_us = u64::from_le_bytes([
263 buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
264 ]);
265 let crc_valid = match buf[16] {
266 0 => false,
267 1 => true,
268 _ => return Err(DeviceMessageParseError::InvalidField),
269 };
270 let packets_dropped = u16::from_le_bytes([buf[17], buf[18]]);
271 let origin = RxOrigin::from_u8(buf[19]).ok_or(DeviceMessageParseError::InvalidField)?;
272 let mut data = HVec::new();
277 data.extend_from_slice(&buf[Self::METADATA_SIZE..Self::METADATA_SIZE + n])
278 .map_err(|_| DeviceMessageParseError::WrongLength)?;
279 Ok(Self {
280 rssi_tenths_dbm,
281 snr_tenths_db,
282 freq_err_hz,
283 timestamp_us,
284 crc_valid,
285 packets_dropped,
286 origin,
287 data,
288 })
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296#[cfg_attr(feature = "defmt", derive(defmt::Format))]
297#[repr(u8)]
298pub enum TxResult {
299 Transmitted = 0,
300 ChannelBusy = 1,
301 Cancelled = 2,
302}
303
304impl TxResult {
305 pub const fn as_u8(self) -> u8 {
306 self as u8
307 }
308 pub const fn from_u8(v: u8) -> Option<Self> {
309 Some(match v {
310 0 => Self::Transmitted,
311 1 => Self::ChannelBusy,
312 2 => Self::Cancelled,
313 _ => return None,
314 })
315 }
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320#[cfg_attr(feature = "defmt", derive(defmt::Format))]
321pub struct TxDonePayload {
322 pub result: TxResult,
323 pub airtime_us: u32,
326}
327
328impl TxDonePayload {
329 pub const WIRE_SIZE: usize = 5;
330
331 pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
332 if buf.len() < Self::WIRE_SIZE {
333 return Err(DeviceMessageEncodeError::BufferTooSmall);
334 }
335 buf[0] = self.result.as_u8();
336 buf[1..5].copy_from_slice(&self.airtime_us.to_le_bytes());
337 Ok(Self::WIRE_SIZE)
338 }
339
340 pub fn decode(buf: &[u8]) -> Result<Self, DeviceMessageParseError> {
341 if buf.len() != Self::WIRE_SIZE {
342 return Err(DeviceMessageParseError::WrongLength);
343 }
344 let result = TxResult::from_u8(buf[0]).ok_or(DeviceMessageParseError::InvalidField)?;
345 let airtime_us = u32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]);
346 Ok(Self { result, airtime_us })
347 }
348}
349
350pub fn encode_err_payload(
354 code: ErrorCode,
355 buf: &mut [u8],
356) -> Result<usize, DeviceMessageEncodeError> {
357 if buf.len() < 2 {
358 return Err(DeviceMessageEncodeError::BufferTooSmall);
359 }
360 buf[0..2].copy_from_slice(&code.as_u16().to_le_bytes());
361 Ok(2)
362}
363
364pub fn decode_err_payload(buf: &[u8]) -> Result<ErrorCode, DeviceMessageParseError> {
366 if buf.len() != 2 {
367 return Err(DeviceMessageParseError::WrongLength);
368 }
369 Ok(ErrorCode::from_u16(u16::from_le_bytes([buf[0], buf[1]])))
370}
371
372#[derive(Debug, Clone, PartialEq, Eq)]
376#[cfg_attr(feature = "defmt", derive(defmt::Format))]
377pub enum DeviceMessage {
378 Ok(OkPayload),
379 Err(ErrorCode),
380 Rx(RxPayload),
381 TxDone(TxDonePayload),
382}
383
384impl DeviceMessage {
385 pub const fn type_id(&self) -> u8 {
386 match self {
387 Self::Ok(_) => TYPE_OK,
388 Self::Err(_) => TYPE_ERR,
389 Self::Rx(_) => TYPE_RX,
390 Self::TxDone(_) => TYPE_TX_DONE,
391 }
392 }
393
394 pub fn encode_payload(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
397 match self {
398 Self::Ok(ok) => ok.encode(buf),
399 Self::Err(code) => encode_err_payload(*code, buf),
400 Self::Rx(rx) => rx.encode(buf),
401 Self::TxDone(td) => td.encode(buf),
402 }
403 }
404
405 pub fn parse(
412 type_id: u8,
413 payload: &[u8],
414 originating_cmd_type: Option<u8>,
415 ) -> Result<Self, DeviceMessageParseError> {
416 match type_id {
417 TYPE_OK => {
418 let cmd = originating_cmd_type.ok_or(DeviceMessageParseError::MissingContext)?;
419 Ok(Self::Ok(OkPayload::parse_for(cmd, payload)?))
420 }
421 TYPE_ERR => Ok(Self::Err(decode_err_payload(payload)?)),
422 TYPE_RX => Ok(Self::Rx(RxPayload::decode(payload)?)),
423 TYPE_TX_DONE => Ok(Self::TxDone(TxDonePayload::decode(payload)?)),
424 _ => Err(DeviceMessageParseError::UnknownType),
425 }
426 }
427}
428
429impl From<ModulationParseError> for DeviceMessageParseError {
432 fn from(e: ModulationParseError) -> Self {
433 match e {
434 ModulationParseError::WrongLength { .. } | ModulationParseError::TooShort => {
435 Self::WrongLength
436 }
437 ModulationParseError::InvalidField => Self::InvalidField,
438 ModulationParseError::UnknownModulation => Self::UnknownContext,
439 }
440 }
441}
442
443impl From<InfoParseError> for DeviceMessageParseError {
444 fn from(e: InfoParseError) -> Self {
445 match e {
446 InfoParseError::TooShort | InfoParseError::BufferTooSmall => Self::WrongLength,
447 InfoParseError::InvalidField => Self::InvalidField,
448 }
449 }
450}
451
452#[cfg(test)]
453#[allow(clippy::panic, clippy::unwrap_used)]
454mod tests {
455 use super::*;
456 use crate::{LoRaBandwidth, LoRaCodingRate, LoRaConfig, LoRaHeaderMode};
457
458 fn sample_lora() -> LoRaConfig {
459 LoRaConfig {
460 freq_hz: 868_100_000,
461 sf: 7,
462 bw: LoRaBandwidth::Khz125,
463 cr: LoRaCodingRate::Cr4_5,
464 preamble_len: 8,
465 sync_word: 0x1424,
466 tx_power_dbm: 14,
467 header_mode: LoRaHeaderMode::Explicit,
468 payload_crc: true,
469 iq_invert: false,
470 }
471 }
472
473 #[test]
474 fn type_ids_match_spec() {
475 assert_eq!(TYPE_OK, 0x80);
476 assert_eq!(TYPE_ERR, 0x81);
477 assert_eq!(TYPE_RX, 0xC0);
478 assert_eq!(TYPE_TX_DONE, 0xC1);
479 }
480
481 #[test]
482 fn ok_empty_roundtrip_for_ping() {
483 let ok = OkPayload::Empty;
484 let mut buf = [0u8; 4];
485 let n = ok.encode(&mut buf).unwrap();
486 assert_eq!(n, 0);
487 assert_eq!(
488 OkPayload::parse_for(commands::TYPE_PING, &buf[..n]).unwrap(),
489 ok
490 );
491 }
492
493 #[test]
494 fn ok_empty_rejects_nonempty_for_tx() {
495 assert!(matches!(
496 OkPayload::parse_for(commands::TYPE_TX, &[0]),
497 Err(DeviceMessageParseError::WrongLength)
498 ));
499 }
500
501 #[test]
502 fn ok_set_config_roundtrip() {
503 let r = SetConfigResult {
504 result: SetConfigResultCode::Applied,
505 owner: Owner::Mine,
506 current: Modulation::LoRa(sample_lora()),
507 };
508 let mut buf = [0u8; 64];
509 let n = r.encode(&mut buf).unwrap();
510 assert_eq!(n, 2 + 1 + 15);
512 assert_eq!(SetConfigResult::decode(&buf[..n]).unwrap(), r);
513 }
514
515 #[test]
516 fn ok_set_config_spec_bytes_c23() {
517 let r = SetConfigResult {
520 result: SetConfigResultCode::Applied,
521 owner: Owner::Mine,
522 current: Modulation::LoRa(sample_lora()),
523 };
524 let mut buf = [0u8; 64];
525 let n = r.encode(&mut buf).unwrap();
526 let expected: [u8; 18] = [
527 0x00, 0x01, 0x01, 0xA0, 0x27, 0xBE, 0x33, 0x07, 0x07, 0x00, 0x08, 0x00, 0x24, 0x14, 0x0E, 0x00, 0x01,
531 0x00,
532 ];
533 assert_eq!(&buf[..n], &expected);
534 }
535
536 #[test]
537 fn err_payload_roundtrip() {
538 let mut buf = [0u8; 2];
539 let n = encode_err_payload(ErrorCode::ENotConfigured, &mut buf).unwrap();
540 assert_eq!(n, 2);
541 assert_eq!(&buf, &[0x03, 0x00]);
542 let decoded = decode_err_payload(&buf).unwrap();
543 assert_eq!(decoded, ErrorCode::ENotConfigured);
544 }
545
546 #[test]
547 fn err_payload_preserves_unknown_codes() {
548 let mut buf = [0u8; 2];
549 encode_err_payload(ErrorCode::Unknown(0xABCD), &mut buf).unwrap();
550 let decoded = decode_err_payload(&buf).unwrap();
551 assert_eq!(decoded.as_u16(), 0xABCD);
552 }
553
554 #[test]
555 fn rx_payload_roundtrip_with_data() {
556 let mut data = HVec::new();
557 data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]).unwrap();
558 let rx = RxPayload {
559 rssi_tenths_dbm: -735,
560 snr_tenths_db: 95,
561 freq_err_hz: -125,
562 timestamp_us: 42_000_000,
563 crc_valid: true,
564 packets_dropped: 0,
565 origin: RxOrigin::Ota,
566 data,
567 };
568 let mut buf = [0u8; 64];
569 let n = rx.encode(&mut buf).unwrap();
570 assert_eq!(n, 20 + 4);
571 let decoded = RxPayload::decode(&buf[..n]).unwrap();
572 assert_eq!(decoded, rx);
573 }
574
575 #[test]
576 fn rx_payload_spec_bytes_c26() {
577 let mut data = HVec::new();
579 data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]).unwrap();
580 let rx = RxPayload {
581 rssi_tenths_dbm: -735,
582 snr_tenths_db: 95,
583 freq_err_hz: -125,
584 timestamp_us: 42_000_000,
585 crc_valid: true,
586 packets_dropped: 0,
587 origin: RxOrigin::Ota,
588 data,
589 };
590 let mut buf = [0u8; 64];
591 let n = rx.encode(&mut buf).unwrap();
592 let expected: [u8; 24] = [
593 0x21, 0xFD, 0x5F, 0x00, 0x83, 0xFF, 0xFF, 0xFF, 0x80, 0xDE, 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04,
601 ];
602 assert_eq!(&buf[..n], &expected);
603 }
604
605 #[test]
606 fn rx_payload_rejects_invalid_crc_byte() {
607 let mut buf = [0u8; 20];
608 buf[16] = 2; assert!(RxPayload::decode(&buf).is_err());
610 }
611
612 #[test]
613 fn tx_done_roundtrip() {
614 let td = TxDonePayload {
615 result: TxResult::Transmitted,
616 airtime_us: 30_976,
617 };
618 let mut buf = [0u8; 8];
619 let n = td.encode(&mut buf).unwrap();
620 assert_eq!(n, 5);
621 assert_eq!(TxDonePayload::decode(&buf[..n]).unwrap(), td);
622 }
623
624 #[test]
625 fn tx_done_spec_bytes_c24() {
626 let td = TxDonePayload {
628 result: TxResult::Transmitted,
629 airtime_us: 30_976,
630 };
631 let mut buf = [0u8; 5];
632 td.encode(&mut buf).unwrap();
633 assert_eq!(&buf, &[0x00, 0x00, 0x79, 0x00, 0x00]);
634 }
635
636 #[test]
637 fn device_message_parse_requires_ok_context() {
638 let mut buf = [0u8; 4];
639 let n = OkPayload::Empty.encode(&mut buf).unwrap();
640 assert!(matches!(
641 DeviceMessage::parse(TYPE_OK, &buf[..n], None),
642 Err(DeviceMessageParseError::MissingContext)
643 ));
644 assert!(matches!(
645 DeviceMessage::parse(TYPE_OK, &buf[..n], Some(commands::TYPE_PING)).unwrap(),
646 DeviceMessage::Ok(OkPayload::Empty)
647 ));
648 }
649
650 #[test]
651 fn device_message_unknown_type_rejects() {
652 assert!(matches!(
653 DeviceMessage::parse(0x55, &[], None),
654 Err(DeviceMessageParseError::UnknownType)
655 ));
656 }
657}