Fix S300 live data decoding

This commit is contained in:
HVBT Dev 2026-07-26 23:51:56 +05:30
parent 763821171c
commit 1424cafbb4
4 changed files with 132 additions and 88 deletions

View File

@ -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'),
];

View File

@ -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);
}

View File

@ -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) {

View File

@ -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', () {