O2 in volts, injector duty, Fuel Pump default flag, raw frame log

Checked against the USB tool on the bench (3210 rpm):
- O2: raw x 5/256 V (raw 195 -> 3.81 V, as the USB tool shows).
- New derived sensor: injector duty = INJ ms x RPM / 1200
  (16.67 ms @ 3210 rpm -> 44 %, as the USB tool shows).
- Default dashboard flags: MIL, Fuel Pump, FAN Out, VTEC Out (the
  requirement's "FuelCut" is the fuel pump flag on the S300).
- Log one raw frame (bytes 0-63) every 5 s so USB-tool readings can be
  matched to bytes; INJ, IGN, TPS, MIL, AF and PA still need that check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-12 23:47:12 -04:00
parent e4bcfb57a6
commit 6b8d069ef4
7 changed files with 47 additions and 30 deletions

View File

@ -32,9 +32,12 @@ const List<SensorDef> sensorDefs = [
max: 150),
SensorDef(
id: 'bat', displayName: 'Battery voltage', unit: 'V', min: 0, max: 20),
// O2/AFR/AIN scales not yet bench-verified: shown as raw values.
SensorDef(
id: 'o2', displayName: 'O2 sensor', unit: 'raw', min: 0, max: 255, decimals: 0),
id: 'o2', displayName: 'O2 sensor', unit: 'V', min: 0, max: 5, decimals: 2),
// Derived: injector duration × RPM (USB tool: 16.67 ms @ 3210 rpm = 44 %).
SensorDef(
id: 'duty', displayName: 'Injector duty', unit: '%', min: 0, max: 100, decimals: 0),
// AFR/AIN scales not yet bench-verified: shown as raw values.
SensorDef(
id: 'gear', displayName: 'Gear', unit: '', min: 0, max: 6, decimals: 0),
SensorDef(
@ -81,6 +84,8 @@ double sensorValue(SensorState s, String id) {
case 'iat': return s.iat;
case 'bat': return s.bat;
case 'o2': return s.o2;
// One injection per 2 revolutions: ms × rpm / 120000 × 100.
case 'duty': return s.inj * s.rpm / 1200;
case 'gear': return s.gear.toDouble();
case 'eth': return s.eth;
case 'pa': return s.pa;

View File

@ -39,7 +39,8 @@ SensorState parseKPro(Uint8List frame) {
final double ign = frame[13].toDouble();
// O2: byte 14 raw
final double o2 = frame[14].toDouble();
// O2: volts, raw × 5/256 as on the S300. UNVERIFIED for KPro.
final double o2 = frame[14] * 5.0 / 256.0;
// KRtrd: byte 21
final int krtrd = frame[21];

View File

@ -97,8 +97,8 @@ SensorState parseS300(Uint8List frame, {bool validateChecksum = true}) {
// IGN: formula from the PDF. UNVERIFIED (one live check disagreed).
final double ign = (frame[12] + 120) / 2.0;
// O2: raw byte. UNVERIFIED scale — shown as raw.
final double o2 = frame[16].toDouble();
// O2: volts, raw × 5/256 (USB tool: raw 195 → 3.81 V).
final double o2 = frame[16] * 5.0 / 256.0;
final int sw1 = frame[17];
final int sw2 = frame[18];

View File

@ -245,12 +245,31 @@ class BtNotifier extends StateNotifier<BtState> {
} else {
_liveFrames++;
_lastLiveFrameAt = now;
_maybeLogFrame(frame, now);
_defaultsSince = null;
_frameController.add(frame);
}
_publishStats(frame);
}
DateTime? _lastFrameLogAt;
/// Raw hex of one live frame every 5 s, so bench readings from the USB
/// tool can be matched to exact bytes (Debug → Copy app log, or logcat).
void _maybeLogFrame(Uint8List frame, DateTime now) {
final last = _lastFrameLogAt;
if (last != null && now.difference(last) < const Duration(seconds: 5)) {
return;
}
_lastFrameLogAt = now;
final hex = StringBuffer();
for (var i = 0; i < 64; i++) {
if (i > 0) hex.write(i % 16 == 0 ? ' | ' : ' ');
hex.write(frame[i].toRadixString(16).padLeft(2, '0'));
}
AppLog.i('FRAME[0..63] $hex');
}
void _publishStats([Uint8List? frame]) {
final p = _poller;
state = state.copyWith(

View File

@ -23,30 +23,9 @@ class SensorHistory {
if (list.length > _maxPoints) list.removeAt(0);
}
push('rpm', state.rpm);
push('vss', state.vss);
push('map', state.map);
push('tps', state.tps);
push('inj', state.inj);
push('ign', state.ign);
push('ect', state.ect);
push('iat', state.iat);
push('bat', state.bat);
push('o2', state.o2);
push('gear', state.gear.toDouble());
push('eth', state.eth);
push('pa', state.pa);
push('afr', state.afr);
push('strim', state.strim);
push('ltrim', state.ltrim);
push('ain0', state.ain0);
push('ain1', state.ain1);
push('ain2', state.ain2);
push('ain3', state.ain3);
push('ain4', state.ain4);
push('ain5', state.ain5);
push('ain6', state.ain6);
push('ain7', state.ain7);
for (final d in sensorDefs) {
push(d.id, sensorValue(state, d.id));
}
}
List<FlSpot> get(String id) => List.unmodifiable(_data[id] ?? []);

View File

@ -29,7 +29,9 @@ class DashboardConfig {
'vss', 'ect', 'iat', 'map', 'o2', // left
'tps', 'bat', 'ign', 'inj', 'eth', // right
],
flags: ['mil', 'fanout', 'vtec', 'knock'],
// Requirement picture: MIL, FuelCut, FAN Out, VTEC Out. On the S300
// the fuel flag is the fuel pump relay (client).
flags: ['mil', 'fuelpump', 'fanout', 'vtec'],
);
DashboardConfig copyWith({

View File

@ -58,6 +58,17 @@ void main() {
});
});
group('values checked against the USB tool (bench, 3210 rpm)', () {
test('O2 volts = raw × 5/256 (raw 195 → 3.81 V)', () {
expect(parseS300(_s300({16: 195})).o2, closeTo(3.81, 0.005));
});
test('injector duty = INJ ms × RPM / 1200 (16.67 ms @ 3210 → 44 %)', () {
final s = parseS300(_s300({})).copyWith(inj: 16.67, rpm: 3210);
expect(sensorValue(s, 'duty'), closeTo(44.6, 0.1));
});
});
group('S300 flags', () {
test('SW3 cut/launch bits (byte 19)', () {
final s = parseS300(_s300({19: 0x20 | 0x08}));