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 _frameController = StreamController.broadcast(); Stream get frameStream => _frameController.stream; final List _rxBuf = []; Timer? _pollTimer; StreamSubscription? _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; } }