Files
smart-serow/pi/ui/lib/services/backend_service.dart
Mikkeli Matlock 896ba322c0 gps: debug stub mode with satellites field and signal loss simulation
- _GPS_DEBUG flag for development without hardware
- stub mode: realistic mock data with occasional signal loss
- satellites field in backend and UI data models
- periodic status logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 03:04:44 +09:00

162 lines
4.8 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
/// Data from Arduino (voltage, rpm, engine temp, gear, IMU)
class ArduinoData {
final double? voltage;
final int? rpm;
final int? engTemp;
final int? gear; // 0 = neutral, 1-6 = gear
final double? roll; // Euler angle in degrees (negative = left, positive = right)
final double? pitch; // Euler angle in degrees (negative = nose down)
final double? ax; // Lateral acceleration (g)
final double? ay; // Longitudinal acceleration (g)
final double? az; // Vertical acceleration (g)
ArduinoData({this.voltage, this.rpm, this.engTemp, this.gear, this.roll, this.pitch, this.ax, this.ay, this.az});
factory ArduinoData.fromJson(Map<String, dynamic> json) {
return ArduinoData(
voltage: (json['voltage'] as num?)?.toDouble(),
rpm: (json['rpm'] as num?)?.toInt(),
engTemp: (json['eng_temp'] as num?)?.toInt(),
gear: (json['gear'] as num?)?.toInt(),
roll: (json['roll'] as num?)?.toDouble(), // IMU mounted with axes swapped
pitch: (json['pitch'] as num?)?.toDouble(),
ax: (json['ax'] as num?)?.toDouble(),
ay: (json['ay'] as num?)?.toDouble(),
az: (json['az'] as num?)?.toDouble(),
);
}
}
/// Data from GPS
class GpsData {
final double? lat;
final double? lon;
final double? speed; // m/s
final double? alt;
final double? track;
final int? mode; // 0=no fix, 2=2D, 3=3D
final int? satellites;
GpsData({this.lat, this.lon, this.speed, this.alt, this.track, this.mode, this.satellites});
factory GpsData.fromJson(Map<String, dynamic> json) {
return GpsData(
lat: (json['lat'] as num?)?.toDouble(),
lon: (json['lon'] as num?)?.toDouble(),
speed: (json['speed'] as num?)?.toDouble(),
alt: (json['alt'] as num?)?.toDouble(),
track: (json['track'] as num?)?.toDouble(),
mode: (json['mode'] as num?)?.toInt(),
satellites: (json['satellites'] as num?)?.toInt(),
);
}
}
/// HTTP client for Flask backend - fire-and-forget async fetch, sync cache return
///
/// Follows the same pattern as PiIO: never blocks UI, always returns cached data.
class BackendService {
BackendService._() {
// Kick off initial fetches
_refreshArduino();
_refreshGps();
}
static final instance = BackendService._();
static const _baseUrl = 'http://127.0.0.1:5000';
static const _timeout = Duration(seconds: 2);
// Caches
ArduinoData? _arduinoCache;
GpsData? _gpsCache;
bool _connected = false;
// In-progress flags (prevent duplicate requests)
bool _arduinoFetchInProgress = false;
bool _gpsFetchInProgress = false;
/// Whether backend is reachable
bool get isConnected => _connected;
/// Get Arduino data (sync, returns cached value)
ArduinoData? getArduinoData() {
if (!_arduinoFetchInProgress) {
_refreshArduino();
}
return _arduinoCache;
}
/// Get GPS data (sync, returns cached value)
GpsData? getGpsData() {
if (!_gpsFetchInProgress) {
_refreshGps();
}
return _gpsCache;
}
/// Background fetch for Arduino data
Future<void> _refreshArduino() async {
if (_arduinoFetchInProgress) return;
_arduinoFetchInProgress = true;
try {
final response = await http
.get(Uri.parse('$_baseUrl/arduino'))
.timeout(_timeout);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
// Skip if backend returns error (no data yet) - keep cached value
if (!json.containsKey('error')) {
_arduinoCache = ArduinoData.fromJson(json);
}
_connected = true;
}
// Non-200: keep cached data, just mark disconnected
} catch (e) {
// Network error, timeout, etc - keep cached data for transient hiccups
_connected = false;
} finally {
_arduinoFetchInProgress = false;
}
}
/// Background fetch for GPS data
Future<void> _refreshGps() async {
if (_gpsFetchInProgress) return;
_gpsFetchInProgress = true;
try {
final response = await http
.get(Uri.parse('$_baseUrl/gps'))
.timeout(_timeout);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
// Skip if backend returns error (no data yet) - keep cached value
if (!json.containsKey('error')) {
_gpsCache = GpsData.fromJson(json);
}
_connected = true;
}
// Non-200: keep cached data, just mark disconnected
} catch (e) {
// Network error, timeout, etc - keep cached data for transient hiccups
_connected = false;
} finally {
_gpsFetchInProgress = false;
}
}
/// Force clear all caches
void clearCache() {
_arduinoCache = null;
_gpsCache = null;
_connected = false;
}
}