v1.2.0 — Phase 2: Riverpod providers wired up
- settings_provider: AppSettings (ecuType, pollingInterval) StateNotifier - bt_provider: BtNotifier (disconnected/connecting/connected/error states), btServiceProvider singleton, pairedDevicesProvider FutureProvider, internal frameStream piped through StreamController - sensor_provider: sensorStateProvider StreamProvider (auto S300/KPro), latestSensorProvider convenience Provider - theme_provider: AppThemeVariant (red/green × dark/light) StateNotifier - main.dart: fully Riverpod — ProviderScope, ConsumerWidget throughout, no setState for BT state, providers own all lifecycle
This commit is contained in:
parent
11cf7c2b63
commit
6a80d8fc1f
138
lib/core/providers/bt_provider.dart
Normal file
138
lib/core/providers/bt_provider.dart
Normal file
@ -0,0 +1,138 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../bluetooth/bt_poller.dart';
|
||||
import '../bluetooth/bt_service.dart';
|
||||
import 'settings_provider.dart';
|
||||
|
||||
// ─── Connection status enum ──────────────────────────────────────────────────
|
||||
|
||||
enum BtStatus { disconnected, connecting, connected, error }
|
||||
|
||||
// ─── State ───────────────────────────────────────────────────────────────────
|
||||
|
||||
class BtState {
|
||||
final BtStatus status;
|
||||
final String? deviceName;
|
||||
final String? error;
|
||||
final int frameCount;
|
||||
final int droppedFrames;
|
||||
|
||||
const BtState({
|
||||
required this.status,
|
||||
this.deviceName,
|
||||
this.error,
|
||||
this.frameCount = 0,
|
||||
this.droppedFrames = 0,
|
||||
});
|
||||
|
||||
const BtState.disconnected()
|
||||
: this(status: BtStatus.disconnected);
|
||||
|
||||
BtState connecting(String name) =>
|
||||
BtState(status: BtStatus.connecting, deviceName: name);
|
||||
|
||||
BtState connected(String name) =>
|
||||
BtState(status: BtStatus.connected, deviceName: name);
|
||||
|
||||
BtState withError(String msg) =>
|
||||
BtState(status: BtStatus.error, deviceName: deviceName, error: msg);
|
||||
|
||||
BtState withFrameStats(int frames, int dropped) => BtState(
|
||||
status: status,
|
||||
deviceName: deviceName,
|
||||
frameCount: frames,
|
||||
droppedFrames: dropped,
|
||||
);
|
||||
|
||||
bool get isConnected => status == BtStatus.connected;
|
||||
bool get isConnecting => status == BtStatus.connecting;
|
||||
}
|
||||
|
||||
// ─── Notifier ────────────────────────────────────────────────────────────────
|
||||
|
||||
class BtNotifier extends StateNotifier<BtState> {
|
||||
final Ref _ref;
|
||||
BtPoller? _poller;
|
||||
StreamSubscription<Uint8List>? _pollSub;
|
||||
final StreamController<Uint8List> _frameController =
|
||||
StreamController<Uint8List>.broadcast();
|
||||
|
||||
BtNotifier(this._ref) : super(const BtState.disconnected());
|
||||
|
||||
/// Raw validated 128-byte frame stream — consumed by sensorStateProvider.
|
||||
Stream<Uint8List> get frameStream => _frameController.stream;
|
||||
|
||||
Future<void> connect(BluetoothDevice device) async {
|
||||
state = state.connecting(device.name ?? device.address);
|
||||
final service = _ref.read(btServiceProvider);
|
||||
final settings = _ref.read(settingsProvider);
|
||||
|
||||
try {
|
||||
await service.connect(device);
|
||||
|
||||
_poller = BtPoller(
|
||||
service,
|
||||
ecuType: settings.ecuType,
|
||||
pollingInterval: settings.pollingInterval,
|
||||
);
|
||||
|
||||
_pollSub = _poller!.frameStream.listen(
|
||||
(frame) {
|
||||
_frameController.add(frame);
|
||||
state = state.withFrameStats(
|
||||
_poller!.validFrames, _poller!.droppedFrames);
|
||||
},
|
||||
onError: (Object e) {
|
||||
_frameController.addError(e);
|
||||
state = state.withError(e.toString());
|
||||
},
|
||||
onDone: () => state = state.withError('BT stream closed'),
|
||||
);
|
||||
|
||||
_poller!.start();
|
||||
state = state.connected(device.name ?? device.address);
|
||||
} catch (e) {
|
||||
await service.disconnect();
|
||||
state = state.withError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
await _pollSub?.cancel();
|
||||
_poller?.stop();
|
||||
_poller?.dispose();
|
||||
_poller = null;
|
||||
await _ref.read(btServiceProvider).disconnect();
|
||||
state = const BtState.disconnected();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollSub?.cancel();
|
||||
_poller?.dispose();
|
||||
_frameController.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Providers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Singleton BtService — lives as long as the app.
|
||||
final btServiceProvider = Provider<BtService>((ref) {
|
||||
final service = BtService();
|
||||
ref.onDispose(service.dispose);
|
||||
return service;
|
||||
});
|
||||
|
||||
/// BT connection state notifier.
|
||||
final btProvider =
|
||||
StateNotifierProvider<BtNotifier, BtState>((ref) => BtNotifier(ref));
|
||||
|
||||
/// Paired BT devices list (re-fetchable via ref.refresh).
|
||||
final pairedDevicesProvider = FutureProvider<List<BluetoothDevice>>((ref) {
|
||||
return FlutterBluetoothSerial.instance.getBondedDevices();
|
||||
});
|
||||
29
lib/core/providers/sensor_provider.dart
Normal file
29
lib/core/providers/sensor_provider.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../bluetooth/bt_poller.dart';
|
||||
import '../protocol/kpro_parser.dart';
|
||||
import '../protocol/s300_parser.dart';
|
||||
import '../protocol/sensor_state.dart';
|
||||
import 'bt_provider.dart';
|
||||
import 'settings_provider.dart';
|
||||
|
||||
/// Emits a parsed [SensorState] for every valid frame received from the ECU.
|
||||
/// Automatically picks S300 or KPro parser based on [settingsProvider].
|
||||
final sensorStateProvider = StreamProvider<SensorState>((ref) {
|
||||
final ecuType = ref.watch(settingsProvider).ecuType;
|
||||
final btNotifier = ref.watch(btProvider.notifier);
|
||||
|
||||
return btNotifier.frameStream.map((frame) {
|
||||
if (ecuType == EcuType.s300) return parseS300(frame);
|
||||
return parseKPro(frame);
|
||||
});
|
||||
});
|
||||
|
||||
/// Last successfully parsed sensor state (never null after first frame).
|
||||
/// Falls back to SensorState.zero() before any data arrives.
|
||||
final latestSensorProvider = Provider<SensorState>((ref) {
|
||||
return ref
|
||||
.watch(sensorStateProvider)
|
||||
.whenData((s) => s)
|
||||
.value ?? SensorState.zero();
|
||||
});
|
||||
31
lib/core/providers/settings_provider.dart
Normal file
31
lib/core/providers/settings_provider.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../bluetooth/bt_poller.dart';
|
||||
|
||||
class AppSettings {
|
||||
final EcuType ecuType;
|
||||
final Duration pollingInterval;
|
||||
|
||||
const AppSettings({
|
||||
this.ecuType = EcuType.s300,
|
||||
this.pollingInterval = const Duration(milliseconds: 100),
|
||||
});
|
||||
|
||||
AppSettings copyWith({EcuType? ecuType, Duration? pollingInterval}) =>
|
||||
AppSettings(
|
||||
ecuType: ecuType ?? this.ecuType,
|
||||
pollingInterval: pollingInterval ?? this.pollingInterval,
|
||||
);
|
||||
}
|
||||
|
||||
class SettingsNotifier extends StateNotifier<AppSettings> {
|
||||
SettingsNotifier() : super(const AppSettings());
|
||||
|
||||
void setEcuType(EcuType type) => state = state.copyWith(ecuType: type);
|
||||
void setPollingInterval(Duration d) =>
|
||||
state = state.copyWith(pollingInterval: d);
|
||||
}
|
||||
|
||||
final settingsProvider =
|
||||
StateNotifierProvider<SettingsNotifier, AppSettings>(
|
||||
(ref) => SettingsNotifier(),
|
||||
);
|
||||
32
lib/core/providers/theme_provider.dart
Normal file
32
lib/core/providers/theme_provider.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
enum AppThemeVariant { redDark, redLight, greenDark, greenLight }
|
||||
|
||||
class ThemeNotifier extends StateNotifier<AppThemeVariant> {
|
||||
ThemeNotifier() : super(AppThemeVariant.redDark);
|
||||
|
||||
void set(AppThemeVariant v) => state = v;
|
||||
}
|
||||
|
||||
final themeProvider =
|
||||
StateNotifierProvider<ThemeNotifier, AppThemeVariant>(
|
||||
(ref) => ThemeNotifier(),
|
||||
);
|
||||
|
||||
/// Returns the accent color for the current theme variant.
|
||||
extension AppThemeVariantX on AppThemeVariant {
|
||||
Color get accentColor {
|
||||
switch (this) {
|
||||
case AppThemeVariant.redDark:
|
||||
case AppThemeVariant.redLight:
|
||||
return const Color(0xFFFF4444);
|
||||
case AppThemeVariant.greenDark:
|
||||
case AppThemeVariant.greenLight:
|
||||
return const Color(0xFF00E676);
|
||||
}
|
||||
}
|
||||
|
||||
bool get isDark =>
|
||||
this == AppThemeVariant.redDark || this == AppThemeVariant.greenDark;
|
||||
}
|
||||
830
lib/main.dart
830
lib/main.dart
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ description: HV BT Automotive ECU Dashboard
|
||||
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.1+2
|
||||
version: 1.2.0+3
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user