Client reported crashes, wrong/frozen data and that the app floods the adapter MCU. See docs/FIX_2026-10-12.md for the full analysis. - Poller: strictly one request outstanding; next request only after a valid 128-byte reply or a 500 ms timeout, then a quiet period + buffer flush. Default 100 ms (10 Hz), minimum 80 ms. DTC clear/probe share the same lock. - BtNotifier: filter the adapter's default (no-ECU-data) frame instead of holding last values; EcuDataStatus + status banner; auto-reconnect on link loss, stalls or long default-only runs (rate-limited). A reconnect still in flight when the user disconnects is discarded. - S300 offsets corrected by column counting (trims, PA, ERR00-03, Eth, SW5, AF); INJ/AIN little-endian; fan = SW5 FANC only. Default-frame detection requires bytes 0-16 and 32-36. - KPro RPM/MAP2/AIN little-endian (RPM still needs a bench check). - Crash hardening: zone/Flutter/platform error handlers to an in-app log (copyable from Debug); permissions requested before listing devices. - Remove temporary debug prints; graph redraws live; polling options 100-300 ms, persisted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
2.4 KiB
Dart
72 lines
2.4 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../bluetooth/bt_poller.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(),
|
|
);
|