250 lines
7.6 KiB
Dart
250 lines
7.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../core/bluetooth/bt_poller.dart';
|
|
import '../../../core/providers/bt_provider.dart';
|
|
import '../../../core/providers/sensor_provider.dart';
|
|
import '../../../core/providers/settings_provider.dart';
|
|
import '../../../core/protocol/neg8.dart';
|
|
import '../../../core/protocol/sensor_state.dart';
|
|
|
|
class RawFrameDebugScreen extends ConsumerWidget {
|
|
const RawFrameDebugScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final bt = ref.watch(btProvider);
|
|
final settings = ref.watch(settingsProvider);
|
|
final state = ref.watch(latestSensorProvider);
|
|
final frame = bt.lastFrame;
|
|
final hex = frame == null ? '' : _hex(frame);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('ECU Frame Debug'),
|
|
actions: [
|
|
IconButton(
|
|
tooltip: 'Copy raw frame',
|
|
icon: const Icon(Icons.copy),
|
|
onPressed: frame == null
|
|
? null
|
|
: () async {
|
|
await Clipboard.setData(ClipboardData(text: hex));
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Raw frame copied')),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: ListView(
|
|
padding: const EdgeInsets.all(12),
|
|
children: [
|
|
_Section(
|
|
title: 'Connection',
|
|
rows: [
|
|
_RowData('Protocol', settings.ecuType.name.toUpperCase()),
|
|
_RowData('Frames', bt.frameCount.toString()),
|
|
_RowData('Dropped', bt.droppedFrames.toString()),
|
|
_RowData('Last frame bytes', frame?.length.toString() ?? 'none'),
|
|
if (frame != null)
|
|
_RowData(
|
|
'Checksum',
|
|
validateFrame(frame) ? 'valid' : 'bad, accepted for S300',
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
_Section(
|
|
title: 'Current App Decode',
|
|
rows: _decodedRows(state),
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (frame == null)
|
|
const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(24),
|
|
child: Text(
|
|
'No ECU frame received yet.',
|
|
style: TextStyle(color: Colors.grey),
|
|
),
|
|
),
|
|
)
|
|
else ...[
|
|
if (settings.ecuType == EcuType.s300) ...[
|
|
_Section(
|
|
title: 'S300 Raw Candidates',
|
|
rows: _s300CandidateRows(frame),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
_RawHexBlock(hex: hex),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
List<_RowData> _decodedRows(SensorState s) => [
|
|
_RowData('RPM', s.rpm.toStringAsFixed(0)),
|
|
_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)),
|
|
_RowData('Battery', s.bat.toStringAsFixed(2)),
|
|
_RowData('O2', s.o2.toStringAsFixed(2)),
|
|
_RowData('AFR', s.afr.toStringAsFixed(2)),
|
|
];
|
|
|
|
List<_RowData> _s300CandidateRows(Uint8List f) {
|
|
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);
|
|
final mapLe = _u16le(f, 7);
|
|
|
|
return [
|
|
_RowData('Header bytes', '${_b(f, 0)} ${_b(f, 1)} ${_b(f, 2)}'),
|
|
_RowData('RPM @2 BE', rpmBe.toString()),
|
|
_RowData('RPM shifted @3 BE', rpmShiftedBe.toString()),
|
|
_RowData('VSS raw @5 BE / LE', '$vssBe / $vssLe'),
|
|
_RowData('VSS current LE mph', _s300Vss(vssLe).toStringAsFixed(2)),
|
|
_RowData('MAP raw @7 BE / LE', '$mapBe / $mapLe'),
|
|
_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 @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)}'),
|
|
];
|
|
}
|
|
|
|
static int _u16be(Uint8List f, int o) => (f[o] << 8) | f[o + 1];
|
|
static int _u16le(Uint8List f, int o) => f[o] | (f[o + 1] << 8);
|
|
static String _b(Uint8List f, int o) =>
|
|
f[o].toRadixString(16).padLeft(2, '0');
|
|
|
|
static double _s300Vss(int 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) {
|
|
return raw < 25 ? 0.0 : raw * 51.0 / 46.0;
|
|
}
|
|
|
|
static String _hex(Uint8List frame) {
|
|
final parts = <String>[];
|
|
for (var i = 0; i < frame.length; i++) {
|
|
if (i > 0 && i % 16 == 0) parts.add('\n');
|
|
parts.add(frame[i].toRadixString(16).padLeft(2, '0'));
|
|
}
|
|
return parts.join(' ');
|
|
}
|
|
}
|
|
|
|
class _Section extends StatelessWidget {
|
|
final String title;
|
|
final List<_RowData> rows;
|
|
|
|
const _Section({required this.title, required this.rows});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
|
),
|
|
const SizedBox(height: 8),
|
|
for (final row in rows) _InfoRow(row),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _InfoRow extends StatelessWidget {
|
|
final _RowData row;
|
|
|
|
const _InfoRow(this.row);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
row.label,
|
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Flexible(
|
|
child: Text(
|
|
row.value,
|
|
textAlign: TextAlign.right,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
fontFamily: 'monospace',
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RawHexBlock extends StatelessWidget {
|
|
final String hex;
|
|
|
|
const _RawHexBlock({required this.hex});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: SelectableText(
|
|
hex,
|
|
style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RowData {
|
|
final String label;
|
|
final String value;
|
|
|
|
const _RowData(this.label, this.value);
|
|
}
|