From 08f3858dd856bc11e62bd3fda792f524a0836d65 Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 10 Aug 2026 04:21:35 -0400 Subject: [PATCH] feat(#550): adaptive CoreScope poll + tap-to-refresh badge Poll now runs quick-then-slow (10/20/30/60x4s, ~5min cap) and stops early once the count is flat for 2 checks, so it climbs to the near-final observer count instead of stopping at a fixed cutoff. The badge is tappable to re-query on demand (counts only ever grow), covering later re-checks. Part of #524. Agent: QuietSnow (session 31eaba02) --- lib/connector/meshcore_connector.dart | 48 +++++++++++++++++++-------- lib/screens/channel_chat_screen.dart | 37 ++++++++++++++------- 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b07c6aa..2ef42fb 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -4754,16 +4754,15 @@ class MeshCoreConnector extends ChangeNotifier { /// After sending a channel message, fetch its firmware hash then CoreScope's /// observer count and stamp both on the message. Best-effort and gated; any /// failure leaves the message showing radio-only. - /// Backoff schedule for polling CoreScope after a send. Observer counts - /// accrue over time: the packet must propagate the mesh and be reported by - /// observers before CoreScope has any record, then the count keeps climbing - /// for a few seconds. Cumulative wall-clock: ~10s, 30s, 60s, 120s. - static const List _coreScopePollSchedule = [ - Duration(seconds: 10), - Duration(seconds: 20), - Duration(seconds: 30), - Duration(seconds: 60), - ]; + /// Poll intervals (seconds) after a send: quick at first, then settle. The + /// list also caps the total window (sum ~5 min). Observer counts accrue over + /// time (the packet must propagate and be reported), so a single instant + /// query misses it. The loop also stops early once the count stabilises, no + /// increase across [_coreScopeStableChecks] consecutive polls. There is no + /// true "final" count (it only ever grows), so this is a pragmatic stop, the + /// tappable badge covers re-checking later. + static const List _coreScopePollSecs = [10, 20, 30, 60, 60, 60, 60]; + static const int _coreScopeStableChecks = 2; Future _fetchAndStoreCoreScopeCount( int channelIndex, @@ -4785,14 +4784,16 @@ class MeshCoreConnector extends ChangeNotifier { messageId, (m) => m.copyWith(onAirHash: pkt.hashHex), ); - // Poll with backoff, keeping the highest count as observers report in. var best = 0; - for (final delay in _coreScopePollSchedule) { - await Future.delayed(delay); + var flat = 0; + for (final secs in _coreScopePollSecs) { + await Future.delayed(Duration(seconds: secs)); if (!isConnected) return; final count = await _coreScopeService.fetchObserverCount(pkt.hashHex); - if (count != null && count > best) { + if (count == null) continue; + if (count > best) { best = count; + flat = 0; _updateChannelMessageById( channelIndex, messageId, @@ -4800,6 +4801,8 @@ class MeshCoreConnector extends ChangeNotifier { ); appLogger.info('observer count=$count', tag: 'CoreScope'); notifyListeners(); + } else if (++flat >= _coreScopeStableChecks) { + return; // stabilised } } } catch (e) { @@ -4810,6 +4813,23 @@ class MeshCoreConnector extends ChangeNotifier { } } + /// Tap-to-refresh: re-query CoreScope for a message's stored on-air hash and + /// bump the observer count if it grew (counts only ever climb). No-op without + /// a stored hash. + Future refreshCoreScopeObserverCount( + int channelIndex, + String messageId, + String hashHex, + ) async { + final count = await _coreScopeService.fetchObserverCount(hashHex); + if (count == null) return; + _updateChannelMessageById(channelIndex, messageId, (m) { + final current = m.coreScopeObserverCount ?? 0; + return count > current ? m.copyWith(coreScopeObserverCount: count) : m; + }); + notifyListeners(); + } + void _updateChannelMessageById( int channelIndex, String messageId, diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 73b9545..cd17a4c 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1518,20 +1518,33 @@ class _ChannelChatScreenState extends State { ), ], // CoreScope observer count (#524), shown parallel to the radio-heard - // count with a distinct icon, never merged into one number. + // count with a distinct icon, never merged into one number. Tap to + // re-query on demand (the count only ever climbs). if (isOutgoing && (message.coreScopeObserverCount ?? 0) > 0) ...[ dot, - Tooltip( - message: context.l10n.channel_coreScopeTooltip( - message.coreScopeObserverCount!, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.cloud_outlined, size: 12, color: metaColor), - const SizedBox(width: 4), - Text('${message.coreScopeObserverCount}', style: metaStyle), - ], + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: message.onAirHash == null + ? null + : () => context + .read() + .refreshCoreScopeObserverCount( + message.channelIndex ?? _currentChannel.index, + message.messageId, + message.onAirHash!, + ), + child: Tooltip( + message: context.l10n.channel_coreScopeTooltip( + message.coreScopeObserverCount!, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.cloud_outlined, size: 12, color: metaColor), + const SizedBox(width: 4), + Text('${message.coreScopeObserverCount}', style: metaStyle), + ], + ), ), ), ],