Add advanced path management, debug logging, and fix channel sync

New features:
- In-app debug log viewer with copy/clear functionality
- Advanced path management UI with history and custom path builder
- Battery indicator widget with voltage/percentage toggle
- Contact/channel filtering and sorting improvements
- Repeater command ACK tracking with path history integration

Fixes:
- Switch channel sync from parallel to sequential to prevent timeouts
- Preserve path overrides when contacts refresh from device
- Fix ACK hash computation for SMAZ-encoded messages
- Proper cleanup of pending operations on disconnect
This commit is contained in:
zach
2026-01-02 14:22:39 -07:00
parent 361dfb7808
commit ad911a1d80
32 changed files with 2914 additions and 849 deletions
+123 -16
View File
@@ -5,11 +5,19 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../models/radio_settings.dart';
import 'app_settings_screen.dart';
import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart';
class SettingsScreen extends StatelessWidget {
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
bool _showBatteryVoltage = false;
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -58,6 +66,7 @@ class SettingsScreen extends StatelessWidget {
_buildInfoRow('Name', connector.deviceDisplayName),
_buildInfoRow('ID', connector.deviceIdLabel),
_buildInfoRow('Status', connector.isConnected ? 'Connected' : 'Disconnected'),
_buildBatteryInfoRow(connector),
if (connector.selfName != null)
_buildInfoRow('Node Name', connector.selfName!),
if (connector.selfPublicKey != null)
@@ -70,6 +79,53 @@ class SettingsScreen extends StatelessWidget {
);
}
Widget _buildBatteryInfoRow(MeshCoreConnector connector) {
final percent = connector.batteryPercent;
final millivolts = connector.batteryMillivolts;
// figure out display value
final String displayValue;
if (millivolts == null) {
displayValue = '';
} else if (_showBatteryVoltage) {
displayValue = '${(millivolts / 1000.0).toStringAsFixed(2)} V';
} else {
displayValue = percent != null ? '$percent%' : '';
}
final IconData icon;
final Color? iconColor;
final Color? valueColor;
if (percent == null) {
icon = Icons.battery_unknown;
iconColor = Colors.grey;
valueColor = null;
} else if (percent <= 15) {
icon = Icons.battery_alert;
iconColor = Colors.orange;
valueColor = Colors.orange;
} else {
icon = Icons.battery_full;
iconColor = null;
valueColor = null;
}
return _buildInfoRow(
'Battery',
displayValue,
leading: Icon(icon, size: 18, color: iconColor),
valueColor: valueColor,
onTap: millivolts != null
? () {
setState(() {
_showBatteryVoltage = !_showBatteryVoltage;
});
}
: null,
);
}
Widget _buildAppSettingsCard(BuildContext context) {
return Card(
child: ListTile(
@@ -192,38 +248,89 @@ class SettingsScreen extends StatelessWidget {
Widget _buildDebugCard(BuildContext context) {
return Card(
child: ListTile(
leading: const Icon(Icons.bug_report_outlined),
title: const Text('BLE Debug Log'),
subtitle: const Text('Commands, responses, and status'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const BleDebugLogScreen()),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
'Debug',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
ListTile(
leading: const Icon(Icons.bluetooth_outlined),
title: const Text('BLE Debug Log'),
subtitle: const Text('BLE commands, responses, and raw data'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const BleDebugLogScreen()),
);
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.code_outlined),
title: const Text('App Debug Log'),
subtitle: const Text('Application debug messages'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AppDebugLogScreen()),
);
},
),
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
Widget _buildInfoRow(
String label,
String value, {
Widget? leading,
Color? valueColor,
VoidCallback? onTap,
}) {
final row = Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(color: Colors.grey[600])),
Row(
children: [
if (leading != null) ...[
leading,
const SizedBox(width: 8),
],
Text(label, style: TextStyle(color: Colors.grey[600])),
],
),
Flexible(
child: Text(
value,
style: const TextStyle(fontWeight: FontWeight.w500),
style: TextStyle(
fontWeight: FontWeight.w500,
color: valueColor,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
if (onTap != null) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(4),
child: row,
);
}
return row;
}
void _editNodeName(BuildContext context, MeshCoreConnector connector) {