Fix S300 live data decoding
This commit is contained in:
parent
763821171c
commit
1424cafbb4
@ -3,39 +3,65 @@ import 'flag_def.dart';
|
||||
|
||||
/// Master list of all analog sensor definitions.
|
||||
const List<SensorDef> sensorDefs = [
|
||||
SensorDef(id: 'rpm', displayName: 'RPM', unit: 'revs', min: 0, max: 9000),
|
||||
SensorDef(id: 'vss', displayName: 'Speed', unit: 'km/h', min: 0, max: 280),
|
||||
SensorDef(id: 'map', displayName: 'Manifold pressure', unit: 'kPa', min: 0, max: 300),
|
||||
SensorDef(id: 'tps', displayName: 'Throttle pedal', unit: '%', min: 0, max: 100),
|
||||
SensorDef(id: 'inj', displayName: 'Injector duration', unit: 'ms', min: 0, max: 20),
|
||||
SensorDef(id: 'ign', displayName: 'Timing advance', unit: 'deg', min: -20, max: 60),
|
||||
SensorDef(id: 'ect', displayName: 'Coolant temp', unit: '°C', min: -40, max: 150),
|
||||
SensorDef(id: 'iat', displayName: 'Intake air temp', unit: '°C', min: -40, max: 150),
|
||||
SensorDef(id: 'bat', displayName: 'Battery voltage', unit: 'V', min: 0, max: 20),
|
||||
SensorDef(id: 'o2', displayName: 'O2 sensor', unit: 'V', min: 0, max: 1.5),
|
||||
SensorDef(id: 'gear', displayName: 'Gear', unit: '—', min: 1, max: 6),
|
||||
SensorDef(id: 'eth', displayName: 'Ethanol', unit: '%', min: 0, max: 100),
|
||||
SensorDef(id: 'pa', displayName: 'Baro pressure', unit: 'kPa', min: 0, max: 120),
|
||||
SensorDef(id: 'afr', displayName: 'AFR', unit: 'λ', min: 0.5, max: 2.0),
|
||||
SensorDef(id: 'strim', displayName: 'Short trim', unit: '%', min: -30, max: 30),
|
||||
SensorDef(id: 'ltrim', displayName: 'Long trim', unit: '%', min: -30, max: 30),
|
||||
SensorDef(id: 'ain0', displayName: 'Analog input 0', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(id: 'ain1', displayName: 'Analog input 1', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(id: 'ain2', displayName: 'Analog input 2', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(id: 'ain3', displayName: 'Analog input 3', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(id: 'ain4', displayName: 'Analog input 4', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(id: 'ain5', displayName: 'Analog input 5', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(id: 'ain6', displayName: 'Analog input 6', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(id: 'ain7', displayName: 'Analog input 7', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(id: 'rpm', displayName: 'RPM', unit: 'revs', min: 0, max: 9000),
|
||||
SensorDef(id: 'vss', displayName: 'Speed', unit: 'mph', min: 0, max: 180),
|
||||
SensorDef(
|
||||
id: 'map',
|
||||
displayName: 'Manifold pressure',
|
||||
unit: 'psi',
|
||||
min: -30,
|
||||
max: 30),
|
||||
SensorDef(
|
||||
id: 'tps', displayName: 'Throttle pedal', unit: '%', min: 0, max: 100),
|
||||
SensorDef(
|
||||
id: 'inj', displayName: 'Injector duration', unit: 'ms', min: 0, max: 20),
|
||||
SensorDef(
|
||||
id: 'ign', displayName: 'Timing advance', unit: 'deg', min: -20, max: 60),
|
||||
SensorDef(
|
||||
id: 'ect', displayName: 'Coolant temp', unit: '°C', min: -40, max: 150),
|
||||
SensorDef(
|
||||
id: 'iat',
|
||||
displayName: 'Intake air temp',
|
||||
unit: '°C',
|
||||
min: -40,
|
||||
max: 150),
|
||||
SensorDef(
|
||||
id: 'bat', displayName: 'Battery voltage', unit: 'V', min: 0, max: 20),
|
||||
SensorDef(id: 'o2', displayName: 'O2 sensor', unit: 'V', min: 0, max: 1.5),
|
||||
SensorDef(id: 'gear', displayName: 'Gear', unit: '—', min: 1, max: 6),
|
||||
SensorDef(id: 'eth', displayName: 'Ethanol', unit: '%', min: 0, max: 100),
|
||||
SensorDef(
|
||||
id: 'pa', displayName: 'Baro pressure', unit: 'kPa', min: 0, max: 120),
|
||||
SensorDef(id: 'afr', displayName: 'AFR', unit: 'λ', min: 0.5, max: 2.0),
|
||||
SensorDef(
|
||||
id: 'strim', displayName: 'Short trim', unit: '%', min: -30, max: 30),
|
||||
SensorDef(
|
||||
id: 'ltrim', displayName: 'Long trim', unit: '%', min: -30, max: 30),
|
||||
SensorDef(
|
||||
id: 'ain0', displayName: 'Analog input 0', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(
|
||||
id: 'ain1', displayName: 'Analog input 1', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(
|
||||
id: 'ain2', displayName: 'Analog input 2', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(
|
||||
id: 'ain3', displayName: 'Analog input 3', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(
|
||||
id: 'ain4', displayName: 'Analog input 4', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(
|
||||
id: 'ain5', displayName: 'Analog input 5', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(
|
||||
id: 'ain6', displayName: 'Analog input 6', unit: 'V', min: 0, max: 5),
|
||||
SensorDef(
|
||||
id: 'ain7', displayName: 'Analog input 7', unit: 'V', min: 0, max: 5),
|
||||
];
|
||||
|
||||
/// Master list of boolean flag sensors.
|
||||
const List<FlagDef> flagDefs = [
|
||||
FlagDef(id: 'mil', displayName: 'MIL'),
|
||||
FlagDef(id: 'fuelcut', displayName: 'Fuel Cut'),
|
||||
FlagDef(id: 'fanout', displayName: 'FAN Out'),
|
||||
FlagDef(id: 'vtec', displayName: 'VTEC'),
|
||||
FlagDef(id: 'knock', displayName: 'Knock'),
|
||||
FlagDef(id: 'mil', displayName: 'MIL'),
|
||||
FlagDef(id: 'fuelcut', displayName: 'Fuel Cut'),
|
||||
FlagDef(id: 'fanout', displayName: 'FAN Out'),
|
||||
FlagDef(id: 'vtec', displayName: 'VTEC'),
|
||||
FlagDef(id: 'knock', displayName: 'Knock'),
|
||||
FlagDef(id: 'revlimit', displayName: 'Rev Limit'),
|
||||
FlagDef(id: 'launch', displayName: 'Launch'),
|
||||
FlagDef(id: 'launch', displayName: 'Launch'),
|
||||
];
|
||||
|
||||
@ -13,32 +13,35 @@ SensorState parseS300(Uint8List frame) {
|
||||
throw ArgumentError('S300 frame NEG8 checksum failed');
|
||||
}
|
||||
|
||||
// RPM: bytes 3..4 uint16 BE, used directly
|
||||
final int rpmRaw = _readUint16BE(frame, 3);
|
||||
final double rpm = rpmRaw.toDouble();
|
||||
// RPM: bytes 2..3 uint16 BE, used directly.
|
||||
// The S300 live frame starts its sensor payload at byte 2; byte 0..1 are
|
||||
// the protocol header. Reading from byte 3 shifts all high-rate values.
|
||||
final int rpmRaw = _readUint16BE(frame, 2);
|
||||
final double rpm = (rpmRaw - 256).clamp(0, 12000).toDouble();
|
||||
|
||||
// VSS: bytes 5..6
|
||||
final int vssRaw = _readUint16BE(frame, 5);
|
||||
// VSS: bytes 5..6 little-endian pulse period.
|
||||
final int vssRaw = _readUint16LE(frame, 5);
|
||||
final double vss =
|
||||
(vssRaw < 893 || vssRaw == 0xFFFF) ? 0.0 : 228480.0 / vssRaw;
|
||||
(vssRaw < 893 || vssRaw == 0xFFFF) ? 0.0 : 144256.0 / vssRaw;
|
||||
|
||||
// MAP: bytes 7..8 — raw / 10 = kPa
|
||||
final int mapRaw = _readUint16BE(frame, 7);
|
||||
final double map = mapRaw / 10.0;
|
||||
// MAP: bytes 7..8 little-endian. SManager displays this as gauge pressure
|
||||
// in its "/psi column rather than raw absolute kPa.
|
||||
final int mapRaw = _readUint16LE(frame, 7);
|
||||
final double map = ((mapRaw / 10.0) - 103.8) * 0.1450377377;
|
||||
|
||||
// TPS: byte 9
|
||||
final int tpsRaw = frame[9];
|
||||
final double tps = (tpsRaw < 25) ? 0.0 : (tpsRaw * 51.0) / 46.0;
|
||||
final double tps = ((tpsRaw - 25) * 100.0 / (239 - 25)).clamp(0.0, 100.0);
|
||||
|
||||
// INJ: bytes 10..11 raw ms
|
||||
final int injRaw = _readUint16BE(frame, 10);
|
||||
final double inj = injRaw.toDouble();
|
||||
// INJ: bytes 10..11 little-endian timer ticks.
|
||||
final int injRaw = _readUint16LE(frame, 10);
|
||||
final double inj = injRaw / 240.5;
|
||||
|
||||
// IGN: byte 12 — (raw + 120) / 2
|
||||
final double ign = (frame[12] + 120) / 2.0;
|
||||
// IGN: byte 12 — SManager-aligned degrees.
|
||||
final double ign = (frame[12] - 66) / 2.0;
|
||||
|
||||
// O2: byte 16 raw
|
||||
final double o2 = frame[16].toDouble();
|
||||
// O2: byte 16, 0..5V scaled over 8-bit ADC range.
|
||||
final double o2 = frame[16] * 5.0 / 256.0;
|
||||
|
||||
// SW bitmaps
|
||||
final int sw1 = frame[0x11]; // +17
|
||||
@ -55,8 +58,8 @@ SensorState parseS300(Uint8List frame) {
|
||||
// Ltrim: byte 0x2B hex = 43 decimal
|
||||
final double ltrim = frame[0x2B].toDouble();
|
||||
|
||||
// PA: byte 0x2C hex = 44 decimal
|
||||
final double pa = frame[0x2C].toDouble();
|
||||
// PA: byte 0x3C hex = 60 decimal
|
||||
final double pa = frame[0x3C].toDouble();
|
||||
|
||||
// ECT: byte 0x2D hex = 45 decimal
|
||||
final double ect = (tempXlt[frame[0x2D]] + 40).toDouble();
|
||||
@ -64,8 +67,8 @@ SensorState parseS300(Uint8List frame) {
|
||||
// IAT: byte 0x2E hex = 46 decimal
|
||||
final double iat = (tempXlt[frame[0x2E]] + 40).toDouble();
|
||||
|
||||
// BAT: byte 0x30 hex = 48 decimal
|
||||
final double bat = frame[0x30] * 26.0 / 270.0;
|
||||
// BAT: byte 0x24 hex = 36 decimal
|
||||
final double bat = frame[0x24] * 26.0 / 270.0;
|
||||
|
||||
// ERR bytes: 0x31..0x34 (4 bytes for S300)
|
||||
final List<int> errBytes = [
|
||||
@ -147,3 +150,7 @@ SensorState parseS300(Uint8List frame) {
|
||||
int _readUint16BE(Uint8List frame, int offset) {
|
||||
return (frame[offset] << 8) | frame[offset + 1];
|
||||
}
|
||||
|
||||
int _readUint16LE(Uint8List frame, int offset) {
|
||||
return frame[offset] | (frame[offset + 1] << 8);
|
||||
}
|
||||
|
||||
@ -87,8 +87,8 @@ class RawFrameDebugScreen extends ConsumerWidget {
|
||||
|
||||
List<_RowData> _decodedRows(SensorState s) => [
|
||||
_RowData('RPM', s.rpm.toStringAsFixed(0)),
|
||||
_RowData('Speed', s.vss.toStringAsFixed(2)),
|
||||
_RowData('MAP', s.map.toStringAsFixed(2)),
|
||||
_RowData('Speed mph', s.vss.toStringAsFixed(2)),
|
||||
_RowData('MAP psi', s.map.toStringAsFixed(2)),
|
||||
_RowData('TPS', s.tps.toStringAsFixed(2)),
|
||||
_RowData('ECT', s.ect.toStringAsFixed(2)),
|
||||
_RowData('IAT', s.iat.toStringAsFixed(2)),
|
||||
@ -98,8 +98,8 @@ class RawFrameDebugScreen extends ConsumerWidget {
|
||||
];
|
||||
|
||||
List<_RowData> _s300CandidateRows(Uint8List f) {
|
||||
final rpmBe = _u16be(f, 3);
|
||||
final rpmLe = _u16le(f, 3);
|
||||
final rpmBe = _u16be(f, 2);
|
||||
final rpmShiftedBe = _u16be(f, 3);
|
||||
final vssBe = _u16be(f, 5);
|
||||
final vssLe = _u16le(f, 5);
|
||||
final mapBe = _u16be(f, 7);
|
||||
@ -107,18 +107,24 @@ class RawFrameDebugScreen extends ConsumerWidget {
|
||||
|
||||
return [
|
||||
_RowData('Header bytes', '${_b(f, 0)} ${_b(f, 1)} ${_b(f, 2)}'),
|
||||
_RowData('RPM @3 BE / LE', '$rpmBe / $rpmLe'),
|
||||
_RowData('RPM @2 BE', rpmBe.toString()),
|
||||
_RowData('RPM shifted @3 BE', rpmShiftedBe.toString()),
|
||||
_RowData('VSS raw @5 BE / LE', '$vssBe / $vssLe'),
|
||||
_RowData('VSS current BE km/h', _s300Vss(vssBe).toStringAsFixed(2)),
|
||||
_RowData('VSS alternate LE km/h', _s300Vss(vssLe).toStringAsFixed(2)),
|
||||
_RowData('VSS current LE mph', _s300Vss(vssLe).toStringAsFixed(2)),
|
||||
_RowData('MAP raw @7 BE / LE', '$mapBe / $mapLe'),
|
||||
_RowData('MAP /10 BE / LE',
|
||||
'${(mapBe / 10).toStringAsFixed(2)} / ${(mapLe / 10).toStringAsFixed(2)}'),
|
||||
_RowData('MAP current psi', _s300Map(mapLe).toStringAsFixed(2)),
|
||||
_RowData('TPS raw @9', f[9].toString()),
|
||||
_RowData('TPS current', _s300Tps(f[9]).toStringAsFixed(2)),
|
||||
_RowData('INJ raw @10 LE', _u16le(f, 10).toString()),
|
||||
_RowData('INJ current ms', (_u16le(f, 10) / 240.5).toStringAsFixed(2)),
|
||||
_RowData('IGN raw @12', f[12].toString()),
|
||||
_RowData('IGN current deg', ((f[12] - 66) / 2.0).toStringAsFixed(2)),
|
||||
_RowData('O2 current V', (f[16] * 5.0 / 256.0).toStringAsFixed(2)),
|
||||
_RowData('ECT raw @45', f[0x2D].toString()),
|
||||
_RowData('IAT raw @46', f[0x2E].toString()),
|
||||
_RowData('BAT raw @48', f[0x30].toString()),
|
||||
_RowData('BAT raw @36', f[0x24].toString()),
|
||||
_RowData('BAT current V', (f[0x24] * 26.0 / 270.0).toStringAsFixed(2)),
|
||||
_RowData('PA raw @60', f[0x3C].toString()),
|
||||
_RowData('ERR bytes @49-52',
|
||||
'${_b(f, 0x31)} ${_b(f, 0x32)} ${_b(f, 0x33)} ${_b(f, 0x34)}'),
|
||||
];
|
||||
@ -130,7 +136,11 @@ class RawFrameDebugScreen extends ConsumerWidget {
|
||||
f[o].toRadixString(16).padLeft(2, '0');
|
||||
|
||||
static double _s300Vss(int raw) {
|
||||
return (raw < 893 || raw == 0xFFFF) ? 0.0 : 228480.0 / raw;
|
||||
return (raw < 893 || raw == 0xFFFF) ? 0.0 : 144256.0 / raw;
|
||||
}
|
||||
|
||||
static double _s300Map(int raw) {
|
||||
return ((raw / 10.0) - 103.8) * 0.1450377377;
|
||||
}
|
||||
|
||||
static double _s300Tps(int raw) {
|
||||
|
||||
@ -22,20 +22,21 @@ Uint8List _buildS300Frame({
|
||||
// Header
|
||||
frame[0] = 0x1B;
|
||||
frame[1] = 0x00;
|
||||
// RPM: bytes 3..4
|
||||
frame[3] = (rpm >> 8) & 0xFF;
|
||||
frame[4] = rpm & 0xFF;
|
||||
// VSS: bytes 5..6
|
||||
frame[5] = (vss >> 8) & 0xFF;
|
||||
frame[6] = vss & 0xFF;
|
||||
// MAP: bytes 7..8
|
||||
frame[7] = (mapRaw >> 8) & 0xFF;
|
||||
frame[8] = mapRaw & 0xFF;
|
||||
// RPM: bytes 2..3, raw stream is display RPM + 256
|
||||
final rpmRaw = rpm + 256;
|
||||
frame[2] = (rpmRaw >> 8) & 0xFF;
|
||||
frame[3] = rpmRaw & 0xFF;
|
||||
// VSS: bytes 5..6 little-endian
|
||||
frame[5] = vss & 0xFF;
|
||||
frame[6] = (vss >> 8) & 0xFF;
|
||||
// MAP: bytes 7..8 little-endian
|
||||
frame[7] = mapRaw & 0xFF;
|
||||
frame[8] = (mapRaw >> 8) & 0xFF;
|
||||
// TPS: byte 9
|
||||
frame[9] = tps;
|
||||
// INJ: bytes 10..11
|
||||
frame[10] = (inj >> 8) & 0xFF;
|
||||
frame[11] = inj & 0xFF;
|
||||
// INJ: bytes 10..11 little-endian
|
||||
frame[10] = inj & 0xFF;
|
||||
frame[11] = (inj >> 8) & 0xFF;
|
||||
// IGN: byte 12
|
||||
frame[12] = ign;
|
||||
// KRtrd: byte 13
|
||||
@ -46,8 +47,8 @@ Uint8List _buildS300Frame({
|
||||
frame[0x13] = sw3;
|
||||
// ECT: byte 0x2D
|
||||
frame[0x2D] = ect;
|
||||
// BAT: byte 0x30
|
||||
frame[0x30] = bat;
|
||||
// BAT: byte 0x24
|
||||
frame[0x24] = bat;
|
||||
|
||||
// Stamp NEG8 checksum at byte 127
|
||||
int neg8 = 0;
|
||||
@ -74,17 +75,17 @@ void main() {
|
||||
});
|
||||
|
||||
test('parses VSS correctly when raw >= 893', () {
|
||||
// vss = 228480 / 1000 = 228.48 km/h
|
||||
// vss = 144256 / 1000 = 144.256 mph
|
||||
final frame = _buildS300Frame(vss: 1000);
|
||||
final state = parseS300(frame);
|
||||
expect(state.vss, closeTo(228.48, 0.01));
|
||||
expect(state.vss, closeTo(144.26, 0.01));
|
||||
});
|
||||
|
||||
test('parses MAP = raw / 10', () {
|
||||
// mapRaw = 1000 → 100.0 kPa
|
||||
final frame = _buildS300Frame(mapRaw: 1000);
|
||||
test('parses MAP as SManager-style psi', () {
|
||||
// mapRaw = 1148 -> 114.8 kPa absolute -> about 1.6 psi displayed
|
||||
final frame = _buildS300Frame(mapRaw: 1148);
|
||||
final state = parseS300(frame);
|
||||
expect(state.map, equals(100.0));
|
||||
expect(state.map, closeTo(1.6, 0.01));
|
||||
});
|
||||
|
||||
test('parses TPS = 0 when raw < 25', () {
|
||||
@ -94,10 +95,10 @@ void main() {
|
||||
});
|
||||
|
||||
test('parses TPS correctly when raw >= 25', () {
|
||||
// tps raw=46 → 46*51/46 = 51.0
|
||||
final frame = _buildS300Frame(tps: 46);
|
||||
// tps raw=134 -> approximately 51%
|
||||
final frame = _buildS300Frame(tps: 134);
|
||||
final state = parseS300(frame);
|
||||
expect(state.tps, closeTo(51.0, 0.01));
|
||||
expect(state.tps, closeTo(50.93, 0.01));
|
||||
});
|
||||
|
||||
test('parses ECT using temp lookup table', () {
|
||||
@ -115,10 +116,10 @@ void main() {
|
||||
});
|
||||
|
||||
test('IGN decode: (raw + 120) / 2', () {
|
||||
// ign raw = 20 → (20+120)/2 = 70.0°
|
||||
final frame = _buildS300Frame(ign: 20);
|
||||
// ign raw = 164 -> 49.0°
|
||||
final frame = _buildS300Frame(ign: 164);
|
||||
final state = parseS300(frame);
|
||||
expect(state.ign, equals(70.0));
|
||||
expect(state.ign, equals(49.0));
|
||||
});
|
||||
|
||||
test('MIL flag set from SW2 bit5', () {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user