Init
This commit is contained in:
1
lorawan-device-patch/.cargo-ok
Normal file
1
lorawan-device-patch/.cargo-ok
Normal file
@@ -0,0 +1 @@
|
||||
{"v":1}
|
||||
6
lorawan-device-patch/.cargo_vcs_info.json
Normal file
6
lorawan-device-patch/.cargo_vcs_info.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "0efdb6b26407053c877cc6659a76c83b2dbdd952"
|
||||
},
|
||||
"path_in_vcs": "lorawan-device"
|
||||
}
|
||||
33
lorawan-device-patch/CHANGELOG.md
Normal file
33
lorawan-device-patch/CHANGELOG.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
|
||||
to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [v0.12.1]
|
||||
|
||||
- Allow multilple RXC frames during RXC window ([#217](https://github.com/lora-rs/lora-rs/pull/217))
|
||||
- Individually feature-gate all regions ([#216](https://github.com/lora-rs/lora-rs/pull/236))
|
||||
- Fix log macro for
|
||||
error ([commit](https://github.com/lora-rs/lora-rs/pull/256/commits/99cb10b77baf0f1c51ae97b1830a80b4873864e1))
|
||||
|
||||
## [v0.12.0]
|
||||
|
||||
- Fixes bug related to FCntUp and confirmed uplink ([#182](https://github.com/lora-rs/lora-rs/pull/182))
|
||||
- Extend PhyRxTx to support antenna gain and max power ([#159](https://github.com/lora-rs/lora-rs/pull/159))
|
||||
- Implement Class C functionality for async_device ([#158](https://github.com/lora-rs/lora-rs/pull/159))
|
||||
- Implement rapid subband acquisition, aka "Join Bias" for US915 & AU915
|
||||
([#110](https://github.com/lora-rs/lora-rs/pull/110) / [#170](https://github.com/lora-rs/lora-rs/pull/170) )
|
||||
- Develops `async_device` API to provide `JoinResponse` and
|
||||
`SendResponse` (#[144](https://github.com/lora-rs/lora-rs/pull/144))
|
||||
- Develops `nb_device` API around sending a join to be consistent with
|
||||
`async_device` (#[144](https://github.com/lora-rs/lora-rs/pull/144))
|
||||
- Refactor `external-lora-phy` in `lorawan-device` as `lorawan-radio` in
|
||||
`lora-phy` ([#189](https://github.com/lora-rs/lora-rs/pull/189))
|
||||
- Add `Timer` implementation based on embassy-time ([#171](https://github.com/lora-rs/lora-rs/pull/171))
|
||||
- Use radio timeout for end of RX1 and RX2 windows; preamble detection cancels
|
||||
timeout ([#204](https://github.com/lora-rs/lora-rs/pull/204))
|
||||
- Remove `async` feature-flag as async fn in traits is stable
|
||||
|
||||
Change tracking starting at version 0.11.0.
|
||||
141
lorawan-device-patch/Cargo.toml
Normal file
141
lorawan-device-patch/Cargo.toml
Normal file
@@ -0,0 +1,141 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
rust-version = "1.75"
|
||||
name = "lorawan-device"
|
||||
version = "0.12.2"
|
||||
authors = [
|
||||
"Louis Thiery <thiery.louis@gmail.com>",
|
||||
"Ulf Lilleengen <lulf@redhat.com>",
|
||||
]
|
||||
build = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A Rust LoRaWAN device stack implementation"
|
||||
readme = "README.md"
|
||||
categories = [
|
||||
"embedded",
|
||||
"hardware-support",
|
||||
"no-std",
|
||||
]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/lora-rs/lora-rs"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = [
|
||||
"--cfg",
|
||||
"docsrs",
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "lorawan_device"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.defmt]
|
||||
version = "0.3"
|
||||
optional = true
|
||||
|
||||
[dependencies.document-features]
|
||||
version = "0.2.8"
|
||||
|
||||
[dependencies.embassy-time]
|
||||
version = "0.3.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.fastrand]
|
||||
version = "2"
|
||||
default-features = false
|
||||
|
||||
[dependencies.futures]
|
||||
version = "0.3"
|
||||
default-features = false
|
||||
|
||||
[dependencies.generic-array]
|
||||
version = "0.14"
|
||||
|
||||
[dependencies.heapless]
|
||||
version = "0.7"
|
||||
|
||||
[dependencies.lora-modulation]
|
||||
version = ">=0.1.2"
|
||||
default-features = false
|
||||
|
||||
[dependencies.lorawan]
|
||||
version = "0.9"
|
||||
default-features = false
|
||||
|
||||
[dependencies.rand_core]
|
||||
version = "0.6"
|
||||
default-features = false
|
||||
|
||||
[dependencies.seq-macro]
|
||||
version = "0.3.5"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.lazy_static]
|
||||
version = "1"
|
||||
|
||||
[dev-dependencies.rand]
|
||||
version = "0"
|
||||
features = ["getrandom"]
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
version = "1"
|
||||
features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
]
|
||||
|
||||
[features]
|
||||
all-regions = [
|
||||
"region-as923-1",
|
||||
"region-as923-2",
|
||||
"region-as923-3",
|
||||
"region-as923-4",
|
||||
"region-au915",
|
||||
"region-eu433",
|
||||
"region-eu868",
|
||||
"region-in865",
|
||||
"region-us915",
|
||||
]
|
||||
default = ["all-regions"]
|
||||
default-crypto = ["lorawan/default-crypto"]
|
||||
defmt = [
|
||||
"dep:defmt",
|
||||
"lorawan/defmt",
|
||||
"lora-modulation/defmt",
|
||||
]
|
||||
embassy-time = ["dep:embassy-time"]
|
||||
region-as923-1 = []
|
||||
region-as923-2 = []
|
||||
region-as923-3 = []
|
||||
region-as923-4 = []
|
||||
region-au915 = []
|
||||
region-eu433 = []
|
||||
region-eu868 = []
|
||||
region-in865 = []
|
||||
region-us915 = []
|
||||
serde = [
|
||||
"dep:serde",
|
||||
"lorawan/serde",
|
||||
]
|
||||
74
lorawan-device-patch/Cargo.toml.orig
generated
Normal file
74
lorawan-device-patch/Cargo.toml.orig
generated
Normal file
@@ -0,0 +1,74 @@
|
||||
[package]
|
||||
name = "lorawan-device"
|
||||
version = "0.12.2"
|
||||
authors = ["Louis Thiery <thiery.louis@gmail.com>", "Ulf Lilleengen <lulf@redhat.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.75"
|
||||
categories = [
|
||||
"embedded",
|
||||
"hardware-support",
|
||||
"no-std",
|
||||
]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
description = "A Rust LoRaWAN device stack implementation"
|
||||
repository = "https://github.com/lora-rs/lora-rs"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
lora-modulation = { path = "../lora-modulation", version = ">=0.1.2", default-features = false }
|
||||
lorawan = { path = "../lorawan-encoding", version = "0.9", default-features = false }
|
||||
heapless = "0.7"
|
||||
generic-array = "0.14"
|
||||
defmt = { version = "0.3", optional = true }
|
||||
fastrand = { version = "2", default-features = false }
|
||||
futures = { version = "0.3", default-features = false }
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
||||
seq-macro = "0.3.5"
|
||||
document-features = "0.2.8"
|
||||
embassy-time = { version = "0.3.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt", "macros", "time", "sync"] }
|
||||
rand = { version = "0", features = ["getrandom"] }
|
||||
lazy_static = "1"
|
||||
|
||||
[features]
|
||||
default = ["all-regions"]
|
||||
all-regions = ["region-as923-1", "region-as923-2", "region-as923-3", "region-as923-4", "region-au915", "region-eu433", "region-eu868", "region-in865", "region-us915"]
|
||||
|
||||
## Use pure Rust implementations of [`AES`](https://docs.rs/aes/latest/aes/) and [`CMAC`](https://docs.rs/cmac/latest/cmac/) for the LoRaWAN crypto layer.
|
||||
default-crypto = ["lorawan/default-crypto"]
|
||||
|
||||
## Use [`defmt`](https://docs.rs/defmt/latest/defmt/) for logging.
|
||||
defmt = ["dep:defmt", "lorawan/defmt", "lora-modulation/defmt"]
|
||||
|
||||
## Provide an `async_device::Timer` impl based on `embassy-time`.
|
||||
embassy-time = ["dep:embassy-time"]
|
||||
|
||||
## Enable [`serde`](https://docs.rs/serde/latest/serde/) serialization/deserialization for data structures.
|
||||
serde = ["dep:serde", "lorawan/serde"]
|
||||
|
||||
## Enable support for AS923-1 region (by default all regions are enabled).
|
||||
region-as923-1 = []
|
||||
## Enable support for AS923-2 region (by default all regions are enabled).
|
||||
region-as923-2 = []
|
||||
## Enable support for AS923-3 region (by default all regions are enabled).
|
||||
region-as923-3 = []
|
||||
## Enable support for AS923-4 region (by default all regions are enabled).
|
||||
region-as923-4 = []
|
||||
## Enable support for AU915 region (by default all regions are enabled).
|
||||
region-au915 = []
|
||||
## Enable support for EU433 region (by default all regions are enabled).
|
||||
region-eu433 = []
|
||||
## Enable support for EU868 region (by default all regions are enabled).
|
||||
region-eu868 = []
|
||||
## Enable support for IN865 region (by default all regions are enabled).
|
||||
region-in865 = []
|
||||
## Enable support for US915 region (by default all regions are enabled).
|
||||
region-us915 = []
|
||||
|
||||
38
lorawan-device-patch/README.md
Normal file
38
lorawan-device-patch/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# lorawan-device
|
||||
|
||||
[![Latest Version]][crates.io]
|
||||
[![Docs]][doc.rs]
|
||||
|
||||
This is an experimental LoRaWAN device stack with both non-blocking (`nb_device`) and async (`async_device`)
|
||||
implementations. Both implementations have their respective `radio::PhyRxTx` traits that describe the radio interface
|
||||
required.
|
||||
|
||||
Note: The `lorawan-radio` feature in the `lora-phy` crate provides `LorawanRadio` as an async implementation of
|
||||
`radio::PhyRxTx`.
|
||||
|
||||
Both stacks share a dependency on the internal module, `mac` where LoRaWAN 1.0.x is approximately implemented:
|
||||
|
||||
- Class A device behavior
|
||||
- Class C device behavior (async only)
|
||||
- Over-the-Air Activation (OTAA) and Activation by Personalization (ABP)
|
||||
- CFList is supported for fixed and dynamic channel plans
|
||||
- Regional support for AS923_1, AS923_2, AS923_3, AS923_4, AU915, EU868, EU433, IN865, US915 (note: regional power
|
||||
limits are not enforced ([#168](https://github.com/lora-rs/lora-rs/issues/168))
|
||||
|
||||
**Currently, MAC commands are minimally mocked. For example, an ADRReq is responded with an ADRResp, but not much
|
||||
is actually done with the payload**.
|
||||
|
||||
Furthermore, both async and non-blocking implementation do not implement any retries for failed joins or failed
|
||||
confirmed uplinks. It is up to the client to implement retry behavior; see the examples for more.
|
||||
|
||||
Please see [examples](https://github.com/lora-rs/lora-rs/tree/main/examples) for usage.
|
||||
|
||||
A public chat on LoRa/LoRaWAN topics using Rust is [here](https://matrix.to/#/#public-lora-wan-rs:matrix.org).
|
||||
|
||||
[Latest Version]: https://img.shields.io/crates/v/lorawan-device.svg
|
||||
|
||||
[crates.io]: https://crates.io/crates/lorawan-device
|
||||
|
||||
[Docs]: https://docs.rs/lorawan-device/badge.svg
|
||||
|
||||
[doc.rs]: https://docs.rs/lorawan-device
|
||||
34
lorawan-device-patch/src/async_device/embassy_time.rs
Normal file
34
lorawan-device-patch/src/async_device/embassy_time.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use embassy_time::{Duration, Instant};
|
||||
|
||||
use super::radio::Timer;
|
||||
|
||||
/// A [`Timer`] implementation based on [`embassy-time`].
|
||||
pub struct EmbassyTimer {
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl EmbassyTimer {
|
||||
pub fn new() -> Self {
|
||||
Self { start: Instant::now() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EmbassyTimer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Timer for EmbassyTimer {
|
||||
fn reset(&mut self) {
|
||||
self.start = Instant::now();
|
||||
}
|
||||
|
||||
async fn at(&mut self, millis: u64) {
|
||||
embassy_time::Timer::at(self.start + Duration::from_millis(millis)).await
|
||||
}
|
||||
|
||||
async fn delay_ms(&mut self, millis: u64) {
|
||||
embassy_time::Timer::after_millis(millis).await
|
||||
}
|
||||
}
|
||||
487
lorawan-device-patch/src/async_device/mod.rs
Normal file
487
lorawan-device-patch/src/async_device/mod.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
//! LoRaWAN device which uses async-await for driving the protocol state against pin and timer events,
|
||||
//! allowing for asynchronous radio implementations. Requires the `async` feature.
|
||||
use super::mac::Mac;
|
||||
|
||||
use super::mac::{self, Frame, Window};
|
||||
pub use super::{
|
||||
mac::{NetworkCredentials, SendData, Session},
|
||||
region::{self, Region},
|
||||
Downlink, JoinMode,
|
||||
};
|
||||
use crate::log;
|
||||
use core::marker::PhantomData;
|
||||
use futures::{future::select, future::Either, pin_mut};
|
||||
use heapless::Vec;
|
||||
use lorawan::{self, keys::CryptoFactory};
|
||||
use rand_core::RngCore;
|
||||
|
||||
pub use crate::region::DR;
|
||||
use crate::{radio::RadioBuffer, rng};
|
||||
|
||||
pub mod radio;
|
||||
|
||||
#[cfg(feature = "embassy-time")]
|
||||
mod embassy_time;
|
||||
#[cfg(feature = "embassy-time")]
|
||||
pub use embassy_time::EmbassyTimer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use self::radio::{RxQuality, RxStatus};
|
||||
|
||||
/// Type representing a LoRaWAN capable device.
|
||||
///
|
||||
/// A device is bound to the following types:
|
||||
/// - R: An asynchronous radio implementation
|
||||
/// - T: An asynchronous timer implementation
|
||||
/// - C: A CryptoFactory implementation
|
||||
/// - RNG: A random number generator implementation. An external RNG may be provided, or you may use a builtin PRNG by
|
||||
/// providing a random seed
|
||||
/// - N: The size of the radio buffer. Generally, this should be set to 256 to support the largest possible LoRa frames.
|
||||
/// - D: The amount of downlinks that may be buffered. This is used to support Class C operation. See below for more.
|
||||
///
|
||||
/// Note that the const generics N and D are used to configure the size of the radio buffer and the number of downlinks
|
||||
/// that may be buffered. The defaults are 256 and 1 respectively which should be fine for Class A devices. **For Class
|
||||
/// C operation**, it is recommended to increase D to at least 2, if not 3. This is because during the RX1/RX2 windows
|
||||
/// after a Class A transmit, it is possible to receive Class C downlinks (in additional to any RX1/RX2 responses!).
|
||||
pub struct Device<R, C, T, G, const N: usize = 256, const D: usize = 1>
|
||||
where
|
||||
R: radio::PhyRxTx + Timings,
|
||||
T: radio::Timer,
|
||||
C: CryptoFactory + Default,
|
||||
G: RngCore,
|
||||
{
|
||||
crypto: PhantomData<C>,
|
||||
radio: R,
|
||||
rng: G,
|
||||
timer: T,
|
||||
mac: Mac,
|
||||
radio_buffer: RadioBuffer<N>,
|
||||
downlink: Vec<Downlink, D>,
|
||||
class_c: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[derive(Debug)]
|
||||
pub enum Error<R> {
|
||||
Radio(R),
|
||||
Mac(mac::Error),
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[derive(Debug)]
|
||||
pub enum SendResponse {
|
||||
DownlinkReceived(mac::FcntDown),
|
||||
SessionExpired,
|
||||
NoAck,
|
||||
RxComplete,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[derive(Debug)]
|
||||
pub enum JoinResponse {
|
||||
JoinSuccess,
|
||||
NoJoinAccept,
|
||||
}
|
||||
|
||||
impl<R> From<mac::Error> for Error<R> {
|
||||
fn from(e: mac::Error) -> Self {
|
||||
Error::Mac(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, C, T, const N: usize> Device<R, C, T, rng::Prng, N>
|
||||
where
|
||||
R: radio::PhyRxTx + Timings,
|
||||
C: CryptoFactory + Default,
|
||||
T: radio::Timer,
|
||||
{
|
||||
/// Create a new [`Device`] by providing your own random seed. Using this method, [`Device`] will internally
|
||||
/// use an algorithmic PRNG. Depending on your use case, this may or may not be faster than using your own
|
||||
/// hardware RNG.
|
||||
///
|
||||
/// # ⚠️Warning⚠️
|
||||
///
|
||||
/// This function must **always** be called with a new randomly generated seed! **Never** call this function more
|
||||
/// than once using the same seed. Generate the seed using a true random number generator. Using the same seed will
|
||||
/// leave you vulnerable to replay attacks.
|
||||
pub fn new_with_seed(region: region::Configuration, radio: R, timer: T, seed: u64) -> Self {
|
||||
Device::new_with_seed_and_session(region, radio, timer, seed, None)
|
||||
}
|
||||
|
||||
/// Create a new [`Device`] by providing your own random seed. Also optionally provide your own [`Session`].
|
||||
/// Using this method, [`Device`] will internally use an algorithmic PRNG to generate random numbers. Depending on
|
||||
/// your use case, this may or may not be faster than using your own hardware RNG.
|
||||
///
|
||||
/// # ⚠️Warning⚠️
|
||||
///
|
||||
/// This function must **always** be called with a new randomly generated seed! **Never** call this function more
|
||||
/// than once using the same seed. Generate the seed using a true random number generator. Using the same seed will
|
||||
/// leave you vulnerable to replay attacks.
|
||||
pub fn new_with_seed_and_session(
|
||||
region: region::Configuration,
|
||||
radio: R,
|
||||
timer: T,
|
||||
seed: u64,
|
||||
session: Option<Session>,
|
||||
) -> Self {
|
||||
let rng = rng::Prng::new(seed);
|
||||
Device::new_with_session(region, radio, timer, rng, session)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, C, T, G, const N: usize, const D: usize> Device<R, C, T, G, N, D>
|
||||
where
|
||||
R: radio::PhyRxTx + Timings,
|
||||
C: CryptoFactory + Default,
|
||||
T: radio::Timer,
|
||||
G: RngCore,
|
||||
{
|
||||
/// Create a new instance of [`Device`] with a RNG external to the LoRa chip. You must provide your own RNG
|
||||
/// implementing [`RngCore`].
|
||||
///
|
||||
/// See also [`new_with_seed`](Device::new_with_seed) to let [`Device`] use a builtin PRNG by providing a random
|
||||
/// seed.
|
||||
pub fn new(region: region::Configuration, radio: R, timer: T, rng: G) -> Self {
|
||||
Device::new_with_session(region, radio, timer, rng, None)
|
||||
}
|
||||
|
||||
/// Create a new [`Device`] and provide an optional [`Session`].
|
||||
pub fn new_with_session(
|
||||
region: region::Configuration,
|
||||
radio: R,
|
||||
timer: T,
|
||||
rng: G,
|
||||
session: Option<Session>,
|
||||
) -> Self {
|
||||
let mut mac = Mac::new(region, R::MAX_RADIO_POWER, R::ANTENNA_GAIN);
|
||||
if let Some(session) = session {
|
||||
mac.set_session(session);
|
||||
}
|
||||
Self {
|
||||
crypto: PhantomData,
|
||||
radio,
|
||||
rng,
|
||||
mac,
|
||||
radio_buffer: RadioBuffer::new(),
|
||||
timer,
|
||||
downlink: Vec::new(),
|
||||
class_c: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables Class C behavior. Note that Class C downlinks are not possible until a confirmed
|
||||
/// uplink is sent to the LNS.
|
||||
|
||||
pub fn enable_class_c(&mut self) {
|
||||
self.class_c = true;
|
||||
}
|
||||
|
||||
/// Disables Class C behavior. Note that an uplink must be set for the radio to disable
|
||||
/// Class C listen.
|
||||
pub fn disable_class_c(&mut self) {
|
||||
self.class_c = false;
|
||||
}
|
||||
|
||||
pub fn get_session(&mut self) -> Option<&Session> {
|
||||
self.mac.get_session()
|
||||
}
|
||||
|
||||
pub fn get_region(&mut self) -> ®ion::Configuration {
|
||||
&self.mac.region
|
||||
}
|
||||
|
||||
pub fn get_radio(&mut self) -> &R {
|
||||
&self.radio
|
||||
}
|
||||
|
||||
pub fn get_mut_radio(&mut self) -> &mut R {
|
||||
&mut self.radio
|
||||
}
|
||||
|
||||
/// Retrieve the current data rate being used by this device.
|
||||
pub fn get_datarate(&mut self) -> DR {
|
||||
self.mac.configuration.data_rate
|
||||
}
|
||||
|
||||
/// Set the data rate being used by this device. This overrides the region default.
|
||||
pub fn set_datarate(&mut self, datarate: DR) {
|
||||
self.mac.configuration.data_rate = datarate;
|
||||
}
|
||||
|
||||
/// Join the LoRaWAN network asynchronously. The returned future completes when
|
||||
/// the LoRaWAN network has been joined successfully, or an error has occurred.
|
||||
///
|
||||
/// Repeatedly calling join using OTAA will result in a new LoRaWAN session to be created.
|
||||
///
|
||||
/// Note that for a Class C enabled device, you must repeatedly send *confirmed* uplink until
|
||||
/// LoRaWAN Network Server (LNS) confirmation after joining.
|
||||
pub async fn join(&mut self, join_mode: &JoinMode) -> Result<JoinResponse, Error<R::PhyError>> {
|
||||
match join_mode {
|
||||
JoinMode::OTAA { deveui, appeui, appkey } => {
|
||||
let (tx_config, _) = self.mac.join_otaa::<C, G, N>(
|
||||
&mut self.rng,
|
||||
NetworkCredentials::new(*appeui, *deveui, *appkey),
|
||||
&mut self.radio_buffer,
|
||||
);
|
||||
|
||||
// Transmit the join payload
|
||||
let ms = self
|
||||
.radio
|
||||
.tx(tx_config, self.radio_buffer.as_ref_for_read())
|
||||
.await
|
||||
.map_err(Error::Radio)?;
|
||||
|
||||
// Receive join response within RX window
|
||||
self.timer.reset();
|
||||
Ok(self.rx_downlink(&Frame::Join, ms).await?.try_into()?)
|
||||
}
|
||||
JoinMode::ABP { newskey, appskey, devaddr } => {
|
||||
self.mac.join_abp(*newskey, *appskey, *devaddr);
|
||||
Ok(JoinResponse::JoinSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send data on a given port with the expected confirmation. If downlink data is provided, the
|
||||
/// data is copied into the provided byte slice.
|
||||
///
|
||||
/// The returned future completes when the data have been sent successfully and downlink data,
|
||||
/// if any, is available by calling take_downlink. Response::DownlinkReceived indicates a
|
||||
/// downlink is available.
|
||||
///
|
||||
/// In Class C mode, it is possible to get one or more downlinks and `Reponse::DownlinkReceived`
|
||||
/// maybe not even be indicated. It is recommended to call `take_downlink` after `send` until
|
||||
/// it returns `None`.
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
data: &[u8],
|
||||
fport: u8,
|
||||
confirmed: bool,
|
||||
) -> Result<SendResponse, Error<R::PhyError>> {
|
||||
// Prepare transmission buffer
|
||||
let (tx_config, _fcnt_up) = self.mac.send::<C, G, N>(
|
||||
&mut self.rng,
|
||||
&mut self.radio_buffer,
|
||||
&SendData { data, fport, confirmed },
|
||||
)?;
|
||||
// Transmit our data packet
|
||||
let ms = self
|
||||
.radio
|
||||
.tx(tx_config, self.radio_buffer.as_ref_for_read())
|
||||
.await
|
||||
.map_err(Error::Radio)?;
|
||||
|
||||
// Wait for received data within window
|
||||
self.timer.reset();
|
||||
Ok(self.rx_downlink(&Frame::Data, ms).await?.try_into()?)
|
||||
}
|
||||
|
||||
/// Take the downlink data from the device. This is typically called after a
|
||||
/// `Response::DownlinkReceived` is returned from `send`. This call consumes the downlink
|
||||
/// data. If no downlink data is available, `None` is returned.
|
||||
pub fn take_downlink(&mut self) -> Option<Downlink> {
|
||||
self.downlink.pop()
|
||||
}
|
||||
|
||||
async fn window_complete(&mut self) -> Result<(), Error<R::PhyError>> {
|
||||
if self.class_c {
|
||||
let rf_config = self.mac.get_rxc_config();
|
||||
self.radio.setup_rx(rf_config).await.map_err(Error::Radio)
|
||||
} else {
|
||||
self.radio.low_power().await.map_err(Error::Radio)
|
||||
}
|
||||
}
|
||||
|
||||
async fn between_windows(
|
||||
&mut self,
|
||||
duration: u32,
|
||||
) -> Result<Option<mac::Response>, Error<R::PhyError>> {
|
||||
if !self.class_c {
|
||||
self.radio.low_power().await.map_err(Error::Radio)?;
|
||||
self.timer.at(duration.into()).await;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
enum RxcWindowResponse<F: futures::Future<Output=()> + Sized + Unpin> {
|
||||
Rx(usize, RxQuality, F),
|
||||
Timeout(u32),
|
||||
}
|
||||
|
||||
/// RXC window listen until timeout
|
||||
async fn rxc_listen_until_timeout<F, R, const N: usize>(
|
||||
radio: &mut R,
|
||||
rx_buf: &mut RadioBuffer<N>,
|
||||
window_duration: u32,
|
||||
timeout_fut: F,
|
||||
) -> RxcWindowResponse<F>
|
||||
where
|
||||
F: futures::Future<Output=()> + Sized + Unpin,
|
||||
R: radio::PhyRxTx + Timings,
|
||||
{
|
||||
let rx_fut = radio.rx_continuous(rx_buf.as_mut());
|
||||
pin_mut!(rx_fut);
|
||||
// Wait until either a RF frame is received or the timeout future fires
|
||||
match select(rx_fut, timeout_fut).await {
|
||||
Either::Left((r, timeout_fut)) => match r {
|
||||
Ok((sz, q)) => RxcWindowResponse::Rx(sz, q, timeout_fut),
|
||||
// Ignore errors or timeouts and wait until the RX2 window is ready.
|
||||
// Setting timeout to 0 ensures that `window_duration != rx2_start_delay`
|
||||
_ => {
|
||||
timeout_fut.await;
|
||||
RxcWindowResponse::Timeout(0)
|
||||
}
|
||||
},
|
||||
// Timeout! Prepare for the next window.
|
||||
Either::Right(_) => RxcWindowResponse::Timeout(window_duration),
|
||||
}
|
||||
}
|
||||
|
||||
// Class C listen while waiting for the window
|
||||
let rx_config = self.mac.get_rxc_config();
|
||||
log::debug!("Configuring RXC window with config {}.", rx_config);
|
||||
self.radio.setup_rx(rx_config).await.map_err(Error::Radio)?;
|
||||
let mut response = None;
|
||||
let timeout_fut = self.timer.at(duration.into());
|
||||
pin_mut!(timeout_fut);
|
||||
let mut maybe_timeout_fut = Some(timeout_fut);
|
||||
|
||||
// Keep processing RF frames until the timeout fires
|
||||
while let Some(timeout_fut) = maybe_timeout_fut.take() {
|
||||
match rxc_listen_until_timeout(
|
||||
&mut self.radio,
|
||||
&mut self.radio_buffer,
|
||||
duration,
|
||||
timeout_fut,
|
||||
)
|
||||
.await
|
||||
{
|
||||
RxcWindowResponse::Rx(sz, _, timeout_fut) => {
|
||||
log::debug!("RXC window received {} bytes.", sz);
|
||||
self.radio_buffer.set_pos(sz);
|
||||
match self
|
||||
.mac
|
||||
.handle_rxc::<C, N, D>(&mut self.radio_buffer, &mut self.downlink)?
|
||||
{
|
||||
mac::Response::NoUpdate => {
|
||||
log::debug!("RXC frame was invalid.");
|
||||
self.radio_buffer.clear();
|
||||
// we preserve the timeout
|
||||
maybe_timeout_fut = Some(timeout_fut);
|
||||
}
|
||||
r => {
|
||||
log::debug!("Valid RXC frame received.");
|
||||
self.radio_buffer.clear();
|
||||
response = Some(r);
|
||||
// more than one downlink may be received so we preserve the timeout
|
||||
maybe_timeout_fut = Some(timeout_fut);
|
||||
}
|
||||
}
|
||||
}
|
||||
RxcWindowResponse::Timeout(_) => return Ok(response),
|
||||
};
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Attempt to receive data within RX1 and RX2 windows. This function will populate the
|
||||
/// provided buffer with data if received.
|
||||
async fn rx_downlink(
|
||||
&mut self,
|
||||
frame: &Frame,
|
||||
window_delay: u32,
|
||||
) -> Result<mac::Response, Error<R::PhyError>> {
|
||||
self.radio_buffer.clear();
|
||||
|
||||
let rx1_start_delay = self.mac.get_rx_delay(frame, &Window::_1) + window_delay
|
||||
- self.radio.get_rx_window_lead_time_ms();
|
||||
|
||||
log::debug!("Starting RX1 in {} ms.", rx1_start_delay);
|
||||
// sleep or RXC
|
||||
let _ = self.between_windows(rx1_start_delay).await?;
|
||||
|
||||
// RX1
|
||||
let rx_config =
|
||||
self.mac.get_rx_config(self.radio.get_rx_window_buffer(), frame, &Window::_1);
|
||||
log::debug!("Configuring RX1 window with config {}.", rx_config);
|
||||
self.radio.setup_rx(rx_config).await.map_err(Error::Radio)?;
|
||||
|
||||
if let Some(response) = self.rx_listen().await? {
|
||||
log::debug!("RX1 received {}", response);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
let rx2_start_delay = self.mac.get_rx_delay(frame, &Window::_2) + window_delay
|
||||
- self.radio.get_rx_window_lead_time_ms();
|
||||
log::debug!("RX1 did not receive anything. Awaiting RX2 for {} ms.", rx2_start_delay);
|
||||
// sleep or RXC
|
||||
let _ = self.between_windows(rx2_start_delay).await?;
|
||||
|
||||
// RX2
|
||||
let rx_config =
|
||||
self.mac.get_rx_config(self.radio.get_rx_window_buffer(), frame, &Window::_2);
|
||||
log::debug!("Configuring RX2 window with config {}.", rx_config);
|
||||
self.radio.setup_rx(rx_config).await.map_err(Error::Radio)?;
|
||||
|
||||
if let Some(response) = self.rx_listen().await? {
|
||||
log::debug!("RX2 received {}", response);
|
||||
return Ok(response);
|
||||
}
|
||||
log::debug!("RX2 did not receive anything.");
|
||||
Ok(self.mac.rx2_complete())
|
||||
}
|
||||
|
||||
async fn rx_listen(&mut self) -> Result<Option<mac::Response>, Error<R::PhyError>> {
|
||||
let response =
|
||||
match self.radio.rx_single(self.radio_buffer.as_mut()).await.map_err(Error::Radio)? {
|
||||
RxStatus::Rx(s, _q) => {
|
||||
self.radio_buffer.set_pos(s);
|
||||
match self.mac.handle_rx::<C, N, D>(&mut self.radio_buffer, &mut self.downlink)
|
||||
{
|
||||
mac::Response::NoUpdate => None,
|
||||
r => Some(r),
|
||||
}
|
||||
}
|
||||
RxStatus::RxTimeout => None,
|
||||
};
|
||||
self.radio_buffer.clear();
|
||||
self.window_complete().await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// When not involved in sending and RX1/RX2 windows, a class C configured device will be
|
||||
/// listening to RXC frames. The caller is expected to be awaiting this message at all times.
|
||||
pub async fn rxc_listen(&mut self) -> Result<mac::Response, Error<R::PhyError>> {
|
||||
loop {
|
||||
let (sz, _rx_quality) =
|
||||
self.radio.rx_continuous(self.radio_buffer.as_mut()).await.map_err(Error::Radio)?;
|
||||
self.radio_buffer.set_pos(sz);
|
||||
match self.mac.handle_rxc::<C, N, D>(&mut self.radio_buffer, &mut self.downlink)? {
|
||||
mac::Response::NoUpdate => {
|
||||
self.radio_buffer.clear();
|
||||
}
|
||||
r => {
|
||||
self.radio_buffer.clear();
|
||||
return Ok(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows to fine-tune the beginning and end of the receive windows for a specific board and runtime.
|
||||
pub trait Timings {
|
||||
/// How many milliseconds before the RX window should the SPI transaction start?
|
||||
/// This value needs to account for the time it takes to wake up the radio and start the SPI transaction, as
|
||||
/// well as any non-deterministic delays in the system.
|
||||
fn get_rx_window_lead_time_ms(&self) -> u32;
|
||||
|
||||
/// Explicitly set the amount of milliseconds to listen before the window starts. By default, the pessimistic assumption
|
||||
/// of `Self::get_rx_window_lead_time_ms` will be used. If you override, be sure that: `Self::get_rx_window_buffer
|
||||
/// < Self::get_rx_window_lead_time_ms`.
|
||||
fn get_rx_window_buffer(&self) -> u32 {
|
||||
self.get_rx_window_lead_time_ms()
|
||||
}
|
||||
}
|
||||
69
lorawan-device-patch/src/async_device/radio.rs
Normal file
69
lorawan-device-patch/src/async_device/radio.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
pub use crate::radio::{RfConfig, RxConfig, RxMode, RxQuality, TxConfig};
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub struct Error<E>(pub E);
|
||||
|
||||
impl<R> From<Error<R>> for super::Error<R> {
|
||||
fn from(radio_error: Error<R>) -> super::Error<R> {
|
||||
super::Error::Radio(radio_error.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RxStatus {
|
||||
Rx(usize, RxQuality),
|
||||
RxTimeout,
|
||||
}
|
||||
|
||||
/// An asynchronous timer that allows the state machine to await
|
||||
/// between RX windows.
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait Timer {
|
||||
fn reset(&mut self);
|
||||
|
||||
/// Wait until millis milliseconds after reset has passed
|
||||
async fn at(&mut self, millis: u64);
|
||||
|
||||
/// Delay for millis milliseconds
|
||||
async fn delay_ms(&mut self, millis: u64);
|
||||
}
|
||||
|
||||
/// An asynchronous radio implementation that can transmit and receive data.
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait PhyRxTx: Sized {
|
||||
#[cfg(feature = "defmt")]
|
||||
type PhyError: defmt::Format;
|
||||
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
type PhyError;
|
||||
|
||||
/// Board-specific antenna gain and power loss in dBi.
|
||||
const ANTENNA_GAIN: i8 = 0;
|
||||
|
||||
/// Maximum power (dBm) that the radio is able to output. When preparing instructions for radio,
|
||||
/// the value of maximum power will be used as an upper bound.
|
||||
const MAX_RADIO_POWER: u8;
|
||||
|
||||
/// Transmit data buffer with the given transceiver configuration. The returned future
|
||||
/// should only complete once data have been transmitted.
|
||||
async fn tx(&mut self, config: TxConfig, buf: &[u8]) -> Result<u32, Self::PhyError>;
|
||||
|
||||
/// Configures the radio to receive data. This future should not actually await the data itself.
|
||||
async fn setup_rx(&mut self, config: RxConfig) -> Result<(), Self::PhyError>;
|
||||
|
||||
/// Receive data into the provided buffer with the given transceiver configuration. The returned
|
||||
/// future should only complete when RX data has been received. Furthermore, it should be
|
||||
/// possible to await the future again without settings up the receive config again.
|
||||
async fn rx_continuous(
|
||||
&mut self,
|
||||
rx_buf: &mut [u8],
|
||||
) -> Result<(usize, RxQuality), Self::PhyError>;
|
||||
|
||||
/// Receive data into the provided buffer with the given transceiver configuration. The returned
|
||||
/// future should complete when RX data has been received or when the timeout has expired.
|
||||
async fn rx_single(&mut self, buf: &mut [u8]) -> Result<RxStatus, Self::PhyError>;
|
||||
|
||||
/// Puts the radio into a low-power mode
|
||||
async fn low_power(&mut self) -> Result<(), Self::PhyError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
329
lorawan-device-patch/src/async_device/test/mod.rs
Normal file
329
lorawan-device-patch/src/async_device/test/mod.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
radio::{RxQuality, TxConfig},
|
||||
region,
|
||||
test_util::*,
|
||||
};
|
||||
use lorawan::default_crypto::DefaultFactory;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
mod timer;
|
||||
use timer::TestTimer;
|
||||
|
||||
mod radio;
|
||||
use radio::TestRadio;
|
||||
|
||||
mod util;
|
||||
use util::{setup, setup_with_session, setup_with_session_class_c};
|
||||
|
||||
type Device =
|
||||
crate::async_device::Device<TestRadio, DefaultFactory, TestTimer, rand_core::OsRng, 512, 4>;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_join_rx1() {
|
||||
let (radio, timer, mut async_device) = setup();
|
||||
// Run the device
|
||||
let async_device =
|
||||
tokio::spawn(async move { async_device.join(&get_otaa_credentials()).await });
|
||||
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
// Trigger handling of JoinAccept
|
||||
radio.handle_rxtx(handle_join_request::<3>).await;
|
||||
|
||||
// Await the device to return and verify state
|
||||
if let Ok(JoinResponse::JoinSuccess) = async_device.await.unwrap() {
|
||||
assert_eq!(1, timer.get_armed_count().await);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_join_rx2() {
|
||||
let (radio, timer, mut async_device) = setup();
|
||||
// Run the device
|
||||
let async_device =
|
||||
tokio::spawn(async move { async_device.join(&get_otaa_credentials()).await });
|
||||
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
// Trigger end of RX1
|
||||
radio.handle_timeout().await;
|
||||
// Trigger start of RX2
|
||||
timer.fire_most_recent().await;
|
||||
// Pass the join request handler
|
||||
radio.handle_rxtx(handle_join_request::<4>).await;
|
||||
|
||||
// Await the device to return and verify state
|
||||
if async_device.await.unwrap().is_ok() {
|
||||
assert_eq!(2, timer.get_armed_count().await);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_join_accept() {
|
||||
let (radio, timer, mut async_device) = setup();
|
||||
// Run the device
|
||||
let async_device =
|
||||
tokio::spawn(async move { async_device.join(&get_otaa_credentials()).await });
|
||||
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
// Trigger end of RX1
|
||||
radio.handle_timeout().await;
|
||||
// Trigger start of RX2
|
||||
timer.fire_most_recent().await;
|
||||
// Trigger end of RX2
|
||||
radio.handle_timeout().await;
|
||||
|
||||
// Await the device to return and verify state
|
||||
let response = async_device.await.unwrap();
|
||||
if let Ok(JoinResponse::NoJoinAccept) = response {
|
||||
assert_eq!(2, timer.get_armed_count().await);
|
||||
} else {
|
||||
panic!("Unexpected response: {response:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unconfirmed_uplink_no_downlink() {
|
||||
let (radio, timer, mut async_device) = setup_with_session();
|
||||
let send_await_complete = Arc::new(Mutex::new(false));
|
||||
|
||||
// Run the device
|
||||
let complete = send_await_complete.clone();
|
||||
let async_device = tokio::spawn(async move {
|
||||
let response = async_device.send(&[1, 2, 3], 3, false).await;
|
||||
|
||||
let mut complete = complete.lock().await;
|
||||
*complete = true;
|
||||
response
|
||||
});
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
assert!(!*send_await_complete.lock().await);
|
||||
// Trigger end of RX1
|
||||
radio.handle_timeout().await;
|
||||
// Trigger start of RX2
|
||||
timer.fire_most_recent().await;
|
||||
assert!(!*send_await_complete.lock().await);
|
||||
// Trigger end of RX2
|
||||
radio.handle_timeout().await;
|
||||
|
||||
match async_device.await.unwrap() {
|
||||
Ok(SendResponse::RxComplete) => (),
|
||||
_ => panic!(),
|
||||
}
|
||||
assert!(*send_await_complete.lock().await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_confirmed_uplink_no_ack() {
|
||||
let (radio, timer, mut async_device) = setup_with_session();
|
||||
let send_await_complete = Arc::new(Mutex::new(false));
|
||||
|
||||
// Run the device
|
||||
let complete = send_await_complete.clone();
|
||||
let async_device = tokio::spawn(async move {
|
||||
let response = async_device.send(&[1, 2, 3], 3, true).await;
|
||||
|
||||
let mut complete = complete.lock().await;
|
||||
*complete = true;
|
||||
response
|
||||
});
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
assert!(!*send_await_complete.lock().await);
|
||||
// Trigger end of RX1
|
||||
radio.handle_timeout().await;
|
||||
// Trigger start of RX2
|
||||
timer.fire_most_recent().await;
|
||||
assert!(!*send_await_complete.lock().await);
|
||||
// Trigger end of RX1
|
||||
radio.handle_timeout().await;
|
||||
|
||||
match async_device.await.unwrap() {
|
||||
Ok(SendResponse::NoAck) => (),
|
||||
_ => panic!(),
|
||||
}
|
||||
assert!(*send_await_complete.lock().await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_confirmed_uplink_with_ack_rx1() {
|
||||
let (radio, timer, mut async_device) = setup_with_session();
|
||||
let send_await_complete = Arc::new(Mutex::new(false));
|
||||
|
||||
// Run the device
|
||||
let complete = send_await_complete.clone();
|
||||
let async_device = tokio::spawn(async move {
|
||||
let response = async_device.send(&[1, 2, 3], 3, true).await;
|
||||
|
||||
let mut complete = complete.lock().await;
|
||||
*complete = true;
|
||||
response
|
||||
});
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
assert!(!*send_await_complete.lock().await);
|
||||
|
||||
// Send a downlink with confirmation
|
||||
radio.handle_rxtx(handle_data_uplink_with_link_adr_req::<0, 0>).await;
|
||||
match async_device.await.unwrap() {
|
||||
Ok(SendResponse::DownlinkReceived(_)) => (),
|
||||
_ => {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_confirmed_uplink_with_ack_rx2() {
|
||||
let (radio, timer, mut async_device) = setup_with_session();
|
||||
let send_await_complete = Arc::new(Mutex::new(false));
|
||||
|
||||
// Run the device
|
||||
let complete = send_await_complete.clone();
|
||||
let async_device = tokio::spawn(async move {
|
||||
let response = async_device.send(&[1, 2, 3], 3, true).await;
|
||||
|
||||
let mut complete = complete.lock().await;
|
||||
*complete = true;
|
||||
response
|
||||
});
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
assert!(!*send_await_complete.lock().await);
|
||||
// Trigger end of RX1
|
||||
radio.handle_timeout().await;
|
||||
assert!(!*send_await_complete.lock().await);
|
||||
// Trigger start of RX2
|
||||
timer.fire_most_recent().await;
|
||||
|
||||
// Send a downlink confirmation
|
||||
radio.handle_rxtx(handle_data_uplink_with_link_adr_req::<0, 0>).await;
|
||||
|
||||
match async_device.await.unwrap() {
|
||||
Ok(SendResponse::DownlinkReceived(_)) => (),
|
||||
_ => {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_link_adr_ans() {
|
||||
let (radio, timer, mut async_device) = setup_with_session();
|
||||
let send_await_complete = Arc::new(Mutex::new(false));
|
||||
|
||||
// Run the device
|
||||
let complete = send_await_complete.clone();
|
||||
let async_device = tokio::spawn(async move {
|
||||
async_device.send(&[1, 2, 3], 3, true).await.unwrap();
|
||||
{
|
||||
let mut complete = complete.lock().await;
|
||||
*complete = true;
|
||||
}
|
||||
async_device.send(&[1, 2, 3], 3, true).await
|
||||
});
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
// Send a downlink with confirmation
|
||||
radio.handle_rxtx(handle_data_uplink_with_link_adr_req::<0, 0>).await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(15)).await;
|
||||
assert!(*send_await_complete.lock().await);
|
||||
// at this point, the device thread should be sending the second frame
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
// Send a downlink with confirmation
|
||||
radio.handle_rxtx(handle_data_uplink_with_link_adr_ans).await;
|
||||
match async_device.await.unwrap() {
|
||||
Ok(SendResponse::DownlinkReceived(_)) => (),
|
||||
_ => {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_class_c_data_before_rx1() {
|
||||
let (radio, timer, mut async_device) = setup_with_session_class_c().await;
|
||||
// Run the device
|
||||
let task = tokio::spawn(async move {
|
||||
let response = async_device.send(&[1, 2, 3], 3, true).await;
|
||||
(async_device, response)
|
||||
});
|
||||
|
||||
// send first downlink before RX1
|
||||
radio.handle_rxtx(class_c_downlink::<1>).await;
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
// We expect FCntUp 1 up since the test util for Class C setup sends first frame
|
||||
// We set FcntDown to 2, since ACK to setup (1) and Class C downlink above (2)
|
||||
radio.handle_rxtx(handle_data_uplink_with_link_adr_req::<1, 2>).await;
|
||||
let (mut device, response) = task.await.unwrap();
|
||||
match response {
|
||||
Ok(SendResponse::DownlinkReceived(_)) => (),
|
||||
_ => {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
let _ = device.take_downlink().unwrap();
|
||||
let _ = device.take_downlink().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_class_c_data_before_rx2() {
|
||||
let (radio, timer, mut async_device) = setup_with_session_class_c().await;
|
||||
// Run the device
|
||||
let task = tokio::spawn(async move {
|
||||
let response = async_device.send(&[1, 2, 3], 3, true).await;
|
||||
(async_device, response)
|
||||
});
|
||||
|
||||
// send first downlink before RX1
|
||||
// Trigger beginning of RX1
|
||||
timer.fire_most_recent().await;
|
||||
// Trigger end of RX1
|
||||
radio.handle_timeout().await;
|
||||
|
||||
radio.handle_rxtx(class_c_downlink::<1>).await;
|
||||
// Trigger beginning of RX2
|
||||
timer.fire_most_recent().await;
|
||||
// We expect FCntUp 1 up since the test util for Class C setup sends first frame
|
||||
// We set FcntDown to 2, since ACK to setup (1) and Class C downlink above (2)
|
||||
radio.handle_rxtx(handle_data_uplink_with_link_adr_req::<1, 2>).await;
|
||||
let (mut device, response) = task.await.unwrap();
|
||||
match response {
|
||||
Ok(SendResponse::DownlinkReceived(_)) => (),
|
||||
_ => {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
let _ = device.take_downlink().unwrap();
|
||||
let _ = device.take_downlink().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_class_c_async_down() {
|
||||
let (radio, _timer, mut async_device) = setup_with_session_class_c().await;
|
||||
// Run the device
|
||||
let task = tokio::spawn(async move {
|
||||
let response = async_device.rxc_listen().await;
|
||||
(async_device, response)
|
||||
});
|
||||
|
||||
radio.handle_rxtx(class_c_downlink::<1>).await;
|
||||
let (mut device, response) = task.await.unwrap();
|
||||
match response {
|
||||
Ok(mac::Response::DownlinkReceived(_)) => (),
|
||||
_ => {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
let _ = device.take_downlink().unwrap();
|
||||
}
|
||||
111
lorawan-device-patch/src/async_device/test/radio.rs
Normal file
111
lorawan-device-patch/src/async_device/test/radio.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use crate::async_device::radio::{PhyRxTx, RxConfig, RxStatus};
|
||||
use std::sync::Arc;
|
||||
use tokio::{
|
||||
sync::{mpsc, Mutex},
|
||||
time,
|
||||
};
|
||||
impl TestRadio {
|
||||
pub fn new() -> (RadioChannel, Self) {
|
||||
let (tx, rx) = mpsc::channel(2);
|
||||
let last_uplink = Arc::new(Mutex::new(None));
|
||||
(
|
||||
RadioChannel { tx, last_uplink: last_uplink.clone() },
|
||||
Self { rx, last_uplink, current_config: None },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Msg {
|
||||
RxTx(RxTxHandler),
|
||||
Timeout,
|
||||
}
|
||||
|
||||
pub struct TestRadio {
|
||||
current_config: Option<RxConfig>,
|
||||
last_uplink: Arc<Mutex<Option<Uplink>>>,
|
||||
rx: mpsc::Receiver<Msg>,
|
||||
}
|
||||
|
||||
impl PhyRxTx for TestRadio {
|
||||
type PhyError = &'static str;
|
||||
|
||||
const MAX_RADIO_POWER: u8 = 26;
|
||||
|
||||
const ANTENNA_GAIN: i8 = 0;
|
||||
|
||||
async fn tx(&mut self, config: TxConfig, buffer: &[u8]) -> Result<u32, Self::PhyError> {
|
||||
let length = buffer.len();
|
||||
// stash the uplink, to be consumed by channel or by rx handler
|
||||
let mut last_uplink = self.last_uplink.lock().await;
|
||||
*last_uplink = Some(Uplink::new(buffer, config).map_err(|_| "Parse error")?);
|
||||
Ok(length as u32)
|
||||
}
|
||||
|
||||
async fn setup_rx(&mut self, config: RxConfig) -> Result<(), Self::PhyError> {
|
||||
self.current_config = Some(config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rx_continuous(
|
||||
&mut self,
|
||||
rx_buf: &mut [u8],
|
||||
) -> Result<(usize, RxQuality), Self::PhyError> {
|
||||
let msg = self.rx.recv().await.unwrap();
|
||||
match msg {
|
||||
Msg::RxTx(handler) => {
|
||||
let last_uplink = self.last_uplink.lock().await;
|
||||
// a quick yield to let timer arm
|
||||
time::sleep(time::Duration::from_millis(5)).await;
|
||||
if let Some(config) = &self.current_config {
|
||||
let length = handler(last_uplink.clone(), config.rf, rx_buf);
|
||||
Ok((length, RxQuality::new(-80, 0)))
|
||||
} else {
|
||||
panic!("Trying to rx before settings config!")
|
||||
}
|
||||
}
|
||||
Msg::Timeout => Err("Unexpected Timeout"),
|
||||
}
|
||||
}
|
||||
async fn rx_single(&mut self, rx_buf: &mut [u8]) -> Result<RxStatus, Self::PhyError> {
|
||||
let msg = self.rx.recv().await.unwrap();
|
||||
match msg {
|
||||
Msg::RxTx(handler) => {
|
||||
let last_uplink = self.last_uplink.lock().await;
|
||||
// a quick yield to let timer arm
|
||||
time::sleep(time::Duration::from_millis(5)).await;
|
||||
if let Some(config) = &self.current_config {
|
||||
let length = handler(last_uplink.clone(), config.rf, rx_buf);
|
||||
Ok(RxStatus::Rx(length, RxQuality::new(-80, 0)))
|
||||
} else {
|
||||
panic!("Trying to rx before settings config!")
|
||||
}
|
||||
}
|
||||
Msg::Timeout => Ok(RxStatus::RxTimeout),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Timings for TestRadio {
|
||||
fn get_rx_window_lead_time_ms(&self) -> u32 {
|
||||
10
|
||||
}
|
||||
}
|
||||
|
||||
/// A channel for the test fixture to trigger fires and to check calls.
|
||||
pub struct RadioChannel {
|
||||
#[allow(unused)]
|
||||
last_uplink: Arc<Mutex<Option<Uplink>>>,
|
||||
tx: mpsc::Sender<Msg>,
|
||||
}
|
||||
|
||||
impl RadioChannel {
|
||||
pub async fn handle_rxtx(&self, handler: RxTxHandler) {
|
||||
tokio::time::sleep(time::Duration::from_millis(5)).await;
|
||||
self.tx.send(Msg::RxTx(handler)).await.unwrap();
|
||||
}
|
||||
pub async fn handle_timeout(&self) {
|
||||
tokio::time::sleep(time::Duration::from_millis(5)).await;
|
||||
self.tx.send(Msg::Timeout).await.unwrap();
|
||||
}
|
||||
}
|
||||
73
lorawan-device-patch/src/async_device/test/timer.rs
Normal file
73
lorawan-device-patch/src/async_device/test/timer.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use crate::async_device::radio::Timer;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
impl TestTimer {
|
||||
pub fn new() -> (TimerChannel, Self) {
|
||||
let tx = Arc::new(Mutex::new(HashMap::new()));
|
||||
let armed_count = Arc::new(Mutex::new(0));
|
||||
(
|
||||
TimerChannel { tx: tx.clone(), armed_count: armed_count.clone() },
|
||||
Self { tx, armed_count },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestTimer {
|
||||
armed_count: Arc<Mutex<usize>>,
|
||||
tx: Arc<Mutex<HashMap<usize, mpsc::Sender<()>>>>,
|
||||
}
|
||||
|
||||
impl TestTimer {
|
||||
async fn create_channel_and_await(&mut self) {
|
||||
let (tx, mut rx) = mpsc::channel(1);
|
||||
{
|
||||
*self.armed_count.lock().await += 1;
|
||||
let mut tx_map = self.tx.lock().await;
|
||||
tx_map.insert(*self.armed_count.lock().await, tx);
|
||||
}
|
||||
rx.recv().await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Timer for TestTimer {
|
||||
fn reset(&mut self) {}
|
||||
|
||||
async fn at(&mut self, _millis: u64) {
|
||||
self.create_channel_and_await().await;
|
||||
}
|
||||
|
||||
async fn delay_ms(&mut self, _millis: u64) {
|
||||
self.create_channel_and_await().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// A channel for the test fixture to trigger fires and to check calls.
|
||||
pub struct TimerChannel {
|
||||
armed_count: Arc<Mutex<usize>>,
|
||||
tx: Arc<Mutex<HashMap<usize, mpsc::Sender<()>>>>,
|
||||
}
|
||||
|
||||
impl TimerChannel {
|
||||
pub async fn fire_most_recent(&self) {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||
let mut tx_map = self.tx.lock().await;
|
||||
let armed_count = *self.armed_count.lock().await;
|
||||
let tx = tx_map.remove(&armed_count).unwrap();
|
||||
tx.send(()).await.unwrap();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub async fn confirm_dropped_timer(&self, index: usize) {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||
let mut tx_map = self.tx.lock().await;
|
||||
let tx = tx_map.remove(&index).unwrap();
|
||||
if tx.try_send(()).is_ok() {
|
||||
panic!("Timer was not dropped");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_armed_count(&self) -> usize {
|
||||
*self.armed_count.lock().await
|
||||
}
|
||||
}
|
||||
57
lorawan-device-patch/src/async_device/test/util.rs
Normal file
57
lorawan-device-patch/src/async_device/test/util.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use super::{get_dev_addr, get_key, region, Device, SendResponse};
|
||||
|
||||
use crate::mac::Session;
|
||||
use crate::test_util::handle_class_c_uplink_after_join;
|
||||
use crate::{AppSKey, NewSKey};
|
||||
|
||||
fn setup_internal(session_data: Option<Session>) -> (RadioChannel, TimerChannel, Device) {
|
||||
let (radio_channel, mock_radio) = TestRadio::new();
|
||||
let (timer_channel, mock_timer) = TestTimer::new();
|
||||
let region = region::US915::default();
|
||||
let async_device = Device::new_with_session(
|
||||
region.into(),
|
||||
mock_radio,
|
||||
mock_timer,
|
||||
rand::rngs::OsRng,
|
||||
session_data,
|
||||
);
|
||||
(radio_channel, timer_channel, async_device)
|
||||
}
|
||||
|
||||
pub fn setup_with_session() -> (RadioChannel, TimerChannel, Device) {
|
||||
setup_internal(Some(Session {
|
||||
newskey: NewSKey::from(get_key()),
|
||||
appskey: AppSKey::from(get_key()),
|
||||
devaddr: get_dev_addr(),
|
||||
fcnt_up: 0,
|
||||
fcnt_down: 0,
|
||||
confirmed: false,
|
||||
uplink: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn setup_with_session_class_c() -> (RadioChannel, TimerChannel, Device) {
|
||||
let (radio, timer, mut async_device) = setup_with_session();
|
||||
async_device.enable_class_c();
|
||||
// Run the device
|
||||
let task = tokio::spawn(async move {
|
||||
let response = async_device.send(&[3, 2, 1], 3, false).await;
|
||||
(async_device, response)
|
||||
});
|
||||
// timeout the first sends RX windows which enables class C
|
||||
timer.fire_most_recent().await;
|
||||
radio.handle_rxtx(handle_class_c_uplink_after_join).await;
|
||||
|
||||
let (device, response) = task.await.unwrap();
|
||||
match response {
|
||||
Ok(SendResponse::DownlinkReceived(0)) => (),
|
||||
_ => {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
(radio, timer, device)
|
||||
}
|
||||
|
||||
pub fn setup() -> (RadioChannel, TimerChannel, Device) {
|
||||
setup_internal(None)
|
||||
}
|
||||
77
lorawan-device-patch/src/lib.rs
Normal file
77
lorawan-device-patch/src/lib.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
#![cfg_attr(not(test), no_std)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
//! ## Feature flags
|
||||
#![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use core::default::Default;
|
||||
use heapless::Vec;
|
||||
|
||||
mod radio;
|
||||
|
||||
pub mod mac;
|
||||
use mac::NetworkCredentials;
|
||||
|
||||
pub mod region;
|
||||
pub use region::Region;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_util;
|
||||
|
||||
pub mod async_device;
|
||||
|
||||
pub mod nb_device;
|
||||
use nb_device::state::State;
|
||||
|
||||
use core::marker::PhantomData;
|
||||
#[cfg(feature = "default-crypto")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "default-crypto")))]
|
||||
pub use lorawan::default_crypto;
|
||||
pub use lorawan::{
|
||||
keys::{AppEui, AppKey, AppSKey, CryptoFactory, DevEui, NewSKey},
|
||||
parser::DevAddr,
|
||||
};
|
||||
|
||||
pub use rand_core::RngCore;
|
||||
mod rng;
|
||||
pub use rng::Prng;
|
||||
|
||||
mod log;
|
||||
|
||||
/// Provides the application payload and FPort of a downlink message.
|
||||
pub struct Downlink {
|
||||
pub data: Vec<u8, 256>,
|
||||
pub fport: u8,
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
impl defmt::Format for Downlink {
|
||||
fn format(&self, f: defmt::Formatter) {
|
||||
defmt::write!(f, "Downlink {{ fport: {}, data: ", self.fport, );
|
||||
|
||||
for byte in self.data.iter() {
|
||||
defmt::write!(f, "{:02x}", byte);
|
||||
}
|
||||
defmt::write!(f, " }}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows to fine-tune the beginning and end of the receive windows for a specific board.
|
||||
pub trait Timings {
|
||||
/// The offset in milliseconds from the beginning of the receive windows. For example, settings this to 100
|
||||
/// tell the LoRaWAN stack to begin configuring the receive window 100 ms before the window needs to start.
|
||||
fn get_rx_window_offset_ms(&self) -> i32;
|
||||
|
||||
/// How long to leave the receive window open in milliseconds. For example, if offset was set to 100 and duration
|
||||
/// was set to 200, the window would be open 100 ms before and close 100 ms after the target time.
|
||||
fn get_rx_window_duration_ms(&self) -> u32;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
/// Join the network using either OTAA or ABP.
|
||||
pub enum JoinMode {
|
||||
OTAA { deveui: DevEui, appeui: AppEui, appkey: AppKey },
|
||||
ABP { newskey: NewSKey, appskey: AppSKey, devaddr: DevAddr<[u8; 4]> },
|
||||
}
|
||||
35
lorawan-device-patch/src/log.rs
Normal file
35
lorawan-device-patch/src/log.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#![allow(unused_macros)]
|
||||
#![allow(unused)]
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
macro_rules! llog {
|
||||
(trace, $($arg:expr),*) => { defmt::trace!($($arg),*) };
|
||||
(debug, $($arg:expr),*) => { defmt::debug!($($arg),*) };
|
||||
(info, $($arg:expr),*) => { defmt::info!($($arg),*) };
|
||||
(error, $($arg:expr),*) => { defmt::error!($($arg),*) };
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
macro_rules! llog {
|
||||
($level:ident, $($arg:expr),*) => {{ $( let _ = $arg; )* }}
|
||||
}
|
||||
pub(crate) use llog;
|
||||
|
||||
macro_rules! trace {
|
||||
($($arg:expr),*) => (log::llog!(trace, $($arg),*));
|
||||
}
|
||||
pub(crate) use trace;
|
||||
|
||||
macro_rules! debug {
|
||||
($($arg:expr),*) => (log::llog!(debug, $($arg),*));
|
||||
}
|
||||
pub(crate) use debug;
|
||||
macro_rules! info {
|
||||
($($arg:expr),*) => (log::llog!(info, $($arg),*));
|
||||
}
|
||||
pub(crate) use info;
|
||||
|
||||
macro_rules! error {
|
||||
($($arg:expr),*) => (log::llog!(error, $($arg),*));
|
||||
}
|
||||
pub(crate) use error;
|
||||
368
lorawan-device-patch/src/mac/mod.rs
Normal file
368
lorawan-device-patch/src/mac/mod.rs
Normal file
@@ -0,0 +1,368 @@
|
||||
//! LoRaWAN MAC layer implementation written as a non-async state machine (leveraged by `async_device` and `nb_device`).
|
||||
//! Manages state internally while providing client with transmit and receive frequencies, while writing to and
|
||||
//! decrypting from send and receive buffers.
|
||||
|
||||
use crate::{
|
||||
radio::{self, RadioBuffer, RfConfig, RxConfig, RxMode},
|
||||
region, AppSKey, Downlink, NewSKey,
|
||||
};
|
||||
use heapless::Vec;
|
||||
use lorawan::{self, keys::CryptoFactory};
|
||||
use lorawan::{maccommands::DownlinkMacCommand, parser::DevAddr};
|
||||
|
||||
pub type FcntDown = u32;
|
||||
pub type FcntUp = u32;
|
||||
|
||||
mod session;
|
||||
use rand_core::RngCore;
|
||||
pub use session::{Session, SessionKeys};
|
||||
|
||||
mod otaa;
|
||||
pub use otaa::NetworkCredentials;
|
||||
|
||||
use crate::async_device;
|
||||
use crate::nb_device;
|
||||
|
||||
pub(crate) mod uplink;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum Frame {
|
||||
Join,
|
||||
Data,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum Window {
|
||||
_1,
|
||||
_2,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
/// LoRaWAN Session and Network Configurations
|
||||
pub struct Configuration {
|
||||
pub(crate) data_rate: region::DR,
|
||||
rx1_delay: u32,
|
||||
join_accept_delay1: u32,
|
||||
join_accept_delay2: u32,
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
fn handle_downlink_macs(
|
||||
&mut self,
|
||||
region: &mut region::Configuration,
|
||||
uplink: &mut uplink::Uplink,
|
||||
cmds: lorawan::maccommands::MacCommandIterator<DownlinkMacCommand>,
|
||||
) {
|
||||
use uplink::MacAnsTrait;
|
||||
for cmd in cmds {
|
||||
match cmd {
|
||||
DownlinkMacCommand::LinkADRReq(payload) => {
|
||||
// we ignore DR and TxPwr
|
||||
region.set_channel_mask(
|
||||
payload.redundancy().channel_mask_control(),
|
||||
payload.channel_mask(),
|
||||
);
|
||||
uplink.adr_ans.add();
|
||||
}
|
||||
DownlinkMacCommand::RXTimingSetupReq(payload) => {
|
||||
self.rx1_delay = del_to_delay_ms(payload.delay());
|
||||
uplink.ack_rx_delay();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Mac {
|
||||
pub configuration: Configuration,
|
||||
pub region: region::Configuration,
|
||||
board_eirp: BoardEirp,
|
||||
state: State,
|
||||
}
|
||||
|
||||
struct BoardEirp {
|
||||
max_power: u8,
|
||||
antenna_gain: i8,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum State {
|
||||
Joined(Session),
|
||||
Otaa(otaa::Otaa),
|
||||
Unjoined,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum Error {
|
||||
NotJoined,
|
||||
InvalidResponse(Response),
|
||||
}
|
||||
|
||||
pub struct SendData<'a> {
|
||||
pub data: &'a [u8],
|
||||
pub fport: u8,
|
||||
pub confirmed: bool,
|
||||
}
|
||||
|
||||
pub(crate) type Result<T = ()> = core::result::Result<T, Error>;
|
||||
|
||||
impl Mac {
|
||||
pub(crate) fn new(region: region::Configuration, max_power: u8, antenna_gain: i8) -> Self {
|
||||
let data_rate = region.get_default_datarate();
|
||||
Self {
|
||||
board_eirp: BoardEirp { max_power, antenna_gain },
|
||||
region,
|
||||
state: State::Unjoined,
|
||||
configuration: Configuration {
|
||||
data_rate,
|
||||
rx1_delay: region::constants::RECEIVE_DELAY1,
|
||||
join_accept_delay1: region::constants::JOIN_ACCEPT_DELAY1,
|
||||
join_accept_delay2: region::constants::JOIN_ACCEPT_DELAY2,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare the radio buffer with transmitting a join request frame and provides the radio
|
||||
/// configuration for the transmission.
|
||||
pub(crate) fn join_otaa<C: CryptoFactory + Default, RNG: RngCore, const N: usize>(
|
||||
&mut self,
|
||||
rng: &mut RNG,
|
||||
credentials: NetworkCredentials,
|
||||
buf: &mut RadioBuffer<N>,
|
||||
) -> (radio::TxConfig, u16) {
|
||||
let mut otaa = otaa::Otaa::new(credentials);
|
||||
let dev_nonce = otaa.prepare_buffer::<C, RNG, N>(rng, buf);
|
||||
self.state = State::Otaa(otaa);
|
||||
let mut tx_config =
|
||||
self.region.create_tx_config(rng, self.configuration.data_rate, &Frame::Join);
|
||||
tx_config.adjust_power(self.board_eirp.max_power, self.board_eirp.antenna_gain);
|
||||
(tx_config, dev_nonce)
|
||||
}
|
||||
|
||||
/// Join via ABP. This does not transmit a join request frame, but instead sets the session.
|
||||
pub(crate) fn join_abp(
|
||||
&mut self,
|
||||
newskey: NewSKey,
|
||||
appskey: AppSKey,
|
||||
devaddr: DevAddr<[u8; 4]>,
|
||||
) {
|
||||
self.state = State::Joined(Session::new(newskey, appskey, devaddr));
|
||||
}
|
||||
|
||||
/// Join via ABP. This does not transmit a join request frame, but instead sets the session.
|
||||
pub(crate) fn set_session(&mut self, session: Session) {
|
||||
self.state = State::Joined(session);
|
||||
}
|
||||
|
||||
/// Prepare the radio buffer for transmitting a data frame and provide the radio configuration
|
||||
/// for the transmission. Returns an error if the device is not joined.
|
||||
pub(crate) fn send<C: CryptoFactory + Default, RNG: RngCore, const N: usize>(
|
||||
&mut self,
|
||||
rng: &mut RNG,
|
||||
buf: &mut RadioBuffer<N>,
|
||||
send_data: &SendData,
|
||||
) -> Result<(radio::TxConfig, FcntUp)> {
|
||||
let fcnt = match &mut self.state {
|
||||
State::Joined(ref mut session) => Ok(session.prepare_buffer::<C, N>(send_data, buf)),
|
||||
State::Otaa(_) => Err(Error::NotJoined),
|
||||
State::Unjoined => Err(Error::NotJoined),
|
||||
}?;
|
||||
let mut tx_config =
|
||||
self.region.create_tx_config(rng, self.configuration.data_rate, &Frame::Data);
|
||||
tx_config.adjust_power(self.board_eirp.max_power, self.board_eirp.antenna_gain);
|
||||
Ok((tx_config, fcnt))
|
||||
}
|
||||
|
||||
pub(crate) fn get_rx_delay(&self, frame: &Frame, window: &Window) -> u32 {
|
||||
match frame {
|
||||
Frame::Join => match window {
|
||||
Window::_1 => self.configuration.join_accept_delay1,
|
||||
Window::_2 => self.configuration.join_accept_delay2,
|
||||
},
|
||||
Frame::Data => match window {
|
||||
Window::_1 => self.configuration.rx1_delay,
|
||||
// RECEIVE_DELAY2 is not configurable. LoRaWAN 1.0.3 Section 5.7:
|
||||
// "The second reception slot opens one second after the first reception slot."
|
||||
Window::_2 => self.configuration.rx1_delay + 1000,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the radio configuration and timing for a given frame type and window.
|
||||
pub(crate) fn get_rx_parameters_legacy(
|
||||
&mut self,
|
||||
frame: &Frame,
|
||||
window: &Window,
|
||||
) -> (RfConfig, u32) {
|
||||
(
|
||||
self.region.get_rx_config(self.configuration.data_rate, frame, window),
|
||||
self.get_rx_delay(frame, window),
|
||||
)
|
||||
}
|
||||
|
||||
/// Handles a received RF frame. Returns None is unparseable, fails decryption, or fails MIC
|
||||
/// verification. Upon successful join, provides Response::JoinSuccess. Upon successful data
|
||||
/// rx, provides Response::DownlinkReceived. User must take the downlink from vec for
|
||||
/// application data.
|
||||
pub(crate) fn handle_rx<C: CryptoFactory + Default, const N: usize, const D: usize>(
|
||||
&mut self,
|
||||
buf: &mut RadioBuffer<N>,
|
||||
dl: &mut Vec<Downlink, D>,
|
||||
) -> Response {
|
||||
match &mut self.state {
|
||||
State::Joined(ref mut session) => session.handle_rx::<C, N, D>(
|
||||
&mut self.region,
|
||||
&mut self.configuration,
|
||||
buf,
|
||||
dl,
|
||||
false,
|
||||
),
|
||||
State::Otaa(ref mut otaa) => {
|
||||
if let Some(session) =
|
||||
otaa.handle_rx::<C, N>(&mut self.region, &mut self.configuration, buf)
|
||||
{
|
||||
self.state = State::Joined(session);
|
||||
Response::JoinSuccess
|
||||
} else {
|
||||
Response::NoUpdate
|
||||
}
|
||||
}
|
||||
State::Unjoined => Response::NoUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a received RF frame during RXC window. Returns None if unparseable, fails decryption,
|
||||
/// or fails MIC verification. Upon successful data rx, provides Response::DownlinkReceived.
|
||||
/// User must later call `take_downlink()` on the device to get the application data.
|
||||
pub(crate) fn handle_rxc<C: CryptoFactory + Default, const N: usize, const D: usize>(
|
||||
&mut self,
|
||||
buf: &mut RadioBuffer<N>,
|
||||
dl: &mut Vec<Downlink, D>,
|
||||
) -> Result<Response> {
|
||||
match &mut self.state {
|
||||
State::Joined(ref mut session) => Ok(session.handle_rx::<C, N, D>(
|
||||
&mut self.region,
|
||||
&mut self.configuration,
|
||||
buf,
|
||||
dl,
|
||||
true,
|
||||
)),
|
||||
State::Otaa(_) => Err(Error::NotJoined),
|
||||
State::Unjoined => Err(Error::NotJoined),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rx2_complete(&mut self) -> Response {
|
||||
match &mut self.state {
|
||||
State::Joined(session) => session.rx2_complete(),
|
||||
State::Otaa(otaa) => otaa.rx2_complete(),
|
||||
State::Unjoined => Response::NoUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_session_keys(&self) -> Option<SessionKeys> {
|
||||
match &self.state {
|
||||
State::Joined(session) => session.get_session_keys(),
|
||||
State::Otaa(_) => None,
|
||||
State::Unjoined => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_session(&self) -> Option<&Session> {
|
||||
match &self.state {
|
||||
State::Joined(session) => Some(session),
|
||||
State::Otaa(_) => None,
|
||||
State::Unjoined => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_joined(&self) -> bool {
|
||||
matches!(&self.state, State::Joined(_))
|
||||
}
|
||||
|
||||
pub(crate) fn get_fcnt_up(&self) -> Option<FcntUp> {
|
||||
match &self.state {
|
||||
State::Joined(session) => Some(session.fcnt_up),
|
||||
State::Otaa(_) => None,
|
||||
State::Unjoined => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_rx_config(&self, buffer_ms: u32, frame: &Frame, window: &Window) -> RxConfig {
|
||||
RxConfig {
|
||||
rf: self.region.get_rx_config(self.configuration.data_rate, frame, window),
|
||||
mode: RxMode::Single { ms: buffer_ms },
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_rxc_config(&self) -> RxConfig {
|
||||
RxConfig {
|
||||
rf: self.region.get_rxc_config(self.configuration.data_rate),
|
||||
mode: RxMode::Continuous,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[derive(Debug)]
|
||||
pub enum Response {
|
||||
NoAck,
|
||||
SessionExpired,
|
||||
DownlinkReceived(FcntDown),
|
||||
NoJoinAccept,
|
||||
JoinSuccess,
|
||||
NoUpdate,
|
||||
RxComplete,
|
||||
}
|
||||
|
||||
impl From<Response> for nb_device::Response {
|
||||
fn from(r: Response) -> Self {
|
||||
match r {
|
||||
Response::SessionExpired => nb_device::Response::SessionExpired,
|
||||
Response::DownlinkReceived(fcnt) => nb_device::Response::DownlinkReceived(fcnt),
|
||||
Response::NoAck => nb_device::Response::NoAck,
|
||||
Response::NoJoinAccept => nb_device::Response::NoJoinAccept,
|
||||
Response::JoinSuccess => nb_device::Response::JoinSuccess,
|
||||
Response::NoUpdate => nb_device::Response::NoUpdate,
|
||||
Response::RxComplete => nb_device::Response::RxComplete,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Response> for async_device::SendResponse {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(r: Response) -> Result<async_device::SendResponse> {
|
||||
match r {
|
||||
Response::SessionExpired => Ok(async_device::SendResponse::SessionExpired),
|
||||
Response::DownlinkReceived(fcnt) => {
|
||||
Ok(async_device::SendResponse::DownlinkReceived(fcnt))
|
||||
}
|
||||
Response::NoAck => Ok(async_device::SendResponse::NoAck),
|
||||
Response::RxComplete => Ok(async_device::SendResponse::RxComplete),
|
||||
r => Err(Error::InvalidResponse(r)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Response> for async_device::JoinResponse {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(r: Response) -> Result<async_device::JoinResponse> {
|
||||
match r {
|
||||
Response::NoJoinAccept => Ok(async_device::JoinResponse::NoJoinAccept),
|
||||
Response::JoinSuccess => Ok(async_device::JoinResponse::JoinSuccess),
|
||||
r => Err(Error::InvalidResponse(r)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn del_to_delay_ms(del: u8) -> u32 {
|
||||
match del {
|
||||
2..=15 => del as u32 * 1000,
|
||||
_ => region::constants::RECEIVE_DELAY1,
|
||||
}
|
||||
}
|
||||
92
lorawan-device-patch/src/mac/otaa.rs
Normal file
92
lorawan-device-patch/src/mac/otaa.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use super::{del_to_delay_ms, session::Session, Response};
|
||||
use crate::radio::RadioBuffer;
|
||||
use crate::region::Configuration;
|
||||
use crate::{AppEui, AppKey, DevEui};
|
||||
use lorawan::keys::CryptoFactory;
|
||||
use lorawan::{
|
||||
creator::JoinRequestCreator,
|
||||
parser::{parse_with_factory as lorawan_parse, *},
|
||||
};
|
||||
use rand_core::RngCore;
|
||||
|
||||
pub(crate) type DevNonce = lorawan::parser::DevNonce<[u8; 2]>;
|
||||
|
||||
pub(crate) struct Otaa {
|
||||
dev_nonce: DevNonce,
|
||||
network_credentials: NetworkCredentials,
|
||||
}
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkCredentials {
|
||||
deveui: DevEui,
|
||||
appeui: AppEui,
|
||||
appkey: AppKey,
|
||||
}
|
||||
|
||||
impl Otaa {
|
||||
pub fn new(network_credentials: NetworkCredentials) -> Self {
|
||||
Self { dev_nonce: DevNonce::from([0, 0]), network_credentials }
|
||||
}
|
||||
|
||||
/// Prepare a join request to be sent. This populates the radio buffer with the request to be
|
||||
/// sent, and returns the radio config to use for transmitting.
|
||||
pub(crate) fn prepare_buffer<C: CryptoFactory + Default, G: RngCore, const N: usize>(
|
||||
&mut self,
|
||||
rng: &mut G,
|
||||
buf: &mut RadioBuffer<N>,
|
||||
) -> u16 {
|
||||
self.dev_nonce = DevNonce::from(rng.next_u32() as u16);
|
||||
buf.clear();
|
||||
let mut phy: JoinRequestCreator<[u8; 23], C> = JoinRequestCreator::default();
|
||||
phy.set_app_eui(self.network_credentials.appeui)
|
||||
.set_dev_eui(self.network_credentials.deveui)
|
||||
.set_dev_nonce(self.dev_nonce);
|
||||
let vec = phy.build(&self.network_credentials.appkey);
|
||||
buf.extend_from_slice(vec).unwrap();
|
||||
u16::from(self.dev_nonce)
|
||||
}
|
||||
|
||||
pub(crate) fn handle_rx<C: CryptoFactory + Default, const N: usize>(
|
||||
&mut self,
|
||||
region: &mut Configuration,
|
||||
configuration: &mut super::Configuration,
|
||||
rx: &mut RadioBuffer<N>,
|
||||
) -> Option<Session> {
|
||||
if let Ok(PhyPayload::JoinAccept(JoinAcceptPayload::Encrypted(encrypted))) =
|
||||
lorawan_parse(rx.as_mut_for_read(), C::default())
|
||||
{
|
||||
let decrypt = encrypted.decrypt(&self.network_credentials.appkey);
|
||||
region.process_join_accept(&decrypt);
|
||||
configuration.rx1_delay = del_to_delay_ms(decrypt.rx_delay());
|
||||
if decrypt.validate_mic(&self.network_credentials.appkey) {
|
||||
return Some(Session::derive_new(
|
||||
&decrypt,
|
||||
self.dev_nonce,
|
||||
&self.network_credentials,
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn rx2_complete(&mut self) -> Response {
|
||||
Response::NoJoinAccept
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkCredentials {
|
||||
pub fn new(appeui: AppEui, deveui: DevEui, appkey: AppKey) -> Self {
|
||||
Self { deveui, appeui, appkey }
|
||||
}
|
||||
pub fn appeui(&self) -> &AppEui {
|
||||
&self.appeui
|
||||
}
|
||||
|
||||
pub fn deveui(&self) -> &DevEui {
|
||||
&self.deveui
|
||||
}
|
||||
|
||||
pub fn appkey(&self) -> &AppKey {
|
||||
&self.appkey
|
||||
}
|
||||
}
|
||||
222
lorawan-device-patch/src/mac/session.rs
Normal file
222
lorawan-device-patch/src/mac/session.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use crate::{region, AppSKey, Downlink, NewSKey};
|
||||
use heapless::Vec;
|
||||
use lorawan::keys::CryptoFactory;
|
||||
use lorawan::maccommands::{DownlinkMacCommand, MacCommandIterator};
|
||||
use lorawan::{
|
||||
creator::DataPayloadCreator,
|
||||
maccommands::SerializableMacCommand,
|
||||
parser::{parse_with_factory as lorawan_parse, *},
|
||||
parser::{DecryptedJoinAcceptPayload, DevAddr},
|
||||
};
|
||||
|
||||
use generic_array::{typenum::U256, GenericArray};
|
||||
|
||||
use crate::radio::RadioBuffer;
|
||||
|
||||
use super::{
|
||||
otaa::{DevNonce, NetworkCredentials},
|
||||
uplink, FcntUp, Response, SendData,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Session {
|
||||
pub uplink: uplink::Uplink,
|
||||
pub confirmed: bool,
|
||||
pub newskey: NewSKey,
|
||||
pub appskey: AppSKey,
|
||||
pub devaddr: DevAddr<[u8; 4]>,
|
||||
pub fcnt_up: u32,
|
||||
pub fcnt_down: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub struct SessionKeys {
|
||||
pub newskey: NewSKey,
|
||||
pub appskey: AppSKey,
|
||||
pub devaddr: DevAddr<[u8; 4]>,
|
||||
}
|
||||
|
||||
impl From<Session> for SessionKeys {
|
||||
fn from(session: Session) -> Self {
|
||||
Self { newskey: session.newskey, appskey: session.appskey, devaddr: session.devaddr }
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn derive_new<T: AsRef<[u8]>, F: CryptoFactory>(
|
||||
decrypt: &DecryptedJoinAcceptPayload<T, F>,
|
||||
devnonce: DevNonce,
|
||||
credentials: &NetworkCredentials,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
decrypt.derive_newskey(&devnonce, credentials.appkey()),
|
||||
decrypt.derive_appskey(&devnonce, credentials.appkey()),
|
||||
DevAddr::new([
|
||||
decrypt.dev_addr().as_ref()[0],
|
||||
decrypt.dev_addr().as_ref()[1],
|
||||
decrypt.dev_addr().as_ref()[2],
|
||||
decrypt.dev_addr().as_ref()[3],
|
||||
])
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new(newskey: NewSKey, appskey: AppSKey, devaddr: DevAddr<[u8; 4]>) -> Self {
|
||||
Self {
|
||||
newskey,
|
||||
appskey,
|
||||
devaddr,
|
||||
confirmed: false,
|
||||
fcnt_down: 0,
|
||||
fcnt_up: 0,
|
||||
uplink: uplink::Uplink::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn devaddr(&self) -> &DevAddr<[u8; 4]> {
|
||||
&self.devaddr
|
||||
}
|
||||
pub fn appskey(&self) -> &AppSKey {
|
||||
&self.appskey
|
||||
}
|
||||
pub fn newskey(&self) -> &NewSKey {
|
||||
&self.newskey
|
||||
}
|
||||
|
||||
pub fn get_session_keys(&self) -> Option<SessionKeys> {
|
||||
Some(SessionKeys { newskey: self.newskey, appskey: self.appskey, devaddr: self.devaddr })
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub(crate) fn handle_rx<C: CryptoFactory + Default, const N: usize, const D: usize>(
|
||||
&mut self,
|
||||
region: &mut region::Configuration,
|
||||
configuration: &mut super::Configuration,
|
||||
rx: &mut RadioBuffer<N>,
|
||||
dl: &mut Vec<Downlink, D>,
|
||||
ignore_mac: bool,
|
||||
) -> Response {
|
||||
if let Ok(PhyPayload::Data(DataPayload::Encrypted(encrypted_data))) =
|
||||
lorawan_parse(rx.as_mut_for_read(), C::default())
|
||||
{
|
||||
if self.devaddr() == &encrypted_data.fhdr().dev_addr() {
|
||||
let fcnt = encrypted_data.fhdr().fcnt() as u32;
|
||||
let confirmed = encrypted_data.is_confirmed();
|
||||
if encrypted_data.validate_mic(self.newskey().inner(), fcnt)
|
||||
&& (fcnt > self.fcnt_down || fcnt == 0)
|
||||
{
|
||||
self.fcnt_down = fcnt;
|
||||
// We can safely unwrap here because we already validated the MIC
|
||||
let decrypted = encrypted_data
|
||||
.decrypt(
|
||||
Some(self.newskey().inner()),
|
||||
Some(self.appskey().inner()),
|
||||
self.fcnt_down,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !ignore_mac {
|
||||
// MAC commands may be in the FHDR or the FRMPayload
|
||||
configuration.handle_downlink_macs(
|
||||
region,
|
||||
&mut self.uplink,
|
||||
MacCommandIterator::<DownlinkMacCommand>::new(decrypted.fhdr().data()),
|
||||
);
|
||||
if let FRMPayload::MACCommands(mac_cmds) = decrypted.frm_payload() {
|
||||
configuration.handle_downlink_macs(
|
||||
region,
|
||||
&mut self.uplink,
|
||||
MacCommandIterator::<DownlinkMacCommand>::new(mac_cmds.data()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if confirmed {
|
||||
self.uplink.set_downlink_confirmation();
|
||||
}
|
||||
|
||||
return if self.fcnt_up == 0xFFFF_FFFF {
|
||||
// if the FCnt is used up, the session has expired
|
||||
Response::SessionExpired
|
||||
} else {
|
||||
// we can always increment fcnt_up when we receive a downlink
|
||||
self.fcnt_up += 1;
|
||||
if let (Some(fport), FRMPayload::Data(data)) =
|
||||
(decrypted.f_port(), decrypted.frm_payload())
|
||||
{
|
||||
// heapless Vec from slice fails only if slice is too large.
|
||||
// A data FRM payload will never exceed 256 bytes.
|
||||
let data = Vec::from_slice(data).unwrap();
|
||||
// TODO: propagate error type when heapless vec is full?
|
||||
let _ = dl.push(Downlink { data, fport });
|
||||
}
|
||||
Response::DownlinkReceived(fcnt)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Response::NoUpdate
|
||||
}
|
||||
|
||||
pub(crate) fn rx2_complete(&mut self) -> Response {
|
||||
// Until we handle NbTrans, there is no case where we should not increment FCntUp.
|
||||
if self.fcnt_up == 0xFFFF_FFFF {
|
||||
// if the FCnt is used up, the session has expired
|
||||
return Response::SessionExpired;
|
||||
} else {
|
||||
self.fcnt_up += 1;
|
||||
}
|
||||
if self.confirmed {
|
||||
Response::NoAck
|
||||
} else {
|
||||
Response::RxComplete
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_buffer<C: CryptoFactory + Default, const N: usize>(
|
||||
&mut self,
|
||||
data: &SendData,
|
||||
tx_buffer: &mut RadioBuffer<N>,
|
||||
) -> FcntUp {
|
||||
tx_buffer.clear();
|
||||
let fcnt = self.fcnt_up;
|
||||
let mut phy: DataPayloadCreator<GenericArray<u8, U256>, C> = DataPayloadCreator::default();
|
||||
|
||||
let mut fctrl = FCtrl(0x0, true);
|
||||
if self.uplink.confirms_downlink() {
|
||||
fctrl.set_ack();
|
||||
self.uplink.clear_downlink_confirmation();
|
||||
}
|
||||
|
||||
self.confirmed = data.confirmed;
|
||||
|
||||
phy.set_confirmed(data.confirmed)
|
||||
.set_fctrl(&fctrl)
|
||||
.set_f_port(data.fport)
|
||||
.set_dev_addr(self.devaddr)
|
||||
.set_fcnt(fcnt);
|
||||
|
||||
let mut cmds = Vec::new();
|
||||
self.uplink.get_cmds(&mut cmds);
|
||||
let mut dyn_cmds: Vec<&dyn SerializableMacCommand, 8> = Vec::new();
|
||||
|
||||
for cmd in &cmds {
|
||||
if let Err(_e) = dyn_cmds.push(cmd) {
|
||||
panic!("dyn_cmds too small compared to cmds")
|
||||
}
|
||||
}
|
||||
|
||||
match phy.build(data.data, dyn_cmds.as_slice(), &self.newskey, &self.appskey) {
|
||||
Ok(packet) => {
|
||||
tx_buffer.clear();
|
||||
tx_buffer.extend_from_slice(packet).unwrap();
|
||||
}
|
||||
Err(e) => panic!("Error assembling packet! {:?} ", e),
|
||||
}
|
||||
fcnt
|
||||
}
|
||||
}
|
||||
88
lorawan-device-patch/src/mac/uplink.rs
Normal file
88
lorawan-device-patch/src/mac/uplink.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
This a temporary design where flags will be left about desired MAC uplinks by the stack
|
||||
During Uplink assembly, this struct will be inquired to drive construction
|
||||
*/
|
||||
use heapless::Vec;
|
||||
use lorawan::maccommands::{LinkADRAnsPayload, RXTimingSetupAnsPayload, UplinkMacCommand};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Uplink {
|
||||
pub adr_ans: AdrAns,
|
||||
pub rx_delay_ans: RxDelayAns,
|
||||
confirmed: bool,
|
||||
}
|
||||
|
||||
// multiple AdrAns may happen per downlink
|
||||
// so we aggregate how many AdrAns are required
|
||||
type AdrAns = u8;
|
||||
// only one RxDelayReq will happen
|
||||
// so we only need to implement this as a bool
|
||||
type RxDelayAns = bool;
|
||||
|
||||
//work around for E0390
|
||||
pub(crate) trait MacAnsTrait {
|
||||
fn add(&mut self);
|
||||
fn clear(&mut self);
|
||||
// we use a uint instead of bool because some ADR responses
|
||||
// require a counter for state.
|
||||
// eg: ADR Req may be batched in a single downlink and require
|
||||
// multiple ADR Ans in the next uplink
|
||||
fn get(&self) -> u8;
|
||||
}
|
||||
|
||||
impl MacAnsTrait for AdrAns {
|
||||
fn add(&mut self) {
|
||||
*self += 1;
|
||||
}
|
||||
fn clear(&mut self) {
|
||||
*self = 0;
|
||||
}
|
||||
fn get(&self) -> u8 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl MacAnsTrait for RxDelayAns {
|
||||
fn add(&mut self) {
|
||||
*self = true;
|
||||
}
|
||||
fn clear(&mut self) {
|
||||
*self = false;
|
||||
}
|
||||
fn get(&self) -> u8 {
|
||||
u8::from(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Uplink {
|
||||
pub fn set_downlink_confirmation(&mut self) {
|
||||
self.confirmed = true;
|
||||
}
|
||||
|
||||
pub fn clear_downlink_confirmation(&mut self) {
|
||||
self.confirmed = false;
|
||||
}
|
||||
pub fn confirms_downlink(&self) -> bool {
|
||||
self.confirmed
|
||||
}
|
||||
|
||||
pub fn ack_rx_delay(&mut self) {
|
||||
self.rx_delay_ans.add();
|
||||
}
|
||||
|
||||
pub fn get_cmds(&mut self, macs: &mut Vec<UplinkMacCommand, 8>) {
|
||||
for _ in 0..self.adr_ans.get() {
|
||||
macs.push(UplinkMacCommand::LinkADRAns(LinkADRAnsPayload::new(&[0x07]).unwrap()))
|
||||
.unwrap();
|
||||
}
|
||||
self.adr_ans.clear();
|
||||
|
||||
if self.rx_delay_ans.get() != 0 {
|
||||
macs.push(UplinkMacCommand::RXTimingSetupAns(RXTimingSetupAnsPayload::new(&[])))
|
||||
.unwrap();
|
||||
}
|
||||
self.rx_delay_ans.clear();
|
||||
}
|
||||
}
|
||||
173
lorawan-device-patch/src/nb_device/mod.rs
Normal file
173
lorawan-device-patch/src/nb_device/mod.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
//! A non-blocking LoRaWAN device implementation which uses an explicitly defined state machine
|
||||
//! for driving the protocol state against pin and timer events. Depends on a non-async radio
|
||||
//! implementation.
|
||||
use super::radio::RadioBuffer;
|
||||
use super::*;
|
||||
use crate::nb_device::radio::PhyRxTx;
|
||||
use mac::{Mac, SendData};
|
||||
|
||||
pub(crate) mod state;
|
||||
|
||||
pub mod radio;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
type TimestampMs = u32;
|
||||
|
||||
pub struct Device<R, C, RNG, const N: usize, const D: usize = 1>
|
||||
where
|
||||
R: PhyRxTx + Timings,
|
||||
C: CryptoFactory + Default,
|
||||
RNG: RngCore,
|
||||
{
|
||||
state: State,
|
||||
shared: Shared<R, RNG, N, D>,
|
||||
crypto: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<R, C, RNG, const N: usize, const D: usize> Device<R, C, RNG, N, D>
|
||||
where
|
||||
R: PhyRxTx + Timings,
|
||||
C: CryptoFactory + Default,
|
||||
RNG: RngCore,
|
||||
{
|
||||
pub fn new(region: region::Configuration, radio: R, rng: RNG) -> Device<R, C, RNG, N, D> {
|
||||
Device {
|
||||
crypto: PhantomData,
|
||||
state: State::default(),
|
||||
shared: Shared {
|
||||
radio,
|
||||
rng,
|
||||
tx_buffer: RadioBuffer::new(),
|
||||
mac: Mac::new(region, R::MAX_RADIO_POWER, R::ANTENNA_GAIN),
|
||||
downlink: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(&mut self, join_mode: JoinMode) -> Result<Response, Error<R>> {
|
||||
match join_mode {
|
||||
JoinMode::OTAA { deveui, appeui, appkey } => {
|
||||
self.handle_event(Event::Join(NetworkCredentials::new(appeui, deveui, appkey)))
|
||||
}
|
||||
JoinMode::ABP { devaddr, appskey, newskey } => {
|
||||
self.shared.mac.join_abp(newskey, appskey, devaddr);
|
||||
Ok(Response::JoinSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_radio(&mut self) -> &mut R {
|
||||
&mut self.shared.radio
|
||||
}
|
||||
|
||||
pub fn get_datarate(&mut self) -> region::DR {
|
||||
self.shared.mac.configuration.data_rate
|
||||
}
|
||||
|
||||
pub fn set_datarate(&mut self, datarate: region::DR) {
|
||||
self.shared.mac.configuration.data_rate = datarate
|
||||
}
|
||||
|
||||
pub fn ready_to_send_data(&self) -> bool {
|
||||
matches!(&self.state, State::Idle(_)) && self.shared.mac.is_joined()
|
||||
}
|
||||
|
||||
pub fn send(&mut self, data: &[u8], fport: u8, confirmed: bool) -> Result<Response, Error<R>> {
|
||||
self.handle_event(Event::SendDataRequest(SendData { data, fport, confirmed }))
|
||||
}
|
||||
|
||||
pub fn get_fcnt_up(&self) -> Option<u32> {
|
||||
self.shared.mac.get_fcnt_up()
|
||||
}
|
||||
|
||||
pub fn get_session(&self) -> Option<&mac::Session> {
|
||||
self.shared.mac.get_session()
|
||||
}
|
||||
|
||||
pub fn set_session(&mut self, s: mac::Session) {
|
||||
self.shared.mac.set_session(s)
|
||||
}
|
||||
|
||||
pub fn get_session_keys(&self) -> Option<mac::SessionKeys> {
|
||||
self.shared.mac.get_session_keys()
|
||||
}
|
||||
|
||||
pub fn take_downlink(&mut self) -> Option<Downlink> {
|
||||
self.shared.downlink.pop()
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, event: Event<R>) -> Result<Response, Error<R>> {
|
||||
let (new_state, result) = self.state.handle_event::<R, C, RNG, N, D>(
|
||||
&mut self.shared.mac,
|
||||
&mut self.shared.radio,
|
||||
&mut self.shared.rng,
|
||||
&mut self.shared.tx_buffer,
|
||||
&mut self.shared.downlink,
|
||||
event,
|
||||
);
|
||||
self.state = new_state;
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Shared<R: PhyRxTx + Timings, RNG: RngCore, const N: usize, const D: usize> {
|
||||
pub(crate) radio: R,
|
||||
pub(crate) rng: RNG,
|
||||
pub(crate) tx_buffer: RadioBuffer<N>,
|
||||
pub(crate) mac: Mac,
|
||||
pub(crate) downlink: Vec<Downlink, D>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Response {
|
||||
NoUpdate,
|
||||
TimeoutRequest(TimestampMs),
|
||||
JoinRequestSending,
|
||||
JoinSuccess,
|
||||
NoJoinAccept,
|
||||
UplinkSending(mac::FcntUp),
|
||||
DownlinkReceived(mac::FcntDown),
|
||||
NoAck,
|
||||
ReadyToSend,
|
||||
SessionExpired,
|
||||
RxComplete,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error<R: PhyRxTx> {
|
||||
Radio(R::PhyError),
|
||||
State(state::Error),
|
||||
Mac(mac::Error),
|
||||
}
|
||||
|
||||
impl<R: PhyRxTx> From<mac::Error> for Error<R> {
|
||||
fn from(mac_error: mac::Error) -> Error<R> {
|
||||
Error::Mac(mac_error)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event<'a, R>
|
||||
where
|
||||
R: PhyRxTx,
|
||||
{
|
||||
Join(NetworkCredentials),
|
||||
SendDataRequest(SendData<'a>),
|
||||
RadioEvent(radio::Event<'a, R>),
|
||||
TimeoutFired,
|
||||
}
|
||||
|
||||
impl<'a, R> core::fmt::Debug for Event<'a, R>
|
||||
where
|
||||
R: PhyRxTx,
|
||||
{
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let event = match self {
|
||||
Event::Join(_) => "Join",
|
||||
Event::SendDataRequest(_) => "SendDataRequest",
|
||||
Event::RadioEvent(_) => "RadioEvent",
|
||||
Event::TimeoutFired => "TimeoutFired",
|
||||
};
|
||||
write!(f, "lorawan_device::Event::{event}")
|
||||
}
|
||||
}
|
||||
50
lorawan-device-patch/src/nb_device/radio.rs
Normal file
50
lorawan-device-patch/src/nb_device/radio.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use super::TimestampMs;
|
||||
pub use crate::radio::*;
|
||||
pub use ::lora_modulation::{Bandwidth, CodingRate, SpreadingFactor};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event<'a, R>
|
||||
where
|
||||
R: PhyRxTx,
|
||||
{
|
||||
TxRequest(TxConfig, &'a [u8]),
|
||||
RxRequest(RfConfig),
|
||||
CancelRx,
|
||||
Phy(R::PhyEvent),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Response<R>
|
||||
where
|
||||
R: PhyRxTx,
|
||||
{
|
||||
Idle,
|
||||
Txing,
|
||||
Rxing,
|
||||
TxDone(TimestampMs),
|
||||
RxDone(RxQuality),
|
||||
Phy(R::PhyResponse),
|
||||
}
|
||||
|
||||
use core::fmt;
|
||||
|
||||
pub trait PhyRxTx {
|
||||
type PhyEvent: fmt::Debug;
|
||||
type PhyError: fmt::Debug;
|
||||
type PhyResponse: fmt::Debug;
|
||||
|
||||
/// Board-specific antenna gain and power loss in dBi.
|
||||
const ANTENNA_GAIN: i8 = 0;
|
||||
|
||||
/// Maximum power (dBm) that the radio is able to output. When preparing instructions for radio,
|
||||
/// the value of maximum power will be used as an upper bound.
|
||||
const MAX_RADIO_POWER: u8;
|
||||
|
||||
fn get_mut_radio(&mut self) -> &mut Self;
|
||||
|
||||
// we require mutability so we may decrypt in place
|
||||
fn get_received_packet(&mut self) -> &mut [u8];
|
||||
fn handle_event(&mut self, event: Event<Self>) -> Result<Response<Self>, Self::PhyError>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
411
lorawan-device-patch/src/nb_device/state.rs
Normal file
411
lorawan-device-patch/src/nb_device/state.rs
Normal file
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
|
||||
This state machine creates a non-blocking and no-async structure for coordinating radio events with
|
||||
the mac state.
|
||||
|
||||
In this implementation, each state (eg: "Idle", "Txing") is a struct. When an event is handled
|
||||
(eg: "SendData", "TxComplete"), a transition may or may not occur. Regardless, a response is always
|
||||
given to the client, and those are indicated here in parenthesis (ie: "(Sending)"). If nothing is
|
||||
indicated in this diagram, the response is "NoUpdate".
|
||||
|
||||
O
|
||||
│
|
||||
╔═══════════════════╗ ╔════════════════════╗
|
||||
║ Idle ║ ║ Txing ║
|
||||
║ SendData ║ if async (Sending) ║ ║
|
||||
║ ─────────╫───────────────┬───────────────>║ ║
|
||||
║ ║ │ ║ TxComplete ║
|
||||
╚═══════════════════╝ │ ║ ──────────╫───┐
|
||||
^ │ ╚════════════════════╝ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
┌─────┘ ╔═══════════════════╗ │ ╔════════════════════╗ │
|
||||
│ ║ WaitingForRx ║ │ ║ WaitingForRxWindow ║ │
|
||||
│ ║ ╔═════════════╗ ║ │else sync ║ ╔═════════════╗ ║ │
|
||||
│ ║ ║ RxWindow1 ║ ║ └──────────╫─>║ RxWindow1 ║<──╫─────────┘
|
||||
│(DataDown)║ ║ Rx ║ ║ (TimeoutReq)║ ║ ║ ║(TimeoutReq)
|
||||
├──────────╫─╫─────── ║ ║(TimeoutReq) ║ ║ Timeout ║ ║
|
||||
│ ║ ║ Timeout ║<──╫───────────────╫──╫──────────── ║ ║
|
||||
│ ║ ║ ─────────╫───╫──┐ ║ ╚═════════════╝ ║
|
||||
│ ║ ╚═════════════╝ ║ │ ║ ║
|
||||
│ ║ ╔═════════════╗ ║ │(TimeoutReq)║ ╔═════════════╗ ║
|
||||
│(DataDown)║ ║ RxWindow2 ║ ║ └────────────╫─> ║ RxWindow2 ║ ║
|
||||
├──────────╫─╫──┐ Rx ║ ║ ║ ║ ║ ║
|
||||
│ ║ ║ └─── ║ ║(TimeoutReq) ║ ║ Timeout ║ ║
|
||||
│ if conf ║ ║ Timeout ║<──╫───────────────╫───╫──────────── ║ ║
|
||||
│ (NoACK) ║ ║ ┌──────── ║ ║ ║ ╚═════════════╝ ║
|
||||
└──────────╫─╫───┘ ║ ║ ║ ║
|
||||
else(Ready)║ ╚═════════════╝ ║ ║ ║
|
||||
╚═══════════════════╝ ╚════════════════════╝
|
||||
*/
|
||||
use super::super::*;
|
||||
use super::{
|
||||
mac::{Frame, Mac, Window},
|
||||
radio, Event, RadioBuffer, Response, Timings,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum State {
|
||||
Idle(Idle),
|
||||
SendingData(SendingData),
|
||||
WaitingForRxWindow(WaitingForRxWindow),
|
||||
WaitingForRx(WaitingForRx),
|
||||
}
|
||||
|
||||
macro_rules! into_state {
|
||||
($($from:tt),*) => {
|
||||
$(
|
||||
impl From<$from> for State
|
||||
{
|
||||
fn from(s: $from) -> State {
|
||||
State::$from(s)
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
into_state!(Idle, SendingData, WaitingForRxWindow, WaitingForRx);
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
State::Idle(Idle)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rx> for Window {
|
||||
fn from(val: Rx) -> Window {
|
||||
match val {
|
||||
Rx::_1(_) => Window::_1,
|
||||
Rx::_2(_) => Window::_2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
RadioEventWhileIdle,
|
||||
RadioEventWhileWaitingForRxWindow,
|
||||
NewSessionWhileWaitingForRxWindow,
|
||||
SendDataWhileWaitingForRxWindow,
|
||||
TxRequestDuringTx,
|
||||
NewSessionWhileWaitingForRx,
|
||||
SendDataWhileWaitingForRx,
|
||||
BufferTooSmall,
|
||||
UnexpectedRadioResponse,
|
||||
}
|
||||
|
||||
impl<R: radio::PhyRxTx> From<Error> for super::Error<R> {
|
||||
fn from(error: Error) -> super::Error<R> {
|
||||
super::Error::State(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub(crate) fn handle_event<
|
||||
R: radio::PhyRxTx + Timings,
|
||||
C: CryptoFactory + Default,
|
||||
RNG: RngCore,
|
||||
const N: usize,
|
||||
const D: usize,
|
||||
>(
|
||||
self,
|
||||
mac: &mut Mac,
|
||||
radio: &mut R,
|
||||
rng: &mut RNG,
|
||||
buf: &mut RadioBuffer<N>,
|
||||
dl: &mut Vec<Downlink, D>,
|
||||
event: Event<R>,
|
||||
) -> (Self, Result<Response, super::Error<R>>) {
|
||||
match self {
|
||||
State::Idle(s) => s.handle_event::<R, C, RNG, N>(mac, radio, rng, buf, event),
|
||||
State::SendingData(s) => s.handle_event::<R, N>(mac, radio, event),
|
||||
State::WaitingForRxWindow(s) => s.handle_event::<R, N>(mac, radio, event),
|
||||
State::WaitingForRx(s) => s.handle_event::<R, C, N, D>(mac, radio, buf, event, dl),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Idle;
|
||||
|
||||
impl Idle {
|
||||
pub(crate) fn handle_event<
|
||||
R: radio::PhyRxTx + Timings,
|
||||
C: CryptoFactory + Default,
|
||||
RNG: RngCore,
|
||||
const N: usize,
|
||||
>(
|
||||
self,
|
||||
mac: &mut Mac,
|
||||
radio: &mut R,
|
||||
rng: &mut RNG,
|
||||
buf: &mut RadioBuffer<N>,
|
||||
event: Event<R>,
|
||||
) -> (State, Result<Response, super::Error<R>>) {
|
||||
enum IntermediateResponse<R: radio::PhyRxTx> {
|
||||
RadioTx((Frame, radio::TxConfig, u32)),
|
||||
EarlyReturn(Result<Response, super::Error<R>>),
|
||||
}
|
||||
|
||||
let response = match event {
|
||||
// tolerate unexpected timeout
|
||||
Event::Join(creds) => {
|
||||
let (tx_config, dev_nonce) = mac.join_otaa::<C, RNG, N>(rng, creds, buf);
|
||||
IntermediateResponse::RadioTx((Frame::Join, tx_config, dev_nonce as u32))
|
||||
}
|
||||
Event::TimeoutFired => IntermediateResponse::EarlyReturn(Ok(Response::NoUpdate)),
|
||||
Event::RadioEvent(_radio_event) => {
|
||||
IntermediateResponse::EarlyReturn(Err(Error::RadioEventWhileIdle.into()))
|
||||
}
|
||||
Event::SendDataRequest(send_data) => {
|
||||
let tx_config = mac.send::<C, RNG, N>(rng, buf, &send_data);
|
||||
match tx_config {
|
||||
Err(e) => IntermediateResponse::EarlyReturn(Err(e.into())),
|
||||
Ok((tx_config, fcnt_up)) => {
|
||||
IntermediateResponse::RadioTx((Frame::Data, tx_config, fcnt_up))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
match response {
|
||||
IntermediateResponse::EarlyReturn(response) => (State::Idle(self), response),
|
||||
IntermediateResponse::RadioTx((frame, tx_config, fcnt_up)) => {
|
||||
let event: radio::Event<R> =
|
||||
radio::Event::TxRequest(tx_config, buf.as_ref_for_read());
|
||||
match radio.handle_event(event) {
|
||||
Ok(response) => {
|
||||
match response {
|
||||
// intermediate state where we wait for Join to complete sending
|
||||
// allows for asynchronous sending
|
||||
radio::Response::Txing => (
|
||||
State::SendingData(SendingData { frame }),
|
||||
Ok(Response::UplinkSending(fcnt_up)),
|
||||
),
|
||||
// directly jump to waiting for RxWindow
|
||||
// allows for synchronous sending
|
||||
radio::Response::TxDone(ms) => {
|
||||
data_rxwindow1_timeout::<R, N>(frame, mac, radio, ms)
|
||||
}
|
||||
_ => (State::Idle(self), Err(Error::UnexpectedRadioResponse.into())),
|
||||
}
|
||||
}
|
||||
Err(e) => (State::Idle(self), Err(super::Error::Radio(e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct SendingData {
|
||||
frame: Frame,
|
||||
}
|
||||
|
||||
impl SendingData {
|
||||
pub(crate) fn handle_event<R: radio::PhyRxTx + Timings, const N: usize>(
|
||||
self,
|
||||
mac: &mut Mac,
|
||||
radio: &mut R,
|
||||
event: Event<R>,
|
||||
) -> (State, Result<Response, super::Error<R>>) {
|
||||
match event {
|
||||
// we are waiting for the async tx to complete
|
||||
Event::RadioEvent(radio_event) => {
|
||||
// send the transmit request to the radio
|
||||
match radio.handle_event(radio_event) {
|
||||
Ok(response) => {
|
||||
match response {
|
||||
// expect a complete transmit
|
||||
radio::Response::TxDone(ms) => {
|
||||
data_rxwindow1_timeout::<R, N>(self.frame, mac, radio, ms)
|
||||
}
|
||||
// anything other than TxComplete is unexpected
|
||||
_ => {
|
||||
panic!("SendingData: Unexpected radio response");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => (State::SendingData(self), Err(super::Error::Radio(e))),
|
||||
}
|
||||
}
|
||||
// tolerate unexpected timeout
|
||||
Event::TimeoutFired => (State::SendingData(self), Ok(Response::NoUpdate)),
|
||||
// anything other than a RadioEvent is unexpected
|
||||
Event::Join(_) | Event::SendDataRequest(_) => {
|
||||
(self.into(), Err(Error::TxRequestDuringTx.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct WaitingForRxWindow {
|
||||
frame: Frame,
|
||||
window: Rx,
|
||||
}
|
||||
|
||||
impl WaitingForRxWindow {
|
||||
pub(crate) fn handle_event<R: radio::PhyRxTx + Timings, const N: usize>(
|
||||
self,
|
||||
mac: &mut Mac,
|
||||
radio: &mut R,
|
||||
event: Event<R>,
|
||||
) -> (State, Result<Response, super::Error<R>>) {
|
||||
match event {
|
||||
// we are waiting for a Timeout
|
||||
Event::TimeoutFired => {
|
||||
let (rx_config, window_start) =
|
||||
mac.get_rx_parameters_legacy(&self.frame, &self.window.into());
|
||||
// configure the radio for the RX
|
||||
match radio.handle_event(radio::Event::RxRequest(rx_config)) {
|
||||
Ok(_) => {
|
||||
let window_close: u32 = match self.window {
|
||||
// RxWindow1 one must timeout before RxWindow2
|
||||
Rx::_1(time) => {
|
||||
let time_between_windows =
|
||||
mac.get_rx_delay(&self.frame, &Window::_2) - window_start;
|
||||
if time_between_windows > radio.get_rx_window_duration_ms() {
|
||||
time + radio.get_rx_window_duration_ms()
|
||||
} else {
|
||||
time + time_between_windows
|
||||
}
|
||||
}
|
||||
// RxWindow2 can last however long
|
||||
Rx::_2(time) => time + radio.get_rx_window_duration_ms(),
|
||||
};
|
||||
(
|
||||
State::WaitingForRx(self.into()),
|
||||
Ok(Response::TimeoutRequest(window_close)),
|
||||
)
|
||||
}
|
||||
Err(e) => (State::WaitingForRxWindow(self), Err(super::Error::Radio(e))),
|
||||
}
|
||||
}
|
||||
Event::RadioEvent(_) => (
|
||||
State::WaitingForRxWindow(self),
|
||||
Err(Error::RadioEventWhileWaitingForRxWindow.into()),
|
||||
),
|
||||
Event::Join(_) => (
|
||||
State::WaitingForRxWindow(self),
|
||||
Err(Error::NewSessionWhileWaitingForRxWindow.into()),
|
||||
),
|
||||
Event::SendDataRequest(_) => (
|
||||
State::WaitingForRxWindow(self),
|
||||
Err(Error::SendDataWhileWaitingForRxWindow.into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WaitingForRxWindow> for WaitingForRx {
|
||||
fn from(val: WaitingForRxWindow) -> WaitingForRx {
|
||||
WaitingForRx { frame: val.frame, window: val.window }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct WaitingForRx {
|
||||
frame: Frame,
|
||||
window: Rx,
|
||||
}
|
||||
|
||||
impl WaitingForRx {
|
||||
pub(crate) fn handle_event<
|
||||
R: radio::PhyRxTx + Timings,
|
||||
C: CryptoFactory + Default,
|
||||
const N: usize,
|
||||
const D: usize,
|
||||
>(
|
||||
self,
|
||||
mac: &mut Mac,
|
||||
radio: &mut R,
|
||||
buf: &mut RadioBuffer<N>,
|
||||
event: Event<R>,
|
||||
dl: &mut Vec<Downlink, D>,
|
||||
) -> (State, Result<Response, super::Error<R>>) {
|
||||
match event {
|
||||
// we are waiting for the async tx to complete
|
||||
Event::RadioEvent(radio_event) => {
|
||||
// send the transmit request to the radio
|
||||
match radio.handle_event(radio_event) {
|
||||
Ok(response) => match response {
|
||||
radio::Response::RxDone(_quality) => {
|
||||
// copy from radio buffer to mac buffer
|
||||
buf.clear();
|
||||
if let Err(()) =
|
||||
buf.extend_from_slice(radio.get_received_packet().as_ref())
|
||||
{
|
||||
return (
|
||||
State::WaitingForRx(self),
|
||||
Err(Error::BufferTooSmall.into()),
|
||||
);
|
||||
}
|
||||
match mac.handle_rx::<C, N, D>(buf, dl) {
|
||||
// NoUpdate can occur when a stray radio packet is received. Maintain state
|
||||
mac::Response::NoUpdate => {
|
||||
(State::WaitingForRx(self), Ok(Response::NoUpdate))
|
||||
}
|
||||
// Any other type of update indicates we are done receiving. Change to Idle
|
||||
r => (State::Idle(Idle), Ok(r.into())),
|
||||
}
|
||||
}
|
||||
_ => (State::WaitingForRx(self), Ok(Response::NoUpdate)),
|
||||
},
|
||||
Err(e) => (State::WaitingForRx(self), Err(super::Error::Radio(e))),
|
||||
}
|
||||
}
|
||||
Event::TimeoutFired => {
|
||||
if let Err(e) = radio.handle_event(radio::Event::CancelRx) {
|
||||
return (State::WaitingForRx(self), Err(super::Error::Radio(e)));
|
||||
}
|
||||
|
||||
match self.window {
|
||||
Rx::_1(t1) => {
|
||||
let time_between_windows = mac.get_rx_delay(&self.frame, &Window::_2)
|
||||
- mac.get_rx_delay(&self.frame, &Window::_1);
|
||||
let t2 = t1 + time_between_windows;
|
||||
// TODO: jump to RxWindow2 if t2 == now
|
||||
(
|
||||
State::WaitingForRxWindow(WaitingForRxWindow {
|
||||
frame: self.frame,
|
||||
window: Rx::_2(t2),
|
||||
}),
|
||||
Ok(Response::TimeoutRequest(t2)),
|
||||
)
|
||||
}
|
||||
// Timeout during second RxWindow leads to giving up
|
||||
Rx::_2(_) => {
|
||||
let response = mac.rx2_complete();
|
||||
(State::Idle(Idle), Ok(response.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Join(_) => {
|
||||
(State::WaitingForRx(self), Err(Error::NewSessionWhileWaitingForRx.into()))
|
||||
}
|
||||
Event::SendDataRequest(_) => {
|
||||
(State::WaitingForRx(self), Err(Error::SendDataWhileWaitingForRx.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum Rx {
|
||||
_1(u32),
|
||||
_2(u32),
|
||||
}
|
||||
|
||||
fn data_rxwindow1_timeout<R: radio::PhyRxTx + Timings, const N: usize>(
|
||||
frame: Frame,
|
||||
mac: &mut Mac,
|
||||
radio: &mut R,
|
||||
timestamp_ms: u32,
|
||||
) -> (State, Result<Response, super::Error<R>>) {
|
||||
let delay = mac.get_rx_delay(&frame, &Window::_1);
|
||||
let t1 = (delay as i32 + timestamp_ms as i32 + radio.get_rx_window_offset_ms()) as u32;
|
||||
(
|
||||
State::WaitingForRxWindow(WaitingForRxWindow { frame, window: Rx::_1(t1) }),
|
||||
Ok(Response::TimeoutRequest(t1)),
|
||||
)
|
||||
}
|
||||
128
lorawan-device-patch/src/nb_device/test/mod.rs
Normal file
128
lorawan-device-patch/src/nb_device/test/mod.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use super::*;
|
||||
mod util;
|
||||
|
||||
use crate::nb_device::Event;
|
||||
#[test]
|
||||
fn test_join_rx1() {
|
||||
let mut device = test_device();
|
||||
let response = device.join(get_otaa_credentials()).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(5000)));
|
||||
// send a timeout for beginning of window
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(5100)));
|
||||
device.get_radio().set_rxtx_handler(handle_join_request::<1>);
|
||||
// send a radio event to let the radio device indicate a packet was received
|
||||
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||
assert!(matches!(response, Response::JoinSuccess));
|
||||
assert!(device.get_session_keys().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_rx2() {
|
||||
let mut device = test_device();
|
||||
device.get_radio().set_rxtx_handler(handle_join_request::<2>);
|
||||
let response = device.join(get_otaa_credentials()).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(5000)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(5100)));
|
||||
// send a timeout for end of rx2
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(6000)));
|
||||
// send a timeout for beginning of rx2
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(6100)));
|
||||
// send a radio event to let the radio device indicate a packet was received
|
||||
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||
assert!(matches!(response, Response::JoinSuccess));
|
||||
assert!(device.get_session_keys().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unconfirmed_uplink_no_downlink() {
|
||||
let mut device = test_device();
|
||||
device.join(get_abp_credentials()).unwrap();
|
||||
let response = device.send(&[0; 1], 1, false).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // end Rx1
|
||||
assert!(matches!(response, Response::TimeoutRequest(2000)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // being Rx2
|
||||
assert!(matches!(response, Response::TimeoutRequest(2100)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // end Rx2
|
||||
assert!(matches!(response, Response::RxComplete));
|
||||
}
|
||||
#[test]
|
||||
fn test_confirmed_uplink_no_ack() {
|
||||
let mut device = test_device();
|
||||
let response = device.join(get_abp_credentials());
|
||||
assert!(matches!(response, Ok(Response::JoinSuccess)));
|
||||
let response = device.send(&[0; 1], 1, true).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // end Rx1
|
||||
assert!(matches!(response, Response::TimeoutRequest(2000)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // being Rx2
|
||||
assert!(matches!(response, Response::TimeoutRequest(2100)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // end Rx2
|
||||
assert!(matches!(response, Response::NoAck));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_confirmed_uplink_with_ack_rx1() {
|
||||
let mut device = test_device();
|
||||
let response = device.join(get_abp_credentials());
|
||||
assert!(matches!(response, Ok(Response::JoinSuccess)));
|
||||
let response = device.send(&[0; 1], 1, true).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||
device.get_radio().set_rxtx_handler(handle_data_uplink_with_link_adr_req::<0, 0>);
|
||||
// send a radio event to let the radio device indicate a packet was received
|
||||
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||
assert!(matches!(response, Response::DownlinkReceived(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_confirmed_uplink_with_ack_rx2() {
|
||||
let mut device = test_device();
|
||||
let response = device.join(get_abp_credentials());
|
||||
assert!(matches!(response, Ok(Response::JoinSuccess)));
|
||||
let response = device.send(&[0; 1], 1, true).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // end Rx1
|
||||
assert!(matches!(response, Response::TimeoutRequest(2000)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // being Rx2
|
||||
assert!(matches!(response, Response::TimeoutRequest(2100)));
|
||||
device.get_radio().set_rxtx_handler(handle_data_uplink_with_link_adr_req::<0, 0>);
|
||||
// send a radio event to let the radio device indicate a packet was received
|
||||
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||
assert!(matches!(response, Response::DownlinkReceived(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_link_adr_ans() {
|
||||
let mut device = test_device();
|
||||
let response = device.join(get_abp_credentials());
|
||||
assert!(matches!(response, Ok(Response::JoinSuccess)));
|
||||
let response = device.send(&[0; 1], 1, true).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||
device.get_radio().set_rxtx_handler(handle_data_uplink_with_link_adr_req::<0, 0>);
|
||||
// send a radio event to let the radio device indicate a packet was received
|
||||
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||
assert!(matches!(response, Response::DownlinkReceived(0)));
|
||||
// send another uplink which should carry the LinkAdrAns
|
||||
let response = device.send(&[0; 1], 1, true).unwrap();
|
||||
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||
device.get_radio().set_rxtx_handler(handle_data_uplink_with_link_adr_ans);
|
||||
// send a radio event to let the radio device indicate a packet was received
|
||||
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||
assert!(matches!(response, Response::DownlinkReceived(1)));
|
||||
}
|
||||
96
lorawan-device-patch/src/nb_device/test/util.rs
Normal file
96
lorawan-device-patch/src/nb_device/test/util.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use crate::radio::{RfConfig, RxQuality};
|
||||
|
||||
use crate::nb_device::{
|
||||
radio::{Event, PhyRxTx, Response},
|
||||
Device, Timings,
|
||||
};
|
||||
use lorawan::default_crypto;
|
||||
use region::{Configuration, Region};
|
||||
|
||||
pub fn test_device() -> Device<TestRadio, default_crypto::DefaultFactory, rand_core::OsRng, 255> {
|
||||
Device::new(Configuration::new(Region::US915), TestRadio::default(), rand::rngs::OsRng)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestRadio {
|
||||
current_config: Option<RfConfig>,
|
||||
last_uplink: Option<Uplink>,
|
||||
rxtx_handler: Option<RxTxHandler>,
|
||||
buffer: [u8; 256],
|
||||
buffer_index: usize,
|
||||
}
|
||||
|
||||
impl TestRadio {
|
||||
pub fn set_rxtx_handler(&mut self, handler: RxTxHandler) {
|
||||
self.rxtx_handler = Some(handler);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TestRadio {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
current_config: None,
|
||||
last_uplink: None,
|
||||
rxtx_handler: None,
|
||||
buffer: [0; 256],
|
||||
buffer_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PhyRxTx for TestRadio {
|
||||
type PhyEvent = ();
|
||||
type PhyError = &'static str;
|
||||
type PhyResponse = ();
|
||||
|
||||
const MAX_RADIO_POWER: u8 = 26;
|
||||
|
||||
const ANTENNA_GAIN: i8 = 0;
|
||||
|
||||
fn get_mut_radio(&mut self) -> &mut Self {
|
||||
self
|
||||
}
|
||||
fn get_received_packet(&mut self) -> &mut [u8] {
|
||||
&mut self.buffer[..self.buffer_index]
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event<Self>) -> Result<Response<Self>, Self::PhyError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match event {
|
||||
Event::TxRequest(config, buf) => {
|
||||
// ensure that we have always consumed the previous uplink
|
||||
if self.last_uplink.is_some() {
|
||||
panic!("last uplink not consumed")
|
||||
}
|
||||
self.last_uplink =
|
||||
Some(Uplink::new(buf, config).map_err(|_| "error creating uplink")?);
|
||||
return Ok(Response::TxDone(0));
|
||||
}
|
||||
Event::RxRequest(rf_config) => {
|
||||
self.current_config = Some(rf_config);
|
||||
}
|
||||
Event::CancelRx => (),
|
||||
Event::Phy(()) => {
|
||||
if let (Some(rf_config), Some(rxtx_handler)) =
|
||||
(self.current_config, self.rxtx_handler)
|
||||
{
|
||||
self.buffer_index =
|
||||
rxtx_handler(self.last_uplink.take(), rf_config, &mut self.buffer);
|
||||
return Ok(Response::RxDone(RxQuality::new(0, 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Response::Idle)
|
||||
}
|
||||
}
|
||||
|
||||
impl Timings for TestRadio {
|
||||
fn get_rx_window_offset_ms(&self) -> i32 {
|
||||
0
|
||||
}
|
||||
fn get_rx_window_duration_ms(&self) -> u32 {
|
||||
100
|
||||
}
|
||||
}
|
||||
112
lorawan-device-patch/src/radio.rs
Normal file
112
lorawan-device-patch/src/radio.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
pub use lora_modulation::BaseBandModulationParams;
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RfConfig {
|
||||
pub frequency: u32,
|
||||
pub bb: BaseBandModulationParams,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum RxMode {
|
||||
Continuous,
|
||||
/// Single shot receive. Argument `ms` indicates how many milliseconds of extra buffer time should
|
||||
/// be added to the preamble detection timeout.
|
||||
Single {
|
||||
ms: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RxConfig {
|
||||
pub rf: RfConfig,
|
||||
pub mode: RxMode,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct TxConfig {
|
||||
pub pw: i8,
|
||||
pub rf: RfConfig,
|
||||
}
|
||||
|
||||
impl TxConfig {
|
||||
pub fn adjust_power(&mut self, max_power: u8, antenna_gain: i8) {
|
||||
self.pw -= antenna_gain;
|
||||
self.pw = core::cmp::min(self.pw, max_power as i8);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RxQuality {
|
||||
rssi: i16,
|
||||
snr: i8,
|
||||
}
|
||||
|
||||
impl RxQuality {
|
||||
pub fn new(rssi: i16, snr: i8) -> RxQuality {
|
||||
RxQuality { rssi, snr }
|
||||
}
|
||||
|
||||
pub fn rssi(self) -> i16 {
|
||||
self.rssi
|
||||
}
|
||||
pub fn snr(self) -> i8 {
|
||||
self.snr
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RadioBuffer<const N: usize> {
|
||||
packet: [u8; N],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> RadioBuffer<N> {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { packet: [0; N], pos: 0 }
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.pos = 0;
|
||||
}
|
||||
|
||||
pub(crate) fn set_pos(&mut self, pos: usize) {
|
||||
self.pos = pos;
|
||||
}
|
||||
|
||||
pub(crate) fn extend_from_slice(&mut self, buf: &[u8]) -> Result<(), ()> {
|
||||
if self.pos + buf.len() < self.packet.len() {
|
||||
self.packet[self.pos..self.pos + buf.len()].copy_from_slice(buf);
|
||||
self.pos += buf.len();
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a mutable slice of the buffer up to the current position.
|
||||
pub(crate) fn as_mut_for_read(&mut self) -> &mut [u8] {
|
||||
&mut self.packet[..self.pos]
|
||||
}
|
||||
|
||||
/// Provides a reference of the buffer up to the current position.
|
||||
|
||||
pub(crate) fn as_ref_for_read(&self) -> &[u8] {
|
||||
&self.packet[..self.pos]
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> AsMut<[u8]> for RadioBuffer<N> {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.packet
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> AsRef<[u8]> for RadioBuffer<N> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.packet
|
||||
}
|
||||
}
|
||||
16
lorawan-device-patch/src/region/constants.rs
Normal file
16
lorawan-device-patch/src/region/constants.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
#![allow(dead_code)]
|
||||
use lora_modulation::{Bandwidth, CodingRate, SpreadingFactor};
|
||||
|
||||
pub(crate) const RECEIVE_DELAY1: u32 = 1000;
|
||||
pub(crate) const RECEIVE_DELAY2: u32 = RECEIVE_DELAY1 + 1000; // must be RECEIVE_DELAY + 1 s
|
||||
pub(crate) const JOIN_ACCEPT_DELAY1: u32 = 5000;
|
||||
pub(crate) const JOIN_ACCEPT_DELAY2: u32 = 6000;
|
||||
pub(crate) const MAX_FCNT_GAP: usize = 16384;
|
||||
pub(crate) const ADR_ACK_LIMIT: usize = 64;
|
||||
pub(crate) const ADR_ACK_DELAY: usize = 32;
|
||||
pub(crate) const ACK_TIMEOUT: usize = 2; // random delay between 1 and 3 seconds
|
||||
|
||||
pub(crate) const DEFAULT_BANDWIDTH: Bandwidth = Bandwidth::_125KHz;
|
||||
pub(crate) const DEFAULT_SPREADING_FACTOR: SpreadingFactor = SpreadingFactor::_7;
|
||||
pub(crate) const DEFAULT_CODING_RATE: CodingRate = CodingRate::_4_5;
|
||||
pub(crate) const DEFAULT_DBM: i8 = 14;
|
||||
@@ -0,0 +1,80 @@
|
||||
use super::*;
|
||||
|
||||
const JOIN_CHANNELS: [u32; 2] = [923200000, 923200000];
|
||||
|
||||
pub(crate) type AS923_1 = DynamicChannelPlan<2, 7, AS923Region<923_200_000, 0>>;
|
||||
pub(crate) type AS923_2 = DynamicChannelPlan<2, 7, AS923Region<921_400_000, 1800000>>;
|
||||
pub(crate) type AS923_3 = DynamicChannelPlan<2, 7, AS923Region<916_600_000, 6600000>>;
|
||||
pub(crate) type AS923_4 = DynamicChannelPlan<2, 7, AS923Region<917_300_000, 5900000>>;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct AS923Region<const DEFAULT_RX2: u32, const O: u32>;
|
||||
|
||||
impl<const DEFAULT_RX2: u32, const OFFSET: u32> ChannelRegion<7>
|
||||
for AS923Region<DEFAULT_RX2, OFFSET>
|
||||
{
|
||||
fn datarates() -> &'static [Option<Datarate>; 7] {
|
||||
&DATARATES
|
||||
}
|
||||
}
|
||||
|
||||
impl<const DEFAULT_RX2: u32, const OFFSET: u32> DynamicChannelRegion<2, 7>
|
||||
for AS923Region<DEFAULT_RX2, OFFSET>
|
||||
{
|
||||
fn join_channels() -> [u32; 2] {
|
||||
[JOIN_CHANNELS[0] + OFFSET, JOIN_CHANNELS[1] + OFFSET]
|
||||
}
|
||||
|
||||
fn get_default_rx2() -> u32 {
|
||||
DEFAULT_RX2
|
||||
}
|
||||
}
|
||||
|
||||
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||
|
||||
pub(crate) const DATARATES: [Option<Datarate>; 7] = [
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_12,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 0,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_11,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 0,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_10,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 123,
|
||||
max_mac_payload_size_with_dwell_time: 19,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_9,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 123,
|
||||
max_mac_payload_size_with_dwell_time: 61,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_8,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 133,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_250KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
// TODO: ignore FSK data rate for now
|
||||
];
|
||||
@@ -0,0 +1,74 @@
|
||||
#![allow(dead_code)]
|
||||
use super::*;
|
||||
|
||||
const JOIN_CHANNELS: [u32; 3] = [433_175_000, 433_375_000, 433_575_000];
|
||||
|
||||
pub(crate) type EU433 = DynamicChannelPlan<3, 7, EU433Region>;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct EU433Region;
|
||||
|
||||
impl ChannelRegion<7> for EU433Region {
|
||||
fn datarates() -> &'static [Option<Datarate>; 7] {
|
||||
&DATARATES
|
||||
}
|
||||
}
|
||||
|
||||
impl DynamicChannelRegion<3, 7> for EU433Region {
|
||||
fn join_channels() -> [u32; 3] {
|
||||
JOIN_CHANNELS
|
||||
}
|
||||
|
||||
fn get_default_rx2() -> u32 {
|
||||
434_665_000
|
||||
}
|
||||
}
|
||||
|
||||
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||
|
||||
pub(crate) const DATARATES: [Option<Datarate>; 7] = [
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_12,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 0,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_11,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 0,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_10,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 123,
|
||||
max_mac_payload_size_with_dwell_time: 19,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_9,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 123,
|
||||
max_mac_payload_size_with_dwell_time: 61,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_8,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 133,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_250KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
// TODO 7 is defined in rp002-1-0-4
|
||||
];
|
||||
@@ -0,0 +1,74 @@
|
||||
#![allow(dead_code)]
|
||||
use super::*;
|
||||
|
||||
const JOIN_CHANNELS: [u32; 3] = [868_100_000, 868_300_000, 868_500_000];
|
||||
|
||||
pub(crate) type EU868 = DynamicChannelPlan<3, 7, EU868Region>;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct EU868Region;
|
||||
|
||||
impl ChannelRegion<7> for EU868Region {
|
||||
fn datarates() -> &'static [Option<Datarate>; 7] {
|
||||
&DATARATES
|
||||
}
|
||||
}
|
||||
|
||||
impl DynamicChannelRegion<3, 7> for EU868Region {
|
||||
fn join_channels() -> [u32; 3] {
|
||||
JOIN_CHANNELS
|
||||
}
|
||||
|
||||
fn get_default_rx2() -> u32 {
|
||||
869_525_000
|
||||
}
|
||||
}
|
||||
|
||||
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||
|
||||
pub(crate) const DATARATES: [Option<Datarate>; 7] = [
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_12,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 59,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_11,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 59,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_10,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 59,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_9,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 123,
|
||||
max_mac_payload_size_with_dwell_time: 123,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_8,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_250KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
// TODO: ignore FSK data rate for now
|
||||
];
|
||||
@@ -0,0 +1,68 @@
|
||||
#![allow(dead_code)]
|
||||
use super::*;
|
||||
|
||||
const JOIN_CHANNELS: [u32; 3] = [865_062_500, 865_402_500, 865_985_000];
|
||||
|
||||
pub(crate) type IN865 = DynamicChannelPlan<3, 6, IN865Region>;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct IN865Region;
|
||||
|
||||
impl ChannelRegion<6> for IN865Region {
|
||||
fn datarates() -> &'static [Option<Datarate>; 6] {
|
||||
&DATARATES
|
||||
}
|
||||
}
|
||||
|
||||
impl DynamicChannelRegion<3, 6> for IN865Region {
|
||||
fn join_channels() -> [u32; 3] {
|
||||
JOIN_CHANNELS
|
||||
}
|
||||
|
||||
fn get_default_rx2() -> u32 {
|
||||
866_550_000
|
||||
}
|
||||
}
|
||||
|
||||
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||
|
||||
pub(crate) const DATARATES: [Option<Datarate>; 6] = [
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_12,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 59,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_11,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 59,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_10,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 59,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_9,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 123,
|
||||
max_mac_payload_size_with_dwell_time: 123,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_8,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
// TODO: ignore FSK data rate for now
|
||||
];
|
||||
220
lorawan-device-patch/src/region/dynamic_channel_plans/mod.rs
Normal file
220
lorawan-device-patch/src/region/dynamic_channel_plans/mod.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use super::*;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
#[cfg(any(
|
||||
feature = "region-as923-1",
|
||||
feature = "region-as923-2",
|
||||
feature = "region-as923-3",
|
||||
feature = "region-as923-4"
|
||||
))]
|
||||
mod as923;
|
||||
#[cfg(feature = "region-eu433")]
|
||||
mod eu433;
|
||||
#[cfg(feature = "region-eu868")]
|
||||
mod eu868;
|
||||
#[cfg(feature = "region-in865")]
|
||||
mod in865;
|
||||
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
pub(crate) use as923::AS923_1;
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
pub(crate) use as923::AS923_2;
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
pub(crate) use as923::AS923_3;
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
pub(crate) use as923::AS923_4;
|
||||
#[cfg(feature = "region-eu433")]
|
||||
pub(crate) use eu433::EU433;
|
||||
#[cfg(feature = "region-eu868")]
|
||||
pub(crate) use eu868::EU868;
|
||||
#[cfg(feature = "region-in865")]
|
||||
pub(crate) use in865::IN865;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub(crate) struct DynamicChannelPlan<
|
||||
const NUM_JOIN_CHANNELS: usize,
|
||||
const NUM_DATARATES: usize,
|
||||
R: DynamicChannelRegion<NUM_JOIN_CHANNELS, NUM_DATARATES>,
|
||||
> {
|
||||
additional_channels: [Option<u32>; 5],
|
||||
channel_mask: ChannelMask<9>,
|
||||
last_tx_channel: u8,
|
||||
_fixed_channel_region: PhantomData<R>,
|
||||
rx1_offset: usize,
|
||||
rx2_dr: usize,
|
||||
}
|
||||
|
||||
impl<
|
||||
const NUM_JOIN_CHANNELS: usize,
|
||||
const NUM_DATARATES: usize,
|
||||
R: DynamicChannelRegion<NUM_JOIN_CHANNELS, NUM_DATARATES>,
|
||||
> DynamicChannelPlan<NUM_JOIN_CHANNELS, NUM_DATARATES, R>
|
||||
{
|
||||
fn get_channel(&self, channel: usize) -> Option<u32> {
|
||||
if channel < NUM_JOIN_CHANNELS {
|
||||
Some(R::join_channels()[channel])
|
||||
} else {
|
||||
let index = channel - NUM_JOIN_CHANNELS;
|
||||
if index < self.additional_channels.len() {
|
||||
self.additional_channels[index]
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn highest_additional_channel_index_plus_one(&self) -> usize {
|
||||
let mut index_plus_one = 0;
|
||||
for (i, channel) in self.additional_channels.iter().enumerate() {
|
||||
if channel.is_some() {
|
||||
index_plus_one = i + 1;
|
||||
}
|
||||
}
|
||||
index_plus_one
|
||||
}
|
||||
|
||||
fn get_random_in_range<RNG: RngCore>(&self, rng: &mut RNG) -> usize {
|
||||
let range = self.highest_additional_channel_index_plus_one() + NUM_JOIN_CHANNELS;
|
||||
let cm = if range > 16 {
|
||||
0b11111
|
||||
} else if range > 8 {
|
||||
0b1111
|
||||
} else {
|
||||
0b111
|
||||
};
|
||||
(rng.next_u32() as usize) & cm
|
||||
}
|
||||
|
||||
pub fn get_max_payload_length(datarate: DR, repeater_compatible: bool, dwell_time: bool) -> u8 {
|
||||
R::get_max_payload_length(datarate, repeater_compatible, dwell_time)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait DynamicChannelRegion<const NUM_JOIN_CHANNELS: usize, const NUM_DATARATES: usize>:
|
||||
ChannelRegion<NUM_DATARATES>
|
||||
{
|
||||
fn join_channels() -> [u32; NUM_JOIN_CHANNELS];
|
||||
fn get_default_rx2() -> u32;
|
||||
}
|
||||
|
||||
impl<
|
||||
const NUM_JOIN_CHANNELS: usize,
|
||||
const NUM_DATARATES: usize,
|
||||
R: DynamicChannelRegion<NUM_JOIN_CHANNELS, NUM_DATARATES>,
|
||||
> RegionHandler for DynamicChannelPlan<NUM_JOIN_CHANNELS, NUM_DATARATES, R>
|
||||
{
|
||||
fn process_join_accept<T: AsRef<[u8]>, C>(
|
||||
&mut self,
|
||||
join_accept: &DecryptedJoinAcceptPayload<T, C>,
|
||||
) {
|
||||
match join_accept.c_f_list() {
|
||||
Some(CfList::DynamicChannel(cf_list)) => {
|
||||
// If CfList of Type 0 is present, it may contain up to 5 frequencies
|
||||
// which define channels J to (J+4)
|
||||
for (index, freq) in cf_list.iter().enumerate() {
|
||||
let value = freq.value();
|
||||
// unused channels are set to 0
|
||||
if value != 0 {
|
||||
self.additional_channels[index] = Some(value);
|
||||
} else {
|
||||
self.additional_channels[index] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(CfList::FixedChannel(_cf_list)) => {
|
||||
//TODO: dynamic channel plans have corresponding fixed channel lists,
|
||||
// however, this feature is entirely optional
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_link_adr_channel_mask(
|
||||
&mut self,
|
||||
channel_mask_control: u8,
|
||||
channel_mask: ChannelMask<2>,
|
||||
) {
|
||||
match channel_mask_control {
|
||||
0..=4 => {
|
||||
let base_index = channel_mask_control as usize * 2;
|
||||
self.channel_mask.set_bank(base_index, channel_mask.get_index(0));
|
||||
self.channel_mask.set_bank(base_index + 1, channel_mask.get_index(1));
|
||||
}
|
||||
5 => {
|
||||
let channel_mask: u16 =
|
||||
channel_mask.get_index(0) as u16 | ((channel_mask.get_index(1) as u16) << 8);
|
||||
self.channel_mask.set_bank(0, ((channel_mask & 0b1) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(1, ((channel_mask & 0b10) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(2, ((channel_mask & 0b100) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(3, ((channel_mask & 0b1000) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(4, ((channel_mask & 0b10000) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(5, ((channel_mask & 0b100000) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(6, ((channel_mask & 0b1000000) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(7, ((channel_mask & 0b10000000) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(8, ((channel_mask & 0b100000000) * 0xFF) as u8);
|
||||
}
|
||||
6 => {
|
||||
// all channels on
|
||||
for i in 0..8 {
|
||||
self.channel_mask.set_bank(i, 0xFF);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
//RFU
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tx_dr_and_frequency<RNG: RngCore>(
|
||||
&mut self,
|
||||
rng: &mut RNG,
|
||||
datarate: DR,
|
||||
frame: &Frame,
|
||||
) -> (Datarate, u32) {
|
||||
match frame {
|
||||
Frame::Join => {
|
||||
// there are at most 8 join channels
|
||||
let mut channel = (rng.next_u32() & 0b111) as u8;
|
||||
// keep sampling until we select a join channel depending
|
||||
// on the frequency plan
|
||||
while channel as usize >= NUM_JOIN_CHANNELS {
|
||||
channel = (rng.next_u32() & 0b111) as u8;
|
||||
}
|
||||
self.last_tx_channel = channel;
|
||||
(
|
||||
R::datarates()[datarate as usize].clone().unwrap(),
|
||||
R::join_channels()[channel as usize],
|
||||
)
|
||||
}
|
||||
Frame::Data => {
|
||||
let mut channel = self.get_random_in_range(rng);
|
||||
loop {
|
||||
if self.channel_mask.is_enabled(channel).unwrap() {
|
||||
if let Some(freq) = self.get_channel(channel) {
|
||||
self.last_tx_channel = channel as u8;
|
||||
return (R::datarates()[datarate as usize].clone().unwrap(), freq);
|
||||
}
|
||||
}
|
||||
channel = self.get_random_in_range(rng)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_rx_frequency(&self, _frame: &Frame, window: &Window) -> u32 {
|
||||
match window {
|
||||
// TODO: implement RxOffset but first need to implement RxOffset MacCommand
|
||||
Window::_1 => self.get_channel(self.last_tx_channel as usize).unwrap(),
|
||||
Window::_2 => R::get_default_rx2(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_rx_datarate(&self, tx_datarate: DR, _frame: &Frame, window: &Window) -> Datarate {
|
||||
let datarate = match window {
|
||||
Window::_1 => tx_datarate as usize + self.rx1_offset,
|
||||
Window::_2 => self.rx2_dr,
|
||||
};
|
||||
R::datarates()[datarate].clone().unwrap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||
|
||||
pub(crate) const DATARATES: [Option<Datarate>; 16] = [
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_12,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 0,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_11,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 0,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_10,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 59,
|
||||
max_mac_payload_size_with_dwell_time: 19,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_9,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 123,
|
||||
max_mac_payload_size_with_dwell_time: 61,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_8,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 133,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_8,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
None, // LR-FHSS -- not currently supported, TODO: defined in rp002-1-0-4
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_12,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 61,
|
||||
max_mac_payload_size_with_dwell_time: 61,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_11,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 137,
|
||||
max_mac_payload_size_with_dwell_time: 137,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_10,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_9,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_8,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
None, // RFU, TODO: defined in rp002-1-0-4
|
||||
None, // TODO: defined in rp002-1-0-4
|
||||
];
|
||||
@@ -0,0 +1,94 @@
|
||||
pub(crate) const UPLINK_CHANNEL_MAP: [u32; 72] = [
|
||||
// channels 0-7 (125 kHz)
|
||||
915_200_000,
|
||||
915_400_000,
|
||||
915_600_000,
|
||||
915_800_000,
|
||||
916_000_000,
|
||||
916_200_000,
|
||||
916_400_000,
|
||||
916_600_000,
|
||||
// channels 8-15 (125 kHz)
|
||||
916_800_000,
|
||||
917_000_000,
|
||||
917_200_000,
|
||||
917_400_000,
|
||||
917_600_000,
|
||||
917_800_000,
|
||||
918_000_000,
|
||||
918_200_000,
|
||||
// channels 16-23 (125 kHz)
|
||||
918_400_000,
|
||||
918_600_000,
|
||||
918_800_000,
|
||||
919_000_000,
|
||||
919_200_000,
|
||||
919_400_000,
|
||||
919_600_000,
|
||||
919_800_000,
|
||||
// channels 24-31 (125 kHz)
|
||||
920_000_000,
|
||||
920_200_000,
|
||||
920_400_000,
|
||||
920_600_000,
|
||||
920_800_000,
|
||||
921_000_000,
|
||||
921_200_000,
|
||||
921_400_000,
|
||||
// channels 32-39 (125 kHz)
|
||||
921_600_000,
|
||||
921_800_000,
|
||||
922_000_000,
|
||||
922_200_000,
|
||||
922_400_000,
|
||||
922_600_000,
|
||||
922_800_000,
|
||||
923_000_000,
|
||||
// channels 39-47 (125 kHz)
|
||||
923_200_000,
|
||||
923_400_000,
|
||||
923_600_000,
|
||||
923_800_000,
|
||||
924_000_000,
|
||||
924_200_000,
|
||||
924_400_000,
|
||||
924_600_000,
|
||||
// channels 47-55 (125 kHz)
|
||||
924_800_000,
|
||||
925_000_000,
|
||||
925_200_000,
|
||||
925_400_000,
|
||||
925_600_000,
|
||||
925_800_000,
|
||||
926_000_000,
|
||||
926_200_000,
|
||||
// channels 55-63 (125 kHz)
|
||||
926_400_000,
|
||||
926_600_000,
|
||||
926_800_000,
|
||||
927_000_000,
|
||||
927_200_000,
|
||||
927_400_000,
|
||||
927_600_000,
|
||||
927_800_000,
|
||||
// channels 63-71 (500 kHz)
|
||||
915_900_000,
|
||||
917_500_000,
|
||||
919_100_000,
|
||||
920_700_000,
|
||||
922_300_000,
|
||||
923_900_000,
|
||||
925_500_000,
|
||||
927_100_000,
|
||||
];
|
||||
|
||||
pub(crate) const DOWNLINK_CHANNEL_MAP: [u32; 8] = [
|
||||
922_300_000,
|
||||
923_900_000,
|
||||
924_500_000,
|
||||
925_100_000,
|
||||
925_700_000,
|
||||
926_300_000,
|
||||
926_900_000,
|
||||
927_500_000,
|
||||
];
|
||||
@@ -0,0 +1,84 @@
|
||||
use super::*;
|
||||
|
||||
mod frequencies;
|
||||
use frequencies::*;
|
||||
|
||||
mod datarates;
|
||||
use datarates::*;
|
||||
|
||||
const AU_DBM: i8 = 21;
|
||||
const DEFAULT_RX2: u32 = 923_300_000;
|
||||
|
||||
/// State struct for the `AU915` region. This struct may be created directly if you wish to fine-tune some parameters.
|
||||
/// At this time specifying a bias for the subband used during the join process is supported using
|
||||
/// [`set_join_bias`](Self::set_join_bias) and [`set_join_bias_and_noncompliant_retries`](Self::set_join_bias_and_noncompliant_retries)
|
||||
/// is suppored. This struct can then be turned into a [`Configuration`] as it implements [`Into<Configuration>`].
|
||||
///
|
||||
/// # Note:
|
||||
///
|
||||
/// Only [`US915`] and [`AU915`] can be created using this method, because they are the only ones which have
|
||||
/// parameters that may be fine-tuned at the region level. To create a [`Configuration`] for other regions, use
|
||||
/// [`Configuration::new`] and specify the region using the [`Region`] enum.
|
||||
///
|
||||
/// # Example: Setting up join bias
|
||||
///
|
||||
/// ```
|
||||
/// use lorawan_device::region::{Configuration, AU915, Subband};
|
||||
///
|
||||
/// let mut au915 = AU915::new();
|
||||
/// // Subband 2 is commonly used for The Things Network.
|
||||
/// au915.set_join_bias(Subband::_2);
|
||||
/// let configuration: Configuration = au915.into();
|
||||
/// ```
|
||||
#[derive(Default, Clone)]
|
||||
pub struct AU915(pub(crate) FixedChannelPlan<16, AU915Region>);
|
||||
|
||||
impl AU915 {
|
||||
pub fn get_max_payload_length(datarate: DR, repeater_compatible: bool, dwell_time: bool) -> u8 {
|
||||
AU915Region::get_max_payload_length(datarate, repeater_compatible, dwell_time)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct AU915Region;
|
||||
|
||||
impl ChannelRegion<16> for AU915Region {
|
||||
fn datarates() -> &'static [Option<Datarate>; 16] {
|
||||
&DATARATES
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedChannelRegion<16> for AU915Region {
|
||||
fn uplink_channels() -> &'static [u32; 72] {
|
||||
&UPLINK_CHANNEL_MAP
|
||||
}
|
||||
fn downlink_channels() -> &'static [u32; 8] {
|
||||
&DOWNLINK_CHANNEL_MAP
|
||||
}
|
||||
fn get_default_rx2() -> u32 {
|
||||
DEFAULT_RX2
|
||||
}
|
||||
fn get_rx_datarate(tx_datarate: DR, _frame: &Frame, window: &Window) -> Datarate {
|
||||
let datarate = match window {
|
||||
Window::_1 => {
|
||||
// no support for RX1 DR Offset
|
||||
match tx_datarate {
|
||||
DR::_0 => DR::_8,
|
||||
DR::_1 => DR::_9,
|
||||
DR::_2 => DR::_10,
|
||||
DR::_3 => DR::_11,
|
||||
DR::_4 => DR::_12,
|
||||
DR::_5 => DR::_13,
|
||||
DR::_6 => DR::_13,
|
||||
DR::_7 => DR::_9,
|
||||
_ => panic!("Invalid TX datarate"),
|
||||
}
|
||||
}
|
||||
Window::_2 => DR::_8,
|
||||
};
|
||||
DATARATES[datarate as usize].clone().unwrap()
|
||||
}
|
||||
fn get_dbm() -> i8 {
|
||||
AU_DBM
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
use super::*;
|
||||
use core::cmp::Ordering;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub(crate) struct JoinChannels {
|
||||
/// The maximum amount of times we attempt to join on the preferred subband.
|
||||
max_retries: usize,
|
||||
/// The amount of times we've currently attempted to join on the preferred subband.
|
||||
pub num_retries: usize,
|
||||
/// Preferred subband
|
||||
preferred_subband: Option<Subband>,
|
||||
/// Channels that have been attempted.
|
||||
pub(crate) available_channels: AvailableChannels,
|
||||
/// The channel used for the previous join request.
|
||||
pub(crate) previous_channel: u8,
|
||||
}
|
||||
|
||||
impl JoinChannels {
|
||||
pub(crate) fn has_bias_and_not_exhausted(&self) -> bool {
|
||||
// there are remaining retries AND we have not yet been reset
|
||||
self.preferred_subband.is_some()
|
||||
&& self.num_retries < self.max_retries
|
||||
&& self.num_retries != 0
|
||||
}
|
||||
|
||||
/// The first data channel will always be some random channel (possibly the same as previous)
|
||||
/// of the preferred subband. Returns None if there is no preferred subband.
|
||||
pub(crate) fn first_data_channel(&mut self, rng: &mut impl RngCore) -> Option<u8> {
|
||||
if self.preferred_subband.is_some() && self.num_retries != 0 {
|
||||
self.clear_join_bias();
|
||||
// determine which subband the successful join was sent on
|
||||
let sb = if self.previous_channel < 64 {
|
||||
self.previous_channel / 8
|
||||
} else {
|
||||
self.previous_channel % 8
|
||||
};
|
||||
// pick another channel on that subband
|
||||
Some((rng.next_u32() & 0b111) as u8 + (sb * 8))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_join_bias(&mut self, subband: Subband, max_retries: usize) {
|
||||
self.preferred_subband = Some(subband);
|
||||
self.max_retries = max_retries;
|
||||
}
|
||||
|
||||
pub(crate) fn clear_join_bias(&mut self) {
|
||||
self.preferred_subband = None;
|
||||
self.max_retries = 0;
|
||||
}
|
||||
|
||||
/// To be called after a join accept is received. Resets state for the next join attempt.
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.num_retries = 0;
|
||||
self.available_channels = AvailableChannels::default();
|
||||
}
|
||||
|
||||
pub(crate) fn get_next_channel(&mut self, rng: &mut impl RngCore) -> u8 {
|
||||
match (self.preferred_subband, self.num_retries.cmp(&self.max_retries)) {
|
||||
(Some(sb), Ordering::Less) => {
|
||||
self.num_retries += 1;
|
||||
// pick a random number 0-7 on the preferred subband
|
||||
// NB: we don't use 500 kHz channels
|
||||
let channel = (rng.next_u32() % 8) as u8 + ((sb as usize - 1) as u8 * 8);
|
||||
if self.num_retries == self.max_retries {
|
||||
// this is our last try with our favorite subband, so will initialize the
|
||||
// standard join logic with the channel we just tried. This will ensure
|
||||
// standard and compliant behavior when num_retries is set to 1.
|
||||
self.available_channels.previous = Some(channel);
|
||||
self.available_channels.data.set_channel(channel.into(), false);
|
||||
}
|
||||
self.previous_channel = channel;
|
||||
channel
|
||||
}
|
||||
_ => {
|
||||
self.num_retries += 1;
|
||||
self.available_channels.get_next(rng)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub(crate) struct AvailableChannels {
|
||||
data: ChannelMask<9>,
|
||||
previous: Option<u8>,
|
||||
}
|
||||
|
||||
impl AvailableChannels {
|
||||
fn is_exhausted(&self) -> bool {
|
||||
// check if every underlying byte is entirely cleared to 0
|
||||
for byte in self.data.as_ref() {
|
||||
if *byte != 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn get_next(&mut self, rng: &mut impl RngCore) -> u8 {
|
||||
// this guarantees that there will be _some_ open channel available
|
||||
if self.is_exhausted() {
|
||||
self.reset();
|
||||
}
|
||||
|
||||
let channel = self.get_next_channel_inner(rng);
|
||||
// mark the channel invalid for future selection
|
||||
self.data.set_channel(channel.into(), false);
|
||||
self.previous = Some(channel);
|
||||
channel
|
||||
}
|
||||
|
||||
fn get_next_channel_inner(&mut self, rng: &mut impl RngCore) -> u8 {
|
||||
if let Some(previous) = self.previous {
|
||||
// choose the next one by possibly wrapping around
|
||||
let next = (previous + 8) % 72;
|
||||
// if the channel is valid, great!
|
||||
if self.data.is_enabled(next.into()).unwrap() {
|
||||
next
|
||||
} else {
|
||||
// We've wrapped around to our original random bank.
|
||||
// Randomly select a new channel on the original bank.
|
||||
// NB: there shall always be something because this will be the first
|
||||
// bank to get exhausted and the caller of this function will reset
|
||||
// when the last one is exhausted.
|
||||
let bank = next / 8;
|
||||
let mut entropy = rng.next_u32();
|
||||
let mut channel = (entropy & 0b111) as u8 + bank * 8;
|
||||
let mut entropy_used = 1;
|
||||
loop {
|
||||
if self.data.is_enabled(channel.into()).unwrap() {
|
||||
return channel;
|
||||
} else {
|
||||
// we've used 30 of the 32 bits of entropy. reset the byte
|
||||
if entropy_used == 10 {
|
||||
entropy = rng.next_u32();
|
||||
entropy_used = 0;
|
||||
}
|
||||
entropy >>= 3;
|
||||
entropy_used += 1;
|
||||
channel = (entropy & 0b111) as u8 + bank * 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// pick a completely random channel on the bottom 64
|
||||
// NB: all channels are currently valid
|
||||
(rng.next_u32() as u8) & 0b111111
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.data = ChannelMask::default();
|
||||
self.previous = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// This macro implements public functions relating to a fixed plan region. This is preferred to a
|
||||
/// trait implementation because the user does not have to worry about importing the trait to make
|
||||
/// use of these functions.
|
||||
macro_rules! impl_join_bias {
|
||||
($region:ident) => {
|
||||
impl $region {
|
||||
/// Create this struct directly if you want to specify a subband on which to bias the join process.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Specify a preferred subband when joining the network. Only the first join attempt
|
||||
/// will occur on this subband. After that, each bank will be attempted sequentially
|
||||
/// as described in the US915/AU915 regional specifications.
|
||||
pub fn set_join_bias(&mut self, subband: Subband) {
|
||||
self.0.join_channels.set_join_bias(subband, 1)
|
||||
}
|
||||
|
||||
/// # ⚠️Warning⚠️
|
||||
///
|
||||
/// This method is explicitly not compliant with the LoRaWAN spec when more than one
|
||||
/// try is attempted.
|
||||
///
|
||||
/// This method is similar to `set_join_bias`, but allows you to specify a potentially
|
||||
/// non-compliant amount of times your preferred join subband should be attempted.
|
||||
///
|
||||
/// It is recommended to set a low number (ie, < 10) of join retries using the
|
||||
/// preferred subband. The reason for this is if you *only* try to join
|
||||
/// with a channel bias, and the network is configured to use a
|
||||
/// strictly different set of channels than the ones you provide, the
|
||||
/// network will NEVER be joined.
|
||||
pub fn set_join_bias_and_noncompliant_retries(
|
||||
&mut self,
|
||||
subband: Subband,
|
||||
max_retries: usize,
|
||||
) {
|
||||
self.0.join_channels.set_join_bias(subband, max_retries)
|
||||
}
|
||||
|
||||
pub fn clear_join_bias(&mut self) {
|
||||
self.0.join_channels.clear_join_bias()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "region-au915")]
|
||||
impl_join_bias!(AU915);
|
||||
#[cfg(feature = "region-us915")]
|
||||
impl_join_bias!(US915);
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::mac::Response;
|
||||
use crate::{
|
||||
mac::{Mac, SendData},
|
||||
test_util::{get_key, handle_join_request, Uplink},
|
||||
AppEui, AppKey, DevEui, NetworkCredentials,
|
||||
};
|
||||
use heapless::Vec;
|
||||
use lorawan::default_crypto::DefaultFactory;
|
||||
|
||||
#[test]
|
||||
fn test_join_channels_standard() {
|
||||
let mut rng = rand_core::OsRng;
|
||||
// run the test a bunch of times due to the rng
|
||||
for _ in 0..100 {
|
||||
let mut join_channels = JoinChannels::default();
|
||||
let first_channel = join_channels.get_next_channel(&mut rng);
|
||||
// the first channel is always in the bottom 64
|
||||
assert!(first_channel < 64);
|
||||
let next_channel = join_channels.get_next_channel(&mut rng);
|
||||
// the next channel is always incremented by 8, since we always have
|
||||
// the fat bank (channels 64-71)
|
||||
assert_eq!(next_channel, first_channel + 8);
|
||||
// we generate 6 more channels
|
||||
for _ in 0..7 {
|
||||
let c = join_channels.get_next_channel(&mut rng);
|
||||
assert!(c < 72);
|
||||
}
|
||||
// after 8 tries, we should be back at the original bank but on a different channel
|
||||
let ninth_channel = join_channels.get_next_channel(&mut rng);
|
||||
assert_eq!(ninth_channel / 8, first_channel / 8);
|
||||
assert_ne!(ninth_channel, first_channel);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_channels_standard_exhausted() {
|
||||
let mut rng = rand_core::OsRng;
|
||||
|
||||
let mut join_channels = JoinChannels::default();
|
||||
let first_channel = join_channels.get_next_channel(&mut rng);
|
||||
// the first channel is always in the bottom 64
|
||||
assert!(first_channel < 64);
|
||||
let next_channel = join_channels.get_next_channel(&mut rng);
|
||||
// the next channel is always incremented by 8, since we always have
|
||||
// the fat bank (channels 64-71)
|
||||
assert_eq!(next_channel, first_channel + 8);
|
||||
// we generate 6000
|
||||
for _ in 0..6000 {
|
||||
let c = join_channels.get_next_channel(&mut rng);
|
||||
assert!(c < 72);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_channels_biased() {
|
||||
let mut rng = rand_core::OsRng;
|
||||
// run the test a bunch of times due to the rng
|
||||
for _ in 0..100 {
|
||||
let mut join_channels = JoinChannels::default();
|
||||
join_channels.set_join_bias(Subband::_2, 1);
|
||||
let first_channel = join_channels.get_next_channel(&mut rng);
|
||||
// the first is on subband 2
|
||||
assert!(first_channel > 7);
|
||||
assert!(first_channel < 16);
|
||||
let next_channel = join_channels.get_next_channel(&mut rng);
|
||||
// the next channel is always incremented by 8, since we always have
|
||||
// the fat bank (channels 64-71)
|
||||
assert_eq!(next_channel, first_channel + 8);
|
||||
// we generate 6 more channels
|
||||
for _ in 0..7 {
|
||||
let c = join_channels.get_next_channel(&mut rng);
|
||||
assert!(c < 72);
|
||||
}
|
||||
// after 8 tries, we should be back at the biased bank but on a different channel
|
||||
let ninth_channel = join_channels.get_next_channel(&mut rng);
|
||||
assert_eq!(ninth_channel / 8, first_channel / 8);
|
||||
assert_ne!(ninth_channel, first_channel);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_mac_compliant_bias() {
|
||||
let mut us915 = US915::new();
|
||||
us915.set_join_bias(Subband::_2);
|
||||
let mut mac = Mac::new(us915.into(), 21, 2);
|
||||
|
||||
let mut buf: RadioBuffer<255> = RadioBuffer::new();
|
||||
let (tx_config, _len) = mac.join_otaa::<DefaultFactory, _, 255>(
|
||||
&mut rand::rngs::OsRng,
|
||||
NetworkCredentials::new(
|
||||
AppEui::from([0x0; 8]),
|
||||
DevEui::from([0x0; 8]),
|
||||
AppKey::from(get_key()),
|
||||
),
|
||||
&mut buf,
|
||||
);
|
||||
// Confirm that the join request occurs on our subband
|
||||
assert!(
|
||||
tx_config.rf.frequency >= 903_900_000,
|
||||
"Unexpected frequency: {} is below 903.9 MHz!",
|
||||
tx_config.rf.frequency
|
||||
);
|
||||
assert!(
|
||||
tx_config.rf.frequency <= 905_300_000,
|
||||
"Unexpected frequency: {} is above 905.3 MHz!",
|
||||
tx_config.rf.frequency
|
||||
);
|
||||
let mut downlinks: Vec<_, 3> = Vec::new();
|
||||
let mut data = std::vec::Vec::new();
|
||||
data.extend_from_slice(buf.as_ref_for_read());
|
||||
let uplink = Uplink::new(buf.as_ref_for_read(), tx_config).unwrap();
|
||||
|
||||
let mut rx_buf = [0; 255];
|
||||
let len = handle_join_request::<0>(Some(uplink), tx_config.rf, &mut rx_buf);
|
||||
buf.clear();
|
||||
buf.extend_from_slice(&rx_buf[..len]).unwrap();
|
||||
let response = mac.handle_rx::<DefaultFactory, 255, 3>(&mut buf, &mut downlinks);
|
||||
if let Response::JoinSuccess = response {} else {
|
||||
panic!("Did not receive join success");
|
||||
}
|
||||
let (tx_config, _len) = mac
|
||||
.send::<DefaultFactory, _, 255>(
|
||||
&mut rand::rngs::OsRng,
|
||||
&mut buf,
|
||||
&SendData { fport: 1, data: &[0x0; 1], confirmed: false },
|
||||
)
|
||||
.unwrap();
|
||||
// Confirm that the first data frame occurs on our subband
|
||||
assert!(
|
||||
tx_config.rf.frequency >= 903_900_000,
|
||||
"Unexpected frequency: {} is below 903.9 MHz!",
|
||||
tx_config.rf.frequency
|
||||
);
|
||||
assert!(
|
||||
tx_config.rf.frequency <= 905_300_000,
|
||||
"Unexpected frequency: {} is above 905.3 MHz!",
|
||||
tx_config.rf.frequency
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_mac_non_compliant_bias() {
|
||||
let mut us915 = US915::new();
|
||||
us915.set_join_bias_and_noncompliant_retries(Subband::_2, 8);
|
||||
let mut mac = Mac::new(us915.into(), 21, 2);
|
||||
|
||||
let mut buf: RadioBuffer<255> = RadioBuffer::new();
|
||||
let (tx_config, _len) = mac.join_otaa::<DefaultFactory, _, 255>(
|
||||
&mut rand::rngs::OsRng,
|
||||
NetworkCredentials::new(
|
||||
AppEui::from([0x0; 8]),
|
||||
DevEui::from([0x0; 8]),
|
||||
AppKey::from(get_key()),
|
||||
),
|
||||
&mut buf,
|
||||
);
|
||||
// Confirm that the join request occurs on our subband
|
||||
assert!(
|
||||
tx_config.rf.frequency >= 903_900_000,
|
||||
"Unexpected frequency: {} is below 903.9 MHz!",
|
||||
tx_config.rf.frequency
|
||||
);
|
||||
assert!(
|
||||
tx_config.rf.frequency <= 905_300_000,
|
||||
"Unexpected frequency: {} is above 905.3 MHz!",
|
||||
tx_config.rf.frequency
|
||||
);
|
||||
let mut downlinks: Vec<_, 3> = Vec::new();
|
||||
let mut data = std::vec::Vec::new();
|
||||
data.extend_from_slice(buf.as_ref_for_read());
|
||||
let uplink = Uplink::new(buf.as_ref_for_read(), tx_config).unwrap();
|
||||
|
||||
let mut rx_buf = [0; 255];
|
||||
let len = handle_join_request::<0>(Some(uplink), tx_config.rf, &mut rx_buf);
|
||||
buf.clear();
|
||||
buf.extend_from_slice(&rx_buf[..len]).unwrap();
|
||||
let response = mac.handle_rx::<DefaultFactory, 255, 3>(&mut buf, &mut downlinks);
|
||||
if let Response::JoinSuccess = response {} else {
|
||||
panic!("Did not receive JoinSuccess")
|
||||
}
|
||||
for _ in 0..8 {
|
||||
let (tx_config, _len) = mac
|
||||
.send::<DefaultFactory, _, 255>(
|
||||
&mut rand::rngs::OsRng,
|
||||
&mut buf,
|
||||
&SendData { fport: 1, data: &[0x0; 1], confirmed: false },
|
||||
)
|
||||
.unwrap();
|
||||
// Confirm that the first data frame occurs on our subband
|
||||
assert!(
|
||||
tx_config.rf.frequency >= 903_900_000,
|
||||
"Unexpected frequency: {} is below 903.9 MHz!",
|
||||
tx_config.rf.frequency
|
||||
);
|
||||
assert!(
|
||||
tx_config.rf.frequency <= 905_300_000,
|
||||
"Unexpected frequency: {} is above 905.3 MHz!",
|
||||
tx_config.rf.frequency
|
||||
);
|
||||
mac.rx2_complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
205
lorawan-device-patch/src/region/fixed_channel_plans/mod.rs
Normal file
205
lorawan-device-patch/src/region/fixed_channel_plans/mod.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use super::*;
|
||||
use core::marker::PhantomData;
|
||||
use lorawan::maccommands::ChannelMask;
|
||||
|
||||
mod join_channels;
|
||||
use join_channels::JoinChannels;
|
||||
|
||||
#[cfg(feature = "region-au915")]
|
||||
mod au915;
|
||||
#[cfg(feature = "region-us915")]
|
||||
mod us915;
|
||||
|
||||
#[cfg(feature = "region-au915")]
|
||||
pub use au915::AU915;
|
||||
#[cfg(feature = "region-us915")]
|
||||
pub use us915::US915;
|
||||
|
||||
seq_macro::seq!(
|
||||
N in 1..=8 {
|
||||
/// Subband definitions used to bias the join process of a fixed channel plan (ie: US915, AU915).
|
||||
/// Each Subband holds 8 channels. eg: subband 1 contains: channels 0-7, subband 2: channels 8-15, etc.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(usize)]
|
||||
pub enum Subband {
|
||||
#(
|
||||
_~N = N,
|
||||
)*
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl From<Subband> for usize {
|
||||
fn from(value: Subband) -> Self {
|
||||
value as usize
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub(crate) struct FixedChannelPlan<const NUM_DR: usize, F: FixedChannelRegion<NUM_DR>> {
|
||||
last_tx_channel: u8,
|
||||
channel_mask: ChannelMask<9>,
|
||||
_fixed_channel_region: PhantomData<F>,
|
||||
join_channels: JoinChannels,
|
||||
}
|
||||
|
||||
impl<const D: usize, F: FixedChannelRegion<D>> FixedChannelPlan<D, F> {
|
||||
pub fn set_125k_channels(&mut self, enabled: bool) {
|
||||
let mask = if enabled {
|
||||
0xFF
|
||||
} else {
|
||||
0x00
|
||||
};
|
||||
self.channel_mask.set_bank(0, mask);
|
||||
self.channel_mask.set_bank(1, mask);
|
||||
self.channel_mask.set_bank(2, mask);
|
||||
self.channel_mask.set_bank(3, mask);
|
||||
self.channel_mask.set_bank(4, mask);
|
||||
self.channel_mask.set_bank(5, mask);
|
||||
self.channel_mask.set_bank(6, mask);
|
||||
self.channel_mask.set_bank(7, mask);
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn get_max_payload_length(datarate: DR, repeater_compatible: bool, dwell_time: bool) -> u8 {
|
||||
F::get_max_payload_length(datarate, repeater_compatible, dwell_time)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait FixedChannelRegion<const D: usize>: ChannelRegion<D> {
|
||||
fn uplink_channels() -> &'static [u32; 72];
|
||||
fn downlink_channels() -> &'static [u32; 8];
|
||||
fn get_default_rx2() -> u32;
|
||||
fn get_rx_datarate(tx_datarate: DR, frame: &Frame, window: &Window) -> Datarate;
|
||||
fn get_dbm() -> i8;
|
||||
}
|
||||
|
||||
impl<const D: usize, F: FixedChannelRegion<D>> RegionHandler for FixedChannelPlan<D, F> {
|
||||
fn process_join_accept<T: AsRef<[u8]>, C>(
|
||||
&mut self,
|
||||
join_accept: &DecryptedJoinAcceptPayload<T, C>,
|
||||
) {
|
||||
if let Some(CfList::FixedChannel(channel_mask)) = join_accept.c_f_list() {
|
||||
// Reset the join channels state
|
||||
self.join_channels.reset();
|
||||
self.channel_mask = channel_mask;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_link_adr_channel_mask(
|
||||
&mut self,
|
||||
channel_mask_control: u8,
|
||||
channel_mask: ChannelMask<2>,
|
||||
) {
|
||||
self.join_channels.reset();
|
||||
match channel_mask_control {
|
||||
0..=4 => {
|
||||
let base_index = channel_mask_control as usize * 2;
|
||||
self.channel_mask.set_bank(base_index, channel_mask.get_index(0));
|
||||
self.channel_mask.set_bank(base_index + 1, channel_mask.get_index(1));
|
||||
}
|
||||
5 => {
|
||||
let channel_mask: u16 =
|
||||
channel_mask.get_index(0) as u16 | ((channel_mask.get_index(1) as u16) << 8);
|
||||
self.channel_mask.set_bank(0, ((channel_mask & 0b1) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(1, ((channel_mask & 0b10) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(2, ((channel_mask & 0b100) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(3, ((channel_mask & 0b1000) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(4, ((channel_mask & 0b10000) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(5, ((channel_mask & 0b100000) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(6, ((channel_mask & 0b1000000) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(7, ((channel_mask & 0b10000000) * 0xFF) as u8);
|
||||
self.channel_mask.set_bank(8, ((channel_mask & 0b100000000) * 0xFF) as u8);
|
||||
}
|
||||
6 => {
|
||||
self.set_125k_channels(true);
|
||||
}
|
||||
7 => {
|
||||
self.set_125k_channels(false);
|
||||
}
|
||||
_ => {
|
||||
//RFU
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tx_dr_and_frequency<RNG: RngCore>(
|
||||
&mut self,
|
||||
rng: &mut RNG,
|
||||
datarate: DR,
|
||||
frame: &Frame,
|
||||
) -> (Datarate, u32) {
|
||||
match frame {
|
||||
Frame::Join => {
|
||||
let channel = self.join_channels.get_next_channel(rng);
|
||||
let dr = if channel < 64 {
|
||||
DR::_0
|
||||
} else {
|
||||
DR::_4
|
||||
};
|
||||
self.last_tx_channel = channel;
|
||||
let data_rate = F::datarates()[dr as usize].clone().unwrap();
|
||||
(data_rate, F::uplink_channels()[channel as usize])
|
||||
}
|
||||
Frame::Data => {
|
||||
// The join bias gets reset after receiving CFList in Join Frame
|
||||
// or ChannelMask in the LinkADRReq in Data Frame.
|
||||
// If it has not been reset yet, we continue to use the bias for the data frames.
|
||||
// We hope to acquire ChannelMask via LinkADRReq.
|
||||
let (data_rate, channel) = if self.join_channels.has_bias_and_not_exhausted() {
|
||||
let channel = self.join_channels.get_next_channel(rng);
|
||||
let dr = if channel < 64 {
|
||||
DR::_0
|
||||
} else {
|
||||
DR::_4
|
||||
};
|
||||
(F::datarates()[dr as usize].clone().unwrap(), channel)
|
||||
// Alternatively, we will ask JoinChannel logic to determine a channel from the
|
||||
// subband that the join succeeded on.
|
||||
} else if let Some(channel) = self.join_channels.first_data_channel(rng) {
|
||||
(F::datarates()[datarate as usize].clone().unwrap(), channel)
|
||||
} else {
|
||||
// For the data frame, the datarate impacts which channel sets we can choose
|
||||
// from. If the datarate bandwidth is 500 kHz, we must use
|
||||
// channels 64-71. Else, we must use 0-63
|
||||
let datarate = F::datarates()[datarate as usize].clone().unwrap();
|
||||
if datarate.bandwidth == Bandwidth::_500KHz {
|
||||
let mut channel = (rng.next_u32() & 0b111) as u8;
|
||||
// keep selecting a random channel until we find one that is enabled
|
||||
while !self.channel_mask.is_enabled(channel.into()).unwrap() {
|
||||
channel = (rng.next_u32() & 0b111) as u8;
|
||||
}
|
||||
(datarate, 64 + channel)
|
||||
} else {
|
||||
let mut channel = (rng.next_u32() & 0b111111) as u8;
|
||||
// keep selecting a random channel until we find one that is enabled
|
||||
while !self.channel_mask.is_enabled(channel.into()).unwrap() {
|
||||
channel = (rng.next_u32() & 0b111111) as u8;
|
||||
}
|
||||
(datarate, channel)
|
||||
}
|
||||
};
|
||||
self.last_tx_channel = channel;
|
||||
(data_rate, F::uplink_channels()[channel as usize])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_rx_frequency(&self, _frame: &Frame, window: &Window) -> u32 {
|
||||
let channel = self.last_tx_channel % 8;
|
||||
match window {
|
||||
Window::_1 => F::downlink_channels()[channel as usize],
|
||||
Window::_2 => F::get_default_rx2(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dbm(&self) -> i8 {
|
||||
F::get_dbm()
|
||||
}
|
||||
|
||||
fn get_rx_datarate(&self, tx_datarate: DR, frame: &Frame, window: &Window) -> Datarate {
|
||||
F::get_rx_datarate(tx_datarate, frame, window)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||
|
||||
pub(crate) const DATARATES: [Option<Datarate>; 14] = [
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_10,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 19,
|
||||
max_mac_payload_size_with_dwell_time: 19,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_9,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 61,
|
||||
max_mac_payload_size_with_dwell_time: 61,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_8,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 133,
|
||||
max_mac_payload_size_with_dwell_time: 133,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_125KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_8,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
None, // TODO: defined in rp002-1-0-4
|
||||
None, // TODO: defined in rp002-1-0-4
|
||||
None,
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_12,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 61,
|
||||
max_mac_payload_size_with_dwell_time: 61,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_11,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 137,
|
||||
max_mac_payload_size_with_dwell_time: 137,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_10,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_9,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_8,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
Some(Datarate {
|
||||
spreading_factor: SpreadingFactor::_7,
|
||||
bandwidth: Bandwidth::_500KHz,
|
||||
max_mac_payload_size: 250,
|
||||
max_mac_payload_size_with_dwell_time: 250,
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,94 @@
|
||||
pub(crate) const UPLINK_CHANNEL_MAP: [u32; 72] = [
|
||||
// channels 0-7 (125 kHz)
|
||||
902_300_000,
|
||||
902_500_000,
|
||||
902_700_000,
|
||||
902_900_000,
|
||||
903_100_000,
|
||||
903_300_000,
|
||||
903_500_000,
|
||||
903_700_000,
|
||||
// channels 8-15 (125 kHz)
|
||||
903_900_000,
|
||||
904_100_000,
|
||||
904_300_000,
|
||||
904_500_000,
|
||||
904_700_000,
|
||||
904_900_000,
|
||||
905_100_000,
|
||||
905_300_000,
|
||||
// channels 16-23 (125 kHz)
|
||||
905_500_000,
|
||||
905_700_000,
|
||||
905_900_000,
|
||||
906_100_000,
|
||||
906_300_000,
|
||||
906_500_000,
|
||||
906_700_000,
|
||||
906_900_000,
|
||||
// channels 24-31 (125 kHz)
|
||||
907_100_000,
|
||||
907_300_000,
|
||||
907_500_000,
|
||||
907_700_000,
|
||||
907_900_000,
|
||||
908_100_000,
|
||||
908_300_000,
|
||||
908_500_000,
|
||||
// channels 32-39 (125 kHz)
|
||||
908_700_000,
|
||||
908_900_000,
|
||||
909_100_000,
|
||||
909_300_000,
|
||||
909_500_000,
|
||||
909_700_000,
|
||||
909_900_000,
|
||||
910_100_000,
|
||||
// channels 39-47 (125 kHz)
|
||||
910_300_000,
|
||||
910_500_000,
|
||||
910_700_000,
|
||||
910_900_000,
|
||||
911_100_000,
|
||||
911_300_000,
|
||||
911_500_000,
|
||||
911_700_000,
|
||||
// channels 47-55 (125 kHz)
|
||||
911_900_000,
|
||||
912_100_000,
|
||||
912_300_000,
|
||||
912_500_000,
|
||||
912_700_000,
|
||||
912_900_000,
|
||||
913_100_000,
|
||||
913_300_000,
|
||||
// channels 55-63
|
||||
913_500_000,
|
||||
913_700_000,
|
||||
913_900_000,
|
||||
914_100_000,
|
||||
914_300_000,
|
||||
914_500_000,
|
||||
914_700_000,
|
||||
914_900_000,
|
||||
// channels 63-71 (500 kHz)
|
||||
903_000_000,
|
||||
904_600_000,
|
||||
906_200_000,
|
||||
907_800_000,
|
||||
909_400_000,
|
||||
911_000_000,
|
||||
912_600_000,
|
||||
914_200_000,
|
||||
];
|
||||
|
||||
pub(crate) const DOWNLINK_CHANNEL_MAP: [u32; 8] = [
|
||||
923_300_000,
|
||||
923_900_000,
|
||||
924_500_000,
|
||||
925_100_000,
|
||||
925_700_000,
|
||||
926_300_000,
|
||||
926_900_000,
|
||||
927_500_000,
|
||||
];
|
||||
@@ -0,0 +1,81 @@
|
||||
use super::*;
|
||||
|
||||
mod frequencies;
|
||||
use frequencies::*;
|
||||
|
||||
mod datarates;
|
||||
use datarates::*;
|
||||
|
||||
const US_DBM: i8 = 21;
|
||||
const DEFAULT_RX2: u32 = 923_300_000;
|
||||
|
||||
/// State struct for the `US915` region. This struct may be created directly if you wish to fine-tune some parameters.
|
||||
/// At this time specifying a bias for the subband used during the join process is supported using
|
||||
/// [`set_join_bias`](Self::set_join_bias) and [`set_join_bias_and_noncompliant_retries`](Self::set_join_bias_and_noncompliant_retries)
|
||||
/// is suppored. This struct can then be turned into a [`Configuration`] as it implements [`Into<Configuration>`].
|
||||
///
|
||||
/// # Note:
|
||||
///
|
||||
/// Only [`US915`] and [`AU915`] can be created using this method, because they are the only ones which have
|
||||
/// parameters that may be fine-tuned at the region level. To create a [`Configuration`] for other regions, use
|
||||
/// [`Configuration::new`] and specify the region using the [`Region`] enum.
|
||||
///
|
||||
/// # Example: Setting up join bias
|
||||
///
|
||||
/// ```
|
||||
/// use lorawan_device::region::{Configuration, US915, Subband};
|
||||
///
|
||||
/// let mut us915 = US915::new();
|
||||
/// // Subband 2 is commonly used for The Things Network.
|
||||
/// us915.set_join_bias(Subband::_2);
|
||||
/// let configuration: Configuration = us915.into();
|
||||
/// ```
|
||||
#[derive(Default, Clone)]
|
||||
pub struct US915(pub(crate) FixedChannelPlan<14, US915Region>);
|
||||
|
||||
impl US915 {
|
||||
pub fn get_max_payload_length(datarate: DR, repeater_compatible: bool, dwell_time: bool) -> u8 {
|
||||
US915Region::get_max_payload_length(datarate, repeater_compatible, dwell_time)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct US915Region;
|
||||
|
||||
impl ChannelRegion<14> for US915Region {
|
||||
fn datarates() -> &'static [Option<Datarate>; 14] {
|
||||
&DATARATES
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedChannelRegion<14> for US915Region {
|
||||
fn uplink_channels() -> &'static [u32; 72] {
|
||||
&UPLINK_CHANNEL_MAP
|
||||
}
|
||||
fn downlink_channels() -> &'static [u32; 8] {
|
||||
&DOWNLINK_CHANNEL_MAP
|
||||
}
|
||||
fn get_default_rx2() -> u32 {
|
||||
DEFAULT_RX2
|
||||
}
|
||||
fn get_rx_datarate(tx_datarate: DR, _frame: &Frame, window: &Window) -> Datarate {
|
||||
let datarate = match window {
|
||||
Window::_1 => {
|
||||
// no support for RX1 DR Offset
|
||||
match tx_datarate {
|
||||
DR::_0 => DR::_10,
|
||||
DR::_1 => DR::_11,
|
||||
DR::_2 => DR::_12,
|
||||
DR::_3 => DR::_13,
|
||||
DR::_4 => DR::_13,
|
||||
_ => panic!("Invalid TX datarate"),
|
||||
}
|
||||
}
|
||||
Window::_2 => DR::_8,
|
||||
};
|
||||
DATARATES[datarate as usize].clone().unwrap()
|
||||
}
|
||||
fn get_dbm() -> i8 {
|
||||
US_DBM
|
||||
}
|
||||
}
|
||||
528
lorawan-device-patch/src/region/mod.rs
Normal file
528
lorawan-device-patch/src/region/mod.rs
Normal file
@@ -0,0 +1,528 @@
|
||||
//! LoRaWAN device region definitions (eg: EU868, US915, etc).
|
||||
use lora_modulation::{Bandwidth, BaseBandModulationParams, CodingRate, SpreadingFactor};
|
||||
use lorawan::{maccommands::ChannelMask, parser::CfList};
|
||||
use rand_core::RngCore;
|
||||
|
||||
use crate::mac::{Frame, Window};
|
||||
pub(crate) mod constants;
|
||||
pub(crate) use crate::radio::*;
|
||||
use constants::*;
|
||||
|
||||
#[cfg(not(any(
|
||||
feature = "region-as923-1",
|
||||
feature = "region-as923-2",
|
||||
feature = "region-as923-3",
|
||||
feature = "region-as923-4",
|
||||
feature = "region-eu433",
|
||||
feature = "region-eu868",
|
||||
feature = "region-in865",
|
||||
feature = "region-au915",
|
||||
feature = "region-us915"
|
||||
)))]
|
||||
compile_error!("You must enable at least one region! eg: `region-eu868`, `region-us915`...");
|
||||
|
||||
#[cfg(any(
|
||||
feature = "region-as923-1",
|
||||
feature = "region-as923-2",
|
||||
feature = "region-as923-3",
|
||||
feature = "region-as923-4",
|
||||
feature = "region-eu433",
|
||||
feature = "region-eu868",
|
||||
feature = "region-in865"
|
||||
))]
|
||||
mod dynamic_channel_plans;
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
pub(crate) use dynamic_channel_plans::AS923_1;
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
pub(crate) use dynamic_channel_plans::AS923_2;
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
pub(crate) use dynamic_channel_plans::AS923_3;
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
pub(crate) use dynamic_channel_plans::AS923_4;
|
||||
#[cfg(feature = "region-eu433")]
|
||||
pub(crate) use dynamic_channel_plans::EU433;
|
||||
#[cfg(feature = "region-eu868")]
|
||||
pub(crate) use dynamic_channel_plans::EU868;
|
||||
#[cfg(feature = "region-in865")]
|
||||
pub(crate) use dynamic_channel_plans::IN865;
|
||||
|
||||
#[cfg(any(feature = "region-us915", feature = "region-au915"))]
|
||||
mod fixed_channel_plans;
|
||||
#[cfg(any(feature = "region-us915", feature = "region-au915"))]
|
||||
pub use fixed_channel_plans::Subband;
|
||||
#[cfg(feature = "region-au915")]
|
||||
pub use fixed_channel_plans::AU915;
|
||||
#[cfg(feature = "region-us915")]
|
||||
pub use fixed_channel_plans::US915;
|
||||
|
||||
pub(crate) trait ChannelRegion<const D: usize> {
|
||||
fn datarates() -> &'static [Option<Datarate>; D];
|
||||
|
||||
fn get_max_payload_length(datarate: DR, repeater_compatible: bool, dwell_time: bool) -> u8 {
|
||||
let Some(Some(dr)) = Self::datarates().get(datarate as usize) else {
|
||||
return 0;
|
||||
};
|
||||
let max_size = if dwell_time {
|
||||
dr.max_mac_payload_size_with_dwell_time
|
||||
} else {
|
||||
dr.max_mac_payload_size
|
||||
};
|
||||
if repeater_compatible && max_size > 230 {
|
||||
230
|
||||
} else {
|
||||
max_size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Contains LoRaWAN region-specific configuration; is required for creating a LoRaWAN Device.
|
||||
/// Generally constructed using the `Region` enum, unless you need to fine-tune US915 or AU915.
|
||||
pub struct Configuration {
|
||||
state: State,
|
||||
}
|
||||
|
||||
seq_macro::seq!(
|
||||
N in 0..=15 {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[repr(u8)]
|
||||
/// A restricted data rate type that exposes the number of variants to only what _may_ be
|
||||
/// potentially be possible. Note that not all data rates are valid in all regions.
|
||||
pub enum DR {
|
||||
#(
|
||||
_~N = N,
|
||||
)*
|
||||
}
|
||||
}
|
||||
);
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
/// Regions supported by this crate: AS923_1, AS923_2, AS923_3, AS923_4, AU915, EU868, EU433, IN865, US915.
|
||||
/// Each region is individually feature-gated (eg: `region-eu868`), however, by default, all regions are enabled.
|
||||
///
|
||||
pub enum Region {
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
AS923_1,
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
AS923_2,
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
AS923_3,
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
AS923_4,
|
||||
#[cfg(feature = "region-au915")]
|
||||
AU915,
|
||||
#[cfg(feature = "region-eu868")]
|
||||
EU868,
|
||||
#[cfg(feature = "region-eu433")]
|
||||
EU433,
|
||||
#[cfg(feature = "region-in865")]
|
||||
IN865,
|
||||
#[cfg(feature = "region-us915")]
|
||||
US915,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum State {
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
AS923_1(AS923_1),
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
AS923_2(AS923_2),
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
AS923_3(AS923_3),
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
AS923_4(AS923_4),
|
||||
#[cfg(feature = "region-au915")]
|
||||
AU915(AU915),
|
||||
#[cfg(feature = "region-eu868")]
|
||||
EU868(EU868),
|
||||
#[cfg(feature = "region-eu433")]
|
||||
EU433(EU433),
|
||||
#[cfg(feature = "region-in865")]
|
||||
IN865(IN865),
|
||||
#[cfg(feature = "region-us915")]
|
||||
US915(US915),
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(region: Region) -> State {
|
||||
match region {
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
Region::AS923_1 => State::AS923_1(AS923_1::default()),
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
Region::AS923_2 => State::AS923_2(AS923_2::default()),
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
Region::AS923_3 => State::AS923_3(AS923_3::default()),
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
Region::AS923_4 => State::AS923_4(AS923_4::default()),
|
||||
#[cfg(feature = "region-au915")]
|
||||
Region::AU915 => State::AU915(AU915::default()),
|
||||
#[cfg(feature = "region-eu868")]
|
||||
Region::EU868 => State::EU868(EU868::default()),
|
||||
#[cfg(feature = "region-eu433")]
|
||||
Region::EU433 => State::EU433(EU433::default()),
|
||||
#[cfg(feature = "region-in865")]
|
||||
Region::IN865 => State::IN865(IN865::default()),
|
||||
#[cfg(feature = "region-us915")]
|
||||
Region::US915 => State::US915(US915::default()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn region(&self) -> Region {
|
||||
match self {
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
Self::AS923_1(_) => Region::AS923_1,
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
Self::AS923_2(_) => Region::AS923_2,
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
Self::AS923_3(_) => Region::AS923_3,
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
Self::AS923_4(_) => Region::AS923_4,
|
||||
#[cfg(feature = "region-au915")]
|
||||
Self::AU915(_) => Region::AU915,
|
||||
#[cfg(feature = "region-eu433")]
|
||||
Self::EU433(_) => Region::EU433,
|
||||
#[cfg(feature = "region-eu868")]
|
||||
Self::EU868(_) => Region::EU868,
|
||||
#[cfg(feature = "region-in865")]
|
||||
Self::IN865(_) => Region::IN865,
|
||||
#[cfg(feature = "region-us915")]
|
||||
Self::US915(_) => Region::US915,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This datarate type is used internally for defining bandwidth/sf per region
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Datarate {
|
||||
bandwidth: Bandwidth,
|
||||
spreading_factor: SpreadingFactor,
|
||||
max_mac_payload_size: u8,
|
||||
max_mac_payload_size_with_dwell_time: u8,
|
||||
}
|
||||
macro_rules! mut_region_dispatch {
|
||||
($s:expr, $t:tt) => {
|
||||
match &mut $s.state {
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
State::AS923_1(state) => state.$t(),
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
State::AS923_2(state) => state.$t(),
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
State::AS923_3(state) => state.$t(),
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
State::AS923_4(state) => state.$t(),
|
||||
#[cfg(feature = "region-au915")]
|
||||
State::AU915(state) => state.0.$t(),
|
||||
#[cfg(feature = "region-eu868")]
|
||||
State::EU868(state) => state.$t(),
|
||||
#[cfg(feature = "region-eu433")]
|
||||
State::EU433(state) => state.$t(),
|
||||
#[cfg(feature = "region-in865")]
|
||||
State::IN865(state) => state.$t(),
|
||||
#[cfg(feature = "region-us915")]
|
||||
State::US915(state) => state.0.$t(),
|
||||
}
|
||||
};
|
||||
($s:expr, $t:tt, $($arg:tt)*) => {
|
||||
match &mut $s.state {
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
State::AS923_1(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
State::AS923_2(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
State::AS923_3(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
State::AS923_4(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-au915")]
|
||||
State::AU915(state) => state.0.$t($($arg)*),
|
||||
#[cfg(feature = "region-eu868")]
|
||||
State::EU868(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-eu433")]
|
||||
State::EU433(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-in865")]
|
||||
State::IN865(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-us915")]
|
||||
State::US915(state) => state.0.$t($($arg)*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! region_dispatch {
|
||||
($s:expr, $t:tt) => {
|
||||
match &$s.state {
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
State::AS923_1(state) => state.$t(),
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
State::AS923_2(state) => state.$t(),
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
State::AS923_3(state) => state.$t(),
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
State::AS923_4(state) => state.$t(),
|
||||
#[cfg(feature = "region-au915")]
|
||||
State::AU915(state) => state.0.$t(),
|
||||
#[cfg(feature = "region-eu868")]
|
||||
State::EU868(state) => state.$t(),
|
||||
#[cfg(feature = "region-eu433")]
|
||||
State::EU433(state) => state.$t(),
|
||||
#[cfg(feature = "region-in865")]
|
||||
State::IN865(state) => state.$t(),
|
||||
#[cfg(feature = "region-us915")]
|
||||
State::US915(state) => state.0.$t(),
|
||||
}
|
||||
};
|
||||
($s:expr, $t:tt, $($arg:tt)*) => {
|
||||
match &$s.state {
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
State::AS923_1(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
State::AS923_2(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
State::AS923_3(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
State::AS923_4(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-au915")]
|
||||
State::AU915(state) => state.0.$t($($arg)*),
|
||||
#[cfg(feature = "region-eu868")]
|
||||
State::EU868(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-eu433")]
|
||||
State::EU433(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-in865")]
|
||||
State::IN865(state) => state.$t($($arg)*),
|
||||
#[cfg(feature = "region-us915")]
|
||||
State::US915(state) => state.0.$t($($arg)*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! region_static_dispatch {
|
||||
($s:expr, $t:tt) => {
|
||||
match &$s.state {
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
State::AS923_1(_) => dynamic_channel_plans::AS923_1::$t(),
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
State::AS923_2(_) => dynamic_channel_plans::AS923_2::$t(),
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
State::AS923_3(_) => dynamic_channel_plans::AS923_3::$t(),
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
State::AS923_4(_) => dynamic_channel_plans::AS923_4::$t(),
|
||||
#[cfg(feature = "region-au915")]
|
||||
State::AU915(_) => fixed_channel_plans::AU915::$t(),
|
||||
#[cfg(feature = "region-eu868")]
|
||||
State::EU868(_) => dynamic_channel_plans::EU868::$t(),
|
||||
#[cfg(feature = "region-eu433")]
|
||||
State::EU433(_) => dynamic_channel_plans::EU433::$t(),
|
||||
#[cfg(feature = "region-in865")]
|
||||
State::IN865(_) => dynamic_channel_plans::IN865::$t(),
|
||||
#[cfg(feature = "region-us915")]
|
||||
State::US915(_) => fixed_channel_plans::US915::$t(),
|
||||
}
|
||||
};
|
||||
($s:expr, $t:tt, $($arg:tt)*) => {
|
||||
match &$s.state {
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
State::AS923_1(_) => dynamic_channel_plans::AS923_1::$t($($arg)*),
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
State::AS923_2(_) => dynamic_channel_plans::AS923_2::$t($($arg)*),
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
State::AS923_3(_) => dynamic_channel_plans::AS923_3::$t($($arg)*),
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
State::AS923_4(_) => dynamic_channel_plans::AS923_4::$t($($arg)*),
|
||||
#[cfg(feature = "region-au915")]
|
||||
State::AU915(_) => fixed_channel_plans::AU915::$t($($arg)*),
|
||||
#[cfg(feature = "region-eu868")]
|
||||
State::EU868(_) => dynamic_channel_plans::EU868::$t($($arg)*),
|
||||
#[cfg(feature = "region-eu433")]
|
||||
State::EU433(_) => dynamic_channel_plans::EU433::$t($($arg)*),
|
||||
#[cfg(feature = "region-in865")]
|
||||
State::IN865(_) => dynamic_channel_plans::IN865::$t($($arg)*),
|
||||
#[cfg(feature = "region-us915")]
|
||||
State::US915(_) => fixed_channel_plans::US915::$t($($arg)*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
pub fn new(region: Region) -> Configuration {
|
||||
Configuration::with_state(State::new(region))
|
||||
}
|
||||
|
||||
fn with_state(state: State) -> Configuration {
|
||||
Configuration { state }
|
||||
}
|
||||
|
||||
pub fn get_max_payload_length(
|
||||
&self,
|
||||
datarate: DR,
|
||||
repeater_compatible: bool,
|
||||
dwell_time: bool,
|
||||
) -> u8 {
|
||||
region_static_dispatch!(
|
||||
self,
|
||||
get_max_payload_length,
|
||||
datarate,
|
||||
repeater_compatible,
|
||||
dwell_time
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn create_tx_config<RNG: RngCore>(
|
||||
&mut self,
|
||||
rng: &mut RNG,
|
||||
datarate: DR,
|
||||
frame: &Frame,
|
||||
) -> TxConfig {
|
||||
let (dr, frequency) = self.get_tx_dr_and_frequency(rng, datarate, frame);
|
||||
TxConfig {
|
||||
pw: self.get_dbm(),
|
||||
rf: RfConfig {
|
||||
frequency,
|
||||
bb: BaseBandModulationParams::new(
|
||||
dr.spreading_factor,
|
||||
dr.bandwidth,
|
||||
self.get_coding_rate(),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tx_dr_and_frequency<RNG: RngCore>(
|
||||
&mut self,
|
||||
rng: &mut RNG,
|
||||
datarate: DR,
|
||||
frame: &Frame,
|
||||
) -> (Datarate, u32) {
|
||||
mut_region_dispatch!(self, get_tx_dr_and_frequency, rng, datarate, frame)
|
||||
}
|
||||
|
||||
pub(crate) fn get_rx_config(&self, datarate: DR, frame: &Frame, window: &Window) -> RfConfig {
|
||||
let dr = self.get_rx_datarate(datarate, frame, window);
|
||||
RfConfig {
|
||||
frequency: self.get_rx_frequency(frame, window),
|
||||
bb: BaseBandModulationParams::new(
|
||||
dr.spreading_factor,
|
||||
dr.bandwidth,
|
||||
self.get_coding_rate(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process_join_accept<T: AsRef<[u8]>, C>(
|
||||
&mut self,
|
||||
join_accept: &DecryptedJoinAcceptPayload<T, C>,
|
||||
) {
|
||||
mut_region_dispatch!(self, process_join_accept, join_accept)
|
||||
}
|
||||
|
||||
pub(crate) fn set_channel_mask(
|
||||
&mut self,
|
||||
channel_mask_control: u8,
|
||||
channel_mask: ChannelMask<2>,
|
||||
) {
|
||||
mut_region_dispatch!(self, handle_link_adr_channel_mask, channel_mask_control, channel_mask)
|
||||
}
|
||||
|
||||
pub(crate) fn get_rx_frequency(&self, frame: &Frame, window: &Window) -> u32 {
|
||||
region_dispatch!(self, get_rx_frequency, frame, window)
|
||||
}
|
||||
|
||||
pub(crate) fn get_default_datarate(&self) -> DR {
|
||||
region_dispatch!(self, get_default_datarate)
|
||||
}
|
||||
|
||||
pub(crate) fn get_rx_datarate(&self, datarate: DR, frame: &Frame, window: &Window) -> Datarate {
|
||||
region_dispatch!(self, get_rx_datarate, datarate, frame, window)
|
||||
}
|
||||
|
||||
// Unicast: The RXC parameters are identical to the RX2 parameters, and they use the same
|
||||
// channel and data rate. Modifying the RX2 parameters using the appropriate MAC
|
||||
// commands also modifies the RXC parameters.
|
||||
pub(crate) fn get_rxc_config(&self, datarate: DR) -> RfConfig {
|
||||
let dr = self.get_rx_datarate(datarate, &Frame::Data, &Window::_2);
|
||||
let frequency = self.get_rx_frequency(&Frame::Data, &Window::_2);
|
||||
RfConfig {
|
||||
frequency,
|
||||
bb: BaseBandModulationParams::new(
|
||||
dr.spreading_factor,
|
||||
dr.bandwidth,
|
||||
self.get_coding_rate(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_dbm(&self) -> i8 {
|
||||
region_dispatch!(self, get_dbm)
|
||||
}
|
||||
|
||||
pub(crate) fn get_coding_rate(&self) -> CodingRate {
|
||||
region_dispatch!(self, get_coding_rate)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_current_region(&self) -> super::region::Region {
|
||||
self.state.region()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! from_region {
|
||||
($r:tt) => {
|
||||
impl From<$r> for Configuration {
|
||||
fn from(region: $r) -> Configuration {
|
||||
Configuration::with_state(State::$r(region))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "region-as923-1")]
|
||||
from_region!(AS923_1);
|
||||
#[cfg(feature = "region-as923-2")]
|
||||
from_region!(AS923_2);
|
||||
#[cfg(feature = "region-as923-3")]
|
||||
from_region!(AS923_3);
|
||||
#[cfg(feature = "region-as923-4")]
|
||||
from_region!(AS923_4);
|
||||
#[cfg(feature = "region-in865")]
|
||||
from_region!(IN865);
|
||||
#[cfg(feature = "region-au915")]
|
||||
from_region!(AU915);
|
||||
#[cfg(feature = "region-eu868")]
|
||||
from_region!(EU868);
|
||||
#[cfg(feature = "region-eu433")]
|
||||
from_region!(EU433);
|
||||
#[cfg(feature = "region-us915")]
|
||||
from_region!(US915);
|
||||
|
||||
use lorawan::parser::DecryptedJoinAcceptPayload;
|
||||
|
||||
pub(crate) trait RegionHandler {
|
||||
fn process_join_accept<T: AsRef<[u8]>, C>(
|
||||
&mut self,
|
||||
join_accept: &DecryptedJoinAcceptPayload<T, C>,
|
||||
);
|
||||
|
||||
fn handle_link_adr_channel_mask(
|
||||
&mut self,
|
||||
channel_mask_control: u8,
|
||||
channel_mask: ChannelMask<2>,
|
||||
);
|
||||
|
||||
fn get_default_datarate(&self) -> DR {
|
||||
DR::_0
|
||||
}
|
||||
fn get_tx_dr_and_frequency<RNG: RngCore>(
|
||||
&mut self,
|
||||
rng: &mut RNG,
|
||||
datarate: DR,
|
||||
frame: &Frame,
|
||||
) -> (Datarate, u32);
|
||||
|
||||
fn get_rx_frequency(&self, frame: &Frame, window: &Window) -> u32;
|
||||
fn get_rx_datarate(&self, datarate: DR, frame: &Frame, window: &Window) -> Datarate;
|
||||
fn get_dbm(&self) -> i8 {
|
||||
DEFAULT_DBM
|
||||
}
|
||||
fn get_coding_rate(&self) -> CodingRate {
|
||||
DEFAULT_CODING_RATE
|
||||
}
|
||||
}
|
||||
54
lorawan-device-patch/src/rng.rs
Normal file
54
lorawan-device-patch/src/rng.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! RNG based on the `wyrand` pseudorandom number generator.
|
||||
//!
|
||||
//! This crate uses the random number generator for exactly two things:
|
||||
//!
|
||||
//! * Generating DevNonces for join requests
|
||||
//! * Selecting random channels when transmitting uplinks.
|
||||
//!
|
||||
//! The good news is that both these operations don't require true
|
||||
//! cryptographic randomness. In fact, in both cases, we don't even care about
|
||||
//! predictability! A pseudorandom number generator initialized with a seed
|
||||
//! generated by a true random number generator is plenty enough:
|
||||
//!
|
||||
//! * DevNonces must only be unique with a low chance of collision.
|
||||
//! The 1.0.4 LoRaWAN spec even explicitly requires the DevNonces to be
|
||||
//! a sequence of incrementing integers, which is obviously predictable.
|
||||
//! * No one cares if the channel selected for the next uplink is predictable,
|
||||
//! as long as the channel selection yields an uniform distribution.
|
||||
//!
|
||||
//! By providing a PRNG `RngCore` implementation, we enable the crate users the
|
||||
//! flexibility of choosing whether they want to provide their own RNG, or just
|
||||
//! a seed to instantiate this PRNG to generate the random numbers for them.
|
||||
|
||||
use fastrand::Rng;
|
||||
use rand_core::RngCore;
|
||||
|
||||
#[derive(Clone)]
|
||||
/// A pseudorandom number generator utilizing Wyrand algorithm via
|
||||
/// the `fastrand` crate.
|
||||
pub struct Prng(Rng);
|
||||
|
||||
impl Prng {
|
||||
pub(crate) fn new(seed: u64) -> Self {
|
||||
Self(Rng::with_seed(seed))
|
||||
}
|
||||
}
|
||||
|
||||
impl RngCore for Prng {
|
||||
fn next_u32(&mut self) -> u32 {
|
||||
self.0.u32(..)
|
||||
}
|
||||
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
self.0.u64(..)
|
||||
}
|
||||
|
||||
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
||||
rand_core::impls::fill_bytes_via_next(self, dest)
|
||||
}
|
||||
|
||||
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
|
||||
self.fill_bytes(dest);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
272
lorawan-device-patch/src/test_util.rs
Normal file
272
lorawan-device-patch/src/test_util.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
use super::*;
|
||||
use lorawan::maccommands::{
|
||||
ChannelMask, DownlinkMacCommand, MacCommandIterator, SerializableMacCommand, UplinkMacCommand,
|
||||
};
|
||||
use lorawan::parser::{self, DataHeader};
|
||||
use lorawan::{
|
||||
default_crypto::DefaultFactory,
|
||||
maccommandcreator::LinkADRReqCreator,
|
||||
maccommands::LinkADRReqPayload,
|
||||
parser::{parse, DataPayload, JoinAcceptPayload, PhyPayload},
|
||||
};
|
||||
use mac::Session;
|
||||
|
||||
use parser::FCtrl;
|
||||
use radio::{RfConfig, TxConfig};
|
||||
use std::{collections::HashMap, sync::Mutex, vec::Vec};
|
||||
|
||||
/// This module contains some functions for both async device and state machine driven devices
|
||||
/// to operate unit tests.
|
||||
///
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Uplink {
|
||||
data: Vec<u8>,
|
||||
#[allow(unused)]
|
||||
tx_config: TxConfig,
|
||||
}
|
||||
|
||||
impl Uplink {
|
||||
/// Creates a copy from a reference and ensures the packet is at least parseable.
|
||||
pub fn new(data_in: &[u8], tx_config: TxConfig) -> Result<Self, parser::Error> {
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
data.extend_from_slice(data_in);
|
||||
let _parse = parse(data.as_mut_slice())?;
|
||||
Ok(Self { data, tx_config })
|
||||
}
|
||||
|
||||
pub fn get_payload(&mut self) -> PhyPayload<&mut [u8], DefaultFactory> {
|
||||
match parse(self.data.as_mut_slice()) {
|
||||
Ok(p) => p,
|
||||
Err(e) => panic!("Failed to parse payload: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test functions shared by async_device and no_async_device tests
|
||||
pub fn get_key() -> [u8; 16] {
|
||||
[0; 16]
|
||||
}
|
||||
|
||||
pub fn get_dev_addr() -> DevAddr<[u8; 4]> {
|
||||
DevAddr::from(0)
|
||||
}
|
||||
pub fn get_otaa_credentials() -> JoinMode {
|
||||
JoinMode::OTAA {
|
||||
deveui: DevEui::from([0; 8]),
|
||||
appeui: AppEui::from([0; 8]),
|
||||
appkey: AppKey::from(get_key()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_abp_credentials() -> JoinMode {
|
||||
JoinMode::ABP {
|
||||
devaddr: get_dev_addr(),
|
||||
appskey: AppSKey::from(get_key()),
|
||||
newskey: NewSKey::from(get_key()),
|
||||
}
|
||||
}
|
||||
|
||||
pub type RxTxHandler = fn(Option<Uplink>, RfConfig, &mut [u8]) -> usize;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref SESSION: Mutex<HashMap<usize, Session>> = Mutex::new(HashMap::new());
|
||||
|
||||
}
|
||||
|
||||
/// Handle join request and pack a JoinAccept into RxBuffer
|
||||
pub fn handle_join_request<const I: usize>(
|
||||
uplink: Option<Uplink>,
|
||||
_config: RfConfig,
|
||||
rx_buffer: &mut [u8],
|
||||
) -> usize {
|
||||
if let Some(mut uplink) = uplink {
|
||||
if let PhyPayload::JoinRequest(join_request) = uplink.get_payload() {
|
||||
let devnonce = join_request.dev_nonce().to_owned();
|
||||
assert!(join_request.validate_mic(&get_key().into()));
|
||||
let mut buffer: [u8; 17] = [0; 17];
|
||||
let mut phy =
|
||||
lorawan::creator::JoinAcceptCreator::with_options(&mut buffer, DefaultFactory)
|
||||
.unwrap();
|
||||
let app_nonce_bytes = [1; 3];
|
||||
phy.set_app_nonce(&app_nonce_bytes);
|
||||
phy.set_net_id(&[1; 3]);
|
||||
phy.set_dev_addr(get_dev_addr());
|
||||
let finished = phy.build(&get_key().into()).unwrap();
|
||||
rx_buffer[..finished.len()].copy_from_slice(finished);
|
||||
|
||||
let mut copy = rx_buffer[..finished.len()].to_vec();
|
||||
if let PhyPayload::JoinAccept(JoinAcceptPayload::Encrypted(encrypted)) =
|
||||
parse(copy.as_mut_slice()).unwrap()
|
||||
{
|
||||
let decrypt = encrypted.decrypt(&get_key().into());
|
||||
let session = Session::derive_new(
|
||||
&decrypt,
|
||||
devnonce,
|
||||
&NetworkCredentials::new(
|
||||
AppEui::from([0; 8]),
|
||||
DevEui::from([0; 8]),
|
||||
AppKey::from(get_key()),
|
||||
),
|
||||
);
|
||||
{
|
||||
let mut session_map = SESSION.lock().unwrap();
|
||||
session_map.insert(I, session);
|
||||
}
|
||||
} else {
|
||||
panic!("Somehow unable to parse my own join accept?")
|
||||
}
|
||||
finished.len()
|
||||
} else {
|
||||
panic!("Did not parse join request from uplink");
|
||||
}
|
||||
} else {
|
||||
panic!("No uplink passed to handle_join_request");
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an uplink and respond with two LinkAdrReq on Port 0
|
||||
pub fn handle_data_uplink_with_link_adr_req<const FCNT_UP: u16, const FCNT_DOWN: u32>(
|
||||
uplink: Option<Uplink>,
|
||||
_config: RfConfig,
|
||||
rx_buffer: &mut [u8],
|
||||
) -> usize {
|
||||
if let Some(mut uplink) = uplink {
|
||||
if let PhyPayload::Data(DataPayload::Encrypted(data)) = uplink.get_payload() {
|
||||
let fcnt = data.fhdr().fcnt() as u32;
|
||||
assert!(data.validate_mic(&get_key().into(), fcnt));
|
||||
let uplink =
|
||||
data.decrypt(Some(&get_key().into()), Some(&get_key().into()), fcnt).unwrap();
|
||||
assert_eq!(uplink.fhdr().fcnt(), FCNT_UP);
|
||||
let mac_cmds = [link_adr_req_with_bank_ctrl(0b10), link_adr_req_with_bank_ctrl(0b100)];
|
||||
let mac_cmds = [
|
||||
// drop the CID byte when building the MAC Command (ie: [1..])
|
||||
DownlinkMacCommand::LinkADRReq(
|
||||
LinkADRReqPayload::new(&mac_cmds[0].build()[1..]).unwrap(),
|
||||
),
|
||||
DownlinkMacCommand::LinkADRReq(
|
||||
LinkADRReqPayload::new(&mac_cmds[1].build()[1..]).unwrap(),
|
||||
),
|
||||
];
|
||||
let cmd: Vec<&dyn SerializableMacCommand> = vec![&mac_cmds[0], &mac_cmds[1]];
|
||||
let mut phy =
|
||||
lorawan::creator::DataPayloadCreator::with_options(rx_buffer, DefaultFactory)
|
||||
.unwrap();
|
||||
phy.set_confirmed(uplink.is_confirmed());
|
||||
phy.set_f_port(4);
|
||||
phy.set_dev_addr(&[0; 4]);
|
||||
phy.set_uplink(false);
|
||||
phy.set_fcnt(FCNT_DOWN);
|
||||
let finished =
|
||||
phy.build(&[3, 2, 1], &cmd, &get_key().into(), &get_key().into()).unwrap();
|
||||
finished.len()
|
||||
} else {
|
||||
panic!("Did not decode PhyPayload::Data!");
|
||||
}
|
||||
} else {
|
||||
panic!("No uplink passed to handle_data_uplink_with_link_adr_req");
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an uplink and respond with two LinkAdrReq on Port 0
|
||||
pub fn handle_class_c_uplink_after_join(
|
||||
uplink: Option<Uplink>,
|
||||
_config: RfConfig,
|
||||
rx_buffer: &mut [u8],
|
||||
) -> usize {
|
||||
if let Some(mut uplink) = uplink {
|
||||
if let PhyPayload::Data(DataPayload::Encrypted(data)) = uplink.get_payload() {
|
||||
let fcnt = data.fhdr().fcnt() as u32;
|
||||
assert!(data.validate_mic(&get_key().into(), fcnt));
|
||||
let uplink =
|
||||
data.decrypt(Some(&get_key().into()), Some(&get_key().into()), fcnt).unwrap();
|
||||
assert_eq!(uplink.fhdr().fcnt(), 0);
|
||||
let mut phy =
|
||||
lorawan::creator::DataPayloadCreator::with_options(rx_buffer, DefaultFactory)
|
||||
.unwrap();
|
||||
let mut fctrl = FCtrl::new(0, false);
|
||||
fctrl.set_ack();
|
||||
phy.set_confirmed(false);
|
||||
phy.set_dev_addr(&[0; 4]);
|
||||
phy.set_uplink(false);
|
||||
phy.set_fctrl(&fctrl);
|
||||
// set ack bit
|
||||
let finished = phy.build(&[], &[], &get_key().into(), &get_key().into()).unwrap();
|
||||
finished.len()
|
||||
} else {
|
||||
panic!("Did not decode PhyPayload::Data!");
|
||||
}
|
||||
} else {
|
||||
panic!("No uplink passed to handle_data_uplink_with_link_adr_req");
|
||||
}
|
||||
}
|
||||
|
||||
fn link_adr_req_with_bank_ctrl(cm: u16) -> LinkADRReqCreator {
|
||||
// prepare a confirmed downlink
|
||||
let mut adr_req = LinkADRReqCreator::new();
|
||||
adr_req.set_data_rate(0).unwrap();
|
||||
adr_req.set_tx_power(0).unwrap();
|
||||
// this should give us a chmask ctrl value of 5 which allows us to turn banks on and off
|
||||
adr_req.set_redundancy(0x50);
|
||||
// the second bit is the only high bit, so only bank 2 should be enabled
|
||||
let tmp = [cm as u8, (cm >> 8) as u8];
|
||||
let cm = ChannelMask::new(&tmp).unwrap();
|
||||
adr_req.set_channel_mask(cm);
|
||||
adr_req
|
||||
}
|
||||
|
||||
/// Looks for LinkAdrAns
|
||||
pub fn handle_data_uplink_with_link_adr_ans(
|
||||
uplink: Option<Uplink>,
|
||||
_config: RfConfig,
|
||||
rx_buffer: &mut [u8],
|
||||
) -> usize {
|
||||
if let Some(mut uplink) = uplink {
|
||||
if let PhyPayload::Data(DataPayload::Encrypted(data)) = uplink.get_payload() {
|
||||
let fcnt = data.fhdr().fcnt() as u32;
|
||||
assert!(data.validate_mic(&get_key().into(), fcnt));
|
||||
let uplink =
|
||||
data.decrypt(Some(&get_key().into()), Some(&get_key().into()), fcnt).unwrap();
|
||||
let fhdr = uplink.fhdr();
|
||||
let mac_cmds: Vec<UplinkMacCommand> =
|
||||
MacCommandIterator::<UplinkMacCommand>::new(fhdr.data()).collect();
|
||||
|
||||
assert_eq!(mac_cmds.len(), 2);
|
||||
assert!(matches!(mac_cmds[0], UplinkMacCommand::LinkADRAns(_)));
|
||||
assert!(matches!(mac_cmds[1], UplinkMacCommand::LinkADRAns(_)));
|
||||
|
||||
// Build the actual data payload with FPort 0 which allows MAC Commands in payload
|
||||
rx_buffer.iter_mut().for_each(|x| *x = 0);
|
||||
let mut phy =
|
||||
lorawan::creator::DataPayloadCreator::with_options(rx_buffer, DefaultFactory)
|
||||
.unwrap();
|
||||
phy.set_confirmed(uplink.is_confirmed());
|
||||
phy.set_dev_addr(&[0; 4]);
|
||||
phy.set_uplink(false);
|
||||
//phy.set_f_port(3);
|
||||
phy.set_fcnt(1);
|
||||
// zero out rx_buffer
|
||||
let finished = phy.build(&[], &[], &get_key().into(), &get_key().into()).unwrap();
|
||||
finished.len()
|
||||
} else {
|
||||
panic!("Unable to parse PhyPayload::Data from uplink in handle_data_uplink_with_link_adr_ans")
|
||||
}
|
||||
} else {
|
||||
panic!("No uplink passed to handle_data_uplink_with_link_adr_ans")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn class_c_downlink<const FCNT_DOWN: u32>(
|
||||
_uplink: Option<Uplink>,
|
||||
_config: RfConfig,
|
||||
rx_buffer: &mut [u8],
|
||||
) -> usize {
|
||||
let mut phy =
|
||||
lorawan::creator::DataPayloadCreator::with_options(rx_buffer, DefaultFactory).unwrap();
|
||||
phy.set_f_port(3);
|
||||
phy.set_dev_addr(&[0; 4]);
|
||||
phy.set_uplink(false);
|
||||
phy.set_fcnt(FCNT_DOWN);
|
||||
let finished = phy.build(&[1, 2, 3], &[], &get_key().into(), &get_key().into()).unwrap();
|
||||
finished.len()
|
||||
}
|
||||
Reference in New Issue
Block a user