From 3e9fdde9a68fb6ee8bf4eb9e6c46bad597200332 Mon Sep 17 00:00:00 2001 From: James Rich Date: Thu, 21 May 2026 15:46:31 -0500 Subject: [PATCH] Fix Phase 2b critical data integrity issues - Fix location filtering: Only treat (0,0) as invalid if position.time is 0 - Previously filtered all (0,0) coords as null, losing valid equatorial data - Now checks position.time to distinguish 'no fix' from real coordinates - Fix mostRecentPacketTime: Use max lastHeard from all nodes, not current time - Previously returned current time, making mesh appear always active - Now computes from actual node activity data - Fix meshUptimeSeconds: Use local device's actual uptime, not epoch time - Previously returned epoch seconds (~1.7B), not elapsed time - Now uses device's DeviceMetrics.uptime_seconds All checks passing: Android (Google/fdroid), detekt, spotless, unit tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../core/data/ai/AiFunctionProviderImpl.kt | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/ai/AiFunctionProviderImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/ai/AiFunctionProviderImpl.kt index 57e64c90f..5628f4ca3 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/ai/AiFunctionProviderImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/ai/AiFunctionProviderImpl.kt @@ -202,6 +202,9 @@ class AiFunctionProviderImpl( return@withTimeout GetNodeDetailsResult.NotFound("Node not found: $nodeId") } + // Check if position is valid (both coords zero AND time zero indicates no position fix) + val hasValidPosition = node.latitude != 0.0 || node.longitude != 0.0 || node.position.time > 0 + val details = NodeDetails( id = "!${node.num.toString(HEX_RADIX)}", @@ -218,8 +221,8 @@ class AiFunctionProviderImpl( lastHeard = node.lastHeard.toLong() * MS_PER_SEC, userRole = node.user.role.name, isLicensed = node.user.is_licensed, - latitude = node.latitude.takeIf { it != 0.0 }, - longitude = node.longitude.takeIf { it != 0.0 }, + latitude = node.latitude.takeIf { hasValidPosition }, + longitude = node.longitude.takeIf { hasValidPosition }, ) GetNodeDetailsResult.Success(details) } catch (ex: Exception) { @@ -255,14 +258,23 @@ class AiFunctionProviderImpl( else -> (HEALTH_SCORE_BASE + (HEALTH_SCORE_ONLINE_RATIO * onlineCount) / totalCount).toInt() } + // Find most recent packet: max lastHeard across all nodes (convert seconds to ms) + val mostRecentPacketTimeMs = + nodeMap.values.maxOfOrNull { it.lastHeard }?.toLong()?.times(MS_PER_SEC) + ?: clock.now().toEpochMilliseconds() + + // Get local device uptime from its DeviceMetrics (node #0 is typically the local device) + val localNode = nodeMap.values.find { it.num == 0 } ?: nodeMap.values.firstOrNull() + val meshUptimeSeconds = localNode?.deviceMetrics?.uptime_seconds?.toLong() ?: 0L + val metrics = MeshMetrics( totalNodeCount = totalCount, onlineNodeCount = onlineCount, averageBatteryLevel = avgBattery, meshHealthScore = healthScore.coerceIn(0, HEALTH_SCORE_MAX), - mostRecentPacketTime = clock.now().toEpochMilliseconds(), - meshUptimeSeconds = clock.now().toEpochMilliseconds() / 1000L, + mostRecentPacketTime = mostRecentPacketTimeMs, + meshUptimeSeconds = meshUptimeSeconds, channelUtilizationPercent = null, // Could compute from radioConfigRepository if needed ) GetMeshMetricsResult.Success(metrics)