User-guide: new Region Scopes section (registry CRUD, per-channel picker, firmware v1.15 default), updated Switching Channels (searchable picker on narrow screens, sidebar previews on wide), Settings Regions tab, path_hash_mode in Device tab. Architecture: regions/channel_scopes tables, /api/regions and /api/channels/scopes endpoints, per-channel scope-key push under _send_lock in DeviceManager, path_hash_mode field in /api/device/config, channel POST/join idempotency. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
17 KiB
mc-webui Architecture
Technical documentation for mc-webui, covering system architecture, project structure, and internal APIs.
Table of Contents
- Tech Stack
- Container Architecture
- DeviceManager Architecture
- Project Structure
- Database Architecture
- API Reference
- WebSocket API
- Offline Support
Tech Stack
- Backend: Python 3.11+, Flask, Flask-SocketIO (gevent), SQLite
- Frontend: HTML5, Bootstrap 5, vanilla JavaScript, Socket.IO client
- Deployment: Docker / Docker Compose (Single-container architecture)
- Communication: Direct hardware access (USB, BLE, or TCP) via
meshcorelibrary - Data source: SQLite Database (
./data/meshcore/<pubkey_prefix>.db)
Container Architecture
mc-webui uses a single-container architecture for simplified deployment and direct hardware communication:
┌─────────────────────────────────────────────────────────────┐
│ Docker Network │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ mc-webui │ │
│ │ │ │
│ │ - Flask web app (Port 5000) │ │
│ │ - DeviceManager (Direct USB/BLE/TCP access) │ │
│ │ - Database (SQLite) │ │
│ │ │ │
│ └─────────┬─────────────────────────────────────────────┘ │
│ │ │
└────────────┼─────────────────────────────────────────────────┘
│
▼
┌──────────────┐
│ USB/BLE/TCP │
│ Device │
└──────────────┘
Three transport options are supported with the following priority: BLE > TCP > Serial (USB). Set the MC_BLE_ADDRESS or MC_TCP_HOST environment variable to activate BLE or TCP transport respectively; otherwise, USB serial is used by default.
This v2 architecture eliminates the need for a separate bridge container and relies on the native meshcore Python library for direct communication, ensuring lower latency and greater stability.
Docker Entrypoint (BLE cleanup)
scripts/docker-entrypoint.sh runs before the Flask app starts. When MC_BLE_ADDRESS is set, it uses D-Bus to check if BlueZ has an active session to the device and disconnects it. BlueZ auto-reconnects trusted devices, which leaves stale GATT notification handles that block bleak from establishing a new session. A clean disconnect at startup ensures the app starts with a fresh BLE state.
Multi-architecture Images
Official images are built via GitHub Actions for linux/amd64, linux/arm64, and linux/arm/v7 (Raspberry Pi 2/3/4/5 supported). Build dependencies (gcc, python3-dev, libjpeg-dev, zlib1g-dev) are installed and then purged to keep the final image size small while still compiling Pillow and pycryptodome from source when wheels are unavailable (notably on arm/v7). GHA layer cache (cache-from / cache-to) speeds up subsequent rebuilds. Images are pushed to both Docker Hub (mawoj/mc-webui) and GitHub Container Registry (ghcr.io/marekwo/mc-webui), with latest tag on main and dev tag on the dev branch.
DeviceManager Architecture
The DeviceManager handles the connection to the MeshCore device via a direct session:
- Single persistent session - One long-lived connection utilizing the
meshcorelibrary - Event-driven - Subscribes to device events (e.g., incoming messages, advert receptions, ACKs) and triggers appropriate handlers
- Direct Database integration - Seamlessly syncs contacts, messages, and device settings to the SQLite database
- Real-time messages - Instant message processing via callback events without polling
- Thread-safe queue - Commands are serialized to prevent device lockups
- Auto-restart watchdog - Monitors connection health and restarts the session on crash
- BLE keepalive & reconnect - When using Bluetooth transport, a 60s keepalive loop detects "zombie" connections (reads still succeed but writes silently fail). On disconnect or keepalive failure, the manager marks the session as permanently failed and the
/healthendpoint returns 503, letting the Docker healthcheck trigger a fast container restart (~5s) to get a clean BLE state rather than attempting unreliable in-process reconnects - Echo correlation - Sent channel messages pre-compute their expected
pkt_payloadusing the channel secret and send timestamp (±3s for clock drift), so incoming echoes are matched exactly instead of only by 1-byte channel hash (prevents misattribution when two messages go out simultaneously on the same channel) - Per-channel region scope - Before each channel send, the channel's mapped region scope key (16 bytes) is pushed to the firmware via
CMD_SET_FLOOD_SCOPE_KEY(54). The scope-set + send pair is serialised under a_send_lockso concurrent sends on different channels can't swap each other's scope. Channels without a mapping get an all-zero key so a previously-set scope doesn't leak across channels
Project Structure
mc-webui/
├── Dockerfile # Main app Docker image
├── docker-compose.yml # Single-container orchestration
├── app/
│ ├── __init__.py
│ ├── main.py # Flask entry point + Socket.IO handlers
│ ├── config.py # Configuration from env vars
│ ├── database.py # SQLite database models and CRUD operations
│ ├── device_manager.py # Core logic for meshcore communication
│ ├── contacts_cache.py # Persistent contacts cache (DB-backed)
│ ├── read_status.py # Server-side read status manager (DB-backed)
│ ├── version.py # Git-based version management
│ ├── migrate_v1.py # Migration script from v1 flat files to v2 SQLite
│ ├── meshcore/
│ │ ├── __init__.py
│ │ ├── cli.py # Meshcore library wrapper interface
│ │ └── parser.py # Data parsers
│ ├── archiver/
│ │ └── manager.py # Archive scheduler and management
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── api.py # REST API endpoints
│ │ └── views.py # HTML views
│ ├── static/ # Frontend assets (CSS, JS, images, vendors)
│ │ └── js/fab-utils.js # Floating-button drag/collapse/sizing helpers
│ └── templates/ # HTML templates
├── docs/ # Documentation
├── scripts/
│ ├── update.sh # Automated update script
│ ├── docker-entrypoint.sh # Container startup (BLE cleanup)
│ ├── updater/ # Remote update webhook service
│ └── watchdog/ # Container health monitor
└── README.md
Database Architecture
mc-webui v2 uses a robust SQLite Database with WAL (Write-Ahead Logging) enabled.
Location: ./data/meshcore/<pubkey_prefix>.db
Key tables:
messages- All channel and direct messages (with FTS5 index for full-text search)contacts- Contact list with sync status, types, block/ignore flags,no_auto_floodflagchannels- Channel configuration and keysechoes- Sent message tracking and repeater paths,hash_sizefor path_hash_modedirect_messages- DM messages with delivery tracking (delivery_status,delivery_attempt,delivery_max_attempts,delivery_path)acks- DM delivery statussettings- Application settings (migrated from .webui_settings.json)regions- User-curated MeshCore flood scopes (name,key_hex,is_default)channel_scopes- Per-channel region mapping (channel_idx→region_id, CASCADE on region delete; absent row = no override → firmware default applies)
The use of SQLite allows for fast queries, reliable data storage, full-text search, and complex filtering (such as contact ignoring/blocking) without the risk of file corruption inherent to flat JSON files.
API Reference
Messages
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/messages |
List messages (?archive_date, ?days, ?channel_idx) |
| POST | /api/messages |
Send message ({text, channel_idx, reply_to?}) |
| GET | /api/messages/updates |
Check for new messages (smart refresh) |
| GET | /api/messages/<id>/meta |
Get message metadata (echoes, paths) |
| GET | /api/messages/search |
Full-text search (?q=, ?channel_idx=, ?limit=) |
Contacts
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/contacts |
List contacts |
| GET | /api/contacts/detailed |
Full contact data (includes protection, ignore, block flags) |
| GET | /api/contacts/cached |
Get cached contacts (superset of device contacts) |
| POST | /api/contacts/delete |
Soft-delete contact ({selector}) |
| POST | /api/contacts/cached/delete |
Delete cached contact |
| GET | /api/contacts/protected |
List protected public keys |
| POST | /api/contacts/<key>/protect |
Toggle contact protection |
| POST | /api/contacts/<key>/ignore |
Toggle contact ignore |
| POST | /api/contacts/<key>/block |
Toggle contact block |
| GET | /api/contacts/blocked-names |
Get blocked names count |
| POST | /api/contacts/block-name |
Block a name pattern |
| GET | /api/contacts/blocked-names-list |
List blocked name patterns |
| POST | /api/contacts/preview-cleanup |
Preview cleanup criteria |
| POST | /api/contacts/cleanup |
Remove contacts by filter |
| GET | /api/contacts/cleanup-settings |
Get auto-cleanup settings |
| POST | /api/contacts/cleanup-settings |
Update auto-cleanup settings |
| GET | /api/contacts/pending |
Pending contacts (?types=1&types=2) |
| POST | /api/contacts/pending/approve |
Approve pending contact |
| POST | /api/contacts/pending/reject |
Reject pending contact |
| POST | /api/contacts/pending/clear |
Clear all pending contacts |
| POST | /api/contacts/manual-add |
Add contact from URI or params |
| POST | /api/contacts/<key>/push-to-device |
Push cached contact to device |
| POST | /api/contacts/<key>/move-to-cache |
Move device contact to cache |
| GET | /api/contacts/repeaters |
List repeater contacts (for path picker) |
| GET | /api/contacts/<key>/paths |
Get contact paths |
| POST | /api/contacts/<key>/paths |
Add path to contact |
| PUT | /api/contacts/<key>/paths/<id> |
Update path (star, label) |
| DELETE | /api/contacts/<key>/paths/<id> |
Delete path |
| POST | /api/contacts/<key>/paths/reorder |
Reorder paths |
| POST | /api/contacts/<key>/paths/reset_flood |
Reset to FLOOD routing |
| POST | /api/contacts/<key>/paths/clear |
Clear all paths |
| GET | /api/contacts/<key>/no_auto_flood |
Get "Keep path" flag |
| PUT | /api/contacts/<key>/no_auto_flood |
Set "Keep path" flag |
Channels
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/channels |
List all channels |
| POST | /api/channels |
Create new channel (idempotent — returns existing slot if name already used) |
| POST | /api/channels/join |
Join existing channel (idempotent unless explicit index overrides) |
| DELETE | /api/channels/<index> |
Remove channel |
| GET | /api/channels/<index>/qr |
QR code (?format=json|png) |
| GET | /api/channels/muted |
Get muted channels |
| POST | /api/channels/<index>/mute |
Toggle channel mute |
| GET | /api/channels/scopes |
Bulk per-channel region mapping for UI |
| PUT | /api/channels/<index>/scope |
Assign/clear region scope ({region_id: int|null}) |
Regions (MeshCore flood scopes)
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/regions |
List the device's region registry |
| POST | /api/regions |
Create region ({name}); key derived as SHA256('#'+name)[:16] |
| DELETE | /api/regions/<id> |
Delete region; CASCADE clears channel mappings; if it was the firmware default, clears it on device |
| POST | /api/regions/<id>/default |
Mark default in DB AND push to firmware (CMD_SET_DEFAULT_FLOOD_SCOPE = 63, requires firmware v1.15+) |
| DELETE | /api/regions/default |
Clear default region in DB and on firmware |
Direct Messages
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/dm/conversations |
List DM conversations |
| GET | /api/dm/messages |
Get messages (?conversation_id=, ?limit=) |
| POST | /api/dm/messages |
Send DM ({recipient, text}) |
| GET | /api/dm/updates |
Check for new DMs |
| GET | /api/dm/auto_retry |
Get DM retry configuration |
| POST | /api/dm/auto_retry |
Update DM retry configuration |
Device & Settings
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/status |
Connection status (device name, transport type, serial port / BLE address) |
| GET | /api/device/info |
Device information |
| GET | /api/device/stats |
Device statistics |
| GET | /api/device/settings |
Get device settings |
| POST | /api/device/settings |
Update device settings |
| GET | /api/device/config |
Get device configuration (name, coords, advert_loc_policy, path_hash_mode, radio params, tx_power) |
| POST | /api/device/config |
Update device configuration from Settings > Device tab. Subset of fields incl. path_hash_mode (0=1B, 1=2B, 2=3B) |
| POST | /api/device/command |
Execute command (advert, floodadv) |
| GET | /api/device/commands |
List available special commands |
| GET | /api/chat/settings |
Get chat settings (quote length, route popup timeout/no-autoclose) |
| POST | /api/chat/settings |
Update chat settings |
| GET | /api/ui/settings |
Get UI settings (toast timeout, no-autoclose, position) |
| POST | /api/ui/settings |
Update UI settings |
| GET | /api/retention-settings |
Get message retention settings |
| POST | /api/retention-settings |
Update retention settings |
Archives & Backup
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/archives |
List archives |
| POST | /api/archive/trigger |
Manual archive |
| GET | /api/backup/list |
List database backups |
| POST | /api/backup/create |
Create database backup |
| GET | /api/backup/download |
Download backup file |
Other
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/read_status |
Get server-side read status |
| POST | /api/read_status/mark_read |
Mark messages as read |
| POST | /api/read_status/mark_all_read |
Mark all messages as read |
| GET | /api/version |
Get app version |
| GET | /api/check-update |
Check for available updates |
| GET | /api/updater/status |
Get updater service status |
| POST | /api/updater/trigger |
Trigger remote update |
| GET | /api/advertisements |
Get recent advertisements |
| GET | /api/console/history |
Get console command history |
| POST | /api/console/history |
Save console command |
| DELETE | /api/console/history |
Clear console history |
| GET | /api/logs |
Get application logs |
WebSocket API
Console Namespace (/console)
Interactive console via Socket.IO WebSocket connection.
Client → Server:
send_command- Execute command ({command: "infos"})
Server → Client:
console_status- Connection statuscommand_response- Command result ({success, command, output})
Chat Namespace (/chat)
Real-time message delivery via Socket.IO.
Server → Client:
new_channel_message- New channel message receivednew_dm_message- New DM receivedmessage_echo- Echo/ACK update for sent message (includeshash_size)dm_ack- DM delivery confirmationdm_retry_status- Real-time retry progress (dm_id,attempt,max_attempts)dm_retry_failed- All retry attempts exhausted (dm_id)dm_delivered_info- Delivery details after ACK (dm_id,attempt,max_attempts,path,hash_size)path_changed- Contact path discovered/updated (public_key)
Logs Namespace (/logs)
Real-time log streaming via Socket.IO.
Server → Client:
log_line- New log line
Offline Support
The application works completely offline without internet connection. Vendor libraries (Bootstrap, Bootstrap Icons, Socket.IO, Emoji Picker) are bundled locally. A Service Worker provides hybrid caching to ensure functionality without connectivity.