This commit is contained in:
2026-03-30 15:32:51 +08:00
commit 5c95cc40f7
63 changed files with 6747 additions and 0 deletions

19
.cargo/config.toml Normal file
View File

@@ -0,0 +1,19 @@
[target.xtensa-esp32s3-none-elf]
runner = "espflash flash --monitor -B 921600 --chip esp32s3 --log-format defmt"
linker = "/home/fromost/.rustup/toolchains/esp/xtensa-esp-elf/esp-15.2.0_20250920/xtensa-esp-elf/bin/xtensa-esp32s3-elf-gcc"
[env]
DEFMT_LOG = "info,lora=debug,eeprom24x=debug"
DEV_EUI = "000000DD44F30FAE"
APP_KEY = "00000000000000000000000040000000"
APP_EUI = "0000000000000040"
[build]
rustflags = [
"-C", "link-arg=-nostartfiles",
"-Z", "stack-protector=all",
]
target = "xtensa-esp32s3-none-elf"
[unstable]
build-std = ["alloc", "core"]

1
.clippy.toml Normal file
View File

@@ -0,0 +1 @@
stack-size-threshold = 1024

28
.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
# will have compiled files and executables
debug/
target/
Cargo.lock
.idea/workspace.xml
# Editor configuration
.vscode/
.zed/
.helix/
.nvim.lua
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ignore .DS_Store file in mac
**/.DS_Store

8
.idea/dictionaries/project.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>Trng</w>
<w>gpst</w>
</words>
</dictionary>
</component>

11
.idea/lora.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

12
.idea/material_theme_project_new.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="2773e5a3:19a9b2047d0:-7ffc" />
</MTProjectMetadataState>
</option>
</component>
</project>

22
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<expanded-state>
<State>
<id>Dev Container</id>
</State>
<State>
<id>General</id>
</State>
</expanded-state>
<selected-state>
<State>
<id>User defined</id>
</State>
</selected-state>
</profile-state>
</entry>
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/lora.iml" filepath="$PROJECT_DIR$/.idea/lora.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

71
Cargo.toml Normal file
View File

@@ -0,0 +1,71 @@
[package]
edition = "2024"
name = "lora"
rust-version = "1.88"
version = "0.1.0"
[[bin]]
name = "lora"
path = "./src/main.rs"
doctest = false
bench = false
test = false
[features]
debug = []
[dependencies]
esp-hal = { version = "~1.0", features = ["defmt", "esp32s3", "unstable"] }
esp-rtos = { version = "0.2.0", features = [
"defmt",
"embassy",
"esp-alloc",
"esp32s3",
] }
esp-alloc = { version = "0.9.0", features = ["defmt"] }
esp-backtrace = { version = "0.18.1", features = [
"defmt",
"esp32s3",
"panic-handler",
] }
esp-println = { version = "0.16.1", features = ["defmt-espflash", "esp32s3"] }
esp-bootloader-esp-idf = { version = "0.4.0", features = ["defmt", "esp32s3"] }
embassy-executor = { version = "0.9.1", features = ["defmt"] }
embassy-time = { version = "0.5.0", features = ["defmt", "generic-queue-8", "defmt-timestamp-uptime"] }
embassy-embedded-hal = { version = "0.5.0", features = ["defmt"] }
embassy-sync = { version = "0.7.2", features = ["defmt"] }
static_cell = "2.1.1"
defmt = "1.0.1"
anyhow = { version = "1.0.102", default-features = false }
thiserror = { version = "2.0.18", default-features = false }
lora-phy = { version = "3.0.1", features = ["lorawan-radio"] }
lorawan-device = { version = "0.12.2", default-features = false, features = ["region-as923-1", "defmt", "default-crypto"] }
ds3231 = { version = "0.3.0", features = ["defmt"] }
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
chrono = { version = "0.4.44", default-features = false, features = ["alloc", "defmt", "serde"] }
hifitime = { version = "4.2.5", default-features = false, features = ["serde_derive"] }
eeprom24x = { version = "0.7.2", features = ["defmt-03"] }
postcard = { version = "1.1.3", default-features = false, features = ["defmt", "alloc", "embedded-io", "postcard-derive"] }
serde = { version = "1.0.228", default-features = false, features = ["derive"] }
[patch.crates-io]
lorawan-device = { path = "lorawan-device-patch" }
[profile.dev]
# Rust debug is too slow.
# For debug builds always builds with some optimization
opt-level = "s"
[profile.release]
codegen-units = 1 # LLVM can perform better optimizations using a single thread
debug = 2
debug-assertions = false
incremental = false
lto = 'fat'
opt-level = 's'
overflow-checks = false

71
build.rs Normal file
View File

@@ -0,0 +1,71 @@
fn main() {
linker_be_nice();
println!("cargo:rustc-link-arg=-Tdefmt.x");
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
println!("cargo:rustc-link-arg=-Tlinkall.x");
}
fn linker_be_nice() {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
let kind = &args[1];
let what = &args[2];
match kind.as_str() {
"undefined-symbol" => match what.as_str() {
what if what.starts_with("_defmt_") => {
eprintln!();
eprintln!(
"💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`"
);
eprintln!();
}
"_stack_start" => {
eprintln!();
eprintln!("💡 Is the linker script `linkall.x` missing?");
eprintln!();
}
what if what.starts_with("esp_rtos_") => {
eprintln!();
eprintln!(
"💡 `esp-radio` has no scheduler enabled. Make sure you have initialized `esp-rtos` or provided an external scheduler."
);
eprintln!();
}
"embedded_test_linker_file_not_added_to_rustflags" => {
eprintln!();
eprintln!(
"💡 `embedded-test` not found - make sure `embedded-test.x` is added as a linker script for tests"
);
eprintln!();
}
"free"
| "malloc"
| "calloc"
| "get_free_internal_heap_size"
| "malloc_internal"
| "realloc_internal"
| "calloc_internal"
| "free_internal" => {
eprintln!();
eprintln!(
"💡 Did you forget the `esp-alloc` dependency or didn't enable the `compat` feature on it?"
);
eprintln!();
}
_ => (),
},
// we don't have anything helpful for "missing-lib" yet
_ => {
std::process::exit(1);
}
}
std::process::exit(0);
}
println!(
"cargo:rustc-link-arg=-Wl,--error-handling-script={}",
std::env::current_exe().unwrap().display()
);
}

View File

@@ -0,0 +1 @@
{"v":1}

View File

@@ -0,0 +1,6 @@
{
"git": {
"sha1": "0efdb6b26407053c877cc6659a76c83b2dbdd952"
},
"path_in_vcs": "lorawan-device"
}

View 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.

View 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
View 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 = []

View 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

View 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
}
}

View 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) -> &region::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()
}
}

View 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(())
}
}

View 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();
}

View 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();
}
}

View 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
}
}

View 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)
}

View 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]> },
}

View 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;

View 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,
}
}

View 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
}
}

View 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
}
}

View 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();
}
}

View 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}")
}
}

View 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;
}

View 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)),
)
}

View 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)));
}

View 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
}
}

View 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
}
}

View 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;

View File

@@ -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
];

View File

@@ -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
];

View File

@@ -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
];

View File

@@ -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
];

View 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()
}
}

View File

@@ -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
];

View File

@@ -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,
];

View File

@@ -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
}
}

View File

@@ -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();
}
}
}

View 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)
}
}

View File

@@ -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,
}),
];

View File

@@ -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,
];

View File

@@ -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
}
}

View 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
}
}

View 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(())
}
}

View 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()
}

2
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "esp"

117
src/board.rs Normal file
View File

@@ -0,0 +1,117 @@
use core::cell::RefCell;
use crate::components::clock::Clock;
use crate::components::lora::Lora;
use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
use defmt::Debug2Format;
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice as BlockingI2cDevice;
use embassy_executor::Spawner;
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
use embassy_sync::mutex::Mutex;
use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
use embassy_time::{Duration, Timer};
use esp_hal::gpio::{InputConfig, Level};
use esp_hal::i2c::master::I2c;
use esp_hal::peripherals::Peripherals;
use esp_hal::spi::master::{Config, Spi};
use esp_hal::timer::timg::TimerGroup;
use esp_hal::{gpio, Async, Blocking};
use static_cell::StaticCell;
use crate::components::rom_storage::RomStorage;
use crate::types::{AppData, SharedI2c};
pub(crate) static LED_STATE: AtomicBool = AtomicBool::new(false);
pub struct Board {
spawner: Spawner,
lora: Lora<'static>,
clock: Clock<'static>,
storage: RomStorage<'static>
}
impl Board {
pub async fn new(spawner: Spawner, peripherals: Peripherals) -> Result<Self, crate::error::Error> {
let timer_group = TimerGroup::new(peripherals.TIMG0);
esp_rtos::start(timer_group.timer0);
static SPI_BUS: StaticCell<Mutex<CriticalSectionRawMutex, Spi<'static, Async>>> = StaticCell::new();
let spi = Spi::new(peripherals.SPI2, Config::default())?
.into_async()
.with_sck(peripherals.GPIO9)
.with_mosi(peripherals.GPIO10)
.with_miso(peripherals.GPIO11);
let spi_bus = SPI_BUS.init(Mutex::new(spi));
static I2C_BUS: StaticCell<BlockingMutex<NoopRawMutex, RefCell<I2c<'static, Blocking>>>> = StaticCell::new();
let async_i2c = I2c::new(peripherals.I2C0, Default::default())?
.with_sda(peripherals.GPIO21)
.with_scl(peripherals.GPIO19);
let i2c_bus = I2C_BUS.init(BlockingMutex::new(RefCell::new(async_i2c)));
i2c_bus.lock(|bus| {
let mut bus = bus.borrow_mut();
for addr in 0x08..0x78u8 {
let mut buf = [0u8; 1];
if bus.read(addr, &mut buf).is_ok() {
defmt::debug!("I2C device found at 0x{:02x}", addr);
}
}
});
let vext = gpio::Output::new(peripherals.GPIO36, Level::Low, Default::default());
let lora = {
let reset = gpio::Output::new(peripherals.GPIO12, Level::Low, Default::default());
let busy = gpio::Input::new(peripherals.GPIO13, InputConfig::default());
let dio1 = gpio::Input::new(peripherals.GPIO14, InputConfig::default());
let cs = gpio::Output::new(peripherals.GPIO8, Level::High, Default::default());
let spi_device = SpiDevice::new(spi_bus, cs);
Lora::new(spi_device, reset, busy, dio1, vext).await?
};
let clock = Clock::new(SharedI2c::new(i2c_bus), 0x68)?;
let led = gpio::Output::new(peripherals.GPIO35, Level::Low, Default::default());
spawner.spawn(led_task(led))?;
let storage = RomStorage::new(
SharedI2c::new(i2c_bus)
);
let instance = Self { spawner, lora, clock, storage };
Ok(instance)
}
pub async fn start(&mut self) -> Result<(), crate::error::Error> {
self.lora.try_join().await?;
defmt::info!("Board started");
LED_STATE.store(true, Ordering::Relaxed);
let time = self.lora.get_time(self.clock.get_datetime()?).await?;
let datetime = self.clock.update_time(time)?;
let app_data = AppData {
last_boot_time: datetime
};
defmt::debug!("stored app data: {:?}", Debug2Format(&self.storage.get_data::<AppData>()?));
self.storage.write_data(&app_data).await?;
let atomic_lora = AtomicPtr::new(&mut self.lora);
self.spawner.spawn(crate::components::lora::start_heartbeat_task(atomic_lora))?;
loop {
defmt::trace!("tick");
Timer::after(Duration::from_secs(1)).await;
}
}
}
#[embassy_executor::task]
async fn led_task(mut led: gpio::Output<'static>) {
loop {
if LED_STATE.load(Ordering::Relaxed) {
led.set_high();
} else {
led.set_low();
}
Timer::after(Duration::from_millis(50)).await;
}
}

68
src/components/clock.rs Normal file
View File

@@ -0,0 +1,68 @@
use crate::types::SharedI2c;
use chrono::{DateTime, NaiveDateTime, Utc};
use defmt::Debug2Format;
use ds3231::{DS3231Error, InterruptControl, Oscillator, SquareWaveFrequency, TimeRepresentation, DS3231};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_embedded_hal::shared_bus::I2cDeviceError;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use esp_hal::i2c::master::Error as I2cError;
use esp_hal::i2c::master::I2c;
use esp_hal::Async;
use hifitime::Epoch;
#[derive(thiserror::Error, Debug)]
pub(crate) enum ClockError {
#[error("DS2321 error: {0:?}")]
DS2321Error(DS3231Error<I2cDeviceError<I2cError>>),
#[error("Invalid time")]
InvalidTime,
}
impl From<DS3231Error<I2cDeviceError<I2cError>>> for ClockError {
fn from(value: DS3231Error<I2cDeviceError<I2cError>>) -> Self {
Self::DS2321Error(value)
}
}
pub(crate) struct Clock<'d> {
driver: DS3231<SharedI2c<'d>>,
}
impl<'d> Clock<'d> {
pub fn new(i2c_device: SharedI2c<'d>, address: u8) -> Result<Self, ClockError> {
let config = ds3231::Config {
time_representation: TimeRepresentation::TwentyFourHour,
square_wave_frequency: SquareWaveFrequency::Hz1,
interrupt_control: InterruptControl::SquareWave,
battery_backed_square_wave: false,
oscillator_enable: Oscillator::Enabled,
};
let mut clock = DS3231::new(i2c_device, address);
clock.configure(&config)?;
let datetime = clock.datetime()?;
defmt::debug!("Clock Time: {:?}", Debug2Format(&datetime));
Ok(
Self {
driver: clock
}
)
}
pub fn update_time(&mut self, time: Epoch) -> Result<DateTime<Utc>, ClockError> {
let Some(utc_datetime) = DateTime::from_timestamp_secs(time.to_unix_seconds() as i64) else {
defmt::error!("Failed to convert time to datetime");
return Err(ClockError::InvalidTime);
};
let time = utc_datetime.time();
let date = utc_datetime.date_naive();
self.driver.set_datetime(&NaiveDateTime::new(date, time))?;
let datetime = self.driver.datetime()?;
defmt::debug!("Clock Time: {:?}", Debug2Format(&datetime));
Ok(utc_datetime)
}
pub fn get_datetime(&mut self) -> Result<NaiveDateTime, ClockError> {
let datetime = self.driver.datetime()?;
Ok(datetime)
}
}

213
src/components/lora.rs Normal file
View File

@@ -0,0 +1,213 @@
use crate::types::SharedAsyncSpi;
use crate::timer::EmbassyTimer;
use chrono::{DateTime, NaiveDateTime, Utc};
use core::str::FromStr;
use core::sync::atomic::{AtomicPtr, Ordering};
use defmt::Formatter;
use embassy_time::{Delay, Duration, Instant, Timer};
use esp_hal::gpio;
use esp_hal::rng::Rng;
use hifitime::Epoch;
use lora_phy::iv::GenericSx126xInterfaceVariant;
use lora_phy::lorawan_radio::Error as LoraRadioError;
use lora_phy::lorawan_radio::LorawanRadio;
use lora_phy::mod_params::RadioError;
use lora_phy::sx126x::{Sx1262, Sx126x, TcxoCtrlVoltage};
use lora_phy::{sx126x, LoRa};
use lorawan_device::async_device::Error as LoraDeviceError;
use lorawan_device::async_device::Device;
use lorawan_device::default_crypto::DefaultFactory;
use lorawan_device::{region, AppEui, AppKey, DevEui, JoinMode};
const MAX_TX_POWER: u8 = 14;
const LORAWAN_REGION: region::Region = region::Region::AS923_1;
pub const CLOCK_SYNC_FPORT: u8 = 202;
const APP_TIME_CID: u8 = 0x01;
#[derive(thiserror::Error, Debug)]
pub enum LoraError {
#[error("Lora radio error: {0:?}")]
LoraRadioError(RadioError),
#[error("Lora device error: {0:?}")]
LoraDeviceError(LoraDeviceError<LoraRadioError>),
#[error("Invalid downlink data")]
InvalidDownlinkData,
#[error("No downlink received")]
NoDownlinkReceived,
}
pub struct Lora<'d> {
driver: Device<
LorawanRadio<
Sx126x<
SharedAsyncSpi<'d>,
GenericSx126xInterfaceVariant<
gpio::Output<'d>,
gpio::Input<'d>
>,
Sx1262
>,
Delay,
MAX_TX_POWER
>,
DefaultFactory,
EmbassyTimer,
Rng
>,
power: gpio::Output<'d>,
}
impl<'d> Lora<'d> {
pub async fn new(
spi_bus: SharedAsyncSpi<'d>,
reset: gpio::Output<'d>,
busy: gpio::Input<'d>,
dio1: gpio::Input<'d>,
power: gpio::Output<'d>,
) -> Result<Self, LoraError> {
let config = sx126x::Config {
chip: Sx1262,
tcxo_ctrl: Some(TcxoCtrlVoltage::Ctrl1V8),
use_dcdc: true,
rx_boost: true,
};
let iv = GenericSx126xInterfaceVariant::new(
reset,
dio1,
busy,
None,
None,
)?;
let lora = LoRa::new(
Sx126x::new(
spi_bus,
iv,
config,
),
true,
Delay,
).await?;
let radio: LorawanRadio<_, _, MAX_TX_POWER> = lora.into();
let region = region::Configuration::new(LORAWAN_REGION);
let device = Device::new(region, radio, EmbassyTimer::new(), Rng::new());
Ok(
Self {
driver: device,
power,
}
)
}
pub async fn try_join(&mut self) -> Result<(), crate::error::Error> {
defmt::debug!("Joining LoRaWAN network");
let otaa_config = JoinMode::OTAA {
deveui: DevEui::from_str(&env!("DEV_EUI"))?,
appkey: AppKey::from_str(&env!("APP_KEY"))?,
appeui: AppEui::from_str(&env!("APP_EUI"))?,
};
loop {
let response = self.driver
.join(&otaa_config)
.await;
match response {
Ok(response) => match response {
lorawan_device::async_device::JoinResponse::JoinSuccess => {
defmt::info!("LoRaWAN network joined successfully!");
self.power.set_high();
break Ok(());
}
lorawan_device::async_device::JoinResponse::NoJoinAccept => {
defmt::error!("No join accept from LoRaWAN network");
}
},
Err(err) => {
defmt::error!("{}", err);
continue;
}
};
Timer::after(Duration::from_secs(1)).await;
}
}
pub async fn get_time(&mut self, current_time: NaiveDateTime) -> Result<Epoch, LoraError> {
self.driver.set_datarate(region::DR::_0);
let param: u8 = (1 & 0x0F) | 1 << 4;
let datetime: DateTime<Utc> = DateTime::from_naive_utc_and_offset(current_time, Utc);
let epoch = Epoch::from_unix_seconds(datetime.timestamp() as f64);
let gpst_time = epoch.to_gpst_seconds();
let gpst_bytes: [u8; 4] = (gpst_time as u32).to_le_bytes();
let payload = [APP_TIME_CID, gpst_bytes[0], gpst_bytes[1], gpst_bytes[2], gpst_bytes[3], param];
let start = Instant::now();
let _ = self.driver.send(&payload, CLOCK_SYNC_FPORT, false).await?;
let elapsed_secs = start.elapsed().as_secs() as f64;
defmt::debug!("Received downlink after {} seconds", elapsed_secs);
let time_offset = if let Some(downlink) = self.driver.take_downlink() {
defmt::debug!("Received Downlink: {:?}", downlink);
let Ok(data) = downlink.data.into_array::<6>() else {
return Err(LoraError::InvalidDownlinkData);
};
let num_bytes: [u8; 4] = data[1..5].try_into().unwrap();
let value = u32::from_le_bytes(num_bytes);
defmt::debug!("Time value: {}", value);
value as f64
} else {
return Err(LoraError::NoDownlinkReceived);
};
Ok(Epoch::from_gpst_seconds(gpst_time + elapsed_secs + time_offset))
}
async fn send_heartbeat(&mut self) -> Result<(), LoraError> {
self.driver.set_datarate(region::DR::_0);
self.driver.send(&[], 1, false).await?;
Ok(())
}
}
#[embassy_executor::task]
pub(crate) async fn start_heartbeat_task(lora: AtomicPtr<Lora<'static>>) {
let Some(lora) = (unsafe { lora.load(Ordering::Relaxed).as_mut() }) else {
defmt::error!("Lora is not initialized");
return;
};
loop {
let timer = match lora.send_heartbeat().await {
Ok(_) => {
defmt::debug!("Heartbeat sent successfully");
#[cfg(feature = "debug")]
let timer = Timer::after_secs(5);
#[cfg(not(feature = "debug"))]
let timer = Timer::after_secs(60);
timer
}
Err(e) => {
defmt::error!("Error while sending heartbeat: {}", e);
Timer::after_secs(1)
}
};
timer.await;
}
}
impl From<RadioError> for LoraError {
fn from(e: RadioError) -> Self {
Self::LoraRadioError(e)
}
}
impl From<LoraDeviceError<LoraRadioError>> for LoraError {
fn from(value: LoraDeviceError<LoraRadioError>) -> Self {
Self::LoraDeviceError(value)
}
}
impl defmt::Format for LoraError {
fn format(&self, fmt: Formatter) {
defmt::write!(fmt, "{:?}", self);
}
}

3
src/components/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub(crate) mod lora;
pub(crate) mod clock;
pub(crate) mod rom_storage;

View File

@@ -0,0 +1,83 @@
use alloc::collections::BTreeMap;
use core::any::{TypeId};
use eeprom24x::{Eeprom24x, SlaveAddr};
use embassy_embedded_hal::shared_bus::I2cDeviceError;
use embassy_time::{Duration, Timer};
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::types::SharedI2c;
const ADDR_SIZE: usize = 256;
#[derive(thiserror::Error, Debug)]
pub(crate) enum StorageError {
#[error("Data oversized: {0}")]
StorageOverSize(usize),
#[error("EEPROM Error: {0:?}")]
EEPROMError(eeprom24x::Error<I2cDeviceError<esp_hal::i2c::master::Error>>),
#[error("Out of address space")]
OutOfAddress
}
impl From<eeprom24x::Error<I2cDeviceError<esp_hal::i2c::master::Error>>> for StorageError {
fn from(value: eeprom24x::Error<I2cDeviceError<esp_hal::i2c::master::Error>>) -> Self {
Self::EEPROMError(value)
}
}
pub(crate) struct RomStorage<'d> {
driver: Eeprom24x<SharedI2c<'d>, eeprom24x::page_size::B32, eeprom24x::addr_size::TwoBytes, eeprom24x::unique_serial::No>,
addr_table: BTreeMap<TypeId, u32>
}
impl<'d> RomStorage<'d> {
pub fn new(i2c: SharedI2c<'d>) -> Self {
RomStorage {
driver: Eeprom24x::new_24x32(i2c, SlaveAddr::Alternative(true, true, true)),
addr_table: BTreeMap::new()
}
}
fn get_or_insert_addr<T: 'static>(&mut self) -> Result<u32, StorageError> {
let id = TypeId::of::<T>();
if let Some(addr) = self.addr_table.get(&id) {
Ok(addr.clone())
} else {
let size = self.addr_table.len();
let addr = size as u32 * ADDR_SIZE as u32;
if addr >= u16::MAX as u32 {
return Err(StorageError::OutOfAddress)
}
self.addr_table.insert(id, addr);
Ok(addr)
}
}
pub async fn write_data<T: Serialize + 'static>(&mut self, data: &T) -> Result<(), crate::error::Error> {
defmt::debug!("Writing app data");
let addr = self.get_or_insert_addr::<T>()?;
let bytes = postcard::to_allocvec(data)?;
let bytes_len = bytes.len();
if bytes_len > ADDR_SIZE {
return Err(StorageError::StorageOverSize(bytes_len).into());
}
for (i, chunk) in bytes.chunks(32).enumerate() {
defmt::debug!("Writing chunk {}", i);
let page_addr = addr + (i as u32 * 32);
self.driver.write_page(page_addr, chunk)
.map_err(|e| StorageError::EEPROMError(e))?;
Timer::after(Duration::from_millis(10)).await;
}
defmt::info!("Finished writing app data");
Ok(())
}
pub fn get_data<T: DeserializeOwned + 'static>(&mut self) -> Result<T, crate::error::Error> {
let addr = self.get_or_insert_addr::<T>()?;
let mut bytes = [0u8; ADDR_SIZE];
self.driver.read_data(addr, &mut bytes)
.map_err(|e| StorageError::EEPROMError(e))?;
let data = postcard::from_bytes(&bytes)?;
Ok(data)
}
}

69
src/error.rs Normal file
View File

@@ -0,0 +1,69 @@
use crate::components::clock::ClockError;
use crate::components::lora::LoraError;
use core::convert::Infallible;
use defmt::Formatter;
use embassy_executor::SpawnError;
use esp_hal::i2c::master::ConfigError as I2cConfigError;
use esp_hal::rng::TrngError;
use esp_hal::spi::master::ConfigError as SpiConfigError;
use crate::components::rom_storage::StorageError;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("SPI config error: {0}")]
SpiConfigError(#[from] SpiConfigError),
#[error("I2C config error: {0}")]
I2cConfigError(#[from] I2cConfigError),
#[error("Encounter Infallible: {0}")]
Infallible(#[from] Infallible),
#[error("Lora Error: {0}")]
Lora(#[from] LoraError),
#[error("Clock Error: {0}")]
ClockError(#[from] ClockError),
#[error("Spawn Error: {0}")]
TaskSpawnError(#[from] SpawnError),
#[error("Trng error: {0:?}")]
TrngError(TrngError),
#[error("Hex decode error: {0:?}")]
HexDecodeError(hex::FromHexError),
#[error("LEDC Timer Error {0:?}")]
LEDCTimerError(esp_hal::ledc::timer::Error),
#[error("LEDC Channel Error {0:?}")]
LEDCChannelError(esp_hal::ledc::channel::Error),
#[error("Error serializing postcard: {0}")]
PostcardError(#[from] postcard::Error),
#[error("ROM Storage Error: {0}")]
StorageError(#[from] StorageError),
#[error("Unknown error: {0}")]
Other(#[from] anyhow::Error),
}
impl From<TrngError> for Error {
fn from(e: TrngError) -> Self {
Self::TrngError(e)
}
}
impl From<hex::FromHexError> for Error {
fn from(value: hex::FromHexError) -> Self {
Self::HexDecodeError(value)
}
}
impl From<esp_hal::ledc::timer::Error> for Error {
fn from(value: esp_hal::ledc::timer::Error) -> Self {
Self::LEDCTimerError(value)
}
}
impl From<esp_hal::ledc::channel::Error> for Error {
fn from(value: esp_hal::ledc::channel::Error) -> Self {
Self::LEDCChannelError(value)
}
}
impl defmt::Format for Error {
fn format(&self, fmt: Formatter) {
defmt::write!(fmt, "{}", self);
}
}

60
src/main.rs Normal file
View File

@@ -0,0 +1,60 @@
#![no_std]
#![no_main]
#![deny(
clippy::mem_forget,
reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
holding buffers for the duration of a data transfer."
)]
#![deny(clippy::large_stack_frames)]
extern crate alloc;
mod error;
mod board;
mod timer;
mod components;
mod types;
#[allow(unused_imports)]
use {esp_backtrace as _, esp_println as _};
use embassy_executor::Spawner;
use esp_hal::clock::CpuClock;
// This creates a default app-descriptor required by the esp-idf bootloader.
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!();
const RETRY_COUNT: usize = 3;
#[allow(
clippy::large_stack_frames,
reason = "it's not unusual to allocate larger buffers etc. in main"
)]
#[esp_rtos::main]
async fn main(spawner: Spawner) -> () {
esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 73744);
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let mut board = match board::Board::new(spawner, peripherals).await {
Ok(board) => board,
Err(e) => {
defmt::error!("Error while initializing board: {:?}", e);
return;
}
};
defmt::info!("Board initialized");
let mut retry_count = RETRY_COUNT;
loop {
if retry_count == 0 {
break;
}
if let Err(e) = board.start().await {
defmt::error!("Error: {}", e);
}
retry_count -= 1;
}
defmt::error!("Unable to start after {} retries", RETRY_COUNT);
}

32
src/timer.rs Normal file
View File

@@ -0,0 +1,32 @@
use embassy_time::{Duration, Instant};
use lorawan_device::async_device::radio::Timer;
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
}
}

18
src/types/mod.rs Normal file
View File

@@ -0,0 +1,18 @@
use chrono::{DateTime, NaiveDateTime, Utc};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
use esp_hal::i2c::master::I2c;
use esp_hal::spi::master::Spi;
use esp_hal::{gpio, Async, Blocking};
use serde::{Deserialize, Serialize};
pub(crate) type SharedAsyncSpi<'d> =
SpiDevice<'d, CriticalSectionRawMutex, Spi<'d, Async>, gpio::Output<'d>>;
pub(crate) type SharedI2c<'d> =
I2cDevice<'d, NoopRawMutex, I2c<'d, Blocking>>;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub(crate) struct AppData {
pub last_boot_time: DateTime<Utc>,
}