diff --git a/Cargo.lock b/Cargo.lock index c56b45b8..beb6236a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "corncobs" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0e03e9489176ebd301922fdcd0234f6dee954cbf65311863353f20d2746a8db" + [[package]] name = "cortex-m" version = "0.7.7" @@ -359,6 +365,10 @@ dependencies = [ [[package]] name = "donglora-cobs" version = "0.1.0" +dependencies = [ + "corncobs", + "rand", +] [[package]] name = "embassy-embedded-hal" @@ -979,6 +989,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "half" version = "2.7.1" @@ -1046,6 +1067,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + [[package]] name = "litrs" version = "0.4.2" @@ -1347,6 +1374,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -1443,11 +1479,35 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46b71a9d9206e8b46714c74255adcaea8b11e0350c1d8456165073c3f75fc81a" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "riscv" @@ -1838,6 +1898,12 @@ dependencies = [ "vcell", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "winapi-util" version = "0.1.11" diff --git a/crates/donglora-cobs/Cargo.toml b/crates/donglora-cobs/Cargo.toml index 1b053e16..fd78f909 100644 --- a/crates/donglora-cobs/Cargo.toml +++ b/crates/donglora-cobs/Cargo.toml @@ -7,3 +7,7 @@ description = "COBS encoder/decoder — no_std, zero-alloc, spec-compliant" [features] default = [] + +[dev-dependencies] +corncobs = "0.1" +rand = "0.8" diff --git a/crates/donglora-cobs/README.md b/crates/donglora-cobs/README.md new file mode 100644 index 00000000..4a45afe5 --- /dev/null +++ b/crates/donglora-cobs/README.md @@ -0,0 +1,31 @@ +# donglora-cobs + +Minimal COBS (Consistent Overhead Byte Stuffing) encoder/decoder. + +- `no_std`, zero-alloc, zero dependencies at runtime +- Spec-compliant: implements Cheshire & Baker (IEEE/ACM 1999) +- Exhaustively tested: 39 tests including cross-validation against `corncobs` + +## Why our own crate? + +DongLoRa is an embedded firmware project. We want: +- Zero runtime dependencies (just 130 lines of code) +- Complete control over the implementation +- Spec compliance verified against both the canonical Wikipedia vectors + AND a battle-tested third-party crate (`corncobs`) + +For higher-throughput use cases, consider +[corncobs](https://crates.io/crates/corncobs) which has been +benchmark-optimized for streaming applications. + +## Testing + +```sh +cargo test -p donglora-cobs +``` + +Tests include: +- 18 canonical COBS vectors (Wikipedia / IEEE/ACM spec) +- Round-trip verification for various payload sizes +- 8 interop tests cross-validating against `corncobs` (dev-dependency) +- 500 randomized round-trip payloads verified against `corncobs` diff --git a/crates/donglora-cobs/src/lib.rs b/crates/donglora-cobs/src/lib.rs index 5a00aa11..0a9fbd22 100644 --- a/crates/donglora-cobs/src/lib.rs +++ b/crates/donglora-cobs/src/lib.rs @@ -388,4 +388,101 @@ mod tests { let dec_len = decode(encoded, &mut dec_buf).expect("decode failed"); assert_eq!(&dec_buf[..dec_len], input, "round-trip mismatch"); } + + // ── Cross-validation against corncobs ──────────────────────── + + /// Encode with ours, decode with corncobs — must match. + /// Note: our encoder omits the trailing 0x00 sentinel; corncobs expects it. + fn cross_encode(input: &[u8]) { + let mut our_buf = vec![0u8; max_encoded_len(input.len()) + 2]; + let our_len = encode(input, &mut our_buf).expect("our encode failed"); + // Append sentinel for corncobs compatibility + our_buf[our_len] = 0x00; + let our_with_sentinel = &our_buf[..our_len + 1]; + + let mut their_buf = vec![0u8; input.len() + 1]; + let their_len = corncobs::decode_buf(our_with_sentinel, &mut their_buf) + .expect("corncobs failed to decode our output"); + assert_eq!(&their_buf[..their_len], input, "cross-encode mismatch"); + } + + /// Encode with corncobs, decode with ours — must match. + /// Note: corncobs includes the trailing 0x00 sentinel; our decoder doesn't expect it. + fn cross_decode(input: &[u8]) { + let mut their_buf = vec![0u8; corncobs::max_encoded_len(input.len())]; + let their_len = corncobs::encode_buf(input, &mut their_buf); + // corncobs includes the sentinel — strip it for our decoder + let their_encoded = &their_buf[..their_len]; + // Find the sentinel and exclude it + let data_end = their_encoded.iter().rposition(|&b| b != 0x00).map_or(0, |i| i + 1); + let their_no_sentinel = &their_encoded[..data_end]; + + let mut our_buf = vec![0u8; input.len() + 1]; + let our_len = decode(their_no_sentinel, &mut our_buf) + .expect("our decode failed on corncobs output"); + assert_eq!(&our_buf[..our_len], input, "cross-decode mismatch"); + } + + #[test] + fn interop_empty() { + cross_encode(&[]); + cross_decode(&[]); + } + + #[test] + fn interop_single_zero() { + cross_encode(&[0x00]); + cross_decode(&[0x00]); + } + + #[test] + fn interop_mixed() { + let data = [0x11, 0x22, 0x00, 0x33, 0x00, 0x00, 0x44]; + cross_encode(&data); + cross_decode(&data); + } + + #[test] + fn interop_254_block() { + let data: Vec = (1..=254).map(|i| i as u8).collect(); + cross_encode(&data); + cross_decode(&data); + } + + #[test] + fn interop_255_block() { + let data: Vec = (0..255).map(|i| (i + 1) as u8).collect(); + cross_encode(&data); + cross_decode(&data); + } + + #[test] + fn interop_random_payloads() { + use rand::Rng; + let mut rng = rand::thread_rng(); + for _ in 0..500 { + let len = rng.gen_range(0..=512); + let data: Vec = (0..len).map(|_| rng.gen()).collect(); + cross_encode(&data); + cross_decode(&data); + } + } + + #[test] + fn interop_all_zeros() { + for len in [0, 1, 2, 10, 100, 254, 255, 256, 512] { + let data = vec![0u8; len]; + cross_encode(&data); + cross_decode(&data); + } + } + + #[test] + fn interop_all_ff() { + for len in [0, 1, 2, 10, 100, 254, 255, 256, 512] { + let data = vec![0xFFu8; len]; + cross_encode(&data); + cross_decode(&data); + } + } } diff --git a/src/board/PORTING.md b/src/board/PORTING.md new file mode 100644 index 00000000..639991ff --- /dev/null +++ b/src/board/PORTING.md @@ -0,0 +1,86 @@ +# Porting DongLoRa to a New Board + +## Steps + +### 1. Create the board file + +Create `src/board/your_board.rs`. Use an existing board as a template: +- ESP32-S3 boards: copy `heltec_v4.rs` +- nRF52840 boards: copy `rak_wisblock_4631.rs` + +### 2. Define concrete types + +Your board must export these type aliases: + +```rust +pub type RadioDriver = ...; // Must work with lora_phy::LoRa +pub type UsbDriver = ...; // Must implement embassy_usb_driver::Driver<'static> +pub type DisplayI2c = ...; // Must implement embedded_hal_async::i2c::I2c +``` + +### 3. Define peripheral bundles + +```rust +pub struct RadioParts { + pub driver: RadioDriver, + pub delay: embassy_time::Delay, +} + +pub struct UsbParts { + pub driver: UsbDriver, +} + +pub struct DisplayParts { + pub i2c: DisplayI2c, + pub mac: [u8; 6], +} +``` + +### 4. Implement the LoRaBoard trait + +```rust +use super::traits::LoRaBoard; + +impl LoRaBoard for Board { + const NAME: &'static str = "Your Board Name"; + const TX_POWER_RANGE: (i8, i8) = (-9, 22); // check your radio + PA + fn init() -> Self { ... } + fn mac_address() -> [u8; 6] { ... } // read from efuse, FICR, etc. +} +``` + +The compiler will refuse to build if any of these are missing. + +### 5. Implement `Board::into_parts()` + +```rust +impl Board { + pub fn into_parts(self) -> (RadioParts, UsbParts, Option) { + // Initialize SPI, DMA, radio, USB, I2C + // Return peripheral bundles + } +} +``` + +### 6. Add Cargo feature + +In `Cargo.toml`, add a feature with your board's HAL dependencies: + +```toml +[features] +your_board = ["dep:your-hal", ...] +``` + +### 7. Add to Justfile + +Add your board's feature/target/chip to the `boards` list at the top of `Justfile`. + +### 8. Build and test + +```sh +just check your_board # Must compile +just clippy your_board # Must be clean +just build your_board # Produces firmware +``` + +`build.rs` auto-discovers your `.rs` file — no template editing needed.