lorawan_device/region/fixed_channel_plans/
join_channels.rs

1use super::*;
2use core::cmp::Ordering;
3
4#[derive(Clone, Default)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub(crate) struct JoinChannels {
7    /// The maximum amount of times we attempt to join on the preferred subband.
8    max_retries: usize,
9    /// The amount of times we've currently attempted to join on the preferred subband.
10    pub num_retries: usize,
11    /// Preferred subband
12    preferred_subband: Option<Subband>,
13    /// Channels that have been attempted.
14    pub(crate) available_channels: AvailableChannels,
15    /// The channel used for the previous join request.
16    pub(crate) previous_channel: u8,
17}
18
19impl JoinChannels {
20    pub(crate) fn has_bias_and_not_exhausted(&self) -> bool {
21        // there are remaining retries AND we have not yet been reset
22        self.preferred_subband.is_some()
23            && self.num_retries < self.max_retries
24            && self.num_retries != 0
25    }
26
27    /// The first data channel will always be some random channel (possibly the same as previous)
28    /// of the preferred subband. Returns None if there is no preferred subband.
29    pub(crate) fn first_data_channel(&mut self, rng: &mut impl RngCore) -> Option<u8> {
30        if self.preferred_subband.is_some() && self.num_retries != 0 {
31            self.clear_join_bias();
32            // determine which subband the successful join was sent on
33            let sb = if self.previous_channel < 64 {
34                self.previous_channel / 8
35            } else {
36                self.previous_channel % 8
37            };
38            // pick another channel on that subband
39            Some((rng.next_u32() & 0b111) as u8 + (sb * 8))
40        } else {
41            None
42        }
43    }
44
45    pub(crate) fn set_join_bias(&mut self, subband: Subband, max_retries: usize) {
46        self.preferred_subband = Some(subband);
47        self.max_retries = max_retries;
48    }
49
50    pub(crate) fn clear_join_bias(&mut self) {
51        self.preferred_subband = None;
52        self.max_retries = 0;
53    }
54
55    /// To be called after a join accept is received. Resets state for the next join attempt.
56    pub(crate) fn reset(&mut self) {
57        self.num_retries = 0;
58        self.available_channels = AvailableChannels::default();
59    }
60
61    pub(crate) fn get_next_channel(&mut self, rng: &mut impl RngCore) -> u8 {
62        match (self.preferred_subband, self.num_retries.cmp(&self.max_retries)) {
63            (Some(sb), Ordering::Less) => {
64                self.num_retries += 1;
65                // pick a  random number 0-7 on the preferred subband
66                // NB: we don't use 500 kHz channels
67                let channel = (rng.next_u32() % 8) as u8 + ((sb as usize - 1) as u8 * 8);
68                if self.num_retries == self.max_retries {
69                    // this is our last try with our favorite subband, so will initialize the
70                    // standard join logic with the channel we just tried. This will ensure
71                    // standard and compliant behavior when num_retries is set to 1.
72                    self.available_channels.previous = Some(channel);
73                    self.available_channels.data.set_channel(channel.into(), false);
74                }
75                self.previous_channel = channel;
76                channel
77            }
78            _ => {
79                self.num_retries += 1;
80                self.available_channels.get_next(rng)
81            }
82        }
83    }
84}
85
86#[derive(Clone, Default)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
88pub(crate) struct AvailableChannels {
89    data: ChannelMask<9>,
90    previous: Option<u8>,
91}
92
93impl AvailableChannels {
94    fn is_exhausted(&self) -> bool {
95        // check if every underlying byte is entirely cleared to 0
96        for byte in self.data.as_ref() {
97            if *byte != 0 {
98                return false;
99            }
100        }
101        true
102    }
103
104    fn get_next(&mut self, rng: &mut impl RngCore) -> u8 {
105        // this guarantees that there will be _some_ open channel available
106        if self.is_exhausted() {
107            self.reset();
108        }
109
110        let channel = self.get_next_channel_inner(rng);
111        // mark the channel invalid for future selection
112        self.data.set_channel(channel.into(), false);
113        self.previous = Some(channel);
114        channel
115    }
116
117    fn get_next_channel_inner(&mut self, rng: &mut impl RngCore) -> u8 {
118        if let Some(previous) = self.previous {
119            // choose the next one by possibly wrapping around
120            let next = (previous + 8) % 72;
121            // if the channel is valid, great!
122            if self.data.is_enabled(next.into()).unwrap() {
123                next
124            } else {
125                // We've wrapped around to our original random bank.
126                // Randomly select a new channel on the original bank.
127                // NB: there shall always be something because this will be the first
128                // bank to get exhausted and the caller of this function will reset
129                // when the last one is exhausted.
130                let bank = next / 8;
131                let mut entropy = rng.next_u32();
132                let mut channel = (entropy & 0b111) as u8 + bank * 8;
133                let mut entropy_used = 1;
134                loop {
135                    if self.data.is_enabled(channel.into()).unwrap() {
136                        return channel;
137                    } else {
138                        // we've used 30 of the 32 bits of entropy. reset the byte
139                        if entropy_used == 10 {
140                            entropy = rng.next_u32();
141                            entropy_used = 0;
142                        }
143                        entropy >>= 3;
144                        entropy_used += 1;
145                        channel = (entropy & 0b111) as u8 + bank * 8;
146                    }
147                }
148            }
149        } else {
150            // pick a completely random channel on the bottom 64
151            // NB: all channels are currently valid
152            (rng.next_u32() as u8) & 0b111111
153        }
154    }
155
156    fn reset(&mut self) {
157        self.data = ChannelMask::default();
158        self.previous = None;
159    }
160}
161
162/// This macro implements public functions relating to a fixed plan region. This is preferred to a
163/// trait implementation because the user does not have to worry about importing the trait to make
164/// use of these functions.
165macro_rules! impl_join_bias {
166    ($region:ident) => {
167        impl $region {
168            /// Create this struct directly if you want to specify a subband on which to bias the join process.
169            pub fn new() -> Self {
170                Self::default()
171            }
172
173            /// Specify a preferred subband when joining the network. Only the first join attempt
174            /// will occur on this subband. After that, each bank will be attempted sequentially
175            /// as described in the US915/AU915 regional specifications.
176            pub fn set_join_bias(&mut self, subband: Subband) {
177                self.0.join_channels.set_join_bias(subband, 1)
178            }
179
180            /// # ⚠️Warning⚠️
181            ///
182            /// This method is explicitly not compliant with the LoRaWAN spec when more than one
183            /// try is attempted.
184            ///
185            /// This method is similar to `set_join_bias`, but allows you to specify a potentially
186            /// non-compliant amount of times your preferred join subband should be attempted.
187            ///
188            /// It is recommended to set a low number (ie, < 10) of join retries using the
189            /// preferred subband. The reason for this is if you *only* try to join
190            /// with a channel bias, and the network is configured to use a
191            /// strictly different set of channels than the ones you provide, the
192            /// network will NEVER be joined.
193            pub fn set_join_bias_and_noncompliant_retries(
194                &mut self,
195                subband: Subband,
196                max_retries: usize,
197            ) {
198                self.0.join_channels.set_join_bias(subband, max_retries)
199            }
200
201            pub fn clear_join_bias(&mut self) {
202                self.0.join_channels.clear_join_bias()
203            }
204        }
205    };
206}
207
208#[cfg(feature = "region-au915")]
209impl_join_bias!(AU915);
210#[cfg(feature = "region-us915")]
211impl_join_bias!(US915);
212
213#[cfg(test)]
214mod test {
215    use super::*;
216    use crate::mac::Response;
217    use crate::{
218        mac::{Mac, SendData},
219        test_util::{get_key, handle_join_request, Uplink},
220        AppEui, AppKey, DevEui, NetworkCredentials,
221    };
222    use heapless::Vec;
223    use lorawan::default_crypto::DefaultFactory;
224
225    #[test]
226    fn test_join_channels_standard() {
227        let mut rng = rand_core::OsRng;
228        // run the test a bunch of times due to the rng
229        for _ in 0..100 {
230            let mut join_channels = JoinChannels::default();
231            let first_channel = join_channels.get_next_channel(&mut rng);
232            // the first channel is always in the bottom 64
233            assert!(first_channel < 64);
234            let next_channel = join_channels.get_next_channel(&mut rng);
235            // the next channel is always incremented by 8, since we always have
236            // the fat bank (channels 64-71)
237            assert_eq!(next_channel, first_channel + 8);
238            // we generate 6 more channels
239            for _ in 0..7 {
240                let c = join_channels.get_next_channel(&mut rng);
241                assert!(c < 72);
242            }
243            // after 8 tries, we should be back at the original bank but on a different channel
244            let ninth_channel = join_channels.get_next_channel(&mut rng);
245            assert_eq!(ninth_channel / 8, first_channel / 8);
246            assert_ne!(ninth_channel, first_channel);
247        }
248    }
249
250    #[test]
251    fn test_join_channels_standard_exhausted() {
252        let mut rng = rand_core::OsRng;
253
254        let mut join_channels = JoinChannels::default();
255        let first_channel = join_channels.get_next_channel(&mut rng);
256        // the first channel is always in the bottom 64
257        assert!(first_channel < 64);
258        let next_channel = join_channels.get_next_channel(&mut rng);
259        // the next channel is always incremented by 8, since we always have
260        // the fat bank (channels 64-71)
261        assert_eq!(next_channel, first_channel + 8);
262        // we generate 6000
263        for _ in 0..6000 {
264            let c = join_channels.get_next_channel(&mut rng);
265            assert!(c < 72);
266        }
267    }
268
269    #[test]
270    fn test_join_channels_biased() {
271        let mut rng = rand_core::OsRng;
272        // run the test a bunch of times due to the rng
273        for _ in 0..100 {
274            let mut join_channels = JoinChannels::default();
275            join_channels.set_join_bias(Subband::_2, 1);
276            let first_channel = join_channels.get_next_channel(&mut rng);
277            // the first is on subband 2
278            assert!(first_channel > 7);
279            assert!(first_channel < 16);
280            let next_channel = join_channels.get_next_channel(&mut rng);
281            // the next channel is always incremented by 8, since we always have
282            // the fat bank (channels 64-71)
283            assert_eq!(next_channel, first_channel + 8);
284            // we generate 6 more channels
285            for _ in 0..7 {
286                let c = join_channels.get_next_channel(&mut rng);
287                assert!(c < 72);
288            }
289            // after 8 tries, we should be back at the biased bank but on a different channel
290            let ninth_channel = join_channels.get_next_channel(&mut rng);
291            assert_eq!(ninth_channel / 8, first_channel / 8);
292            assert_ne!(ninth_channel, first_channel);
293        }
294    }
295
296    #[test]
297    fn test_full_mac_compliant_bias() {
298        let mut us915 = US915::new();
299        us915.set_join_bias(Subband::_2);
300        let mut mac = Mac::new(us915.into(), 21, 2);
301
302        let mut buf: RadioBuffer<255> = RadioBuffer::new();
303        let (tx_config, _len) = mac.join_otaa::<DefaultFactory, _, 255>(
304            &mut rand::rngs::OsRng,
305            NetworkCredentials::new(
306                AppEui::from([0x0; 8]),
307                DevEui::from([0x0; 8]),
308                AppKey::from(get_key()),
309            ),
310            &mut buf,
311        );
312        // Confirm that the join request occurs on our subband
313        assert!(
314            tx_config.rf.frequency >= 903_900_000,
315            "Unexpected frequency: {} is below 903.9 MHz!",
316            tx_config.rf.frequency
317        );
318        assert!(
319            tx_config.rf.frequency <= 905_300_000,
320            "Unexpected frequency: {} is above 905.3 MHz!",
321            tx_config.rf.frequency
322        );
323        let mut downlinks: Vec<_, 3> = Vec::new();
324        let mut data = std::vec::Vec::new();
325        data.extend_from_slice(buf.as_ref_for_read());
326        let uplink = Uplink::new(buf.as_ref_for_read(), tx_config).unwrap();
327
328        let mut rx_buf = [0; 255];
329        let len = handle_join_request::<0>(Some(uplink), tx_config.rf, &mut rx_buf);
330        buf.clear();
331        buf.extend_from_slice(&rx_buf[..len]).unwrap();
332        let response = mac.handle_rx::<DefaultFactory, 255, 3>(&mut buf, &mut downlinks);
333        if let Response::JoinSuccess = response {
334        } else {
335            panic!("Did not receive join success");
336        }
337        let (tx_config, _len) = mac
338            .send::<DefaultFactory, _, 255>(
339                &mut rand::rngs::OsRng,
340                &mut buf,
341                &SendData { fport: 1, data: &[0x0; 1], confirmed: false },
342            )
343            .unwrap();
344        // Confirm that the first data frame occurs on our subband
345        assert!(
346            tx_config.rf.frequency >= 903_900_000,
347            "Unexpected frequency: {} is below 903.9 MHz!",
348            tx_config.rf.frequency
349        );
350        assert!(
351            tx_config.rf.frequency <= 905_300_000,
352            "Unexpected frequency: {} is above 905.3 MHz!",
353            tx_config.rf.frequency
354        );
355    }
356
357    #[test]
358    fn test_full_mac_non_compliant_bias() {
359        let mut us915 = US915::new();
360        us915.set_join_bias_and_noncompliant_retries(Subband::_2, 8);
361        let mut mac = Mac::new(us915.into(), 21, 2);
362
363        let mut buf: RadioBuffer<255> = RadioBuffer::new();
364        let (tx_config, _len) = mac.join_otaa::<DefaultFactory, _, 255>(
365            &mut rand::rngs::OsRng,
366            NetworkCredentials::new(
367                AppEui::from([0x0; 8]),
368                DevEui::from([0x0; 8]),
369                AppKey::from(get_key()),
370            ),
371            &mut buf,
372        );
373        // Confirm that the join request occurs on our subband
374        assert!(
375            tx_config.rf.frequency >= 903_900_000,
376            "Unexpected frequency: {} is below 903.9 MHz!",
377            tx_config.rf.frequency
378        );
379        assert!(
380            tx_config.rf.frequency <= 905_300_000,
381            "Unexpected frequency: {} is above 905.3 MHz!",
382            tx_config.rf.frequency
383        );
384        let mut downlinks: Vec<_, 3> = Vec::new();
385        let mut data = std::vec::Vec::new();
386        data.extend_from_slice(buf.as_ref_for_read());
387        let uplink = Uplink::new(buf.as_ref_for_read(), tx_config).unwrap();
388
389        let mut rx_buf = [0; 255];
390        let len = handle_join_request::<0>(Some(uplink), tx_config.rf, &mut rx_buf);
391        buf.clear();
392        buf.extend_from_slice(&rx_buf[..len]).unwrap();
393        let response = mac.handle_rx::<DefaultFactory, 255, 3>(&mut buf, &mut downlinks);
394        if let Response::JoinSuccess = response {
395        } else {
396            panic!("Did not receive JoinSuccess")
397        }
398        for _ in 0..8 {
399            let (tx_config, _len) = mac
400                .send::<DefaultFactory, _, 255>(
401                    &mut rand::rngs::OsRng,
402                    &mut buf,
403                    &SendData { fport: 1, data: &[0x0; 1], confirmed: false },
404                )
405                .unwrap();
406            // Confirm that the first data frame occurs on our subband
407            assert!(
408                tx_config.rf.frequency >= 903_900_000,
409                "Unexpected frequency: {} is below 903.9 MHz!",
410                tx_config.rf.frequency
411            );
412            assert!(
413                tx_config.rf.frequency <= 905_300_000,
414                "Unexpected frequency: {} is above 905.3 MHz!",
415                tx_config.rf.frequency
416            );
417            mac.rx2_complete();
418        }
419    }
420}