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/logging/app_log.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 ConsumerStatefulWidget { const RawFrameDebugScreen({super.key}); @override ConsumerState createState() => _RawFrameDebugScreenState(); } class _RawFrameDebugScreenState extends ConsumerState { Uint8List? _probeResult; bool _probing = false; Future _runProbe() async { setState(() { _probing = true; _probeResult = null; }); final result = await ref.read(btProvider.notifier).probeOfficialProtocol(); if (!mounted) return; setState(() { _probing = false; _probeResult = result; }); } @override Widget build(BuildContext context) { final bt = ref.watch(btProvider); final settings = ref.watch(settingsProvider); final sensorAsync = ref.watch(sensorStateProvider); final state = ref.watch(latestSensorProvider); final decodeStats = ref.watch(decodeStatsProvider); final frame = bt.lastFrame; final hex = frame == null ? '' : _hex(frame); return Scaffold( appBar: AppBar( title: const Text('ECU Frame Debug'), actions: [ IconButton( tooltip: 'Copy app log', icon: const Icon(Icons.description_outlined), onPressed: () async { await Clipboard.setData(ClipboardData(text: AppLog.dump())); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'App log copied (${AppLog.length} lines) — paste it into an email/message')), ); } }, ), 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('Link status', bt.status.name), _RowData('ECU data', bt.dataStatus.name), _RowData('Poll interval', '${settings.pollingInterval.inMilliseconds} ms'), _RowData('Requests sent', bt.requestsSent.toString()), _RowData('Valid replies', bt.frameCount.toString()), _RowData(' live ECU frames', bt.liveFrames.toString()), _RowData(' adapter default frames', bt.defaultFrames.toString()), _RowData('Timeouts (no reply)', bt.timeouts.toString()), _RowData('Dropped (bad checksum)', bt.droppedFrames.toString()), _RowData('Last reply time', bt.lastResponseMs == null ? '-' : '${bt.lastResponseMs} ms'), _RowData('Auto-reconnects', bt.reconnects.toString()), _RowData('Last frame bytes', frame?.length.toString() ?? 'none'), if (frame != null) _RowData( 'Checksum', validateFrame(frame) ? 'valid' : 'bad', ), ], ), const SizedBox(height: 12), _ProtocolProbeCard( connected: bt.isConnected, probing: _probing, result: _probeResult, onProbe: _runProbe, ), const SizedBox(height: 12), _Section( title: 'Decode Pipeline', rows: [ _RowData('Decoded -> UI', decodeStats.decoded.toString()), _RowData('Skipped (decode error)', decodeStats.decodeErrors.toString()), _RowData( 'sensorStateProvider raw', sensorAsync.when( data: (s) => 'data(rpm=${s.rpm.toStringAsFixed(0)}, age=${DateTime.now().difference(s.timestamp).inMilliseconds}ms)', loading: () => 'loading (no frame yet)', error: (e, st) => 'error: $e', ), ), _RowData( 'latestSensorProvider age', '${DateTime.now().difference(state.timestamp).inMilliseconds}ms since decode', ), ], ), 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 km/h', s.vss.toStringAsFixed(2)), _RowData('MAP kPa', 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)), _RowData('MIL (unverified)', s.mil.toString()), _RowData('Fuel Pump', s.fuelPump.toString()), _RowData('Fan Out (unverified)', s.fanOut.toString()), _RowData('VTEC (unverified)', s.vtec.toString()), _RowData('Knock', s.knock.toString()), _RowData('Rev Limit (unverified)', s.revLimit.toString()), _RowData('Launch (unverified)', s.launch.toString()), ]; List<_RowData> _s300CandidateRows(Uint8List f) { final rpmLe = _u16le(f, 3); final vssLe = _u16le(f, 5); final mapLe = _u16le(f, 7); return [ _RowData('Header bytes', '${_b(f, 0)} ${_b(f, 1)} ${_b(f, 2)}'), _RowData('RPM raw @3 LE', rpmLe.toString()), _RowData('VSS raw @5 LE', vssLe.toString()), _RowData('VSS current km/h', _s300Vss(vssLe).toStringAsFixed(2)), _RowData('MAP raw @7 LE', mapLe.toString()), _RowData('MAP current kPa', _s300Map(mapLe).toStringAsFixed(2)), _RowData('TPS raw @9', f[9].toString()), _RowData('TPS current', _s300Tps(f[9]).toStringAsFixed(2)), _RowData('Gear raw @27 (decimal)', f[27].toString()), _RowData('INJ raw @10 LE', _u16le(f, 10).toString()), _RowData('IGN raw @12', f[12].toString()), _RowData('O2 raw @16', f[16].toString()), _RowData('Strim raw @29 / Ltrim raw @31', '${f[29]} / ${f[31]}'), _RowData('PA raw @32', f[32].toString()), _RowData('ECT raw @33', f[33].toString()), _RowData('IAT raw @34', f[34].toString()), _RowData('BAT raw @36', f[36].toString()), _RowData('ERR00-03 @37-40', '${_b(f, 37)} ${_b(f, 38)} ${_b(f, 39)} ${_b(f, 40)}'), _RowData('Eth raw @41', f[41].toString()), _RowData('AF raw @55', f[55].toString()), _RowData('SW1 @17 (bit7..0)', '${_b(f, 17)} ${_bin(f, 17)}'), _RowData('SW2 @18 (bit7..0)', '${_b(f, 18)} ${_bin(f, 18)}'), _RowData('SW3 @19 (bit7..0)', '${_b(f, 19)} ${_bin(f, 19)}'), _RowData('SW5 @49 (bit7..0)', '${_b(f, 49)} ${_bin(f, 49)}'), _RowData('SW6 @50 (bit7..0)', '${_b(f, 50)} ${_bin(f, 50)}'), ]; } 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 String _bin(Uint8List f, int o) => f[o].toRadixString(2).padLeft(8, '0'); static double _s300Vss(int raw) { return (raw < 893 || raw == 0xFFFF) ? 0.0 : 228480.0 / raw; } static double _s300Map(int raw) { return raw / 10.0; } static double _s300Tps(int raw) { return (raw * 100.0 / 255.0).clamp(0.0, 100.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(' '); } } /// Step 1 of the data-layer review: checks whether this device/firmware /// understands the official, self-describing Hondata Bluetooth protocol /// (Bluetooth.pdf) rather than only the legacy fixed-offset frame format /// this app currently speaks. A real reply here (Protocol=0x00, ID=0x02, /// with a recognizable DeviceType byte) means migrating to that protocol is /// on the table; silence means this hardware is legacy-only. class _ProtocolProbeCard extends StatelessWidget { final bool connected; final bool probing; final Uint8List? result; final VoidCallback onProbe; const _ProtocolProbeCard({ required this.connected, required this.probing, required this.result, required this.onProbe, }); @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Official Protocol Probe', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13), ), const SizedBox(height: 4), const Text( 'Sends DL_DeviceInfo (00 02 04 00). Pauses normal polling for ' 'the duration of the probe.', style: TextStyle(color: Colors.grey, fontSize: 11), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: (connected && !probing) ? onProbe : null, child: Text(probing ? 'Probing…' : 'Send Probe'), ), ), if (result != null) ...[ const SizedBox(height: 8), for (final row in _interpret(result!)) _InfoRow(row), ], ], ), ), ); } List<_RowData> _interpret(Uint8List r) { if (r.isEmpty) { return const [ _RowData('Result', 'No response — likely legacy protocol only'), ]; } final hex = r.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' '); final rows = [_RowData('Raw bytes', hex)]; if (r.length >= 4) { final size = r[2] | (r[3] << 8); // little-endian per TDLHeader.Size rows.add(_RowData('Protocol / ID / Size', '${r[0]} / ${r[1]} / $size')); } if (r.length >= 6) { final deviceType = switch (r[4]) { 0xC0 => 'S300', 0xC1 => 'KPro', 0xC2 => 'FlashPro', _ => 'unknown (0x${r[4].toRadixString(16)})', }; rows.add(_RowData('Device type', deviceType)); rows.add(_RowData('Ignition', r[5] != 0 ? 'On' : 'Off')); } return rows; } } 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); }