2026-01-25 18:47:35 +09:00
|
|
|
import 'dart:async';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
2026-01-26 16:50:52 +09:00
|
|
|
import '../services/backend_service.dart';
|
|
|
|
|
import '../services/websocket_service.dart';
|
2026-01-25 19:23:03 +09:00
|
|
|
import '../services/pi_io.dart';
|
2026-01-26 00:20:52 +09:00
|
|
|
import '../theme/app_theme.dart';
|
|
|
|
|
import '../widgets/navigator_widget.dart';
|
2026-01-25 18:47:35 +09:00
|
|
|
import '../widgets/stat_box.dart';
|
2026-01-26 15:47:59 +09:00
|
|
|
import '../widgets/stat_box_main.dart';
|
|
|
|
|
import '../widgets/system_bar.dart';
|
2026-01-26 22:22:33 +09:00
|
|
|
import '../widgets/debug_console.dart';
|
2026-02-01 17:01:45 +09:00
|
|
|
import '../widgets/whiskey_mark.dart';
|
2026-02-04 23:19:24 +09:00
|
|
|
import '../widgets/accel_graph.dart';
|
2026-01-25 18:47:35 +09:00
|
|
|
|
2026-01-26 00:20:52 +09:00
|
|
|
// test service for triggers
|
|
|
|
|
import '../services/test_flipflop_service.dart';
|
|
|
|
|
|
2026-01-25 19:23:03 +09:00
|
|
|
/// Main dashboard - displays Pi vitals and placeholder stats
|
2026-01-25 18:47:35 +09:00
|
|
|
class DashboardScreen extends StatefulWidget {
|
|
|
|
|
const DashboardScreen({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _DashboardScreenState extends State<DashboardScreen> {
|
2026-01-26 00:20:52 +09:00
|
|
|
final _navigatorKey = GlobalKey<NavigatorWidgetState>();
|
2026-01-25 18:47:35 +09:00
|
|
|
|
2026-01-26 16:50:52 +09:00
|
|
|
// Timer for Pi temp only (safety critical, direct file read)
|
|
|
|
|
Timer? _piTempTimer;
|
|
|
|
|
|
|
|
|
|
// WebSocket stream subscriptions
|
|
|
|
|
StreamSubscription<ArduinoData>? _arduinoSub;
|
|
|
|
|
StreamSubscription<GpsData>? _gpsSub;
|
|
|
|
|
StreamSubscription<WsConnectionState>? _connectionSub;
|
|
|
|
|
|
|
|
|
|
// Pi temperature - direct file read (safety critical)
|
2026-01-25 19:23:03 +09:00
|
|
|
double? _piTemp;
|
2026-01-26 16:50:52 +09:00
|
|
|
|
|
|
|
|
// From backend - Arduino data
|
|
|
|
|
int? _rpm;
|
|
|
|
|
double? _voltage;
|
|
|
|
|
int? _engineTemp;
|
|
|
|
|
int? _gear;
|
2026-02-01 17:01:45 +09:00
|
|
|
double? _roll;
|
|
|
|
|
double? _pitch;
|
2026-02-04 23:19:24 +09:00
|
|
|
double? _ax;
|
|
|
|
|
double? _ay;
|
2026-01-26 16:50:52 +09:00
|
|
|
|
|
|
|
|
// From backend - GPS data
|
|
|
|
|
double? _gpsSpeed;
|
2026-01-26 15:47:59 +09:00
|
|
|
|
|
|
|
|
// Placeholder values for system bar
|
|
|
|
|
int? _gpsSatellites;
|
|
|
|
|
int? _lteSignal;
|
2026-01-25 18:47:35 +09:00
|
|
|
|
2026-01-26 16:50:52 +09:00
|
|
|
// WebSocket connection state
|
|
|
|
|
WsConnectionState _wsState = WsConnectionState.disconnected;
|
|
|
|
|
|
2026-01-25 18:47:35 +09:00
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2026-01-25 19:23:03 +09:00
|
|
|
|
2026-01-26 16:50:52 +09:00
|
|
|
// Connect to WebSocket
|
|
|
|
|
WebSocketService.instance.connect();
|
|
|
|
|
|
|
|
|
|
// Subscribe to Arduino data stream
|
|
|
|
|
_arduinoSub = WebSocketService.instance.arduinoStream.listen((data) {
|
2026-01-25 18:47:35 +09:00
|
|
|
setState(() {
|
2026-01-26 16:50:52 +09:00
|
|
|
_voltage = data.voltage;
|
|
|
|
|
_rpm = data.rpm;
|
|
|
|
|
_engineTemp = data.engTemp;
|
|
|
|
|
_gear = data.gear;
|
2026-02-01 17:01:45 +09:00
|
|
|
_roll = data.roll;
|
|
|
|
|
_pitch = data.pitch;
|
2026-02-04 23:19:24 +09:00
|
|
|
_ax = data.ax;
|
|
|
|
|
_ay = data.ay;
|
2026-01-26 16:50:52 +09:00
|
|
|
});
|
|
|
|
|
});
|
2026-01-25 19:23:03 +09:00
|
|
|
|
2026-01-26 16:50:52 +09:00
|
|
|
// Subscribe to GPS data stream
|
|
|
|
|
_gpsSub = WebSocketService.instance.gpsStream.listen((data) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_gpsSpeed = data.speed;
|
|
|
|
|
// Derive satellites from mode (placeholder logic)
|
|
|
|
|
_gpsSatellites = data.mode == 3 ? 8 : (data.mode == 2 ? 4 : 0);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-26 15:47:59 +09:00
|
|
|
|
2026-01-26 16:50:52 +09:00
|
|
|
// Subscribe to connection state
|
|
|
|
|
_connectionSub = WebSocketService.instance.connectionStream.listen((state) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_wsState = state;
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-26 15:47:59 +09:00
|
|
|
|
2026-01-26 16:50:52 +09:00
|
|
|
// Timer for Pi temp only (safety critical - bypasses backend)
|
|
|
|
|
_piTempTimer = Timer.periodic(const Duration(milliseconds: 500), (_) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_piTemp = PiIO.instance.getTemperature();
|
2026-01-25 18:47:35 +09:00
|
|
|
});
|
|
|
|
|
});
|
2026-01-26 00:20:52 +09:00
|
|
|
|
2026-01-26 16:50:52 +09:00
|
|
|
// Initialize with any cached data from WebSocketService
|
|
|
|
|
final cachedArduino = WebSocketService.instance.latestArduino;
|
|
|
|
|
if (cachedArduino != null) {
|
|
|
|
|
_voltage = cachedArduino.voltage;
|
|
|
|
|
_rpm = cachedArduino.rpm;
|
|
|
|
|
_engineTemp = cachedArduino.engTemp;
|
|
|
|
|
_gear = cachedArduino.gear;
|
2026-02-01 17:01:45 +09:00
|
|
|
_roll = cachedArduino.roll;
|
|
|
|
|
_pitch = cachedArduino.pitch;
|
2026-02-04 23:19:24 +09:00
|
|
|
_ax = cachedArduino.ax;
|
|
|
|
|
_ay = cachedArduino.ay;
|
2026-01-26 16:50:52 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final cachedGps = WebSocketService.instance.latestGps;
|
|
|
|
|
if (cachedGps != null) {
|
|
|
|
|
_gpsSpeed = cachedGps.speed;
|
|
|
|
|
_gpsSatellites = cachedGps.mode == 3 ? 8 : (cachedGps.mode == 2 ? 4 : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_wsState = WebSocketService.instance.connectionState;
|
|
|
|
|
|
|
|
|
|
// Placeholder: LTE signal (TODO: wire up when LTE service exists)
|
|
|
|
|
_lteSignal = null;
|
|
|
|
|
|
2026-01-26 00:20:52 +09:00
|
|
|
// DEBUG: flip-flop theme + navigator every 2s
|
|
|
|
|
TestFlipFlopService.instance.start(navigatorKey: _navigatorKey);
|
2026-01-25 18:47:35 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
2026-01-26 16:50:52 +09:00
|
|
|
_piTempTimer?.cancel();
|
|
|
|
|
_arduinoSub?.cancel();
|
|
|
|
|
_gpsSub?.cancel();
|
|
|
|
|
_connectionSub?.cancel();
|
2026-01-26 00:20:52 +09:00
|
|
|
TestFlipFlopService.instance.stop();
|
2026-01-25 18:47:35 +09:00
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 16:50:52 +09:00
|
|
|
/// Format gear for display: null → "—", 0 → "N", 1-6 → "1"-"6"
|
|
|
|
|
String _formatGear(int? gear) {
|
|
|
|
|
if (gear == null) return '—';
|
|
|
|
|
if (gear == 0) return 'N';
|
|
|
|
|
return gear.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Format nullable int for display
|
|
|
|
|
String _formatInt(int? value) => value?.toString() ?? '—';
|
|
|
|
|
|
|
|
|
|
/// Format nullable double for display with decimal places
|
|
|
|
|
String _formatDouble(double? value, [int decimals = 1]) {
|
|
|
|
|
if (value == null) return '—';
|
|
|
|
|
return value.toStringAsFixed(decimals);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 18:47:35 +09:00
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-01-26 00:20:52 +09:00
|
|
|
final theme = AppTheme.of(context);
|
|
|
|
|
|
2026-01-25 18:47:35 +09:00
|
|
|
return Scaffold(
|
2026-01-26 00:20:52 +09:00
|
|
|
backgroundColor: theme.background,
|
2026-01-25 18:47:35 +09:00
|
|
|
body: Padding(
|
2026-01-26 15:47:59 +09:00
|
|
|
padding: const EdgeInsets.all(16),
|
2026-01-25 22:49:13 +09:00
|
|
|
child: Row(
|
2026-01-25 18:47:35 +09:00
|
|
|
children: [
|
2026-01-25 22:49:13 +09:00
|
|
|
// Left side: All dashboard widgets (flex: 2)
|
|
|
|
|
Expanded(
|
|
|
|
|
flex: 2,
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
2026-01-26 15:47:59 +09:00
|
|
|
// System status bar
|
|
|
|
|
SystemBar(
|
|
|
|
|
gpsSatellites: _gpsSatellites,
|
|
|
|
|
lteSignal: _lteSignal,
|
|
|
|
|
piTemp: _piTemp,
|
|
|
|
|
voltage: _voltage,
|
2026-01-26 16:50:52 +09:00
|
|
|
wsState: _wsState,
|
2026-01-25 18:47:35 +09:00
|
|
|
),
|
2026-01-25 22:49:13 +09:00
|
|
|
|
2026-01-28 01:01:23 +09:00
|
|
|
const SizedBox(height: 5),
|
2026-01-25 22:49:13 +09:00
|
|
|
|
2026-01-26 15:47:59 +09:00
|
|
|
// Main content area - big stat boxes
|
2026-01-25 22:49:13 +09:00
|
|
|
Expanded(
|
2026-01-26 15:47:59 +09:00
|
|
|
flex: 8,
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
2026-02-01 17:01:45 +09:00
|
|
|
// Attitude indicator (whiskey mark)
|
|
|
|
|
Expanded(
|
|
|
|
|
child: WhiskeyMark(
|
|
|
|
|
roll: _roll,
|
|
|
|
|
pitch: _pitch,
|
|
|
|
|
),
|
2026-01-26 15:47:59 +09:00
|
|
|
),
|
2026-02-04 23:19:24 +09:00
|
|
|
Expanded(
|
|
|
|
|
child: AccelGraph(
|
|
|
|
|
ax: _ay, // Swapped: IMU Y → screen X (lateral)
|
|
|
|
|
ay: _ax, // Swapped: IMU X → screen Y (longitudinal)
|
|
|
|
|
maxG: 1.0,
|
|
|
|
|
ghostTrackPeriod: const Duration(seconds: 3),
|
|
|
|
|
),
|
|
|
|
|
)
|
2026-01-26 15:47:59 +09:00
|
|
|
],
|
2026-01-25 22:49:13 +09:00
|
|
|
),
|
2026-01-25 18:47:35 +09:00
|
|
|
),
|
2026-01-25 22:49:13 +09:00
|
|
|
|
|
|
|
|
// Bottom stats row
|
2026-01-26 15:47:59 +09:00
|
|
|
Expanded(
|
|
|
|
|
flex: 2,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
|
|
|
children: [
|
2026-01-30 22:47:18 +09:00
|
|
|
StatBox(value: _formatInt(_rpm), label: 'RPM', isWarning: () => (_rpm ?? 0) > 4000),
|
2026-01-26 16:50:52 +09:00
|
|
|
StatBox(value: _formatGear(_gear), label: 'GEAR'),
|
2026-01-26 15:47:59 +09:00
|
|
|
],
|
|
|
|
|
),
|
2026-01-25 22:49:13 +09:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-01-25 18:47:35 +09:00
|
|
|
),
|
|
|
|
|
|
2026-01-25 22:49:13 +09:00
|
|
|
const SizedBox(width: 32),
|
2026-01-25 18:47:35 +09:00
|
|
|
|
2026-01-28 01:01:23 +09:00
|
|
|
// Right side: Navigator on top, debug console below
|
2026-01-25 18:47:35 +09:00
|
|
|
Expanded(
|
2026-01-25 22:49:13 +09:00
|
|
|
flex: 1,
|
2026-01-28 01:01:23 +09:00
|
|
|
child: Column(
|
2026-01-26 22:22:33 +09:00
|
|
|
children: [
|
2026-01-28 01:01:23 +09:00
|
|
|
// Navigator
|
|
|
|
|
Expanded(
|
|
|
|
|
flex: 3,
|
|
|
|
|
child: Center(
|
|
|
|
|
child: NavigatorWidget(key: _navigatorKey),
|
|
|
|
|
),
|
2026-01-26 22:22:33 +09:00
|
|
|
),
|
2026-01-28 01:01:23 +09:00
|
|
|
// Debug console
|
|
|
|
|
Expanded(
|
|
|
|
|
flex: 1,
|
|
|
|
|
child:
|
|
|
|
|
DebugConsole(
|
|
|
|
|
messageStream: WebSocketService.instance.debugStream,
|
|
|
|
|
initialMessages: WebSocketService.instance.debugMessages,
|
|
|
|
|
maxLines: 6,
|
|
|
|
|
title: 'WebSocket messages',
|
|
|
|
|
),
|
2026-01-26 22:22:33 +09:00
|
|
|
),
|
|
|
|
|
],
|
2026-01-25 18:47:35 +09:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|