mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-18 00:16:26 +10:00
issue #112 fixes and more
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>[];
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user