Dashboard - Selectable main sensor (gauge) + sub sensor, 10 selectable values (landscape 5 left + 5 right, portrait 2x5 on one screen) and 4 selectable flags; long-press to change; persisted. - Flags: the requirement examples (MIL, FAN Out, VTEC Out, Knock, WBO / IAT / CLT Error - errors derived from the stored DTC bits) plus the other S300 status bits (cuts, launch, closed loop, VTP, AC, brake). - Record button moved to the app bar; debug screen moved to Settings. Sensor list: 1 column on phone portrait, 2 on tablet/landscape; name, unit, min/max, segmented bar and value. Graph: auto-scaled Y, window max/min, current value, seconds axis and a touch cursor; RPM/MAP/TPS/ECT by default. Units: km/h|mph, degC|degF, kPa|bar|psi (display only), persisted. Light/dark: palette is a ThemeExtension used by every screen; theme persisted. SafeArea keeps content off the side navigation bar. Datalog playback now drives the dashboard, list, graph and DTC screen, with the playback bar on the main screen and seek updating the displays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
4.2 KiB
Dart
135 lines
4.2 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../bluetooth/bt_poller.dart';
|
|
import '../models/units.dart';
|
|
|
|
/// Polling choices offered in Settings. Never faster than 10 Hz: the adapter
|
|
/// MCU must have time to read the ECU between requests (device maker's
|
|
/// feedback). The poller additionally never sends a new request before the
|
|
/// previous one is answered or has timed out.
|
|
const List<int> pollingIntervalOptionsMs = [100, 150, 200, 300];
|
|
|
|
class AppSettings {
|
|
final EcuType ecuType;
|
|
final Duration pollingInterval;
|
|
|
|
const AppSettings({
|
|
this.ecuType = EcuType.s300,
|
|
this.pollingInterval = BtPoller.defaultInterval,
|
|
});
|
|
|
|
AppSettings copyWith({EcuType? ecuType, Duration? pollingInterval}) =>
|
|
AppSettings(
|
|
ecuType: ecuType ?? this.ecuType,
|
|
pollingInterval: pollingInterval ?? this.pollingInterval,
|
|
);
|
|
}
|
|
|
|
const _ecuTypeKey = 'ecuType';
|
|
const _pollingKey = 'pollingIntervalMs';
|
|
|
|
class SettingsNotifier extends StateNotifier<AppSettings> {
|
|
SettingsNotifier() : super(const AppSettings()) {
|
|
_load();
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final saved = prefs.getString(_ecuTypeKey);
|
|
final ms = prefs.getInt(_pollingKey);
|
|
var next = state;
|
|
if (saved == EcuType.kpro.name) next = next.copyWith(ecuType: EcuType.kpro);
|
|
if (ms != null && pollingIntervalOptionsMs.contains(ms)) {
|
|
next = next.copyWith(pollingInterval: Duration(milliseconds: ms));
|
|
}
|
|
if (mounted) state = next;
|
|
} catch (_) {
|
|
// Keep defaults if preferences can't be read.
|
|
}
|
|
}
|
|
|
|
void setEcuType(EcuType type) {
|
|
state = state.copyWith(ecuType: type);
|
|
SharedPreferences.getInstance()
|
|
.then((prefs) => prefs.setString(_ecuTypeKey, type.name))
|
|
.catchError((Object _) => false);
|
|
}
|
|
|
|
void setPollingInterval(Duration d) {
|
|
final ms = pollingIntervalOptionsMs.contains(d.inMilliseconds)
|
|
? d.inMilliseconds
|
|
: BtPoller.defaultInterval.inMilliseconds;
|
|
state = state.copyWith(pollingInterval: Duration(milliseconds: ms));
|
|
SharedPreferences.getInstance()
|
|
.then((prefs) => prefs.setInt(_pollingKey, ms))
|
|
.catchError((Object _) => false);
|
|
}
|
|
}
|
|
|
|
final settingsProvider = StateNotifierProvider<SettingsNotifier, AppSettings>(
|
|
(ref) => SettingsNotifier(),
|
|
);
|
|
|
|
// ─── Display units ───────────────────────────────────────────────────────────
|
|
|
|
const _unitKeys = (speed: 'unitSpeed', temp: 'unitTemp', pressure: 'unitPressure');
|
|
|
|
T? _byName<T extends Enum>(List<T> values, String? name) {
|
|
for (final v in values) {
|
|
if (v.name == name) return v;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Display units (speed / temperature / pressure), persisted.
|
|
class UnitPrefsNotifier extends StateNotifier<UnitPrefs> {
|
|
UnitPrefsNotifier() : super(const UnitPrefs()) {
|
|
_load();
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final next = UnitPrefs(
|
|
speed: _byName(SpeedUnit.values, prefs.getString(_unitKeys.speed)) ??
|
|
state.speed,
|
|
temp: _byName(TempUnit.values, prefs.getString(_unitKeys.temp)) ??
|
|
state.temp,
|
|
pressure: _byName(
|
|
PressureUnit.values, prefs.getString(_unitKeys.pressure)) ??
|
|
state.pressure,
|
|
);
|
|
if (mounted) state = next;
|
|
} catch (_) {
|
|
// Keep defaults if preferences can't be read.
|
|
}
|
|
}
|
|
|
|
void _save(String key, Enum v) {
|
|
SharedPreferences.getInstance()
|
|
.then((prefs) => prefs.setString(key, v.name))
|
|
.catchError((Object _) => false);
|
|
}
|
|
|
|
void setSpeed(SpeedUnit v) {
|
|
state = state.copyWith(speed: v);
|
|
_save(_unitKeys.speed, v);
|
|
}
|
|
|
|
void setTemp(TempUnit v) {
|
|
state = state.copyWith(temp: v);
|
|
_save(_unitKeys.temp, v);
|
|
}
|
|
|
|
void setPressure(PressureUnit v) {
|
|
state = state.copyWith(pressure: v);
|
|
_save(_unitKeys.pressure, v);
|
|
}
|
|
}
|
|
|
|
final unitPrefsProvider =
|
|
StateNotifierProvider<UnitPrefsNotifier, UnitPrefs>(
|
|
(ref) => UnitPrefsNotifier(),
|
|
);
|