Skip to main content

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}