https://github.com/mr-tbot/mesh-api
MESH-API (previously MESH-AI) — Off-Grid AI & API Router with over 30 API extensions for Meshtastic & MeshCore - Seamlessly connect LM Studio, Ollama, AI Providers , 3rd-party APIs, & Home Assistant to your LoRa mesh. Supports custom commands, Twilio SMS, Discord channel routing, & GPS emergency alerts via SMS, email, or Discord + SO MUCH MORE
https://github.com/mr-tbot/mesh-api
ai api chatbot chatgpt claude deepseek discord emergency home-assistant home-automation lm-studio lmstudio lora mesh meshcore meshtastic off-grid ollama openai prepper
Last synced: 2 days ago
JSON representation
MESH-API (previously MESH-AI) — Off-Grid AI & API Router with over 30 API extensions for Meshtastic & MeshCore - Seamlessly connect LM Studio, Ollama, AI Providers , 3rd-party APIs, & Home Assistant to your LoRa mesh. Supports custom commands, Twilio SMS, Discord channel routing, & GPS emergency alerts via SMS, email, or Discord + SO MUCH MORE
- Host: GitHub
- URL: https://github.com/mr-tbot/mesh-api
- Owner: mr-tbot
- License: gpl-3.0
- Created: 2025-02-12T02:04:23.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2026-06-10T04:29:10.000Z (3 days ago)
- Last Synced: 2026-06-10T05:05:23.418Z (3 days ago)
- Topics: ai, api, chatbot, chatgpt, claude, deepseek, discord, emergency, home-assistant, home-automation, lm-studio, lmstudio, lora, mesh, meshcore, meshtastic, off-grid, ollama, openai, prepper
- Language: Python
- Homepage: https://mr-tbot.com
- Size: 1.04 MB
- Stars: 151
- Watchers: 9
- Forks: 17
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# MESH-API v0.7.0 Beta — Initial Full MeshCore Support Is Here!
> ## 🎉 v0.7.0 Beta — Initial Full MeshCore Support
>
> **You can now run MESH-API with EITHER a Meshtastic node, a MeshCore node — or BOTH at the same time, with MESH-API handling cross-network routing between them.** MeshCore is no longer a half-baked bridge plugin; it is a **first-class, core-owned radio** on equal footing with Meshtastic. Slash commands, the AI assistant, and *every* extension now work across both networks, and the WebUI adapts to whichever radios you have connected.
>
> **⚠️ These features are widely untested and I am actively seeking community feedback on how to improve the implementation.** If you run a Meshtastic-only, MeshCore-only, or dual-radio setup, please tell me what works, what breaks, and what you'd like to see — open a [GitHub Issue](https://github.com/mr-tbot/mesh-api/issues) or start a discussion. Your real-world reports directly shape the MeshCore implementation going forward.
>
> Also new in v0.7.0: a built-in **MCP (Model Context Protocol) server** that turns MESH-API into an agentic backend for AI tools, and a **firmware/software update system** that detects your device and notifies you when a newer Meshtastic, MeshCore, or MESH-API version is available. See the dedicated sections below.
- **v0.7.2.5 Beta** — 🕸️ **Mouse-reactive mesh background.** The dashboard's plain black background is replaced with a dense, animated **"mesh" grid** — a field of points connected by lines that drift continuously and **warp toward your mouse** in real time, evoking a living mesh network behind the UI. The UI boxes are now slightly translucent (~18%) so the grid shows through behind the panels (inputs and controls stay fully opaque). Fully controllable from **UI Settings → Appearance**: toggle it **on/off**, adjust the **speed**, **line thickness**, and **color**. Renders on a single GPU-friendly canvas behind the panels.
- **v0.7.2.4 Beta** — ☁️ **MQTT node indicators + smaller traffic graph.** Nodes heard over **MQTT** (rather than direct RF) now show a teal **☁ MQTT** badge in the Available Nodes list and a distinct teal map marker (with an "☁ Heard via MQTT" note in the popup and a ☁ on the map label), so you can tell at a glance which nodes are coming in via MQTT. The Traffic Monitor graph is now **half the height** for a more compact dashboard.
- **v0.7.2.3 Beta** — 📊 **Traffic monitor upgrades.** The Traffic Monitor is now **full-width** (double-wide) and sits **above the Node Map / Send row** by default (still draggable, hideable, and reorderable like every other box). It now counts **all received packets** — position, telemetry, nodeinfo, routing, text, etc. — not just chat messages, via a dedicated all-packet radio hook. Added a **user-selectable time window** (1 min / 5 min / 15 min / 30 min / 1 hour / 6 hours), persisted per browser, with the graph re-bucketed so it stays readable at any length. `GET /api/traffic` gained `seconds` (up to 6 h) and `buckets` parameters.
- **v0.7.2.2 Beta** — 📊 **Traffic monitor + 🚨 emergency alerts + Hermes is now an extension.**
- **Real-time traffic monitor** — a new movable **📊 Traffic Monitor** box at the top of the dashboard draws live mesh radio activity as green→red bars (RX received / TX sent, last 60 s). Like every panel it can be dragged, hidden, or reordered. Backed by a new `GET /api/traffic` endpoint.
- **Emergency alert box** — when a node triggers `/emergency` (or `/911`), a large **flashing red box** appears in the masthead (between the logo and the toolbar, below the connection status). Click it to open a modal with the alert text and node information (name, ID, GPS/map link, reply). Alerts **must be cleared by the user** and persist until then.
- **Hermes moved out of core** — the Nous Research **Hermes** AI provider is now a bundled **extension** (`extensions/hermes`) instead of core code, keeping the core lean. It registers itself as the `hermes` AI provider automatically, so `ai_provider: "hermes"` and Channel-Agent `provider: hermes` keep working; configure it from the **🧩 Extensions** manager. Legacy `hermes_*` keys in the main config are imported automatically on first load.
- **v0.7.2.1 Beta** — 🧩 **All v0.7.x settings are now in the WebUI config form**, plus a channel-panel fix. The friendly **⚙️ Config** editor gained sections for **Multi-Radio** (`meshtastic_enabled`, `default_send_network`), the **MeshCore** radio, the **MCP Server**, **Firmware & Updates**, and the **Hermes** and **Home Assistant** AI providers — previously these v0.7.x blocks were only reachable via the Raw JSON tab. 🐛 Also hardened the **Channel Messages** panel so a single malformed message can no longer blank the entire panel (each channel/message now renders in isolation; channels sort numerically).
- **v0.7.2 Beta** — 🤖 **Channel Agents are now manageable from the WebUI.** A new **🤖 Agents** toolbar button opens a Channel Agents manager where you can assign any channel to a specific **AI provider** (OpenAI, Hermes, Ollama, Claude, …) or a loaded **extension** (e.g. OpenClaw), with an optional per-channel **PIN** gate. Assignments **apply live** (no restart) and persist to [config.json](config.json). Channels with an assigned agent now show a badge (🤖 provider / 🧩 extension) in the Channel Messages panel, and the legacy `home_assistant_channel_index` mapping is surfaced automatically. The `/api/channel_agents` endpoint gained a `POST` to save assignments.
- **v0.7.1 Beta** — 🐛 **Bug fix:** the WebUI Extensions Manager **Enable / Disable** buttons now update immediately. The toggle wrote the new state to the extension's `config.json` but the in-memory status returned by `/extensions/status` stayed stale, so the button label and status dot never changed (the change only appeared after a restart). The loader's in-memory state is now kept in sync, so toggling reflects instantly; use **Reload** (or restart) to apply it to the running extensions.
- **v0.7.0 Beta** — 🚀 **Multi-radio overhaul + MCP tool server + firmware updates.**
- **MeshCore is a first-class radio.** Run Meshtastic-only, MeshCore-only (fully standalone — no Meshtastic device required), or both with MESH-API as the man-in-the-middle. MeshCore connects over serial / TCP / BLE.
- **Cross-network routing & UI parity** — per-network connection banner, a node **network filter** with collapsible Meshtastic/MeshCore sections, network badges on nodes and messages, distinct map markers, MeshCore channels (group chats / private channels) in the send form, and DMs to MeshCore contacts — mirroring the Meshtastic experience.
- **MCP (Model Context Protocol) server** — external AI agents (Claude, Perplexity, Hermes, custom) can call MESH-API core functions **and** extensions as tools, driving the mesh networks as an agentic backend. `POST /mcp` (Streamable HTTP / JSON-RPC 2.0), disabled by default, bearer-token auth. See **[MCP Server](#mcp-server-model-context-protocol)** below.
- **Firmware & software updates** — detects the connected Meshtastic/MeshCore device, checks GitHub for newer Meshtastic firmware, MeshCore firmware, and MESH-API itself, and shows a 🔄 **Updates** notification with **stable / beta / alpha release-channel** selection per firmware. Optional one-click ESP32 flashing (off by default). See **[Firmware Updates](#firmware--software-updates)**.
- **Bug fixes:** **#59** (MeshCore-origin messages now reach plugins like Telegram and the AI) and **#58** (bounded Meshtastic (re)connect + an actually-running connection watchdog).
- New config blocks in [config.json](config.json): `meshtastic_enabled`, `default_send_network`, `meshcore`, `mcp`, and `firmware`.
- MeshCore requires the `meshcore` Python package (already in [requirements.txt](requirements.txt)): `pip install meshcore`. ESP32 firmware flashing additionally needs `esptool`.
- **v0.6.0** — Full release! Plugin-based extensions system with 30 built-in extensions, 12 AI providers, drop-in plugin architecture, interactive node map, collapsible channel views, draggable dashboard layout, and a fully revamped WebUI. **Docker images now available** for x86_64 and ARM64 (Raspberry Pi 4/5)!
> ### Community-Driven Improvements
>
> A massive amount of work has landed — the new plugin-based extensions system, 30+ extensions, OpenClaw AI agent integration, MeshCore bridging, and the full WebUI overhaul all shipped in a compressed timeline. **v0.6.0 includes critical bug fixes reported by the community** — thank you to everyone who filed issues and tested!
>
> **I am depending on the community to help test, identify, and crush bugs.** If something breaks, doesn't work as documented, or behaves unexpectedly — please open a [GitHub Issue](https://github.com/mr-tbot/mesh-api/issues) with as much detail as possible. Every report helps make this project better for everyone.
>
> If MESH-API is useful to you, please consider MAKING A DONATION — this project is built and maintained by one developer with the help of AI tools, and your support directly fuels continued development.
- PLEASE NOTE - There are new requirements and new config options - v0.6.0 updates many required library versions and brings us into alignment with the 2.7 branch of the Meshtastic Python library! Old configs should work out of the box - but there are new config flags and a new "description" feature for custom commands in commands_config.json. Read the changelogs.
## ❤️ Support MESH-API Development — Keep the Lights On
MESH-API is built and maintained by **one developer** with the help of AI tools. There is no corporate sponsor, no VC funding — just late nights, community feedback, and a passion for off-grid communication.
If MESH-API has been useful to you — whether you're running it on a Raspberry Pi in your go-bag, bridging your local mesh to Discord, or experimenting with AI on LoRa — **please consider making a donation.** Every contribution, no matter the size, directly fuels continued development, bug fixes, new extensions, and keeping this project free and open-source for everyone.
### Donate via PayPal (Preferred)
[](https://www.paypal.com/donate/?business=7DQWLBARMM3FE&no_recurring=0&item_name=Support+the+development+and+growth+of+innovative+MR_TBOT+projects.¤cy_code=USD)
[**Click here to donate via PayPal**](https://www.paypal.com/donate/?business=7DQWLBARMM3FE&no_recurring=0&item_name=Support+the+development+and+growth+of+innovative+MR_TBOT+projects.¤cy_code=USD)
### Crypto Donations
| Currency | Address |
|----------|---------|
| **BTC** | `bc1qalnp0xze5t9nner2754k2pj7yjhkrt3uzvzdvt` |
| **ETH** | `0xAd640c506f5d2368cAF420a117380820C0C5F61C` |
| **XRP** | `rpciwKrQSaRZ1UjPunH8vLJhoM2s4NaYoL` |
| **DOGE** | `DM79aRx58J6RYuWakHjiELWbNJkTTDj1cv` |
**Thank you to everyone who has donated, filed issues, tested pre-releases, and spread the word.** You are what makes this project possible. 🙏

**MESH-API** is an experimental project that bridges [Meshtastic](https://meshtastic.org/) (& now [MeshCore](https://meshcore.co.uk/) ) LoRa mesh networks with powerful AI chatbots and 3rd party APIs.
## What Sets MESH-API Apart?
Most projects in this space stop at being "AI chatbot integrations" — but **MESH-API is much more than that.**
- **Full Router / Mesh Operator**
MESH-API isn’t just talking to an LLM. It’s a **protocol bridge** and **mesh backbone**, designed to let LoRa networks, online (or offline) services, and APIs talk to each other in real time.
- **Not a One-Trick Pony**
Where other tools simply connect to AI, MESH-API is built to **route, translate, and post messages** between different systems and services — making it a true hub for both on-grid and off-grid communication.
- **Expandable by Design**
Any software with a working API can be integrated. That means you can merge in external services, dashboards, or automation platforms, extending the mesh far beyond its original scope.
- **AI-Powered Off-Grid Networks**
MESH-API provides the foundation for **self-sufficient LoRa mesh networks enhanced with AI**, ensuring communication, automation, and decision-making remain possible — even without the internet.
In short, MESH-API bridges the gap between **mesh services** and **online/locally hosted services**, making it a powerful backbone for resilient, intelligent LoRa networks.
> **Disclaimer:**
> This project is **NOT ASSOCIATED** with the official Meshtastic Project. It is provided solely as an extension to add AI and advanced features to your Mesh network.
> **v0.7.0 Beta — Initial Full MeshCore Support:**
> This release makes MeshCore a first-class radio and adds the MCP server and firmware-update system. **These features are widely untested in the field — I am actively seeking community feedback.** Run it with a Meshtastic node, a MeshCore node, or both, and please report what works and what breaks. Avoid relying on it for mission‑critical tasks or emergencies; always keep backup communication methods available and use responsibly.
>
> *I am one robot using other robots to write this code. Some features are still untested in the field. Check the GitHub issues for fixes or feedback!*
---
[](https://meshtastic.org)
The Meshtastic logo trademark is the trademark of Meshtastic LLC.
## Features
- **Multi-Radio: Meshtastic, MeshCore, or Both** *(New in v0.7.0)*
- **MeshCore is a first-class, core-owned radio** (not an extension) on equal footing with Meshtastic, connecting over serial / TCP / BLE.
- Run a **Meshtastic radio only**, a **MeshCore radio only** (fully standalone — no Meshtastic device required), or **one of each** with MESH-API as a cross-network man-in-the-middle.
- Slash commands, the AI assistant, and **every extension work across both networks**. Send to one network or both at once.
- **WebUI adapts** — per-network connection banner, node **network filter** with collapsible Meshtastic/MeshCore sections, network badges on nodes & messages, distinct map markers, MeshCore group/private channels in the send form, and DMs to MeshCore contacts.
- **MCP (Model Context Protocol) Server** *(New in v0.7.0)*
- Exposes MESH-API core functions **and** extensions as MCP **tools** at `POST /mcp`, so external AI agents (Claude, Perplexity, Hermes, custom) can drive the mesh as an agentic backend. Disabled by default; bearer-token auth. See **[MCP Server](#mcp-server-model-context-protocol)**.
- **Firmware & Software Updates** *(New in v0.7.0)*
- Detects your connected Meshtastic/MeshCore device and notifies you (🔄 Updates badge) when newer **Meshtastic firmware**, **MeshCore firmware**, or **MESH-API** is available, with **stable / beta / alpha** release-channel selection. Optional ESP32 over-USB flashing (off by default).
- **Plugin-Based Extensions System** *(New in v0.6.0)*
- 30 built-in extensions across 7 categories: Communication, Notifications, Emergency/Weather, Ham Radio/Off-Grid, Smart Home, Mesh Bridging, and AI Agents.
- Drop-in plugin architecture — add or remove extensions by copying a folder. No core code changes required.
- Extensions can register slash commands, react to emergencies, observe messages, expose HTTP endpoints, and run background services.
- **WebUI Extensions Manager** — view, enable/disable, and configure extensions from the dashboard.
- See the [Extensions Reference](#extensions-reference) section below for full details on all built-in extensions, or [Developing Custom Extensions](#developing-custom-extensions) to build your own.
- **Multiple AI Providers**
- Support for **Local** models (LM Studio, Ollama), **OpenAI**, **Claude**, **Gemini**, **Grok**, **OpenRouter**, **Groq**, **DeepSeek**, **Mistral**, **Hermes** (Nous Research), generic OpenAI-compatible endpoints, and **Home Assistant** integration.
- **Home Assistant Integration**
- Seamlessly forward messages from a designated channel to Home Assistant’s conversation API. Optionally secure the integration using a PIN.
- **NASA Space Weather Monitoring**
- Track geomagnetic storms, solar flares, coronal mass ejections, and more via NASA's DONKI API. Auto-broadcast significant events to the mesh with configurable Kp index and flare class thresholds. Slash commands: `/spaceweather`, `/solarflare`, `/geomagstorm`.
- **n8n Workflow Automation**
- Bidirectional bridge with [n8n](https://n8n.io) — forward mesh messages and emergencies to n8n webhook triggers, receive workflow outputs on the mesh, list active workflows, and trigger them via slash commands. Enables powerful no-code automation pipelines for your mesh network.
- **Advanced Slash Commands**
- Built‑in commands: suffixed `/about-XY`, `/help-XY`, `/motd-XY`, `/whereami-XY`, `/nodes-XY`, AI commands with your unique suffix (e.g., `/ai-XY`, `/bot-XY`, `/query-XY`, `/data-XY`), unsuffixed `/test`, and unsuffixed `/emergency` (or `/911`), plus custom commands via `commands_config.json`.
- Commands are now case‑insensitive for improved mobile usability.
- New: a per-install randomized alias (e.g. `/ai-9z`) is generated on first run to reduce collisions when multiple bots exist on the same mesh or MQTT network. You can change it in `config.json` (field `ai_command`). All AI commands require this suffix, and other built‑ins (except emergency/911) also require your suffix.
- Strongly encouraged: customize your commands in `commands_config.json` to avoid collisions with other users.
- **Emergency Alerts**
- Trigger alerts that are sent via **Twilio SMS**, **SMTP Email**, and, if enabled, **Discord**.
- Emergency notifications include GPS coordinates, UTC timestamps, and user messages.
- **Enhanced REST API & WebUI Dashboard**
- A modern three‑column layout showing direct messages, channel messages, and available nodes. Stacks on mobile; 3‑wide on desktop. Controls (⌘ Commands, 🧩 Extensions, ⚙️ Config, 📜 Logs) live in the “Send a Message” header (top‑right).
- **Interactive Node Map** — Leaflet.js‑powered map view with markers for all GPS‑enabled nodes. **25 tile providers** including OpenStreetMap, Carto (Light, Dark, Voyager, No‑Label variants), OpenTopoMap, Esri (Street, Satellite, Topo, NatGeo, Light/Dark Gray Canvas, Ocean), Stadia (Stamen Terrain/Toner/Toner Lite/Watercolor, Alidade Smooth/Dark, Outdoors, OSM Bright), Humanitarian OSM, CyclOSM, and OPNVKarte. Defaults to Carto Positron (Light). **Offline map image support** — upload a local map image with lat/lon bounds via settings; Leaflet overlays it as a fully functional map layer with markers, pan, and zoom. Offline detection with fallback notice. Popups include node name, custom name, favorite star, ID, last heard, hop count, DM/PING/PONG buttons, and Google Maps link — all on a single row. Mini DM box over the map includes Send, PING, and PONG on the same row.
- **Collapsible Channel Groups** — Each channel is a toggle‑able group with an unread‑count badge. Click the 📻 header to expand/collapse.
- **Draggable Dashboard** — All major sections (Send Form, Node Map, Message Panels, Discord) can be reordered via ☰ drag handles. The three message columns (DMs, Channels, Nodes) are also independently sortable. Layout order is saved to localStorage. Sections can be hidden/shown from the UI Settings panel.
- **Notification Sounds** — Five built‑in Web Audio API sounds (Two‑Tone Beep, Triple Chirp, Alert Chime, Sonar Ping, Radio Blip) plus a **custom sounds library** supporting multiple uploaded audio files (stored as base64 in localStorage). Separate sound selection for Default, DMs, Channels, and individual nodes — each with its own dropdown populated from built‑in plus all custom sounds. Test button, volume slider, and per‑node sound management in settings.
- **Node Enhancements** — Every node shows DM, PING, and PONG buttons, last‑heard time (📡), beacon time, hop count, distance, Show on Map (fly‑to), and Google Maps link on a single line. **Favorite nodes** — toggle a ⭐ star to pin nodes to the top of the Available Nodes list (persisted in localStorage). **Custom node names** — click ✏️ to assign a logical name displayed in cyan alongside the original shortName. Favorites and custom names also appear in map popup titles and tooltip labels.
- Emoji enhancements: each message has a React button that reveals a compact, hidden emoji picker; choosing an emoji auto‑sends a reaction (DM or channel). The send form includes a Quick Emoji bar that inserts emojis into your draft (does not auto‑send).
- Additional endpoints include `/messages`, `/nodes`, `/connection_status`, `/logs`, `/logs_stream`, `/send`, `/ui_send`, `/commands_info` (JSON commands list), and a new `/discord_webhook` for inbound Discord messages.
- UI customization through settings panel including button theme color, section colors, 25 map tile styles (default: Carto Light), offline map image upload with lat/lon bounds, hue rotation, notification sounds (built‑in or custom library with multiple files), per‑node sound assignments, section visibility toggles, and volume control. An About section with links to Meshtastic, MeshCore, and the project's GitHub and website.
- Config Editor (WebUI): Click the “Config” button in the header to view/edit `config.json`, `commands_config.json`, and `motd.json` in a tabbed editor. JSON is validated before saving; writes are atomic. Some changes may require a restart to take effect.
- Extensions Manager (WebUI): Click the “Extensions” button to view extension status, enable/disable extensions, edit extension configs, and hot‑reload all extensions — all from the browser.
- **Improved Message Chunking & Routing**
- Automatically splits long AI responses into configurable chunks with delays to reduce radio congestion.
- Configurable flags control whether the bot replies to broadcast channels and/or direct messages.
- New: LongFast (channel 0) response toggle — by default the bot will NOT respond on LongFast to avoid congestion. Enable `ai_respond_on_longfast` only if your local mesh agrees.
- Etiquette: Using AI bots on public LongFast is discouraged; keep it off unless you’re on an isolated/private mesh with community consent.
- **Robust Error Handling & Logging**
- Uses UTC‑based timestamps with an auto‑truncating script log file (keeping the last 100 lines if the file grows beyond 100 MB).
- Enhanced error detection (including specific OSError codes) and graceful reconnection using threaded exception hooks.
- **Discord Integration Enhancements**
- Route messages to and from Discord.
- New configuration options and a dedicated `/discord_webhook` endpoint allow for inbound Discord message processing.
- MQTT-aware response gating: set `respond_to_mqtt_messages` to true in `config.json` if you want the bot to respond to messages that arrive via MQTT. Off by default to prevent multiple server responses.
- User‑initiated only: The AI does not auto‑message or greet new nodes; it responds only to explicit user input.
- **Commands modal & startup helper**
- WebUI includes a Commands modal (button in the “Send a Message” header) that lists available commands with descriptions.
- The current alias suffix and a one‑line commands list are printed at startup for easy reference.
- **Windows & Linux Focused**
- Official support for Windows environments with installation guides; instructions for Linux available now - MacOS coming soon!
---

> An example of an awesome Raspberry Pi 5 powered mini terminal - running MESH-API & Ollama with HomeAssistant integration!
> - Top case model here by oinkers1: https://www.thingiverse.com/thing:6571150
> - Bottom Keyboard tray model here by mr_tbot: https://www.thingiverse.com/thing:7084222
> - Keyboard on Amazon here: https://a.co/d/2dAC9ph
---
## Quick Start (Windows)
1. **Prerequisites**
- **Python 3.11+** — Download from [python.org](https://www.python.org/downloads/). During install, check **“Add Python to PATH”**.
- **Git** (optional) — [git-scm.com](https://git-scm.com/downloads/win) for cloning, or download the ZIP from GitHub.
2. **Download/Clone**
- Clone the repository (or download and extract the ZIP):
```bash
git clone https://github.com/mr-tbot/mesh-api.git
cd mesh-api
```
3. **Create & Activate a Virtual Environment:**
```bash
python -m venv venv
.\venv\Scripts\activate
```
4. **Install Dependencies:**
```bash
pip install --upgrade pip
pip install -r requirements.txt
```
5. **Configure Files:**
- Edit `config.json`, `commands_config.json`, and `motd.json` as needed. Refer to the **Configuration** section below.
- Or use the **Setup Wizard** on first launch in the WebUI.
6. **Start the Bot:**
- Double-click `Run MESH-API - Windows.bat` or run:
```bash
python mesh-api.py
```
7. **Access the WebUI Dashboard:**
- Open your browser and navigate to [http://localhost:5000/dashboard](http://localhost:5000/dashboard).
---
## Quick Start (Ubuntu / Linux)
1. **Prerequisites**
- Python 3.11+ and pip:
```bash
sudo apt update && sudo apt install -y python3 python3-pip python3-venv git
```
- Grant serial port access (required for USB-connected Meshtastic devices):
```bash
sudo usermod -aG dialout $USER
```
Log out and back in for the group change to take effect.
2. **Download/Clone**
```bash
git clone https://github.com/mr-tbot/mesh-api.git
cd mesh-api
```
3. **Create & Activate a Virtual Environment:**
```bash
python3 -m venv venv
source venv/bin/activate
```
4. **Install Dependencies:**
```bash
pip install --upgrade pip
pip install -r requirements.txt
```
5. **Configure Files:**
- Edit `config.json`, `commands_config.json`, and `motd.json` as needed. Refer to the **Configuration** section below.
- Or use the **Setup Wizard** on first launch in the WebUI.
6. **Start the Bot:**
```bash
python mesh-api.py
```
7. **Access the WebUI Dashboard:**
- Open your browser and navigate to [http://localhost:5000/dashboard](http://localhost:5000/dashboard).
## Quick Start (Docker)
Multi-arch Docker images are published for **linux/amd64** (x86_64) and **linux/arm64** (Raspberry Pi 4/5, Apple Silicon).
1. **Prerequisites**
- [Docker Engine](https://docs.docker.com/engine/install/) (or Docker Desktop) installed on your host.
- A Meshtastic device connected via USB serial, Wi-Fi, or Bluetooth.
2. **Prepare the Volume Structure**
- The `docker-required-volumes/` folder in the repository contains a ready-to-use `mesh-api/` directory with default configs, all 30 built-in extensions, and empty log files. Copy it to your working directory:
```bash
git clone https://github.com/mr-tbot/mesh-api.git
cd mesh-api
cp -r docker-required-volumes/mesh-api ./mesh-api
```
- Edit the config files inside `mesh-api/config/` before starting:
```
mesh-api/
├── config/
│ ├── config.json # Core configuration (AI provider, connection, etc.)
│ ├── commands_config.json # Custom slash commands
│ └── motd.json # Message of the Day
├── extensions/ # All 30 built-in extensions (add your own here too)
│ ├── __init__.py
│ ├── base_extension.py
│ ├── loader.py
│ ├── discord/
│ ├── telegram/
│ ├── mqtt/
│ └── ... (30 built-in extensions)
└── logs/
├── script.log
├── messages.log
└── messages_archive.json
```
3. **Pull & Run with Docker Compose**
- Copy the included `docker-compose.yml` to the same directory as your `mesh-api/` folder, then:
```bash
docker compose pull
docker compose up -d
```
- **USB Serial devices:** Uncomment the `devices` and `/dev` volume lines in `docker-compose.yml` and set your serial device path (e.g. `/dev/ttyUSB0` or `/dev/ttyACM0`).
- **Wi-Fi connection:** Set `use_wifi: true` and `wifi_host` in `mesh-api/config/config.json` — no device passthrough needed.
4. **Verify the Container:**
```bash
docker compose logs -f mesh-api
```
5. **Access the WebUI Dashboard:**
- Open your browser and navigate to [http://localhost:5000/dashboard](http://localhost:5000/dashboard).
- On first launch the **Setup Wizard** will guide you through initial configuration.
> **Tip:** To add custom extensions, drop the extension folder into `mesh-api/extensions/` on the host — it's volume-mounted into the container so no rebuild is needed. Restart the container with `docker compose restart` to pick up new extensions.
---
## Supported Mesh Networks
MESH-API supports **two mesh radio platforms** that can operate independently or be **bridged together** for cross-network communication.
### Meshtastic (Primary)
[Meshtastic](https://meshtastic.org/) is MESH-API's primary mesh network. Connection is handled automatically by the core — just plug in your Meshtastic device and configure the connection method in `config.json`.
| Setting | Description |
|---------|-------------|
| `use_wifi` | Set `true` to connect via TCP/WiFi instead of USB serial |
| `wifi_host` | IP address of your Meshtastic node (when using WiFi) |
| `wifi_port` | TCP port (default `4403`) |
| `serial_port` | USB serial port (e.g. `/dev/ttyUSB0` or `COM3`) — leave empty for auto-detect |
| `serial_baud` | Baud rate (default `460800`) |
| `use_mesh_interface` | Set `true` for direct MeshInterface mode (no serial/WiFi) |
**All MESH-API features** — AI commands, slash commands, emergency alerts, extensions, WebUI dashboard — work natively over the Meshtastic connection.
### MeshCore (First-Class Radio — v0.7.0)
[MeshCore](https://meshcore.co.uk/) is a lightweight, multi-hop LoRa mesh firmware focused on embedded packet routing. **As of v0.7.0, MeshCore is a first-class radio owned by the MESH-API core** (`meshcore_core.py`) — *not* an extension. It connects directly to a MeshCore companion-firmware device over **USB serial, TCP, or BLE**, and its inbound traffic flows through the **same network-agnostic pipeline** as Meshtastic, so commands, AI, and every extension work on it natively.
> ⬆️ **Upgrading from an earlier build?** The old `extensions/meshcore` bridge plugin is **deprecated** and automatically defers to the core when the core MeshCore radio is enabled — you do not need (and should not use) the extension anymore. Move your settings into the `meshcore` block of `config.json` (see below).
You can run **one Meshtastic radio, one MeshCore radio, or one of each.** With both connected, MESH-API acts as the man-in-the-middle, optionally bridging chat between the two networks. See **[Running With Meshtastic, MeshCore, or Both](#running-with-meshtastic-meshcore-or-both-v070)** just below for the full configuration, topologies, and WebUI behavior.
**Quick start:**
1. `pip install meshcore` *(already in `requirements.txt`)*.
2. Flash a companion device with MeshCore **Companion** firmware (USB‑serial, BLE, or TCP variant) from [https://flasher.meshcore.co.uk](https://flasher.meshcore.co.uk).
3. Enable and configure the radio in the **`meshcore`** block of [config.json](config.json) (set `enabled: true`, pick `connection_type`, and the port/host/address).
4. Restart MESH-API. The 🟣 MeshCore status appears in the connection banner, MeshCore nodes show on the harmonized map, and you can send to MeshCore (or both networks) from the dashboard.
---

The latest v0.6.0 Web-UI revamp! NEW MAPS FEATURES AND TONS OF NEW GOODIES!
---
## Running With Meshtastic, MeshCore, or Both (v0.7.0)
MESH-API v0.7.0 treats **MeshCore as a first-class radio** managed by the core
(`meshcore_core.py`), feeding inbound messages through the *same* network-agnostic
pipeline as Meshtastic. You can run any of three topologies:
| Topology | How |
|----------|-----|
| **Meshtastic only** (classic) | Leave `meshcore.enabled: false`. Nothing changes. |
| **MeshCore only** (standalone) | Set `meshtastic_enabled: false` and `meshcore.enabled: true`. No Meshtastic device required. |
| **Both** (cross-network router) | `meshtastic_enabled: true` + `meshcore.enabled: true`. MESH-API bridges traffic between the two networks. |
Configure MeshCore in the `meshcore` block of [config.json](config.json):
```jsonc
"meshtastic_enabled": true, // set false to run MeshCore-only
"default_send_network": "auto", // auto | meshtastic | meshcore | both
"meshcore": {
"enabled": true,
"connection_type": "serial", // serial | tcp | ble
"serial_port": "/dev/ttyUSB1", // or tcp_host/tcp_port, or ble_address
"serial_baud": 115200,
"bridge_enabled": true, // mirror chat between networks when both present
"bridge_meshcore_channel_to_meshtastic_channel": { "0": 0 },
"bridge_meshtastic_channel_to_meshcore_channel": { "0": 0 }
}
```
**What works across both networks:** slash commands, the AI assistant, every
extension/plugin, the harmonized node map, DMs, channel/group messaging, and
emergency broadcasts. The WebUI adapts automatically — a per-network connection
banner, a node **network filter** with collapsible Meshtastic/MeshCore sections,
network badges (📡 MT / 🟣 MC) on nodes and messages, distinct map markers, and a
**Network** selector in the send form (Auto / Meshtastic / MeshCore / Both) that
appears when both radios are present.
> ⚠️ MeshCore support is brand new and widely untested — please report your
> experience (any topology) on [GitHub](https://github.com/mr-tbot/mesh-api/issues).
---
## MCP Server (Model Context Protocol)
v0.7.0 ships a built-in **MCP server** that exposes MESH-API's core functions
**and** its extensions as callable *tools*, so external AI agents and services —
**Claude, Perplexity, Hermes, custom agents, OpenClaw, etc.** — can drive your
Meshtastic and/or MeshCore networks as an **agentic backend**. This enables
advanced workflows: an agent can read the mesh, decide, and act (send messages,
run commands, trigger extensions) over LoRa.
### How it works
- **Transport:** Streamable HTTP / JSON-RPC 2.0 at a single endpoint, `POST /mcp`.
Implemented directly in Flask (no async/uvicorn dependency — runs fine on a Pi
Zero). Compatible with MCP clients that speak HTTP directly, or with stdio-only
clients (e.g. Claude Desktop) via the [`mcp-remote`](https://github.com/geelen/mcp-remote) bridge.
- **Disabled by default.** Enable it in the `mcp` block of [config.json](config.json):
```jsonc
"mcp": {
"enabled": true,
"require_auth": true, // bearer token (auto-generated + printed on first start)
"auth_token": "", // leave blank to auto-generate; also accepted as X-API-Key
"allowed_origins": ["*"], // DNS-rebinding allowlist
"allow_emergency": false, // gate the emergency tool
"rate_limit_per_min": 120
}
```
On first start with auth enabled, a token like `mesh-mcp-XXXX` is generated,
saved to config, and printed to the log. Pass it as `Authorization: Bearer `.
### Connecting a client
Point any MCP client at `http://:5000/mcp` with the bearer token. For
Claude Desktop / stdio clients:
```jsonc
{
"mcpServers": {
"mesh-api": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://:5000/mcp",
"--header", "Authorization: Bearer mesh-mcp-YOURTOKEN"]
}
}
}
```
### Built-in core tools
| Tool | Purpose |
|------|---------|
| `mesh_send_message` | Send to a network (meshtastic / meshcore / both / auto), broadcast or DM |
| `mesh_list_nodes` | List nodes across both networks (filterable) |
| `mesh_get_messages` | Read the recent mesh chat log |
| `mesh_network_status` | Status of both radios |
| `mesh_list_channels` | Meshtastic + MeshCore channels |
| `mesh_ai_query` | Ask the configured AI provider |
| `mesh_list_commands` / `mesh_run_command` | List / run any slash command |
| `meshcore_list_contacts` | MeshCore DM targets |
| `mesh_send_emergency` | Emergency broadcast (gated by `allow_emergency`) |
### Extensions as MCP tools
**Every loaded extension's slash command is auto-exposed** as an `ext_cmd_`
tool — no work required. Extensions can additionally provide **richer, typed
tools** by implementing two optional, duck-typed methods (no base-class change):
```python
def get_mcp_tools(self) -> list[dict]:
return [{
"name": "lookup_city", # exposed as ext__lookup_city
"description": "Look up weather for a city",
"inputSchema": {"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]},
}]
def call_mcp_tool(self, name: str, arguments: dict) -> str:
if name == "lookup_city":
return self._weather_for(arguments.get("city", ""))
return f"Unknown tool: {name}"
```
The tool list is rebuilt on every `tools/list`, so enabling/reloading an
extension surfaces its tools without restarting MESH-API. See
[DEVELOPING_EXTENSIONS.md](DEVELOPING_EXTENSIONS.md#mcp-tools-v070) for details.
### Security
Tool calls can send mesh traffic and trigger actions, so the server validates
inputs, requires a bearer token by default, validates the `Origin` header,
rate-limits calls, clamps output size, and gates the emergency tool. Treat the
token like a password. There should always be a human in the loop for sensitive
operations.
---
## Firmware & Software Updates
v0.7.0 adds a comprehensive, **safe-by-default** update system (`firmware_updater.py`):
- **Detection** — identifies the connected Meshtastic device (hardware model +
firmware variant `pioEnv` + version) and the MeshCore device (model + version).
- **Update notifications** — a 🔄 **Updates** button in the WebUI masthead shows a
badge when a newer **Meshtastic firmware**, **MeshCore firmware**, or **MESH-API**
release is available (checked against GitHub on a periodic timer + on demand).
- **Release channels** — pick **stable / beta / alpha** independently for
Meshtastic and MeshCore firmware. Beta/alpha track GitHub pre-releases; the
WebUI shows the newest matching version for the channel you choose. (Your device
is not always a Heltec V3 — detection adapts to whatever is connected.)
- **Flashing (optional, off by default)** — for ESP32-class Meshtastic devices,
the latest firmware can be downloaded and flashed over USB serial via `esptool`.
nRF52/UF2 devices and MeshCore companions fall back to guided
[web-flasher](https://flasher.meshtastic.org/) instructions (unattended flashing
there is unsafe). Enable with `firmware.allow_flashing: true`; optional
`firmware.auto_update: true` flashes ESP32 Meshtastic devices unattended.
```jsonc
"firmware": {
"auto_check": true,
"check_interval_sec": 86400,
"allow_flashing": false, // master gate for flashing
"auto_update": false, // never flashes unattended unless true
"meshtastic_channel": "stable", // stable | beta | alpha
"meshcore_channel": "stable",
"meshtastic_fw_repo": "meshtastic/firmware",
"meshcore_fw_repo": "meshcore-dev/MeshCore",
"mesh_api_repo": "mr-tbot/mesh-api"
}
```
> ⚠️ Flashing can brick a radio. Always keep a backup device, and prefer the web
> flasher unless you understand the risks. The radio goes offline during flashing.
---
## Channel Agents — Assign a Channel to an Agent
v0.7.0 generalizes the old Home Assistant per-channel routing into **Channel
Agents**: assign any mesh channel to a specific agent, and all plain-text
(non-command) traffic on that channel is handled by it. Use it to dedicate a
channel to **OpenClaw**, **Hermes**, **Home Assistant**, or any AI provider —
on either Meshtastic or MeshCore.
Configure it in the top-level `channel_agents` block of [config.json](config.json),
mapping a channel index (as seen by MESH-API) to an agent spec:
```jsonc
"channel_agents": {
"6": { "agent": "ai", "provider": "hermes" }, // ch6 → Hermes
"7": { "agent": "extension", "slug": "openclaw" }, // ch7 → OpenClaw agent
"5": { "agent": "ai", "provider": "home_assistant" }, // ch5 → Home Assistant
"8": { "agent": "ai", "provider": "openai", "require_pin": true }
}
```
- **`agent: "ai"`** routes the channel to a named AI provider (`openai`, `hermes`,
`ollama`, `claude`, `home_assistant`, etc.) — independent of the global
`ai_provider`, so different channels can use different models.
- **`agent: "extension"`** routes to a loaded extension (e.g. `openclaw`) via its
`handle_channel_message()` hook, falling back to `get_ai_response()` or a
configured `command`. See [DEVELOPING_EXTENSIONS.md](DEVELOPING_EXTENSIONS.md#channel-agents-v070).
- **`require_pin: true`** gates the channel behind a `PIN=XXXX` prefix (like the
Home Assistant secure mode).
- Channels with an assigned agent **always respond**, bypassing the global
`reply_in_channels` setting. Slash commands still work normally there.
- The legacy `home_assistant_channel_index` setting continues to work and is
surfaced via `GET /api/channel_agents`.
> Channel indices differ between physical MeshCore devices — assign the agent to
> the index **as MESH-API receives it** (visible in the logs / message panel).
---
## Extensions Reference
> **Note:** The extensions system and all corresponding extensions are **new and largely untested**. Please report any issues on [GitHub](https://github.com/mr-tbot/mesh-api/issues) so they may be investigated and addressed.
Complete reference for all built-in extensions included with MESH-API.
Each extension is a self-contained plugin in the `extensions/` directory with its own `config.json`, `extension.py`, and `__init__.py`.
**Quick start:** Enable any extension by setting `"enabled": true` in its `config.json` file and restarting MESH-API.
---
### Extensions Table of Contents
- **[Communication Extensions](#communication-extensions):** [Discord](#discord) · [Slack](#slack) · [Telegram](#telegram) · [Matrix](#matrix) · [Signal](#signal) · [WhatsApp](#whatsapp) · [Mattermost](#mattermost) · [Zello](#zello) · [MQTT (Extension)](#mqtt-extension) · [Webhook Generic](#webhook-generic) · [IMAP](#imap) · [Mastodon](#mastodon) · [n8n](#n8n)
- **[Notification Extensions](#notification-extensions):** [Apprise](#apprise) · [Ntfy](#ntfy) · [Pushover](#pushover) · [PagerDuty](#pagerduty) · [OpsGenie](#opsgenie)
- **[Emergency & Weather Extensions](#emergency--weather-extensions):** [NWS Alerts](#nws-alerts) · [OpenWeatherMap](#openweathermap) · [USGS Earthquakes](#usgs-earthquakes) · [GDACS](#gdacs) · [Amber Alerts](#amber-alerts) · [NASA Space Weather](#nasa-space-weather)
- **[Ham Radio & Off-Grid Extensions](#ham-radio--off-grid-extensions):** [Winlink](#winlink) · [APRS](#aprs) · [BBS](#bbs)
- **[Smart Home Extensions](#smart-home-extensions):** [Home Assistant (Extension)](#home-assistant-extension)
- **[Mesh Bridging Extensions](#mesh-bridging-extensions):** [MeshCore (deprecated — now a core radio)](#meshcore)
- **[AI Agent Extensions](#ai-agent-extensions):** [OpenClaw](#openclaw)
---
### Communication Extensions
#### Discord
Bidirectional bridge between Meshtastic mesh and a Discord channel.
**Commands:**
| Command | Description |
|---------|-------------|
| `/discord` | Show Discord integration status |
**Config (`extensions/discord/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `webhook_url` | string | `""` | Discord webhook URL for outbound messages |
| `bot_token` | string | `""` | Bot token for reading Discord messages |
| `channel_id` | string | `""` | Discord channel ID to bridge |
| `poll_interval_seconds` | int | `5` | How often to poll for new Discord messages |
| `forward_to_mesh` | bool | `true` | Forward Discord messages to mesh |
| `mesh_channel_index` | int | `0` | Mesh channel to bridge |
| `bot_name` | string | `"MESH-API"` | Display name for webhook posts |
**Hooks:** `on_message` (forwards mesh→Discord), `on_emergency` (posts alerts), Flask route `/discord_webhook`.
---
#### Slack
Bidirectional Slack integration using Bot API and incoming webhooks.
**Commands:**
| Command | Description |
|---------|-------------|
| `/slack` | Show Slack integration status |
**Config (`extensions/slack/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `bot_token` | string | `""` | Slack Bot User OAuth Token (`xoxb-...`) |
| `webhook_url` | string | `""` | Incoming webhook URL for outbound messages |
| `channel_id` | string | `""` | Slack channel ID to bridge |
| `poll_interval_seconds` | int | `10` | Polling interval for new messages |
| `forward_to_mesh` | bool | `true` | Forward Slack messages to mesh |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `bot_name` | string | `"MESH-API"` | Bot display name |
**Hooks:** `on_message`, `on_emergency`.
---
#### Telegram
Bidirectional Telegram bot bridge using the Bot API with `getUpdates` long-polling.
**Commands:**
| Command | Description |
|---------|-------------|
| `/telegram` | Show Telegram bot status |
**Config (`extensions/telegram/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `bot_token` | string | `""` | Telegram Bot API token from @BotFather |
| `chat_id` | string | `""` | Target chat/group/channel ID |
| `poll_interval_seconds` | int | `5` | Polling interval |
| `forward_to_mesh` | bool | `true` | Forward Telegram→mesh |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `parse_mode` | string | `"HTML"` | Telegram parse mode |
**Hooks:** `on_message`, `on_emergency`.
---
#### Matrix
Bidirectional Matrix (Element) bridge using the Client-Server API.
**Commands:**
| Command | Description |
|---------|-------------|
| `/matrix` | Show Matrix connection status |
**Config (`extensions/matrix/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `homeserver_url` | string | `""` | Matrix homeserver (e.g. `https://matrix.org`) |
| `access_token` | string | `""` | Matrix access token |
| `room_id` | string | `""` | Room ID to bridge (`!abc:matrix.org`) |
| `user_id` | string | `""` | Bot user ID (`@bot:matrix.org`) |
| `poll_interval_seconds` | int | `5` | Sync polling interval |
| `forward_to_mesh` | bool | `true` | Forward Matrix→mesh |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
**Hooks:** `on_message`, `on_emergency`.
---
#### Signal
Bidirectional Signal bridge using the signal-cli-rest-api.
**Commands:**
| Command | Description |
|---------|-------------|
| `/signal` | Show Signal integration status |
**Config (`extensions/signal/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `signal_api_url` | string | `"http://localhost:8080"` | signal-cli REST API URL |
| `phone_number` | string | `""` | Registered Signal phone number |
| `recipient` | string | `""` | Recipient number or group ID |
| `poll_interval_seconds` | int | `5` | Polling interval |
| `forward_to_mesh` | bool | `true` | Forward Signal→mesh |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
**Hooks:** `on_message`, `on_emergency`.
---
#### WhatsApp
Bidirectional WhatsApp bridge using the Meta WhatsApp Business Cloud API. Outbound messages are sent via the Cloud API; inbound messages are received via a webhook endpoint registered with Meta.
**Commands:**
| Command | Description |
|---------|-------------|
| `/whatsapp` | Show WhatsApp integration status |
**Config (`extensions/whatsapp/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `api_url` | string | `"https://graph.facebook.com/v21.0"` | WhatsApp Cloud API base URL |
| `phone_number_id` | string | `""` | WhatsApp Business phone number ID |
| `access_token` | string | `""` | System User permanent token |
| `verify_token` | string | `""` | Webhook verification token (you choose this) |
| `recipient_number` | string | `""` | Default recipient in E.164 format (e.g. `+15551234567`) |
| `send_emergency` | bool | `false` | Forward emergency alerts to WhatsApp |
| `send_ai` | bool | `false` | Forward AI responses to WhatsApp |
| `send_all` | bool | `false` | Forward all mesh messages to WhatsApp |
| `receive_enabled` | bool | `true` | Accept inbound WhatsApp messages |
| `inbound_channel_index` | int\|null | `null` | Mesh channel filter for outbound |
| `webhook_path` | string | `"/whatsapp/webhook"` | Flask endpoint for Meta webhook |
| `broadcast_channel_index` | int | `0` | Mesh channel for inbound messages |
| `bot_name` | string | `"MESH-API"` | Bot display name |
**API Endpoints:**
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/whatsapp/webhook` | GET | Meta webhook verification (hub challenge) |
| `/whatsapp/webhook` | POST | Receive inbound WhatsApp messages |
**Hooks:** `on_message`, `on_emergency`, Flask routes for inbound webhook.
---
#### Mattermost
Bidirectional Mattermost bridge using REST API + incoming webhook.
**Commands:**
| Command | Description |
|---------|-------------|
| `/mattermost` | Show Mattermost integration status |
**Config (`extensions/mattermost/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `server_url` | string | `""` | Mattermost server URL |
| `access_token` | string | `""` | Personal access token or bot token |
| `channel_id` | string | `""` | Channel ID to bridge |
| `webhook_url` | string | `""` | Incoming webhook URL |
| `poll_interval_seconds` | int | `10` | Polling interval |
| `forward_to_mesh` | bool | `true` | Forward Mattermost→mesh |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `bot_name` | string | `"MESH-API"` | Bot display name |
**Hooks:** `on_message`, `on_emergency`.
---
#### Zello
Outbound message forwarding to Zello Work PTT channels.
**Commands:**
| Command | Description |
|---------|-------------|
| `/zello` | Show Zello status |
**Config (`extensions/zello/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `api_url` | string | `""` | Zello Work API URL |
| `api_token` | string | `""` | Zello API token |
| `channel_name` | string | `""` | Target Zello channel |
| `forward_mesh_messages` | bool | `true` | Forward mesh→Zello |
**Hooks:** `on_message`, `on_emergency`.
---
#### MQTT (Extension)
Bidirectional MQTT messaging with JSON payloads and optional TLS.
**Commands:**
| Command | Description |
|---------|-------------|
| `/mqtt` | Show MQTT broker status |
**Config (`extensions/mqtt/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `broker_host` | string | `"localhost"` | MQTT broker hostname |
| `broker_port` | int | `1883` | MQTT broker port |
| `username` | string | `""` | MQTT username |
| `password` | string | `""` | MQTT password |
| `topic_publish` | string | `"mesh-api/outbound"` | Publish topic |
| `topic_subscribe` | string | `"mesh-api/inbound"` | Subscribe topic |
| `topic_emergency` | string | `"mesh-api/emergency"` | Emergency topic |
| `use_tls` | bool | `false` | Enable TLS |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `qos` | int | `1` | MQTT QoS level |
| `client_id` | string | `"mesh-api"` | MQTT client ID |
**Hooks:** `on_message`, `on_emergency`.
---
#### Webhook Generic
Fully configurable bidirectional HTTP webhook with HMAC verification.
**Commands:**
| Command | Description |
|---------|-------------|
| `/webhook` | Show webhook status |
**Config (`extensions/webhook_generic/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `outbound_url` | string | `""` | URL to POST outbound messages to |
| `outbound_method` | string | `"POST"` | HTTP method |
| `outbound_headers` | object | `{}` | Custom headers |
| `outbound_template` | string | `""` | JSON body template (`{message}`, `{sender}` placeholders) |
| `hmac_secret` | string | `""` | HMAC-SHA256 secret for inbound verification |
| `inbound_message_field` | string | `"message"` | JSON field containing the message |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
**Hooks:** `on_message`, `on_emergency`. Flask route for inbound webhooks.
---
#### IMAP
Inbound email monitoring with subject/sender filtering.
**Commands:**
| Command | Description |
|---------|-------------|
| `/imap` | Show IMAP monitoring status |
**Config (`extensions/imap/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `imap_server` | string | `""` | IMAP server hostname |
| `imap_port` | int | `993` | IMAP port |
| `username` | string | `""` | Email username |
| `password` | string | `""` | Email password |
| `use_ssl` | bool | `true` | Use SSL/TLS |
| `folder` | string | `"INBOX"` | Mailbox folder |
| `poll_interval_seconds` | int | `60` | Polling interval |
| `subject_filter` | string | `""` | Only forward emails matching this subject |
| `sender_filter` | string | `""` | Only forward emails from this sender |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `max_body_length` | int | `250` | Max message body length |
| `mark_as_read` | bool | `true` | Mark processed emails as read |
**Hooks:** `on_emergency` (none — inbound only).
---
#### Mastodon
Fediverse / Mastodon bridge for posting toots and reading timeline from the mesh.
**Commands:**
| Command | Description |
|---------|-------------|
| `/toot ` | Post a toot from the mesh |
| `/fedi status` | Show Mastodon account info |
| `/fedi timeline [n]` | Show home timeline (last n posts) |
**Config (`extensions/mastodon/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `instance_url` | string | `"https://mastodon.social"` | Mastodon instance URL |
| `access_token` | string | `""` | Application access token (read+write) |
| `default_visibility` | string | `"public"` | Post visibility (`public`, `unlisted`, `private`, `direct`) |
| `post_prefix` | string | `"📡 [Mesh]"` | Prefix for toots |
| `max_toot_length` | int | `500` | Maximum toot length |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `auto_post_emergency` | bool | `true` | Post emergencies to Mastodon |
| `poll_mentions` | bool | `false` | Poll for @mentions |
| `poll_interval_seconds` | int | `120` | Mention polling interval |
| `forward_mentions_to_mesh` | bool | `true` | Forward mentions to mesh |
| `hashtags` | array | `["meshtastic", "meshnetwork"]` | Hashtags appended to toots |
| `content_warning` | string | `""` | Default content warning / spoiler text |
**Hooks:** `on_emergency` (auto-post).
---
#### n8n
Bidirectional workflow automation bridge with [n8n](https://n8n.io). Forward mesh messages and emergencies to n8n webhook triggers, receive messages from n8n workflows, query instance status, and list or trigger workflows from the mesh.
**Commands:**
| Command | Description |
|---------|-------------|
| `/n8n` | Show n8n integration status |
| `/n8n trigger ` | Trigger (activate) an n8n workflow by ID |
| `/n8n workflows` | List active n8n workflows |
**Config (`extensions/n8n/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `webhook_url` | string | `""` | n8n Webhook Trigger URL for outbound messages |
| `webhook_secret` | string | `""` | Shared secret sent as `X-Webhook-Secret` header |
| `api_base_url` | string | `"http://localhost:5678"` | n8n instance base URL |
| `api_key` | string | `""` | n8n API key (Settings → API → Create Key) |
| `send_emergency` | bool | `true` | Forward emergency alerts to n8n |
| `send_ai` | bool | `false` | Forward AI responses to n8n |
| `send_all` | bool | `false` | Forward all mesh messages to n8n |
| `receive_enabled` | bool | `true` | Accept inbound messages from n8n |
| `receive_endpoint` | string | `"/n8n/webhook"` | Flask endpoint for inbound n8n→mesh messages |
| `receive_secret` | string | `""` | Shared secret for inbound verification |
| `inbound_channel_index` | int/null | `null` | Default mesh channel for inbound messages |
| `message_field` | string | `"message"` | JSON field containing the message text |
| `sender_field` | string | `"sender"` | JSON field containing the sender name |
| `include_metadata` | bool | `true` | Include metadata in outbound payloads |
| `poll_executions` | bool | `false` | Poll n8n for completed workflow executions |
| `poll_interval_seconds` | int | `60` | Execution polling interval |
| `broadcast_channel_index` | int | `0` | Mesh channel index for broadcasts |
| `bot_name` | string | `"MESH-API"` | Source name in outbound payloads |
**Hooks:** `on_message`, `on_emergency`. Flask route for inbound webhooks.
---
### Notification Extensions
#### Apprise
Universal notification gateway supporting 100+ services through URL-based configuration.
**Commands:**
| Command | Description |
|---------|-------------|
| `/notify ` | Send notification via all configured Apprise URLs |
**Config (`extensions/apprise/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `apprise_urls` | array | `[]` | List of Apprise notification URLs |
| `notify_on_emergency` | bool | `true` | Auto-notify on emergency |
| `default_title` | string | `"MESH-API"` | Notification title |
| `default_type` | string | `"info"` | Notification type (`info`, `warning`, `failure`, `success`) |
Apprise URL examples: `slack://token`, `telegram://bot_token/chat_id`, `discord://webhook_id/webhook_token`, etc. See [Apprise docs](https://github.com/caronc/apprise/wiki) for 100+ supported services.
**Hooks:** `on_emergency`.
---
#### Ntfy
Push notifications via [ntfy.sh](https://ntfy.sh) with SSE-based inbound subscription.
**Commands:**
| Command | Description |
|---------|-------------|
| `/ntfy ` | Send ntfy push notification |
**Config (`extensions/ntfy/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `server_url` | string | `"https://ntfy.sh"` | ntfy server URL |
| `topic` | string | `""` | ntfy topic name |
| `token` | string | `""` | Access token (optional) |
| `priority` | int | `3` | Default priority (1-5) |
| `tags` | string | `"mesh,meshtastic"` | Comma-separated tags |
| `notify_on_emergency` | bool | `true` | Auto-notify on emergency |
| `subscribe_inbound` | bool | `false` | Subscribe for inbound messages |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
**Hooks:** `on_emergency`.
---
#### Pushover
Push notifications via the Pushover API with priority levels.
**Commands:**
| Command | Description |
|---------|-------------|
| `/pushover ` | Send Pushover notification |
**Config (`extensions/pushover/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `api_token` | string | `""` | Pushover application API token |
| `user_key` | string | `""` | Pushover user/group key |
| `default_priority` | int | `0` | Default priority (-2 to 2) |
| `emergency_priority` | int | `2` | Priority for emergency alerts |
| `default_sound` | string | `"pushover"` | Notification sound |
| `device` | string | `""` | Target device (blank = all) |
| `retry` | int | `60` | Retry interval for emergency priority (seconds) |
| `expire` | int | `3600` | Expiry for emergency priority (seconds) |
| `notify_on_emergency` | bool | `true` | Auto-notify on emergency |
**Hooks:** `on_emergency`.
---
#### PagerDuty
PagerDuty incident management — trigger, acknowledge, and resolve incidents from the mesh.
**Commands:**
| Command | Description |
|---------|-------------|
| `/pd trigger ` | Create a PagerDuty incident |
| `/pd ack ` | Acknowledge an incident |
| `/pd resolve ` | Resolve an incident |
| `/pd status` | List open incidents |
**Config (`extensions/pagerduty/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `routing_key` | string | `""` | Events API v2 integration/routing key |
| `api_token` | string | `""` | REST API token (for ack/resolve/list) |
| `service_id` | string | `""` | Service ID to filter incidents |
| `default_severity` | string | `"warning"` | Default severity (`critical`, `error`, `warning`, `info`) |
| `escalation_policy_id` | string | `""` | Escalation policy ID |
| `trigger_on_emergency` | bool | `true` | Auto-trigger on emergency broadcasts |
| `trigger_on_keywords` | array | `["SOS", "MAYDAY", "EMERGENCY"]` | Keywords that trigger incidents |
| `auto_resolve_minutes` | int | `0` | Auto-resolve after N minutes (0 = off) |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `poll_incidents` | bool | `false` | Poll and broadcast new incidents |
| `poll_interval_seconds` | int | `120` | Polling interval |
| `dedup_key_prefix` | string | `"mesh-api"` | Deduplication key prefix |
**Hooks:** `on_emergency`, `on_message` (keyword matching).
---
#### OpsGenie
Atlassian OpsGenie alert management from the mesh.
**Commands:**
| Command | Description |
|---------|-------------|
| `/og alert ` | Create an OpsGenie alert |
| `/og ack ` | Acknowledge an alert |
| `/og close ` | Close an alert |
| `/og status` | List open alerts |
**Config (`extensions/opsgenie/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `api_key` | string | `""` | OpsGenie API key (GenieKey) |
| `api_base` | string | `"https://api.opsgenie.com"` | API base URL |
| `default_priority` | string | `"P3"` | Default priority (P1-P5) |
| `responders` | array | `[]` | Responder list (OpsGenie format) |
| `tags` | array | `["mesh-api", "meshtastic"]` | Alert tags |
| `trigger_on_emergency` | bool | `true` | Auto-trigger on emergency |
| `trigger_on_keywords` | array | `["SOS", "MAYDAY", "EMERGENCY"]` | Keywords that trigger alerts |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `poll_alerts` | bool | `false` | Poll and broadcast new alerts |
| `poll_interval_seconds` | int | `120` | Polling interval |
| `auto_close_minutes` | int | `0` | Auto-close after N minutes (0 = off) |
**Hooks:** `on_emergency`, `on_message` (keyword matching).
---
### Emergency & Weather Extensions
#### NWS Alerts
National Weather Service severe weather alerts with zone-based filtering.
**Commands:**
| Command | Description |
|---------|-------------|
| `/nws` | Show active NWS alerts for configured zones |
| `/nwszones` | Show configured alert zones |
**Config (`extensions/nws_alerts/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `zones` | array | `[]` | NWS zone codes (e.g. `["TXC453", "TXZ211"]`) |
| `poll_interval_seconds` | int | `300` | Polling interval |
| `min_severity` | string | `"Moderate"` | Minimum severity (`Minor`, `Moderate`, `Severe`, `Extreme`) |
| `min_urgency` | string | `"Expected"` | Minimum urgency |
| `min_certainty` | string | `"Likely"` | Minimum certainty |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `auto_broadcast` | bool | `true` | Auto-broadcast new alerts |
| `max_alert_length` | int | `300` | Max alert text length |
**Hooks:** `on_emergency` (none — outbound broadcast only).
---
#### OpenWeatherMap
Weather data including current conditions, forecasts, and severe weather alerts.
**Commands:**
| Command | Description |
|---------|-------------|
| `/weather [city]` | Current weather for a city or default location |
| `/forecast [city]` | 5-day forecast |
| `/wxalerts` | Active weather alerts for configured coordinates |
**Config (`extensions/openweathermap/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `api_key` | string | `""` | OpenWeatherMap API key |
| `default_city` | string | `""` | Default city for queries |
| `default_lat` | string | `""` | Latitude for alerts (One Call 3.0) |
| `default_lon` | string | `""` | Longitude for alerts |
| `units` | string | `"imperial"` | Units (`imperial`, `metric`, `standard`) |
| `poll_interval_seconds` | int | `600` | Alert polling interval |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `auto_broadcast_alerts` | bool | `true` | Auto-broadcast severe weather |
| `max_alert_length` | int | `250` | Max alert text length |
**Hooks:** `on_emergency` (none — outbound only).
---
#### USGS Earthquakes
USGS earthquake monitoring with magnitude and radius filtering.
**Commands:**
| Command | Description |
|---------|-------------|
| `/quake` | Show recent earthquakes matching filters |
| `/quakeconfig` | Show current earthquake monitor config |
**Config (`extensions/usgs_earthquakes/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `min_magnitude` | float | `4.0` | Minimum magnitude to report |
| `filter_lat` | string | `""` | Center latitude for radius filter |
| `filter_lon` | string | `""` | Center longitude |
| `filter_radius_km` | int | `500` | Radius in km (0 = worldwide) |
| `poll_interval_seconds` | int | `300` | Polling interval |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `auto_broadcast` | bool | `true` | Auto-broadcast new quakes |
| `include_tsunami_warning` | bool | `true` | Include tsunami flag |
| `max_results` | int | `5` | Max results per query |
**Hooks:** `on_emergency` (none — outbound only).
---
#### GDACS
Global Disaster Alerting Coordination System — monitors 6 disaster types worldwide.
**Commands:**
| Command | Description |
|---------|-------------|
| `/gdacs [type]` | Show GDACS alerts (optional type filter: EQ, TC, FL, VO, DR, WF) |
**Config (`extensions/gdacs/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `alert_levels` | array | `["Orange", "Red"]` | Alert levels to include (`Green`, `Orange`, `Red`) |
| `disaster_types` | array | `["EQ", "TC", "FL", "VO", "DR", "WF"]` | Disaster types |
| `filter_lat` | string | `""` | Center latitude for proximity filter |
| `filter_lon` | string | `""` | Center longitude |
| `filter_radius_km` | int | `0` | Radius (0 = all global) |
| `poll_interval_seconds` | int | `600` | Polling interval |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `auto_broadcast` | bool | `true` | Auto-broadcast new alerts |
| `max_alert_length` | int | `300` | Max alert text length |
Disaster type codes: **EQ** = Earthquake, **TC** = Tropical Cyclone, **FL** = Flood, **VO** = Volcano, **DR** = Drought, **WF** = Wildfire.
**Hooks:** `on_emergency` (none — outbound only).
---
#### Amber Alerts
Missing person alerts (AMBER, Silver, Blue) from the NWS CAP feed with state filtering.
**Commands:**
| Command | Description |
|---------|-------------|
| `/amber` | Show active AMBER alerts |
| `/silver` | Show active Silver alerts |
| `/blue` | Show active Blue alerts |
**Config (`extensions/amber_alerts/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `states` | array | `[]` | State codes to filter (e.g. `["TX", "CA"]`, empty = all) |
| `poll_interval_seconds` | int | `300` | Polling interval |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `auto_broadcast` | bool | `true` | Auto-broadcast new alerts |
| `max_alert_length` | int | `300` | Max alert text length |
| `include_silver` | bool | `true` | Include Silver (elderly) alerts |
| `include_blue` | bool | `true` | Include Blue (law enforcement) alerts |
**Hooks:** `on_emergency` (none — outbound only).
---
#### NASA Space Weather
NASA DONKI (Database Of Notifications, Knowledge, Information) space weather monitor — tracks geomagnetic storms, solar flares, coronal mass ejections, and more.
**Commands:**
| Command | Description |
|---------|-------------|
| `/spaceweather` | Show recent space weather events |
| `/solarflare` | Show recent solar flare activity |
| `/geomagstorm` | Show recent geomagnetic storms |
**Config (`extensions/nasa_space_weather/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `api_key` | string | `"DEMO_KEY"` | NASA API key (free at [api.nasa.gov](https://api.nasa.gov)) |
| `poll_interval_seconds` | int | `600` | Polling interval |
| `auto_broadcast` | bool | `true` | Auto-broadcast new events |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `event_types` | array | `["GST","FLR","CME","IPS","SEP","RBE"]` | Event types to monitor |
| `min_kp_index` | int | `5` | Min Kp index for geomagnetic storm alerts (1-9) |
| `min_flare_class` | string | `"M"` | Min solar flare class to broadcast (`A`,`B`,`C`,`M`,`X`) |
| `lookback_days` | int | `3` | Days of history to query |
| `max_alert_length` | int | `300` | Max alert text length |
| `max_results` | int | `5` | Max results per command query |
**Event Types:**
| Code | Description |
|------|-------------|
| `GST` | Geomagnetic Storm — Kp index & G-scale impact |
| `FLR` | Solar Flare — class (A/B/C/M/X) & source region |
| `CME` | Coronal Mass Ejection — speed & direction |
| `IPS` | Interplanetary Shock |
| `SEP` | Solar Energetic Particle |
| `RBE` | Radiation Belt Enhancement |
**Hooks:** `on_emergency` (none — outbound only).
---
### Ham Radio & Off-Grid Extensions
#### Winlink
Winlink Global Radio Email gateway for ham radio operators.
**Commands:**
| Command | Description |
|---------|-------------|
| `/winlink
` | Send a Winlink email |
| `/wlcheck` | Check for new Winlink messages |
| `/wlstatus` | Show Winlink connection status |
**Config (`extensions/winlink/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `callsign` | string | `""` | Ham radio callsign |
| `gateway_host` | string | `""` | RMS gateway hostname |
| `gateway_port` | int | `8772` | RMS gateway port |
| `password` | string | `""` | Winlink password |
| `poll_interval_seconds` | int | `300` | Mailbox polling interval |
| `auto_forward_to_mesh` | bool | `true` | Auto-forward new mail to mesh |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `max_body_length` | int | `250` | Max message body length |
| `outbound_enabled` | bool | `true` | Allow sending outbound messages |
| `winlink_api_url` | string | `"https://api.winlink.org"` | Winlink REST API URL |
| `winlink_api_key` | string | `""` | Winlink API key |
| `rms_relay_path` | string | `""` | RMS relay path |
| `default_to_address` | string | `""` | Default recipient for emergency forwarding |
Integration methods (priority order): Winlink REST API → Pat local client → RMS gateway.
**Hooks:** `on_emergency` (forwards emergency to default address).
---
#### APRS
Automatic Packet Reporting System integration for position and message bridging.
**Commands:**
| Command | Description |
|---------|-------------|
| `/aprs ` | Look up station position via aprs.fi |
| `/aprsmsg ` | Send APRS message via APRS-IS |
| `/aprsnear` | Show nearby APRS stations |
**Config (`extensions/aprs/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `callsign` | string | `""` | Ham radio callsign |
| `passcode` | string | `""` | APRS-IS passcode |
| `aprs_is_server` | string | `"rotate.aprs2.net"` | APRS-IS server |
| `aprs_is_port` | int | `14580` | APRS-IS port |
| `aprs_fi_api_key` | string | `""` | aprs.fi API key (free) |
| `filter_range_km` | int | `100` | Position filter radius |
| `filter_lat` | string | `""` | Filter center latitude |
| `filter_lon` | string | `""` | Filter center longitude |
| `poll_interval_seconds` | int | `60` | De-dupe interval for positions |
| `auto_broadcast_positions` | bool | `false` | Broadcast nearby APRS positions to mesh |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `send_position_to_aprs` | bool | `false` | Publish mesh positions to APRS-IS |
| `position_comment` | string | `"MESH-API Node"` | APRS position comment |
| `symbol_table` | string | `"/"` | APRS symbol table |
| `symbol_code` | string | `"-"` | APRS symbol code |
| `message_ssid` | string | `"-5"` | SSID for outbound messages |
**Note:** Transmitting on APRS requires a valid amateur radio licence.
**Hooks:** None (command-driven + background listener).
---
#### BBS
Full-featured Bulletin Board System with SQLite store-and-forward messaging.
**Commands:**
| Command | Description |
|---------|-------------|
| `/bbs help` | Show all BBS commands |
| `/bbs boards` | List available boards |
| `/bbs read [n]` | Read last n messages (default 5, max 20) |
| `/bbs post ` | Post a message |
| `/bbs search ` | Search across all boards |
| `/bbs msg ` | Send a private message |
| `/bbs inbox` | Check private messages |
| `/bbs new ` | Create a new board |
| `/bbs del ` | Delete your own message |
| `/bbs info` | Show BBS statistics |
**Config (`extensions/bbs/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `db_filename` | string | `"bbs.db"` | SQLite database filename |
| `board_name` | string | `"MESH-BBS"` | BBS display name |
| `motd` | string | `"Welcome to the Mesh BBS!"` | Message of the day |
| `max_messages_per_board` | int | `500` | Max messages per board (oldest auto-deleted) |
| `max_message_length` | int | `500` | Max message character length |
| `max_boards` | int | `20` | Maximum number of boards |
| `default_boards` | array | `["general", "emergency", "trading", "tech"]` | Default boards created on init |
| `allow_private_messages` | bool | `true` | Enable private messaging |
| `message_retention_days` | int | `90` | Auto-delete messages older than N days |
| `broadcast_channel_index` | int | `0` | Mesh channel index |
| `announce_new_posts` | bool | `true` | Broadcast new post announcements |
| `require_shortname` | bool | `true` | Require node shortname |
**Database:** SQLite (`bbs.db`) stored in the extension directory. Three tables: `boards`, `messages`, `private_messages`. Automatic cleanup runs every 6 hours.
**Hooks:** None (fully command-driven).
---
### Smart Home Extensions
#### Home Assistant (Extension)
Routes mesh messages to the Home Assistant Conversation API as an AI provider.
This extension functions as an **AI provider** — when `ai_provider` is set to `"home_assistant"` in the main `config.json`, AI queries are routed through HA's conversation API.
**Config (`extensions/home_assistant/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `ha_url` | string | `""` | Home Assistant URL (e.g. `http://homeassistant.local:8123`) |
| `ha_token` | string | `""` | Long-lived access token |
| `ha_channel_index` | int | `-1` | Mesh channel for HA (-1 = DM only) |
| `ha_pin` | string | `""` | Optional PIN to protect access |
| `ha_language` | string | `"en"` | Conversation language |
**Hooks:** AI provider via `get_ai_response()`.
---
### Mesh Bridging Extensions
#### MeshCore
> ⚠️ **Deprecated as of v0.7.0 — MeshCore is now a core radio, not an extension.**
>
> The legacy `extensions/meshcore` bridge plugin has been superseded by the
> **first-class, core-owned MeshCore radio** (`meshcore_core.py`). When the core
> MeshCore radio is enabled in `config.json`, the old extension automatically
> defers (it will not open the device or bridge). Do **not** enable both.
>
> Configure MeshCore in the `meshcore` block of [config.json](config.json) instead
> — see **[Running With Meshtastic, MeshCore, or Both](#running-with-meshtastic-meshcore-or-both-v070)**
> for the full setup. With the core radio you get native cross-network commands,
> AI, every extension, a harmonized map, DMs, group/private channels, the
> per-network UI, and firmware update detection — none of which the old bridge
> plugin provided.
>
> **Migrating:** move your old `extensions/meshcore/config.json` values into the
> `meshcore` block of the main `config.json` (the keys are similarly named:
> `connection_type`, `serial_port`, `serial_baud`, `tcp_host`/`tcp_port`,
> `bridge_enabled`, the channel-map objects, and the origin tags), then leave the
> extension disabled.
---
### AI Agent Extensions
#### OpenClaw
Bridges the Meshtastic mesh network to an [OpenClaw](https://openclaw.dev) AI agent instance. Mesh users can query the OpenClaw agent via slash commands; the agent can fan-out responses through its own channels (Telegram, Discord, SMS, etc.). Emergency alerts are optionally forwarded to OpenClaw for multi-channel distribution. An optional polling mode injects proactive messages (scheduled alerts, reminders) from OpenClaw into the mesh.
**Commands:**
| Command | Description |
|---------|-------------|
| `/claw-XY` | Query the OpenClaw AI agent (XY = your install suffix) |
| `/agent-XY` | Alias for `/claw-XY` |
**Config (`extensions/openclaw/config.json`):**
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Enable the extension |
| `openclaw_url` | string | `"http://localhost:18789"` | OpenClaw gateway API URL |
| `openclaw_token` | string | `""` | Bearer token for OpenClaw authentication (optional) |
| `agent_name` | string | `"mesh-api"` | Agent name to address in OpenClaw |
| `allowed_nodes` | array | `[]` | Node IDs allowed to use OpenClaw (empty = all nodes) |
| `forward_emergency` | bool | `true` | Forward `/emergency` alerts to OpenClaw |
| `timeout` | int | `15` | HTTP request timeout in seconds |
| `poll_enabled` | bool | `false` | Poll OpenClaw for proactively queued messages |
| `poll_interval` | int | `30` | Polling interval in seconds |
**Hooks:** `handle_command()` (query agent), `on_emergency()` (forward alerts), `send_message()` (relay tagged messages), `receive_message()` (poll queue).
**Companion Skill:** A MESH-API skill file for OpenClaw is included at `openclaw-release/skills/mesh-api/SKILL.md` — copy it to `~/.openclaw/skills/mesh-api/SKILL.md` to teach an OpenClaw agent how to interact with MESH-API's REST API. If you install the `@mesh-api/openclaw-meshtastic` plugin via npm, the skill ships automatically.
**OpenClaw Community Plugin:** A full OpenClaw-native TypeScript plugin is being prepared for release as `@mesh-api/openclaw-meshtastic` — see the [OpenClaw Integration & Community Plugin](#openclaw-integration--community-plugin) section below for the full plan.
---
### Extension Management
**Listing Extensions:** Use the `/extensions` command on the mesh to see all loaded extensions and their status.
**Enabling/Disabling:** Edit the extension's `config.json` and set `"enabled": true` or `"enabled": false`, then restart MESH-API.
**Extension Loading:** Extensions are automatically discovered from the `extensions_path` directory (default: `./extensions`). Each subfolder containing an `extension.py` file is loaded. Folders starting with `_` (like `_example`) are skipped.
---
## Developing Custom Extensions
> A step-by-step guide for building custom extensions for the MESH-API plugin system.
### Overview
MESH-API uses a plugin-based extension system where each extension is a self-contained Python package that lives in the `extensions/` directory. Extensions can:
- Register slash commands accessible from the mesh network
- Send and receive messages to/from the mesh
- React to emergency broadcasts
- Observe all inbound mesh messages
- Expose HTTP endpoints via Flask
- Run background threads for polling external services
- Act as AI providers
Extensions are automatically discovered and loaded at startup. No changes to core code are required.
### Quick Start (Extensions)
1. **Copy the template:**
```bash
cp -r extensions/_example extensions/my_extension
```
2. **Edit the three files:**
- `__init__.py` — leave empty (marks the folder as a Python package)
- `config.json` — define your settings (must include `"enabled": true`)
- `extension.py` — implement your extension class
3. **Restart MESH-API** — your extension is auto-discovered and loaded.
4. **Verify** — send `/extensions` on the mesh to see it listed.
### Extension Structure
Every extension lives in its own subfolder under `extensions/`:
```
extensions/
├── base_extension.py # Abstract base class (DO NOT MODIFY)
├── loader.py # Extension loader (DO NOT MODIFY)
├── __init__.py
└── my_extension/ # Your extension folder
├── __init__.py # Empty file (required)
├── config.json # Extension configuration
└── extension.py # Extension implementation
```
**Naming Rules:**
- Folder names must be valid Python identifiers (lowercase, underscores OK)
- Folders starting with `_` are **skipped** by the loader (used for templates)
- The class inside `extension.py` must subclass `BaseExtension`
- Class name convention: `Extension` (e.g. `MyExtension`)
### Base Class API Reference
Every extension inherits from `BaseExtension`. Here's the complete API:
**Constructor (automatic):**
```python
def __init__(self, extension_dir: str, app_context: dict):
```
You do **not** override `__init__`. The base class handles:
- `self.extension_dir` — absolute path to your extension's folder
- `self.app_context` — shared dict with core helpers (see below)
- `self._config` — loaded from your `config.json`
**Required Properties (must override):**
| Property | Returns | Description |
|----------|---------|-------------|
| `name` | `str` | Human-readable name (e.g. `"My Extension"`) |
| `version` | `str` | Semantic version (e.g. `"1.0.0"`) |
**Built-in Properties (inherited):**
| Property | Returns | Description |
|----------|---------|-------------|
| `enabled` | `bool` | `config["enabled"]` — the loader checks this |
| `commands` | `dict` | Slash commands to register (override to add) |
| `config` | `dict` | Read-only access to loaded config |
**Lifecycle Hooks:**
| Method | When Called |
|--------|------------|
| `on_load()` | Once after instantiation at startup |
| `on_unload()` | On shutdown or before hot-reload |
**Message Hooks:**
| Method | Signature | Purpose |
|--------|-----------|---------|
| `send_message()` | `(message: str, metadata: dict \| None)` | Outbound: mesh → external service |
| `receive_message()` | `()` | Inbound polling (prefer background threads) |
| `handle_command()` | `(command: str, args: str, node_info: dict) → str \| None` | Handle a registered slash command |
| `on_emergency()` | `(message: str, gps_coords: dict \| None)` | Emergency broadcast hook |
| `on_message()` | `(message: str, metadata: dict \| None)` | Observe all inbound mesh messages |
**Flask Integration:**
| Method | Signature | Purpose |
|--------|-----------|---------|
| `register_routes()` | `(app: Flask)` | Register HTTP endpoints |
**Helper Methods:**
| Method | Signature | Purpose |
|--------|-----------|---------|
| `send_to_mesh()` | `(text, channel_index=None, destination_id=None)` | Send a message to the mesh network |
| `log()` | `(message: str)` | Write to the MESH-API script log |
| `_save_config()` | `()` | Persist config changes to disk |
**app_context Dict:**
The `app_context` dict provides access to core functionality:
| Key | Type | Description |
|-----|------|-------------|
| `interface` | `MeshInterface` | The Meshtastic serial/TCP/BLE interface |
| `send_broadcast_chunks` | `function(iface, text, channel_idx)` | Send broadcast message |
| `send_direct_chunks` | `function(iface, text, destination_id)` | Send direct message |
| `add_script_log` | `function(message)` | Core logging function |
| `flask_app` | `Flask` | The Flask application instance |
| `config` | `dict` | Main `config.json` contents |
### Step-by-Step Tutorial
**1. Create the folder structure:**
```
extensions/my_sensor/
├── __init__.py # Empty
├── config.json
└── extension.py
```
**2. Define config.json:**
```json
{
"enabled": true,
"sensor_url": "http://localhost:9000/api/reading",
"poll_interval_seconds": 300,
"broadcast_channel_index": 0,
"unit": "°F"
}
```
The only required key is `"enabled"`. Everything else is up to you.
**3. Implement extension.py:**
```python
"""My Sensor extension — reads temperature from a local sensor API."""
import threading
import time
try:
import requests
except ImportError:
requests = None
from extensions.base_extension import BaseExtension
class MySensorExtension(BaseExtension):
@property
def name(self) -> str:
return "My Sensor"
@property
def version(self) -> str:
return "1.0.0"
@property
def commands(self) -> dict:
return {
"/temp": "Read the current temperature",
}
def on_load(self) -> None:
self._stop = threading.Event()
self.log(f"My Sensor loaded. URL: {self.config.get('sensor_url')}")
def on_unload(self) -> None:
self._stop.set()
self.log("My Sensor unloaded.")
def handle_command(self, command: str, args: str,
node_info: dict) -> str | None:
if command == "/temp":
return self._read_sensor()
return None
def _read_sensor(self) -> str:
url = self.config.get("sensor_url", "")
unit = self.config.get("unit", "°F")
if not url:
return "Sensor URL not configured."
try:
resp = requests.get(url, timeout=5)
data = resp.json()
temp = data.get("temperature", "?")
return f"🌡️ Current temperature: {temp}{unit}"
except Exception as exc:
return f"⚠️ Sensor error: {exc}"
```
**4. Test it:**
1. Restart MESH-API
2. Send `/extensions` on mesh — should show "My Sensor v1.0.0 [enabled]"
3. Send `/temp` — should return the temperature reading
### Hook Reference
**handle_command(command, args, node_info) → str | None**
The most commonly used hook. Called when a mesh user sends one of your registered commands.
```python
def handle_command(self, command: str, args: str, node_info: dict) -> str | None:
if command == "/mycmd":
sender = node_info.get("shortname", "?")
return f"Hello {sender}! You said: {args}"
return None # Not our command
```
**node_info dict:**
```python
{
"node_id": "!abcd1234", # Hex node ID
"shortname": "ABC", # 4-char node short name
"longname": "Alpha Bravo", # Full node name
"channel_index": 0, # Channel the message arrived on
"is_direct": False, # True if DM, False if broadcast
}
```
**Return value:**
- `str` — text sent back to the mesh (broadcast or DM depending on context)
- `None` — command not handled, loader passes to next extension
**on_message(message, metadata)**
Read-only observer hook. Called for **every** inbound mesh message. Use for logging, analytics, keyword scanning, or triggering side-effects.
```python
def on_message(self, message: str, metadata: dict | None = None) -> None:
if "help" in message.lower():
self.log(f"Help request detected: {message}")
```
**Do NOT** return a response from `on_message`. Use `handle_command` for responses, or `send_to_mesh()` for async replies.
**on_emergency(message, gps_coords)**
Called when `/emergency` or `/911` is triggered on the mesh.
```python
def on_emergency(self, message: str, gps_coords: dict | None = None) -> None:
lat = gps_coords.get("lat", "?") if gps_coords else "?"
lon = gps_coords.get("lon", "?") if gps_coords else "?"
self.log(f"EMERGENCY at {lat},{lon}: {message}")
# Forward to your external service here
```
**send_message(message, metadata)**
Outbound hook. Called by the loader when the core wants to push a message to external services.
```python
def send_message(self, message: str, metadata: dict | None = None) -> None:
requests.post("https://example.com/api", json={"text": message})
```
**register_routes(app)**
Register Flask HTTP endpoints for inbound webhooks or APIs.
```python
def register_routes(self, app) -> None:
@app.route("/my_extension/webhook", methods=["POST"])
def my_webhook():
from flask import request, jsonify
data = request.get_json()
message = data.get("message", "")
self.send_to_mesh(message, channel_index=0)
return jsonify({"status": "ok"})
```
### Extension Configuration
**Reading Config:**
```python
api_key = self.config.get("api_key", "")
interval = int(self.config.get("poll_interval", 60))
```
**Updating Config at Runtime:**
```python
self._config["last_check"] = "2025-01-01T00:00:00Z"
self._save_config() # Writes to config.json on disk
```
**Config Best Practices:**
- Always provide defaults with `.get(key, default)`
- Include `"enabled": false` as the first key
- Use descriptive key names: `poll_interval_seconds`, `broadcast_channel_index`
- Document every key in your extension's comments or README
### Flask Routes (Extensions)
Extensions can expose HTTP endpoints. The Flask app is passed to `register_routes()`:
```python
def register_routes(self, app) -> None:
@app.route("/my_ext/data", methods=["GET"])
def my_data():
from flask import jsonify
return jsonify({"status": "ok", "extension": self.name})
@app.route("/my_ext/inbound", methods=["POST"])
def my_inbound():
from flask import request
data = request.get_json(force=True)
text = data.get("message", "")
if text:
self.send_to_mesh(text)
return "OK", 200
```
**Rules:**
- Use unique route paths prefixed with your extension name
- Import Flask utilities inside the route functions (avoid circular imports)
- Keep route handlers lightweight
### Background Threads
Many extensions need to poll external services. Use daemon threads:
```python
import threading
import time
class MyExtension(BaseExtension):
def on_load(self) -> None:
self._stop = threading.Event()
self._thread = threading.Thread(
target=self._poll_loop,
daemon=True,
name="my-ext-poll",
)
self._thread.start()
def on_unload(self) -> None:
self._stop.set()
if self._thread.is_alive():
self._thread.join(timeout=10)
def _poll_loop(self) -> None:
time.sleep(10) # Initial delay to let system stabilize
while not self._stop.is_set():
try:
data = self._fetch_data()
if data:
self.send_to_mesh(f"New data: {data}")
except Exception as exc:
self.log(f"Poll error: {exc}")
# Interruptible sleep (checks stop event every second)
interval = int(self.config.get("poll_interval_seconds", 60))
for _ in range(interval):
if self._stop.is_set():
break
time.sleep(1)
```
**Thread Safety Tips:**
- Use `threading.Lock()` if shared state is accessed from multiple threads
- Use `threading.Event()` for clean shutdown signaling
- Use interruptible sleep pattern (loop with 1-second sleeps)
- Always set `daemon=True` so threads don't prevent exit
- Give threads descriptive names
### Extension Best Practices
1. **Guard imports** — wrap optional dependencies in try/except:
```python
try:
import requests
except ImportError:
requests = None
```
2. **Handle errors gracefully** — never let exceptions crash the main process:
```python
try:
result = self._call_api()
except Exception as exc:
return f"⚠️ Error: {exc}"
```
3. **Respect mesh bandwidth** — keep messages short (< 230 chars if possible). The mesh has limited capacity.
4. **De-duplicate** — track seen message IDs to avoid broadcasting the same alert twice:
```python
if msg_id in self._seen_ids:
return
self._seen_ids.add(msg_id)
```
5. **Clean up in on_unload()** — stop threads, close sockets, flush buffers.
**Naming Conventions:**
- Folder: `snake_case` (e.g. `my_extension`)
- Class: `PascalCaseExtension` (e.g. `MyExtension`)
- Commands: `/` — avoid collisions with built-in commands
- Config keys: `snake_case` with descriptive names
**Message Formatting — Use emoji prefixes for visual scanning on small screens:**
- 📡 — radio/connectivity
- 🚨 — alerts/emergencies
- ✅ — success/confirmation
- ⚠️ — warnings/errors
- 📋 — lists/info
- 📧 — email/messages
- 🌡️ — weather/sensors
### Testing Extensions
1. Set `"enabled": true` in your extension's `config.json`
2. Restart MESH-API
3. Check the logs for `[ext:YourName]` entries
4. Send `/extensions` to verify it's loaded
5. Test each command from a mesh node
Your `self.log()` calls appear in the MESH-API script log with the prefix `[ext:YourName]`. Check the WebUI Logs panel or the log file.
### Extension Troubleshooting
**Extension not loading:**
- Check that `extension.py` exists in the folder
- Ensure `__init__.py` exists (even if empty)
- Verify the class inherits from `BaseExtension`
- Check that `name` and `version` properties are defined
- Folder names starting with `_` are ignored intentionally
- Check logs for import errors
**Commands not responding:**
- Verify `commands` property returns a dict with your command
- Check `handle_command()` matches the exact command string
- Make sure no other extension registers the same command
- Confirm `"enabled": true` in your config.json
**send_to_mesh not working:**
- Ensure `app_context` contains a valid `interface`
- Check that the mesh interface is connected
- Verify channel index is valid for your mesh configuration
**Config not loading:**
- Validate JSON syntax in `config.json` (use a JSON linter)
- Check file permissions
- Look for log entries about config load failures
### Extension Examples Reference
The `extensions/` directory includes 25+ working extensions you can reference:
| Extension | Complexity | Good Example Of |
|-----------|-----------|-----------------|
| `_example` | Minimal | Basic structure, all hooks documented |
| `ntfy` | Simple | HTTP API + push notifications |
| `pushover` | Simple | Outbound-only notifications |
| `nws_alerts` | Medium | Polling + auto-broadcast + filtering |
| `telegram` | Medium | Bidirectional bridge + long-polling |
| `mqtt` | Medium | Event-driven with paho-mqtt |
| `bbs` | Complex | SQLite database + thread safety + subcommands |
| `aprs` | Complex | Raw TCP sockets + protocol parsing |
| `discord` | Complex | Webhook + bot + Flask route |
---
## Roadmap
- **API Integration Workflow (Planned)**
- A guided workflow for adding new API integrations, including configuration templates, validation checks, and safe test routes before production use.
- Goal: make it easier to connect external services without editing core code.
- **MeshCore Routing Support (Initial Implementation — v0.6.0)**
- MeshCore extension added with bidirectional bridge, command support, and AI integration.
- Supports USB serial and TCP/WiFi connections to MeshCore companion devices.
- *(Superseded in v0.7.0 — MeshCore is now a first-class core radio; see [Running With Meshtastic, MeshCore, or Both](#running-with-meshtastic-meshcore-or-both-v070).)*
---
## Changelog
### v0.7.0 Beta — Initial Full MeshCore Support
> Run MESH-API with a Meshtastic node, a MeshCore node, or **both** with
> cross-network routing. Widely untested — community feedback wanted.
#### Multi-radio (MeshCore as a first-class radio)
- **MeshCore promoted to a core-owned radio** (`meshcore_core.py`) on equal footing with Meshtastic, feeding the *same* network-agnostic message pipeline. Connects over serial / TCP / BLE.
- **Three topologies:** Meshtastic-only (unchanged), MeshCore-only standalone (`meshtastic_enabled: false`, no Meshtastic device needed), or both with MESH-API as the man-in-the-middle bridge.
- **Cross-network everything** — slash commands, AI, and every extension now work on both networks; outbound send to one network or both at once.
- **WebUI parity for MeshCore** — per-network connection banner; node **network filter** with **collapsible Meshtastic/MeshCore sections**; network badges (📡/🟣) on nodes & messages; distinct purple map markers with DM/interact; MeshCore channels (group chats / private channels) in the send form; DMs to MeshCore contacts.
- **Staged startup + exponential backoff** so two USB radios on a power-limited host (e.g. Pi Zero) don't fight over the bus during connect.
#### MCP server (Model Context Protocol)
- New built-in **MCP server** (`mcp_server.py`) at `POST /mcp` (Streamable HTTP / JSON-RPC 2.0, no async deps). Lets external AI agents (Claude, Perplexity, Hermes, custom) call MESH-API core functions and extensions as **tools**, usi