hondavert-dev/lib/core/bluetooth/bt_poller.dart
2026-07-28 01:08:17 +05:30

156 lines
4.2 KiB
Dart

import 'dart:async';
import 'dart:typed_data';
import '../protocol/neg8.dart';
import 'bt_service.dart';
enum EcuType { s300, kpro }
/// Sends the ECU request byte every [pollingInterval] ms,
/// accumulates raw bytes into 128-byte frames, validates NEG8,
/// and emits the newest usable frame on [frameStream].
class BtPoller {
final BtService _service;
EcuType ecuType;
final Duration pollingInterval;
// S300 request: [0x1B, 0x00, 0xE5]
static final Uint8List _s300Request = Uint8List.fromList([0x1B, 0x00, 0xE5]);
// KPro request: [0x1B, 0x01, 0xE4]
static final Uint8List _kproRequest = Uint8List.fromList([0x1B, 0x01, 0xE4]);
final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
Stream<Uint8List> get frameStream => _frameController.stream;
final List<int> _rxBuf = [];
Timer? _pollTimer;
StreamSubscription<Uint8List>? _rxSub;
int droppedFrames = 0;
int validFrames = 0;
BtPoller(
this._service, {
this.ecuType = EcuType.s300,
this.pollingInterval = const Duration(milliseconds: 50),
});
void start() {
_rxBuf.clear();
// Listen to raw bytes from BT and accumulate into 128-byte frames
_rxSub = _service.rawStream.listen(
(Uint8List chunk) {
_rxBuf.addAll(chunk);
_processBuffer();
},
onError: (Object e) {
_frameController.addError(e);
stop();
},
);
// Send request at polling interval
_pollTimer = Timer.periodic(pollingInterval, (_) async {
if (_service.isConnected) {
await _service.write(
ecuType == EcuType.s300 ? _s300Request : _kproRequest,
);
}
});
}
void _processBuffer() {
// Extract as many 128-byte frames as possible.
// Frame start sync: first byte should be 0x1B (header marker).
// If we have a misaligned buffer, scan forward.
Uint8List? latestUsableFrame;
while (_rxBuf.length >= 128) {
// Scan for 0x1B frame header
int startIdx = 0;
while (startIdx < _rxBuf.length && _rxBuf[startIdx] != 0x1B) {
startIdx++;
}
// Not enough data after sync byte
if (_rxBuf.length - startIdx < 128) {
// Discard bytes before potential header
if (startIdx > 0) {
_rxBuf.removeRange(0, startIdx);
}
break;
}
final Uint8List frame =
Uint8List.fromList(_rxBuf.sublist(startIdx, startIdx + 128));
if (ecuType == EcuType.s300 && _isS300PlaceholderFrame(frame)) {
droppedFrames++;
_rxBuf.removeRange(0, startIdx + 128);
continue;
}
final checksumOk = validateFrame(frame);
final canUseFrame = checksumOk || ecuType == EcuType.s300;
if (canUseFrame) {
if (!checksumOk) droppedFrames++;
validFrames++;
latestUsableFrame = frame;
_rxBuf.removeRange(0, startIdx + 128);
} else {
// Bad checksum — skip this byte and try again
droppedFrames++;
_rxBuf.removeRange(0, startIdx + 1);
}
}
if (latestUsableFrame != null) {
_frameController.add(latestUsableFrame);
}
}
void stop() {
_pollTimer?.cancel();
_pollTimer = null;
_rxSub?.cancel();
_rxSub = null;
_rxBuf.clear();
}
void dispose() {
stop();
_frameController.close();
}
bool _isS300PlaceholderFrame(Uint8List frame) {
if (frame.length != 128) return false;
// The S300 Bluetooth stream can interleave a valid-checksum placeholder
// frame that is not the SManager live sensor frame. It has a fixed header
// and zero/overflow values for speed, TPS, injection and ignition, then it
// overwrites the dashboard with stale/empty values. Filter only this exact
// signature so true idle/stopped live frames can still pass.
return frame[0] == 0x1B &&
frame[1] == 0x00 &&
frame[2] == 0x14 &&
frame[3] == 0x00 &&
frame[4] == 0x00 &&
frame[5] == 0xFF &&
frame[6] == 0xFF &&
frame[7] == 0xF2 &&
frame[8] == 0x03 &&
frame[9] == 0x18 &&
frame[10] == 0x00 &&
frame[11] == 0x00 &&
frame[12] == 0x10 &&
frame[13] == 0x00 &&
frame[14] == 0x00 &&
frame[15] == 0x00 &&
frame[16] == 0xC4;
}
}