6 Commits
Author SHA1 Message Date
swaits 7d18f3791c chore: repoint URLs at git.swaits.com and bump for release
CI / check (push) Canceled after 0s
Rewrites remaining github.com / git.sr.ht references to git.swaits.com,
translating GitHub URL shapes to Gitea's (/blob/<ref>/ -> /src/branch/<ref>/,
/tree/<tag> -> /src/tag/<tag>, /actions/workflows/X -> /actions?workflow=X,
/edit/ -> /_edit/).

Left pointing at GitHub on purpose: pull-request links (PRs were not migrated)
and GitHub-only surfaces Gitea lacks (security advisories, discussions).
2026-08-31 17:58:28 -06:00
swaits 3b2c2b94dd chore: point repository at git.swaits.com
CI / check (push) Canceled after 0s
2026-08-31 17:19:31 -06:00
swaits e47e378769 release 3.0.1: pull rendezvous 0.2.1 (fast PublishFailing recovery)
CI / check (push) Canceled after 0s
2026-05-11 16:18:40 -06:00
swaits fcf2f5826c release 3.0.0: iroh 1.0-rc.0, gossip 0.99, rendezvous 0.2 2026-05-11 16:02:23 -06:00
swaits 6d0f8cd50d chore: release 2.0.1 2026-04-25 20:52:41 -06:00
swaits b8e957fe33 fix(tui): drop stale "updating DHT" overlay, bound shutdown wait
Pre-upgrade, `Gossip::shutdown()` synchronously published a "remove
self" entry to mainline DHT — that's where the TUI's "updating
DHT..." overlay came from, and it was genuinely slow. The
iroh-gossip-rendezvous 0.1 migration dropped that publish (entries
age out instead), but the shutdown flow didn't change: the overlay
still drew before shutdown ran, and the terminal was restored after.
So `q` would land the user on the misleading screen for as long as
`endpoint.close()` took to flush.

Restore the terminal the instant `q` is pressed, remove the overlay,
and bound `swarm.shutdown()` with a 2s timeout so a slow endpoint
close can't stall the process.
2026-04-25 20:50:50 -06:00
8 changed files with 451 additions and 526 deletions

No files matched your search

+73
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+6 -6
View File
@@ -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"] }
+4 -4
View File
@@ -1,6 +1,6 @@
# donglora-bridge # donglora-bridge
[![CI](https://github.com/donglora/bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/donglora/bridge/actions/workflows/ci.yml) [![CI](https://git.swaits.com/donglora/bridge/actions/workflows/ci.yml/badge.svg)](https://git.swaits.com/donglora/bridge/actions?workflow=ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
![Rust](https://img.shields.io/badge/rust-1.94.1%2B-orange) ![Rust](https://img.shields.io/badge/rust-1.94.1%2B-orange)
@@ -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
``` ```
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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> {