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 = []; 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); }