diff --git a/.cargo/config.toml b/.cargo/config.toml index d571e1f..39e0735 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,11 +3,14 @@ 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" +DEFMT_LOG = "info,lora=trace,eeprom24x=debug,pn532=trace" + DEV_EUI = "000000DD44F30FAE" APP_KEY = "00000000000000000000000040000000" APP_EUI = "0000000000000040" +CARD_KEY = "00000000000000000000000000000000" + [build] rustflags = [ "-C", "link-arg=-nostartfiles", diff --git a/.idea/GitLink.xml b/.idea/GitLink.xml new file mode 100644 index 0000000..5143819 --- /dev/null +++ b/.idea/GitLink.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index b69120c..6baf2b5 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -2,7 +2,9 @@ Trng + felica gpst + trng \ No newline at end of file diff --git a/.idea/lora.iml b/.idea/lora.iml index cf84ae4..71d18d2 100644 --- a/.idea/lora.iml +++ b/.idea/lora.iml @@ -2,7 +2,11 @@ + + + + diff --git a/Cargo.toml b/Cargo.toml index 78ba1b7..c53c2b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,46 +15,57 @@ test = false debug = [] [dependencies] -esp-hal = { version = "~1.0", features = ["defmt", "esp32s3", "unstable"] } -esp-rtos = { version = "0.2.0", features = [ +esp-hal = { version = "~1.1", features = ["defmt", "esp32s3", "unstable"] } +esp-rtos = { version = "0.3.0", features = [ "defmt", "embassy", "esp-alloc", "esp32s3", ] } -esp-alloc = { version = "0.9.0", features = ["defmt"] } -esp-backtrace = { version = "0.18.1", features = [ +esp-alloc = { version = "0.10.0", features = ["defmt"] } +esp-backtrace = { version = "0.19.0", 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"] } +esp-println = { version = "0.17.0", features = ["defmt-espflash", "esp32s3"] } +esp-bootloader-esp-idf = { version = "0.5.0", features = ["defmt", "esp32s3"] } -embassy-executor = { version = "0.9.1", features = ["defmt"] } +embassy-executor = { version = "0.10.0", 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"] } +embassy-embedded-hal = { version = "0.6.0", features = ["defmt"] } +embassy-sync = { version = "0.8.0", 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 } +defmt = "1.1.1" +anyhow = { version = "1.0.104", default-features = false } +thiserror = { version = "2.0.20", 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"] } +chrono = { version = "0.4.45", default-features = false, features = ["alloc", "defmt", "serde"] } +hifitime = { version = "4.3.1", 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"] } +serde = { version = "1.0.229", default-features = false, features = ["derive"] } +atomic = "0.6.1" +bytemuck = { version = "1.25.2", features = ["derive", "alloc_uninit", "extern_crate_alloc", "zeroable_atomics", "zeroable_maybe_uninit"] } +pn532 = { path = "pn532", features = ["defmt", "alloc"] } +once_cell = { version = "1.21.4", default-features = false, features = ["alloc"] } +heapless = { version = "0.9.3", features = ["alloc", "defmt", "serde"] } +crossbeam = { version = "0.8.4", default-features = false, features = ["alloc"] } +[build-dependencies] +patch-crate = "0.1.13" + +[package.metadata.patch] +crates = ["lorawan-device"] [patch.crates-io] -lorawan-device = { path = "lorawan-device-patch" } +lorawan-device = { path = "./target/patch/lorawan-device-0.12.2" } [profile.dev] # Rust debug is too slow. @@ -69,3 +80,7 @@ incremental = false lto = 'fat' opt-level = 's' overflow-checks = false + +[workspace] +members = ["pn532"] +resolver = "3" diff --git a/build.rs b/build.rs index 4ec39ea..c54d3c7 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,7 @@ fn main() { + println!("cargo:rerun-if-changed=Cargo.toml"); + patch_crate::run().expect("Failed while patching"); + 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) diff --git a/lorawan-device-patch/.cargo-ok b/lorawan-device-patch/.cargo-ok deleted file mode 100644 index 5f8b795..0000000 --- a/lorawan-device-patch/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index f5c7a1e..0000000 --- a/lorawan-device-patch/.cargo_vcs_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "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 deleted file mode 100644 index 3bfc22c..0000000 --- a/lorawan-device-patch/CHANGELOG.md +++ /dev/null @@ -1,33 +0,0 @@ -# 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 deleted file mode 100644 index 9a8bb49..0000000 --- a/lorawan-device-patch/Cargo.toml +++ /dev/null @@ -1,141 +0,0 @@ -# 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 deleted file mode 100644 index 014ba50..0000000 --- a/lorawan-device-patch/Cargo.toml.orig +++ /dev/null @@ -1,74 +0,0 @@ -[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 deleted file mode 100644 index 5407048..0000000 --- a/lorawan-device-patch/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# 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 deleted file mode 100644 index 09d3dd5..0000000 --- a/lorawan-device-patch/src/async_device/embassy_time.rs +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index aac0e75..0000000 --- a/lorawan-device-patch/src/async_device/mod.rs +++ /dev/null @@ -1,487 +0,0 @@ -//! 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 deleted file mode 100644 index 945c8a1..0000000 --- a/lorawan-device-patch/src/async_device/radio.rs +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index e90ba2d..0000000 --- a/lorawan-device-patch/src/async_device/test/mod.rs +++ /dev/null @@ -1,329 +0,0 @@ -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 deleted file mode 100644 index 827a408..0000000 --- a/lorawan-device-patch/src/async_device/test/radio.rs +++ /dev/null @@ -1,111 +0,0 @@ -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 deleted file mode 100644 index 1c450a2..0000000 --- a/lorawan-device-patch/src/async_device/test/timer.rs +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index c758271..0000000 --- a/lorawan-device-patch/src/async_device/test/util.rs +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index 539a0ed..0000000 --- a/lorawan-device-patch/src/lib.rs +++ /dev/null @@ -1,77 +0,0 @@ -#![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 deleted file mode 100644 index 3fafd3e..0000000 --- a/lorawan-device-patch/src/log.rs +++ /dev/null @@ -1,35 +0,0 @@ -#![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 deleted file mode 100644 index 00da5b4..0000000 --- a/lorawan-device-patch/src/mac/mod.rs +++ /dev/null @@ -1,368 +0,0 @@ -//! 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 deleted file mode 100644 index 730fd18..0000000 --- a/lorawan-device-patch/src/mac/otaa.rs +++ /dev/null @@ -1,92 +0,0 @@ -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 deleted file mode 100644 index d946a1f..0000000 --- a/lorawan-device-patch/src/mac/session.rs +++ /dev/null @@ -1,222 +0,0 @@ -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 deleted file mode 100644 index 0b4cb3c..0000000 --- a/lorawan-device-patch/src/mac/uplink.rs +++ /dev/null @@ -1,88 +0,0 @@ -/* -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 deleted file mode 100644 index 8e58574..0000000 --- a/lorawan-device-patch/src/nb_device/mod.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! 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 deleted file mode 100644 index d7c9c8f..0000000 --- a/lorawan-device-patch/src/nb_device/radio.rs +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 92ff26a..0000000 --- a/lorawan-device-patch/src/nb_device/state.rs +++ /dev/null @@ -1,411 +0,0 @@ -/* - -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 deleted file mode 100644 index 0430947..0000000 --- a/lorawan-device-patch/src/nb_device/test/mod.rs +++ /dev/null @@ -1,128 +0,0 @@ -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 deleted file mode 100644 index ab676be..0000000 --- a/lorawan-device-patch/src/nb_device/test/util.rs +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index b3637d4..0000000 --- a/lorawan-device-patch/src/radio.rs +++ /dev/null @@ -1,112 +0,0 @@ -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 deleted file mode 100644 index 4560b11..0000000 --- a/lorawan-device-patch/src/region/constants.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![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 deleted file mode 100644 index 9fb571d..0000000 --- a/lorawan-device-patch/src/region/dynamic_channel_plans/as923.rs +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index 413aa4d..0000000 --- a/lorawan-device-patch/src/region/dynamic_channel_plans/eu433.rs +++ /dev/null @@ -1,74 +0,0 @@ -#![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 deleted file mode 100644 index bc7a5ee..0000000 --- a/lorawan-device-patch/src/region/dynamic_channel_plans/eu868.rs +++ /dev/null @@ -1,74 +0,0 @@ -#![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 deleted file mode 100644 index 73e26d4..0000000 --- a/lorawan-device-patch/src/region/dynamic_channel_plans/in865.rs +++ /dev/null @@ -1,68 +0,0 @@ -#![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 deleted file mode 100644 index 3552dce..0000000 --- a/lorawan-device-patch/src/region/dynamic_channel_plans/mod.rs +++ /dev/null @@ -1,220 +0,0 @@ -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 deleted file mode 100644 index fd58b1b..0000000 --- a/lorawan-device-patch/src/region/fixed_channel_plans/au915/datarates.rs +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index 7a5a020..0000000 --- a/lorawan-device-patch/src/region/fixed_channel_plans/au915/frequencies.rs +++ /dev/null @@ -1,94 +0,0 @@ -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 deleted file mode 100644 index 16cbe8a..0000000 --- a/lorawan-device-patch/src/region/fixed_channel_plans/au915/mod.rs +++ /dev/null @@ -1,84 +0,0 @@ -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 deleted file mode 100644 index d0ee5e1..0000000 --- a/lorawan-device-patch/src/region/fixed_channel_plans/join_channels.rs +++ /dev/null @@ -1,418 +0,0 @@ -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 deleted file mode 100644 index 7fb0f1a..0000000 --- a/lorawan-device-patch/src/region/fixed_channel_plans/mod.rs +++ /dev/null @@ -1,205 +0,0 @@ -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 deleted file mode 100644 index 1a0453c..0000000 --- a/lorawan-device-patch/src/region/fixed_channel_plans/us915/datarates.rs +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index 6b1a681..0000000 --- a/lorawan-device-patch/src/region/fixed_channel_plans/us915/frequencies.rs +++ /dev/null @@ -1,94 +0,0 @@ -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 deleted file mode 100644 index 17e4701..0000000 --- a/lorawan-device-patch/src/region/fixed_channel_plans/us915/mod.rs +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index a924caa..0000000 --- a/lorawan-device-patch/src/region/mod.rs +++ /dev/null @@ -1,528 +0,0 @@ -//! 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 deleted file mode 100644 index 822182a..0000000 --- a/lorawan-device-patch/src/rng.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! 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 deleted file mode 100644 index d4b8f6f..0000000 --- a/lorawan-device-patch/src/test_util.rs +++ /dev/null @@ -1,272 +0,0 @@ -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/patches/lorawan-device+0.12.2.patch b/patches/lorawan-device+0.12.2.patch new file mode 100644 index 0000000..317fcc6 --- /dev/null +++ b/patches/lorawan-device+0.12.2.patch @@ -0,0 +1,217 @@ +diff --git a/src/async_device/mod.rs b/src/async_device/mod.rs +index 362c3b4..aac0e75 100644 +--- a/src/async_device/mod.rs ++++ b/src/async_device/mod.rs +@@ -305,7 +305,7 @@ where + } + + #[allow(unused)] +- enum RxcWindowResponse + Sized + Unpin> { ++ enum RxcWindowResponse + Sized + Unpin> { + Rx(usize, RxQuality, F), + Timeout(u32), + } +@@ -318,7 +318,7 @@ where + timeout_fut: F, + ) -> RxcWindowResponse + where +- F: futures::Future + Sized + Unpin, ++ F: futures::Future + Sized + Unpin, + R: radio::PhyRxTx + Timings, + { + let rx_fut = radio.rx_continuous(rx_buf.as_mut()); +@@ -356,7 +356,7 @@ where + duration, + timeout_fut, + ) +- .await ++ .await + { + RxcWindowResponse::Rx(sz, _, timeout_fut) => { + log::debug!("RXC window received {} bytes.", sz); +diff --git a/src/async_device/test/mod.rs b/src/async_device/test/mod.rs +index 4d8c9b4..e90ba2d 100644 +--- a/src/async_device/test/mod.rs ++++ b/src/async_device/test/mod.rs +@@ -18,7 +18,7 @@ mod util; + use util::{setup, setup_with_session, setup_with_session_class_c}; + + type Device = +- crate::async_device::Device; ++crate::async_device::Device; + + #[tokio::test] + async fn test_join_rx1() { +diff --git a/src/async_device/test/radio.rs b/src/async_device/test/radio.rs +index e0be1d3..827a408 100644 +--- a/src/async_device/test/radio.rs ++++ b/src/async_device/test/radio.rs +@@ -1,4 +1,3 @@ +-use super::*; + use crate::async_device::radio::{PhyRxTx, RxConfig, RxStatus}; + use std::sync::Arc; + use tokio::{ +diff --git a/src/async_device/test/util.rs b/src/async_device/test/util.rs +index d289d32..c758271 100644 +--- a/src/async_device/test/util.rs ++++ b/src/async_device/test/util.rs +@@ -1,4 +1,4 @@ +-use super::{get_dev_addr, get_key, radio::*, region, timer::*, Device, SendResponse}; ++use super::{get_dev_addr, get_key, region, Device, SendResponse}; + + use crate::mac::Session; + use crate::test_util::handle_class_c_uplink_after_join; +diff --git a/src/lib.rs b/src/lib.rs +index ad1ddda..539a0ed 100644 +--- a/src/lib.rs ++++ b/src/lib.rs +@@ -48,7 +48,7 @@ pub struct Downlink { + #[cfg(feature = "defmt")] + impl defmt::Format for Downlink { + fn format(&self, f: defmt::Formatter) { +- defmt::write!(f, "Downlink {{ fport: {}, data: ", self.fport,); ++ defmt::write!(f, "Downlink {{ fport: {}, data: ", self.fport, ); + + for byte in self.data.iter() { + defmt::write!(f, "{:02x}", byte); +diff --git a/src/mac/session.rs b/src/mac/session.rs +index 6ff62fa..d946a1f 100644 +--- a/src/mac/session.rs ++++ b/src/mac/session.rs +@@ -60,7 +60,7 @@ impl Session { + decrypt.dev_addr().as_ref()[2], + decrypt.dev_addr().as_ref()[3], + ]) +- .unwrap(), ++ .unwrap(), + ) + } + +diff --git a/src/nb_device/test/mod.rs b/src/nb_device/test/mod.rs +index 51cd67f..0430947 100644 +--- a/src/nb_device/test/mod.rs ++++ b/src/nb_device/test/mod.rs +@@ -1,7 +1,5 @@ + use super::*; + mod util; +-use crate::test_util::*; +-use util::*; + + use crate::nb_device::Event; + #[test] +diff --git a/src/nb_device/test/util.rs b/src/nb_device/test/util.rs +index 6d1e12c..ab676be 100644 +--- a/src/nb_device/test/util.rs ++++ b/src/nb_device/test/util.rs +@@ -1,11 +1,9 @@ +-use super::*; + use crate::radio::{RfConfig, RxQuality}; + + use crate::nb_device::{ + radio::{Event, PhyRxTx, Response}, + Device, Timings, + }; +-use crate::test_util::*; + use lorawan::default_crypto; + use region::{Configuration, Region}; + +diff --git a/src/region/dynamic_channel_plans/as923.rs b/src/region/dynamic_channel_plans/as923.rs +index 4a48b67..9fb571d 100644 +--- a/src/region/dynamic_channel_plans/as923.rs ++++ b/src/region/dynamic_channel_plans/as923.rs +@@ -12,7 +12,7 @@ pub(crate) type AS923_4 = DynamicChannelPlan<2, 7, AS923Region<917_300_000, 5900 + pub struct AS923Region; + + impl ChannelRegion<7> +- for AS923Region ++for AS923Region + { + fn datarates() -> &'static [Option; 7] { + &DATARATES +@@ -20,7 +20,7 @@ impl ChannelRegion<7> + } + + impl DynamicChannelRegion<2, 7> +- for AS923Region ++for AS923Region + { + fn join_channels() -> [u32; 2] { + [JOIN_CHANNELS[0] + OFFSET, JOIN_CHANNELS[1] + OFFSET] +diff --git a/src/region/dynamic_channel_plans/mod.rs b/src/region/dynamic_channel_plans/mod.rs +index 00f9a5d..3552dce 100644 +--- a/src/region/dynamic_channel_plans/mod.rs ++++ b/src/region/dynamic_channel_plans/mod.rs +@@ -46,16 +46,21 @@ pub(crate) struct DynamicChannelPlan< + } + + impl< +- const NUM_JOIN_CHANNELS: usize, +- const NUM_DATARATES: usize, +- R: DynamicChannelRegion, +- > DynamicChannelPlan ++ 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 { +- self.additional_channels[channel - NUM_JOIN_CHANNELS] ++ let index = channel - NUM_JOIN_CHANNELS; ++ if index < self.additional_channels.len() { ++ self.additional_channels[index] ++ } else { ++ None ++ } + } + } + +@@ -87,17 +92,17 @@ impl< + } + + pub(crate) trait DynamicChannelRegion: +- ChannelRegion ++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 ++ const NUM_JOIN_CHANNELS: usize, ++ const NUM_DATARATES: usize, ++ R: DynamicChannelRegion, ++> RegionHandler for DynamicChannelPlan + { + fn process_join_accept, C>( + &mut self, +diff --git a/src/region/fixed_channel_plans/join_channels.rs b/src/region/fixed_channel_plans/join_channels.rs +index 297cce3..d0ee5e1 100644 +--- a/src/region/fixed_channel_plans/join_channels.rs ++++ b/src/region/fixed_channel_plans/join_channels.rs +@@ -330,8 +330,7 @@ mod test { + 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 { ++ if let Response::JoinSuccess = response {} else { + panic!("Did not receive join success"); + } + let (tx_config, _len) = mac +@@ -391,8 +390,7 @@ mod test { + 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 { ++ if let Response::JoinSuccess = response {} else { + panic!("Did not receive JoinSuccess") + } + for _ in 0..8 { diff --git a/pn532/Cargo.toml b/pn532/Cargo.toml new file mode 100644 index 0000000..fa44c89 --- /dev/null +++ b/pn532/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "pn532" +version = "0.1.0" +edition = "2024" +description = "A no_std, embedded-hal driver for the NXP PN532 NFC controller (I2C interface)" +license = "BSD-3-Clause" +keywords = ["nfc", "pn532", "embedded-hal", "no-std", "i2c"] +categories = ["embedded", "no-std", "hardware-support"] + +[lib] +name = "pn532" +path = "src/lib.rs" +test = false +bench = false +doctest = false + +[dependencies] +embedded-hal = "~1" +defmt = { version = "~1.1", optional = true } +hex = { version = "~0.4", default-features = false } +des = { version = "~0.9", default-features = false } +cbc = { version = "~0.2", default-features = false, features = ["block-padding"] } +strum = { version = "0.28.0", default-features = false, features = ["derive", "strum_macros"] } +heapless = "~0.9" +thiserror = { version = "2.0.20", default-features = false } + +[features] +default = [] +defmt = ["dep:defmt", "heapless/defmt"] +alloc = ["defmt/alloc", "hex/alloc", "heapless/alloc"] diff --git a/pn532/src/commands.rs b/pn532/src/commands.rs new file mode 100644 index 0000000..6551524 --- /dev/null +++ b/pn532/src/commands.rs @@ -0,0 +1,146 @@ +//! Command and protocol constants for the PN532. + +// PN532 commands. +pub const PN532_COMMAND_DIAGNOSE: u8 = 0x00; +pub const PN532_COMMAND_GETFIRMWAREVERSION: u8 = 0x02; +pub const PN532_COMMAND_GETGENERALSTATUS: u8 = 0x04; +pub const PN532_COMMAND_READREGISTER: u8 = 0x06; +pub const PN532_COMMAND_WRITEREGISTER: u8 = 0x08; +pub const PN532_COMMAND_READGPIO: u8 = 0x0C; +pub const PN532_COMMAND_WRITEGPIO: u8 = 0x0E; +pub const PN532_COMMAND_SETSERIALBAUDRATE: u8 = 0x10; +pub const PN532_COMMAND_SETPARAMETERS: u8 = 0x12; +pub const PN532_COMMAND_SAMCONFIGURATION: u8 = 0x14; +pub const PN532_COMMAND_POWERDOWN: u8 = 0x16; +pub const PN532_COMMAND_RFCONFIGURATION: u8 = 0x32; +pub const PN532_COMMAND_RFREGULATIONTEST: u8 = 0x58; +pub const PN532_COMMAND_INJUMPFORDEP: u8 = 0x56; +pub const PN532_COMMAND_INJUMPFORPSL: u8 = 0x46; +pub const PN532_COMMAND_INLISTPASSIVETARGET: u8 = 0x4A; +pub const PN532_COMMAND_INATR: u8 = 0x50; +pub const PN532_COMMAND_INPSL: u8 = 0x4E; +pub const PN532_COMMAND_INDATAEXCHANGE: u8 = 0x40; +pub const PN532_COMMAND_INCOMMUNICATETHRU: u8 = 0x42; +pub const PN532_COMMAND_INDESELECT: u8 = 0x44; +pub const PN532_COMMAND_INRELEASE: u8 = 0x52; +pub const PN532_COMMAND_INSELECT: u8 = 0x54; +pub const PN532_COMMAND_INAUTOPOLL: u8 = 0x60; +pub const PN532_COMMAND_TGINITASTARGET: u8 = 0x8C; +pub const PN532_COMMAND_TGSETGENERALBYTES: u8 = 0x92; +pub const PN532_COMMAND_TGGETDATA: u8 = 0x86; +pub const PN532_COMMAND_TGSETDATA: u8 = 0x8E; +pub const PN532_COMMAND_TGSETMETADATA: u8 = 0x94; +pub const PN532_COMMAND_TGGETINITIATORCOMMAND: u8 = 0x88; +pub const PN532_COMMAND_TGRESPONSETOINITIATOR: u8 = 0x90; +pub const PN532_COMMAND_TGGETTARGETSTATUS: u8 = 0x8A; + +pub const PN532_RESPONSE_INDATAEXCHANGE: u8 = 0x41; +pub const PN532_RESPONSE_INLISTPASSIVETARGET: u8 = 0x4B; + +pub const PN532_MIFARE_ISO14443A: u8 = 0x00; + +// Mifare commands. +pub const MIFARE_CMD_AUTH_A: u8 = 0x60; +pub const MIFARE_CMD_AUTH_B: u8 = 0x61; +pub const MIFARE_CMD_READ: u8 = 0x30; +pub const MIFARE_CMD_WRITE: u8 = 0xA0; +pub const MIFARE_CMD_WRITE_ULTRALIGHT: u8 = 0xA2; +pub const MIFARE_CMD_TRANSFER: u8 = 0xB0; +pub const MIFARE_CMD_DECREMENT: u8 = 0xC0; +pub const MIFARE_CMD_INCREMENT: u8 = 0xC1; +pub const MIFARE_CMD_STORE: u8 = 0xC2; + +// FeliCa commands. +pub const FELICA_CMD_POLLING: u8 = 0x00; +pub const FELICA_CMD_REQUEST_SERVICE: u8 = 0x02; +pub const FELICA_CMD_REQUEST_RESPONSE: u8 = 0x04; +pub const FELICA_CMD_READ_WITHOUT_ENCRYPTION: u8 = 0x06; +pub const FELICA_CMD_WRITE_WITHOUT_ENCRYPTION: u8 = 0x08; +pub const FELICA_CMD_REQUEST_SYSTEM_CODE: u8 = 0x0C; + +/// NDEF URI record identifier codes (see NFC Forum "URI Record Type Definition"). +pub mod ndef { + pub const NONE: u8 = 0x00; + pub const HTTP_WWWDOT: u8 = 0x01; + pub const HTTPS_WWWDOT: u8 = 0x02; + pub const HTTP: u8 = 0x03; + pub const HTTPS: u8 = 0x04; + pub const TEL: u8 = 0x05; + pub const MAILTO: u8 = 0x06; + pub const FTP_ANONAT: u8 = 0x07; + pub const FTP_FTPDOT: u8 = 0x08; + pub const FTPS: u8 = 0x09; + pub const SFTP: u8 = 0x0A; + pub const SMB: u8 = 0x0B; + pub const NFS: u8 = 0x0C; + pub const FTP: u8 = 0x0D; + pub const DAV: u8 = 0x0E; + pub const NEWS: u8 = 0x0F; + pub const TELNET: u8 = 0x10; + pub const IMAP: u8 = 0x11; + pub const RTSP: u8 = 0x12; + pub const URN: u8 = 0x13; + pub const POP: u8 = 0x14; + pub const SIP: u8 = 0x15; + pub const SIPS: u8 = 0x16; + pub const TFTP: u8 = 0x17; + pub const BTSPP: u8 = 0x18; + pub const BTL2CAP: u8 = 0x19; + pub const BTGOEP: u8 = 0x1A; + pub const TCPOBEX: u8 = 0x1B; + pub const IRDAOBEX: u8 = 0x1C; + pub const FILE: u8 = 0x1D; + pub const URN_EPC_ID: u8 = 0x1E; + pub const URN_EPC_TAG: u8 = 0x1F; + pub const URN_EPC_PAT: u8 = 0x20; + pub const URN_EPC_RAW: u8 = 0x21; + pub const URN_EPC: u8 = 0x22; + pub const URN_NFC: u8 = 0x23; +} + +// GPIO. +pub const PN532_GPIO_VALIDATIONBIT: u8 = 0x80; +pub const PN532_GPIO_P30: u8 = 0; +pub const PN532_GPIO_P31: u8 = 1; +pub const PN532_GPIO_P32: u8 = 2; +pub const PN532_GPIO_P33: u8 = 3; +pub const PN532_GPIO_P34: u8 = 4; +pub const PN532_GPIO_P35: u8 = 5; + +// FeliCa limits. +pub const FELICA_READ_MAX_SERVICE_NUM: usize = 16; +pub const FELICA_READ_MAX_BLOCK_NUM: usize = 12; +pub const FELICA_WRITE_MAX_SERVICE_NUM: usize = 16; +pub const FELICA_WRITE_MAX_BLOCK_NUM: usize = 10; +pub const FELICA_REQ_SERVICE_MAX_NODE_NUM: usize = 32; + +// Frame protocol constants. +pub const PN532_PREAMBLE: u8 = 0x00; +pub const PN532_STARTCODE1: u8 = 0x00; +pub const PN532_STARTCODE2: u8 = 0xFF; +pub const PN532_POSTAMBLE: u8 = 0x00; + +pub const PN532_HOST_TO_PN532: u8 = 0xD4; +pub const PN532_PN532_TO_HOST: u8 = 0xD5; + +pub const PN532_ACK_WAIT_TIME_MS: u16 = 10; + +/// The fixed 7-bit I2C address of the PN532 (0x48 >> 1). +pub const PN532_I2C_ADDRESS: u8 = 0x48 >> 1; + +/// Passive target baud rates supported by [`crate::Pn532::read_passive_target_id`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum BaudRate { + ISO14443A106kbps = 0x00, + Felica212kbps = 0x01, + Felica424kbps = 0x02, + ISO14443B106kbps = 0x03, + InnovisionJewel106kbps = 0x04, +} + +impl From for u8 { + fn from(value: BaudRate) -> Self { + value as u8 + } +} diff --git a/pn532/src/driver.rs b/pn532/src/driver.rs new file mode 100644 index 0000000..f9ec761 --- /dev/null +++ b/pn532/src/driver.rs @@ -0,0 +1,213 @@ +//! High-level PN532 command driver. + +use crate::commands::*; +use crate::error::{Error}; +use crate::interface::Interface; + +pub(crate) const PACKET_BUFFER_SIZE: usize = 64; + +/// A card UID read by [`Pn532::read_passive_target_id`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Uid { + pub bytes: heapless::Vec, +} + +impl core::fmt::Display for Uid { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "alloc")] + return write!(f, "{}", hex::encode(&self.bytes)); + #[cfg(not(feature = "alloc"))] + { + let mut buf: heapless::Vec = heapless::Vec::new(); + hex::encode_to_slice(&self.bytes, &mut buf) + .map_err(|_| core::fmt::Error)?; + let s = heapless::String::from_utf8(buf) + .map_err(|_| core::fmt::Error)?; + write!(f, "{}", s) + } + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Uid { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{}", defmt::Display2Format(self)); + } +} + +/// Outcome of [`Pn532::tg_init_as_target`] / [`Pn532::tg_init_as_target_default`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TargetInitStatus { + /// The PN532 entered target mode. + Success, + /// No initiator appeared within the timeout. + Timeout, + /// The operation failed. + Failed, +} + +/// Result of a successful FeliCa polling request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FelicaPollingResponse { + /// The card's IDm (NFCID2). + pub idm: [u8; 8], + /// The card's PMm (PAD). + pub pmm: [u8; 8], + /// The card's system code, when returned. + pub system_code_response: Option, +} + +/// Driver for the NXP PN532 NFC controller. +pub struct Pn532 { + pub(crate) interface: B, + pub(crate) in_listed_tag: u8, + pub(crate) felica_idm: [u8; 8], + pub(crate) felica_pmm: [u8; 8], + pub(crate) buffer: [u8; PACKET_BUFFER_SIZE], +} + +impl Pn532 { + /// Create a driver over the given transport interface. + pub fn new(interface: B) -> Self { + Self { + interface, + in_listed_tag: 0, + felica_idm: [0; 8], + felica_pmm: [0; 8], + buffer: [0; PACKET_BUFFER_SIZE], + } + } + + /// Initialise the PN532: pulse the reset pin (if provided) and wait for it + /// to become ready. + pub fn begin(&mut self) -> Result<(), Error> { + self.interface.begin() + } + + // -- internal helpers --------------------------------------------------- + + pub(crate) fn send(&mut self, header_len: usize) -> Result<(), Error> { + self.interface.write_command(&self.buffer[..header_len], &[]) + } + + pub(crate) fn send_with_body( + &mut self, + header_len: usize, + body: &[u8], + ) -> Result<(), Error> { + self.interface + .write_command(&self.buffer[..header_len], body) + } + + pub(crate) fn read(&mut self) -> Result> { + self.interface.read_response(&mut self.buffer, 1000) + } + + pub(crate) fn read_timeout(&mut self, timeout_ms: u16) -> Result> { + self.interface.read_response(&mut self.buffer, timeout_ms) + } + + // -- generic PN532 functions ------------------------------------------- + + /// Read the PN532 firmware version and ID. + pub fn get_firmware_version(&mut self) -> Result> { + self.buffer[0] = PN532_COMMAND_GETFIRMWAREVERSION; + self.send(1)?; + let len = self.read()?; + if len < 4 { + return Err(Error::InvalidFrame); + } + Ok(u32::from_be_bytes([ + self.buffer[0], + self.buffer[1], + self.buffer[2], + self.buffer[3], + ])) + } + + /// Read a 16-bit PN532 register. + pub fn read_register(&mut self, reg: u16) -> Result> { + self.buffer[0] = PN532_COMMAND_READREGISTER; + self.buffer[1] = (reg >> 8) as u8; + self.buffer[2] = reg as u8; + self.send(3)?; + let len = self.read()?; + if len < 1 { + return Err(Error::InvalidFrame); + } + Ok(self.buffer[0]) + } + + /// Write to a 16-bit PN532 register. + pub fn write_register(&mut self, reg: u16, val: u8) -> Result<(), Error> { + self.buffer[0] = PN532_COMMAND_WRITEREGISTER; + self.buffer[1] = (reg >> 8) as u8; + self.buffer[2] = reg as u8; + self.buffer[3] = val; + self.send(4)?; + self.read()?; + Ok(()) + } + + /// Set the PN532's GPIO pins (see the PN532 user manual for valid pins). + pub fn write_gpio(&mut self, pinstate: u8) -> Result<(), Error> { + let pinstate = pinstate | (1 << PN532_GPIO_P32) | (1 << PN532_GPIO_P34); + self.buffer[0] = PN532_COMMAND_WRITEGPIO; + self.buffer[1] = PN532_GPIO_VALIDATIONBIT | pinstate; + self.buffer[2] = 0x00; + self.send(3)?; + let len = self.read()?; + if len == 0 { + return Err(Error::InvalidFrame); + } + Ok(()) + } + + /// Read the state of the PN532's GPIO pins. + pub fn read_gpio(&mut self) -> Result> { + self.buffer[0] = PN532_COMMAND_READGPIO; + self.send(1)?; + self.read()?; + Ok(self.buffer[0]) + } + + /// Configure the SAM (Secure Access Module) in normal mode. + pub fn sam_config(&mut self) -> Result<(), Error> { + self.buffer[0] = PN532_COMMAND_SAMCONFIGURATION; + self.buffer[1] = 0x01; // normal mode + self.buffer[2] = 0x14; // timeout 50 ms * 20 = 1 second + self.buffer[3] = 0x01; // use IRQ pin + self.send(4)?; + self.read()?; + Ok(()) + } + + /// Set the `MxRtyPassiveActivation` RF configuration item. + pub fn set_passive_activation_retries( + &mut self, + max_retries: u8, + ) -> Result<(), Error> { + self.buffer[0] = PN532_COMMAND_RFCONFIGURATION; + self.buffer[1] = 5; + self.buffer[2] = 0xFF; + self.buffer[3] = 0x01; + self.buffer[4] = max_retries; + self.send(5)?; + self.read()?; + Ok(()) + } + + /// Switch the RF field on/off. + pub fn set_rf_field( + &mut self, + auto_rfca: u8, + rf_on_off: u8, + ) -> Result<(), Error> { + self.buffer[0] = PN532_COMMAND_RFCONFIGURATION; + self.buffer[1] = 1; + self.buffer[2] = 0x00 | auto_rfca | rf_on_off; + self.send(3)?; + self.read()?; + Ok(()) + } +} diff --git a/pn532/src/error.rs b/pn532/src/error.rs new file mode 100644 index 0000000..d4ad258 --- /dev/null +++ b/pn532/src/error.rs @@ -0,0 +1,81 @@ +use defmt::Debug2Format; +use strum::FromRepr; + +/// Driver-level error type, generic over the transport (I2C) error. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum Error { + /// The underlying transport (I2C) reported an error. + #[error("transport error: {0:?}")] + Transport(E), + /// Timed out waiting for a response or acknowledgement. + #[error("Timed out waiting for a response or acknowledgement")] + Timeout, + /// The acknowledgement frame was malformed. + #[error("The acknowledgement frame was malformed")] + InvalidAck, + /// The response frame was malformed (bad preamble, length or checksum). + #[error("The response frame was malformed")] + InvalidFrame, + /// The response did not fit in the provided buffer. + #[error("The response did not fit in the provided buffer")] + NoSpace, + /// A parameter passed to a command was out of range. + #[error("A parameter passed to a command was out of range")] + InvalidParam, + /// The device returned a non-zero status code. + #[error("Received non zero status code: {0:?}")] + Status(StatusCode), +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Error { + fn format(&self, fmt: defmt::Formatter) { + match self { + Error::Transport(e) => defmt::write!(fmt, "transport error: {}", e), + Error::Timeout => defmt::write!(fmt, "timeout"), + Error::InvalidAck => defmt::write!(fmt, "invalid ACK frame"), + Error::InvalidFrame => defmt::write!(fmt, "invalid frame"), + Error::NoSpace => defmt::write!(fmt, "not enough space in buffer"), + Error::InvalidParam => defmt::write!(fmt, "invalid parameter"), + Error::Status(code) => defmt::write!(fmt, "status error {:?}", code), + } + } +} + +#[repr(u8)] +#[derive(FromRepr, Debug, Clone, Copy, PartialEq, Eq)] +pub enum StatusCode { + TimeOut = 0x01, + CRC = 0x02, + Parity = 0x03, + BitCount = 0x04, + Framing = 0x05, + Collision = 0x06, + Space = 0x07, + Overflow = 0x09, + NoField = 0x0A, + Protocol = 0x0B, + Temperature = 0x0D, + IntOverflow = 0x0E, + Parameter = 0x10, + DepProtocol = 0x12, + DepFormat = 0x13, + MifareAuth = 0x14, + UIDCheck = 0x23, + DepState = 0x25, + NotAllowed = 0x26, + NotAcceptable = 0x27, + Released = 0x29, + CardSwapped = 0x2A, + Disappeared = 0x2B, + MismatchUd = 0x2C, + OverCurrent = 0x2D, + NADMissing = 0x2E, + Max = 0x2F +} + +impl defmt::Format for StatusCode { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{}", Debug2Format(self)) + } +} diff --git a/pn532/src/felica/mod.rs b/pn532/src/felica/mod.rs new file mode 100644 index 0000000..712c101 --- /dev/null +++ b/pn532/src/felica/mod.rs @@ -0,0 +1,306 @@ +mod types; + +use crate::driver::Pn532; +use crate::error::{Error, StatusCode}; +use crate::interface::Interface; + +use types::*; +pub use types::Block as FelicaBlock; +pub(crate) use types::AsBlock; +use crate::commands::{FELICA_CMD_POLLING, FELICA_CMD_READ_WITHOUT_ENCRYPTION, FELICA_CMD_REQUEST_RESPONSE, FELICA_CMD_REQUEST_SERVICE, FELICA_CMD_REQUEST_SYSTEM_CODE, FELICA_CMD_WRITE_WITHOUT_ENCRYPTION, FELICA_READ_MAX_BLOCK_NUM, FELICA_READ_MAX_SERVICE_NUM, FELICA_REQ_SERVICE_MAX_NODE_NUM, FELICA_WRITE_MAX_BLOCK_NUM, FELICA_WRITE_MAX_SERVICE_NUM, PN532_COMMAND_INDATAEXCHANGE, PN532_COMMAND_INLISTPASSIVETARGET, PN532_COMMAND_INRELEASE}; +use crate::{BaudRate, FelicaPollingResponse}; + +impl Pn532 { + /// Poll for a FeliCa card. + pub fn felica_polling( + &mut self, + system_code: u16, + request_code: u8, + timeout_ms: u16, + ) -> Result, Error> { + self.buffer[0] = PN532_COMMAND_INLISTPASSIVETARGET; + self.buffer[1] = 1; + self.buffer[2] = BaudRate::Felica212kbps.into(); + self.buffer[3] = FELICA_CMD_POLLING; + self.buffer[4] = (system_code >> 8) as u8; + self.buffer[5] = system_code as u8; + self.buffer[6] = request_code; + self.buffer[7] = 0; + self.send(8)?; + + match self.read_timeout(timeout_ms) { + Err(Error::Timeout) => return Ok(None), + Err(e) => return Err(e), + Ok(_) => {} + } + + if self.buffer[0] == 0 { + return Ok(None); + } + if self.buffer[0] != 1 { + return Err(Error::InvalidFrame); + } + + self.in_listed_tag = self.buffer[1]; + let response_length = self.buffer[2]; + if response_length != 18 && response_length != 20 { + return Err(Error::InvalidFrame); + } + + let mut idm = [0u8; 8]; + idm.copy_from_slice(&self.buffer[4..12]); + self.felica_idm = idm; + + let mut pmm = [0u8; 8]; + pmm.copy_from_slice(&self.buffer[12..20]); + self.felica_pmm = pmm; + + let system_code_response = if response_length == 20 { + Some(u16::from_be_bytes([self.buffer[20], self.buffer[21]])) + } else { + None + }; + + Ok(Some(FelicaPollingResponse { + idm, + pmm, + system_code_response, + })) + } + + /// Send a raw FeliCa command to the currently in-listed card. + /// + /// Returns the number of bytes written to `response`. + pub fn felica_send_command( + &mut self, + command: &[u8], + response: &mut [u8], + ) -> Result> { + if command.len() > 0xFE { + return Err(Error::InvalidParam); + } + self.buffer[0] = PN532_COMMAND_INDATAEXCHANGE; + self.buffer[1] = self.in_listed_tag; + self.buffer[2] = (command.len() + 1) as u8; + self.send_with_body(3, command)?; + + let len = self.read_timeout(200)?; + if len == 0 || (self.buffer[0] & 0x3F) != 0 { + return Err(Error::Status(StatusCode::from_repr(self.buffer[0] & 0x3F).unwrap())); + } + + let response_len = self.buffer[1] as usize - 1; + if len - 2 != response_len { + return Err(Error::InvalidFrame); + } + if response_len > response.len() { + return Err(Error::NoSpace); + } + response[..response_len].copy_from_slice(&self.buffer[2..2 + response_len]); + Ok(response_len) + } + + /// Send a FeliCa "Request Service" command. + pub fn felica_request_service( + &mut self, + node_code_list: &[u16], + key_versions: &mut [u16], + ) -> Result<(), Error> { + let num_node = node_code_list.len(); + if num_node > FELICA_REQ_SERVICE_MAX_NODE_NUM || num_node > key_versions.len() { + return Err(Error::InvalidParam); + } + + let mut cmd = [0u8; 1 + 8 + 1 + 2 * FELICA_REQ_SERVICE_MAX_NODE_NUM]; + let mut j = 0; + cmd[j] = FELICA_CMD_REQUEST_SERVICE; + j += 1; + cmd[j..j + 8].copy_from_slice(&self.felica_idm); + j += 8; + cmd[j] = num_node as u8; + j += 1; + for &code in node_code_list { + cmd[j] = code as u8; + cmd[j + 1] = (code >> 8) as u8; + j += 2; + } + + let mut response = [0u8; 10 + 2 * FELICA_REQ_SERVICE_MAX_NODE_NUM]; + let response_len = self.felica_send_command(&cmd[..j], &mut response)?; + if response_len != 10 + 2 * num_node { + return Err(Error::InvalidFrame); + } + for (i, kv) in key_versions.iter_mut().enumerate().take(num_node) { + *kv = u16::from_le_bytes([response[10 + i * 2], response[10 + i * 2 + 1]]); + } + Ok(()) + } + + /// Send a FeliCa "Request Response" command, returning the card's mode. + pub fn felica_request_response(&mut self) -> Result> { + let mut cmd = [0u8; 9]; + cmd[0] = FELICA_CMD_REQUEST_RESPONSE; + cmd[1..9].copy_from_slice(&self.felica_idm); + + let mut response = [0u8; 10]; + let response_len = self.felica_send_command(&cmd, &mut response)?; + if response_len != 10 { + return Err(Error::InvalidFrame); + } + Ok(response[9]) + } + + /// Send a FeliCa "Read Without Encryption" command. + /// + /// Note: the number of blocks is limited by the 64-byte response buffer. + pub fn felica_read_without_encryption( + &mut self, + service_code_list: &[u16], + block_list: &[u16], + block_data: &mut [[u8; 16]], + ) -> Result<(), Error> { + let num_service = service_code_list.len(); + let num_block = block_list.len(); + if num_service > FELICA_READ_MAX_SERVICE_NUM + || num_block > FELICA_READ_MAX_BLOCK_NUM + || num_block > block_data.len() + { + return Err(Error::InvalidParam); + } + + let mut cmd = [0u8; 1 + 8 + 1 + 2 * FELICA_READ_MAX_SERVICE_NUM + 1 + 2 * FELICA_READ_MAX_BLOCK_NUM]; + let mut j = 0; + cmd[j] = FELICA_CMD_READ_WITHOUT_ENCRYPTION; + j += 1; + cmd[j..j + 8].copy_from_slice(&self.felica_idm); + j += 8; + cmd[j] = num_service as u8; + j += 1; + for &sc in service_code_list { + cmd[j] = sc as u8; + cmd[j + 1] = (sc >> 8) as u8; + j += 2; + } + cmd[j] = num_block as u8; + j += 1; + for &bl in block_list { + cmd[j] = (bl >> 8) as u8; + cmd[j + 1] = bl as u8; + j += 2; + } + + let mut response = [0u8; 12 + 16 * FELICA_READ_MAX_BLOCK_NUM]; + let response_len = self.felica_send_command(&cmd[..j], &mut response)?; + if response_len != 12 + 16 * num_block { + return Err(Error::InvalidFrame); + } + if response[9] != 0 || response[10] != 0 { + return Err(Error::Status(StatusCode::from_repr(response[9]).unwrap())); + } + + let mut k = 12; + for block in block_data.iter_mut().take(num_block) { + block.copy_from_slice(&response[k..k + 16]); + k += 16; + } + Ok(()) + } + + /// Send a FeliCa "Write Without Encryption" command. + pub fn felica_write_without_encryption( + &mut self, + service_code_list: &[u16], + block_list: &[impl AsBlock], + block_data: &[[u8; 16]], + ) -> Result<(), Error> { + let num_service = service_code_list.len(); + let num_block = block_list.len(); + if num_service > FELICA_WRITE_MAX_SERVICE_NUM + || num_block > FELICA_WRITE_MAX_BLOCK_NUM + || num_block > block_data.len() + { + return Err(Error::InvalidParam); + } + + let mut cmd = [0u8; 1 + 8 + 1 + 2 * FELICA_WRITE_MAX_SERVICE_NUM + 1 + 2 * FELICA_WRITE_MAX_BLOCK_NUM + + 16 * FELICA_WRITE_MAX_BLOCK_NUM]; + let mut j = 0; + cmd[j] = FELICA_CMD_WRITE_WITHOUT_ENCRYPTION; + j += 1; + cmd[j..j + 8].copy_from_slice(&self.felica_idm); + j += 8; + cmd[j] = num_service as u8; + j += 1; + for &sc in service_code_list { + cmd[j] = sc as u8; + cmd[j + 1] = (sc >> 8) as u8; + j += 2; + } + cmd[j] = num_block as u8; + j += 1; + for block in block_list { + let block = block.as_block(); + cmd[j] = (block >> 8) as u8; + cmd[j + 1] = block as u8; + j += 2; + } + for block in block_data.iter().take(num_block) { + cmd[j..j + 16].copy_from_slice(block); + j += 16; + } + + let mut response = [0u8; 11]; + let response_len = self.felica_send_command(&cmd[..j], &mut response)?; + if response_len != 11 { + return Err(Error::InvalidFrame); + } + if response[9] != 0 || response[10] != 0 { + return Err(Error::Status(StatusCode::from_repr(response[9]).unwrap())); + } + Ok(()) + } + + /// Send a FeliCa "Request System Code" command. + /// + /// Returns the number of system codes written to `system_code_list`. + pub fn felica_request_system_code( + &mut self, + system_code_list: &mut [impl AsServiceCode], + ) -> Result> { + let mut cmd = [0u8; 9]; + cmd[0] = FELICA_CMD_REQUEST_SYSTEM_CODE; + cmd[1..9].copy_from_slice(&self.felica_idm); + + let mut response = [0u8; 10 + 2 * 16]; + let response_len = self.felica_send_command(&cmd, &mut response)?; + if response_len < 10 { + return Err(Error::InvalidFrame); + } + let num_system_code = response[9] as usize; + if response_len < 10 + 2 * num_system_code { + return Err(Error::InvalidFrame); + } + if num_system_code > system_code_list.len() { + return Err(Error::NoSpace); + } + for (i, out) in system_code_list.iter_mut().enumerate().take(num_system_code) { + *out = AsServiceCode::from_service_code( + u16::from_be_bytes([response[10 + i * 2], response[10 + i * 2 + 1]]) + ); + } + Ok(num_system_code) + } + + /// Release the FeliCa card. + pub fn felica_release(&mut self) -> Result<(), Error> { + self.buffer[0] = PN532_COMMAND_INRELEASE; + self.buffer[1] = 0x00; + self.send(2)?; + + let len = self.read_timeout(1000)?; + if len == 0 || (self.buffer[0] & 0x3F) != 0 { + return Err(Error::Status(StatusCode::from_repr(self.buffer[0] & 0x3F).unwrap())); + } + Ok(()) + } +} diff --git a/pn532/src/felica/types.rs b/pn532/src/felica/types.rs new file mode 100644 index 0000000..84934ec --- /dev/null +++ b/pn532/src/felica/types.rs @@ -0,0 +1,74 @@ +use strum::FromRepr; + +pub trait AsServiceCode { + fn as_service_code(&self) -> u16; + fn from_service_code(val: u16) -> Self; +} + +#[derive(FromRepr, Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u16)] +pub enum ServiceCode { + /// FeliCa Lite read service code (read without key). + Read = 0x000B, + /// FeliCa Lite write service code (write without key). + Write = 0x0009 +} + +impl AsServiceCode for ServiceCode { + fn as_service_code(&self) -> u16 { + *self as u16 + } + + fn from_service_code(val: u16) -> Self { + Self::from_repr(val).unwrap() + } +} + +impl AsServiceCode for u16 { + fn as_service_code(&self) -> u16 { + *self + } + + fn from_service_code(val: u16) -> Self { + val + } +} + +pub trait AsBlock { + fn as_block(&self) -> u16; +} + +/// FeliCa Lite system block numbers. +#[derive(FromRepr, Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u16)] +pub enum Block { + /// Random challenge block. + RC = 0x8080, + /// MAC block. + MAC = 0x8081, + /// ID block. + ID = 0x8082, + /// Card key version block. + CKV = 0x8086, + /// Card key block. + CK= 0x8087, + /// Memory configuration block. + MemoryConfig = 0x8088, + /// Write counter block. + WriteCounter = 0x8090, + /// MAC_A block. + MACA = 0x8091 +} + +impl AsBlock for Block { + fn as_block(&self) -> u16 { + *self as u16 + } +} + +impl AsBlock for u16 { + fn as_block(&self) -> u16 { + *self + } +} + diff --git a/pn532/src/interface.rs b/pn532/src/interface.rs new file mode 100644 index 0000000..b357c86 --- /dev/null +++ b/pn532/src/interface.rs @@ -0,0 +1,363 @@ +//! Transport abstraction and the I2C implementation. +//! +//! The PN532 communicates over a framed protocol regardless of the physical +//! transport (I2C, SPI or HSU). The [`Interface`] trait mirrors that boundary: +//! the driver builds logical commands, the interface handles framing and the +//! acknowledgement/response handshake. + +use core::convert::Infallible; +use embedded_hal::delay::DelayNs; +use embedded_hal::digital::{ErrorType, InputPin, OutputPin}; +use embedded_hal::i2c::{I2c, Operation}; + +use crate::commands::{ + PN532_ACK_WAIT_TIME_MS, PN532_HOST_TO_PN532, PN532_I2C_ADDRESS, PN532_PN532_TO_HOST, + PN532_POSTAMBLE, PN532_PREAMBLE, PN532_STARTCODE1, PN532_STARTCODE2, +}; +use crate::error::Error; + +const ACK: [u8; 6] = [0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00]; + +/// Maximum response data length supported (matches the PN532's 64-byte packet buffer). +const MAX_DATA_LEN: usize = 64; +/// Maximum length of an outbound command frame. +const WRITE_FRAME_CAPACITY: usize = 64; + +/// Marker type for when no reset pin is connected. +#[derive(Clone, Copy, Debug, Default)] +pub struct NoReset; + +/// A PN532 reset pin (`RSTPD_N`, active-low). +/// +/// Implemented for any [`OutputPin`] as well as for [`NoReset`] (which does +/// nothing). A reset pin is optional but strongly recommended: without a reset +/// pulse the PN532 can end up in an uninitialised state and NACK subsequent +/// commands (surfacing as `Error::Transport(AcknowledgeCheckFailed(Data))`). +pub trait ResetPin { + /// Assert reset (drive the pin low). + fn assert(&mut self); + /// Deassert reset (drive the pin high). + fn deassert(&mut self); +} + +impl ResetPin for NoReset { + fn assert(&mut self) {} + fn deassert(&mut self) {} +} + +impl ResetPin for P { + fn assert(&mut self) { + let _ = self.set_low(); + } + + fn deassert(&mut self) { + let _ = self.set_high(); + } +} + +/// Marker type for when no IRQ pin is connected. +#[derive(Clone, Copy, Debug, Default)] +pub struct NoIrq; + +impl ErrorType for NoIrq { type Error = Infallible; } + +impl InputPin for NoIrq { + fn is_high(&mut self) -> Result { + panic!("NoIrq should not be used"); + } + + fn is_low(&mut self) -> Result { + panic!("NoIrq should not be used"); + } +} + +/// A framed transport to the PN532. +/// +/// Implementations are responsible for the frame/ack handshake described in +/// the PN532 user manual (UM0701-02). +pub trait Interface { + /// The error type produced by the underlying physical transport. + type TransportError; + + /// Initialise the hardware: pulse the reset pin (if provided) and wait for + /// the PN532 to become ready. + fn begin(&mut self) -> Result<(), Error>; + + /// Write a command frame (`header` + optional `body`) and wait for the ACK. + fn write_command( + &mut self, + header: &[u8], + body: &[u8], + ) -> Result<(), Error>; + + /// Read a response frame, returning the length of the payload (excluding + /// the `TFI` and command bytes) placed in `buf`. + fn read_response( + &mut self, + buf: &mut [u8], + timeout_ms: u16, + ) -> Result>; +} + +/// [`Interface`] implementation over a blocking `embedded_hal::i2c::I2c` bus. +/// +/// `RST` and `IRQ` are the optional reset and IRQ pins; use [`NoReset`] / +/// [`NoIrq`] (the defaults) or the relevant `embedded_hal` pins via the +/// [`I2cInterface::with_reset`] / [`I2cInterface::with_reset_irq`] constructors. +pub struct I2cInterface { + i2c: I2C, + delay: D, + reset: RST, + irq: IRQ, + command: u8, +} + +impl I2cInterface { + /// Create an interface from an already-configured I2C bus and a delay + /// source, without a reset or IRQ pin. + pub fn new(i2c: I2C, delay: D) -> Self { + Self { + i2c, + delay, + reset: NoReset, + irq: NoIrq, + command: 0, + } + } +} + +impl I2cInterface { + /// Create an interface with a reset pin (`RSTPD_N`). + pub fn with_reset(i2c: I2C, delay: D, reset: RST) -> Self { + Self { + i2c, + delay, + reset, + irq: NoIrq, + command: 0, + } + } +} + +impl I2cInterface { + /// Create an interface with an IRQ pin but no reset pin. + pub fn with_irq(i2c: I2C, delay: D, irq: IRQ) -> Self { + Self { + i2c, + delay, + reset: NoReset, + irq, + command: 0, + } + } +} + +impl I2cInterface { + /// Create an interface with both a reset pin and an IRQ pin. + pub fn with_reset_irq(i2c: I2C, delay: D, reset: RST, irq: IRQ) -> Self { + Self { + i2c, + delay, + reset, + irq, + command: 0, + } + } +} + +impl I2cInterface { + /// Consume the interface and return the underlying I2C bus. + pub fn release(self) -> I2C { + self.i2c + } +} + +impl Interface for I2cInterface +where + I2C: I2c, + D: DelayNs, + RST: ResetPin, + IRQ: InputPin, +{ + type TransportError = I2C::Error; + + fn begin(&mut self) -> Result<(), Error> { + // Pulse RSTPD_N: high -> low -> wait -> high -> wait. This mirrors the + // Adafruit library's begin() reset sequence. + self.reset.deassert(); + self.reset.assert(); + self.delay.delay_ms(400); + self.reset.deassert(); + // Let the PN532 boot after the reset is released. The Adafruit library + // waits ~10 ms + a 500 ms wakeup here; give it a full 500 ms. + self.delay.delay_ms(500); + Ok(()) + } + + fn write_command( + &mut self, + header: &[u8], + body: &[u8], + ) -> Result<(), Error> { + self.command = header[0]; + + let data_len = header.len() + body.len() + 1; // TFI + payload + if data_len > 0xFF { + return Err(Error::NoSpace); + } + + // PREAMBLE + STARTCODE1 + STARTCODE2 + LEN + LCS + TFI + payload + DCS + POSTAMBLE + let frame_len = 6 + header.len() + body.len() + 2; + let mut frame = [0u8; WRITE_FRAME_CAPACITY]; + if frame_len > frame.len() { + return Err(Error::NoSpace); + } + + frame[0] = PN532_PREAMBLE; + frame[1] = PN532_STARTCODE1; + frame[2] = PN532_STARTCODE2; + frame[3] = data_len as u8; + frame[4] = (!(data_len as u8)).wrapping_add(1); + frame[5] = PN532_HOST_TO_PN532; + + let mut sum = PN532_HOST_TO_PN532; + let mut idx = 6; + for &b in header.iter().chain(body.iter()) { + frame[idx] = b; + idx += 1; + sum = sum.wrapping_add(b); + } + frame[idx] = (!sum).wrapping_add(1); + frame[idx + 1] = PN532_POSTAMBLE; + + #[cfg(feature = "defmt")] + defmt::debug!("pn532: write cmd=0x{:02X} len={}", header[0], frame_len); + self.i2c + .write(PN532_I2C_ADDRESS, &frame[..frame_len]) + .map_err(Error::Transport)?; + #[cfg(feature = "defmt")] + defmt::debug!("pn532: write ACKed, reading ACK frame"); + self.read_ack_frame() + } + + fn read_response( + &mut self, + buf: &mut [u8], + timeout_ms: u16, + ) -> Result> { + // Wait for the PN532 to signal data is ready. + self.wait_ready(timeout_ms)?; + + // Read the full response frame in a single transaction, stripping the + // RDY byte. Frame layout: PREAMBLE(3) + LEN + LCS + TFI + CMD + payload + DCS + POSTAMBLE. + let frame_len = buf.len() + 9; + let mut frame = [0u8; MAX_DATA_LEN + 10]; + if frame_len > frame.len() { + return Err(Error::NoSpace); + } + self.i2c + .transaction( + PN532_I2C_ADDRESS, + &mut [ + Operation::Read(&mut [0]), + Operation::Read(&mut frame[..frame_len]), + ], + ) + .map_err(Error::Transport)?; + + if frame[0] != PN532_PREAMBLE + || frame[1] != PN532_STARTCODE1 + || frame[2] != PN532_STARTCODE2 + { + return Err(Error::InvalidFrame); + } + let length = frame[3] as usize; + if frame[3].wrapping_add(frame[4]) != 0 { + return Err(Error::InvalidFrame); + } + + let cmd = self.command.wrapping_add(1); + if frame[5] != PN532_PN532_TO_HOST || frame[6] != cmd { + return Err(Error::InvalidFrame); + } + + let data_len = length.saturating_sub(2); + if data_len > buf.len() { + return Err(Error::NoSpace); + } + let mut sum = frame[5].wrapping_add(frame[6]); + for i in 0..data_len { + buf[i] = frame[7 + i]; + sum = sum.wrapping_add(frame[7 + i]); + } + if sum.wrapping_add(frame[7 + data_len]) != 0 { + return Err(Error::InvalidFrame); + } + + Ok(data_len) + } +} + +impl I2cInterface +where + I2C: I2c, + D: DelayNs, + IRQ: InputPin, +{ + /// Wait until the PN532 signals data is ready. + /// + /// With an IRQ pin this blocks until the pin goes low; without one it polls + /// the I2C status byte. Either way the status byte is consumed, so the next + /// read returns the frame itself. + fn wait_ready(&mut self, timeout_ms: u16) -> Result<(), Error> { + let mut elapsed = 0u16; + loop { + match self.irq.is_low() { + Ok(true) => { + // IRQ asserted (active-low): data is ready. Poll the status + // byte to confirm and consume it so the next read gets the + // frame itself. + let mut status = [0u8; 1]; + match self.i2c.read(PN532_I2C_ADDRESS, &mut status) { + Ok(()) if status[0] & 1 == 1 => return Ok(()), + _ => { + self.delay.delay_ms(1); + elapsed += 1; + if timeout_ms != 0 && elapsed >= timeout_ms { + return Err(Error::Timeout); + } + } + } + } + Ok(false) | Err(_) => { + // IRQ connected but not asserted yet (or a read error): + // keep waiting. + self.delay.delay_ms(1); + elapsed += 1; + if timeout_ms != 0 && elapsed >= timeout_ms { + return Err(Error::Timeout); + } + } + } + } + } + + fn read_ack_frame(&mut self) -> Result<(), Error> { + self.wait_ready(PN532_ACK_WAIT_TIME_MS)?; + let mut ack = [0u8; 6]; + self.i2c + .transaction( + PN532_I2C_ADDRESS, + &mut [ + Operation::Read(&mut [0]), + Operation::Read(&mut ack), + ], + ) + .map_err(Error::Transport)?; + if ack == ACK { + Ok(()) + } else { + Err(Error::InvalidAck) + } + } +} diff --git a/pn532/src/iso14443a/mod.rs b/pn532/src/iso14443a/mod.rs new file mode 100644 index 0000000..99a729b --- /dev/null +++ b/pn532/src/iso14443a/mod.rs @@ -0,0 +1,91 @@ +use crate::{BaudRate, Error, Interface, Pn532, Uid}; +use crate::commands::{PN532_COMMAND_INDATAEXCHANGE, PN532_COMMAND_INLISTPASSIVETARGET, PN532_COMMAND_INRELEASE}; +use crate::error::StatusCode; + +impl Pn532 { + /// Wait for an ISO14443A target and read its UID. + /// + /// Returns `Ok(None)` when no card appears within `timeout_ms` milliseconds. + pub fn read_passive_target_id( + &mut self, + card_baudrate: BaudRate, + timeout_ms: u16, + ) -> Result, Error> { + self.buffer[0] = PN532_COMMAND_INLISTPASSIVETARGET; + self.buffer[1] = 1; + self.buffer[2] = card_baudrate.into(); + self.send(3)?; + + match self.read_timeout(timeout_ms) { + Err(Error::Timeout) => return Ok(None), + Err(e) => return Err(e), + Ok(_) => {} + } + + if self.buffer[0] != 1 { + return Ok(None); + } + + let uid_len = self.buffer[5]; + if uid_len > 7 { + return Err(Error::InvalidFrame); + } + + let mut bytes = [0u8; 7]; + bytes[..uid_len as usize].copy_from_slice(&self.buffer[6..6 + uid_len as usize]); + + Ok(Some(Uid { bytes: heapless::Vec::from_array(bytes) })) + } + + /// In-list a passive target (used before [`Self::in_data_exchange`]). + pub fn in_list_passive_target(&mut self) -> Result> { + self.buffer[0] = PN532_COMMAND_INLISTPASSIVETARGET; + self.buffer[1] = 1; + self.buffer[2] = 0; + self.send(3)?; + + match self.read_timeout(30_000) { + Err(Error::Timeout) => return Ok(false), + Err(e) => return Err(e), + Ok(_) => {} + } + + if self.buffer[0] != 1 { + return Ok(false); + } + self.in_listed_tag = self.buffer[1]; + Ok(true) + } + + /// Exchange data with the currently in-listed peer. + /// + /// Returns the number of bytes written to `response`. The response buffer + /// is limited to 64 bytes. + pub fn in_data_exchange( + &mut self, + send: &[u8], + response: &mut [u8], + ) -> Result> { + self.buffer[0] = PN532_COMMAND_INDATAEXCHANGE; + self.buffer[1] = self.in_listed_tag; + self.send_with_body(2, send)?; + + let len = self.read_timeout(1000)?; + if len == 0 || (self.buffer[0] & 0x3F) != 0 { + return Err(Error::Status(StatusCode::from_repr(self.buffer[0] & 0x3F).unwrap())); + } + + let data_len = len - 1; + let copy_len = data_len.min(response.len()); + response[..copy_len].copy_from_slice(&self.buffer[1..1 + copy_len]); + Ok(copy_len) + } + + /// Release an in-listed target. + pub fn in_release(&mut self, relevant_target: u8) -> Result> { + self.buffer[0] = PN532_COMMAND_INRELEASE; + self.buffer[1] = relevant_target; + self.send(2)?; + self.read() + } +} \ No newline at end of file diff --git a/pn532/src/lib.rs b/pn532/src/lib.rs new file mode 100644 index 0000000..4a1dcb2 --- /dev/null +++ b/pn532/src/lib.rs @@ -0,0 +1,59 @@ +#![no_std] +#![forbid(unsafe_code)] + +//! A `no_std` [`embedded-hal`] driver for the NXP PN532 NFC controller over I2C. +//! +//! This crate is a Rust port of the well-known +//! [Adafruit/Seeed PN532 Arduino library](https://github.com/elechouse/PN532), +//! targeting the I2C interface (the most common breakout-board wiring). +//! +//! The driver is transport-agnostic via the [`Interface`] trait; [`I2cInterface`] +//! provides the I2C implementation on top of a blocking `embedded_hal::i2c::I2c` +//! bus plus a `embedded_hal::delay::DelayNs` delay source. +//! +//! # Example +//! +//! ```ignore +//! use embedded_hal::delay::DelayNs; +//! use pn532::{BaudRate, I2cInterface, Pn532}; +//! +//! # struct Delay; +//! # impl DelayNs for Delay { fn delay_ns(&mut self, _: u32) {} } +//! # +//! # fn demo(i2c: I2C, delay: Delay) +//! # where I2C: embedded_hal::i2c::I2c +//! # { +//! let mut nfc = Pn532::new(I2cInterface::new(i2c, delay)); +//! nfc.begin().unwrap(); +//! nfc.sam_config().unwrap(); +//! +//! let uid = nfc +//! .read_passive_target_id(BaudRate::ISO14443A106kbps, 1000) +//! .unwrap(); +//! if let Some(uid) = uid { +//! // uid.bytes[..uid.len as usize] contains the card UID +//! } +//! # } +//! ``` +//! +//! # Limitations +//! +//! * Responses are limited to 64 bytes, matching the PN532's internal packet +//! buffer (this also constrains multi-block FeliCa reads). +//! * The I2C interface is blocking and uses busy-wait polling for the PN532's +//! "ready" flag, mirroring the original Arduino implementation. + +pub mod commands; +pub mod error; +pub mod felica; +pub mod interface; + +pub(crate) mod driver; +pub mod tg; +pub mod mifare; +pub mod iso14443a; + +pub use commands::BaudRate; +pub use driver::{FelicaPollingResponse, Pn532, TargetInitStatus, Uid}; +pub use error::Error; +pub use interface::{I2cInterface, Interface, NoIrq, NoReset}; diff --git a/pn532/src/mifare/mod.rs b/pn532/src/mifare/mod.rs new file mode 100644 index 0000000..a141b34 --- /dev/null +++ b/pn532/src/mifare/mod.rs @@ -0,0 +1,218 @@ +use crate::{Error, Interface, Pn532}; +use crate::commands::{MIFARE_CMD_AUTH_A, MIFARE_CMD_AUTH_B, MIFARE_CMD_READ, MIFARE_CMD_WRITE, MIFARE_CMD_WRITE_ULTRALIGHT, PN532_COMMAND_INDATAEXCHANGE}; +use crate::error::StatusCode; + +impl Pn532 { + /// Whether the block number is the first block of a sector. + pub fn mifare_classic_is_first_block(block: u32) -> bool { + if block < 128 { + block % 4 == 0 + } else { + block % 16 == 0 + } + } + + /// Whether the block number is a sector trailer. + pub fn mifare_classic_is_trailer_block(block: u32) -> bool { + if block < 128 { + (block + 1) % 4 == 0 + } else { + (block + 1) % 16 == 0 + } + } + + /// Authenticate a Mifare Classic block. + pub fn mifare_classic_authenticate_block( + &mut self, + uid: &[u8], + block_number: u8, + key_number: u8, + key_data: &[u8; 6], + ) -> Result<(), Error> { + self.buffer[0] = PN532_COMMAND_INDATAEXCHANGE; + self.buffer[1] = 1; + self.buffer[2] = if key_number != 0 { + MIFARE_CMD_AUTH_B + } else { + MIFARE_CMD_AUTH_A + }; + self.buffer[3] = block_number; + self.buffer[4..10].copy_from_slice(key_data); + + let uid_len = uid.len().min(7); + self.buffer[10..10 + uid_len].copy_from_slice(&uid[..uid_len]); + + self.send(10 + uid_len)?; + let len = self.read()?; + if len == 0 || self.buffer[0] != 0x00 { + return Err(Error::Status(StatusCode::from_repr(self.buffer[0]).unwrap())); + } + Ok(()) + } + + /// Read a 16-byte Mifare Classic data block. + pub fn mifare_classic_read_data_block( + &mut self, + block_number: u8, + data: &mut [u8; 16], + ) -> Result<(), Error> { + self.buffer[0] = PN532_COMMAND_INDATAEXCHANGE; + self.buffer[1] = 1; + self.buffer[2] = MIFARE_CMD_READ; + self.buffer[3] = block_number; + self.send(4)?; + + let len = self.read()?; + if len < 17 || self.buffer[0] != 0x00 { + return Err(Error::Status(StatusCode::from_repr(self.buffer[0]).unwrap())); + } + data.copy_from_slice(&self.buffer[1..17]); + Ok(()) + } + + /// Write a 16-byte Mifare Classic data block. + pub fn mifare_classic_write_data_block( + &mut self, + block_number: u8, + data: &[u8; 16], + ) -> Result<(), Error> { + self.buffer[0] = PN532_COMMAND_INDATAEXCHANGE; + self.buffer[1] = 1; + self.buffer[2] = MIFARE_CMD_WRITE; + self.buffer[3] = block_number; + self.buffer[4..20].copy_from_slice(data); + self.send(20)?; + + let len = self.read()?; + if len == 0 { + return Err(Error::InvalidFrame); + } + Ok(()) + } + + /// Format a Mifare Classic card to store NDEF records. + pub fn mifare_classic_format_ndef(&mut self) -> Result<(), Error> { + let sector1 = [ + 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, + 0x03, 0xE1, + ]; + let sector2 = [ + 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, + 0x03, 0xE1, + ]; + let sector3 = [ + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, + ]; + self.mifare_classic_write_data_block(1, §or1)?; + self.mifare_classic_write_data_block(2, §or2)?; + self.mifare_classic_write_data_block(3, §or3)?; + Ok(()) + } + + /// Write an NDEF URI record to the given sector (1..15). + pub fn mifare_classic_write_ndef_uri( + &mut self, + sector_number: u8, + uri_identifier: u8, + url: &str, + ) -> Result<(), Error> { + let len = url.len(); + if !(1..=15).contains(§or_number) { + return Err(Error::InvalidParam); + } + if !(1..=38).contains(&len) { + return Err(Error::InvalidParam); + } + + let url = url.as_bytes(); + let mut s1 = [0u8; 16]; + let mut s2 = [0u8; 16]; + let mut s3 = [0u8; 16]; + let s4 = [ + 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, + ]; + + s1[0] = 0x00; + s1[1] = 0x00; + s1[2] = 0x03; + s1[3] = (len + 5) as u8; + s1[4] = 0xD1; + s1[5] = 0x01; + s1[6] = (len + 1) as u8; + s1[7] = 0x55; + s1[8] = uri_identifier; + + if len <= 6 { + s1[9..9 + len].copy_from_slice(&url[..len]); + s1[9 + len] = 0xFE; + } else if len == 7 { + s1[9..16].copy_from_slice(&url[..7]); + s2[0] = 0xFE; + } else if len <= 22 { + s1[9..16].copy_from_slice(&url[..7]); + s2[..len - 7].copy_from_slice(&url[7..len]); + s2[len - 7] = 0xFE; + } else if len == 23 { + s1[9..16].copy_from_slice(&url[..7]); + s2.copy_from_slice(&url[7..23]); + s3[0] = 0xFE; + } else { + s1[9..16].copy_from_slice(&url[..7]); + s2.copy_from_slice(&url[7..23]); + s3[..len - 23].copy_from_slice(&url[23..len]); + s3[len - 23] = 0xFE; + } + + self.mifare_classic_write_data_block(sector_number * 4, &s1)?; + self.mifare_classic_write_data_block(sector_number * 4 + 1, &s2)?; + self.mifare_classic_write_data_block(sector_number * 4 + 2, &s3)?; + self.mifare_classic_write_data_block(sector_number * 4 + 3, &s4)?; + Ok(()) + } + + // -- Mifare Ultralight / NTAG ------------------------------------------ + + /// Read a 4-byte Mifare Ultralight / NTAG page. + pub fn mifare_ultralight_read_page( + &mut self, + page: u8, + buffer: &mut [u8; 4], + ) -> Result<(), Error> { + if page >= 64 { + return Err(Error::InvalidParam); + } + self.buffer[0] = PN532_COMMAND_INDATAEXCHANGE; + self.buffer[1] = 1; + self.buffer[2] = MIFARE_CMD_READ; + self.buffer[3] = page; + self.send(4)?; + + let len = self.read()?; + if len < 5 || self.buffer[0] != 0x00 { + return Err(Error::Status(StatusCode::from_repr(self.buffer[0]).unwrap())); + } + buffer.copy_from_slice(&self.buffer[1..5]); + Ok(()) + } + + /// Write a 4-byte Mifare Ultralight / NTAG page. + pub fn mifare_ultralight_write_page( + &mut self, + page: u8, + buffer: &[u8; 4], + ) -> Result<(), Error> { + if page >= 64 { + return Err(Error::InvalidParam); + } + self.buffer[0] = PN532_COMMAND_INDATAEXCHANGE; + self.buffer[1] = 1; + self.buffer[2] = MIFARE_CMD_WRITE_ULTRALIGHT; + self.buffer[3] = page; + self.buffer[4..8].copy_from_slice(buffer); + self.send(8)?; + self.read()?; + Ok(()) + } +} \ No newline at end of file diff --git a/pn532/src/tg/mod.rs b/pn532/src/tg/mod.rs new file mode 100644 index 0000000..f6112bf --- /dev/null +++ b/pn532/src/tg/mod.rs @@ -0,0 +1,74 @@ +use crate::{Error, Interface, Pn532, TargetInitStatus}; +use crate::commands::{PN532_COMMAND_TGGETDATA, PN532_COMMAND_TGINITASTARGET, PN532_COMMAND_TGSETDATA}; +use crate::error::StatusCode; + +impl Pn532 { + /// Initialize the PN532 as a target using a raw command frame. + pub fn tg_init_as_target( + &mut self, + command: &[u8], + timeout_ms: u16, + ) -> Result> { + self.interface.write_command(command, &[])?; + match self.interface.read_response(&mut self.buffer, timeout_ms) { + Ok(_) => Ok(TargetInitStatus::Success), + Err(Error::Timeout) => Ok(TargetInitStatus::Timeout), + Err(_) => Ok(TargetInitStatus::Failed), + } + } + + /// Initialize the PN532 as a target with the default LLCP parameters. + pub fn tg_init_as_target_default( + &mut self, + timeout_ms: u16, + ) -> Result> { + const COMMAND: [u8; 44] = [ + PN532_COMMAND_TGINITASTARGET, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x01, 0xFE, + 0x0F, 0xBB, 0xBA, 0xA6, 0xC9, 0x89, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x01, 0xFE, 0x0F, 0xBB, 0xBA, 0xA6, 0xC9, 0x89, 0x00, 0x00, 0x06, 0x46, + 0x66, 0x6D, 0x01, 0x01, 0x10, 0x00, + ]; + self.tg_init_as_target(&COMMAND, timeout_ms) + } + + /// Retrieve data received from the initiator. + pub fn tg_get_data(&mut self, buf: &mut [u8]) -> Result> { + self.buffer[0] = PN532_COMMAND_TGGETDATA; + self.send(1)?; + + let len = self.read_timeout(3000)?; + if len == 0 { + return Ok(0); + } + if self.buffer[0] != 0 { + return Err(Error::Status(StatusCode::from_repr(self.buffer[0]).unwrap())); + } + let data_len = len - 1; + let copy_len = data_len.min(buf.len()); + buf[..copy_len].copy_from_slice(&self.buffer[1..1 + copy_len]); + Ok(copy_len) + } + + /// Send data to the initiator. + pub fn tg_set_data( + &mut self, + header: &[u8], + body: &[u8], + ) -> Result<(), Error> { + if header.len() > crate::driver::PACKET_BUFFER_SIZE - 1 { + self.buffer[0] = PN532_COMMAND_TGSETDATA; + self.interface.write_command(&self.buffer[..1], header)?; + } else { + self.buffer[0] = PN532_COMMAND_TGSETDATA; + self.buffer[1..1 + header.len()].copy_from_slice(header); + self.interface + .write_command(&self.buffer[..1 + header.len()], body)?; + } + + let len = self.read_timeout(3000)?; + if len == 0 || self.buffer[0] != 0 { + return Err(Error::Status(StatusCode::from_repr(self.buffer[0]).unwrap())); + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/board.rs b/src/board.rs index 0d487dd..5aaeb4a 100644 --- a/src/board.rs +++ b/src/board.rs @@ -2,23 +2,25 @@ 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 crossbeam::atomic::AtomicCell; 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_sync::pubsub::PubSubChannel; use embassy_time::{Duration, Timer}; -use esp_hal::gpio::{InputConfig, Level}; -use esp_hal::i2c::master::I2c; +use esp_hal::gpio::{InputConfig, Level, Pull}; +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 esp_hal::interrupt::software::SoftwareInterruptControl; +use esp_hal::rng::{Trng, TrngSource}; use static_cell::StaticCell; -use crate::components::rom_storage::RomStorage; -use crate::types::{AppData, SharedI2c}; +use crate::components::{ rom_storage::RomStorage, card_reader::CardReader, self }; +use crate::types::{AppData, LoraPayload, LoraPubSub, SharedI2c}; pub(crate) static LED_STATE: AtomicBool = AtomicBool::new(false); @@ -26,13 +28,16 @@ pub struct Board { spawner: Spawner, lora: Lora<'static>, clock: Clock<'static>, - storage: RomStorage<'static> + storage: RomStorage<'static>, + card_reader: CardReader<'static>, + trng: Trng } impl Board { pub async fn new(spawner: Spawner, peripherals: Peripherals) -> Result { let timer_group = TimerGroup::new(peripherals.TIMG0); - esp_rtos::start(timer_group.timer0); + let sw_ctrl = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); + esp_rtos::start(timer_group.timer0, sw_ctrl.software_interrupt0); static SPI_BUS: StaticCell>> = StaticCell::new(); let spi = Spi::new(peripherals.SPI2, Config::default())? @@ -43,20 +48,16 @@ impl Board { let spi_bus = SPI_BUS.init(Mutex::new(spi)); static I2C_BUS: StaticCell>>> = StaticCell::new(); - let async_i2c = I2c::new(peripherals.I2C0, Default::default())? + let async_i2c = I2c::new( + peripherals.I2C0, + esp_hal::i2c::master::Config::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 irq = gpio::Input::new(peripherals.GPIO6, InputConfig::default().with_pull(Pull::Up)); + let card_reader = CardReader::new(SharedI2c::new(i2c_bus), irq); let vext = gpio::Output::new(peripherals.GPIO36, Level::Low, Default::default()); let lora = { @@ -71,17 +72,35 @@ impl Board { 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))?; + spawner.spawn(led_task(led)?); let storage = RomStorage::new( SharedI2c::new(i2c_bus) ); - let instance = Self { spawner, lora, clock, storage }; + let trng_source = TrngSource::new(peripherals.RNG, peripherals.ADC1); + core::mem::forget(trng_source); + let trng = Trng::try_new()?; + + let instance = Self { + spawner, + lora, + clock, + storage, + card_reader, + trng + }; Ok(instance) } - pub async fn start(&mut self) -> Result<(), crate::error::Error> { + pub async fn start(mut self) -> Result<(), crate::error::Error> { + // loop { + // if let Some(res) = self.nfc.felica_polling(0xFFFF, 0x00, 100)? { + // defmt::debug!("{}", Debug2Format(&res)); + // break; + // } + // } + self.lora.try_join().await?; defmt::info!("Board started"); LED_STATE.store(true, Ordering::Relaxed); @@ -91,15 +110,33 @@ impl Board { let app_data = AppData { last_boot_time: datetime }; - defmt::debug!("stored app data: {:?}", Debug2Format(&self.storage.get_data::()?)); + defmt::debug!("stored app data: {}", 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))?; + static LORA_CHANNEL: StaticCell = StaticCell::new(); + let lora_channel = LORA_CHANNEL.init(PubSubChannel::new()); + + self.spawner.spawn( + components::card_reader::start_card_reader( + self.card_reader, + lora_channel.publisher()?, + self.trng.clone() + )? + ); + + self.spawner.spawn( + components::lora::start_send_data_task( + self.lora, + lora_channel.subscriber()? + )? + ); + + self.spawner.spawn( + components::lora::start_heartbeat_task(lora_channel.publisher()?)? + ); loop { - defmt::trace!("tick"); - Timer::after(Duration::from_secs(1)).await; + Timer::after_millis(100).await; } } } diff --git a/src/components/card_reader.rs b/src/components/card_reader.rs new file mode 100644 index 0000000..6d3761d --- /dev/null +++ b/src/components/card_reader.rs @@ -0,0 +1,80 @@ +use core::sync::atomic::{AtomicPtr, Ordering}; +use defmt::Debug2Format; +use embassy_sync::once_lock::OnceLock; +use embassy_time::{Delay, Timer}; +use esp_hal::gpio; +use esp_hal::rng::Trng; +use lorawan_device::RngCore; +use pn532::felica::FelicaBlock; +use pn532::Pn532; +use crate::types::{LoraPublisher, SharedI2c}; + +static CARD_KEY: OnceLock> = OnceLock::new(); + +fn get_card_key() -> &'static heapless::Vec { + CARD_KEY.get_or_init(|| { + let hex_string = env!("CARD_KEY"); + let mut buffer = [0u8; 16]; + hex::decode_to_slice(hex_string, &mut buffer).unwrap(); + heapless::Vec::from_array(buffer) + }) +} + +pub struct CardReader<'a> { + driver: Pn532, Delay, pn532::NoReset, gpio::Input<'a>>> +} + +impl<'a> CardReader<'a> { + pub fn new(i2c: SharedI2c<'a>, irq: gpio::Input<'a>) -> Self { + Self { + driver: Pn532::new(pn532::I2cInterface::with_irq(i2c, Delay, irq)) + } + } + + pub fn init(&mut self) -> Result<(), crate::error::Error> { + self.driver.sam_config()?; + Ok(()) + } +} + +#[embassy_executor::task] +pub async fn start_card_reader( + mut card_reader: CardReader<'static>, + mut publisher: LoraPublisher<'static>, + mut trng: Trng +) -> () { + let card_key = get_card_key(); + let mut rc = [0u8; 16]; + trng.fill_bytes(&mut rc); + defmt::trace!("card_key: {}", hex::encode_upper(card_key)); + + loop { + Timer::after_millis(50).await; + let poll_result = match card_reader.driver.felica_polling(0xFFFF, 0x01, 1000) { + Ok(v) => v, + Err(e) => { + defmt::error!("card_reader driver error: {}", e); + continue; + } + }; + let Some(res) = poll_result else { + defmt::trace!("No card detected"); + continue; + }; + defmt::info!("Polling card: {}", Debug2Format(&res)); + + match card_reader.driver.felica_release() { + Ok(_) => {}, + Err(e) => { + defmt::error!("card_reader release error: {}", e); + continue; + } + }; + + // InRelease switches the RF field off when no target remains + // (UM0701-02 §7.3.11), so re-enable it before the next poll. + if let Err(e) = card_reader.driver.set_rf_field(0x00, 0x01) { + defmt::error!("card_reader RF field error: {}", e); + } + } +} \ No newline at end of file diff --git a/src/components/clock.rs b/src/components/clock.rs index 8eb9074..96d1d09 100644 --- a/src/components/clock.rs +++ b/src/components/clock.rs @@ -2,20 +2,18 @@ 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, + #[error("Invalid time: {time}")] + InvalidTime { + time: Epoch + }, } impl From>> for ClockError { @@ -51,7 +49,9 @@ impl<'d> Clock<'d> { 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); + return Err(ClockError::InvalidTime { + time + }); }; let time = utc_datetime.time(); let date = utc_datetime.date_naive(); diff --git a/src/components/lora.rs b/src/components/lora.rs index aacb604..2a89b4b 100644 --- a/src/components/lora.rs +++ b/src/components/lora.rs @@ -1,9 +1,12 @@ -use crate::types::SharedAsyncSpi; +use alloc::vec::Vec; +use crate::types::{LoraPayload, LoraPublisher, LoraSubscriber, 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_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::pubsub::{Publisher, Subscriber}; use embassy_time::{Delay, Duration, Instant, Timer}; use esp_hal::gpio; use esp_hal::rng::Rng; @@ -25,6 +28,11 @@ const LORAWAN_REGION: region::Region = region::Region::AS923_1; pub const CLOCK_SYNC_FPORT: u8 = 202; const APP_TIME_CID: u8 = 0x01; +#[cfg(debug_assertions)] +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(15); +#[cfg(not(debug_assertions))] +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(120); + #[derive(thiserror::Error, Debug)] pub enum LoraError { #[error("Lora radio error: {0:?}")] @@ -161,39 +169,40 @@ impl<'d> Lora<'d> { }; Ok(Epoch::from_gpst_seconds(gpst_time + elapsed_secs + time_offset)) } - - async fn send_heartbeat(&mut self) -> Result<(), LoraError> { + + pub async fn send_data(&mut self, fport: u8, data: &[u8]) -> Result<(), LoraError> { self.driver.set_datarate(region::DR::_0); - self.driver.send(&[], 1, false).await?; + self.driver.send(data, fport, false).await?; + defmt::debug!("Sent data to fport {}", fport); + defmt::trace!("Data: {:?}", data); 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; - }; +pub(crate) async fn start_send_data_task( + mut lora: Lora<'static>, + mut subscriber: LoraSubscriber<'static>, +) { 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; + while let Some(payload) = subscriber.try_next_message_pure() { + lora.send_data(payload.fport, &payload.data).await + .unwrap_or_else(|e| defmt::error!("Error sending heartbeat: {}", e)); + } + Timer::after_millis(200).await; } } +#[embassy_executor::task] +pub(crate) async fn start_heartbeat_task(publisher: LoraPublisher<'static>) { + loop { + publisher.publish(LoraPayload { + fport: 1, + data: Vec::with_capacity(0), + }).await; + Timer::after(HEARTBEAT_INTERVAL).await; + } +} impl From for LoraError { fn from(e: RadioError) -> Self { diff --git a/src/components/mod.rs b/src/components/mod.rs index 78c7dcd..025dd58 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod lora; pub(crate) mod clock; -pub(crate) mod rom_storage; \ No newline at end of file +pub(crate) mod rom_storage; +pub mod card_reader; \ No newline at end of file diff --git a/src/components/rom_storage.rs b/src/components/rom_storage.rs index 708e342..f796cdf 100644 --- a/src/components/rom_storage.rs +++ b/src/components/rom_storage.rs @@ -54,7 +54,7 @@ impl<'d> RomStorage<'d> { } pub async fn write_data(&mut self, data: &T) -> Result<(), crate::error::Error> { - defmt::debug!("Writing app data"); + defmt::trace!("Writing app data"); let addr = self.get_or_insert_addr::()?; let bytes = postcard::to_allocvec(data)?; let bytes_len = bytes.len(); @@ -62,13 +62,13 @@ impl<'d> RomStorage<'d> { return Err(StorageError::StorageOverSize(bytes_len).into()); } for (i, chunk) in bytes.chunks(32).enumerate() { - defmt::debug!("Writing chunk {}", i); + defmt::trace!("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"); + defmt::debug!("Finished writing app data"); Ok(()) } diff --git a/src/error.rs b/src/error.rs index 968a31e..5e75fb0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,11 @@ use crate::components::clock::ClockError; use crate::components::lora::LoraError; use core::convert::Infallible; -use defmt::Formatter; +use defmt::{Formatter}; +use embassy_embedded_hal::shared_bus::I2cDeviceError; use embassy_executor::SpawnError; use esp_hal::i2c::master::ConfigError as I2cConfigError; +use esp_hal::i2c::master::Error as I2cError; use esp_hal::rng::TrngError; use esp_hal::spi::master::ConfigError as SpiConfigError; use crate::components::rom_storage::StorageError; @@ -34,10 +36,20 @@ pub enum Error { PostcardError(#[from] postcard::Error), #[error("ROM Storage Error: {0}")] StorageError(#[from] StorageError), + #[error("pub sub channel error: {0:?}")] + PubSubChannelError(embassy_sync::pubsub::Error), + #[error("NFC error: {0:?}")] + NFCError(pn532::Error>), #[error("Unknown error: {0}")] Other(#[from] anyhow::Error), } +impl From>> for Error { + fn from(e: pn532::Error>) -> Self { + Self::NFCError(e) + } +} + impl From for Error { fn from(e: TrngError) -> Self { Self::TrngError(e) @@ -62,8 +74,14 @@ impl From for Error { } } +impl From for Error { + fn from(value: embassy_sync::pubsub::Error) -> Self { + Self::PubSubChannelError(value) + } +} + impl defmt::Format for Error { fn format(&self, fmt: Formatter) { - defmt::write!(fmt, "{}", self); + defmt::write!(fmt, "{}", defmt::Display2Format(self)); } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f16560b..9bc33c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,29 +15,27 @@ mod components; mod types; #[allow(unused_imports)] -use {esp_backtrace as _, esp_println as _}; +use {esp_backtrace as _, esp_println as _, esp_alloc as _}; use embassy_executor::Spawner; +use embassy_time::{Duration, Timer}; 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 { + esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 1024 * 64); + + let board = match board::Board::new(spawner, peripherals).await { Ok(board) => board, Err(e) => { defmt::error!("Error while initializing board: {:?}", e); @@ -46,15 +44,10 @@ async fn main(spawner: Spawner) -> () { }; defmt::info!("Board initialized"); - let mut retry_count = RETRY_COUNT; + if let Err(e) = board.start().await { + defmt::error!("Error: {}", e); + } loop { - if retry_count == 0 { - break; - } - if let Err(e) = board.start().await { - defmt::error!("Error: {}", e); - } - retry_count -= 1; + Timer::after(Duration::from_secs(1)).await; } - defmt::error!("Unable to start after {} retries", RETRY_COUNT); } diff --git a/src/types/mod.rs b/src/types/mod.rs index cc924fb..99542e1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,18 +1,50 @@ +use alloc::vec::Vec; +use core::fmt::{Display, Formatter}; 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 embassy_sync::pubsub::{PubSubChannel, Publisher, Subscriber}; 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>>; +pub type SharedAsyncSpi<'d> = + SpiDevice<'d, CriticalSectionRawMutex, Spi<'d, Async>, gpio::Output<'d>>; +pub type SharedI2c<'d> = + I2cDevice<'d, NoopRawMutex, I2c<'d, Blocking>>; #[derive(Serialize, Deserialize, Clone, Debug)] -pub(crate) struct AppData { +pub struct AppData { pub last_boot_time: DateTime, -} \ No newline at end of file +} + +impl Display for AppData { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{:?}", self.last_boot_time) + } +} + +impl defmt::Format for AppData { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{}", defmt::Display2Format(self)); + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct LoraPayload { + pub fport: u8, + pub data: Vec +} + +pub const LORA_CH_CAP: usize = 5; +pub const LORA_CH_PUBS: usize = 5; +pub const LORA_CH_SUB: usize = 1; + +pub type LoraPubSub = + PubSubChannel; +pub type LoraPublisher<'a> = + Publisher<'a, CriticalSectionRawMutex, LoraPayload, LORA_CH_CAP, LORA_CH_SUB, LORA_CH_PUBS>; +pub type LoraSubscriber<'a> = + Subscriber<'a, CriticalSectionRawMutex, LoraPayload, LORA_CH_CAP, LORA_CH_SUB, LORA_CH_PUBS>; \ No newline at end of file