From 7fd92d862b19053765c417fde086f25c273b3996 Mon Sep 17 00:00:00 2001 From: Mikkeli Matlock Date: Mon, 26 Jan 2026 22:22:33 +0900 Subject: [PATCH] websocket comms console widget --- pi/ui/lib/screens/dashboard_screen.dart | 22 ++++++- pi/ui/lib/services/websocket_service.dart | 35 +++++++++-- pi/ui/lib/widgets/debug_console.dart | 72 +++++++++++++++++++++++ 3 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 pi/ui/lib/widgets/debug_console.dart diff --git a/pi/ui/lib/screens/dashboard_screen.dart b/pi/ui/lib/screens/dashboard_screen.dart index c182854..fa39cd0 100644 --- a/pi/ui/lib/screens/dashboard_screen.dart +++ b/pi/ui/lib/screens/dashboard_screen.dart @@ -9,6 +9,7 @@ import '../widgets/navigator_widget.dart'; import '../widgets/stat_box.dart'; import '../widgets/stat_box_main.dart'; import '../widgets/system_bar.dart'; +import '../widgets/debug_console.dart'; // test service for triggers import '../services/test_flipflop_service.dart'; @@ -202,11 +203,26 @@ class _DashboardScreenState extends State { const SizedBox(width: 32), - // Right side: Image display (flex: 1) + // Right side: Navigator with debug console overlay Expanded( flex: 1, - child: Center( - child: NavigatorWidget(key: _navigatorKey), + child: Stack( + children: [ + // Bottom layer: Navigator + Center( + child: NavigatorWidget(key: _navigatorKey), + ), + // Top layer: Debug console on lower half only + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Spacer(), // Top half - empty + const Expanded( + child: DebugConsole(maxLines: 8), + ), + ], + ), + ], ), ), ], diff --git a/pi/ui/lib/services/websocket_service.dart b/pi/ui/lib/services/websocket_service.dart index 67589ea..1ba8e14 100644 --- a/pi/ui/lib/services/websocket_service.dart +++ b/pi/ui/lib/services/websocket_service.dart @@ -73,6 +73,11 @@ class WebSocketService { late StreamController _ackController; late StreamController _alertController; late StreamController _connectionController; + late StreamController _debugController; + + // Debug message buffer + static const int _maxDebugMessages = 50; + final List _debugMessages = []; void _setupStreams() { _arduinoController = StreamController.broadcast(); @@ -81,6 +86,16 @@ class WebSocketService { _ackController = StreamController.broadcast(); _alertController = StreamController.broadcast(); _connectionController = StreamController.broadcast(); + _debugController = StreamController.broadcast(); + } + + /// Log a debug message (adds to buffer and stream) + void _log(String message) { + _debugMessages.add(message); + if (_debugMessages.length > _maxDebugMessages) { + _debugMessages.removeAt(0); + } + _debugController.add(message); } // --- Public API: Streams --- @@ -103,6 +118,12 @@ class WebSocketService { /// Stream of connection state changes Stream get connectionStream => _connectionController.stream; + /// Stream of debug log messages + Stream get debugStream => _debugController.stream; + + /// Current debug message buffer (for initial display) + List get debugMessages => List.unmodifiable(_debugMessages); + // --- Public API: Sync getters (backward compat) --- /// Current connection state @@ -135,25 +156,25 @@ class WebSocketService { }); _socket!.onConnect((_) { - print('[WS] Connected to $_serverUrl'); + _log('connected'); _setConnectionState(WsConnectionState.connected); _cancelReconnect(); }); _socket!.onDisconnect((_) { - print('[WS] Disconnected'); + _log('disconnected'); _setConnectionState(WsConnectionState.disconnected); _scheduleReconnect(); }); _socket!.onConnectError((error) { - print('[WS] Connection error: $error'); + _log('error: $error'); _setConnectionState(WsConnectionState.disconnected); _scheduleReconnect(); }); _socket!.onError((error) { - print('[WS] Error: $error'); + _log('error: $error'); }); // --- Telemetry Events --- @@ -163,6 +184,7 @@ class WebSocketService { final arduino = ArduinoData.fromJson(data); _latestArduino = arduino; _arduinoController.add(arduino); + _log('ard: ${arduino.rpm ?? "-"}rpm ${arduino.voltage ?? "-"}V g${arduino.gear ?? "-"}'); } }); @@ -171,6 +193,7 @@ class WebSocketService { final gps = GpsData.fromJson(data); _latestGps = gps; _gpsController.add(gps); + _log('gps: ${gps.speed?.toStringAsFixed(1) ?? "-"}m/s mode${gps.mode ?? "-"}'); } }); @@ -182,6 +205,7 @@ class WebSocketService { ); _latestStatus = status; _statusController.add(status); + _log('status: gps=${status.gpsConnected} ard=${status.arduinoConnected}'); } }); @@ -196,6 +220,7 @@ class WebSocketService { extra: data['extra'], ); _ackController.add(ack); + _log('ack: ${ack.id}=${ack.status}${ack.error != null ? " err:${ack.error}" : ""}'); } }); @@ -206,6 +231,7 @@ class WebSocketService { message: data['message'] ?? '', ); _alertController.add(alert); + _log('alert: [${alert.type}] ${alert.message}'); } }); @@ -283,5 +309,6 @@ class WebSocketService { _ackController.close(); _alertController.close(); _connectionController.close(); + _debugController.close(); } } diff --git a/pi/ui/lib/widgets/debug_console.dart b/pi/ui/lib/widgets/debug_console.dart new file mode 100644 index 0000000..a223352 --- /dev/null +++ b/pi/ui/lib/widgets/debug_console.dart @@ -0,0 +1,72 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; + +import '../services/websocket_service.dart'; +import '../theme/app_theme.dart'; + +/// Self-contained debug console that displays WebSocket log messages. +/// Subscribes to WebSocketService.debugStream internally. +class DebugConsole extends StatefulWidget { + /// Maximum lines to display + final int maxLines; + + const DebugConsole({ + super.key, + this.maxLines = 8, + }); + + @override + State createState() => _DebugConsoleState(); +} + +class _DebugConsoleState extends State { + final List _messages = []; + StreamSubscription? _debugSub; + + @override + void initState() { + super.initState(); + + // Initialize with existing buffer + _messages.addAll(WebSocketService.instance.debugMessages); + _trimMessages(); + + // Subscribe to new messages + _debugSub = WebSocketService.instance.debugStream.listen((msg) { + setState(() { + _messages.add(msg); + _trimMessages(); + }); + }); + } + + void _trimMessages() { + while (_messages.length > widget.maxLines) { + _messages.removeAt(0); + } + } + + @override + void dispose() { + _debugSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = AppTheme.of(context); + + return Container( + padding: const EdgeInsets.all(4), + child: Text( + _messages.isEmpty ? '(no messages)' : _messages.join('\n'), + style: TextStyle( + fontFamily: 'monospace', + fontSize: 34, + color: theme.foreground, + height: 1.2, + ), + ), + ); + } +}