refactor(logging): Reduce log noise by lowering severity of common errors (#4591)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich
2026-02-18 15:16:11 -06:00
committed by GitHub
parent 7ffbbd6113
commit f012e3818d
14 changed files with 91 additions and 56 deletions

View File

@@ -24,6 +24,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
@@ -51,8 +52,6 @@ import org.meshtastic.core.strings.meshtastic
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import javax.inject.Inject
// ... (DeviceListEntry sealed class remains the same) ...
@HiltViewModel
@Suppress("LongParameterList", "TooManyFunctions")
class BTScanModel
@@ -209,11 +208,20 @@ constructor(
changeDeviceAddress(entry.fullAddress)
} catch (ex: SecurityException) {
Logger.w(ex) { "Bonding failed for ${entry.peripheral.address.anonymize} Permissions not granted" }
serviceRepository.setErrorMessage("Bonding failed: ${ex.message} Permissions not granted")
serviceRepository.setErrorMessage(
text = "Bonding failed: ${ex.message} Permissions not granted",
severity = Severity.Warn,
)
} catch (ex: Exception) {
// Bonding is often flaky and can fail for many reasons (timeout, user cancel, etc)
Logger.w(ex) { "Bonding failed for ${entry.peripheral.address.anonymize}" }
serviceRepository.setErrorMessage("Bonding failed: ${ex.message}")
val message = ex.message ?: ""
if (message.contains("Received bond state changed 11")) {
// This is a known issue where bonding is still in progress, ignore as error
Logger.d { "Bonding still in progress for ${entry.peripheral.address.anonymize}" }
} else {
Logger.w(ex) { "Bonding failed for ${entry.peripheral.address.anonymize}" }
serviceRepository.setErrorMessage(text = "Bonding failed: ${ex.message}", severity = Severity.Warn)
}
}
}
}

View File

@@ -70,10 +70,12 @@ constructor(
fun disconnect() {
Logger.i { "MQTT Disconnected" }
mqttClient?.apply {
ignoreException { disconnect() }
close(true)
mqttClient = null
if (isConnected) {
ignoreException { disconnect() }
}
ignoreException { close(true) }
}
mqttClient = null
}
val proxyMessageFlow: Flow<MqttClientProxyMessage> = callbackFlow {
@@ -166,7 +168,11 @@ constructor(
val token = mqttClient?.publish(topic, data, DEFAULT_QOS, retained)
Logger.i { "MQTT Publish messageId: ${token?.messageId}" }
} catch (ex: Exception) {
Logger.e { "MQTT Publish error: ${ex.message}" }
if (ex.message?.contains("Client is disconnected") == true) {
Logger.w { "MQTT Publish skipped: Client is disconnected" }
} else {
Logger.e(ex) { "MQTT Publish error: ${ex.message}" }
}
}
}
}

View File

@@ -96,7 +96,8 @@ constructor(
0
}
thrown?.let { e ->
Logger.e(e) { "[$address] Serial error after ${uptime}ms: ${e.message}" }
// USB errors are common when unplugging; log as warning to avoid Crashlytics noise
Logger.w(e) { "[$address] Serial error after ${uptime}ms: ${e.message}" }
}
Logger.w {
"[$address] Serial device disconnected - " +

View File

@@ -84,7 +84,8 @@ constructor(
try {
stream.write(p)
} catch (ex: IOException) {
Logger.e(ex) { "[$address] TCP write error: ${ex.message}" }
// TCP write errors are common when the connection is lost; log as warning to avoid Crashlytics noise
Logger.w(ex) { "[$address] TCP write error: ${ex.message}" }
onDeviceDisconnect(false)
}
}
@@ -95,7 +96,8 @@ constructor(
try {
stream.flush()
} catch (ex: IOException) {
Logger.e(ex) { "[$address] TCP flush error: ${ex.message}" }
// TCP flush errors are common when the connection is lost; log as warning to avoid Crashlytics noise
Logger.w(ex) { "[$address] TCP flush error: ${ex.message}" }
onDeviceDisconnect(false)
}
}

View File

@@ -42,29 +42,30 @@ internal class SerialConnectionImpl(
@Suppress("TooGenericExceptionCaught")
override fun sendBytes(bytes: ByteArray) {
ioRef.get()?.let {
Logger.d { "writing ${bytes.size} byte(s }" }
Logger.d { "writing ${bytes.size} byte(s)" }
try {
it.writeAsync(bytes)
} catch (e: BufferOverflowException) {
Logger.e(e) { "Buffer overflow while writing to serial port" }
Logger.w(e) { "Buffer overflow while writing to serial port" }
} catch (e: Exception) {
Logger.e(e) { "Failed to write to serial port" }
// USB disconnections often cause IOExceptions here; log as warning to avoid Crashlytics noise
Logger.w(e) { "Failed to write to serial port (likely disconnected)" }
}
}
}
override fun close(waitForStopped: Boolean) {
ignoreException {
if (closed.compareAndSet(false, true)) {
ioRef.get()?.stop()
if (closed.compareAndSet(false, true)) {
ignoreException(silent = true) { ioRef.get()?.stop() }
ignoreException(silent = true) {
port.close() // This will cause the reader thread to exit
}
}
// Allow a short amount of time for the manager to quit (so the port can be cleanly closed)
if (waitForStopped) {
Logger.d { "Waiting for USB manager to stop..." }
closedLatch.await(1.seconds)
}
// Allow a short amount of time for the manager to quit (so the port can be cleanly closed)
if (waitForStopped) {
Logger.d { "Waiting for USB manager to stop..." }
ignoreException(silent = true) { closedLatch.await(1.seconds) }
}
}
@@ -99,11 +100,9 @@ internal class SerialConnectionImpl(
override fun onRunError(e: Exception?) {
closed.set(true)
ignoreException {
port.dtr = false
port.rts = false
port.close()
}
// Connection is already failing, don't try to set DTR/RTS as it will just throw more
// IOExceptions
ignoreException(silent = true) { port.close() }
closedLatch.countDown()
listener.onDisconnected(e)
}

View File

@@ -18,6 +18,7 @@ package com.geeksville.mesh.service
import android.util.Log
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.radio.InterfaceId
@@ -459,7 +460,7 @@ constructor(
val payload = packet.decoded?.payload ?: return
val r = Routing.ADAPTER.decodeOrNull(payload, Logger) ?: return
if (r.error_reason == Routing.Error.DUTY_CYCLE_LIMIT) {
serviceRepository.setErrorMessage(getString(Res.string.error_duty_cycle))
serviceRepository.setErrorMessage(getString(Res.string.error_duty_cycle), Severity.Warn)
}
handleAckNak(
packet.decoded?.request_id ?: 0,

View File

@@ -17,6 +17,7 @@
package com.geeksville.mesh.service
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import com.geeksville.mesh.repository.network.MQTTRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -49,7 +50,12 @@ constructor(
mqttMessageFlow =
mqttRepository.proxyMessageFlow
.onEach { message -> packetHandler.sendToRadio(ToRadio(mqttClientProxyMessage = message)) }
.catch { throwable -> serviceRepository.setErrorMessage("MqttClientProxy failed: $throwable") }
.catch { throwable ->
serviceRepository.setErrorMessage(
text = "MqttClientProxy failed: $throwable",
severity = Severity.Warn,
)
}
.launchIn(scope)
}
}

View File

@@ -196,6 +196,10 @@ constructor(
throw RadioNotConnectedException()
}
sendToRadio(ToRadio(packet = packet))
} catch (ex: RadioNotConnectedException) {
// Expected when radio is not connected, log as warning to avoid Crashlytics noise
Logger.w(ex) { "sendToRadio skipped: Not connected to radio" }
deferred.complete(false)
} catch (ex: Exception) {
Logger.e(ex) { "sendToRadio error: ${ex.message}" }
deferred.complete(false)

View File

@@ -63,7 +63,7 @@ import kotlin.time.Duration.Companion.seconds
private const val RSSI_DELAY = 10
private const val RSSI_TIMEOUT = 5
@Suppress("LongMethod", "LoopWithTooManyJumpStatements")
@Suppress("LongMethod", "LoopWithTooManyJumpStatements", "TooGenericExceptionCaught")
@Composable
fun CurrentlyConnectedInfo(
node: Node,
@@ -80,13 +80,17 @@ fun CurrentlyConnectedInfo(
rssi = withTimeout(RSSI_TIMEOUT.seconds) { bleDevice.peripheral.readRssi() }
delay(RSSI_DELAY.seconds)
} catch (e: PeripheralNotConnectedException) {
Logger.e(e) { "Failed to read RSSI ${e.message}" }
Logger.w(e) { "Failed to read RSSI ${e.message}" }
break
} catch (e: OperationFailedException) {
Logger.e(e) { "Failed to read RSSI ${e.message}" }
// RSSI reading failures are common when disconnecting; log as warning to avoid Crashlytics noise
Logger.w(e) { "Failed to read RSSI ${e.message}" }
break
} catch (e: SecurityException) {
Logger.e(e) { "Failed to read RSSI ${e.message}" }
Logger.w(e) { "Failed to read RSSI ${e.message}" }
break
} catch (e: Exception) {
Logger.w(e) { "Unexpected error reading RSSI: ${e.message}" }
break
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 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
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.util
import android.os.RemoteException
@@ -57,7 +56,7 @@ fun ignoreException(silent: Boolean = false, inner: () -> Unit) {
inner()
} catch (ex: Throwable) {
// DO NOT THROW users expect we have fully handled/discarded the exception
if (!silent) Logger.e(ex) { "ignoring exception" }
if (!silent) Logger.w(ex) { "ignoring exception" }
}
}

View File

@@ -17,6 +17,7 @@
package org.meshtastic.core.service
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -65,7 +66,7 @@ class ServiceRepository @Inject constructor() {
get() = _clientNotification
fun setClientNotification(notification: ClientNotification?) {
Logger.e { notification?.message.orEmpty() }
notification?.message?.let { Logger.w { it } }
_clientNotification.value = notification
}
@@ -78,8 +79,8 @@ class ServiceRepository @Inject constructor() {
val errorMessage: StateFlow<String?>
get() = _errorMessage
fun setErrorMessage(text: String) {
Logger.e { text }
fun setErrorMessage(text: String, severity: Severity = Severity.Error) {
Logger.log(severity, "ServiceRepository", null, text)
_errorMessage.value = text
}

View File

@@ -50,6 +50,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.navigation.common)
implementation(libs.androidx.savedstate.compose)
implementation(libs.androidx.savedstate.ktx)
implementation(libs.material)
implementation(libs.kermit)

View File

@@ -20,11 +20,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.LifecycleOwner
import androidx.compose.ui.platform.LocalView
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.compose.LocalSavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.maps.android.clustering.Cluster
@@ -44,20 +44,22 @@ fun NodeClusterMarkers(
navigateToNodeDetails: (Int) -> Unit,
onClusterClick: (Cluster<NodeClusterItem>) -> Boolean,
) {
val context = LocalContext.current
val view = LocalView.current
val lifecycleOwner = LocalLifecycleOwner.current
val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
// Workaround for https://github.com/googlemaps/android-maps-compose/issues/858
// Ensure owners are set on the Activity decor view so the internal ComposeView created by
// the clustering renderer can find them when walking up the view tree.
LaunchedEffect(Unit) {
val activity = context as? android.app.Activity
if (activity != null) {
val decorView = activity.window.decorView
if (decorView.findViewTreeLifecycleOwner() == null && activity is LifecycleOwner) {
decorView.setViewTreeLifecycleOwner(activity)
}
if (decorView.findViewTreeSavedStateRegistryOwner() == null && activity is SavedStateRegistryOwner) {
decorView.setViewTreeSavedStateRegistryOwner(activity)
}
// The maps clustering library creates an internal ComposeView to snapshot markers.
// If that view is not attached to the hierarchy (which it often isn't during rendering),
// it fails to find the Lifecycle and SavedState owners. We propagate them to the root view
// so the internal snapshot view can find them when walking up the tree.
LaunchedEffect(view, lifecycleOwner, savedStateRegistryOwner) {
val root = view.rootView
if (root.findViewTreeLifecycleOwner() == null) {
root.setViewTreeLifecycleOwner(lifecycleOwner)
}
if (root.findViewTreeSavedStateRegistryOwner() == null) {
root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
}
}

View File

@@ -98,6 +98,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
androidx-savedstate-compose = { module = "androidx.savedstate:savedstate-compose", version.ref = "savedstate" }
androidx-savedstate-ktx = { module = "androidx.savedstate:savedstate-ktx", version.ref = "savedstate" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.1" }