Skip to main content

pamoja_gateway/
station.rs

1//! The LoRa Basics Station protocol, both sides.
2//!
3//! The packet forwarder protocol in [`udp`](crate::udp) is a gateway shouting datagrams at a
4//! server that may or may not be listening. Basics Station replaces it with a conversation: the
5//! gateway asks a discovery endpoint where its network server is, opens a websocket there, says
6//! what it is, and is told how to configure its radios. After that the two exchange JSON
7//! messages, each naming its kind in a `msgtype` field.
8//!
9//! This module builds and reads those messages from either side, the way `udp` does. It opens
10//! no sockets: a station reaches its server through whatever the caller provides, and the
11//! `station-client` feature adds a live one.
12//!
13//! - [`Discovery`] and [`Router`] are the request and answer on `/router-info`, which hands
14//!   back the websocket address to connect to, or says why it will not.
15//! - [`Message`] is every message the websocket carries afterwards: what the station reports
16//!   about itself, the configuration it is given, the join requests and frames it heard, the
17//!   downlinks it is asked to transmit and what became of them, and the clock the two keep
18//!   between them.
19//!
20//! Identifiers cross as [`Eui`], and are written in the ID6 form the protocol prefers,
21//! which folds a run of zero groups the way an IPv6 address does.
22//!
23//! # Examples
24//!
25//! A station asks where its network server is, and reads the answer.
26//!
27//! ```
28//! use pamoja_gateway::station::{Discovery, Router};
29//! use pamoja_gateway::udp::Eui;
30//!
31//! // The station names itself in the ID6 form, four groups with leading zeros dropped.
32//! let gateway = Eui::from_hex("b827ebfffe010203").expect("sixteen hex digits");
33//! let asking = Discovery::new(gateway);
34//! assert_eq!(asking.to_json(), r#"{"router":"b827:ebff:fe01:203"}"#);
35//!
36//! // The server answers with the websocket to open.
37//! let answer = Router::from_json(
38//!     br#"{"router":"b827:ebff:fe01:203","muxs":"::1","uri":"ws://lns.example.invalid:3001/router"}"#,
39//! )
40//! .expect("the answer is well formed");
41//! assert_eq!(answer.uri.as_deref(), Some("ws://lns.example.invalid:3001/router"));
42//! assert!(answer.error.is_none());
43//! ```
44
45use serde_json::{json, Map, Value};
46
47use crate::udp::{Eui, ProtocolError};
48
49/// The path a station appends to its configured address to find its network server.
50pub const DISCOVERY_PATH: &str = "/router-info";
51
52/// The protocol version a station reports, which is 2 for the protocol described here.
53pub const PROTOCOL_VERSION: u32 = 2;
54
55/// The shortest frame that can carry a header and an integrity code.
56const MINIMUM_FRAME: usize = 5;
57
58/// How many bytes the message integrity code takes, at the end of every frame.
59const MIC_LEN: usize = 4;
60
61/// What a join request carries between its header and its integrity code.
62const JOIN_REQUEST_LEN: usize = 18;
63
64/// The address, control byte, and counter every data frame starts with.
65const FRAME_HEADER_LEN: usize = 7;
66
67/// A join request, in the top three bits of the header byte.
68const JOIN_REQUEST: u8 = 0;
69
70/// An uplink that asks for no acknowledgment.
71const UNCONFIRMED_UP: u8 = 2;
72
73/// An uplink that asks to be acknowledged.
74const CONFIRMED_UP: u8 = 4;
75
76/// A frame this protocol does not describe, which a station passes along whole.
77const PROPRIETARY: u8 = 7;
78
79/// The spreading factor a data-rate table writes for a number it leaves undefined, which the
80/// reference station reads as no data rate at all.
81const UNDEFINED_DATA_RATE: i8 = -1;
82
83/// Writes an identifier in the ID6 form the protocol prefers.
84///
85/// The eight bytes are read as four groups of sixteen bits and written like an IPv6 address:
86/// leading zeros in a group are dropped, and the longest run of zero groups becomes `::`.
87///
88/// # Arguments
89///
90/// * `eui` - the identifier to write.
91///
92/// # Returns
93///
94/// The identifier, such as `1:203:405:607:8` or `::1`.
95pub fn id6(eui: Eui) -> String {
96    let bytes = eui.bytes();
97    let groups: [u16; 4] = [
98        u16::from_be_bytes([bytes[0], bytes[1]]),
99        u16::from_be_bytes([bytes[2], bytes[3]]),
100        u16::from_be_bytes([bytes[4], bytes[5]]),
101        u16::from_be_bytes([bytes[6], bytes[7]]),
102    ];
103
104    // The longest run of zero groups is elided, and a tie keeps the first run.
105    let mut best_at = 0;
106    let mut best_len = 0;
107    let mut at = 0;
108    while at < groups.len() {
109        if groups[at] != 0 {
110            at += 1;
111            continue;
112        }
113        let start = at;
114        while at < groups.len() && groups[at] == 0 {
115            at += 1;
116        }
117        if at - start > best_len {
118            best_at = start;
119            best_len = at - start;
120        }
121    }
122
123    if best_len == groups.len() {
124        return "::".to_string();
125    }
126    if best_len < 2 {
127        let written: Vec<String> = groups.iter().map(|group| format!("{group:x}")).collect();
128        return written.join(":");
129    }
130
131    let head: Vec<String> = groups[..best_at]
132        .iter()
133        .map(|group| format!("{group:x}"))
134        .collect();
135    let tail: Vec<String> = groups[best_at + best_len..]
136        .iter()
137        .map(|group| format!("{group:x}"))
138        .collect();
139    format!("{}::{}", head.join(":"), tail.join(":"))
140}
141
142/// Reads an identifier written in ID6, the dashed EUI form, or as a decimal number.
143///
144/// # Arguments
145///
146/// * `text` - the identifier as the protocol writes it.
147///
148/// # Returns
149///
150/// The identifier, or `None` when the text is none of those three forms.
151pub fn eui_of(text: &str) -> Option<Eui> {
152    // The dashed form, which is eight pairs of hexadecimal digits.
153    if text.contains('-') {
154        let mut bytes = [0u8; 8];
155        let mut seen = 0;
156        for part in text.split('-') {
157            if seen == bytes.len() {
158                return None;
159            }
160            bytes[seen] = u8::from_str_radix(part, 16).ok()?;
161            seen += 1;
162        }
163        return (seen == bytes.len()).then(|| Eui::new(bytes));
164    }
165
166    // A plain number, which the protocol also accepts.
167    if !text.contains(':') {
168        return text
169            .parse::<u64>()
170            .ok()
171            .map(|value| Eui::new(value.to_be_bytes()));
172    }
173
174    // The ID6 form, four groups with at most one elision.
175    let (head, tail) = match text.split_once("::") {
176        Some((head, tail)) => (head, tail),
177        None => (text, ""),
178    };
179    let elided = text.contains("::");
180
181    let mut groups: Vec<u16> = Vec::new();
182    for part in head.split(':').filter(|part| !part.is_empty()) {
183        groups.push(u16::from_str_radix(part, 16).ok()?);
184    }
185    let mut rest: Vec<u16> = Vec::new();
186    for part in tail.split(':').filter(|part| !part.is_empty()) {
187        rest.push(u16::from_str_radix(part, 16).ok()?);
188    }
189
190    if elided {
191        let missing = 4usize.checked_sub(groups.len() + rest.len())?;
192        groups.extend(core::iter::repeat_n(0u16, missing));
193    } else if groups.len() + rest.len() != 4 {
194        return None;
195    }
196    groups.extend(rest);
197    if groups.len() != 4 {
198        return None;
199    }
200
201    let mut bytes = [0u8; 8];
202    for (index, group) in groups.iter().enumerate() {
203        let pair = group.to_be_bytes();
204        bytes[index * 2] = pair[0];
205        bytes[index * 2 + 1] = pair[1];
206    }
207    Some(Eui::new(bytes))
208}
209
210/// Where the run of the station sits in a station clock value, above the microseconds.
211const XTIME_SESSION_SHIFT: u32 = 48;
212
213/// Where the radio unit sits, above the run.
214const XTIME_UNIT_SHIFT: u32 = 56;
215
216/// The microseconds a run counts in, the low 48 bits.
217const XTIME_MICROS: u64 = (1 << XTIME_SESSION_SHIFT) - 1;
218
219/// The highest radio unit, which has seven bits.
220const XTIME_MAX_UNIT: u8 = 0x7f;
221
222/// A station clock value taken apart: the radio it was read on, the run of the station it
223/// belongs to, and the microseconds that run had counted.
224///
225/// A station reports every uplink with an `xtime`, and a server hands the same value back
226/// with the downlink that answers it, so the station knows which radio and which run the
227/// answer is timed against. The reference station writes the microseconds in bits 0 to 47,
228/// the run in bits 48 to 55, which it picks at random and never leaves at zero, and the
229/// radio in bits 56 to 62 (`src/ral.h` in `lorabasics/basicstation`). A value with a run
230/// past 31 is larger than a double holds exactly, which is why every reader here takes it as
231/// an integer.
232///
233/// # Examples
234///
235/// ```
236/// use pamoja_gateway::station::Xtime;
237///
238/// // A frame heard 3512.348611 seconds into the station's first run, on its only radio.
239/// let heard = Xtime::new(0, 1, 3_512_348_611).expect("in range");
240/// let carried = heard.value();
241/// assert_eq!(Xtime::of(carried), heard);
242///
243/// // A radio past 127, or a run longer than 48 bits of microseconds, has no value.
244/// assert_eq!(Xtime::new(128, 1, 0), None);
245/// assert_eq!(Xtime::new(0, 1, 1 << 48), None);
246/// ```
247#[derive(Clone, Copy, Debug, PartialEq, Eq)]
248pub struct Xtime {
249    /// The radio unit the time was read on, 0 to 127.
250    pub unit: u8,
251    /// The run of the station the time belongs to.
252    pub session: u8,
253    /// The microseconds the run had counted, below 2^48.
254    pub micros: u64,
255}
256
257impl Xtime {
258    /// Builds a clock value from its parts.
259    ///
260    /// # Arguments
261    ///
262    /// * `unit` - the radio unit, 0 to 127.
263    /// * `session` - the run of the station.
264    /// * `micros` - the microseconds the run had counted.
265    ///
266    /// # Returns
267    ///
268    /// The value, or `None` when the unit is past 127 or the microseconds do not fit 48 bits.
269    pub const fn new(unit: u8, session: u8, micros: u64) -> Option<Xtime> {
270        if unit > XTIME_MAX_UNIT || micros > XTIME_MICROS {
271            return None;
272        }
273        Some(Xtime {
274            unit,
275            session,
276            micros,
277        })
278    }
279
280    /// Returns the value a message carries.
281    ///
282    /// # Returns
283    ///
284    /// The `xtime`.
285    pub const fn value(self) -> i64 {
286        (((self.unit & XTIME_MAX_UNIT) as i64) << XTIME_UNIT_SHIFT)
287            | ((self.session as i64) << XTIME_SESSION_SHIFT)
288            | (self.micros & XTIME_MICROS) as i64
289    }
290
291    /// Takes apart a value a message carried.
292    ///
293    /// # Arguments
294    ///
295    /// * `value` - the `xtime`.
296    ///
297    /// # Returns
298    ///
299    /// Its radio unit, run, and microseconds.
300    pub const fn of(value: i64) -> Xtime {
301        Xtime {
302            unit: (value >> XTIME_UNIT_SHIFT) as u8 & XTIME_MAX_UNIT,
303            session: (value >> XTIME_SESSION_SHIFT) as u8,
304            micros: value as u64 & XTIME_MICROS,
305        }
306    }
307}
308
309/// What a station asks the discovery endpoint, naming itself.
310#[derive(Clone, Copy, Debug, PartialEq, Eq)]
311pub struct Discovery {
312    /// The station asking.
313    pub router: Eui,
314}
315
316impl Discovery {
317    /// Builds the discovery request for a station.
318    ///
319    /// # Arguments
320    ///
321    /// * `router` - the station's own identifier.
322    ///
323    /// # Returns
324    ///
325    /// The request.
326    pub const fn new(router: Eui) -> Discovery {
327        Discovery { router }
328    }
329
330    /// Writes the request as the endpoint expects it.
331    ///
332    /// # Returns
333    ///
334    /// The JSON text to send on [`DISCOVERY_PATH`].
335    pub fn to_json(&self) -> String {
336        json!({ "router": id6(self.router) }).to_string()
337    }
338
339    /// Reads a discovery request, as a server does.
340    ///
341    /// # Arguments
342    ///
343    /// * `body` - the request body.
344    ///
345    /// # Returns
346    ///
347    /// The request.
348    ///
349    /// # Errors
350    ///
351    /// Returns [`ProtocolError::Payload`] when the body is not this request.
352    pub fn from_json(body: &[u8]) -> Result<Discovery, ProtocolError> {
353        let value: Value = serde_json::from_slice(body)
354            .map_err(|error| ProtocolError::Payload(error.to_string()))?;
355        let router = value
356            .get("router")
357            .and_then(Value::as_str)
358            .and_then(eui_of)
359            .ok_or_else(|| ProtocolError::Payload("router is not an identifier".to_string()))?;
360        Ok(Discovery { router })
361    }
362}
363
364/// What the discovery endpoint answers: where to connect, or why not.
365#[derive(Clone, Debug, Default, PartialEq, Eq)]
366pub struct Router {
367    /// The station, as the server read it.
368    pub router: Option<Eui>,
369    /// The server endpoint that will carry the session.
370    pub muxs: Option<Eui>,
371    /// The websocket to open, absolute.
372    pub uri: Option<String>,
373    /// Why the station was refused, when it was.
374    pub error: Option<String>,
375}
376
377impl Router {
378    /// Answers a station with the websocket to open.
379    ///
380    /// # Arguments
381    ///
382    /// * `router` - the station being answered.
383    /// * `muxs` - the server endpoint carrying the session.
384    /// * `uri` - the websocket address.
385    ///
386    /// # Returns
387    ///
388    /// The answer.
389    pub fn accepted(router: Eui, muxs: Eui, uri: impl Into<String>) -> Router {
390        Router {
391            router: Some(router),
392            muxs: Some(muxs),
393            uri: Some(uri.into()),
394            error: None,
395        }
396    }
397
398    /// Refuses a station, saying why.
399    ///
400    /// # Arguments
401    ///
402    /// * `router` - the station being refused.
403    /// * `error` - what is wrong, in words the operator can act on.
404    ///
405    /// # Returns
406    ///
407    /// The answer.
408    pub fn refused(router: Eui, error: impl Into<String>) -> Router {
409        Router {
410            router: Some(router),
411            muxs: None,
412            uri: None,
413            error: Some(error.into()),
414        }
415    }
416
417    /// Writes the answer.
418    ///
419    /// # Returns
420    ///
421    /// The JSON body.
422    pub fn to_json(&self) -> String {
423        let mut object = serde_json::Map::new();
424        if let Some(router) = self.router {
425            object.insert("router".to_string(), Value::String(id6(router)));
426        }
427        if let Some(muxs) = self.muxs {
428            object.insert("muxs".to_string(), Value::String(id6(muxs)));
429        }
430        if let Some(uri) = &self.uri {
431            object.insert("uri".to_string(), Value::String(uri.clone()));
432        }
433        if let Some(error) = &self.error {
434            object.insert("error".to_string(), Value::String(error.clone()));
435        }
436        Value::Object(object).to_string()
437    }
438
439    /// Reads the answer, as a station does.
440    ///
441    /// # Arguments
442    ///
443    /// * `body` - the answer body.
444    ///
445    /// # Returns
446    ///
447    /// The answer, whose `error` says whether the station was refused.
448    ///
449    /// # Errors
450    ///
451    /// Returns [`ProtocolError::Payload`] when the body is not JSON.
452    pub fn from_json(body: &[u8]) -> Result<Router, ProtocolError> {
453        let value: Value = serde_json::from_slice(body)
454            .map_err(|error| ProtocolError::Payload(error.to_string()))?;
455        Ok(Router {
456            router: value.get("router").and_then(Value::as_str).and_then(eui_of),
457            muxs: value.get("muxs").and_then(Value::as_str).and_then(eui_of),
458            uri: value.get("uri").and_then(Value::as_str).map(str::to_string),
459            error: value
460                .get("error")
461                .and_then(Value::as_str)
462                .map(str::to_string),
463        })
464    }
465}
466
467/// The levels a packet was heard at, which every uplink carries.
468#[derive(Clone, Copy, Debug, Default, PartialEq)]
469pub struct Levels {
470    /// The radio context the station uses to answer this packet.
471    pub rctx: i64,
472    /// The station's own clock, in microseconds.
473    pub xtime: i64,
474    /// The GPS time, when the station has one.
475    pub gpstime: Option<i64>,
476    /// The received signal strength, in dBm.
477    pub rssi: f64,
478    /// The signal-to-noise ratio, in dB.
479    pub snr: f64,
480}
481
482/// One frame of a [`Message::Schedule`], transmitted to a group rather than a device.
483#[derive(Clone, Debug, Default, PartialEq)]
484pub struct Broadcast {
485    /// The frame to transmit.
486    pub pdu: Vec<u8>,
487    /// The data rate to transmit at.
488    pub data_rate: u8,
489    /// The frequency to transmit on, in hertz.
490    pub frequency_hz: u32,
491    /// How urgent it is.
492    pub priority: u8,
493    /// The GPS time to transmit at.
494    pub gpstime: Option<i64>,
495    /// The radio to transmit on.
496    pub rctx: Option<i64>,
497}
498
499impl Broadcast {
500    /// Writes the frame as a schedule carries it.
501    fn to_value(&self) -> Value {
502        let mut object = Map::new();
503        object.insert("pdu".to_owned(), Value::String(hex(&self.pdu)));
504        object.insert("DR".to_owned(), json!(self.data_rate));
505        object.insert("Freq".to_owned(), json!(self.frequency_hz));
506        object.insert("priority".to_owned(), json!(self.priority));
507        if let Some(gpstime) = self.gpstime {
508            object.insert("gpstime".to_owned(), json!(gpstime));
509        }
510        if let Some(rctx) = self.rctx {
511            object.insert("rctx".to_owned(), json!(rctx));
512        }
513        Value::Object(object)
514    }
515
516    /// Reads one frame of a schedule.
517    fn from_value(value: &Value) -> Result<Broadcast, ProtocolError> {
518        let object = value
519            .as_object()
520            .ok_or_else(|| refused("a scheduled frame is not a JSON object"))?;
521        Ok(Broadcast {
522            pdu: bytes_of(object, "pdu")?,
523            data_rate: whole(object, "DR").unwrap_or(0) as u8,
524            frequency_hz: whole(object, "Freq").unwrap_or(0) as u32,
525            priority: whole(object, "priority").unwrap_or(0) as u8,
526            gpstime: whole(object, "gpstime"),
527            rctx: whole(object, "rctx"),
528        })
529    }
530}
531
532/// Every message the websocket carries once the session is open.
533#[derive(Clone, Debug, PartialEq)]
534pub enum Message {
535    /// What the station is, sent first thing after the websocket opens.
536    Version {
537        /// The station software.
538        station: String,
539        /// Its firmware.
540        firmware: String,
541        /// The package it came from.
542        package: String,
543        /// The hardware model.
544        model: String,
545        /// The protocol version it speaks.
546        protocol: u32,
547        /// What it can do, such as `gps` or `prod`.
548        features: String,
549    },
550    /// How the server tells the station to configure its radios.
551    RouterConfig {
552        /// The networks whose data frames the station forwards, or `None` to forward every
553        /// network's.
554        ///
555        /// A station matches each entry against the top seven bits of a frame's device
556        /// address, so an empty list forwards no data frame at all, while `None`, written as
557        /// `null`, filters nothing.
558        net_id: Option<Vec<u32>>,
559        /// The join identifier ranges whose join requests are forwarded, as inclusive pairs;
560        /// an empty list filters nothing.
561        join_eui: Vec<(u64, u64)>,
562        /// The region name, such as `EU868`.
563        region: String,
564        /// The highest radiated power the region allows, in dBm.
565        max_eirp: f64,
566        /// The concentrator the configuration is written for, such as `sx1301/1`.
567        hwspec: String,
568        /// The lowest and highest frequency the station may use, in hertz.
569        freq_range: (u32, u32),
570        /// The data rates, indexed by data-rate number, each a spreading factor, a bandwidth
571        /// in hertz, and whether it is downlink only.
572        ///
573        /// A spreading factor of 0 is FSK. `None` is a number the table leaves undefined,
574        /// which the protocol writes as a spreading factor of -1 so the entries after it keep
575        /// their numbers.
576        data_rates: Vec<Option<(u8, u32, bool)>>,
577    },
578    /// A join request the station heard.
579    JoinRequest {
580        /// The MAC header byte.
581        mhdr: u8,
582        /// The application the device is joining.
583        join_eui: Eui,
584        /// The device asking.
585        dev_eui: Eui,
586        /// The nonce it used, which a server must not see twice.
587        dev_nonce: u16,
588        /// The message integrity code.
589        mic: i32,
590        /// The data rate it arrived at.
591        data_rate: u8,
592        /// The frequency it arrived on, in hertz.
593        frequency_hz: u32,
594        /// How it was heard.
595        levels: Levels,
596    },
597    /// A data frame the station heard.
598    Uplink {
599        /// The MAC header byte.
600        mhdr: u8,
601        /// The device address it came from.
602        dev_addr: i32,
603        /// The frame control byte.
604        fctrl: u8,
605        /// The frame counter, as the sixteen bits on the air.
606        fcnt: u16,
607        /// The frame options, as bytes.
608        fopts: Vec<u8>,
609        /// The port, or `None` for a frame carrying only options.
610        fport: Option<u8>,
611        /// The encrypted payload.
612        payload: Vec<u8>,
613        /// The message integrity code.
614        mic: i32,
615        /// The data rate it arrived at.
616        data_rate: u8,
617        /// The frequency it arrived on, in hertz.
618        frequency_hz: u32,
619        /// How it was heard.
620        levels: Levels,
621    },
622    /// A frame this protocol does not describe, which the station heard and passes on whole.
623    Proprietary {
624        /// The frame as it arrived, header and integrity code included.
625        payload: Vec<u8>,
626        /// The data rate it arrived at.
627        data_rate: u8,
628        /// The frequency it arrived on, in hertz.
629        frequency_hz: u32,
630        /// How it was heard.
631        levels: Levels,
632    },
633    /// A frame the server asks the station to transmit.
634    ///
635    /// A class A frame, and a class C frame answering an uplink, are timed from that uplink's
636    /// `xtime` and go out in `rx1` or `rx2`, the first preferred. A class B frame goes out at
637    /// `gpstime` in a ping slot. A class C frame answering nothing names `rx2` alone.
638    Downlink {
639        /// The device it is for.
640        dev_eui: Eui,
641        /// Which class of downlink this is: 0 for A, 1 for B, 2 for C.
642        class: u8,
643        /// The identifier the transmission report carries back.
644        diid: i64,
645        /// The frame to transmit.
646        pdu: Vec<u8>,
647        /// The delay before the first receive window, in seconds.
648        rx_delay: Option<u8>,
649        /// The first window, as a data rate and a frequency in hertz.
650        rx1: Option<(u8, u32)>,
651        /// The second window, as a data rate and a frequency in hertz.
652        rx2: Option<(u8, u32)>,
653        /// The ping slot a class B frame goes out in, as a data rate and a frequency in
654        /// hertz.
655        ping_slot: Option<(u8, u32)>,
656        /// How urgent it is.
657        priority: u8,
658        /// The station clock the first window is counted from.
659        xtime: Option<i64>,
660        /// The radio context the uplink was heard on.
661        rctx: Option<i64>,
662        /// When a class B frame goes out, in microseconds since the GPS epoch.
663        gpstime: Option<i64>,
664    },
665    /// Frames the server asks the station to transmit to a group at a given time.
666    ///
667    /// A station reports nothing back about these, unlike a [`Message::Downlink`].
668    Schedule {
669        /// What to transmit, in the order given.
670        frames: Vec<Broadcast>,
671    },
672    /// What became of a frame the station was asked to transmit.
673    Transmitted {
674        /// The identifier from the downlink.
675        diid: i64,
676        /// The device it was for.
677        dev_eui: Eui,
678        /// The radio context it went out on.
679        rctx: i64,
680        /// The station clock it went out at.
681        xtime: i64,
682        /// The time it went out, in seconds.
683        txtime: f64,
684        /// The GPS time, when the station has one.
685        gpstime: Option<i64>,
686    },
687    /// The clock the two keep between them.
688    TimeSync {
689        /// The station clock.
690        txtime: Option<i64>,
691        /// The station clock, when the server is transferring GPS time against it.
692        xtime: Option<i64>,
693        /// The GPS time.
694        gpstime: Option<i64>,
695    },
696    /// A message this crate does not read, kept so a caller can see what arrived.
697    Other {
698        /// What the message called itself.
699        msgtype: String,
700    },
701}
702
703impl Message {
704    /// Returns the word this message calls itself on the wire.
705    ///
706    /// # Returns
707    ///
708    /// The `msgtype` value, such as `updf`.
709    pub fn msgtype(&self) -> &str {
710        match self {
711            Message::Version { .. } => "version",
712            Message::RouterConfig { .. } => "router_config",
713            Message::JoinRequest { .. } => "jreq",
714            Message::Uplink { .. } => "updf",
715            Message::Proprietary { .. } => "propdf",
716            Message::Downlink { .. } => "dnmsg",
717            Message::Schedule { .. } => "dnsched",
718            Message::Transmitted { .. } => "dntxed",
719            Message::TimeSync { .. } => "timesync",
720            Message::Other { msgtype } => msgtype,
721        }
722    }
723
724    /// Reads a frame the radio heard into the message that reports it.
725    ///
726    /// A station holds no key, so nothing is verified here: the frame is split into the
727    /// fields the protocol names and the server judges them. A join request becomes
728    /// [`Message::JoinRequest`] and a data frame [`Message::Uplink`].
729    ///
730    /// # Arguments
731    ///
732    /// * `frame` - the bytes as they arrived, header through message integrity code.
733    /// * `data_rate` - the data rate it arrived at.
734    /// * `frequency_hz` - the frequency it arrived on, in hertz.
735    /// * `levels` - how it was heard.
736    ///
737    /// # Returns
738    ///
739    /// The message to send up.
740    ///
741    /// # Errors
742    ///
743    /// Returns [`ProtocolError::Payload`] when the frame is shorter than the fields its
744    /// header names, or carries a kind a station does not send up.
745    ///
746    /// # Examples
747    ///
748    /// ```
749    /// use pamoja_gateway::station::{Levels, Message};
750    /// use pamoja_lorawan::{Session, Uplink};
751    ///
752    /// // A node's seventh uplink, a reading on port 2, as its radio sent it.
753    /// let node = Session::new(0x2601_1BDA, [7; 16], [8; 16]);
754    /// let frame = node
755    ///     .encode_uplink(&Uplink::new(7, 2, b"21.5"))
756    ///     .expect("an uplink frame");
757    ///
758    /// let heard = Message::heard(frame.as_bytes(), 5, 868_100_000, Levels::default())
759    ///     .expect("a station sends this one up");
760    /// match heard {
761    ///     Message::Uplink { dev_addr, fcnt, fport, payload, .. } => {
762    ///         assert_eq!(dev_addr as u32, node.dev_addr());
763    ///         assert_eq!(fcnt, 7);
764    ///         assert_eq!(fport, Some(2));
765    ///         assert_eq!(payload.len(), 4, "still encrypted: a station holds no key");
766    ///     }
767    ///     other => panic!("that is a data frame, not {}", other.msgtype()),
768    /// }
769    /// ```
770    pub fn heard(
771        frame: &[u8],
772        data_rate: u8,
773        frequency_hz: u32,
774        levels: Levels,
775    ) -> Result<Message, ProtocolError> {
776        // Every frame is one header byte, what it carries, and four bytes of integrity code.
777        if frame.len() < MINIMUM_FRAME {
778            return Err(refused(&format!(
779                "a frame of {} bytes is shorter than a header and an integrity code",
780                frame.len()
781            )));
782        }
783        let mhdr = frame[0];
784        let body = &frame[1..frame.len() - MIC_LEN];
785        let mic = i32::from_le_bytes([
786            frame[frame.len() - 4],
787            frame[frame.len() - 3],
788            frame[frame.len() - 2],
789            frame[frame.len() - 1],
790        ]);
791
792        // The top three bits of the header byte say what kind of frame this is.
793        match mhdr >> 5 {
794            JOIN_REQUEST => {
795                if body.len() != JOIN_REQUEST_LEN {
796                    return Err(refused(&format!(
797                        "a join request carries {JOIN_REQUEST_LEN} bytes, not {}",
798                        body.len()
799                    )));
800                }
801                Ok(Message::JoinRequest {
802                    mhdr,
803                    join_eui: least_first(&body[0..8]),
804                    dev_eui: least_first(&body[8..16]),
805                    dev_nonce: u16::from_le_bytes([body[16], body[17]]),
806                    mic,
807                    data_rate,
808                    frequency_hz,
809                    levels,
810                })
811            }
812            UNCONFIRMED_UP | CONFIRMED_UP => {
813                if body.len() < FRAME_HEADER_LEN {
814                    return Err(refused(&format!(
815                        "a data frame carries {FRAME_HEADER_LEN} bytes of header, not {}",
816                        body.len()
817                    )));
818                }
819                let fctrl = body[4];
820                // The low four bits of the frame control byte count the options after it.
821                let options = usize::from(fctrl & 0x0f);
822                let carried = FRAME_HEADER_LEN + options;
823                if body.len() < carried {
824                    return Err(refused(&format!(
825                        "a frame naming {options} bytes of options carries {} in all",
826                        body.len()
827                    )));
828                }
829                let rest = &body[carried..];
830                Ok(Message::Uplink {
831                    mhdr,
832                    dev_addr: i32::from_le_bytes([body[0], body[1], body[2], body[3]]),
833                    fctrl,
834                    fcnt: u16::from_le_bytes([body[5], body[6]]),
835                    fopts: body[FRAME_HEADER_LEN..carried].to_vec(),
836                    // A frame with nothing after its options carries no port at all.
837                    fport: rest.first().copied(),
838                    payload: rest.get(1..).unwrap_or_default().to_vec(),
839                    mic,
840                    data_rate,
841                    frequency_hz,
842                    levels,
843                })
844            }
845            PROPRIETARY => Ok(Message::Proprietary {
846                // The protocol carries a proprietary frame whole, header and all.
847                payload: frame.to_vec(),
848                data_rate,
849                frequency_hz,
850                levels,
851            }),
852            kind => Err(refused(&format!(
853                "a station does not send a frame of kind {kind} up"
854            ))),
855        }
856    }
857
858    /// Writes the message as the websocket carries it.
859    ///
860    /// # Returns
861    ///
862    /// The JSON text to send.
863    pub fn to_json(&self) -> String {
864        let mut object = Map::new();
865        object.insert(
866            "msgtype".to_owned(),
867            Value::String(self.msgtype().to_owned()),
868        );
869
870        match self {
871            Message::Version {
872                station,
873                firmware,
874                package,
875                model,
876                protocol,
877                features,
878            } => {
879                object.insert("station".to_owned(), Value::String(station.clone()));
880                object.insert("firmware".to_owned(), Value::String(firmware.clone()));
881                object.insert("package".to_owned(), Value::String(package.clone()));
882                object.insert("model".to_owned(), Value::String(model.clone()));
883                object.insert("protocol".to_owned(), json!(protocol));
884                object.insert("features".to_owned(), Value::String(features.clone()));
885            }
886            Message::RouterConfig {
887                net_id,
888                join_eui,
889                region,
890                max_eirp,
891                hwspec,
892                freq_range,
893                data_rates,
894            } => {
895                object.insert("NetID".to_owned(), json!(net_id));
896                object.insert(
897                    "JoinEui".to_owned(),
898                    Value::Array(
899                        join_eui
900                            .iter()
901                            .map(|(begin, end)| json!([begin, end]))
902                            .collect(),
903                    ),
904                );
905                object.insert("region".to_owned(), Value::String(region.clone()));
906                object.insert("max_eirp".to_owned(), json!(max_eirp));
907                object.insert("hwspec".to_owned(), Value::String(hwspec.clone()));
908                object.insert("freq_range".to_owned(), json!([freq_range.0, freq_range.1]));
909                object.insert(
910                    "DRs".to_owned(),
911                    Value::Array(
912                        data_rates
913                            .iter()
914                            .map(|entry| match entry {
915                                Some((sf, bw, down)) => json!([sf, bw / 1_000, u8::from(*down)]),
916                                None => json!([UNDEFINED_DATA_RATE, 0, 0]),
917                            })
918                            .collect(),
919                    ),
920                );
921            }
922            Message::JoinRequest {
923                mhdr,
924                join_eui,
925                dev_eui,
926                dev_nonce,
927                mic,
928                data_rate,
929                frequency_hz,
930                levels,
931            } => {
932                object.insert("MHdr".to_owned(), json!(mhdr));
933                object.insert("JoinEui".to_owned(), Value::String(id6(*join_eui)));
934                object.insert("DevEui".to_owned(), Value::String(id6(*dev_eui)));
935                object.insert("DevNonce".to_owned(), json!(dev_nonce));
936                object.insert("MIC".to_owned(), json!(mic));
937                object.insert("DR".to_owned(), json!(data_rate));
938                object.insert("Freq".to_owned(), json!(frequency_hz));
939                object.insert("upinfo".to_owned(), levels.to_value());
940            }
941            Message::Uplink {
942                mhdr,
943                dev_addr,
944                fctrl,
945                fcnt,
946                fopts,
947                fport,
948                payload,
949                mic,
950                data_rate,
951                frequency_hz,
952                levels,
953            } => {
954                object.insert("MHdr".to_owned(), json!(mhdr));
955                object.insert("DevAddr".to_owned(), json!(dev_addr));
956                object.insert("FCtrl".to_owned(), json!(fctrl));
957                object.insert("FCnt".to_owned(), json!(fcnt));
958                object.insert("FOpts".to_owned(), Value::String(hex(fopts)));
959                object.insert("FPort".to_owned(), json!(fport.map_or(-1, i16::from)));
960                object.insert("FRMPayload".to_owned(), Value::String(hex(payload)));
961                object.insert("MIC".to_owned(), json!(mic));
962                object.insert("DR".to_owned(), json!(data_rate));
963                object.insert("Freq".to_owned(), json!(frequency_hz));
964                object.insert("upinfo".to_owned(), levels.to_value());
965            }
966            Message::Proprietary {
967                payload,
968                data_rate,
969                frequency_hz,
970                levels,
971            } => {
972                object.insert("FRMPayload".to_owned(), Value::String(hex(payload)));
973                object.insert("DR".to_owned(), json!(data_rate));
974                object.insert("Freq".to_owned(), json!(frequency_hz));
975                object.insert("upinfo".to_owned(), levels.to_value());
976            }
977            Message::Downlink {
978                dev_eui,
979                class,
980                diid,
981                pdu,
982                rx_delay,
983                rx1,
984                rx2,
985                ping_slot,
986                priority,
987                xtime,
988                rctx,
989                gpstime,
990            } => {
991                object.insert("DevEui".to_owned(), Value::String(id6(*dev_eui)));
992                object.insert("dC".to_owned(), json!(class));
993                object.insert("diid".to_owned(), json!(diid));
994                object.insert("pdu".to_owned(), Value::String(hex(pdu)));
995                if let Some(delay) = rx_delay {
996                    object.insert("RxDelay".to_owned(), json!(delay));
997                }
998                if let Some((data_rate, frequency_hz)) = rx1 {
999                    object.insert("RX1DR".to_owned(), json!(data_rate));
1000                    object.insert("RX1Freq".to_owned(), json!(frequency_hz));
1001                }
1002                if let Some((data_rate, frequency_hz)) = rx2 {
1003                    object.insert("RX2DR".to_owned(), json!(data_rate));
1004                    object.insert("RX2Freq".to_owned(), json!(frequency_hz));
1005                }
1006                if let Some((data_rate, frequency_hz)) = ping_slot {
1007                    object.insert("DR".to_owned(), json!(data_rate));
1008                    object.insert("Freq".to_owned(), json!(frequency_hz));
1009                }
1010                object.insert("priority".to_owned(), json!(priority));
1011                if let Some(xtime) = xtime {
1012                    object.insert("xtime".to_owned(), json!(xtime));
1013                }
1014                if let Some(rctx) = rctx {
1015                    object.insert("rctx".to_owned(), json!(rctx));
1016                }
1017                if let Some(gpstime) = gpstime {
1018                    object.insert("gpstime".to_owned(), json!(gpstime));
1019                }
1020            }
1021            Message::Schedule { frames } => {
1022                object.insert(
1023                    "schedule".to_owned(),
1024                    Value::Array(frames.iter().map(Broadcast::to_value).collect()),
1025                );
1026            }
1027            Message::Transmitted {
1028                diid,
1029                dev_eui,
1030                rctx,
1031                xtime,
1032                txtime,
1033                gpstime,
1034            } => {
1035                object.insert("diid".to_owned(), json!(diid));
1036                object.insert("DevEui".to_owned(), Value::String(id6(*dev_eui)));
1037                object.insert("rctx".to_owned(), json!(rctx));
1038                object.insert("xtime".to_owned(), json!(xtime));
1039                object.insert("txtime".to_owned(), json!(txtime));
1040                if let Some(gpstime) = gpstime {
1041                    object.insert("gpstime".to_owned(), json!(gpstime));
1042                }
1043            }
1044            Message::TimeSync {
1045                txtime,
1046                xtime,
1047                gpstime,
1048            } => {
1049                if let Some(txtime) = txtime {
1050                    object.insert("txtime".to_owned(), json!(txtime));
1051                }
1052                if let Some(xtime) = xtime {
1053                    object.insert("xtime".to_owned(), json!(xtime));
1054                }
1055                if let Some(gpstime) = gpstime {
1056                    object.insert("gpstime".to_owned(), json!(gpstime));
1057                }
1058            }
1059            Message::Other { .. } => {}
1060        }
1061
1062        Value::Object(object).to_string()
1063    }
1064
1065    /// Reads a message off the websocket.
1066    ///
1067    /// A kind this crate does not build is returned as [`Message::Other`] rather than
1068    /// refused, because a station and a server may each speak more than the other does.
1069    ///
1070    /// # Arguments
1071    ///
1072    /// * `body` - the message text.
1073    ///
1074    /// # Returns
1075    ///
1076    /// What arrived.
1077    ///
1078    /// # Errors
1079    ///
1080    /// Returns [`ProtocolError::Payload`] when the body is not a JSON object, carries no
1081    /// message kind, or is missing an identifier the kind it names requires.
1082    pub fn from_json(body: &[u8]) -> Result<Message, ProtocolError> {
1083        let value: Value = serde_json::from_slice(body)
1084            .map_err(|error| refused(&format!("the message is not JSON: {error}")))?;
1085        let object = value
1086            .as_object()
1087            .ok_or_else(|| refused("the message is not a JSON object"))?;
1088        let msgtype =
1089            text(object, "msgtype").ok_or_else(|| refused("the message names no kind"))?;
1090
1091        Ok(match msgtype.as_str() {
1092            "version" => Message::Version {
1093                station: text(object, "station").unwrap_or_default(),
1094                firmware: text(object, "firmware").unwrap_or_default(),
1095                package: text(object, "package").unwrap_or_default(),
1096                model: text(object, "model").unwrap_or_default(),
1097                protocol: whole(object, "protocol").unwrap_or(0) as u32,
1098                features: text(object, "features").unwrap_or_default(),
1099            },
1100            "router_config" => Message::RouterConfig {
1101                net_id: numbers(object, "NetID")
1102                    .map(|ids| ids.into_iter().map(|id| id as u32).collect()),
1103                join_eui: pairs(object, "JoinEui"),
1104                region: text(object, "region").unwrap_or_default(),
1105                max_eirp: number(object, "max_eirp").unwrap_or(0.0),
1106                hwspec: text(object, "hwspec").unwrap_or_default(),
1107                freq_range: {
1108                    let range = numbers(object, "freq_range").unwrap_or_default();
1109                    (
1110                        range.first().copied().unwrap_or(0) as u32,
1111                        range.get(1).copied().unwrap_or(0) as u32,
1112                    )
1113                },
1114                data_rates: data_rates(object),
1115            },
1116            "jreq" => Message::JoinRequest {
1117                // Implementations differ on the spelling of this one field, so both are read.
1118                mhdr: whole(object, "MHdr")
1119                    .or_else(|| whole(object, "Mhdr"))
1120                    .unwrap_or(0) as u8,
1121                join_eui: identifier(object, "JoinEui")?,
1122                dev_eui: identifier(object, "DevEui")?,
1123                dev_nonce: whole(object, "DevNonce").unwrap_or(0) as u16,
1124                mic: whole(object, "MIC").unwrap_or(0) as i32,
1125                data_rate: whole(object, "DR").unwrap_or(0) as u8,
1126                frequency_hz: whole(object, "Freq").unwrap_or(0) as u32,
1127                levels: Levels::from_value(object.get("upinfo")),
1128            },
1129            "updf" => {
1130                let port = whole(object, "FPort").unwrap_or(-1);
1131                Message::Uplink {
1132                    mhdr: whole(object, "MHdr")
1133                        .or_else(|| whole(object, "Mhdr"))
1134                        .unwrap_or(0) as u8,
1135                    dev_addr: whole(object, "DevAddr").unwrap_or(0) as i32,
1136                    fctrl: whole(object, "FCtrl").unwrap_or(0) as u8,
1137                    fcnt: whole(object, "FCnt").unwrap_or(0) as u16,
1138                    fopts: bytes_of(object, "FOpts")?,
1139                    fport: (port >= 0).then_some(port as u8),
1140                    payload: bytes_of(object, "FRMPayload")?,
1141                    mic: whole(object, "MIC").unwrap_or(0) as i32,
1142                    data_rate: whole(object, "DR").unwrap_or(0) as u8,
1143                    frequency_hz: whole(object, "Freq").unwrap_or(0) as u32,
1144                    levels: Levels::from_value(object.get("upinfo")),
1145                }
1146            }
1147            "propdf" => Message::Proprietary {
1148                payload: bytes_of(object, "FRMPayload")?,
1149                data_rate: whole(object, "DR").unwrap_or(0) as u8,
1150                frequency_hz: whole(object, "Freq").unwrap_or(0) as u32,
1151                levels: Levels::from_value(object.get("upinfo")),
1152            },
1153            "dnmsg" => Message::Downlink {
1154                dev_eui: identifier(object, "DevEui")?,
1155                class: whole(object, "dC").unwrap_or(0) as u8,
1156                diid: whole(object, "diid").unwrap_or(0),
1157                pdu: bytes_of(object, "pdu")?,
1158                rx_delay: whole(object, "RxDelay").map(|value| value as u8),
1159                rx1: window(object, "RX1DR", "RX1Freq"),
1160                rx2: window(object, "RX2DR", "RX2Freq"),
1161                ping_slot: window(object, "DR", "Freq"),
1162                priority: whole(object, "priority").unwrap_or(0) as u8,
1163                xtime: whole(object, "xtime"),
1164                rctx: whole(object, "rctx"),
1165                gpstime: whole(object, "gpstime"),
1166            },
1167            "dnsched" => {
1168                let mut frames = Vec::new();
1169                if let Some(scheduled) = object.get("schedule").and_then(Value::as_array) {
1170                    for frame in scheduled {
1171                        frames.push(Broadcast::from_value(frame)?);
1172                    }
1173                }
1174                Message::Schedule { frames }
1175            }
1176            "dntxed" => Message::Transmitted {
1177                diid: whole(object, "diid").unwrap_or(0),
1178                dev_eui: identifier(object, "DevEui")?,
1179                rctx: whole(object, "rctx").unwrap_or(0),
1180                xtime: whole(object, "xtime").unwrap_or(0),
1181                txtime: number(object, "txtime").unwrap_or(0.0),
1182                gpstime: whole(object, "gpstime"),
1183            },
1184            "timesync" => Message::TimeSync {
1185                txtime: whole(object, "txtime"),
1186                xtime: whole(object, "xtime"),
1187                gpstime: whole(object, "gpstime"),
1188            },
1189            other => Message::Other {
1190                msgtype: other.to_owned(),
1191            },
1192        })
1193    }
1194}
1195
1196impl Levels {
1197    /// Writes the levels as an uplink carries them.
1198    fn to_value(self) -> Value {
1199        let mut object = Map::new();
1200        object.insert("rctx".to_owned(), json!(self.rctx));
1201        object.insert("xtime".to_owned(), json!(self.xtime));
1202        if let Some(gpstime) = self.gpstime {
1203            object.insert("gpstime".to_owned(), json!(gpstime));
1204        }
1205        object.insert("rssi".to_owned(), json!(self.rssi));
1206        object.insert("snr".to_owned(), json!(self.snr));
1207        Value::Object(object)
1208    }
1209
1210    /// Reads the levels beside an uplink, which a station always sends.
1211    fn from_value(value: Option<&Value>) -> Levels {
1212        let Some(object) = value.and_then(Value::as_object) else {
1213            return Levels::default();
1214        };
1215        Levels {
1216            rctx: whole(object, "rctx").unwrap_or(0),
1217            xtime: whole(object, "xtime").unwrap_or(0),
1218            gpstime: whole(object, "gpstime"),
1219            rssi: number(object, "rssi").unwrap_or(0.0),
1220            snr: number(object, "snr").unwrap_or(0.0),
1221        }
1222    }
1223}
1224
1225/// Writes bytes as the lowercase hexadecimal the protocol carries them in.
1226fn hex(bytes: &[u8]) -> String {
1227    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
1228}
1229
1230/// Reads a hexadecimal field, which may be absent or empty.
1231fn bytes_of(object: &Map<String, Value>, key: &str) -> Result<Vec<u8>, ProtocolError> {
1232    let Some(written) = text(object, key) else {
1233        return Ok(Vec::new());
1234    };
1235    if written.len() % 2 != 0 {
1236        return Err(refused(&format!("{key} is not whole bytes")));
1237    }
1238    (0..written.len())
1239        .step_by(2)
1240        .map(|at| {
1241            u8::from_str_radix(&written[at..at + 2], 16)
1242                .map_err(|_| refused(&format!("{key} is not hexadecimal")))
1243        })
1244        .collect()
1245}
1246
1247/// Reads an identifier field in any form the protocol writes one.
1248fn identifier(object: &Map<String, Value>, key: &str) -> Result<Eui, ProtocolError> {
1249    text(object, key)
1250        .as_deref()
1251        .and_then(eui_of)
1252        .ok_or_else(|| refused(&format!("{key} is not an identifier")))
1253}
1254
1255/// Reads a receive window, which is a data rate and a frequency together or not at all.
1256fn window(object: &Map<String, Value>, rate: &str, frequency: &str) -> Option<(u8, u32)> {
1257    let rate = whole(object, rate)?;
1258    let frequency = whole(object, frequency)?;
1259    Some((rate as u8, frequency as u32))
1260}
1261
1262/// Reads the data rates, each a spreading factor, a bandwidth in hertz, and whether it is
1263/// downlink only. The protocol writes the bandwidth in kilohertz.
1264///
1265/// The table is read by position, so an entry the table leaves undefined, or one that cannot
1266/// be read, stays in place as `None` rather than moving every entry after it down a number.
1267fn data_rates(object: &Map<String, Value>) -> Vec<Option<(u8, u32, bool)>> {
1268    let Some(Value::Array(entries)) = object.get("DRs") else {
1269        return Vec::new();
1270    };
1271    entries
1272        .iter()
1273        .map(|entry| {
1274            let triple = entry.as_array()?;
1275            let spreading = u8::try_from(integer(triple.first()?)?).ok()?;
1276            let bandwidth = u32::try_from(integer(triple.get(1)?)?).ok()?;
1277            let down = triple.get(2).and_then(integer).unwrap_or(0) != 0;
1278            Some((spreading, bandwidth.saturating_mul(1_000), down))
1279        })
1280        .collect()
1281}
1282
1283/// Reads an array of whole numbers, or `None` when the field is absent or not an array.
1284fn numbers(object: &Map<String, Value>, key: &str) -> Option<Vec<i64>> {
1285    let entries = object.get(key)?.as_array()?;
1286    Some(entries.iter().filter_map(integer).collect())
1287}
1288
1289/// Reads an array of inclusive pairs, as the admitted join identifiers are written.
1290///
1291/// Each bound is a join identifier read as a number, which a double cannot hold exactly, so
1292/// it is read as the integer it was written as.
1293fn pairs(object: &Map<String, Value>, key: &str) -> Vec<(u64, u64)> {
1294    let Some(Value::Array(entries)) = object.get(key) else {
1295        return Vec::new();
1296    };
1297    let bound = |value: &Value| {
1298        value
1299            .as_u64()
1300            .or_else(|| value.as_f64().map(|number| number as u64))
1301    };
1302    entries
1303        .iter()
1304        .filter_map(|entry| {
1305            let pair = entry.as_array()?;
1306            Some((bound(pair.first()?)?, bound(pair.get(1)?)?))
1307        })
1308        .collect()
1309}
1310
1311/// Reads a text field.
1312fn text(object: &Map<String, Value>, key: &str) -> Option<String> {
1313    object.get(key).and_then(Value::as_str).map(str::to_owned)
1314}
1315
1316/// Reads a number field.
1317fn number(object: &Map<String, Value>, key: &str) -> Option<f64> {
1318    object.get(key).and_then(Value::as_f64)
1319}
1320
1321/// Reads a whole number field.
1322fn whole(object: &Map<String, Value>, key: &str) -> Option<i64> {
1323    object.get(key).and_then(integer)
1324}
1325
1326/// Reads a whole number, exactly when it was written as an integer.
1327///
1328/// An `xtime` carries a session byte in bits 48 to 55, so it is routinely larger than a
1329/// double holds exactly, and reading it through one would move the moment it names.
1330fn integer(value: &Value) -> Option<i64> {
1331    value
1332        .as_i64()
1333        .or_else(|| value.as_f64().map(|number| number.round() as i64))
1334}
1335
1336/// Reads an identifier that arrived least significant byte first.
1337fn least_first(bytes: &[u8]) -> Eui {
1338    let mut eui = [0u8; 8];
1339    for (index, byte) in bytes.iter().rev().enumerate() {
1340        eui[index] = *byte;
1341    }
1342    Eui::new(eui)
1343}
1344
1345/// Refuses a message, saying what is wrong with it.
1346fn refused(why: &str) -> ProtocolError {
1347    ProtocolError::Payload(why.to_owned())
1348}
1349
1350/// A live station, which reaches its network server over a websocket.
1351///
1352/// The protocol above is data; this is the part that opens a socket. It asks the discovery
1353/// endpoint where the server is, opens the websocket it names, says what it is, and reads the
1354/// configuration it is given. After that the two exchange [`Message`] values.
1355///
1356/// Available with the `station-client` feature, which is off by default so the message layer
1357/// costs nothing.
1358#[cfg(feature = "station-client")]
1359pub struct Station {
1360    socket: tokio_tungstenite::WebSocketStream<
1361        tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1362    >,
1363    config: Message,
1364}
1365
1366#[cfg(feature = "station-client")]
1367impl core::fmt::Debug for Station {
1368    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1369        f.debug_struct("Station")
1370            .field("config", &self.config)
1371            .finish_non_exhaustive()
1372    }
1373}
1374
1375#[cfg(feature = "station-client")]
1376impl Station {
1377    /// Finds the network server for a station and opens its websocket.
1378    ///
1379    /// Discovery is a short-lived websocket of its own: the station asks on
1380    /// [`DISCOVERY_PATH`], is told where its session runs, and opens that instead.
1381    ///
1382    /// # Arguments
1383    ///
1384    /// * `endpoint` - the address a station is configured with, as `ws://host:port`.
1385    /// * `router` - the station asking.
1386    ///
1387    /// # Returns
1388    ///
1389    /// The open station, whose [`config`](Station::config) holds what the server answered
1390    /// with.
1391    ///
1392    /// # Errors
1393    ///
1394    /// Returns [`ProtocolError::Payload`] when the endpoint refuses the station, answers
1395    /// something that is not this protocol, or cannot be reached.
1396    pub async fn connect(endpoint: &str, router: Eui) -> Result<Station, ProtocolError> {
1397        let answer = discover(endpoint, router).await?;
1398        if let Some(error) = answer.error {
1399            return Err(refused(&format!(
1400                "the server refused this station: {error}"
1401            )));
1402        }
1403        let uri = answer
1404            .uri
1405            .ok_or_else(|| refused("the server named no websocket to open"))?;
1406
1407        let (mut socket, _) = tokio_tungstenite::connect_async(uri.as_str())
1408            .await
1409            .map_err(|error| refused(&format!("the websocket did not open: {error}")))?;
1410
1411        // The station speaks first, and the server answers with how to configure the radios.
1412        let version = Message::Version {
1413            station: "pamoja".to_owned(),
1414            firmware: env!("CARGO_PKG_VERSION").to_owned(),
1415            package: "pamoja-gateway".to_owned(),
1416            model: "pamoja".to_owned(),
1417            protocol: PROTOCOL_VERSION,
1418            features: String::new(),
1419        };
1420        send(&mut socket, &version).await?;
1421        let config = receive(&mut socket).await?;
1422
1423        Ok(Station { socket, config })
1424    }
1425
1426    /// Returns what the server said when the session opened.
1427    ///
1428    /// # Returns
1429    ///
1430    /// The `router_config` message, which names the region, the channel plan and the limits.
1431    pub const fn config(&self) -> &Message {
1432        &self.config
1433    }
1434
1435    /// Sends a message to the network server.
1436    ///
1437    /// # Arguments
1438    ///
1439    /// * `message` - what to send.
1440    ///
1441    /// # Returns
1442    ///
1443    /// `Ok(())` once the websocket has taken it.
1444    ///
1445    /// # Errors
1446    ///
1447    /// Returns [`ProtocolError::Payload`] when the websocket fails.
1448    pub async fn send(&mut self, message: &Message) -> Result<(), ProtocolError> {
1449        send(&mut self.socket, message).await
1450    }
1451
1452    /// Waits for the next message from the network server.
1453    ///
1454    /// # Returns
1455    ///
1456    /// What arrived.
1457    ///
1458    /// # Errors
1459    ///
1460    /// Returns [`ProtocolError::Payload`] when the websocket closes or carries something that
1461    /// is not this protocol.
1462    pub async fn recv(&mut self) -> Result<Message, ProtocolError> {
1463        receive(&mut self.socket).await
1464    }
1465}
1466
1467/// Asks the discovery endpoint where the network server for a station is.
1468#[cfg(feature = "station-client")]
1469async fn discover(endpoint: &str, router: Eui) -> Result<Router, ProtocolError> {
1470    use futures_util::{SinkExt, StreamExt};
1471
1472    let (mut socket, _) = tokio_tungstenite::connect_async(asking(endpoint))
1473        .await
1474        .map_err(|error| refused(&format!("the discovery endpoint is not reachable: {error}")))?;
1475
1476    socket
1477        .send(tokio_tungstenite::tungstenite::Message::text(
1478            Discovery::new(router).to_json(),
1479        ))
1480        .await
1481        .map_err(|error| refused(&format!("the discovery request failed: {error}")))?;
1482
1483    let answer = socket
1484        .next()
1485        .await
1486        .ok_or_else(|| refused("the discovery endpoint answered nothing"))?
1487        .map_err(|error| refused(&format!("the discovery answer failed: {error}")))?;
1488    let answer = Router::from_json(
1489        answer
1490            .to_text()
1491            .map_err(|_| refused("the discovery answer is not text"))?
1492            .as_bytes(),
1493    )?;
1494
1495    // The session runs on a socket of its own, so this one is finished with.
1496    let _ = socket.close(None).await;
1497    Ok(answer)
1498}
1499
1500/// Builds the discovery address from the endpoint a station is configured with.
1501#[cfg(feature = "station-client")]
1502fn asking(endpoint: &str) -> String {
1503    let endpoint = endpoint.trim_end_matches("/");
1504    // A station is configured with a websocket address; the plain forms are read as one.
1505    let endpoint = match endpoint.split_once("://") {
1506        Some(("http", rest)) => format!("ws://{rest}"),
1507        Some(("https", rest)) => format!("wss://{rest}"),
1508        Some(_) => endpoint.to_owned(),
1509        None => format!("ws://{endpoint}"),
1510    };
1511    format!("{endpoint}{DISCOVERY_PATH}")
1512}
1513
1514/// Writes one message onto the websocket.
1515#[cfg(feature = "station-client")]
1516async fn send<S>(socket: &mut S, message: &Message) -> Result<(), ProtocolError>
1517where
1518    S: futures_util::Sink<tokio_tungstenite::tungstenite::Message> + Unpin,
1519    S::Error: core::fmt::Display,
1520{
1521    use futures_util::SinkExt;
1522
1523    socket
1524        .send(tokio_tungstenite::tungstenite::Message::text(
1525            message.to_json(),
1526        ))
1527        .await
1528        .map_err(|error| refused(&format!("the websocket would not take a message: {error}")))
1529}
1530
1531/// Reads the next message the websocket carries, skipping the frames that are not one.
1532#[cfg(feature = "station-client")]
1533async fn receive<S>(socket: &mut S) -> Result<Message, ProtocolError>
1534where
1535    S: futures_util::Stream<
1536            Item = Result<
1537                tokio_tungstenite::tungstenite::Message,
1538                tokio_tungstenite::tungstenite::Error,
1539            >,
1540        > + Unpin,
1541{
1542    use futures_util::StreamExt;
1543
1544    loop {
1545        let frame = socket
1546            .next()
1547            .await
1548            .ok_or_else(|| refused("the websocket closed"))?
1549            .map_err(|error| refused(&format!("the websocket failed: {error}")))?;
1550
1551        // Keepalives and the close frame are the websocket talking, not the protocol.
1552        match frame.to_text() {
1553            Ok(text) if !text.is_empty() => return Message::from_json(text.as_bytes()),
1554            _ => continue,
1555        }
1556    }
1557}
1558
1559#[cfg(test)]
1560mod tests {
1561    use super::*;
1562
1563    // The three examples the protocol glossary prints, which are the only concrete
1564    // identifiers it publishes.
1565    #[test]
1566    fn the_published_identifier_examples_read_and_write() {
1567        let zero = Eui::new([0; 8]);
1568        assert_eq!(id6(zero), "::");
1569        assert_eq!(eui_of("::0"), Some(zero));
1570
1571        let one_high = Eui::new([0x00, 0x01, 0, 0, 0, 0, 0, 0]);
1572        assert_eq!(eui_of("1::"), Some(one_high));
1573        assert_eq!(id6(one_high), "1::");
1574
1575        let low_pair = Eui::new([0, 0, 0, 0, 0, 0x0a, 0, 0x0b]);
1576        assert_eq!(eui_of("::a:b"), Some(low_pair));
1577        assert_eq!(id6(low_pair), "::a:b");
1578    }
1579
1580    #[test]
1581    fn an_identifier_reads_in_every_form_the_protocol_accepts() {
1582        let eui = Eui::new([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
1583        assert_eq!(eui_of("102:304:506:708"), Some(eui));
1584        assert_eq!(eui_of("01-02-03-04-05-06-07-08"), Some(eui));
1585        assert_eq!(
1586            eui_of(&u64::from_be_bytes(eui.bytes()).to_string()),
1587            Some(eui)
1588        );
1589        assert_eq!(eui_of("not an identifier"), None);
1590        assert_eq!(eui_of("1:2:3"), None);
1591    }
1592
1593    #[test]
1594    fn discovery_asks_and_is_answered() {
1595        let station = Eui::from_hex("b827ebfffe010203").expect("sixteen digits");
1596        let asking = Discovery::new(station);
1597        let read = Discovery::from_json(asking.to_json().as_bytes()).expect("it round trips");
1598        assert_eq!(read.router, station);
1599
1600        let muxs = Eui::new([0, 0, 0, 0, 0, 0, 0, 1]);
1601        let answer = Router::accepted(station, muxs, "ws://lns.example.invalid:3001/router");
1602        let read = Router::from_json(answer.to_json().as_bytes()).expect("it round trips");
1603        assert_eq!(read.router, Some(station));
1604        assert_eq!(read.muxs, Some(muxs));
1605        assert_eq!(
1606            read.uri.as_deref(),
1607            Some("ws://lns.example.invalid:3001/router")
1608        );
1609        assert!(read.error.is_none());
1610    }
1611
1612    #[test]
1613    fn a_refused_station_is_told_why() {
1614        let station = Eui::from_hex("b827ebfffe010203").expect("sixteen digits");
1615        let refusal = Router::refused(station, "this gateway is not registered");
1616        let read = Router::from_json(refusal.to_json().as_bytes()).expect("it round trips");
1617        assert_eq!(
1618            read.error.as_deref(),
1619            Some("this gateway is not registered")
1620        );
1621        assert!(read.uri.is_none());
1622    }
1623
1624    // The protocol publishes field definitions rather than populated examples, so these
1625    // hold the documented field names and check that what is written reads back.
1626    #[test]
1627    fn every_message_kind_survives_a_round_trip() {
1628        let device = Eui::from_hex("1111111111111111").expect("sixteen digits");
1629        let application = Eui::from_hex("2222222222222222").expect("sixteen digits");
1630        let levels = Levels {
1631            rctx: 1,
1632            xtime: 3_512_348_611,
1633            gpstime: Some(1_364_746_877),
1634            rssi: -35.0,
1635            snr: 5.1,
1636        };
1637
1638        let kinds = [
1639            Message::Version {
1640                station: "pamoja".to_owned(),
1641                firmware: "0.1.18".to_owned(),
1642                package: "pamoja-gateway".to_owned(),
1643                model: "linux".to_owned(),
1644                protocol: PROTOCOL_VERSION,
1645                features: "gps".to_owned(),
1646            },
1647            Message::RouterConfig {
1648                net_id: Some(vec![0]),
1649                join_eui: vec![(0, u64::MAX)],
1650                region: "EU868".to_owned(),
1651                max_eirp: 16.0,
1652                hwspec: "sx1301/1".to_owned(),
1653                freq_range: (863_000_000, 870_000_000),
1654                data_rates: vec![
1655                    Some((12, 125_000, false)),
1656                    Some((7, 250_000, false)),
1657                    Some((0, 0, true)),
1658                ],
1659            },
1660            Message::JoinRequest {
1661                mhdr: 0x00,
1662                join_eui: application,
1663                dev_eui: device,
1664                dev_nonce: 0x0102,
1665                mic: -12345,
1666                data_rate: 5,
1667                frequency_hz: 868_100_000,
1668                levels,
1669            },
1670            Message::Uplink {
1671                mhdr: 0x40,
1672                dev_addr: 0x2601_0001,
1673                fctrl: 0,
1674                fcnt: 7,
1675                fopts: vec![0x03, 0x07],
1676                fport: Some(2),
1677                payload: b"21.5".to_vec(),
1678                mic: 987,
1679                data_rate: 5,
1680                frequency_hz: 868_100_000,
1681                levels,
1682            },
1683            Message::Downlink {
1684                dev_eui: device,
1685                class: 0,
1686                diid: 42,
1687                pdu: vec![0x60, 0x01, 0x02],
1688                rx_delay: Some(1),
1689                rx1: Some((5, 868_100_000)),
1690                rx2: Some((0, 869_525_000)),
1691                ping_slot: None,
1692                priority: 10,
1693                xtime: Some(3_513_348_611),
1694                rctx: Some(1),
1695                gpstime: None,
1696            },
1697            Message::Transmitted {
1698                diid: 42,
1699                dev_eui: device,
1700                rctx: 1,
1701                xtime: 3_513_348_611,
1702                txtime: 1.5,
1703                gpstime: Some(1_364_746_878),
1704            },
1705            Message::TimeSync {
1706                txtime: Some(1_000),
1707                xtime: None,
1708                gpstime: Some(1_364_746_879),
1709            },
1710            Message::Proprietary {
1711                payload: vec![0xe0, 0x01, 0x02, 0x03],
1712                data_rate: 5,
1713                frequency_hz: 868_100_000,
1714                levels,
1715            },
1716            Message::Schedule {
1717                frames: vec![Broadcast {
1718                    pdu: vec![0x60, 0x01, 0x02],
1719                    data_rate: 3,
1720                    frequency_hz: 869_525_000,
1721                    priority: 1,
1722                    gpstime: Some(1_364_746_880),
1723                    rctx: Some(0),
1724                }],
1725            },
1726        ];
1727
1728        for sent in kinds {
1729            let written = sent.to_json();
1730            let read = Message::from_json(written.as_bytes()).expect("it is well formed");
1731            assert_eq!(
1732                read,
1733                sent,
1734                "{} did not survive the round trip",
1735                sent.msgtype()
1736            );
1737            assert!(
1738                written.contains(&format!("\"msgtype\":\"{}\"", sent.msgtype())),
1739                "a message names its kind"
1740            );
1741        }
1742    }
1743
1744    // lorabasics/basicstation src/ral.h reads the radio unit as (xtime >> 56) & 0x7f, the
1745    // session as (xtime >> 48) & 0xff, and keeps the microseconds in bits 47 to 0.
1746    #[test]
1747    fn an_xtime_holds_its_parts_where_the_reference_station_reads_them() {
1748        let clock = Xtime::new(3, 0xa5, 3_512_348_611).expect("in range");
1749        let value = clock.value();
1750        assert_eq!((value >> 56) & 0x7f, 3);
1751        assert_eq!((value >> 48) & 0xff, 0xa5);
1752        assert_eq!(value & 0xffff_ffff_ffff, 3_512_348_611);
1753        assert!(value > 0, "the sign bit stays clear");
1754        assert_eq!(Xtime::of(value), clock);
1755        assert_eq!(
1756            Xtime::of(i64::MAX),
1757            Xtime {
1758                unit: 0x7f,
1759                session: 0xff,
1760                micros: (1 << 48) - 1
1761            }
1762        );
1763    }
1764
1765    // lorabasics/basicstation src/ral.h puts a random session byte, never zero, in bits 48
1766    // to 55 of every xtime, so the value a server echoes back is past what a double holds.
1767    #[test]
1768    fn an_xtime_a_double_cannot_hold_comes_back_to_the_microsecond() {
1769        let xtime = (0xa5_i64 << 48) | 3_512_348_611;
1770        assert!(
1771            xtime > 1 << 53,
1772            "the session byte puts it out of a double's reach"
1773        );
1774        let answer = Message::Downlink {
1775            dev_eui: Eui::from_hex("70b3d57ed0001234").expect("sixteen digits"),
1776            class: 0,
1777            diid: i64::MAX - 1,
1778            pdu: vec![0x60],
1779            rx_delay: Some(1),
1780            rx1: Some((5, 868_100_000)),
1781            rx2: Some((0, 869_525_000)),
1782            ping_slot: None,
1783            priority: 0,
1784            xtime: Some(xtime + 1_000_001),
1785            rctx: Some(0),
1786            gpstime: None,
1787        };
1788        let read = Message::from_json(answer.to_json().as_bytes()).expect("it is well formed");
1789        assert_eq!(read, answer);
1790    }
1791
1792    #[test]
1793    fn a_join_identifier_range_reads_back_exactly() {
1794        let first = u64::from_be_bytes(
1795            Eui::from_hex("70b3d57ed0000001")
1796                .expect("sixteen digits")
1797                .bytes(),
1798        );
1799        let config = Message::RouterConfig {
1800            net_id: None,
1801            join_eui: vec![(first, first + 0xff)],
1802            region: "EU868".to_owned(),
1803            max_eirp: 16.0,
1804            hwspec: "sx1301/1".to_owned(),
1805            freq_range: (863_000_000, 870_000_000),
1806            data_rates: Vec::new(),
1807        };
1808        let read = Message::from_json(config.to_json().as_bytes()).expect("it is well formed");
1809        assert_eq!(read, config);
1810    }
1811
1812    // lorabasics/basicstation src/s2e.c handle_router_config reads the table by position and
1813    // takes a negative spreading factor as a number with no data rate.
1814    #[test]
1815    fn a_table_with_undefined_numbers_keeps_every_rate_at_its_number() {
1816        let mut table: Vec<Option<(u8, u32, bool)>> = (0..=3)
1817            .map(|index| Some((10 - index, 125_000, false)))
1818            .collect();
1819        table.push(Some((8, 500_000, false)));
1820        table.extend([None, None, None]);
1821        table.extend((0..=5).map(|index| Some((12 - index, 500_000, true))));
1822        let config = Message::RouterConfig {
1823            net_id: Some(vec![0x13]),
1824            join_eui: Vec::new(),
1825            region: "US915".to_owned(),
1826            max_eirp: 30.0,
1827            hwspec: "sx1301/1".to_owned(),
1828            freq_range: (902_000_000, 928_000_000),
1829            data_rates: table.clone(),
1830        };
1831
1832        let written = config.to_json();
1833        assert!(
1834            written.contains("[8,500,0],[-1,0,0],[-1,0,0],[-1,0,0],[12,500,1]"),
1835            "{written}"
1836        );
1837        let Message::RouterConfig { data_rates, .. } =
1838            Message::from_json(written.as_bytes()).expect("it is well formed")
1839        else {
1840            panic!("a configuration reads back as one");
1841        };
1842        assert_eq!(data_rates, table);
1843        assert_eq!(data_rates[8], Some((12, 500_000, true)));
1844    }
1845
1846    #[test]
1847    fn an_entry_that_cannot_be_read_holds_its_place() {
1848        let read = Message::from_json(
1849            br#"{"msgtype":"router_config","DRs":[[12,125,0],"none",[-1,0,0],[7,125,0]]}"#,
1850        )
1851        .expect("it is well formed");
1852        let Message::RouterConfig { data_rates, .. } = read else {
1853            panic!("a configuration reads back as one");
1854        };
1855        assert_eq!(
1856            data_rates,
1857            vec![
1858                Some((12, 125_000, false)),
1859                None,
1860                None,
1861                Some((7, 125_000, false))
1862            ]
1863        );
1864    }
1865
1866    // lorabasics/basicstation src/s2e.c: a null NetID accepts every network, while a list
1867    // clears the filter and accepts only what it names, so an empty one accepts none.
1868    #[test]
1869    fn no_network_filter_is_written_as_null_and_an_empty_one_as_empty() {
1870        let filtered = |net_id: Option<Vec<u32>>| Message::RouterConfig {
1871            net_id,
1872            join_eui: Vec::new(),
1873            region: "EU868".to_owned(),
1874            max_eirp: 16.0,
1875            hwspec: "sx1301/1".to_owned(),
1876            freq_range: (863_000_000, 870_000_000),
1877            data_rates: Vec::new(),
1878        };
1879
1880        let open = filtered(None);
1881        assert!(open.to_json().contains("\"NetID\":null"));
1882        assert_eq!(
1883            Message::from_json(open.to_json().as_bytes()).expect("well formed"),
1884            open
1885        );
1886
1887        let closed = filtered(Some(Vec::new()));
1888        assert!(closed.to_json().contains("\"NetID\":[]"));
1889        assert_eq!(
1890            Message::from_json(closed.to_json().as_bytes()).expect("well formed"),
1891            closed
1892        );
1893    }
1894
1895    #[test]
1896    fn a_class_b_frame_carries_its_ping_slot_and_gps_time() {
1897        let beaconed = Message::Downlink {
1898            dev_eui: Eui::from_hex("70b3d57ed0001234").expect("sixteen digits"),
1899            class: 1,
1900            diid: 7,
1901            pdu: vec![0x60, 0x01],
1902            rx_delay: None,
1903            rx1: None,
1904            rx2: None,
1905            ping_slot: Some((3, 869_525_000)),
1906            priority: 0,
1907            xtime: None,
1908            rctx: Some(0),
1909            gpstime: Some(1_442_000_000_000_000),
1910        };
1911        let written = beaconed.to_json();
1912        assert!(written.contains("\"DR\":3"), "{written}");
1913        assert!(written.contains("\"Freq\":869525000"), "{written}");
1914        assert!(
1915            written.contains("\"gpstime\":1442000000000000"),
1916            "{written}"
1917        );
1918        assert!(!written.contains("RX1DR"), "{written}");
1919        let read = Message::from_json(written.as_bytes()).expect("it is well formed");
1920        assert_eq!(read, beaconed);
1921    }
1922
1923    #[test]
1924    fn a_frame_carrying_only_options_has_no_port() {
1925        let quiet = Message::Uplink {
1926            mhdr: 0x40,
1927            dev_addr: 1,
1928            fctrl: 0,
1929            fcnt: 0,
1930            fopts: vec![0x02],
1931            fport: None,
1932            payload: Vec::new(),
1933            mic: 0,
1934            data_rate: 5,
1935            frequency_hz: 868_100_000,
1936            levels: Levels::default(),
1937        };
1938        let written = quiet.to_json();
1939        // The protocol writes a missing port as -1 rather than leaving the field out.
1940        assert!(written.contains("\"FPort\":-1"), "{written}");
1941        let read = Message::from_json(written.as_bytes()).expect("it is well formed");
1942        assert_eq!(read, quiet);
1943    }
1944
1945    #[test]
1946    fn a_kind_this_crate_does_not_build_is_reported_not_refused() {
1947        let read = Message::from_json(br#"{"msgtype":"rmtsh","user":"root"}"#)
1948            .expect("an unknown kind is not an error");
1949        assert_eq!(
1950            read,
1951            Message::Other {
1952                msgtype: "rmtsh".to_owned()
1953            }
1954        );
1955    }
1956
1957    #[test]
1958    fn a_message_that_is_not_one_is_refused() {
1959        assert!(Message::from_json(b"not json").is_err());
1960        assert!(Message::from_json(br#"{"station":"pamoja"}"#).is_err());
1961        assert!(Message::from_json(br#"{"msgtype":"jreq","DevEui":"nonsense"}"#).is_err());
1962        assert!(Message::from_json(br#"{"msgtype":"dnmsg","DevEui":"::1","pdu":"abc"}"#).is_err());
1963    }
1964
1965    // A station splits frames it did not build, so these are built by the device code a
1966    // real node runs and split back by the station side.
1967    #[cfg(feature = "network")]
1968    #[test]
1969    fn a_join_request_a_device_built_reads_back_field_for_field() {
1970        use pamoja_lorawan::Device;
1971
1972        let dev_eui = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
1973        let join_eui = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
1974        let request = Device::new(dev_eui, join_eui, [0x2b; 16]).join_request(0x0102);
1975
1976        let heard = Message::heard(request.as_bytes(), 5, 868_100_000, Levels::default())
1977            .expect("a station sends a join request up");
1978        match heard {
1979            Message::JoinRequest {
1980                mhdr,
1981                join_eui: application,
1982                dev_eui: node,
1983                dev_nonce,
1984                ..
1985            } => {
1986                assert_eq!(mhdr, 0x00);
1987                assert_eq!(application, Eui::new(join_eui));
1988                assert_eq!(node, Eui::new(dev_eui));
1989                assert_eq!(dev_nonce, 0x0102);
1990            }
1991            other => panic!("that is a join request, not {}", other.msgtype()),
1992        }
1993    }
1994
1995    #[cfg(feature = "network")]
1996    #[test]
1997    fn an_encrypted_frame_reads_back_with_its_address_port_and_counter() {
1998        use pamoja_lorawan::{Session, Uplink as LorawanUplink};
1999
2000        let session = Session::new(0x2601_0001, [0x11; 16], [0x22; 16]);
2001        let frame = session
2002            .encode_uplink(&LorawanUplink::new(7, 2, b"21.5"))
2003            .expect("it fits one frame");
2004
2005        let heard = Message::heard(frame.as_bytes(), 5, 868_100_000, Levels::default())
2006            .expect("a station sends a data frame up");
2007        match heard {
2008            Message::Uplink {
2009                dev_addr,
2010                fcnt,
2011                fport,
2012                fopts,
2013                payload,
2014                ..
2015            } => {
2016                assert_eq!(dev_addr, 0x2601_0001);
2017                assert_eq!(fcnt, 7);
2018                assert_eq!(fport, Some(2));
2019                assert!(fopts.is_empty());
2020                // The payload stays encrypted: a station holds no session key.
2021                assert_eq!(payload.len(), 4);
2022                assert_ne!(payload, b"21.5".to_vec());
2023            }
2024            other => panic!("that is a data frame, not {}", other.msgtype()),
2025        }
2026    }
2027
2028    #[test]
2029    fn a_frame_of_options_alone_carries_no_port() {
2030        // An address, a control byte naming two bytes of options, a counter, the options,
2031        // and the integrity code. Nothing follows the options, so there is no port.
2032        let frame = [
2033            0x40, 0x01, 0x00, 0x00, 0x00, 0x02, 0x05, 0x00, 0x03, 0x07, 0x11, 0x22, 0x33, 0x44,
2034        ];
2035        let heard = Message::heard(&frame, 5, 868_100_000, Levels::default())
2036            .expect("a station sends it up");
2037        match heard {
2038            Message::Uplink {
2039                dev_addr,
2040                fcnt,
2041                fopts,
2042                fport,
2043                payload,
2044                ..
2045            } => {
2046                assert_eq!(dev_addr, 1);
2047                assert_eq!(fcnt, 5);
2048                assert_eq!(fopts, vec![0x03, 0x07]);
2049                assert_eq!(fport, None);
2050                assert!(payload.is_empty());
2051            }
2052            other => panic!("that is a data frame, not {}", other.msgtype()),
2053        }
2054    }
2055
2056    #[test]
2057    fn a_proprietary_frame_is_passed_along_whole() {
2058        let frame = [0xe0, 0x01, 0x02, 0x03, 0x04, 0x05];
2059        let heard = Message::heard(&frame, 5, 868_100_000, Levels::default())
2060            .expect("a station sends it up");
2061        match heard {
2062            Message::Proprietary { payload, .. } => assert_eq!(payload, frame.to_vec()),
2063            other => panic!("that one is proprietary, not {}", other.msgtype()),
2064        }
2065    }
2066
2067    #[test]
2068    fn a_frame_a_station_does_not_send_up_is_refused() {
2069        let levels = Levels::default();
2070        // Shorter than a header and an integrity code.
2071        assert!(Message::heard(&[0x40, 0x00], 5, 868_100_000, levels).is_err());
2072        // A downlink, which a station transmits rather than reports.
2073        let downlink = [0x60, 1, 0, 0, 0, 0, 0, 0, 0, 0x11, 0x22, 0x33, 0x44];
2074        assert!(Message::heard(&downlink, 5, 868_100_000, levels).is_err());
2075        // A frame naming more bytes of options than it carries.
2076        let overrun = [0x40, 1, 0, 0, 0, 0x0f, 0, 0, 0x11, 0x22, 0x33, 0x44];
2077        assert!(Message::heard(&overrun, 5, 868_100_000, levels).is_err());
2078    }
2079
2080    #[test]
2081    fn what_is_not_this_protocol_is_refused() {
2082        assert!(Discovery::from_json(b"not json").is_err());
2083        assert!(Discovery::from_json(br#"{"router":"not an identifier"}"#).is_err());
2084    }
2085}
2086
2087#[cfg(all(test, feature = "station-client"))]
2088mod live {
2089    use super::*;
2090
2091    use futures_util::{SinkExt, StreamExt};
2092    use tokio::net::TcpListener;
2093    use tokio_tungstenite::tungstenite::handshake::server::{Request, Response};
2094
2095    /// The station used throughout.
2096    fn router() -> Eui {
2097        Eui::from_hex("0102030405060708").expect("sixteen digits")
2098    }
2099
2100    /// The server endpoint that carries the session.
2101    fn muxs() -> Eui {
2102        Eui::from_hex("0000000000000000").expect("sixteen digits")
2103    }
2104
2105    /// Serves one discovery request from a fresh port and answers it with `answer`.
2106    ///
2107    /// Returns the endpoint to ask and a handle that yields the path the station asked on
2108    /// and what it asked.
2109    // The error a handshake callback returns is a type the websocket crate sizes.
2110    #[allow(clippy::result_large_err)]
2111    async fn endpoint(answer: String) -> (String, tokio::task::JoinHandle<(String, String)>) {
2112        let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
2113        let address = listener.local_addr().expect("a bound port");
2114
2115        let serving = tokio::spawn(async move {
2116            let (stream, _) = listener.accept().await.expect("the station asks");
2117
2118            let seen = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
2119            let noted = std::sync::Arc::clone(&seen);
2120            let mut socket = tokio_tungstenite::accept_hdr_async(
2121                stream,
2122                move |request: &Request, response: Response| {
2123                    noted
2124                        .lock()
2125                        .expect("the path is recorded once")
2126                        .push_str(request.uri().path());
2127                    Ok(response)
2128                },
2129            )
2130            .await
2131            .expect("the websocket opens");
2132            let path = seen.lock().expect("the path was recorded").clone();
2133
2134            let asked = socket
2135                .next()
2136                .await
2137                .expect("the station asks")
2138                .expect("it is a frame");
2139            let asked = asked.to_text().expect("it is text").to_owned();
2140
2141            socket
2142                .send(tokio_tungstenite::tungstenite::Message::text(answer))
2143                .await
2144                .expect("the answer is sent");
2145            let _ = socket.close(None).await;
2146
2147            (path, asked)
2148        });
2149
2150        (format!("ws://{address}"), serving)
2151    }
2152
2153    #[tokio::test]
2154    async fn a_station_asks_where_its_network_server_is() {
2155        let uri = "ws://127.0.0.1:6038/router";
2156        let (where_to_ask, serving) =
2157            endpoint(Router::accepted(router(), muxs(), uri).to_json()).await;
2158
2159        let answer = discover(&where_to_ask, router())
2160            .await
2161            .expect("the endpoint answers");
2162        assert_eq!(answer.uri.as_deref(), Some(uri));
2163
2164        let (path, asked) = serving.await.expect("the endpoint served");
2165        assert_eq!(path, DISCOVERY_PATH);
2166        // The station names itself in the form the protocol writes.
2167        assert_eq!(asked, r#"{"router":"102:304:506:708"}"#);
2168    }
2169
2170    #[tokio::test]
2171    async fn a_station_the_server_will_not_have_is_told_why() {
2172        let (where_to_ask, serving) =
2173            endpoint(Router::refused(router(), "unknown gateway").to_json()).await;
2174
2175        let refusal = Station::connect(&where_to_ask, router())
2176            .await
2177            .expect_err("a refused station does not connect");
2178        assert!(
2179            format!("{refusal}").contains("unknown gateway"),
2180            "{refusal}"
2181        );
2182
2183        serving.await.expect("the endpoint served");
2184    }
2185
2186    #[tokio::test]
2187    async fn a_station_says_what_it_is_and_is_given_a_configuration() {
2188        let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
2189        let address = listener.local_addr().expect("a bound port");
2190
2191        let configuration = Message::RouterConfig {
2192            net_id: Some(vec![0]),
2193            join_eui: vec![(0, u64::MAX)],
2194            region: "EU868".to_owned(),
2195            max_eirp: 16.0,
2196            hwspec: "sx1301/1".to_owned(),
2197            freq_range: (863_000_000, 870_000_000),
2198            data_rates: vec![Some((12, 125_000, false)), Some((7, 250_000, false))],
2199        };
2200        let sent_down = Message::Downlink {
2201            dev_eui: Eui::from_hex("1111111111111111").expect("sixteen digits"),
2202            class: 0,
2203            diid: 7,
2204            pdu: vec![0x60, 0x01],
2205            rx_delay: Some(1),
2206            rx1: Some((5, 868_100_000)),
2207            rx2: None,
2208            ping_slot: None,
2209            priority: 0,
2210            xtime: Some(3_513_348_611),
2211            rctx: Some(1),
2212            gpstime: None,
2213        };
2214
2215        let answering = tokio::spawn({
2216            let configuration = configuration.clone();
2217            let sent_down = sent_down.clone();
2218            async move {
2219                let (stream, _) = listener.accept().await.expect("the station connects");
2220                let mut socket = tokio_tungstenite::accept_async(stream)
2221                    .await
2222                    .expect("the websocket opens");
2223
2224                // A station speaks first, and what it says is how it identifies itself.
2225                let opening = socket
2226                    .next()
2227                    .await
2228                    .expect("the station speaks")
2229                    .expect("it is a frame");
2230                let opening = Message::from_json(opening.to_text().expect("it is text").as_bytes())
2231                    .expect("it is this protocol");
2232
2233                socket
2234                    .send(tokio_tungstenite::tungstenite::Message::text(
2235                        configuration.to_json(),
2236                    ))
2237                    .await
2238                    .expect("the configuration is sent");
2239
2240                let carried = socket
2241                    .next()
2242                    .await
2243                    .expect("the station reports")
2244                    .expect("it is a frame");
2245                let carried = Message::from_json(carried.to_text().expect("it is text").as_bytes())
2246                    .expect("it is this protocol");
2247
2248                socket
2249                    .send(tokio_tungstenite::tungstenite::Message::text(
2250                        sent_down.to_json(),
2251                    ))
2252                    .await
2253                    .expect("the downlink is sent");
2254
2255                (opening, carried)
2256            }
2257        });
2258
2259        let (where_to_ask, serving) = endpoint(
2260            Router::accepted(router(), muxs(), format!("ws://{address}/router")).to_json(),
2261        )
2262        .await;
2263
2264        let mut station = Station::connect(&where_to_ask, router())
2265            .await
2266            .expect("the station connects");
2267        assert_eq!(station.config(), &configuration);
2268
2269        let reported = Message::Uplink {
2270            mhdr: 0x40,
2271            dev_addr: 0x2601_0001,
2272            fctrl: 0,
2273            fcnt: 1,
2274            fopts: Vec::new(),
2275            fport: Some(2),
2276            payload: b"21.5".to_vec(),
2277            mic: 987,
2278            data_rate: 5,
2279            frequency_hz: 868_100_000,
2280            levels: Levels::default(),
2281        };
2282        station.send(&reported).await.expect("the uplink is sent");
2283        assert_eq!(station.recv().await.expect("the server answers"), sent_down);
2284
2285        serving.await.expect("the endpoint served");
2286        let (opening, carried) = answering.await.expect("the server served");
2287        assert_eq!(carried, reported);
2288        match opening {
2289            Message::Version {
2290                station, protocol, ..
2291            } => {
2292                assert_eq!(station, "pamoja");
2293                assert_eq!(protocol, PROTOCOL_VERSION);
2294            }
2295            other => panic!("a station opens with its version, not {}", other.msgtype()),
2296        }
2297    }
2298}