2026-05-06 01:28:59 +00:00
# MeshAI
2026-07-04 00:47:11 -06:00
**An LLM-powered assistant for LoRa mesh networks — on Meshtastic * and * MeshCore, at the same time.**
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
MeshAI connects to your mesh, watches network health and the world around it in real time, answers questions over the air, and broadcasts the alerts that matter — weather, wildfire, road, seismic, RF, and mesh-health — with a full web dashboard to drive it all.
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
> ### 🤖 Built with AI ("vibecoded")
> In the interest of transparency: **MeshAI was vibecoded** — designed, built, debugged, and documented in close collaboration with LLM coding assistants. The architecture, most of the implementation, and this README were produced that way. It's a real, running project on a live mesh, but expect the pragmatic style, opinionated shortcuts, and occasional rough edges that come with the territory. Issues and PRs are welcome.
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00

2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
---
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
## Highlights
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
- **Dual-transport** — runs on **Meshtastic ** and **MeshCore ** simultaneously. Each mesh is first-class: independent connection, routing, and behavior, one shared brain.
- **Conversational bot** — DM it "how's the mesh?" or ask about weather, fires, roads, or a specific node, and get a data-driven answer over LoRa. The reply goes back on whichever mesh you asked from.
- **Per-mesh awareness** — it watches chat on each mesh separately (rolling short-term memory), so "what's happening on the mesh?" answers about * your * mesh. Private DMs stay private; curated knowledge stays separate.
- **Broadcast intelligence** — weather alerts, wildfire updates, road/traffic, seismic, RF/band conditions, and mesh-health notifications, formatted to fit LoRa and routed per-mesh, per-family.
- **Mesh health** — a 5-pillar health score with per-region breakdowns, infrastructure monitoring, coverage-gap analysis, and battery/solar tracking (Meshtastic).
- **Web dashboard** — a clean React UI to configure every transport, route every message type, watch a live activity feed, browse contacts, and tune the bot — no config-file spelunking required.
- **Knowledge base (RAG)** — optional hybrid retrieval over a large curated vector store for survival/comms/technical Q&A.
- **Multi-backend LLM** — Google Gemini, OpenAI, Anthropic, or any OpenAI-compatible local model (Ollama, LiteLLM, etc.).
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
---
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
## The dashboard
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
Everything is driven from the web UI, organized into **General ** , **Meshtastic ** , and **MeshCore ** sections — each mesh mirrors the other so there's nothing to relearn when you add the second transport.
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
**Live activity log** — every broadcast, on both meshes, with per-mesh badges and Sent/Skip status:
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00

2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
**Per-family routing** — decide exactly where each message type goes: broadcast vs. DM, which channel, which recipients — independently for each mesh:
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00

2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
**MeshCore contacts & companion** — the live roster from your MeshCore companion node, with names, types, last-heard, position, and optional telemetry polling:
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00

2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
**Data feeds** — turn environmental sources on/off and tune thresholds in one place:
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00

2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
**Nodes & health** — per-node infrastructure detail: battery, utilization, coverage, neighbors, hardware:
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00

2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
---
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
## Quick start
2026-05-12 21:02:17 -06:00
docs: point onboarding at the dashboard; drop 3 phantom keys from the seeded default (#147)
* fix(entrypoint): drop three phantom history keys from the seeded default
The config written on first boot -- what EVERY fresh Docker install starts
from -- seeded three keys that do not exist on HistoryConfig and are
silently discarded on load:
auto_cleanup: true
cleanup_interval_hours: 24
max_age_days: 30
HistoryConfig has only `database`, `max_messages_per_user`, and
`conversation_timeout`.
To be precise about the impact: history cleanup DOES work -- cleanup_expired()
is wired at main.py:237 and _prune_history() honours max_messages_per_user.
What never existed is the time-based retention model these keys describe (a
30-day age cutoff on a 24h interval). An operator setting max_age_days: 90
expecting 90-day retention was silently ignored.
Not implemented here -- whether time-based retention should exist is a
product decision, not a cleanup. This only stops the default config
promising it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: point onboarding at the dashboard, not the legacy example config
The documented setup path was `cp config.example.yaml config.yaml`, but that
file is ~40% incomplete: of the 20 top-level Config fields it covers 15,
omitting coverage (the universal bbox), danger_zones, generic_sources,
meshcore_context, commands, and the current notification model
(toggles/destinations/region_routes). Its own notifications header still says
the schema "will be replaced in v0.3 by the 8-toggle model" -- which shipped
long ago. Anyone following the project's own instructions landed on a
degraded surface with no signal a richer config existed.
It is also read by NOTHING at runtime: docker-entrypoint.sh sets
MESHAI_CONFIG=/data/config.yaml and writes its own inline default on first
boot. The Dockerfile still COPYs config.example.yaml into the image, so it is
kept and now labelled reference-only rather than a starting point.
- README: Docker quick-start seeds itself; configure via the dashboard. The
pip path still uses config.example.yaml (nothing seeds one there) but now
carries an honest note that it is a minimal bootstrap, not a reference.
Adds an Advanced section for the split /data/config/ layout and the
migrate_config_v03 path into it.
- docker-compose.yml: the comment claimed config lives at /data/config.yaml
as though that were the only layout; corrected to describe both, and note
secrets live in /data/secrets/.env.
- Dockerfile: document why config.example.yaml is still shipped.
Not done deliberately: config.example.yaml is NOT expanded to cover all 20
sections. A second hand-maintained schema is what caused this drift; the
dashboard is the authoritative surface. The legacy single-file loader is
untouched and still fully supported.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(readme): move dashboard-first onboarding rewrite to root README
work/README.md is about to become a symlink to root README.md on
docs/single-readme. Retarget the Docker/dashboard-first quick start
and config.example.yaml honesty note added in be867d23 from
work/README.md to root README.md, adapted to the root file's own
Quick start structure (cd meshai/work, /work/-prefixed curl URLs), so
none of it is silently dropped when that symlink lands.
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:07:31 -06:00
The dashboard is the primary way to configure MeshAI — connection, LLM backend, both transports, feeds, and routing. You shouldn't need to hand-edit YAML for a normal setup.
### Docker (recommended)
```bash
mkdir -p meshai/data && cd meshai
curl -O https://raw.githubusercontent.com/zvx-echo6/meshai/main/work/docker-compose.yml
docker compose up -d
```
On first boot MeshAI writes a minimal starter config into the `meshai_data` volume (`/data/config.yaml` , plus an empty `/data/secrets/.env` ) and starts the dashboard — nothing to pre-seed. Open * * `http://localhost:8080` ** and configure everything from there.
As you save settings, MeshAI persists them back into `/data` as focused per-domain YAML files (`llm.yaml` , `meshtastic.yaml` , `notifications.yaml` , `env_feeds.yaml` , …) alongside `config.yaml` — the same multi-file config system MeshAI uses in production; the dashboard is the intended way to drive it. API keys and other secrets you enter in the dashboard are written only to `/data/secrets/.env` , never into the YAML.
### From source (pip)
2026-07-04 00:47:11 -06:00
```bash
git clone https://github.com/zvx-echo6/meshai.git
chore: remove duplicate top-level source tree (build only from work/) (#125)
Commit 2e1fb325 ("refactor: move source tree into work/, multi-stage Docker
build") COPIED the source tree into work/ but never removed the top-level
originals, so main has been tracking two full copies of the app ever since.
Only work/ is live:
- CI (.github/workflows/docker-publish.yml, the sole workflow) builds with
`context: work` / `file: work/Dockerfile`. It never references the
top-level meshai/, Dockerfile, tests/, or pyproject.toml.
- Production (CT 108) runs work/docker-compose.yml, whose build context is
work/ and whose Dockerfile does `COPY meshai/ ./meshai/` relative to it.
- work/ is self-contained: its own Dockerfile, docker-compose.yml,
pyproject.toml, requirements.txt, config.example.yaml, dashboard-frontend/
and tests/.
The top-level copy carried ZERO unique content. work/ is strictly newer
everywhere (migrations v19-v28 vs top-level's v18; whole subsystems the top
level never had: coverage, generic_http, satpass, wzdx, notifications/gating,
notifications/formatters, transport/). Every file that existed ONLY at the top
level is legacy code that work/ deliberately removed after the fork:
- meshai/cli/configurator.py removed from work/ in 5de683f7 (#45, legacy TUI configurator)
- meshai/commands/subscribe.py removed from work/ in 9e6a3715 / 04604624 (subscription backend)
- meshai/subscriptions.py removed from work/ in 9e6a3715 / 04604624 (subscription backend)
- meshai/notifications/scheduled/fire_digest.py
removed from work/ in b0b0697b (#107, fire digest feature)
- meshai/notifications/pipeline/severity_router.py
never existed in work/; superseded by notifications/gating + formatters
- tests/test_fire_digest_recency.py tests the removed fire digest
- tests/test_meshcore_per_family_routing.py
tests the top-level-only meshcore_channel impl, superseded in work/ by transport/
- dashboard-frontend/src/pages/Alerts.tsx
removed from work/ in 9e6a3715 (subscription backend)
- dashboard-frontend/src/pages/Environment.tsx.bak
stray backup file, never in any build
The duplication is an ACTIVE HAZARD, not just dead weight: a fix applied to the
top-level meshai/ passes review, gets merged, and then silently does nothing,
because neither CI nor prod ever reads that tree. This already happened --
ff3ded8c (#9, "independent per-family MeshCore channel") landed its config.py /
connector.py / channels.py / dispatcher.py changes in the DEAD top-level copy
while work/ got only the transport-layer half. (Nothing was lost there: work/
carries a full, later-generation implementation of the same feature wired
through config.py, connector.py, transport/base.py, transport/composite_transport.py,
transport/meshcore_transport.py and the dashboard routes.) Deleting the copy
makes that class of mistake impossible.
Removed (280 files): meshai/, tests/, dashboard-frontend/, config/, Dockerfile,
docker-compose.yml, docker-entrypoint.sh, config.example.yaml, pyproject.toml,
requirements.txt.
Also fixes README Quick start, which pointed at the now-deleted root paths
(main/docker-compose.yml, main/config.example.yaml -> main/work/...) and still
advertised `meshai --config`, the interactive TUI removed from work/ in 5de683f7.
Verification: work/ test suite unchanged before and after --
20 failed, 2240 passed, 72 skipped (identical failure list; all pre-existing).
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:37:40 -06:00
cd meshai/work
2026-07-04 00:47:11 -06:00
pip install -e .
docs: point onboarding at the dashboard; drop 3 phantom keys from the seeded default (#147)
* fix(entrypoint): drop three phantom history keys from the seeded default
The config written on first boot -- what EVERY fresh Docker install starts
from -- seeded three keys that do not exist on HistoryConfig and are
silently discarded on load:
auto_cleanup: true
cleanup_interval_hours: 24
max_age_days: 30
HistoryConfig has only `database`, `max_messages_per_user`, and
`conversation_timeout`.
To be precise about the impact: history cleanup DOES work -- cleanup_expired()
is wired at main.py:237 and _prune_history() honours max_messages_per_user.
What never existed is the time-based retention model these keys describe (a
30-day age cutoff on a 24h interval). An operator setting max_age_days: 90
expecting 90-day retention was silently ignored.
Not implemented here -- whether time-based retention should exist is a
product decision, not a cleanup. This only stops the default config
promising it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: point onboarding at the dashboard, not the legacy example config
The documented setup path was `cp config.example.yaml config.yaml`, but that
file is ~40% incomplete: of the 20 top-level Config fields it covers 15,
omitting coverage (the universal bbox), danger_zones, generic_sources,
meshcore_context, commands, and the current notification model
(toggles/destinations/region_routes). Its own notifications header still says
the schema "will be replaced in v0.3 by the 8-toggle model" -- which shipped
long ago. Anyone following the project's own instructions landed on a
degraded surface with no signal a richer config existed.
It is also read by NOTHING at runtime: docker-entrypoint.sh sets
MESHAI_CONFIG=/data/config.yaml and writes its own inline default on first
boot. The Dockerfile still COPYs config.example.yaml into the image, so it is
kept and now labelled reference-only rather than a starting point.
- README: Docker quick-start seeds itself; configure via the dashboard. The
pip path still uses config.example.yaml (nothing seeds one there) but now
carries an honest note that it is a minimal bootstrap, not a reference.
Adds an Advanced section for the split /data/config/ layout and the
migrate_config_v03 path into it.
- docker-compose.yml: the comment claimed config lives at /data/config.yaml
as though that were the only layout; corrected to describe both, and note
secrets live in /data/secrets/.env.
- Dockerfile: document why config.example.yaml is still shipped.
Not done deliberately: config.example.yaml is NOT expanded to cover all 20
sections. A second hand-maintained schema is what caused this drift; the
dashboard is the authoritative surface. The legacy single-file loader is
untouched and still fully supported.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(readme): move dashboard-first onboarding rewrite to root README
work/README.md is about to become a symlink to root README.md on
docs/single-readme. Retarget the Docker/dashboard-first quick start
and config.example.yaml honesty note added in be867d23 from
work/README.md to root README.md, adapted to the root file's own
Quick start structure (cd meshai/work, /work/-prefixed curl URLs), so
none of it is silently dropped when that symlink lands.
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:07:31 -06:00
cp config.example.yaml config.yaml # minimal starting point, not exhaustive — see note below
2026-07-04 00:47:11 -06:00
meshai
2026-05-12 21:02:17 -06:00
```
docs: point onboarding at the dashboard; drop 3 phantom keys from the seeded default (#147)
* fix(entrypoint): drop three phantom history keys from the seeded default
The config written on first boot -- what EVERY fresh Docker install starts
from -- seeded three keys that do not exist on HistoryConfig and are
silently discarded on load:
auto_cleanup: true
cleanup_interval_hours: 24
max_age_days: 30
HistoryConfig has only `database`, `max_messages_per_user`, and
`conversation_timeout`.
To be precise about the impact: history cleanup DOES work -- cleanup_expired()
is wired at main.py:237 and _prune_history() honours max_messages_per_user.
What never existed is the time-based retention model these keys describe (a
30-day age cutoff on a 24h interval). An operator setting max_age_days: 90
expecting 90-day retention was silently ignored.
Not implemented here -- whether time-based retention should exist is a
product decision, not a cleanup. This only stops the default config
promising it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: point onboarding at the dashboard, not the legacy example config
The documented setup path was `cp config.example.yaml config.yaml`, but that
file is ~40% incomplete: of the 20 top-level Config fields it covers 15,
omitting coverage (the universal bbox), danger_zones, generic_sources,
meshcore_context, commands, and the current notification model
(toggles/destinations/region_routes). Its own notifications header still says
the schema "will be replaced in v0.3 by the 8-toggle model" -- which shipped
long ago. Anyone following the project's own instructions landed on a
degraded surface with no signal a richer config existed.
It is also read by NOTHING at runtime: docker-entrypoint.sh sets
MESHAI_CONFIG=/data/config.yaml and writes its own inline default on first
boot. The Dockerfile still COPYs config.example.yaml into the image, so it is
kept and now labelled reference-only rather than a starting point.
- README: Docker quick-start seeds itself; configure via the dashboard. The
pip path still uses config.example.yaml (nothing seeds one there) but now
carries an honest note that it is a minimal bootstrap, not a reference.
Adds an Advanced section for the split /data/config/ layout and the
migrate_config_v03 path into it.
- docker-compose.yml: the comment claimed config lives at /data/config.yaml
as though that were the only layout; corrected to describe both, and note
secrets live in /data/secrets/.env.
- Dockerfile: document why config.example.yaml is still shipped.
Not done deliberately: config.example.yaml is NOT expanded to cover all 20
sections. A second hand-maintained schema is what caused this drift; the
dashboard is the authoritative surface. The legacy single-file loader is
untouched and still fully supported.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(readme): move dashboard-first onboarding rewrite to root README
work/README.md is about to become a symlink to root README.md on
docs/single-readme. Retarget the Docker/dashboard-first quick start
and config.example.yaml honesty note added in be867d23 from
work/README.md to root README.md, adapted to the root file's own
Quick start structure (cd meshai/work, /work/-prefixed curl URLs), so
none of it is silently dropped when that symlink lands.
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:07:31 -06:00
Unlike Docker, `meshai` won't create a config file for you — it needs one to exist before it will start. `config.example.yaml` bootstraps the basics (connection, LLM backend, bot behavior); once it's running, open `http://localhost:8080` and use the dashboard for everything else.
> **Note on `config.example.yaml`:** it documents the legacy single-file schema and is no longer complete — it predates `coverage`, `danger_zones`, `generic_sources`, `meshcore_context`, `commands`, and the current 8-family notification-routing model (`notifications.toggles` / `destinations` / `region_routes`). The legacy single-file loader still works and is fully supported, but the dashboard — backed by the full config schema — is the authoritative way to reach every setting. Don't treat this file as a complete reference.
### Advanced: the split `/data/config/` layout
Production and multi-operator deployments typically move to a fully split config directory — `/data/config/config.yaml` plus one file per domain, `local.yaml` for operator-identifying values, and `!include` orchestration — instead of the single flat file above. MeshAI loads this layout automatically whenever it's present. To convert an existing single-file install:
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
```bash
docs: point onboarding at the dashboard; drop 3 phantom keys from the seeded default (#147)
* fix(entrypoint): drop three phantom history keys from the seeded default
The config written on first boot -- what EVERY fresh Docker install starts
from -- seeded three keys that do not exist on HistoryConfig and are
silently discarded on load:
auto_cleanup: true
cleanup_interval_hours: 24
max_age_days: 30
HistoryConfig has only `database`, `max_messages_per_user`, and
`conversation_timeout`.
To be precise about the impact: history cleanup DOES work -- cleanup_expired()
is wired at main.py:237 and _prune_history() honours max_messages_per_user.
What never existed is the time-based retention model these keys describe (a
30-day age cutoff on a 24h interval). An operator setting max_age_days: 90
expecting 90-day retention was silently ignored.
Not implemented here -- whether time-based retention should exist is a
product decision, not a cleanup. This only stops the default config
promising it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: point onboarding at the dashboard, not the legacy example config
The documented setup path was `cp config.example.yaml config.yaml`, but that
file is ~40% incomplete: of the 20 top-level Config fields it covers 15,
omitting coverage (the universal bbox), danger_zones, generic_sources,
meshcore_context, commands, and the current notification model
(toggles/destinations/region_routes). Its own notifications header still says
the schema "will be replaced in v0.3 by the 8-toggle model" -- which shipped
long ago. Anyone following the project's own instructions landed on a
degraded surface with no signal a richer config existed.
It is also read by NOTHING at runtime: docker-entrypoint.sh sets
MESHAI_CONFIG=/data/config.yaml and writes its own inline default on first
boot. The Dockerfile still COPYs config.example.yaml into the image, so it is
kept and now labelled reference-only rather than a starting point.
- README: Docker quick-start seeds itself; configure via the dashboard. The
pip path still uses config.example.yaml (nothing seeds one there) but now
carries an honest note that it is a minimal bootstrap, not a reference.
Adds an Advanced section for the split /data/config/ layout and the
migrate_config_v03 path into it.
- docker-compose.yml: the comment claimed config lives at /data/config.yaml
as though that were the only layout; corrected to describe both, and note
secrets live in /data/secrets/.env.
- Dockerfile: document why config.example.yaml is still shipped.
Not done deliberately: config.example.yaml is NOT expanded to cover all 20
sections. A second hand-maintained schema is what caused this drift; the
dashboard is the authoritative surface. The legacy single-file loader is
untouched and still fully supported.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(readme): move dashboard-first onboarding rewrite to root README
work/README.md is about to become a symlink to root README.md on
docs/single-readme. Retarget the Docker/dashboard-first quick start
and config.example.yaml honesty note added in be867d23 from
work/README.md to root README.md, adapted to the root file's own
Quick start structure (cd meshai/work, /work/-prefixed curl URLs), so
none of it is silently dropped when that symlink lands.
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:07:31 -06:00
docker compose exec meshai python -m meshai.scripts.migrate_config_v03
2026-05-12 21:02:17 -06:00
```
docs: point onboarding at the dashboard; drop 3 phantom keys from the seeded default (#147)
* fix(entrypoint): drop three phantom history keys from the seeded default
The config written on first boot -- what EVERY fresh Docker install starts
from -- seeded three keys that do not exist on HistoryConfig and are
silently discarded on load:
auto_cleanup: true
cleanup_interval_hours: 24
max_age_days: 30
HistoryConfig has only `database`, `max_messages_per_user`, and
`conversation_timeout`.
To be precise about the impact: history cleanup DOES work -- cleanup_expired()
is wired at main.py:237 and _prune_history() honours max_messages_per_user.
What never existed is the time-based retention model these keys describe (a
30-day age cutoff on a 24h interval). An operator setting max_age_days: 90
expecting 90-day retention was silently ignored.
Not implemented here -- whether time-based retention should exist is a
product decision, not a cleanup. This only stops the default config
promising it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: point onboarding at the dashboard, not the legacy example config
The documented setup path was `cp config.example.yaml config.yaml`, but that
file is ~40% incomplete: of the 20 top-level Config fields it covers 15,
omitting coverage (the universal bbox), danger_zones, generic_sources,
meshcore_context, commands, and the current notification model
(toggles/destinations/region_routes). Its own notifications header still says
the schema "will be replaced in v0.3 by the 8-toggle model" -- which shipped
long ago. Anyone following the project's own instructions landed on a
degraded surface with no signal a richer config existed.
It is also read by NOTHING at runtime: docker-entrypoint.sh sets
MESHAI_CONFIG=/data/config.yaml and writes its own inline default on first
boot. The Dockerfile still COPYs config.example.yaml into the image, so it is
kept and now labelled reference-only rather than a starting point.
- README: Docker quick-start seeds itself; configure via the dashboard. The
pip path still uses config.example.yaml (nothing seeds one there) but now
carries an honest note that it is a minimal bootstrap, not a reference.
Adds an Advanced section for the split /data/config/ layout and the
migrate_config_v03 path into it.
- docker-compose.yml: the comment claimed config lives at /data/config.yaml
as though that were the only layout; corrected to describe both, and note
secrets live in /data/secrets/.env.
- Dockerfile: document why config.example.yaml is still shipped.
Not done deliberately: config.example.yaml is NOT expanded to cover all 20
sections. A second hand-maintained schema is what caused this drift; the
dashboard is the authoritative surface. The legacy single-file loader is
untouched and still fully supported.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(readme): move dashboard-first onboarding rewrite to root README
work/README.md is about to become a symlink to root README.md on
docs/single-readme. Retarget the Docker/dashboard-first quick start
and config.example.yaml honesty note added in be867d23 from
work/README.md to root README.md, adapted to the root file's own
Quick start structure (cd meshai/work, /work/-prefixed curl URLs), so
none of it is silently dropped when that symlink lands.
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:07:31 -06:00
This backs up the original `config.yaml` , splits it into `/data/config/` , extracts secrets to `/data/secrets/.env` , and verifies the new layout loads identically before finishing — restart the container afterward to pick it up. It's optional: the dashboard is fully functional against either layout. (Templates for building a split layout from scratch also ship in the repo's `work/config/` directory: `local.yaml.example` , `.env.example` .)
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
---
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
## Transports
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
MeshAI speaks two mesh protocols. **Meshtastic is always the base transport. ** **MeshCore turns on automatically the moment you set a MeshCore host ** — there's no separate on/off toggle to forget.
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
### Meshtastic
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
Connect over TCP (recommended) or serial:
2026-05-12 21:02:17 -06:00
```yaml
2026-07-04 00:47:11 -06:00
connection:
type: "tcp" # or "serial"
tcp_host: "192.168.1.100"
tcp_port: 4403
# serial_port: "/dev/ttyUSB0"
2026-05-12 21:02:17 -06:00
```
2026-07-04 00:47:11 -06:00
### MeshCore
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
MeshAI attaches to a MeshCore **companion ** (the pyMC / MeshCore companion frame server) over TCP and acts as a node on the MeshCore mesh:
2026-05-12 21:02:17 -06:00
```yaml
2026-07-04 00:47:11 -06:00
connection:
meshcore_host: "192.168.1.253" # blank = MeshCore off
meshcore_port: 5050
2026-05-12 21:02:17 -06:00
```
2026-07-04 00:47:11 -06:00
Once connected, MeshCore gets its own **Connection ** , **Routing ** , **Scheduled Broadcasts ** , **Contacts & Companion ** , and **Danger Zones ** pages in the dashboard — the same capabilities as Meshtastic, using MeshCore's own idioms (channels by name, contacts by pubkey). Messages are sized to fit whichever mesh they go out on.
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
---
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
## The conversational bot
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
DM MeshAI on either mesh and it answers with the LLM, using live mesh data, environmental feeds, and (optionally) a knowledge base. A few things it's careful about:
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
- **Answers on the mesh you asked from.** A MeshCore DM gets a MeshCore reply; a Meshtastic DM gets a Meshtastic reply. Each mesh's "answer DMs" switch is independent.
- **Per-mesh chat memory.** It keeps a short rolling window of recent channel chatter * per mesh * (configurable retention, default 14 days) so "what's happening on the mesh?" reflects the mesh you're on. Ask about the other mesh by name to cross over.
- **Three separate lanes.** Shared channel context, your private DM history, and the curated knowledge base never bleed into each other.
- **LoRa-fit replies.** Responses are chunked to a per-mesh character budget with sentence-aware splitting and continuation prompts.
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
### Commands
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
Alongside natural-language questions, a set of `!` commands are available (all toggleable, so they can defer to another service like MeshMonitor):
2026-05-12 21:02:17 -06:00
2026-07-04 00:47:11 -06:00
| Category | Commands |
|----------|----------|
| Mesh | `!health` · `!mesh` · `!status` · `!region [name]` · `!neighbors [node]` |
| Weather / RF | `!wx-alerts` · `!solar` · `!hf` · `!satpass` |
| Fire | `!fire` · `!hotspots` · `!ignitions` |
| Hazards | `!avalanche` · `!roads` / `!traffic` · `!rivers` / `!gauges` |
| Utility | `!help` · `!clear` |
2026-05-12 21:02:17 -06:00
---
2026-07-04 00:47:11 -06:00
## Mesh intelligence (Meshtastic)
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
MeshAI continuously aggregates mesh data and computes a **5-pillar health score ** :
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
| Pillar | Weight | Measures |
|--------|--------|----------|
| Infrastructure | 30% | Router/repeater uptime |
| Utilization | 25% | Channel busyness / RF congestion |
| Coverage | 20% | How many monitoring sources see each node |
| Behavior | 15% | Traffic patterns (noisy/misconfigured nodes) |
| Power | 10% | Battery health of infrastructure nodes |
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
Infrastructure nodes are tracked individually (battery, offline alerts, coverage, neighbors, hardware); client nodes coming and going is normal and ignored. Regions are fully configurable — local names, aliases, cities, and radius — with no hardcoded geography.
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
Data comes from one or more **Meshview ** instances and a **MeshMonitor ** instance, polled on a staggered schedule with built-in rate-limiting:
2026-05-06 01:28:59 +00:00
```yaml
mesh_sources:
2026-07-04 00:47:11 -06:00
- name: "meshview"
2026-05-06 01:28:59 +00:00
type: meshview
url: "http://192.168.1.100:8080"
enabled: true
- name: "meshmonitor"
type: meshmonitor
url: "http://192.168.1.100:3333"
api_token: "your-bearer-token"
enabled: true
```
2026-07-04 00:47:11 -06:00
---
## Environmental & hazard feeds
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
MeshAI pulls real-time situational data and turns it into LoRa broadcasts and query answers. Sources include **NWS weather alerts ** , **NIFC wildfire perimeters ** , **NASA FIRMS satellite fire detections ** , **USGS earthquakes ** , **USGS stream gauges ** , **road/traffic (511 / TomTom) ** , **NOAA space weather ** , and **avalanche/RF-propagation ** feeds.
2026-05-06 16:29:03 +00:00
2026-07-04 00:47:11 -06:00
Everything is switched on/off and tuned from the dashboard's **Data Feeds ** page — enable a source, set thresholds and geography, and route its output per-mesh on the **Routing ** page. Broadcast wording is tightened to fit a single LoRa packet without dropping the important details (e.g. affected towns on a weather alert).
2026-05-06 16:29:03 +00:00
2026-07-04 00:56:27 -06:00
### Native adapters vs. Central
Each hazard feed can get its data one of two ways, chosen per-feed with a `feed_source` switch:
- **`native` ** — MeshAI fetches the source's public API **directly ** (api.weather.gov, NIFC, USGS, NOAA SWPC, NASA FIRMS, TomTom, 511, avalanche centers). Self-contained — no extra infrastructure. This is the default and the original data path.
- **`central` ** — MeshAI subscribes to **Central ** , a companion service that pre-aggregates the same hazard data and republishes it as a **NATS JetStream ** firehose, so many bots/nodes can share one set of upstream API calls and geo/severity filtering instead of each hammering the source APIs.
```yaml
environmental:
central:
enabled: true
url: "nats://central.echo6.mesh:4222" # NATS server (tailnet-gated, no auth)
durable: "meshai-consumer" # durable consumer name prefix
region: "us.id" # server-side subject filtering
connect_timeout: 10
nws: { feed_source: central } # this feed comes from Central …
fires: { feed_source: native } # … this one is fetched directly
# …one feed_source per hazard adapter
```
2026-07-04 01:15:45 -06:00
Native and Central are **mutually exclusive per feed ** — flip any adapter between them independently. Two special cases: * * `satpass` ** is Central-only (there's no native predictor), and * * `ducting` ** (VHF tropo) is native-only (no Central equivalent). MeshAI keeps running whether or not Central is up: a **runtime ** drop auto-reconnects (durable consumers resume where they left off), and a **startup ** outage is logged and retried in the background rather than blocking boot — the LLM bot, both transports, mesh-health, and any `native` feeds all come up regardless; only the Central-sourced hazard feeds wait for Central to return. Feeds set to `native` don't depend on Central at all.
2026-07-04 00:56:27 -06:00
2026-07-04 00:47:11 -06:00
---
2026-05-06 16:29:03 +00:00
2026-07-04 00:47:11 -06:00
## Knowledge base (RAG)
2026-05-06 16:29:03 +00:00
2026-07-04 00:47:11 -06:00
Optional hybrid retrieval for survival, comms, medical, and technical Q&A.
- **Primary** — queries a **Qdrant ** hybrid store (dense `bge-m3` + sparse, Reciprocal Rank Fusion) over a large curated vector set, via a networked TEI embedding service. Nothing is copied locally.
- **Fallback** — a local **SQLite ** knowledge base (FTS5 keyword + `bge-small-en-v1.5` vectors) if the vector service is unreachable.
2026-05-06 16:29:03 +00:00
```yaml
knowledge:
enabled: true
2026-07-04 00:47:11 -06:00
backend: auto # qdrant | sqlite | auto
2026-05-06 16:29:03 +00:00
qdrant_host: "192.168.1.150"
qdrant_port: 6333
qdrant_collection: "recon_knowledge_hybrid"
tei_host: "192.168.1.150"
tei_port: 8090
top_k: 5
```
2026-07-04 00:47:11 -06:00
The curated channel chatter your bot observes is used only as short-term * context * — it is never written into the knowledge base.
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
---
2026-05-06 16:29:03 +00:00
2026-07-04 00:47:11 -06:00
## LLM configuration
2026-05-06 01:28:59 +00:00
```yaml
llm:
2026-07-04 00:47:11 -06:00
backend: "google" # google | openai | anthropic
2026-05-06 01:28:59 +00:00
api_key: "your-api-key"
2026-07-04 00:47:11 -06:00
model: "gemini-2.5-flash"
2026-05-06 01:28:59 +00:00
```
2026-07-04 00:47:11 -06:00
Any OpenAI-compatible endpoint works for local models — point `base_url` at Ollama (`http://localhost:11434/v1` ), LiteLLM (`http://localhost:4000/v1` ), or Open WebUI.
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
---
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
## Architecture
2026-05-06 01:28:59 +00:00
```
2026-07-04 00:47:11 -06:00
┌─────────────────────────────┐
Meshtastic ────────▶│ │◀──────── MeshCore
(TCP / serial) │ CompositeTransport │ (companion / pyMC TCP)
│ per-mesh routing + sizing │
└──────────────┬──────────────┘
│
┌─────────────────────────────┼─────────────────────────────┐
▼ ▼ ▼
┌───────────┐ ┌─────────────────┐ ┌──────────────┐
│ Router │ │ Notification │ │ Mesh Data │
│ LLM / cmd │ │ Pipeline │ │ Store + │
│ DM gating │ │ weather · fire │ │ Health │
│ per-mesh │ │ road · seismic │ │ Engine │
│ context │ │ RF · mesh-health│ │ 5-pillar │
└─────┬─────┘ └────────┬─────────┘ └──────┬───────┘
│ │ │
┌────▼─────┐ ┌──────────────┐ │ ┌──────────────┐ │
│ LLM │ │ Knowledge │ │ │ Env / Central │◀─────┘
│ backend │ │ Qdrant/FTS5 │ │ │ feed adapters │
└──────────┘ └──────────────┘ ▼ └──────────────┘
┌──────────┐
│ Responder│ ACK-paced, LoRa-fit,
│ + Chunker│ routed per mesh
└──────────┘
│
Web Dashboard (React) ── configure everything
2026-05-06 01:28:59 +00:00
```
2026-07-04 00:47:11 -06:00
---
2026-05-06 01:28:59 +00:00
2026-07-04 00:47:11 -06:00
## Running as a service
2026-05-06 01:28:59 +00:00
```ini
# /etc/systemd/system/meshai.service
[Unit]
2026-07-04 00:47:11 -06:00
Description=MeshAI
2026-05-06 01:28:59 +00:00
After=network.target
[Service]
Type=simple
User=your-user
WorkingDirectory=/path/to/meshai
ExecStart=/usr/bin/python3 -m meshai
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
2026-07-04 00:47:11 -06:00
sudo systemctl enable --now meshai
2026-05-06 01:28:59 +00:00
```
2026-07-04 00:47:11 -06:00
Every deployment is designed to survive a reboot; the dashboard's connection settings drive both transports.
---
## Playing nice with other services
- **advBBS** — MeshAI coexists on the same Meshtastic node; BBS protocol traffic (sync, RAP, mail) is auto-filtered (`bot.filter_bbs_protocols: true` ).
- **MeshMonitor** — MeshAI reads MeshMonitor's auto-responder patterns to avoid duplicate replies, and uses its API as a mesh-intelligence data source.
- **MeshCore companion** — MeshAI attaches as its own companion identity so it can share the radio without evicting other companion clients.
---
2026-05-06 01:28:59 +00:00
## Acknowledgments
2026-07-04 00:47:11 -06:00
- [Meshtastic ](https://meshtastic.org/ ) — the mesh platform it started on
- [MeshCore ](https://meshcore.io/ ) & [pyMC ](https://github.com/rightup/pyMC_core ) — the second transport
- [MeshMonitor ](https://github.com/Yeraze/meshmonitor ) by Yeraze — monitoring integration & data source
- [advBBS ](https://github.com/zvx-echo6/advbbs ) — coexistence design
- [Qdrant ](https://github.com/qdrant/qdrant ) · [sqlite-vec ](https://github.com/asg017/sqlite-vec ) · [fastembed ](https://github.com/qdrant/fastembed ) — retrieval stack
- The LLM coding assistants that vibecoded most of this
2026-05-06 01:28:59 +00:00
## License
2026-07-04 00:47:11 -06:00
MIT
2026-05-06 01:28:59 +00:00
## Author
2026-07-04 00:47:11 -06:00
K7ZVX — matt@echo6 .co