feat!: Protocol v2 client rewrite with Dongle/Session model
The full 1.0 client surface, matching the protocol v2 wire definitions from donglora-protocol 1.0.0.
* Public API is now async. The blocking Client<T: Transport> is replaced by Dongle (from donglora_client::connect / connect_with / ConnectOptions).
* src/client.rs and src/codec.rs are removed in favor of src/dongle.rs (high-level surface), src/session.rs (per-client tag/slot state), src/retry.rs (RetryPolicy + tx_with_retry), src/errors.rs (thiserror-based ClientError / ClientResult). src/protocol.rs is replaced by direct donglora-protocol imports.
* Transport is now tokio-serial for USB and tokio::net::{UnixStream, TcpStream} for mux, replacing the blocking serial + shim model.
* Dongle::tx_with_retry(data, &RetryPolicy) implements PROTOCOL.md §6.10 randomized backoff for CHANNEL_BUSY / EBUSY, returning a TxOutcome with per-attempt trace.
* Background keepalive task pings every 500 ms on host idle, tracking the 1 s inactivity timeout from §3.4 with 2x margin. Disable via ConnectOptions::keepalive(false).
* Auto-recovery from ERR(ENOTCONFIGURED): the Dongle caches the last SET_CONFIG and silently re-applies on session drop, retrying once.
* Error taxonomy switches from anyhow to thiserror. Transport-level conditions (Timeout, TransportClosed, ReaderExited, BadFrame) are first-class.
* Examples rewritten for the async API: examples/ping.rs, examples/tx.rs, examples/rx.rs all target the MeshCore US preset (910.525 MHz / BW 62.5 / SF 7 / CR 4/5 / sync 0x1424 / 20 dBm). examples/common/mod.rs holds the shared helper.
* tests/vectors.rs gains protocol-level roundtrip coverage anchored to the published donglora-protocol 1.0.0 types.
This commit is contained in:
19 files changed
+2897
-1605
No files matched your search
@@ -0,0 +1,98 @@
|
||||
//! Smoke-level vector round-trips through the `donglora-client` re-exports.
|
||||
//!
|
||||
//! The authoritative Appendix C vectors live in
|
||||
//! `donglora-protocol/tests/vectors.rs`. Here we check a representative
|
||||
//! subset just to confirm the client crate's re-exports cover the shapes
|
||||
//! a real application would use (so a refactor that accidentally hides
|
||||
//! one of them fails here rather than in downstream code).
|
||||
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use donglora_client::{
|
||||
Command, LoRaBandwidth, LoRaCodingRate, LoRaConfig, LoRaHeaderMode, Modulation, RxOrigin, RxPayload, TxDonePayload,
|
||||
TxResult,
|
||||
};
|
||||
use donglora_protocol::{FrameDecoder, FrameResult, MAX_WIRE_FRAME, encode_frame};
|
||||
|
||||
fn meshcore_us() -> Modulation {
|
||||
Modulation::LoRa(LoRaConfig {
|
||||
freq_hz: 910_525_000,
|
||||
sf: 7,
|
||||
bw: LoRaBandwidth::Khz62,
|
||||
cr: LoRaCodingRate::Cr4_5,
|
||||
preamble_len: 16,
|
||||
sync_word: 0x1424,
|
||||
tx_power_dbm: 20,
|
||||
header_mode: LoRaHeaderMode::Explicit,
|
||||
payload_crc: true,
|
||||
iq_invert: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// `SET_CONFIG(MeshCore US)` should encode/decode byte-for-byte through
|
||||
/// the public API.
|
||||
#[test]
|
||||
fn set_config_roundtrip() {
|
||||
let cmd = Command::SetConfig(meshcore_us());
|
||||
let mut payload = [0u8; 256];
|
||||
let n = cmd.encode_payload(&mut payload).unwrap();
|
||||
let mut wire = [0u8; MAX_WIRE_FRAME];
|
||||
let wire_n = encode_frame(cmd.type_id(), 42, &payload[..n], &mut wire).unwrap();
|
||||
|
||||
let mut decoder = FrameDecoder::new();
|
||||
let mut saw: Option<(u8, u16, Vec<u8>)> = None;
|
||||
decoder.feed(&wire[..wire_n], |res| match res {
|
||||
FrameResult::Ok { type_id, tag, payload } => {
|
||||
saw = Some((type_id, tag, payload.to_vec()));
|
||||
}
|
||||
FrameResult::Err(e) => panic!("decode error: {e:?}"),
|
||||
});
|
||||
let (type_id, tag, payload) = saw.expect("frame");
|
||||
assert_eq!(type_id, cmd.type_id());
|
||||
assert_eq!(tag, 42);
|
||||
|
||||
let parsed = Command::parse(type_id, &payload).unwrap();
|
||||
match parsed {
|
||||
Command::SetConfig(Modulation::LoRa(c)) => {
|
||||
assert_eq!(c.freq_hz, 910_525_000);
|
||||
assert_eq!(c.sync_word, 0x1424);
|
||||
assert_eq!(c.tx_power_dbm, 20);
|
||||
}
|
||||
_ => panic!("expected LoRa SET_CONFIG"),
|
||||
}
|
||||
}
|
||||
|
||||
/// `TX_DONE` round-trips with airtime preserved.
|
||||
#[test]
|
||||
fn tx_done_roundtrip() {
|
||||
let td = TxDonePayload { result: TxResult::Transmitted, airtime_us: 98_765 };
|
||||
let mut buf = [0u8; TxDonePayload::WIRE_SIZE];
|
||||
td.encode(&mut buf).unwrap();
|
||||
let got = TxDonePayload::decode(&buf).unwrap();
|
||||
assert_eq!(got, td);
|
||||
}
|
||||
|
||||
/// `RX` event round-trips with metadata preserved.
|
||||
#[test]
|
||||
fn rx_roundtrip() {
|
||||
let mut data = heapless::Vec::<u8, { donglora_protocol::MAX_OTA_PAYLOAD }>::new();
|
||||
data.extend_from_slice(b"hello, lora!").unwrap();
|
||||
let rx = RxPayload {
|
||||
rssi_tenths_dbm: -812,
|
||||
snr_tenths_db: 73,
|
||||
freq_err_hz: -211,
|
||||
timestamp_us: 1_000_000_000_000,
|
||||
crc_valid: true,
|
||||
packets_dropped: 3,
|
||||
origin: RxOrigin::Ota,
|
||||
data,
|
||||
};
|
||||
let mut buf = [0u8; RxPayload::METADATA_SIZE + 64];
|
||||
let n = rx.encode(&mut buf).unwrap();
|
||||
let got = RxPayload::decode(&buf[..n]).unwrap();
|
||||
assert_eq!(got.rssi_tenths_dbm, -812);
|
||||
assert_eq!(got.snr_tenths_db, 73);
|
||||
assert_eq!(got.freq_err_hz, -211);
|
||||
assert_eq!(got.packets_dropped, 3);
|
||||
assert_eq!(&got.data[..], b"hello, lora!");
|
||||
}
|
||||
Reference in new issue
Block a user