Skip to content

feat(notifications): Live Update for mesh status or a tracked node - #7664

Open
jamesarich wants to merge 2 commits into
mainfrom
feat/live-update
Open

jamesarich wants to merge 2 commits into
mainfrom
feat/live-update

Conversation

@jamesarich

@jamesarich jamesarich commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

iOS now shows a Live Activity the user picks (Meshtastic-Apple#2670); Android had only the fixed service notification. This is the Android 16 counterpart: a Live Update the user starts, switches or stops, showing either the mesh at a glance or one node. It's off by default, and Off leaves the service notification as it was.

🌟 New Features

  • Live Update on the service notification, promoted only while a mode is on:
    • Mesh status: connection, nodes online, unread messages, radio battery; chip online/total while connected.
    • Track a node: battery, signal, last heard, hops, distance; chip is battery, or time since heard once that's a minute or more. Tap opens the node.
  • Connections: a Live Update row and chooser (Mesh status, or Track a node from a searchable list, favorites first then most recently heard) with Start, Switch and Stop, plus a settings link when Live Updates are turned off for the app.
  • Node detail: a Track in Live Update switch.
  • A user disconnect, a radio switch, or deleting or ignoring the tracked node ends it. A dropped link keeps it and shows the state. Firmware progress still takes over the notification.
  • Android 16+ only; desktop and iOS don't show it.

🛠️ Refactoring & Architecture

  • Mode lives in DataStore through LiveUpdatePrefs (commonMain), LiveUpdateGuard in core:service commonMain, and a rememberLiveUpdateSupport() expect in core:ui.
  • The stats branch of renderServiceNotification moved unchanged into renderServiceStateNotification.
  • feature/node/detekt-baseline.xml: the ID of the existing ViewModelForwarding entry changed with the call's arguments.

📝 Documentation

  • Connections: a Live Update section. Notifications: a pointer to it.

Testing Performed

  • MeshNotificationManagerImplTest: not promoted while Off; Mesh status promoted with chip and text, and reverts on Off; firmware progress outranks it; a tracked node renders, then turns Off when deleted.
  • LiveUpdateGuardTest, LiveUpdatePrefsTest, LiveUpdateCandidatesTest.
  • Full baseline with kmpSmokeCompile and -PwarningsAsErrors=true green locally.
  • Emulator against a simulated TCP radio:
    • API 37 (CE2A.260420): Mesh status and Track a node both post with PROMOTED_ONGOING; the status bar chip shows 41/41. Stopping from node detail restores the stock notification. Dropping the link keeps the Live Update, with "Disconnected" as subtext.
    • API 36.0 (BE2A.250530): the request is set but never granted; that image predates the promoted chip UI.
    • The notification tap targets nodes/<num> for a tracked node.
  • Not seen: the lock screen (the emulator has no keyguard), battery and signal lines (the sim sends neither). After a dropped link, "Heard just now" doesn't age until the next packet, since nothing re-renders on a timer.
Before (Off) Mesh status Status bar chip Track a node
before mesh-shade mesh-chip track-shade
Chooser Track a node Node detail
chooser track-picker node-detail

Constitution Check

  • I: prefs, guard, view models and UI in commonMain; Android APIs only in androidMain.
  • II: spotless and detekt (incl. type-resolved) clean.
  • III: shared Compose, stringResource, MeshtasticIcons, MetricFormatter; platform gate through an expect.
  • IV: no new logging of node data or location.
  • V: cross-platform feature that Apple already shipped. meshtastic/design has no features/ directory and no Live Activity spec, so there is none to reference.
  • VI: connections.md and notifications.md updated, last_updated bumped.
  • VII: baseline green before push.

Summary by CodeRabbit

  • New Features

    • Added Live Updates for Android 16+, showing mesh status or tracked-node information in the status bar.
    • Added Connections controls to start, stop, switch, and search for tracked nodes.
    • Added node-detail controls for enabling or disabling tracking.
    • Live Updates display battery, signal, timing, hop, distance, and unread-status details.
    • Tracking stops when a node is removed, ignored, disconnected, or the radio changes.
  • Documentation

    • Added guidance for configuring Live Updates and notification permissions.

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: meshtastic/Meshtastic-Android/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 51ca9e5d-d6bc-4786-91b3-c12a7bba878c
📥 Commits

Reviewing files that changed from the base of the PR and between 0e458fa and 5aa96d9.

📒 Files selected for processing: 8
  • .skills/compose-ui/strings-index.txt
  • core/resources/src/commonMain/composeResources/values/strings.xml
  • docs/en/user/connections.md
  • docs/en/user/notifications.md
  • feature/node/detekt-baseline.xml
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt
🚧 Files skipped from review as they are similar to previous changes: 4
  • docs/en/user/notifications.md
  • .skills/compose-ui/strings-index.txt
  • core/resources/src/commonMain/composeResources/values/strings.xml
  • feature/node/detekt-baseline.xml

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

📝 Walkthrough

This change adds persisted Live Update modes for mesh status and individual node tracking. It adds controls in the Connections screen and node details, and displays active Live Update readings in the Android service notification.

Changes: Live Updates

Layer / File(s) Summary
Mode persistence and platform support
core/repository/.../LiveUpdatePrefs.kt, core/prefs/.../LiveUpdatePrefsImpl.kt, core/prefs/.../LiveUpdatePrefsTest.kt, core/testing/.../FakeLiveUpdatePrefs.kt, core/ui/.../LiveUpdateSupport*, core/resources/.../strings.xml, .skills/compose-ui/strings-index.txt
Defines Off, MeshStatus, and TrackNode modes, encodes them for persistence, and defaults invalid or missing values to Off. Adds platform support detection and localized Live Update strings.
Connection screen selection flow
feature/connections/.../LiveUpdateViewModel.kt, feature/connections/.../ConnectionsScreen.kt, feature/connections/.../LiveUpdateCard.kt, feature/connections/src/commonTest/.../LiveUpdateCandidatesTest.kt, docs/en/user/connections.md
Adds a Connections screen card and chooser for mesh status or a searchable tracked node. Candidates exclude the local and ignored nodes, with favorites and recent activity affecting sort order.
Service notification and lifecycle
core/service/.../MeshNotificationManagerImpl.kt, core/service/.../LiveUpdateGuard.kt, core/service/src/commonTest/.../LiveUpdateGuardTest.kt, core/service/src/androidHostTest/.../MeshNotificationManagerImpl*Test.kt, docs/en/user/notifications.md
Combines mesh or tracked-node data into notification readings and refreshes active readings at most every two seconds. The guard disables updates when a tracked node is ignored or disappears, or when the selected radio changes outside firmware operations. Adds notification and guard tests.
Node-detail tracking control
feature/node/.../NodeDetailAction.kt, feature/node/.../NodeLiveUpdateViewModel.kt, feature/node/.../NodeDetailScreens.kt, feature/node/.../NodeDetailContent.kt, feature/node/.../DeviceActions.kt, feature/node/.../NodesNavigation.kt, feature/node/detekt-baseline.xml
Adds a node-detail tracking switch for eligible non-local nodes. The action updates the Live Update mode through a view model provided at the navigation entry point.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 5aa96

Tracking can show stale “last heard” information after a link drop. This is a bounded issue to address or accept before merging.

Pre-merge checks: Passed 6 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Tests Prove The Path, Not The End State Warning Two added tests still do not prove the production path. LiveUpdateGuardTest.the firmware flow deselecting the radio keeps the live update seeds FakeLiveUpdatePrefs with MeshStatus, runs the guar… Update the firmware guard test to retain the assertion that firmware deselection and reselection preserve MeshStatus, then release the firmware lock, perform a real user radio switch, and assert that the mode becomes Off. Update the pre…
Regression Coverage For Changed Behavior Warning Regression coverage remains incomplete. The changed production paths have only partial happy-path coverage: - createTrackedNodeLiveUpdate adds the <60s chip suppression in lines 956-985. `MeshNoti… Add the missing common, Android host, and Compose tests. In particular, add the pre-fix-failing sub-minute chip boundary test; notification formatting, disconnected-state, locale, and deep-link assertions; virtual-time throttle tests for me…
Passed Passed checks: 6 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: adding Live Update support for mesh status and tracked nodes.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Sibling Call Sites And Presence Semantics Passed PASS. The PR does not change a physical metric field or its sentinel representation. It adds a new tracked-node notification call site that uses the existing nullable Node.snrOrNull and `Node.rssiOr…
Moved Code Diffed Against Its Original Passed PASS: The PR does not move a type or function to another file or module. The only extraction is in MeshNotificationManagerImpl: the old state-rendering block from base lines 430-443 is now `renderSe…
Full details: Sibling Call Sites And Presence Semantics

Explanation

PASS. The PR does not change a physical metric field or its sentinel representation. It adds a new tracked-node notification call site that uses the existing nullable Node.snrOrNull and Node.rssiOrNull accessors and checks presence before rendering. NodeItem.kt and NodeItemCompact.kt are unchanged, and no new RSSI, temperature, current, voltage, or particulate field defaults to 0.

Full details: Moved Code Diffed Against Its Original

Explanation

PASS: The PR does not move a type or function to another file or module. The only extraction is in MeshNotificationManagerImpl: the old state-rendering block from base lines 430-443 is now renderServiceStateNotification at head lines 535-553. The block retains the same stats selection, fallback, notification construction, return type, and exception behavior; only title becomes a helper parameter. renderServiceNotification remains the same declaration, and its existing caller remains unchanged. The other deletions only update a test placeholder, a baseline ID, and the ManagementActions signature/call. No moved-code override, validation, default, nullability, annotation, scope, dispatcher, or sharing-policy change is present.

Full details: Tests Prove The Path, Not The End State

Explanation

Two added tests still do not prove the production path. LiveUpdateGuardTest.the firmware flow deselecting the radio keeps the live update seeds FakeLiveUpdatePrefs with MeshStatus, runs the guard, and only checks that the value remains unchanged at lines 100-110. Removing guard().run() makes it pass. LiveUpdatePrefsTest.each mode round trips checks the same StateFlow immediately after setMode at lines 67-76. A setter that updates only memory and does not persist to DataStore would pass. The candidate tests assert exact surviving node IDs, and no added test asserts emission order under UnconfinedTestDispatcher.

Resolution

Update the firmware guard test to retain the assertion that firmware deselection and reselection preserve MeshStatus, then release the firmware lock, perform a real user radio switch, and assert that the mode becomes Off. Update the preference test to await the DataStore write, construct a new LiveUpdatePrefsImpl over the same DataStore file, and assert that the new instance decodes the stored mode. Cover all supported modes or inspect the persisted DataStore entry directly.

Full details: Regression Coverage For Changed Behavior

Explanation

Regression coverage remains incomplete. The changed production paths have only partial happy-path coverage: - createTrackedNodeLiveUpdate adds the &lt;60s chip suppression in lines 956-985. MeshNotificationManagerImplTest covers only a named node with a battery and deletion. It lacks a pre-fix-failing boundary test for no battery with lastHeard &lt; 60s and lastHeard &gt;= 60s. It also lacks assertions for signal, heard text, direct/hops, distance and locale units, powered battery, title fallback, disconnected mesh-status text, and both node and Connections deep-link intents. - liveUpdateReadings applies throttleLatest to mesh and tracked-node flows at lines 386-410, and throttleLatest emits immediately then coalesces for two seconds at lines 1512-1516. No test advances virtual time or verifies immediate delivery, latest-value delivery, and suppression of intermediate posts for either mode. - LiveUpdateGuard.watchSelection uses valid-address-to-null and valid-address-to-new-radio transitions, and checks both firmware operations in lines 60-69 of LiveUpdateGuard.kt. The tests cover only a helper for switching and an integration case for FirmwareUpdate; they do not cover valid-to-null, valid-to-new-radio through run(), FirmwareMaintenance, or the dropped-link preservation case across those transitions. - LiveUpdatePrefsImpl persists off, mesh, and node:&lt;num&gt; through DataStore at lines 36-46. LiveUpdatePrefsTest round-trips one instance and tests decoder inputs, but does not re-instantiate the implementation against the same store or assert the raw persisted encodings. - rememberLiveUpdateSupport gates API 36+, refreshes promotion permission on resume, and falls back between promotion and app notification settings at lines 33-62 of LiveUpdateSupport.android.kt. The diff adds no tests for API 35/36, resume refresh, or either settings fallback branch. - LiveUpdateSection and LiveUpdateChooserSheet add connected/disconnected visibility, Off/active state, support gating, Start/Switch/Stop, search, selection, and settings behavior at lines 80-265 of LiveUpdateCard.kt. No Compose interaction tests cover these paths. NodeDetailScreen and ManagementActions add the tracking switch wiring, but no UI test covers it. - LiveUpdateViewModel and NodeLiveUpdateViewModel add state-flow and mode-transition behavior at the cited new files. No tests cover candidate/tracked-node updates, tracking, stopping only the matching node, or switching to another node. The existing candidate-ordering, preference-decoder, guard-helper, basic notification, and basic node deletion tests do not close these gaps.

Resolution

Add the missing common, Android host, and Compose tests. In particular, add the pre-fix-failing sub-minute chip boundary test; notification formatting, disconnected-state, locale, and deep-link assertions; virtual-time throttle tests for mesh and tracked-node readings; full LiveUpdateGuard.run() integration coverage for null, radio-switch, FirmwareUpdate, and FirmwareMaintenance transitions; DataStore re-instantiation and raw encoding assertions; Robolectric tests for API gates, resume refresh, and settings fallback; Compose tests for Connections and node-detail interactions; and ViewModel tests for state updates and mode transitions.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the enhancement New feature or request label Oct 18, 2026
@jamesarich
jamesarich marked this pull request as ready for review October 20, 2026 15:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: meshtastic/Meshtastic-Android/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5d06896e-d459-4597-a695-15b43c546f84
📥 Commits

Reviewing files that changed from the base of the PR and between 2a41b35 and 0e458fa.

📒 Files selected for processing: 29
  • .skills/compose-ui/strings-index.txt
  • core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/notification/LiveUpdatePrefsImpl.kt
  • core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/notification/LiveUpdatePrefsTest.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LiveUpdatePrefs.kt
  • core/resources/src/commonMain/composeResources/values/strings.xml
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplRoutingTest.kt
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt
  • core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt
  • core/service/src/commonMain/kotlin/org/meshtastic/core/service/LiveUpdateGuard.kt
  • core/service/src/commonTest/kotlin/org/meshtastic/core/service/LiveUpdateGuardTest.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLiveUpdatePrefs.kt
  • core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/LiveUpdateSupport.android.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LiveUpdateSupport.kt
  • core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/util/LiveUpdateSupport.ios.kt
  • core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/LiveUpdateSupport.jvm.kt
  • docs/en/user/connections.md
  • docs/en/user/notifications.md
  • feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/LiveUpdateViewModel.kt
  • feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt
  • feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/LiveUpdateCard.kt
  • feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/LiveUpdateCandidatesTest.kt
  • feature/node/detekt-baseline.xml
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeLiveUpdateViewModel.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Comment on lines +402 to +411
is LiveUpdateMode.TrackNode -> {
val nodes = nodeRepository.value
combine(nodes.nodeDBbyNum.map { it[mode.nodeNum] }.distinctUntilChanged(), nodes.ourNodeInfo) {
node,
ourNode,
->
node?.let { LiveUpdateReading.TrackNode(it, ourNode) }
}
.throttleLatest(LIVE_UPDATE_MIN_INTERVAL)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The tracked-node "last heard" text and chip stop aging after the link drops.

createTrackedNodeLiveUpdate computes heardSecondsAgo and the heardText string once per render. A render happens only when the TrackNode flow emits a new reading. That flow emits only when the tracked node's nodeDBbyNum row or ourNodeInfo changes. After a dropped link, neither flow changes. The PR keeps the Live Update running in that state.

  • Trigger: Track a node that was heard seconds ago. Then lose the link, for example by walking out of BLE range.
  • Consequence: The promoted notification keeps "Heard just now" indefinitely. If the node reports no battery, the status-bar chip also keeps its first elapsed time, such as "5m". The text tells the user that a node is fresh when it may be hours old. This contradicts the PR's stated behavior for a dropped link: keep the update and show the connection state.
  • No other refresh source: setShowWhen(false) disables the platform timestamp, so the system does not refresh the time either. The PR description lists timer-based aging after a dropped link as not observed.

Add a periodic tick to the TrackNode reading so that the reading re-emits at least once per minute. An alternative is to carry a render timestamp in the reading.

🐛 Proposed fix: re-render the tracked node once per minute
--- "a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt"
+++ "b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt"
@@ -399,16 +399,21 @@
                 .throttleLatest(LIVE_UPDATE_MIN_INTERVAL)
         }
 
         is LiveUpdateMode.TrackNode -> {
             val nodes = nodeRepository.value
-            combine(nodes.nodeDBbyNum.map { it[mode.nodeNum] }.distinctUntilChanged(), nodes.ourNodeInfo) {
+            combine(
+                nodes.nodeDBbyNum.map { it[mode.nodeNum] }.distinctUntilChanged(),
+                nodes.ourNodeInfo,
+                minuteTicks(),
+            ) {
                     node,
                     ourNode,
+                    tick,
                 ->
-                node?.let { LiveUpdateReading.TrackNode(it, ourNode) }
+                node?.let { LiveUpdateReading.TrackNode(it, ourNode, tick) }
             }
                 .throttleLatest(LIVE_UPDATE_MIN_INTERVAL)
         }
     }
 
     /**
// LiveUpdateReading.TrackNode gains `val tick: Long` so distinctUntilChanged() lets each tick through.
private fun minuteTicks(): Flow<Long> = flow {
    var n = 0L
    while (true) {
        emit(n++)
        delay(1.minutes)
    }
}

A user-started Android 16 Live Update on the service notification, the
counterpart of the Apple Live Activity. Off by default; Off leaves the
service notification as it was.

- Mesh status: connection, nodes online, unread messages and the radio's
  battery, with nodes online in the status bar chip.
- Track a node: its battery, signal, last heard, hops and distance, with
  its battery (or time since heard) in the chip.
- The mode is stored in DataStore (`LiveUpdatePrefs`). A user disconnect,
  a radio switch, or the tracked node being deleted or ignored turns it
  off; a dropped link keeps it and shows the connection state.
- Connections gets a Live Update row and chooser (favorites first, then
  most recently heard, searchable); node detail gets a Track in Live
  Update switch. Both are Android 16+ only.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant