pamoja_gateway/udp.rs
1//! The Semtech packet forwarder protocol, both sides.
2//!
3//! A gateway and a network server exchange six kinds of UDP datagram, described by
4//! PROTOCOL.TXT in Semtech's `packet_forwarder`. Each begins with the same three bytes, a
5//! protocol version of 2 and a random token, then an identifier that says which kind it is:
6//!
7//! - The gateway sends PUSH_DATA with the packets it heard, and the server answers PUSH_ACK
8//! with the same token.
9//! - The gateway sends PULL_DATA to hold a route open through whatever network address
10//! translation sits in front of it, and the server answers PULL_ACK.
11//! - The server sends PULL_RESP with a packet to transmit, and the gateway answers TX_ACK
12//! saying whether it accepted it.
13//!
14//! The protocol has no authentication and no retries, which is why it belongs on a private
15//! network or behind a tunnel. [`Packet`] builds and parses every kind, and the objects they
16//! carry are in [`Rxpk`], [`Stat`], [`Txpk`], and [`TxStatus`].
17//!
18//! # Examples
19//!
20//! A server reads a forwarded packet and answers it.
21//!
22//! ```
23//! use pamoja_gateway::udp::{Eui, Packet, PacketKind, Rxpk, Uplink};
24//! use pamoja_lora::LinkSettings;
25//!
26//! let gateway = Eui::new([0xB8, 0x27, 0xEB, 0xFF, 0xFE, 0x01, 0x02, 0x03]);
27//! let datagram = Packet::PushData {
28//! token: 0x0102,
29//! gateway,
30//! uplink: Uplink::from(Rxpk::new(
31//! 868_100_000,
32//! LinkSettings::new(7, 125_000),
33//! b"hello".to_vec(),
34//! )),
35//! }
36//! .to_bytes();
37//!
38//! let packet = Packet::parse(&datagram)?;
39//! assert_eq!(packet.kind(), PacketKind::PushData);
40//! assert_eq!(packet.gateway(), Some(gateway));
41//! assert_eq!(packet.acknowledgment().map(|ack| ack.to_bytes()), Some(vec![2, 0x01, 0x02, 0x01]));
42//! # Ok::<(), pamoja_gateway::udp::ProtocolError>(())
43//! ```
44
45mod payload;
46
47use std::fmt;
48
49pub use payload::{CrcStatus, Modulation, Rxpk, Stat, TxStatus, Txpk, Uplink};
50
51/// The protocol version every datagram starts with.
52pub const PROTOCOL_VERSION: u8 = 2;
53
54/// The bytes before a datagram's payload: the version, the token, and the identifier.
55pub const HEADER_LEN: usize = 4;
56
57/// The length of a gateway's unique identifier.
58pub const EUI_LEN: usize = 8;
59
60/// Which kind of datagram, as its fourth byte says.
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
62pub enum PacketKind {
63 /// 0x00, the gateway forwarding what it heard.
64 PushData,
65 /// 0x01, the server acknowledging a PUSH_DATA.
66 PushAck,
67 /// 0x02, the gateway holding its route open.
68 PullData,
69 /// 0x03, the server sending a packet to transmit.
70 PullResp,
71 /// 0x04, the server acknowledging a PULL_DATA.
72 PullAck,
73 /// 0x05, the gateway reporting what became of a PULL_RESP.
74 TxAck,
75}
76
77impl PacketKind {
78 /// Returns the identifier byte.
79 ///
80 /// # Returns
81 ///
82 /// The value the datagram's fourth byte carries.
83 pub const fn identifier(self) -> u8 {
84 match self {
85 PacketKind::PushData => 0x00,
86 PacketKind::PushAck => 0x01,
87 PacketKind::PullData => 0x02,
88 PacketKind::PullResp => 0x03,
89 PacketKind::PullAck => 0x04,
90 PacketKind::TxAck => 0x05,
91 }
92 }
93
94 /// Names the kind an identifier byte selects.
95 ///
96 /// # Arguments
97 ///
98 /// * `identifier` - the datagram's fourth byte.
99 ///
100 /// # Returns
101 ///
102 /// The kind, or `None` for a byte the protocol does not define.
103 pub const fn from_identifier(identifier: u8) -> Option<PacketKind> {
104 match identifier {
105 0x00 => Some(PacketKind::PushData),
106 0x01 => Some(PacketKind::PushAck),
107 0x02 => Some(PacketKind::PullData),
108 0x03 => Some(PacketKind::PullResp),
109 0x04 => Some(PacketKind::PullAck),
110 0x05 => Some(PacketKind::TxAck),
111 _ => None,
112 }
113 }
114
115 /// Reports whether this kind carries the gateway's identifier.
116 ///
117 /// # Returns
118 ///
119 /// `true` for PUSH_DATA, PULL_DATA, and TX_ACK.
120 pub const fn carries_gateway(self) -> bool {
121 matches!(
122 self,
123 PacketKind::PushData | PacketKind::PullData | PacketKind::TxAck
124 )
125 }
126}
127
128/// A gateway's unique identifier, the eight bytes it puts in every datagram it sends.
129///
130/// It is written from the host's MAC address, usually with `FF FE` in the middle, which is
131/// why a Raspberry Pi gateway's identifier starts with the three bytes of its network
132/// interface.
133///
134/// # Examples
135///
136/// ```
137/// use pamoja_gateway::udp::Eui;
138///
139/// let gateway = Eui::from_hex("b827ebfffe010203").expect("sixteen hex digits");
140/// assert_eq!(gateway.to_hex(), "b827ebfffe010203");
141/// assert_eq!(gateway.bytes()[0], 0xB8);
142/// ```
143#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
144pub struct Eui([u8; EUI_LEN]);
145
146impl Eui {
147 /// Takes the eight bytes as they go on the wire.
148 ///
149 /// # Arguments
150 ///
151 /// * `bytes` - the identifier, most significant byte first.
152 ///
153 /// # Returns
154 ///
155 /// The identifier.
156 pub const fn new(bytes: [u8; EUI_LEN]) -> Eui {
157 Eui(bytes)
158 }
159
160 /// Returns the bytes.
161 ///
162 /// # Returns
163 ///
164 /// The identifier as it goes on the wire.
165 pub const fn bytes(&self) -> [u8; EUI_LEN] {
166 self.0
167 }
168
169 /// Reads an identifier written as sixteen hexadecimal digits.
170 ///
171 /// # Arguments
172 ///
173 /// * `text` - the digits, in either case, with no separators.
174 ///
175 /// # Returns
176 ///
177 /// The identifier, or `None` when the text is not sixteen hexadecimal digits.
178 pub fn from_hex(text: &str) -> Option<Eui> {
179 if text.len() != EUI_LEN * 2 {
180 return None;
181 }
182 let mut bytes = [0u8; EUI_LEN];
183 for (index, byte) in bytes.iter_mut().enumerate() {
184 *byte = u8::from_str_radix(text.get(index * 2..index * 2 + 2)?, 16).ok()?;
185 }
186 Some(Eui(bytes))
187 }
188
189 /// Writes the identifier as sixteen lowercase hexadecimal digits.
190 ///
191 /// # Returns
192 ///
193 /// The digits, which is how a network server's console shows a gateway.
194 pub fn to_hex(&self) -> String {
195 self.0.iter().map(|byte| format!("{byte:02x}")).collect()
196 }
197}
198
199impl fmt::Display for Eui {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 f.write_str(&self.to_hex())
202 }
203}
204
205/// Why a datagram is not one this protocol defines.
206#[derive(Clone, Debug, PartialEq, Eq)]
207pub enum ProtocolError {
208 /// A datagram shorter than the four bytes every kind starts with, with its length.
209 Short(usize),
210 /// A protocol version this crate does not speak.
211 Version(u8),
212 /// An identifier byte the protocol does not define.
213 Identifier(u8),
214 /// A datagram too short for the fields its kind carries.
215 Truncated {
216 /// The kind its identifier named.
217 kind: PacketKind,
218 /// How many bytes it holds.
219 len: usize,
220 },
221 /// A payload that is not the JSON object the protocol describes, and why.
222 Payload(String),
223}
224
225impl fmt::Display for ProtocolError {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 match self {
228 ProtocolError::Short(len) => {
229 write!(f, "a datagram of {len} bytes is shorter than the header")
230 }
231 ProtocolError::Version(version) => {
232 write!(f, "protocol version {version} is not {PROTOCOL_VERSION}")
233 }
234 ProtocolError::Identifier(identifier) => {
235 write!(f, "{identifier:#04x} is not a packet identifier")
236 }
237 ProtocolError::Truncated { kind, len } => {
238 write!(
239 f,
240 "a {kind:?} of {len} bytes is missing fields it must carry"
241 )
242 }
243 ProtocolError::Payload(why) => write!(f, "{why}"),
244 }
245 }
246}
247
248impl std::error::Error for ProtocolError {}
249
250/// One datagram of the protocol.
251///
252/// [`to_bytes`](Packet::to_bytes) writes it and [`parse`](Packet::parse) reads one, so the
253/// same type serves a gateway and a server.
254#[derive(Clone, Debug, PartialEq)]
255pub enum Packet {
256 /// What the gateway heard, and how its own radio is doing.
257 PushData {
258 /// The random token the acknowledgment carries back.
259 token: u16,
260 /// The gateway's identifier.
261 gateway: Eui,
262 /// The packets and the status report.
263 uplink: Uplink,
264 },
265 /// The server acknowledging a PUSH_DATA, which says only that it arrived.
266 PushAck {
267 /// The token of the PUSH_DATA being acknowledged.
268 token: u16,
269 },
270 /// The gateway holding a route open for downlinks.
271 PullData {
272 /// The random token the acknowledgment carries back.
273 token: u16,
274 /// The gateway's identifier.
275 gateway: Eui,
276 },
277 /// The server acknowledging a PULL_DATA.
278 PullAck {
279 /// The token of the PULL_DATA being acknowledged.
280 token: u16,
281 },
282 /// The server asking the gateway to transmit a packet.
283 PullResp {
284 /// The random token the TX_ACK carries back.
285 token: u16,
286 /// What to transmit, and when.
287 transmit: Txpk,
288 },
289 /// The gateway reporting what became of a PULL_RESP.
290 TxAck {
291 /// The token of the PULL_RESP being answered.
292 token: u16,
293 /// The gateway's identifier.
294 gateway: Eui,
295 /// Whether it was scheduled, or why it was refused.
296 status: TxStatus,
297 },
298}
299
300impl Packet {
301 /// Returns which kind of datagram this is.
302 ///
303 /// # Returns
304 ///
305 /// The kind.
306 pub const fn kind(&self) -> PacketKind {
307 match self {
308 Packet::PushData { .. } => PacketKind::PushData,
309 Packet::PushAck { .. } => PacketKind::PushAck,
310 Packet::PullData { .. } => PacketKind::PullData,
311 Packet::PullAck { .. } => PacketKind::PullAck,
312 Packet::PullResp { .. } => PacketKind::PullResp,
313 Packet::TxAck { .. } => PacketKind::TxAck,
314 }
315 }
316
317 /// Returns the token, which pairs a datagram with its answer.
318 ///
319 /// # Returns
320 ///
321 /// The token.
322 pub const fn token(&self) -> u16 {
323 match self {
324 Packet::PushData { token, .. }
325 | Packet::PushAck { token }
326 | Packet::PullData { token, .. }
327 | Packet::PullAck { token }
328 | Packet::PullResp { token, .. }
329 | Packet::TxAck { token, .. } => *token,
330 }
331 }
332
333 /// Returns the gateway's identifier, for the kinds that carry it.
334 ///
335 /// # Returns
336 ///
337 /// The identifier, or `None` for the datagrams a server sends, which do not name one.
338 pub const fn gateway(&self) -> Option<Eui> {
339 match self {
340 Packet::PushData { gateway, .. }
341 | Packet::PullData { gateway, .. }
342 | Packet::TxAck { gateway, .. } => Some(*gateway),
343 _ => None,
344 }
345 }
346
347 /// Returns the acknowledgment a server owes this datagram.
348 ///
349 /// # Returns
350 ///
351 /// The PUSH_ACK or PULL_ACK to send back, or `None` for a datagram that is itself an
352 /// answer. A PULL_RESP is answered with a TX_ACK, which names the gateway and what
353 /// became of the transmission, so the gateway builds that one itself.
354 pub fn acknowledgment(&self) -> Option<Packet> {
355 match self {
356 Packet::PushData { token, .. } => Some(Packet::PushAck { token: *token }),
357 Packet::PullData { token, .. } => Some(Packet::PullAck { token: *token }),
358 _ => None,
359 }
360 }
361
362 /// Writes the datagram.
363 ///
364 /// # Returns
365 ///
366 /// The bytes to send.
367 pub fn to_bytes(&self) -> Vec<u8> {
368 let mut out = Vec::with_capacity(HEADER_LEN + EUI_LEN);
369 out.push(PROTOCOL_VERSION);
370 out.extend_from_slice(&self.token().to_be_bytes());
371 out.push(self.kind().identifier());
372 if let Some(gateway) = self.gateway() {
373 out.extend_from_slice(&gateway.bytes());
374 }
375 match self {
376 Packet::PushData { uplink, .. } => out.extend_from_slice(uplink.to_json().as_bytes()),
377 Packet::PullResp { transmit, .. } => {
378 out.extend_from_slice(transmit.to_json().as_bytes());
379 }
380 Packet::TxAck { status, .. } => out.extend_from_slice(status.to_json().as_bytes()),
381 _ => {}
382 }
383 out
384 }
385
386 /// Reads a datagram.
387 ///
388 /// # Arguments
389 ///
390 /// * `bytes` - the datagram as it arrived.
391 ///
392 /// # Returns
393 ///
394 /// The packet.
395 ///
396 /// # Errors
397 ///
398 /// Returns [`ProtocolError`] for a datagram shorter than its header, another protocol
399 /// version, an identifier the protocol does not define, a datagram missing the fields its
400 /// kind carries, or a payload that is not the JSON object described for it.
401 pub fn parse(bytes: &[u8]) -> Result<Packet, ProtocolError> {
402 if bytes.len() < HEADER_LEN {
403 return Err(ProtocolError::Short(bytes.len()));
404 }
405 if bytes[0] != PROTOCOL_VERSION {
406 return Err(ProtocolError::Version(bytes[0]));
407 }
408 let token = u16::from_be_bytes([bytes[1], bytes[2]]);
409 let kind =
410 PacketKind::from_identifier(bytes[3]).ok_or(ProtocolError::Identifier(bytes[3]))?;
411
412 let body = &bytes[HEADER_LEN..];
413 let (gateway, body) = if kind.carries_gateway() {
414 if body.len() < EUI_LEN {
415 return Err(ProtocolError::Truncated {
416 kind,
417 len: bytes.len(),
418 });
419 }
420 let mut identifier = [0u8; EUI_LEN];
421 identifier.copy_from_slice(&body[..EUI_LEN]);
422 (Eui(identifier), &body[EUI_LEN..])
423 } else {
424 (Eui::default(), body)
425 };
426
427 Ok(match kind {
428 PacketKind::PushData => Packet::PushData {
429 token,
430 gateway,
431 uplink: Uplink::from_json(body)?,
432 },
433 PacketKind::PushAck => Packet::PushAck { token },
434 PacketKind::PullData => Packet::PullData { token, gateway },
435 PacketKind::PullAck => Packet::PullAck { token },
436 PacketKind::PullResp => Packet::PullResp {
437 token,
438 transmit: Txpk::from_json(body)?,
439 },
440 PacketKind::TxAck => Packet::TxAck {
441 token,
442 gateway,
443 status: TxStatus::from_json(body)?,
444 },
445 })
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use pamoja_lora::LinkSettings;
453
454 fn gateway() -> Eui {
455 Eui::new([0xB8, 0x27, 0xEB, 0xFF, 0xFE, 0x01, 0x02, 0x03])
456 }
457
458 fn heard() -> Rxpk {
459 Rxpk::new(
460 868_100_000,
461 LinkSettings::new(7, 125_000),
462 b"hello".to_vec(),
463 )
464 }
465
466 #[test]
467 fn every_identifier_is_the_one_the_protocol_gives() {
468 for (kind, identifier) in [
469 (PacketKind::PushData, 0x00),
470 (PacketKind::PushAck, 0x01),
471 (PacketKind::PullData, 0x02),
472 (PacketKind::PullResp, 0x03),
473 (PacketKind::PullAck, 0x04),
474 (PacketKind::TxAck, 0x05),
475 ] {
476 assert_eq!(kind.identifier(), identifier);
477 assert_eq!(PacketKind::from_identifier(identifier), Some(kind));
478 }
479 assert_eq!(PacketKind::from_identifier(0x06), None);
480 }
481
482 #[test]
483 fn a_push_data_carries_its_header_then_its_json() {
484 let datagram = Packet::PushData {
485 token: 0x1234,
486 gateway: gateway(),
487 uplink: Uplink::from(heard()),
488 }
489 .to_bytes();
490
491 assert_eq!(datagram[0], PROTOCOL_VERSION);
492 assert_eq!(&datagram[1..3], [0x12, 0x34]);
493 assert_eq!(datagram[3], 0x00);
494 assert_eq!(&datagram[4..12], gateway().bytes());
495 assert!(datagram[12..].starts_with(b"{\"rxpk\":["));
496 assert_eq!(Packet::parse(&datagram).expect("it parses").token(), 0x1234);
497 }
498
499 #[test]
500 fn the_acknowledgments_are_four_bytes_with_the_same_token() {
501 let push = Packet::PushData {
502 token: 0x0102,
503 gateway: gateway(),
504 uplink: Uplink::from(heard()),
505 };
506 let pull = Packet::PullData {
507 token: 0x0304,
508 gateway: gateway(),
509 };
510
511 assert_eq!(
512 push.acknowledgment()
513 .expect("a push is answered")
514 .to_bytes(),
515 [2, 0x01, 0x02, 0x01]
516 );
517 assert_eq!(
518 pull.acknowledgment()
519 .expect("a pull is answered")
520 .to_bytes(),
521 [2, 0x03, 0x04, 0x04]
522 );
523 assert_eq!(Packet::PushAck { token: 1 }.acknowledgment(), None);
524 }
525
526 #[test]
527 fn a_pull_data_is_twelve_bytes_and_a_tx_ack_carries_its_status() {
528 let pull = Packet::PullData {
529 token: 0xABCD,
530 gateway: gateway(),
531 };
532 let bytes = pull.to_bytes();
533 assert_eq!(bytes.len(), 12);
534 assert_eq!(Packet::parse(&bytes), Ok(pull));
535
536 let ack = Packet::TxAck {
537 token: 0xABCD,
538 gateway: gateway(),
539 status: TxStatus::CollisionPacket,
540 };
541 let bytes = ack.to_bytes();
542 assert!(bytes[12..].starts_with(b"{\"txpk_ack\":"));
543 assert_eq!(Packet::parse(&bytes), Ok(ack));
544 }
545
546 #[test]
547 fn a_tx_ack_without_a_payload_reports_no_error() {
548 let bytes = [vec![2, 0x00, 0x01, 0x05], gateway().bytes().to_vec()].concat();
549 assert_eq!(
550 Packet::parse(&bytes),
551 Ok(Packet::TxAck {
552 token: 1,
553 gateway: gateway(),
554 status: TxStatus::None,
555 })
556 );
557 }
558
559 #[test]
560 fn a_datagram_that_is_not_this_protocol_is_refused() {
561 assert_eq!(Packet::parse(&[2, 0, 1]), Err(ProtocolError::Short(3)));
562 assert_eq!(Packet::parse(&[1, 0, 1, 0]), Err(ProtocolError::Version(1)));
563 assert_eq!(
564 Packet::parse(&[2, 0, 1, 0x09]),
565 Err(ProtocolError::Identifier(0x09))
566 );
567 assert_eq!(
568 Packet::parse(&[2, 0, 1, 0x02, 0xB8]),
569 Err(ProtocolError::Truncated {
570 kind: PacketKind::PullData,
571 len: 5,
572 })
573 );
574 assert!(matches!(
575 Packet::parse(
576 &[
577 vec![2, 0, 1, 0x00],
578 gateway().bytes().to_vec(),
579 b"{}".to_vec()
580 ]
581 .concat()
582 ),
583 Err(ProtocolError::Payload(_))
584 ));
585 }
586
587 #[test]
588 fn an_eui_reads_and_writes_as_hex() {
589 assert_eq!(Eui::from_hex("b827ebfffe010203"), Some(gateway()));
590 assert_eq!(Eui::from_hex("B827EBFFFE010203"), Some(gateway()));
591 assert_eq!(gateway().to_string(), "b827ebfffe010203");
592 assert_eq!(Eui::from_hex("b827ebfffe0102"), None);
593 assert_eq!(Eui::from_hex("b827ebfffe01020g"), None);
594 }
595}