Commit Graph
86 Commits
Author SHA1 Message Date
swaits f12f72a793 docs: add example image of firmware running 2026-04-03 16:32:29 -06:00
swaitsandClaude Opus 4.6 86d35bf80a feat(examples): add SNR-based packet filtering to LoRa bridge
Drop INVALID and UNRELIABLE packets per PROTOCOL.md quality grading
to avoid retransmitting likely-corrupt data. MARGINAL packets are
forwarded with a warning tag. All RX log lines now include SNR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:33:56 -06:00
swaits 9ffa5c10d6 docs: add cd examples, mux usage, and Linux device permissions to READMEs 2026-04-03 10:43:28 -06:00
swaits 25f4b347f5 docs: improve architecture diagram with direct serial and mux multiplexing modes 2026-04-03 10:33:40 -06:00
swaits 89c2d51693 chore: massive repo reorganization 2026-04-03 10:28:38 -06:00
swaits 48f8d6c472 chore: update to latest ucogs 2026-04-01 13:06:52 -06:00
swaitsandClaude Opus 4.6 405f4f2f13 chore: tooling improvements
- Add cargo-binutils to mise.toml (needed for `just size`)
- Remove jj from mise.toml (optional, not a build dependency)
- Silence `mise install` output when tools are current (--quiet)
- Add `just outdated` recipe with embassy-executor exclusion
  (esp-rtos 0.2 pins embassy-executor ^0.9; 0.10 renamed arch-*
  features to platform-*, which crashes cargo-outdated)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:57:44 -06:00
swaitsandClaude Opus 4.6 d01de9d14d fix: MeshCore spec compliance audit and packet quality warnings
Audited against meshcore-spec and fixed all compliance gaps:

- Fix trace hash size encoding: use 1<<(flags&3) not (flags&3)+1
- Reject reserved path hash_size_code 3 per spec Section 3
- Minimum packet size 3 bytes (was 2)
- Add MAX_PACKET_PAYLOAD (184) and min payload (1) validation
- Tighten payload minimums: ANON_REQ≥51, encrypted≥20, group≥19,
  multipart≥2
- Add node_type 0 ("none") to NODE_TYPES
- Fix node_type fallback display (was "node=type=14", now "node=0x0e")

Add packet quality warnings for likely-corrupt frames:
- decode_meshcore_packet returns (str, list[str]) with quality warnings
- ADVERT: flag bad node_type, bad UTF-8 in name, future/bad timestamps
- Combine content warnings with low-SNR signal at display layer
- Warnings stack: one flag = maybe, multiple = high confidence corrupt

Update test corpus and generators for tightened validation rules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:57:32 -06:00
swaitsandClaude Opus 4.6 d551095f3f chore: stop tracking generated corpus and pycache
These paths are already in .gitignore but were committed before the
ignore rules existed. Remove them from the index so they stay local.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:57:14 -06:00
swaits a4544ba10f fix: audit improvements (SAFETY comment, warn on invalid params, lint config)
Add SAFETY comment to nRF52840 FICR unsafe block explaining the
register addresses and why read_volatile is correct.

Replace silent fallback defaults in to_sf()/to_cr() with defmt::warn
so invalid values are logged instead of silently accepted.

Remove unnecessary #[allow(dead_code)] on DisplayCommand enum.
Add clippy::undocumented_unsafe_blocks = "deny" lint to Cargo.toml.
2026-03-31 17:32:09 -06:00
swaits a0fbd150cf feat: MAC-based USB serial number and protocol unit tests
Replace hardcoded USB serial "001" with the board's MAC address so
multiple dongles are uniquely identifiable on one host. Thread MAC
through usb_task as a parameter, removing the Board import.

Add 31 unit tests for protocol serialization: RadioConfig roundtrips,
all Command tags, all Response variants, validation boundaries, and
edge cases. Gate firmware modules with #[cfg(not(test))] and add
DONGLORA_HOST_TEST env var so `just test` runs on the host.
2026-03-31 17:32:02 -06:00
swaits 0943274496 refactor: extract shared ESP32-S3 board module
Move common type aliases, peripheral bundles, and init helpers for
ESP32-S3 boards into src/board/esp32s3.rs. Heltec V3 (146->48 lines)
and V4 (155->57 lines) become thin wrappers containing only
board-specific code (Vext GPIO pin, display reset sequence).

Update build.rs to exclude esp32s3.rs from board discovery and
mod.rs.j2 to include the shared module under an ESP feature gate.
2026-03-31 17:31:55 -06:00
swaits 83a76d4b48 chore: update tooling (mise, justfile, examples)
Add mise.toml for tool version management, rename Justfile to justfile,
add examples/justfile for running Python examples, and update docs.
2026-03-31 17:31:47 -06:00
swaits 9560349547 feat: switch ucobs to published crate 0.2, note upgrade blockers
Switch ucobs from local path dependency to crates.io v0.2. Add note
documenting that embassy-executor 0.10 and ucobs 0.3 are blocked on
the ESP Xtensa toolchain supporting Rust >= 1.93 stable.
2026-03-30 20:46:08 -06:00
swaits dd3b7c195f feat: add #nodeops, #weekly-net, #net, #repeater-ops channels 2026-03-30 17:40:23 -06:00
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
swaits eab42e9ad8 fix: invert only RX/TX letters, not surrounding spaces in display 2026-03-29 20:42:56 -06:00
swaitsandClaude Opus 4.6 a569e09b42 fix: reliable USB disconnect/reconnect display handling
- Poll DTR every 250ms via select3 timer arm (read_packet blocks
  forever after host closes port, so DTR check must not depend on it)
- DTR drop → send DisplayCommand::Reset (blocking send, not try_send)
- DTR rise → send DisplayCommand::On
- Display task: disconnected flag ignores StatusWatch while set,
  preventing stale radio status from overriding the idle screen
- On reconnect, display grabs current RadioStatus from watch and
  renders dashboard immediately (no waiting for next radio event)
- Simplified clippy to use _cargo helper

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:56 -06:00
swaitsandClaude Opus 4.6 4e7e6d2167 feat: USB OTG CDC-ACM with custom VID:PID, disconnect UX
Switch all ESP32-S3 boards from UsbSerialJtag to USB OTG CDC-ACM via
embassy-usb. The USB device stack is now shared code — boards only
provide the platform-specific embassy-usb Driver.

- Custom VID:PID 1209:5741 (open-source VID, "WA" PID)
- CDC-ACM class with proper device enumeration
- COBS+postcard protocol over CDC-ACM packet read/write
- Concurrent USB device loop + protocol loop via join()
- On disconnect: sends DisplayCommand::Reset to show "Waiting for host..."
- Justfile: espflash no longer uses --monitor (OTG steals PHY)
- Test script updated to detect by new VID:PID

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:56 -06:00
swaitsandClaude Opus 4.6 78b0c25040 chore: note custom USB VID:PID 1209:5741 for future OTG implementation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:56 -06:00
swaitsandClaude Opus 4.6 e8744e17da fix: CR encoding bug, display overflow, auto-reconnect script
- Fix CodingRate encoding: postcard uses 0-based variant index, not
  repr(u8) value. CR 4/5 is variant 0, not 1.
- Shorten RSSI/SNR display line to fit 21-char width
- Python script auto-detects DongLoRa by USB VID:PID (303a:1001),
  reconnects on device reset/disconnect, no more crash on RST

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:56 -06:00
swaitsandClaude Opus 4.6 102ce417ff fix: ack SetConfig/StartRx/StopRx, improve display layout
Protocol:
- SetConfig, StartRx, StopRx now send Response::Ok on success
  (host was timing out waiting for acks that never came)

Display:
- Full frequency precision: 910.525MHz not 910.5MHz
- Full bandwidth labels: 62.5kHz not 62k
- RSSI/SNR row shows units (dBm, dB)
- Separator line moved down 3px to give RSSI/SNR breathing room
- Sparkline is now time-based: each bar = 1 second, scrolls left,
  showing ~1 minute of RSSI history. Empty slots = no packets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:56 -06:00
swaitsandClaude Opus 4.6 024b03c7ad feat: implement LoRa radio operations (TX, RX, config)
Wire up the lora-phy driver in radio_task:
- SetConfig stores radio parameters
- StartRx enters continuous receive mode via lora-phy
- rx_once waits for a packet, reports RSSI/SNR/payload to host
- StopRx returns to standby
- Transmit sends a payload with the configured (or overridden) params
- Concurrent RX: select between incoming packets and host commands
- Automatic RX restart after receiving a packet or completing TX
- Error handling: radio errors are reported to host, RX fallback to idle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:55 -06:00
swaitsandClaude Opus 4.6 b6fd85495a feat: implement USB serial with COBS-framed postcard protocol
Wire up UsbSerialJtag on ESP32-S3 boards for host communication:
- Read COBS-framed postcard commands from host via serial
- Route DisplayOn/Off to display task, everything else to radio
- Serialize responses back to host as COBS frames
- Use postcard::CobsAccumulator for chunked deserialization
- Concurrent read/write via embassy select

RAK nRF52840 USB (embassy-usb CDC-ACM) is still a stub — different
init path, will be implemented separately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:55 -06:00
swaitsandClaude Opus 4.6 f1df675363 refactor: rename project to DongLoRa
Rebrand from lora-dongle to DongLoRa (portmanteau of dongle + LoRa).
Lowercase donglora in Cargo package name, firmware filenames, and code.
Splash screen shows "DongLoRa" in 9x15 bold.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:55 -06:00
swaitsandClaude Opus 4.6 7d20e3ef97 fix: resolve ESP32-S3 runtime crash, match official esp-hal examples
Root causes found and fixed:
- USB OTG init stole the PHY from USB-Serial-JTAG (the only USB on V4)
- executor-thread feature conflicted with esp-hal-embassy's executor
- Linker flags needed -Wl, prefix for GCC-to-LD passthrough
- Missing exception-handler feature on esp-backtrace
- Missing alloc in build-std
- Missing LTO and codegen-units=1 in release profile
- Vext on V4 is active LOW (datasheet correct, Arduino lib was wrong)

Now matches the official esp-hal v0.22.0 examples exactly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:55 -06:00
swaitsandClaude Opus 4.6 b90739d2a2 fix: use espflash for ESP32, add prereq checks with install instructions
- flash command uses espflash for Xtensa boards, probe-rs for ARM
  (ESP32-S3 has a USB-serial bootloader, not a debug probe)
- Pre-flight checks before flash: missing tools get a clear error with
  the exact install command instead of a cryptic failure
- Pre-flight check for esp toolchain before build/clippy too

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:55 -06:00
swaitsandClaude Opus 4.6 da37f6aa8a fix: resolve ESP32-S3 linker errors, all boards produce firmware
Three fixes:
- Add .cargo/config.toml with -Tlinkall.x for xtensa target (resolves
  ROM functions, interrupt handlers, memory layout)
- Add integrated-timers feature to embassy-executor for ESP32 boards
  (resolves _embassy_time_schedule_wake)
- Add `use esp_println as _` to pull in defmt global_logger (was being
  stripped as dead code because nothing referenced the crate)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:55 -06:00
swaitsandClaude Opus 4.6 7bf4041357 docs: add README with board support matrix
Three tiers: supported now, next up (popular/straightforward), and
future (niche/contributions welcome). Board names follow MeshCore
flasher conventions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:55 -06:00
swaitsandClaude Opus 4.6 78827155b5 fix: source espup env for linker, add build-all, fix Ctrl+C handling
- Source ~/export-esp.sh in Justfile for xtensa builds (provides GCC)
- Add build-all target for producing firmware
- Trap SIGINT in build-all/check-all so Ctrl+C stops immediately
- Add .cargo/config.toml to .gitignore (may be generated)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:54 -06:00
swaitsandClaude Opus 4.6 c37b63fa18 refactor: rename rak_4631 to rak_wisblock_4631, add proper display names
Board naming now follows MeshCore conventions:
- rak_4631 → rak_wisblock_4631 (feature, file, Justfile, splash screen)
- Display splash shows human-readable names: "RAK WisBlock 4631",
  "Heltec v3", "Heltec v4"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:54 -06:00
swaitsandClaude Opus 4.6 110172ded7 feat: add Heltec V4, wire up V3/V4 peripherals, restructure entry point
- Add heltec_v4 board: ESP32-S3R2, SX1262, SSD1315 display, native USB,
  Vext on GPIO36 (active HIGH)
- Wire up heltec_v3 with real esp-hal peripherals (was a stub)
- V3 and V4 share radio/I2C pins, differ in Vext pin/polarity and
  display reset
- Restructure main.rs: cfg_if selects embassy_executor::main (Cortex-M)
  vs esp_hal_embassy::main (Xtensa) per board
- Display detection via SSD1306 init in display_task — boards always
  provide I2C bus, task exits gracefully if no display responds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:54 -06:00
swaitsandClaude Opus 4.6 8649b2ec46 fix: get Heltec V3 (ESP32-S3) building with esp toolchain
Use nightly cargo + esp rustc (RUSTC override) + -Zbuild-std=core for
Xtensa targets. This avoids the esp cargo's inability to parse modern
crate manifests (edition 2024) while still using esp's LLVM with full
Xtensa support.

Pin proc-macro-crate to 3.2.0 to avoid transitive deps requiring
rustc 1.85+.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:54 -06:00
swaitsandClaude Opus 4.6 3102fc747a 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>
2026-03-29 20:42:54 -06:00
swaits 7064110293 feat: restore Heltec V3, add firmware output, support all common boards
Reverts the removal of heltec_v3 — it's a major board. Xtensa builds
use the esp toolchain (+esp via espup); check-all gracefully skips
boards whose toolchain isn't installed.

Build artifacts now land in firmware/ with readable names like
lora-dongle-rak_4631-release.elf instead of being buried in target/.

DESIGN.md updated with "support all common boards" principle (#10)
and corrected toolchain policy (#11).
2026-03-29 20:42:54 -06:00
swaits 0e9315280b refactor: drop Heltec V3, enforce standard toolchain, remove dead code
Heltec V3 (ESP32-S3) requires the espup Xtensa toolchain fork, which
violates the standard-toolchain-only principle. Removed until upstream
Rust supports Xtensa targets via stock rustup.

Also removes all #[allow(dead_code)] band-aids — unused items are
deleted rather than suppressed. DESIGN.md updated with standard
toolchain principle (#10) and peripheral model (input/telemetry slots).
2026-03-26 15:29:58 -06:00
swaits c30f480c34 feat: initial lora-dongle firmware scaffold
Transparent LoRa-over-USB dongle firmware supporting Heltec V3 (ESP32-S3)
and RAK 4631 (nRF52840) boards. Actor-model architecture with typed
channels between USB, radio, and display tasks.

- Protocol: Command/Response enums with COBS+postcard framing design
- Board abstraction: auto-discovered via build.rs Jinja2 codegen
- Radio task: SX1262 state machine (Idle → Receiving ↔ Transmitting)
- Display task: optional SSD1306 dashboard via StatusWatch
- RAK 4631: fully wired SPI/USB/I2C peripherals, clippy-clean
- Heltec V3: structure in place, awaits espup Xtensa toolchain
2026-03-26 15:16:42 -06:00