Init
This commit is contained in:
19
.cargo/config.toml
Normal file
19
.cargo/config.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[target.xtensa-esp32s3-none-elf]
|
||||||
|
runner = "espflash flash --monitor -B 921600 --chip esp32s3 --log-format defmt"
|
||||||
|
linker = "/home/fromost/.rustup/toolchains/esp/xtensa-esp-elf/esp-15.2.0_20250920/xtensa-esp-elf/bin/xtensa-esp32s3-elf-gcc"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
DEFMT_LOG = "info,lora=debug,eeprom24x=debug"
|
||||||
|
DEV_EUI = "000000DD44F30FAE"
|
||||||
|
APP_KEY = "00000000000000000000000040000000"
|
||||||
|
APP_EUI = "0000000000000040"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "link-arg=-nostartfiles",
|
||||||
|
"-Z", "stack-protector=all",
|
||||||
|
]
|
||||||
|
target = "xtensa-esp32s3-none-elf"
|
||||||
|
|
||||||
|
[unstable]
|
||||||
|
build-std = ["alloc", "core"]
|
||||||
1
.clippy.toml
Normal file
1
.clippy.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
stack-size-threshold = 1024
|
||||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
Cargo.lock
|
||||||
|
.idea/workspace.xml
|
||||||
|
|
||||||
|
# Editor configuration
|
||||||
|
.vscode/
|
||||||
|
.zed/
|
||||||
|
.helix/
|
||||||
|
.nvim.lua
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# RustRover
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
|
||||||
|
# Ignore .DS_Store file in mac
|
||||||
|
**/.DS_Store
|
||||||
8
.idea/dictionaries/project.xml
generated
Normal file
8
.idea/dictionaries/project.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="project">
|
||||||
|
<words>
|
||||||
|
<w>Trng</w>
|
||||||
|
<w>gpst</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
11
.idea/lora.iml
generated
Normal file
11
.idea/lora.iml
generated
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="EMPTY_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
12
.idea/material_theme_project_new.xml
generated
Normal file
12
.idea/material_theme_project_new.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MaterialThemeProjectNewConfig">
|
||||||
|
<option name="metadata">
|
||||||
|
<MTProjectMetadataState>
|
||||||
|
<option name="migrated" value="true" />
|
||||||
|
<option name="pristineConfig" value="false" />
|
||||||
|
<option name="userId" value="2773e5a3:19a9b2047d0:-7ffc" />
|
||||||
|
</MTProjectMetadataState>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
22
.idea/misc.xml
generated
Normal file
22
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectInspectionProfilesVisibleTreeState">
|
||||||
|
<entry key="Project Default">
|
||||||
|
<profile-state>
|
||||||
|
<expanded-state>
|
||||||
|
<State>
|
||||||
|
<id>Dev Container</id>
|
||||||
|
</State>
|
||||||
|
<State>
|
||||||
|
<id>General</id>
|
||||||
|
</State>
|
||||||
|
</expanded-state>
|
||||||
|
<selected-state>
|
||||||
|
<State>
|
||||||
|
<id>User defined</id>
|
||||||
|
</State>
|
||||||
|
</selected-state>
|
||||||
|
</profile-state>
|
||||||
|
</entry>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/lora.iml" filepath="$PROJECT_DIR$/.idea/lora.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
71
Cargo.toml
Normal file
71
Cargo.toml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
[package]
|
||||||
|
edition = "2024"
|
||||||
|
name = "lora"
|
||||||
|
rust-version = "1.88"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "lora"
|
||||||
|
path = "./src/main.rs"
|
||||||
|
doctest = false
|
||||||
|
bench = false
|
||||||
|
test = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
debug = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
esp-hal = { version = "~1.0", features = ["defmt", "esp32s3", "unstable"] }
|
||||||
|
esp-rtos = { version = "0.2.0", features = [
|
||||||
|
"defmt",
|
||||||
|
"embassy",
|
||||||
|
"esp-alloc",
|
||||||
|
"esp32s3",
|
||||||
|
] }
|
||||||
|
esp-alloc = { version = "0.9.0", features = ["defmt"] }
|
||||||
|
esp-backtrace = { version = "0.18.1", features = [
|
||||||
|
"defmt",
|
||||||
|
"esp32s3",
|
||||||
|
"panic-handler",
|
||||||
|
] }
|
||||||
|
esp-println = { version = "0.16.1", features = ["defmt-espflash", "esp32s3"] }
|
||||||
|
esp-bootloader-esp-idf = { version = "0.4.0", features = ["defmt", "esp32s3"] }
|
||||||
|
|
||||||
|
embassy-executor = { version = "0.9.1", features = ["defmt"] }
|
||||||
|
embassy-time = { version = "0.5.0", features = ["defmt", "generic-queue-8", "defmt-timestamp-uptime"] }
|
||||||
|
embassy-embedded-hal = { version = "0.5.0", features = ["defmt"] }
|
||||||
|
embassy-sync = { version = "0.7.2", features = ["defmt"] }
|
||||||
|
|
||||||
|
static_cell = "2.1.1"
|
||||||
|
defmt = "1.0.1"
|
||||||
|
anyhow = { version = "1.0.102", default-features = false }
|
||||||
|
thiserror = { version = "2.0.18", default-features = false }
|
||||||
|
lora-phy = { version = "3.0.1", features = ["lorawan-radio"] }
|
||||||
|
lorawan-device = { version = "0.12.2", default-features = false, features = ["region-as923-1", "defmt", "default-crypto"] }
|
||||||
|
|
||||||
|
|
||||||
|
ds3231 = { version = "0.3.0", features = ["defmt"] }
|
||||||
|
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
|
||||||
|
chrono = { version = "0.4.44", default-features = false, features = ["alloc", "defmt", "serde"] }
|
||||||
|
hifitime = { version = "4.2.5", default-features = false, features = ["serde_derive"] }
|
||||||
|
eeprom24x = { version = "0.7.2", features = ["defmt-03"] }
|
||||||
|
postcard = { version = "1.1.3", default-features = false, features = ["defmt", "alloc", "embedded-io", "postcard-derive"] }
|
||||||
|
serde = { version = "1.0.228", default-features = false, features = ["derive"] }
|
||||||
|
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
lorawan-device = { path = "lorawan-device-patch" }
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
# Rust debug is too slow.
|
||||||
|
# For debug builds always builds with some optimization
|
||||||
|
opt-level = "s"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1 # LLVM can perform better optimizations using a single thread
|
||||||
|
debug = 2
|
||||||
|
debug-assertions = false
|
||||||
|
incremental = false
|
||||||
|
lto = 'fat'
|
||||||
|
opt-level = 's'
|
||||||
|
overflow-checks = false
|
||||||
71
build.rs
Normal file
71
build.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
fn main() {
|
||||||
|
linker_be_nice();
|
||||||
|
println!("cargo:rustc-link-arg=-Tdefmt.x");
|
||||||
|
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
|
||||||
|
println!("cargo:rustc-link-arg=-Tlinkall.x");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linker_be_nice() {
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
if args.len() > 1 {
|
||||||
|
let kind = &args[1];
|
||||||
|
let what = &args[2];
|
||||||
|
|
||||||
|
match kind.as_str() {
|
||||||
|
"undefined-symbol" => match what.as_str() {
|
||||||
|
what if what.starts_with("_defmt_") => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(
|
||||||
|
"💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`"
|
||||||
|
);
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
"_stack_start" => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("💡 Is the linker script `linkall.x` missing?");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
what if what.starts_with("esp_rtos_") => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(
|
||||||
|
"💡 `esp-radio` has no scheduler enabled. Make sure you have initialized `esp-rtos` or provided an external scheduler."
|
||||||
|
);
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
"embedded_test_linker_file_not_added_to_rustflags" => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(
|
||||||
|
"💡 `embedded-test` not found - make sure `embedded-test.x` is added as a linker script for tests"
|
||||||
|
);
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
"free"
|
||||||
|
| "malloc"
|
||||||
|
| "calloc"
|
||||||
|
| "get_free_internal_heap_size"
|
||||||
|
| "malloc_internal"
|
||||||
|
| "realloc_internal"
|
||||||
|
| "calloc_internal"
|
||||||
|
| "free_internal" => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(
|
||||||
|
"💡 Did you forget the `esp-alloc` dependency or didn't enable the `compat` feature on it?"
|
||||||
|
);
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
// we don't have anything helpful for "missing-lib" yet
|
||||||
|
_ => {
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-link-arg=-Wl,--error-handling-script={}",
|
||||||
|
std::env::current_exe().unwrap().display()
|
||||||
|
);
|
||||||
|
}
|
||||||
1
lorawan-device-patch/.cargo-ok
Normal file
1
lorawan-device-patch/.cargo-ok
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"v":1}
|
||||||
6
lorawan-device-patch/.cargo_vcs_info.json
Normal file
6
lorawan-device-patch/.cargo_vcs_info.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"git": {
|
||||||
|
"sha1": "0efdb6b26407053c877cc6659a76c83b2dbdd952"
|
||||||
|
},
|
||||||
|
"path_in_vcs": "lorawan-device"
|
||||||
|
}
|
||||||
33
lorawan-device-patch/CHANGELOG.md
Normal file
33
lorawan-device-patch/CHANGELOG.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
|
||||||
|
to [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [v0.12.1]
|
||||||
|
|
||||||
|
- Allow multilple RXC frames during RXC window ([#217](https://github.com/lora-rs/lora-rs/pull/217))
|
||||||
|
- Individually feature-gate all regions ([#216](https://github.com/lora-rs/lora-rs/pull/236))
|
||||||
|
- Fix log macro for
|
||||||
|
error ([commit](https://github.com/lora-rs/lora-rs/pull/256/commits/99cb10b77baf0f1c51ae97b1830a80b4873864e1))
|
||||||
|
|
||||||
|
## [v0.12.0]
|
||||||
|
|
||||||
|
- Fixes bug related to FCntUp and confirmed uplink ([#182](https://github.com/lora-rs/lora-rs/pull/182))
|
||||||
|
- Extend PhyRxTx to support antenna gain and max power ([#159](https://github.com/lora-rs/lora-rs/pull/159))
|
||||||
|
- Implement Class C functionality for async_device ([#158](https://github.com/lora-rs/lora-rs/pull/159))
|
||||||
|
- Implement rapid subband acquisition, aka "Join Bias" for US915 & AU915
|
||||||
|
([#110](https://github.com/lora-rs/lora-rs/pull/110) / [#170](https://github.com/lora-rs/lora-rs/pull/170) )
|
||||||
|
- Develops `async_device` API to provide `JoinResponse` and
|
||||||
|
`SendResponse` (#[144](https://github.com/lora-rs/lora-rs/pull/144))
|
||||||
|
- Develops `nb_device` API around sending a join to be consistent with
|
||||||
|
`async_device` (#[144](https://github.com/lora-rs/lora-rs/pull/144))
|
||||||
|
- Refactor `external-lora-phy` in `lorawan-device` as `lorawan-radio` in
|
||||||
|
`lora-phy` ([#189](https://github.com/lora-rs/lora-rs/pull/189))
|
||||||
|
- Add `Timer` implementation based on embassy-time ([#171](https://github.com/lora-rs/lora-rs/pull/171))
|
||||||
|
- Use radio timeout for end of RX1 and RX2 windows; preamble detection cancels
|
||||||
|
timeout ([#204](https://github.com/lora-rs/lora-rs/pull/204))
|
||||||
|
- Remove `async` feature-flag as async fn in traits is stable
|
||||||
|
|
||||||
|
Change tracking starting at version 0.11.0.
|
||||||
141
lorawan-device-patch/Cargo.toml
Normal file
141
lorawan-device-patch/Cargo.toml
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||||
|
#
|
||||||
|
# When uploading crates to the registry Cargo will automatically
|
||||||
|
# "normalize" Cargo.toml files for maximal compatibility
|
||||||
|
# with all versions of Cargo and also rewrite `path` dependencies
|
||||||
|
# to registry (e.g., crates.io) dependencies.
|
||||||
|
#
|
||||||
|
# If you are reading this file be aware that the original Cargo.toml
|
||||||
|
# will likely look very different (and much more reasonable).
|
||||||
|
# See Cargo.toml.orig for the original contents.
|
||||||
|
|
||||||
|
[package]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.75"
|
||||||
|
name = "lorawan-device"
|
||||||
|
version = "0.12.2"
|
||||||
|
authors = [
|
||||||
|
"Louis Thiery <thiery.louis@gmail.com>",
|
||||||
|
"Ulf Lilleengen <lulf@redhat.com>",
|
||||||
|
]
|
||||||
|
build = false
|
||||||
|
autobins = false
|
||||||
|
autoexamples = false
|
||||||
|
autotests = false
|
||||||
|
autobenches = false
|
||||||
|
description = "A Rust LoRaWAN device stack implementation"
|
||||||
|
readme = "README.md"
|
||||||
|
categories = [
|
||||||
|
"embedded",
|
||||||
|
"hardware-support",
|
||||||
|
"no-std",
|
||||||
|
]
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/lora-rs/lora-rs"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = [
|
||||||
|
"--cfg",
|
||||||
|
"docsrs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "lorawan_device"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies.defmt]
|
||||||
|
version = "0.3"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.document-features]
|
||||||
|
version = "0.2.8"
|
||||||
|
|
||||||
|
[dependencies.embassy-time]
|
||||||
|
version = "0.3.0"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.fastrand]
|
||||||
|
version = "2"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.futures]
|
||||||
|
version = "0.3"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.generic-array]
|
||||||
|
version = "0.14"
|
||||||
|
|
||||||
|
[dependencies.heapless]
|
||||||
|
version = "0.7"
|
||||||
|
|
||||||
|
[dependencies.lora-modulation]
|
||||||
|
version = ">=0.1.2"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.lorawan]
|
||||||
|
version = "0.9"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.rand_core]
|
||||||
|
version = "0.6"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.seq-macro]
|
||||||
|
version = "0.3.5"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
version = "1"
|
||||||
|
features = ["derive"]
|
||||||
|
optional = true
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dev-dependencies.lazy_static]
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dev-dependencies.rand]
|
||||||
|
version = "0"
|
||||||
|
features = ["getrandom"]
|
||||||
|
|
||||||
|
[dev-dependencies.tokio]
|
||||||
|
version = "1"
|
||||||
|
features = [
|
||||||
|
"rt",
|
||||||
|
"macros",
|
||||||
|
"time",
|
||||||
|
"sync",
|
||||||
|
]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
all-regions = [
|
||||||
|
"region-as923-1",
|
||||||
|
"region-as923-2",
|
||||||
|
"region-as923-3",
|
||||||
|
"region-as923-4",
|
||||||
|
"region-au915",
|
||||||
|
"region-eu433",
|
||||||
|
"region-eu868",
|
||||||
|
"region-in865",
|
||||||
|
"region-us915",
|
||||||
|
]
|
||||||
|
default = ["all-regions"]
|
||||||
|
default-crypto = ["lorawan/default-crypto"]
|
||||||
|
defmt = [
|
||||||
|
"dep:defmt",
|
||||||
|
"lorawan/defmt",
|
||||||
|
"lora-modulation/defmt",
|
||||||
|
]
|
||||||
|
embassy-time = ["dep:embassy-time"]
|
||||||
|
region-as923-1 = []
|
||||||
|
region-as923-2 = []
|
||||||
|
region-as923-3 = []
|
||||||
|
region-as923-4 = []
|
||||||
|
region-au915 = []
|
||||||
|
region-eu433 = []
|
||||||
|
region-eu868 = []
|
||||||
|
region-in865 = []
|
||||||
|
region-us915 = []
|
||||||
|
serde = [
|
||||||
|
"dep:serde",
|
||||||
|
"lorawan/serde",
|
||||||
|
]
|
||||||
74
lorawan-device-patch/Cargo.toml.orig
generated
Normal file
74
lorawan-device-patch/Cargo.toml.orig
generated
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
[package]
|
||||||
|
name = "lorawan-device"
|
||||||
|
version = "0.12.2"
|
||||||
|
authors = ["Louis Thiery <thiery.louis@gmail.com>", "Ulf Lilleengen <lulf@redhat.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.75"
|
||||||
|
categories = [
|
||||||
|
"embedded",
|
||||||
|
"hardware-support",
|
||||||
|
"no-std",
|
||||||
|
]
|
||||||
|
license = "MIT"
|
||||||
|
readme = "README.md"
|
||||||
|
description = "A Rust LoRaWAN device stack implementation"
|
||||||
|
repository = "https://github.com/lora-rs/lora-rs"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lora-modulation = { path = "../lora-modulation", version = ">=0.1.2", default-features = false }
|
||||||
|
lorawan = { path = "../lorawan-encoding", version = "0.9", default-features = false }
|
||||||
|
heapless = "0.7"
|
||||||
|
generic-array = "0.14"
|
||||||
|
defmt = { version = "0.3", optional = true }
|
||||||
|
fastrand = { version = "2", default-features = false }
|
||||||
|
futures = { version = "0.3", default-features = false }
|
||||||
|
rand_core = { version = "0.6", default-features = false }
|
||||||
|
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
||||||
|
seq-macro = "0.3.5"
|
||||||
|
document-features = "0.2.8"
|
||||||
|
embassy-time = { version = "0.3.0", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1", features = ["rt", "macros", "time", "sync"] }
|
||||||
|
rand = { version = "0", features = ["getrandom"] }
|
||||||
|
lazy_static = "1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["all-regions"]
|
||||||
|
all-regions = ["region-as923-1", "region-as923-2", "region-as923-3", "region-as923-4", "region-au915", "region-eu433", "region-eu868", "region-in865", "region-us915"]
|
||||||
|
|
||||||
|
## Use pure Rust implementations of [`AES`](https://docs.rs/aes/latest/aes/) and [`CMAC`](https://docs.rs/cmac/latest/cmac/) for the LoRaWAN crypto layer.
|
||||||
|
default-crypto = ["lorawan/default-crypto"]
|
||||||
|
|
||||||
|
## Use [`defmt`](https://docs.rs/defmt/latest/defmt/) for logging.
|
||||||
|
defmt = ["dep:defmt", "lorawan/defmt", "lora-modulation/defmt"]
|
||||||
|
|
||||||
|
## Provide an `async_device::Timer` impl based on `embassy-time`.
|
||||||
|
embassy-time = ["dep:embassy-time"]
|
||||||
|
|
||||||
|
## Enable [`serde`](https://docs.rs/serde/latest/serde/) serialization/deserialization for data structures.
|
||||||
|
serde = ["dep:serde", "lorawan/serde"]
|
||||||
|
|
||||||
|
## Enable support for AS923-1 region (by default all regions are enabled).
|
||||||
|
region-as923-1 = []
|
||||||
|
## Enable support for AS923-2 region (by default all regions are enabled).
|
||||||
|
region-as923-2 = []
|
||||||
|
## Enable support for AS923-3 region (by default all regions are enabled).
|
||||||
|
region-as923-3 = []
|
||||||
|
## Enable support for AS923-4 region (by default all regions are enabled).
|
||||||
|
region-as923-4 = []
|
||||||
|
## Enable support for AU915 region (by default all regions are enabled).
|
||||||
|
region-au915 = []
|
||||||
|
## Enable support for EU433 region (by default all regions are enabled).
|
||||||
|
region-eu433 = []
|
||||||
|
## Enable support for EU868 region (by default all regions are enabled).
|
||||||
|
region-eu868 = []
|
||||||
|
## Enable support for IN865 region (by default all regions are enabled).
|
||||||
|
region-in865 = []
|
||||||
|
## Enable support for US915 region (by default all regions are enabled).
|
||||||
|
region-us915 = []
|
||||||
|
|
||||||
38
lorawan-device-patch/README.md
Normal file
38
lorawan-device-patch/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# lorawan-device
|
||||||
|
|
||||||
|
[![Latest Version]][crates.io]
|
||||||
|
[![Docs]][doc.rs]
|
||||||
|
|
||||||
|
This is an experimental LoRaWAN device stack with both non-blocking (`nb_device`) and async (`async_device`)
|
||||||
|
implementations. Both implementations have their respective `radio::PhyRxTx` traits that describe the radio interface
|
||||||
|
required.
|
||||||
|
|
||||||
|
Note: The `lorawan-radio` feature in the `lora-phy` crate provides `LorawanRadio` as an async implementation of
|
||||||
|
`radio::PhyRxTx`.
|
||||||
|
|
||||||
|
Both stacks share a dependency on the internal module, `mac` where LoRaWAN 1.0.x is approximately implemented:
|
||||||
|
|
||||||
|
- Class A device behavior
|
||||||
|
- Class C device behavior (async only)
|
||||||
|
- Over-the-Air Activation (OTAA) and Activation by Personalization (ABP)
|
||||||
|
- CFList is supported for fixed and dynamic channel plans
|
||||||
|
- Regional support for AS923_1, AS923_2, AS923_3, AS923_4, AU915, EU868, EU433, IN865, US915 (note: regional power
|
||||||
|
limits are not enforced ([#168](https://github.com/lora-rs/lora-rs/issues/168))
|
||||||
|
|
||||||
|
**Currently, MAC commands are minimally mocked. For example, an ADRReq is responded with an ADRResp, but not much
|
||||||
|
is actually done with the payload**.
|
||||||
|
|
||||||
|
Furthermore, both async and non-blocking implementation do not implement any retries for failed joins or failed
|
||||||
|
confirmed uplinks. It is up to the client to implement retry behavior; see the examples for more.
|
||||||
|
|
||||||
|
Please see [examples](https://github.com/lora-rs/lora-rs/tree/main/examples) for usage.
|
||||||
|
|
||||||
|
A public chat on LoRa/LoRaWAN topics using Rust is [here](https://matrix.to/#/#public-lora-wan-rs:matrix.org).
|
||||||
|
|
||||||
|
[Latest Version]: https://img.shields.io/crates/v/lorawan-device.svg
|
||||||
|
|
||||||
|
[crates.io]: https://crates.io/crates/lorawan-device
|
||||||
|
|
||||||
|
[Docs]: https://docs.rs/lorawan-device/badge.svg
|
||||||
|
|
||||||
|
[doc.rs]: https://docs.rs/lorawan-device
|
||||||
34
lorawan-device-patch/src/async_device/embassy_time.rs
Normal file
34
lorawan-device-patch/src/async_device/embassy_time.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use embassy_time::{Duration, Instant};
|
||||||
|
|
||||||
|
use super::radio::Timer;
|
||||||
|
|
||||||
|
/// A [`Timer`] implementation based on [`embassy-time`].
|
||||||
|
pub struct EmbassyTimer {
|
||||||
|
start: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmbassyTimer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { start: Instant::now() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EmbassyTimer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timer for EmbassyTimer {
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.start = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn at(&mut self, millis: u64) {
|
||||||
|
embassy_time::Timer::at(self.start + Duration::from_millis(millis)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delay_ms(&mut self, millis: u64) {
|
||||||
|
embassy_time::Timer::after_millis(millis).await
|
||||||
|
}
|
||||||
|
}
|
||||||
487
lorawan-device-patch/src/async_device/mod.rs
Normal file
487
lorawan-device-patch/src/async_device/mod.rs
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
//! LoRaWAN device which uses async-await for driving the protocol state against pin and timer events,
|
||||||
|
//! allowing for asynchronous radio implementations. Requires the `async` feature.
|
||||||
|
use super::mac::Mac;
|
||||||
|
|
||||||
|
use super::mac::{self, Frame, Window};
|
||||||
|
pub use super::{
|
||||||
|
mac::{NetworkCredentials, SendData, Session},
|
||||||
|
region::{self, Region},
|
||||||
|
Downlink, JoinMode,
|
||||||
|
};
|
||||||
|
use crate::log;
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
use futures::{future::select, future::Either, pin_mut};
|
||||||
|
use heapless::Vec;
|
||||||
|
use lorawan::{self, keys::CryptoFactory};
|
||||||
|
use rand_core::RngCore;
|
||||||
|
|
||||||
|
pub use crate::region::DR;
|
||||||
|
use crate::{radio::RadioBuffer, rng};
|
||||||
|
|
||||||
|
pub mod radio;
|
||||||
|
|
||||||
|
#[cfg(feature = "embassy-time")]
|
||||||
|
mod embassy_time;
|
||||||
|
#[cfg(feature = "embassy-time")]
|
||||||
|
pub use embassy_time::EmbassyTimer;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
use self::radio::{RxQuality, RxStatus};
|
||||||
|
|
||||||
|
/// Type representing a LoRaWAN capable device.
|
||||||
|
///
|
||||||
|
/// A device is bound to the following types:
|
||||||
|
/// - R: An asynchronous radio implementation
|
||||||
|
/// - T: An asynchronous timer implementation
|
||||||
|
/// - C: A CryptoFactory implementation
|
||||||
|
/// - RNG: A random number generator implementation. An external RNG may be provided, or you may use a builtin PRNG by
|
||||||
|
/// providing a random seed
|
||||||
|
/// - N: The size of the radio buffer. Generally, this should be set to 256 to support the largest possible LoRa frames.
|
||||||
|
/// - D: The amount of downlinks that may be buffered. This is used to support Class C operation. See below for more.
|
||||||
|
///
|
||||||
|
/// Note that the const generics N and D are used to configure the size of the radio buffer and the number of downlinks
|
||||||
|
/// that may be buffered. The defaults are 256 and 1 respectively which should be fine for Class A devices. **For Class
|
||||||
|
/// C operation**, it is recommended to increase D to at least 2, if not 3. This is because during the RX1/RX2 windows
|
||||||
|
/// after a Class A transmit, it is possible to receive Class C downlinks (in additional to any RX1/RX2 responses!).
|
||||||
|
pub struct Device<R, C, T, G, const N: usize = 256, const D: usize = 1>
|
||||||
|
where
|
||||||
|
R: radio::PhyRxTx + Timings,
|
||||||
|
T: radio::Timer,
|
||||||
|
C: CryptoFactory + Default,
|
||||||
|
G: RngCore,
|
||||||
|
{
|
||||||
|
crypto: PhantomData<C>,
|
||||||
|
radio: R,
|
||||||
|
rng: G,
|
||||||
|
timer: T,
|
||||||
|
mac: Mac,
|
||||||
|
radio_buffer: RadioBuffer<N>,
|
||||||
|
downlink: Vec<Downlink, D>,
|
||||||
|
class_c: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error<R> {
|
||||||
|
Radio(R),
|
||||||
|
Mac(mac::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SendResponse {
|
||||||
|
DownlinkReceived(mac::FcntDown),
|
||||||
|
SessionExpired,
|
||||||
|
NoAck,
|
||||||
|
RxComplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum JoinResponse {
|
||||||
|
JoinSuccess,
|
||||||
|
NoJoinAccept,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> From<mac::Error> for Error<R> {
|
||||||
|
fn from(e: mac::Error) -> Self {
|
||||||
|
Error::Mac(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, C, T, const N: usize> Device<R, C, T, rng::Prng, N>
|
||||||
|
where
|
||||||
|
R: radio::PhyRxTx + Timings,
|
||||||
|
C: CryptoFactory + Default,
|
||||||
|
T: radio::Timer,
|
||||||
|
{
|
||||||
|
/// Create a new [`Device`] by providing your own random seed. Using this method, [`Device`] will internally
|
||||||
|
/// use an algorithmic PRNG. Depending on your use case, this may or may not be faster than using your own
|
||||||
|
/// hardware RNG.
|
||||||
|
///
|
||||||
|
/// # ⚠️Warning⚠️
|
||||||
|
///
|
||||||
|
/// This function must **always** be called with a new randomly generated seed! **Never** call this function more
|
||||||
|
/// than once using the same seed. Generate the seed using a true random number generator. Using the same seed will
|
||||||
|
/// leave you vulnerable to replay attacks.
|
||||||
|
pub fn new_with_seed(region: region::Configuration, radio: R, timer: T, seed: u64) -> Self {
|
||||||
|
Device::new_with_seed_and_session(region, radio, timer, seed, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`Device`] by providing your own random seed. Also optionally provide your own [`Session`].
|
||||||
|
/// Using this method, [`Device`] will internally use an algorithmic PRNG to generate random numbers. Depending on
|
||||||
|
/// your use case, this may or may not be faster than using your own hardware RNG.
|
||||||
|
///
|
||||||
|
/// # ⚠️Warning⚠️
|
||||||
|
///
|
||||||
|
/// This function must **always** be called with a new randomly generated seed! **Never** call this function more
|
||||||
|
/// than once using the same seed. Generate the seed using a true random number generator. Using the same seed will
|
||||||
|
/// leave you vulnerable to replay attacks.
|
||||||
|
pub fn new_with_seed_and_session(
|
||||||
|
region: region::Configuration,
|
||||||
|
radio: R,
|
||||||
|
timer: T,
|
||||||
|
seed: u64,
|
||||||
|
session: Option<Session>,
|
||||||
|
) -> Self {
|
||||||
|
let rng = rng::Prng::new(seed);
|
||||||
|
Device::new_with_session(region, radio, timer, rng, session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, C, T, G, const N: usize, const D: usize> Device<R, C, T, G, N, D>
|
||||||
|
where
|
||||||
|
R: radio::PhyRxTx + Timings,
|
||||||
|
C: CryptoFactory + Default,
|
||||||
|
T: radio::Timer,
|
||||||
|
G: RngCore,
|
||||||
|
{
|
||||||
|
/// Create a new instance of [`Device`] with a RNG external to the LoRa chip. You must provide your own RNG
|
||||||
|
/// implementing [`RngCore`].
|
||||||
|
///
|
||||||
|
/// See also [`new_with_seed`](Device::new_with_seed) to let [`Device`] use a builtin PRNG by providing a random
|
||||||
|
/// seed.
|
||||||
|
pub fn new(region: region::Configuration, radio: R, timer: T, rng: G) -> Self {
|
||||||
|
Device::new_with_session(region, radio, timer, rng, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`Device`] and provide an optional [`Session`].
|
||||||
|
pub fn new_with_session(
|
||||||
|
region: region::Configuration,
|
||||||
|
radio: R,
|
||||||
|
timer: T,
|
||||||
|
rng: G,
|
||||||
|
session: Option<Session>,
|
||||||
|
) -> Self {
|
||||||
|
let mut mac = Mac::new(region, R::MAX_RADIO_POWER, R::ANTENNA_GAIN);
|
||||||
|
if let Some(session) = session {
|
||||||
|
mac.set_session(session);
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
crypto: PhantomData,
|
||||||
|
radio,
|
||||||
|
rng,
|
||||||
|
mac,
|
||||||
|
radio_buffer: RadioBuffer::new(),
|
||||||
|
timer,
|
||||||
|
downlink: Vec::new(),
|
||||||
|
class_c: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables Class C behavior. Note that Class C downlinks are not possible until a confirmed
|
||||||
|
/// uplink is sent to the LNS.
|
||||||
|
|
||||||
|
pub fn enable_class_c(&mut self) {
|
||||||
|
self.class_c = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disables Class C behavior. Note that an uplink must be set for the radio to disable
|
||||||
|
/// Class C listen.
|
||||||
|
pub fn disable_class_c(&mut self) {
|
||||||
|
self.class_c = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_session(&mut self) -> Option<&Session> {
|
||||||
|
self.mac.get_session()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_region(&mut self) -> ®ion::Configuration {
|
||||||
|
&self.mac.region
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_radio(&mut self) -> &R {
|
||||||
|
&self.radio
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut_radio(&mut self) -> &mut R {
|
||||||
|
&mut self.radio
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the current data rate being used by this device.
|
||||||
|
pub fn get_datarate(&mut self) -> DR {
|
||||||
|
self.mac.configuration.data_rate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the data rate being used by this device. This overrides the region default.
|
||||||
|
pub fn set_datarate(&mut self, datarate: DR) {
|
||||||
|
self.mac.configuration.data_rate = datarate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join the LoRaWAN network asynchronously. The returned future completes when
|
||||||
|
/// the LoRaWAN network has been joined successfully, or an error has occurred.
|
||||||
|
///
|
||||||
|
/// Repeatedly calling join using OTAA will result in a new LoRaWAN session to be created.
|
||||||
|
///
|
||||||
|
/// Note that for a Class C enabled device, you must repeatedly send *confirmed* uplink until
|
||||||
|
/// LoRaWAN Network Server (LNS) confirmation after joining.
|
||||||
|
pub async fn join(&mut self, join_mode: &JoinMode) -> Result<JoinResponse, Error<R::PhyError>> {
|
||||||
|
match join_mode {
|
||||||
|
JoinMode::OTAA { deveui, appeui, appkey } => {
|
||||||
|
let (tx_config, _) = self.mac.join_otaa::<C, G, N>(
|
||||||
|
&mut self.rng,
|
||||||
|
NetworkCredentials::new(*appeui, *deveui, *appkey),
|
||||||
|
&mut self.radio_buffer,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transmit the join payload
|
||||||
|
let ms = self
|
||||||
|
.radio
|
||||||
|
.tx(tx_config, self.radio_buffer.as_ref_for_read())
|
||||||
|
.await
|
||||||
|
.map_err(Error::Radio)?;
|
||||||
|
|
||||||
|
// Receive join response within RX window
|
||||||
|
self.timer.reset();
|
||||||
|
Ok(self.rx_downlink(&Frame::Join, ms).await?.try_into()?)
|
||||||
|
}
|
||||||
|
JoinMode::ABP { newskey, appskey, devaddr } => {
|
||||||
|
self.mac.join_abp(*newskey, *appskey, *devaddr);
|
||||||
|
Ok(JoinResponse::JoinSuccess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send data on a given port with the expected confirmation. If downlink data is provided, the
|
||||||
|
/// data is copied into the provided byte slice.
|
||||||
|
///
|
||||||
|
/// The returned future completes when the data have been sent successfully and downlink data,
|
||||||
|
/// if any, is available by calling take_downlink. Response::DownlinkReceived indicates a
|
||||||
|
/// downlink is available.
|
||||||
|
///
|
||||||
|
/// In Class C mode, it is possible to get one or more downlinks and `Reponse::DownlinkReceived`
|
||||||
|
/// maybe not even be indicated. It is recommended to call `take_downlink` after `send` until
|
||||||
|
/// it returns `None`.
|
||||||
|
pub async fn send(
|
||||||
|
&mut self,
|
||||||
|
data: &[u8],
|
||||||
|
fport: u8,
|
||||||
|
confirmed: bool,
|
||||||
|
) -> Result<SendResponse, Error<R::PhyError>> {
|
||||||
|
// Prepare transmission buffer
|
||||||
|
let (tx_config, _fcnt_up) = self.mac.send::<C, G, N>(
|
||||||
|
&mut self.rng,
|
||||||
|
&mut self.radio_buffer,
|
||||||
|
&SendData { data, fport, confirmed },
|
||||||
|
)?;
|
||||||
|
// Transmit our data packet
|
||||||
|
let ms = self
|
||||||
|
.radio
|
||||||
|
.tx(tx_config, self.radio_buffer.as_ref_for_read())
|
||||||
|
.await
|
||||||
|
.map_err(Error::Radio)?;
|
||||||
|
|
||||||
|
// Wait for received data within window
|
||||||
|
self.timer.reset();
|
||||||
|
Ok(self.rx_downlink(&Frame::Data, ms).await?.try_into()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take the downlink data from the device. This is typically called after a
|
||||||
|
/// `Response::DownlinkReceived` is returned from `send`. This call consumes the downlink
|
||||||
|
/// data. If no downlink data is available, `None` is returned.
|
||||||
|
pub fn take_downlink(&mut self) -> Option<Downlink> {
|
||||||
|
self.downlink.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn window_complete(&mut self) -> Result<(), Error<R::PhyError>> {
|
||||||
|
if self.class_c {
|
||||||
|
let rf_config = self.mac.get_rxc_config();
|
||||||
|
self.radio.setup_rx(rf_config).await.map_err(Error::Radio)
|
||||||
|
} else {
|
||||||
|
self.radio.low_power().await.map_err(Error::Radio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn between_windows(
|
||||||
|
&mut self,
|
||||||
|
duration: u32,
|
||||||
|
) -> Result<Option<mac::Response>, Error<R::PhyError>> {
|
||||||
|
if !self.class_c {
|
||||||
|
self.radio.low_power().await.map_err(Error::Radio)?;
|
||||||
|
self.timer.at(duration.into()).await;
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
enum RxcWindowResponse<F: futures::Future<Output=()> + Sized + Unpin> {
|
||||||
|
Rx(usize, RxQuality, F),
|
||||||
|
Timeout(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RXC window listen until timeout
|
||||||
|
async fn rxc_listen_until_timeout<F, R, const N: usize>(
|
||||||
|
radio: &mut R,
|
||||||
|
rx_buf: &mut RadioBuffer<N>,
|
||||||
|
window_duration: u32,
|
||||||
|
timeout_fut: F,
|
||||||
|
) -> RxcWindowResponse<F>
|
||||||
|
where
|
||||||
|
F: futures::Future<Output=()> + Sized + Unpin,
|
||||||
|
R: radio::PhyRxTx + Timings,
|
||||||
|
{
|
||||||
|
let rx_fut = radio.rx_continuous(rx_buf.as_mut());
|
||||||
|
pin_mut!(rx_fut);
|
||||||
|
// Wait until either a RF frame is received or the timeout future fires
|
||||||
|
match select(rx_fut, timeout_fut).await {
|
||||||
|
Either::Left((r, timeout_fut)) => match r {
|
||||||
|
Ok((sz, q)) => RxcWindowResponse::Rx(sz, q, timeout_fut),
|
||||||
|
// Ignore errors or timeouts and wait until the RX2 window is ready.
|
||||||
|
// Setting timeout to 0 ensures that `window_duration != rx2_start_delay`
|
||||||
|
_ => {
|
||||||
|
timeout_fut.await;
|
||||||
|
RxcWindowResponse::Timeout(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Timeout! Prepare for the next window.
|
||||||
|
Either::Right(_) => RxcWindowResponse::Timeout(window_duration),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class C listen while waiting for the window
|
||||||
|
let rx_config = self.mac.get_rxc_config();
|
||||||
|
log::debug!("Configuring RXC window with config {}.", rx_config);
|
||||||
|
self.radio.setup_rx(rx_config).await.map_err(Error::Radio)?;
|
||||||
|
let mut response = None;
|
||||||
|
let timeout_fut = self.timer.at(duration.into());
|
||||||
|
pin_mut!(timeout_fut);
|
||||||
|
let mut maybe_timeout_fut = Some(timeout_fut);
|
||||||
|
|
||||||
|
// Keep processing RF frames until the timeout fires
|
||||||
|
while let Some(timeout_fut) = maybe_timeout_fut.take() {
|
||||||
|
match rxc_listen_until_timeout(
|
||||||
|
&mut self.radio,
|
||||||
|
&mut self.radio_buffer,
|
||||||
|
duration,
|
||||||
|
timeout_fut,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
RxcWindowResponse::Rx(sz, _, timeout_fut) => {
|
||||||
|
log::debug!("RXC window received {} bytes.", sz);
|
||||||
|
self.radio_buffer.set_pos(sz);
|
||||||
|
match self
|
||||||
|
.mac
|
||||||
|
.handle_rxc::<C, N, D>(&mut self.radio_buffer, &mut self.downlink)?
|
||||||
|
{
|
||||||
|
mac::Response::NoUpdate => {
|
||||||
|
log::debug!("RXC frame was invalid.");
|
||||||
|
self.radio_buffer.clear();
|
||||||
|
// we preserve the timeout
|
||||||
|
maybe_timeout_fut = Some(timeout_fut);
|
||||||
|
}
|
||||||
|
r => {
|
||||||
|
log::debug!("Valid RXC frame received.");
|
||||||
|
self.radio_buffer.clear();
|
||||||
|
response = Some(r);
|
||||||
|
// more than one downlink may be received so we preserve the timeout
|
||||||
|
maybe_timeout_fut = Some(timeout_fut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RxcWindowResponse::Timeout(_) => return Ok(response),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive data within RX1 and RX2 windows. This function will populate the
|
||||||
|
/// provided buffer with data if received.
|
||||||
|
async fn rx_downlink(
|
||||||
|
&mut self,
|
||||||
|
frame: &Frame,
|
||||||
|
window_delay: u32,
|
||||||
|
) -> Result<mac::Response, Error<R::PhyError>> {
|
||||||
|
self.radio_buffer.clear();
|
||||||
|
|
||||||
|
let rx1_start_delay = self.mac.get_rx_delay(frame, &Window::_1) + window_delay
|
||||||
|
- self.radio.get_rx_window_lead_time_ms();
|
||||||
|
|
||||||
|
log::debug!("Starting RX1 in {} ms.", rx1_start_delay);
|
||||||
|
// sleep or RXC
|
||||||
|
let _ = self.between_windows(rx1_start_delay).await?;
|
||||||
|
|
||||||
|
// RX1
|
||||||
|
let rx_config =
|
||||||
|
self.mac.get_rx_config(self.radio.get_rx_window_buffer(), frame, &Window::_1);
|
||||||
|
log::debug!("Configuring RX1 window with config {}.", rx_config);
|
||||||
|
self.radio.setup_rx(rx_config).await.map_err(Error::Radio)?;
|
||||||
|
|
||||||
|
if let Some(response) = self.rx_listen().await? {
|
||||||
|
log::debug!("RX1 received {}", response);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rx2_start_delay = self.mac.get_rx_delay(frame, &Window::_2) + window_delay
|
||||||
|
- self.radio.get_rx_window_lead_time_ms();
|
||||||
|
log::debug!("RX1 did not receive anything. Awaiting RX2 for {} ms.", rx2_start_delay);
|
||||||
|
// sleep or RXC
|
||||||
|
let _ = self.between_windows(rx2_start_delay).await?;
|
||||||
|
|
||||||
|
// RX2
|
||||||
|
let rx_config =
|
||||||
|
self.mac.get_rx_config(self.radio.get_rx_window_buffer(), frame, &Window::_2);
|
||||||
|
log::debug!("Configuring RX2 window with config {}.", rx_config);
|
||||||
|
self.radio.setup_rx(rx_config).await.map_err(Error::Radio)?;
|
||||||
|
|
||||||
|
if let Some(response) = self.rx_listen().await? {
|
||||||
|
log::debug!("RX2 received {}", response);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
log::debug!("RX2 did not receive anything.");
|
||||||
|
Ok(self.mac.rx2_complete())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rx_listen(&mut self) -> Result<Option<mac::Response>, Error<R::PhyError>> {
|
||||||
|
let response =
|
||||||
|
match self.radio.rx_single(self.radio_buffer.as_mut()).await.map_err(Error::Radio)? {
|
||||||
|
RxStatus::Rx(s, _q) => {
|
||||||
|
self.radio_buffer.set_pos(s);
|
||||||
|
match self.mac.handle_rx::<C, N, D>(&mut self.radio_buffer, &mut self.downlink)
|
||||||
|
{
|
||||||
|
mac::Response::NoUpdate => None,
|
||||||
|
r => Some(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RxStatus::RxTimeout => None,
|
||||||
|
};
|
||||||
|
self.radio_buffer.clear();
|
||||||
|
self.window_complete().await?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When not involved in sending and RX1/RX2 windows, a class C configured device will be
|
||||||
|
/// listening to RXC frames. The caller is expected to be awaiting this message at all times.
|
||||||
|
pub async fn rxc_listen(&mut self) -> Result<mac::Response, Error<R::PhyError>> {
|
||||||
|
loop {
|
||||||
|
let (sz, _rx_quality) =
|
||||||
|
self.radio.rx_continuous(self.radio_buffer.as_mut()).await.map_err(Error::Radio)?;
|
||||||
|
self.radio_buffer.set_pos(sz);
|
||||||
|
match self.mac.handle_rxc::<C, N, D>(&mut self.radio_buffer, &mut self.downlink)? {
|
||||||
|
mac::Response::NoUpdate => {
|
||||||
|
self.radio_buffer.clear();
|
||||||
|
}
|
||||||
|
r => {
|
||||||
|
self.radio_buffer.clear();
|
||||||
|
return Ok(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows to fine-tune the beginning and end of the receive windows for a specific board and runtime.
|
||||||
|
pub trait Timings {
|
||||||
|
/// How many milliseconds before the RX window should the SPI transaction start?
|
||||||
|
/// This value needs to account for the time it takes to wake up the radio and start the SPI transaction, as
|
||||||
|
/// well as any non-deterministic delays in the system.
|
||||||
|
fn get_rx_window_lead_time_ms(&self) -> u32;
|
||||||
|
|
||||||
|
/// Explicitly set the amount of milliseconds to listen before the window starts. By default, the pessimistic assumption
|
||||||
|
/// of `Self::get_rx_window_lead_time_ms` will be used. If you override, be sure that: `Self::get_rx_window_buffer
|
||||||
|
/// < Self::get_rx_window_lead_time_ms`.
|
||||||
|
fn get_rx_window_buffer(&self) -> u32 {
|
||||||
|
self.get_rx_window_lead_time_ms()
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lorawan-device-patch/src/async_device/radio.rs
Normal file
69
lorawan-device-patch/src/async_device/radio.rs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
pub use crate::radio::{RfConfig, RxConfig, RxMode, RxQuality, TxConfig};
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub struct Error<E>(pub E);
|
||||||
|
|
||||||
|
impl<R> From<Error<R>> for super::Error<R> {
|
||||||
|
fn from(radio_error: Error<R>) -> super::Error<R> {
|
||||||
|
super::Error::Radio(radio_error.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RxStatus {
|
||||||
|
Rx(usize, RxQuality),
|
||||||
|
RxTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An asynchronous timer that allows the state machine to await
|
||||||
|
/// between RX windows.
|
||||||
|
#[allow(async_fn_in_trait)]
|
||||||
|
pub trait Timer {
|
||||||
|
fn reset(&mut self);
|
||||||
|
|
||||||
|
/// Wait until millis milliseconds after reset has passed
|
||||||
|
async fn at(&mut self, millis: u64);
|
||||||
|
|
||||||
|
/// Delay for millis milliseconds
|
||||||
|
async fn delay_ms(&mut self, millis: u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An asynchronous radio implementation that can transmit and receive data.
|
||||||
|
#[allow(async_fn_in_trait)]
|
||||||
|
pub trait PhyRxTx: Sized {
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
type PhyError: defmt::Format;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
type PhyError;
|
||||||
|
|
||||||
|
/// Board-specific antenna gain and power loss in dBi.
|
||||||
|
const ANTENNA_GAIN: i8 = 0;
|
||||||
|
|
||||||
|
/// Maximum power (dBm) that the radio is able to output. When preparing instructions for radio,
|
||||||
|
/// the value of maximum power will be used as an upper bound.
|
||||||
|
const MAX_RADIO_POWER: u8;
|
||||||
|
|
||||||
|
/// Transmit data buffer with the given transceiver configuration. The returned future
|
||||||
|
/// should only complete once data have been transmitted.
|
||||||
|
async fn tx(&mut self, config: TxConfig, buf: &[u8]) -> Result<u32, Self::PhyError>;
|
||||||
|
|
||||||
|
/// Configures the radio to receive data. This future should not actually await the data itself.
|
||||||
|
async fn setup_rx(&mut self, config: RxConfig) -> Result<(), Self::PhyError>;
|
||||||
|
|
||||||
|
/// Receive data into the provided buffer with the given transceiver configuration. The returned
|
||||||
|
/// future should only complete when RX data has been received. Furthermore, it should be
|
||||||
|
/// possible to await the future again without settings up the receive config again.
|
||||||
|
async fn rx_continuous(
|
||||||
|
&mut self,
|
||||||
|
rx_buf: &mut [u8],
|
||||||
|
) -> Result<(usize, RxQuality), Self::PhyError>;
|
||||||
|
|
||||||
|
/// Receive data into the provided buffer with the given transceiver configuration. The returned
|
||||||
|
/// future should complete when RX data has been received or when the timeout has expired.
|
||||||
|
async fn rx_single(&mut self, buf: &mut [u8]) -> Result<RxStatus, Self::PhyError>;
|
||||||
|
|
||||||
|
/// Puts the radio into a low-power mode
|
||||||
|
async fn low_power(&mut self) -> Result<(), Self::PhyError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
329
lorawan-device-patch/src/async_device/test/mod.rs
Normal file
329
lorawan-device-patch/src/async_device/test/mod.rs
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
radio::{RxQuality, TxConfig},
|
||||||
|
region,
|
||||||
|
test_util::*,
|
||||||
|
};
|
||||||
|
use lorawan::default_crypto::DefaultFactory;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
mod timer;
|
||||||
|
use timer::TestTimer;
|
||||||
|
|
||||||
|
mod radio;
|
||||||
|
use radio::TestRadio;
|
||||||
|
|
||||||
|
mod util;
|
||||||
|
use util::{setup, setup_with_session, setup_with_session_class_c};
|
||||||
|
|
||||||
|
type Device =
|
||||||
|
crate::async_device::Device<TestRadio, DefaultFactory, TestTimer, rand_core::OsRng, 512, 4>;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_join_rx1() {
|
||||||
|
let (radio, timer, mut async_device) = setup();
|
||||||
|
// Run the device
|
||||||
|
let async_device =
|
||||||
|
tokio::spawn(async move { async_device.join(&get_otaa_credentials()).await });
|
||||||
|
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
// Trigger handling of JoinAccept
|
||||||
|
radio.handle_rxtx(handle_join_request::<3>).await;
|
||||||
|
|
||||||
|
// Await the device to return and verify state
|
||||||
|
if let Ok(JoinResponse::JoinSuccess) = async_device.await.unwrap() {
|
||||||
|
assert_eq!(1, timer.get_armed_count().await);
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_join_rx2() {
|
||||||
|
let (radio, timer, mut async_device) = setup();
|
||||||
|
// Run the device
|
||||||
|
let async_device =
|
||||||
|
tokio::spawn(async move { async_device.join(&get_otaa_credentials()).await });
|
||||||
|
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
// Trigger end of RX1
|
||||||
|
radio.handle_timeout().await;
|
||||||
|
// Trigger start of RX2
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
// Pass the join request handler
|
||||||
|
radio.handle_rxtx(handle_join_request::<4>).await;
|
||||||
|
|
||||||
|
// Await the device to return and verify state
|
||||||
|
if async_device.await.unwrap().is_ok() {
|
||||||
|
assert_eq!(2, timer.get_armed_count().await);
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_no_join_accept() {
|
||||||
|
let (radio, timer, mut async_device) = setup();
|
||||||
|
// Run the device
|
||||||
|
let async_device =
|
||||||
|
tokio::spawn(async move { async_device.join(&get_otaa_credentials()).await });
|
||||||
|
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
// Trigger end of RX1
|
||||||
|
radio.handle_timeout().await;
|
||||||
|
// Trigger start of RX2
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
// Trigger end of RX2
|
||||||
|
radio.handle_timeout().await;
|
||||||
|
|
||||||
|
// Await the device to return and verify state
|
||||||
|
let response = async_device.await.unwrap();
|
||||||
|
if let Ok(JoinResponse::NoJoinAccept) = response {
|
||||||
|
assert_eq!(2, timer.get_armed_count().await);
|
||||||
|
} else {
|
||||||
|
panic!("Unexpected response: {response:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_unconfirmed_uplink_no_downlink() {
|
||||||
|
let (radio, timer, mut async_device) = setup_with_session();
|
||||||
|
let send_await_complete = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
// Run the device
|
||||||
|
let complete = send_await_complete.clone();
|
||||||
|
let async_device = tokio::spawn(async move {
|
||||||
|
let response = async_device.send(&[1, 2, 3], 3, false).await;
|
||||||
|
|
||||||
|
let mut complete = complete.lock().await;
|
||||||
|
*complete = true;
|
||||||
|
response
|
||||||
|
});
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
assert!(!*send_await_complete.lock().await);
|
||||||
|
// Trigger end of RX1
|
||||||
|
radio.handle_timeout().await;
|
||||||
|
// Trigger start of RX2
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
assert!(!*send_await_complete.lock().await);
|
||||||
|
// Trigger end of RX2
|
||||||
|
radio.handle_timeout().await;
|
||||||
|
|
||||||
|
match async_device.await.unwrap() {
|
||||||
|
Ok(SendResponse::RxComplete) => (),
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
assert!(*send_await_complete.lock().await);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_confirmed_uplink_no_ack() {
|
||||||
|
let (radio, timer, mut async_device) = setup_with_session();
|
||||||
|
let send_await_complete = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
// Run the device
|
||||||
|
let complete = send_await_complete.clone();
|
||||||
|
let async_device = tokio::spawn(async move {
|
||||||
|
let response = async_device.send(&[1, 2, 3], 3, true).await;
|
||||||
|
|
||||||
|
let mut complete = complete.lock().await;
|
||||||
|
*complete = true;
|
||||||
|
response
|
||||||
|
});
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
assert!(!*send_await_complete.lock().await);
|
||||||
|
// Trigger end of RX1
|
||||||
|
radio.handle_timeout().await;
|
||||||
|
// Trigger start of RX2
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
assert!(!*send_await_complete.lock().await);
|
||||||
|
// Trigger end of RX1
|
||||||
|
radio.handle_timeout().await;
|
||||||
|
|
||||||
|
match async_device.await.unwrap() {
|
||||||
|
Ok(SendResponse::NoAck) => (),
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
assert!(*send_await_complete.lock().await);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_confirmed_uplink_with_ack_rx1() {
|
||||||
|
let (radio, timer, mut async_device) = setup_with_session();
|
||||||
|
let send_await_complete = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
// Run the device
|
||||||
|
let complete = send_await_complete.clone();
|
||||||
|
let async_device = tokio::spawn(async move {
|
||||||
|
let response = async_device.send(&[1, 2, 3], 3, true).await;
|
||||||
|
|
||||||
|
let mut complete = complete.lock().await;
|
||||||
|
*complete = true;
|
||||||
|
response
|
||||||
|
});
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
assert!(!*send_await_complete.lock().await);
|
||||||
|
|
||||||
|
// Send a downlink with confirmation
|
||||||
|
radio.handle_rxtx(handle_data_uplink_with_link_adr_req::<0, 0>).await;
|
||||||
|
match async_device.await.unwrap() {
|
||||||
|
Ok(SendResponse::DownlinkReceived(_)) => (),
|
||||||
|
_ => {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_confirmed_uplink_with_ack_rx2() {
|
||||||
|
let (radio, timer, mut async_device) = setup_with_session();
|
||||||
|
let send_await_complete = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
// Run the device
|
||||||
|
let complete = send_await_complete.clone();
|
||||||
|
let async_device = tokio::spawn(async move {
|
||||||
|
let response = async_device.send(&[1, 2, 3], 3, true).await;
|
||||||
|
|
||||||
|
let mut complete = complete.lock().await;
|
||||||
|
*complete = true;
|
||||||
|
response
|
||||||
|
});
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
assert!(!*send_await_complete.lock().await);
|
||||||
|
// Trigger end of RX1
|
||||||
|
radio.handle_timeout().await;
|
||||||
|
assert!(!*send_await_complete.lock().await);
|
||||||
|
// Trigger start of RX2
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
|
||||||
|
// Send a downlink confirmation
|
||||||
|
radio.handle_rxtx(handle_data_uplink_with_link_adr_req::<0, 0>).await;
|
||||||
|
|
||||||
|
match async_device.await.unwrap() {
|
||||||
|
Ok(SendResponse::DownlinkReceived(_)) => (),
|
||||||
|
_ => {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_link_adr_ans() {
|
||||||
|
let (radio, timer, mut async_device) = setup_with_session();
|
||||||
|
let send_await_complete = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
// Run the device
|
||||||
|
let complete = send_await_complete.clone();
|
||||||
|
let async_device = tokio::spawn(async move {
|
||||||
|
async_device.send(&[1, 2, 3], 3, true).await.unwrap();
|
||||||
|
{
|
||||||
|
let mut complete = complete.lock().await;
|
||||||
|
*complete = true;
|
||||||
|
}
|
||||||
|
async_device.send(&[1, 2, 3], 3, true).await
|
||||||
|
});
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
// Send a downlink with confirmation
|
||||||
|
radio.handle_rxtx(handle_data_uplink_with_link_adr_req::<0, 0>).await;
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(15)).await;
|
||||||
|
assert!(*send_await_complete.lock().await);
|
||||||
|
// at this point, the device thread should be sending the second frame
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
// Send a downlink with confirmation
|
||||||
|
radio.handle_rxtx(handle_data_uplink_with_link_adr_ans).await;
|
||||||
|
match async_device.await.unwrap() {
|
||||||
|
Ok(SendResponse::DownlinkReceived(_)) => (),
|
||||||
|
_ => {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_class_c_data_before_rx1() {
|
||||||
|
let (radio, timer, mut async_device) = setup_with_session_class_c().await;
|
||||||
|
// Run the device
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let response = async_device.send(&[1, 2, 3], 3, true).await;
|
||||||
|
(async_device, response)
|
||||||
|
});
|
||||||
|
|
||||||
|
// send first downlink before RX1
|
||||||
|
radio.handle_rxtx(class_c_downlink::<1>).await;
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
// We expect FCntUp 1 up since the test util for Class C setup sends first frame
|
||||||
|
// We set FcntDown to 2, since ACK to setup (1) and Class C downlink above (2)
|
||||||
|
radio.handle_rxtx(handle_data_uplink_with_link_adr_req::<1, 2>).await;
|
||||||
|
let (mut device, response) = task.await.unwrap();
|
||||||
|
match response {
|
||||||
|
Ok(SendResponse::DownlinkReceived(_)) => (),
|
||||||
|
_ => {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = device.take_downlink().unwrap();
|
||||||
|
let _ = device.take_downlink().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_class_c_data_before_rx2() {
|
||||||
|
let (radio, timer, mut async_device) = setup_with_session_class_c().await;
|
||||||
|
// Run the device
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let response = async_device.send(&[1, 2, 3], 3, true).await;
|
||||||
|
(async_device, response)
|
||||||
|
});
|
||||||
|
|
||||||
|
// send first downlink before RX1
|
||||||
|
// Trigger beginning of RX1
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
// Trigger end of RX1
|
||||||
|
radio.handle_timeout().await;
|
||||||
|
|
||||||
|
radio.handle_rxtx(class_c_downlink::<1>).await;
|
||||||
|
// Trigger beginning of RX2
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
// We expect FCntUp 1 up since the test util for Class C setup sends first frame
|
||||||
|
// We set FcntDown to 2, since ACK to setup (1) and Class C downlink above (2)
|
||||||
|
radio.handle_rxtx(handle_data_uplink_with_link_adr_req::<1, 2>).await;
|
||||||
|
let (mut device, response) = task.await.unwrap();
|
||||||
|
match response {
|
||||||
|
Ok(SendResponse::DownlinkReceived(_)) => (),
|
||||||
|
_ => {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = device.take_downlink().unwrap();
|
||||||
|
let _ = device.take_downlink().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_class_c_async_down() {
|
||||||
|
let (radio, _timer, mut async_device) = setup_with_session_class_c().await;
|
||||||
|
// Run the device
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let response = async_device.rxc_listen().await;
|
||||||
|
(async_device, response)
|
||||||
|
});
|
||||||
|
|
||||||
|
radio.handle_rxtx(class_c_downlink::<1>).await;
|
||||||
|
let (mut device, response) = task.await.unwrap();
|
||||||
|
match response {
|
||||||
|
Ok(mac::Response::DownlinkReceived(_)) => (),
|
||||||
|
_ => {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = device.take_downlink().unwrap();
|
||||||
|
}
|
||||||
111
lorawan-device-patch/src/async_device/test/radio.rs
Normal file
111
lorawan-device-patch/src/async_device/test/radio.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use crate::async_device::radio::{PhyRxTx, RxConfig, RxStatus};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::{
|
||||||
|
sync::{mpsc, Mutex},
|
||||||
|
time,
|
||||||
|
};
|
||||||
|
impl TestRadio {
|
||||||
|
pub fn new() -> (RadioChannel, Self) {
|
||||||
|
let (tx, rx) = mpsc::channel(2);
|
||||||
|
let last_uplink = Arc::new(Mutex::new(None));
|
||||||
|
(
|
||||||
|
RadioChannel { tx, last_uplink: last_uplink.clone() },
|
||||||
|
Self { rx, last_uplink, current_config: None },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Msg {
|
||||||
|
RxTx(RxTxHandler),
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestRadio {
|
||||||
|
current_config: Option<RxConfig>,
|
||||||
|
last_uplink: Arc<Mutex<Option<Uplink>>>,
|
||||||
|
rx: mpsc::Receiver<Msg>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PhyRxTx for TestRadio {
|
||||||
|
type PhyError = &'static str;
|
||||||
|
|
||||||
|
const MAX_RADIO_POWER: u8 = 26;
|
||||||
|
|
||||||
|
const ANTENNA_GAIN: i8 = 0;
|
||||||
|
|
||||||
|
async fn tx(&mut self, config: TxConfig, buffer: &[u8]) -> Result<u32, Self::PhyError> {
|
||||||
|
let length = buffer.len();
|
||||||
|
// stash the uplink, to be consumed by channel or by rx handler
|
||||||
|
let mut last_uplink = self.last_uplink.lock().await;
|
||||||
|
*last_uplink = Some(Uplink::new(buffer, config).map_err(|_| "Parse error")?);
|
||||||
|
Ok(length as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_rx(&mut self, config: RxConfig) -> Result<(), Self::PhyError> {
|
||||||
|
self.current_config = Some(config);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rx_continuous(
|
||||||
|
&mut self,
|
||||||
|
rx_buf: &mut [u8],
|
||||||
|
) -> Result<(usize, RxQuality), Self::PhyError> {
|
||||||
|
let msg = self.rx.recv().await.unwrap();
|
||||||
|
match msg {
|
||||||
|
Msg::RxTx(handler) => {
|
||||||
|
let last_uplink = self.last_uplink.lock().await;
|
||||||
|
// a quick yield to let timer arm
|
||||||
|
time::sleep(time::Duration::from_millis(5)).await;
|
||||||
|
if let Some(config) = &self.current_config {
|
||||||
|
let length = handler(last_uplink.clone(), config.rf, rx_buf);
|
||||||
|
Ok((length, RxQuality::new(-80, 0)))
|
||||||
|
} else {
|
||||||
|
panic!("Trying to rx before settings config!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::Timeout => Err("Unexpected Timeout"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn rx_single(&mut self, rx_buf: &mut [u8]) -> Result<RxStatus, Self::PhyError> {
|
||||||
|
let msg = self.rx.recv().await.unwrap();
|
||||||
|
match msg {
|
||||||
|
Msg::RxTx(handler) => {
|
||||||
|
let last_uplink = self.last_uplink.lock().await;
|
||||||
|
// a quick yield to let timer arm
|
||||||
|
time::sleep(time::Duration::from_millis(5)).await;
|
||||||
|
if let Some(config) = &self.current_config {
|
||||||
|
let length = handler(last_uplink.clone(), config.rf, rx_buf);
|
||||||
|
Ok(RxStatus::Rx(length, RxQuality::new(-80, 0)))
|
||||||
|
} else {
|
||||||
|
panic!("Trying to rx before settings config!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::Timeout => Ok(RxStatus::RxTimeout),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timings for TestRadio {
|
||||||
|
fn get_rx_window_lead_time_ms(&self) -> u32 {
|
||||||
|
10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A channel for the test fixture to trigger fires and to check calls.
|
||||||
|
pub struct RadioChannel {
|
||||||
|
#[allow(unused)]
|
||||||
|
last_uplink: Arc<Mutex<Option<Uplink>>>,
|
||||||
|
tx: mpsc::Sender<Msg>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RadioChannel {
|
||||||
|
pub async fn handle_rxtx(&self, handler: RxTxHandler) {
|
||||||
|
tokio::time::sleep(time::Duration::from_millis(5)).await;
|
||||||
|
self.tx.send(Msg::RxTx(handler)).await.unwrap();
|
||||||
|
}
|
||||||
|
pub async fn handle_timeout(&self) {
|
||||||
|
tokio::time::sleep(time::Duration::from_millis(5)).await;
|
||||||
|
self.tx.send(Msg::Timeout).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
73
lorawan-device-patch/src/async_device/test/timer.rs
Normal file
73
lorawan-device-patch/src/async_device/test/timer.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
use crate::async_device::radio::Timer;
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
|
||||||
|
impl TestTimer {
|
||||||
|
pub fn new() -> (TimerChannel, Self) {
|
||||||
|
let tx = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
let armed_count = Arc::new(Mutex::new(0));
|
||||||
|
(
|
||||||
|
TimerChannel { tx: tx.clone(), armed_count: armed_count.clone() },
|
||||||
|
Self { tx, armed_count },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestTimer {
|
||||||
|
armed_count: Arc<Mutex<usize>>,
|
||||||
|
tx: Arc<Mutex<HashMap<usize, mpsc::Sender<()>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestTimer {
|
||||||
|
async fn create_channel_and_await(&mut self) {
|
||||||
|
let (tx, mut rx) = mpsc::channel(1);
|
||||||
|
{
|
||||||
|
*self.armed_count.lock().await += 1;
|
||||||
|
let mut tx_map = self.tx.lock().await;
|
||||||
|
tx_map.insert(*self.armed_count.lock().await, tx);
|
||||||
|
}
|
||||||
|
rx.recv().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timer for TestTimer {
|
||||||
|
fn reset(&mut self) {}
|
||||||
|
|
||||||
|
async fn at(&mut self, _millis: u64) {
|
||||||
|
self.create_channel_and_await().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delay_ms(&mut self, _millis: u64) {
|
||||||
|
self.create_channel_and_await().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A channel for the test fixture to trigger fires and to check calls.
|
||||||
|
pub struct TimerChannel {
|
||||||
|
armed_count: Arc<Mutex<usize>>,
|
||||||
|
tx: Arc<Mutex<HashMap<usize, mpsc::Sender<()>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimerChannel {
|
||||||
|
pub async fn fire_most_recent(&self) {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||||
|
let mut tx_map = self.tx.lock().await;
|
||||||
|
let armed_count = *self.armed_count.lock().await;
|
||||||
|
let tx = tx_map.remove(&armed_count).unwrap();
|
||||||
|
tx.send(()).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub async fn confirm_dropped_timer(&self, index: usize) {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||||
|
let mut tx_map = self.tx.lock().await;
|
||||||
|
let tx = tx_map.remove(&index).unwrap();
|
||||||
|
if tx.try_send(()).is_ok() {
|
||||||
|
panic!("Timer was not dropped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_armed_count(&self) -> usize {
|
||||||
|
*self.armed_count.lock().await
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lorawan-device-patch/src/async_device/test/util.rs
Normal file
57
lorawan-device-patch/src/async_device/test/util.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use super::{get_dev_addr, get_key, region, Device, SendResponse};
|
||||||
|
|
||||||
|
use crate::mac::Session;
|
||||||
|
use crate::test_util::handle_class_c_uplink_after_join;
|
||||||
|
use crate::{AppSKey, NewSKey};
|
||||||
|
|
||||||
|
fn setup_internal(session_data: Option<Session>) -> (RadioChannel, TimerChannel, Device) {
|
||||||
|
let (radio_channel, mock_radio) = TestRadio::new();
|
||||||
|
let (timer_channel, mock_timer) = TestTimer::new();
|
||||||
|
let region = region::US915::default();
|
||||||
|
let async_device = Device::new_with_session(
|
||||||
|
region.into(),
|
||||||
|
mock_radio,
|
||||||
|
mock_timer,
|
||||||
|
rand::rngs::OsRng,
|
||||||
|
session_data,
|
||||||
|
);
|
||||||
|
(radio_channel, timer_channel, async_device)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_with_session() -> (RadioChannel, TimerChannel, Device) {
|
||||||
|
setup_internal(Some(Session {
|
||||||
|
newskey: NewSKey::from(get_key()),
|
||||||
|
appskey: AppSKey::from(get_key()),
|
||||||
|
devaddr: get_dev_addr(),
|
||||||
|
fcnt_up: 0,
|
||||||
|
fcnt_down: 0,
|
||||||
|
confirmed: false,
|
||||||
|
uplink: Default::default(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup_with_session_class_c() -> (RadioChannel, TimerChannel, Device) {
|
||||||
|
let (radio, timer, mut async_device) = setup_with_session();
|
||||||
|
async_device.enable_class_c();
|
||||||
|
// Run the device
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let response = async_device.send(&[3, 2, 1], 3, false).await;
|
||||||
|
(async_device, response)
|
||||||
|
});
|
||||||
|
// timeout the first sends RX windows which enables class C
|
||||||
|
timer.fire_most_recent().await;
|
||||||
|
radio.handle_rxtx(handle_class_c_uplink_after_join).await;
|
||||||
|
|
||||||
|
let (device, response) = task.await.unwrap();
|
||||||
|
match response {
|
||||||
|
Ok(SendResponse::DownlinkReceived(0)) => (),
|
||||||
|
_ => {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(radio, timer, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup() -> (RadioChannel, TimerChannel, Device) {
|
||||||
|
setup_internal(None)
|
||||||
|
}
|
||||||
77
lorawan-device-patch/src/lib.rs
Normal file
77
lorawan-device-patch/src/lib.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#![cfg_attr(not(test), no_std)]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
//! ## Feature flags
|
||||||
|
#![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
|
use core::default::Default;
|
||||||
|
use heapless::Vec;
|
||||||
|
|
||||||
|
mod radio;
|
||||||
|
|
||||||
|
pub mod mac;
|
||||||
|
use mac::NetworkCredentials;
|
||||||
|
|
||||||
|
pub mod region;
|
||||||
|
pub use region::Region;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_util;
|
||||||
|
|
||||||
|
pub mod async_device;
|
||||||
|
|
||||||
|
pub mod nb_device;
|
||||||
|
use nb_device::state::State;
|
||||||
|
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
#[cfg(feature = "default-crypto")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "default-crypto")))]
|
||||||
|
pub use lorawan::default_crypto;
|
||||||
|
pub use lorawan::{
|
||||||
|
keys::{AppEui, AppKey, AppSKey, CryptoFactory, DevEui, NewSKey},
|
||||||
|
parser::DevAddr,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use rand_core::RngCore;
|
||||||
|
mod rng;
|
||||||
|
pub use rng::Prng;
|
||||||
|
|
||||||
|
mod log;
|
||||||
|
|
||||||
|
/// Provides the application payload and FPort of a downlink message.
|
||||||
|
pub struct Downlink {
|
||||||
|
pub data: Vec<u8, 256>,
|
||||||
|
pub fport: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
impl defmt::Format for Downlink {
|
||||||
|
fn format(&self, f: defmt::Formatter) {
|
||||||
|
defmt::write!(f, "Downlink {{ fport: {}, data: ", self.fport, );
|
||||||
|
|
||||||
|
for byte in self.data.iter() {
|
||||||
|
defmt::write!(f, "{:02x}", byte);
|
||||||
|
}
|
||||||
|
defmt::write!(f, " }}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows to fine-tune the beginning and end of the receive windows for a specific board.
|
||||||
|
pub trait Timings {
|
||||||
|
/// The offset in milliseconds from the beginning of the receive windows. For example, settings this to 100
|
||||||
|
/// tell the LoRaWAN stack to begin configuring the receive window 100 ms before the window needs to start.
|
||||||
|
fn get_rx_window_offset_ms(&self) -> i32;
|
||||||
|
|
||||||
|
/// How long to leave the receive window open in milliseconds. For example, if offset was set to 100 and duration
|
||||||
|
/// was set to 200, the window would be open 100 ms before and close 100 ms after the target time.
|
||||||
|
fn get_rx_window_duration_ms(&self) -> u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
/// Join the network using either OTAA or ABP.
|
||||||
|
pub enum JoinMode {
|
||||||
|
OTAA { deveui: DevEui, appeui: AppEui, appkey: AppKey },
|
||||||
|
ABP { newskey: NewSKey, appskey: AppSKey, devaddr: DevAddr<[u8; 4]> },
|
||||||
|
}
|
||||||
35
lorawan-device-patch/src/log.rs
Normal file
35
lorawan-device-patch/src/log.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#![allow(unused_macros)]
|
||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
macro_rules! llog {
|
||||||
|
(trace, $($arg:expr),*) => { defmt::trace!($($arg),*) };
|
||||||
|
(debug, $($arg:expr),*) => { defmt::debug!($($arg),*) };
|
||||||
|
(info, $($arg:expr),*) => { defmt::info!($($arg),*) };
|
||||||
|
(error, $($arg:expr),*) => { defmt::error!($($arg),*) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
macro_rules! llog {
|
||||||
|
($level:ident, $($arg:expr),*) => {{ $( let _ = $arg; )* }}
|
||||||
|
}
|
||||||
|
pub(crate) use llog;
|
||||||
|
|
||||||
|
macro_rules! trace {
|
||||||
|
($($arg:expr),*) => (log::llog!(trace, $($arg),*));
|
||||||
|
}
|
||||||
|
pub(crate) use trace;
|
||||||
|
|
||||||
|
macro_rules! debug {
|
||||||
|
($($arg:expr),*) => (log::llog!(debug, $($arg),*));
|
||||||
|
}
|
||||||
|
pub(crate) use debug;
|
||||||
|
macro_rules! info {
|
||||||
|
($($arg:expr),*) => (log::llog!(info, $($arg),*));
|
||||||
|
}
|
||||||
|
pub(crate) use info;
|
||||||
|
|
||||||
|
macro_rules! error {
|
||||||
|
($($arg:expr),*) => (log::llog!(error, $($arg),*));
|
||||||
|
}
|
||||||
|
pub(crate) use error;
|
||||||
368
lorawan-device-patch/src/mac/mod.rs
Normal file
368
lorawan-device-patch/src/mac/mod.rs
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
//! LoRaWAN MAC layer implementation written as a non-async state machine (leveraged by `async_device` and `nb_device`).
|
||||||
|
//! Manages state internally while providing client with transmit and receive frequencies, while writing to and
|
||||||
|
//! decrypting from send and receive buffers.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
radio::{self, RadioBuffer, RfConfig, RxConfig, RxMode},
|
||||||
|
region, AppSKey, Downlink, NewSKey,
|
||||||
|
};
|
||||||
|
use heapless::Vec;
|
||||||
|
use lorawan::{self, keys::CryptoFactory};
|
||||||
|
use lorawan::{maccommands::DownlinkMacCommand, parser::DevAddr};
|
||||||
|
|
||||||
|
pub type FcntDown = u32;
|
||||||
|
pub type FcntUp = u32;
|
||||||
|
|
||||||
|
mod session;
|
||||||
|
use rand_core::RngCore;
|
||||||
|
pub use session::{Session, SessionKeys};
|
||||||
|
|
||||||
|
mod otaa;
|
||||||
|
pub use otaa::NetworkCredentials;
|
||||||
|
|
||||||
|
use crate::async_device;
|
||||||
|
use crate::nb_device;
|
||||||
|
|
||||||
|
pub(crate) mod uplink;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub(crate) enum Frame {
|
||||||
|
Join,
|
||||||
|
Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub(crate) enum Window {
|
||||||
|
_1,
|
||||||
|
_2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
/// LoRaWAN Session and Network Configurations
|
||||||
|
pub struct Configuration {
|
||||||
|
pub(crate) data_rate: region::DR,
|
||||||
|
rx1_delay: u32,
|
||||||
|
join_accept_delay1: u32,
|
||||||
|
join_accept_delay2: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
fn handle_downlink_macs(
|
||||||
|
&mut self,
|
||||||
|
region: &mut region::Configuration,
|
||||||
|
uplink: &mut uplink::Uplink,
|
||||||
|
cmds: lorawan::maccommands::MacCommandIterator<DownlinkMacCommand>,
|
||||||
|
) {
|
||||||
|
use uplink::MacAnsTrait;
|
||||||
|
for cmd in cmds {
|
||||||
|
match cmd {
|
||||||
|
DownlinkMacCommand::LinkADRReq(payload) => {
|
||||||
|
// we ignore DR and TxPwr
|
||||||
|
region.set_channel_mask(
|
||||||
|
payload.redundancy().channel_mask_control(),
|
||||||
|
payload.channel_mask(),
|
||||||
|
);
|
||||||
|
uplink.adr_ans.add();
|
||||||
|
}
|
||||||
|
DownlinkMacCommand::RXTimingSetupReq(payload) => {
|
||||||
|
self.rx1_delay = del_to_delay_ms(payload.delay());
|
||||||
|
uplink.ack_rx_delay();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Mac {
|
||||||
|
pub configuration: Configuration,
|
||||||
|
pub region: region::Configuration,
|
||||||
|
board_eirp: BoardEirp,
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BoardEirp {
|
||||||
|
max_power: u8,
|
||||||
|
antenna_gain: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
enum State {
|
||||||
|
Joined(Session),
|
||||||
|
Otaa(otaa::Otaa),
|
||||||
|
Unjoined,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub enum Error {
|
||||||
|
NotJoined,
|
||||||
|
InvalidResponse(Response),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SendData<'a> {
|
||||||
|
pub data: &'a [u8],
|
||||||
|
pub fport: u8,
|
||||||
|
pub confirmed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type Result<T = ()> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
|
impl Mac {
|
||||||
|
pub(crate) fn new(region: region::Configuration, max_power: u8, antenna_gain: i8) -> Self {
|
||||||
|
let data_rate = region.get_default_datarate();
|
||||||
|
Self {
|
||||||
|
board_eirp: BoardEirp { max_power, antenna_gain },
|
||||||
|
region,
|
||||||
|
state: State::Unjoined,
|
||||||
|
configuration: Configuration {
|
||||||
|
data_rate,
|
||||||
|
rx1_delay: region::constants::RECEIVE_DELAY1,
|
||||||
|
join_accept_delay1: region::constants::JOIN_ACCEPT_DELAY1,
|
||||||
|
join_accept_delay2: region::constants::JOIN_ACCEPT_DELAY2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare the radio buffer with transmitting a join request frame and provides the radio
|
||||||
|
/// configuration for the transmission.
|
||||||
|
pub(crate) fn join_otaa<C: CryptoFactory + Default, RNG: RngCore, const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
rng: &mut RNG,
|
||||||
|
credentials: NetworkCredentials,
|
||||||
|
buf: &mut RadioBuffer<N>,
|
||||||
|
) -> (radio::TxConfig, u16) {
|
||||||
|
let mut otaa = otaa::Otaa::new(credentials);
|
||||||
|
let dev_nonce = otaa.prepare_buffer::<C, RNG, N>(rng, buf);
|
||||||
|
self.state = State::Otaa(otaa);
|
||||||
|
let mut tx_config =
|
||||||
|
self.region.create_tx_config(rng, self.configuration.data_rate, &Frame::Join);
|
||||||
|
tx_config.adjust_power(self.board_eirp.max_power, self.board_eirp.antenna_gain);
|
||||||
|
(tx_config, dev_nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join via ABP. This does not transmit a join request frame, but instead sets the session.
|
||||||
|
pub(crate) fn join_abp(
|
||||||
|
&mut self,
|
||||||
|
newskey: NewSKey,
|
||||||
|
appskey: AppSKey,
|
||||||
|
devaddr: DevAddr<[u8; 4]>,
|
||||||
|
) {
|
||||||
|
self.state = State::Joined(Session::new(newskey, appskey, devaddr));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join via ABP. This does not transmit a join request frame, but instead sets the session.
|
||||||
|
pub(crate) fn set_session(&mut self, session: Session) {
|
||||||
|
self.state = State::Joined(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare the radio buffer for transmitting a data frame and provide the radio configuration
|
||||||
|
/// for the transmission. Returns an error if the device is not joined.
|
||||||
|
pub(crate) fn send<C: CryptoFactory + Default, RNG: RngCore, const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
rng: &mut RNG,
|
||||||
|
buf: &mut RadioBuffer<N>,
|
||||||
|
send_data: &SendData,
|
||||||
|
) -> Result<(radio::TxConfig, FcntUp)> {
|
||||||
|
let fcnt = match &mut self.state {
|
||||||
|
State::Joined(ref mut session) => Ok(session.prepare_buffer::<C, N>(send_data, buf)),
|
||||||
|
State::Otaa(_) => Err(Error::NotJoined),
|
||||||
|
State::Unjoined => Err(Error::NotJoined),
|
||||||
|
}?;
|
||||||
|
let mut tx_config =
|
||||||
|
self.region.create_tx_config(rng, self.configuration.data_rate, &Frame::Data);
|
||||||
|
tx_config.adjust_power(self.board_eirp.max_power, self.board_eirp.antenna_gain);
|
||||||
|
Ok((tx_config, fcnt))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_rx_delay(&self, frame: &Frame, window: &Window) -> u32 {
|
||||||
|
match frame {
|
||||||
|
Frame::Join => match window {
|
||||||
|
Window::_1 => self.configuration.join_accept_delay1,
|
||||||
|
Window::_2 => self.configuration.join_accept_delay2,
|
||||||
|
},
|
||||||
|
Frame::Data => match window {
|
||||||
|
Window::_1 => self.configuration.rx1_delay,
|
||||||
|
// RECEIVE_DELAY2 is not configurable. LoRaWAN 1.0.3 Section 5.7:
|
||||||
|
// "The second reception slot opens one second after the first reception slot."
|
||||||
|
Window::_2 => self.configuration.rx1_delay + 1000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the radio configuration and timing for a given frame type and window.
|
||||||
|
pub(crate) fn get_rx_parameters_legacy(
|
||||||
|
&mut self,
|
||||||
|
frame: &Frame,
|
||||||
|
window: &Window,
|
||||||
|
) -> (RfConfig, u32) {
|
||||||
|
(
|
||||||
|
self.region.get_rx_config(self.configuration.data_rate, frame, window),
|
||||||
|
self.get_rx_delay(frame, window),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles a received RF frame. Returns None is unparseable, fails decryption, or fails MIC
|
||||||
|
/// verification. Upon successful join, provides Response::JoinSuccess. Upon successful data
|
||||||
|
/// rx, provides Response::DownlinkReceived. User must take the downlink from vec for
|
||||||
|
/// application data.
|
||||||
|
pub(crate) fn handle_rx<C: CryptoFactory + Default, const N: usize, const D: usize>(
|
||||||
|
&mut self,
|
||||||
|
buf: &mut RadioBuffer<N>,
|
||||||
|
dl: &mut Vec<Downlink, D>,
|
||||||
|
) -> Response {
|
||||||
|
match &mut self.state {
|
||||||
|
State::Joined(ref mut session) => session.handle_rx::<C, N, D>(
|
||||||
|
&mut self.region,
|
||||||
|
&mut self.configuration,
|
||||||
|
buf,
|
||||||
|
dl,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
State::Otaa(ref mut otaa) => {
|
||||||
|
if let Some(session) =
|
||||||
|
otaa.handle_rx::<C, N>(&mut self.region, &mut self.configuration, buf)
|
||||||
|
{
|
||||||
|
self.state = State::Joined(session);
|
||||||
|
Response::JoinSuccess
|
||||||
|
} else {
|
||||||
|
Response::NoUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::Unjoined => Response::NoUpdate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles a received RF frame during RXC window. Returns None if unparseable, fails decryption,
|
||||||
|
/// or fails MIC verification. Upon successful data rx, provides Response::DownlinkReceived.
|
||||||
|
/// User must later call `take_downlink()` on the device to get the application data.
|
||||||
|
pub(crate) fn handle_rxc<C: CryptoFactory + Default, const N: usize, const D: usize>(
|
||||||
|
&mut self,
|
||||||
|
buf: &mut RadioBuffer<N>,
|
||||||
|
dl: &mut Vec<Downlink, D>,
|
||||||
|
) -> Result<Response> {
|
||||||
|
match &mut self.state {
|
||||||
|
State::Joined(ref mut session) => Ok(session.handle_rx::<C, N, D>(
|
||||||
|
&mut self.region,
|
||||||
|
&mut self.configuration,
|
||||||
|
buf,
|
||||||
|
dl,
|
||||||
|
true,
|
||||||
|
)),
|
||||||
|
State::Otaa(_) => Err(Error::NotJoined),
|
||||||
|
State::Unjoined => Err(Error::NotJoined),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn rx2_complete(&mut self) -> Response {
|
||||||
|
match &mut self.state {
|
||||||
|
State::Joined(session) => session.rx2_complete(),
|
||||||
|
State::Otaa(otaa) => otaa.rx2_complete(),
|
||||||
|
State::Unjoined => Response::NoUpdate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_session_keys(&self) -> Option<SessionKeys> {
|
||||||
|
match &self.state {
|
||||||
|
State::Joined(session) => session.get_session_keys(),
|
||||||
|
State::Otaa(_) => None,
|
||||||
|
State::Unjoined => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_session(&self) -> Option<&Session> {
|
||||||
|
match &self.state {
|
||||||
|
State::Joined(session) => Some(session),
|
||||||
|
State::Otaa(_) => None,
|
||||||
|
State::Unjoined => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_joined(&self) -> bool {
|
||||||
|
matches!(&self.state, State::Joined(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_fcnt_up(&self) -> Option<FcntUp> {
|
||||||
|
match &self.state {
|
||||||
|
State::Joined(session) => Some(session.fcnt_up),
|
||||||
|
State::Otaa(_) => None,
|
||||||
|
State::Unjoined => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_rx_config(&self, buffer_ms: u32, frame: &Frame, window: &Window) -> RxConfig {
|
||||||
|
RxConfig {
|
||||||
|
rf: self.region.get_rx_config(self.configuration.data_rate, frame, window),
|
||||||
|
mode: RxMode::Single { ms: buffer_ms },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_rxc_config(&self) -> RxConfig {
|
||||||
|
RxConfig {
|
||||||
|
rf: self.region.get_rxc_config(self.configuration.data_rate),
|
||||||
|
mode: RxMode::Continuous,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Response {
|
||||||
|
NoAck,
|
||||||
|
SessionExpired,
|
||||||
|
DownlinkReceived(FcntDown),
|
||||||
|
NoJoinAccept,
|
||||||
|
JoinSuccess,
|
||||||
|
NoUpdate,
|
||||||
|
RxComplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Response> for nb_device::Response {
|
||||||
|
fn from(r: Response) -> Self {
|
||||||
|
match r {
|
||||||
|
Response::SessionExpired => nb_device::Response::SessionExpired,
|
||||||
|
Response::DownlinkReceived(fcnt) => nb_device::Response::DownlinkReceived(fcnt),
|
||||||
|
Response::NoAck => nb_device::Response::NoAck,
|
||||||
|
Response::NoJoinAccept => nb_device::Response::NoJoinAccept,
|
||||||
|
Response::JoinSuccess => nb_device::Response::JoinSuccess,
|
||||||
|
Response::NoUpdate => nb_device::Response::NoUpdate,
|
||||||
|
Response::RxComplete => nb_device::Response::RxComplete,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Response> for async_device::SendResponse {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(r: Response) -> Result<async_device::SendResponse> {
|
||||||
|
match r {
|
||||||
|
Response::SessionExpired => Ok(async_device::SendResponse::SessionExpired),
|
||||||
|
Response::DownlinkReceived(fcnt) => {
|
||||||
|
Ok(async_device::SendResponse::DownlinkReceived(fcnt))
|
||||||
|
}
|
||||||
|
Response::NoAck => Ok(async_device::SendResponse::NoAck),
|
||||||
|
Response::RxComplete => Ok(async_device::SendResponse::RxComplete),
|
||||||
|
r => Err(Error::InvalidResponse(r)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Response> for async_device::JoinResponse {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(r: Response) -> Result<async_device::JoinResponse> {
|
||||||
|
match r {
|
||||||
|
Response::NoJoinAccept => Ok(async_device::JoinResponse::NoJoinAccept),
|
||||||
|
Response::JoinSuccess => Ok(async_device::JoinResponse::JoinSuccess),
|
||||||
|
r => Err(Error::InvalidResponse(r)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn del_to_delay_ms(del: u8) -> u32 {
|
||||||
|
match del {
|
||||||
|
2..=15 => del as u32 * 1000,
|
||||||
|
_ => region::constants::RECEIVE_DELAY1,
|
||||||
|
}
|
||||||
|
}
|
||||||
92
lorawan-device-patch/src/mac/otaa.rs
Normal file
92
lorawan-device-patch/src/mac/otaa.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use super::{del_to_delay_ms, session::Session, Response};
|
||||||
|
use crate::radio::RadioBuffer;
|
||||||
|
use crate::region::Configuration;
|
||||||
|
use crate::{AppEui, AppKey, DevEui};
|
||||||
|
use lorawan::keys::CryptoFactory;
|
||||||
|
use lorawan::{
|
||||||
|
creator::JoinRequestCreator,
|
||||||
|
parser::{parse_with_factory as lorawan_parse, *},
|
||||||
|
};
|
||||||
|
use rand_core::RngCore;
|
||||||
|
|
||||||
|
pub(crate) type DevNonce = lorawan::parser::DevNonce<[u8; 2]>;
|
||||||
|
|
||||||
|
pub(crate) struct Otaa {
|
||||||
|
dev_nonce: DevNonce,
|
||||||
|
network_credentials: NetworkCredentials,
|
||||||
|
}
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NetworkCredentials {
|
||||||
|
deveui: DevEui,
|
||||||
|
appeui: AppEui,
|
||||||
|
appkey: AppKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Otaa {
|
||||||
|
pub fn new(network_credentials: NetworkCredentials) -> Self {
|
||||||
|
Self { dev_nonce: DevNonce::from([0, 0]), network_credentials }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare a join request to be sent. This populates the radio buffer with the request to be
|
||||||
|
/// sent, and returns the radio config to use for transmitting.
|
||||||
|
pub(crate) fn prepare_buffer<C: CryptoFactory + Default, G: RngCore, const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
rng: &mut G,
|
||||||
|
buf: &mut RadioBuffer<N>,
|
||||||
|
) -> u16 {
|
||||||
|
self.dev_nonce = DevNonce::from(rng.next_u32() as u16);
|
||||||
|
buf.clear();
|
||||||
|
let mut phy: JoinRequestCreator<[u8; 23], C> = JoinRequestCreator::default();
|
||||||
|
phy.set_app_eui(self.network_credentials.appeui)
|
||||||
|
.set_dev_eui(self.network_credentials.deveui)
|
||||||
|
.set_dev_nonce(self.dev_nonce);
|
||||||
|
let vec = phy.build(&self.network_credentials.appkey);
|
||||||
|
buf.extend_from_slice(vec).unwrap();
|
||||||
|
u16::from(self.dev_nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_rx<C: CryptoFactory + Default, const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
region: &mut Configuration,
|
||||||
|
configuration: &mut super::Configuration,
|
||||||
|
rx: &mut RadioBuffer<N>,
|
||||||
|
) -> Option<Session> {
|
||||||
|
if let Ok(PhyPayload::JoinAccept(JoinAcceptPayload::Encrypted(encrypted))) =
|
||||||
|
lorawan_parse(rx.as_mut_for_read(), C::default())
|
||||||
|
{
|
||||||
|
let decrypt = encrypted.decrypt(&self.network_credentials.appkey);
|
||||||
|
region.process_join_accept(&decrypt);
|
||||||
|
configuration.rx1_delay = del_to_delay_ms(decrypt.rx_delay());
|
||||||
|
if decrypt.validate_mic(&self.network_credentials.appkey) {
|
||||||
|
return Some(Session::derive_new(
|
||||||
|
&decrypt,
|
||||||
|
self.dev_nonce,
|
||||||
|
&self.network_credentials,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn rx2_complete(&mut self) -> Response {
|
||||||
|
Response::NoJoinAccept
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkCredentials {
|
||||||
|
pub fn new(appeui: AppEui, deveui: DevEui, appkey: AppKey) -> Self {
|
||||||
|
Self { deveui, appeui, appkey }
|
||||||
|
}
|
||||||
|
pub fn appeui(&self) -> &AppEui {
|
||||||
|
&self.appeui
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deveui(&self) -> &DevEui {
|
||||||
|
&self.deveui
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn appkey(&self) -> &AppKey {
|
||||||
|
&self.appkey
|
||||||
|
}
|
||||||
|
}
|
||||||
222
lorawan-device-patch/src/mac/session.rs
Normal file
222
lorawan-device-patch/src/mac/session.rs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
use crate::{region, AppSKey, Downlink, NewSKey};
|
||||||
|
use heapless::Vec;
|
||||||
|
use lorawan::keys::CryptoFactory;
|
||||||
|
use lorawan::maccommands::{DownlinkMacCommand, MacCommandIterator};
|
||||||
|
use lorawan::{
|
||||||
|
creator::DataPayloadCreator,
|
||||||
|
maccommands::SerializableMacCommand,
|
||||||
|
parser::{parse_with_factory as lorawan_parse, *},
|
||||||
|
parser::{DecryptedJoinAcceptPayload, DevAddr},
|
||||||
|
};
|
||||||
|
|
||||||
|
use generic_array::{typenum::U256, GenericArray};
|
||||||
|
|
||||||
|
use crate::radio::RadioBuffer;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
otaa::{DevNonce, NetworkCredentials},
|
||||||
|
uplink, FcntUp, Response, SendData,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct Session {
|
||||||
|
pub uplink: uplink::Uplink,
|
||||||
|
pub confirmed: bool,
|
||||||
|
pub newskey: NewSKey,
|
||||||
|
pub appskey: AppSKey,
|
||||||
|
pub devaddr: DevAddr<[u8; 4]>,
|
||||||
|
pub fcnt_up: u32,
|
||||||
|
pub fcnt_down: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub struct SessionKeys {
|
||||||
|
pub newskey: NewSKey,
|
||||||
|
pub appskey: AppSKey,
|
||||||
|
pub devaddr: DevAddr<[u8; 4]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Session> for SessionKeys {
|
||||||
|
fn from(session: Session) -> Self {
|
||||||
|
Self { newskey: session.newskey, appskey: session.appskey, devaddr: session.devaddr }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn derive_new<T: AsRef<[u8]>, F: CryptoFactory>(
|
||||||
|
decrypt: &DecryptedJoinAcceptPayload<T, F>,
|
||||||
|
devnonce: DevNonce,
|
||||||
|
credentials: &NetworkCredentials,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(
|
||||||
|
decrypt.derive_newskey(&devnonce, credentials.appkey()),
|
||||||
|
decrypt.derive_appskey(&devnonce, credentials.appkey()),
|
||||||
|
DevAddr::new([
|
||||||
|
decrypt.dev_addr().as_ref()[0],
|
||||||
|
decrypt.dev_addr().as_ref()[1],
|
||||||
|
decrypt.dev_addr().as_ref()[2],
|
||||||
|
decrypt.dev_addr().as_ref()[3],
|
||||||
|
])
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(newskey: NewSKey, appskey: AppSKey, devaddr: DevAddr<[u8; 4]>) -> Self {
|
||||||
|
Self {
|
||||||
|
newskey,
|
||||||
|
appskey,
|
||||||
|
devaddr,
|
||||||
|
confirmed: false,
|
||||||
|
fcnt_down: 0,
|
||||||
|
fcnt_up: 0,
|
||||||
|
uplink: uplink::Uplink::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn devaddr(&self) -> &DevAddr<[u8; 4]> {
|
||||||
|
&self.devaddr
|
||||||
|
}
|
||||||
|
pub fn appskey(&self) -> &AppSKey {
|
||||||
|
&self.appskey
|
||||||
|
}
|
||||||
|
pub fn newskey(&self) -> &NewSKey {
|
||||||
|
&self.newskey
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_session_keys(&self) -> Option<SessionKeys> {
|
||||||
|
Some(SessionKeys { newskey: self.newskey, appskey: self.appskey, devaddr: self.devaddr })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub(crate) fn handle_rx<C: CryptoFactory + Default, const N: usize, const D: usize>(
|
||||||
|
&mut self,
|
||||||
|
region: &mut region::Configuration,
|
||||||
|
configuration: &mut super::Configuration,
|
||||||
|
rx: &mut RadioBuffer<N>,
|
||||||
|
dl: &mut Vec<Downlink, D>,
|
||||||
|
ignore_mac: bool,
|
||||||
|
) -> Response {
|
||||||
|
if let Ok(PhyPayload::Data(DataPayload::Encrypted(encrypted_data))) =
|
||||||
|
lorawan_parse(rx.as_mut_for_read(), C::default())
|
||||||
|
{
|
||||||
|
if self.devaddr() == &encrypted_data.fhdr().dev_addr() {
|
||||||
|
let fcnt = encrypted_data.fhdr().fcnt() as u32;
|
||||||
|
let confirmed = encrypted_data.is_confirmed();
|
||||||
|
if encrypted_data.validate_mic(self.newskey().inner(), fcnt)
|
||||||
|
&& (fcnt > self.fcnt_down || fcnt == 0)
|
||||||
|
{
|
||||||
|
self.fcnt_down = fcnt;
|
||||||
|
// We can safely unwrap here because we already validated the MIC
|
||||||
|
let decrypted = encrypted_data
|
||||||
|
.decrypt(
|
||||||
|
Some(self.newskey().inner()),
|
||||||
|
Some(self.appskey().inner()),
|
||||||
|
self.fcnt_down,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if !ignore_mac {
|
||||||
|
// MAC commands may be in the FHDR or the FRMPayload
|
||||||
|
configuration.handle_downlink_macs(
|
||||||
|
region,
|
||||||
|
&mut self.uplink,
|
||||||
|
MacCommandIterator::<DownlinkMacCommand>::new(decrypted.fhdr().data()),
|
||||||
|
);
|
||||||
|
if let FRMPayload::MACCommands(mac_cmds) = decrypted.frm_payload() {
|
||||||
|
configuration.handle_downlink_macs(
|
||||||
|
region,
|
||||||
|
&mut self.uplink,
|
||||||
|
MacCommandIterator::<DownlinkMacCommand>::new(mac_cmds.data()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if confirmed {
|
||||||
|
self.uplink.set_downlink_confirmation();
|
||||||
|
}
|
||||||
|
|
||||||
|
return if self.fcnt_up == 0xFFFF_FFFF {
|
||||||
|
// if the FCnt is used up, the session has expired
|
||||||
|
Response::SessionExpired
|
||||||
|
} else {
|
||||||
|
// we can always increment fcnt_up when we receive a downlink
|
||||||
|
self.fcnt_up += 1;
|
||||||
|
if let (Some(fport), FRMPayload::Data(data)) =
|
||||||
|
(decrypted.f_port(), decrypted.frm_payload())
|
||||||
|
{
|
||||||
|
// heapless Vec from slice fails only if slice is too large.
|
||||||
|
// A data FRM payload will never exceed 256 bytes.
|
||||||
|
let data = Vec::from_slice(data).unwrap();
|
||||||
|
// TODO: propagate error type when heapless vec is full?
|
||||||
|
let _ = dl.push(Downlink { data, fport });
|
||||||
|
}
|
||||||
|
Response::DownlinkReceived(fcnt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Response::NoUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn rx2_complete(&mut self) -> Response {
|
||||||
|
// Until we handle NbTrans, there is no case where we should not increment FCntUp.
|
||||||
|
if self.fcnt_up == 0xFFFF_FFFF {
|
||||||
|
// if the FCnt is used up, the session has expired
|
||||||
|
return Response::SessionExpired;
|
||||||
|
} else {
|
||||||
|
self.fcnt_up += 1;
|
||||||
|
}
|
||||||
|
if self.confirmed {
|
||||||
|
Response::NoAck
|
||||||
|
} else {
|
||||||
|
Response::RxComplete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prepare_buffer<C: CryptoFactory + Default, const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
data: &SendData,
|
||||||
|
tx_buffer: &mut RadioBuffer<N>,
|
||||||
|
) -> FcntUp {
|
||||||
|
tx_buffer.clear();
|
||||||
|
let fcnt = self.fcnt_up;
|
||||||
|
let mut phy: DataPayloadCreator<GenericArray<u8, U256>, C> = DataPayloadCreator::default();
|
||||||
|
|
||||||
|
let mut fctrl = FCtrl(0x0, true);
|
||||||
|
if self.uplink.confirms_downlink() {
|
||||||
|
fctrl.set_ack();
|
||||||
|
self.uplink.clear_downlink_confirmation();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.confirmed = data.confirmed;
|
||||||
|
|
||||||
|
phy.set_confirmed(data.confirmed)
|
||||||
|
.set_fctrl(&fctrl)
|
||||||
|
.set_f_port(data.fport)
|
||||||
|
.set_dev_addr(self.devaddr)
|
||||||
|
.set_fcnt(fcnt);
|
||||||
|
|
||||||
|
let mut cmds = Vec::new();
|
||||||
|
self.uplink.get_cmds(&mut cmds);
|
||||||
|
let mut dyn_cmds: Vec<&dyn SerializableMacCommand, 8> = Vec::new();
|
||||||
|
|
||||||
|
for cmd in &cmds {
|
||||||
|
if let Err(_e) = dyn_cmds.push(cmd) {
|
||||||
|
panic!("dyn_cmds too small compared to cmds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match phy.build(data.data, dyn_cmds.as_slice(), &self.newskey, &self.appskey) {
|
||||||
|
Ok(packet) => {
|
||||||
|
tx_buffer.clear();
|
||||||
|
tx_buffer.extend_from_slice(packet).unwrap();
|
||||||
|
}
|
||||||
|
Err(e) => panic!("Error assembling packet! {:?} ", e),
|
||||||
|
}
|
||||||
|
fcnt
|
||||||
|
}
|
||||||
|
}
|
||||||
88
lorawan-device-patch/src/mac/uplink.rs
Normal file
88
lorawan-device-patch/src/mac/uplink.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
This a temporary design where flags will be left about desired MAC uplinks by the stack
|
||||||
|
During Uplink assembly, this struct will be inquired to drive construction
|
||||||
|
*/
|
||||||
|
use heapless::Vec;
|
||||||
|
use lorawan::maccommands::{LinkADRAnsPayload, RXTimingSetupAnsPayload, UplinkMacCommand};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct Uplink {
|
||||||
|
pub adr_ans: AdrAns,
|
||||||
|
pub rx_delay_ans: RxDelayAns,
|
||||||
|
confirmed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiple AdrAns may happen per downlink
|
||||||
|
// so we aggregate how many AdrAns are required
|
||||||
|
type AdrAns = u8;
|
||||||
|
// only one RxDelayReq will happen
|
||||||
|
// so we only need to implement this as a bool
|
||||||
|
type RxDelayAns = bool;
|
||||||
|
|
||||||
|
//work around for E0390
|
||||||
|
pub(crate) trait MacAnsTrait {
|
||||||
|
fn add(&mut self);
|
||||||
|
fn clear(&mut self);
|
||||||
|
// we use a uint instead of bool because some ADR responses
|
||||||
|
// require a counter for state.
|
||||||
|
// eg: ADR Req may be batched in a single downlink and require
|
||||||
|
// multiple ADR Ans in the next uplink
|
||||||
|
fn get(&self) -> u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MacAnsTrait for AdrAns {
|
||||||
|
fn add(&mut self) {
|
||||||
|
*self += 1;
|
||||||
|
}
|
||||||
|
fn clear(&mut self) {
|
||||||
|
*self = 0;
|
||||||
|
}
|
||||||
|
fn get(&self) -> u8 {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MacAnsTrait for RxDelayAns {
|
||||||
|
fn add(&mut self) {
|
||||||
|
*self = true;
|
||||||
|
}
|
||||||
|
fn clear(&mut self) {
|
||||||
|
*self = false;
|
||||||
|
}
|
||||||
|
fn get(&self) -> u8 {
|
||||||
|
u8::from(*self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Uplink {
|
||||||
|
pub fn set_downlink_confirmation(&mut self) {
|
||||||
|
self.confirmed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_downlink_confirmation(&mut self) {
|
||||||
|
self.confirmed = false;
|
||||||
|
}
|
||||||
|
pub fn confirms_downlink(&self) -> bool {
|
||||||
|
self.confirmed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ack_rx_delay(&mut self) {
|
||||||
|
self.rx_delay_ans.add();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cmds(&mut self, macs: &mut Vec<UplinkMacCommand, 8>) {
|
||||||
|
for _ in 0..self.adr_ans.get() {
|
||||||
|
macs.push(UplinkMacCommand::LinkADRAns(LinkADRAnsPayload::new(&[0x07]).unwrap()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
self.adr_ans.clear();
|
||||||
|
|
||||||
|
if self.rx_delay_ans.get() != 0 {
|
||||||
|
macs.push(UplinkMacCommand::RXTimingSetupAns(RXTimingSetupAnsPayload::new(&[])))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
self.rx_delay_ans.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
173
lorawan-device-patch/src/nb_device/mod.rs
Normal file
173
lorawan-device-patch/src/nb_device/mod.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
//! A non-blocking LoRaWAN device implementation which uses an explicitly defined state machine
|
||||||
|
//! for driving the protocol state against pin and timer events. Depends on a non-async radio
|
||||||
|
//! implementation.
|
||||||
|
use super::radio::RadioBuffer;
|
||||||
|
use super::*;
|
||||||
|
use crate::nb_device::radio::PhyRxTx;
|
||||||
|
use mac::{Mac, SendData};
|
||||||
|
|
||||||
|
pub(crate) mod state;
|
||||||
|
|
||||||
|
pub mod radio;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
type TimestampMs = u32;
|
||||||
|
|
||||||
|
pub struct Device<R, C, RNG, const N: usize, const D: usize = 1>
|
||||||
|
where
|
||||||
|
R: PhyRxTx + Timings,
|
||||||
|
C: CryptoFactory + Default,
|
||||||
|
RNG: RngCore,
|
||||||
|
{
|
||||||
|
state: State,
|
||||||
|
shared: Shared<R, RNG, N, D>,
|
||||||
|
crypto: PhantomData<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, C, RNG, const N: usize, const D: usize> Device<R, C, RNG, N, D>
|
||||||
|
where
|
||||||
|
R: PhyRxTx + Timings,
|
||||||
|
C: CryptoFactory + Default,
|
||||||
|
RNG: RngCore,
|
||||||
|
{
|
||||||
|
pub fn new(region: region::Configuration, radio: R, rng: RNG) -> Device<R, C, RNG, N, D> {
|
||||||
|
Device {
|
||||||
|
crypto: PhantomData,
|
||||||
|
state: State::default(),
|
||||||
|
shared: Shared {
|
||||||
|
radio,
|
||||||
|
rng,
|
||||||
|
tx_buffer: RadioBuffer::new(),
|
||||||
|
mac: Mac::new(region, R::MAX_RADIO_POWER, R::ANTENNA_GAIN),
|
||||||
|
downlink: Vec::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn join(&mut self, join_mode: JoinMode) -> Result<Response, Error<R>> {
|
||||||
|
match join_mode {
|
||||||
|
JoinMode::OTAA { deveui, appeui, appkey } => {
|
||||||
|
self.handle_event(Event::Join(NetworkCredentials::new(appeui, deveui, appkey)))
|
||||||
|
}
|
||||||
|
JoinMode::ABP { devaddr, appskey, newskey } => {
|
||||||
|
self.shared.mac.join_abp(newskey, appskey, devaddr);
|
||||||
|
Ok(Response::JoinSuccess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_radio(&mut self) -> &mut R {
|
||||||
|
&mut self.shared.radio
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_datarate(&mut self) -> region::DR {
|
||||||
|
self.shared.mac.configuration.data_rate
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_datarate(&mut self, datarate: region::DR) {
|
||||||
|
self.shared.mac.configuration.data_rate = datarate
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ready_to_send_data(&self) -> bool {
|
||||||
|
matches!(&self.state, State::Idle(_)) && self.shared.mac.is_joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&mut self, data: &[u8], fport: u8, confirmed: bool) -> Result<Response, Error<R>> {
|
||||||
|
self.handle_event(Event::SendDataRequest(SendData { data, fport, confirmed }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_fcnt_up(&self) -> Option<u32> {
|
||||||
|
self.shared.mac.get_fcnt_up()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_session(&self) -> Option<&mac::Session> {
|
||||||
|
self.shared.mac.get_session()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_session(&mut self, s: mac::Session) {
|
||||||
|
self.shared.mac.set_session(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_session_keys(&self) -> Option<mac::SessionKeys> {
|
||||||
|
self.shared.mac.get_session_keys()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_downlink(&mut self) -> Option<Downlink> {
|
||||||
|
self.shared.downlink.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_event(&mut self, event: Event<R>) -> Result<Response, Error<R>> {
|
||||||
|
let (new_state, result) = self.state.handle_event::<R, C, RNG, N, D>(
|
||||||
|
&mut self.shared.mac,
|
||||||
|
&mut self.shared.radio,
|
||||||
|
&mut self.shared.rng,
|
||||||
|
&mut self.shared.tx_buffer,
|
||||||
|
&mut self.shared.downlink,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
self.state = new_state;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Shared<R: PhyRxTx + Timings, RNG: RngCore, const N: usize, const D: usize> {
|
||||||
|
pub(crate) radio: R,
|
||||||
|
pub(crate) rng: RNG,
|
||||||
|
pub(crate) tx_buffer: RadioBuffer<N>,
|
||||||
|
pub(crate) mac: Mac,
|
||||||
|
pub(crate) downlink: Vec<Downlink, D>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Response {
|
||||||
|
NoUpdate,
|
||||||
|
TimeoutRequest(TimestampMs),
|
||||||
|
JoinRequestSending,
|
||||||
|
JoinSuccess,
|
||||||
|
NoJoinAccept,
|
||||||
|
UplinkSending(mac::FcntUp),
|
||||||
|
DownlinkReceived(mac::FcntDown),
|
||||||
|
NoAck,
|
||||||
|
ReadyToSend,
|
||||||
|
SessionExpired,
|
||||||
|
RxComplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error<R: PhyRxTx> {
|
||||||
|
Radio(R::PhyError),
|
||||||
|
State(state::Error),
|
||||||
|
Mac(mac::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: PhyRxTx> From<mac::Error> for Error<R> {
|
||||||
|
fn from(mac_error: mac::Error) -> Error<R> {
|
||||||
|
Error::Mac(mac_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event<'a, R>
|
||||||
|
where
|
||||||
|
R: PhyRxTx,
|
||||||
|
{
|
||||||
|
Join(NetworkCredentials),
|
||||||
|
SendDataRequest(SendData<'a>),
|
||||||
|
RadioEvent(radio::Event<'a, R>),
|
||||||
|
TimeoutFired,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, R> core::fmt::Debug for Event<'a, R>
|
||||||
|
where
|
||||||
|
R: PhyRxTx,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
let event = match self {
|
||||||
|
Event::Join(_) => "Join",
|
||||||
|
Event::SendDataRequest(_) => "SendDataRequest",
|
||||||
|
Event::RadioEvent(_) => "RadioEvent",
|
||||||
|
Event::TimeoutFired => "TimeoutFired",
|
||||||
|
};
|
||||||
|
write!(f, "lorawan_device::Event::{event}")
|
||||||
|
}
|
||||||
|
}
|
||||||
50
lorawan-device-patch/src/nb_device/radio.rs
Normal file
50
lorawan-device-patch/src/nb_device/radio.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use super::TimestampMs;
|
||||||
|
pub use crate::radio::*;
|
||||||
|
pub use ::lora_modulation::{Bandwidth, CodingRate, SpreadingFactor};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Event<'a, R>
|
||||||
|
where
|
||||||
|
R: PhyRxTx,
|
||||||
|
{
|
||||||
|
TxRequest(TxConfig, &'a [u8]),
|
||||||
|
RxRequest(RfConfig),
|
||||||
|
CancelRx,
|
||||||
|
Phy(R::PhyEvent),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Response<R>
|
||||||
|
where
|
||||||
|
R: PhyRxTx,
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Txing,
|
||||||
|
Rxing,
|
||||||
|
TxDone(TimestampMs),
|
||||||
|
RxDone(RxQuality),
|
||||||
|
Phy(R::PhyResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
pub trait PhyRxTx {
|
||||||
|
type PhyEvent: fmt::Debug;
|
||||||
|
type PhyError: fmt::Debug;
|
||||||
|
type PhyResponse: fmt::Debug;
|
||||||
|
|
||||||
|
/// Board-specific antenna gain and power loss in dBi.
|
||||||
|
const ANTENNA_GAIN: i8 = 0;
|
||||||
|
|
||||||
|
/// Maximum power (dBm) that the radio is able to output. When preparing instructions for radio,
|
||||||
|
/// the value of maximum power will be used as an upper bound.
|
||||||
|
const MAX_RADIO_POWER: u8;
|
||||||
|
|
||||||
|
fn get_mut_radio(&mut self) -> &mut Self;
|
||||||
|
|
||||||
|
// we require mutability so we may decrypt in place
|
||||||
|
fn get_received_packet(&mut self) -> &mut [u8];
|
||||||
|
fn handle_event(&mut self, event: Event<Self>) -> Result<Response<Self>, Self::PhyError>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
411
lorawan-device-patch/src/nb_device/state.rs
Normal file
411
lorawan-device-patch/src/nb_device/state.rs
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
This state machine creates a non-blocking and no-async structure for coordinating radio events with
|
||||||
|
the mac state.
|
||||||
|
|
||||||
|
In this implementation, each state (eg: "Idle", "Txing") is a struct. When an event is handled
|
||||||
|
(eg: "SendData", "TxComplete"), a transition may or may not occur. Regardless, a response is always
|
||||||
|
given to the client, and those are indicated here in parenthesis (ie: "(Sending)"). If nothing is
|
||||||
|
indicated in this diagram, the response is "NoUpdate".
|
||||||
|
|
||||||
|
O
|
||||||
|
│
|
||||||
|
╔═══════════════════╗ ╔════════════════════╗
|
||||||
|
║ Idle ║ ║ Txing ║
|
||||||
|
║ SendData ║ if async (Sending) ║ ║
|
||||||
|
║ ─────────╫───────────────┬───────────────>║ ║
|
||||||
|
║ ║ │ ║ TxComplete ║
|
||||||
|
╚═══════════════════╝ │ ║ ──────────╫───┐
|
||||||
|
^ │ ╚════════════════════╝ │
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
┌─────┘ ╔═══════════════════╗ │ ╔════════════════════╗ │
|
||||||
|
│ ║ WaitingForRx ║ │ ║ WaitingForRxWindow ║ │
|
||||||
|
│ ║ ╔═════════════╗ ║ │else sync ║ ╔═════════════╗ ║ │
|
||||||
|
│ ║ ║ RxWindow1 ║ ║ └──────────╫─>║ RxWindow1 ║<──╫─────────┘
|
||||||
|
│(DataDown)║ ║ Rx ║ ║ (TimeoutReq)║ ║ ║ ║(TimeoutReq)
|
||||||
|
├──────────╫─╫─────── ║ ║(TimeoutReq) ║ ║ Timeout ║ ║
|
||||||
|
│ ║ ║ Timeout ║<──╫───────────────╫──╫──────────── ║ ║
|
||||||
|
│ ║ ║ ─────────╫───╫──┐ ║ ╚═════════════╝ ║
|
||||||
|
│ ║ ╚═════════════╝ ║ │ ║ ║
|
||||||
|
│ ║ ╔═════════════╗ ║ │(TimeoutReq)║ ╔═════════════╗ ║
|
||||||
|
│(DataDown)║ ║ RxWindow2 ║ ║ └────────────╫─> ║ RxWindow2 ║ ║
|
||||||
|
├──────────╫─╫──┐ Rx ║ ║ ║ ║ ║ ║
|
||||||
|
│ ║ ║ └─── ║ ║(TimeoutReq) ║ ║ Timeout ║ ║
|
||||||
|
│ if conf ║ ║ Timeout ║<──╫───────────────╫───╫──────────── ║ ║
|
||||||
|
│ (NoACK) ║ ║ ┌──────── ║ ║ ║ ╚═════════════╝ ║
|
||||||
|
└──────────╫─╫───┘ ║ ║ ║ ║
|
||||||
|
else(Ready)║ ╚═════════════╝ ║ ║ ║
|
||||||
|
╚═══════════════════╝ ╚════════════════════╝
|
||||||
|
*/
|
||||||
|
use super::super::*;
|
||||||
|
use super::{
|
||||||
|
mac::{Frame, Mac, Window},
|
||||||
|
radio, Event, RadioBuffer, Response, Timings,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum State {
|
||||||
|
Idle(Idle),
|
||||||
|
SendingData(SendingData),
|
||||||
|
WaitingForRxWindow(WaitingForRxWindow),
|
||||||
|
WaitingForRx(WaitingForRx),
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! into_state {
|
||||||
|
($($from:tt),*) => {
|
||||||
|
$(
|
||||||
|
impl From<$from> for State
|
||||||
|
{
|
||||||
|
fn from(s: $from) -> State {
|
||||||
|
State::$from(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*};
|
||||||
|
}
|
||||||
|
|
||||||
|
into_state!(Idle, SendingData, WaitingForRxWindow, WaitingForRx);
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
State::Idle(Idle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Rx> for Window {
|
||||||
|
fn from(val: Rx) -> Window {
|
||||||
|
match val {
|
||||||
|
Rx::_1(_) => Window::_1,
|
||||||
|
Rx::_2(_) => Window::_2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
RadioEventWhileIdle,
|
||||||
|
RadioEventWhileWaitingForRxWindow,
|
||||||
|
NewSessionWhileWaitingForRxWindow,
|
||||||
|
SendDataWhileWaitingForRxWindow,
|
||||||
|
TxRequestDuringTx,
|
||||||
|
NewSessionWhileWaitingForRx,
|
||||||
|
SendDataWhileWaitingForRx,
|
||||||
|
BufferTooSmall,
|
||||||
|
UnexpectedRadioResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: radio::PhyRxTx> From<Error> for super::Error<R> {
|
||||||
|
fn from(error: Error) -> super::Error<R> {
|
||||||
|
super::Error::State(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub(crate) fn handle_event<
|
||||||
|
R: radio::PhyRxTx + Timings,
|
||||||
|
C: CryptoFactory + Default,
|
||||||
|
RNG: RngCore,
|
||||||
|
const N: usize,
|
||||||
|
const D: usize,
|
||||||
|
>(
|
||||||
|
self,
|
||||||
|
mac: &mut Mac,
|
||||||
|
radio: &mut R,
|
||||||
|
rng: &mut RNG,
|
||||||
|
buf: &mut RadioBuffer<N>,
|
||||||
|
dl: &mut Vec<Downlink, D>,
|
||||||
|
event: Event<R>,
|
||||||
|
) -> (Self, Result<Response, super::Error<R>>) {
|
||||||
|
match self {
|
||||||
|
State::Idle(s) => s.handle_event::<R, C, RNG, N>(mac, radio, rng, buf, event),
|
||||||
|
State::SendingData(s) => s.handle_event::<R, N>(mac, radio, event),
|
||||||
|
State::WaitingForRxWindow(s) => s.handle_event::<R, N>(mac, radio, event),
|
||||||
|
State::WaitingForRx(s) => s.handle_event::<R, C, N, D>(mac, radio, buf, event, dl),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct Idle;
|
||||||
|
|
||||||
|
impl Idle {
|
||||||
|
pub(crate) fn handle_event<
|
||||||
|
R: radio::PhyRxTx + Timings,
|
||||||
|
C: CryptoFactory + Default,
|
||||||
|
RNG: RngCore,
|
||||||
|
const N: usize,
|
||||||
|
>(
|
||||||
|
self,
|
||||||
|
mac: &mut Mac,
|
||||||
|
radio: &mut R,
|
||||||
|
rng: &mut RNG,
|
||||||
|
buf: &mut RadioBuffer<N>,
|
||||||
|
event: Event<R>,
|
||||||
|
) -> (State, Result<Response, super::Error<R>>) {
|
||||||
|
enum IntermediateResponse<R: radio::PhyRxTx> {
|
||||||
|
RadioTx((Frame, radio::TxConfig, u32)),
|
||||||
|
EarlyReturn(Result<Response, super::Error<R>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = match event {
|
||||||
|
// tolerate unexpected timeout
|
||||||
|
Event::Join(creds) => {
|
||||||
|
let (tx_config, dev_nonce) = mac.join_otaa::<C, RNG, N>(rng, creds, buf);
|
||||||
|
IntermediateResponse::RadioTx((Frame::Join, tx_config, dev_nonce as u32))
|
||||||
|
}
|
||||||
|
Event::TimeoutFired => IntermediateResponse::EarlyReturn(Ok(Response::NoUpdate)),
|
||||||
|
Event::RadioEvent(_radio_event) => {
|
||||||
|
IntermediateResponse::EarlyReturn(Err(Error::RadioEventWhileIdle.into()))
|
||||||
|
}
|
||||||
|
Event::SendDataRequest(send_data) => {
|
||||||
|
let tx_config = mac.send::<C, RNG, N>(rng, buf, &send_data);
|
||||||
|
match tx_config {
|
||||||
|
Err(e) => IntermediateResponse::EarlyReturn(Err(e.into())),
|
||||||
|
Ok((tx_config, fcnt_up)) => {
|
||||||
|
IntermediateResponse::RadioTx((Frame::Data, tx_config, fcnt_up))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match response {
|
||||||
|
IntermediateResponse::EarlyReturn(response) => (State::Idle(self), response),
|
||||||
|
IntermediateResponse::RadioTx((frame, tx_config, fcnt_up)) => {
|
||||||
|
let event: radio::Event<R> =
|
||||||
|
radio::Event::TxRequest(tx_config, buf.as_ref_for_read());
|
||||||
|
match radio.handle_event(event) {
|
||||||
|
Ok(response) => {
|
||||||
|
match response {
|
||||||
|
// intermediate state where we wait for Join to complete sending
|
||||||
|
// allows for asynchronous sending
|
||||||
|
radio::Response::Txing => (
|
||||||
|
State::SendingData(SendingData { frame }),
|
||||||
|
Ok(Response::UplinkSending(fcnt_up)),
|
||||||
|
),
|
||||||
|
// directly jump to waiting for RxWindow
|
||||||
|
// allows for synchronous sending
|
||||||
|
radio::Response::TxDone(ms) => {
|
||||||
|
data_rxwindow1_timeout::<R, N>(frame, mac, radio, ms)
|
||||||
|
}
|
||||||
|
_ => (State::Idle(self), Err(Error::UnexpectedRadioResponse.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => (State::Idle(self), Err(super::Error::Radio(e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct SendingData {
|
||||||
|
frame: Frame,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendingData {
|
||||||
|
pub(crate) fn handle_event<R: radio::PhyRxTx + Timings, const N: usize>(
|
||||||
|
self,
|
||||||
|
mac: &mut Mac,
|
||||||
|
radio: &mut R,
|
||||||
|
event: Event<R>,
|
||||||
|
) -> (State, Result<Response, super::Error<R>>) {
|
||||||
|
match event {
|
||||||
|
// we are waiting for the async tx to complete
|
||||||
|
Event::RadioEvent(radio_event) => {
|
||||||
|
// send the transmit request to the radio
|
||||||
|
match radio.handle_event(radio_event) {
|
||||||
|
Ok(response) => {
|
||||||
|
match response {
|
||||||
|
// expect a complete transmit
|
||||||
|
radio::Response::TxDone(ms) => {
|
||||||
|
data_rxwindow1_timeout::<R, N>(self.frame, mac, radio, ms)
|
||||||
|
}
|
||||||
|
// anything other than TxComplete is unexpected
|
||||||
|
_ => {
|
||||||
|
panic!("SendingData: Unexpected radio response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => (State::SendingData(self), Err(super::Error::Radio(e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// tolerate unexpected timeout
|
||||||
|
Event::TimeoutFired => (State::SendingData(self), Ok(Response::NoUpdate)),
|
||||||
|
// anything other than a RadioEvent is unexpected
|
||||||
|
Event::Join(_) | Event::SendDataRequest(_) => {
|
||||||
|
(self.into(), Err(Error::TxRequestDuringTx.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct WaitingForRxWindow {
|
||||||
|
frame: Frame,
|
||||||
|
window: Rx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaitingForRxWindow {
|
||||||
|
pub(crate) fn handle_event<R: radio::PhyRxTx + Timings, const N: usize>(
|
||||||
|
self,
|
||||||
|
mac: &mut Mac,
|
||||||
|
radio: &mut R,
|
||||||
|
event: Event<R>,
|
||||||
|
) -> (State, Result<Response, super::Error<R>>) {
|
||||||
|
match event {
|
||||||
|
// we are waiting for a Timeout
|
||||||
|
Event::TimeoutFired => {
|
||||||
|
let (rx_config, window_start) =
|
||||||
|
mac.get_rx_parameters_legacy(&self.frame, &self.window.into());
|
||||||
|
// configure the radio for the RX
|
||||||
|
match radio.handle_event(radio::Event::RxRequest(rx_config)) {
|
||||||
|
Ok(_) => {
|
||||||
|
let window_close: u32 = match self.window {
|
||||||
|
// RxWindow1 one must timeout before RxWindow2
|
||||||
|
Rx::_1(time) => {
|
||||||
|
let time_between_windows =
|
||||||
|
mac.get_rx_delay(&self.frame, &Window::_2) - window_start;
|
||||||
|
if time_between_windows > radio.get_rx_window_duration_ms() {
|
||||||
|
time + radio.get_rx_window_duration_ms()
|
||||||
|
} else {
|
||||||
|
time + time_between_windows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// RxWindow2 can last however long
|
||||||
|
Rx::_2(time) => time + radio.get_rx_window_duration_ms(),
|
||||||
|
};
|
||||||
|
(
|
||||||
|
State::WaitingForRx(self.into()),
|
||||||
|
Ok(Response::TimeoutRequest(window_close)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(e) => (State::WaitingForRxWindow(self), Err(super::Error::Radio(e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::RadioEvent(_) => (
|
||||||
|
State::WaitingForRxWindow(self),
|
||||||
|
Err(Error::RadioEventWhileWaitingForRxWindow.into()),
|
||||||
|
),
|
||||||
|
Event::Join(_) => (
|
||||||
|
State::WaitingForRxWindow(self),
|
||||||
|
Err(Error::NewSessionWhileWaitingForRxWindow.into()),
|
||||||
|
),
|
||||||
|
Event::SendDataRequest(_) => (
|
||||||
|
State::WaitingForRxWindow(self),
|
||||||
|
Err(Error::SendDataWhileWaitingForRxWindow.into()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<WaitingForRxWindow> for WaitingForRx {
|
||||||
|
fn from(val: WaitingForRxWindow) -> WaitingForRx {
|
||||||
|
WaitingForRx { frame: val.frame, window: val.window }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct WaitingForRx {
|
||||||
|
frame: Frame,
|
||||||
|
window: Rx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaitingForRx {
|
||||||
|
pub(crate) fn handle_event<
|
||||||
|
R: radio::PhyRxTx + Timings,
|
||||||
|
C: CryptoFactory + Default,
|
||||||
|
const N: usize,
|
||||||
|
const D: usize,
|
||||||
|
>(
|
||||||
|
self,
|
||||||
|
mac: &mut Mac,
|
||||||
|
radio: &mut R,
|
||||||
|
buf: &mut RadioBuffer<N>,
|
||||||
|
event: Event<R>,
|
||||||
|
dl: &mut Vec<Downlink, D>,
|
||||||
|
) -> (State, Result<Response, super::Error<R>>) {
|
||||||
|
match event {
|
||||||
|
// we are waiting for the async tx to complete
|
||||||
|
Event::RadioEvent(radio_event) => {
|
||||||
|
// send the transmit request to the radio
|
||||||
|
match radio.handle_event(radio_event) {
|
||||||
|
Ok(response) => match response {
|
||||||
|
radio::Response::RxDone(_quality) => {
|
||||||
|
// copy from radio buffer to mac buffer
|
||||||
|
buf.clear();
|
||||||
|
if let Err(()) =
|
||||||
|
buf.extend_from_slice(radio.get_received_packet().as_ref())
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
State::WaitingForRx(self),
|
||||||
|
Err(Error::BufferTooSmall.into()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
match mac.handle_rx::<C, N, D>(buf, dl) {
|
||||||
|
// NoUpdate can occur when a stray radio packet is received. Maintain state
|
||||||
|
mac::Response::NoUpdate => {
|
||||||
|
(State::WaitingForRx(self), Ok(Response::NoUpdate))
|
||||||
|
}
|
||||||
|
// Any other type of update indicates we are done receiving. Change to Idle
|
||||||
|
r => (State::Idle(Idle), Ok(r.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (State::WaitingForRx(self), Ok(Response::NoUpdate)),
|
||||||
|
},
|
||||||
|
Err(e) => (State::WaitingForRx(self), Err(super::Error::Radio(e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::TimeoutFired => {
|
||||||
|
if let Err(e) = radio.handle_event(radio::Event::CancelRx) {
|
||||||
|
return (State::WaitingForRx(self), Err(super::Error::Radio(e)));
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.window {
|
||||||
|
Rx::_1(t1) => {
|
||||||
|
let time_between_windows = mac.get_rx_delay(&self.frame, &Window::_2)
|
||||||
|
- mac.get_rx_delay(&self.frame, &Window::_1);
|
||||||
|
let t2 = t1 + time_between_windows;
|
||||||
|
// TODO: jump to RxWindow2 if t2 == now
|
||||||
|
(
|
||||||
|
State::WaitingForRxWindow(WaitingForRxWindow {
|
||||||
|
frame: self.frame,
|
||||||
|
window: Rx::_2(t2),
|
||||||
|
}),
|
||||||
|
Ok(Response::TimeoutRequest(t2)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Timeout during second RxWindow leads to giving up
|
||||||
|
Rx::_2(_) => {
|
||||||
|
let response = mac.rx2_complete();
|
||||||
|
(State::Idle(Idle), Ok(response.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Join(_) => {
|
||||||
|
(State::WaitingForRx(self), Err(Error::NewSessionWhileWaitingForRx.into()))
|
||||||
|
}
|
||||||
|
Event::SendDataRequest(_) => {
|
||||||
|
(State::WaitingForRx(self), Err(Error::SendDataWhileWaitingForRx.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
enum Rx {
|
||||||
|
_1(u32),
|
||||||
|
_2(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_rxwindow1_timeout<R: radio::PhyRxTx + Timings, const N: usize>(
|
||||||
|
frame: Frame,
|
||||||
|
mac: &mut Mac,
|
||||||
|
radio: &mut R,
|
||||||
|
timestamp_ms: u32,
|
||||||
|
) -> (State, Result<Response, super::Error<R>>) {
|
||||||
|
let delay = mac.get_rx_delay(&frame, &Window::_1);
|
||||||
|
let t1 = (delay as i32 + timestamp_ms as i32 + radio.get_rx_window_offset_ms()) as u32;
|
||||||
|
(
|
||||||
|
State::WaitingForRxWindow(WaitingForRxWindow { frame, window: Rx::_1(t1) }),
|
||||||
|
Ok(Response::TimeoutRequest(t1)),
|
||||||
|
)
|
||||||
|
}
|
||||||
128
lorawan-device-patch/src/nb_device/test/mod.rs
Normal file
128
lorawan-device-patch/src/nb_device/test/mod.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use super::*;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use crate::nb_device::Event;
|
||||||
|
#[test]
|
||||||
|
fn test_join_rx1() {
|
||||||
|
let mut device = test_device();
|
||||||
|
let response = device.join(get_otaa_credentials()).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(5000)));
|
||||||
|
// send a timeout for beginning of window
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(5100)));
|
||||||
|
device.get_radio().set_rxtx_handler(handle_join_request::<1>);
|
||||||
|
// send a radio event to let the radio device indicate a packet was received
|
||||||
|
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||||
|
assert!(matches!(response, Response::JoinSuccess));
|
||||||
|
assert!(device.get_session_keys().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_join_rx2() {
|
||||||
|
let mut device = test_device();
|
||||||
|
device.get_radio().set_rxtx_handler(handle_join_request::<2>);
|
||||||
|
let response = device.join(get_otaa_credentials()).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(5000)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(5100)));
|
||||||
|
// send a timeout for end of rx2
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(6000)));
|
||||||
|
// send a timeout for beginning of rx2
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(6100)));
|
||||||
|
// send a radio event to let the radio device indicate a packet was received
|
||||||
|
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||||
|
assert!(matches!(response, Response::JoinSuccess));
|
||||||
|
assert!(device.get_session_keys().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unconfirmed_uplink_no_downlink() {
|
||||||
|
let mut device = test_device();
|
||||||
|
device.join(get_abp_credentials()).unwrap();
|
||||||
|
let response = device.send(&[0; 1], 1, false).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // end Rx1
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(2000)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // being Rx2
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(2100)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // end Rx2
|
||||||
|
assert!(matches!(response, Response::RxComplete));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_confirmed_uplink_no_ack() {
|
||||||
|
let mut device = test_device();
|
||||||
|
let response = device.join(get_abp_credentials());
|
||||||
|
assert!(matches!(response, Ok(Response::JoinSuccess)));
|
||||||
|
let response = device.send(&[0; 1], 1, true).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // end Rx1
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(2000)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // being Rx2
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(2100)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // end Rx2
|
||||||
|
assert!(matches!(response, Response::NoAck));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_confirmed_uplink_with_ack_rx1() {
|
||||||
|
let mut device = test_device();
|
||||||
|
let response = device.join(get_abp_credentials());
|
||||||
|
assert!(matches!(response, Ok(Response::JoinSuccess)));
|
||||||
|
let response = device.send(&[0; 1], 1, true).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||||
|
device.get_radio().set_rxtx_handler(handle_data_uplink_with_link_adr_req::<0, 0>);
|
||||||
|
// send a radio event to let the radio device indicate a packet was received
|
||||||
|
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||||
|
assert!(matches!(response, Response::DownlinkReceived(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_confirmed_uplink_with_ack_rx2() {
|
||||||
|
let mut device = test_device();
|
||||||
|
let response = device.join(get_abp_credentials());
|
||||||
|
assert!(matches!(response, Ok(Response::JoinSuccess)));
|
||||||
|
let response = device.send(&[0; 1], 1, true).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // end Rx1
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(2000)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // being Rx2
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(2100)));
|
||||||
|
device.get_radio().set_rxtx_handler(handle_data_uplink_with_link_adr_req::<0, 0>);
|
||||||
|
// send a radio event to let the radio device indicate a packet was received
|
||||||
|
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||||
|
assert!(matches!(response, Response::DownlinkReceived(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_link_adr_ans() {
|
||||||
|
let mut device = test_device();
|
||||||
|
let response = device.join(get_abp_credentials());
|
||||||
|
assert!(matches!(response, Ok(Response::JoinSuccess)));
|
||||||
|
let response = device.send(&[0; 1], 1, true).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||||
|
device.get_radio().set_rxtx_handler(handle_data_uplink_with_link_adr_req::<0, 0>);
|
||||||
|
// send a radio event to let the radio device indicate a packet was received
|
||||||
|
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||||
|
assert!(matches!(response, Response::DownlinkReceived(0)));
|
||||||
|
// send another uplink which should carry the LinkAdrAns
|
||||||
|
let response = device.send(&[0; 1], 1, true).unwrap();
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1000)));
|
||||||
|
let response = device.handle_event(Event::TimeoutFired).unwrap(); // begin Rx1
|
||||||
|
assert!(matches!(response, Response::TimeoutRequest(1100)));
|
||||||
|
device.get_radio().set_rxtx_handler(handle_data_uplink_with_link_adr_ans);
|
||||||
|
// send a radio event to let the radio device indicate a packet was received
|
||||||
|
let response = device.handle_event(Event::RadioEvent(radio::Event::Phy(()))).unwrap();
|
||||||
|
assert!(matches!(response, Response::DownlinkReceived(1)));
|
||||||
|
}
|
||||||
96
lorawan-device-patch/src/nb_device/test/util.rs
Normal file
96
lorawan-device-patch/src/nb_device/test/util.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use crate::radio::{RfConfig, RxQuality};
|
||||||
|
|
||||||
|
use crate::nb_device::{
|
||||||
|
radio::{Event, PhyRxTx, Response},
|
||||||
|
Device, Timings,
|
||||||
|
};
|
||||||
|
use lorawan::default_crypto;
|
||||||
|
use region::{Configuration, Region};
|
||||||
|
|
||||||
|
pub fn test_device() -> Device<TestRadio, default_crypto::DefaultFactory, rand_core::OsRng, 255> {
|
||||||
|
Device::new(Configuration::new(Region::US915), TestRadio::default(), rand::rngs::OsRng)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TestRadio {
|
||||||
|
current_config: Option<RfConfig>,
|
||||||
|
last_uplink: Option<Uplink>,
|
||||||
|
rxtx_handler: Option<RxTxHandler>,
|
||||||
|
buffer: [u8; 256],
|
||||||
|
buffer_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestRadio {
|
||||||
|
pub fn set_rxtx_handler(&mut self, handler: RxTxHandler) {
|
||||||
|
self.rxtx_handler = Some(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TestRadio {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
current_config: None,
|
||||||
|
last_uplink: None,
|
||||||
|
rxtx_handler: None,
|
||||||
|
buffer: [0; 256],
|
||||||
|
buffer_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PhyRxTx for TestRadio {
|
||||||
|
type PhyEvent = ();
|
||||||
|
type PhyError = &'static str;
|
||||||
|
type PhyResponse = ();
|
||||||
|
|
||||||
|
const MAX_RADIO_POWER: u8 = 26;
|
||||||
|
|
||||||
|
const ANTENNA_GAIN: i8 = 0;
|
||||||
|
|
||||||
|
fn get_mut_radio(&mut self) -> &mut Self {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn get_received_packet(&mut self) -> &mut [u8] {
|
||||||
|
&mut self.buffer[..self.buffer_index]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(&mut self, event: Event<Self>) -> Result<Response<Self>, Self::PhyError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
match event {
|
||||||
|
Event::TxRequest(config, buf) => {
|
||||||
|
// ensure that we have always consumed the previous uplink
|
||||||
|
if self.last_uplink.is_some() {
|
||||||
|
panic!("last uplink not consumed")
|
||||||
|
}
|
||||||
|
self.last_uplink =
|
||||||
|
Some(Uplink::new(buf, config).map_err(|_| "error creating uplink")?);
|
||||||
|
return Ok(Response::TxDone(0));
|
||||||
|
}
|
||||||
|
Event::RxRequest(rf_config) => {
|
||||||
|
self.current_config = Some(rf_config);
|
||||||
|
}
|
||||||
|
Event::CancelRx => (),
|
||||||
|
Event::Phy(()) => {
|
||||||
|
if let (Some(rf_config), Some(rxtx_handler)) =
|
||||||
|
(self.current_config, self.rxtx_handler)
|
||||||
|
{
|
||||||
|
self.buffer_index =
|
||||||
|
rxtx_handler(self.last_uplink.take(), rf_config, &mut self.buffer);
|
||||||
|
return Ok(Response::RxDone(RxQuality::new(0, 0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Response::Idle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timings for TestRadio {
|
||||||
|
fn get_rx_window_offset_ms(&self) -> i32 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
fn get_rx_window_duration_ms(&self) -> u32 {
|
||||||
|
100
|
||||||
|
}
|
||||||
|
}
|
||||||
112
lorawan-device-patch/src/radio.rs
Normal file
112
lorawan-device-patch/src/radio.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
pub use lora_modulation::BaseBandModulationParams;
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct RfConfig {
|
||||||
|
pub frequency: u32,
|
||||||
|
pub bb: BaseBandModulationParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum RxMode {
|
||||||
|
Continuous,
|
||||||
|
/// Single shot receive. Argument `ms` indicates how many milliseconds of extra buffer time should
|
||||||
|
/// be added to the preamble detection timeout.
|
||||||
|
Single {
|
||||||
|
ms: u32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct RxConfig {
|
||||||
|
pub rf: RfConfig,
|
||||||
|
pub mode: RxMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct TxConfig {
|
||||||
|
pub pw: i8,
|
||||||
|
pub rf: RfConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TxConfig {
|
||||||
|
pub fn adjust_power(&mut self, max_power: u8, antenna_gain: i8) {
|
||||||
|
self.pw -= antenna_gain;
|
||||||
|
self.pw = core::cmp::min(self.pw, max_power as i8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct RxQuality {
|
||||||
|
rssi: i16,
|
||||||
|
snr: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RxQuality {
|
||||||
|
pub fn new(rssi: i16, snr: i8) -> RxQuality {
|
||||||
|
RxQuality { rssi, snr }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rssi(self) -> i16 {
|
||||||
|
self.rssi
|
||||||
|
}
|
||||||
|
pub fn snr(self) -> i8 {
|
||||||
|
self.snr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct RadioBuffer<const N: usize> {
|
||||||
|
packet: [u8; N],
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> RadioBuffer<N> {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self { packet: [0; N], pos: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear(&mut self) {
|
||||||
|
self.pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_pos(&mut self, pos: usize) {
|
||||||
|
self.pos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn extend_from_slice(&mut self, buf: &[u8]) -> Result<(), ()> {
|
||||||
|
if self.pos + buf.len() < self.packet.len() {
|
||||||
|
self.packet[self.pos..self.pos + buf.len()].copy_from_slice(buf);
|
||||||
|
self.pos += buf.len();
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides a mutable slice of the buffer up to the current position.
|
||||||
|
pub(crate) fn as_mut_for_read(&mut self) -> &mut [u8] {
|
||||||
|
&mut self.packet[..self.pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides a reference of the buffer up to the current position.
|
||||||
|
|
||||||
|
pub(crate) fn as_ref_for_read(&self) -> &[u8] {
|
||||||
|
&self.packet[..self.pos]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> AsMut<[u8]> for RadioBuffer<N> {
|
||||||
|
fn as_mut(&mut self) -> &mut [u8] {
|
||||||
|
&mut self.packet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> AsRef<[u8]> for RadioBuffer<N> {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
&self.packet
|
||||||
|
}
|
||||||
|
}
|
||||||
16
lorawan-device-patch/src/region/constants.rs
Normal file
16
lorawan-device-patch/src/region/constants.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
use lora_modulation::{Bandwidth, CodingRate, SpreadingFactor};
|
||||||
|
|
||||||
|
pub(crate) const RECEIVE_DELAY1: u32 = 1000;
|
||||||
|
pub(crate) const RECEIVE_DELAY2: u32 = RECEIVE_DELAY1 + 1000; // must be RECEIVE_DELAY + 1 s
|
||||||
|
pub(crate) const JOIN_ACCEPT_DELAY1: u32 = 5000;
|
||||||
|
pub(crate) const JOIN_ACCEPT_DELAY2: u32 = 6000;
|
||||||
|
pub(crate) const MAX_FCNT_GAP: usize = 16384;
|
||||||
|
pub(crate) const ADR_ACK_LIMIT: usize = 64;
|
||||||
|
pub(crate) const ADR_ACK_DELAY: usize = 32;
|
||||||
|
pub(crate) const ACK_TIMEOUT: usize = 2; // random delay between 1 and 3 seconds
|
||||||
|
|
||||||
|
pub(crate) const DEFAULT_BANDWIDTH: Bandwidth = Bandwidth::_125KHz;
|
||||||
|
pub(crate) const DEFAULT_SPREADING_FACTOR: SpreadingFactor = SpreadingFactor::_7;
|
||||||
|
pub(crate) const DEFAULT_CODING_RATE: CodingRate = CodingRate::_4_5;
|
||||||
|
pub(crate) const DEFAULT_DBM: i8 = 14;
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
const JOIN_CHANNELS: [u32; 2] = [923200000, 923200000];
|
||||||
|
|
||||||
|
pub(crate) type AS923_1 = DynamicChannelPlan<2, 7, AS923Region<923_200_000, 0>>;
|
||||||
|
pub(crate) type AS923_2 = DynamicChannelPlan<2, 7, AS923Region<921_400_000, 1800000>>;
|
||||||
|
pub(crate) type AS923_3 = DynamicChannelPlan<2, 7, AS923Region<916_600_000, 6600000>>;
|
||||||
|
pub(crate) type AS923_4 = DynamicChannelPlan<2, 7, AS923Region<917_300_000, 5900000>>;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
pub struct AS923Region<const DEFAULT_RX2: u32, const O: u32>;
|
||||||
|
|
||||||
|
impl<const DEFAULT_RX2: u32, const OFFSET: u32> ChannelRegion<7>
|
||||||
|
for AS923Region<DEFAULT_RX2, OFFSET>
|
||||||
|
{
|
||||||
|
fn datarates() -> &'static [Option<Datarate>; 7] {
|
||||||
|
&DATARATES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const DEFAULT_RX2: u32, const OFFSET: u32> DynamicChannelRegion<2, 7>
|
||||||
|
for AS923Region<DEFAULT_RX2, OFFSET>
|
||||||
|
{
|
||||||
|
fn join_channels() -> [u32; 2] {
|
||||||
|
[JOIN_CHANNELS[0] + OFFSET, JOIN_CHANNELS[1] + OFFSET]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_default_rx2() -> u32 {
|
||||||
|
DEFAULT_RX2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||||
|
|
||||||
|
pub(crate) const DATARATES: [Option<Datarate>; 7] = [
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_12,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 0,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_11,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 0,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_10,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 123,
|
||||||
|
max_mac_payload_size_with_dwell_time: 19,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_9,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 123,
|
||||||
|
max_mac_payload_size_with_dwell_time: 61,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_8,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 133,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_250KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
// TODO: ignore FSK data rate for now
|
||||||
|
];
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const JOIN_CHANNELS: [u32; 3] = [433_175_000, 433_375_000, 433_575_000];
|
||||||
|
|
||||||
|
pub(crate) type EU433 = DynamicChannelPlan<3, 7, EU433Region>;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
pub struct EU433Region;
|
||||||
|
|
||||||
|
impl ChannelRegion<7> for EU433Region {
|
||||||
|
fn datarates() -> &'static [Option<Datarate>; 7] {
|
||||||
|
&DATARATES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicChannelRegion<3, 7> for EU433Region {
|
||||||
|
fn join_channels() -> [u32; 3] {
|
||||||
|
JOIN_CHANNELS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_default_rx2() -> u32 {
|
||||||
|
434_665_000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||||
|
|
||||||
|
pub(crate) const DATARATES: [Option<Datarate>; 7] = [
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_12,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 0,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_11,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 0,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_10,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 123,
|
||||||
|
max_mac_payload_size_with_dwell_time: 19,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_9,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 123,
|
||||||
|
max_mac_payload_size_with_dwell_time: 61,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_8,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 133,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_250KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
// TODO 7 is defined in rp002-1-0-4
|
||||||
|
];
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const JOIN_CHANNELS: [u32; 3] = [868_100_000, 868_300_000, 868_500_000];
|
||||||
|
|
||||||
|
pub(crate) type EU868 = DynamicChannelPlan<3, 7, EU868Region>;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
pub struct EU868Region;
|
||||||
|
|
||||||
|
impl ChannelRegion<7> for EU868Region {
|
||||||
|
fn datarates() -> &'static [Option<Datarate>; 7] {
|
||||||
|
&DATARATES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicChannelRegion<3, 7> for EU868Region {
|
||||||
|
fn join_channels() -> [u32; 3] {
|
||||||
|
JOIN_CHANNELS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_default_rx2() -> u32 {
|
||||||
|
869_525_000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||||
|
|
||||||
|
pub(crate) const DATARATES: [Option<Datarate>; 7] = [
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_12,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 59,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_11,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 59,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_10,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 59,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_9,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 123,
|
||||||
|
max_mac_payload_size_with_dwell_time: 123,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_8,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_250KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
// TODO: ignore FSK data rate for now
|
||||||
|
];
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const JOIN_CHANNELS: [u32; 3] = [865_062_500, 865_402_500, 865_985_000];
|
||||||
|
|
||||||
|
pub(crate) type IN865 = DynamicChannelPlan<3, 6, IN865Region>;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
pub struct IN865Region;
|
||||||
|
|
||||||
|
impl ChannelRegion<6> for IN865Region {
|
||||||
|
fn datarates() -> &'static [Option<Datarate>; 6] {
|
||||||
|
&DATARATES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicChannelRegion<3, 6> for IN865Region {
|
||||||
|
fn join_channels() -> [u32; 3] {
|
||||||
|
JOIN_CHANNELS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_default_rx2() -> u32 {
|
||||||
|
866_550_000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||||
|
|
||||||
|
pub(crate) const DATARATES: [Option<Datarate>; 6] = [
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_12,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 59,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_11,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 59,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_10,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 59,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_9,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 123,
|
||||||
|
max_mac_payload_size_with_dwell_time: 123,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_8,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
// TODO: ignore FSK data rate for now
|
||||||
|
];
|
||||||
220
lorawan-device-patch/src/region/dynamic_channel_plans/mod.rs
Normal file
220
lorawan-device-patch/src/region/dynamic_channel_plans/mod.rs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
use super::*;
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "region-as923-1",
|
||||||
|
feature = "region-as923-2",
|
||||||
|
feature = "region-as923-3",
|
||||||
|
feature = "region-as923-4"
|
||||||
|
))]
|
||||||
|
mod as923;
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
mod eu433;
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
mod eu868;
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
mod in865;
|
||||||
|
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
pub(crate) use as923::AS923_1;
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
pub(crate) use as923::AS923_2;
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
pub(crate) use as923::AS923_3;
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
pub(crate) use as923::AS923_4;
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
pub(crate) use eu433::EU433;
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
pub(crate) use eu868::EU868;
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
pub(crate) use in865::IN865;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub(crate) struct DynamicChannelPlan<
|
||||||
|
const NUM_JOIN_CHANNELS: usize,
|
||||||
|
const NUM_DATARATES: usize,
|
||||||
|
R: DynamicChannelRegion<NUM_JOIN_CHANNELS, NUM_DATARATES>,
|
||||||
|
> {
|
||||||
|
additional_channels: [Option<u32>; 5],
|
||||||
|
channel_mask: ChannelMask<9>,
|
||||||
|
last_tx_channel: u8,
|
||||||
|
_fixed_channel_region: PhantomData<R>,
|
||||||
|
rx1_offset: usize,
|
||||||
|
rx2_dr: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
const NUM_JOIN_CHANNELS: usize,
|
||||||
|
const NUM_DATARATES: usize,
|
||||||
|
R: DynamicChannelRegion<NUM_JOIN_CHANNELS, NUM_DATARATES>,
|
||||||
|
> DynamicChannelPlan<NUM_JOIN_CHANNELS, NUM_DATARATES, R>
|
||||||
|
{
|
||||||
|
fn get_channel(&self, channel: usize) -> Option<u32> {
|
||||||
|
if channel < NUM_JOIN_CHANNELS {
|
||||||
|
Some(R::join_channels()[channel])
|
||||||
|
} else {
|
||||||
|
let index = channel - NUM_JOIN_CHANNELS;
|
||||||
|
if index < self.additional_channels.len() {
|
||||||
|
self.additional_channels[index]
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highest_additional_channel_index_plus_one(&self) -> usize {
|
||||||
|
let mut index_plus_one = 0;
|
||||||
|
for (i, channel) in self.additional_channels.iter().enumerate() {
|
||||||
|
if channel.is_some() {
|
||||||
|
index_plus_one = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index_plus_one
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_random_in_range<RNG: RngCore>(&self, rng: &mut RNG) -> usize {
|
||||||
|
let range = self.highest_additional_channel_index_plus_one() + NUM_JOIN_CHANNELS;
|
||||||
|
let cm = if range > 16 {
|
||||||
|
0b11111
|
||||||
|
} else if range > 8 {
|
||||||
|
0b1111
|
||||||
|
} else {
|
||||||
|
0b111
|
||||||
|
};
|
||||||
|
(rng.next_u32() as usize) & cm
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_max_payload_length(datarate: DR, repeater_compatible: bool, dwell_time: bool) -> u8 {
|
||||||
|
R::get_max_payload_length(datarate, repeater_compatible, dwell_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait DynamicChannelRegion<const NUM_JOIN_CHANNELS: usize, const NUM_DATARATES: usize>:
|
||||||
|
ChannelRegion<NUM_DATARATES>
|
||||||
|
{
|
||||||
|
fn join_channels() -> [u32; NUM_JOIN_CHANNELS];
|
||||||
|
fn get_default_rx2() -> u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
const NUM_JOIN_CHANNELS: usize,
|
||||||
|
const NUM_DATARATES: usize,
|
||||||
|
R: DynamicChannelRegion<NUM_JOIN_CHANNELS, NUM_DATARATES>,
|
||||||
|
> RegionHandler for DynamicChannelPlan<NUM_JOIN_CHANNELS, NUM_DATARATES, R>
|
||||||
|
{
|
||||||
|
fn process_join_accept<T: AsRef<[u8]>, C>(
|
||||||
|
&mut self,
|
||||||
|
join_accept: &DecryptedJoinAcceptPayload<T, C>,
|
||||||
|
) {
|
||||||
|
match join_accept.c_f_list() {
|
||||||
|
Some(CfList::DynamicChannel(cf_list)) => {
|
||||||
|
// If CfList of Type 0 is present, it may contain up to 5 frequencies
|
||||||
|
// which define channels J to (J+4)
|
||||||
|
for (index, freq) in cf_list.iter().enumerate() {
|
||||||
|
let value = freq.value();
|
||||||
|
// unused channels are set to 0
|
||||||
|
if value != 0 {
|
||||||
|
self.additional_channels[index] = Some(value);
|
||||||
|
} else {
|
||||||
|
self.additional_channels[index] = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(CfList::FixedChannel(_cf_list)) => {
|
||||||
|
//TODO: dynamic channel plans have corresponding fixed channel lists,
|
||||||
|
// however, this feature is entirely optional
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_link_adr_channel_mask(
|
||||||
|
&mut self,
|
||||||
|
channel_mask_control: u8,
|
||||||
|
channel_mask: ChannelMask<2>,
|
||||||
|
) {
|
||||||
|
match channel_mask_control {
|
||||||
|
0..=4 => {
|
||||||
|
let base_index = channel_mask_control as usize * 2;
|
||||||
|
self.channel_mask.set_bank(base_index, channel_mask.get_index(0));
|
||||||
|
self.channel_mask.set_bank(base_index + 1, channel_mask.get_index(1));
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
let channel_mask: u16 =
|
||||||
|
channel_mask.get_index(0) as u16 | ((channel_mask.get_index(1) as u16) << 8);
|
||||||
|
self.channel_mask.set_bank(0, ((channel_mask & 0b1) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(1, ((channel_mask & 0b10) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(2, ((channel_mask & 0b100) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(3, ((channel_mask & 0b1000) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(4, ((channel_mask & 0b10000) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(5, ((channel_mask & 0b100000) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(6, ((channel_mask & 0b1000000) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(7, ((channel_mask & 0b10000000) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(8, ((channel_mask & 0b100000000) * 0xFF) as u8);
|
||||||
|
}
|
||||||
|
6 => {
|
||||||
|
// all channels on
|
||||||
|
for i in 0..8 {
|
||||||
|
self.channel_mask.set_bank(i, 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
//RFU
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tx_dr_and_frequency<RNG: RngCore>(
|
||||||
|
&mut self,
|
||||||
|
rng: &mut RNG,
|
||||||
|
datarate: DR,
|
||||||
|
frame: &Frame,
|
||||||
|
) -> (Datarate, u32) {
|
||||||
|
match frame {
|
||||||
|
Frame::Join => {
|
||||||
|
// there are at most 8 join channels
|
||||||
|
let mut channel = (rng.next_u32() & 0b111) as u8;
|
||||||
|
// keep sampling until we select a join channel depending
|
||||||
|
// on the frequency plan
|
||||||
|
while channel as usize >= NUM_JOIN_CHANNELS {
|
||||||
|
channel = (rng.next_u32() & 0b111) as u8;
|
||||||
|
}
|
||||||
|
self.last_tx_channel = channel;
|
||||||
|
(
|
||||||
|
R::datarates()[datarate as usize].clone().unwrap(),
|
||||||
|
R::join_channels()[channel as usize],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Frame::Data => {
|
||||||
|
let mut channel = self.get_random_in_range(rng);
|
||||||
|
loop {
|
||||||
|
if self.channel_mask.is_enabled(channel).unwrap() {
|
||||||
|
if let Some(freq) = self.get_channel(channel) {
|
||||||
|
self.last_tx_channel = channel as u8;
|
||||||
|
return (R::datarates()[datarate as usize].clone().unwrap(), freq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channel = self.get_random_in_range(rng)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_rx_frequency(&self, _frame: &Frame, window: &Window) -> u32 {
|
||||||
|
match window {
|
||||||
|
// TODO: implement RxOffset but first need to implement RxOffset MacCommand
|
||||||
|
Window::_1 => self.get_channel(self.last_tx_channel as usize).unwrap(),
|
||||||
|
Window::_2 => R::get_default_rx2(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_rx_datarate(&self, tx_datarate: DR, _frame: &Frame, window: &Window) -> Datarate {
|
||||||
|
let datarate = match window {
|
||||||
|
Window::_1 => tx_datarate as usize + self.rx1_offset,
|
||||||
|
Window::_2 => self.rx2_dr,
|
||||||
|
};
|
||||||
|
R::datarates()[datarate].clone().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||||
|
|
||||||
|
pub(crate) const DATARATES: [Option<Datarate>; 16] = [
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_12,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 0,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_11,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 0,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_10,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 59,
|
||||||
|
max_mac_payload_size_with_dwell_time: 19,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_9,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 123,
|
||||||
|
max_mac_payload_size_with_dwell_time: 61,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_8,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 133,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_8,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
None, // LR-FHSS -- not currently supported, TODO: defined in rp002-1-0-4
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_12,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 61,
|
||||||
|
max_mac_payload_size_with_dwell_time: 61,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_11,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 137,
|
||||||
|
max_mac_payload_size_with_dwell_time: 137,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_10,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_9,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_8,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
None, // RFU, TODO: defined in rp002-1-0-4
|
||||||
|
None, // TODO: defined in rp002-1-0-4
|
||||||
|
];
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
pub(crate) const UPLINK_CHANNEL_MAP: [u32; 72] = [
|
||||||
|
// channels 0-7 (125 kHz)
|
||||||
|
915_200_000,
|
||||||
|
915_400_000,
|
||||||
|
915_600_000,
|
||||||
|
915_800_000,
|
||||||
|
916_000_000,
|
||||||
|
916_200_000,
|
||||||
|
916_400_000,
|
||||||
|
916_600_000,
|
||||||
|
// channels 8-15 (125 kHz)
|
||||||
|
916_800_000,
|
||||||
|
917_000_000,
|
||||||
|
917_200_000,
|
||||||
|
917_400_000,
|
||||||
|
917_600_000,
|
||||||
|
917_800_000,
|
||||||
|
918_000_000,
|
||||||
|
918_200_000,
|
||||||
|
// channels 16-23 (125 kHz)
|
||||||
|
918_400_000,
|
||||||
|
918_600_000,
|
||||||
|
918_800_000,
|
||||||
|
919_000_000,
|
||||||
|
919_200_000,
|
||||||
|
919_400_000,
|
||||||
|
919_600_000,
|
||||||
|
919_800_000,
|
||||||
|
// channels 24-31 (125 kHz)
|
||||||
|
920_000_000,
|
||||||
|
920_200_000,
|
||||||
|
920_400_000,
|
||||||
|
920_600_000,
|
||||||
|
920_800_000,
|
||||||
|
921_000_000,
|
||||||
|
921_200_000,
|
||||||
|
921_400_000,
|
||||||
|
// channels 32-39 (125 kHz)
|
||||||
|
921_600_000,
|
||||||
|
921_800_000,
|
||||||
|
922_000_000,
|
||||||
|
922_200_000,
|
||||||
|
922_400_000,
|
||||||
|
922_600_000,
|
||||||
|
922_800_000,
|
||||||
|
923_000_000,
|
||||||
|
// channels 39-47 (125 kHz)
|
||||||
|
923_200_000,
|
||||||
|
923_400_000,
|
||||||
|
923_600_000,
|
||||||
|
923_800_000,
|
||||||
|
924_000_000,
|
||||||
|
924_200_000,
|
||||||
|
924_400_000,
|
||||||
|
924_600_000,
|
||||||
|
// channels 47-55 (125 kHz)
|
||||||
|
924_800_000,
|
||||||
|
925_000_000,
|
||||||
|
925_200_000,
|
||||||
|
925_400_000,
|
||||||
|
925_600_000,
|
||||||
|
925_800_000,
|
||||||
|
926_000_000,
|
||||||
|
926_200_000,
|
||||||
|
// channels 55-63 (125 kHz)
|
||||||
|
926_400_000,
|
||||||
|
926_600_000,
|
||||||
|
926_800_000,
|
||||||
|
927_000_000,
|
||||||
|
927_200_000,
|
||||||
|
927_400_000,
|
||||||
|
927_600_000,
|
||||||
|
927_800_000,
|
||||||
|
// channels 63-71 (500 kHz)
|
||||||
|
915_900_000,
|
||||||
|
917_500_000,
|
||||||
|
919_100_000,
|
||||||
|
920_700_000,
|
||||||
|
922_300_000,
|
||||||
|
923_900_000,
|
||||||
|
925_500_000,
|
||||||
|
927_100_000,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) const DOWNLINK_CHANNEL_MAP: [u32; 8] = [
|
||||||
|
922_300_000,
|
||||||
|
923_900_000,
|
||||||
|
924_500_000,
|
||||||
|
925_100_000,
|
||||||
|
925_700_000,
|
||||||
|
926_300_000,
|
||||||
|
926_900_000,
|
||||||
|
927_500_000,
|
||||||
|
];
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
mod frequencies;
|
||||||
|
use frequencies::*;
|
||||||
|
|
||||||
|
mod datarates;
|
||||||
|
use datarates::*;
|
||||||
|
|
||||||
|
const AU_DBM: i8 = 21;
|
||||||
|
const DEFAULT_RX2: u32 = 923_300_000;
|
||||||
|
|
||||||
|
/// State struct for the `AU915` region. This struct may be created directly if you wish to fine-tune some parameters.
|
||||||
|
/// At this time specifying a bias for the subband used during the join process is supported using
|
||||||
|
/// [`set_join_bias`](Self::set_join_bias) and [`set_join_bias_and_noncompliant_retries`](Self::set_join_bias_and_noncompliant_retries)
|
||||||
|
/// is suppored. This struct can then be turned into a [`Configuration`] as it implements [`Into<Configuration>`].
|
||||||
|
///
|
||||||
|
/// # Note:
|
||||||
|
///
|
||||||
|
/// Only [`US915`] and [`AU915`] can be created using this method, because they are the only ones which have
|
||||||
|
/// parameters that may be fine-tuned at the region level. To create a [`Configuration`] for other regions, use
|
||||||
|
/// [`Configuration::new`] and specify the region using the [`Region`] enum.
|
||||||
|
///
|
||||||
|
/// # Example: Setting up join bias
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use lorawan_device::region::{Configuration, AU915, Subband};
|
||||||
|
///
|
||||||
|
/// let mut au915 = AU915::new();
|
||||||
|
/// // Subband 2 is commonly used for The Things Network.
|
||||||
|
/// au915.set_join_bias(Subband::_2);
|
||||||
|
/// let configuration: Configuration = au915.into();
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct AU915(pub(crate) FixedChannelPlan<16, AU915Region>);
|
||||||
|
|
||||||
|
impl AU915 {
|
||||||
|
pub fn get_max_payload_length(datarate: DR, repeater_compatible: bool, dwell_time: bool) -> u8 {
|
||||||
|
AU915Region::get_max_payload_length(datarate, repeater_compatible, dwell_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub(crate) struct AU915Region;
|
||||||
|
|
||||||
|
impl ChannelRegion<16> for AU915Region {
|
||||||
|
fn datarates() -> &'static [Option<Datarate>; 16] {
|
||||||
|
&DATARATES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixedChannelRegion<16> for AU915Region {
|
||||||
|
fn uplink_channels() -> &'static [u32; 72] {
|
||||||
|
&UPLINK_CHANNEL_MAP
|
||||||
|
}
|
||||||
|
fn downlink_channels() -> &'static [u32; 8] {
|
||||||
|
&DOWNLINK_CHANNEL_MAP
|
||||||
|
}
|
||||||
|
fn get_default_rx2() -> u32 {
|
||||||
|
DEFAULT_RX2
|
||||||
|
}
|
||||||
|
fn get_rx_datarate(tx_datarate: DR, _frame: &Frame, window: &Window) -> Datarate {
|
||||||
|
let datarate = match window {
|
||||||
|
Window::_1 => {
|
||||||
|
// no support for RX1 DR Offset
|
||||||
|
match tx_datarate {
|
||||||
|
DR::_0 => DR::_8,
|
||||||
|
DR::_1 => DR::_9,
|
||||||
|
DR::_2 => DR::_10,
|
||||||
|
DR::_3 => DR::_11,
|
||||||
|
DR::_4 => DR::_12,
|
||||||
|
DR::_5 => DR::_13,
|
||||||
|
DR::_6 => DR::_13,
|
||||||
|
DR::_7 => DR::_9,
|
||||||
|
_ => panic!("Invalid TX datarate"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Window::_2 => DR::_8,
|
||||||
|
};
|
||||||
|
DATARATES[datarate as usize].clone().unwrap()
|
||||||
|
}
|
||||||
|
fn get_dbm() -> i8 {
|
||||||
|
AU_DBM
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
use super::*;
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub(crate) struct JoinChannels {
|
||||||
|
/// The maximum amount of times we attempt to join on the preferred subband.
|
||||||
|
max_retries: usize,
|
||||||
|
/// The amount of times we've currently attempted to join on the preferred subband.
|
||||||
|
pub num_retries: usize,
|
||||||
|
/// Preferred subband
|
||||||
|
preferred_subband: Option<Subband>,
|
||||||
|
/// Channels that have been attempted.
|
||||||
|
pub(crate) available_channels: AvailableChannels,
|
||||||
|
/// The channel used for the previous join request.
|
||||||
|
pub(crate) previous_channel: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JoinChannels {
|
||||||
|
pub(crate) fn has_bias_and_not_exhausted(&self) -> bool {
|
||||||
|
// there are remaining retries AND we have not yet been reset
|
||||||
|
self.preferred_subband.is_some()
|
||||||
|
&& self.num_retries < self.max_retries
|
||||||
|
&& self.num_retries != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first data channel will always be some random channel (possibly the same as previous)
|
||||||
|
/// of the preferred subband. Returns None if there is no preferred subband.
|
||||||
|
pub(crate) fn first_data_channel(&mut self, rng: &mut impl RngCore) -> Option<u8> {
|
||||||
|
if self.preferred_subband.is_some() && self.num_retries != 0 {
|
||||||
|
self.clear_join_bias();
|
||||||
|
// determine which subband the successful join was sent on
|
||||||
|
let sb = if self.previous_channel < 64 {
|
||||||
|
self.previous_channel / 8
|
||||||
|
} else {
|
||||||
|
self.previous_channel % 8
|
||||||
|
};
|
||||||
|
// pick another channel on that subband
|
||||||
|
Some((rng.next_u32() & 0b111) as u8 + (sb * 8))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_join_bias(&mut self, subband: Subband, max_retries: usize) {
|
||||||
|
self.preferred_subband = Some(subband);
|
||||||
|
self.max_retries = max_retries;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_join_bias(&mut self) {
|
||||||
|
self.preferred_subband = None;
|
||||||
|
self.max_retries = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To be called after a join accept is received. Resets state for the next join attempt.
|
||||||
|
pub(crate) fn reset(&mut self) {
|
||||||
|
self.num_retries = 0;
|
||||||
|
self.available_channels = AvailableChannels::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_next_channel(&mut self, rng: &mut impl RngCore) -> u8 {
|
||||||
|
match (self.preferred_subband, self.num_retries.cmp(&self.max_retries)) {
|
||||||
|
(Some(sb), Ordering::Less) => {
|
||||||
|
self.num_retries += 1;
|
||||||
|
// pick a random number 0-7 on the preferred subband
|
||||||
|
// NB: we don't use 500 kHz channels
|
||||||
|
let channel = (rng.next_u32() % 8) as u8 + ((sb as usize - 1) as u8 * 8);
|
||||||
|
if self.num_retries == self.max_retries {
|
||||||
|
// this is our last try with our favorite subband, so will initialize the
|
||||||
|
// standard join logic with the channel we just tried. This will ensure
|
||||||
|
// standard and compliant behavior when num_retries is set to 1.
|
||||||
|
self.available_channels.previous = Some(channel);
|
||||||
|
self.available_channels.data.set_channel(channel.into(), false);
|
||||||
|
}
|
||||||
|
self.previous_channel = channel;
|
||||||
|
channel
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.num_retries += 1;
|
||||||
|
self.available_channels.get_next(rng)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub(crate) struct AvailableChannels {
|
||||||
|
data: ChannelMask<9>,
|
||||||
|
previous: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AvailableChannels {
|
||||||
|
fn is_exhausted(&self) -> bool {
|
||||||
|
// check if every underlying byte is entirely cleared to 0
|
||||||
|
for byte in self.data.as_ref() {
|
||||||
|
if *byte != 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_next(&mut self, rng: &mut impl RngCore) -> u8 {
|
||||||
|
// this guarantees that there will be _some_ open channel available
|
||||||
|
if self.is_exhausted() {
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel = self.get_next_channel_inner(rng);
|
||||||
|
// mark the channel invalid for future selection
|
||||||
|
self.data.set_channel(channel.into(), false);
|
||||||
|
self.previous = Some(channel);
|
||||||
|
channel
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_next_channel_inner(&mut self, rng: &mut impl RngCore) -> u8 {
|
||||||
|
if let Some(previous) = self.previous {
|
||||||
|
// choose the next one by possibly wrapping around
|
||||||
|
let next = (previous + 8) % 72;
|
||||||
|
// if the channel is valid, great!
|
||||||
|
if self.data.is_enabled(next.into()).unwrap() {
|
||||||
|
next
|
||||||
|
} else {
|
||||||
|
// We've wrapped around to our original random bank.
|
||||||
|
// Randomly select a new channel on the original bank.
|
||||||
|
// NB: there shall always be something because this will be the first
|
||||||
|
// bank to get exhausted and the caller of this function will reset
|
||||||
|
// when the last one is exhausted.
|
||||||
|
let bank = next / 8;
|
||||||
|
let mut entropy = rng.next_u32();
|
||||||
|
let mut channel = (entropy & 0b111) as u8 + bank * 8;
|
||||||
|
let mut entropy_used = 1;
|
||||||
|
loop {
|
||||||
|
if self.data.is_enabled(channel.into()).unwrap() {
|
||||||
|
return channel;
|
||||||
|
} else {
|
||||||
|
// we've used 30 of the 32 bits of entropy. reset the byte
|
||||||
|
if entropy_used == 10 {
|
||||||
|
entropy = rng.next_u32();
|
||||||
|
entropy_used = 0;
|
||||||
|
}
|
||||||
|
entropy >>= 3;
|
||||||
|
entropy_used += 1;
|
||||||
|
channel = (entropy & 0b111) as u8 + bank * 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// pick a completely random channel on the bottom 64
|
||||||
|
// NB: all channels are currently valid
|
||||||
|
(rng.next_u32() as u8) & 0b111111
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.data = ChannelMask::default();
|
||||||
|
self.previous = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This macro implements public functions relating to a fixed plan region. This is preferred to a
|
||||||
|
/// trait implementation because the user does not have to worry about importing the trait to make
|
||||||
|
/// use of these functions.
|
||||||
|
macro_rules! impl_join_bias {
|
||||||
|
($region:ident) => {
|
||||||
|
impl $region {
|
||||||
|
/// Create this struct directly if you want to specify a subband on which to bias the join process.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify a preferred subband when joining the network. Only the first join attempt
|
||||||
|
/// will occur on this subband. After that, each bank will be attempted sequentially
|
||||||
|
/// as described in the US915/AU915 regional specifications.
|
||||||
|
pub fn set_join_bias(&mut self, subband: Subband) {
|
||||||
|
self.0.join_channels.set_join_bias(subband, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # ⚠️Warning⚠️
|
||||||
|
///
|
||||||
|
/// This method is explicitly not compliant with the LoRaWAN spec when more than one
|
||||||
|
/// try is attempted.
|
||||||
|
///
|
||||||
|
/// This method is similar to `set_join_bias`, but allows you to specify a potentially
|
||||||
|
/// non-compliant amount of times your preferred join subband should be attempted.
|
||||||
|
///
|
||||||
|
/// It is recommended to set a low number (ie, < 10) of join retries using the
|
||||||
|
/// preferred subband. The reason for this is if you *only* try to join
|
||||||
|
/// with a channel bias, and the network is configured to use a
|
||||||
|
/// strictly different set of channels than the ones you provide, the
|
||||||
|
/// network will NEVER be joined.
|
||||||
|
pub fn set_join_bias_and_noncompliant_retries(
|
||||||
|
&mut self,
|
||||||
|
subband: Subband,
|
||||||
|
max_retries: usize,
|
||||||
|
) {
|
||||||
|
self.0.join_channels.set_join_bias(subband, max_retries)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_join_bias(&mut self) {
|
||||||
|
self.0.join_channels.clear_join_bias()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
impl_join_bias!(AU915);
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
impl_join_bias!(US915);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::mac::Response;
|
||||||
|
use crate::{
|
||||||
|
mac::{Mac, SendData},
|
||||||
|
test_util::{get_key, handle_join_request, Uplink},
|
||||||
|
AppEui, AppKey, DevEui, NetworkCredentials,
|
||||||
|
};
|
||||||
|
use heapless::Vec;
|
||||||
|
use lorawan::default_crypto::DefaultFactory;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_join_channels_standard() {
|
||||||
|
let mut rng = rand_core::OsRng;
|
||||||
|
// run the test a bunch of times due to the rng
|
||||||
|
for _ in 0..100 {
|
||||||
|
let mut join_channels = JoinChannels::default();
|
||||||
|
let first_channel = join_channels.get_next_channel(&mut rng);
|
||||||
|
// the first channel is always in the bottom 64
|
||||||
|
assert!(first_channel < 64);
|
||||||
|
let next_channel = join_channels.get_next_channel(&mut rng);
|
||||||
|
// the next channel is always incremented by 8, since we always have
|
||||||
|
// the fat bank (channels 64-71)
|
||||||
|
assert_eq!(next_channel, first_channel + 8);
|
||||||
|
// we generate 6 more channels
|
||||||
|
for _ in 0..7 {
|
||||||
|
let c = join_channels.get_next_channel(&mut rng);
|
||||||
|
assert!(c < 72);
|
||||||
|
}
|
||||||
|
// after 8 tries, we should be back at the original bank but on a different channel
|
||||||
|
let ninth_channel = join_channels.get_next_channel(&mut rng);
|
||||||
|
assert_eq!(ninth_channel / 8, first_channel / 8);
|
||||||
|
assert_ne!(ninth_channel, first_channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_join_channels_standard_exhausted() {
|
||||||
|
let mut rng = rand_core::OsRng;
|
||||||
|
|
||||||
|
let mut join_channels = JoinChannels::default();
|
||||||
|
let first_channel = join_channels.get_next_channel(&mut rng);
|
||||||
|
// the first channel is always in the bottom 64
|
||||||
|
assert!(first_channel < 64);
|
||||||
|
let next_channel = join_channels.get_next_channel(&mut rng);
|
||||||
|
// the next channel is always incremented by 8, since we always have
|
||||||
|
// the fat bank (channels 64-71)
|
||||||
|
assert_eq!(next_channel, first_channel + 8);
|
||||||
|
// we generate 6000
|
||||||
|
for _ in 0..6000 {
|
||||||
|
let c = join_channels.get_next_channel(&mut rng);
|
||||||
|
assert!(c < 72);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_join_channels_biased() {
|
||||||
|
let mut rng = rand_core::OsRng;
|
||||||
|
// run the test a bunch of times due to the rng
|
||||||
|
for _ in 0..100 {
|
||||||
|
let mut join_channels = JoinChannels::default();
|
||||||
|
join_channels.set_join_bias(Subband::_2, 1);
|
||||||
|
let first_channel = join_channels.get_next_channel(&mut rng);
|
||||||
|
// the first is on subband 2
|
||||||
|
assert!(first_channel > 7);
|
||||||
|
assert!(first_channel < 16);
|
||||||
|
let next_channel = join_channels.get_next_channel(&mut rng);
|
||||||
|
// the next channel is always incremented by 8, since we always have
|
||||||
|
// the fat bank (channels 64-71)
|
||||||
|
assert_eq!(next_channel, first_channel + 8);
|
||||||
|
// we generate 6 more channels
|
||||||
|
for _ in 0..7 {
|
||||||
|
let c = join_channels.get_next_channel(&mut rng);
|
||||||
|
assert!(c < 72);
|
||||||
|
}
|
||||||
|
// after 8 tries, we should be back at the biased bank but on a different channel
|
||||||
|
let ninth_channel = join_channels.get_next_channel(&mut rng);
|
||||||
|
assert_eq!(ninth_channel / 8, first_channel / 8);
|
||||||
|
assert_ne!(ninth_channel, first_channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_full_mac_compliant_bias() {
|
||||||
|
let mut us915 = US915::new();
|
||||||
|
us915.set_join_bias(Subband::_2);
|
||||||
|
let mut mac = Mac::new(us915.into(), 21, 2);
|
||||||
|
|
||||||
|
let mut buf: RadioBuffer<255> = RadioBuffer::new();
|
||||||
|
let (tx_config, _len) = mac.join_otaa::<DefaultFactory, _, 255>(
|
||||||
|
&mut rand::rngs::OsRng,
|
||||||
|
NetworkCredentials::new(
|
||||||
|
AppEui::from([0x0; 8]),
|
||||||
|
DevEui::from([0x0; 8]),
|
||||||
|
AppKey::from(get_key()),
|
||||||
|
),
|
||||||
|
&mut buf,
|
||||||
|
);
|
||||||
|
// Confirm that the join request occurs on our subband
|
||||||
|
assert!(
|
||||||
|
tx_config.rf.frequency >= 903_900_000,
|
||||||
|
"Unexpected frequency: {} is below 903.9 MHz!",
|
||||||
|
tx_config.rf.frequency
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
tx_config.rf.frequency <= 905_300_000,
|
||||||
|
"Unexpected frequency: {} is above 905.3 MHz!",
|
||||||
|
tx_config.rf.frequency
|
||||||
|
);
|
||||||
|
let mut downlinks: Vec<_, 3> = Vec::new();
|
||||||
|
let mut data = std::vec::Vec::new();
|
||||||
|
data.extend_from_slice(buf.as_ref_for_read());
|
||||||
|
let uplink = Uplink::new(buf.as_ref_for_read(), tx_config).unwrap();
|
||||||
|
|
||||||
|
let mut rx_buf = [0; 255];
|
||||||
|
let len = handle_join_request::<0>(Some(uplink), tx_config.rf, &mut rx_buf);
|
||||||
|
buf.clear();
|
||||||
|
buf.extend_from_slice(&rx_buf[..len]).unwrap();
|
||||||
|
let response = mac.handle_rx::<DefaultFactory, 255, 3>(&mut buf, &mut downlinks);
|
||||||
|
if let Response::JoinSuccess = response {} else {
|
||||||
|
panic!("Did not receive join success");
|
||||||
|
}
|
||||||
|
let (tx_config, _len) = mac
|
||||||
|
.send::<DefaultFactory, _, 255>(
|
||||||
|
&mut rand::rngs::OsRng,
|
||||||
|
&mut buf,
|
||||||
|
&SendData { fport: 1, data: &[0x0; 1], confirmed: false },
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Confirm that the first data frame occurs on our subband
|
||||||
|
assert!(
|
||||||
|
tx_config.rf.frequency >= 903_900_000,
|
||||||
|
"Unexpected frequency: {} is below 903.9 MHz!",
|
||||||
|
tx_config.rf.frequency
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
tx_config.rf.frequency <= 905_300_000,
|
||||||
|
"Unexpected frequency: {} is above 905.3 MHz!",
|
||||||
|
tx_config.rf.frequency
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_full_mac_non_compliant_bias() {
|
||||||
|
let mut us915 = US915::new();
|
||||||
|
us915.set_join_bias_and_noncompliant_retries(Subband::_2, 8);
|
||||||
|
let mut mac = Mac::new(us915.into(), 21, 2);
|
||||||
|
|
||||||
|
let mut buf: RadioBuffer<255> = RadioBuffer::new();
|
||||||
|
let (tx_config, _len) = mac.join_otaa::<DefaultFactory, _, 255>(
|
||||||
|
&mut rand::rngs::OsRng,
|
||||||
|
NetworkCredentials::new(
|
||||||
|
AppEui::from([0x0; 8]),
|
||||||
|
DevEui::from([0x0; 8]),
|
||||||
|
AppKey::from(get_key()),
|
||||||
|
),
|
||||||
|
&mut buf,
|
||||||
|
);
|
||||||
|
// Confirm that the join request occurs on our subband
|
||||||
|
assert!(
|
||||||
|
tx_config.rf.frequency >= 903_900_000,
|
||||||
|
"Unexpected frequency: {} is below 903.9 MHz!",
|
||||||
|
tx_config.rf.frequency
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
tx_config.rf.frequency <= 905_300_000,
|
||||||
|
"Unexpected frequency: {} is above 905.3 MHz!",
|
||||||
|
tx_config.rf.frequency
|
||||||
|
);
|
||||||
|
let mut downlinks: Vec<_, 3> = Vec::new();
|
||||||
|
let mut data = std::vec::Vec::new();
|
||||||
|
data.extend_from_slice(buf.as_ref_for_read());
|
||||||
|
let uplink = Uplink::new(buf.as_ref_for_read(), tx_config).unwrap();
|
||||||
|
|
||||||
|
let mut rx_buf = [0; 255];
|
||||||
|
let len = handle_join_request::<0>(Some(uplink), tx_config.rf, &mut rx_buf);
|
||||||
|
buf.clear();
|
||||||
|
buf.extend_from_slice(&rx_buf[..len]).unwrap();
|
||||||
|
let response = mac.handle_rx::<DefaultFactory, 255, 3>(&mut buf, &mut downlinks);
|
||||||
|
if let Response::JoinSuccess = response {} else {
|
||||||
|
panic!("Did not receive JoinSuccess")
|
||||||
|
}
|
||||||
|
for _ in 0..8 {
|
||||||
|
let (tx_config, _len) = mac
|
||||||
|
.send::<DefaultFactory, _, 255>(
|
||||||
|
&mut rand::rngs::OsRng,
|
||||||
|
&mut buf,
|
||||||
|
&SendData { fport: 1, data: &[0x0; 1], confirmed: false },
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Confirm that the first data frame occurs on our subband
|
||||||
|
assert!(
|
||||||
|
tx_config.rf.frequency >= 903_900_000,
|
||||||
|
"Unexpected frequency: {} is below 903.9 MHz!",
|
||||||
|
tx_config.rf.frequency
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
tx_config.rf.frequency <= 905_300_000,
|
||||||
|
"Unexpected frequency: {} is above 905.3 MHz!",
|
||||||
|
tx_config.rf.frequency
|
||||||
|
);
|
||||||
|
mac.rx2_complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
lorawan-device-patch/src/region/fixed_channel_plans/mod.rs
Normal file
205
lorawan-device-patch/src/region/fixed_channel_plans/mod.rs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
use super::*;
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
use lorawan::maccommands::ChannelMask;
|
||||||
|
|
||||||
|
mod join_channels;
|
||||||
|
use join_channels::JoinChannels;
|
||||||
|
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
mod au915;
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
mod us915;
|
||||||
|
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
pub use au915::AU915;
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
pub use us915::US915;
|
||||||
|
|
||||||
|
seq_macro::seq!(
|
||||||
|
N in 1..=8 {
|
||||||
|
/// Subband definitions used to bias the join process of a fixed channel plan (ie: US915, AU915).
|
||||||
|
/// Each Subband holds 8 channels. eg: subband 1 contains: channels 0-7, subband 2: channels 8-15, etc.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[repr(usize)]
|
||||||
|
pub enum Subband {
|
||||||
|
#(
|
||||||
|
_~N = N,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
impl From<Subband> for usize {
|
||||||
|
fn from(value: Subband) -> Self {
|
||||||
|
value as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub(crate) struct FixedChannelPlan<const NUM_DR: usize, F: FixedChannelRegion<NUM_DR>> {
|
||||||
|
last_tx_channel: u8,
|
||||||
|
channel_mask: ChannelMask<9>,
|
||||||
|
_fixed_channel_region: PhantomData<F>,
|
||||||
|
join_channels: JoinChannels,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const D: usize, F: FixedChannelRegion<D>> FixedChannelPlan<D, F> {
|
||||||
|
pub fn set_125k_channels(&mut self, enabled: bool) {
|
||||||
|
let mask = if enabled {
|
||||||
|
0xFF
|
||||||
|
} else {
|
||||||
|
0x00
|
||||||
|
};
|
||||||
|
self.channel_mask.set_bank(0, mask);
|
||||||
|
self.channel_mask.set_bank(1, mask);
|
||||||
|
self.channel_mask.set_bank(2, mask);
|
||||||
|
self.channel_mask.set_bank(3, mask);
|
||||||
|
self.channel_mask.set_bank(4, mask);
|
||||||
|
self.channel_mask.set_bank(5, mask);
|
||||||
|
self.channel_mask.set_bank(6, mask);
|
||||||
|
self.channel_mask.set_bank(7, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn get_max_payload_length(datarate: DR, repeater_compatible: bool, dwell_time: bool) -> u8 {
|
||||||
|
F::get_max_payload_length(datarate, repeater_compatible, dwell_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait FixedChannelRegion<const D: usize>: ChannelRegion<D> {
|
||||||
|
fn uplink_channels() -> &'static [u32; 72];
|
||||||
|
fn downlink_channels() -> &'static [u32; 8];
|
||||||
|
fn get_default_rx2() -> u32;
|
||||||
|
fn get_rx_datarate(tx_datarate: DR, frame: &Frame, window: &Window) -> Datarate;
|
||||||
|
fn get_dbm() -> i8;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const D: usize, F: FixedChannelRegion<D>> RegionHandler for FixedChannelPlan<D, F> {
|
||||||
|
fn process_join_accept<T: AsRef<[u8]>, C>(
|
||||||
|
&mut self,
|
||||||
|
join_accept: &DecryptedJoinAcceptPayload<T, C>,
|
||||||
|
) {
|
||||||
|
if let Some(CfList::FixedChannel(channel_mask)) = join_accept.c_f_list() {
|
||||||
|
// Reset the join channels state
|
||||||
|
self.join_channels.reset();
|
||||||
|
self.channel_mask = channel_mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_link_adr_channel_mask(
|
||||||
|
&mut self,
|
||||||
|
channel_mask_control: u8,
|
||||||
|
channel_mask: ChannelMask<2>,
|
||||||
|
) {
|
||||||
|
self.join_channels.reset();
|
||||||
|
match channel_mask_control {
|
||||||
|
0..=4 => {
|
||||||
|
let base_index = channel_mask_control as usize * 2;
|
||||||
|
self.channel_mask.set_bank(base_index, channel_mask.get_index(0));
|
||||||
|
self.channel_mask.set_bank(base_index + 1, channel_mask.get_index(1));
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
let channel_mask: u16 =
|
||||||
|
channel_mask.get_index(0) as u16 | ((channel_mask.get_index(1) as u16) << 8);
|
||||||
|
self.channel_mask.set_bank(0, ((channel_mask & 0b1) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(1, ((channel_mask & 0b10) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(2, ((channel_mask & 0b100) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(3, ((channel_mask & 0b1000) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(4, ((channel_mask & 0b10000) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(5, ((channel_mask & 0b100000) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(6, ((channel_mask & 0b1000000) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(7, ((channel_mask & 0b10000000) * 0xFF) as u8);
|
||||||
|
self.channel_mask.set_bank(8, ((channel_mask & 0b100000000) * 0xFF) as u8);
|
||||||
|
}
|
||||||
|
6 => {
|
||||||
|
self.set_125k_channels(true);
|
||||||
|
}
|
||||||
|
7 => {
|
||||||
|
self.set_125k_channels(false);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
//RFU
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tx_dr_and_frequency<RNG: RngCore>(
|
||||||
|
&mut self,
|
||||||
|
rng: &mut RNG,
|
||||||
|
datarate: DR,
|
||||||
|
frame: &Frame,
|
||||||
|
) -> (Datarate, u32) {
|
||||||
|
match frame {
|
||||||
|
Frame::Join => {
|
||||||
|
let channel = self.join_channels.get_next_channel(rng);
|
||||||
|
let dr = if channel < 64 {
|
||||||
|
DR::_0
|
||||||
|
} else {
|
||||||
|
DR::_4
|
||||||
|
};
|
||||||
|
self.last_tx_channel = channel;
|
||||||
|
let data_rate = F::datarates()[dr as usize].clone().unwrap();
|
||||||
|
(data_rate, F::uplink_channels()[channel as usize])
|
||||||
|
}
|
||||||
|
Frame::Data => {
|
||||||
|
// The join bias gets reset after receiving CFList in Join Frame
|
||||||
|
// or ChannelMask in the LinkADRReq in Data Frame.
|
||||||
|
// If it has not been reset yet, we continue to use the bias for the data frames.
|
||||||
|
// We hope to acquire ChannelMask via LinkADRReq.
|
||||||
|
let (data_rate, channel) = if self.join_channels.has_bias_and_not_exhausted() {
|
||||||
|
let channel = self.join_channels.get_next_channel(rng);
|
||||||
|
let dr = if channel < 64 {
|
||||||
|
DR::_0
|
||||||
|
} else {
|
||||||
|
DR::_4
|
||||||
|
};
|
||||||
|
(F::datarates()[dr as usize].clone().unwrap(), channel)
|
||||||
|
// Alternatively, we will ask JoinChannel logic to determine a channel from the
|
||||||
|
// subband that the join succeeded on.
|
||||||
|
} else if let Some(channel) = self.join_channels.first_data_channel(rng) {
|
||||||
|
(F::datarates()[datarate as usize].clone().unwrap(), channel)
|
||||||
|
} else {
|
||||||
|
// For the data frame, the datarate impacts which channel sets we can choose
|
||||||
|
// from. If the datarate bandwidth is 500 kHz, we must use
|
||||||
|
// channels 64-71. Else, we must use 0-63
|
||||||
|
let datarate = F::datarates()[datarate as usize].clone().unwrap();
|
||||||
|
if datarate.bandwidth == Bandwidth::_500KHz {
|
||||||
|
let mut channel = (rng.next_u32() & 0b111) as u8;
|
||||||
|
// keep selecting a random channel until we find one that is enabled
|
||||||
|
while !self.channel_mask.is_enabled(channel.into()).unwrap() {
|
||||||
|
channel = (rng.next_u32() & 0b111) as u8;
|
||||||
|
}
|
||||||
|
(datarate, 64 + channel)
|
||||||
|
} else {
|
||||||
|
let mut channel = (rng.next_u32() & 0b111111) as u8;
|
||||||
|
// keep selecting a random channel until we find one that is enabled
|
||||||
|
while !self.channel_mask.is_enabled(channel.into()).unwrap() {
|
||||||
|
channel = (rng.next_u32() & 0b111111) as u8;
|
||||||
|
}
|
||||||
|
(datarate, channel)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.last_tx_channel = channel;
|
||||||
|
(data_rate, F::uplink_channels()[channel as usize])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_rx_frequency(&self, _frame: &Frame, window: &Window) -> u32 {
|
||||||
|
let channel = self.last_tx_channel % 8;
|
||||||
|
match window {
|
||||||
|
Window::_1 => F::downlink_channels()[channel as usize],
|
||||||
|
Window::_2 => F::get_default_rx2(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_dbm(&self) -> i8 {
|
||||||
|
F::get_dbm()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_rx_datarate(&self, tx_datarate: DR, frame: &Frame, window: &Window) -> Datarate {
|
||||||
|
F::get_rx_datarate(tx_datarate, frame, window)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
use super::{Bandwidth, Datarate, SpreadingFactor};
|
||||||
|
|
||||||
|
pub(crate) const DATARATES: [Option<Datarate>; 14] = [
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_10,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 19,
|
||||||
|
max_mac_payload_size_with_dwell_time: 19,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_9,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 61,
|
||||||
|
max_mac_payload_size_with_dwell_time: 61,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_8,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 133,
|
||||||
|
max_mac_payload_size_with_dwell_time: 133,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_125KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_8,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
None, // TODO: defined in rp002-1-0-4
|
||||||
|
None, // TODO: defined in rp002-1-0-4
|
||||||
|
None,
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_12,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 61,
|
||||||
|
max_mac_payload_size_with_dwell_time: 61,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_11,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 137,
|
||||||
|
max_mac_payload_size_with_dwell_time: 137,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_10,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_9,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_8,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
Some(Datarate {
|
||||||
|
spreading_factor: SpreadingFactor::_7,
|
||||||
|
bandwidth: Bandwidth::_500KHz,
|
||||||
|
max_mac_payload_size: 250,
|
||||||
|
max_mac_payload_size_with_dwell_time: 250,
|
||||||
|
}),
|
||||||
|
];
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
pub(crate) const UPLINK_CHANNEL_MAP: [u32; 72] = [
|
||||||
|
// channels 0-7 (125 kHz)
|
||||||
|
902_300_000,
|
||||||
|
902_500_000,
|
||||||
|
902_700_000,
|
||||||
|
902_900_000,
|
||||||
|
903_100_000,
|
||||||
|
903_300_000,
|
||||||
|
903_500_000,
|
||||||
|
903_700_000,
|
||||||
|
// channels 8-15 (125 kHz)
|
||||||
|
903_900_000,
|
||||||
|
904_100_000,
|
||||||
|
904_300_000,
|
||||||
|
904_500_000,
|
||||||
|
904_700_000,
|
||||||
|
904_900_000,
|
||||||
|
905_100_000,
|
||||||
|
905_300_000,
|
||||||
|
// channels 16-23 (125 kHz)
|
||||||
|
905_500_000,
|
||||||
|
905_700_000,
|
||||||
|
905_900_000,
|
||||||
|
906_100_000,
|
||||||
|
906_300_000,
|
||||||
|
906_500_000,
|
||||||
|
906_700_000,
|
||||||
|
906_900_000,
|
||||||
|
// channels 24-31 (125 kHz)
|
||||||
|
907_100_000,
|
||||||
|
907_300_000,
|
||||||
|
907_500_000,
|
||||||
|
907_700_000,
|
||||||
|
907_900_000,
|
||||||
|
908_100_000,
|
||||||
|
908_300_000,
|
||||||
|
908_500_000,
|
||||||
|
// channels 32-39 (125 kHz)
|
||||||
|
908_700_000,
|
||||||
|
908_900_000,
|
||||||
|
909_100_000,
|
||||||
|
909_300_000,
|
||||||
|
909_500_000,
|
||||||
|
909_700_000,
|
||||||
|
909_900_000,
|
||||||
|
910_100_000,
|
||||||
|
// channels 39-47 (125 kHz)
|
||||||
|
910_300_000,
|
||||||
|
910_500_000,
|
||||||
|
910_700_000,
|
||||||
|
910_900_000,
|
||||||
|
911_100_000,
|
||||||
|
911_300_000,
|
||||||
|
911_500_000,
|
||||||
|
911_700_000,
|
||||||
|
// channels 47-55 (125 kHz)
|
||||||
|
911_900_000,
|
||||||
|
912_100_000,
|
||||||
|
912_300_000,
|
||||||
|
912_500_000,
|
||||||
|
912_700_000,
|
||||||
|
912_900_000,
|
||||||
|
913_100_000,
|
||||||
|
913_300_000,
|
||||||
|
// channels 55-63
|
||||||
|
913_500_000,
|
||||||
|
913_700_000,
|
||||||
|
913_900_000,
|
||||||
|
914_100_000,
|
||||||
|
914_300_000,
|
||||||
|
914_500_000,
|
||||||
|
914_700_000,
|
||||||
|
914_900_000,
|
||||||
|
// channels 63-71 (500 kHz)
|
||||||
|
903_000_000,
|
||||||
|
904_600_000,
|
||||||
|
906_200_000,
|
||||||
|
907_800_000,
|
||||||
|
909_400_000,
|
||||||
|
911_000_000,
|
||||||
|
912_600_000,
|
||||||
|
914_200_000,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) const DOWNLINK_CHANNEL_MAP: [u32; 8] = [
|
||||||
|
923_300_000,
|
||||||
|
923_900_000,
|
||||||
|
924_500_000,
|
||||||
|
925_100_000,
|
||||||
|
925_700_000,
|
||||||
|
926_300_000,
|
||||||
|
926_900_000,
|
||||||
|
927_500_000,
|
||||||
|
];
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
mod frequencies;
|
||||||
|
use frequencies::*;
|
||||||
|
|
||||||
|
mod datarates;
|
||||||
|
use datarates::*;
|
||||||
|
|
||||||
|
const US_DBM: i8 = 21;
|
||||||
|
const DEFAULT_RX2: u32 = 923_300_000;
|
||||||
|
|
||||||
|
/// State struct for the `US915` region. This struct may be created directly if you wish to fine-tune some parameters.
|
||||||
|
/// At this time specifying a bias for the subband used during the join process is supported using
|
||||||
|
/// [`set_join_bias`](Self::set_join_bias) and [`set_join_bias_and_noncompliant_retries`](Self::set_join_bias_and_noncompliant_retries)
|
||||||
|
/// is suppored. This struct can then be turned into a [`Configuration`] as it implements [`Into<Configuration>`].
|
||||||
|
///
|
||||||
|
/// # Note:
|
||||||
|
///
|
||||||
|
/// Only [`US915`] and [`AU915`] can be created using this method, because they are the only ones which have
|
||||||
|
/// parameters that may be fine-tuned at the region level. To create a [`Configuration`] for other regions, use
|
||||||
|
/// [`Configuration::new`] and specify the region using the [`Region`] enum.
|
||||||
|
///
|
||||||
|
/// # Example: Setting up join bias
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use lorawan_device::region::{Configuration, US915, Subband};
|
||||||
|
///
|
||||||
|
/// let mut us915 = US915::new();
|
||||||
|
/// // Subband 2 is commonly used for The Things Network.
|
||||||
|
/// us915.set_join_bias(Subband::_2);
|
||||||
|
/// let configuration: Configuration = us915.into();
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct US915(pub(crate) FixedChannelPlan<14, US915Region>);
|
||||||
|
|
||||||
|
impl US915 {
|
||||||
|
pub fn get_max_payload_length(datarate: DR, repeater_compatible: bool, dwell_time: bool) -> u8 {
|
||||||
|
US915Region::get_max_payload_length(datarate, repeater_compatible, dwell_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub(crate) struct US915Region;
|
||||||
|
|
||||||
|
impl ChannelRegion<14> for US915Region {
|
||||||
|
fn datarates() -> &'static [Option<Datarate>; 14] {
|
||||||
|
&DATARATES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixedChannelRegion<14> for US915Region {
|
||||||
|
fn uplink_channels() -> &'static [u32; 72] {
|
||||||
|
&UPLINK_CHANNEL_MAP
|
||||||
|
}
|
||||||
|
fn downlink_channels() -> &'static [u32; 8] {
|
||||||
|
&DOWNLINK_CHANNEL_MAP
|
||||||
|
}
|
||||||
|
fn get_default_rx2() -> u32 {
|
||||||
|
DEFAULT_RX2
|
||||||
|
}
|
||||||
|
fn get_rx_datarate(tx_datarate: DR, _frame: &Frame, window: &Window) -> Datarate {
|
||||||
|
let datarate = match window {
|
||||||
|
Window::_1 => {
|
||||||
|
// no support for RX1 DR Offset
|
||||||
|
match tx_datarate {
|
||||||
|
DR::_0 => DR::_10,
|
||||||
|
DR::_1 => DR::_11,
|
||||||
|
DR::_2 => DR::_12,
|
||||||
|
DR::_3 => DR::_13,
|
||||||
|
DR::_4 => DR::_13,
|
||||||
|
_ => panic!("Invalid TX datarate"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Window::_2 => DR::_8,
|
||||||
|
};
|
||||||
|
DATARATES[datarate as usize].clone().unwrap()
|
||||||
|
}
|
||||||
|
fn get_dbm() -> i8 {
|
||||||
|
US_DBM
|
||||||
|
}
|
||||||
|
}
|
||||||
528
lorawan-device-patch/src/region/mod.rs
Normal file
528
lorawan-device-patch/src/region/mod.rs
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
//! LoRaWAN device region definitions (eg: EU868, US915, etc).
|
||||||
|
use lora_modulation::{Bandwidth, BaseBandModulationParams, CodingRate, SpreadingFactor};
|
||||||
|
use lorawan::{maccommands::ChannelMask, parser::CfList};
|
||||||
|
use rand_core::RngCore;
|
||||||
|
|
||||||
|
use crate::mac::{Frame, Window};
|
||||||
|
pub(crate) mod constants;
|
||||||
|
pub(crate) use crate::radio::*;
|
||||||
|
use constants::*;
|
||||||
|
|
||||||
|
#[cfg(not(any(
|
||||||
|
feature = "region-as923-1",
|
||||||
|
feature = "region-as923-2",
|
||||||
|
feature = "region-as923-3",
|
||||||
|
feature = "region-as923-4",
|
||||||
|
feature = "region-eu433",
|
||||||
|
feature = "region-eu868",
|
||||||
|
feature = "region-in865",
|
||||||
|
feature = "region-au915",
|
||||||
|
feature = "region-us915"
|
||||||
|
)))]
|
||||||
|
compile_error!("You must enable at least one region! eg: `region-eu868`, `region-us915`...");
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "region-as923-1",
|
||||||
|
feature = "region-as923-2",
|
||||||
|
feature = "region-as923-3",
|
||||||
|
feature = "region-as923-4",
|
||||||
|
feature = "region-eu433",
|
||||||
|
feature = "region-eu868",
|
||||||
|
feature = "region-in865"
|
||||||
|
))]
|
||||||
|
mod dynamic_channel_plans;
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
pub(crate) use dynamic_channel_plans::AS923_1;
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
pub(crate) use dynamic_channel_plans::AS923_2;
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
pub(crate) use dynamic_channel_plans::AS923_3;
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
pub(crate) use dynamic_channel_plans::AS923_4;
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
pub(crate) use dynamic_channel_plans::EU433;
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
pub(crate) use dynamic_channel_plans::EU868;
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
pub(crate) use dynamic_channel_plans::IN865;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "region-us915", feature = "region-au915"))]
|
||||||
|
mod fixed_channel_plans;
|
||||||
|
#[cfg(any(feature = "region-us915", feature = "region-au915"))]
|
||||||
|
pub use fixed_channel_plans::Subband;
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
pub use fixed_channel_plans::AU915;
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
pub use fixed_channel_plans::US915;
|
||||||
|
|
||||||
|
pub(crate) trait ChannelRegion<const D: usize> {
|
||||||
|
fn datarates() -> &'static [Option<Datarate>; D];
|
||||||
|
|
||||||
|
fn get_max_payload_length(datarate: DR, repeater_compatible: bool, dwell_time: bool) -> u8 {
|
||||||
|
let Some(Some(dr)) = Self::datarates().get(datarate as usize) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let max_size = if dwell_time {
|
||||||
|
dr.max_mac_payload_size_with_dwell_time
|
||||||
|
} else {
|
||||||
|
dr.max_mac_payload_size
|
||||||
|
};
|
||||||
|
if repeater_compatible && max_size > 230 {
|
||||||
|
230
|
||||||
|
} else {
|
||||||
|
max_size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
/// Contains LoRaWAN region-specific configuration; is required for creating a LoRaWAN Device.
|
||||||
|
/// Generally constructed using the `Region` enum, unless you need to fine-tune US915 or AU915.
|
||||||
|
pub struct Configuration {
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
seq_macro::seq!(
|
||||||
|
N in 0..=15 {
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
#[repr(u8)]
|
||||||
|
/// A restricted data rate type that exposes the number of variants to only what _may_ be
|
||||||
|
/// potentially be possible. Note that not all data rates are valid in all regions.
|
||||||
|
pub enum DR {
|
||||||
|
#(
|
||||||
|
_~N = N,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
/// Regions supported by this crate: AS923_1, AS923_2, AS923_3, AS923_4, AU915, EU868, EU433, IN865, US915.
|
||||||
|
/// Each region is individually feature-gated (eg: `region-eu868`), however, by default, all regions are enabled.
|
||||||
|
///
|
||||||
|
pub enum Region {
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
AS923_1,
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
AS923_2,
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
AS923_3,
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
AS923_4,
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
AU915,
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
EU868,
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
EU433,
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
IN865,
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
US915,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum State {
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
AS923_1(AS923_1),
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
AS923_2(AS923_2),
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
AS923_3(AS923_3),
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
AS923_4(AS923_4),
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
AU915(AU915),
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
EU868(EU868),
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
EU433(EU433),
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
IN865(IN865),
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
US915(US915),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn new(region: Region) -> State {
|
||||||
|
match region {
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
Region::AS923_1 => State::AS923_1(AS923_1::default()),
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
Region::AS923_2 => State::AS923_2(AS923_2::default()),
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
Region::AS923_3 => State::AS923_3(AS923_3::default()),
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
Region::AS923_4 => State::AS923_4(AS923_4::default()),
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
Region::AU915 => State::AU915(AU915::default()),
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
Region::EU868 => State::EU868(EU868::default()),
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
Region::EU433 => State::EU433(EU433::default()),
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
Region::IN865 => State::IN865(IN865::default()),
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
Region::US915 => State::US915(US915::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn region(&self) -> Region {
|
||||||
|
match self {
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
Self::AS923_1(_) => Region::AS923_1,
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
Self::AS923_2(_) => Region::AS923_2,
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
Self::AS923_3(_) => Region::AS923_3,
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
Self::AS923_4(_) => Region::AS923_4,
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
Self::AU915(_) => Region::AU915,
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
Self::EU433(_) => Region::EU433,
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
Self::EU868(_) => Region::EU868,
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
Self::IN865(_) => Region::IN865,
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
Self::US915(_) => Region::US915,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This datarate type is used internally for defining bandwidth/sf per region
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct Datarate {
|
||||||
|
bandwidth: Bandwidth,
|
||||||
|
spreading_factor: SpreadingFactor,
|
||||||
|
max_mac_payload_size: u8,
|
||||||
|
max_mac_payload_size_with_dwell_time: u8,
|
||||||
|
}
|
||||||
|
macro_rules! mut_region_dispatch {
|
||||||
|
($s:expr, $t:tt) => {
|
||||||
|
match &mut $s.state {
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
State::AS923_1(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
State::AS923_2(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
State::AS923_3(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
State::AS923_4(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
State::AU915(state) => state.0.$t(),
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
State::EU868(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
State::EU433(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
State::IN865(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
State::US915(state) => state.0.$t(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($s:expr, $t:tt, $($arg:tt)*) => {
|
||||||
|
match &mut $s.state {
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
State::AS923_1(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
State::AS923_2(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
State::AS923_3(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
State::AS923_4(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
State::AU915(state) => state.0.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
State::EU868(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
State::EU433(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
State::IN865(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
State::US915(state) => state.0.$t($($arg)*),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! region_dispatch {
|
||||||
|
($s:expr, $t:tt) => {
|
||||||
|
match &$s.state {
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
State::AS923_1(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
State::AS923_2(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
State::AS923_3(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
State::AS923_4(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
State::AU915(state) => state.0.$t(),
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
State::EU868(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
State::EU433(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
State::IN865(state) => state.$t(),
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
State::US915(state) => state.0.$t(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($s:expr, $t:tt, $($arg:tt)*) => {
|
||||||
|
match &$s.state {
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
State::AS923_1(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
State::AS923_2(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
State::AS923_3(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
State::AS923_4(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
State::AU915(state) => state.0.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
State::EU868(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
State::EU433(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
State::IN865(state) => state.$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
State::US915(state) => state.0.$t($($arg)*),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! region_static_dispatch {
|
||||||
|
($s:expr, $t:tt) => {
|
||||||
|
match &$s.state {
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
State::AS923_1(_) => dynamic_channel_plans::AS923_1::$t(),
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
State::AS923_2(_) => dynamic_channel_plans::AS923_2::$t(),
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
State::AS923_3(_) => dynamic_channel_plans::AS923_3::$t(),
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
State::AS923_4(_) => dynamic_channel_plans::AS923_4::$t(),
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
State::AU915(_) => fixed_channel_plans::AU915::$t(),
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
State::EU868(_) => dynamic_channel_plans::EU868::$t(),
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
State::EU433(_) => dynamic_channel_plans::EU433::$t(),
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
State::IN865(_) => dynamic_channel_plans::IN865::$t(),
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
State::US915(_) => fixed_channel_plans::US915::$t(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($s:expr, $t:tt, $($arg:tt)*) => {
|
||||||
|
match &$s.state {
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
State::AS923_1(_) => dynamic_channel_plans::AS923_1::$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
State::AS923_2(_) => dynamic_channel_plans::AS923_2::$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
State::AS923_3(_) => dynamic_channel_plans::AS923_3::$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
State::AS923_4(_) => dynamic_channel_plans::AS923_4::$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
State::AU915(_) => fixed_channel_plans::AU915::$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
State::EU868(_) => dynamic_channel_plans::EU868::$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
State::EU433(_) => dynamic_channel_plans::EU433::$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
State::IN865(_) => dynamic_channel_plans::IN865::$t($($arg)*),
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
State::US915(_) => fixed_channel_plans::US915::$t($($arg)*),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
pub fn new(region: Region) -> Configuration {
|
||||||
|
Configuration::with_state(State::new(region))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_state(state: State) -> Configuration {
|
||||||
|
Configuration { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_max_payload_length(
|
||||||
|
&self,
|
||||||
|
datarate: DR,
|
||||||
|
repeater_compatible: bool,
|
||||||
|
dwell_time: bool,
|
||||||
|
) -> u8 {
|
||||||
|
region_static_dispatch!(
|
||||||
|
self,
|
||||||
|
get_max_payload_length,
|
||||||
|
datarate,
|
||||||
|
repeater_compatible,
|
||||||
|
dwell_time
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_tx_config<RNG: RngCore>(
|
||||||
|
&mut self,
|
||||||
|
rng: &mut RNG,
|
||||||
|
datarate: DR,
|
||||||
|
frame: &Frame,
|
||||||
|
) -> TxConfig {
|
||||||
|
let (dr, frequency) = self.get_tx_dr_and_frequency(rng, datarate, frame);
|
||||||
|
TxConfig {
|
||||||
|
pw: self.get_dbm(),
|
||||||
|
rf: RfConfig {
|
||||||
|
frequency,
|
||||||
|
bb: BaseBandModulationParams::new(
|
||||||
|
dr.spreading_factor,
|
||||||
|
dr.bandwidth,
|
||||||
|
self.get_coding_rate(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tx_dr_and_frequency<RNG: RngCore>(
|
||||||
|
&mut self,
|
||||||
|
rng: &mut RNG,
|
||||||
|
datarate: DR,
|
||||||
|
frame: &Frame,
|
||||||
|
) -> (Datarate, u32) {
|
||||||
|
mut_region_dispatch!(self, get_tx_dr_and_frequency, rng, datarate, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_rx_config(&self, datarate: DR, frame: &Frame, window: &Window) -> RfConfig {
|
||||||
|
let dr = self.get_rx_datarate(datarate, frame, window);
|
||||||
|
RfConfig {
|
||||||
|
frequency: self.get_rx_frequency(frame, window),
|
||||||
|
bb: BaseBandModulationParams::new(
|
||||||
|
dr.spreading_factor,
|
||||||
|
dr.bandwidth,
|
||||||
|
self.get_coding_rate(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn process_join_accept<T: AsRef<[u8]>, C>(
|
||||||
|
&mut self,
|
||||||
|
join_accept: &DecryptedJoinAcceptPayload<T, C>,
|
||||||
|
) {
|
||||||
|
mut_region_dispatch!(self, process_join_accept, join_accept)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_channel_mask(
|
||||||
|
&mut self,
|
||||||
|
channel_mask_control: u8,
|
||||||
|
channel_mask: ChannelMask<2>,
|
||||||
|
) {
|
||||||
|
mut_region_dispatch!(self, handle_link_adr_channel_mask, channel_mask_control, channel_mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_rx_frequency(&self, frame: &Frame, window: &Window) -> u32 {
|
||||||
|
region_dispatch!(self, get_rx_frequency, frame, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_default_datarate(&self) -> DR {
|
||||||
|
region_dispatch!(self, get_default_datarate)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_rx_datarate(&self, datarate: DR, frame: &Frame, window: &Window) -> Datarate {
|
||||||
|
region_dispatch!(self, get_rx_datarate, datarate, frame, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unicast: The RXC parameters are identical to the RX2 parameters, and they use the same
|
||||||
|
// channel and data rate. Modifying the RX2 parameters using the appropriate MAC
|
||||||
|
// commands also modifies the RXC parameters.
|
||||||
|
pub(crate) fn get_rxc_config(&self, datarate: DR) -> RfConfig {
|
||||||
|
let dr = self.get_rx_datarate(datarate, &Frame::Data, &Window::_2);
|
||||||
|
let frequency = self.get_rx_frequency(&Frame::Data, &Window::_2);
|
||||||
|
RfConfig {
|
||||||
|
frequency,
|
||||||
|
bb: BaseBandModulationParams::new(
|
||||||
|
dr.spreading_factor,
|
||||||
|
dr.bandwidth,
|
||||||
|
self.get_coding_rate(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_dbm(&self) -> i8 {
|
||||||
|
region_dispatch!(self, get_dbm)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_coding_rate(&self) -> CodingRate {
|
||||||
|
region_dispatch!(self, get_coding_rate)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn get_current_region(&self) -> super::region::Region {
|
||||||
|
self.state.region()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! from_region {
|
||||||
|
($r:tt) => {
|
||||||
|
impl From<$r> for Configuration {
|
||||||
|
fn from(region: $r) -> Configuration {
|
||||||
|
Configuration::with_state(State::$r(region))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "region-as923-1")]
|
||||||
|
from_region!(AS923_1);
|
||||||
|
#[cfg(feature = "region-as923-2")]
|
||||||
|
from_region!(AS923_2);
|
||||||
|
#[cfg(feature = "region-as923-3")]
|
||||||
|
from_region!(AS923_3);
|
||||||
|
#[cfg(feature = "region-as923-4")]
|
||||||
|
from_region!(AS923_4);
|
||||||
|
#[cfg(feature = "region-in865")]
|
||||||
|
from_region!(IN865);
|
||||||
|
#[cfg(feature = "region-au915")]
|
||||||
|
from_region!(AU915);
|
||||||
|
#[cfg(feature = "region-eu868")]
|
||||||
|
from_region!(EU868);
|
||||||
|
#[cfg(feature = "region-eu433")]
|
||||||
|
from_region!(EU433);
|
||||||
|
#[cfg(feature = "region-us915")]
|
||||||
|
from_region!(US915);
|
||||||
|
|
||||||
|
use lorawan::parser::DecryptedJoinAcceptPayload;
|
||||||
|
|
||||||
|
pub(crate) trait RegionHandler {
|
||||||
|
fn process_join_accept<T: AsRef<[u8]>, C>(
|
||||||
|
&mut self,
|
||||||
|
join_accept: &DecryptedJoinAcceptPayload<T, C>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn handle_link_adr_channel_mask(
|
||||||
|
&mut self,
|
||||||
|
channel_mask_control: u8,
|
||||||
|
channel_mask: ChannelMask<2>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn get_default_datarate(&self) -> DR {
|
||||||
|
DR::_0
|
||||||
|
}
|
||||||
|
fn get_tx_dr_and_frequency<RNG: RngCore>(
|
||||||
|
&mut self,
|
||||||
|
rng: &mut RNG,
|
||||||
|
datarate: DR,
|
||||||
|
frame: &Frame,
|
||||||
|
) -> (Datarate, u32);
|
||||||
|
|
||||||
|
fn get_rx_frequency(&self, frame: &Frame, window: &Window) -> u32;
|
||||||
|
fn get_rx_datarate(&self, datarate: DR, frame: &Frame, window: &Window) -> Datarate;
|
||||||
|
fn get_dbm(&self) -> i8 {
|
||||||
|
DEFAULT_DBM
|
||||||
|
}
|
||||||
|
fn get_coding_rate(&self) -> CodingRate {
|
||||||
|
DEFAULT_CODING_RATE
|
||||||
|
}
|
||||||
|
}
|
||||||
54
lorawan-device-patch/src/rng.rs
Normal file
54
lorawan-device-patch/src/rng.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
//! RNG based on the `wyrand` pseudorandom number generator.
|
||||||
|
//!
|
||||||
|
//! This crate uses the random number generator for exactly two things:
|
||||||
|
//!
|
||||||
|
//! * Generating DevNonces for join requests
|
||||||
|
//! * Selecting random channels when transmitting uplinks.
|
||||||
|
//!
|
||||||
|
//! The good news is that both these operations don't require true
|
||||||
|
//! cryptographic randomness. In fact, in both cases, we don't even care about
|
||||||
|
//! predictability! A pseudorandom number generator initialized with a seed
|
||||||
|
//! generated by a true random number generator is plenty enough:
|
||||||
|
//!
|
||||||
|
//! * DevNonces must only be unique with a low chance of collision.
|
||||||
|
//! The 1.0.4 LoRaWAN spec even explicitly requires the DevNonces to be
|
||||||
|
//! a sequence of incrementing integers, which is obviously predictable.
|
||||||
|
//! * No one cares if the channel selected for the next uplink is predictable,
|
||||||
|
//! as long as the channel selection yields an uniform distribution.
|
||||||
|
//!
|
||||||
|
//! By providing a PRNG `RngCore` implementation, we enable the crate users the
|
||||||
|
//! flexibility of choosing whether they want to provide their own RNG, or just
|
||||||
|
//! a seed to instantiate this PRNG to generate the random numbers for them.
|
||||||
|
|
||||||
|
use fastrand::Rng;
|
||||||
|
use rand_core::RngCore;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
/// A pseudorandom number generator utilizing Wyrand algorithm via
|
||||||
|
/// the `fastrand` crate.
|
||||||
|
pub struct Prng(Rng);
|
||||||
|
|
||||||
|
impl Prng {
|
||||||
|
pub(crate) fn new(seed: u64) -> Self {
|
||||||
|
Self(Rng::with_seed(seed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RngCore for Prng {
|
||||||
|
fn next_u32(&mut self) -> u32 {
|
||||||
|
self.0.u32(..)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_u64(&mut self) -> u64 {
|
||||||
|
self.0.u64(..)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
||||||
|
rand_core::impls::fill_bytes_via_next(self, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
|
||||||
|
self.fill_bytes(dest);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
272
lorawan-device-patch/src/test_util.rs
Normal file
272
lorawan-device-patch/src/test_util.rs
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
use super::*;
|
||||||
|
use lorawan::maccommands::{
|
||||||
|
ChannelMask, DownlinkMacCommand, MacCommandIterator, SerializableMacCommand, UplinkMacCommand,
|
||||||
|
};
|
||||||
|
use lorawan::parser::{self, DataHeader};
|
||||||
|
use lorawan::{
|
||||||
|
default_crypto::DefaultFactory,
|
||||||
|
maccommandcreator::LinkADRReqCreator,
|
||||||
|
maccommands::LinkADRReqPayload,
|
||||||
|
parser::{parse, DataPayload, JoinAcceptPayload, PhyPayload},
|
||||||
|
};
|
||||||
|
use mac::Session;
|
||||||
|
|
||||||
|
use parser::FCtrl;
|
||||||
|
use radio::{RfConfig, TxConfig};
|
||||||
|
use std::{collections::HashMap, sync::Mutex, vec::Vec};
|
||||||
|
|
||||||
|
/// This module contains some functions for both async device and state machine driven devices
|
||||||
|
/// to operate unit tests.
|
||||||
|
///
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Uplink {
|
||||||
|
data: Vec<u8>,
|
||||||
|
#[allow(unused)]
|
||||||
|
tx_config: TxConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Uplink {
|
||||||
|
/// Creates a copy from a reference and ensures the packet is at least parseable.
|
||||||
|
pub fn new(data_in: &[u8], tx_config: TxConfig) -> Result<Self, parser::Error> {
|
||||||
|
let mut data: Vec<u8> = Vec::new();
|
||||||
|
data.extend_from_slice(data_in);
|
||||||
|
let _parse = parse(data.as_mut_slice())?;
|
||||||
|
Ok(Self { data, tx_config })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_payload(&mut self) -> PhyPayload<&mut [u8], DefaultFactory> {
|
||||||
|
match parse(self.data.as_mut_slice()) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => panic!("Failed to parse payload: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test functions shared by async_device and no_async_device tests
|
||||||
|
pub fn get_key() -> [u8; 16] {
|
||||||
|
[0; 16]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_dev_addr() -> DevAddr<[u8; 4]> {
|
||||||
|
DevAddr::from(0)
|
||||||
|
}
|
||||||
|
pub fn get_otaa_credentials() -> JoinMode {
|
||||||
|
JoinMode::OTAA {
|
||||||
|
deveui: DevEui::from([0; 8]),
|
||||||
|
appeui: AppEui::from([0; 8]),
|
||||||
|
appkey: AppKey::from(get_key()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_abp_credentials() -> JoinMode {
|
||||||
|
JoinMode::ABP {
|
||||||
|
devaddr: get_dev_addr(),
|
||||||
|
appskey: AppSKey::from(get_key()),
|
||||||
|
newskey: NewSKey::from(get_key()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RxTxHandler = fn(Option<Uplink>, RfConfig, &mut [u8]) -> usize;
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref SESSION: Mutex<HashMap<usize, Session>> = Mutex::new(HashMap::new());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle join request and pack a JoinAccept into RxBuffer
|
||||||
|
pub fn handle_join_request<const I: usize>(
|
||||||
|
uplink: Option<Uplink>,
|
||||||
|
_config: RfConfig,
|
||||||
|
rx_buffer: &mut [u8],
|
||||||
|
) -> usize {
|
||||||
|
if let Some(mut uplink) = uplink {
|
||||||
|
if let PhyPayload::JoinRequest(join_request) = uplink.get_payload() {
|
||||||
|
let devnonce = join_request.dev_nonce().to_owned();
|
||||||
|
assert!(join_request.validate_mic(&get_key().into()));
|
||||||
|
let mut buffer: [u8; 17] = [0; 17];
|
||||||
|
let mut phy =
|
||||||
|
lorawan::creator::JoinAcceptCreator::with_options(&mut buffer, DefaultFactory)
|
||||||
|
.unwrap();
|
||||||
|
let app_nonce_bytes = [1; 3];
|
||||||
|
phy.set_app_nonce(&app_nonce_bytes);
|
||||||
|
phy.set_net_id(&[1; 3]);
|
||||||
|
phy.set_dev_addr(get_dev_addr());
|
||||||
|
let finished = phy.build(&get_key().into()).unwrap();
|
||||||
|
rx_buffer[..finished.len()].copy_from_slice(finished);
|
||||||
|
|
||||||
|
let mut copy = rx_buffer[..finished.len()].to_vec();
|
||||||
|
if let PhyPayload::JoinAccept(JoinAcceptPayload::Encrypted(encrypted)) =
|
||||||
|
parse(copy.as_mut_slice()).unwrap()
|
||||||
|
{
|
||||||
|
let decrypt = encrypted.decrypt(&get_key().into());
|
||||||
|
let session = Session::derive_new(
|
||||||
|
&decrypt,
|
||||||
|
devnonce,
|
||||||
|
&NetworkCredentials::new(
|
||||||
|
AppEui::from([0; 8]),
|
||||||
|
DevEui::from([0; 8]),
|
||||||
|
AppKey::from(get_key()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let mut session_map = SESSION.lock().unwrap();
|
||||||
|
session_map.insert(I, session);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Somehow unable to parse my own join accept?")
|
||||||
|
}
|
||||||
|
finished.len()
|
||||||
|
} else {
|
||||||
|
panic!("Did not parse join request from uplink");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("No uplink passed to handle_join_request");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle an uplink and respond with two LinkAdrReq on Port 0
|
||||||
|
pub fn handle_data_uplink_with_link_adr_req<const FCNT_UP: u16, const FCNT_DOWN: u32>(
|
||||||
|
uplink: Option<Uplink>,
|
||||||
|
_config: RfConfig,
|
||||||
|
rx_buffer: &mut [u8],
|
||||||
|
) -> usize {
|
||||||
|
if let Some(mut uplink) = uplink {
|
||||||
|
if let PhyPayload::Data(DataPayload::Encrypted(data)) = uplink.get_payload() {
|
||||||
|
let fcnt = data.fhdr().fcnt() as u32;
|
||||||
|
assert!(data.validate_mic(&get_key().into(), fcnt));
|
||||||
|
let uplink =
|
||||||
|
data.decrypt(Some(&get_key().into()), Some(&get_key().into()), fcnt).unwrap();
|
||||||
|
assert_eq!(uplink.fhdr().fcnt(), FCNT_UP);
|
||||||
|
let mac_cmds = [link_adr_req_with_bank_ctrl(0b10), link_adr_req_with_bank_ctrl(0b100)];
|
||||||
|
let mac_cmds = [
|
||||||
|
// drop the CID byte when building the MAC Command (ie: [1..])
|
||||||
|
DownlinkMacCommand::LinkADRReq(
|
||||||
|
LinkADRReqPayload::new(&mac_cmds[0].build()[1..]).unwrap(),
|
||||||
|
),
|
||||||
|
DownlinkMacCommand::LinkADRReq(
|
||||||
|
LinkADRReqPayload::new(&mac_cmds[1].build()[1..]).unwrap(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let cmd: Vec<&dyn SerializableMacCommand> = vec![&mac_cmds[0], &mac_cmds[1]];
|
||||||
|
let mut phy =
|
||||||
|
lorawan::creator::DataPayloadCreator::with_options(rx_buffer, DefaultFactory)
|
||||||
|
.unwrap();
|
||||||
|
phy.set_confirmed(uplink.is_confirmed());
|
||||||
|
phy.set_f_port(4);
|
||||||
|
phy.set_dev_addr(&[0; 4]);
|
||||||
|
phy.set_uplink(false);
|
||||||
|
phy.set_fcnt(FCNT_DOWN);
|
||||||
|
let finished =
|
||||||
|
phy.build(&[3, 2, 1], &cmd, &get_key().into(), &get_key().into()).unwrap();
|
||||||
|
finished.len()
|
||||||
|
} else {
|
||||||
|
panic!("Did not decode PhyPayload::Data!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("No uplink passed to handle_data_uplink_with_link_adr_req");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle an uplink and respond with two LinkAdrReq on Port 0
|
||||||
|
pub fn handle_class_c_uplink_after_join(
|
||||||
|
uplink: Option<Uplink>,
|
||||||
|
_config: RfConfig,
|
||||||
|
rx_buffer: &mut [u8],
|
||||||
|
) -> usize {
|
||||||
|
if let Some(mut uplink) = uplink {
|
||||||
|
if let PhyPayload::Data(DataPayload::Encrypted(data)) = uplink.get_payload() {
|
||||||
|
let fcnt = data.fhdr().fcnt() as u32;
|
||||||
|
assert!(data.validate_mic(&get_key().into(), fcnt));
|
||||||
|
let uplink =
|
||||||
|
data.decrypt(Some(&get_key().into()), Some(&get_key().into()), fcnt).unwrap();
|
||||||
|
assert_eq!(uplink.fhdr().fcnt(), 0);
|
||||||
|
let mut phy =
|
||||||
|
lorawan::creator::DataPayloadCreator::with_options(rx_buffer, DefaultFactory)
|
||||||
|
.unwrap();
|
||||||
|
let mut fctrl = FCtrl::new(0, false);
|
||||||
|
fctrl.set_ack();
|
||||||
|
phy.set_confirmed(false);
|
||||||
|
phy.set_dev_addr(&[0; 4]);
|
||||||
|
phy.set_uplink(false);
|
||||||
|
phy.set_fctrl(&fctrl);
|
||||||
|
// set ack bit
|
||||||
|
let finished = phy.build(&[], &[], &get_key().into(), &get_key().into()).unwrap();
|
||||||
|
finished.len()
|
||||||
|
} else {
|
||||||
|
panic!("Did not decode PhyPayload::Data!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("No uplink passed to handle_data_uplink_with_link_adr_req");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_adr_req_with_bank_ctrl(cm: u16) -> LinkADRReqCreator {
|
||||||
|
// prepare a confirmed downlink
|
||||||
|
let mut adr_req = LinkADRReqCreator::new();
|
||||||
|
adr_req.set_data_rate(0).unwrap();
|
||||||
|
adr_req.set_tx_power(0).unwrap();
|
||||||
|
// this should give us a chmask ctrl value of 5 which allows us to turn banks on and off
|
||||||
|
adr_req.set_redundancy(0x50);
|
||||||
|
// the second bit is the only high bit, so only bank 2 should be enabled
|
||||||
|
let tmp = [cm as u8, (cm >> 8) as u8];
|
||||||
|
let cm = ChannelMask::new(&tmp).unwrap();
|
||||||
|
adr_req.set_channel_mask(cm);
|
||||||
|
adr_req
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks for LinkAdrAns
|
||||||
|
pub fn handle_data_uplink_with_link_adr_ans(
|
||||||
|
uplink: Option<Uplink>,
|
||||||
|
_config: RfConfig,
|
||||||
|
rx_buffer: &mut [u8],
|
||||||
|
) -> usize {
|
||||||
|
if let Some(mut uplink) = uplink {
|
||||||
|
if let PhyPayload::Data(DataPayload::Encrypted(data)) = uplink.get_payload() {
|
||||||
|
let fcnt = data.fhdr().fcnt() as u32;
|
||||||
|
assert!(data.validate_mic(&get_key().into(), fcnt));
|
||||||
|
let uplink =
|
||||||
|
data.decrypt(Some(&get_key().into()), Some(&get_key().into()), fcnt).unwrap();
|
||||||
|
let fhdr = uplink.fhdr();
|
||||||
|
let mac_cmds: Vec<UplinkMacCommand> =
|
||||||
|
MacCommandIterator::<UplinkMacCommand>::new(fhdr.data()).collect();
|
||||||
|
|
||||||
|
assert_eq!(mac_cmds.len(), 2);
|
||||||
|
assert!(matches!(mac_cmds[0], UplinkMacCommand::LinkADRAns(_)));
|
||||||
|
assert!(matches!(mac_cmds[1], UplinkMacCommand::LinkADRAns(_)));
|
||||||
|
|
||||||
|
// Build the actual data payload with FPort 0 which allows MAC Commands in payload
|
||||||
|
rx_buffer.iter_mut().for_each(|x| *x = 0);
|
||||||
|
let mut phy =
|
||||||
|
lorawan::creator::DataPayloadCreator::with_options(rx_buffer, DefaultFactory)
|
||||||
|
.unwrap();
|
||||||
|
phy.set_confirmed(uplink.is_confirmed());
|
||||||
|
phy.set_dev_addr(&[0; 4]);
|
||||||
|
phy.set_uplink(false);
|
||||||
|
//phy.set_f_port(3);
|
||||||
|
phy.set_fcnt(1);
|
||||||
|
// zero out rx_buffer
|
||||||
|
let finished = phy.build(&[], &[], &get_key().into(), &get_key().into()).unwrap();
|
||||||
|
finished.len()
|
||||||
|
} else {
|
||||||
|
panic!("Unable to parse PhyPayload::Data from uplink in handle_data_uplink_with_link_adr_ans")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("No uplink passed to handle_data_uplink_with_link_adr_ans")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn class_c_downlink<const FCNT_DOWN: u32>(
|
||||||
|
_uplink: Option<Uplink>,
|
||||||
|
_config: RfConfig,
|
||||||
|
rx_buffer: &mut [u8],
|
||||||
|
) -> usize {
|
||||||
|
let mut phy =
|
||||||
|
lorawan::creator::DataPayloadCreator::with_options(rx_buffer, DefaultFactory).unwrap();
|
||||||
|
phy.set_f_port(3);
|
||||||
|
phy.set_dev_addr(&[0; 4]);
|
||||||
|
phy.set_uplink(false);
|
||||||
|
phy.set_fcnt(FCNT_DOWN);
|
||||||
|
let finished = phy.build(&[1, 2, 3], &[], &get_key().into(), &get_key().into()).unwrap();
|
||||||
|
finished.len()
|
||||||
|
}
|
||||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "esp"
|
||||||
117
src/board.rs
Normal file
117
src/board.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use core::cell::RefCell;
|
||||||
|
use crate::components::clock::Clock;
|
||||||
|
use crate::components::lora::Lora;
|
||||||
|
use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
|
||||||
|
use defmt::Debug2Format;
|
||||||
|
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
||||||
|
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice as BlockingI2cDevice;
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
|
||||||
|
use embassy_sync::mutex::Mutex;
|
||||||
|
use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use esp_hal::gpio::{InputConfig, Level};
|
||||||
|
use esp_hal::i2c::master::I2c;
|
||||||
|
use esp_hal::peripherals::Peripherals;
|
||||||
|
use esp_hal::spi::master::{Config, Spi};
|
||||||
|
use esp_hal::timer::timg::TimerGroup;
|
||||||
|
use esp_hal::{gpio, Async, Blocking};
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
use crate::components::rom_storage::RomStorage;
|
||||||
|
use crate::types::{AppData, SharedI2c};
|
||||||
|
|
||||||
|
pub(crate) static LED_STATE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
pub struct Board {
|
||||||
|
spawner: Spawner,
|
||||||
|
lora: Lora<'static>,
|
||||||
|
clock: Clock<'static>,
|
||||||
|
storage: RomStorage<'static>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Board {
|
||||||
|
pub async fn new(spawner: Spawner, peripherals: Peripherals) -> Result<Self, crate::error::Error> {
|
||||||
|
let timer_group = TimerGroup::new(peripherals.TIMG0);
|
||||||
|
esp_rtos::start(timer_group.timer0);
|
||||||
|
|
||||||
|
static SPI_BUS: StaticCell<Mutex<CriticalSectionRawMutex, Spi<'static, Async>>> = StaticCell::new();
|
||||||
|
let spi = Spi::new(peripherals.SPI2, Config::default())?
|
||||||
|
.into_async()
|
||||||
|
.with_sck(peripherals.GPIO9)
|
||||||
|
.with_mosi(peripherals.GPIO10)
|
||||||
|
.with_miso(peripherals.GPIO11);
|
||||||
|
let spi_bus = SPI_BUS.init(Mutex::new(spi));
|
||||||
|
|
||||||
|
static I2C_BUS: StaticCell<BlockingMutex<NoopRawMutex, RefCell<I2c<'static, Blocking>>>> = StaticCell::new();
|
||||||
|
let async_i2c = I2c::new(peripherals.I2C0, Default::default())?
|
||||||
|
.with_sda(peripherals.GPIO21)
|
||||||
|
.with_scl(peripherals.GPIO19);
|
||||||
|
let i2c_bus = I2C_BUS.init(BlockingMutex::new(RefCell::new(async_i2c)));
|
||||||
|
|
||||||
|
i2c_bus.lock(|bus| {
|
||||||
|
let mut bus = bus.borrow_mut();
|
||||||
|
for addr in 0x08..0x78u8 {
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
if bus.read(addr, &mut buf).is_ok() {
|
||||||
|
defmt::debug!("I2C device found at 0x{:02x}", addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let vext = gpio::Output::new(peripherals.GPIO36, Level::Low, Default::default());
|
||||||
|
let lora = {
|
||||||
|
let reset = gpio::Output::new(peripherals.GPIO12, Level::Low, Default::default());
|
||||||
|
let busy = gpio::Input::new(peripherals.GPIO13, InputConfig::default());
|
||||||
|
let dio1 = gpio::Input::new(peripherals.GPIO14, InputConfig::default());
|
||||||
|
|
||||||
|
let cs = gpio::Output::new(peripherals.GPIO8, Level::High, Default::default());
|
||||||
|
let spi_device = SpiDevice::new(spi_bus, cs);
|
||||||
|
Lora::new(spi_device, reset, busy, dio1, vext).await?
|
||||||
|
};
|
||||||
|
let clock = Clock::new(SharedI2c::new(i2c_bus), 0x68)?;
|
||||||
|
|
||||||
|
let led = gpio::Output::new(peripherals.GPIO35, Level::Low, Default::default());
|
||||||
|
spawner.spawn(led_task(led))?;
|
||||||
|
|
||||||
|
let storage = RomStorage::new(
|
||||||
|
SharedI2c::new(i2c_bus)
|
||||||
|
);
|
||||||
|
|
||||||
|
let instance = Self { spawner, lora, clock, storage };
|
||||||
|
Ok(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(&mut self) -> Result<(), crate::error::Error> {
|
||||||
|
self.lora.try_join().await?;
|
||||||
|
defmt::info!("Board started");
|
||||||
|
LED_STATE.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
|
let time = self.lora.get_time(self.clock.get_datetime()?).await?;
|
||||||
|
let datetime = self.clock.update_time(time)?;
|
||||||
|
let app_data = AppData {
|
||||||
|
last_boot_time: datetime
|
||||||
|
};
|
||||||
|
defmt::debug!("stored app data: {:?}", Debug2Format(&self.storage.get_data::<AppData>()?));
|
||||||
|
self.storage.write_data(&app_data).await?;
|
||||||
|
|
||||||
|
let atomic_lora = AtomicPtr::new(&mut self.lora);
|
||||||
|
self.spawner.spawn(crate::components::lora::start_heartbeat_task(atomic_lora))?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
defmt::trace!("tick");
|
||||||
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn led_task(mut led: gpio::Output<'static>) {
|
||||||
|
loop {
|
||||||
|
if LED_STATE.load(Ordering::Relaxed) {
|
||||||
|
led.set_high();
|
||||||
|
} else {
|
||||||
|
led.set_low();
|
||||||
|
}
|
||||||
|
Timer::after(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/components/clock.rs
Normal file
68
src/components/clock.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
use crate::types::SharedI2c;
|
||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
use defmt::Debug2Format;
|
||||||
|
use ds3231::{DS3231Error, InterruptControl, Oscillator, SquareWaveFrequency, TimeRepresentation, DS3231};
|
||||||
|
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
||||||
|
use embassy_embedded_hal::shared_bus::I2cDeviceError;
|
||||||
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
|
use esp_hal::i2c::master::Error as I2cError;
|
||||||
|
use esp_hal::i2c::master::I2c;
|
||||||
|
use esp_hal::Async;
|
||||||
|
use hifitime::Epoch;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub(crate) enum ClockError {
|
||||||
|
#[error("DS2321 error: {0:?}")]
|
||||||
|
DS2321Error(DS3231Error<I2cDeviceError<I2cError>>),
|
||||||
|
#[error("Invalid time")]
|
||||||
|
InvalidTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DS3231Error<I2cDeviceError<I2cError>>> for ClockError {
|
||||||
|
fn from(value: DS3231Error<I2cDeviceError<I2cError>>) -> Self {
|
||||||
|
Self::DS2321Error(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Clock<'d> {
|
||||||
|
driver: DS3231<SharedI2c<'d>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'d> Clock<'d> {
|
||||||
|
pub fn new(i2c_device: SharedI2c<'d>, address: u8) -> Result<Self, ClockError> {
|
||||||
|
let config = ds3231::Config {
|
||||||
|
time_representation: TimeRepresentation::TwentyFourHour,
|
||||||
|
square_wave_frequency: SquareWaveFrequency::Hz1,
|
||||||
|
interrupt_control: InterruptControl::SquareWave,
|
||||||
|
battery_backed_square_wave: false,
|
||||||
|
oscillator_enable: Oscillator::Enabled,
|
||||||
|
};
|
||||||
|
let mut clock = DS3231::new(i2c_device, address);
|
||||||
|
clock.configure(&config)?;
|
||||||
|
let datetime = clock.datetime()?;
|
||||||
|
defmt::debug!("Clock Time: {:?}", Debug2Format(&datetime));
|
||||||
|
Ok(
|
||||||
|
Self {
|
||||||
|
driver: clock
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_time(&mut self, time: Epoch) -> Result<DateTime<Utc>, ClockError> {
|
||||||
|
let Some(utc_datetime) = DateTime::from_timestamp_secs(time.to_unix_seconds() as i64) else {
|
||||||
|
defmt::error!("Failed to convert time to datetime");
|
||||||
|
return Err(ClockError::InvalidTime);
|
||||||
|
};
|
||||||
|
let time = utc_datetime.time();
|
||||||
|
let date = utc_datetime.date_naive();
|
||||||
|
self.driver.set_datetime(&NaiveDateTime::new(date, time))?;
|
||||||
|
let datetime = self.driver.datetime()?;
|
||||||
|
defmt::debug!("Clock Time: {:?}", Debug2Format(&datetime));
|
||||||
|
Ok(utc_datetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_datetime(&mut self) -> Result<NaiveDateTime, ClockError> {
|
||||||
|
let datetime = self.driver.datetime()?;
|
||||||
|
Ok(datetime)
|
||||||
|
}
|
||||||
|
}
|
||||||
213
src/components/lora.rs
Normal file
213
src/components/lora.rs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
use crate::types::SharedAsyncSpi;
|
||||||
|
use crate::timer::EmbassyTimer;
|
||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
use core::str::FromStr;
|
||||||
|
use core::sync::atomic::{AtomicPtr, Ordering};
|
||||||
|
use defmt::Formatter;
|
||||||
|
use embassy_time::{Delay, Duration, Instant, Timer};
|
||||||
|
use esp_hal::gpio;
|
||||||
|
use esp_hal::rng::Rng;
|
||||||
|
use hifitime::Epoch;
|
||||||
|
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
||||||
|
use lora_phy::lorawan_radio::Error as LoraRadioError;
|
||||||
|
use lora_phy::lorawan_radio::LorawanRadio;
|
||||||
|
use lora_phy::mod_params::RadioError;
|
||||||
|
use lora_phy::sx126x::{Sx1262, Sx126x, TcxoCtrlVoltage};
|
||||||
|
use lora_phy::{sx126x, LoRa};
|
||||||
|
use lorawan_device::async_device::Error as LoraDeviceError;
|
||||||
|
use lorawan_device::async_device::Device;
|
||||||
|
use lorawan_device::default_crypto::DefaultFactory;
|
||||||
|
use lorawan_device::{region, AppEui, AppKey, DevEui, JoinMode};
|
||||||
|
|
||||||
|
const MAX_TX_POWER: u8 = 14;
|
||||||
|
const LORAWAN_REGION: region::Region = region::Region::AS923_1;
|
||||||
|
|
||||||
|
pub const CLOCK_SYNC_FPORT: u8 = 202;
|
||||||
|
const APP_TIME_CID: u8 = 0x01;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum LoraError {
|
||||||
|
#[error("Lora radio error: {0:?}")]
|
||||||
|
LoraRadioError(RadioError),
|
||||||
|
#[error("Lora device error: {0:?}")]
|
||||||
|
LoraDeviceError(LoraDeviceError<LoraRadioError>),
|
||||||
|
#[error("Invalid downlink data")]
|
||||||
|
InvalidDownlinkData,
|
||||||
|
#[error("No downlink received")]
|
||||||
|
NoDownlinkReceived,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Lora<'d> {
|
||||||
|
driver: Device<
|
||||||
|
LorawanRadio<
|
||||||
|
Sx126x<
|
||||||
|
SharedAsyncSpi<'d>,
|
||||||
|
GenericSx126xInterfaceVariant<
|
||||||
|
gpio::Output<'d>,
|
||||||
|
gpio::Input<'d>
|
||||||
|
>,
|
||||||
|
Sx1262
|
||||||
|
>,
|
||||||
|
Delay,
|
||||||
|
MAX_TX_POWER
|
||||||
|
>,
|
||||||
|
DefaultFactory,
|
||||||
|
EmbassyTimer,
|
||||||
|
Rng
|
||||||
|
>,
|
||||||
|
power: gpio::Output<'d>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'d> Lora<'d> {
|
||||||
|
pub async fn new(
|
||||||
|
spi_bus: SharedAsyncSpi<'d>,
|
||||||
|
reset: gpio::Output<'d>,
|
||||||
|
busy: gpio::Input<'d>,
|
||||||
|
dio1: gpio::Input<'d>,
|
||||||
|
power: gpio::Output<'d>,
|
||||||
|
) -> Result<Self, LoraError> {
|
||||||
|
let config = sx126x::Config {
|
||||||
|
chip: Sx1262,
|
||||||
|
tcxo_ctrl: Some(TcxoCtrlVoltage::Ctrl1V8),
|
||||||
|
use_dcdc: true,
|
||||||
|
rx_boost: true,
|
||||||
|
};
|
||||||
|
let iv = GenericSx126xInterfaceVariant::new(
|
||||||
|
reset,
|
||||||
|
dio1,
|
||||||
|
busy,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
let lora = LoRa::new(
|
||||||
|
Sx126x::new(
|
||||||
|
spi_bus,
|
||||||
|
iv,
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
Delay,
|
||||||
|
).await?;
|
||||||
|
let radio: LorawanRadio<_, _, MAX_TX_POWER> = lora.into();
|
||||||
|
let region = region::Configuration::new(LORAWAN_REGION);
|
||||||
|
let device = Device::new(region, radio, EmbassyTimer::new(), Rng::new());
|
||||||
|
Ok(
|
||||||
|
Self {
|
||||||
|
driver: device,
|
||||||
|
power,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn try_join(&mut self) -> Result<(), crate::error::Error> {
|
||||||
|
defmt::debug!("Joining LoRaWAN network");
|
||||||
|
let otaa_config = JoinMode::OTAA {
|
||||||
|
deveui: DevEui::from_str(&env!("DEV_EUI"))?,
|
||||||
|
appkey: AppKey::from_str(&env!("APP_KEY"))?,
|
||||||
|
appeui: AppEui::from_str(&env!("APP_EUI"))?,
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let response = self.driver
|
||||||
|
.join(&otaa_config)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Ok(response) => match response {
|
||||||
|
lorawan_device::async_device::JoinResponse::JoinSuccess => {
|
||||||
|
defmt::info!("LoRaWAN network joined successfully!");
|
||||||
|
self.power.set_high();
|
||||||
|
break Ok(());
|
||||||
|
}
|
||||||
|
lorawan_device::async_device::JoinResponse::NoJoinAccept => {
|
||||||
|
defmt::error!("No join accept from LoRaWAN network");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
defmt::error!("{}", err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_time(&mut self, current_time: NaiveDateTime) -> Result<Epoch, LoraError> {
|
||||||
|
self.driver.set_datarate(region::DR::_0);
|
||||||
|
|
||||||
|
let param: u8 = (1 & 0x0F) | 1 << 4;
|
||||||
|
let datetime: DateTime<Utc> = DateTime::from_naive_utc_and_offset(current_time, Utc);
|
||||||
|
let epoch = Epoch::from_unix_seconds(datetime.timestamp() as f64);
|
||||||
|
let gpst_time = epoch.to_gpst_seconds();
|
||||||
|
let gpst_bytes: [u8; 4] = (gpst_time as u32).to_le_bytes();
|
||||||
|
let payload = [APP_TIME_CID, gpst_bytes[0], gpst_bytes[1], gpst_bytes[2], gpst_bytes[3], param];
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let _ = self.driver.send(&payload, CLOCK_SYNC_FPORT, false).await?;
|
||||||
|
let elapsed_secs = start.elapsed().as_secs() as f64;
|
||||||
|
defmt::debug!("Received downlink after {} seconds", elapsed_secs);
|
||||||
|
|
||||||
|
let time_offset = if let Some(downlink) = self.driver.take_downlink() {
|
||||||
|
defmt::debug!("Received Downlink: {:?}", downlink);
|
||||||
|
let Ok(data) = downlink.data.into_array::<6>() else {
|
||||||
|
return Err(LoraError::InvalidDownlinkData);
|
||||||
|
};
|
||||||
|
let num_bytes: [u8; 4] = data[1..5].try_into().unwrap();
|
||||||
|
let value = u32::from_le_bytes(num_bytes);
|
||||||
|
defmt::debug!("Time value: {}", value);
|
||||||
|
value as f64
|
||||||
|
} else {
|
||||||
|
return Err(LoraError::NoDownlinkReceived);
|
||||||
|
};
|
||||||
|
Ok(Epoch::from_gpst_seconds(gpst_time + elapsed_secs + time_offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_heartbeat(&mut self) -> Result<(), LoraError> {
|
||||||
|
self.driver.set_datarate(region::DR::_0);
|
||||||
|
self.driver.send(&[], 1, false).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
pub(crate) async fn start_heartbeat_task(lora: AtomicPtr<Lora<'static>>) {
|
||||||
|
let Some(lora) = (unsafe { lora.load(Ordering::Relaxed).as_mut() }) else {
|
||||||
|
defmt::error!("Lora is not initialized");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let timer = match lora.send_heartbeat().await {
|
||||||
|
Ok(_) => {
|
||||||
|
defmt::debug!("Heartbeat sent successfully");
|
||||||
|
#[cfg(feature = "debug")]
|
||||||
|
let timer = Timer::after_secs(5);
|
||||||
|
#[cfg(not(feature = "debug"))]
|
||||||
|
let timer = Timer::after_secs(60);
|
||||||
|
timer
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
defmt::error!("Error while sending heartbeat: {}", e);
|
||||||
|
Timer::after_secs(1)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timer.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl From<RadioError> for LoraError {
|
||||||
|
fn from(e: RadioError) -> Self {
|
||||||
|
Self::LoraRadioError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LoraDeviceError<LoraRadioError>> for LoraError {
|
||||||
|
fn from(value: LoraDeviceError<LoraRadioError>) -> Self {
|
||||||
|
Self::LoraDeviceError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl defmt::Format for LoraError {
|
||||||
|
fn format(&self, fmt: Formatter) {
|
||||||
|
defmt::write!(fmt, "{:?}", self);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/components/mod.rs
Normal file
3
src/components/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub(crate) mod lora;
|
||||||
|
pub(crate) mod clock;
|
||||||
|
pub(crate) mod rom_storage;
|
||||||
83
src/components/rom_storage.rs
Normal file
83
src/components/rom_storage.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use alloc::collections::BTreeMap;
|
||||||
|
use core::any::{TypeId};
|
||||||
|
use eeprom24x::{Eeprom24x, SlaveAddr};
|
||||||
|
use embassy_embedded_hal::shared_bus::I2cDeviceError;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
use crate::types::SharedI2c;
|
||||||
|
|
||||||
|
const ADDR_SIZE: usize = 256;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub(crate) enum StorageError {
|
||||||
|
#[error("Data oversized: {0}")]
|
||||||
|
StorageOverSize(usize),
|
||||||
|
#[error("EEPROM Error: {0:?}")]
|
||||||
|
EEPROMError(eeprom24x::Error<I2cDeviceError<esp_hal::i2c::master::Error>>),
|
||||||
|
#[error("Out of address space")]
|
||||||
|
OutOfAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<eeprom24x::Error<I2cDeviceError<esp_hal::i2c::master::Error>>> for StorageError {
|
||||||
|
fn from(value: eeprom24x::Error<I2cDeviceError<esp_hal::i2c::master::Error>>) -> Self {
|
||||||
|
Self::EEPROMError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct RomStorage<'d> {
|
||||||
|
driver: Eeprom24x<SharedI2c<'d>, eeprom24x::page_size::B32, eeprom24x::addr_size::TwoBytes, eeprom24x::unique_serial::No>,
|
||||||
|
addr_table: BTreeMap<TypeId, u32>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'d> RomStorage<'d> {
|
||||||
|
pub fn new(i2c: SharedI2c<'d>) -> Self {
|
||||||
|
RomStorage {
|
||||||
|
driver: Eeprom24x::new_24x32(i2c, SlaveAddr::Alternative(true, true, true)),
|
||||||
|
addr_table: BTreeMap::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_or_insert_addr<T: 'static>(&mut self) -> Result<u32, StorageError> {
|
||||||
|
let id = TypeId::of::<T>();
|
||||||
|
if let Some(addr) = self.addr_table.get(&id) {
|
||||||
|
Ok(addr.clone())
|
||||||
|
} else {
|
||||||
|
let size = self.addr_table.len();
|
||||||
|
let addr = size as u32 * ADDR_SIZE as u32;
|
||||||
|
if addr >= u16::MAX as u32 {
|
||||||
|
return Err(StorageError::OutOfAddress)
|
||||||
|
}
|
||||||
|
self.addr_table.insert(id, addr);
|
||||||
|
Ok(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_data<T: Serialize + 'static>(&mut self, data: &T) -> Result<(), crate::error::Error> {
|
||||||
|
defmt::debug!("Writing app data");
|
||||||
|
let addr = self.get_or_insert_addr::<T>()?;
|
||||||
|
let bytes = postcard::to_allocvec(data)?;
|
||||||
|
let bytes_len = bytes.len();
|
||||||
|
if bytes_len > ADDR_SIZE {
|
||||||
|
return Err(StorageError::StorageOverSize(bytes_len).into());
|
||||||
|
}
|
||||||
|
for (i, chunk) in bytes.chunks(32).enumerate() {
|
||||||
|
defmt::debug!("Writing chunk {}", i);
|
||||||
|
let page_addr = addr + (i as u32 * 32);
|
||||||
|
self.driver.write_page(page_addr, chunk)
|
||||||
|
.map_err(|e| StorageError::EEPROMError(e))?;
|
||||||
|
Timer::after(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
defmt::info!("Finished writing app data");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_data<T: DeserializeOwned + 'static>(&mut self) -> Result<T, crate::error::Error> {
|
||||||
|
let addr = self.get_or_insert_addr::<T>()?;
|
||||||
|
let mut bytes = [0u8; ADDR_SIZE];
|
||||||
|
self.driver.read_data(addr, &mut bytes)
|
||||||
|
.map_err(|e| StorageError::EEPROMError(e))?;
|
||||||
|
let data = postcard::from_bytes(&bytes)?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/error.rs
Normal file
69
src/error.rs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
use crate::components::clock::ClockError;
|
||||||
|
use crate::components::lora::LoraError;
|
||||||
|
use core::convert::Infallible;
|
||||||
|
use defmt::Formatter;
|
||||||
|
use embassy_executor::SpawnError;
|
||||||
|
use esp_hal::i2c::master::ConfigError as I2cConfigError;
|
||||||
|
use esp_hal::rng::TrngError;
|
||||||
|
use esp_hal::spi::master::ConfigError as SpiConfigError;
|
||||||
|
use crate::components::rom_storage::StorageError;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("SPI config error: {0}")]
|
||||||
|
SpiConfigError(#[from] SpiConfigError),
|
||||||
|
#[error("I2C config error: {0}")]
|
||||||
|
I2cConfigError(#[from] I2cConfigError),
|
||||||
|
#[error("Encounter Infallible: {0}")]
|
||||||
|
Infallible(#[from] Infallible),
|
||||||
|
#[error("Lora Error: {0}")]
|
||||||
|
Lora(#[from] LoraError),
|
||||||
|
#[error("Clock Error: {0}")]
|
||||||
|
ClockError(#[from] ClockError),
|
||||||
|
#[error("Spawn Error: {0}")]
|
||||||
|
TaskSpawnError(#[from] SpawnError),
|
||||||
|
#[error("Trng error: {0:?}")]
|
||||||
|
TrngError(TrngError),
|
||||||
|
#[error("Hex decode error: {0:?}")]
|
||||||
|
HexDecodeError(hex::FromHexError),
|
||||||
|
#[error("LEDC Timer Error {0:?}")]
|
||||||
|
LEDCTimerError(esp_hal::ledc::timer::Error),
|
||||||
|
#[error("LEDC Channel Error {0:?}")]
|
||||||
|
LEDCChannelError(esp_hal::ledc::channel::Error),
|
||||||
|
#[error("Error serializing postcard: {0}")]
|
||||||
|
PostcardError(#[from] postcard::Error),
|
||||||
|
#[error("ROM Storage Error: {0}")]
|
||||||
|
StorageError(#[from] StorageError),
|
||||||
|
#[error("Unknown error: {0}")]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TrngError> for Error {
|
||||||
|
fn from(e: TrngError) -> Self {
|
||||||
|
Self::TrngError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<hex::FromHexError> for Error {
|
||||||
|
fn from(value: hex::FromHexError) -> Self {
|
||||||
|
Self::HexDecodeError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<esp_hal::ledc::timer::Error> for Error {
|
||||||
|
fn from(value: esp_hal::ledc::timer::Error) -> Self {
|
||||||
|
Self::LEDCTimerError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<esp_hal::ledc::channel::Error> for Error {
|
||||||
|
fn from(value: esp_hal::ledc::channel::Error) -> Self {
|
||||||
|
Self::LEDCChannelError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl defmt::Format for Error {
|
||||||
|
fn format(&self, fmt: Formatter) {
|
||||||
|
defmt::write!(fmt, "{}", self);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/main.rs
Normal file
60
src/main.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
#![deny(
|
||||||
|
clippy::mem_forget,
|
||||||
|
reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
|
||||||
|
holding buffers for the duration of a data transfer."
|
||||||
|
)]
|
||||||
|
#![deny(clippy::large_stack_frames)]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
mod error;
|
||||||
|
mod board;
|
||||||
|
mod timer;
|
||||||
|
mod components;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use {esp_backtrace as _, esp_println as _};
|
||||||
|
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use esp_hal::clock::CpuClock;
|
||||||
|
|
||||||
|
// This creates a default app-descriptor required by the esp-idf bootloader.
|
||||||
|
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
|
||||||
|
esp_bootloader_esp_idf::esp_app_desc!();
|
||||||
|
|
||||||
|
const RETRY_COUNT: usize = 3;
|
||||||
|
|
||||||
|
#[allow(
|
||||||
|
clippy::large_stack_frames,
|
||||||
|
reason = "it's not unusual to allocate larger buffers etc. in main"
|
||||||
|
)]
|
||||||
|
#[esp_rtos::main]
|
||||||
|
async fn main(spawner: Spawner) -> () {
|
||||||
|
esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 73744);
|
||||||
|
|
||||||
|
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
|
||||||
|
let peripherals = esp_hal::init(config);
|
||||||
|
|
||||||
|
let mut board = match board::Board::new(spawner, peripherals).await {
|
||||||
|
Ok(board) => board,
|
||||||
|
Err(e) => {
|
||||||
|
defmt::error!("Error while initializing board: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
defmt::info!("Board initialized");
|
||||||
|
|
||||||
|
let mut retry_count = RETRY_COUNT;
|
||||||
|
loop {
|
||||||
|
if retry_count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Err(e) = board.start().await {
|
||||||
|
defmt::error!("Error: {}", e);
|
||||||
|
}
|
||||||
|
retry_count -= 1;
|
||||||
|
}
|
||||||
|
defmt::error!("Unable to start after {} retries", RETRY_COUNT);
|
||||||
|
}
|
||||||
32
src/timer.rs
Normal file
32
src/timer.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use embassy_time::{Duration, Instant};
|
||||||
|
use lorawan_device::async_device::radio::Timer;
|
||||||
|
|
||||||
|
pub struct EmbassyTimer {
|
||||||
|
start: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmbassyTimer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { start: Instant::now() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EmbassyTimer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timer for EmbassyTimer {
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.start = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn at(&mut self, millis: u64) {
|
||||||
|
embassy_time::Timer::at(self.start + Duration::from_millis(millis)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delay_ms(&mut self, millis: u64) {
|
||||||
|
embassy_time::Timer::after_millis(millis).await
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/types/mod.rs
Normal file
18
src/types/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
||||||
|
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
||||||
|
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
|
||||||
|
use esp_hal::i2c::master::I2c;
|
||||||
|
use esp_hal::spi::master::Spi;
|
||||||
|
use esp_hal::{gpio, Async, Blocking};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub(crate) type SharedAsyncSpi<'d> =
|
||||||
|
SpiDevice<'d, CriticalSectionRawMutex, Spi<'d, Async>, gpio::Output<'d>>;
|
||||||
|
pub(crate) type SharedI2c<'d> =
|
||||||
|
I2cDevice<'d, NoopRawMutex, I2c<'d, Blocking>>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub(crate) struct AppData {
|
||||||
|
pub last_boot_time: DateTime<Utc>,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user