Files
mc-webui/app/main.py
MarekWo badf67cf74 feat(v2): Rewrite main.py and cli.py for direct device communication
main.py: Initialize Database + DeviceManager in create_app(), replace
bridge-dependent startup code, simplified console command router.
cli.py: All functions now delegate to DeviceManager instead of HTTP
bridge calls. Same signatures preserved for api.py compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 07:23:59 +01:00

238 lines
7.5 KiB
Python

"""
mc-webui v2 — Flask application entry point
Direct device communication via meshcore library (no bridge).
"""
import logging
import shlex
import threading
import time
from flask import Flask, request as flask_request
from flask_socketio import SocketIO, emit
from app.config import config, runtime_config
from app.database import Database
from app.device_manager import DeviceManager
from app.routes.views import views_bp
from app.routes.api import api_bp
from app.version import VERSION_STRING, GIT_BRANCH
# Configure logging
logging.basicConfig(
level=getattr(logging, config.MC_LOG_LEVEL, logging.INFO),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Filter to suppress known werkzeug WebSocket errors
class WerkzeugWebSocketFilter(logging.Filter):
def filter(self, record):
if record.levelno == logging.ERROR:
if 'write() before start_response' in str(record.msg):
return False
if record.exc_info and record.exc_info[1]:
if 'write() before start_response' in str(record.exc_info[1]):
return False
return True
logging.getLogger('werkzeug').addFilter(WerkzeugWebSocketFilter())
# Initialize SocketIO globally
socketio = SocketIO()
# Global references (set in create_app)
db = None
device_manager = None
def create_app():
"""Create and configure Flask application"""
global db, device_manager
app = Flask(__name__)
# Load configuration
app.config['DEBUG'] = config.FLASK_DEBUG
app.config['SECRET_KEY'] = 'mc-webui-secret-key-change-in-production'
# Inject version and branch into all templates
@app.context_processor
def inject_version():
return {'version': VERSION_STRING, 'git_branch': GIT_BRANCH}
# Register blueprints
app.register_blueprint(views_bp)
app.register_blueprint(api_bp)
# Initialize SocketIO
socketio.init_app(app, cors_allowed_origins="*", async_mode='threading')
# v2: Initialize database
db = Database(config.db_path)
app.db = db
# v2: Initialize and start device manager
device_manager = DeviceManager(config, db, socketio)
app.device_manager = device_manager
# Start device connection in background (non-blocking)
device_manager.start()
# Update runtime config when device connects
def _wait_for_device_name():
"""Wait for device manager to connect and update runtime config."""
for _ in range(60): # wait up to 60 seconds
time.sleep(1)
if device_manager.is_connected:
runtime_config.set_device_name(
device_manager.device_name, "device"
)
logger.info(f"Device name resolved: {device_manager.device_name}")
return
logger.warning("Timeout waiting for device connection")
threading.Thread(target=_wait_for_device_name, daemon=True).start()
logger.info(f"mc-webui v2 started — transport: {'TCP' if config.use_tcp else 'serial'}")
logger.info(f"Database: {config.db_path}")
return app
# ============================================================
# WebSocket handlers for Console
# ============================================================
@socketio.on('connect', namespace='/console')
def handle_console_connect():
"""Handle console WebSocket connection"""
logger.info("Console WebSocket client connected")
emit('console_status', {'message': 'Connected to mc-webui console'})
@socketio.on('disconnect', namespace='/console')
def handle_console_disconnect():
"""Handle console WebSocket disconnection"""
logger.info("Console WebSocket client disconnected")
@socketio.on('send_command', namespace='/console')
def handle_send_command(data):
"""Handle command from console client — route through DeviceManager."""
command = data.get('command', '').strip()
sid = flask_request.sid
if not command:
emit('command_response', {'success': False, 'error': 'Empty command'})
return
logger.info(f"Console command received: {command}")
def execute_and_respond():
try:
try:
args = shlex.split(command)
except ValueError:
args = command.split()
if not args:
socketio.emit('command_response', {
'success': False, 'command': command, 'error': 'Empty command'
}, room=sid, namespace='/console')
return
output = _execute_console_command(args)
socketio.emit('command_response', {
'success': True,
'command': command,
'output': output or '(no output)'
}, room=sid, namespace='/console')
except Exception as e:
logger.error(f"Console command error: {e}")
socketio.emit('command_response', {
'success': False,
'command': command,
'error': str(e)
}, room=sid, namespace='/console')
socketio.start_background_task(execute_and_respond)
def _execute_console_command(args: list) -> str:
"""
Execute a console command via DeviceManager.
Maps meshcli-style text commands to DeviceManager methods.
Simplified router — full ConsoleRouter planned for Phase 2.
"""
cmd = args[0].lower()
if not device_manager or not device_manager.is_connected:
return "Error: Device not connected"
if cmd == 'infos':
info = device_manager.get_device_info()
if info:
lines = [f" {k}: {v}" for k, v in info.items()]
return "Device Info:\n" + "\n".join(lines)
return "No device info available"
elif cmd == 'contacts':
contacts = device_manager.get_contacts_from_device()
if not contacts:
return "No contacts"
lines = []
for c in contacts:
name = c.get('name', '?')
pk = c.get('public_key', '')[:8]
lines.append(f" {name} ({pk}...)")
return f"Contacts ({len(contacts)}):\n" + "\n".join(lines)
elif cmd == 'bat':
bat = device_manager.get_battery()
if bat:
return f"Battery: {bat}"
return "Battery info unavailable"
elif cmd in ('advert', 'floodadv'):
result = device_manager.send_advert(flood=(cmd == 'floodadv'))
return result.get('message', result.get('error', 'Unknown'))
elif cmd == 'chan' and len(args) >= 3:
try:
ch_idx = int(args[1])
text = ' '.join(args[2:])
result = device_manager.send_channel_message(ch_idx, text)
return result.get('message', result.get('error', 'Unknown'))
except (ValueError, IndexError):
return "Usage: chan <channel_idx> <message>"
elif cmd == 'msg' and len(args) >= 3:
recipient = args[1]
text = ' '.join(args[2:])
contact = device_manager.mc.get_contact_by_name(recipient)
if contact:
pubkey = contact.get('public_key', recipient)
else:
pubkey = recipient
result = device_manager.send_dm(pubkey, text)
return result.get('message', result.get('error', 'Unknown'))
else:
return f"Unknown command: {cmd}\nAvailable: infos, contacts, bat, advert, floodadv, chan, msg"
if __name__ == '__main__':
app = create_app()
socketio.run(
app,
host=config.FLASK_HOST,
port=config.FLASK_PORT,
debug=config.FLASK_DEBUG,
allow_unsafe_werkzeug=True
)