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