From 6590ea0ef08deb1479661f97b95e4005954fd8b6 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Thu, 13 Nov 2025 13:53:37 -0600
Subject: [PATCH] feat(ble): Add support for LogRadio characteristic, enhance
logs (#3691)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
---
app/detekt-baseline.xml | 19 +--
.../mesh/repository/radio/BleError.kt | 138 +++++++++++++++++
.../repository/radio/NordicBleInterface.kt | 143 ++++++++++--------
.../repository/radio/RadioInterfaceService.kt | 18 +--
.../geeksville/mesh/service/MeshService.kt | 126 ++++++++-------
.../mesh/service/MeshServiceBroadcasts.kt | 3 +-
.../geeksville/mesh/service/PacketHandler.kt | 4 +-
core/model/detekt-baseline.xml | 1 +
.../meshtastic/core/model/util/Extensions.kt | 7 +
.../feature/settings/debugging/Debug.kt | 1 -
.../settings/debugging/DebugViewModel.kt | 1 +
11 files changed, 318 insertions(+), 143 deletions(-)
create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/BleError.kt
diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 4adea4197..6bc93d176 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -10,6 +10,7 @@
ComposableParamOrder:ConnectionsNavIcon.kt$ConnectionsNavIcon
ComposableParamOrder:EmptyStateContent.kt$EmptyStateContent
ComposableParamOrder:Share.kt$ShareScreen
+ CyclomaticComplexMethod:BleError.kt$BleError.Companion$fun from(exception: Throwable): BleError
CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)
CyclomaticComplexMethod:SettingsNavigation.kt$@Suppress("LongMethod") fun NavGraphBuilder.settingsGraph(navController: NavHostController)
EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }
@@ -17,7 +18,6 @@
EmptyFunctionBlock:NsdManager.kt$<no name provided>${ }
EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}
FinalNewline:BLEException.kt$com.geeksville.mesh.service.BLEException.kt
- FinalNewline:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt
FinalNewline:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt
FinalNewline:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt
FinalNewline:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt
@@ -33,9 +33,7 @@
FinalNewline:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt
FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt
FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt
- ImplicitDefaultLocale:MeshService.kt$MeshService$String.format("0x%02x", b)
- ImplicitDefaultLocale:NordicBleInterface.kt$NordicBleInterface$String.format("0x%02x", b)
- ImplicitDefaultLocale:RadioInterfaceService.kt$RadioInterfaceService$String.format("0x%02x", b)
+ ImplicitDefaultLocale:MeshService.kt$MeshService$String.format("0x%02x", byte)
LambdaParameterEventTrailing:Channel.kt$onConfirm
LambdaParameterInRestartableEffect:Channel.kt$onConfirm
LargeClass:MeshService.kt$MeshService : Service
@@ -69,14 +67,13 @@
MagicNumber:UIState.kt$4
MatchingDeclarationName:MeshServiceStarter.kt$ServiceStarter : Worker
MaxLineLength:MeshService.kt$MeshService$"Config complete id mismatch: received=$configCompleteId expected one of [$configOnlyNonce,$nodeInfoNonce]"
- MaxLineLength:MeshService.kt$MeshService$"handleConfigComplete called with id=$configCompleteId, configOnly=$configOnlyNonce, nodeInfo=$nodeInfoNonce"
+ MaxLineLength:MeshService.kt$MeshService$"Failed to parse radio packet (len=${bytes.size} contents=$packet). Not a valid FromRadio or LogRecord."
MaxLineLength:MeshService.kt$MeshService$"setOwner Id: $id longName: ${longName.anonymize} shortName: $shortName isLicensed: $isLicensed isUnmessagable: $isUnmessagable"
MaxLineLength:MeshService.kt$MeshService.<no name provided>$"sendData dest=${p.to}, id=${p.id} <- ${bytes.size} bytes (connectionState=${connectionStateHolder.getState()})"
- MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"Peripheral not ready, cannot send data. Peripheral is ${if (peripheral == null) "null" else "not null"}. Characteristic is ${if (characteristic == null) "null" else "not null"}."
- MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"fromNumCharacteristic discovered: uuid=${fromNumCharacteristic?.uuid} instanceId=${fromNumCharacteristic?.instanceId}"
- MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"fromRadioCharacteristic discovered (packet queue): uuid=${fromRadioCharacteristic?.uuid} instanceId=${fromRadioCharacteristic?.instanceId}"
- MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"service.serviceScope not active while dispatching from packet queue (source=$source); using localScope as fallback"
- MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"toRadioCharacteristic discovered: uuid=${toRadioCharacteristic?.uuid} instanceId=${toRadioCharacteristic?.instanceId}"
+ MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found fromNum: ${fromNumCharacteristic?.uuid}, ${fromNumCharacteristic?.instanceId}"
+ MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found fromRadio: ${fromRadioCharacteristic?.uuid}, ${fromRadioCharacteristic?.instanceId}"
+ MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found logRadio: ${logRadioCharacteristic?.uuid}, ${logRadioCharacteristic?.instanceId}"
+ MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found toRadio: ${toRadioCharacteristic?.uuid}, ${toRadioCharacteristic?.instanceId}"
ModifierClickableOrder:Channel.kt$clickable(onClick = onClick)
ModifierMissing:BLEDevices.kt$BLEDevices
ModifierMissing:Channel.kt$ChannelScreen
@@ -91,7 +88,6 @@
NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage)
NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)
NewLineAtEndOfFile:BLEException.kt$com.geeksville.mesh.service.BLEException.kt
- NewLineAtEndOfFile:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt
NewLineAtEndOfFile:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt
NewLineAtEndOfFile:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt
NewLineAtEndOfFile:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt
@@ -131,7 +127,6 @@
TooGenericExceptionCaught:MeshService.kt$MeshService.<no name provided>$ex: Exception
TooGenericExceptionCaught:MeshServiceStarter.kt$ServiceStarter$ex: Exception
TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$e: Exception
- TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$ex: Exception
TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$t: Throwable
TooGenericExceptionCaught:RadioInterfaceService.kt$RadioInterfaceService$t: Throwable
TooGenericExceptionCaught:SyncContinuation.kt$Continuation$ex: Throwable
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BleError.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BleError.kt
new file mode 100644
index 000000000..c9100ad5f
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BleError.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.repository.radio
+
+import com.geeksville.mesh.service.RadioNotConnectedException
+import no.nordicsemi.kotlin.ble.client.exception.BluetoothUnavailableException
+import no.nordicsemi.kotlin.ble.client.exception.ConnectionFailedException
+import no.nordicsemi.kotlin.ble.client.exception.InvalidAttributeException
+import no.nordicsemi.kotlin.ble.client.exception.OperationFailedException
+import no.nordicsemi.kotlin.ble.client.exception.PeripheralNotConnectedException
+import no.nordicsemi.kotlin.ble.client.exception.ScanningException
+import no.nordicsemi.kotlin.ble.client.exception.ValueDoesNotMatchException
+import no.nordicsemi.kotlin.ble.core.ConnectionState
+import no.nordicsemi.kotlin.ble.core.exception.BluetoothException
+import no.nordicsemi.kotlin.ble.core.exception.GattException
+import no.nordicsemi.kotlin.ble.core.exception.ManagerClosedException
+
+/**
+ * Represents specific BLE failures, modeled after the iOS implementation's AccessoryError. This allows for more
+ * granular error handling and intelligent reconnection strategies.
+ */
+sealed class BleError(val message: String, val shouldReconnect: Boolean) {
+
+ /**
+ * An error indicating that the peripheral was not found. This is a non-recoverable error and should not trigger a
+ * reconnect.
+ */
+ data object PeripheralNotFound : BleError("Peripheral not found", shouldReconnect = false)
+
+ /**
+ * An error indicating a failure during the connection attempt. This may be recoverable, so a reconnect attempt is
+ * warranted.
+ */
+ class ConnectionFailed(exception: Throwable) :
+ BleError("Connection failed: ${exception.message}", shouldReconnect = true)
+
+ /**
+ * An error indicating a failure during the service discovery process. This may be recoverable, so a reconnect
+ * attempt is warranted.
+ */
+ class DiscoveryFailed(message: String) : BleError("Discovery failed: $message", shouldReconnect = true)
+
+ /**
+ * An error indicating a disconnection initiated by the peripheral. This may be recoverable, so a reconnect attempt
+ * is warranted.
+ */
+ class Disconnected(reason: ConnectionState.Disconnected.Reason?) :
+ BleError("Disconnected: ${reason ?: "Unknown reason"}", shouldReconnect = true)
+
+ /**
+ * Wraps a generic GattException. The reconnection strategy depends on the nature of the Gatt error.
+ *
+ * @param exception The underlying GattException.
+ */
+ class GattError(exception: GattException) :
+ BleError("Gatt exception: ${exception.message}", shouldReconnect = true)
+
+ /**
+ * Wraps a generic BluetoothException. The reconnection strategy depends on the nature of the Bluetooth error.
+ *
+ * @param exception The underlying BluetoothException.
+ */
+ class BluetoothError(exception: BluetoothException) :
+ BleError("Bluetooth exception: ${exception.message}", shouldReconnect = true)
+
+ /** The BLE manager was closed. This is a non-recoverable error. */
+ class ManagerClosed(exception: ManagerClosedException) :
+ BleError("Manager closed: ${exception.message}", shouldReconnect = false)
+
+ /** A BLE operation failed. This may be recoverable. */
+ class OperationFailed(exception: OperationFailedException) :
+ BleError("Operation failed: ${exception.message}", shouldReconnect = true)
+
+ /** An invalid attribute was used. This is a non-recoverable error. */
+ class InvalidAttribute(exception: InvalidAttributeException) :
+ BleError("Invalid attribute: ${exception.message}", shouldReconnect = false)
+
+ /** An error occurred while scanning for devices. This may be recoverable. */
+ class Scanning(exception: ScanningException) :
+ BleError("Scanning error: ${exception.message}", shouldReconnect = true)
+
+ /** Bluetooth is unavailable on the device. This is a non-recoverable error. */
+ class BluetoothUnavailable(exception: BluetoothUnavailableException) :
+ BleError("Bluetooth unavailable: ${exception.message}", shouldReconnect = false)
+
+ /** The peripheral is not connected. This may be recoverable. */
+ class PeripheralNotConnected(exception: PeripheralNotConnectedException) :
+ BleError("Peripheral not connected: ${exception.message}", shouldReconnect = true)
+
+ /** A value did not match what was expected. This may be recoverable. */
+ class ValueDoesNotMatch(exception: ValueDoesNotMatchException) :
+ BleError("Value does not match: ${exception.message}", shouldReconnect = true)
+
+ /** A generic error for other exceptions that may occur. */
+ class GenericError(exception: Throwable) :
+ BleError("An unexpected error occurred: ${exception.message}", shouldReconnect = true)
+
+ companion object {
+ fun from(exception: Throwable): BleError = when (exception) {
+ is GattException -> {
+ when (exception) {
+ is ConnectionFailedException -> ConnectionFailed(exception)
+ is PeripheralNotConnectedException -> PeripheralNotConnected(exception)
+ is OperationFailedException -> OperationFailed(exception)
+ is ValueDoesNotMatchException -> ValueDoesNotMatch(exception)
+ else -> GattError(exception)
+ }
+ }
+ is BluetoothException -> {
+ when (exception) {
+ is BluetoothUnavailableException -> BluetoothUnavailable(exception)
+ is InvalidAttributeException -> InvalidAttribute(exception)
+ is ScanningException -> Scanning(exception)
+ else -> BluetoothError(exception)
+ }
+ }
+
+ is RadioNotConnectedException -> PeripheralNotFound
+ is ManagerClosedException -> ManagerClosed(exception)
+ else -> GenericError(exception)
+ }
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt
index ee241a23a..2c933f8c9 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt
@@ -20,6 +20,7 @@ package com.geeksville.mesh.repository.radio
import android.annotation.SuppressLint
import com.geeksville.mesh.repository.radio.BleUuidConstants.BTM_FROMNUM_CHARACTER
import com.geeksville.mesh.repository.radio.BleUuidConstants.BTM_FROMRADIO_CHARACTER
+import com.geeksville.mesh.repository.radio.BleUuidConstants.BTM_LOGRADIO_CHARACTER
import com.geeksville.mesh.repository.radio.BleUuidConstants.BTM_SERVICE_UUID
import com.geeksville.mesh.repository.radio.BleUuidConstants.BTM_TORADIO_CHARACTER
import com.geeksville.mesh.service.RadioNotConnectedException
@@ -32,10 +33,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -45,6 +46,8 @@ import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.android.ConnectionPriority
import no.nordicsemi.kotlin.ble.client.android.Peripheral
+import no.nordicsemi.kotlin.ble.core.CharacteristicProperty
+import no.nordicsemi.kotlin.ble.core.ConnectionState
import no.nordicsemi.kotlin.ble.core.WriteType
import timber.log.Timber
import java.util.UUID
@@ -80,6 +83,7 @@ constructor(
private var toRadioCharacteristic: RemoteCharacteristic? = null
private var fromNumCharacteristic: RemoteCharacteristic? = null
private var fromRadioCharacteristic: RemoteCharacteristic? = null
+ private var logRadioCharacteristic: RemoteCharacteristic? = null
init {
connect()
@@ -87,42 +91,43 @@ constructor(
// --- Packet Flow Management ---
- private fun packetQueueFlow(): Flow = channelFlow {
+ private fun fromRadioPacketFlow(): Flow = channelFlow {
while (isActive) {
// Use safe call and Elvis operator for cleaner loop termination if read fails or returns empty
val packet =
fromRadioCharacteristic?.read()?.takeIf { it.isNotEmpty() }
?: run {
- Timber.d("Packet queue drain complete (empty or null read)")
+ Timber.d("[$address] fromRadio queue drain complete (read empty/null)")
break
}
send(packet)
}
}
- private fun dispatchPacket(packet: ByteArray, source: String) {
+ private fun dispatchPacket(packet: ByteArray) {
+ Timber.d("[$address] Dispatching packet to service.handleFromRadio()")
connectionScope.launch {
try {
service.handleFromRadio(p = packet)
} catch (t: Throwable) {
- Timber.e(t, "Failed to schedule service.handleFromRadio (source=$source)")
+ Timber.e(t, "[$address] Failed to schedule service.handleFromRadio)")
}
}
}
- private suspend fun drainPacketQueueAndDispatch(source: String) {
+ private suspend fun drainPacketQueueAndDispatch() {
drainMutex.withLock {
var drainedCount = 0
- packetQueueFlow()
+ fromRadioPacketFlow()
.onEach { packet ->
drainedCount++
- logPacketRead(source, packet)
- dispatchPacket(packet, source)
+ Timber.d("[$address] Read packet from queue (${packet.size} bytes)")
+ dispatchPacket(packet)
}
- .catch { ex -> Timber.w(ex, "Exception while draining packet queue (source=$source)") }
+ .catch { ex -> Timber.w(ex, "[$address] Exception while draining packet queue") }
.onCompletion {
if (drainedCount > 0) {
- Timber.d("[$source] Drained $drainedCount packets from packet queue")
+ Timber.d("[$address] Drained $drainedCount packets from packet queue")
}
}
.collect()
@@ -145,8 +150,8 @@ constructor(
discoverServicesAndSetupCharacteristics(it)
}
} catch (e: Exception) {
- Timber.e(e, "Failed to connect to peripheral $address")
- service.onDisconnect(false)
+ Timber.e(e, "[$address] Failed to connect to peripheral")
+ service.onDisconnect(BleError.from(e))
}
}
}
@@ -165,27 +170,27 @@ constructor(
try {
peripheral?.let { p ->
val rssi = p.readRssi()
- Timber.d("Peripheral $address: RSSI: $rssi dBm")
+ Timber.d("[$address] Connection established. RSSI: $rssi dBm")
val phyInUse = p.readPhy()
- Timber.d("Peripheral $address: PHY in use: $phyInUse")
+ Timber.d("[$address] PHY in use: $phyInUse")
}
} catch (e: Exception) {
- Timber.w(e, "Failed to read initial connection properties for $address")
+ Timber.w(e, "[$address] Failed to read initial connection properties")
}
}
private fun observePeripheralChanges() {
peripheral?.let { p ->
- p.phy.onEach { phy -> Timber.d("Peripheral $address: PHY changed to $phy") }.launchIn(connectionScope)
+ p.phy.onEach { phy -> Timber.d("[$address] PHY changed to $phy") }.launchIn(connectionScope)
p.connectionParameters
- .onEach { Timber.d("Peripheral $address: Connection parameters changed to $it") }
+ .onEach { Timber.d("[$address] Connection parameters changed to $it") }
.launchIn(connectionScope)
p.state
.onEach { state ->
- Timber.d("Peripheral $address: State changed to $state")
- if (!state.isConnected) {
- service.onDisconnect(false)
+ Timber.d("[$address] State changed to $state")
+ if (state is ConnectionState.Disconnected) {
+ service.onDisconnect(BleError.Disconnected(reason = state.reason))
}
}
.launchIn(connectionScope)
@@ -210,22 +215,35 @@ constructor(
meshtasticService.characteristics.find { it.uuid == BTM_FROMNUM_CHARACTER.toKotlinUuid() }
fromRadioCharacteristic =
meshtasticService.characteristics.find { it.uuid == BTM_FROMRADIO_CHARACTER.toKotlinUuid() }
+ logRadioCharacteristic =
+ meshtasticService.characteristics.find { it.uuid == BTM_LOGRADIO_CHARACTER.toKotlinUuid() }
if (
listOf(toRadioCharacteristic, fromNumCharacteristic, fromRadioCharacteristic).all {
it != null
}
) {
- logCharacteristicInfo()
+ Timber.d(
+ "[$address] Found toRadio: ${toRadioCharacteristic?.uuid}, ${toRadioCharacteristic?.instanceId}",
+ )
+ Timber.d(
+ "[$address] Found fromNum: ${fromNumCharacteristic?.uuid}, ${fromNumCharacteristic?.instanceId}",
+ )
+ Timber.d(
+ "[$address] Found fromRadio: ${fromRadioCharacteristic?.uuid}, ${fromRadioCharacteristic?.instanceId}",
+ )
+ Timber.d(
+ "[$address] Found logRadio: ${logRadioCharacteristic?.uuid}, ${logRadioCharacteristic?.instanceId}",
+ )
setupNotifications()
service.onConnect()
} else {
- Timber.w("One or more characteristics not found on peripheral $address")
- service.onDisconnect(false)
+ Timber.w("[$address] Discovery failed: missing required characteristics")
+ service.onDisconnect(BleError.DiscoveryFailed("One or more characteristics not found"))
}
} else {
- Timber.w("Meshtastic service not found on peripheral $address")
- service.onDisconnect(false)
+ Timber.w("[$address] Discovery failed: Meshtastic service not found")
+ service.onDisconnect(BleError.DiscoveryFailed("Meshtastic service not found"))
}
}
.launchIn(connectionScope)
@@ -238,12 +256,30 @@ constructor(
private suspend fun setupNotifications() {
fromNumCharacteristic
?.subscribe()
+ ?.onStart { Timber.d("[$address] Subscribing to fromNumCharacteristic") }
?.onEach { notifyBytes ->
- logFromNumNotification(notifyBytes)
- connectionScope.launch { drainPacketQueueAndDispatch("notify") }
+ Timber.d("[$address] FromNum Notification (${notifyBytes.size} bytes), draining queue")
+ connectionScope.launch { drainPacketQueueAndDispatch() }
}
- ?.catch { e -> Timber.e(e, "Error in subscribe flow for fromNumCharacteristic") }
- ?.onCompletion { cause -> Timber.d("fromNum subscribe flow completed, cause=$cause") }
+ ?.catch { e ->
+ Timber.e(e, "[$address] Error subscribing to fromNumCharacteristic")
+ service.onDisconnect(BleError.from(e))
+ }
+ ?.onCompletion { cause -> Timber.d("[$address] fromNum sub flow completed, cause=$cause") }
+ ?.launchIn(scope = connectionScope)
+
+ logRadioCharacteristic
+ ?.subscribe()
+ ?.onStart { Timber.d("[$address] Subscribing to logRadioCharacteristic") }
+ ?.onEach { notifyBytes ->
+ Timber.d("[$address] LogRadio Notification (${notifyBytes.size} bytes), dispatching packet")
+ dispatchPacket(notifyBytes)
+ }
+ ?.catch { e ->
+ Timber.e(e, "[$address] Error subscribing to logRadioCharacteristic")
+ service.onDisconnect(BleError.from(e))
+ }
+ ?.onCompletion { cause -> Timber.d("[$address] logRadio sub flow completed, cause=$cause") }
?.launchIn(scope = connectionScope)
}
@@ -257,17 +293,23 @@ constructor(
override fun handleSendToRadio(p: ByteArray) {
toRadioCharacteristic?.let { characteristic ->
if (peripheral == null) return@let
-
connectionScope.launch {
try {
- characteristic.write(p, writeType = WriteType.WITHOUT_RESPONSE)
- // Post-write action initiation
- drainPacketQueueAndDispatch("post-write")
+ val writeType =
+ if (characteristic.properties.contains(CharacteristicProperty.WRITE_WITHOUT_RESPONSE)) {
+ WriteType.WITHOUT_RESPONSE
+ } else {
+ WriteType.WITH_RESPONSE
+ }
+ Timber.d("[$address] Writing packet to toRadioCharacteristic with $writeType")
+ characteristic.write(p, writeType = writeType)
+ drainPacketQueueAndDispatch()
} catch (e: Exception) {
- Timber.e(e, "Failed to write packet to $address")
+ Timber.e(e, "[$address] Failed to write packet to toRadioCharacteristic")
+ service.onDisconnect(BleError.from(e))
}
}
- } ?: Timber.w("toRadioCharacteristic not available when attempting to send data to $address")
+ } ?: Timber.w("[$address] toRadio unavailable, can't send data")
}
/** Closes the connection to the device. */
@@ -278,34 +320,6 @@ constructor(
service.onDisconnect(true)
}
}
-
- // --- Logging Helpers ---
-
- @OptIn(ExperimentalUuidApi::class)
- private fun logCharacteristicInfo() {
- Timber.d(
- "toRadioCharacteristic discovered: uuid=${toRadioCharacteristic?.uuid} instanceId=${toRadioCharacteristic?.instanceId}",
- )
- Timber.d(
- "fromNumCharacteristic discovered: uuid=${fromNumCharacteristic?.uuid} instanceId=${fromNumCharacteristic?.instanceId}",
- )
- Timber.d(
- "fromRadioCharacteristic discovered (packet queue): uuid=${fromRadioCharacteristic?.uuid} instanceId=${fromRadioCharacteristic?.instanceId}",
- )
- }
-
- private fun logPacketRead(source: String, packet: ByteArray) {
- val hexString = packet.joinToString(prefix = "[", postfix = "]") { b -> String.format("0x%02x", b) }
- Timber.d(
- "[$source] Read packet queue returned ${packet.size}" +
- " bytes: $hexString - dispatching to service.handleFromRadio()",
- )
- }
-
- private fun logFromNumNotification(notifyBytes: ByteArray) {
- val hexString = notifyBytes.joinToString(prefix = "[", postfix = "]") { b -> String.format("0x%02x", b) }
- Timber.d("FROMNUM notify, ${notifyBytes.size} bytes: $hexString - reading packet queue")
- }
}
object BleUuidConstants {
@@ -313,4 +327,5 @@ object BleUuidConstants {
val BTM_TORADIO_CHARACTER: UUID = UUID.fromString("f75c76d2-129e-4dad-a1dd-7866124401e7")
val BTM_FROMNUM_CHARACTER: UUID = UUID.fromString("ed9da18c-a800-4f66-a670-aa7547e34453")
val BTM_FROMRADIO_CHARACTER: UUID = UUID.fromString("2c55e69e-4993-11ed-b878-0242ac120002")
+ val BTM_LOGRADIO_CHARACTER: UUID = UUID.fromString("5a3d6e49-06e6-4423-9944-e9de8cdf9547")
}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
index 03d7f8f36..f78516889 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
@@ -83,6 +83,9 @@ constructor(
private val _receivedData = MutableSharedFlow()
val receivedData: SharedFlow = _receivedData
+ private val _connectionError = MutableSharedFlow()
+ val connectionError: SharedFlow = _connectionError.asSharedFlow()
+
// Thread-safe StateFlow for tracking device address changes
private val _currentDeviceAddressFlow = MutableStateFlow(radioPrefs.devAddr)
val currentDeviceAddressFlow: StateFlow = _currentDeviceAddressFlow.asStateFlow()
@@ -221,15 +224,6 @@ constructor(
// Handle an incoming packet from the radio, broadcasts it as an android intent
fun handleFromRadio(p: ByteArray) {
- Timber.d(
- "RadioInterfaceService.handleFromRadio called with ${p.size} bytes: ${p.joinToString(
- prefix = "[",
- postfix = "]",
- ) { b ->
- String.format("0x%02x", b)
- }}",
- )
-
if (logReceives) {
try {
receivedPacketsLog.write(p)
@@ -248,7 +242,6 @@ constructor(
try {
processLifecycle.coroutineScope.launch(dispatchers.io) { _receivedData.emit(p) }
emitReceiveActivity()
- Timber.d("RadioInterfaceService.handleFromRadio dispatched successfully")
} catch (t: Throwable) {
Timber.e(t, "RadioInterfaceService.handleFromRadio failed while emitting data")
}
@@ -267,6 +260,11 @@ constructor(
}
}
+ fun onDisconnect(error: BleError) {
+ processLifecycle.coroutineScope.launch(dispatchers.default) { _connectionError.emit(error) }
+ onDisconnect(!error.shouldReconnect)
+ }
+
/** Start our configured interface (if it isn't already running) */
private fun startInterface() {
if (radioIf !is NopInterface) {
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index fc664ec50..2b0a40189 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -220,7 +220,7 @@ class MeshService : Service() {
private const val DEFAULT_CONFIG_ONLY_NONCE = 69420
private const val DEFAULT_NODE_INFO_NONCE = 69421
- private const val WANT_CONFIG_DELAY = 50L
+ private const val WANT_CONFIG_DELAY = 250L
}
private val serviceJob = Job()
@@ -313,6 +313,9 @@ class MeshService : Service() {
serviceScope.handledLaunch { radioInterfaceService.connect() }
radioInterfaceService.connectionState.onEach(::onRadioConnectionState).launchIn(serviceScope)
radioInterfaceService.receivedData.onEach(::onReceiveFromRadio).launchIn(serviceScope)
+ radioInterfaceService.connectionError
+ .onEach { error -> Timber.e("BLE Connection Error: ${error.message}") }
+ .launchIn(serviceScope)
radioConfigRepository.localConfigFlow.onEach { localConfig = it }.launchIn(serviceScope)
radioConfigRepository.moduleConfigFlow.onEach { moduleConfig = it }.launchIn(serviceScope)
radioConfigRepository.channelSetFlow.onEach { channelSet = it }.launchIn(serviceScope)
@@ -758,6 +761,7 @@ class MeshService : Service() {
}
Portnums.PortNum.WAYPOINT_APP_VALUE -> {
+ Timber.d("Received WAYPOINT_APP from $fromId")
val u = MeshProtos.Waypoint.parseFrom(data.payload)
// Validate locked Waypoints from the original sender
if (u.lockedTo != 0 && u.lockedTo != packet.from) return
@@ -765,8 +769,8 @@ class MeshService : Service() {
}
Portnums.PortNum.POSITION_APP_VALUE -> {
+ Timber.d("Received POSITION_APP from $fromId")
val u = MeshProtos.Position.parseFrom(data.payload)
- // Timber.d("position_app ${packet.from} ${u.toOneLineString()}")
if (data.wantResponse && u.latitudeI == 0 && u.longitudeI == 0) {
Timber.d("Ignoring nop position update from position request")
} else {
@@ -776,6 +780,7 @@ class MeshService : Service() {
Portnums.PortNum.NODEINFO_APP_VALUE ->
if (!fromUs) {
+ Timber.d("Received NODEINFO_APP from $fromId")
val u =
MeshProtos.User.parseFrom(data.payload).copy {
if (isLicensed) clearPublicKey()
@@ -786,6 +791,7 @@ class MeshService : Service() {
// Handle new telemetry info
Portnums.PortNum.TELEMETRY_APP_VALUE -> {
+ Timber.d("Received TELEMETRY_APP from $fromId")
val u =
TelemetryProtos.Telemetry.parseFrom(data.payload).copy {
if (time == 0) time = (dataPacket.time / 1000L).toInt()
@@ -794,6 +800,7 @@ class MeshService : Service() {
}
Portnums.PortNum.ROUTING_APP_VALUE -> {
+ Timber.d("Received ROUTING_APP from $fromId")
// We always send ACKs to other apps, because they might care about the
// messages they sent
shouldBroadcast = true
@@ -808,24 +815,28 @@ class MeshService : Service() {
}
Portnums.PortNum.ADMIN_APP_VALUE -> {
+ Timber.d("Received ADMIN_APP from $fromId")
val u = AdminProtos.AdminMessage.parseFrom(data.payload)
handleReceivedAdmin(packet.from, u)
shouldBroadcast = false
}
Portnums.PortNum.PAXCOUNTER_APP_VALUE -> {
+ Timber.d("Received PAXCOUNTER_APP from $fromId")
val p = PaxcountProtos.Paxcount.parseFrom(data.payload)
handleReceivedPaxcounter(packet.from, p)
shouldBroadcast = false
}
Portnums.PortNum.STORE_FORWARD_APP_VALUE -> {
+ Timber.d("Received STORE_FORWARD_APP from $fromId")
val u = StoreAndForwardProtos.StoreAndForward.parseFrom(data.payload)
handleReceivedStoreAndForward(dataPacket, u)
shouldBroadcast = false
}
Portnums.PortNum.RANGE_TEST_APP_VALUE -> {
+ Timber.d("Received RANGE_TEST_APP from $fromId")
if (!moduleConfig.rangeTest.enabled) return
val u = dataPacket.copy(dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
rememberDataPacket(u)
@@ -856,7 +867,7 @@ class MeshService : Service() {
}
}
- else -> Timber.d("No custom processing needed for ${data.portnumValue}")
+ else -> Timber.d("No custom processing needed for ${data.portnumValue} from $fromId")
}
// We always tell other apps when new data packets arrive
@@ -962,10 +973,7 @@ class MeshService : Service() {
if (myNodeNum == fromNum && p.latitudeI == 0 && p.longitudeI == 0) {
Timber.d("Ignoring nop position update for the local node")
} else {
- updateNodeInfo(fromNum) {
- Timber.d("update position: ${it.longName?.toPIIString()} with ${p.toPIIString()}")
- it.setPosition(p, (defaultTime / 1000L).toInt())
- }
+ updateNodeInfo(fromNum) { it.setPosition(p, (defaultTime / 1000L).toInt()) }
}
}
@@ -1033,7 +1041,6 @@ class MeshService : Service() {
}
private fun handleReceivedStoreAndForward(dataPacket: DataPacket, s: StoreAndForwardProtos.StoreAndForward) {
- Timber.d("StoreAndForward: ${s.variantCase} ${s.rr} from ${dataPacket.from}")
when (s.variantCase) {
StoreAndForwardProtos.StoreAndForward.VariantCase.STATS -> {
val u =
@@ -1078,6 +1085,7 @@ class MeshService : Service() {
// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun handleReceivedMeshPacket(packet: MeshPacket) {
+ Timber.d("[packet]: ${packet.toOneLineString()}")
if (haveNodeDB) {
processReceivedMeshPacket(
packet
@@ -1142,12 +1150,6 @@ class MeshService : Service() {
// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun processReceivedMeshPacket(packet: MeshPacket) {
val fromNum = packet.from
-
- // FIXME, perhaps we could learn our node ID by looking at any to packets the radio
- // decided to pass through to us (except for broadcast packets)
- // val toNum = packet.to
-
- // Timber.d("Received: $packet")
if (packet.hasDecoded()) {
val packetToSave =
MeshLog(
@@ -1276,6 +1278,7 @@ class MeshService : Service() {
}
fun startDisconnect() {
+ Timber.d("Starting disconnect")
packetHandler.stopPacketQueue()
stopLocationRequests()
stopMqttClientProxy()
@@ -1288,6 +1291,7 @@ class MeshService : Service() {
}
fun startConnect() {
+ Timber.d("Starting connect")
try {
connectTimeMsec = System.currentTimeMillis()
startConfigOnly()
@@ -1355,7 +1359,6 @@ class MeshService : Service() {
private val packetHandlers: Map Unit)> by lazy {
PayloadVariantCase.entries.associateWith { variant: PayloadVariantCase ->
- Timber.d("PacketHandler - handling $variant")
when (variant) {
PayloadVariantCase.PACKET -> { proto: MeshProtos.FromRadio -> handleReceivedMeshPacket(proto.packet) }
@@ -1364,9 +1367,11 @@ class MeshService : Service() {
}
PayloadVariantCase.MY_INFO -> { proto: MeshProtos.FromRadio -> handleMyInfo(proto.myInfo) }
+
PayloadVariantCase.NODE_INFO -> { proto: MeshProtos.FromRadio -> handleNodeInfo(proto.nodeInfo) }
PayloadVariantCase.CHANNEL -> { proto: MeshProtos.FromRadio -> handleChannel(proto.channel) }
+
PayloadVariantCase.CONFIG -> { proto: MeshProtos.FromRadio -> handleDeviceConfig(proto.config) }
PayloadVariantCase.MODULECONFIG -> { proto: MeshProtos.FromRadio ->
@@ -1378,6 +1383,7 @@ class MeshService : Service() {
}
PayloadVariantCase.METADATA -> { proto: MeshProtos.FromRadio -> handleMetadata(proto.metadata) }
+
PayloadVariantCase.MQTTCLIENTPROXYMESSAGE -> { proto: MeshProtos.FromRadio ->
handleMqttProxyMessage(proto.mqttClientProxyMessage)
}
@@ -1387,22 +1393,22 @@ class MeshService : Service() {
}
PayloadVariantCase.FILEINFO -> { proto: MeshProtos.FromRadio -> handleFileInfo(proto.fileInfo) }
+
PayloadVariantCase.CLIENTNOTIFICATION -> { proto: MeshProtos.FromRadio ->
handleClientNotification(proto.clientNotification)
}
- PayloadVariantCase.LOG_RECORD -> { proto: MeshProtos.FromRadio -> handleLogReord(proto.logRecord) }
+ PayloadVariantCase.LOG_RECORD -> { proto: MeshProtos.FromRadio -> handleLogRecord(proto.logRecord) }
PayloadVariantCase.REBOOTED -> { proto: MeshProtos.FromRadio -> handleRebooted(proto.rebooted) }
+
PayloadVariantCase.XMODEMPACKET -> { proto: MeshProtos.FromRadio ->
handleXmodemPacket(proto.xmodemPacket)
}
// Explicitly handle default/unwanted cases to satisfy the exhaustive `when`
PayloadVariantCase.PAYLOADVARIANT_NOT_SET -> { proto ->
- Timber.e("Unexpected or unrecognized FromRadio variant: ${proto.payloadVariantCase}")
- // Additional debug: log raw bytes if possible (can't access bytes here) and full proto
- Timber.d("Full FromRadio proto: $proto")
+ Timber.d("Received variant PayloadVariantUnset: Full FromRadio proto: ${proto.toPIIString()}")
}
}
}
@@ -1412,22 +1418,33 @@ class MeshService : Service() {
packetHandlers[this.payloadVariantCase]?.invoke(this)
}
+ /**
+ * Parses and routes incoming data from the radio.
+ *
+ * This function first attempts to parse the data as a `FromRadio` protobuf message. If that fails, it then tries to
+ * parse it as a `LogRecord` for debugging purposes.
+ */
private fun onReceiveFromRadio(bytes: ByteArray) {
- try {
- val proto = MeshProtos.FromRadio.parseFrom(bytes)
- if (proto.payloadVariantCase == PayloadVariantCase.PAYLOADVARIANT_NOT_SET) {
- Timber.w(
- "Received FromRadio with PAYLOADVARIANT_NOT_SET. rawBytes=${bytes.joinToString(",") { b ->
- String.format("0x%02x", b)
- }} proto=$proto",
- )
+ runCatching { MeshProtos.FromRadio.parseFrom(bytes) }
+ .onSuccess { proto -> proto.route() }
+ .onFailure { primaryException ->
+ runCatching {
+ val logRecord = MeshProtos.LogRecord.parseFrom(bytes)
+ handleLogRecord(logRecord)
+ }
+ .onFailure { _ ->
+ val packet = bytes.toHexString()
+ Timber.e(
+ primaryException,
+ "Failed to parse radio packet (len=${bytes.size} contents=$packet). Not a valid FromRadio or LogRecord.",
+ )
+ }
}
- proto.route()
- } catch (ex: InvalidProtocolBufferException) {
- Timber.e("Invalid Protobuf from radio, len=${bytes.size}", ex)
- }
}
+ /** Extension function to convert a ByteArray to a hex string for logging. Example output: "0x0a,0x1f,0x..." */
+ private fun ByteArray.toHexString(): String = this.joinToString(",") { byte -> String.format("0x%02x", byte) }
+
// A provisional MyNodeInfo that we will install if all of our node config downloads go okay
private var newMyNodeInfo: MyNodeEntity? = null
@@ -1439,7 +1456,7 @@ class MeshService : Service() {
private var nodeInfoNonce: Int = DEFAULT_NODE_INFO_NONCE
private fun handleDeviceConfig(config: ConfigProtos.Config) {
- Timber.d("Received config ${config.toOneLineString()}")
+ Timber.d("[deviceConfig] ${config.toPIIString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1455,7 +1472,7 @@ class MeshService : Service() {
}
private fun handleModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
- Timber.d("Received moduleConfig ${config.toOneLineString()}")
+ Timber.d("[moduleConfig] ${config.toPIIString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1471,7 +1488,7 @@ class MeshService : Service() {
}
private fun handleChannel(ch: ChannelProtos.Channel) {
- Timber.d("Received channel ${ch.index}")
+ Timber.d("[channel] ${ch.toPIIString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1529,13 +1546,7 @@ class MeshService : Service() {
}
private fun handleNodeInfo(info: MeshProtos.NodeInfo) {
- Timber.d(
- "Received nodeinfo num=${info.num}," +
- " hasUser=${info.hasUser()}," +
- " hasPosition=${info.hasPosition()}," +
- " hasDeviceMetrics=${info.hasDeviceMetrics()}",
- )
-
+ Timber.d("[nodeInfo] ${info.toPIIString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1600,6 +1611,7 @@ class MeshService : Service() {
/** Update MyNodeInfo (called from either new API version or the old one) */
private fun handleMyInfo(myInfo: MeshProtos.MyNodeInfo) {
+ Timber.d("[myInfo] ${myInfo.toPIIString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1623,7 +1635,7 @@ class MeshService : Service() {
/** Update our DeviceMetadata */
private fun handleMetadata(metadata: MeshProtos.DeviceMetadata) {
- Timber.d("Received deviceMetadata ${metadata.toOneLineString()}")
+ Timber.d("[deviceMetadata] ${metadata.toPIIString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1639,6 +1651,7 @@ class MeshService : Service() {
/** Publish MqttClientProxyMessage (fromRadio) */
private fun handleMqttProxyMessage(message: MeshProtos.MqttClientProxyMessage) {
+ Timber.d("[mqttClientProxyMessage] ${message.toPIIString()}")
with(message) {
when (payloadVariantCase) {
MeshProtos.MqttClientProxyMessage.PayloadVariantCase.TEXT -> {
@@ -1655,16 +1668,25 @@ class MeshService : Service() {
}
private fun handleClientNotification(notification: MeshProtos.ClientNotification) {
- Timber.d("Received clientNotification ${notification.toOneLineString()}")
+ Timber.d("[clientNotification] ${notification.toPIIString()}")
serviceRepository.setClientNotification(notification)
serviceNotifications.showClientNotification(notification)
// if the future for the originating request is still in the queue, complete as unsuccessful
// for now
packetHandler.removeResponse(notification.replyId, complete = false)
+ val packetToSave =
+ MeshLog(
+ uuid = UUID.randomUUID().toString(),
+ message_type = "ClientNotification",
+ received_date = System.currentTimeMillis(),
+ raw_message = notification.toString(),
+ fromRadio = fromRadio { this.clientNotification = notification },
+ )
+ insertMeshLog(packetToSave)
}
private fun handleFileInfo(fileInfo: MeshProtos.FileInfo) {
- Timber.d("Received fileInfo ${fileInfo.toOneLineString()}")
+ Timber.d("[fileInfo] ${fileInfo.toPIIString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1676,8 +1698,8 @@ class MeshService : Service() {
insertMeshLog(packetToSave)
}
- private fun handleLogReord(logRecord: MeshProtos.LogRecord) {
- Timber.d("Received logRecord ${logRecord.toOneLineString()}")
+ private fun handleLogRecord(logRecord: MeshProtos.LogRecord) {
+ Timber.d("[logRecord] ${logRecord.toPIIString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1690,7 +1712,7 @@ class MeshService : Service() {
}
private fun handleRebooted(rebooted: Boolean) {
- Timber.d("Received rebooted ${rebooted.toOneLineString()}")
+ Timber.d("[rebooted] $rebooted")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1703,7 +1725,7 @@ class MeshService : Service() {
}
private fun handleXmodemPacket(xmodemPacket: XmodemProtos.XModem) {
- Timber.d("Received XmodemPacket ${xmodemPacket.toOneLineString()}")
+ Timber.d("[xmodemPacket] ${xmodemPacket.toPIIString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1716,7 +1738,7 @@ class MeshService : Service() {
}
private fun handleDeviceUiConfig(deviceuiConfig: DeviceUIProtos.DeviceUIConfig) {
- Timber.d("Received deviceUIConfig ${deviceuiConfig.toOneLineString()}")
+ Timber.d("[deviceuiConfig] ${deviceuiConfig.toPIIString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1751,18 +1773,16 @@ class MeshService : Service() {
}
private fun onHasSettings() {
- packetHandler.sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { setTimeOnly = currentSecond() })
processQueuedPackets()
startMqttClientProxy()
serviceBroadcasts.broadcastConnection()
sendAnalytics()
reportConnection()
+ packetHandler.sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { setTimeOnly = currentSecond() })
}
private fun handleConfigComplete(configCompleteId: Int) {
- Timber.d(
- "handleConfigComplete called with id=$configCompleteId, configOnly=$configOnlyNonce, nodeInfo=$nodeInfoNonce",
- )
+ Timber.d("[configCompleteId]: ${configCompleteId.toPIIString()}")
when (configCompleteId) {
configOnlyNonce -> handleConfigOnlyComplete()
nodeInfoNonce -> handleNodeInfoComplete()
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
index 432e24d60..68b830542 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
@@ -24,6 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.NodeInfo
+import org.meshtastic.core.model.util.toPIIString
import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import javax.inject.Inject
@@ -50,7 +51,7 @@ constructor(
}
fun broadcastNodeChange(info: NodeInfo) {
- Timber.d("Broadcasting node change $info")
+ Timber.d("Broadcasting node change ${info.user?.toPIIString()}")
val intent = Intent(MeshService.ACTION_NODE_CHANGE).putExtra(EXTRA_NODEINFO, info)
explicitBroadcast(intent)
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt b/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
index b368e1510..160491ebe 100644
--- a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
@@ -115,7 +115,7 @@ constructor(
}
fun handleQueueStatus(queueStatus: MeshProtos.QueueStatus) {
- Timber.d("queueStatus ${queueStatus.toOneLineString()}")
+ Timber.d("[queueStatus] ${queueStatus.toOneLineString()}")
val (success, isFull, requestId) = with(queueStatus) { Triple(res == 0, free == 0, meshPacketId) }
if (success && isFull) return // Queue is full, wait for free != 0
if (requestId != 0) {
@@ -184,7 +184,7 @@ constructor(
if (connectionStateHolder.getState() != ConnectionState.CONNECTED) throw RadioNotConnectedException()
sendToRadio(ToRadio.newBuilder().apply { this.packet = packet })
} catch (ex: Exception) {
- Timber.e("sendToRadio error:", ex)
+ Timber.e(ex, "sendToRadio error: ${ex.message}")
future.complete(false)
}
return future
diff --git a/core/model/detekt-baseline.xml b/core/model/detekt-baseline.xml
index 3cc9a334d..f459aeba8 100644
--- a/core/model/detekt-baseline.xml
+++ b/core/model/detekt-baseline.xml
@@ -30,5 +30,6 @@
MagicNumber:ChannelSet.kt$960
SwallowedException:ChannelSet.kt$ex: Throwable
TooGenericExceptionCaught:ChannelSet.kt$ex: Throwable
+ TooManyFunctions:Extensions.kt$org.meshtastic.core.model.util.Extensions.kt
diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt
index f72273522..4886f333f 100644
--- a/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt
@@ -44,6 +44,13 @@ fun ConfigProtos.Config.toOneLineString(): String {
.replace('\n', ' ')
}
+fun MeshProtos.MeshPacket.toOneLineString(): String {
+ val redactedFields = """(public_key:|private_key:|admin_key:)\s*".*""" // Redact keys
+ return this.toString()
+ .replace(redactedFields.toRegex()) { "${it.groupValues[1]} \"[REDACTED]\"" }
+ .replace('\n', ' ')
+}
+
fun MeshProtos.toOneLineString(): String {
val redactedFields = """(public_key:|private_key:|admin_key:)\s*".*""" // Redact keys
return this.toString()
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
index 7bb215118..ac0126f43 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
+++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
@@ -238,7 +238,6 @@ internal fun DebugItem(
val messageAnnotatedString = rememberAnnotatedLogMessage(log, searchText)
Text(
text = messageAnnotatedString,
- softWrap = false,
style =
TextStyle(
fontSize = if (isSelected) 12.sp else 9.sp,
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt
index 1380b1e00..6d93f02f3 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt
+++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt
@@ -267,6 +267,7 @@ constructor(
/** Transform the input [MeshLog] by enhancing the raw message with annotations. */
private fun annotateMeshLogMessage(meshLog: MeshLog): String = when (meshLog.message_type) {
+ "LogRecord" -> meshLog.fromRadio.logRecord.toString().replace("\\n\"", "\"")
"Packet" -> meshLog.meshPacket?.let { packet -> annotatePacketLog(packet) } ?: meshLog.raw_message
"NodeInfo" ->
meshLog.nodeInfo?.let { nodeInfo -> annotateRawMessage(meshLog.raw_message, nodeInfo.num) }