hondavert-dev/lib/ui/screens/dtc/dtc_screen.dart
metatroncubeswdev e4bcfb57a6 Meet Requrements.pdf: selectable dashboard, units, list/graph, light mode, playback
Dashboard
- Selectable main sensor (gauge) + sub sensor, 10 selectable values
  (landscape 5 left + 5 right, portrait 2x5 on one screen) and 4
  selectable flags; long-press to change; persisted.
- Flags: the requirement examples (MIL, FAN Out, VTEC Out, Knock, WBO /
  IAT / CLT Error - errors derived from the stored DTC bits) plus the
  other S300 status bits (cuts, launch, closed loop, VTP, AC, brake).
- Record button moved to the app bar; debug screen moved to Settings.

Sensor list: 1 column on phone portrait, 2 on tablet/landscape; name,
unit, min/max, segmented bar and value.
Graph: auto-scaled Y, window max/min, current value, seconds axis and a
touch cursor; RPM/MAP/TPS/ECT by default.
Units: km/h|mph, degC|degF, kPa|bar|psi (display only), persisted.
Light/dark: palette is a ThemeExtension used by every screen; theme
persisted. SafeArea keeps content off the side navigation bar.
Datalog playback now drives the dashboard, list, graph and DTC screen,
with the playback bar on the main screen and seek updating the displays.

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

244 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/datalog_provider.dart';
import '../../theme/app_colors.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(effectiveEcuTypeProvider);
final c = AppColors.of(context);
final dtcMap =
ecuType == EcuType.s300 ? s300DtcMap : kproDtcMap;
final active = decodeDtcs(state.errBytes, dtcMap);
return Column(
children: [
// Header bar
Container(
color: c.surface,
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:
TextStyle(color: c.onSurfaceMuted, fontSize: 12),
),
],
),
),
Divider(height: 1, color: c.divider),
// 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: (_, __) =>
Divider(height: 1, color: c.divider),
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),
),
),
),
),
),
],
);
}
}