issue #112 fixes and more

This commit is contained in:
Zach
2026-02-01 18:37:14 -07:00
parent 1d4c9ad9bd
commit c742d98fbb
7 changed files with 300 additions and 62 deletions
+30 -21
View File
@@ -95,6 +95,7 @@ class MeshCoreConnector extends ChangeNotifier {
double? _selfLongitude;
bool _isLoadingContacts = false;
bool _isLoadingChannels = false;
bool _hasLoadedChannels = false;
bool _batteryRequested = false;
bool _awaitingSelfInfo = false;
bool _preserveContactsOnRefresh = false;
@@ -122,7 +123,7 @@ class MeshCoreConnector extends ChangeNotifier {
List<Channel> _previousChannelsCache = [];
static const int _maxChannelSyncRetries = 3;
static const int _channelSyncTimeoutMs = 2000; // 2 second timeout per channel
static const Duration _batteryPollInterval = Duration(seconds: 30);
static const Duration _batteryPollInterval = Duration(seconds: 120);
// Services
MessageRetryService? _retryService;
@@ -927,6 +928,7 @@ class MeshCoreConnector extends ChangeNotifier {
_pendingQueueSync = false;
_isSyncingChannels = false;
_channelSyncInFlight = false;
_hasLoadedChannels = false;
_setState(MeshCoreConnectionState.disconnected);
if (!manual) {
@@ -1493,13 +1495,19 @@ class MeshCoreConnector extends ChangeNotifier {
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
}
Future<void> getChannels({int? maxChannels}) async {
Future<void> getChannels({int? maxChannels, bool force = false}) async {
if (!isConnected) return;
if (_isSyncingChannels) {
debugPrint('[ChannelSync] Already syncing channels, ignoring request');
return;
}
// Skip fetching if already loaded and not forced
if (_hasLoadedChannels && !force) {
debugPrint('[ChannelSync] Channels already loaded, skipping fetch (use force=true to reload)');
return;
}
_isLoadingChannels = true;
_isSyncingChannels = true;
_previousChannelsCache = List<Channel>.from(_channels);
@@ -1619,6 +1627,7 @@ class MeshCoreConnector extends ChangeNotifier {
_totalChannelsToRequest = 0;
if (completed) {
_hasLoadedChannels = true;
_previousChannelsCache.clear();
}
// Keep cache on failure/disconnection for future attempts
@@ -1629,7 +1638,7 @@ class MeshCoreConnector extends ChangeNotifier {
await sendFrame(buildSetChannelFrame(index, name, psk));
// Refresh channels after setting
await getChannels();
await getChannels(force: true);
}
Future<void> deleteChannel(int index) async {
@@ -1644,7 +1653,7 @@ class MeshCoreConnector extends ChangeNotifier {
// Clear in-memory messages for this channel
_channelMessages.remove(index);
// Refresh channels after deleting
await getChannels();
await getChannels(force: true);
}
void _handleFrame(List<int> data) {
@@ -2105,6 +2114,15 @@ class MeshCoreConnector extends ChangeNotifier {
}
if (message != null) {
// Ignore messages from self (device hearing its own broadcast)
// BUT allow repeated messages (pathLength indicates it went through repeater)
if (_selfPublicKey != null &&
message.senderKeyHex == pubKeyToHex(_selfPublicKey!) &&
(message.pathLength == null || message.pathLength == 0)) {
debugPrint('Ignoring direct message from self');
return;
}
final contact = _contacts.cast<Contact?>().firstWhere(
(c) => c?.publicKeyHex == message!.senderKeyHex,
orElse: () => null,
@@ -3066,28 +3084,19 @@ class MeshCoreConnector extends ChangeNotifier {
}
bool _shouldDropSelfChannelMessage(String senderName, Uint8List pathBytes) {
final selfKey = _selfPublicKey;
if (selfKey == null) return false;
if (pathBytes.length < pathHashSize) return false;
final trimmed = senderName.trim();
if (trimmed.isEmpty) return false;
final selfName = _selfName?.trim();
if (selfName == null || selfName.isEmpty) return false;
// If sender name doesn't match, keep the message
if (trimmed != selfName) return false;
final prefix = selfKey.sublist(0, pathHashSize);
for (int i = 0; i + pathHashSize <= pathBytes.length; i += pathHashSize) {
var match = true;
for (int j = 0; j < pathHashSize; j++) {
if (pathBytes[i + j] != prefix[j]) {
match = false;
break;
}
}
if (match) {
return true;
}
}
return false;
// Name matches - this is from self
// Drop only if pathBytes is empty (direct broadcast)
// Keep if pathBytes has data (repeated through another node)
return pathBytes.isEmpty;
}
Uint8List _selectPreferredPathBytes(Uint8List existing, Uint8List incoming) {
+6
View File
@@ -128,6 +128,9 @@ class MeshCoreApp extends StatelessWidget {
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
@@ -135,6 +138,9 @@ class MeshCoreApp extends StatelessWidget {
brightness: Brightness.dark,
),
useMaterial3: true,
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
),
themeMode: _themeModeFromSetting(settingsService.settings.themeMode),
home: const ScannerScreen(),
+1 -1
View File
@@ -164,7 +164,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
),
body: RefreshIndicator(
onRefresh: () async {
await context.read<MeshCoreConnector>().getChannels();
await context.read<MeshCoreConnector>().getChannels(force: true);
},
child: () {
if (connector.isLoadingChannels) {
+9 -30
View File
@@ -225,13 +225,15 @@ class _MapScreenState extends State<MapScreen> {
}
// Re center map after removed markers have loaded
if (!_hasInitializedMap && _removedMarkersLoaded && hasMapContent) {
if (!_hasInitializedMap && _removedMarkersLoaded) {
_hasInitializedMap = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_mapController.move(center, initialZoom);
}
});
if (hasMapContent) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_mapController.move(center, initialZoom);
}
});
}
}
final allowBack = !connector.isConnected;
@@ -275,9 +277,7 @@ class _MapScreenState extends State<MapScreen> {
),
],
),
body: !hasMapContent
? _buildEmptyState()
: Stack(
body: Stack(
children: [
FlutterMap(
mapController: _mapController,
@@ -376,27 +376,6 @@ class _MapScreenState extends State<MapScreen> {
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_off, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
context.l10n.map_noNodesWithLocation,
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
context.l10n.map_nodesNeedGps,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],
),
);
}
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
final markers = <Marker>[];
+10 -3
View File
@@ -780,10 +780,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
return;
}
if (txPower == null || txPower < 0 || txPower > 22) {
final maxTxPower = widget.connector.maxTxPower ?? 22;
if (txPower == null || txPower < 0 || txPower > maxTxPower) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid)));
).showSnackBar(
SnackBar(
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
),
);
return;
}
@@ -932,7 +937,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
decoration: InputDecoration(
labelText: l10n.settings_txPower,
border: const OutlineInputBorder(),
helperText: l10n.settings_txPowerHelper,
helperText: widget.connector.maxTxPower != null
? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)'
: l10n.settings_txPowerHelper,
),
keyboardType: TextInputType.number,
),