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 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}