Updated documentation to reflect the fundamental architectural change from per-request subprocess spawning to a persistent meshcli session in meshcore-bridge. Changes: - Updated README.md with detailed bridge session architecture section - Added TZ environment variable to configuration table - Created comprehensive technical note (technotes/persistent-meshcli-session.md) documenting the refactor, implementation details, and benefits Key architectural changes documented: - Single subprocess.Popen with stdin/stdout pipes (not subprocess.run per request) - Multiplexing: JSON adverts → .adverts.jsonl log, CLI responses → HTTP - Real-time message reception via msgs_subscribe (no polling required) - Thread-safe command queue with event-based synchronization - Watchdog thread for automatic crash recovery - Timeout-based response detection (300ms idle threshold) This persistent session enables: ✅ Real-time message reception without polling ✅ Network advertisement logging ✅ Advanced interactive features (manual_add_contacts, etc.) ✅ Better stability and lower latency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
17 KiB
mc-webui
A lightweight web interface for meshcore-cli, providing browser-based access to MeshCore mesh network.
Overview
mc-webui is a Flask-based web application that wraps meshcore-cli, eliminating the need for SSH/terminal access when using MeshCore chat on a LoRa device connected to a Debian VM via BLE or USB. Tested on Heltec V4.
Key Features
- 📱 Mobile-first design - Optimized responsive UI with slide-out menu for small screens
- 💬 View messages - Display chat history with intelligent auto-refresh
- 🔔 Smart notifications - Bell icon with unread message counter across all channels
- 📊 Per-channel badges - Unread count displayed on each channel in selector
- ✉️ Send messages - Publish to any channel (140 byte limit for LoRa)
- 💌 Direct messages (DM) - Send and receive private messages with delivery status tracking
- 📡 Channel management - Create, join, and switch between encrypted channels
- 🔐 Channel sharing - Share channels via QR code or encrypted keys
- 🔓 Public channels - Join public channels (starting with #) without encryption keys
- 🎯 Reply to users - Quick reply with
@[UserName]format - 🧹 Clean contacts - Remove inactive contacts with configurable threshold
- 📦 Message archiving - Automatic daily archiving with browse-by-date selector
- ⚡ Efficient polling - Lightweight update checks every 10s, UI refreshes only when needed
- 📡 Network commands - Send advertisement (advert) or flood advertisement (floodadv) for network management
Tech Stack
- Backend: Python 3.11+, Flask
- Frontend: HTML5, Bootstrap 5, vanilla JavaScript
- Deployment: Docker / Docker Compose (2-container architecture)
- Communication: HTTP bridge to meshcore-cli (USB isolation for stability)
- Data source:
~/.config/meshcore/<device_name>.msgs(JSON Lines)
Quick Start
Prerequisites
- Docker and Docker Compose installed
- Heltec V4 device connected via USB
Note: meshcore-cli is automatically installed inside the Docker container - no host installation required!
Important: This application requires meshcore-cli version 1.3.12 or newer for proper Direct Messages (DM) functionality. The Docker container automatically installs the latest version.
Installation
-
Clone the repository
git clone <repository-url> cd mc-webui -
Configure environment
cp .env.example .env # Edit .env with your settings nano .env -
Find your serial device
ls -l /dev/serial/by-id/Update
MC_SERIAL_PORTin.envwith your device path. -
Build and run
docker compose up -d --build -
Access the web interface Open your browser and navigate to:
http://localhost:5000Or from another device on your network:
http://<server-ip>:5000
Configuration
All configuration is done via environment variables in the .env file:
| Variable | Description | Default |
|---|---|---|
MC_SERIAL_PORT |
Path to serial device | /dev/ttyUSB0 |
MC_DEVICE_NAME |
Device name (for .msgs and .adverts.jsonl files) | MeshCore |
MC_CONFIG_DIR |
meshcore configuration directory | /root/.config/meshcore |
MC_REFRESH_INTERVAL |
Auto-refresh interval (seconds) | 60 |
MC_INACTIVE_HOURS |
Inactivity threshold for cleanup | 48 |
MC_ARCHIVE_DIR |
Archive directory path | /mnt/archive/meshcore |
MC_ARCHIVE_ENABLED |
Enable automatic archiving | true |
MC_ARCHIVE_RETENTION_DAYS |
Days to show in live view | 7 |
FLASK_HOST |
Listen address | 0.0.0.0 |
FLASK_PORT |
Application port | 5000 |
FLASK_DEBUG |
Debug mode | false |
TZ |
Timezone for container logs | UTC |
See .env.example for a complete example.
Architecture
mc-webui uses a 2-container architecture for improved USB stability:
-
meshcore-bridge - Lightweight service with exclusive USB device access
- Maintains a persistent meshcli session (single long-lived process)
- Multiplexes stdout: JSON adverts →
.adverts.jsonllog, CLI commands → HTTP responses - Real-time message reception via
msgs_subscribe(no polling) - Thread-safe command queue with event-based synchronization
- Watchdog thread for automatic crash recovery
- Exposes HTTP API on port 5001 (internal only)
-
mc-webui - Main web application
- Flask-based web interface
- Communicates with bridge via HTTP
- No direct USB access (prevents device locking)
This separation solves USB timeout/deadlock issues common in Docker + VM environments.
Bridge Session Architecture
The meshcore-bridge maintains a single persistent meshcli session instead of spawning new processes per request:
- Single subprocess.Popen - One long-lived meshcli process with stdin/stdout pipes
- Multiplexing - Intelligently routes output:
- JSON adverts (with
payload_typename: "ADVERT") → logged to{device_name}.adverts.jsonl - CLI command responses → returned via HTTP API
- JSON adverts (with
- Real-time messages -
msgs_subscribecommand enables instant message reception without polling - Thread-safe queue - Commands are serialized through a queue.Queue for FIFO execution
- Timeout-based detection - Response completion detected when no new lines arrive for 300ms
- Auto-restart watchdog - Monitors process health and restarts on crash
This architecture enables advanced features like pending contact management (manual_add_contacts) and provides better stability and performance.
Project Structure
mc-webui/
├── Dockerfile # Main app Docker image
├── docker-compose.yml # Multi-container orchestration
├── meshcore-bridge/
│ ├── Dockerfile # Bridge service image
│ ├── bridge.py # HTTP API wrapper for meshcli
│ └── requirements.txt # Bridge dependencies (Flask only)
├── app/
│ ├── __init__.py
│ ├── main.py # Flask entry point
│ ├── config.py # Configuration from env vars
│ ├── meshcore/
│ │ ├── __init__.py
│ │ ├── cli.py # HTTP client for bridge API
│ │ └── parser.py # .msgs file parser
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── api.py # REST API endpoints
│ │ └── views.py # HTML views
│ ├── static/
│ │ ├── css/
│ │ │ └── style.css # Custom styles
│ │ └── js/
│ │ ├── app.js # Main page frontend logic
│ │ └── dm.js # Direct Messages page logic
│ └── templates/
│ ├── base.html # Base template
│ ├── index.html # Main chat view
│ ├── dm.html # Direct Messages full-page view
│ └── components/ # Reusable components
├── requirements.txt # Python dependencies
├── .env.example # Example environment config
├── .gitignore
└── README.md # This file
Development Status
🚀 Core Features Complete ✅
Completed Features
- Environment Setup & Docker Architecture
- Backend Basics (REST API, message parsing, CLI wrapper)
- Frontend Chat View (Bootstrap UI, message display)
- Message Sending (Send form, reply functionality)
- Intelligent Auto-refresh (10s checks, UI updates only when needed)
- Contact Management (Cleanup modal with configurable threshold)
- Channel Management (Create, join, share via QR, delete with auto-cleanup)
- Public Channels (# prefix support, auto-key generation)
- Message Archiving (Daily archiving with browse-by-date selector)
- Smart Notifications (Unread counters per channel and total)
- Direct Messages (DM) - Private messaging with delivery status tracking
Next Steps
- Performance Optimization - Frontend and backend improvements
- Enhanced Testing - Unit and integration tests
- Documentation Polish - API docs and usage guides
Usage
Viewing Messages
The main page displays chat history from the currently selected channel. The app uses an intelligent refresh system that checks for new messages every 10 seconds and updates the UI only when new messages actually arrive.
Unread notifications:
- Bell icon in navbar shows total unread count across all channels
- Channel badges display unread count per channel (e.g., "Malopolska (3)")
- Messages are automatically marked as read when you view them
- Read status persists across browser sessions
By default, the live view shows messages from the last 7 days. Older messages are automatically archived and can be accessed via the date selector.
Managing Channels
Access channel management:
- Click the menu icon (☰) in the navbar
- Select "Manage Channels" from the slide-out menu
Creating a New Channel
- Click "Add New Channel"
- Enter a channel name (letters, numbers, _ and - only)
- Click "Create & Auto-generate Key"
- The channel is created with a secure encryption key
Sharing a Channel
- In the Channels modal, click the share icon next to any channel
- Share the QR code (scan with another device) or copy the encryption key
- Others can join using the "Join Existing" option
Joining a Channel
For private channels:
- Click "Join Existing"
- Enter the channel name and encryption key (received from channel creator)
- Click "Join Channel"
- The channel will be added to your available channels
For public channels (starting with #):
- Click "Join Existing"
- Enter the channel name (e.g.,
#test,#krakow) - Leave the encryption key field empty (key is auto-generated based on channel name)
- Click "Join Channel"
- You can now chat with other users on the same public channel
Deleting a Channel
- In the Channels modal, click the delete icon (trash) next to any channel
- Confirm the deletion
- The channel configuration and all its messages will be permanently removed
Note: Deleting a channel removes all message history for that channel from your device to prevent data leakage when reusing channel slots.
Switching Channels
Use the channel selector dropdown in the navbar to switch between channels. Your selection is remembered between sessions.
Viewing Message Archives
Access historical messages using the date selector:
- Click the menu icon (☰) in the navbar
- Under "Message History" select a date to view archived messages for that day
- Select "Today (Live)" to return to live view
Archives are created automatically at midnight (00:00 UTC) each day. The live view always shows the most recent messages (last 7 days by default).
Sending Messages
- Select your target channel using the channel selector
- Type your message in the text field at the bottom
- Press Enter or click "Send"
- Your message will be published to the selected channel
Replying to Users
Click the reply button on any message to insert @[UserName] into the text field, then type your reply.
Direct Messages (DM)
Access the Direct Messages feature:
From the menu:
- Click the menu icon (☰) in the navbar
- Select "Direct Messages" from the menu
- Opens a dedicated full-page DM view
Using the DM page:
- Select a recipient from the dropdown at the top:
- Existing conversations are shown first (with message history)
- Separator: "--- Available contacts ---"
- All client contacts from your device (only CLI type, no repeaters/rooms)
- You can start a new conversation with anyone in your contacts list
- Type your message in the input field (max 140 bytes, same as channels)
- Use the emoji picker button to insert emojis
- Press Enter or click Send
- Click "Back" button to return to the main chat view
Note: Only client contacts (CLI) are shown in the dropdown. Repeaters (REP), rooms (ROOM), and sensors (SENS) are automatically filtered out.
Message status indicators:
- ⏳ Pending (clock icon, yellow) - Message sent, awaiting delivery confirmation
- Note: Due to meshcore-cli limitations, we cannot track actual delivery status
Notifications:
- The bell icon shows a secondary green badge for unread DMs
- Each conversation shows unread indicator (*) in the dropdown
- DM badge in the menu shows total unread DM count
Managing Contacts
Access the settings panel to clean up inactive contacts:
- Click the settings icon
- Adjust the inactivity threshold (default: 48 hours)
- Click "Clean Inactive Contacts"
- Confirm the action
Network Commands
Access network commands from the slide-out menu under "Network Commands" section:
Send Advert (Recommended)
Sends a single advertisement frame to announce your node's presence in the mesh network. This is the normal, energy-efficient way to advertise.
- Click the menu icon (☰) in the navbar
- Click "Send Advert" under Network Commands
- Wait for confirmation toast
Flood Advert (Use Sparingly!)
Sends advertisement in flooding mode, forcing all nodes to retransmit. Use only when:
- Starting a completely new network
- After device reset or firmware change
- When routing is broken and node is not visible
- For debugging/testing purposes
⚠️ Warning: Flood advertisement causes high airtime usage and can destabilize larger LoRa networks. A confirmation dialog will appear before execution.
- Click the menu icon (☰) in the navbar
- Click "Flood Advert" (highlighted in warning color)
- Confirm you want to proceed
- Wait for confirmation toast
Docker Commands
# Start the application
docker compose up -d
# View logs
docker compose logs -f
# Stop the application
docker compose down
# Rebuild after code changes
docker compose up -d --build
# Check container status
docker compose ps
Troubleshooting
Device not found
# Check if device is connected
ls -l /dev/serial/by-id/
# Verify device permissions
sudo chmod 666 /dev/serial/by-id/usb-Espressif*
Container won't start
# Check logs for both services
docker compose logs meshcore-bridge
docker compose logs mc-webui
# Verify .env file exists
ls -la .env
# Check if ports are available
sudo netstat -tulpn | grep -E '5000|5001'
USB Communication Issues
The 2-container architecture resolves common USB timeout/deadlock problems:
- meshcore-bridge has exclusive USB access
- mc-webui uses HTTP (no direct device access)
- Restarting
mc-webuidoes not affect USB connection - If bridge has USB issues, restart only that service:
docker compose restart meshcore-bridge
Bridge connection errors
# Check bridge health
docker compose exec mc-webui curl http://meshcore-bridge:5001/health
# Bridge logs
docker compose logs -f meshcore-bridge
# Test meshcli directly in bridge container
docker compose exec meshcore-bridge meshcli -s /dev/ttyUSB0 infos
Messages not updating
- Check that
.msgsfile exists inMC_CONFIG_DIR - Verify bridge service is healthy:
docker compose ps - Check bridge logs for command errors
Security Notes
⚠️ Important: This application is designed for trusted local networks only and has no authentication. Do not expose it to the internet without implementing proper security measures.
Contributing
This is an open-source project. Contributions are welcome!
- All code, comments, and documentation must be in English
- Follow the existing code style
- Test your changes with real hardware if possible
License
References
Contact





