feat: Finish discovery flow

ref:
This commit is contained in:
Janez T
2026-03-14 21:04:23 +01:00
parent 6e9e9e397d
commit 6de12225fc
14 changed files with 2170 additions and 404 deletions

View File

@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:meshcore_client/meshcore_client.dart' show LogRxDataInfo;
class SignalMetric {
final String valueLabel;
final Color color;
final int activeBars;
const SignalMetric({
required this.valueLabel,
required this.color,
required this.activeBars,
});
static SignalMetric? fromRxInfo(LogRxDataInfo? rxInfo) {
if (rxInfo == null) return null;
return fromValues(rssiDbm: rxInfo.rssiDbm, snrDb: rxInfo.snrDb);
}
static SignalMetric? fromValues({int? rssiDbm, double? snrDb}) {
if (snrDb != null) {
return SignalMetric(
valueLabel: '${snrDb.toStringAsFixed(1)}dB',
color: snrDb >= 10
? Colors.green
: snrDb >= 0
? Colors.amber
: Colors.redAccent,
activeBars: snrDb >= 10
? 3
: snrDb >= 0
? 2
: 1,
);
}
if (rssiDbm != null) {
return SignalMetric(
valueLabel: '$rssiDbm dBm',
color: rssiDbm >= -80
? Colors.green
: rssiDbm >= -95
? Colors.amber
: Colors.redAccent,
activeBars: rssiDbm >= -80
? 3
: rssiDbm >= -95
? 2
: 1,
);
}
return null;
}
}
class CompactSignalIndicator extends StatelessWidget {
final SignalMetric metric;
final bool dense;
const CompactSignalIndicator({
super.key,
required this.metric,
this.dense = false,
});
@override
Widget build(BuildContext context) {
final inactive = Theme.of(context).colorScheme.outlineVariant;
final barWidth = dense ? 4.0 : 5.0;
final baseHeight = dense ? 8.0 : 10.0;
final barStep = dense ? 6.0 : 8.0;
final barSpacing = dense ? 2.0 : 3.0;
final labelGap = dense ? 4.0 : 6.0;
final labelFontSize = dense ? 10.0 : 12.0;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (var index = 0; index < 3; index++) ...[
if (index > 0) SizedBox(width: barSpacing),
Container(
width: barWidth,
height: baseHeight + (index * barStep),
decoration: BoxDecoration(
color: index < metric.activeBars ? metric.color : inactive,
borderRadius: BorderRadius.circular(2),
),
),
],
],
),
SizedBox(height: labelGap),
Text(
metric.valueLabel,
style: TextStyle(
fontSize: labelFontSize,
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
);
}
}

View File

@@ -17,9 +17,24 @@ import '../../utils/log_rx_route_decoder.dart';
import '../../utils/trace_node_resolver.dart';
class MessageTraceSheet extends StatefulWidget {
final Message message;
final Message? message;
final List<int>? packetPathOverride;
final String? descriptionOverride;
final String? noRelayMatchTextOverride;
const MessageTraceSheet({super.key, required this.message});
const MessageTraceSheet({super.key, required this.message})
: assert(message != null),
packetPathOverride = null,
descriptionOverride = null,
noRelayMatchTextOverride = null;
const MessageTraceSheet.packetPath({
super.key,
required List<int> packetPath,
this.descriptionOverride,
this.noRelayMatchTextOverride,
}) : message = null,
packetPathOverride = packetPath;
@override
State<MessageTraceSheet> createState() => _MessageTraceSheetState();
@@ -40,20 +55,27 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
final contactsProvider = context.read<ContactsProvider>();
final messagesProvider = context.read<MessagesProvider>();
final preferredHashSize = await RouteHashPreferences.getHashSize();
final storedPath = messagesProvider
.getMessageReceptionDetails(widget.message.id)
?.pathBytes;
final packetPath = (storedPath != null && storedPath.isNotEmpty)
? storedPath
: _extractPathFromPacketLogs(
logs: connectionProvider.bleService.packetLogs,
message: widget.message,
);
List<int>? packetPath = widget.packetPathOverride;
String? senderPrefix;
String? recipientPrefix;
if (widget.message case final message?) {
final storedPath = messagesProvider
.getMessageReceptionDetails(message.id)
?.pathBytes;
packetPath = (storedPath != null && storedPath.isNotEmpty)
? storedPath
: _extractPathFromPacketLogs(
logs: connectionProvider.bleService.packetLogs,
message: message,
);
senderPrefix = _toPrefixHex(message.senderPublicKeyPrefix);
recipientPrefix = message.recipientPublicKey != null
? _toPrefixHex(message.recipientPublicKey)
: _toPrefixHex(connectionProvider.deviceInfo.publicKey);
}
final senderPrefix = _toPrefixHex(widget.message.senderPublicKeyPrefix);
final recipientPrefix = widget.message.recipientPublicKey != null
? _toPrefixHex(widget.message.recipientPublicKey)
: _toPrefixHex(connectionProvider.deviceInfo.publicKey);
final localNodes = _localNodesFromContacts(contactsProvider);
final localPublicKeys = localNodes.map((node) => node.publicKey).toSet();
var trace = _buildTraceResult(
@@ -66,7 +88,9 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
);
if (_isCompleteTrace(
trace,
expectedRelayCount: math.max(0, widget.message.pathLen),
expectedRelayCount: widget.message == null
? 0
: math.max(0, widget.message!.pathLen),
)) {
return trace;
}
@@ -153,9 +177,10 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
trace.mode == TraceMode.packetPath
? 'Relay path from packet path bytes'
: 'Relay path inferred from hop count (${widget.message.pathLen})',
widget.descriptionOverride ??
(trace.mode == TraceMode.packetPath
? 'Relay path from packet path bytes'
: 'Relay path inferred from hop count (${widget.message!.pathLen})'),
style: Theme.of(context).textTheme.bodySmall,
),
),
@@ -257,13 +282,14 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
),
),
if (routeEntries.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(
'No named nodes could be matched for this trace.',
widget.noRelayMatchTextOverride ??
'No named nodes could be matched for this trace.',
),
),
...routeEntries.asMap().entries.map(
@@ -345,6 +371,9 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
if (concrete.isEmpty) return const [];
if (trace.mode == TraceMode.packetPath) {
if (widget.message == null) {
return concrete;
}
if (concrete.length <= 1) return const [];
return concrete.sublist(1);
}
@@ -488,7 +517,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
nodes: nodes,
sender: senderNode.node,
recipient: recipientNode.node,
relayCount: math.max(0, widget.message.pathLen),
relayCount: math.max(0, widget.message!.pathLen),
);
final matchedPathNodes = <ResolvedTraceNode>[
if (senderNode.node != null) senderNode,