161 lines
5.2 KiB
Dart
161 lines
5.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;
|
|
|
|
// Tracks whether a request is still awaiting its response, so the poller
|
|
// never pipelines more than one request ahead of the device. Without this,
|
|
// a device that answers slower than [pollingInterval] falls permanently
|
|
// behind — every displayed frame ends up answering a stale, already-queued
|
|
// request instead of the most recent one, and the dashboard visibly lags
|
|
// real engine changes by a growing amount.
|
|
bool _awaitingResponse = false;
|
|
DateTime? _lastRequestSentAt;
|
|
|
|
BtPoller(
|
|
this._service, {
|
|
this.ecuType = EcuType.s300,
|
|
this.pollingInterval = const Duration(milliseconds: 50),
|
|
});
|
|
|
|
void start() {
|
|
_rxBuf.clear();
|
|
_awaitingResponse = false;
|
|
_lastRequestSentAt = null;
|
|
|
|
// 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();
|
|
},
|
|
);
|
|
|
|
// Check every pollingInterval whether it's time to send the next
|
|
// request — but only actually send once the prior one has been
|
|
// answered (or has clearly been lost).
|
|
_pollTimer = Timer.periodic(pollingInterval, (_) => _maybeSendRequest());
|
|
_maybeSendRequest();
|
|
}
|
|
|
|
void _maybeSendRequest() {
|
|
if (!_service.isConnected) return;
|
|
|
|
final stalled = _lastRequestSentAt != null &&
|
|
DateTime.now().difference(_lastRequestSentAt!) > pollingInterval * 4;
|
|
if (_awaitingResponse && !stalled) return;
|
|
|
|
_awaitingResponse = true;
|
|
_lastRequestSentAt = DateTime.now();
|
|
_service
|
|
.write(ecuType == EcuType.s300 ? _s300Request : _kproRequest)
|
|
.catchError((Object _) {
|
|
// A failed write must not leave the poller permanently waiting for a
|
|
// response that was never actually requested — let the next tick retry.
|
|
_awaitingResponse = false;
|
|
});
|
|
}
|
|
|
|
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));
|
|
|
|
final checksumOk = validateFrame(frame);
|
|
|
|
if (checksumOk) {
|
|
validFrames++;
|
|
latestUsableFrame = frame;
|
|
_rxBuf.removeRange(0, startIdx + 128);
|
|
} else {
|
|
// Bad checksum — the window is misaligned (or the frame was
|
|
// corrupted in transit). Drop one byte and rescan for the next
|
|
// 0x1B so the stream resynchronizes instead of permanently
|
|
// sliding a fixed-size window over garbage.
|
|
droppedFrames++;
|
|
_rxBuf.removeRange(0, startIdx + 1);
|
|
}
|
|
}
|
|
|
|
if (latestUsableFrame != null) {
|
|
_frameController.add(latestUsableFrame);
|
|
// The device answered — clear the road for the next request. Wait out
|
|
// the configured pollingInterval before sending it rather than firing
|
|
// immediately: back-to-back requests with no gap can outrun the ECU's
|
|
// own tach input-capture timing, which needs a moment to latch a fresh
|
|
// RPM count between reads — hammering it faster than that makes it
|
|
// fall back to reporting "no fresh count yet" on every subsequent
|
|
// request instead of a real reading. pollingInterval is also what the
|
|
// Settings screen exposes, so this makes that control actually pace
|
|
// the link instead of just being a stalled-request fallback.
|
|
_awaitingResponse = false;
|
|
Timer(pollingInterval, _maybeSendRequest);
|
|
}
|
|
}
|
|
|
|
void stop() {
|
|
_pollTimer?.cancel();
|
|
_pollTimer = null;
|
|
_rxSub?.cancel();
|
|
_rxSub = null;
|
|
_rxBuf.clear();
|
|
}
|
|
|
|
void dispose() {
|
|
stop();
|
|
_frameController.close();
|
|
}
|
|
}
|