lorawan_device/async_device/
mod.rs

1//! LoRaWAN device which uses async-await for driving the protocol state against pin and timer events,
2//! allowing for asynchronous radio implementations. Requires the `async` feature.
3use super::mac::Mac;
4
5use super::mac::{self, Frame, Window};
6pub use super::{
7    mac::{NetworkCredentials, SendData, Session},
8    region::{self, Region},
9    Downlink, JoinMode,
10};
11use crate::log;
12use core::marker::PhantomData;
13use futures::{future::select, future::Either, pin_mut};
14use heapless::Vec;
15use lorawan::{self, keys::CryptoFactory};
16use rand_core::RngCore;
17
18pub use crate::region::DR;
19use crate::{radio::RadioBuffer, rng};
20
21pub mod radio;
22
23#[cfg(feature = "embassy-time")]
24mod embassy_time;
25#[cfg(feature = "embassy-time")]
26pub use embassy_time::EmbassyTimer;
27
28#[cfg(test)]
29mod test;
30
31use self::radio::{RxQuality, RxStatus};
32
33/// Type representing a LoRaWAN capable device.
34///
35/// A device is bound to the following types:
36/// - R: An asynchronous radio implementation
37/// - T: An asynchronous timer implementation
38/// - C: A CryptoFactory implementation
39/// - RNG: A random number generator implementation. An external RNG may be provided, or you may use a builtin PRNG by
40///   providing a random seed
41/// - N: The size of the radio buffer. Generally, this should be set to 256 to support the largest possible LoRa frames.
42/// - D: The amount of downlinks that may be buffered. This is used to support Class C operation. See below for more.
43///
44/// Note that the const generics N and D are used to configure the size of the radio buffer and the number of downlinks
45/// that may be buffered. The defaults are 256 and 1 respectively which should be fine for Class A devices. **For Class
46/// C operation**, it is recommended to increase D to at least 2, if not 3. This is because during the RX1/RX2 windows
47/// after a Class A transmit, it is possible to receive Class C downlinks (in additional to any RX1/RX2 responses!).
48pub struct Device<R, C, T, G, const N: usize = 256, const D: usize = 1>
49where
50    R: radio::PhyRxTx + Timings,
51    T: radio::Timer,
52    C: CryptoFactory + Default,
53    G: RngCore,
54{
55    crypto: PhantomData<C>,
56    radio: R,
57    rng: G,
58    timer: T,
59    mac: Mac,
60    radio_buffer: RadioBuffer<N>,
61    downlink: Vec<Downlink, D>,
62    class_c: bool,
63}
64
65#[cfg_attr(feature = "defmt", derive(defmt::Format))]
66#[derive(Debug)]
67pub enum Error<R> {
68    Radio(R),
69    Mac(mac::Error),
70}
71
72#[cfg_attr(feature = "defmt", derive(defmt::Format))]
73#[derive(Debug)]
74pub enum SendResponse {
75    DownlinkReceived(mac::FcntDown),
76    SessionExpired,
77    NoAck,
78    RxComplete,
79}
80
81#[cfg_attr(feature = "defmt", derive(defmt::Format))]
82#[derive(Debug)]
83pub enum JoinResponse {
84    JoinSuccess,
85    NoJoinAccept,
86}
87
88impl<R> From<mac::Error> for Error<R> {
89    fn from(e: mac::Error) -> Self {
90        Error::Mac(e)
91    }
92}
93
94impl<R, C, T, const N: usize> Device<R, C, T, rng::Prng, N>
95where
96    R: radio::PhyRxTx + Timings,
97    C: CryptoFactory + Default,
98    T: radio::Timer,
99{
100    /// Create a new [`Device`] by providing your own random seed. Using this method, [`Device`] will internally
101    /// use an algorithmic PRNG. Depending on your use case, this may or may not be faster than using your own
102    /// hardware RNG.
103    ///
104    /// # ⚠️Warning⚠️
105    ///
106    /// This function must **always** be called with a new randomly generated seed! **Never** call this function more
107    /// than once using the same seed. Generate the seed using a true random number generator. Using the same seed will
108    /// leave you vulnerable to replay attacks.
109    pub fn new_with_seed(region: region::Configuration, radio: R, timer: T, seed: u64) -> Self {
110        Device::new_with_seed_and_session(region, radio, timer, seed, None)
111    }
112
113    /// Create a new [`Device`] by providing your own random seed. Also optionally provide your own [`Session`].
114    /// Using this method, [`Device`] will internally use an algorithmic PRNG to generate random numbers. Depending on
115    /// your use case, this may or may not be faster than using your own hardware RNG.
116    ///
117    /// # ⚠️Warning⚠️
118    ///
119    /// This function must **always** be called with a new randomly generated seed! **Never** call this function more
120    /// than once using the same seed. Generate the seed using a true random number generator. Using the same seed will
121    /// leave you vulnerable to replay attacks.
122    pub fn new_with_seed_and_session(
123        region: region::Configuration,
124        radio: R,
125        timer: T,
126        seed: u64,
127        session: Option<Session>,
128    ) -> Self {
129        let rng = rng::Prng::new(seed);
130        Device::new_with_session(region, radio, timer, rng, session)
131    }
132}
133
134impl<R, C, T, G, const N: usize, const D: usize> Device<R, C, T, G, N, D>
135where
136    R: radio::PhyRxTx + Timings,
137    C: CryptoFactory + Default,
138    T: radio::Timer,
139    G: RngCore,
140{
141    /// Create a new instance of [`Device`] with a RNG external to the LoRa chip. You must provide your own RNG
142    /// implementing [`RngCore`].
143    ///
144    /// See also [`new_with_seed`](Device::new_with_seed) to let [`Device`] use a builtin PRNG by providing a random
145    /// seed.
146    pub fn new(region: region::Configuration, radio: R, timer: T, rng: G) -> Self {
147        Device::new_with_session(region, radio, timer, rng, None)
148    }
149
150    /// Create a new [`Device`] and provide an optional [`Session`].
151    pub fn new_with_session(
152        region: region::Configuration,
153        radio: R,
154        timer: T,
155        rng: G,
156        session: Option<Session>,
157    ) -> Self {
158        let mut mac = Mac::new(region, R::MAX_RADIO_POWER, R::ANTENNA_GAIN);
159        if let Some(session) = session {
160            mac.set_session(session);
161        }
162        Self {
163            crypto: PhantomData,
164            radio,
165            rng,
166            mac,
167            radio_buffer: RadioBuffer::new(),
168            timer,
169            downlink: Vec::new(),
170            class_c: false,
171        }
172    }
173
174    /// Enables Class C behavior. Note that Class C downlinks are not possible until a confirmed
175    /// uplink is sent to the LNS.
176
177    pub fn enable_class_c(&mut self) {
178        self.class_c = true;
179    }
180
181    /// Disables Class C behavior. Note that an uplink must be set for the radio to disable
182    /// Class C listen.
183    pub fn disable_class_c(&mut self) {
184        self.class_c = false;
185    }
186
187    pub fn get_session(&mut self) -> Option<&Session> {
188        self.mac.get_session()
189    }
190
191    pub fn get_region(&mut self) -> &region::Configuration {
192        &self.mac.region
193    }
194
195    pub fn get_radio(&mut self) -> &R {
196        &self.radio
197    }
198
199    pub fn get_mut_radio(&mut self) -> &mut R {
200        &mut self.radio
201    }
202
203    /// Retrieve the current data rate being used by this device.
204    pub fn get_datarate(&mut self) -> DR {
205        self.mac.configuration.data_rate
206    }
207
208    /// Set the data rate being used by this device. This overrides the region default.
209    pub fn set_datarate(&mut self, datarate: DR) {
210        self.mac.configuration.data_rate = datarate;
211    }
212
213    /// Join the LoRaWAN network asynchronously. The returned future completes when
214    /// the LoRaWAN network has been joined successfully, or an error has occurred.
215    ///
216    /// Repeatedly calling join using OTAA will result in a new LoRaWAN session to be created.
217    ///
218    /// Note that for a Class C enabled device, you must repeatedly send *confirmed* uplink until
219    /// LoRaWAN Network Server (LNS) confirmation after joining.
220    pub async fn join(&mut self, join_mode: &JoinMode) -> Result<JoinResponse, Error<R::PhyError>> {
221        match join_mode {
222            JoinMode::OTAA { deveui, appeui, appkey } => {
223                let (tx_config, _) = self.mac.join_otaa::<C, G, N>(
224                    &mut self.rng,
225                    NetworkCredentials::new(*appeui, *deveui, *appkey),
226                    &mut self.radio_buffer,
227                );
228
229                // Transmit the join payload
230                let ms = self
231                    .radio
232                    .tx(tx_config, self.radio_buffer.as_ref_for_read())
233                    .await
234                    .map_err(Error::Radio)?;
235
236                // Receive join response within RX window
237                self.timer.reset();
238                Ok(self.rx_downlink(&Frame::Join, ms).await?.try_into()?)
239            }
240            JoinMode::ABP { newskey, appskey, devaddr } => {
241                self.mac.join_abp(*newskey, *appskey, *devaddr);
242                Ok(JoinResponse::JoinSuccess)
243            }
244        }
245    }
246
247    /// Send data on a given port with the expected confirmation. If downlink data is provided, the
248    /// data is copied into the provided byte slice.
249    ///
250    /// The returned future completes when the data have been sent successfully and downlink data,
251    /// if any, is available by calling take_downlink. Response::DownlinkReceived indicates a
252    /// downlink is available.
253    ///
254    /// In Class C mode, it is possible to get one or more downlinks and `Reponse::DownlinkReceived`
255    /// maybe not even be indicated. It is recommended to call `take_downlink` after `send` until
256    /// it returns `None`.
257    pub async fn send(
258        &mut self,
259        data: &[u8],
260        fport: u8,
261        confirmed: bool,
262    ) -> Result<SendResponse, Error<R::PhyError>> {
263        // Prepare transmission buffer
264        let (tx_config, _fcnt_up) = self.mac.send::<C, G, N>(
265            &mut self.rng,
266            &mut self.radio_buffer,
267            &SendData { data, fport, confirmed },
268        )?;
269        // Transmit our data packet
270        let ms = self
271            .radio
272            .tx(tx_config, self.radio_buffer.as_ref_for_read())
273            .await
274            .map_err(Error::Radio)?;
275
276        // Wait for received data within window
277        self.timer.reset();
278        Ok(self.rx_downlink(&Frame::Data, ms).await?.try_into()?)
279    }
280
281    /// Take the downlink data from the device. This is typically called after a
282    /// `Response::DownlinkReceived` is returned from `send`. This call consumes the downlink
283    /// data. If no downlink data is available, `None` is returned.
284    pub fn take_downlink(&mut self) -> Option<Downlink> {
285        self.downlink.pop()
286    }
287
288    async fn window_complete(&mut self) -> Result<(), Error<R::PhyError>> {
289        if self.class_c {
290            let rf_config = self.mac.get_rxc_config();
291            self.radio.setup_rx(rf_config).await.map_err(Error::Radio)
292        } else {
293            self.radio.low_power().await.map_err(Error::Radio)
294        }
295    }
296
297    async fn between_windows(
298        &mut self,
299        duration: u32,
300    ) -> Result<Option<mac::Response>, Error<R::PhyError>> {
301        if !self.class_c {
302            self.radio.low_power().await.map_err(Error::Radio)?;
303            self.timer.at(duration.into()).await;
304            return Ok(None);
305        }
306
307        #[allow(unused)]
308        enum RxcWindowResponse<F: futures::Future<Output = ()> + Sized + Unpin> {
309            Rx(usize, RxQuality, F),
310            Timeout(u32),
311        }
312
313        /// RXC window listen until timeout
314        async fn rxc_listen_until_timeout<F, R, const N: usize>(
315            radio: &mut R,
316            rx_buf: &mut RadioBuffer<N>,
317            window_duration: u32,
318            timeout_fut: F,
319        ) -> RxcWindowResponse<F>
320        where
321            F: futures::Future<Output = ()> + Sized + Unpin,
322            R: radio::PhyRxTx + Timings,
323        {
324            let rx_fut = radio.rx_continuous(rx_buf.as_mut());
325            pin_mut!(rx_fut);
326            // Wait until either a RF frame is received or the timeout future fires
327            match select(rx_fut, timeout_fut).await {
328                Either::Left((r, timeout_fut)) => match r {
329                    Ok((sz, q)) => RxcWindowResponse::Rx(sz, q, timeout_fut),
330                    // Ignore errors or timeouts and wait until the RX2 window is ready.
331                    // Setting timeout to 0 ensures that `window_duration != rx2_start_delay`
332                    _ => {
333                        timeout_fut.await;
334                        RxcWindowResponse::Timeout(0)
335                    }
336                },
337                // Timeout! Prepare for the next window.
338                Either::Right(_) => RxcWindowResponse::Timeout(window_duration),
339            }
340        }
341
342        // Class C listen while waiting for the window
343        let rx_config = self.mac.get_rxc_config();
344        log::debug!("Configuring RXC window with config {}.", rx_config);
345        self.radio.setup_rx(rx_config).await.map_err(Error::Radio)?;
346        let mut response = None;
347        let timeout_fut = self.timer.at(duration.into());
348        pin_mut!(timeout_fut);
349        let mut maybe_timeout_fut = Some(timeout_fut);
350
351        // Keep processing RF frames until the timeout fires
352        while let Some(timeout_fut) = maybe_timeout_fut.take() {
353            match rxc_listen_until_timeout(
354                &mut self.radio,
355                &mut self.radio_buffer,
356                duration,
357                timeout_fut,
358            )
359            .await
360            {
361                RxcWindowResponse::Rx(sz, _, timeout_fut) => {
362                    log::debug!("RXC window received {} bytes.", sz);
363                    self.radio_buffer.set_pos(sz);
364                    match self
365                        .mac
366                        .handle_rxc::<C, N, D>(&mut self.radio_buffer, &mut self.downlink)?
367                    {
368                        mac::Response::NoUpdate => {
369                            log::debug!("RXC frame was invalid.");
370                            self.radio_buffer.clear();
371                            // we preserve the timeout
372                            maybe_timeout_fut = Some(timeout_fut);
373                        }
374                        r => {
375                            log::debug!("Valid RXC frame received.");
376                            self.radio_buffer.clear();
377                            response = Some(r);
378                            // more than one downlink may be received so we preserve the timeout
379                            maybe_timeout_fut = Some(timeout_fut);
380                        }
381                    }
382                }
383                RxcWindowResponse::Timeout(_) => return Ok(response),
384            };
385        }
386        Ok(response)
387    }
388
389    /// Attempt to receive data within RX1 and RX2 windows. This function will populate the
390    /// provided buffer with data if received.
391    async fn rx_downlink(
392        &mut self,
393        frame: &Frame,
394        window_delay: u32,
395    ) -> Result<mac::Response, Error<R::PhyError>> {
396        self.radio_buffer.clear();
397
398        let rx1_start_delay = self.mac.get_rx_delay(frame, &Window::_1) + window_delay
399            - self.radio.get_rx_window_lead_time_ms();
400
401        log::debug!("Starting RX1 in {} ms.", rx1_start_delay);
402        // sleep or RXC
403        let _ = self.between_windows(rx1_start_delay).await?;
404
405        // RX1
406        let rx_config =
407            self.mac.get_rx_config(self.radio.get_rx_window_buffer(), frame, &Window::_1);
408        log::debug!("Configuring RX1 window with config {}.", rx_config);
409        self.radio.setup_rx(rx_config).await.map_err(Error::Radio)?;
410
411        if let Some(response) = self.rx_listen().await? {
412            log::debug!("RX1 received {}", response);
413            return Ok(response);
414        }
415
416        let rx2_start_delay = self.mac.get_rx_delay(frame, &Window::_2) + window_delay
417            - self.radio.get_rx_window_lead_time_ms();
418        log::debug!("RX1 did not receive anything. Awaiting RX2 for {} ms.", rx2_start_delay);
419        // sleep or RXC
420        let _ = self.between_windows(rx2_start_delay).await?;
421
422        // RX2
423        let rx_config =
424            self.mac.get_rx_config(self.radio.get_rx_window_buffer(), frame, &Window::_2);
425        log::debug!("Configuring RX2 window with config {}.", rx_config);
426        self.radio.setup_rx(rx_config).await.map_err(Error::Radio)?;
427
428        if let Some(response) = self.rx_listen().await? {
429            log::debug!("RX2 received {}", response);
430            return Ok(response);
431        }
432        log::debug!("RX2 did not receive anything.");
433        Ok(self.mac.rx2_complete())
434    }
435
436    async fn rx_listen(&mut self) -> Result<Option<mac::Response>, Error<R::PhyError>> {
437        let response =
438            match self.radio.rx_single(self.radio_buffer.as_mut()).await.map_err(Error::Radio)? {
439                RxStatus::Rx(s, _q) => {
440                    self.radio_buffer.set_pos(s);
441                    match self.mac.handle_rx::<C, N, D>(&mut self.radio_buffer, &mut self.downlink)
442                    {
443                        mac::Response::NoUpdate => None,
444                        r => Some(r),
445                    }
446                }
447                RxStatus::RxTimeout => None,
448            };
449        self.radio_buffer.clear();
450        self.window_complete().await?;
451        Ok(response)
452    }
453
454    /// When not involved in sending and RX1/RX2 windows, a class C configured device will be
455    /// listening to RXC frames. The caller is expected to be awaiting this message at all times.
456    pub async fn rxc_listen(&mut self) -> Result<mac::Response, Error<R::PhyError>> {
457        loop {
458            let (sz, _rx_quality) =
459                self.radio.rx_continuous(self.radio_buffer.as_mut()).await.map_err(Error::Radio)?;
460            self.radio_buffer.set_pos(sz);
461            match self.mac.handle_rxc::<C, N, D>(&mut self.radio_buffer, &mut self.downlink)? {
462                mac::Response::NoUpdate => {
463                    self.radio_buffer.clear();
464                }
465                r => {
466                    self.radio_buffer.clear();
467                    return Ok(r);
468                }
469            }
470        }
471    }
472}
473
474/// Allows to fine-tune the beginning and end of the receive windows for a specific board and runtime.
475pub trait Timings {
476    /// How many milliseconds before the RX window should the SPI transaction start?
477    /// This value needs to account for the time it takes to wake up the radio and start the SPI transaction, as
478    /// well as any non-deterministic delays in the system.
479    fn get_rx_window_lead_time_ms(&self) -> u32;
480
481    /// Explicitly set the amount of milliseconds to listen before the window starts. By default, the pessimistic assumption
482    /// of `Self::get_rx_window_lead_time_ms` will be used. If you override, be sure that: `Self::get_rx_window_buffer
483    /// < Self::get_rx_window_lead_time_ms`.
484    fn get_rx_window_buffer(&self) -> u32 {
485        self.get_rx_window_lead_time_ms()
486    }
487}