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}