feat: Add Network Commands section with advert and floodadv

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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2025-12-25 11:27:17 +01:00
parent 47911884b3
commit 80e9405449
5 changed files with 231 additions and 0 deletions
+26
View File
@@ -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
+36
View File
@@ -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
+88
View File
@@ -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():
"""
+58
View File
@@ -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
+23
View File
@@ -73,6 +73,29 @@
<!-- Archive dates loaded dynamically via JavaScript -->
</select>
</div>
<!-- Network Commands Section -->
<div class="list-group-item py-2 mt-2">
<small class="text-muted fw-bold text-uppercase">Network Commands</small>
</div>
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" id="advertBtn" title="Send single advertisement (recommended for normal operation)">
<i class="bi bi-megaphone" style="font-size: 1.5rem;"></i>
<div>
<span>Send Advert</span>
<small class="d-block text-muted">Announce presence (normal)</small>
</div>
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3 text-warning" id="floodadvBtn" title="Flood advertisement - use sparingly! High airtime usage.">
<i class="bi bi-broadcast" style="font-size: 1.5rem;"></i>
<div>
<span>Flood Advert</span>
<small class="d-block text-muted">Network recovery only!</small>
</div>
</button>
<div class="list-group-item py-2 mt-2">
<small class="text-muted fw-bold text-uppercase">Configuration</small>
</div>
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#settingsModal" data-bs-dismiss="offcanvas">
<i class="bi bi-gear" style="font-size: 1.5rem;"></i>
<span>Settings</span>