1pub const MAX_PAYLOAD: usize = 256;
8
9pub const RADIO_CONFIG_SIZE: usize = 13;
11
12pub const TX_POWER_MAX: i8 = i8::MIN; pub const PREAMBLE_DEFAULT: u16 = 0;
17
18pub const CMD_TAG_SET_CONFIG: u8 = 2;
22pub const CMD_TAG_START_RX: u8 = 3;
24pub const CMD_TAG_STOP_RX: u8 = 4;
26
27pub const RESP_TAG_RX_PACKET: u8 = 2;
29pub const RESP_TAG_OK: u8 = 4;
31pub const RESP_TAG_ERROR: u8 = 5;
33
34pub const ERROR_INVALID_CONFIG: u8 = 0;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[repr(u8)]
42pub enum Bandwidth {
43 Khz7 = 0,
44 Khz10 = 1,
45 Khz15 = 2,
46 Khz20 = 3,
47 Khz31 = 4,
48 Khz41 = 5,
49 Khz62 = 6,
50 Khz125 = 7,
51 Khz250 = 8,
52 Khz500 = 9,
53}
54
55impl Bandwidth {
56 pub fn from_u8(v: u8) -> Option<Self> {
58 match v {
59 0 => Some(Self::Khz7),
60 1 => Some(Self::Khz10),
61 2 => Some(Self::Khz15),
62 3 => Some(Self::Khz20),
63 4 => Some(Self::Khz31),
64 5 => Some(Self::Khz41),
65 6 => Some(Self::Khz62),
66 7 => Some(Self::Khz125),
67 8 => Some(Self::Khz250),
68 9 => Some(Self::Khz500),
69 _ => None,
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct RadioConfig {
84 pub freq_hz: u32,
86 pub bw: Bandwidth,
87 pub sf: u8,
89 pub cr: u8,
91 pub sync_word: u16,
92 pub tx_power_dbm: i8,
94 pub preamble_len: u16,
96 pub cad: u8,
98}
99
100impl RadioConfig {
101 pub fn to_bytes(&self) -> [u8; RADIO_CONFIG_SIZE] {
103 let mut buf = [0u8; RADIO_CONFIG_SIZE];
104 buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
105 buf[4] = self.bw as u8;
106 buf[5] = self.sf;
107 buf[6] = self.cr;
108 buf[7..9].copy_from_slice(&self.sync_word.to_le_bytes());
109 buf[9] = self.tx_power_dbm as u8;
110 buf[10..12].copy_from_slice(&self.preamble_len.to_le_bytes());
111 buf[12] = self.cad;
112 buf
113 }
114
115 pub fn from_bytes(buf: &[u8]) -> Option<Self> {
117 if buf.len() < RADIO_CONFIG_SIZE {
118 return None;
119 }
120 Some(Self {
121 freq_hz: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
122 bw: Bandwidth::from_u8(buf[4])?,
123 sf: buf[5],
124 cr: buf[6],
125 sync_word: u16::from_le_bytes([buf[7], buf[8]]),
126 tx_power_dbm: buf[9] as i8,
127 preamble_len: u16::from_le_bytes([buf[10], buf[11]]),
128 cad: buf[12],
129 })
130 }
131}
132
133impl Default for RadioConfig {
135 fn default() -> Self {
136 Self {
137 freq_hz: 915_000_000,
138 bw: Bandwidth::Khz125,
139 sf: 7,
140 cr: 5,
141 sync_word: 0x1424,
142 tx_power_dbm: TX_POWER_MAX,
143 preamble_len: PREAMBLE_DEFAULT,
144 cad: 1,
145 }
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
153pub enum Command {
154 Ping,
155 GetConfig,
156 SetConfig(RadioConfig),
157 StartRx,
158 StopRx,
159 Transmit { config: Option<RadioConfig>, payload: Vec<u8> },
160 DisplayOn,
161 DisplayOff,
162 GetMac,
163}
164
165impl Command {
166 pub fn tag(&self) -> u8 {
168 match self {
169 Self::Ping => 0,
170 Self::GetConfig => 1,
171 Self::SetConfig(_) => 2,
172 Self::StartRx => 3,
173 Self::StopRx => 4,
174 Self::Transmit { .. } => 5,
175 Self::DisplayOn => 6,
176 Self::DisplayOff => 7,
177 Self::GetMac => 8,
178 }
179 }
180
181 pub fn to_bytes(&self) -> Vec<u8> {
183 match self {
184 Self::Ping => vec![0],
185 Self::GetConfig => vec![1],
186 Self::SetConfig(cfg) => {
187 let mut out = vec![2];
188 out.extend_from_slice(&cfg.to_bytes());
189 out
190 }
191 Self::StartRx => vec![3],
192 Self::StopRx => vec![4],
193 Self::Transmit { config, payload } => {
194 let mut out = vec![5];
195 match config {
196 None => out.push(0),
197 Some(cfg) => {
198 out.push(1);
199 out.extend_from_slice(&cfg.to_bytes());
200 }
201 }
202 out.extend_from_slice(&(payload.len() as u16).to_le_bytes());
203 out.extend_from_slice(payload);
204 out
205 }
206 Self::DisplayOn => vec![6],
207 Self::DisplayOff => vec![7],
208 Self::GetMac => vec![8],
209 }
210 }
211
212 pub fn from_bytes(buf: &[u8]) -> Option<Self> {
214 let tag = *buf.first()?;
215 let rest = &buf[1..];
216 match tag {
217 0 => Some(Self::Ping),
218 1 => Some(Self::GetConfig),
219 2 => Some(Self::SetConfig(RadioConfig::from_bytes(rest)?)),
220 3 => Some(Self::StartRx),
221 4 => Some(Self::StopRx),
222 5 => {
223 if rest.is_empty() {
224 return None;
225 }
226 let (config, pos) = if rest[0] == 0 {
227 (None, 1)
228 } else if rest[0] == 1 && rest.len() > RADIO_CONFIG_SIZE {
229 (Some(RadioConfig::from_bytes(&rest[1..])?), 1 + RADIO_CONFIG_SIZE)
230 } else {
231 return None;
232 };
233 if rest.len() < pos + 2 {
234 return None;
235 }
236 let len = u16::from_le_bytes([rest[pos], rest[pos + 1]]) as usize;
237 let data_start = pos + 2;
238 if rest.len() < data_start + len || len > MAX_PAYLOAD {
239 return None;
240 }
241 Some(Self::Transmit { config, payload: rest[data_start..data_start + len].to_vec() })
242 }
243 6 => Some(Self::DisplayOn),
244 7 => Some(Self::DisplayOff),
245 8 => Some(Self::GetMac),
246 _ => None,
247 }
248 }
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255#[repr(u8)]
256pub enum ErrorCode {
257 InvalidConfig = 0,
258 RadioBusy = 1,
259 TxTimeout = 2,
260 CrcError = 3,
261 NotConfigured = 4,
262 NoDisplay = 5,
263}
264
265impl ErrorCode {
266 pub fn from_u8(v: u8) -> Option<Self> {
268 match v {
269 0 => Some(Self::InvalidConfig),
270 1 => Some(Self::RadioBusy),
271 2 => Some(Self::TxTimeout),
272 3 => Some(Self::CrcError),
273 4 => Some(Self::NotConfigured),
274 5 => Some(Self::NoDisplay),
275 _ => None,
276 }
277 }
278}
279
280impl std::error::Error for ErrorCode {}
281
282impl std::fmt::Display for ErrorCode {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 match self {
285 Self::InvalidConfig => write!(f, "InvalidConfig"),
286 Self::RadioBusy => write!(f, "RadioBusy"),
287 Self::TxTimeout => write!(f, "TxTimeout"),
288 Self::CrcError => write!(f, "CrcError"),
289 Self::NotConfigured => write!(f, "NotConfigured"),
290 Self::NoDisplay => write!(f, "NoDisplay"),
291 }
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
299pub enum Response {
300 Pong,
301 Config(RadioConfig),
302 RxPacket { rssi: i16, snr: i16, payload: Vec<u8> },
303 TxDone,
304 Ok,
305 Error(ErrorCode),
306 MacAddress([u8; 6]),
307}
308
309impl Response {
310 pub fn tag(&self) -> u8 {
312 match self {
313 Self::Pong => 0,
314 Self::Config(_) => 1,
315 Self::RxPacket { .. } => 2,
316 Self::TxDone => 3,
317 Self::Ok => 4,
318 Self::Error(_) => 5,
319 Self::MacAddress(_) => 6,
320 }
321 }
322
323 pub fn is_rx_packet(&self) -> bool {
325 matches!(self, Self::RxPacket { .. })
326 }
327
328 pub fn to_bytes(&self) -> Vec<u8> {
330 match self {
331 Self::Pong => vec![0],
332 Self::Config(cfg) => {
333 let mut out = vec![1];
334 out.extend_from_slice(&cfg.to_bytes());
335 out
336 }
337 Self::RxPacket { rssi, snr, payload } => {
338 let mut out = vec![2];
339 out.extend_from_slice(&rssi.to_le_bytes());
340 out.extend_from_slice(&snr.to_le_bytes());
341 out.extend_from_slice(&(payload.len() as u16).to_le_bytes());
342 out.extend_from_slice(payload);
343 out
344 }
345 Self::TxDone => vec![3],
346 Self::Ok => vec![4],
347 Self::Error(code) => vec![5, *code as u8],
348 Self::MacAddress(mac) => {
349 let mut out = vec![6];
350 out.extend_from_slice(mac);
351 out
352 }
353 }
354 }
355
356 pub fn from_bytes(buf: &[u8]) -> Option<Self> {
358 let tag = *buf.first()?;
359 let rest = &buf[1..];
360 match tag {
361 0 => Some(Self::Pong),
362 1 => Some(Self::Config(RadioConfig::from_bytes(rest)?)),
363 2 => {
364 if rest.len() < 6 {
365 return None;
366 }
367 let rssi = i16::from_le_bytes([rest[0], rest[1]]);
368 let snr = i16::from_le_bytes([rest[2], rest[3]]);
369 let len = u16::from_le_bytes([rest[4], rest[5]]) as usize;
370 if rest.len() < 6 + len || len > MAX_PAYLOAD {
371 return None;
372 }
373 Some(Self::RxPacket { rssi, snr, payload: rest[6..6 + len].to_vec() })
374 }
375 3 => Some(Self::TxDone),
376 4 => Some(Self::Ok),
377 5 => Some(Self::Error(ErrorCode::from_u8(*rest.first()?)?)),
378 6 => {
379 if rest.len() < 6 {
380 return None;
381 }
382 let mut mac = [0u8; 6];
383 mac.copy_from_slice(&rest[..6]);
384 Some(Self::MacAddress(mac))
385 }
386 _ => None,
387 }
388 }
389}
390
391#[cfg(test)]
394#[allow(clippy::unwrap_used)]
395mod tests {
396 use super::*;
397
398 fn make_config() -> RadioConfig {
399 RadioConfig {
400 freq_hz: 915_000_000,
401 bw: Bandwidth::Khz125,
402 sf: 7,
403 cr: 5,
404 sync_word: 0x3444,
405 tx_power_dbm: 22,
406 preamble_len: 16,
407 cad: 1,
408 }
409 }
410
411 #[test]
414 fn radio_config_roundtrip() {
415 let cfg = make_config();
416 let bytes = cfg.to_bytes();
417 assert_eq!(bytes.len(), RADIO_CONFIG_SIZE);
418 assert_eq!(RadioConfig::from_bytes(&bytes), Some(cfg));
419 }
420
421 #[test]
422 fn radio_config_default_roundtrip() {
423 let cfg = RadioConfig::default();
424 let bytes = cfg.to_bytes();
425 assert_eq!(RadioConfig::from_bytes(&bytes), Some(cfg));
426 }
427
428 #[test]
429 fn radio_config_all_bandwidths() {
430 for bw_val in 0u8..=9 {
431 let bw = Bandwidth::from_u8(bw_val);
432 assert!(bw.is_some(), "bandwidth {bw_val} should be valid");
433 let cfg = RadioConfig { bw: bw.unwrap(), ..make_config() };
434 let bytes = cfg.to_bytes();
435 assert_eq!(RadioConfig::from_bytes(&bytes), Some(cfg));
436 }
437 }
438
439 #[test]
440 fn radio_config_invalid_bandwidth() {
441 let mut buf = make_config().to_bytes();
442 buf[4] = 255;
443 assert!(RadioConfig::from_bytes(&buf).is_none());
444 }
445
446 #[test]
447 fn radio_config_negative_power() {
448 let cfg = RadioConfig { tx_power_dbm: TX_POWER_MAX, ..make_config() };
449 let bytes = cfg.to_bytes();
450 assert_eq!(RadioConfig::from_bytes(&bytes), Some(cfg));
451 }
452
453 #[test]
454 fn radio_config_short_buffer() {
455 assert!(RadioConfig::from_bytes(&[0u8; 12]).is_none());
456 assert!(RadioConfig::from_bytes(&[]).is_none());
457 }
458
459 #[test]
462 fn command_simple_roundtrips() {
463 for cmd in [
464 Command::Ping,
465 Command::GetConfig,
466 Command::StartRx,
467 Command::StopRx,
468 Command::DisplayOn,
469 Command::DisplayOff,
470 Command::GetMac,
471 ] {
472 let bytes = cmd.to_bytes();
473 assert_eq!(Command::from_bytes(&bytes), Some(cmd));
474 }
475 }
476
477 #[test]
478 fn command_set_config_roundtrip() {
479 let cmd = Command::SetConfig(make_config());
480 let bytes = cmd.to_bytes();
481 assert_eq!(Command::from_bytes(&bytes), Some(cmd));
482 }
483
484 #[test]
485 fn command_transmit_no_config_roundtrip() {
486 let cmd = Command::Transmit { config: None, payload: b"hello".to_vec() };
487 let bytes = cmd.to_bytes();
488 assert_eq!(Command::from_bytes(&bytes), Some(cmd));
489 }
490
491 #[test]
492 fn command_transmit_with_config_roundtrip() {
493 let cmd = Command::Transmit { config: Some(make_config()), payload: b"test".to_vec() };
494 let bytes = cmd.to_bytes();
495 assert_eq!(Command::from_bytes(&bytes), Some(cmd));
496 }
497
498 #[test]
499 fn command_transmit_empty_payload() {
500 let cmd = Command::Transmit { config: None, payload: vec![] };
501 let bytes = cmd.to_bytes();
502 assert_eq!(Command::from_bytes(&bytes), Some(cmd));
503 }
504
505 #[test]
506 fn command_transmit_truncated() {
507 assert!(Command::from_bytes(&[5]).is_none()); assert!(Command::from_bytes(&[5, 1]).is_none()); assert!(Command::from_bytes(&[5, 0]).is_none()); }
511
512 #[test]
513 fn command_invalid_tag() {
514 assert!(Command::from_bytes(&[9]).is_none());
515 assert!(Command::from_bytes(&[255]).is_none());
516 }
517
518 #[test]
519 fn command_empty_buffer() {
520 assert!(Command::from_bytes(&[]).is_none());
521 }
522
523 #[test]
524 fn command_tags() {
525 assert_eq!(Command::Ping.tag(), 0);
526 assert_eq!(Command::GetConfig.tag(), 1);
527 assert_eq!(Command::SetConfig(make_config()).tag(), 2);
528 assert_eq!(Command::StartRx.tag(), 3);
529 assert_eq!(Command::StopRx.tag(), 4);
530 assert_eq!(Command::Transmit { config: None, payload: vec![] }.tag(), 5);
531 assert_eq!(Command::DisplayOn.tag(), 6);
532 assert_eq!(Command::DisplayOff.tag(), 7);
533 assert_eq!(Command::GetMac.tag(), 8);
534 }
535
536 #[test]
539 fn response_simple_roundtrips() {
540 for resp in [Response::Pong, Response::TxDone, Response::Ok] {
541 let bytes = resp.to_bytes();
542 assert_eq!(Response::from_bytes(&bytes), Some(resp));
543 }
544 }
545
546 #[test]
547 fn response_config_roundtrip() {
548 let resp = Response::Config(make_config());
549 let bytes = resp.to_bytes();
550 assert_eq!(Response::from_bytes(&bytes), Some(resp));
551 }
552
553 #[test]
554 fn response_rx_packet_roundtrip() {
555 let resp = Response::RxPacket { rssi: -80, snr: 10, payload: b"data".to_vec() };
556 let bytes = resp.to_bytes();
557 assert_eq!(Response::from_bytes(&bytes), Some(resp));
558 }
559
560 #[test]
561 fn response_rx_packet_empty_payload() {
562 let resp = Response::RxPacket { rssi: -120, snr: -5, payload: vec![] };
563 let bytes = resp.to_bytes();
564 assert_eq!(Response::from_bytes(&bytes), Some(resp));
565 }
566
567 #[test]
568 fn response_error_codes() {
569 for code in [
570 ErrorCode::InvalidConfig,
571 ErrorCode::RadioBusy,
572 ErrorCode::TxTimeout,
573 ErrorCode::CrcError,
574 ErrorCode::NotConfigured,
575 ErrorCode::NoDisplay,
576 ] {
577 let resp = Response::Error(code);
578 let bytes = resp.to_bytes();
579 assert_eq!(Response::from_bytes(&bytes), Some(resp));
580 }
581 }
582
583 #[test]
584 fn response_mac_address_roundtrip() {
585 let resp = Response::MacAddress([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
586 let bytes = resp.to_bytes();
587 assert_eq!(Response::from_bytes(&bytes), Some(resp));
588 }
589
590 #[test]
591 fn response_tags() {
592 assert_eq!(Response::Pong.tag(), 0);
593 assert_eq!(Response::Config(make_config()).tag(), 1);
594 assert_eq!(Response::RxPacket { rssi: 0, snr: 0, payload: vec![] }.tag(), 2);
595 assert_eq!(Response::TxDone.tag(), 3);
596 assert_eq!(Response::Ok.tag(), 4);
597 assert_eq!(Response::Error(ErrorCode::RadioBusy).tag(), 5);
598 assert_eq!(Response::MacAddress([0; 6]).tag(), 6);
599 }
600
601 #[test]
602 fn response_is_rx_packet() {
603 assert!(Response::RxPacket { rssi: 0, snr: 0, payload: vec![] }.is_rx_packet());
604 assert!(!Response::Ok.is_rx_packet());
605 assert!(!Response::Pong.is_rx_packet());
606 }
607
608 #[test]
609 fn response_invalid_tag() {
610 assert!(Response::from_bytes(&[7]).is_none());
611 assert!(Response::from_bytes(&[255]).is_none());
612 }
613
614 #[test]
615 fn response_truncated() {
616 assert!(Response::from_bytes(&[]).is_none());
617 assert!(Response::from_bytes(&[5]).is_none()); assert!(Response::from_bytes(&[2, 0, 0]).is_none()); assert!(Response::from_bytes(&[6, 0, 0, 0]).is_none()); }
621
622 #[test]
625 fn error_code_from_u8() {
626 for v in 0..=5 {
627 assert!(ErrorCode::from_u8(v).is_some());
628 }
629 assert!(ErrorCode::from_u8(6).is_none());
630 assert!(ErrorCode::from_u8(255).is_none());
631 }
632
633 #[test]
634 fn error_code_display() {
635 assert_eq!(ErrorCode::InvalidConfig.to_string(), "InvalidConfig");
636 assert_eq!(ErrorCode::RadioBusy.to_string(), "RadioBusy");
637 }
638
639 #[test]
642 fn firmware_worked_example() {
643 let cfg = RadioConfig {
645 freq_hz: 915_000_000,
646 bw: Bandwidth::Khz125,
647 sf: 7,
648 cr: 5,
649 sync_word: 0x1424,
650 tx_power_dbm: TX_POWER_MAX,
651 preamble_len: PREAMBLE_DEFAULT,
652 cad: 1,
653 };
654 let cmd = Command::SetConfig(cfg);
655 let bytes = cmd.to_bytes();
656 assert_eq!(bytes, [0x02, 0xC0, 0xCA, 0x89, 0x36, 0x07, 0x07, 0x05, 0x24, 0x14, 0x80, 0x00, 0x00, 0x01]);
658 }
659}