mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-30 11:44:01 -04:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 - " +
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user