A standalone daemon that connects two MeshCore devices operating on different radio frequencies. It forwards messages on one or more configurable bridge channels from one device to the other, effectively extending your mesh network across frequency boundaries.
- 1. Overview
- 2. Features
- 3. Requirements
- 4. Installation
- 5. Configuration
- 6. How It Works
- 7. Dashboard
- 8. File Structure
- 9. Assumptions
- 10. Troubleshooting
- 11. License
- 12. Author
The bridge runs as an independent process on the same hardware as two running meshcore-gui service instances. It imports the existing meshcore_gui modules (SharedData, Worker, models, config) as a library and requires zero modifications to the meshcore_gui codebase.
⚠️ Prerequisite: The bridge cannot function without two active meshcore-gui services running on the same host — one per MeshCore device. Install and configure meshcore-gui first: github.com/pe1hvh/meshcore-gui
┌───────────────────────────────────────────────┐
│ meshcore_bridge daemon │
│ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ SharedData A │ │ BridgeEngine │ │
│ │ + Worker A │◄──►│ - multi-pair forward │ │
│ │ (ttyUSB1) │ │ - direction filter │ │
│ └──────────────┘ │ - loop prevention │ │
│ ┌──────────────┐ └──────────────────────┘ │
│ │ SharedData B │◄────────────────────────────┤ │
│ │ + Worker B │ │ │
│ │ (ttyUSB2) │ │ │
│ └──────────────┘ │ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Bridge Dashboard (NiceGUI :9092) │ │
│ │ - Device A & B status │ │
│ │ - Bridge configuration panel (3-panel) │ │
│ │ - Forwarded message log │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
Key properties:
- Separate process — the bridge runs alongside the two meshcore-gui services on the same host, as an independent process
- Multiple bridge pairs — any number of channel pairs can be bridged simultaneously, each with its own direction setting
- Per-pair direction — each bridge can be
A→B,B→Aor bidirectionalA↔B - Live reconfiguration — bridges can be added, removed and saved from the dashboard without restarting the daemon
- Loop prevention — three mechanisms prevent message loops: direction filter, message hash tracking, and echo suppression
- Private channels — encrypted channels work transparently because the bridge operates at the plaintext level
- DOMCA dashboard — status page on its own port with device status, bridge configurator and forwarded message log
- JSON configuration — all settings in
~/.meshcore-gui/bridge/config.json; device/channel list read automatically from existing meshcore-gui cache files
- Multiple channel bridges — Configure any number of channel pairs between the two devices
- Directional forwarding — Per-pair direction:
A → B,B → A, orA ↔ B(bidirectional) - Live bridge configurator — 3-panel GUI: device A channels (left), device B channels (middle), bridge list (right); save without restart
- Auto device discovery — Device names and channel lists read from
~/.meshcore-gui/device_identity.jsonand the per-device cache files - Loop prevention — Direction filter, message hash tracking, and echo suppression prevent infinite loops
- Private channel support — Encrypted channels work transparently; bridge operates at the plaintext level
- DOMCA dashboard — Live status page with device connections, bridge statistics and forwarded message log
- JSON configuration — No YAML; pure JSON config; no extra dependencies
- systemd integration — Install as a background daemon with automatic restart
- Zero meshcore_gui changes — Imports existing modules as a library, 0 changed files
- Python 3.10+
- meshcore_gui (installed or on PYTHONPATH)
- meshcore Python library (
pip install meshcore) - Two MeshCore devices connected via USB serial
- meshcore_gui must have been run at least once so the device cache files exist
Note:
pyyamlis no longer required.
| ID | Requirement | Status |
|---|---|---|
| M1 | Bridge as separate process, meshcore_gui unchanged | ✅ |
| M2 | Forward messages on configured channels A↔B within <2s | ✅ |
| M3 | JSON config for channels, ports, polling interval | ✅ |
| M4 | 0 changed files in meshcore_gui/ | ✅ |
| M5 | GUI identical to meshcore_gui (DOMCA theme) | ✅ |
| M6 | Configurable port (--port=9092) | ✅ |
| M7 | Loop prevention via forwarded-hash set | ✅ |
| M8 | Two devices on different frequencies | ✅ |
| M9 | Private (encrypted) channels fully supported | ✅ |
| M10 | Multiple bridge pairs with per-pair direction | ✅ |
| M11 | Live reconfiguration without daemon restart | ✅ |
| M12 | Device/channel list from meshcore-gui cache files | ✅ |
# 1. Run the bridge (no extra pip installs needed)
python meshcore_bridge.py
# 2. Open the dashboard at http://your-host:9092
# 3. Use the Bridge Configuration panel to add channel bridges and savePrerequisites: Two meshcore-gui services must be running on this host — one per MeshCore device. See meshcore-gui for installation. Both services must have completed at least one successful connection so their cache files exist at ~/.meschcore/cache/.
Install the bridge as a systemd daemon for production use:
# Run the installer script
sudo bash install_bridge.sh
# Start the service
sudo systemctl start meshcore-bridge
sudo systemctl enable meshcore-bridgeUseful service commands:
| Command | Description |
|---|---|
sudo systemctl status meshcore-bridge |
Check if the service is running |
sudo journalctl -u meshcore-bridge -f |
Follow the live log output |
sudo systemctl restart meshcore-bridge |
Restart after a configuration change |
sudo systemctl stop meshcore-bridge |
Stop the service |
Uninstall:
sudo bash install_bridge.sh --uninstall| File | Purpose |
|---|---|
~/.meshcore-gui/bridge/config.json |
Bridge configuration (devices, bridge pairs, runtime settings) |
~/.meshcore-gui/device_identity.json |
Device registry read by meshcore_gui — used to resolve device names |
~/.meschcore/cache/_dev_ttyUSBX.json |
Per-device cache read by meshcore_gui — provides channel names and radio info |
The bridge config directory and file are created automatically on first save via the dashboard.
{
"device_a": {
"port": "/dev/ttyUSB1",
"baud": 115200,
"label": "ZwolsBotje"
},
"device_b": {
"port": "/dev/ttyUSB2",
"baud": 115200,
"label": "ZwolsBotje-CZ"
},
"bridges": [
{
"channel_a_key": "Public",
"channel_b_key": "Public",
"direction": "both",
"enabled": true
},
{
"channel_a_key": "Bridge",
"channel_b_key": "Bridge",
"direction": "a_to_b",
"enabled": true
}
],
"poll_interval_ms": 200,
"forward_prefix": true,
"max_forwarded_cache": 500,
"gui_port": 9092,
"gui_title": "MeshCore Bridge"
}Direction values:
| Value | Meaning |
|---|---|
"both" |
Forward in both directions (A↔B) |
"a_to_b" |
Forward only from device A to device B |
"b_to_a" |
Forward only from device B to device A |
Bridge pair fields:
| Field | Type | Description |
|---|---|---|
channel_a_key |
string | Channel name on device A (stable identifier) |
channel_b_key |
string | Channel name on device B (stable identifier) |
direction |
string | Forwarding direction (see table above) |
enabled |
bool | Whether this bridge is active |
Note: Bridges are stored by channel name (key), not by channel index. At startup the bridge resolves each key to the current channel index from the device cache. If a channel has been re-indexed since the last save, the index is corrected automatically and the config is written back to disk.
| Flag | Description | Default |
|---|---|---|
--config=PATH |
Path to JSON config file | ~/.meshcore-gui/bridge/config.json |
--port=PORT |
Override GUI port | From config (9092) |
--debug-on |
Enable verbose debug logging | Off |
--help |
Show usage info | — |
The dashboard includes an interactive Bridge Configuration panel with three columns:
- Left — Channels available on device A (read from the meshcore-gui cache file)
- Middle — Channels available on device B (read from the meshcore-gui cache file)
- Right — Active bridge list, with per-bridge enable toggle, direction label and remove button; plus an "Add bridge" form and a "Save configuration" button
Clicking Save configuration writes config.json and hot-reloads the bridge engine immediately — no restart required.
- Device A receives a channel message on a bridge channel via LoRa
- MeshCore firmware decrypts the message (if private channel) and passes plaintext to the Worker
- The Worker's EventHandler stores the message in SharedData A
- BridgeEngine polls SharedData A, detects the new message, matches it against active bridge pairs
- If the message matches an enabled pair whose direction allows A→B, BridgeEngine injects a
send_messagecommand into SharedData B's command queue - Worker B picks up the command and transmits the message on Device B's configured channel
- MeshCore firmware on Device B encrypts (if private channel) and transmits via LoRa
The reverse direction (B→A) works identically and can run simultaneously for bidirectional pairs.
The bridge uses three mechanisms to prevent message loops:
-
Direction filter — Only incoming messages (
direction='in') are forwarded. Messages we transmitted (direction='out') are never forwarded. -
Message hash tracking — Each forwarded message's hash is stored in a bounded set (configurable via
max_forwarded_cache). If the same hash appears again, it is blocked. -
Echo suppression — When a message is forwarded, the hash of the forwarded text (including
[sender]prefix) is also registered, preventing the forwarded message from being re-forwarded when it appears on the target device.
The bridge works transparently with both public and private channels. Both devices must have the bridge channel configured with the same channel secret/password:
- Inbound: MeshCore firmware decrypts → Worker receives plaintext → BridgeEngine reads plaintext
- Outbound: BridgeEngine injects command → Worker sends via meshcore lib → Firmware encrypts → LoRa TX
Prerequisite: Each bridged channel MUST be configured on both devices with identical channel secret/password. Only the frequency and channel index may differ.
The bridge dashboard is accessible at http://your-host:9092 (or your configured port) and shows:
- Configuration summary — config file name, poll interval, prefix setting, number of active bridges
- Device A status — connection state, device name, radio frequency
- Device B status — connection state, device name, radio frequency
- Bridge statistics — messages forwarded (total, A→B, B→A), duplicates blocked, uptime
- Bridge Configuration — 3-panel live configurator (see section 5.4)
- Forwarded message log — last 200 forwarded messages with timestamps and direction
The dashboard uses the same DOMCA theme as meshcore_gui with dark/light mode toggle.
meshcore_bridge/
├── __init__.py # Package init
├── __main__.py # CLI, dual-worker setup, NiceGUI server
├── config.py # JSON config loading, BridgePair dataclass
├── bridge_engine.py # Core bridge logic, multi-pair, direction filter
├── device_reader.py # Reads device_identity.json + cache files
└── gui/
├── __init__.py # GUI package init
├── dashboard.py # Bridge dashboard page
└── panels/
├── __init__.py # Panels package init
├── status_panel.py # Device connection status
├── log_panel.py # Forwarded message log
└── bridge_config_panel.py # 3-panel bridge configurator (new)
install_bridge.sh # systemd service installer
README.md # This documentation
Changed files in meshcore_gui/: 0 (zero)
- Both MeshCore devices are connected via USB serial to the same host (Raspberry Pi / Linux server)
- meshcore_gui has connected to both devices at least once, so
~/.meschcore/cache/_dev_ttyUSBX.jsonfiles exist - Each bridged channel has identical channel secret/password on both devices
- The meshcore_gui package is importable (installed via
pip install -e .or on PYTHONPATH) - Sufficient CPU/RAM for two simultaneous MeshCore connections (~100MB)
- Messages are forwarded with a sender prefix
[original_sender]for identification (configurable)
- Check that both serial ports exist:
ls -l /dev/ttyUSB* - Verify meshcore_gui is importable:
python -c "from meshcore_gui.core.shared_data import SharedData" - Check that the device cache files exist:
ls ~/.meschcore/cache/
- Open the dashboard and verify both devices show "Connected"
- Check the Bridge Configuration panel: is the relevant bridge pair enabled?
- Verify the channel secret is identical on both devices for that channel
- Enable debug mode:
python meshcore_bridge.py --debug-on
- The channel list is read from
~/.meschcore/cache/_dev_ttyUSBX.json - Run meshcore_gui and connect to both devices at least once to populate these files
- Check the file exists:
ls ~/.meschcore/cache/
| Daemon | Default Port |
|---|---|
| meshcore_gui | 8081 |
| meshcore_bridge | 9092 |
| meshcore_observer | 9093 |
Change via --port=XXXX or in config.json.
sudo systemctl status meshcore-bridge
journalctl -u meshcore-bridge -f
sudo systemctl restart meshcore-bridgeMIT License — Copyright (c) 2026 PE1HVH
PE1HVH — GitHub — DOMCA MeshCore Project