Skip to content

Commit c634802

Browse files
authored
v0.8.2
- Added a version indicator to the bottom of the Web Viewer - Added ability to filter by packet type in Packet Capture Service. - Improved mesh graph efficiency, added documentation for how to configure mesh graph calculations (or disable it) on lightweight nodes like Raspberry Pi Zero 2s. - Simplify contact removal logic. Occasional errors will still occur until race condition in meshcore_py - Improvements to documentation.
2 parents da7db84 + 99f4ed3 commit c634802

File tree

19 files changed

+1060
-848
lines changed

19 files changed

+1060
-848
lines changed

.github/workflows/docker-build.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,24 @@ jobs:
5757
type=sha,prefix=sha-
5858
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' }}
5959
60+
- name: Set version for web viewer footer
61+
id: version
62+
run: |
63+
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
64+
echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT
65+
else
66+
echo "version=dev" >> $GITHUB_OUTPUT
67+
fi
68+
6069
- name: Build and push Docker image
6170
uses: docker/build-push-action@v5
6271
with:
6372
context: .
6473
push: ${{ github.event_name != 'pull_request' }}
6574
tags: ${{ steps.meta.outputs.tags }}
6675
labels: ${{ steps.meta.outputs.labels }}
76+
build-args: |
77+
MESHCORE_BOT_VERSION=${{ steps.version.outputs.version }}
6778
cache-from: type=gha
6879
cache-to: type=gha,mode=max
6980
platforms: linux/amd64,linux/arm64

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ COPY --from=builder /root/.local /home/meshcore/.local
4343
# Set working directory
4444
WORKDIR /app
4545

46+
# Version for web viewer footer (set at build time; e.g. --build-arg MESHCORE_BOT_VERSION=v1.2.3)
47+
ARG MESHCORE_BOT_VERSION
48+
ENV MESHCORE_BOT_VERSION=${MESHCORE_BOT_VERSION}
49+
4650
# Copy application files
4751
COPY --chown=meshcore:meshcore . /app/
4852

config.ini.example

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -801,11 +801,25 @@ graph_path_validation_max_bonus = 0.3
801801
# Lower values = stronger bonus from observation count. 50.0 means 50 observations = 0.15 bonus
802802
graph_path_validation_obs_divisor = 50.0
803803

804-
# Load only recent edges on startup (days, 0 = load all)
805-
# Useful for large graphs. Set to 0 to load all edges, or N to load only edges seen in last N days.
806-
# For development with frequent restarts, 0 (load all) is recommended to maintain graph quality
804+
# Load only recent edges on startup (days, 0 = load all historical edges)
805+
# Edges older than this are skipped at startup to bound initial memory usage.
806+
# The in-code default is 14 days when this setting is absent from config.ini.
807+
# Recommended values:
808+
# 0 - Load all historical edges (servers with ample RAM, e.g. x86 VM)
809+
# 14 - Good balance of coverage vs. memory (default for unconfigured installs)
810+
# 7 - Reduced memory footprint for Raspberry Pi Zero 2 W
811+
# Note: edges older than graph_edge_expiration_days are never loaded regardless of this value.
807812
graph_startup_load_days = 0
808813

814+
# Enable graph data capture from incoming packets (default: true)
815+
# When true, the bot observes routing paths from advertisements, messages, and trace
816+
# packets and stores edges in the mesh graph.
817+
# When false, NO new edge data is collected and the background batch writer thread is
818+
# not started — reducing both CPU and RAM overhead. Any edges already in the database
819+
# are still available for graph_based_validation if that is also enabled.
820+
# Set to false on devices that don't use the path command and want minimal overhead.
821+
graph_capture_enabled = true
822+
809823
# Star bias multiplier for path command
810824
# When a contact is starred in the web viewer, multiply its selection score by this value
811825
# Higher values = stronger preference for starred repeaters
@@ -1224,6 +1238,7 @@ mqtt_enabled = true
12241238
# mqttN_topic_packets = # Packets topic template (uses placeholders below)
12251239
# mqttN_topic_prefix = # Legacy topic prefix (fallback if topic_status/topic_packets not set)
12261240
# mqttN_client_id = # MQTT client ID (optional, auto-generated from bot name)
1241+
# mqttN_upload_packet_types = # Comma-separated packet types to upload (e.g. 2,4); empty = all
12271242
#
12281243
# Topic template placeholders:
12291244
# {IATA} - Uppercase IATA code (e.g., SEA)
@@ -1243,6 +1258,7 @@ mqtt1_topic_status = meshcore/{IATA}/{PUBLIC_KEY}/status
12431258
mqtt1_topic_packets = meshcore/{IATA}/{PUBLIC_KEY}/packets
12441259
mqtt1_websocket_path = /mqtt
12451260
mqtt1_client_id =
1261+
mqtt1_upload_packet_types =
12461262

12471263
# MQTT Broker 2 - Let's Mesh Analyzer (EU)
12481264
mqtt2_enabled = true
@@ -1256,6 +1272,7 @@ mqtt2_topic_status = meshcore/{IATA}/{PUBLIC_KEY}/status
12561272
mqtt2_topic_packets = meshcore/{IATA}/{PUBLIC_KEY}/packets
12571273
mqtt2_websocket_path = /mqtt
12581274
mqtt2_client_id =
1275+
mqtt2_upload_packet_types =
12591276

12601277
# Stats and status publishing
12611278
# Enable stats in status messages

docs/faq.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,52 @@ Without `--upgrade`, the script does *not* update the service file (systemd/laun
2727
### How can I generate a custom command reference for my bot users?
2828

2929
See [Custom command reference website](command-reference-website.md): it explains how to use `generate_website.py` to build a single-page HTML from your config (with optional styles) and upload it to your site.
30+
31+
## Hardware and performance
32+
33+
### How do I run meshcore-bot on a Raspberry Pi Zero 2 W?
34+
35+
The Pi Zero 2 W has 512 MB of RAM. The bot and the web viewer are two separate
36+
Python processes; together they use roughly 300 MB on a busy mesh, which leaves
37+
little headroom. Follow the two steps below to keep things comfortable.
38+
39+
#### Step 1 — Run the bot only (saves ~150 MB)
40+
41+
The web viewer is optional. If you don't need the browser-based dashboard on
42+
the Pi itself, disable it and access it from another machine instead:
43+
44+
```ini
45+
[Web_Viewer]
46+
enabled = false
47+
auto_start = false
48+
```
49+
50+
The bot continues to work normally; the web viewer just won't start on the Pi.
51+
If you still want the dashboard, run the viewer on a desktop or server that
52+
shares the same database file (see [MeshCore Bot Data Viewer](web-viewer.md)).
53+
54+
#### Step 2 — Tune the Mesh Graph (saves another 50–100 MB on busy meshes)
55+
56+
Even with the web viewer off, the Mesh Graph can grow large. Add the following
57+
to the `[Path_Command]` section of your `config.ini`:
58+
59+
```ini
60+
[Path_Command]
61+
# Limit startup memory: only load edges seen in the last 7 days.
62+
# Edges older than this have near-zero path confidence anyway.
63+
graph_startup_load_days = 7
64+
65+
# Evict edges from RAM after 7 days without a new observation.
66+
graph_edge_expiration_days = 7
67+
68+
# Write graph updates in batches rather than on every packet.
69+
graph_write_strategy = batched
70+
71+
# If you don't use the !path command at all, disable graph capture
72+
# entirely to eliminate the background thread and all graph overhead.
73+
# graph_capture_enabled = false
74+
```
75+
76+
These settings do not affect path prediction accuracy: edges older than a few
77+
days carry negligible confidence due to the 48-hour recency half-life used by
78+
the scoring algorithm.

docs/packet-capture.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,37 @@ mqtt2_username = user
8585
mqtt2_password = pass
8686
```
8787

88+
#### Filtering by packet type
89+
90+
You can limit which packet types are uploaded to each broker with `mqttN_upload_packet_types`. Use a comma-separated list of type numbers; if unset or empty, all packet types are uploaded.
91+
92+
```ini
93+
# Only upload text messages and adverts to this broker
94+
mqtt1_upload_packet_types = 2, 4
95+
96+
# Broker 2 gets everything (default)
97+
# mqtt2_upload_packet_types =
98+
```
99+
100+
**Packet type reference:**
101+
102+
| Type | Name | Description |
103+
|------|------------|--------------------|
104+
| 0 | REQ | Request |
105+
| 1 | RESPONSE | Response |
106+
| 2 | TXT_MSG | Text message |
107+
| 3 | ACK | Acknowledgment |
108+
| 4 | ADVERT | Advertisement |
109+
| 5 | GRP_TXT | Group text |
110+
| 6 | GRP_DATA | Group data |
111+
| 7 | ANON_REQ | Anonymous request |
112+
| 8 | PATH | Path |
113+
| 9 | TRACE | Trace |
114+
| 10 | MULTIPART | Multipart |
115+
| 11–15| Type11–RAW_CUSTOM | Other types |
116+
117+
Packets that are excluded by this filter are still written to the output file (if configured) and still counted; they are only skipped for MQTT upload to that broker. Debug logs will show "Skipping" for those packets.
118+
88119
### Topic Templates
89120

90121
Placeholders:
@@ -170,8 +201,9 @@ Common issues:
170201
### No Packets Being Published
171202

172203
1. **Verify MQTT connection** - Check logs for "Connected to MQTT broker"
173-
2. **Check packet count** - Service logs "Captured packet #N" for each packet
204+
2. **Check packet count** - Service logs "Captured packet #N" (or "Skipping packet #N" when filtered) for each packet
174205
3. **Verify topics** - Ensure topics match broker expectations
206+
4. **Check upload filter** - If `mqttN_upload_packet_types` is set, only those types are uploaded. DEBUG Logs show "packet type X not in [Y, Z]" when a packet is skipped
175207

176208
---
177209

docs/path-command-config.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,15 @@ These settings control how graph edges are stored in the database.
201201

202202
**`graph_startup_load_days`** (days, 0 = load all)
203203
- Load only edges seen in last N days on startup
204-
- `0` = load all edges (recommended for development)
205-
- Default: `0`
204+
- `0` = load all edges (use on servers with ample RAM)
205+
- Default: `14` (set to `0` in `config.ini` to load all)
206+
207+
**`graph_capture_enabled`** (boolean)
208+
- When `false`, no new edge data is collected from packets and the background
209+
batch writer thread is not started — reducing CPU and RAM overhead
210+
- Edges already in the database are still used for path validation
211+
- Set to `false` on devices that don't use the path command
212+
- Default: `true`
206213

207214
## Preset Configurations
208215

docs/web-viewer.md

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,73 @@ You can keep or remove the old `bot_data.db` file after verifying the viewer wor
147147

148148
## Troubleshooting
149149

150+
### Web viewer not accessible (e.g. Orange Pi / SBC)
151+
152+
If the viewer does not load from another device (e.g. from your phone or PC while the bot runs on an Orange Pi), work through these steps on the Pi.
153+
154+
1. **Confirm config**
155+
- In `config.ini` under `[Web_Viewer]`:
156+
- `enabled = true`
157+
- `auto_start = true` (if you want it to start with the bot)
158+
- `host = 0.0.0.0` (required for access from other devices; `127.0.0.1` is localhost only)
159+
- `port = 8080` (or another port 1024–65535)
160+
- Restart the bot after changing config.
161+
162+
2. **Check that the viewer process is running**
163+
```bash
164+
# From project root on the Pi
165+
ss -tlnp | grep 8080
166+
# or
167+
netstat -tlnp | grep 8080
168+
```
169+
If nothing listens on your port, the viewer did not start or has exited.
170+
171+
3. **Inspect viewer logs**
172+
- When run by the bot, the viewer writes to:
173+
- `logs/web_viewer_stdout.log`
174+
- `logs/web_viewer_stderr.log`
175+
- Look for Python tracebacks, "Address already in use", or missing dependencies (e.g. Flask, flask-socketio).
176+
- Optional: run the viewer manually to see errors in the terminal:
177+
```bash
178+
cd /path/to/meshcore-bot
179+
python3 modules/web_viewer/app.py --config config.ini --host 0.0.0.0 --port 8080
180+
```
181+
182+
4. **Check integration startup**
183+
- Bot logs may show: `Web viewer integration failed: ...` or `Web viewer integration initialized`.
184+
- If integration failed, the viewer subprocess is never started; fix the error shown (e.g. invalid `host` or `port` in config).
185+
186+
5. **Firewall**
187+
- Many SBC images (e.g. Orange Pi, Armbian minimal) do **not** ship with a firewall; if `curl` to localhost works and `host = 0.0.0.0`, the blocker may be network (Wi‑Fi client isolation, different subnet, or router). Check from a device on the same LAN using `http://<PI_IP>:8080`.
188+
- If your system uses **ufw**:
189+
```bash
190+
sudo ufw status
191+
sudo ufw allow 8080/tcp
192+
sudo ufw reload
193+
```
194+
- If `ufw` is not installed (e.g. `sudo: ufw: command not found`), you may have no host firewall—that’s common on embedded images. To allow the port with **iptables** (often available when ufw is not):
195+
```bash
196+
sudo iptables -I INPUT -p tcp --dport 8080 -j ACCEPT
197+
```
198+
(Rules may not persist across reboots unless you use a persistence method for your distro.)
199+
- If you prefer ufw, install it (e.g. `sudo apt install ufw`) and use the ufw commands above.
200+
201+
6. **Test from the Pi first**
202+
```bash
203+
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/
204+
```
205+
If this returns `200`, the viewer is running and the issue is binding or firewall. If you use `host = 0.0.0.0`, then try from another device: `http://<PI_IP>:8080`.
206+
207+
7. **Standalone run (no bot)**
208+
- To rule out bot integration issues, start the viewer by itself (same config path so it finds the DB):
209+
```bash
210+
python3 modules/web_viewer/app.py --config config.ini --host 0.0.0.0 --port 8080
211+
```
212+
- If `restart_viewer.sh` is used, note it binds to `127.0.0.1` by default; for network access run the command above with `--host 0.0.0.0` or edit the script.
213+
150214
### Flask Not Found
151215
```bash
152-
pip3 install flask
216+
pip3 install flask flask-socketio
153217
```
154218
155219
### Database Not Found
@@ -158,7 +222,7 @@ pip3 install flask
158222
159223
### Port Already in Use
160224
- Change the port in `config.ini` or stop the conflicting service
161-
- Use `lsof -i :5000` to find what's using the port
225+
- Use `ss -tlnp | grep 8080` or `lsof -i :8080` (if available) to find what's using the port
162226
163227
### Permission Denied
164228
```bash

install-service.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,18 @@ copy_files_smart "$SCRIPT_DIR" "$INSTALL_DIR" || {
405405
exit 1
406406
}
407407

408+
# Write .version_info at install dir so web viewer and packet_capture show version after install
409+
if command -v git &>/dev/null && [ -d "$SCRIPT_DIR/.git" ]; then
410+
GIT_HASH="$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")"
411+
if VERSION="$(git -C "$SCRIPT_DIR" describe --exact-match HEAD 2>/dev/null)"; then
412+
INSTALLER_VER="$VERSION"
413+
else
414+
INSTALLER_VER="dev-${GIT_HASH}"
415+
fi
416+
printf '%s\n' "{\"installer_version\": \"${INSTALLER_VER}\", \"git_hash\": \"${GIT_HASH}\"}" > "$INSTALL_DIR/.version_info"
417+
print_success "Wrote version info (${INSTALLER_VER}) to $INSTALL_DIR/.version_info"
418+
fi
419+
408420
# If no config.ini in install dir, create it from config.ini.example
409421
if [ ! -f "$INSTALL_DIR/config.ini" ]; then
410422
if [ -f "$INSTALL_DIR/config.ini.example" ]; then

modules/commands/repeater_command.py

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,7 @@ async def _handle_purge(self, args: List[str]) -> str:
280280
# Force a complete refresh of contacts from device after purging
281281
self.logger.info("Forcing contact list refresh from device to ensure persistence...")
282282
try:
283-
from meshcore_cli.meshcore_cli import next_cmd
284-
await asyncio.wait_for(
285-
next_cmd(self.bot.meshcore, ["contacts"]),
286-
timeout=30.0
287-
)
283+
await self.bot.meshcore.commands.get_contacts()
288284
self.logger.info("Contact list refreshed from device")
289285
except Exception as e:
290286
self.logger.warning(f"Failed to refresh contact list: {e}")
@@ -314,21 +310,16 @@ async def _handle_purge(self, args: List[str]) -> str:
314310
# Final verification: Check if contacts were actually removed from device
315311
self.logger.info("Performing final verification of contact removal...")
316312
try:
317-
from meshcore_cli.meshcore_cli import next_cmd
318-
await asyncio.wait_for(
319-
next_cmd(self.bot.meshcore, ["contacts"]),
320-
timeout=30.0
321-
)
322-
313+
await self.bot.meshcore.commands.get_contacts()
314+
323315
# Count remaining repeaters on device
324-
remaining_repeaters = 0
325-
if hasattr(self.bot.meshcore, 'contacts'):
326-
for contact_key, contact_data in self.bot.meshcore.contacts.items():
327-
if self.bot.repeater_manager._is_repeater_device(contact_data):
328-
remaining_repeaters += 1
329-
316+
remaining_repeaters = sum(
317+
1 for contact_data in self.bot.meshcore.contacts.values()
318+
if self.bot.repeater_manager._is_repeater_device(contact_data)
319+
)
320+
330321
self.logger.info(f"Final verification: {remaining_repeaters} repeaters still on device")
331-
322+
332323
except Exception as e:
333324
self.logger.warning(f"Final verification failed: {e}")
334325

modules/core.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@
4343
from .utils import resolve_path
4444

4545

46+
class _DuplicateAwareConfigParser(configparser.ConfigParser):
47+
"""ConfigParser that allows duplicate options (last value wins) and logs ERROR when seen."""
48+
49+
def __init__(self, *args, **kwargs):
50+
kwargs['strict'] = False
51+
super().__init__(*args, **kwargs)
52+
53+
def _handle_option(self, st, line, fpname):
54+
if st.optname in st.options:
55+
logging.getLogger(__name__).error(
56+
"Duplicate option in config file: section [%s], option '%s' (file %s, line %s). Using last value.",
57+
st.sectname, st.optname, fpname, getattr(st, 'lineno', '?'),
58+
)
59+
st.options[st.optname] = st.optvalue
60+
61+
4662
class MeshCoreBot:
4763
"""MeshCore Bot using official meshcore package.
4864
@@ -52,7 +68,7 @@ class MeshCoreBot:
5268

5369
def __init__(self, config_file: str = "config.ini"):
5470
self.config_file = config_file
55-
self.config = configparser.ConfigParser()
71+
self.config = _DuplicateAwareConfigParser()
5672
self.load_config()
5773

5874
# Setup logging

0 commit comments

Comments
 (0)