feat: auto-detect connection — fall back to direct USB serial when mux is unavailable

This commit is contained in:
swaits committed 2026-04-06 16:17:39 -06:00
1 parent 6bae65c464
commit ca26368136
5 files changed
+69 -23

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
# Changelog
## [0.2.0] - 2026-04-06
### Added
- Auto-detect connection mode: when no `port` is configured, the bridge now
tries the mux daemon first and falls back to direct USB serial
(`find_port()`) if the mux is unavailable. Previously it would retry the mux
forever.
### Changed
- Config negotiation on direct serial connections now pushes `SetConfig` with
the desired radio config when it differs from the dongle's current config.
On mux connections, the existing behavior (accept the mux's config) is
unchanged.
## [0.1.0] - 2026-04-06
Initial release.
Generated
+1 -1
View File
@@ -906,7 +906,7 @@ dependencies = [
[[package]] [[package]]
name = "donglora-bridge" name = "donglora-bridge"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"blake3", "blake3",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "donglora-bridge" name = "donglora-bridge"
version = "0.1.0" version = "0.2.0"
edition = "2024" edition = "2024"
authors = ["Stephen Waits <steve@waits.net>"] authors = ["Stephen Waits <steve@waits.net>"]
license = "MIT" license = "MIT"
+23 -6
View File
@@ -39,16 +39,17 @@ similar ESP32+SX1262 board) into a USB LoRa dongle. You will need to:
### 2. (Optional) donglora-mux ### 2. (Optional) donglora-mux
If you want multiple programs to share a single DongLoRa radio at the same time, The bridge works fine without the mux -- it connects directly to the USB device.
install the multiplexer daemon: The mux is only needed if you want multiple programs to share a single DongLoRa
radio at the same time (e.g. running two bridges with different configs, or a
bridge alongside another donglora-client program).
```sh ```sh
cargo install donglora-mux cargo install donglora-mux
``` ```
Run `donglora-mux` before starting the bridge. The bridge will auto-detect and Run `donglora-mux` before starting the bridge. The bridge auto-detects the mux
connect to the mux. Without the mux, the bridge talks to the USB device when it's running and falls back to direct USB serial when it's not.
directly, which means only one program can use the radio at a time.
### 3. donglora-bridge (this project) ### 3. donglora-bridge (this project)
@@ -112,6 +113,22 @@ donglora-bridge --config path.toml # custom config file
donglora-bridge config # re-run setup wizard donglora-bridge config # re-run setup wizard
``` ```
### Running multiple bridges
To run more than one bridge on the same machine (e.g. bridging two separate
MeshCore networks), create a config file per bridge and run each with
`--config`:
```sh
donglora-bridge --config ~/bridge-network-a.toml
donglora-bridge --config ~/bridge-network-b.toml
```
Each config can specify a different passphrase, different radio settings, and
(if using the mux) share the same radio. Without the mux, each bridge needs its
own USB dongle and an explicit `port` in its config to avoid both trying to open
the same device.
## Features ## Features
- **Gossip swarm** -- broadcast packets via [iroh-gossip](https://github.com/n0-computer/iroh), a QUIC-based overlay network - **Gossip swarm** -- broadcast packets via [iroh-gossip](https://github.com/n0-computer/iroh), a QUIC-based overlay network
@@ -142,7 +159,7 @@ Default location: `~/.config/donglora-bridge/config.toml`
```toml ```toml
[radio] [radio]
# port = "/dev/ttyACM0" # serial port (omit for auto-detect via mux) # port = "/dev/ttyACM0" # serial port (omit for auto-detect: tries mux, then USB)
frequency = 910525000 # Hz (default: 910.525 MHz) frequency = 910525000 # Hz (default: 910.525 MHz)
bandwidth = "62.5kHz" # 7.8kHz, 10.4kHz, 15.6kHz, 20.8kHz, 31.25kHz, bandwidth = "62.5kHz" # 7.8kHz, 10.4kHz, 15.6kHz, 20.8kHz, 31.25kHz,
# 41.7kHz, 62.5kHz, 125kHz, 250kHz, 500kHz # 41.7kHz, 62.5kHz, 125kHz, 250kHz, 500kHz
+23 -15
View File
@@ -96,9 +96,14 @@ fn connect_and_run(
let (mut client, device) = if let Some(port) = port { let (mut client, device) = if let Some(port) = port {
let c = donglora_client::connect(Some(port), timeout)?; let c = donglora_client::connect(Some(port), timeout)?;
(c, shorten_path(port)) (c, shorten_path(port))
} else { } else if let Ok(c) = donglora_client::connect_mux_auto(timeout) {
let c = donglora_client::connect_mux_auto(timeout)?;
(c, "mux".to_string()) (c, "mux".to_string())
} else {
let port_path = donglora_client::find_port()
.ok_or_else(|| anyhow::anyhow!("no mux or USB dongle found"))?;
info!("no mux found, connecting to serial port {port_path}");
let c = donglora_client::connect(Some(&port_path), timeout)?;
(c, shorten_path(&port_path))
}; };
info!("[radio] transport connected: {device}"); info!("[radio] transport connected: {device}");
@@ -112,7 +117,7 @@ fn connect_and_run(
} }
info!("[radio] negotiating config..."); info!("[radio] negotiating config...");
let (active_config, config_source) = match negotiate_config(&mut client, config) { let (active_config, config_source) = match negotiate_config(&mut client, config, &device) {
Ok(result) => { Ok(result) => {
info!("[radio] config negotiated: source={:?}, config={}", result.1, format_radio_config(&result.0)); info!("[radio] config negotiated: source={:?}, config={}", result.1, format_radio_config(&result.0));
result result
@@ -203,28 +208,31 @@ fn connect_and_run(
/// Negotiate radio config with the dongle/mux. /// Negotiate radio config with the dongle/mux.
/// ///
/// Strategy: /// Strategy:
/// 1. `GetConfig` first to read what's active (caches the result). /// 1. `GetConfig` first to read what's active.
/// 2. If it matches our desired config, use it — no `SetConfig` needed. /// 2. If it matches our desired config, use it — no `SetConfig` needed.
/// 3. If it differs, only try `SetConfig` if we haven't been rejected before. /// 3. If it differs and we're on the mux, accept the mux's config.
/// 4. If `SetConfig` is rejected, set the flag IMMEDIATELY (before the mux can /// 4. If it differs and we're on direct serial, `SetConfig` to push ours.
/// disconnect us and lose this state), then return the cached `GetConfig` result. /// 5. If `GetConfig` fails, try `SetConfig` with our desired config.
///
/// On reconnect, the flag ensures we never retry `SetConfig`.
fn negotiate_config<T: Transport>( fn negotiate_config<T: Transport>(
client: &mut donglora_client::Client<T>, client: &mut donglora_client::Client<T>,
desired: &RadioConfig, desired: &RadioConfig,
device: &str,
) -> Result<(RadioConfig, ConfigSource)> { ) -> Result<(RadioConfig, ConfigSource)> {
info!("[negotiate] get_config..."); info!("[negotiate] get_config...");
match client.get_config() { match client.get_config() {
Ok(cfg) => { Ok(cfg) => {
let source = if configs_match(&cfg, desired) { if configs_match(&cfg, desired) {
info!("[negotiate] get_config OK (matches desired): {}", format_radio_config(&cfg)); info!("[negotiate] get_config OK (matches desired): {}", format_radio_config(&cfg));
ConfigSource::Ours Ok((cfg, ConfigSource::Ours))
} else if device == "mux" {
info!("[negotiate] get_config OK (mux config differs, accepting): {}", format_radio_config(&cfg));
Ok((cfg, ConfigSource::Mux))
} else { } else {
info!("[negotiate] get_config OK (differs from desired): {}", format_radio_config(&cfg)); info!("[negotiate] get_config OK (serial config differs, setting ours)");
ConfigSource::Mux client.set_config(*desired)?;
}; info!("[negotiate] set_config OK");
Ok((cfg, source)) Ok((*desired, ConfigSource::Ours))
}
} }
Err(e) => { Err(e) => {
info!("[negotiate] get_config FAILED: {e:#}"); info!("[negotiate] get_config FAILED: {e:#}");