From 80e94054496c7d180fd54f8cc08bffd602e61660 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Thu, 25 Dec 2025 11:27:17 +0100 Subject: [PATCH] feat: Add Network Commands section with advert and floodadv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new menu section "Network Commands" with two special commands: - Send Advert: sends single advertisement (recommended for normal use) - Flood Advert: floods network with advertisement (for recovery only) Changes: - cli.py: Add advert() and floodadv() functions - api.py: Add POST /api/device/command and GET /api/device/commands endpoints - base.html: Add Network Commands section to slide-out menu - app.js: Add JavaScript handlers with confirmation for floodadv - README.md: Document new Network Commands feature 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 26 ++++++++++++ app/meshcore/cli.py | 36 +++++++++++++++++ app/routes/api.py | 88 +++++++++++++++++++++++++++++++++++++++++ app/static/js/app.js | 58 +++++++++++++++++++++++++++ app/templates/base.html | 23 +++++++++++ 5 files changed, 231 insertions(+) diff --git a/README.md b/README.md index aa5b295..dac1d3d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ A lightweight web interface for meshcore-cli, providing browser-based access to - 🧹 **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 @@ -258,6 +259,31 @@ Access the settings panel to clean up inactive contacts: 3. Click "Clean Inactive Contacts" 4. 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. + +1. Click the menu icon (☰) in the navbar +2. Click "Send Advert" under Network Commands +3. 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. + +1. Click the menu icon (☰) in the navbar +2. Click "Flood Advert" (highlighted in warning color) +3. Confirm you want to proceed +4. Wait for confirmation toast + ## Docker Commands ```bash diff --git a/app/meshcore/cli.py b/app/meshcore/cli.py index 4ec7fc2..e08175c 100644 --- a/app/meshcore/cli.py +++ b/app/meshcore/cli.py @@ -264,3 +264,39 @@ def remove_channel(index: int) -> Tuple[bool, str]: success, stdout, stderr = _run_command(['remove_channel', str(index)]) return success, stdout or stderr + + +# ============================================================================= +# Special Commands (Network Advertisement) +# ============================================================================= + +def advert() -> Tuple[bool, str]: + """ + Send a single advertisement frame to the mesh network. + + This is the recommended way to announce node presence. + Uses minimal airtime and follows normal routing rules. + + Returns: + Tuple of (success, message) + """ + success, stdout, stderr = _run_command(['advert']) + return success, stdout or stderr + + +def floodadv() -> Tuple[bool, str]: + """ + Send advertisement in flooding mode (broadcast storm). + + WARNING: This should be used sparingly! It causes high airtime usage + and can destabilize larger networks. Use only for: + - Initial network bootstrap + - After device reset/firmware change + - When routing is broken + - Debug/testing purposes + + Returns: + Tuple of (success, message) + """ + success, stdout, stderr = _run_command(['floodadv']) + return success, stdout or stderr diff --git a/app/routes/api.py b/app/routes/api.py index 3581985..a54c936 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -298,6 +298,94 @@ def get_device_info(): }), 500 +# ============================================================================= +# Special Commands +# ============================================================================= + +# Registry of available special commands +SPECIAL_COMMANDS = { + 'advert': { + 'function': cli.advert, + 'description': 'Send single advertisement (recommended)', + }, + 'floodadv': { + 'function': cli.floodadv, + 'description': 'Flood advertisement (use sparingly!)', + }, +} + + +@api_bp.route('/device/command', methods=['POST']) +def execute_special_command(): + """ + Execute a special device command. + + JSON body: + command (str): Command name (required) - one of: advert, floodadv + + Returns: + JSON with command result + """ + try: + data = request.get_json() + + if not data or 'command' not in data: + return jsonify({ + 'success': False, + 'error': 'Missing required field: command' + }), 400 + + command = data['command'].strip().lower() + + if command not in SPECIAL_COMMANDS: + return jsonify({ + 'success': False, + 'error': f'Unknown command: {command}. Available commands: {", ".join(SPECIAL_COMMANDS.keys())}' + }), 400 + + # Execute the command + cmd_info = SPECIAL_COMMANDS[command] + success, message = cmd_info['function']() + + if success: + return jsonify({ + 'success': True, + 'command': command, + 'message': message or f'{command} executed successfully' + }), 200 + else: + return jsonify({ + 'success': False, + 'command': command, + 'error': message + }), 500 + + except Exception as e: + logger.error(f"Error executing special command: {e}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@api_bp.route('/device/commands', methods=['GET']) +def list_special_commands(): + """ + List available special commands. + + Returns: + JSON with list of available commands + """ + commands = [ + {'name': name, 'description': info['description']} + for name, info in SPECIAL_COMMANDS.items() + ] + return jsonify({ + 'success': True, + 'commands': commands + }), 200 + + @api_bp.route('/sync', methods=['POST']) def sync_messages(): """ diff --git a/app/static/js/app.js b/app/static/js/app.js index ec85fd7..5b8c1ec 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -221,6 +221,19 @@ function setupEventListeners() { document.getElementById('scanQRBtn').addEventListener('click', function() { showNotification('QR scanning feature coming soon! For now, manually enter the channel details.', 'info'); }); + + // Network Commands: Advert button + document.getElementById('advertBtn').addEventListener('click', async function() { + await executeSpecialCommand('advert'); + }); + + // Network Commands: Flood Advert button (with confirmation) + document.getElementById('floodadvBtn').addEventListener('click', async function() { + if (!confirm('Flood Advertisement uses high airtime and should only be used for network recovery.\n\nAre you sure you want to proceed?')) { + return; + } + await executeSpecialCommand('floodadv'); + }); } /** @@ -462,6 +475,51 @@ async function cleanupContacts() { } } +/** + * Execute a special device command (advert, floodadv, etc.) + */ +async function executeSpecialCommand(command) { + // Get button element to disable during execution + const btnId = command === 'advert' ? 'advertBtn' : 'floodadvBtn'; + const btn = document.getElementById(btnId); + + if (btn) { + btn.disabled = true; + } + + try { + const response = await fetch('/api/device/command', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ command: command }) + }); + + const data = await response.json(); + + if (data.success) { + showNotification(data.message || `${command} sent successfully`, 'success'); + } else { + showNotification(`Command failed: ${data.error}`, 'danger'); + } + + // Close offcanvas menu after command execution + const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('mainMenu')); + if (offcanvas) { + offcanvas.hide(); + } + + } catch (error) { + console.error(`Error executing ${command}:`, error); + showNotification(`Failed to execute ${command}`, 'danger'); + } finally { + if (btn) { + btn.disabled = false; + } + } +} + /** * Setup intelligent auto-refresh * Checks for updates regularly but only refreshes UI when new messages arrive diff --git a/app/templates/base.html b/app/templates/base.html index aae61bc..1884f6a 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -73,6 +73,29 @@ + + +
+ Network Commands +
+ + + +
+ Configuration +