mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
195 lines
5.8 KiB
Dart
195 lines
5.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../connector/meshcore_connector.dart';
|
|
import '../l10n/l10n.dart';
|
|
import 'signal_ui.dart';
|
|
|
|
class SNRUi {
|
|
final IconData icon;
|
|
final Color color;
|
|
final String text;
|
|
const SNRUi(this.icon, this.color, this.text);
|
|
}
|
|
|
|
List<double> getSNRfromSF(int spreadingFactor) {
|
|
switch (spreadingFactor) {
|
|
case 7:
|
|
return [4.0, -2.0, -4.0, -6.0];
|
|
case 8:
|
|
return [4.0, -4.0, -6.0, -8.0];
|
|
case 9:
|
|
return [4.0, -6.0, -8.0, -10.0];
|
|
case 10:
|
|
return [4.0, -8.0, -10.0, -13.0];
|
|
case 11:
|
|
return [4.0, -10.0, -12.5, -15.0];
|
|
case 12:
|
|
return [4.0, -12.5, -15.0, -18.0];
|
|
default:
|
|
return []; // Or throw Exception('Invalid SF: $spreadingFactor');
|
|
}
|
|
}
|
|
|
|
SNRUi snrUiFromSNR(double? snr, int? spreadingFactor) {
|
|
if (snr == null ||
|
|
spreadingFactor == null ||
|
|
spreadingFactor < 7 ||
|
|
spreadingFactor > 12) {
|
|
return const SNRUi(Icons.signal_cellular_off, Colors.grey, '—');
|
|
}
|
|
|
|
final snrLevels = getSNRfromSF(spreadingFactor);
|
|
|
|
String text = '${snr.toStringAsFixed(1)} dB';
|
|
final tier = snr >= snrLevels[0]
|
|
? 0
|
|
: snr >= snrLevels[1]
|
|
? 1
|
|
: snr >= snrLevels[2]
|
|
? 2
|
|
: snr >= snrLevels[3]
|
|
? 3
|
|
: 4;
|
|
final signalUi = signalUiForStrengthTier(tier);
|
|
|
|
return SNRUi(signalUi.icon, signalUi.color, text);
|
|
}
|
|
|
|
class SNRIndicator extends StatefulWidget {
|
|
final MeshCoreConnector connector;
|
|
|
|
const SNRIndicator({super.key, required this.connector});
|
|
|
|
@override
|
|
State<SNRIndicator> createState() => _SNRIndicatorState();
|
|
}
|
|
|
|
class _SNRIndicatorState extends State<SNRIndicator> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final directRepeaters = widget.connector.directRepeaters;
|
|
final directBestRepeaters = List.of(directRepeaters)
|
|
..sort((a, b) => (b.ranking).compareTo(a.ranking));
|
|
final directRepeater = directBestRepeaters.isEmpty
|
|
? null
|
|
: directBestRepeaters.first;
|
|
|
|
final snrUi = snrUiFromSNR(
|
|
directBestRepeaters.isNotEmpty ? directRepeater!.snr : null,
|
|
widget.connector.currentSf,
|
|
);
|
|
|
|
return ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
|
child: InkWell(
|
|
onTap: directRepeater != null
|
|
? () => _showFullPathDialog(context, directBestRepeaters)
|
|
: null,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(snrUi.icon, size: 18, color: snrUi.color),
|
|
Text(
|
|
snrUi.text,
|
|
style: TextStyle(fontSize: 12, color: snrUi.color),
|
|
),
|
|
if (directRepeater != null)
|
|
Text(
|
|
'${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatLastUpdated(DateTime lastSeen) {
|
|
final now = DateTime.now();
|
|
final diff = now.difference(lastSeen);
|
|
if (diff.isNegative) {
|
|
return "0s";
|
|
}
|
|
if (diff.inMinutes < 1) {
|
|
return "${diff.inSeconds}s";
|
|
}
|
|
if (diff.inMinutes < 60) {
|
|
return "${diff.inMinutes}m";
|
|
}
|
|
if (diff.inHours < 24) {
|
|
final hours = diff.inHours;
|
|
return "${hours}h";
|
|
}
|
|
final days = diff.inDays;
|
|
return "${days}d";
|
|
}
|
|
|
|
void _showFullPathDialog(
|
|
BuildContext context,
|
|
List<DirectRepeater> directBestRepeaters,
|
|
) {
|
|
final l10n = context.l10n;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(l10n.snrIndicator_nearByRepeaters),
|
|
content: SizedBox(
|
|
width: double.maxFinite,
|
|
child: Scrollbar(
|
|
child: ListView.separated(
|
|
shrinkWrap: true,
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
itemCount: directBestRepeaters.length,
|
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
|
itemBuilder: (context, index) {
|
|
final repeater = directBestRepeaters[index];
|
|
final snrUi = snrUiFromSNR(
|
|
repeater.snr,
|
|
widget.connector.currentSf,
|
|
);
|
|
final allContacts = widget.connector.allContacts;
|
|
final name = allContacts
|
|
.where((c) => c.publicKey.first == repeater.pubkeyFirstByte)
|
|
.map((c) => c.name)
|
|
.firstOrNull;
|
|
|
|
return Column(
|
|
children: [
|
|
ListTile(
|
|
leading: Icon(snrUi.icon, color: snrUi.color),
|
|
title: Text(
|
|
name ??
|
|
repeater.pubkeyFirstByte
|
|
.toRadixString(16)
|
|
.padLeft(2, '0'),
|
|
),
|
|
subtitle: Text(
|
|
'SNR: ${repeater.snr.toStringAsFixed(1)} dB\n${l10n.snrIndicator_lastSeen}: ${_formatLastUpdated(repeater.lastUpdated)}',
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(l10n.common_close),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|