hondavert-dev/lib/ui/screens/debug/raw_frame_debug_screen.dart
HVBT Dev 763821171c feat: Add ECU Frame Debug Screen and DTC Screen
- Implemented RawFrameDebugScreen for displaying ECU frame data and diagnostics.
- Created DtcScreen to show active Diagnostic Trouble Codes (DTCs) with detailed descriptions.
- Added DtcRow widget for individual DTC representation.
- Introduced GraphScreen for visualizing sensor data over time with customizable time windows.
- Developed SensorListScreen to display available sensors and their current values.
- Enhanced SettingsScreen with options for Bluetooth connection, ECU protocol selection, polling interval, and theme settings.
- Added AppColors and AppTheme for consistent theming across the application.
- Implemented BtStatusChip to show Bluetooth connection status in the app bar.
- Created RecordFab for starting and stopping data recording sessions.
2026-07-26 23:14:58 +05:30

237 lines
7.1 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'),
],
),
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', s.vss.toStringAsFixed(2)),
_RowData('MAP', 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, 3);
final rpmLe = _u16le(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 @3 BE / LE', '$rpmBe / $rpmLe'),
_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('MAP raw @7 BE / LE', '$mapBe / $mapLe'),
_RowData('MAP /10 BE / LE',
'${(mapBe / 10).toStringAsFixed(2)} / ${(mapLe / 10).toStringAsFixed(2)}'),
_RowData('TPS raw @9', f[9].toString()),
_RowData('TPS current', _s300Tps(f[9]).toStringAsFixed(2)),
_RowData('ECT raw @45', f[0x2D].toString()),
_RowData('IAT raw @46', f[0x2E].toString()),
_RowData('BAT raw @48', f[0x30].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 : 228480.0 / raw;
}
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);
}