commit 5c95cc40f736f9c9026bd47902011ffffb3f4e1b Author: fromost Date: Mon Mar 30 15:32:51 2026 +0800 Init diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..d571e1f --- /dev/null +++ b/.cargo/config.toml @@ -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"] diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 0000000..76f6c1d --- /dev/null +++ b/.clippy.toml @@ -0,0 +1 @@ +stack-size-threshold = 1024 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a95b90 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml new file mode 100644 index 0000000..b69120c --- /dev/null +++ b/.idea/dictionaries/project.xml @@ -0,0 +1,8 @@ + + + + Trng + gpst + + + \ No newline at end of file diff --git a/.idea/lora.iml b/.idea/lora.iml new file mode 100644 index 0000000..cf84ae4 --- /dev/null +++ b/.idea/lora.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml new file mode 100644 index 0000000..715fb49 --- /dev/null +++ b/.idea/material_theme_project_new.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..34bb32c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,22 @@ + + + + + + + + Dev Container + + + General + + + + + User defined + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f931d35 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..78ba1b7 --- /dev/null +++ b/Cargo.toml @@ -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 diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..4ec39ea --- /dev/null +++ b/build.rs @@ -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 = 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() + ); +} diff --git a/lorawan-device-patch/.cargo-ok b/lorawan-device-patch/.cargo-ok new file mode 100644 index 0000000..5f8b795 --- /dev/null +++ b/lorawan-device-patch/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/lorawan-device-patch/.cargo_vcs_info.json b/lorawan-device-patch/.cargo_vcs_info.json new file mode 100644 index 0000000..f5c7a1e --- /dev/null +++ b/lorawan-device-patch/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "0efdb6b26407053c877cc6659a76c83b2dbdd952" + }, + "path_in_vcs": "lorawan-device" +} \ No newline at end of file diff --git a/lorawan-device-patch/CHANGELOG.md b/lorawan-device-patch/CHANGELOG.md new file mode 100644 index 0000000..3bfc22c --- /dev/null +++ b/lorawan-device-patch/CHANGELOG.md @@ -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. diff --git a/lorawan-device-patch/Cargo.toml b/lorawan-device-patch/Cargo.toml new file mode 100644 index 0000000..9a8bb49 --- /dev/null +++ b/lorawan-device-patch/Cargo.toml @@ -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 ", + "Ulf Lilleengen ", +] +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", +] diff --git a/lorawan-device-patch/Cargo.toml.orig b/lorawan-device-patch/Cargo.toml.orig new file mode 100644 index 0000000..014ba50 --- /dev/null +++ b/lorawan-device-patch/Cargo.toml.orig @@ -0,0 +1,74 @@ +[package] +name = "lorawan-device" +version = "0.12.2" +authors = ["Louis Thiery ", "Ulf Lilleengen "] +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 = [] + diff --git a/lorawan-device-patch/README.md b/lorawan-device-patch/README.md new file mode 100644 index 0000000..5407048 --- /dev/null +++ b/lorawan-device-patch/README.md @@ -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 diff --git a/lorawan-device-patch/src/async_device/embassy_time.rs b/lorawan-device-patch/src/async_device/embassy_time.rs new file mode 100644 index 0000000..09d3dd5 --- /dev/null +++ b/lorawan-device-patch/src/async_device/embassy_time.rs @@ -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 + } +} diff --git a/lorawan-device-patch/src/async_device/mod.rs b/lorawan-device-patch/src/async_device/mod.rs new file mode 100644 index 0000000..aac0e75 --- /dev/null +++ b/lorawan-device-patch/src/async_device/mod.rs @@ -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 +where + R: radio::PhyRxTx + Timings, + T: radio::Timer, + C: CryptoFactory + Default, + G: RngCore, +{ + crypto: PhantomData, + radio: R, + rng: G, + timer: T, + mac: Mac, + radio_buffer: RadioBuffer, + downlink: Vec, + class_c: bool, +} + +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Debug)] +pub enum Error { + 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 From for Error { + fn from(e: mac::Error) -> Self { + Error::Mac(e) + } +} + +impl Device +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, + ) -> Self { + let rng = rng::Prng::new(seed); + Device::new_with_session(region, radio, timer, rng, session) + } +} + +impl Device +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, + ) -> Self { + let mut mac = Mac::new(region, R::MAX_RADIO_POWER, R::ANTENNA_GAIN); + if let Some(session) = session { + mac.set_session(session); + } + Self { + crypto: PhantomData, + radio, + rng, + mac, + radio_buffer: RadioBuffer::new(), + timer, + downlink: Vec::new(), + class_c: false, + } + } + + /// Enables Class C behavior. Note that Class C downlinks are not possible until a confirmed + /// uplink is sent to the LNS. + + pub fn enable_class_c(&mut self) { + self.class_c = true; + } + + /// Disables Class C behavior. Note that an uplink must be set for the radio to disable + /// Class C listen. + pub fn disable_class_c(&mut self) { + self.class_c = false; + } + + pub fn get_session(&mut self) -> Option<&Session> { + self.mac.get_session() + } + + pub fn get_region(&mut self) -> ®ion::Configuration { + &self.mac.region + } + + pub fn get_radio(&mut self) -> &R { + &self.radio + } + + pub fn get_mut_radio(&mut self) -> &mut R { + &mut self.radio + } + + /// Retrieve the current data rate being used by this device. + pub fn get_datarate(&mut self) -> DR { + self.mac.configuration.data_rate + } + + /// Set the data rate being used by this device. This overrides the region default. + pub fn set_datarate(&mut self, datarate: DR) { + self.mac.configuration.data_rate = datarate; + } + + /// Join the LoRaWAN network asynchronously. The returned future completes when + /// the LoRaWAN network has been joined successfully, or an error has occurred. + /// + /// Repeatedly calling join using OTAA will result in a new LoRaWAN session to be created. + /// + /// Note that for a Class C enabled device, you must repeatedly send *confirmed* uplink until + /// LoRaWAN Network Server (LNS) confirmation after joining. + pub async fn join(&mut self, join_mode: &JoinMode) -> Result> { + match join_mode { + JoinMode::OTAA { deveui, appeui, appkey } => { + let (tx_config, _) = self.mac.join_otaa::( + &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> { + // Prepare transmission buffer + let (tx_config, _fcnt_up) = self.mac.send::( + &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 { + self.downlink.pop() + } + + async fn window_complete(&mut self) -> Result<(), Error> { + 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, Error> { + 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 + Sized + Unpin> { + Rx(usize, RxQuality, F), + Timeout(u32), + } + + /// RXC window listen until timeout + async fn rxc_listen_until_timeout( + radio: &mut R, + rx_buf: &mut RadioBuffer, + window_duration: u32, + timeout_fut: F, + ) -> RxcWindowResponse + where + F: futures::Future + 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::(&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> { + 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, Error> { + 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::(&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> { + 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::(&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() + } +} diff --git a/lorawan-device-patch/src/async_device/radio.rs b/lorawan-device-patch/src/async_device/radio.rs new file mode 100644 index 0000000..945c8a1 --- /dev/null +++ b/lorawan-device-patch/src/async_device/radio.rs @@ -0,0 +1,69 @@ +pub use crate::radio::{RfConfig, RxConfig, RxMode, RxQuality, TxConfig}; + +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Error(pub E); + +impl From> for super::Error { + fn from(radio_error: Error) -> super::Error { + 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; + + /// 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; + + /// Puts the radio into a low-power mode + async fn low_power(&mut self) -> Result<(), Self::PhyError> { + Ok(()) + } +} diff --git a/lorawan-device-patch/src/async_device/test/mod.rs b/lorawan-device-patch/src/async_device/test/mod.rs new file mode 100644 index 0000000..e90ba2d --- /dev/null +++ b/lorawan-device-patch/src/async_device/test/mod.rs @@ -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; + +#[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(); +} diff --git a/lorawan-device-patch/src/async_device/test/radio.rs b/lorawan-device-patch/src/async_device/test/radio.rs new file mode 100644 index 0000000..827a408 --- /dev/null +++ b/lorawan-device-patch/src/async_device/test/radio.rs @@ -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, + last_uplink: Arc>>, + rx: mpsc::Receiver, +} + +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 { + 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 { + 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>>, + tx: mpsc::Sender, +} + +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(); + } +} diff --git a/lorawan-device-patch/src/async_device/test/timer.rs b/lorawan-device-patch/src/async_device/test/timer.rs new file mode 100644 index 0000000..1c450a2 --- /dev/null +++ b/lorawan-device-patch/src/async_device/test/timer.rs @@ -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>, + tx: Arc>>>, +} + +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>, + tx: Arc>>>, +} + +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 + } +} diff --git a/lorawan-device-patch/src/async_device/test/util.rs b/lorawan-device-patch/src/async_device/test/util.rs new file mode 100644 index 0000000..c758271 --- /dev/null +++ b/lorawan-device-patch/src/async_device/test/util.rs @@ -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) -> (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) +} diff --git a/lorawan-device-patch/src/lib.rs b/lorawan-device-patch/src/lib.rs new file mode 100644 index 0000000..539a0ed --- /dev/null +++ b/lorawan-device-patch/src/lib.rs @@ -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#"{feature}"#)] +#![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, + 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]> }, +} diff --git a/lorawan-device-patch/src/log.rs b/lorawan-device-patch/src/log.rs new file mode 100644 index 0000000..3fafd3e --- /dev/null +++ b/lorawan-device-patch/src/log.rs @@ -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; diff --git a/lorawan-device-patch/src/mac/mod.rs b/lorawan-device-patch/src/mac/mod.rs new file mode 100644 index 0000000..00da5b4 --- /dev/null +++ b/lorawan-device-patch/src/mac/mod.rs @@ -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, + ) { + 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 = core::result::Result; + +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( + &mut self, + rng: &mut RNG, + credentials: NetworkCredentials, + buf: &mut RadioBuffer, + ) -> (radio::TxConfig, u16) { + let mut otaa = otaa::Otaa::new(credentials); + let dev_nonce = otaa.prepare_buffer::(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( + &mut self, + rng: &mut RNG, + buf: &mut RadioBuffer, + send_data: &SendData, + ) -> Result<(radio::TxConfig, FcntUp)> { + let fcnt = match &mut self.state { + State::Joined(ref mut session) => Ok(session.prepare_buffer::(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( + &mut self, + buf: &mut RadioBuffer, + dl: &mut Vec, + ) -> Response { + match &mut self.state { + State::Joined(ref mut session) => session.handle_rx::( + &mut self.region, + &mut self.configuration, + buf, + dl, + false, + ), + State::Otaa(ref mut otaa) => { + if let Some(session) = + otaa.handle_rx::(&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( + &mut self, + buf: &mut RadioBuffer, + dl: &mut Vec, + ) -> Result { + match &mut self.state { + State::Joined(ref mut session) => Ok(session.handle_rx::( + &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 { + 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 { + 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 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 for async_device::SendResponse { + type Error = Error; + + fn try_from(r: Response) -> Result { + 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 for async_device::JoinResponse { + type Error = Error; + + fn try_from(r: Response) -> Result { + 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, + } +} diff --git a/lorawan-device-patch/src/mac/otaa.rs b/lorawan-device-patch/src/mac/otaa.rs new file mode 100644 index 0000000..730fd18 --- /dev/null +++ b/lorawan-device-patch/src/mac/otaa.rs @@ -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( + &mut self, + rng: &mut G, + buf: &mut RadioBuffer, + ) -> 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( + &mut self, + region: &mut Configuration, + configuration: &mut super::Configuration, + rx: &mut RadioBuffer, + ) -> Option { + 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 + } +} diff --git a/lorawan-device-patch/src/mac/session.rs b/lorawan-device-patch/src/mac/session.rs new file mode 100644 index 0000000..d946a1f --- /dev/null +++ b/lorawan-device-patch/src/mac/session.rs @@ -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 for SessionKeys { + fn from(session: Session) -> Self { + Self { newskey: session.newskey, appskey: session.appskey, devaddr: session.devaddr } + } +} + +impl Session { + pub fn derive_new, F: CryptoFactory>( + decrypt: &DecryptedJoinAcceptPayload, + 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 { + Some(SessionKeys { newskey: self.newskey, appskey: self.appskey, devaddr: self.devaddr }) + } +} + +impl Session { + pub(crate) fn handle_rx( + &mut self, + region: &mut region::Configuration, + configuration: &mut super::Configuration, + rx: &mut RadioBuffer, + dl: &mut Vec, + 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::::new(decrypted.fhdr().data()), + ); + if let FRMPayload::MACCommands(mac_cmds) = decrypted.frm_payload() { + configuration.handle_downlink_macs( + region, + &mut self.uplink, + MacCommandIterator::::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( + &mut self, + data: &SendData, + tx_buffer: &mut RadioBuffer, + ) -> FcntUp { + tx_buffer.clear(); + let fcnt = self.fcnt_up; + let mut phy: DataPayloadCreator, 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 + } +} diff --git a/lorawan-device-patch/src/mac/uplink.rs b/lorawan-device-patch/src/mac/uplink.rs new file mode 100644 index 0000000..0b4cb3c --- /dev/null +++ b/lorawan-device-patch/src/mac/uplink.rs @@ -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) { + 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(); + } +} diff --git a/lorawan-device-patch/src/nb_device/mod.rs b/lorawan-device-patch/src/nb_device/mod.rs new file mode 100644 index 0000000..8e58574 --- /dev/null +++ b/lorawan-device-patch/src/nb_device/mod.rs @@ -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 +where + R: PhyRxTx + Timings, + C: CryptoFactory + Default, + RNG: RngCore, +{ + state: State, + shared: Shared, + crypto: PhantomData, +} + +impl Device +where + R: PhyRxTx + Timings, + C: CryptoFactory + Default, + RNG: RngCore, +{ + pub fn new(region: region::Configuration, radio: R, rng: RNG) -> Device { + 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> { + 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> { + self.handle_event(Event::SendDataRequest(SendData { data, fport, confirmed })) + } + + pub fn get_fcnt_up(&self) -> Option { + 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 { + self.shared.mac.get_session_keys() + } + + pub fn take_downlink(&mut self) -> Option { + self.shared.downlink.pop() + } + + pub fn handle_event(&mut self, event: Event) -> Result> { + let (new_state, result) = self.state.handle_event::( + &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 { + pub(crate) radio: R, + pub(crate) rng: RNG, + pub(crate) tx_buffer: RadioBuffer, + pub(crate) mac: Mac, + pub(crate) downlink: Vec, +} + +#[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 { + Radio(R::PhyError), + State(state::Error), + Mac(mac::Error), +} + +impl From for Error { + fn from(mac_error: mac::Error) -> Error { + 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}") + } +} diff --git a/lorawan-device-patch/src/nb_device/radio.rs b/lorawan-device-patch/src/nb_device/radio.rs new file mode 100644 index 0000000..d7c9c8f --- /dev/null +++ b/lorawan-device-patch/src/nb_device/radio.rs @@ -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 +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) -> Result, Self::PhyError> + where + Self: Sized; +} diff --git a/lorawan-device-patch/src/nb_device/state.rs b/lorawan-device-patch/src/nb_device/state.rs new file mode 100644 index 0000000..92ff26a --- /dev/null +++ b/lorawan-device-patch/src/nb_device/state.rs @@ -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 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 From for super::Error { + fn from(error: Error) -> super::Error { + 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, + dl: &mut Vec, + event: Event, + ) -> (Self, Result>) { + match self { + State::Idle(s) => s.handle_event::(mac, radio, rng, buf, event), + State::SendingData(s) => s.handle_event::(mac, radio, event), + State::WaitingForRxWindow(s) => s.handle_event::(mac, radio, event), + State::WaitingForRx(s) => s.handle_event::(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, + event: Event, + ) -> (State, Result>) { + enum IntermediateResponse { + RadioTx((Frame, radio::TxConfig, u32)), + EarlyReturn(Result>), + } + + let response = match event { + // tolerate unexpected timeout + Event::Join(creds) => { + let (tx_config, dev_nonce) = mac.join_otaa::(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::(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 = + 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::(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( + self, + mac: &mut Mac, + radio: &mut R, + event: Event, + ) -> (State, Result>) { + 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::(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( + self, + mac: &mut Mac, + radio: &mut R, + event: Event, + ) -> (State, Result>) { + 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 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, + event: Event, + dl: &mut Vec, + ) -> (State, Result>) { + 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::(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( + frame: Frame, + mac: &mut Mac, + radio: &mut R, + timestamp_ms: u32, +) -> (State, Result>) { + 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)), + ) +} diff --git a/lorawan-device-patch/src/nb_device/test/mod.rs b/lorawan-device-patch/src/nb_device/test/mod.rs new file mode 100644 index 0000000..0430947 --- /dev/null +++ b/lorawan-device-patch/src/nb_device/test/mod.rs @@ -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))); +} diff --git a/lorawan-device-patch/src/nb_device/test/util.rs b/lorawan-device-patch/src/nb_device/test/util.rs new file mode 100644 index 0000000..ab676be --- /dev/null +++ b/lorawan-device-patch/src/nb_device/test/util.rs @@ -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 { + Device::new(Configuration::new(Region::US915), TestRadio::default(), rand::rngs::OsRng) +} + +#[derive(Debug)] +pub struct TestRadio { + current_config: Option, + last_uplink: Option, + rxtx_handler: Option, + 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) -> Result, 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 + } +} diff --git a/lorawan-device-patch/src/radio.rs b/lorawan-device-patch/src/radio.rs new file mode 100644 index 0000000..b3637d4 --- /dev/null +++ b/lorawan-device-patch/src/radio.rs @@ -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 { + packet: [u8; N], + pos: usize, +} + +impl RadioBuffer { + 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 AsMut<[u8]> for RadioBuffer { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.packet + } +} + +impl AsRef<[u8]> for RadioBuffer { + fn as_ref(&self) -> &[u8] { + &self.packet + } +} diff --git a/lorawan-device-patch/src/region/constants.rs b/lorawan-device-patch/src/region/constants.rs new file mode 100644 index 0000000..4560b11 --- /dev/null +++ b/lorawan-device-patch/src/region/constants.rs @@ -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; diff --git a/lorawan-device-patch/src/region/dynamic_channel_plans/as923.rs b/lorawan-device-patch/src/region/dynamic_channel_plans/as923.rs new file mode 100644 index 0000000..9fb571d --- /dev/null +++ b/lorawan-device-patch/src/region/dynamic_channel_plans/as923.rs @@ -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; + +impl ChannelRegion<7> +for AS923Region +{ + fn datarates() -> &'static [Option; 7] { + &DATARATES + } +} + +impl DynamicChannelRegion<2, 7> +for AS923Region +{ + 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; 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 +]; diff --git a/lorawan-device-patch/src/region/dynamic_channel_plans/eu433.rs b/lorawan-device-patch/src/region/dynamic_channel_plans/eu433.rs new file mode 100644 index 0000000..413aa4d --- /dev/null +++ b/lorawan-device-patch/src/region/dynamic_channel_plans/eu433.rs @@ -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; 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; 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 +]; diff --git a/lorawan-device-patch/src/region/dynamic_channel_plans/eu868.rs b/lorawan-device-patch/src/region/dynamic_channel_plans/eu868.rs new file mode 100644 index 0000000..bc7a5ee --- /dev/null +++ b/lorawan-device-patch/src/region/dynamic_channel_plans/eu868.rs @@ -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; 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; 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 +]; diff --git a/lorawan-device-patch/src/region/dynamic_channel_plans/in865.rs b/lorawan-device-patch/src/region/dynamic_channel_plans/in865.rs new file mode 100644 index 0000000..73e26d4 --- /dev/null +++ b/lorawan-device-patch/src/region/dynamic_channel_plans/in865.rs @@ -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; 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; 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 +]; diff --git a/lorawan-device-patch/src/region/dynamic_channel_plans/mod.rs b/lorawan-device-patch/src/region/dynamic_channel_plans/mod.rs new file mode 100644 index 0000000..3552dce --- /dev/null +++ b/lorawan-device-patch/src/region/dynamic_channel_plans/mod.rs @@ -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, +> { + additional_channels: [Option; 5], + channel_mask: ChannelMask<9>, + last_tx_channel: u8, + _fixed_channel_region: PhantomData, + rx1_offset: usize, + rx2_dr: usize, +} + +impl< + const NUM_JOIN_CHANNELS: usize, + const NUM_DATARATES: usize, + R: DynamicChannelRegion, +> DynamicChannelPlan +{ + fn get_channel(&self, channel: usize) -> Option { + 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(&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: +ChannelRegion +{ + fn join_channels() -> [u32; NUM_JOIN_CHANNELS]; + fn get_default_rx2() -> u32; +} + +impl< + const NUM_JOIN_CHANNELS: usize, + const NUM_DATARATES: usize, + R: DynamicChannelRegion, +> RegionHandler for DynamicChannelPlan +{ + fn process_join_accept, C>( + &mut self, + join_accept: &DecryptedJoinAcceptPayload, + ) { + 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( + &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() + } +} diff --git a/lorawan-device-patch/src/region/fixed_channel_plans/au915/datarates.rs b/lorawan-device-patch/src/region/fixed_channel_plans/au915/datarates.rs new file mode 100644 index 0000000..fd58b1b --- /dev/null +++ b/lorawan-device-patch/src/region/fixed_channel_plans/au915/datarates.rs @@ -0,0 +1,85 @@ +use super::{Bandwidth, Datarate, SpreadingFactor}; + +pub(crate) const DATARATES: [Option; 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 +]; diff --git a/lorawan-device-patch/src/region/fixed_channel_plans/au915/frequencies.rs b/lorawan-device-patch/src/region/fixed_channel_plans/au915/frequencies.rs new file mode 100644 index 0000000..7a5a020 --- /dev/null +++ b/lorawan-device-patch/src/region/fixed_channel_plans/au915/frequencies.rs @@ -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, +]; diff --git a/lorawan-device-patch/src/region/fixed_channel_plans/au915/mod.rs b/lorawan-device-patch/src/region/fixed_channel_plans/au915/mod.rs new file mode 100644 index 0000000..16cbe8a --- /dev/null +++ b/lorawan-device-patch/src/region/fixed_channel_plans/au915/mod.rs @@ -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`]. +/// +/// # 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; 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 + } +} diff --git a/lorawan-device-patch/src/region/fixed_channel_plans/join_channels.rs b/lorawan-device-patch/src/region/fixed_channel_plans/join_channels.rs new file mode 100644 index 0000000..d0ee5e1 --- /dev/null +++ b/lorawan-device-patch/src/region/fixed_channel_plans/join_channels.rs @@ -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, + /// 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 { + 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, +} + +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::( + &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::(&mut buf, &mut downlinks); + if let Response::JoinSuccess = response {} else { + panic!("Did not receive join success"); + } + let (tx_config, _len) = mac + .send::( + &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::( + &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::(&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::( + &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(); + } + } +} diff --git a/lorawan-device-patch/src/region/fixed_channel_plans/mod.rs b/lorawan-device-patch/src/region/fixed_channel_plans/mod.rs new file mode 100644 index 0000000..7fb0f1a --- /dev/null +++ b/lorawan-device-patch/src/region/fixed_channel_plans/mod.rs @@ -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 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> { + last_tx_channel: u8, + channel_mask: ChannelMask<9>, + _fixed_channel_region: PhantomData, + join_channels: JoinChannels, +} + +impl> FixedChannelPlan { + 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: ChannelRegion { + 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> RegionHandler for FixedChannelPlan { + fn process_join_accept, C>( + &mut self, + join_accept: &DecryptedJoinAcceptPayload, + ) { + 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( + &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) + } +} diff --git a/lorawan-device-patch/src/region/fixed_channel_plans/us915/datarates.rs b/lorawan-device-patch/src/region/fixed_channel_plans/us915/datarates.rs new file mode 100644 index 0000000..1a0453c --- /dev/null +++ b/lorawan-device-patch/src/region/fixed_channel_plans/us915/datarates.rs @@ -0,0 +1,73 @@ +use super::{Bandwidth, Datarate, SpreadingFactor}; + +pub(crate) const DATARATES: [Option; 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, + }), +]; diff --git a/lorawan-device-patch/src/region/fixed_channel_plans/us915/frequencies.rs b/lorawan-device-patch/src/region/fixed_channel_plans/us915/frequencies.rs new file mode 100644 index 0000000..6b1a681 --- /dev/null +++ b/lorawan-device-patch/src/region/fixed_channel_plans/us915/frequencies.rs @@ -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, +]; diff --git a/lorawan-device-patch/src/region/fixed_channel_plans/us915/mod.rs b/lorawan-device-patch/src/region/fixed_channel_plans/us915/mod.rs new file mode 100644 index 0000000..17e4701 --- /dev/null +++ b/lorawan-device-patch/src/region/fixed_channel_plans/us915/mod.rs @@ -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`]. +/// +/// # 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; 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 + } +} diff --git a/lorawan-device-patch/src/region/mod.rs b/lorawan-device-patch/src/region/mod.rs new file mode 100644 index 0000000..a924caa --- /dev/null +++ b/lorawan-device-patch/src/region/mod.rs @@ -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 { + fn datarates() -> &'static [Option; 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( + &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( + &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, C>( + &mut self, + join_accept: &DecryptedJoinAcceptPayload, + ) { + 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, C>( + &mut self, + join_accept: &DecryptedJoinAcceptPayload, + ); + + 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( + &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 + } +} diff --git a/lorawan-device-patch/src/rng.rs b/lorawan-device-patch/src/rng.rs new file mode 100644 index 0000000..822182a --- /dev/null +++ b/lorawan-device-patch/src/rng.rs @@ -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(()) + } +} diff --git a/lorawan-device-patch/src/test_util.rs b/lorawan-device-patch/src/test_util.rs new file mode 100644 index 0000000..d4b8f6f --- /dev/null +++ b/lorawan-device-patch/src/test_util.rs @@ -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, + #[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 { + let mut data: Vec = 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, RfConfig, &mut [u8]) -> usize; + +lazy_static::lazy_static! { + static ref SESSION: Mutex> = Mutex::new(HashMap::new()); + +} + +/// Handle join request and pack a JoinAccept into RxBuffer +pub fn handle_join_request( + uplink: Option, + _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( + uplink: Option, + _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, + _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, + _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 = + MacCommandIterator::::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( + _uplink: Option, + _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() +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..a2f5ab5 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "esp" diff --git a/src/board.rs b/src/board.rs new file mode 100644 index 0000000..0d487dd --- /dev/null +++ b/src/board.rs @@ -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 { + let timer_group = TimerGroup::new(peripherals.TIMG0); + esp_rtos::start(timer_group.timer0); + + static SPI_BUS: StaticCell>> = 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>>> = 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::()?)); + 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; + } +} \ No newline at end of file diff --git a/src/components/clock.rs b/src/components/clock.rs new file mode 100644 index 0000000..8eb9074 --- /dev/null +++ b/src/components/clock.rs @@ -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>), + #[error("Invalid time")] + InvalidTime, +} + +impl From>> for ClockError { + fn from(value: DS3231Error>) -> Self { + Self::DS2321Error(value) + } +} + +pub(crate) struct Clock<'d> { + driver: DS3231>, +} + +impl<'d> Clock<'d> { + pub fn new(i2c_device: SharedI2c<'d>, address: u8) -> Result { + 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, 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 { + let datetime = self.driver.datetime()?; + Ok(datetime) + } +} \ No newline at end of file diff --git a/src/components/lora.rs b/src/components/lora.rs new file mode 100644 index 0000000..aacb604 --- /dev/null +++ b/src/components/lora.rs @@ -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), + #[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 { + 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 { + self.driver.set_datarate(region::DR::_0); + + let param: u8 = (1 & 0x0F) | 1 << 4; + let datetime: DateTime = 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>) { + 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 for LoraError { + fn from(e: RadioError) -> Self { + Self::LoraRadioError(e) + } +} + +impl From> for LoraError { + fn from(value: LoraDeviceError) -> Self { + Self::LoraDeviceError(value) + } +} +impl defmt::Format for LoraError { + fn format(&self, fmt: Formatter) { + defmt::write!(fmt, "{:?}", self); + } +} \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..78c7dcd --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod lora; +pub(crate) mod clock; +pub(crate) mod rom_storage; \ No newline at end of file diff --git a/src/components/rom_storage.rs b/src/components/rom_storage.rs new file mode 100644 index 0000000..708e342 --- /dev/null +++ b/src/components/rom_storage.rs @@ -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>), + #[error("Out of address space")] + OutOfAddress +} + +impl From>> for StorageError { + fn from(value: eeprom24x::Error>) -> Self { + Self::EEPROMError(value) + } +} + +pub(crate) struct RomStorage<'d> { + driver: Eeprom24x, eeprom24x::page_size::B32, eeprom24x::addr_size::TwoBytes, eeprom24x::unique_serial::No>, + addr_table: BTreeMap +} + +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(&mut self) -> Result { + let id = TypeId::of::(); + 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(&mut self, data: &T) -> Result<(), crate::error::Error> { + defmt::debug!("Writing app data"); + let addr = self.get_or_insert_addr::()?; + 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(&mut self) -> Result { + let addr = self.get_or_insert_addr::()?; + 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) + } +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..968a31e --- /dev/null +++ b/src/error.rs @@ -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 for Error { + fn from(e: TrngError) -> Self { + Self::TrngError(e) + } +} + +impl From for Error { + fn from(value: hex::FromHexError) -> Self { + Self::HexDecodeError(value) + } +} + +impl From for Error { + fn from(value: esp_hal::ledc::timer::Error) -> Self { + Self::LEDCTimerError(value) + } +} + +impl From 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); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f16560b --- /dev/null +++ b/src/main.rs @@ -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: +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); +} diff --git a/src/timer.rs b/src/timer.rs new file mode 100644 index 0000000..2ab3ec0 --- /dev/null +++ b/src/timer.rs @@ -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 + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..cc924fb --- /dev/null +++ b/src/types/mod.rs @@ -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, +} \ No newline at end of file