Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1a23814e2
|
||
|
|
5de24d44c9
|
||
|
|
aa6572e104
|
||
|
|
9c51c10e7e
|
||
|
|
2b3d353c7a
|
||
|
|
4fe11526f7
|
No files matched your search
@@ -1,5 +1,61 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.1.1 — 2026-05-11
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refreshed `Cargo.lock` to pull in the latest compatible patch
|
||||||
|
releases of dependencies:
|
||||||
|
- `donglora-protocol` 1.0.0 → 1.2.0 (range `"1"` unchanged)
|
||||||
|
- `tokio` 1.52.1 → 1.52.3
|
||||||
|
- `libc` 0.2.184 → 0.2.186
|
||||||
|
- `bitflags` 2.11.0 → 2.11.1
|
||||||
|
No source changes; public API is unchanged.
|
||||||
|
|
||||||
|
## 1.1.0 — 2026-04-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `connect()` now validates and auto-adjusts the requested LoRa
|
||||||
|
config against the device's advertised caps (`GET_INFO`), matching
|
||||||
|
the behaviour just landed in client-py 1.1.0:
|
||||||
|
- `tx_power_dbm` is silently clamped into
|
||||||
|
`[tx_power_min_dbm, tx_power_max_dbm]`. Clamps are logged at
|
||||||
|
INFO via `tracing`. `ConnectOptions::config(cfg)` now "just works"
|
||||||
|
on boards with lower PA ceilings (e.g. SX1276 @ 20 dBm vs
|
||||||
|
SX1262 @ 22 dBm) with no per-board bookkeeping.
|
||||||
|
- `freq_hz` outside the device's range, `sf` not in
|
||||||
|
`supported_sf_bitmap`, or `bw` not in `supported_bw_bitmap` now
|
||||||
|
return `ClientError::ConfigNotSupported` *before* `SET_CONFIG`
|
||||||
|
hits the wire. Silent shifts here would cross regulatory
|
||||||
|
boundaries or change airtime/sensitivity without the caller
|
||||||
|
noticing.
|
||||||
|
- `Dongle::config()` continues to reflect `SetConfigResult.current`
|
||||||
|
— the modulation the device actually stored, post-clamp.
|
||||||
|
- New `ClientError::ConfigNotSupported { reason }` variant.
|
||||||
|
|
||||||
|
## 1.0.2 — 2026-04-23
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `donglora-protocol` dependency spec loosened from `"1.0.0"` to `"1"`.
|
||||||
|
The resolver already accepted any `1.x.y` (both specs mean `^1.0.0`),
|
||||||
|
but the `"1"` form signals intent: any 1.x release of the protocol
|
||||||
|
crate is accepted without requiring a fresh `donglora-client`
|
||||||
|
publication. Behavior unchanged.
|
||||||
|
|
||||||
|
## 1.0.1 — 2026-04-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `discovery::find_port` / `wait_for_device` now match the WCH CH340K
|
||||||
|
USB-UART bridge (`1a86:7522`) and standard CH340 (`1a86:7523`) in
|
||||||
|
the `BRIDGE_VID_PIDS` list. The Elecrow ThinkNode-M2 board ships
|
||||||
|
with a CH340K bridge; previously only CP2102, CH9102, and FTDI
|
||||||
|
bridges were recognized, so `donglora-mux` and any other
|
||||||
|
`donglora-client` consumer reported "no device" on that board
|
||||||
|
even when the serial port was clearly present.
|
||||||
|
|
||||||
## 1.0.0 — 2026-04-22
|
## 1.0.0 — 2026-04-22
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
|
|||||||
Generated
+11
-11
@@ -25,9 +25,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.0"
|
version = "2.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@@ -71,7 +71,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "donglora-client"
|
name = "donglora-client"
|
||||||
version = "1.0.0"
|
version = "1.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"donglora-protocol",
|
"donglora-protocol",
|
||||||
@@ -87,9 +87,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "donglora-protocol"
|
name = "donglora-protocol"
|
||||||
version = "1.0.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8f2743ac7668845500e3686890917cdf5b0a09d17ec099bde2db4d3fae2a4a4"
|
checksum = "0eea2a87a5ad31629140accc2bae6c931434bf1946706f2184dec9b3d356505d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heapless",
|
"heapless",
|
||||||
"ucobs",
|
"ucobs",
|
||||||
@@ -232,9 +232,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.184"
|
version = "0.2.186"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
@@ -308,7 +308,7 @@ version = "0.29.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.1",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -426,7 +426,7 @@ version = "4.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3"
|
checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.1",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
@@ -517,9 +517,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.1"
|
version = "1.52.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
|
|||||||
+5
-5
@@ -1,19 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "donglora-client"
|
name = "donglora-client"
|
||||||
version = "1.0.0"
|
version = "1.1.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.85"
|
rust-version = "1.85"
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "DongLoRa host library — connect, configure, send/receive LoRa packets (DongLoRa Protocol v2)"
|
description = "DongLoRa host library — connect, configure, send/receive LoRa packets (DongLoRa Protocol v2)"
|
||||||
repository = "https://github.com/donglora/client-rs"
|
repository = "https://git.swaits.com/donglora/client-rs"
|
||||||
homepage = "https://github.com/donglora/client-rs"
|
homepage = "https://git.swaits.com/donglora/client-rs"
|
||||||
keywords = ["lora", "radio", "usb", "serial"]
|
keywords = ["lora", "radio", "usb", "serial"]
|
||||||
categories = ["hardware-support"]
|
categories = ["hardware-support"]
|
||||||
exclude = ["CLAUDE_TODO.md", "deny.toml", "justfile", "mise.toml", "rustfmt.toml", ".jj"]
|
exclude = ["CLAUDE_TODO.md", "deny.toml", "justfile", "mise.toml", "rustfmt.toml", ".jj"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
donglora-protocol = { version = "1.0.0", default-features = false, features = ["std"] }
|
donglora-protocol = { version = "1", default-features = false, features = ["std"] }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "io-util", "net"] }
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "io-util", "net"] }
|
||||||
tokio-serial = "5"
|
tokio-serial = "5"
|
||||||
@@ -27,7 +27,7 @@ anyhow = "1"
|
|||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
# Direct access to raw wire primitives for tests (frame encoder/decoder
|
# Direct access to raw wire primitives for tests (frame encoder/decoder
|
||||||
# helpers that are not part of the donglora-client public API).
|
# helpers that are not part of the donglora-client public API).
|
||||||
donglora-protocol = { version = "1.0.0", default-features = false, features = ["std"] }
|
donglora-protocol = { version = "1", default-features = false, features = ["std"] }
|
||||||
heapless = "0.9"
|
heapless = "0.9"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
|
|||||||
+190
-2
@@ -23,7 +23,7 @@ use std::path::Path;
|
|||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use donglora_protocol::Modulation;
|
use donglora_protocol::{Info, LoRaConfig, Modulation};
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::discovery;
|
use crate::discovery;
|
||||||
@@ -380,7 +380,8 @@ async fn finalize<T: Transport>(
|
|||||||
let applied = if opts.auto_configure {
|
let applied = if opts.auto_configure {
|
||||||
match opts.config {
|
match opts.config {
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
let result = session.set_config(m, timeout).await?;
|
let prepared = prepare_config(&info, m)?;
|
||||||
|
let result = session.set_config(prepared, timeout).await?;
|
||||||
Some(result.current)
|
Some(result.current)
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
@@ -391,3 +392,190 @@ async fn finalize<T: Transport>(
|
|||||||
|
|
||||||
Ok(Dongle::new(session, info, kind, applied, opts.keepalive))
|
Ok(Dongle::new(session, info, kind, applied, opts.keepalive))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate and auto-adjust `modulation` against the device's advertised caps.
|
||||||
|
///
|
||||||
|
/// Per-field policy:
|
||||||
|
///
|
||||||
|
/// * `tx_power_dbm`: clamped into `[tx_power_min_dbm, tx_power_max_dbm]`.
|
||||||
|
/// A clamp is logged at INFO — "give me max power" quietly returning
|
||||||
|
/// less is the universally-expected behaviour and not worth a hard error.
|
||||||
|
/// * `freq_hz`: rejected with [`ClientError::ConfigNotSupported`] when
|
||||||
|
/// outside `[freq_min_hz, freq_max_hz]`. Silently shifting a 915 MHz
|
||||||
|
/// request to 868 MHz (or vice versa) would cross regulatory boundaries.
|
||||||
|
/// * `sf`, `bw`: rejected with [`ClientError::ConfigNotSupported`] when
|
||||||
|
/// the corresponding capability bit isn't set. These change airtime and
|
||||||
|
/// sensitivity dramatically; silent substitution is more confusing
|
||||||
|
/// than helpful.
|
||||||
|
///
|
||||||
|
/// Non-LoRa modulations pass through untouched — the firmware rejects
|
||||||
|
/// unsupported modulation IDs with `EMODULATION` on its own.
|
||||||
|
pub(crate) fn prepare_config(info: &Info, modulation: Modulation) -> ClientResult<Modulation> {
|
||||||
|
let Modulation::LoRa(cfg) = modulation else {
|
||||||
|
return Ok(modulation);
|
||||||
|
};
|
||||||
|
Ok(Modulation::LoRa(prepare_lora_config(info, cfg)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_lora_config(info: &Info, mut cfg: LoRaConfig) -> ClientResult<LoRaConfig> {
|
||||||
|
if cfg.freq_hz < info.freq_min_hz || cfg.freq_hz > info.freq_max_hz {
|
||||||
|
return Err(ClientError::ConfigNotSupported {
|
||||||
|
reason: format!(
|
||||||
|
"frequency {} Hz outside device range [{}, {}] Hz",
|
||||||
|
cfg.freq_hz, info.freq_min_hz, info.freq_max_hz
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.supported_sf_bitmap & (1u16 << cfg.sf) == 0 {
|
||||||
|
let supported: Vec<u8> = (0u8..16).filter(|i| info.supported_sf_bitmap & (1u16 << i) != 0).collect();
|
||||||
|
return Err(ClientError::ConfigNotSupported {
|
||||||
|
reason: format!("SF{} not supported by this device (supports SF{:?})", cfg.sf, supported),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let bw_bit = cfg.bw.as_u8();
|
||||||
|
if info.supported_bw_bitmap & (1u16 << bw_bit) == 0 {
|
||||||
|
return Err(ClientError::ConfigNotSupported {
|
||||||
|
reason: format!(
|
||||||
|
"bandwidth {:?} (bit {}) not in supported_bw_bitmap 0x{:04X}",
|
||||||
|
cfg.bw, bw_bit, info.supported_bw_bitmap
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.tx_power_dbm > info.tx_power_max_dbm {
|
||||||
|
info!(requested = cfg.tx_power_dbm, device_max = info.tx_power_max_dbm, "clamping tx_power_dbm to device max");
|
||||||
|
cfg.tx_power_dbm = info.tx_power_max_dbm;
|
||||||
|
} else if cfg.tx_power_dbm < info.tx_power_min_dbm {
|
||||||
|
info!(requested = cfg.tx_power_dbm, device_min = info.tx_power_min_dbm, "clamping tx_power_dbm to device min");
|
||||||
|
cfg.tx_power_dbm = info.tx_power_min_dbm;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used, clippy::panic)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use donglora_protocol::{
|
||||||
|
FskConfig, LoRaBandwidth, LoRaCodingRate, LoRaHeaderMode, MAX_MCU_UID_LEN, MAX_RADIO_UID_LEN, RadioChipId,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn info(tx_min: i8, tx_max: i8, freq_min: u32, freq_max: u32, sf_bm: u16, bw_bm: u16) -> Info {
|
||||||
|
Info {
|
||||||
|
proto_major: 1,
|
||||||
|
proto_minor: 0,
|
||||||
|
fw_major: 0,
|
||||||
|
fw_minor: 0,
|
||||||
|
fw_patch: 0,
|
||||||
|
radio_chip_id: RadioChipId::Sx1262.as_u16(),
|
||||||
|
capability_bitmap: donglora_protocol::cap::LORA,
|
||||||
|
supported_sf_bitmap: sf_bm,
|
||||||
|
supported_bw_bitmap: bw_bm,
|
||||||
|
max_payload_bytes: 255,
|
||||||
|
rx_queue_capacity: 32,
|
||||||
|
tx_queue_capacity: 1,
|
||||||
|
freq_min_hz: freq_min,
|
||||||
|
freq_max_hz: freq_max,
|
||||||
|
tx_power_min_dbm: tx_min,
|
||||||
|
tx_power_max_dbm: tx_max,
|
||||||
|
mcu_uid_len: 0,
|
||||||
|
mcu_uid: [0u8; MAX_MCU_UID_LEN],
|
||||||
|
radio_uid_len: 0,
|
||||||
|
radio_uid: [0u8; MAX_RADIO_UID_LEN],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lora(freq_hz: u32, sf: u8, bw: LoRaBandwidth, tx_power_dbm: i8) -> LoRaConfig {
|
||||||
|
LoRaConfig {
|
||||||
|
freq_hz,
|
||||||
|
sf,
|
||||||
|
bw,
|
||||||
|
cr: LoRaCodingRate::Cr4_5,
|
||||||
|
preamble_len: 8,
|
||||||
|
sync_word: 0x3444,
|
||||||
|
tx_power_dbm,
|
||||||
|
header_mode: LoRaHeaderMode::Explicit,
|
||||||
|
payload_crc: true,
|
||||||
|
iq_invert: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUB_GHZ_ALL_SF: u16 = 0x1FE0; // SF5..SF12
|
||||||
|
const SX127X_SF: u16 = 0x1FC0; // SF6..SF12
|
||||||
|
const SUB_GHZ_BW: u16 = 0x03FF; // BW 0..9
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tx_power_above_max_clamps_down() {
|
||||||
|
let i = info(-9, 20, 863_000_000, 928_000_000, SUB_GHZ_ALL_SF, SUB_GHZ_BW);
|
||||||
|
let cfg = lora(915_000_000, 7, LoRaBandwidth::Khz125, 30);
|
||||||
|
let Modulation::LoRa(out) = prepare_config(&i, Modulation::LoRa(cfg)).unwrap() else {
|
||||||
|
panic!("expected LoRa");
|
||||||
|
};
|
||||||
|
assert_eq!(out.tx_power_dbm, 20);
|
||||||
|
assert_eq!(out.freq_hz, 915_000_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tx_power_below_min_clamps_up() {
|
||||||
|
let i = info(2, 20, 863_000_000, 928_000_000, SUB_GHZ_ALL_SF, SUB_GHZ_BW);
|
||||||
|
let cfg = lora(915_000_000, 7, LoRaBandwidth::Khz125, -30);
|
||||||
|
let Modulation::LoRa(out) = prepare_config(&i, Modulation::LoRa(cfg)).unwrap() else {
|
||||||
|
panic!("expected LoRa");
|
||||||
|
};
|
||||||
|
assert_eq!(out.tx_power_dbm, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tx_power_in_range_unchanged() {
|
||||||
|
let i = info(-9, 22, 863_000_000, 928_000_000, SUB_GHZ_ALL_SF, SUB_GHZ_BW);
|
||||||
|
let cfg = lora(915_000_000, 7, LoRaBandwidth::Khz125, 17);
|
||||||
|
let Modulation::LoRa(out) = prepare_config(&i, Modulation::LoRa(cfg)).unwrap() else {
|
||||||
|
panic!("expected LoRa");
|
||||||
|
};
|
||||||
|
assert_eq!(out.tx_power_dbm, 17);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freq_out_of_range_rejected() {
|
||||||
|
let i = info(-9, 22, 863_000_000, 928_000_000, SUB_GHZ_ALL_SF, SUB_GHZ_BW);
|
||||||
|
let cfg = lora(300_000_000, 7, LoRaBandwidth::Khz125, 14);
|
||||||
|
let err = prepare_config(&i, Modulation::LoRa(cfg)).unwrap_err();
|
||||||
|
assert!(matches!(err, ClientError::ConfigNotSupported { ref reason } if reason.contains("frequency")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sf5_rejected_on_sx127x_bitmap() {
|
||||||
|
let i = info(2, 20, 863_000_000, 1_020_000_000, SX127X_SF, SUB_GHZ_BW);
|
||||||
|
let cfg = lora(915_000_000, 5, LoRaBandwidth::Khz125, 14);
|
||||||
|
let err = prepare_config(&i, Modulation::LoRa(cfg)).unwrap_err();
|
||||||
|
assert!(matches!(err, ClientError::ConfigNotSupported { ref reason } if reason.contains("SF5")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bw_not_in_bitmap_rejected() {
|
||||||
|
let i = info(-9, 22, 863_000_000, 928_000_000, SUB_GHZ_ALL_SF, SUB_GHZ_BW);
|
||||||
|
// Bw200 is bit 10 — SX128x only. Sub-GHz bitmap rejects it.
|
||||||
|
let cfg = lora(915_000_000, 7, LoRaBandwidth::Khz200, 14);
|
||||||
|
let err = prepare_config(&i, Modulation::LoRa(cfg)).unwrap_err();
|
||||||
|
assert!(matches!(err, ClientError::ConfigNotSupported { ref reason } if reason.contains("bandwidth")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_lora_modulation_passes_through() {
|
||||||
|
let i = info(-9, 22, 863_000_000, 928_000_000, SUB_GHZ_ALL_SF, SUB_GHZ_BW);
|
||||||
|
let fsk = FskConfig {
|
||||||
|
freq_hz: 50_000_000, // well outside info range — must not be inspected
|
||||||
|
bitrate_bps: 50_000,
|
||||||
|
freq_dev_hz: 25_000,
|
||||||
|
rx_bw: 0,
|
||||||
|
preamble_len: 16,
|
||||||
|
sync_word_len: 0,
|
||||||
|
sync_word: [0u8; 8],
|
||||||
|
};
|
||||||
|
let out = prepare_config(&i, Modulation::FskGfsk(fsk)).unwrap();
|
||||||
|
assert!(matches!(out, Modulation::FskGfsk(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ pub const USB_PID: u16 = 0x5741;
|
|||||||
const BRIDGE_VID_PIDS: &[(u16, u16)] = &[
|
const BRIDGE_VID_PIDS: &[(u16, u16)] = &[
|
||||||
(0x10C4, 0xEA60), // CP2102 (Silicon Labs)
|
(0x10C4, 0xEA60), // CP2102 (Silicon Labs)
|
||||||
(0x1A86, 0x55D4), // CH9102
|
(0x1A86, 0x55D4), // CH9102
|
||||||
|
(0x1A86, 0x7522), // CH340K — Elecrow ThinkNode-M2 ships with this
|
||||||
(0x1A86, 0x7523), // CH340
|
(0x1A86, 0x7523), // CH340
|
||||||
(0x0403, 0x6001), // FT232R (FTDI)
|
(0x0403, 0x6001), // FT232R (FTDI)
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -400,6 +400,7 @@ impl ClientError {
|
|||||||
Self::BadFrame(s) => Self::BadFrame(s.clone()),
|
Self::BadFrame(s) => Self::BadFrame(s.clone()),
|
||||||
Self::EncodeFailed(s) => Self::EncodeFailed(s.clone()),
|
Self::EncodeFailed(s) => Self::EncodeFailed(s.clone()),
|
||||||
Self::Io(e) => Self::Other(format!("io: {e}")),
|
Self::Io(e) => Self::Other(format!("io: {e}")),
|
||||||
|
Self::ConfigNotSupported { reason } => Self::ConfigNotSupported { reason: reason.clone() },
|
||||||
Self::Other(s) => Self::Other(s.clone()),
|
Self::Other(s) => Self::Other(s.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,14 @@ pub enum ClientError {
|
|||||||
#[error("I/O error: {0}")]
|
#[error("I/O error: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// The requested config contains a field the device cannot satisfy.
|
||||||
|
/// Raised client-side, before `SET_CONFIG` hits the wire, for fields
|
||||||
|
/// that are *not* safe to silently auto-adjust (`freq_hz`, `sf`, `bw`).
|
||||||
|
/// `tx_power_dbm` is clamped transparently instead — "give me max"
|
||||||
|
/// returning less is universally expected and not worth a hard error.
|
||||||
|
#[error("config not supported by device: {reason}")]
|
||||||
|
ConfigNotSupported { reason: String },
|
||||||
|
|
||||||
/// Catch-all for transport initialisation issues that don't have a
|
/// Catch-all for transport initialisation issues that don't have a
|
||||||
/// dedicated variant.
|
/// dedicated variant.
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
|
|||||||
Reference in new issue
Block a user