Compare commits

...
13 Commits
Author SHA1 Message Date
swaits 98be9216d8 chore: repoint remaining links at git.swaits.com
Rewrites lingering github.com / *.sr.ht references to this Gitea instance,
translating GitHub and Sourcehut URL shapes to Gitea's (/blob/<ref>/ and
sr.ht /tree/<ref>/item/ -> /src/branch/<ref>/, /tree/<tag> -> /src/tag/<tag>,
/actions/workflows/X -> /actions?workflow=X, /edit/ -> /_edit/).

Left pointing at the old hosts on purpose: pull-request links (PRs were not
migrated), forks that were never migrated, and GitHub-only surfaces Gitea
lacks. OpenPGP identity proofs are untouched by design.
2026-08-31 18:32:09 -06:00
swaits 6ba7353706 chore: use Gitea org-profile layout
Gitea 1.27 renders an org profile from README.md at the root of the .profile
repo, not profile/README.md as GitHub does. The GitHub-shaped layout is
preserved untouched in the .github repo.
2026-08-31 17:25:48 -06:00
swaits 17d637218d chore: add root README for Gitea org profile rendering 2026-08-31 17:25:14 -06:00
swaitsandClaude Opus 4.6 a45ac769a2 chore: add org logo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:56:33 -06:00
swaitsandClaude Opus 4.6 71dac4f358 docs: fix package names and stale paths
- client-py is `donglora` on PyPI (not `donglora-python`)
- mux-py is `donglora-mux` on PyPI
- Fix client library links (clients/python → client-py, clients/rust → client-rs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:45:12 -06:00
swaits f6e87d1ef8 docs: rewrite org profile — explain the ecosystem clearly, no hype 2026-04-07 14:30:51 -06:00
swaits 12e61ebb57 docs: enrich org profile README — ecosystem overview, use cases, quick start 2026-04-07 14:29:47 -06:00
swaits 8383bb88cb fix: update profile README repo links to match GH names 2026-04-07 14:25:57 -06:00
swaits fa3c201584 feat: add org profile README with repo index 2026-04-07 14:24:35 -06:00
swaits 338d0394ee refactor: deduplicate USB/UART protocol framing
- Made protocol_io.rs compile for all boards (not just heltec_v3)
- Renamed FrameAccumulator → CobsDecoder, encode_response → cobs_encode_response
- Refactored USB task to use shared CobsDecoder and route_command from protocol_io
- Removed ~80 lines of duplicated COBS framing and command routing from usb/task.rs
- Single canonical MAX_FRAME definition in protocol_io.rs
2026-04-07 09:24:59 -06:00
swaits 542b1630b7 feat: add Rust client library and mux daemon
Add donglora-client (clients/rust/) and donglora-mux (mux/rust/) as
Rust implementations of the existing Python client library and mux
daemon. Both are standalone Cargo projects with no-panic guarantees
enforced by clippy deny lints.

donglora-client provides protocol types, COBS framing, USB device
discovery, serial/mux transports, and a high-level Client<T> with
send/recv. donglora-mux is a fully async (tokio) multiplexer daemon
with dongle hot-plug reconnect, per-client bounded send queues,
SetConfig locking, and StartRx/StopRx reference counting.

52 unit tests across both crates. Updates README roadmap checkboxes
and CONTRIBUTING docs to reflect the new implementations.
2026-04-03 17:21:51 -06:00
swaits 598f276c2d chore: massive repo reorganization 2026-04-03 10:28:38 -06:00
swaits ec10c77708 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
5 changed files with 367 additions and 9 deletions

No files matched your search

+257
View File
@@ -0,0 +1,257 @@
## Module Naming & Structure
1. The unified host communication module — do you prefer comm/, host/, or something else?
host/
2. Should protocol.rs stay as a single file, or split into protocol/types.rs + protocol/framing.rs (absorbing protocol_io.rs)?
Split it apart logically
3. Or should the framing code live inside the comm/ module since it's only used there?
You decide what makes the most sense based on our first principles
4. The radio/ directory currently has just task.rs (and presumably a mod.rs). Should it stay a directory or flatten to radio.rs?
Flatten. No directory should ever have just one (or even 2?) things in it!
5. Same question for display/ — it has task.rs, render.rs, and the driver. Keep as directory?
Nope. See previous question.
6. Should there be a board/drivers/ subdirectory, or should the SH1106 driver just stay at the board level (e.g. board/sh1106.rs)?
Somehow, things like drivers need to be clearly separated from boards, both by
filename+location and by how the code is structured (abstractions, etc)
7. The channel.rs file is standalone — is that fine, or should inter-task communication be grouped differently?
I guess fine?
Board Definition Contract
8. into_parts() is currently an inherent method, not on the trait. Should it be added to LoRaBoard (requires associated types), or is the implicit contract fine?
Nothing ever implicit. This is rust.
9. Should we add a spawn_host_task() function to each board, or keep that concern in the comm/ module with internal cfg?
If it is going to vary by hardware, then yeah I think we have to push it down
into the board abstraction level. Which sucks, but at least so far seems to be
how it works. I frankly still don't believe that the Heltec V3 cannot be made to
look like a proper ttyACM0 device! I'm not giving up on that shit yet!!!
10. Should boards export a single BoardParts struct (with radio, comm, display fields) instead of returning a tuple?
struct > tuple.
11. Should LoRaBoard gain associated constants like const HAS_DISPLAY: bool or const COMM_TYPE: CommType?
I don't know. You can have a HTV4 with an OLED or without one. Does anything
above the board abstraction layer need to know this???
12. Is the current LoRaBoard trait the right level of abstraction? Should it grow (more methods), shrink (fewer), or stay the same?
You decide.
13. Should mac_address() remain on the trait, or become a field in a parts struct?
I think it can fit nicely in the struct. If that's a pattern we're choosing, be
consistent.
14. The into_parts() currently consumes self (the Board). Is this the right ownership model?
I guess? Sounds rusty.
## Chip-Family Helpers
15. For nrf52840.rs — should it be a single file or a directory (nrf52840/mod.rs + submodules)?
Maybe a directory? But how are we separating components from board
definitions??? I don't think we should just have one directory full of random
directories for MCUs, displays, UARTS, and boards all plopped in there. That
sounds **disorganized**, not **organized**.
16. The nRF52840 bind_interrupts! macro — should it live in the helper or per-board? (Boards might use different peripherals.)
If you're **very** confident we'll need to customize it per-board, then go ahead
and break it out. If not, leave it generic across all boards.
17. The mac_address() unsafe FICR block is identical in both nRF boards. Definitely extract to helper?
Yes.
18. Both ESP32-S3 boards have identical Vext GPIO36 and display reset GPIO21 logic. Should this be in esp32s3.rs or per-board?
If you're confident this **will need to be board specific** before long, break
it out. Otherwise conslidate.
19. Should chip family helpers define ALL possible init functions (USB, UART, etc.) even if not all boards use them, or only what's actually used?
Good question. I'm not sure. Best advice from you?
20. Should esp32s3.rs be split into submodules (esp32s3/radio.rs, esp32s3/usb.rs, esp32s3/display.rs)?
Uhhhh.. and ESP32 is an MCU, not a radio, not a USB, not a display. GET THE
ABSTRACTIONS AND SEPARATION RIGHT!!!!!!!!!!!!!!!!!!!!!
21. If we add an nrf52840.rs, should the existing esp32s3.rs be renamed for consistency (both are chip-family helpers but esp32s3 is named after the chip while nrf52840 would
be named after the chip too — good)?
Make the naming scheme consistent.
## Display
22. Should the board provide an already-initialized display (DisplayParts { display: DisplayDriver, mac }) or raw parts with a separate create_display() async function?
Board should provide the means to its consumer to init display, radio, etc. It
shouldn't preemptively init stuff.
23. Since into_parts() is sync but display init is async — does the create_display() approach seem right?
I don't know. What's your advice?
24. Do we need an AsyncDisplay trait (for flush + brightness), or is relying on the concrete type's methods via duck-typing sufficient?
What do you think?
25. Both drivers implement DrawTarget — rendering is already generic. The only non-generic part is flush() and init(). Is pushing init/flush into the board layer the right call?
I think so.
26. The SH1106 driver (display/sh1106.rs) — is it a hardware driver that belongs in board/drivers/, or display infrastructure that should stay in display/?
Think of the abstractions. Imagine you're just looking at this project for the
first time. Where would you look for a display driver??? drivers/? Something else?
27. The display init retry logic (try twice, give up) is identical for both drivers. Should this be a common wrapper?
I think that retry is stupid. Let the client retry.
28. Display brightness: SSD1306 uses Brightness::BRIGHTEST, SH1106 uses 0xFF. Should create_display() handle this internally?
Make it generic across displays.
29. Currently display errors are silently ignored (let _ = display.flush().await). Is this intentional and should remain?
Seems like it should be elegantly handled and propagated up the abstraction layers.
## Host Communication
30. USB has DTR disconnect detection that sends DisplayCommand::Reset/On. UART has no equivalent. Is it OK for UART boards to simply not have this feature?
I mean, do we have a choice? I **really** don't want to support these UART
boards. And I'm still refusing to believe that the Heltec V3's USB hardware
cannot be made to look like a proper ttyACM. People use these MCUs and boards to
create keyboards/HID and other things after all! So my REAL preference is to NOT
support these boards. That said, we have to support the Heltec V3. So, if we
can't do it properly, then I guess we don't have a choice here.
31. The USB task runs join(usb_dev.run(), protocol_loop(...)) — UART just loops. Is having structurally different task bodies behind the same host_task name acceptable?
Consistency is important to me. I don't know the right answer here. But be
consistent and smart.
32. Should comm/mod.rs compile ONLY the active transport, or compile both and let the linker dead-code-eliminate?
Umm if there's a nice way to do it, then sure. But no leaky abstractions! Use
traits and abstractions and layers so we compile the right thing for a given configuration.
33. The USB task's COBS accumulation is inline and the UART task uses FrameAccumulator. I've verified they're semantically identical. Can I just replace the USB inline code with FrameAccumulator?
Sure. Is "FrameAccumulator" the right name for this though?
34. The USB task's route_command (inline, lines 206-238) is byte-for-byte identical to protocol_io::route_command. Can I just delete the inline one?
Yes.
35. MAX_FRAME is defined in both usb/task.rs (line 16) and protocol_io.rs (line 12). Should there be one canonical definition?
Yes.
## Build System
36. How should build.rs discover chip-family helpers? Options: (a) scan board files for use super::<helper>, (b) naming convention, (c) explicit list in build.rs, (d) marker comments in board files?
I like build.rs scanning for board files. I just want them all in a logical
directory tree.
37. The Jinja template approach — keep it, or is there something better?
I'm pretty happy with it. You tell me: is there something better???
38. The justfile _info helper uses a hardcoded if/elif chain. Should this become data-driven (scan a file or Cargo.toml)?
Yes.
39. Should the justfile board list at line 15 be auto-derived from Cargo.toml features?
Maybe. What if it's derived from the source files it finds?
40. build.rs excludes mod, traits, esp32s3 from board discovery. With nrf52840.rs added, the exclusion list grows. Is a naming convention better (e.g.
all-lowercase-with-underscores = board, CamelCase or chip-family-names = helper)?
"Exclustion lists" are evil. Fix the abstractions and design so we don't have to
deal with this shit.
## Future-Proofing
41. Do ALL future boards use SX1262, or could there be SX1276/SX1280/other radios?
There will be other radios.
42. Could a future board have NO display at all? (Currently all boards return Some(DisplayParts) or have optional display.)
Yes, many boards will have no display.
43. Could a future board use SPI display instead of I2C?
Probably.
44. Could a future board use a completely different MCU family (STM32, RP2040)?
Yes. In fact I have both STM and RP2040 boards sitting here now.
45. Is there a realistic scenario where a board needs both USB AND UART?
No. FUCK UART.
46. Could a future board need additional tasks beyond radio/comm/display (e.g. LED, sensor)?
I am thinking of adding an RGB LED interface down into the boards. Then if the
host has turned on the display, we'd blink the LED green anytime we receive and
red any time we transmit a packet.
And these boards often have lots of other hardware. Wi-Fi, BLE, sensors,
whatever. DongLoRa is really just about the radio though so I'm not prioritizing
supporting these, beyond the RGB LED thing I mentioned.
## Migration & Testing
47. Should this be one atomic refactor, or phased (dedup first, then reorg, then display)? I propose phased — each step independently compilable and testable.
Probably phased. Use jj to checkpoint as you go along. When you're happy with a
phase, "jj commit -m 'WIP: phase description'" (jj commit is the same as a jj
describe + jj new)
48. Are there any in-flight changes on other branches/bookmarks that would conflict?
Nope. We just finished getting the Heltec V3 to work, which are the last few
"WIP" commits.
49. The protocol tests (just test) use DONGLORA_HOST_TEST=1 to skip build.rs board logic. Will the reorg affect this?
You tell me. What I do know is testing is extremely important. I'd like
comprehensive tests, property tests, mutation testing, fuzz testing. Basically
throw everything we have at it.
50. After the reorg, do you want a CI-enforceable rule like "no #[cfg(feature = \"board_name\")] outside board/ and comm/mod.rs"?
That sounds great. Any mechanisms to enforce our abstractions and first
principles are very welcome.
BTW, NO unsafe code unless it is ABSOLUTELY REQUIRED. I know we're doing
embedded stuff. There are probably a few unsafe things we have to do. But always
research for ways to eliminate unsafe code.
+24 -8
View File
@@ -4,10 +4,10 @@
DongLoRa is designed to make adding new boards easy: DongLoRa is designed to make adding new boards easy:
1. Create `src/board/your_board.rs` implementing the `init()` function 1. Create `firmware/src/board/your_board.rs` implementing the `init()` function
2. Add a Cargo feature in `Cargo.toml` with the board's dependencies 2. Add a Cargo feature in `firmware/Cargo.toml` with the board's dependencies
3. Add the board's feature/target/chip to the `Justfile` boards list 3. Add the board's feature/target/chip to the `justfile` boards list
4. `build.rs` auto-discovers the new file and generates the module selector 4. `firmware/build.rs` auto-discovers the new file and generates the module selector
See existing board files (`heltec_v3.rs`, `rak_wisblock_4631.rs`) as templates. See existing board files (`heltec_v3.rs`, `rak_wisblock_4631.rs`) as templates.
Each board file owns all hardware init and returns peripheral bundles that the Each board file owns all hardware init and returns peripheral bundles that the
@@ -20,16 +20,32 @@ common tasks consume. No shared board code — each board is self-contained.
- **Display rendering uses `.ok()`.** Drawing is best-effort; display errors are not recoverable. - **Display rendering uses `.ok()`.** Drawing is best-effort; display errors are not recoverable.
- **Every `let _ =` gets an inline comment** explaining why the result is discarded. - **Every `let _ =` gets an inline comment** explaining why the result is discarded.
## Client Libraries
Client libraries exist in [Python](https://git.swaits.com/donglora/client-py) and [Rust](https://git.swaits.com/donglora/client-rs).
To add a library in another language:
1. Create a directory with the language's standard project layout
2. Implement the wire protocol from [firmware/PROTOCOL.md](firmware/PROTOCOL.md)
3. Include device discovery (USB VID `1209`, PID `5741`)
4. Include COBS framing, command encoding, and response decoding
5. Add a mux client (connect to the mux daemon's Unix socket or TCP)
The protocol is intentionally simple — 8 commands, 7 responses, fixed-size LE.
A minimal client library should be implementable in a weekend.
## Building and Testing ## Building and Testing
```sh ```sh
just check-all # Compile-check all boards (skips unavailable toolchains) cd firmware
just build-all # Build release firmware for all boards just check-all # Compile-check all boards
just build-all # Build release firmware for all boards
just clippy <board> # Lint a specific board just clippy <board> # Lint a specific board
just test # Host-side protocol unit tests
``` ```
Xtensa boards (Heltec V3/V4) require the ESP toolchain via `espup`. Tool versions are pinned in each project's `mise.toml`.
ARM boards (RAK 4631) build with stock `rustup`. Tools are installed automatically on first run.
## Commits ## Commits
+1 -1
View File
@@ -105,4 +105,4 @@ keep, but never prematurely.
| Persistence | None | Always start fresh, true dumb pipe | | Persistence | None | Always start fresh, true dumb pipe |
| Board codegen | Jinja2 via build.rs | Auto-discovers boards from filesystem | | Board codegen | Jinja2 via build.rs | Auto-discovers boards from filesystem |
| Rust toolchain | Stock `rustup` (stable/nightly) | No forks; `espup` for Xtensa boards only | | Rust toolchain | Stock `rustup` (stable/nightly) | No forks; `espup` for Xtensa boards only |
| Firmware output | `firmware/donglora-{board}-{profile}.elf` | Readable names, not buried in `target/` | | Firmware output | `firmware/builds/donglora-{board}-{profile}.elf` | Readable names, not buried in `target/` |
+85
View File
@@ -0,0 +1,85 @@
# DongLoRa
**/ˈdɒŋ.ɡəl.ɔːr.ə/** — a portmanteau of **dongle** and **LoRa**.
LoRa radios are cheap, long-range, low-power, and everywhere. But using one from a computer is surprisingly hard. You either run someone else's firmware with someone else's protocol baked in, or you write your own embedded code from scratch.
DongLoRa takes a different approach. You flash firmware onto a LoRa board once, and from that point on, the board is a dumb USB radio. It doesn't know or care what protocol you're running. It receives bytes from the air and hands them to your computer. It takes bytes from your computer and puts them on the air. That's all it does.
Everything else — what those bytes mean, how to route them, what to display, how to respond — happens in your code, on your computer, in whatever language you want.
## The Layers
DongLoRa is a stack of independent pieces. Each layer builds on the one below it.
**Firmware** sits on the LoRa board. It owns the radio hardware and speaks a simple binary protocol over USB. It boots, waits for commands, and does exactly what it's told. It has no opinion about LoRa protocols, mesh networks, or anything else. It's a pipe.
**Client libraries** (Python and Rust) run on your computer. They handle USB device discovery, protocol framing, and encoding/decoding so you don't have to think about bytes. You call `send()` and `receive()`. That's the API.
**The multiplexer** is optional. Normally, one program opens the USB port and that's it — no one else can use the dongle. The mux daemon sits between your programs and the dongle, so multiple applications can share one radio at the same time. A mesh bot, a packet logger, and a bridge can all run simultaneously on a single dongle.
**Applications** are built on top. A P2P bridge that relays LoRa packets across the internet. An AI chatbot that lives on a MeshCore mesh network. These are real programs that people run, built entirely on the layers below.
```
your code ──► client library ──► USB ──► firmware ──► LoRa radio
your code ──► client library ──┐
your code ──► client library ──┼──► mux ──► USB ──► firmware ──► LoRa radio
your code ──► client library ──┘
```
## Getting Started
Flash the firmware onto a supported board, plug it into USB, and run a script:
```
cd firmware && just flash heltec_v4
cd examples && just rx
```
That's it. You're receiving LoRa packets. The client library finds the dongle automatically — no port configuration, no driver installation, no setup.
From here, transmitting is one function call. Changing the radio frequency, bandwidth, or spreading factor is one function call. You have full control of the radio from Python or Rust, with the feedback loop of a scripting environment instead of a flash-and-pray embedded workflow.
## What People Use It For
Because the firmware is protocol-agnostic, a DongLoRa dongle works with any LoRa network — Meshtastic, MeshCore, custom protocols, or raw packets.
Some concrete examples:
- **Run mesh nodes on real hardware.** Connect to a Meshtastic or MeshCore network from a laptop, desktop, or Raspberry Pi. No dedicated device needed — just a dongle and a script.
- **Build mesh bots.** The AI chatbot in this org runs on a MeshCore mesh, receiving messages over LoRa, responding via an LLM, and transmitting replies — all from a Python script on a server.
- **Bridge distant networks.** The bridge application connects to local LoRa traffic, encrypts it, and relays it to other bridges over the internet. Two bridges in different cities extend a mesh network's range by thousands of miles.
- **Analyze RF traffic.** Sniff, decode, and log every packet on a frequency. Useful for debugging mesh deployments, understanding channel utilization, or reverse-engineering protocols.
- **Prototype without embedded development.** Test a LoRa idea in Python in an afternoon. Change parameters, observe results, iterate. When the idea works, you can move it to embedded if you need to — or just keep running it from your computer.
- **Deploy headless gateways.** A Raspberry Pi with a dongle plugged in is a LoRa gateway. Add the mux and you can run multiple services on it.
## Repositories
| Repo | What it is |
|------|-----------|
| [firmware](https://git.swaits.com/donglora/firmware) | Embedded firmware for LoRa boards (Rust) |
| [client-rs](https://git.swaits.com/donglora/client-rs) | Rust client library — `donglora-client` on crates.io |
| [client-py](https://git.swaits.com/donglora/client-py) | Python client library — `donglora` on PyPI |
| [mux-rs](https://git.swaits.com/donglora/mux-rs) | USB multiplexer daemon (Rust) |
| [mux-py](https://git.swaits.com/donglora/mux-py) | USB multiplexer daemon — `donglora-mux` on PyPI |
| [bridge](https://git.swaits.com/donglora/bridge) | P2P LoRa bridge — relays packets across the internet via encrypted gossip (Rust) |
| [examples](https://git.swaits.com/donglora/examples) | Ready-to-run scripts — receive, transmit, ping-pong, bridge (Python) |
| [ai-bot](https://git.swaits.com/donglora/ai-bot) | MeshCore AI chatbot — default mesh name "Orac" (Python) |
## Supported Boards
The firmware currently runs on these boards, all using the SX1262 LoRa radio:
| Board | MCU | Display |
|-------|-----|---------|
| Heltec V3 | ESP32-S3 | SSD1306 OLED |
| Heltec V4 | ESP32-S3 | SSD1315 OLED |
| RAK WisBlock 4631 | nRF52840 | SSD1306 OLED (optional) |
| Wio Tracker L1 | nRF52840 | SH1106 OLED |
Adding a new board is one Rust file — a pin mapping and peripheral list. The core firmware doesn't change.
## License
[MIT](https://git.swaits.com/donglora/firmware/src/branch/main/LICENSE)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB