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:
parent
e4bcfb57a6
commit
6b8d069ef4
@ -32,9 +32,12 @@ const List<SensorDef> sensorDefs = [
|
|||||||
max: 150),
|
max: 150),
|
||||||
SensorDef(
|
SensorDef(
|
||||||
id: 'bat', displayName: 'Battery voltage', unit: 'V', min: 0, max: 20),
|
id: 'bat', displayName: 'Battery voltage', unit: 'V', min: 0, max: 20),
|
||||||
// O2/AFR/AIN scales not yet bench-verified: shown as raw values.
|
|
||||||
SensorDef(
|
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(
|
SensorDef(
|
||||||
id: 'gear', displayName: 'Gear', unit: '', min: 0, max: 6, decimals: 0),
|
id: 'gear', displayName: 'Gear', unit: '', min: 0, max: 6, decimals: 0),
|
||||||
SensorDef(
|
SensorDef(
|
||||||
@ -81,6 +84,8 @@ double sensorValue(SensorState s, String id) {
|
|||||||
case 'iat': return s.iat;
|
case 'iat': return s.iat;
|
||||||
case 'bat': return s.bat;
|
case 'bat': return s.bat;
|
||||||
case 'o2': return s.o2;
|
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 'gear': return s.gear.toDouble();
|
||||||
case 'eth': return s.eth;
|
case 'eth': return s.eth;
|
||||||
case 'pa': return s.pa;
|
case 'pa': return s.pa;
|
||||||
|
|||||||
@ -39,7 +39,8 @@ SensorState parseKPro(Uint8List frame) {
|
|||||||
final double ign = frame[13].toDouble();
|
final double ign = frame[13].toDouble();
|
||||||
|
|
||||||
// O2: byte 14 raw
|
// 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
|
// KRtrd: byte 21
|
||||||
final int krtrd = frame[21];
|
final int krtrd = frame[21];
|
||||||
|
|||||||
@ -97,8 +97,8 @@ SensorState parseS300(Uint8List frame, {bool validateChecksum = true}) {
|
|||||||
// IGN: formula from the PDF. UNVERIFIED (one live check disagreed).
|
// IGN: formula from the PDF. UNVERIFIED (one live check disagreed).
|
||||||
final double ign = (frame[12] + 120) / 2.0;
|
final double ign = (frame[12] + 120) / 2.0;
|
||||||
|
|
||||||
// O2: raw byte. UNVERIFIED scale — shown as raw.
|
// O2: volts, raw × 5/256 (USB tool: raw 195 → 3.81 V).
|
||||||
final double o2 = frame[16].toDouble();
|
final double o2 = frame[16] * 5.0 / 256.0;
|
||||||
|
|
||||||
final int sw1 = frame[17];
|
final int sw1 = frame[17];
|
||||||
final int sw2 = frame[18];
|
final int sw2 = frame[18];
|
||||||
|
|||||||
@ -245,12 +245,31 @@ class BtNotifier extends StateNotifier<BtState> {
|
|||||||
} else {
|
} else {
|
||||||
_liveFrames++;
|
_liveFrames++;
|
||||||
_lastLiveFrameAt = now;
|
_lastLiveFrameAt = now;
|
||||||
|
_maybeLogFrame(frame, now);
|
||||||
_defaultsSince = null;
|
_defaultsSince = null;
|
||||||
_frameController.add(frame);
|
_frameController.add(frame);
|
||||||
}
|
}
|
||||||
_publishStats(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]) {
|
void _publishStats([Uint8List? frame]) {
|
||||||
final p = _poller;
|
final p = _poller;
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
|
|||||||
@ -23,30 +23,9 @@ class SensorHistory {
|
|||||||
if (list.length > _maxPoints) list.removeAt(0);
|
if (list.length > _maxPoints) list.removeAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
push('rpm', state.rpm);
|
for (final d in sensorDefs) {
|
||||||
push('vss', state.vss);
|
push(d.id, sensorValue(state, d.id));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FlSpot> get(String id) => List.unmodifiable(_data[id] ?? []);
|
List<FlSpot> get(String id) => List.unmodifiable(_data[id] ?? []);
|
||||||
|
|||||||
@ -29,7 +29,9 @@ class DashboardConfig {
|
|||||||
'vss', 'ect', 'iat', 'map', 'o2', // left
|
'vss', 'ect', 'iat', 'map', 'o2', // left
|
||||||
'tps', 'bat', 'ign', 'inj', 'eth', // right
|
'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({
|
DashboardConfig copyWith({
|
||||||
|
|||||||
@ -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', () {
|
group('S300 flags', () {
|
||||||
test('SW3 cut/launch bits (byte 19)', () {
|
test('SW3 cut/launch bits (byte 19)', () {
|
||||||
final s = parseS300(_s300({19: 0x20 | 0x08}));
|
final s = parseS300(_s300({19: 0x20 | 0x08}));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user