Skip to main content

pamoja_lorawan/
cflist.rs

1//! The channel list a join accept can carry.
2//!
3//! A network that wants a device on more channels than its region's defaults says so in the
4//! join accept, in sixteen optional bytes at the end called the CFList. RP002-1.0.5 section
5//! 3.3.1 defines two forms, told apart by the last byte.
6//!
7//! - **Type 0** lists up to five frequencies, three bytes each, least significant first, in
8//!   units of 100 Hz. A zero is an unused slot. The dynamic channel plans (EU868, EU433,
9//!   AS923, KR920, IN865, RU864) use it.
10//! - **Type 1** is six sixteen-bit channel masks, least significant byte first, where bit
11//!   *n* of group *g* is channel `g * 16 + n`. The fixed plans (US915, AU915, CN470) read
12//!   each group as the mask it names. A dynamic plan may take it too, reading a channel
13//!   number against one of the two published numberings, which `pamoja-lora` carries as
14//!   [`FixedChannelList`].
15//!
16//! [`FixedChannelList`]: https://docs.rs/pamoja-lora/latest/pamoja_lora/region/enum.FixedChannelList.html
17//!
18//! A [`CfList`] keeps the sixteen bytes as they arrived and reads either form out of them, so
19//! nothing a network sent is lost to a type this crate does not recognize.
20
21use crate::error::LorawanError;
22
23/// The number of bytes a channel list occupies.
24pub const CFLIST_LEN: usize = 16;
25
26/// How many frequencies a type 0 list carries.
27pub const CFLIST_FREQUENCIES: usize = 5;
28
29/// How many sixteen-bit masks a type 1 list carries.
30pub const CFLIST_MASK_GROUPS: usize = 6;
31
32/// Which form a channel list takes, from its last byte.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34pub enum CfListKind {
35    /// Type 0: a list of frequencies.
36    Frequencies,
37    /// Type 1: groups of channel mask bits.
38    ChannelMasks,
39    /// A type the regional parameters reserve, which a device ignores.
40    Reserved(u8),
41}
42
43impl CfListKind {
44    /// Returns the byte a channel list of this kind ends with.
45    ///
46    /// # Returns
47    ///
48    /// `0` for frequencies, `1` for channel masks, or the reserved byte itself.
49    pub const fn to_byte(self) -> u8 {
50        match self {
51            CfListKind::Frequencies => 0,
52            CfListKind::ChannelMasks => 1,
53            CfListKind::Reserved(byte) => byte,
54        }
55    }
56
57    /// Reads the kind a channel list's last byte names.
58    ///
59    /// # Arguments
60    ///
61    /// * `byte` - the CFListType byte.
62    ///
63    /// # Returns
64    ///
65    /// The kind.
66    pub const fn from_byte(byte: u8) -> CfListKind {
67        match byte {
68            0 => CfListKind::Frequencies,
69            1 => CfListKind::ChannelMasks,
70            other => CfListKind::Reserved(other),
71        }
72    }
73}
74
75/// The optional channel list at the end of a join accept.
76///
77/// # Examples
78///
79/// A European network adding the five channels most EU868 networks run between 867.1 and
80/// 867.9 MHz:
81///
82/// ```
83/// use pamoja_lorawan::{CfList, CfListKind};
84///
85/// let list = CfList::frequencies([867_100_000, 867_300_000, 867_500_000, 867_700_000, 867_900_000])?;
86/// assert_eq!(list.kind(), CfListKind::Frequencies);
87/// assert_eq!(list.to_bytes().len(), 16, "the last sixteen bytes of the accept");
88///
89/// // A device reads the same frequencies back out.
90/// let heard = CfList::from_bytes(list.to_bytes());
91/// assert_eq!(heard.frequencies_hz(), Some([867_100_000, 867_300_000, 867_500_000, 867_700_000, 867_900_000]));
92/// # Ok::<(), pamoja_lorawan::LorawanError>(())
93/// ```
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
95pub struct CfList {
96    bytes: [u8; CFLIST_LEN],
97}
98
99impl CfList {
100    /// Keeps a channel list exactly as it arrived.
101    ///
102    /// # Arguments
103    ///
104    /// * `bytes` - the sixteen CFList bytes.
105    ///
106    /// # Returns
107    ///
108    /// The channel list, whatever its type byte says.
109    pub const fn from_bytes(bytes: [u8; CFLIST_LEN]) -> CfList {
110        CfList { bytes }
111    }
112
113    /// Builds a type 0 list from frequencies.
114    ///
115    /// # Arguments
116    ///
117    /// * `frequencies_hz` - five frequencies in hertz, with `0` for a slot left unused.
118    ///
119    /// # Returns
120    ///
121    /// The channel list.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`LorawanError::MalformedFrame`] if a frequency is not a whole number of
126    /// hundreds of hertz, or does not fit the three bytes it is carried in, which end just
127    /// under 1.678 GHz. Between those, RP002-1.0.5 reserves anything below 100 MHz, and that
128    /// is refused too.
129    pub const fn frequencies(
130        frequencies_hz: [u32; CFLIST_FREQUENCIES],
131    ) -> Result<CfList, LorawanError> {
132        let mut bytes = [0u8; CFLIST_LEN];
133        let mut slot = 0;
134        while slot < CFLIST_FREQUENCIES {
135            let hz = frequencies_hz[slot];
136            if hz != 0 {
137                if !hz.is_multiple_of(100) || hz < 100_000_000 {
138                    return Err(LorawanError::MalformedFrame);
139                }
140                let hundreds = hz / 100;
141                if hundreds > 0x00FF_FFFF {
142                    return Err(LorawanError::MalformedFrame);
143                }
144                let at = slot * 3;
145                bytes[at] = hundreds as u8;
146                bytes[at + 1] = (hundreds >> 8) as u8;
147                bytes[at + 2] = (hundreds >> 16) as u8;
148            }
149            slot += 1;
150        }
151        bytes[CFLIST_LEN - 1] = CfListKind::Frequencies.to_byte();
152        Ok(CfList { bytes })
153    }
154
155    /// Builds a type 1 list from channel mask groups.
156    ///
157    /// # Arguments
158    ///
159    /// * `masks` - six groups, where bit *n* of group *g* enables channel `g * 16 + n`.
160    ///
161    /// # Returns
162    ///
163    /// The channel list, with its three reserved bytes zero.
164    pub const fn channel_masks(masks: [u16; CFLIST_MASK_GROUPS]) -> CfList {
165        let mut bytes = [0u8; CFLIST_LEN];
166        let mut group = 0;
167        while group < CFLIST_MASK_GROUPS {
168            bytes[group * 2] = masks[group] as u8;
169            bytes[group * 2 + 1] = (masks[group] >> 8) as u8;
170            group += 1;
171        }
172        bytes[CFLIST_LEN - 1] = CfListKind::ChannelMasks.to_byte();
173        CfList { bytes }
174    }
175
176    /// Returns the sixteen bytes, as a join accept carries them.
177    ///
178    /// # Returns
179    ///
180    /// The bytes.
181    pub const fn to_bytes(&self) -> [u8; CFLIST_LEN] {
182        self.bytes
183    }
184
185    /// Returns which form the list takes.
186    ///
187    /// # Returns
188    ///
189    /// The kind its last byte names.
190    pub const fn kind(&self) -> CfListKind {
191        CfListKind::from_byte(self.bytes[CFLIST_LEN - 1])
192    }
193
194    /// Reads the frequencies out of a type 0 list.
195    ///
196    /// # Returns
197    ///
198    /// Five frequencies in hertz, with `0` for an unused slot, or [`None`] for a list of any
199    /// other type.
200    pub const fn frequencies_hz(&self) -> Option<[u32; CFLIST_FREQUENCIES]> {
201        if !matches!(self.kind(), CfListKind::Frequencies) {
202            return None;
203        }
204        let mut out = [0u32; CFLIST_FREQUENCIES];
205        let mut slot = 0;
206        while slot < CFLIST_FREQUENCIES {
207            let at = slot * 3;
208            let hundreds = self.bytes[at] as u32
209                | (self.bytes[at + 1] as u32) << 8
210                | (self.bytes[at + 2] as u32) << 16;
211            out[slot] = hundreds * 100;
212            slot += 1;
213        }
214        Some(out)
215    }
216
217    /// Reads the mask groups out of a type 1 list.
218    ///
219    /// # Returns
220    ///
221    /// Six groups, or [`None`] for a list of any other type.
222    pub const fn channel_mask_groups(&self) -> Option<[u16; CFLIST_MASK_GROUPS]> {
223        if !matches!(self.kind(), CfListKind::ChannelMasks) {
224            return None;
225        }
226        let mut out = [0u16; CFLIST_MASK_GROUPS];
227        let mut group = 0;
228        while group < CFLIST_MASK_GROUPS {
229            out[group] = self.bytes[group * 2] as u16 | (self.bytes[group * 2 + 1] as u16) << 8;
230            group += 1;
231        }
232        Some(out)
233    }
234
235    /// Reports whether a type 1 list enables a channel.
236    ///
237    /// # Arguments
238    ///
239    /// * `channel` - the channel number, `group * 16 + bit`.
240    ///
241    /// # Returns
242    ///
243    /// Whether its bit is set, or [`None`] for a list of any other type or a channel past
244    /// the 96 the groups cover.
245    pub const fn enables(&self, channel: u8) -> Option<bool> {
246        let Some(groups) = self.channel_mask_groups() else {
247            return None;
248        };
249        let group = channel as usize / 16;
250        if group >= CFLIST_MASK_GROUPS {
251            return None;
252        }
253        Some(groups[group] & (1 << (channel % 16)) != 0)
254    }
255
256    /// Returns the channels a type 1 list enables, lowest first.
257    ///
258    /// This is the order RP002-1.0.5 section 3.3.1.1 has a dynamic plan create them in, one
259    /// after its last default channel for each bit set.
260    ///
261    /// # Returns
262    ///
263    /// The channel numbers, which is empty for a list of any other type.
264    pub fn enabled_channels(&self) -> impl Iterator<Item = u8> + '_ {
265        let groups = self.channel_mask_groups();
266        (0..(CFLIST_MASK_GROUPS * 16) as u8).filter(move |channel| {
267            groups.is_some_and(|groups| groups[*channel as usize / 16] & (1 << (channel % 16)) != 0)
268        })
269    }
270}
271
272impl From<CfList> for [u8; CFLIST_LEN] {
273    fn from(list: CfList) -> [u8; CFLIST_LEN] {
274        list.bytes
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    fn hex(text: &str) -> [u8; CFLIST_LEN] {
283        let bytes: Vec<u8> = (0..text.len())
284            .step_by(2)
285            .map(|at| u8::from_str_radix(&text[at..at + 2], 16).expect("hex"))
286            .collect();
287        bytes.try_into().expect("sixteen bytes")
288    }
289
290    #[test]
291    fn a_captured_european_list_reads_as_the_five_usual_channels() {
292        // The CFList of the EU868 join accept published at
293        // https://github.com/anthonykirby/lora-packet/issues/10, which this crate's network
294        // tests also rebuild byte for byte.
295        let list = CfList::from_bytes(hex("184f84e85684b85e84886684586e8400"));
296        assert_eq!(list.kind(), CfListKind::Frequencies);
297        assert_eq!(
298            list.frequencies_hz(),
299            Some([
300                867_100_000,
301                867_300_000,
302                867_500_000,
303                867_700_000,
304                867_900_000
305            ])
306        );
307        assert_eq!(list.channel_mask_groups(), None);
308        assert_eq!(
309            CfList::frequencies(list.frequencies_hz().expect("type 0")),
310            Ok(list),
311            "and building those frequencies gives the same bytes"
312        );
313    }
314
315    #[test]
316    fn the_regional_parameters_example_enables_four_channels() {
317        // RP002-1.0.5 section 3.3.1.1: CFList type 1 received as
318        // 0x0100_A010_0000_0000_0000_0000 creates channels 0, 21, 23 and 28, which the
319        // 800 MHz numbering puts at 863.1, 867.3, 867.7 and 868.7 MHz.
320        let list = CfList::from_bytes(hex("0100a010000000000000000000000001"));
321        assert_eq!(list.kind(), CfListKind::ChannelMasks);
322        assert_eq!(
323            list.channel_mask_groups(),
324            Some([0x0001, 0x10A0, 0, 0, 0, 0])
325        );
326
327        let enabled: Vec<u8> = list.enabled_channels().collect();
328        assert_eq!(enabled, [0, 21, 23, 28]);
329
330        assert_eq!(list.enables(21), Some(true));
331        assert_eq!(list.enables(22), Some(false));
332        assert_eq!(list.enables(96), None);
333        assert_eq!(list.frequencies_hz(), None);
334        assert_eq!(
335            CfList::channel_masks([0x0001, 0x10A0, 0, 0, 0, 0]),
336            list,
337            "building the same masks gives the same bytes"
338        );
339    }
340
341    #[test]
342    fn the_notes_under_the_800_mhz_list_name_the_channels_they_say() {
343        // RP002-1.0.5 section 3.3.1.1 writes each group in the order its bytes travel, as its
344        // example above does. The three EU868 default channels are ChMaskGrp1 = 0x000E,
345        // which is bits 9 to 11 of the group, channels 25 to 27.
346        let defaults = CfList::from_bytes(hex("0000000e000000000000000000000001"));
347        assert_eq!(
348            defaults.enabled_channels().collect::<Vec<u8>>(),
349            [25, 26, 27]
350        );
351
352        // The four RFID channels of ETSI EN 302 208 are ChMaskGrp0 = 0x0020 and
353        // ChMaskGrp1 = 0x4900: channels 13, 16, 19 and 22.
354        let rfid = CfList::from_bytes(hex("00204900000000000000000000000001"));
355        assert_eq!(
356            rfid.enabled_channels().collect::<Vec<u8>>(),
357            [13, 16, 19, 22]
358        );
359    }
360
361    #[test]
362    fn a_frequency_the_bytes_cannot_carry_is_refused() {
363        assert_eq!(
364            CfList::frequencies([868_100_050, 0, 0, 0, 0]),
365            Err(LorawanError::MalformedFrame),
366            "not a whole number of hundreds of hertz"
367        );
368        assert_eq!(
369            CfList::frequencies([1_677_721_600, 0, 0, 0, 0]),
370            Err(LorawanError::MalformedFrame),
371            "one step past the three bytes"
372        );
373        assert_eq!(
374            CfList::frequencies([99_999_900, 0, 0, 0, 0]),
375            Err(LorawanError::MalformedFrame),
376            "below 100 MHz is reserved"
377        );
378        assert!(CfList::frequencies([1_677_721_500, 0, 0, 0, 0]).is_ok());
379    }
380
381    #[test]
382    fn a_reserved_type_keeps_its_bytes_and_reads_as_neither_form() {
383        let list = CfList::from_bytes(hex("0102030405060708090a0b0c0d0e0f07"));
384        assert_eq!(list.kind(), CfListKind::Reserved(7));
385        assert_eq!(list.frequencies_hz(), None);
386        assert_eq!(list.channel_mask_groups(), None);
387        assert_eq!(list.enabled_channels().count(), 0);
388        assert_eq!(list.to_bytes(), hex("0102030405060708090a0b0c0d0e0f07"));
389    }
390}