diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ce77000 --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index b886c01..f1d42a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -906,7 +906,7 @@ dependencies = [ [[package]] name = "donglora-bridge" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "blake3", diff --git a/Cargo.toml b/Cargo.toml index 53746c6..eeac257 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "donglora-bridge" -version = "0.1.0" +version = "0.2.0" edition = "2024" authors = ["Stephen Waits "] license = "MIT" diff --git a/README.md b/README.md index 06a30f6..c61115e 100644 --- a/README.md +++ b/README.md @@ -39,16 +39,17 @@ similar ESP32+SX1262 board) into a USB LoRa dongle. You will need to: ### 2. (Optional) donglora-mux -If you want multiple programs to share a single DongLoRa radio at the same time, -install the multiplexer daemon: +The bridge works fine without the mux -- it connects directly to the USB device. +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 cargo install donglora-mux ``` -Run `donglora-mux` before starting the bridge. The bridge will auto-detect and -connect to the mux. Without the mux, the bridge talks to the USB device -directly, which means only one program can use the radio at a time. +Run `donglora-mux` before starting the bridge. The bridge auto-detects the mux +when it's running and falls back to direct USB serial when it's not. ### 3. donglora-bridge (this project) @@ -112,6 +113,22 @@ donglora-bridge --config path.toml # custom config file 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 - **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 [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) bandwidth = "62.5kHz" # 7.8kHz, 10.4kHz, 15.6kHz, 20.8kHz, 31.25kHz, # 41.7kHz, 62.5kHz, 125kHz, 250kHz, 500kHz diff --git a/src/radio.rs b/src/radio.rs index 41e0244..3236838 100644 --- a/src/radio.rs +++ b/src/radio.rs @@ -96,9 +96,14 @@ fn connect_and_run( let (mut client, device) = if let Some(port) = port { let c = donglora_client::connect(Some(port), timeout)?; (c, shorten_path(port)) - } else { - let c = donglora_client::connect_mux_auto(timeout)?; + } else if let Ok(c) = donglora_client::connect_mux_auto(timeout) { (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}"); @@ -112,7 +117,7 @@ fn connect_and_run( } 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) => { info!("[radio] config negotiated: source={:?}, config={}", result.1, format_radio_config(&result.0)); result @@ -203,28 +208,31 @@ fn connect_and_run( /// Negotiate radio config with the dongle/mux. /// /// 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. -/// 3. If it differs, only try `SetConfig` if we haven't been rejected before. -/// 4. If `SetConfig` is rejected, set the flag IMMEDIATELY (before the mux can -/// disconnect us and lose this state), then return the cached `GetConfig` result. -/// -/// On reconnect, the flag ensures we never retry `SetConfig`. +/// 3. If it differs and we're on the mux, accept the mux's config. +/// 4. If it differs and we're on direct serial, `SetConfig` to push ours. +/// 5. If `GetConfig` fails, try `SetConfig` with our desired config. fn negotiate_config( client: &mut donglora_client::Client, desired: &RadioConfig, + device: &str, ) -> Result<(RadioConfig, ConfigSource)> { info!("[negotiate] get_config..."); match client.get_config() { 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)); - 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 { - info!("[negotiate] get_config OK (differs from desired): {}", format_radio_config(&cfg)); - ConfigSource::Mux - }; - Ok((cfg, source)) + info!("[negotiate] get_config OK (serial config differs, setting ours)"); + client.set_config(*desired)?; + info!("[negotiate] set_config OK"); + Ok((*desired, ConfigSource::Ours)) + } } Err(e) => { info!("[negotiate] get_config FAILED: {e:#}");