import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/protocol/dtc_map.dart'; import '../../../core/providers/bt_provider.dart'; import '../../../core/providers/sensor_provider.dart'; import '../../../core/providers/settings_provider.dart'; import '../../../core/bluetooth/bt_poller.dart'; import 'widgets/dtc_row.dart'; class DtcScreen extends ConsumerStatefulWidget { const DtcScreen({super.key}); @override ConsumerState createState() => _DtcScreenState(); } class _DtcScreenState extends ConsumerState { bool _clearing = false; Uint8List? _lastRaw; String _hex(Uint8List bytes) { final parts = []; for (var i = 0; i < bytes.length; i++) { if (i > 0 && i % 16 == 0) parts.add('\n'); parts.add(bytes[i].toRadixString(16).padLeft(2, '0')); } return parts.join(' '); } void _showRawCapture() { final raw = _lastRaw; if (raw == null) return; final hex = _hex(raw); showDialog( context: context, builder: (context) => AlertDialog( title: Text('Raw capture (${raw.length} bytes)'), content: SizedBox( width: double.maxFinite, child: SingleChildScrollView( child: SelectableText( hex, style: const TextStyle(fontFamily: 'monospace', fontSize: 11), ), ), ), actions: [ TextButton( onPressed: () async { await Clipboard.setData(ClipboardData(text: hex)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Copied')), ); } }, child: const Text('Copy'), ), TextButton( onPressed: () => Navigator.pop(context), child: const Text('Close'), ), ], ), ); } Future _confirmAndClear() async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Clear Trouble Codes'), content: const Text( 'This sends a reset command to the ECU and erases all stored ' 'fault codes. This cannot be undone.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('Clear', style: TextStyle(color: Colors.red)), ), ], ), ); if (confirmed != true) return; setState(() => _clearing = true); final result = await ref.read(btProvider.notifier).clearDtcs(); if (!mounted) return; setState(() { _clearing = false; _lastRaw = result.raw; }); final String message; if (result.acknowledged) { message = 'Trouble codes cleared'; } else if (result.raw.isEmpty) { message = 'No response from ECU — codes may not have cleared'; } else { // The scan runs alongside normal polling now, so "not acknowledged" // can mean several seconds' worth of regular datalog traffic with no // ack byte pattern found in it — dumping all of that raw would be // unusable in a snackbar, so just report how much was seen. "View" // opens the full hex to check by hand for anything our narrow 3-byte // pattern match might have missed. message = 'No ack seen — ${result.raw.length} bytes of other traffic during the window'; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), action: (!result.acknowledged && result.raw.isNotEmpty) ? SnackBarAction(label: 'View', onPressed: _showRawCapture) : null, duration: const Duration(seconds: 6), ), ); } @override Widget build(BuildContext context) { final state = ref.watch(latestSensorProvider); final bt = ref.watch(btProvider); final ecuType = ref.watch(settingsProvider).ecuType; final dtcMap = ecuType == EcuType.s300 ? s300DtcMap : kproDtcMap; final active = decodeDtcs(state.errBytes, dtcMap); return Column( children: [ // Header bar Container( color: const Color(0xFF1A1A1A), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Row( children: [ Icon( active.isEmpty ? Icons.check_circle_outline : Icons.warning_amber_rounded, color: active.isEmpty ? Colors.green : Colors.amber, size: 18, ), const SizedBox(width: 8), Text( active.isEmpty ? 'No active fault codes' : '${active.length} active DTC${active.length > 1 ? "s" : ""}', style: TextStyle( color: active.isEmpty ? Colors.green : Colors.amber, fontSize: 13, fontWeight: FontWeight.w600, ), ), const Spacer(), Text( ecuType == EcuType.s300 ? 'S300' : 'KPro', style: const TextStyle(color: Colors.grey, fontSize: 12), ), ], ), ), const Divider(height: 1, color: Colors.white10), // DTC list Expanded( child: active.isEmpty ? const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.check_circle_outline, size: 56, color: Colors.green), SizedBox(height: 12), Text('All clear — no fault codes', style: TextStyle( color: Colors.grey, fontSize: 15)), ], ), ) : ListView.separated( itemCount: active.length, separatorBuilder: (_, __) => const Divider(height: 1, color: Colors.white10), itemBuilder: (context, i) { final full = active[i]; // "P0130 — O2 Sensor (front)" final parts = full.split(' — '); final code = parts.first.trim(); final desc = parts.length > 1 ? parts.sublist(1).join(' — ') : ''; return DtcRow(code: code, description: desc); }, ), ), SafeArea( top: false, child: Padding( padding: const EdgeInsets.all(16), child: SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), ), onPressed: (bt.isConnected && !_clearing) ? _confirmAndClear : null, child: Text( _clearing ? 'Clearing…' : 'CLEAR TROUBLE CODES', style: const TextStyle(fontWeight: FontWeight.bold), ), ), ), ), ), ], ); } }