Commit Graph
13 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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
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 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 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