From 085ccf566f5e9065b6e9cb0b9200a1075819d29b Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Mon, 21 Jul 2025 23:15:21 +1000 Subject: [PATCH] add decoded payload to debug panel (#2472) --- .../geeksville/mesh/model/DebugViewModel.kt | 92 +++++++++++++++++-- .../com/geeksville/mesh/ui/debug/Debug.kt | 47 ++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 130 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt index f4f38c5a3..2ac6c923d 100644 --- a/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt @@ -42,6 +42,12 @@ import com.geeksville.mesh.Portnums.PortNum import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import com.geeksville.mesh.repository.datastore.RadioConfigRepository +import com.google.protobuf.InvalidProtocolBufferException +import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.TelemetryProtos +import com.geeksville.mesh.AdminProtos +import com.geeksville.mesh.PaxcountProtos +import com.geeksville.mesh.StoreAndForwardProtos data class SearchMatch( val logIndex: Int, @@ -156,6 +162,9 @@ class LogFilterManager { } } +private const val HEX_FORMAT = "%02x" + +@Suppress("TooManyFunctions") @HiltViewModel class DebugViewModel @Inject constructor( private val meshLogRepository: MeshLogRepository, @@ -206,6 +215,7 @@ class DebugViewModel @Inject constructor( messageType = log.message_type, formattedReceivedDate = TIME_FORMAT.format(log.received_date), logMessage = annotateMeshLogMessage(log), + decodedPayload = decodePayloadFromMeshLog(log), ) }.toImmutableList() @@ -213,22 +223,33 @@ class DebugViewModel @Inject constructor( * Transform the input [MeshLog] by enhancing the raw message with annotations. */ private fun annotateMeshLogMessage(meshLog: MeshLog): String { - val annotated = when (meshLog.message_type) { + return when (meshLog.message_type) { "Packet" -> meshLog.meshPacket?.let { packet -> - annotateRawMessage(meshLog.raw_message, packet.from, packet.to) - } - + annotatePacketLog(packet) + } ?: meshLog.raw_message "NodeInfo" -> meshLog.nodeInfo?.let { nodeInfo -> annotateRawMessage(meshLog.raw_message, nodeInfo.num) - } - + } ?: meshLog.raw_message "MyNodeInfo" -> meshLog.myNodeInfo?.let { nodeInfo -> annotateRawMessage(meshLog.raw_message, nodeInfo.myNodeNum) - } - - else -> null + } ?: meshLog.raw_message + else -> meshLog.raw_message } - return annotated ?: meshLog.raw_message + } + + private fun annotatePacketLog(packet: MeshProtos.MeshPacket): String { + val builder = packet.toBuilder() + val hasDecoded = builder.hasDecoded() + val decoded = if (hasDecoded) builder.decoded else null + if (hasDecoded) builder.clearDecoded() + val baseText = builder.build().toString().trimEnd() + val result = if (hasDecoded && decoded != null) { + val decodedText = decoded.toString().trimEnd().prependIndent(" ") + "$baseText\ndecoded {\n$decodedText\n}" + } else { + baseText + } + return annotateRawMessage(result, packet.from, packet.to) } /** @@ -274,6 +295,7 @@ class DebugViewModel @Inject constructor( val messageType: String, val formattedReceivedDate: String, val logMessage: String, + val decodedPayload: String? = null, ) companion object { @@ -295,4 +317,54 @@ class DebugViewModel @Inject constructor( } fun setSelectedLogId(id: String?) { _selectedLogId.value = id } + + /** + * Attempts to fully decode the payload of a MeshLog's MeshPacket using the appropriate protobuf definition, + * based on the portnum of the packet. + * + * For known portnums, the payload is parsed into its corresponding proto message and returned as a string. + * For text and alert messages, the payload is interpreted as UTF-8 text. + * For unknown portnums, the payload is shown as a hex string. + * + * @param log The MeshLog containing the packet and payload to decode. + * @return A human-readable string representation of the decoded payload, or an error message if decoding fails, + * or null if the log does not contain a decodable packet. + */ + private fun decodePayloadFromMeshLog(log: MeshLog): String? { + var result: String? = null + val packet = log.meshPacket + if (packet == null || !packet.hasDecoded()) { + result = null + } else { + val portnum = packet.decoded.portnumValue + val payload = packet.decoded.payload.toByteArray() + result = try { + when (portnum) { + PortNum.TEXT_MESSAGE_APP_VALUE, + PortNum.ALERT_APP_VALUE -> + payload.toString(Charsets.UTF_8) + PortNum.POSITION_APP_VALUE -> + MeshProtos.Position.parseFrom(payload).toString() + PortNum.WAYPOINT_APP_VALUE -> + MeshProtos.Waypoint.parseFrom(payload).toString() + PortNum.NODEINFO_APP_VALUE -> + MeshProtos.User.parseFrom(payload).toString() + PortNum.TELEMETRY_APP_VALUE -> + TelemetryProtos.Telemetry.parseFrom(payload).toString() + PortNum.ROUTING_APP_VALUE -> + MeshProtos.Routing.parseFrom(payload).toString() + PortNum.ADMIN_APP_VALUE -> + AdminProtos.AdminMessage.parseFrom(payload).toString() + PortNum.PAXCOUNTER_APP_VALUE -> + PaxcountProtos.Paxcount.parseFrom(payload).toString() + PortNum.STORE_FORWARD_APP_VALUE -> + StoreAndForwardProtos.StoreAndForward.parseFrom(payload).toString() + else -> payload.joinToString(" ") { HEX_FORMAT.format(it) } + } + } catch (e: InvalidProtocolBufferException) { + "Failed to decode payload: ${e.message}" + } + } + return result + } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt index fda133ed8..fcefc1688 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt @@ -237,6 +237,14 @@ internal fun DebugItem( color = colorScheme.onSurface ) ) + // Show decoded payload if available + if (!log.decodedPayload.isNullOrBlank()) { + DecodedPayloadBlock( + decodedPayload = log.decodedPayload, + isSelected = isSelected, + colorScheme = colorScheme + ) + } } } } @@ -780,3 +788,42 @@ private suspend fun exportAllLogs(context: Context, logs: List) = wit warn("Error:IOException: " + e.toString()) } } + +@Composable +private fun DecodedPayloadBlock( + decodedPayload: String, + isSelected: Boolean, + colorScheme: ColorScheme +) { + val commonTextStyle = TextStyle( + fontSize = if (isSelected) 10.sp else 8.sp, + fontWeight = FontWeight.Bold, + color = colorScheme.primary + ) + + Text( + text = stringResource(id = R.string.debug_decoded_payload), + style = commonTextStyle, + modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) + ) + Text( + text = "{", + style = commonTextStyle, + modifier = Modifier.padding(start = 8.dp, bottom = 2.dp) + ) + Text( + text = decodedPayload, + softWrap = true, + style = TextStyle( + fontSize = if (isSelected) 10.sp else 8.sp, + fontFamily = FontFamily.Monospace, + color = colorScheme.onSurface.copy(alpha = 0.8f) + ), + modifier = Modifier.padding(start = 16.dp, bottom = 0.dp) + ) + Text( + text = "}", + style = commonTextStyle, + modifier = Modifier.padding(start = 8.dp, bottom = 4.dp) + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6b36e3489..5fbee7f7c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -165,6 +165,7 @@ Text messages This Channel URL is invalid and can not be used Debug Panel + Decoded Payload: Export Logs 500 last messages Filters