- 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>
242 lines
8.0 KiB
Dart
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),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|