hondavert-dev/lib/ui/screens/dtc/dtc_screen.dart
metatroncubeswdev ba79775fbc Fix DTC tables and Clear DTC result; auto-set ECU type from adapter name
- Rewrite S300 and KPro DTC tables from hvbt_protocol.pdf (the previous
  tables did not match the PDF; e.g. ERR01.6 is P0350 IGM, not P1361).
- Clear DTC: the adapter never echoes the documented ack, but the ECU does
  clear (bench: 3 of 4 codes cleared, P0350 re-set at once as an active
  fault). Judge success from the live ERR bytes; wait only 0.6 s for the
  ack so polling resumes, and report "N of M cleared".
- Set the ECU protocol from the adapter name (S300/KPro) on connect; a
  stale KPro setting made an S300 adapter look unresponsive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:08:29 -04:00

242 lines
8.0 KiB
Dart

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<DtcScreen> createState() => _DtcScreenState();
}
class _DtcScreenState extends ConsumerState<DtcScreen> {
bool _clearing = false;
Uint8List? _lastRaw;
String _hex(Uint8List bytes) {
final parts = <String>[];
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<void>(
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<void> _confirmAndClear() async {
final confirmed = await showDialog<bool>(
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),
),
),
),
),
),
],
);
}
}