feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
+1
@@ -0,0 +1 @@
|
||||
{"files":{".cargo_vcs_info.json":"e99d8e6040aa7fa43adbf54ab673fd1c1b9d276fe53b7b5624cd35abd7adc750","CHANGELOG.md":"a92e4829c4e3eecd41a60b9bbba58c4239d68ed1f4e26c7fe7a992aa76fbc4dc","Cargo.toml":"23e1a4a15ea0ac4a23841ec6dd12557c9b9110cf813a8cda7d99ac2a776e6556","Cargo.toml.orig":"fb6068e070ca3445648b9d23139d1dbabbff8ecc4a9cba757b4a7e5d077e3a98","README.md":"a249e6574262f2c168c1c2737bd0ea8a9b822daac483a2df77310b7781e1f0f2","src/lib.rs":"af034aab4c5f033f3dc3a301a1ed7c533f7961edbf53642407c4c8e988ec290d"},"package":"2c96c4a41a1f1ad04de765306e1829c7007a37898fe4033204fa82cf2e055624"}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "c92bbc35b7be416a321d0e914b0af1d6a112cbcb"
|
||||
},
|
||||
"path_in_vcs": "lora-modulation"
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
# 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.1.5]
|
||||
- Derive Eq for `Bandwidth`, `SpreadingFactor`, and `CodingRate`
|
||||
|
||||
## [v0.1.4]
|
||||
- Add `BaseBandModulationParams::symbols_to_ms`
|
||||
|
||||
---
|
||||
|
||||
Change tracking starting at version 0.1.3.
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
# 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"
|
||||
name = "lora-modulation"
|
||||
version = "0.1.5"
|
||||
authors = ["Louis Thiery <thiery.louis@gmail.com>"]
|
||||
build = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A definition of LoRa modulation characteristics with minimal external dependencies."
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"lora",
|
||||
"iot",
|
||||
"lpwan",
|
||||
]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/lora-rs/lora-rs"
|
||||
|
||||
[lib]
|
||||
name = "lora_modulation"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.defmt]
|
||||
version = "0"
|
||||
optional = true
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[features]
|
||||
defmt = ["dep:defmt"]
|
||||
serde = ["dep:serde"]
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
# lora-modulation
|
||||
|
||||
[![Latest Version]][crates.io]
|
||||
[![Docs]][doc.rs]
|
||||
|
||||
A minimal crate for providing LoRa modulation characteristics of:
|
||||
* Bandwidth
|
||||
* Spreading factor
|
||||
* Coding rate
|
||||
|
||||
Provides utility for calculating time on air.
|
||||
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use lora_modulation::{BaseBandModulationParams, SpreadingFactor, Bandwidth, CodingRate};
|
||||
|
||||
let length = 12;
|
||||
let params = BaseBandModulationParams::new(SpreadingFactor::_9, Bandwidth::_125KHz, CodingRate::_4_5);
|
||||
let time_on_air = params.time_on_air_us(
|
||||
Some(8), // preamble
|
||||
true, // explicit header
|
||||
length); // length of payload
|
||||
|
||||
// Time on air is 144.384 ms
|
||||
assert_eq!(time_on_air, 144384);
|
||||
```
|
||||
|
||||
```rust
|
||||
use lora_modulation::{BaseBandModulationParams, SpreadingFactor, Bandwidth, CodingRate};
|
||||
|
||||
let symbols = 14;
|
||||
let params = BaseBandModulationParams::new(SpreadingFactor::_12, Bandwidth::_125KHz, CodingRate::_4_5);
|
||||
let timeout = params.symbols_to_ms(symbols);
|
||||
|
||||
// Timeout is 458 ms
|
||||
assert_eq!(timeout, 458);
|
||||
```
|
||||
|
||||
[Latest Version]: https://img.shields.io/crates/v/lora-modulation.svg
|
||||
[crates.io]: https://crates.io/crates/lora-modulation
|
||||
[Docs]: https://docs.rs/lora-modulation/badge.svg
|
||||
[doc.rs]: https://docs.rs/lora-modulation
|
||||
Vendored
+266
@@ -0,0 +1,266 @@
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![cfg_attr(not(test), no_std)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// Channel width. Lower values increase time on air, but may be able to find clear frequencies.
|
||||
pub enum Bandwidth {
|
||||
_7KHz,
|
||||
_10KHz,
|
||||
_15KHz,
|
||||
_20KHz,
|
||||
_31KHz,
|
||||
_41KHz,
|
||||
_62KHz,
|
||||
_125KHz,
|
||||
_250KHz,
|
||||
_500KHz,
|
||||
}
|
||||
|
||||
impl Bandwidth {
|
||||
pub const fn hz(self) -> u32 {
|
||||
match self {
|
||||
Bandwidth::_7KHz => 7810u32,
|
||||
Bandwidth::_10KHz => 10420u32,
|
||||
Bandwidth::_15KHz => 15630u32,
|
||||
Bandwidth::_20KHz => 20830u32,
|
||||
Bandwidth::_31KHz => 31250u32,
|
||||
Bandwidth::_41KHz => 41670u32,
|
||||
Bandwidth::_62KHz => 62500u32,
|
||||
Bandwidth::_125KHz => 125000u32,
|
||||
Bandwidth::_250KHz => 250000u32,
|
||||
Bandwidth::_500KHz => 500000u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bandwidth> for u32 {
|
||||
fn from(value: Bandwidth) -> Self {
|
||||
value.hz()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// Controls the chirp rate. Lower values are slower bandwidth (longer time on air), but more robust.
|
||||
pub enum SpreadingFactor {
|
||||
_5,
|
||||
_6,
|
||||
_7,
|
||||
_8,
|
||||
_9,
|
||||
_10,
|
||||
_11,
|
||||
_12,
|
||||
}
|
||||
|
||||
impl SpreadingFactor {
|
||||
pub const fn factor(self) -> u32 {
|
||||
match self {
|
||||
SpreadingFactor::_5 => 5,
|
||||
SpreadingFactor::_6 => 6,
|
||||
SpreadingFactor::_7 => 7,
|
||||
SpreadingFactor::_8 => 8,
|
||||
SpreadingFactor::_9 => 9,
|
||||
SpreadingFactor::_10 => 10,
|
||||
SpreadingFactor::_11 => 11,
|
||||
SpreadingFactor::_12 => 12,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpreadingFactor> for u32 {
|
||||
fn from(sf: SpreadingFactor) -> Self {
|
||||
sf.factor()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// Controls the forward error correction. Higher values are more robust, but reduces the ratio
|
||||
/// of actual data in transmissions.
|
||||
pub enum CodingRate {
|
||||
_4_5,
|
||||
_4_6,
|
||||
_4_7,
|
||||
_4_8,
|
||||
}
|
||||
|
||||
impl CodingRate {
|
||||
pub const fn denom(&self) -> u32 {
|
||||
match self {
|
||||
CodingRate::_4_5 => 5,
|
||||
CodingRate::_4_6 => 6,
|
||||
CodingRate::_4_7 => 7,
|
||||
CodingRate::_4_8 => 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// LoRa modulation parameters barring frequency
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct BaseBandModulationParams {
|
||||
pub sf: SpreadingFactor,
|
||||
pub bw: Bandwidth,
|
||||
pub cr: CodingRate,
|
||||
/// low data rate optimization, see SX127x datasheet section 4.1.1.6
|
||||
pub ldro: bool,
|
||||
/// eagerly pre-calculated symbol duration in microseconds
|
||||
t_sym_us: u32,
|
||||
}
|
||||
|
||||
impl BaseBandModulationParams {
|
||||
/// Create a set of parameters, possible forcing low data rate optimization on or off.
|
||||
/// Low data rate optimization is determined automatically
|
||||
/// based on `sf` and `bw` according to Semtech's datasheets for SX126x/SX127x
|
||||
/// (enabled if symbol length is >= 16.38ms)
|
||||
pub const fn new(sf: SpreadingFactor, bw: Bandwidth, cr: CodingRate) -> Self {
|
||||
let t_sym_us = 2u32.pow(sf.factor()) * 1_000_000 / bw.hz();
|
||||
// according to SX127x 4.1.1.6 it's 16ms
|
||||
// SX126x says it's 16.38ms
|
||||
// probably it's 16.384ms which is SF11@125kHz
|
||||
let ldro = t_sym_us >= 16_384;
|
||||
Self { sf, bw, cr, ldro, t_sym_us }
|
||||
}
|
||||
|
||||
pub const fn delay_in_symbols(&self, delay_in_ms: u32) -> u16 {
|
||||
(delay_in_ms * 1000 / self.t_sym_us) as u16
|
||||
}
|
||||
|
||||
pub const fn symbols_to_ms(&self, symbols: u32) -> u32 {
|
||||
(self.t_sym_us * symbols) / 1_000
|
||||
}
|
||||
|
||||
/// Calculates time on air for a given payload and modulation parameters.
|
||||
/// If `preamble` is None, the whole preamble including syncword is excluded from calculation.
|
||||
pub const fn time_on_air_us(
|
||||
&self,
|
||||
preamble: Option<u8>,
|
||||
explicit_header: bool,
|
||||
len: u8,
|
||||
) -> u32 {
|
||||
let sf = self.sf.factor() as i32;
|
||||
let t_sym_us = self.t_sym_us;
|
||||
|
||||
let cr = self.cr.denom() as i32;
|
||||
let de = if self.ldro {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let h = if explicit_header {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
const fn div_ceil(num: i32, denom: i32) -> i32 {
|
||||
(num - 1) / denom + 1
|
||||
}
|
||||
|
||||
let big_ratio = div_ceil(8 * len as i32 - 4 * sf + 28 + 16 - 20 * h, 4 * (sf - 2 * de));
|
||||
let big_ratio = if big_ratio > 0 {
|
||||
big_ratio
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let payload_symb_nb = (8 + big_ratio * cr) as u32;
|
||||
|
||||
match preamble {
|
||||
None => t_sym_us * payload_symb_nb,
|
||||
Some(preamble) => (4 * preamble as u32 + 17 + 4 * payload_symb_nb) * t_sym_us / 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const LORAWAN_OVERHEAD: u8 = 13;
|
||||
// the shortest t_sym
|
||||
const SF5BW500: BaseBandModulationParams =
|
||||
BaseBandModulationParams::new(SpreadingFactor::_5, Bandwidth::_500KHz, CodingRate::_4_5);
|
||||
|
||||
// EU868 DR6
|
||||
const SF7BW250: BaseBandModulationParams =
|
||||
BaseBandModulationParams::new(SpreadingFactor::_7, Bandwidth::_250KHz, CodingRate::_4_5);
|
||||
// EU868 DR5
|
||||
const SF7BW125: BaseBandModulationParams =
|
||||
BaseBandModulationParams::new(SpreadingFactor::_7, Bandwidth::_125KHz, CodingRate::_4_5);
|
||||
// EU868 DR4
|
||||
const SF8BW125: BaseBandModulationParams =
|
||||
BaseBandModulationParams::new(SpreadingFactor::_8, Bandwidth::_125KHz, CodingRate::_4_5);
|
||||
// EU868 DR3
|
||||
const SF9BW125: BaseBandModulationParams =
|
||||
BaseBandModulationParams::new(SpreadingFactor::_9, Bandwidth::_125KHz, CodingRate::_4_5);
|
||||
// EU868 DR2
|
||||
const SF10BW125: BaseBandModulationParams =
|
||||
BaseBandModulationParams::new(SpreadingFactor::_10, Bandwidth::_125KHz, CodingRate::_4_5);
|
||||
// EU868 DR1
|
||||
const SF11BW125: BaseBandModulationParams =
|
||||
BaseBandModulationParams::new(SpreadingFactor::_11, Bandwidth::_125KHz, CodingRate::_4_5);
|
||||
// EU868 DR0
|
||||
const SF12BW125: BaseBandModulationParams =
|
||||
BaseBandModulationParams::new(SpreadingFactor::_12, Bandwidth::_125KHz, CodingRate::_4_5);
|
||||
|
||||
fn lorawan_airtime_us(params: &BaseBandModulationParams, app_payload_length: u8) -> u32 {
|
||||
params.time_on_air_us(Some(8), true, LORAWAN_OVERHEAD + app_payload_length)
|
||||
}
|
||||
|
||||
// data for time-on-air tests is verified against:
|
||||
// * https://www.thethingsnetwork.org/airtime-calculator
|
||||
// * https://avbentem.github.io/airtime-calculator/ttn/
|
||||
|
||||
#[test]
|
||||
fn time_on_air_for_short_messages() {
|
||||
assert_eq!(1152, SF5BW500.time_on_air_us(None, true, 0));
|
||||
assert_eq!(6656, SF7BW250.time_on_air_us(None, true, 0));
|
||||
assert_eq!(13312, SF7BW125.time_on_air_us(None, true, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_on_air() {
|
||||
let length = 25;
|
||||
assert_eq!(41_088, lorawan_airtime_us(&SF7BW250, length));
|
||||
assert_eq!(82_176, lorawan_airtime_us(&SF7BW125, length));
|
||||
assert_eq!(143_872, lorawan_airtime_us(&SF8BW125, length));
|
||||
assert_eq!(267_264, lorawan_airtime_us(&SF9BW125, length));
|
||||
assert_eq!(493_568, lorawan_airtime_us(&SF10BW125, length));
|
||||
assert_eq!(1_069_056, lorawan_airtime_us(&SF11BW125, length));
|
||||
assert_eq!(1_974_272, lorawan_airtime_us(&SF12BW125, length));
|
||||
|
||||
let length = 26;
|
||||
assert_eq!(41_088, lorawan_airtime_us(&SF7BW250, length));
|
||||
assert_eq!(82_176, lorawan_airtime_us(&SF7BW125, length));
|
||||
assert_eq!(154_112, lorawan_airtime_us(&SF8BW125, length));
|
||||
assert_eq!(267_264, lorawan_airtime_us(&SF9BW125, length));
|
||||
assert_eq!(493_568, lorawan_airtime_us(&SF10BW125, length));
|
||||
assert_eq!(1_069_056, lorawan_airtime_us(&SF11BW125, length));
|
||||
assert_eq!(1_974_272, lorawan_airtime_us(&SF12BW125, length));
|
||||
|
||||
let length = 27;
|
||||
assert_eq!(41_088, lorawan_airtime_us(&SF7BW250, length));
|
||||
assert_eq!(82_176, lorawan_airtime_us(&SF7BW125, length));
|
||||
assert_eq!(154_112, lorawan_airtime_us(&SF8BW125, length));
|
||||
assert_eq!(287_744, lorawan_airtime_us(&SF9BW125, length));
|
||||
assert_eq!(534_528, lorawan_airtime_us(&SF10BW125, length));
|
||||
assert_eq!(1_069_056, lorawan_airtime_us(&SF11BW125, length));
|
||||
assert_eq!(1_974_272, lorawan_airtime_us(&SF12BW125, length));
|
||||
|
||||
let length = 28;
|
||||
assert_eq!(43_648, lorawan_airtime_us(&SF7BW250, length));
|
||||
assert_eq!(87_296, lorawan_airtime_us(&SF7BW125, length));
|
||||
assert_eq!(154_112, lorawan_airtime_us(&SF8BW125, length));
|
||||
assert_eq!(287_744, lorawan_airtime_us(&SF9BW125, length));
|
||||
assert_eq!(534_528, lorawan_airtime_us(&SF10BW125, length));
|
||||
assert_eq!(1_150_976, lorawan_airtime_us(&SF11BW125, length));
|
||||
assert_eq!(2_138_112, lorawan_airtime_us(&SF12BW125, length));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user