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; }); // Success is judged from the live data: the adapter does not send the // ack. Codes for faults that are still present return right away. final String message; if (result.codesBefore == 0) { message = 'No stored trouble codes to clear'; } else if (result.codesCleared > 0) { final cleared = result.codesCleared; final back = result.codesBefore - cleared; message = back == 0 ? 'Trouble codes cleared' : '$cleared of ${result.codesBefore} codes cleared — $back came ' 'straight back (active fault, fix the cause first)'; } else if (result.acknowledged) { message = 'ECU confirmed the reset, but every code came straight back ' '(active faults)'; } else if (result.raw.isEmpty) { // Can't tell apart from "every fault re-set before the next frame". message = 'No codes cleared — the ECU ignored the reset, or every ' 'fault is still active'; } else { // "View" opens the raw reply hex to check by hand. message = 'Unexpected reply (${result.raw.length} bytes) — codes did ' 'not clear'; } 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), ), ), ), ), ), ], ); } }