Commit Graph
71 Commits
Author SHA1 Message Date
swaits 53699917e0 docs: add pronunciation guide and etymology to README 2026-03-30 16:36:27 -06:00
swaits 228a0cbf65 fix: clean up meshcore_rx.py output to match themed styling
Replace raw dict dumps and debug-style prints with consistent
themed output using >>> / <<< markers and formatted responses.
Quiet TxDone noise, dim status messages, and align all output
with the 2-space indent used by RX packet lines.
2026-03-30 16:21:45 -06:00
swaits b11fcac121 feat: redesign active display layout with mode box and title box
New layout:
- Mode box (left): RX/TX indicator with vertical separator
- Title box (right): 'DongLoRa v{ver}' + radio settings (freq/bw/sf/cr)
- Centered info rows: packet counters + RSSI/SNR with labels
- Sparkline bar graph fills remaining height
- Idle state returns to splash screen (active screen only for RX/TX)
- Layout positions derived from display dimensions (h/w agnostic)
- RSSI history buffer doubled to 128 slots (~2 min at 1s/slot)
- Smart compact_count() formatting for large RX/TX counters (k/M)
2026-03-30 15:32:45 -06:00
swaits 466acfc0b6 fix: show live sparkline bar immediately on packet receipt
The rightmost graph bar now renders the current (uncommitted) time
slot, so incoming packets appear on the sparkline instantly instead
of waiting up to 1 second for the slot timer to commit them. When
the slot advances, the live bar shifts left into committed history
and a fresh live slot begins.
2026-03-30 14:44:51 -06:00
swaits b34ca99fd4 feat: upgrade all dependencies to latest versions
Major version bumps:
- defmt 0.3 → 1.0
- esp-hal 0.22 → 1.0 (full API migration)
- esp-hal-embassy 0.5 → esp-rtos 0.2 (replacement crate)
- esp-backtrace 0.14 → 0.18
- esp-println 0.12 → 0.16
- embassy-nrf 0.2 → 0.10
- embassy-embedded-hal 0.2 → 0.6
- embassy-sync 0.6 → 0.8
- embassy-time 0.3 → 0.5
- embassy-usb 0.3 → 0.6
- embassy-usb-driver 0.1 → 0.2
- embedded-io-async 0.6 → 0.7
- heapless 0.8 → 0.9
- panic-probe 0.3 → 1.0
- minijinja 2.0 → 2.18
- owo-colors 4.0 → 4.3

embassy-executor stays at 0.9 (latest with Xtensa support via esp-rtos).
Updated ESP toolchain from 1.82 to 1.93.
2026-03-30 12:59:05 -06:00
swaits 02dfa4844a chore: change display timeout in meshcore_rx example to 90s 2026-03-30 11:33:24 -06:00
swaits 4c6cab0bfe refactor: replace donglora-cobs with external ucobs crate 2026-03-30 11:27:00 -06:00
swaits 579de01fa1 chore: gitignore fuzz target directory 2026-03-30 11:07:16 -06:00
swaits 9a06a5f55e fix: move build-std from .cargo/config.toml to Justfile (Xtensa only)
build-std=core,alloc was globally enabled in .cargo/config.toml, which
conflicted with cargo-fuzz's own -Zbuild-std, causing duplicate core
errors. Now only passed for Xtensa targets via the Justfile.
2026-03-30 11:06:32 -06:00
swaits 4d71020177 fix: add empty [workspace] to fuzz Cargo.toml to prevent parent workspace discovery 2026-03-30 11:01:27 -06:00
swaits 3e1f6eb8e7 fix: exclude fuzz crate from workspace (cargo-fuzz manages its own build) 2026-03-30 11:00:36 -06:00
swaits d7bb127d92 test: proptest property testing + cargo-fuzz fuzzing for donglora-cobs 2026-03-30 10:57:57 -06:00
swaits 1fbbf110bc feat: GetMac command + MacAddress response
New protocol command (tag 8): GetMac returns the board's 6-byte hardware
address. Useful when multiple dongles are connected to one host.

Response (tag 6): MacAddress containing 6 raw bytes.

Updated: PROTOCOL.md, CHANGELOG, donglora.py decoder, all_commands.py.
2026-03-30 09:08:59 -06:00
swaits 1fc86f413a test: exhaustive COBS interop tests against corncobs, porting guide, README
- 8 new interop tests cross-validating our COBS encoder/decoder against
  corncobs (including 500 random payloads). Total: 39 tests.
- New: src/board/PORTING.md — step-by-step guide to adding a new board
- New: crates/donglora-cobs/README.md — documents design choice and testing
- Updated README with lora_bridge.py example
2026-03-30 08:55:48 -06:00
swaits 9cdacc51f5 feat: lora_bridge.py — two-way LoRa bridge over TCP (Tailscale, WireGuard, etc.) 2026-03-30 08:51:15 -06:00
swaits 583bfb72e2 feat: auto-off display after 60s idle in meshcore_rx.py, wake on RX activity 2026-03-30 08:47:24 -06:00
swaits 57478c452a feat: move version to top row of splash screen (DongLoRa left, vX.Y.Z right) 2026-03-30 08:45:18 -06:00
swaits 2633daf437 refactor: LoRaBoard trait for compile-time board completeness verification
Every board's Board struct now implements the LoRaBoard trait, which
enforces that NAME, TX_POWER_RANGE, init(), and mac_address() are all
provided. Missing any of these is a compile error — no "good intentions"
needed.

The trait verifies metadata and initialization. Tasks use the board's
concrete types directly (Embassy tasks must be concrete). Board-specific
type aliases (RadioDriver, UsbDriver, DisplayI2c) and struct definitions
(RadioParts, UsbParts, DisplayParts) remain in each board file.

- New: src/board/traits.rs (LoRaBoard trait definition)
- Modified: all 3 board files (implement trait, remove standalone constants)
- Modified: main.rs (import trait for init)
- Modified: radio/task.rs (use Board::TX_POWER_RANGE via trait)
- Modified: display/task.rs (use Board::NAME via trait)
- Modified: build.rs (exclude traits.rs from board discovery)
- Removed: embedded-hal-async direct dep (not needed for trait approach)
2026-03-30 08:44:39 -06:00
swaits 99a01b867e fix: .expect() on board init unwraps, document forget() and fixed LoRa params
Board init: all .unwrap() → .expect() with diagnostic messages.
Vext/reset forget(): documented why pin must stay in output state.
Fixed LoRa params (PREAMBLE_LEN, CRC_ON, etc.): documented as
intentionally non-configurable.
Watch capacity: documented that display sees latest state only.
2026-03-29 22:29:04 -06:00
swaits f54579bfde feat: decode Config response in donglora.py, add all_commands.py example
Config response (GetConfig) now returns decoded RadioConfig fields instead
of raw hex. New all_commands.py exercises all 8 protocol commands in
sequence — Ping, SetConfig, GetConfig, StartRx, StopRx, Transmit,
DisplayOff, DisplayOn.
2026-03-29 22:24:46 -06:00
swaits ff8762b9e4 fix: use stored RX config for post-TX restart, not per-packet TX override
If Transmit included a per-packet config with different bandwidth/SF,
the radio would silently switch parameters when resuming RX. Now uses
state.config (the config from SetConfig) for post-TX RX restart.
2026-03-29 22:22:43 -06:00
swaits 9a13c4b951 fix: handle SerialException gracefully in all example scripts
All examples now catch serial.SerialException instead of crashing with
a traceback. This covers port contention (multiple processes), device
disconnect, and USB errors. meshcore_rx.py already had this.
2026-03-29 22:01:20 -06:00
swaits 73abecfdb5 docs: document NO_SIGNAL sentinel and DTR polling interval 2026-03-29 21:50:45 -06:00
swaits 729a65e3f3 fix: replace .unwrap() with .expect() in spawn calls and build.rs 2026-03-29 21:49:24 -06:00
swaits 379067de04 fix: remove dead COBS test module from usb/task.rs (tests live in donglora-cobs crate) 2026-03-29 21:48:12 -06:00
swaits 4e221486ee fix: remove stale postcard/serde references from README, DESIGN.md, CHANGELOG, donglora.py 2026-03-29 21:47:03 -06:00
swaits 73a2d02e9c chore: document USB VID/PID/class, abandon empty commit 2026-03-29 21:33:47 -06:00
swaits bfc0acae9b docs: module-level documentation, channel capacity rationale, architecture in main.rs 2026-03-29 21:33:08 -06:00
swaits 89308e3049 docs: fix PROTOCOL.md worked example bytes, add latency principle to DESIGN.md 2026-03-29 21:33:08 -06:00
swaits 65c43cda23 feat: donglora-cobs workspace crate with 31 spec-compliance tests
Extract COBS encoder/decoder into a separate no_std workspace crate
(crates/donglora-cobs) with comprehensive tests against the Cheshire &
Baker 1999 IEEE/ACM specification and Wikipedia canonical vectors.

Tests cover: empty input, single/multiple zeros, block boundaries at
254/255 bytes, alternating patterns, the Ping regression case (tag 0x00),
round-trip verification, and error cases (truncation, unexpected zeros).

Replace inline cobs_decode() in usb/task.rs with the tested crate.
Replace external cobs dependency with workspace crate.
Fix encoder bug at 254-byte block boundary (spurious trailing code byte).
2026-03-29 21:33:08 -06:00
swaits ee71bb69d3 fix: add explicit SF=7 and CR=5 match arms in radio converters
to_sf() relied on the catch-all _ => _7 to handle SF=7, and to_cr()
relied on _ => _4_5 for CR=5. Both work by accident but are misleading
— a reader would think SF=7 and CR=5 are unhandled. Make them explicit.
2026-03-29 21:33:08 -06:00
swaits b2e8b1d366 fix: COBS decoder stripped trailing 0x00 data bytes (broke Ping command)
The cobs_decode() function had a bogus 'strip trailing zero' step that
removed the last byte if it was 0x00. The Ping command IS tag byte 0x00,
so its decoded payload was stripped to empty, causing from_bytes to return
None. The COBS standard algorithm doesn't produce spurious trailing zeros
— the post-processing was simply wrong.

Also fix read_frame to skip bare 0x00 sentinels on fresh USB connects.
2026-03-29 21:33:07 -06:00
swaits d22d2b92ac feat: show MAC address on splash screen
Splash/waiting screen now shows:
  Row 1: "DongLoRa" (bold)
  Row 2: "Board Name vX.Y.Z"
  Row 3: MAC address (XX:XX:XX:XX:XX:XX)
  Row 4: "Waiting for host..."

Each board reads its MAC at init:
- ESP32-S3: efuse MAC via esp_hal::efuse::Efuse::mac_address()
- nRF52840: FICR device address registers

BoardInfo struct bundles name/version/mac to keep dashboard() under
clippy's argument limit.
2026-03-29 21:33:07 -06:00
swaits 7dc79c1a24 fix: clippy-clean — rename to_bytes→write_to, fix Justfile clippy args 2026-03-29 20:42:59 -06:00
swaits fd360e0cfe docs: rewrite PROTOCOL.md for fixed-size LE encoding — no varint, no zigzag 2026-03-29 20:42:59 -06:00
swaits 7a947a1a05 refactor: update Python examples for fixed-size LE wire format
Remove all varint/zigzag encoding. Every field is now struct.pack/unpack
with fixed-size little-endian format strings. RadioConfig is a single
struct.pack('<IBBBHB', ...) call. Response decoding uses struct.unpack_from
with known offsets. No encoding helpers needed beyond struct.
2026-03-29 20:42:59 -06:00
swaits 1646564bc1 refactor: replace postcard/serde with fixed-size LE wire format
Drop postcard and serde dependencies entirely. All integers are now
fixed-width little-endian on the wire — no varints, no zigzag encoding.
A developer sending tx_power_dbm=14 sees 0x0E on the wire. No surprises.

- RadioConfig: 10 bytes fixed (freq:4 LE, bw:1, sf:1, cr:1, sync:2 LE, pwr:1)
- Commands/Responses: tag byte + fixed-size fields
- Vec<u8>: 2-byte LE length prefix + raw bytes
- Option<T>: 1-byte (0=None, 1=Some) + T
- COBS framing preserved (just byte-stuffing, no encoding tricks)
- Manual COBS decode replaces postcard's CobsAccumulator
- Removes ~15KB from firmware binary (no serde monomorphization)
2026-03-29 20:42:58 -06:00
swaits 584524dace docs: fix worked example, encoding table, and CHANGELOG for u8/i8 raw bytes and TX_POWER_MAX 2026-03-29 20:42:58 -06:00
swaits ebe9197d04 feat: per-board TX power range, TX_POWER_MAX sentinel (-128)
- Each board exports TX_POWER_RANGE constant (min, max dBm)
- RadioConfig::validate() uses the board's range, not hardcoded limits
- TX_POWER_MAX sentinel (-128 on the wire) means "use board's maximum"
  — resolved to the actual max before storing config
- GetConfig returns the resolved actual power, not the sentinel
- Default configs in examples use TX_POWER_MAX
- Protocol docs updated
2026-03-29 20:42:58 -06:00
swaits 0b4eb04591 fix: correct postcard encoding for u8/i8 fields (was using varint/zigzag)
ROOT CAUSE: postcard encodes u8 and i8 as raw bytes, NOT varint/zigzag.
The Python encoders used varint() for u8 and zigzag() for i8. For small
values this was silently correct (varint(7) == raw byte 7), but for
tx_power_dbm: zigzag(14) = 28, which the new validation correctly
rejected as out of range (-9..22). This was a pre-existing bug masked
by the absence of validation.

Fix all RadioConfig field encoders in both donglora.py and meshcore_rx.py.
Fix docs/PROTOCOL.md to correctly document i8 as raw byte, not zigzag.
Restore full config validation.
2026-03-29 20:42:58 -06:00
swaits a0d9b3b4f0 docs: README rewrite for publication 2026-03-29 20:42:58 -06:00
swaits 8a5d8c419e refactor: relocate MeshCore tools to examples/meshcore/
MeshCore is a protocol implementation ON TOP of DongLoRa, not part of the
dongle itself. Moving it to examples/ makes the separation clear while
keeping it accessible as a real-world usage example.
2026-03-29 20:42:58 -06:00
swaits 8790ebfd7b feat: example scripts (simple_rx, simple_tx, ping_pong) with shared donglora.py helper 2026-03-29 20:42:58 -06:00
swaits f56ddf196a docs: complete USB protocol specification (docs/PROTOCOL.md) 2026-03-29 20:42:57 -06:00
swaits 9d6ae7db6a docs: CHANGELOG.md and CONTRIBUTING.md 2026-03-29 20:42:57 -06:00
swaits 3a03778e47 chore: LICENSE (MIT), Cargo.toml metadata, gitignore firmware, display cleanup
- Add MIT LICENSE file (Stephen Waits <steve@waits.net>)
- Add Cargo.toml: authors, license, description, repository, readme, keywords
- Gitignore /firmware (built binaries don't belong in VCS)
- Display task: use DisplayState::new() for reset, handle watch/brightness errors
2026-03-29 20:42:57 -06:00
swaits a60c18a8bf fix: eliminate panics, validate config, handle all errors in radio/USB tasks
Radio task:
- Remove all .unwrap() — start_rx/rx_once now take &RadioConfig directly
- Validate RadioConfig on SetConfig and Transmit (freq, SF, CR, power)
- Notify host when RX restart fails (was silently going Idle)
- Clamp payload length with warning instead of silent truncation
- Extract modulation_params() helper, eliminating 3x duplication
- Name all magic numbers (PREAMBLE_LEN, CRC_ON, etc.)
- Add comprehensive module documentation

USB task:
- Log serialization and write failures instead of silently dropping
- Break chunk loop on write error
- Add compile-time buffer size assertion

Protocol:
- Add RadioConfig::validate() with SX1262 hardware limits
- Document all field ranges
2026-03-29 20:42:57 -06:00
swaits ece7cbc8ab refactor: make CodingRate a plain u8 denominator (5-8) on the wire
CodingRate was an enum with repr(u8) values that didn't match postcard's
wire encoding (variant index). Instead of papering over the mismatch,
make cr a plain u8 in RadioConfig. Host sends the denominator directly:
5 for CR 4/5, 6 for CR 4/6, etc. No lookup tables, no index confusion.

Remove CodingRate enum, cr_denom() helper, and the Bandwidth import that
was only needed for cr_denom. ErrorCode repr values also fixed to 0-based.
2026-03-29 20:42:57 -06:00
swaits 920dd7cef8 fix: reject payload_ver > 0 as non-MeshCore (only v0 is implemented) 2026-03-29 20:42:57 -06:00
swaits 3451ceb652 feat: MeshCore decoder, Watchman, test corpus, sparkline TX indicators
Firmware:
- fix(protocol): SNR type i8→i16 to match lora_phy PacketStatus
- fix(display): combine splash + waiting into one screen
- fix(display): move TX power to row 0 right-justified
- feat(display): sparkline distinguishes TX from RX — TX bars are dotted
  (alternating pixel rows), RX bars are solid. TX-only slots show a short
  marker. TX takes precedence over RX in the same time slot.

Host tool (tools/meshcore_rx.py):
- feat: full MeshCore packet decoder with colored ANSI output
- feat: GRP_TXT decryption using 276 channels from channels.csv
- feat: Watchman loop/collision reporter to #watchman
- feat: ADVERT-informed node registry with 12h TTL persistence
- feat: transport code fallback for older MeshCore firmware
- fix: robust error handling for long-running operation
- fix: collision detection uses evidence-based reasoning from advert cache

Channel directory (tools/channels.csv): 276 channels

Test corpus (corpus/): JSON Schema, 28 hand-crafted vectors, Hypothesis generator
2026-03-29 20:42:56 -06:00