Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d18f3791c
|
||
|
|
3b2c2b94dd
|
||
|
|
e47e378769
|
||
|
|
fcf2f5826c
|
||
|
|
6d0f8cd50d
|
||
|
|
b8e957fe33
|
No files matched your search
@@ -1,5 +1,78 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [3.0.1] - 2026-05-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **DHT `PublishFailing` no longer wedges the swarm for minutes at a
|
||||||
|
time.** Pulls in `iroh-gossip-rendezvous` 0.2.1, which retries
|
||||||
|
failed publishes on a 15 s cadence (default) and bypasses the
|
||||||
|
`1/(n+1)` write-probability gate while the DHT is unhealthy. Prior
|
||||||
|
to this, a single failed publish parked the bridge in
|
||||||
|
`PublishFailing` for at least `write_period` (5 min) and on
|
||||||
|
average `(n + 1) × write_period` — half an hour with a populated
|
||||||
|
active view — before the next retry attempt. Recovery is now
|
||||||
|
bounded by `failure_retry_period × consecutive_failures` and
|
||||||
|
flips back to `Ready` the instant a write lands.
|
||||||
|
|
||||||
|
## [3.0.0] - 2026-05-11
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
- **`iroh` bumped 0.98 → `=1.0.0-rc.0`** and **`iroh-gossip` bumped
|
||||||
|
0.98 → 0.99**. The bridge is now built on iroh's first 1.0 release
|
||||||
|
candidate. iroh-gossip 0.99 hard-pins `iroh = "=1.0.0-rc.0"`, so the
|
||||||
|
bridge does too until iroh ships a final 1.0 — once it does, this
|
||||||
|
pin will relax to plain `"1"`. Any downstream consumer that pulls
|
||||||
|
iroh transitively must move in lockstep.
|
||||||
|
- **`iroh-gossip-rendezvous` bumped 0.1 → 0.2** (now consumed from
|
||||||
|
crates.io; the local path-dep is gone). The 0.2 release of the
|
||||||
|
rendezvous crate carries the same iroh 1.0-rc.0 bump and pins
|
||||||
|
`ed25519-dalek = "=3.0.0-pre.7"` to match iroh-base's pre-release.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `donglora-client` resolves to 1.1.1 (was 1.0.x). Adds the
|
||||||
|
`connect()`-time auto-clamp of `tx_power_dbm` against
|
||||||
|
`GET_INFO.tx_power_min/max` and pre-flight rejection of
|
||||||
|
out-of-range `freq_hz` / `sf` / `bw` with
|
||||||
|
`ClientError::ConfigNotSupported` — both surface as ordinary
|
||||||
|
bridge connection errors today.
|
||||||
|
- `Cargo.lock` refreshed end-to-end: `mainline` 6.0 → 6.1.2, plus
|
||||||
|
the usual tokio/rustls/hyper patch sweep.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- **Pre-v2 config files were silently mis-loaded.** A stale
|
||||||
|
`~/.config/donglora-bridge/config.toml` from v0.x / v1.x — with
|
||||||
|
`sync_word = 13380` (`0x3444`, the old LoRaWAN-public default),
|
||||||
|
`tx_power = "22"` (old string field), or `cad = true` (removed in
|
||||||
|
v2) — would deserialize without error: serde silently ignored the
|
||||||
|
unknown `tx_power` / `cad` fields and the bridge fell back to its
|
||||||
|
per-field defaults, producing a "requested config" that didn't
|
||||||
|
match what the user had ever entered. On a shared mux the TUI
|
||||||
|
flagged this as a `!` against the active config. Re-run
|
||||||
|
`donglora-bridge config` (or update the file by hand) if your
|
||||||
|
config still has any of `tx_power`, `cad`, or
|
||||||
|
`sync_word = 13380`. A future minor will tighten the loader so
|
||||||
|
this fails loudly instead.
|
||||||
|
|
||||||
|
## [2.0.1] - 2026-04-25
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **TUI quit no longer hangs on a misleading "updating DHT" screen.**
|
||||||
|
The 2.0.0 migration to `iroh-gossip-rendezvous` removed the
|
||||||
|
synchronous mainline-DHT publish that the old `Gossip::shutdown()`
|
||||||
|
performed (rendezvous entries age out instead), but the TUI flow
|
||||||
|
was unchanged: pressing `q` drew an "updating DHT…" overlay,
|
||||||
|
awaited `swarm.shutdown()`, and only then restored the terminal —
|
||||||
|
so the user sat on the stale message for as long as
|
||||||
|
`endpoint.close()` took to flush in-flight connections. Restore
|
||||||
|
the terminal the moment `q` is pressed, drop the overlay, and
|
||||||
|
cap the graceful-shutdown wait at 2 s so a slow endpoint close
|
||||||
|
can't stall the process.
|
||||||
|
|
||||||
## [2.0.0] - 2026-04-23
|
## [2.0.0] - 2026-04-23
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
|
|||||||
Generated
+354
-480
File diff suppressed because it is too large.
Load diff
+6
-6
@@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "donglora-bridge"
|
name = "donglora-bridge"
|
||||||
version = "2.0.0"
|
version = "3.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Peer-to-peer LoRa bridge using iroh and DongLoRa"
|
description = "Peer-to-peer LoRa bridge using iroh and DongLoRa"
|
||||||
repository = "https://github.com/donglora/bridge"
|
repository = "https://git.swaits.com/donglora/bridge"
|
||||||
homepage = "https://github.com/donglora/bridge"
|
homepage = "https://git.swaits.com/donglora/bridge"
|
||||||
keywords = ["lora", "bridge", "gossip", "p2p", "iot"]
|
keywords = ["lora", "bridge", "gossip", "p2p", "iot"]
|
||||||
categories = ["network-programming", "command-line-utilities"]
|
categories = ["network-programming", "command-line-utilities"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -15,9 +15,9 @@ exclude = [".jj/", ".github/"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
donglora-client = "1"
|
donglora-client = "1"
|
||||||
iroh = "0.98"
|
iroh = "=1.0.0-rc.0"
|
||||||
iroh-gossip = "0.98"
|
iroh-gossip = "0.99"
|
||||||
iroh-gossip-rendezvous = { version = "0.1", path = "../../iroh-gossip-rendezvous" }
|
iroh-gossip-rendezvous = "0.2.1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# donglora-bridge
|
# donglora-bridge
|
||||||
|
|
||||||
[](https://github.com/donglora/bridge/actions/workflows/ci.yml)
|
[](https://git.swaits.com/donglora/bridge/actions?workflow=ci.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||

|

|
||||||
|
|
||||||
@@ -25,12 +25,12 @@ through an encrypted peer-to-peer gossip swarm.
|
|||||||
|
|
||||||
### 1. A DongLoRa radio
|
### 1. A DongLoRa radio
|
||||||
|
|
||||||
[DongLoRa](https://github.com/donglora/firmware) is open-source firmware that
|
[DongLoRa](https://git.swaits.com/donglora/firmware) is open-source firmware that
|
||||||
turns a supported LoRa development board (such as a Heltec WiFi LoRa 32 or
|
turns a supported LoRa development board (such as a Heltec WiFi LoRa 32 or
|
||||||
similar ESP32+SX1262 board) into a USB LoRa dongle. You will need to:
|
similar ESP32+SX1262 board) into a USB LoRa dongle. You will need to:
|
||||||
|
|
||||||
1. **Get a board** -- any ESP32 board with an SX1262 LoRa radio that DongLoRa
|
1. **Get a board** -- any ESP32 board with an SX1262 LoRa radio that DongLoRa
|
||||||
supports. See the [DongLoRa README](https://github.com/donglora/firmware) for
|
supports. See the [DongLoRa README](https://git.swaits.com/donglora/firmware) for
|
||||||
the full list.
|
the full list.
|
||||||
2. **Flash the DongLoRa firmware** onto the board. Instructions are in the
|
2. **Flash the DongLoRa firmware** onto the board. Instructions are in the
|
||||||
DongLoRa repository.
|
DongLoRa repository.
|
||||||
@@ -68,7 +68,7 @@ mise use -g cargo:donglora-bridge
|
|||||||
Or build from source:
|
Or build from source:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/donglora/bridge.git
|
git clone https://git.swaits.com/donglora/bridge.git
|
||||||
cd donglora-bridge
|
cd donglora-bridge
|
||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
The DHT partition-healing algorithm specification is now maintained as part
|
The DHT partition-healing algorithm specification is now maintained as part
|
||||||
of the `iroh-gossip-rendezvous` crate:
|
of the `iroh-gossip-rendezvous` crate:
|
||||||
|
|
||||||
**<https://github.com/swaits/iroh-gossip-rendezvous/blob/main/PROTOCOL.md>**
|
**<https://git.swaits.com/swaits/iroh-gossip-rendezvous/src/branch/main/PROTOCOL.md>**
|
||||||
|
|
||||||
The bridge pins a specific `iroh-gossip-rendezvous` version; the spec that
|
The bridge pins a specific `iroh-gossip-rendezvous` version; the spec that
|
||||||
matches whatever version you're running is in
|
matches whatever version you're running is in
|
||||||
|
|||||||
+1
-3
@@ -142,9 +142,7 @@ fn spawn_receiver(
|
|||||||
if exit_reason == "cancelled" {
|
if exit_reason == "cancelled" {
|
||||||
debug!("gossip receiver stopped ({exit_reason})");
|
debug!("gossip receiver stopped ({exit_reason})");
|
||||||
} else {
|
} else {
|
||||||
error!(
|
error!("gossip receiver STOPPED ({exit_reason}) — this bridge will no longer receive from the swarm",);
|
||||||
"gossip receiver STOPPED ({exit_reason}) — this bridge will no longer receive from the swarm",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-7
@@ -1,7 +1,7 @@
|
|||||||
use donglora_bridge::{config, gossip, radio, rate_limit, router, setup, tui};
|
use donglora_bridge::{config, gossip, radio, rate_limit, router, setup, tui};
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
@@ -90,8 +90,7 @@ async fn main() -> Result<()> {
|
|||||||
info!("rate limiter: {:.1} pps", rate_limiter.rate_pps());
|
info!("rate limiter: {:.1} pps", rate_limiter.rate_pps());
|
||||||
|
|
||||||
// Create gossip network + DHT rendezvous.
|
// Create gossip network + DHT rendezvous.
|
||||||
let (swarm, gossip_event_rx, gossip_frame_tx) =
|
let (swarm, gossip_event_rx, gossip_frame_tx) = gossip::Gossip::new(secret_key, &cfg.bridge.passphrase).await?;
|
||||||
gossip::Gossip::new(secret_key, &cfg.bridge.passphrase).await?;
|
|
||||||
let swarm = Arc::new(swarm);
|
let swarm = Arc::new(swarm);
|
||||||
|
|
||||||
// Start radio.
|
// Start radio.
|
||||||
@@ -174,13 +173,18 @@ async fn main() -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
let terminal = tui::run(config_watch_rx, swarm.clone(), stats, log_rx, cancel.clone(), start_time);
|
let terminal = tui::run(config_watch_rx, swarm.clone(), stats, log_rx, cancel.clone(), start_time);
|
||||||
|
|
||||||
cancel.cancel();
|
// Restore the terminal first so the user's shell reappears the moment
|
||||||
swarm.shutdown().await;
|
// they hit `q`. iroh-gossip-rendezvous needs no synchronous DHT work
|
||||||
info!("donglora-bridge stopped");
|
// on shutdown — entries age out — so the only remaining wait is
|
||||||
|
// `endpoint.close()` flushing in-flight connections. Bound it.
|
||||||
if let Ok(mut term) = terminal {
|
if let Ok(mut term) = terminal {
|
||||||
tui::restore_terminal(&mut term);
|
tui::restore_terminal(&mut term);
|
||||||
}
|
}
|
||||||
|
cancel.cancel();
|
||||||
|
if tokio::time::timeout(Duration::from_secs(2), swarm.shutdown()).await.is_err() {
|
||||||
|
warn!("graceful shutdown timed out — exiting anyway");
|
||||||
|
}
|
||||||
|
info!("donglora-bridge stopped");
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-25
@@ -17,7 +17,7 @@ use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
|||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::symbols;
|
use ratatui::symbols;
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Sparkline};
|
use ratatui::widgets::{Block, Borders, Paragraph, Sparkline};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
@@ -173,7 +173,6 @@ pub fn run(
|
|||||||
&mut series,
|
&mut series,
|
||||||
);
|
);
|
||||||
|
|
||||||
terminal.draw(|frame| draw_shutdown_overlay(frame, frame.area()))?;
|
|
||||||
Ok(terminal)
|
Ok(terminal)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,29 +630,6 @@ fn pad_right_align(data: &[u64], width: usize) -> Vec<u64> {
|
|||||||
padded
|
padded
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Shutdown overlay ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
fn draw_shutdown_overlay(frame: &mut ratatui::Frame, area: Rect) {
|
|
||||||
let dim = Paragraph::new("").style(Style::default().bg(Color::Black));
|
|
||||||
frame.render_widget(dim, area);
|
|
||||||
|
|
||||||
let w = 36u16;
|
|
||||||
let h = 3u16;
|
|
||||||
let x = area.x + area.width.saturating_sub(w) / 2;
|
|
||||||
let y = area.y + area.height.saturating_sub(h) / 2;
|
|
||||||
let popup = Rect::new(x, y, w.min(area.width), h.min(area.height));
|
|
||||||
|
|
||||||
frame.render_widget(Clear, popup);
|
|
||||||
let block = Block::default().borders(Borders::ALL).border_style(Style::default().fg(C_BORDER));
|
|
||||||
let text = Paragraph::new(Line::from(vec![
|
|
||||||
Span::styled(" Shutting down", Style::default().fg(C_RATE_DROP)),
|
|
||||||
Span::styled(" — updating DHT...", Style::default().fg(C_LABEL)),
|
|
||||||
]))
|
|
||||||
.alignment(Alignment::Center)
|
|
||||||
.block(block);
|
|
||||||
frame.render_widget(text, popup);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Shared helpers ───────────────────────────────────────────────
|
// ── Shared helpers ───────────────────────────────────────────────
|
||||||
|
|
||||||
fn panel_block(title: &str) -> Block<'static> {
|
fn panel_block(title: &str) -> Block<'static> {
|
||||||
|
|||||||
Reference in new issue
Block a user