feat(logging): Replace Timber with Kermit for multiplatform logging (#4083)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich
2025-12-28 08:30:15 -06:00
committed by GitHub
parent a927481e4d
commit 0776e029f3
92 changed files with 727 additions and 957 deletions

View File

@@ -232,7 +232,7 @@ dependencies {
implementation(libs.usb.serial.android)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.accompanist.permissions)
implementation(libs.timber)
implementation(libs.kermit)
implementation(libs.nordic)

View File

@@ -5,11 +5,6 @@
<ID>CommentSpacing:BLEException.kt$BLEConnectionClosing$/// Our interface is being shut down</ID>
<ID>CommentSpacing:Constants.kt$/// a bool true means we expect this condition to continue until, false means device might come back</ID>
<ID>CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib</ID>
<ID>ComposableParamOrder:Channel.kt$ChannelScreen</ID>
<ID>ComposableParamOrder:Channel.kt$EditChannelUrl</ID>
<ID>ComposableParamOrder:ConnectionsNavIcon.kt$ConnectionsNavIcon</ID>
<ID>ComposableParamOrder:EmptyStateContent.kt$EmptyStateContent</ID>
<ID>ComposableParamOrder:Share.kt$ShareScreen</ID>
<ID>CyclomaticComplexMethod:BleError.kt$BleError.Companion$fun from(exception: Throwable): BleError</ID>
<ID>CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>CyclomaticComplexMethod:SettingsNavigation.kt$@Suppress("LongMethod") fun NavGraphBuilder.settingsGraph(navController: NavHostController)</ID>
@@ -32,10 +27,9 @@
<ID>FinalNewline:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
<ID>FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>LambdaParameterEventTrailing:Channel.kt$onConfirm</ID>
<ID>LambdaParameterInRestartableEffect:Channel.kt$onConfirm</ID>
<ID>LargeClass:MeshService.kt$MeshService : Service</ID>
<ID>LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>LongMethod:TCPInterface.kt$TCPInterface$private suspend fun startConnect()</ID>
<ID>MagicNumber:Contacts.kt$7</ID>
<ID>MagicNumber:Contacts.kt$8</ID>
<ID>MagicNumber:MQTTRepository.kt$MQTTRepository$512</ID>
@@ -60,27 +54,15 @@
<ID>MagicNumber:StreamInterface.kt$StreamInterface$4</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$8</ID>
<ID>MagicNumber:TCPInterface.kt$TCPInterface$1000</ID>
<ID>MagicNumber:TCPInterface.kt$TCPInterface$180</ID>
<ID>MagicNumber:TCPInterface.kt$TCPInterface$500</ID>
<ID>MagicNumber:UIState.kt$4</ID>
<ID>MaxLineLength:MeshService.kt$MeshService$"Config complete id mismatch: received=$configCompleteId expected one of [$configOnlyNonce,$nodeInfoNonce]"</ID>
<ID>MaxLineLength:MeshService.kt$MeshService$"Neighbor info response filtered: ToUs=$isAddressedToUs, isRecentRequest=$isRecentRequest"</ID>
<ID>MaxLineLength:MeshService.kt$MeshService$"setOwner Id: $id longName: ${longName.anonymize} shortName: $shortName isLicensed: $isLicensed isUnmessagable: $isUnmessagable"</ID>
<ID>MaxLineLength:MeshService.kt$MeshService.&lt;no name provided&gt;$"sendData dest=${p.to}, id=${p.id} &lt;- ${bytes.size} bytes (connectionState=${connectionStateHolder.connectionState.value})"</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found fromNum: ${fromNumCharacteristic?.uuid}, ${fromNumCharacteristic?.instanceId}"</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found fromRadio: ${fromRadioCharacteristic?.uuid}, ${fromRadioCharacteristic?.instanceId}"</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found logRadio: ${logRadioCharacteristic?.uuid}, ${logRadioCharacteristic?.instanceId}"</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found toRadio: ${toRadioCharacteristic?.uuid}, ${toRadioCharacteristic?.instanceId}"</ID>
<ID>ModifierClickableOrder:Channel.kt$clickable(onClick = onClick)</ID>
<ID>ModifierMissing:BLEDevices.kt$BLEDevices</ID>
<ID>ModifierMissing:Channel.kt$ChannelScreen</ID>
<ID>ModifierMissing:Contacts.kt$ContactListView</ID>
<ID>ModifierMissing:Contacts.kt$ContactsScreen</ID>
<ID>ModifierMissing:Contacts.kt$SelectionToolbar</ID>
<ID>ModifierMissing:EmptyStateContent.kt$EmptyStateContent</ID>
<ID>ModifierMissing:Main.kt$MainScreen</ID>
<ID>ModifierMissing:NetworkDevices.kt$NetworkDevices</ID>
<ID>ModifierMissing:Share.kt$ShareScreen</ID>
<ID>MutableStateAutoboxing:Contacts.kt$mutableStateOf(2)</ID>
<ID>NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage)</ID>
<ID>NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>NewLineAtEndOfFile:BLEException.kt$com.geeksville.mesh.service.BLEException.kt</ID>
@@ -106,11 +88,6 @@
<ID>NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ }</ID>
<ID>NoSemicolons:DateUtils.kt$DateUtils$;</ID>
<ID>OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract</ID>
<ID>ParameterNaming:Contacts.kt$onDeleteSelected</ID>
<ID>ParameterNaming:Contacts.kt$onMuteSelected</ID>
<ID>ParameterNaming:UsbDevices.kt$onDeviceSelected</ID>
<ID>PreviewPublic:Channel.kt$ModemPresetInfoPreview</ID>
<ID>PreviewPublic:EmptyStateContent.kt$EmptyStateContentPreview</ID>
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
<ID>SwallowedException:Exceptions.kt$ex: Throwable</ID>
<ID>SwallowedException:NsdManager.kt$ex: IllegalArgumentException</ID>
@@ -138,6 +115,5 @@
<ID>TooManyFunctions:UIState.kt$UIViewModel : ViewModel</ID>
<ID>TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"</ID>
<ID>UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule</ID>
<ID>ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -40,6 +40,7 @@ import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.Logger
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.MainScreen
import dagger.hilt.android.AndroidEntryPoint
@@ -53,7 +54,6 @@ import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.intro.AppIntroductionScreen
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
@@ -118,27 +118,27 @@ class MainActivity : AppCompatActivity() {
when (appLinkAction) {
Intent.ACTION_VIEW -> {
appLinkData?.let {
Timber.d("App link data: $it")
Logger.d { "App link data: $it" }
if (it.path?.startsWith("/e/") == true || it.path?.startsWith("/E/") == true) {
Timber.d("App link data is a channel set")
Logger.d { "App link data is a channel set" }
model.requestChannelUrl(
url = it,
onFailure = { lifecycleScope.launch { showToast(Res.string.channel_invalid) } },
)
} else if (it.path?.startsWith("/v/") == true || it.path?.startsWith("/V/") == true) {
Timber.d("App link data is a shared contact")
Logger.d { "App link data is a shared contact" }
model.setSharedContactRequested(
url = it,
onFailure = { lifecycleScope.launch { showToast(Res.string.contact_invalid) } },
)
} else {
Timber.d("App link data is not a channel set")
Logger.d { "App link data is not a channel set" }
}
}
}
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
Timber.d("USB device attached")
Logger.d { "USB device attached" }
showSettingsPage()
}
@@ -152,7 +152,7 @@ class MainActivity : AppCompatActivity() {
}
else -> {
Timber.w("Unexpected action $appLinkAction")
Logger.w { "Unexpected action $appLinkAction" }
}
}
}

View File

@@ -23,6 +23,7 @@ import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.Logger
import com.geeksville.mesh.android.BindFailedException
import com.geeksville.mesh.android.ServiceClient
import com.geeksville.mesh.concurrent.SequentialJob
@@ -32,7 +33,6 @@ import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import javax.inject.Inject
/** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */
@@ -49,7 +49,7 @@ constructor(
private val lifecycleOwner: LifecycleOwner = context as LifecycleOwner
init {
Timber.d("Adding self as LifecycleObserver for $lifecycleOwner")
Logger.d { "Adding self as LifecycleObserver for $lifecycleOwner" }
lifecycleOwner.lifecycle.addObserver(this)
}
@@ -58,7 +58,7 @@ constructor(
override fun onConnected(service: IMeshService) {
serviceSetupJob.launch(lifecycleOwner.lifecycleScope) {
serviceRepository.setMeshService(service)
Timber.d("connected to mesh service, connectionState=${serviceRepository.connectionState.value}")
Logger.d { "connected to mesh service, connectionState=${serviceRepository.connectionState.value}" }
}
}
@@ -73,32 +73,32 @@ constructor(
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
Timber.d("Lifecycle: ON_START")
Logger.d { "Lifecycle: ON_START" }
try {
bindMeshService()
} catch (ex: BindFailedException) {
Timber.e("Bind of MeshService failed: ${ex.message}")
Logger.e { "Bind of MeshService failed: ${ex.message}" }
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
Timber.d("Lifecycle: ON_DESTROY")
Logger.d { "Lifecycle: ON_DESTROY" }
owner.lifecycle.removeObserver(this)
Timber.d("Removed self as LifecycleObserver to $lifecycleOwner")
Logger.d { "Removed self as LifecycleObserver to $lifecycleOwner" }
}
// endregion
@Suppress("TooGenericExceptionCaught")
private fun bindMeshService() {
Timber.d("Binding to mesh service!")
Logger.d { "Binding to mesh service!" }
try {
MeshService.startService(context)
} catch (ex: Exception) {
Timber.e("Failed to start service from activity - but ignoring because bind will work: ${ex.message}")
Logger.e { "Failed to start service from activity - but ignoring because bind will work: ${ex.message}" }
}
connect(context, MeshService.createIntent(context), BIND_AUTO_CREATE + BIND_ABOVE_CLIENT)

View File

@@ -18,6 +18,7 @@
package com.geeksville.mesh
import android.app.Application
import co.touchlab.kermit.Logger
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
@@ -28,7 +29,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.prefs.mesh.MeshPrefs
import timber.log.Timber
/**
* The main application class for Meshtastic.
@@ -61,7 +61,7 @@ interface AppEntryPoint {
fun logAssert(executeReliableWrite: Boolean) {
if (!executeReliableWrite) {
val ex = AssertionError("Assertion failed")
Timber.e(ex)
Logger.e(ex) { "logAssert" }
throw ex
}
}

View File

@@ -23,10 +23,9 @@ import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.IInterface
import co.touchlab.kermit.Logger
import com.geeksville.mesh.util.exceptionReporter
import timber.log.Timber
import java.io.Closeable
import java.lang.IllegalArgumentException
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@@ -73,14 +72,14 @@ open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T
// Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false.
// Try
// a short sleep to see if that helps
Timber.e("Needed to use the second bind attempt hack")
Logger.e { "Needed to use the second bind attempt hack" }
Thread.sleep(500) // was 200ms, but received an autobug from a Galaxy Note4, android 6.0.1
if (!c.bindService(intent, connection, flags)) {
throw BindFailedException()
}
}
} else {
Timber.w("Ignoring rebind attempt for service")
Logger.w { "Ignoring rebind attempt for service" }
}
}
@@ -90,7 +89,7 @@ open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T
context?.unbindService(connection)
} catch (ex: IllegalArgumentException) {
// Autobugs show this can generate an illegal arg exception for "service not registered" during reinstall?
Timber.w("Ignoring error in ServiceClient.close, probably harmless")
Logger.w { "Ignoring error in ServiceClient.close, probably harmless" }
}
serviceP = null
context = null
@@ -116,7 +115,7 @@ open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T
// If we start to close a service, it seems that there is a possibility a onServiceConnected event
// is the queue
// for us. Be careful not to process that stale event
Timber.w("A service connected while we were closing it, ignoring")
Logger.w { "A service connected while we were closing it, ignoring" }
}
}

View File

@@ -17,7 +17,7 @@
package com.geeksville.mesh.concurrent
import timber.log.Timber
import co.touchlab.kermit.Logger
/**
* Sometimes when starting services we face situations where messages come in that require computation but we can't do
@@ -36,7 +36,7 @@ class DeferredExecution {
// / run all work in the queue and clear it to be ready to accept new work
fun run() {
Timber.d("Running deferred execution numjobs=${queue.size}")
Logger.d { "Running deferred execution numjobs=${queue.size}" }
queue.forEach { it() }
queue.clear()
}

View File

@@ -27,7 +27,7 @@ interface Continuation<in T> {
fun resumeWithException(ex: Throwable) = try {
resume(Result.failure(ex))
} catch (ex: Throwable) {
// Timber.e("Ignoring $ex while resuming, because we are the ones who threw it")
// Logger.e { "Ignoring $ex while resuming because we are the ones who threw it" }
throw ex
}
}

View File

@@ -24,6 +24,7 @@ import android.os.RemoteException
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
@@ -49,7 +50,6 @@ import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.meshtastic
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import timber.log.Timber
import javax.inject.Inject
// ... (DeviceListEntry sealed class remains the same) ...
@@ -164,13 +164,13 @@ constructor(
init {
serviceRepository.statusMessage.onEach { errorText.value = it }.launchIn(viewModelScope)
Timber.d("BTScanModel created")
Logger.d { "BTScanModel created" }
}
override fun onCleared() {
super.onCleared()
bluetoothRepository.stopScan()
Timber.d("BTScanModel cleared")
Logger.d { "BTScanModel cleared" }
}
fun setErrorText(text: String) {
@@ -178,7 +178,7 @@ constructor(
}
fun stopScan() {
Timber.d("stopping scan")
Logger.d { "stopping scan" }
bluetoothRepository.stopScan()
}
@@ -187,7 +187,7 @@ constructor(
}
fun startScan() {
Timber.d("starting ble scan")
Logger.d { "starting ble scan" }
bluetoothRepository.startScan()
}
@@ -195,24 +195,24 @@ constructor(
try {
serviceRepository.meshService?.let { service -> MeshService.changeDeviceAddress(context, service, address) }
} catch (ex: RemoteException) {
Timber.e(ex, "changeDeviceSelection failed, probably it is shutting down")
Logger.e(ex) { "changeDeviceSelection failed, probably it is shutting down" }
}
}
/** Initiates the bonding process and connects to the device upon success. */
private fun requestBonding(entry: DeviceListEntry.Ble) {
Timber.i("Starting bonding for ${entry.peripheral.address.anonymize}")
Logger.i { "Starting bonding for ${entry.peripheral.address.anonymize}" }
viewModelScope.launch {
@Suppress("TooGenericExceptionCaught")
try {
bluetoothRepository.bond(entry.peripheral)
Timber.i("Bonding complete for ${entry.peripheral.address.anonymize}, selecting device...")
Logger.i { "Bonding complete for ${entry.peripheral.address.anonymize}, selecting device..." }
changeDeviceAddress(entry.fullAddress)
} catch (ex: SecurityException) {
Timber.e(ex, "Bonding failed for ${entry.peripheral.address.anonymize} Permissions not granted")
Logger.e(ex) { "Bonding failed for ${entry.peripheral.address.anonymize} Permissions not granted" }
serviceRepository.setErrorMessage("Bonding failed: ${ex.message} Permissions not granted")
} catch (ex: Exception) {
Timber.e(ex, "Bonding failed for ${entry.peripheral.address.anonymize}")
Logger.e(ex) { "Bonding failed for ${entry.peripheral.address.anonymize}" }
serviceRepository.setErrorMessage("Bonding failed: ${ex.message}")
}
}
@@ -223,10 +223,10 @@ constructor(
.requestPermission(it.driver.device)
.onEach { granted ->
if (granted) {
Timber.i("User approved USB access")
Logger.i { "User approved USB access" }
changeDeviceAddress(it.fullAddress)
} else {
Timber.e("USB permission denied for device ${it.address}")
Logger.e { "USB permission denied for device ${it.address}" }
}
}
.launchIn(viewModelScope)

View File

@@ -25,6 +25,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import co.touchlab.kermit.Logger
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -67,7 +68,6 @@ import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.AdminProtos
import org.meshtastic.proto.AppOnlyProtos
import org.meshtastic.proto.MeshProtos
import timber.log.Timber
import javax.inject.Inject
// Given a human name, strip out the first letter of the first three words and return that as the
@@ -212,7 +212,7 @@ constructor(
}
.launchIn(viewModelScope)
Timber.d("ViewModel created")
Logger.d { "ViewModel created" }
}
private val _sharedContactRequested: MutableStateFlow<AdminProtos.SharedContact?> = MutableStateFlow(null)
@@ -222,7 +222,7 @@ constructor(
fun setSharedContactRequested(url: Uri, onFailure: () -> Unit) {
runCatching { _sharedContactRequested.value = url.toSharedContact() }
.onFailure { ex ->
Timber.e(ex, "Shared contact error")
Logger.e(ex) { "Shared contact error" }
onFailure()
}
}
@@ -243,7 +243,7 @@ constructor(
fun requestChannelUrl(url: Uri, onFailure: () -> Unit) =
runCatching { _requestChannelSet.value = url.toChannelSet() }
.onFailure { ex ->
Timber.e(ex, "Channel url error")
Logger.e(ex) { "Channel url error" }
onFailure()
}
@@ -256,7 +256,7 @@ constructor(
override fun onCleared() {
super.onCleared()
Timber.d("ViewModel cleared")
Logger.d { "ViewModel cleared" }
}
val tracerouteResponse: LiveData<TracerouteResponse?>

View File

@@ -22,6 +22,7 @@ import android.app.Application
import android.bluetooth.BluetoothAdapter
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import co.touchlab.kermit.Logger
import com.geeksville.mesh.repository.radio.BleConstants.BLE_NAME_PATTERN
import com.geeksville.mesh.repository.radio.BleConstants.BTM_SERVICE_UUID
import com.geeksville.mesh.util.registerReceiverCompat
@@ -42,7 +43,6 @@ import no.nordicsemi.kotlin.ble.core.Manager
import org.meshtastic.core.common.hasBluetoothPermission
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.di.ProcessLifecycle
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.seconds
@@ -112,7 +112,7 @@ constructor(
.onStart { _isScanning.value = true }
.onCompletion { _isScanning.value = false }
.catch { ex ->
Timber.w(ex, "Bluetooth scan failed")
Logger.w(ex) { "Bluetooth scan failed" }
_isScanning.value = false
}
.collect { peripheral ->
@@ -158,7 +158,7 @@ constructor(
)
_state.emit(newState)
Timber.d("Detected our bluetooth access=$newState")
Logger.d { "Detected our bluetooth access=$newState" }
}
@SuppressLint("MissingPermission")

View File

@@ -17,6 +17,7 @@
package com.geeksville.mesh.repository.network
import co.touchlab.kermit.Logger
import com.geeksville.mesh.util.ignoreException
import com.google.protobuf.ByteString
import kotlinx.coroutines.channels.awaitClose
@@ -36,7 +37,6 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.util.subscribeList
import org.meshtastic.proto.MeshProtos.MqttClientProxyMessage
import org.meshtastic.proto.mqttClientProxyMessage
import timber.log.Timber
import java.net.URI
import java.security.SecureRandom
import javax.inject.Inject
@@ -70,7 +70,7 @@ constructor(
private var mqttClient: MqttAsyncClient? = null
fun disconnect() {
Timber.i("MQTT Disconnected")
Logger.i { "MQTT Disconnected" }
mqttClient?.apply {
ignoreException { disconnect() }
close(true)
@@ -110,7 +110,7 @@ constructor(
val callback =
object : MqttCallbackExtended {
override fun connectComplete(reconnect: Boolean, serverURI: String) {
Timber.i("MQTT connectComplete: $serverURI reconnect: $reconnect")
Logger.i { "MQTT connectComplete: $serverURI reconnect: $reconnect" }
channelSet.subscribeList
.ifEmpty {
return
@@ -123,7 +123,7 @@ constructor(
}
override fun connectionLost(cause: Throwable) {
Timber.i("MQTT connectionLost cause: $cause")
Logger.i { "MQTT connectionLost cause: $cause" }
if (cause is IllegalArgumentException) close(cause)
}
@@ -138,7 +138,7 @@ constructor(
}
override fun deliveryComplete(token: IMqttDeliveryToken?) {
Timber.i("MQTT deliveryComplete messageId: ${token?.messageId}")
Logger.i { "MQTT deliveryComplete messageId: ${token?.messageId}" }
}
}
@@ -161,15 +161,15 @@ constructor(
private fun subscribe(topic: String) {
mqttClient?.subscribe(topic, DEFAULT_QOS)
Timber.i("MQTT Subscribed to topic: $topic")
Logger.i { "MQTT Subscribed to topic: $topic" }
}
fun publish(topic: String, data: ByteArray, retained: Boolean) {
try {
val token = mqttClient?.publish(topic, data, DEFAULT_QOS, retained)
Timber.i("MQTT Publish messageId: ${token?.messageId}")
Logger.i { "MQTT Publish messageId: ${token?.messageId}" }
} catch (ex: Exception) {
Timber.e("MQTT Publish error: ${ex.message}")
Logger.e { "MQTT Publish error: ${ex.message}" }
}
}
}

View File

@@ -21,6 +21,7 @@ import android.annotation.SuppressLint
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asExecutor
@@ -30,7 +31,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.coroutines.resume
@@ -54,22 +54,22 @@ private fun NsdManager.discoverServices(
}
override fun onDiscoveryStarted(serviceType: String) {
Timber.d("NSD Service discovery started")
Logger.d { "NSD Service discovery started" }
}
override fun onDiscoveryStopped(serviceType: String) {
Timber.d("NSD Service discovery stopped")
Logger.d { "NSD Service discovery stopped" }
close()
}
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
Timber.d("NSD Service found: $serviceInfo")
Logger.d { "NSD Service found: $serviceInfo" }
serviceList += serviceInfo
trySend(serviceList)
}
override fun onServiceLost(serviceInfo: NsdServiceInfo) {
Timber.d("NSD Service lost: $serviceInfo")
Logger.d { "NSD Service lost: $serviceInfo" }
serviceList.removeAll { it.serviceName == serviceInfo.serviceName }
trySend(serviceList)
}
@@ -102,7 +102,7 @@ private suspend fun NsdManager.resolveService(serviceInfo: NsdServiceInfo): NsdS
try {
unregisterServiceInfoCallback(this)
} catch (e: IllegalArgumentException) {
Timber.w(e, "Already unregistered")
Logger.w(e) { "Already unregistered" }
}
}
}
@@ -112,7 +112,7 @@ private suspend fun NsdManager.resolveService(serviceInfo: NsdServiceInfo): NsdS
try {
unregisterServiceInfoCallback(this)
} catch (e: IllegalArgumentException) {
Timber.w(e, "Already unregistered")
Logger.w(e) { "Already unregistered" }
}
}
@@ -125,7 +125,7 @@ private suspend fun NsdManager.resolveService(serviceInfo: NsdServiceInfo): NsdS
try {
unregisterServiceInfoCallback(callback)
} catch (e: IllegalArgumentException) {
Timber.w(e, "Already unregistered")
Logger.w(e) { "Already unregistered" }
}
}
} else {

View File

@@ -17,6 +17,7 @@
package com.geeksville.mesh.repository.radio
import co.touchlab.kermit.Logger
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.model.getInitials
import com.google.protobuf.ByteString
@@ -38,7 +39,6 @@ import org.meshtastic.proto.config
import org.meshtastic.proto.deviceMetadata
import org.meshtastic.proto.fromRadio
import org.meshtastic.proto.queueStatus
import timber.log.Timber
import kotlin.random.Random
private val defaultLoRaConfig =
@@ -71,7 +71,7 @@ constructor(
private val packetIdSequence = generateSequence { currentPacketId++ }.iterator()
init {
Timber.i("Starting the mock interface")
Logger.i { "Starting the mock interface" }
service.onConnect() // Tell clients they can use the API
}
@@ -86,7 +86,7 @@ constructor(
data != null && data.portnum == Portnums.PortNum.ADMIN_APP ->
handleAdminPacket(pr, AdminProtos.AdminMessage.parseFrom(data.payload))
pr.hasPacket() && pr.packet.wantAck -> sendFakeAck(pr)
else -> Timber.i("Ignoring data sent to mock interface $pr")
else -> Logger.i { "Ignoring data sent to mock interface $pr" }
}
}
@@ -108,12 +108,12 @@ constructor(
}
}
else -> Timber.i("Ignoring admin sent to mock interface $d")
else -> Logger.i { "Ignoring admin sent to mock interface $d" }
}
}
override fun close() {
Timber.i("Closing the mock interface")
Logger.i { "Closing the mock interface" }
}
// / Generate a fake text message from a node
@@ -297,7 +297,7 @@ constructor(
}
private fun sendConfigResponse(configId: Int) {
Timber.d("Sending mock config response")
Logger.d { "Sending mock config response" }
// / Generate a fake node info entry
@Suppress("MagicNumber")

View File

@@ -18,6 +18,7 @@
package com.geeksville.mesh.repository.radio
import android.annotation.SuppressLint
import co.touchlab.kermit.Logger
import com.geeksville.mesh.repository.radio.BleConstants.BTM_FROMNUM_CHARACTER
import com.geeksville.mesh.repository.radio.BleConstants.BTM_FROMRADIO_CHARACTER
import com.geeksville.mesh.repository.radio.BleConstants.BTM_LOGRADIO_CHARACTER
@@ -52,7 +53,6 @@ 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
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.toKotlinUuid
@@ -79,12 +79,12 @@ constructor(
) : IRadioInterface {
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Timber.e(throwable, "[$address] Uncaught exception in connectionScope")
Logger.e(throwable) { "[$address] Uncaught exception in connectionScope" }
serviceScope.launch {
try {
peripheral?.disconnect()
} catch (e: Exception) {
Timber.e(e, "[$address] Failed to disconnect in exception handler")
Logger.e(e) { "[$address] Failed to disconnect in exception handler" }
}
}
service.onDisconnect(BleError.from(throwable))
@@ -118,7 +118,7 @@ constructor(
val packet =
fromRadioCharacteristic?.read()?.takeIf { it.isNotEmpty() }
?: run {
Timber.d("[$address] fromRadio queue drain complete (read empty/null)")
Logger.d { "[$address] fromRadio queue drain complete (read empty/null)" }
break
}
send(packet)
@@ -128,14 +128,14 @@ constructor(
private fun dispatchPacket(packet: ByteArray) {
packetsReceived++
bytesReceived += packet.size
Timber.d(
Logger.d {
"[$address] Dispatching packet to service.handleFromRadio() - " +
"Packet #$packetsReceived, ${packet.size} bytes (Total: $bytesReceived bytes)",
)
"Packet #$packetsReceived, ${packet.size} bytes (Total: $bytesReceived bytes)"
}
try {
service.handleFromRadio(p = packet)
} catch (t: Throwable) {
Timber.e(t, "[$address] Failed to execute service.handleFromRadio()")
Logger.e(t) { "[$address] Failed to execute service.handleFromRadio()" }
}
}
@@ -145,13 +145,13 @@ constructor(
fromRadioPacketFlow()
.onEach { packet ->
drainedCount++
Timber.d("[$address] Read packet from queue (${packet.size} bytes)")
Logger.d { "[$address] Read packet from queue (${packet.size} bytes)" }
dispatchPacket(packet)
}
.catch { ex -> Timber.w(ex, "[$address] Exception while draining packet queue") }
.catch { ex -> Logger.w(ex) { "[$address] Exception while draining packet queue" } }
.onCompletion {
if (drainedCount > 0) {
Timber.d("[$address] Drained $drainedCount packets from packet queue")
Logger.d { "[$address] Drained $drainedCount packets from packet queue" }
}
}
.collect()
@@ -168,19 +168,19 @@ constructor(
connectionScope.launch {
try {
connectionStartTime = System.currentTimeMillis()
Timber.i("[$address] BLE connection attempt started at $connectionStartTime")
Logger.i { "[$address] BLE connection attempt started at $connectionStartTime" }
peripheral = retryCall { findAndConnectPeripheral() }
peripheral?.let {
val connectionTime = System.currentTimeMillis() - connectionStartTime
Timber.i("[$address] BLE peripheral connected in ${connectionTime}ms")
Logger.i { "[$address] BLE peripheral connected in ${connectionTime}ms" }
onConnected()
observePeripheralChanges()
discoverServicesAndSetupCharacteristics(it)
}
} catch (e: Exception) {
val failureTime = System.currentTimeMillis() - connectionStartTime
Timber.e(e, "[$address] Failed to connect to peripheral after ${failureTime}ms")
Logger.e(e) { "[$address] Failed to connect to peripheral after ${failureTime}ms" }
service.onDisconnect(BleError.from(e))
}
}
@@ -200,27 +200,27 @@ constructor(
try {
peripheral?.let { p ->
val rssi = retryCall { p.readRssi() }
Timber.d("[$address] Connection established. RSSI: $rssi dBm")
Logger.d { "[$address] Connection established. RSSI: $rssi dBm" }
val phyInUse = retryCall { p.readPhy() }
Timber.d("[$address] PHY in use: $phyInUse")
Logger.d { "[$address] PHY in use: $phyInUse" }
}
} catch (e: Exception) {
Timber.w(e, "[$address] Failed to read initial connection properties")
Logger.w(e) { "[$address] Failed to read initial connection properties" }
}
}
private fun observePeripheralChanges() {
peripheral?.let { p ->
p.phy.onEach { phy -> Timber.i("[$address] BLE PHY changed to $phy") }.launchIn(connectionScope)
p.phy.onEach { phy -> Logger.i { "[$address] BLE PHY changed to $phy" } }.launchIn(connectionScope)
p.connectionParameters
.onEach { params -> Timber.i("[$address] BLE connection parameters changed to $params") }
.onEach { params -> Logger.i { "[$address] BLE connection parameters changed to $params" } }
.launchIn(connectionScope)
p.state
.onEach { state ->
Timber.i("[$address] BLE connection state changed to $state")
Logger.i { "[$address] BLE connection state changed to $state" }
if (state is ConnectionState.Disconnected) {
val uptime =
if (connectionStartTime > 0) {
@@ -228,19 +228,19 @@ constructor(
} else {
0
}
Timber.w(
Logger.w {
"[$address] BLE disconnected - Reason: ${state.reason}, " +
"Uptime: ${uptime}ms, " +
"Packets RX: $packetsReceived ($bytesReceived bytes), " +
"Packets TX: $packetsSent ($bytesSent bytes)",
)
"Packets TX: $packetsSent ($bytesSent bytes)"
}
service.onDisconnect(BleError.Disconnected(reason = state.reason))
}
}
.launchIn(connectionScope)
}
centralManager.state
.onEach { state -> Timber.i("[$address] CentralManager state changed to $state") }
.onEach { state -> Logger.i { "[$address] CentralManager state changed to $state" } }
.launchIn(connectionScope)
}
@@ -268,35 +268,35 @@ constructor(
it != null
}
) {
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}",
)
Logger.d {
"[$address] Found toRadio: ${toRadioCharacteristic?.uuid}, ${toRadioCharacteristic?.instanceId}"
}
Logger.d {
"[$address] Found fromNum: ${fromNumCharacteristic?.uuid}, ${fromNumCharacteristic?.instanceId}"
}
Logger.d {
"[$address] Found fromRadio: ${fromRadioCharacteristic?.uuid}, ${fromRadioCharacteristic?.instanceId}"
}
Logger.d {
"[$address] Found logRadio: ${logRadioCharacteristic?.uuid}, ${logRadioCharacteristic?.instanceId}"
}
setupNotifications()
service.onConnect()
} else {
Timber.w("[$address] Discovery failed: missing required characteristics")
Logger.w { "[$address] Discovery failed: missing required characteristics" }
service.onDisconnect(BleError.DiscoveryFailed("One or more characteristics not found"))
}
} else {
Timber.w("[$address] Discovery failed: Meshtastic service not found")
Logger.w { "[$address] Discovery failed: Meshtastic service not found" }
service.onDisconnect(BleError.DiscoveryFailed("Meshtastic service not found"))
}
}
.catch { e ->
Timber.e(e, "[$address] Service discovery failed")
Logger.e(e) { "[$address] Service discovery failed" }
try {
peripheral.disconnect()
} catch (e2: Exception) {
Timber.e(e2, "[$address] Failed to disconnect in discovery catch")
Logger.e(e2) { "[$address] Failed to disconnect in discovery catch" }
}
service.onDisconnect(BleError.from(e))
}
@@ -309,29 +309,29 @@ constructor(
@OptIn(ExperimentalUuidApi::class)
private suspend fun setupNotifications() {
retryCall { fromNumCharacteristic?.subscribe() }
?.onStart { Timber.d("[$address] Subscribing to fromNumCharacteristic") }
?.onStart { Logger.d { "[$address] Subscribing to fromNumCharacteristic" } }
?.onEach { notifyBytes ->
Timber.d("[$address] FromNum Notification (${notifyBytes.size} bytes), draining queue")
Logger.d { "[$address] FromNum Notification (${notifyBytes.size} bytes), draining queue" }
connectionScope.launch { drainPacketQueueAndDispatch() }
}
?.catch { e ->
Timber.e(e, "[$address] Error subscribing to fromNumCharacteristic")
Logger.e(e) { "[$address] Error subscribing to fromNumCharacteristic" }
service.onDisconnect(BleError.from(e))
}
?.onCompletion { cause -> Timber.d("[$address] fromNum sub flow completed, cause=$cause") }
?.onCompletion { cause -> Logger.d { "[$address] fromNum sub flow completed, cause=$cause" } }
?.launchIn(scope = connectionScope)
retryCall { logRadioCharacteristic?.subscribe() }
?.onStart { Timber.d("[$address] Subscribing to logRadioCharacteristic") }
?.onStart { Logger.d { "[$address] Subscribing to logRadioCharacteristic" } }
?.onEach { notifyBytes ->
Timber.d("[$address] LogRadio Notification (${notifyBytes.size} bytes), dispatching packet")
Logger.d { "[$address] LogRadio Notification (${notifyBytes.size} bytes), dispatching packet" }
dispatchPacket(notifyBytes)
}
?.catch { e ->
Timber.e(e, "[$address] Error subscribing to logRadioCharacteristic")
Logger.e(e) { "[$address] Error subscribing to logRadioCharacteristic" }
service.onDisconnect(BleError.from(e))
}
?.onCompletion { cause -> Timber.d("[$address] logRadio sub flow completed, cause=$cause") }
?.onCompletion { cause -> Logger.d { "[$address] logRadio sub flow completed, cause=$cause" } }
?.launchIn(scope = connectionScope)
}
@@ -345,14 +345,13 @@ constructor(
} catch (e: Exception) {
currentAttempt++
if (currentAttempt >= RETRY_COUNT) {
Timber.e(e, "[$address] BLE operation failed after $RETRY_COUNT attempts, giving up")
Logger.e(e) { "[$address] BLE operation failed after $RETRY_COUNT attempts, giving up" }
throw e
}
Timber.w(
e,
Logger.w(e) {
"[$address] BLE operation failed (attempt $currentAttempt/$RETRY_COUNT), " +
"retrying in ${RETRY_DELAY_MS}ms...",
)
"retrying in ${RETRY_DELAY_MS}ms..."
}
delay(RETRY_DELAY_MS)
}
}
@@ -368,7 +367,7 @@ constructor(
override fun handleSendToRadio(p: ByteArray) {
toRadioCharacteristic?.let { characteristic ->
if (peripheral == null) {
Timber.w("[$address] BLE peripheral is null, cannot send packet")
Logger.w { "[$address] BLE peripheral is null, cannot send packet" }
return@let
}
connectionScope.launch {
@@ -383,24 +382,23 @@ constructor(
retryCall {
packetsSent++
bytesSent += p.size
Timber.d(
Logger.d {
"[$address] Writing packet #$packetsSent to toRadioCharacteristic with $writeType - " +
"${p.size} bytes (Total TX: $bytesSent bytes)",
)
"${p.size} bytes (Total TX: $bytesSent bytes)"
}
characteristic.write(p, writeType = writeType)
}
drainPacketQueueAndDispatch()
} catch (e: Exception) {
Timber.e(
e,
Logger.e(e) {
"[$address] Failed to write packet to toRadioCharacteristic after " +
"$packetsSent successful writes",
)
"$packetsSent successful writes"
}
service.onDisconnect(BleError.from(e))
}
}
}
} ?: Timber.w("[$address] toRadio characteristic unavailable, can't send data")
} ?: Logger.w { "[$address] toRadio characteristic unavailable, can't send data" }
}
/** Closes the connection to the device. */
@@ -412,12 +410,12 @@ constructor(
} else {
0
}
Timber.i(
Logger.i {
"[$address] BLE close() called - " +
"Uptime: ${uptime}ms, " +
"Packets RX: $packetsReceived ($bytesReceived bytes), " +
"Packets TX: $packetsSent ($bytesSent bytes)",
)
"Packets TX: $packetsSent ($bytesSent bytes)"
}
connectionScope.cancel()
peripheral?.disconnect()
service.onDisconnect(true)

View File

@@ -17,9 +17,9 @@
package com.geeksville.mesh.repository.radio
import co.touchlab.kermit.Logger
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import org.meshtastic.core.model.util.anonymize
import timber.log.Timber
import javax.inject.Inject
/** Bluetooth backend implementation. */
@@ -35,7 +35,7 @@ constructor(
override fun addressValid(rest: String): Boolean {
val allPaired = bluetoothRepository.state.value.bondedDevices.map { it.address }.toSet()
return if (!allPaired.contains(rest)) {
Timber.w("Ignoring stale bond to ${rest.anonymize}")
Logger.w { "Ignoring stale bond to ${rest.anonymize}" }
false
} else {
true

View File

@@ -21,6 +21,7 @@ import android.app.Application
import android.provider.Settings
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import co.touchlab.kermit.Logger
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.android.BinaryLogFile
import com.geeksville.mesh.android.BuildUtils
@@ -50,7 +51,6 @@ import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.prefs.radio.RadioPrefs
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.proto.MeshProtos
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -141,7 +141,7 @@ constructor(
fun keepAlive(now: Long = System.currentTimeMillis()) {
if (now - lastHeartbeatMillis > HEARTBEAT_INTERVAL_MILLIS) {
if (radioIf is SerialInterface) {
Timber.i("Sending ToRadio heartbeat")
Logger.i { "Sending ToRadio heartbeat" }
val heartbeat =
MeshProtos.ToRadio.newBuilder().setHeartbeat(MeshProtos.Heartbeat.getDefaultInstance()).build()
handleSendToRadio(heartbeat.toByteArray())
@@ -206,7 +206,7 @@ constructor(
}
private fun broadcastConnectionChanged(newState: ConnectionState) {
Timber.d("Broadcasting connection state change to $newState")
Logger.d { "Broadcasting connection state change to $newState" }
processLifecycle.coroutineScope.launch(dispatchers.default) { _connectionState.emit(newState) }
}
@@ -223,7 +223,7 @@ constructor(
receivedPacketsLog.write(p)
receivedPacketsLog.flush()
} catch (t: Throwable) {
Timber.w(t, "Failed to write receive log in handleFromRadio")
Logger.w(t) { "Failed to write receive log in handleFromRadio" }
}
}
@@ -231,13 +231,13 @@ constructor(
keepAlive(System.currentTimeMillis())
}
// ignoreException { Timber.d("FromRadio: ${MeshProtos.FromRadio.parseFrom(p)}") }
// ignoreException { Logger.d { "FromRadio: ${MeshProtos.FromRadio.parseFrom(p }}" } }
try {
processLifecycle.coroutineScope.launch(dispatchers.io) { _receivedData.emit(p) }
emitReceiveActivity()
} catch (t: Throwable) {
Timber.e(t, "RadioInterfaceService.handleFromRadio failed while emitting data")
Logger.e(t) { "RadioInterfaceService.handleFromRadio failed while emitting data" }
}
}
@@ -262,13 +262,13 @@ constructor(
/** Start our configured interface (if it isn't already running) */
private fun startInterface() {
if (radioIf !is NopInterface) {
Timber.w("Can't start interface - $radioIf is already running")
Logger.w { "Can't start interface - $radioIf is already running" }
} else {
val address = getBondedDeviceAddress()
if (address == null) {
Timber.w("No bonded mesh radio, can't start interface")
Logger.w { "No bonded mesh radio, can't start interface" }
} else {
Timber.i("Starting radio ${address.anonymize}")
Logger.i { "Starting radio ${address.anonymize}" }
isStarted = true
if (logSends) {
@@ -285,7 +285,7 @@ constructor(
private fun stopInterface() {
val r = radioIf
Timber.i("stopping interface $r")
Logger.i { "stopping interface $r" }
isStarted = false
radioIf = interfaceFactory.nopInterface
r.close()
@@ -314,7 +314,7 @@ constructor(
*/
private fun setBondedDeviceAddress(address: String?): Boolean =
if (getBondedDeviceAddress() == address && isStarted && _connectionState.value == ConnectionState.Connected) {
Timber.w("Ignoring setBondedDevice ${address.anonymize}, because we are already using that device")
Logger.w { "Ignoring setBondedDevice ${address.anonymize}, because we are already using that device" }
false
} else {
// Record that this use has configured a new radio
@@ -325,7 +325,7 @@ constructor(
// The device address "n" can be used to mean none
Timber.d("Setting bonded device to ${address.anonymize}")
Logger.d { "Setting bonded device to ${address.anonymize}" }
// Stores the address if non-null, otherwise removes the pref
radioPrefs.devAddr = address
@@ -366,14 +366,14 @@ constructor(
// Use tryEmit for SharedFlow as it's non-blocking
val emitted = _meshActivity.tryEmit(MeshActivity.Send)
if (!emitted) {
Timber.d("MeshActivity.Send event was not emitted due to buffer overflow or no collectors")
Logger.d { "MeshActivity.Send event was not emitted due to buffer overflow or no collectors" }
}
}
private fun emitReceiveActivity() {
val emitted = _meshActivity.tryEmit(MeshActivity.Receive)
if (!emitted) {
Timber.d("MeshActivity.Receive event was not emitted due to buffer overflow or no collectors")
Logger.d { "MeshActivity.Receive event was not emitted due to buffer overflow or no collectors" }
}
}
}

View File

@@ -17,12 +17,12 @@
package com.geeksville.mesh.repository.radio
import co.touchlab.kermit.Logger
import com.geeksville.mesh.repository.usb.SerialConnection
import com.geeksville.mesh.repository.usb.SerialConnectionListener
import com.geeksville.mesh.repository.usb.UsbRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference
/** An interface that assumes we are talking to a meshtastic device via USB serial */
@@ -48,10 +48,10 @@ constructor(
override fun connect() {
val device = serialInterfaceSpec.findSerial(address)
if (device == null) {
Timber.e("[$address] Serial device not found at address")
Logger.e { "[$address] Serial device not found at address" }
} else {
val connectStart = System.currentTimeMillis()
Timber.i("[$address] Opening serial device: $device")
Logger.i { "[$address] Opening serial device: $device" }
var packetsReceived = 0
var bytesReceived = 0L
@@ -60,7 +60,7 @@ constructor(
val onConnect: () -> Unit = {
connectionStartTime = System.currentTimeMillis()
val connectionTime = connectionStartTime - connectStart
Timber.i("[$address] Serial device connected in ${connectionTime}ms")
Logger.i { "[$address] Serial device connected in ${connectionTime}ms" }
super.connect()
}
@@ -69,9 +69,9 @@ constructor(
device,
object : SerialConnectionListener {
override fun onMissingPermission() {
Timber.e(
"[$address] Serial connection failed - missing USB permissions for device: $device",
)
Logger.e {
"[$address] Serial connection failed - missing USB permissions for device: $device"
}
}
override fun onConnected() {
@@ -81,10 +81,10 @@ constructor(
override fun onDataReceived(bytes: ByteArray) {
packetsReceived++
bytesReceived += bytes.size
Timber.d(
Logger.d {
"[$address] Serial received packet #$packetsReceived - " +
"${bytes.size} byte(s) (Total RX: $bytesReceived bytes)",
)
"${bytes.size} byte(s) (Total RX: $bytesReceived bytes)"
}
bytes.forEach(::readChar)
}
@@ -95,13 +95,15 @@ constructor(
} else {
0
}
thrown?.let { e -> Timber.e(e, "[$address] Serial error after ${uptime}ms: ${e.message}") }
Timber.w(
thrown?.let { e ->
Logger.e(e) { "[$address] Serial error after ${uptime}ms: ${e.message}" }
}
Logger.w {
"[$address] Serial device disconnected - " +
"Device: $device, " +
"Uptime: ${uptime}ms, " +
"Packets RX: $packetsReceived ($bytesReceived bytes)",
)
"Packets RX: $packetsReceived ($bytesReceived bytes)"
}
onDeviceDisconnect(false)
}
},
@@ -116,10 +118,10 @@ constructor(
override fun sendBytes(p: ByteArray) {
val conn = connRef.get()
if (conn != null) {
Timber.d("[$address] Serial sending ${p.size} bytes")
Logger.d { "[$address] Serial sending ${p.size} bytes" }
conn.sendBytes(p)
} else {
Timber.w("[$address] Serial connection not available, cannot send ${p.size} bytes")
Logger.w { "[$address] Serial connection not available, cannot send ${p.size} bytes" }
}
}
}

View File

@@ -17,7 +17,7 @@
package com.geeksville.mesh.repository.radio
import timber.log.Timber
import co.touchlab.kermit.Logger
/**
* An interface that assumes we are talking to a meshtastic device over some sort of stream connection (serial or TCP
@@ -41,7 +41,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) : I
private var packetLen = 0
override fun close() {
Timber.d("Closing stream for good")
Logger.d { "Closing stream for good" }
onDeviceDisconnect(true)
}
@@ -90,7 +90,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) : I
when (val c = b.toInt().toChar()) {
'\r' -> {} // ignore
'\n' -> {
Timber.d("DeviceLog: $debugLineBuf")
Logger.d { "DeviceLog: $debugLineBuf" }
debugLineBuf.clear()
}
else -> debugLineBuf.append(c)
@@ -104,7 +104,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) : I
var nextPtr = ptr + 1
fun lostSync() {
Timber.e("Lost protocol sync")
Logger.e { "Lost protocol sync" }
nextPtr = 0
}

View File

@@ -17,6 +17,7 @@
package com.geeksville.mesh.repository.radio
import co.touchlab.kermit.Logger
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.util.Exceptions
@@ -25,7 +26,6 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.IOException
@@ -67,12 +67,12 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
override fun sendBytes(p: ByteArray) {
packetsSent++
bytesSent += p.size
Timber.d("[$address] TCP sending packet #$packetsSent - ${p.size} bytes (Total TX: $bytesSent bytes)")
Logger.d { "[$address] TCP sending packet #$packetsSent - ${p.size} bytes (Total TX: $bytesSent bytes)" }
outStream.write(p)
}
override fun flushBytes() {
Timber.d("[$address] TCP flushing output stream")
Logger.d { "[$address] TCP flushing output stream" }
outStream.flush()
}
@@ -85,13 +85,13 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
} else {
0
}
Timber.w(
Logger.w {
"[$address] TCP disconnecting - " +
"Uptime: ${uptime}ms, " +
"Packets RX: $packetsReceived ($bytesReceived bytes), " +
"Packets TX: $packetsSent ($bytesSent bytes), " +
"Timeout events: $timeoutEvents",
)
"Timeout events: $timeoutEvents"
}
s.close()
socket = null
}
@@ -110,7 +110,7 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
} else {
0
}
Timber.e(ex, "[$address] TCP IOException after ${uptime}ms - ${ex.message}")
Logger.e(ex) { "[$address] TCP IOException after ${uptime}ms - ${ex.message}" }
onDeviceDisconnect(false)
} catch (ex: Throwable) {
val uptime =
@@ -119,38 +119,38 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
} else {
0
}
Timber.e(ex, "[$address] TCP exception after ${uptime}ms - ${ex.message}")
Logger.e(ex) { "[$address] TCP exception after ${uptime}ms - ${ex.message}" }
Exceptions.report(ex, "Exception in TCP reader")
onDeviceDisconnect(false)
}
if (retryCount > MAX_RETRIES_ALLOWED) {
Timber.e("[$address] TCP max retries ($MAX_RETRIES_ALLOWED) exceeded, giving up")
Logger.e { "[$address] TCP max retries ($MAX_RETRIES_ALLOWED) exceeded, giving up" }
break
}
Timber.i(
Logger.i {
"[$address] TCP reconnect attempt #$retryCount in ${backoffDelay / 1000}s " +
"(backoff: ${backoffDelay}ms)",
)
"(backoff: ${backoffDelay}ms)"
}
delay(backoffDelay)
retryCount++
backoffDelay = minOf(backoffDelay * 2, MAX_BACKOFF_MILLIS)
}
Timber.i("[$address] TCP reader exiting")
Logger.i { "[$address] TCP reader exiting" }
}
}
// Create a socket to make the connection with the server
private suspend fun startConnect() = withContext(Dispatchers.IO) {
val attemptStart = System.currentTimeMillis()
Timber.i("[$address] TCP connection attempt starting...")
Logger.i { "[$address] TCP connection attempt starting..." }
val (host, port) =
address.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: SERVICE_PORT) }
Timber.d("[$address] Resolving host '$host' and connecting to port $port...")
Logger.d { "[$address] Resolving host '$host' and connecting to port $port..." }
Socket(InetAddress.getByName(host), port).use { socket ->
socket.tcpNoDelay = true
@@ -159,10 +159,10 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
val connectTime = System.currentTimeMillis() - attemptStart
connectionStartTime = System.currentTimeMillis()
Timber.i(
Logger.i {
"[$address] TCP socket connected in ${connectTime}ms - " +
"Local: ${socket.localSocketAddress}, Remote: ${socket.remoteSocketAddress}",
)
"Local: ${socket.localSocketAddress}, Remote: ${socket.remoteSocketAddress}"
}
BufferedOutputStream(socket.getOutputStream()).use { outputStream ->
outStream = outputStream
@@ -178,7 +178,9 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
try { // close after 90s of inactivity
val c = inputStream.read()
if (c == -1) {
Timber.w("[$address] TCP got EOF on stream after $packetsReceived packets received")
Logger.w {
"[$address] TCP got EOF on stream after $packetsReceived packets received"
}
break
} else {
timeoutCount = 0
@@ -190,20 +192,20 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
timeoutCount++
timeoutEvents++
if (timeoutCount % TIMEOUT_LOG_INTERVAL == 0) {
Timber.d(
Logger.d {
"[$address] TCP socket timeout count: $timeoutCount/$SOCKET_RETRIES " +
"(total timeouts: $timeoutEvents)",
)
"(total timeouts: $timeoutEvents)"
}
}
// Ignore and start another read
}
}
if (timeoutCount >= SOCKET_RETRIES) {
val inactivityMs = SOCKET_RETRIES * SOCKET_TIMEOUT
Timber.w(
Logger.w {
"[$address] TCP closing connection due to $SOCKET_RETRIES consecutive timeouts " +
"(${inactivityMs}ms of inactivity)",
)
"(${inactivityMs}ms of inactivity)"
}
}
}
}

View File

@@ -18,11 +18,11 @@
package com.geeksville.mesh.repository.usb
import android.hardware.usb.UsbManager
import co.touchlab.kermit.Logger
import com.geeksville.mesh.util.ignoreException
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.util.SerialInputOutputManager
import timber.log.Timber
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
@@ -40,7 +40,7 @@ internal class SerialConnectionImpl(
override fun sendBytes(bytes: ByteArray) {
ioRef.get()?.let {
Timber.d("writing ${bytes.size} byte(s)")
Logger.d { "writing ${bytes.size} byte(s }" }
it.writeAsync(bytes)
}
}
@@ -54,7 +54,7 @@ internal class SerialConnectionImpl(
// Allow a short amount of time for the manager to quit (so the port can be cleanly closed)
if (waitForStopped) {
Timber.d("Waiting for USB manager to stop...")
Logger.d { "Waiting for USB manager to stop..." }
closedLatch.await(1, TimeUnit.SECONDS)
}
}
@@ -80,7 +80,7 @@ internal class SerialConnectionImpl(
port.dtr = true
port.rts = true
Timber.d("Starting serial reader thread")
Logger.d { "Starting serial reader thread" }
val io =
SerialInputOutputManager(
port,

View File

@@ -23,9 +23,9 @@ import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import co.touchlab.kermit.Logger
import com.geeksville.mesh.util.exceptionReporter
import com.geeksville.mesh.util.getParcelableExtraCompat
import timber.log.Timber
import javax.inject.Inject
/** A helper class to call onChanged when bluetooth is enabled or disabled or when permissions are changed. */
@@ -44,15 +44,15 @@ class UsbBroadcastReceiver @Inject constructor(private val usbRepository: UsbRep
when (intent.action) {
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
Timber.d("USB device '$deviceName' was detached")
Logger.d { "USB device '$deviceName' was detached" }
usbRepository.refreshState()
}
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
Timber.d("USB device '$deviceName' was attached")
Logger.d { "USB device '$deviceName' was attached" }
usbRepository.refreshState()
}
UsbManager.EXTRA_PERMISSION_GRANTED -> {
Timber.d("USB device '$deviceName' was granted permission")
Logger.d { "USB device '$deviceName' was granted permission" }
usbRepository.refreshState()
}
}

View File

@@ -30,6 +30,7 @@ import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.core.app.ServiceCompat
import androidx.core.location.LocationCompat
import co.touchlab.kermit.Logger
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.model.NO_DEVICE_SELECTED
@@ -127,7 +128,6 @@ import org.meshtastic.proto.fromRadio
import org.meshtastic.proto.position
import org.meshtastic.proto.telemetry
import org.meshtastic.proto.user
import timber.log.Timber
import java.util.ArrayDeque
import java.util.Locale
import java.util.UUID
@@ -280,12 +280,20 @@ class MeshService : Service() {
crossinline message: () -> String,
) {
if (!BuildConfig.DEBUG) return
val timber = Timber.tag(HISTORY_TAG)
val severity =
when (priority) {
Log.VERBOSE -> co.touchlab.kermit.Severity.Verbose
Log.DEBUG -> co.touchlab.kermit.Severity.Debug
Log.INFO -> co.touchlab.kermit.Severity.Info
Log.WARN -> co.touchlab.kermit.Severity.Warn
Log.ERROR -> co.touchlab.kermit.Severity.Error
else -> co.touchlab.kermit.Severity.Info
}
val msg = message()
if (throwable != null) {
timber.log(priority, throwable, msg)
Logger.log(severity, HISTORY_TAG, throwable, msg)
} else {
timber.log(priority, msg)
Logger.log(severity, HISTORY_TAG, null, msg)
}
}
@@ -350,7 +358,7 @@ class MeshService : Service() {
private fun stopLocationRequests() {
if (locationFlow?.isActive == true) {
Timber.i("Stopping location requests")
Logger.i { "Stopping location requests" }
locationFlow?.cancel()
locationFlow = null
}
@@ -391,7 +399,7 @@ class MeshService : Service() {
override fun onCreate() {
super.onCreate()
Timber.i("Creating mesh service")
Logger.i { "Creating mesh service" }
serviceNotifications.initChannels()
// Switch to the IO thread
serviceScope.handledLaunch { radioInterfaceService.connect() }
@@ -408,7 +416,7 @@ class MeshService : Service() {
.onEach(::onReceiveFromRadio)
.launchIn(serviceScope)
radioInterfaceService.connectionError
.onEach { error -> Timber.e("BLE Connection Error: ${error.message}") }
.onEach { error -> Logger.e { "BLE Connection Error: ${error.message}" } }
.launchIn(serviceScope)
radioConfigRepository.localConfigFlow.onEach { localConfig = it }.launchIn(serviceScope)
radioConfigRepository.moduleConfigFlow.onEach { moduleConfig = it }.launchIn(serviceScope)
@@ -461,7 +469,7 @@ class MeshService : Service() {
val a = radioInterfaceService.getBondedDeviceAddress()
val wantForeground = a != null && a != NO_DEVICE_SELECTED
Timber.i("Requesting foreground service=$wantForeground")
Logger.i { "Requesting foreground service=$wantForeground" }
val notification = updateServiceStatusNotification()
try {
@@ -480,7 +488,7 @@ class MeshService : Service() {
},
)
} catch (ex: Exception) {
Timber.e(ex, "Error starting foreground service")
Logger.e(ex) { "Error starting foreground service" }
return START_NOT_STICKY
}
return if (!wantForeground) {
@@ -492,7 +500,7 @@ class MeshService : Service() {
}
override fun onDestroy() {
Timber.i("Destroying mesh service")
Logger.i { "Destroying mesh service" }
// Make sure we aren't using the notification first
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
@@ -510,7 +518,7 @@ class MeshService : Service() {
/** discard entire node db & message state - used when downloading a new db from the device */
private fun discardNodeDB() {
Timber.d("Discarding NodeDB")
Logger.d { "Discarding NodeDB" }
myNodeInfo = null
nodeDBbyNodeNum.clear()
isNodeDbReady = false
@@ -836,7 +844,7 @@ class MeshService : Service() {
// We ignore most messages that we sent
val fromUs = myInfo.myNodeNum == packet.from
Timber.d("Received data from $fromId, portnum=${data.portnum} ${bytes.size} bytes")
Logger.d { "Received data from $fromId, portnum=${data.portnum} ${bytes.size} bytes" }
dataPacket.status = MessageStatus.RECEIVED
@@ -851,24 +859,24 @@ class MeshService : Service() {
when (data.portnumValue) {
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> {
if (data.replyId != 0 && data.emoji == 0) {
Timber.d("Received REPLY from $fromId")
Logger.d { "Received REPLY from $fromId" }
rememberDataPacket(dataPacket)
} else if (data.replyId != 0 && data.emoji != 0) {
Timber.d("Received EMOJI from $fromId")
Logger.d { "Received EMOJI from $fromId" }
rememberReaction(packet)
} else {
Timber.d("Received CLEAR_TEXT from $fromId")
Logger.d { "Received CLEAR_TEXT from $fromId" }
rememberDataPacket(dataPacket)
}
}
Portnums.PortNum.ALERT_APP_VALUE -> {
Timber.d("Received ALERT_APP from $fromId")
Logger.d { "Received ALERT_APP from $fromId" }
rememberDataPacket(dataPacket)
}
Portnums.PortNum.WAYPOINT_APP_VALUE -> {
Timber.d("Received WAYPOINT_APP from $fromId")
Logger.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
@@ -876,10 +884,10 @@ class MeshService : Service() {
}
Portnums.PortNum.POSITION_APP_VALUE -> {
Timber.d("Received POSITION_APP from $fromId")
Logger.d { "Received POSITION_APP from $fromId" }
val u = MeshProtos.Position.parseFrom(data.payload)
if (data.wantResponse && u.latitudeI == 0 && u.longitudeI == 0) {
Timber.d("Ignoring nop position update from position request")
Logger.d { "Ignoring nop position update from position request" }
} else {
handleReceivedPosition(packet.from, u, dataPacket.time)
}
@@ -887,7 +895,7 @@ class MeshService : Service() {
Portnums.PortNum.NODEINFO_APP_VALUE ->
if (!fromUs) {
Timber.d("Received NODEINFO_APP from $fromId")
Logger.d { "Received NODEINFO_APP from $fromId" }
val u =
MeshProtos.User.parseFrom(data.payload).copy {
if (isLicensed) clearPublicKey()
@@ -898,7 +906,7 @@ class MeshService : Service() {
// Handle new telemetry info
Portnums.PortNum.TELEMETRY_APP_VALUE -> {
Timber.d("Received TELEMETRY_APP from $fromId")
Logger.d { "Received TELEMETRY_APP from $fromId" }
val u =
TelemetryProtos.Telemetry.parseFrom(data.payload).copy {
if (time == 0) time = (dataPacket.time / 1000L).toInt()
@@ -907,7 +915,7 @@ class MeshService : Service() {
}
Portnums.PortNum.ROUTING_APP_VALUE -> {
Timber.d("Received ROUTING_APP from $fromId")
Logger.d { "Received ROUTING_APP from $fromId" }
// We always send ACKs to other apps, because they might care about the
// messages they sent
shouldBroadcast = true
@@ -922,41 +930,41 @@ class MeshService : Service() {
}
Portnums.PortNum.ADMIN_APP_VALUE -> {
Timber.d("Received ADMIN_APP from $fromId")
Logger.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")
Logger.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")
Logger.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")
Logger.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)
}
Portnums.PortNum.DETECTION_SENSOR_APP_VALUE -> {
Timber.d("Received DETECTION_SENSOR_APP from $fromId")
Logger.d { "Received DETECTION_SENSOR_APP from $fromId" }
val u = dataPacket.copy(dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
rememberDataPacket(u)
}
Portnums.PortNum.TRACEROUTE_APP_VALUE -> {
Timber.d("Received TRACEROUTE_APP from $fromId")
Logger.d { "Received TRACEROUTE_APP from $fromId" }
val routeDiscovery = packet.fullRouteDiscovery
val full = packet.getFullTracerouteResponse(::getUserName)
if (full != null) {
@@ -988,7 +996,7 @@ class MeshService : Service() {
if (start != null) {
val elapsedMs = System.currentTimeMillis() - start
val seconds = elapsedMs / 1000.0
Timber.i("Traceroute $requestId complete in $seconds s")
Logger.i { "Traceroute $requestId complete in $seconds s" }
"$full\n\nDuration: ${"%.1f".format(seconds)} s"
} else {
full
@@ -1012,9 +1020,9 @@ class MeshService : Service() {
Portnums.PortNum.NEIGHBORINFO_APP_VALUE -> {
val requestId = packet.decoded.requestId
Timber.d("Processing NEIGHBORINFO_APP packet with requestId: $requestId")
Logger.d { "Processing NEIGHBORINFO_APP packet with requestId: $requestId" }
val start = neighborInfoStartTimes.remove(requestId)
Timber.d("Found start time for requestId $requestId: $start")
Logger.d { "Found start time for requestId $requestId: $start" }
val info =
runCatching { MeshProtos.NeighborInfo.parseFrom(data.payload.toByteArray()) }.getOrNull()
@@ -1022,7 +1030,7 @@ class MeshService : Service() {
// Store the last neighbor info from our connected radio
if (info != null && packet.from == myInfo.myNodeNum) {
lastNeighborInfo = info
Timber.d("Stored last neighbor info from connected radio")
Logger.d { "Stored last neighbor info from connected radio" }
}
// Only show response if packet is addressed to us and we sent a request in the last 3 minutes
@@ -1060,27 +1068,25 @@ class MeshService : Service() {
if (start != null) {
val elapsedMs = System.currentTimeMillis() - start
val seconds = elapsedMs / 1000.0
Timber.i("Neighbor info $requestId complete in $seconds s")
Logger.i { "Neighbor info $requestId complete in $seconds s" }
"$formatted\n\nDuration: ${"%.1f".format(seconds)} s"
} else {
Timber.w("No start time found for neighbor info requestId: $requestId")
Logger.w { "No start time found for neighbor info requestId: $requestId" }
formatted
}
serviceRepository.setNeighborInfoResponse(response)
} else {
Timber.d(
"Neighbor info response filtered: ToUs=%s, isRecentRequest=%s",
isAddressedToUs,
isRecentRequest,
)
Logger.d {
"Neighbor info response filtered: ToUs=$isAddressedToUs, isRecentRequest=$isRecentRequest"
}
}
}
Portnums.PortNum.NEIGHBORINFO_APP_VALUE -> {
val requestId = packet.decoded.requestId
Timber.d("Processing NEIGHBORINFO_APP packet with requestId: $requestId")
Logger.d { "Processing NEIGHBORINFO_APP packet with requestId: $requestId" }
val start = neighborInfoStartTimes.remove(requestId)
Timber.d("Found start time for requestId $requestId: $start")
Logger.d { "Found start time for requestId $requestId: $start" }
val info =
runCatching { MeshProtos.NeighborInfo.parseFrom(data.payload.toByteArray()) }.getOrNull()
@@ -1088,7 +1094,7 @@ class MeshService : Service() {
// Store the last neighbor info from our connected radio
if (info != null && packet.from == myInfo.myNodeNum) {
lastNeighborInfo = info
Timber.d("Stored last neighbor info from connected radio")
Logger.d { "Stored last neighbor info from connected radio" }
}
// Only show response if packet is addressed to us and we sent a request in the last 3 minutes
@@ -1126,23 +1132,21 @@ class MeshService : Service() {
if (start != null) {
val elapsedMs = System.currentTimeMillis() - start
val seconds = elapsedMs / 1000.0
Timber.i("Neighbor info $requestId complete in $seconds s")
Logger.i { "Neighbor info $requestId complete in $seconds s" }
"$formatted\n\nDuration: ${"%.1f".format(seconds)} s"
} else {
Timber.w("No start time found for neighbor info requestId: $requestId")
Logger.w { "No start time found for neighbor info requestId: $requestId" }
formatted
}
serviceRepository.setNeighborInfoResponse(response)
} else {
Timber.d(
"Neighbor info response filtered: isToUs=%s, isRecent=%s",
isAddressedToUs,
isRecentRequest,
)
Logger.d {
"Neighbor info response filtered: isToUs=$isAddressedToUs, isRecent=$isRecentRequest"
}
}
}
else -> Timber.d("No custom processing needed for ${data.portnumValue} from $fromId")
else -> Logger.d { "No custom processing needed for ${data.portnumValue} from $fromId" }
}
// We always tell other apps when new data packets arrive
@@ -1162,7 +1166,7 @@ class MeshService : Service() {
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
if (fromNodeNum == myNodeNum) {
val response = a.getConfigResponse
Timber.d("Admin: received config ${response.payloadVariantCase}")
Logger.d { "Admin: received config ${response.payloadVariantCase}" }
setLocalConfig(response)
}
}
@@ -1172,7 +1176,7 @@ class MeshService : Service() {
val mi = myNodeInfo
if (mi != null) {
val ch = a.getChannelResponse
Timber.d("Admin: Received channel ${ch.index}")
Logger.d { "Admin: Received channel ${ch.index}" }
if (ch.index + 1 < mi.maxChannels) {
handleChannel(ch)
@@ -1182,15 +1186,15 @@ class MeshService : Service() {
}
AdminProtos.AdminMessage.PayloadVariantCase.GET_DEVICE_METADATA_RESPONSE -> {
Timber.d("Admin: received DeviceMetadata from $fromNodeNum")
Logger.d { "Admin: received DeviceMetadata from $fromNodeNum" }
serviceScope.handledLaunch {
nodeRepository.insertMetadata(MetadataEntity(fromNodeNum, a.getDeviceMetadataResponse))
}
}
else -> Timber.w("No special processing needed for ${a.payloadVariantCase}")
else -> Logger.w { "No special processing needed for ${a.payloadVariantCase}" }
}
Timber.d("Admin: Received session_passkey from $fromNodeNum")
Logger.d { "Admin: Received session_passkey from $fromNodeNum" }
sessionPasskey = a.sessionPasskey
}
@@ -1224,11 +1228,11 @@ class MeshService : Service() {
if (shouldPreserve) {
// Firmware sent us a placeholder - keep all our existing user data
Timber.d(
Logger.d {
"Preserving existing user data for node $fromNum: " +
"kept='${it.user.longName}' (hwModel=${it.user.hwModel}), " +
"skipped default='${p.longName}' (hwModel=UNSET)",
)
"skipped default='${p.longName}' (hwModel=UNSET)"
}
// Ensure denormalized columns are updated from preserved user data
it.longName = it.user.longName
it.shortName = it.user.shortName
@@ -1242,7 +1246,7 @@ class MeshService : Service() {
p
} else {
p.copy {
Timber.w("Public key mismatch from $longName ($shortName)")
Logger.w { "Public key mismatch from $longName ($shortName)" }
publicKey = NodeEntity.ERROR_BYTE_STRING
}
}
@@ -1275,7 +1279,7 @@ class MeshService : Service() {
// (only)
// we don't record these nop position updates
if (myNodeNum == fromNum && p.latitudeI == 0 && p.longitudeI == 0) {
Timber.d("Ignoring nop position update for the local node")
Logger.d { "Ignoring nop position update for the local node" }
} else {
updateNodeInfo(fromNum) { it.setPosition(p, (defaultTime / 1000L).toInt()) }
}
@@ -1407,7 +1411,7 @@ class MeshService : Service() {
}
private fun handleReceivedStoreAndForward(dataPacket: DataPacket, s: StoreAndForwardProtos.StoreAndForward) {
Timber.d("StoreAndForward: ${s.variantCase} ${s.rr} from ${dataPacket.from}")
Logger.d { "StoreAndForward: ${s.variantCase} ${s.rr} from ${dataPacket.from}" }
val transport = currentTransport()
val lastRequest =
if (s.variantCase == StoreAndForwardProtos.StoreAndForward.VariantCase.HISTORY) {
@@ -1481,7 +1485,7 @@ class MeshService : Service() {
if (packet.rxTime == 0) setRxTime(currentSecond())
}
.build()
Timber.d("[packet]: ${packet.toOneLineString()}")
Logger.d { "[packet]: ${packet.toOneLineString()}" }
if (isNodeDbReady) {
processReceivedMeshPacket(preparedPacket)
return
@@ -1524,7 +1528,7 @@ class MeshService : Service() {
private fun sendNow(p: DataPacket) {
val packet = toMeshPacket(p)
p.time = System.currentTimeMillis() // update time to the actual time we started sending
// Timber.d("Sending to radio: ${packet.toPIIString()}")
// Logger.d { "Sending to radio: ${packet.toPIIString()}" }
packetHandler.sendToRadio(packet)
}
@@ -1535,7 +1539,7 @@ class MeshService : Service() {
sendNow(p)
sentPackets.add(p)
} catch (ex: Exception) {
Timber.e(ex, "Error sending queued message:")
Logger.e(ex) { "Error sending queued message:" }
}
}
offlineSentPackets.removeAll(sentPackets)
@@ -1669,7 +1673,7 @@ class MeshService : Service() {
@Suppress("CyclomaticComplexMethod")
private fun onConnectionChanged(c: ConnectionState) {
if (connectionStateHolder.connectionState.value == c && c !is ConnectionState.Connected) return
Timber.d("onConnectionChanged: ${connectionStateHolder.connectionState.value} -> $c")
Logger.d { "onConnectionChanged: ${connectionStateHolder.connectionState.value} -> $c" }
// Cancel any existing timeouts
sleepTimeout?.cancel()
@@ -1697,7 +1701,7 @@ class MeshService : Service() {
private fun handleDisconnected() {
connectionStateHolder.setState(ConnectionState.Disconnected)
Timber.d("Starting disconnect")
Logger.d { "Starting disconnect" }
packetHandler.stopPacketQueue()
stopLocationRequests()
stopMqttClientProxy()
@@ -1730,12 +1734,12 @@ class MeshService : Service() {
// wait 30 seconds
val timeout = (localConfig.power?.lsSecs ?: 0) + 30
Timber.d("Waiting for sleeping device, timeout=$timeout secs")
Logger.d { "Waiting for sleeping device, timeout=$timeout secs" }
delay(timeout * 1000L)
Timber.w("Device timeout out, setting disconnected")
Logger.w { "Device timeout out, setting disconnected" }
onConnectionChanged(ConnectionState.Disconnected)
} catch (_: CancellationException) {
Timber.d("device sleep timeout cancelled")
Logger.d { "device sleep timeout cancelled" }
}
}
@@ -1746,7 +1750,7 @@ class MeshService : Service() {
private fun handleConnected() {
connectionStateHolder.setState(ConnectionState.Connecting)
serviceBroadcasts.broadcastConnection()
Timber.d("Starting connect")
Logger.d { "Starting connect" }
historyLog {
val address = meshPrefs.deviceAddress ?: "null"
"onReconnect transport=${currentTransport()} node=$address"
@@ -1755,9 +1759,9 @@ class MeshService : Service() {
connectTimeMsec = System.currentTimeMillis()
startConfigOnly()
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "Invalid protocol buffer sent by device - update device software and try again")
Logger.e(ex) { "Invalid protocol buffer sent by device - update device software and try again" }
} catch (ex: RadioNotConnectedException) {
Timber.e("Lost connection to radio during init - waiting for reconnect ${ex.message}")
Logger.e { "Lost connection to radio during init - waiting for reconnect ${ex.message}" }
} catch (ex: RemoteException) {
onConnectionChanged(ConnectionState.DeviceSleep)
throw ex
@@ -1852,7 +1856,9 @@ class MeshService : Service() {
// Explicitly handle default/unwanted cases to satisfy the exhaustive `when`
PayloadVariantCase.PAYLOADVARIANT_NOT_SET -> { proto ->
Timber.d("Received variant PayloadVariantUnset: Full FromRadio proto: ${proto.toPIIString()}")
Logger.d {
"Received variant PayloadVariantUnset: Full FromRadio proto: ${proto.toPIIString()}"
}
}
}
}
@@ -1872,9 +1878,9 @@ class MeshService : Service() {
runCatching { MeshProtos.FromRadio.parseFrom(bytes) }
.onSuccess { proto ->
if (proto.payloadVariantCase == PayloadVariantCase.PAYLOADVARIANT_NOT_SET) {
Timber.w(
"Received FromRadio with PAYLOADVARIANT_NOT_SET. rawBytes=${bytes.toHexString()} proto=$proto",
)
Logger.w {
"Received FromRadio with PAYLOADVARIANT_NOT_SET. rawBytes=${bytes.toHexString()} proto=$proto"
}
}
proto.route()
}
@@ -1884,11 +1890,10 @@ class MeshService : Service() {
handleLogRecord(logRecord)
}
.onFailure { _ ->
Timber.e(
primaryException,
Logger.e(primaryException) {
"Failed to parse radio packet (len=${bytes.size} contents=${bytes.toHexString()}). " +
"Not a valid FromRadio or LogRecord.",
)
"Not a valid FromRadio or LogRecord."
}
}
}
}
@@ -1908,7 +1913,7 @@ class MeshService : Service() {
private var nodeInfoNonce: Int = DEFAULT_NODE_INFO_NONCE
private fun handleDeviceConfig(config: ConfigProtos.Config) {
Timber.d("[deviceConfig] ${config.toPIIString()}")
Logger.d { "[deviceConfig] ${config.toPIIString()}" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1924,7 +1929,7 @@ class MeshService : Service() {
}
private fun handleModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
Timber.d("[moduleConfig] ${config.toPIIString()}")
Logger.d { "[moduleConfig] ${config.toPIIString()}" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1940,7 +1945,7 @@ class MeshService : Service() {
}
private fun handleChannel(ch: ChannelProtos.Channel) {
Timber.d("[channel] ${ch.toPIIString()}")
Logger.d { "[channel] ${ch.toPIIString()}" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1965,11 +1970,11 @@ class MeshService : Service() {
if (shouldPreserve) {
// Firmware sent us a placeholder - keep all our existing user data
Timber.d(
Logger.d {
"Preserving existing user data for node ${info.num}: " +
"kept='${it.user.longName}' (hwModel=${it.user.hwModel}), " +
"skipped default='${info.user.longName}' (hwModel=UNSET)",
)
"skipped default='${info.user.longName}' (hwModel=UNSET)"
}
// Ensure denormalized columns are updated from preserved user data
it.longName = it.user.longName
it.shortName = it.user.shortName
@@ -2013,7 +2018,7 @@ class MeshService : Service() {
}
private fun handleNodeInfo(info: MeshProtos.NodeInfo) {
Timber.d("[nodeInfo] ${info.toPIIString()}")
Logger.d { "[nodeInfo] ${info.toPIIString()}" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -2029,7 +2034,7 @@ class MeshService : Service() {
}
private fun handleNodeInfoComplete() {
Timber.d("NodeInfo complete for nonce $nodeInfoNonce")
Logger.d { "NodeInfo complete for nonce $nodeInfoNonce" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -2040,7 +2045,7 @@ class MeshService : Service() {
)
insertMeshLog(packetToSave)
if (newNodes.isEmpty()) {
Timber.e("Did not receive a valid node info")
Logger.e { "Did not receive a valid node info" }
} else {
// Batch update: Update in-memory models first without triggering individual DB writes
val entities =
@@ -2074,13 +2079,13 @@ class MeshService : Service() {
private fun regenMyNodeInfo(metadata: MeshProtos.DeviceMetadata? = MeshProtos.DeviceMetadata.getDefaultInstance()) {
val myInfo = rawMyNodeInfo
val hasMetadata = metadata != null && metadata != MeshProtos.DeviceMetadata.getDefaultInstance()
Timber.i(
Logger.i {
"[MYNODE_REGEN] Called - " +
"rawMyNodeInfo: ${if (myInfo != null) "present" else "null"}, " +
"metadata: ${if (hasMetadata) "present" else "null/default"}, " +
"firmwareVersion: ${metadata?.firmwareVersion ?: "null"}, " +
"hasWifi: ${metadata?.hasWifi}",
)
"hasWifi: ${metadata?.hasWifi}"
}
if (myInfo != null) {
val mi =
@@ -2107,21 +2112,21 @@ class MeshService : Service() {
)
}
Timber.i(
Logger.i {
"[MYNODE_REGEN] Created MyNodeEntity - " +
"nodeNum: ${mi.myNodeNum}, " +
"model: ${mi.model}, " +
"firmwareVersion: ${mi.firmwareVersion}, " +
"hasWifi: ${mi.hasWifi}",
)
"hasWifi: ${mi.hasWifi}"
}
if (metadata != null && metadata != MeshProtos.DeviceMetadata.getDefaultInstance()) {
serviceScope.handledLaunch { nodeRepository.insertMetadata(MetadataEntity(mi.myNodeNum, metadata)) }
}
newMyNodeInfo = mi
Timber.i("[MYNODE_REGEN] Set newMyNodeInfo (will be committed on configComplete)")
Logger.i { "[MYNODE_REGEN] Set newMyNodeInfo (will be committed on configComplete)" }
} else {
Timber.w("[MYNODE_REGEN] rawMyNodeInfo is null, cannot regenerate")
Logger.w { "[MYNODE_REGEN] rawMyNodeInfo is null, cannot regenerate" }
}
}
@@ -2136,12 +2141,12 @@ class MeshService : Service() {
/** Update MyNodeInfo (called from either new API version or the old one) */
private fun handleMyInfo(myInfo: MeshProtos.MyNodeInfo) {
Timber.i(
Logger.i {
"[MYINFO_RECEIVED] MyNodeInfo received - " +
"nodeNum: ${myInfo.myNodeNum}, " +
"minAppVersion: ${myInfo.minAppVersion}, " +
"PII data: ${myInfo.toPIIString()}",
)
"PII data: ${myInfo.toPIIString()}"
}
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -2153,7 +2158,7 @@ class MeshService : Service() {
insertMeshLog(packetToSave)
rawMyNodeInfo = myInfo
Timber.i("[MYINFO_RECEIVED] Set rawMyNodeInfo, calling regenMyNodeInfo()")
Logger.i { "[MYINFO_RECEIVED] Set rawMyNodeInfo, calling regenMyNodeInfo()" }
regenMyNodeInfo()
// We'll need to get a new set of channels and settings now
@@ -2166,14 +2171,14 @@ class MeshService : Service() {
/** Update our DeviceMetadata */
private fun handleMetadata(metadata: MeshProtos.DeviceMetadata) {
Timber.i(
Logger.i {
"[METADATA_RECEIVED] DeviceMetadata received - " +
"firmwareVersion: ${metadata.firmwareVersion}, " +
"hwModel: ${metadata.hwModel}, " +
"hasWifi: ${metadata.hasWifi}, " +
"hasBluetooth: ${metadata.hasBluetooth}, " +
"PII data: ${metadata.toPIIString()}",
)
"PII data: ${metadata.toPIIString()}"
}
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -2184,16 +2189,16 @@ class MeshService : Service() {
)
insertMeshLog(packetToSave)
Timber.i(
Logger.i {
"[METADATA_RECEIVED] Calling regenMyNodeInfo with metadata - " +
"This will update newMyNodeInfo with firmwareVersion: ${metadata.firmwareVersion}",
)
"This will update newMyNodeInfo with firmwareVersion: ${metadata.firmwareVersion}"
}
regenMyNodeInfo(metadata)
}
/** Publish MqttClientProxyMessage (fromRadio) */
private fun handleMqttProxyMessage(message: MeshProtos.MqttClientProxyMessage) {
Timber.d("[mqttClientProxyMessage] ${message.toPIIString()}")
Logger.d { "[mqttClientProxyMessage] ${message.toPIIString()}" }
with(message) {
when (payloadVariantCase) {
MeshProtos.MqttClientProxyMessage.PayloadVariantCase.TEXT -> {
@@ -2210,7 +2215,7 @@ class MeshService : Service() {
}
private fun handleClientNotification(notification: MeshProtos.ClientNotification) {
Timber.d("[clientNotification] ${notification.toPIIString()}")
Logger.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
@@ -2228,7 +2233,7 @@ class MeshService : Service() {
}
private fun handleFileInfo(fileInfo: MeshProtos.FileInfo) {
Timber.d("[fileInfo] ${fileInfo.toPIIString()}")
Logger.d { "[fileInfo] ${fileInfo.toPIIString()}" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -2241,7 +2246,7 @@ class MeshService : Service() {
}
private fun handleLogRecord(logRecord: MeshProtos.LogRecord) {
Timber.d("[logRecord] ${logRecord.toPIIString()}")
Logger.d { "[logRecord] ${logRecord.toPIIString()}" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -2254,7 +2259,7 @@ class MeshService : Service() {
}
private fun handleRebooted(rebooted: Boolean) {
Timber.d("[rebooted] $rebooted")
Logger.d { "[rebooted] $rebooted" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -2267,7 +2272,7 @@ class MeshService : Service() {
}
private fun handleXmodemPacket(xmodemPacket: XmodemProtos.XModem) {
Timber.d("[xmodemPacket] ${xmodemPacket.toPIIString()}")
Logger.d { "[xmodemPacket] ${xmodemPacket.toPIIString()}" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -2280,7 +2285,7 @@ class MeshService : Service() {
}
private fun handleDeviceUiConfig(deviceuiConfig: DeviceUIProtos.DeviceUIConfig) {
Timber.d("[deviceuiConfig] ${deviceuiConfig.toPIIString()}")
Logger.d { "[deviceuiConfig] ${deviceuiConfig.toPIIString()}" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -2308,7 +2313,7 @@ class MeshService : Service() {
private fun stopMqttClientProxy() {
if (mqttMessageFlow?.isActive == true) {
Timber.i("Stopping MqttClientProxy")
Logger.i { "Stopping MqttClientProxy" }
mqttMessageFlow?.cancel()
mqttMessageFlow = null
}
@@ -2329,19 +2334,19 @@ class MeshService : Service() {
}
private fun handleConfigComplete(configCompleteId: Int) {
Timber.d("[configCompleteId]: ${configCompleteId.toPIIString()}")
Logger.d { "[configCompleteId]: ${configCompleteId.toPIIString()}" }
when (configCompleteId) {
configOnlyNonce -> handleConfigOnlyComplete()
nodeInfoNonce -> handleNodeInfoComplete()
else ->
Timber.w(
"Config complete id mismatch: received=$configCompleteId expected one of [$configOnlyNonce,$nodeInfoNonce]",
)
Logger.w {
"Config complete id mismatch: received=$configCompleteId expected one of [$configOnlyNonce,$nodeInfoNonce]"
}
}
}
private fun handleConfigOnlyComplete() {
Timber.i("[CONFIG_COMPLETE] Config-only complete for nonce $configOnlyNonce")
Logger.i { "[CONFIG_COMPLETE] Config-only complete for nonce $configOnlyNonce" }
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -2353,16 +2358,16 @@ class MeshService : Service() {
insertMeshLog(packetToSave)
if (newMyNodeInfo == null) {
Timber.e("[CONFIG_COMPLETE] Did not receive a valid config - newMyNodeInfo is null")
Logger.e { "[CONFIG_COMPLETE] Did not receive a valid config - newMyNodeInfo is null" }
} else {
Timber.i(
Logger.i {
"[CONFIG_COMPLETE] Committing newMyNodeInfo to myNodeInfo - " +
"firmwareVersion: ${newMyNodeInfo?.firmwareVersion}, " +
"hasWifi: ${newMyNodeInfo?.hasWifi}, " +
"model: ${newMyNodeInfo?.model}",
)
"model: ${newMyNodeInfo?.model}"
}
myNodeInfo = newMyNodeInfo
Timber.i("[CONFIG_COMPLETE] myNodeInfo committed successfully")
Logger.i { "[CONFIG_COMPLETE] myNodeInfo committed successfully" }
}
// Keep BLE awake and allow the firmware to settle before the node-info stage.
serviceScope.handledLaunch {
@@ -2379,21 +2384,21 @@ class MeshService : Service() {
packetHandler.sendToRadio(
ToRadio.newBuilder().apply { heartbeat = MeshProtos.Heartbeat.getDefaultInstance() },
)
Timber.d("Heartbeat sent between nonce stages")
Logger.d { "Heartbeat sent between nonce stages" }
} catch (ex: Exception) {
Timber.w(ex, "Failed to send heartbeat; proceeding with node-info stage")
Logger.w(ex) { "Failed to send heartbeat; proceeding with node-info stage" }
}
}
private fun startConfigOnly() {
newMyNodeInfo = null
Timber.d("Starting config-only nonce=$configOnlyNonce")
Logger.d { "Starting config-only nonce=$configOnlyNonce" }
packetHandler.sendToRadio(ToRadio.newBuilder().apply { this.wantConfigId = configOnlyNonce })
}
private fun startNodeInfoOnly() {
newNodes.clear()
Timber.d("Starting node-info nonce=$nodeInfoNonce")
Logger.d { "Starting node-info nonce=$nodeInfoNonce" }
packetHandler.sendToRadio(ToRadio.newBuilder().apply { this.wantConfigId = nodeInfoNonce })
}
@@ -2403,7 +2408,7 @@ class MeshService : Service() {
val mi = myNodeInfo
if (mi != null) {
val idNum = destNum ?: mi.myNodeNum // when null we just send to the local node
Timber.d("Sending our position/time to=$idNum ${Position(position)}")
Logger.d { "Sending our position/time to=$idNum ${Position(position)}" }
// Also update our own map for our nodeNum, by handling the packet just like packets from other users
if (!localConfig.position.fixedPosition) {
@@ -2422,7 +2427,7 @@ class MeshService : Service() {
)
}
} catch (_: BLEException) {
Timber.w("Ignoring disconnected radio during gps location update")
Logger.w { "Ignoring disconnected radio during gps location update" }
}
}
@@ -2433,11 +2438,11 @@ class MeshService : Service() {
@Suppress("ComplexCondition")
if (user == old) {
Timber.d("Ignoring nop owner change")
Logger.d { "Ignoring nop owner change" }
} else {
Timber.d(
"setOwner Id: $id longName: ${longName.anonymize} shortName: $shortName isLicensed: $isLicensed isUnmessagable: $isUnmessagable",
)
Logger.d {
"setOwner Id: $id longName: ${longName.anonymize} shortName: $shortName isLicensed: $isLicensed isUnmessagable: $isUnmessagable"
}
// Also update our own map for our nodeNum, by handling the packet just like packets from other users
handleReceivedUser(dest.num, user)
@@ -2511,10 +2516,10 @@ class MeshService : Service() {
packetHandler.sendToRadio(
newMeshPacketTo(myNodeNum).buildAdminPacket {
if (node.isFavorite) {
Timber.d("removing node ${node.num} from favorite list")
Logger.d { "removing node ${node.num} from favorite list" }
removeFavoriteNode = node.num
} else {
Timber.d("adding node ${node.num} to favorite list")
Logger.d { "adding node ${node.num} to favorite list" }
setFavoriteNode = node.num
}
},
@@ -2526,10 +2531,10 @@ class MeshService : Service() {
packetHandler.sendToRadio(
newMeshPacketTo(myNodeNum).buildAdminPacket {
if (node.isIgnored) {
Timber.d("removing node ${node.num} from ignore list")
Logger.d { "removing node ${node.num} from ignore list" }
removeIgnoredNode = node.num
} else {
Timber.d("adding node ${node.num} to ignore list")
Logger.d { "adding node ${node.num} to ignore list" }
setIgnoredNode = node.num
}
},
@@ -2555,11 +2560,11 @@ class MeshService : Service() {
private fun updateLastAddress(deviceAddr: String?) {
val currentAddr = meshPrefs.deviceAddress
Timber.d("setDeviceAddress: received request to change to: ${deviceAddr.anonymize}")
Logger.d { "setDeviceAddress: received request to change to: ${deviceAddr.anonymize}" }
if (deviceAddr != currentAddr) {
Timber.d(
"SetDeviceAddress: Device address changed from ${currentAddr.anonymize} to ${deviceAddr.anonymize}",
)
Logger.d {
"SetDeviceAddress: Device address changed from ${currentAddr.anonymize} to ${deviceAddr.anonymize}"
}
val currentLabel = currentAddr ?: "null"
val nextLabel = deviceAddr ?: "null"
val nextTransport = currentTransport(deviceAddr)
@@ -2584,7 +2589,7 @@ class MeshService : Service() {
loadCachedNodeDB()
}
} else {
Timber.d("SetDeviceAddress: Device address is unchanged, ignoring.")
Logger.d { "SetDeviceAddress: Device address is unchanged, ignoring." }
}
}
@@ -2596,7 +2601,7 @@ class MeshService : Service() {
object : IMeshService.Stub() {
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
Timber.d("Passing through device change to radio service: ${deviceAddr.anonymize}")
Logger.d { "Passing through device change to radio service: ${deviceAddr.anonymize}" }
updateLastAddress(deviceAddr)
radioInterfaceService.setDeviceAddress(deviceAddr)
}
@@ -2642,9 +2647,9 @@ class MeshService : Service() {
toRemoteExceptions {
if (p.id == 0) p.id = generatePacketId()
val bytes = p.bytes!!
Timber.i(
"sendData dest=${p.to}, id=${p.id} <- ${bytes.size} bytes (connectionState=${connectionStateHolder.connectionState.value})",
)
Logger.i {
"sendData dest=${p.to}, id=${p.id} <- ${bytes.size} bytes (connectionState=${connectionStateHolder.connectionState.value})"
}
if (p.dataType == 0) throw Exception("Port numbers must be non-zero!")
if (bytes.size >= MeshProtos.Constants.DATA_PAYLOAD_LEN.number) {
p.status = MessageStatus.ERROR
@@ -2656,7 +2661,7 @@ class MeshService : Service() {
try {
sendNow(p)
} catch (ex: Exception) {
Timber.e(ex, "Error sending message, so enqueueing")
Logger.e(ex) { "Error sending message, so enqueueing" }
enqueueForSending(p)
}
} else {
@@ -2677,7 +2682,7 @@ class MeshService : Service() {
}
override fun setRemoteConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
Timber.d("Setting new radio config!")
Logger.d { "Setting new radio config!" }
val config = ConfigProtos.Config.parseFrom(payload)
packetHandler.sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setConfig = config })
if (num == myNodeNum) setLocalConfig(config)
@@ -2696,7 +2701,7 @@ class MeshService : Service() {
}
override fun setModuleConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
Timber.d("Setting new module config!")
Logger.d { "Setting new module config!" }
val config = ModuleConfigProtos.ModuleConfig.parseFrom(payload)
packetHandler.sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setModuleConfig = config })
if (num == myNodeNum) setLocalModuleConfig(config)
@@ -2765,13 +2770,13 @@ class MeshService : Service() {
override fun getNodes(): MutableList<NodeInfo> = toRemoteExceptions {
val r = nodeDBbyNodeNum.values.map { it.toNodeInfo() }.toMutableList()
Timber.i("in getOnline, count=${r.size}")
Logger.i { "in getOnline, count=${r.size}" }
r
}
override fun connectionState(): String = toRemoteExceptions {
val r = connectionStateHolder.connectionState.value
Timber.i("in connectionState=$r")
Logger.i { "in connectionState=$r" }
r.toString()
}
@@ -2804,7 +2809,7 @@ class MeshService : Service() {
lastNeighborInfo
?: run {
// If we don't have it, send dummy/interceptable data
Timber.d("No stored neighbor info from connected radio, sending dummy data")
Logger.d { "No stored neighbor info from connected radio, sending dummy data" }
MeshProtos.NeighborInfo.newBuilder()
.setNodeId(myNodeNum)
.setLastSentById(myNodeNum)
@@ -2844,7 +2849,7 @@ class MeshService : Service() {
else -> nodeDBbyNodeNum[myNodeNum]?.position?.let { Position(it) }?.takeIf { it.isValid() }
}
if (currentPosition == null) {
Timber.d("Position request skipped - no valid position available")
Logger.d { "Position request skipped - no valid position available" }
return@toRemoteExceptions
}
val meshPosition = position {

View File

@@ -20,13 +20,13 @@ package com.geeksville.mesh.service
import android.content.Context
import android.content.Intent
import android.os.Parcelable
import co.touchlab.kermit.Logger
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
import javax.inject.Singleton
@@ -51,7 +51,7 @@ constructor(
}
fun broadcastNodeChange(info: NodeInfo) {
Timber.d("Broadcasting node change ${info.user?.toPIIString()}")
Logger.d { "Broadcasting node change ${info.user?.toPIIString()}" }
val intent = Intent(MeshService.ACTION_NODE_CHANGE).putExtra(EXTRA_NODEINFO, info)
explicitBroadcast(intent)
}
@@ -60,10 +60,10 @@ constructor(
fun broadcastMessageStatus(id: Int, status: MessageStatus?) {
if (id == 0) {
Timber.d("Ignoring anonymous packet status")
Logger.d { "Ignoring anonymous packet status" }
} else {
// Do not log, contains PII possibly
// MeshService.Timber.d("Broadcasting message status $p")
// MeshService.Logger.d { "Broadcasting message status $p" }
val intent =
Intent(MeshService.ACTION_MESSAGE_STATUS).apply {
putExtra(EXTRA_PACKET_ID, id)

View File

@@ -20,8 +20,8 @@ package com.geeksville.mesh.service
import android.app.ForegroundServiceStartNotAllowedException
import android.content.Context
import android.os.Build
import co.touchlab.kermit.Logger
import com.geeksville.mesh.BuildConfig
import timber.log.Timber
// / Helper function to start running our service
fun MeshService.Companion.startService(context: Context) {
@@ -33,14 +33,14 @@ fun MeshService.Companion.startService(context: Context) {
// Before binding we want to explicitly create - so the service stays alive forever (so it can keep
// listening for the bluetooth packets arriving from the radio. And when they arrive forward them
// to Signal or whatever.
Timber.i("Trying to start service debug=${BuildConfig.DEBUG}")
Logger.i { "Trying to start service debug=${BuildConfig.DEBUG}" }
val intent = createIntent(context)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
context.startForegroundService(intent)
} catch (ex: ForegroundServiceStartNotAllowedException) {
Timber.e("Unable to start service: ${ex.message}")
Logger.e { "Unable to start service: ${ex.message}" }
}
} else {
context.startForegroundService(intent)

View File

@@ -17,6 +17,7 @@
package com.geeksville.mesh.service
import co.touchlab.kermit.Logger
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import dagger.Lazy
@@ -38,7 +39,6 @@ import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.MeshProtos.MeshPacket
import org.meshtastic.proto.MeshProtos.ToRadio
import org.meshtastic.proto.fromRadio
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.TimeUnit
@@ -73,7 +73,7 @@ constructor(
*/
fun sendToRadio(p: ToRadio.Builder) {
val built = p.build()
Timber.d("Sending to radio ${built.toPIIString()}")
Logger.d { "Sending to radio ${built.toPIIString()}" }
val b = built.toByteArray()
radioInterfaceService.sendToRadio(b)
@@ -105,7 +105,7 @@ constructor(
fun stopPacketQueue() {
if (queueJob?.isActive == true) {
Timber.i("Stopping packet queueJob")
Logger.i { "Stopping packet queueJob" }
queueJob?.cancel()
queueJob = null
queuedPackets.clear()
@@ -115,7 +115,7 @@ constructor(
}
fun handleQueueStatus(queueStatus: MeshProtos.QueueStatus) {
Timber.d("[queueStatus] ${queueStatus.toOneLineString()}")
Logger.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) {
@@ -134,20 +134,20 @@ constructor(
if (queueJob?.isActive == true) return
queueJob =
scope.handledLaunch {
Timber.d("packet queueJob started")
Logger.d { "packet queueJob started" }
while (connectionStateHolder.connectionState.value == ConnectionState.Connected) {
// take the first packet from the queue head
val packet = queuedPackets.poll() ?: break
try {
// send packet to the radio and wait for response
val response = sendPacket(packet)
Timber.d("queueJob packet id=${packet.id.toUInt()} waiting")
Logger.d { "queueJob packet id=${packet.id.toUInt()} waiting" }
val success = response.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
Timber.d("queueJob packet id=${packet.id.toUInt()} success $success")
Logger.d { "queueJob packet id=${packet.id.toUInt()} success $success" }
} catch (e: TimeoutException) {
Timber.d("queueJob packet id=${packet.id.toUInt()} timeout")
Logger.d { "queueJob packet id=${packet.id.toUInt()} timeout" }
} catch (e: Exception) {
Timber.d("queueJob packet id=${packet.id.toUInt()} failed")
Logger.d { "queueJob packet id=${packet.id.toUInt()} failed" }
}
}
}
@@ -186,7 +186,7 @@ constructor(
}
sendToRadio(ToRadio.newBuilder().apply { this.packet = packet })
} catch (ex: Exception) {
Timber.e(ex, "sendToRadio error: ${ex.message}")
Logger.e(ex) { "sendToRadio error: ${ex.message}" }
future.complete(false)
}
return future

View File

@@ -77,7 +77,6 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -88,6 +87,7 @@ import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import co.touchlab.kermit.Logger
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.UIViewModel
@@ -157,7 +157,6 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusBlue
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.feature.node.metrics.annotateTraceroute
import org.meshtastic.proto.MeshProtos
import timber.log.Timber
enum class TopLevelDestination(val label: StringResource, val icon: ImageVector, val route: Route) {
Conversations(Res.string.conversations, MeshtasticIcons.Conversations, ContactsRoutes.ContactsGraph),
@@ -610,7 +609,7 @@ private fun VersionChecks(viewModel: UIViewModel) {
LaunchedEffect(connectionState, firmwareEdition) {
if (connectionState == ConnectionState.Connected) {
firmwareEdition?.let { edition ->
Timber.d("FirmwareEdition: ${edition.name}")
Logger.d { "FirmwareEdition: ${edition.name}" }
when (edition) {
MeshProtos.FirmwareEdition.VANILLA -> {
// Handle any specific logic for VANILLA firmware edition if needed
@@ -627,21 +626,21 @@ private fun VersionChecks(viewModel: UIViewModel) {
// Check if the device is running an old app version or firmware version
LaunchedEffect(connectionState, myNodeInfo) {
if (connectionState == ConnectionState.Connected) {
Timber.i(
Logger.i {
"[FW_CHECK] Connection state: $connectionState, " +
"myNodeInfo: ${if (myNodeInfo != null) "present" else "null"}, " +
"firmwareVersion: ${myFirmwareVersion ?: "null"}",
)
"firmwareVersion: ${myFirmwareVersion ?: "null"}"
}
myNodeInfo?.let { info ->
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE && BuildConfig.DEBUG.not()
Timber.d(
Logger.d {
"[FW_CHECK] App version check - minAppVersion: ${info.minAppVersion}, " +
"currentVersion: ${BuildConfig.VERSION_CODE}, isOld: $isOld",
)
"currentVersion: ${BuildConfig.VERSION_CODE}, isOld: $isOld"
}
if (isOld) {
Timber.w("[FW_CHECK] App too old - showing update prompt")
Logger.w { "[FW_CHECK] App too old - showing update prompt" }
viewModel.showAlert(
getString(Res.string.app_too_old),
getString(Res.string.must_update),
@@ -654,18 +653,18 @@ private fun VersionChecks(viewModel: UIViewModel) {
} else {
myFirmwareVersion?.let { fwVersion ->
val curVer = DeviceVersion(fwVersion)
Timber.i(
Logger.i {
"[FW_CHECK] Firmware version comparison - " +
"device: $curVer (raw: $fwVersion), " +
"absoluteMin: ${MeshService.absoluteMinDeviceVersion}, " +
"min: ${MeshService.minDeviceVersion}",
)
"min: ${MeshService.minDeviceVersion}"
}
if (curVer < MeshService.absoluteMinDeviceVersion) {
Timber.w(
Logger.w {
"[FW_CHECK] Firmware too old - " +
"device: $curVer < absoluteMin: ${MeshService.absoluteMinDeviceVersion}",
)
"device: $curVer < absoluteMin: ${MeshService.absoluteMinDeviceVersion}"
}
val title = getString(Res.string.firmware_too_old)
val message = getString(Res.string.firmware_old)
viewModel.showAlert(
@@ -678,21 +677,21 @@ private fun VersionChecks(viewModel: UIViewModel) {
},
)
} else if (curVer < MeshService.minDeviceVersion) {
Timber.w(
Logger.w {
"[FW_CHECK] Firmware should update - " +
"device: $curVer < min: ${MeshService.minDeviceVersion}",
)
"device: $curVer < min: ${MeshService.minDeviceVersion}"
}
val title = getString(Res.string.should_update_firmware)
val message = getString(Res.string.should_update, latestStableFirmwareRelease.asString)
viewModel.showAlert(title = title, message = message, dismissable = false, onConfirm = {})
} else {
Timber.i("[FW_CHECK] Firmware version OK - device: $curVer meets requirements")
Logger.i { "[FW_CHECK] Firmware version OK - device: $curVer meets requirements" }
}
} ?: run { Timber.w("[FW_CHECK] Firmware version is null despite myNodeInfo being present") }
} ?: run { Logger.w { "[FW_CHECK] Firmware version is null despite myNodeInfo being present" } }
}
} ?: run { Timber.d("[FW_CHECK] myNodeInfo is null, skipping firmware check") }
} ?: run { Logger.d { "[FW_CHECK] myNodeInfo is null, skipping firmware check" } }
} else {
Timber.d("[FW_CHECK] Not connected (state: $connectionState), skipping firmware check")
Logger.d { "[FW_CHECK] Not connected (state: $connectionState), skipping firmware check" }
}
}
}

View File

@@ -40,6 +40,7 @@ import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import co.touchlab.kermit.Logger
import com.geeksville.mesh.model.DeviceListEntry
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
@@ -58,7 +59,6 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.PaxcountProtos
import org.meshtastic.proto.TelemetryProtos
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
private const val RSSI_DELAY = 10
@@ -81,13 +81,13 @@ fun CurrentlyConnectedInfo(
rssi = withTimeout(RSSI_TIMEOUT.seconds) { bleDevice.peripheral.readRssi() }
delay(RSSI_DELAY.seconds)
} catch (e: PeripheralNotConnectedException) {
Timber.e(e, "Failed to read RSSI ${e.message}")
Logger.e(e) { "Failed to read RSSI ${e.message}" }
break
} catch (e: OperationFailedException) {
Timber.e(e, "Failed to read RSSI ${e.message}")
Logger.e(e) { "Failed to read RSSI ${e.message}" }
break
} catch (e: SecurityException) {
Timber.e(e, "Failed to read RSSI ${e.message}")
Logger.e(e) { "Failed to read RSSI ${e.message}" }
break
}
}

View File

@@ -87,6 +87,7 @@ import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -133,7 +134,6 @@ import org.meshtastic.proto.ChannelProtos
import org.meshtastic.proto.ConfigProtos
import org.meshtastic.proto.channelSet
import org.meshtastic.proto.copy
import timber.log.Timber
/**
* Composable screen for managing and sharing Meshtastic channels. Allows users to view, edit, and share channel
@@ -209,7 +209,7 @@ fun ChannelScreen(
}
fun zxingScan() {
Timber.d("Starting zxing QR code scanner")
Logger.d { "Starting zxing QR code scanner" }
val zxingScan = ScanOptions()
zxingScan.setCameraId(0)
zxingScan.setPrompt("")
@@ -236,7 +236,7 @@ fun ChannelScreen(
viewModel.setChannels(newChannelSet)
// Since we are writing to DeviceConfig, that will trigger the rest of the GUI update (QR code etc)
} catch (ex: RemoteException) {
Timber.e(ex, "ignoring channel problem")
Logger.e(ex) { "ignoring channel problem" }
channelSet = channels // Throw away user edits
@@ -264,7 +264,7 @@ fun ChannelScreen(
confirmButton = {
TextButton(
onClick = {
Timber.d("Switching back to default channel")
Logger.d { "Switching back to default channel" }
installSettings(
Channel.default.settings,
Channel.default.loraConfig.copy {

View File

@@ -21,6 +21,7 @@ import android.net.Uri
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -39,7 +40,6 @@ import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import org.meshtastic.proto.channelSet
import org.meshtastic.proto.config
import org.meshtastic.proto.copy
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
@@ -80,7 +80,7 @@ constructor(
fun requestChannelUrl(url: Uri, onError: () -> Unit) = runCatching { _requestChannelSet.value = url.toChannelSet() }
.onFailure { ex ->
Timber.e(ex, "Channel url error")
Logger.e(ex) { "Channel url error" }
onError()
}
@@ -101,7 +101,7 @@ constructor(
try {
serviceRepository.meshService?.setChannel(channel.toByteArray())
} catch (ex: RemoteException) {
Timber.e(ex, "Set channel error")
Logger.e(ex) { "Set channel error" }
}
}
@@ -110,7 +110,7 @@ constructor(
try {
serviceRepository.meshService?.setConfig(config.toByteArray())
} catch (ex: RemoteException) {
Timber.e(ex, "Set config error")
Logger.e(ex) { "Set config error" }
}
}

View File

@@ -19,7 +19,7 @@ package com.geeksville.mesh.util
import android.os.RemoteException
import android.util.Log
import timber.log.Timber
import co.touchlab.kermit.Logger
object Exceptions {
// / Set in Application.onCreate
@@ -31,10 +31,9 @@ object Exceptions {
* After reporting return
*/
fun report(exception: Throwable, tag: String? = null, message: String? = null) {
Timber.e(
exception,
"Exceptions.report: $tag $message",
) // print the message to the log _before_ telling the crash reporter
Logger.e(exception) {
"Exceptions.report: $tag $message"
} // print the message to the log _before_ telling the crash reporter
reporter?.let { r -> r(exception, tag, message) }
}
}
@@ -58,7 +57,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) Timber.e("ignoring exception", ex)
if (!silent) Logger.e(ex) { "ignoring exception" }
}
}

View File

@@ -46,7 +46,7 @@ dependencies {
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.navigation.runtime)
implementation(libs.timber)
implementation(libs.kermit)
googleApi(libs.dd.sdk.android.compose)
googleApi(libs.dd.sdk.android.logs)

View File

@@ -19,9 +19,10 @@ package org.meshtastic.core.analytics.platform
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import org.meshtastic.core.analytics.BuildConfig
import org.meshtastic.core.analytics.DataPair
import timber.log.Timber
import javax.inject.Inject
/**
@@ -34,16 +35,17 @@ class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
// In debug builds we attach a DebugTree for convenient local logging, but
// release builds rely on system logging only.
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
Timber.i("F-Droid platform no-op analytics initialized (DebugTree planted).")
Logger.setMinSeverity(Severity.Debug)
Logger.i { "F-Droid platform no-op analytics initialized (Debug mode }." }
} else {
Timber.i("F-Droid platform no-op analytics initialized.")
Logger.setMinSeverity(Severity.Info)
Logger.i { "F-Droid platform no-op analytics initialized." }
}
}
override fun setDeviceAttributes(firmwareVersion: String, model: String) {
// No-op for F-Droid
Timber.d("Set device attributes called: firmwareVersion=$firmwareVersion, deviceHardware=$model")
Logger.d { "Set device attributes called: firmwareVersion=$firmwareVersion, deviceHardware=$model" }
}
@Composable
@@ -51,7 +53,7 @@ class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
// No-op for F-Droid, but we can log navigation if needed for debugging
if (BuildConfig.DEBUG) {
navController.addOnDestinationChangedListener { _, destination, _ ->
Timber.d("Navigation changed to: ${destination.route}")
Logger.d { "Navigation changed to: ${destination.route}" }
}
}
}
@@ -60,6 +62,6 @@ class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
get() = false
override fun track(event: String, vararg properties: DataPair) {
Timber.d("Track called: event=$event, properties=${properties.toList()}")
Logger.d { "Track called: event=$event, properties=${properties.toList()}" }
}
}

View File

@@ -26,6 +26,8 @@ import androidx.compose.runtime.Composable
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import co.touchlab.kermit.LogWriter
import co.touchlab.kermit.Severity
import com.datadog.android.Datadog
import com.datadog.android.DatadogSite
import com.datadog.android.compose.ExperimentalTrackingApi
@@ -43,7 +45,6 @@ import com.datadog.android.rum.tracking.AcceptAllNavDestinations
import com.datadog.android.sessionreplay.SessionReplay
import com.datadog.android.sessionreplay.SessionReplayConfiguration
import com.datadog.android.sessionreplay.compose.ComposeExtensionSupport
import com.datadog.android.timber.DatadogTree
import com.datadog.android.trace.Trace
import com.datadog.android.trace.TraceConfiguration
import com.datadog.android.trace.opentelemetry.DatadogOpenTelemetry
@@ -61,10 +62,8 @@ import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.analytics.BuildConfig
import org.meshtastic.core.analytics.DataPair
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
import timber.log.Timber
import timber.log.Timber.DebugTree
import timber.log.Timber.Tree
import javax.inject.Inject
import co.touchlab.kermit.Logger as KermitLogger
/**
* Google Play Services specific implementation of [PlatformAnalytics]. This helper initializes and manages Firebase and
@@ -102,14 +101,15 @@ constructor(
.setBundleWithTraceEnabled(true)
.setBundleWithRumEnabled(true)
.build()
buildList {
add(DatadogTree(datadogLogger))
add(CrashlyticsTree())
val writers = buildList {
add(DatadogLogWriter(datadogLogger))
add(CrashlyticsLogWriter())
if (BuildConfig.DEBUG) {
add(DebugTree())
add(co.touchlab.kermit.LogcatWriter())
}
}
.forEach(Timber::plant)
KermitLogger.setLogWriters(writers)
KermitLogger.setMinSeverity(if (BuildConfig.DEBUG) Severity.Debug else Severity.Info)
// Initial consent state
updateAnalyticsConsent(analyticsPrefs.analyticsAllowed)
@@ -177,10 +177,10 @@ constructor(
*/
fun updateAnalyticsConsent(allowed: Boolean) {
if (!isPlatformServicesAvailable || isInTestLab) {
Timber.i("Analytics not available or in test lab, consent update skipped.")
KermitLogger.i { "Analytics not available or in test lab, consent update skipped." }
return
}
Timber.i(if (allowed) "Analytics enabled" else "Analytics disabled")
KermitLogger.i { if (allowed) "Analytics enabled" else "Analytics disabled" }
Datadog.setTrackingConsent(if (allowed) TrackingConsent.GRANTED else TrackingConsent.NOT_GRANTED)
Firebase.crashlytics.isCrashlyticsCollectionEnabled = allowed
@@ -221,30 +221,45 @@ constructor(
override val isPlatformServicesAvailable: Boolean
get() = isGooglePlayAvailable && isDatadogAvailable
private class CrashlyticsTree : Tree() {
private class CrashlyticsLogWriter : LogWriter() {
companion object {
private const val KEY_PRIORITY = "priority"
private const val KEY_TAG = "tag"
private const val KEY_MESSAGE = "message"
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
if (!Firebase.crashlytics.isCrashlyticsCollectionEnabled) return
Firebase.crashlytics.setCustomKeys {
key(KEY_PRIORITY, priority)
key(KEY_TAG, tag ?: "No Tag")
key(KEY_PRIORITY, severity.ordinal)
key(KEY_TAG, tag)
key(KEY_MESSAGE, message)
}
if (t == null) {
if (throwable == null) {
Firebase.crashlytics.recordException(Exception(message))
} else {
Firebase.crashlytics.recordException(t)
Firebase.crashlytics.recordException(throwable)
}
}
}
private class DatadogLogWriter(private val datadogLogger: Logger) : LogWriter() {
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
val datadogPriority =
when (severity) {
Severity.Verbose -> android.util.Log.VERBOSE
Severity.Debug -> android.util.Log.DEBUG
Severity.Info -> android.util.Log.INFO
Severity.Warn -> android.util.Log.WARN
Severity.Error -> android.util.Log.ERROR
Severity.Assert -> android.util.Log.ASSERT
}
datadogLogger.log(datadogPriority, message, throwable, mapOf("tag" to tag))
}
}
private fun String.extractSemanticVersion(): String {
val regex = "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?".toRegex()
val matchResult = regex.find(this)
@@ -263,7 +278,7 @@ constructor(
is String -> bundle.putString(it.name, it.value as String?) // Explicitly handle String
else -> bundle.putString(it.name, it.value.toString()) // Fallback for other types
}
Timber.tag(TAG).d("Analytics: track $event (${it.name} : ${it.value})")
KermitLogger.withTag(TAG).d { "Analytics: track $event (${it.name} : ${it.value})" }
}
Firebase.analytics.logEvent(event, bundle)
}

View File

@@ -61,5 +61,5 @@ dependencies {
implementation(libs.androidx.core.location.altitude)
implementation(libs.androidx.paging.common)
implementation(libs.kotlinx.serialization.json)
implementation(libs.timber)
implementation(libs.kermit)
}

View File

@@ -6,6 +6,11 @@
<ID>MagicNumber:LocationRepository.kt$LocationRepository$30</ID>
<ID>MagicNumber:LocationRepository.kt$LocationRepository$31</ID>
<ID>MagicNumber:PacketRepository.kt$PacketRepository$500</ID>
<ID>MaxLineLength:DeviceHardwareRepository.kt$DeviceHardwareRepository$"DeviceHardwareRepository: applying quirk: requiresBootloaderUpgradeForOta=${quirk.requiresBootloaderUpgradeForOta}, infoUrl=${quirk.infoUrl}"</ID>
<ID>MaxLineLength:DeviceHardwareRepository.kt$DeviceHardwareRepository$"DeviceHardwareRepository: cache ${if (staleEntity == null) "empty" else "incomplete"} for hwModel=$hwModel, falling back to bundled JSON asset"</ID>
<ID>MaxLineLength:DeviceHardwareRepository.kt$DeviceHardwareRepository$"DeviceHardwareRepository: failed to load device hardware from bundled JSON for hwModel=$hwModel"</ID>
<ID>MaxLineLength:DeviceHardwareRepository.kt$DeviceHardwareRepository$"DeviceHardwareRepository: lookup after JSON load for hwModel=$hwModel ${if (base != null) "succeeded" else "returned null"}"</ID>
<ID>MaxLineLength:DeviceHardwareRepository.kt$DeviceHardwareRepository$"DeviceHardwareRepository: lookup after remote fetch for hwModel=$hwModel ${if (fromDb != null) "succeeded" else "returned null"}"</ID>
<ID>TooGenericExceptionCaught:LocationRepository.kt$LocationRepository$e: Exception</ID>
<ID>TooManyFunctions:PacketRepository.kt$PacketRepository</ID>
</CurrentIssues>

View File

@@ -17,6 +17,7 @@
package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -26,10 +27,8 @@ import kotlinx.serialization.json.Json
import org.meshtastic.core.data.model.CustomTileProviderConfig
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.map.MapTileProviderPrefs
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.collections.plus
interface CustomTileProviderRepository {
fun getCustomTileProviders(): Flow<List<CustomTileProviderConfig>>
@@ -88,7 +87,7 @@ constructor(
try {
customTileProvidersStateFlow.value = json.decodeFromString<List<CustomTileProviderConfig>>(jsonString)
} catch (e: SerializationException) {
Timber.e(e, "Error deserializing tile providers")
Logger.e(e) { "Error deserializing tile providers" }
customTileProvidersStateFlow.value = emptyList()
}
} else {
@@ -102,7 +101,7 @@ constructor(
val jsonString = json.encodeToString(providers)
mapTileProviderPrefs.customTileProviders = jsonString
} catch (e: SerializationException) {
Timber.e(e, "Error serializing tile providers")
Logger.e(e) { "Error serializing tile providers" }
}
}
}

View File

@@ -18,12 +18,12 @@
package org.meshtastic.core.data.datasource
import android.app.Application
import co.touchlab.kermit.Logger
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.meshtastic.core.model.BootloaderOtaQuirk
import timber.log.Timber
import javax.inject.Inject
class BootloaderOtaQuirksJsonDataSource @Inject constructor(private val application: Application) {
@@ -32,7 +32,7 @@ class BootloaderOtaQuirksJsonDataSource @Inject constructor(private val applicat
val inputStream = application.assets.open("device_bootloader_ota_quirks.json")
inputStream.use { Json.decodeFromStream<ListWrapper>(it).devices }
}
.onFailure { e -> Timber.w(e, "Failed to load device_bootloader_ota_quirks.json") }
.onFailure { e -> Logger.w(e) { "Failed to load device_bootloader_ota_quirks.json" } }
.getOrDefault(emptyList())
@Serializable private data class ListWrapper(val devices: List<BootloaderOtaQuirk> = emptyList())

View File

@@ -17,6 +17,7 @@
package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource
@@ -27,7 +28,6 @@ import org.meshtastic.core.database.entity.asExternalModel
import org.meshtastic.core.model.BootloaderOtaQuirk
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.network.DeviceHardwareRemoteDataSource
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@@ -59,45 +59,40 @@ constructor(
@Suppress("LongMethod")
suspend fun getDeviceHardwareByModel(hwModel: Int, forceRefresh: Boolean = false): Result<DeviceHardware?> =
withContext(Dispatchers.IO) {
Timber.d(
"DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=%d, forceRefresh=%b)",
hwModel,
forceRefresh,
)
Logger.d {
"DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=$hwModel, forceRefresh=$forceRefresh)"
}
val quirks = loadQuirks()
if (forceRefresh) {
Timber.d("DeviceHardwareRepository: forceRefresh=true, clearing local device hardware cache")
Logger.d { "DeviceHardwareRepository: forceRefresh=true, clearing local device hardware cache" }
localDataSource.deleteAllDeviceHardware()
} else {
// 1. Attempt to retrieve from cache first
val cachedEntity = localDataSource.getByHwModel(hwModel)
if (cachedEntity != null && !cachedEntity.isStale()) {
Timber.d("DeviceHardwareRepository: using fresh cached device hardware for hwModel=%d", hwModel)
Logger.d { "DeviceHardwareRepository: using fresh cached device hardware for hwModel=$hwModel" }
return@withContext Result.success(
applyBootloaderQuirk(hwModel, cachedEntity.asExternalModel(), quirks),
)
}
Timber.d("DeviceHardwareRepository: no fresh cache for hwModel=%d, attempting remote fetch", hwModel)
Logger.d { "DeviceHardwareRepository: no fresh cache for hwModel=$hwModel, attempting remote fetch" }
}
// 2. Fetch from remote API
runCatching {
Timber.d("DeviceHardwareRepository: fetching device hardware from remote API")
Logger.d { "DeviceHardwareRepository: fetching device hardware from remote API" }
val remoteHardware = remoteDataSource.getAllDeviceHardware()
Timber.d(
"DeviceHardwareRepository: remote API returned %d device hardware entries",
remoteHardware.size,
)
Logger.d {
"DeviceHardwareRepository: remote API returned ${remoteHardware.size} device hardware entries"
}
localDataSource.insertAllDeviceHardware(remoteHardware)
val fromDb = localDataSource.getByHwModel(hwModel)?.asExternalModel()
Timber.d(
"DeviceHardwareRepository: lookup after remote fetch for hwModel=%d %s",
hwModel,
if (fromDb != null) "succeeded" else "returned null",
)
Logger.d {
"DeviceHardwareRepository: lookup after remote fetch for hwModel=$hwModel ${if (fromDb != null) "succeeded" else "returned null"}"
}
fromDb
}
.onSuccess {
@@ -105,57 +100,48 @@ constructor(
return@withContext Result.success(applyBootloaderQuirk(hwModel, it, quirks))
}
.onFailure { e ->
Timber.w(
e,
"DeviceHardwareRepository: failed to fetch device hardware from server for hwModel=%d",
hwModel,
)
Logger.w(e) {
"DeviceHardwareRepository: failed to fetch device hardware from server for hwModel=$hwModel"
}
// 3. Attempt to use stale cache as a fallback, but only if it looks complete.
val staleEntity = localDataSource.getByHwModel(hwModel)
if (staleEntity != null && !staleEntity.isIncomplete()) {
Timber.d("DeviceHardwareRepository: using stale cached device hardware for hwModel=%d", hwModel)
Logger.d { "DeviceHardwareRepository: using stale cached device hardware for hwModel=$hwModel" }
return@withContext Result.success(
applyBootloaderQuirk(hwModel, staleEntity.asExternalModel(), quirks),
)
}
// 4. Fallback to bundled JSON if cache is empty or incomplete
Timber.d(
"DeviceHardwareRepository: cache %s for hwModel=%d, falling back to bundled JSON asset",
if (staleEntity == null) "empty" else "incomplete",
hwModel,
)
Logger.d {
"DeviceHardwareRepository: cache ${if (staleEntity == null) "empty" else "incomplete"} for hwModel=$hwModel, falling back to bundled JSON asset"
}
return@withContext loadFromBundledJson(hwModel, quirks)
}
}
private suspend fun loadFromBundledJson(hwModel: Int, quirks: List<BootloaderOtaQuirk>): Result<DeviceHardware?> =
runCatching {
Timber.d("DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=%d", hwModel)
Logger.d { "DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=$hwModel" }
val jsonHardware = jsonDataSource.loadDeviceHardwareFromJsonAsset()
Timber.d(
"DeviceHardwareRepository: bundled JSON returned %d device hardware entries",
jsonHardware.size,
)
Logger.d {
"DeviceHardwareRepository: bundled JSON returned ${jsonHardware.size} device hardware entries"
}
localDataSource.insertAllDeviceHardware(jsonHardware)
val base = localDataSource.getByHwModel(hwModel)?.asExternalModel()
Timber.d(
"DeviceHardwareRepository: lookup after JSON load for hwModel=%d %s",
hwModel,
if (base != null) "succeeded" else "returned null",
)
Logger.d {
"DeviceHardwareRepository: lookup after JSON load for hwModel=$hwModel ${if (base != null) "succeeded" else "returned null"}"
}
applyBootloaderQuirk(hwModel, base, quirks)
}
.also { result ->
result.exceptionOrNull()?.let { e ->
Timber.e(
e,
"DeviceHardwareRepository: failed to load device hardware from bundled JSON for hwModel=%d",
hwModel,
)
Logger.e(e) {
"DeviceHardwareRepository: failed to load device hardware from bundled JSON for hwModel=$hwModel"
}
}
}
@@ -174,7 +160,7 @@ constructor(
private fun loadQuirks(): List<BootloaderOtaQuirk> {
val quirks = bootloaderOtaQuirksJsonDataSource.loadBootloaderOtaQuirksFromJsonAsset()
Timber.d("DeviceHardwareRepository: loaded %d bootloader quirks", quirks.size)
Logger.d { "DeviceHardwareRepository: loaded ${quirks.size} bootloader quirks" }
return quirks
}
@@ -186,17 +172,11 @@ constructor(
if (base == null) return null
val quirk = quirks.firstOrNull { it.hwModel == hwModel }
Timber.d(
"DeviceHardwareRepository: applyBootloaderQuirk for hwModel=%d, quirk found=%b",
hwModel,
quirk != null,
)
Logger.d { "DeviceHardwareRepository: applyBootloaderQuirk for hwModel=$hwModel, quirk found=${quirk != null}" }
return if (quirk != null) {
Timber.d(
"DeviceHardwareRepository: applying quirk: requiresBootloaderUpgradeForOta=%b, infoUrl=%s",
quirk.requiresBootloaderUpgradeForOta,
quirk.infoUrl,
)
Logger.d {
"DeviceHardwareRepository: applying quirk: requiresBootloaderUpgradeForOta=${quirk.requiresBootloaderUpgradeForOta}, infoUrl=${quirk.infoUrl}"
}
base.copy(
requiresBootloaderUpgradeForOta = quirk.requiresBootloaderUpgradeForOta,
bootloaderInfoUrl = quirk.infoUrl,

View File

@@ -17,6 +17,7 @@
package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSource
@@ -26,7 +27,6 @@ import org.meshtastic.core.database.entity.FirmwareReleaseEntity
import org.meshtastic.core.database.entity.FirmwareReleaseType
import org.meshtastic.core.database.entity.asExternalModel
import org.meshtastic.core.network.FirmwareReleaseRemoteDataSource
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@@ -68,7 +68,7 @@ constructor(
// This gives the UI something to show immediately.
val cachedRelease = localDataSource.getLatestRelease(releaseType)
cachedRelease?.let {
Timber.d("Emitting cached firmware for $releaseType (isStale=${it.isStale()})")
Logger.d { "Emitting cached firmware for $releaseType (isStale=${it.isStale()})" }
emit(it.asExternalModel())
}
@@ -84,7 +84,7 @@ constructor(
// The `distinctUntilChanged()` operator on the collector side will prevent
// re-emitting the same data if the cache wasn't actually updated.
val finalRelease = localDataSource.getLatestRelease(releaseType)
Timber.d("Emitting final firmware for $releaseType from cache.")
Logger.d { "Emitting final firmware for $releaseType from cache." }
emit(finalRelease?.asExternalModel())
}
@@ -98,7 +98,7 @@ constructor(
private suspend fun updateCacheFromSources() {
val remoteFetchSuccess =
runCatching {
Timber.d("Fetching fresh firmware releases from remote API.")
Logger.d { "Fetching fresh firmware releases from remote API." }
val networkReleases = remoteDataSource.getFirmwareReleases()
// The API fetches all release types, so we cache them all at once.
@@ -109,13 +109,13 @@ constructor(
// If remote fetch failed, try the JSON fallback as a last resort.
if (!remoteFetchSuccess) {
Timber.w("Remote fetch failed, attempting to cache from bundled JSON.")
Logger.w { "Remote fetch failed, attempting to cache from bundled JSON." }
runCatching {
val jsonReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
localDataSource.insertFirmwareReleases(jsonReleases.releases.stable, FirmwareReleaseType.STABLE)
localDataSource.insertFirmwareReleases(jsonReleases.releases.alpha, FirmwareReleaseType.ALPHA)
}
.onFailure { Timber.w("Failed to cache from JSON: ${it.message}") }
.onFailure { Logger.w { "Failed to cache from JSON: ${it.message}" } }
}
}

View File

@@ -27,6 +27,7 @@ import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.location.LocationRequestCompat
import androidx.core.location.altitude.AltitudeConverterCompat
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
@@ -34,7 +35,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -68,7 +68,7 @@ constructor(
try {
AltitudeConverterCompat.addMslAltitudeToLocation(context, location)
} catch (e: Exception) {
Timber.e(e, "addMslAltitudeToLocation() failed")
Logger.e(e) { "addMslAltitudeToLocation() failed" }
}
}
// info("New location: $location")
@@ -85,9 +85,9 @@ constructor(
}
}
Timber.i(
"Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m",
)
Logger.i {
"Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m"
}
_receivingLocationUpdates.value = true
analytics.track("location_start") // Figure out how many users needed to use the phone GPS
@@ -106,7 +106,7 @@ constructor(
}
awaitClose {
Timber.i("Stopping location requests")
Logger.i { "Stopping location requests" }
_receivingLocationUpdates.value = false
analytics.track("location_stop")

View File

@@ -55,7 +55,7 @@ dependencies {
implementation(libs.androidx.room.paging)
implementation(libs.kotlinx.serialization.json)
implementation(libs.timber)
implementation(libs.kermit)
ksp(libs.androidx.room.compiler)

View File

@@ -18,6 +18,7 @@
package org.meshtastic.core.database
import androidx.room.TypeConverter
import co.touchlab.kermit.Logger
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import kotlinx.serialization.json.Json
@@ -25,7 +26,6 @@ import org.meshtastic.core.model.DataPacket
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.PaxcountProtos
import org.meshtastic.proto.TelemetryProtos
import timber.log.Timber
@Suppress("TooManyFunctions")
class Converters {
@@ -45,7 +45,7 @@ class Converters {
fun bytesToFromRadio(bytes: ByteArray): MeshProtos.FromRadio = try {
MeshProtos.FromRadio.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToFromRadio TypeConverter error")
Logger.e(ex) { "bytesToFromRadio TypeConverter error" }
MeshProtos.FromRadio.getDefaultInstance()
}
@@ -55,7 +55,7 @@ class Converters {
fun bytesToUser(bytes: ByteArray): MeshProtos.User = try {
MeshProtos.User.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToUser TypeConverter error")
Logger.e(ex) { "bytesToUser TypeConverter error" }
MeshProtos.User.getDefaultInstance()
}
@@ -65,7 +65,7 @@ class Converters {
fun bytesToPosition(bytes: ByteArray): MeshProtos.Position = try {
MeshProtos.Position.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToPosition TypeConverter error")
Logger.e(ex) { "bytesToPosition TypeConverter error" }
MeshProtos.Position.getDefaultInstance()
}
@@ -75,7 +75,7 @@ class Converters {
fun bytesToTelemetry(bytes: ByteArray): TelemetryProtos.Telemetry = try {
TelemetryProtos.Telemetry.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToTelemetry TypeConverter error")
Logger.e(ex) { "bytesToTelemetry TypeConverter error" }
TelemetryProtos.Telemetry.newBuilder().build() // Return an empty Telemetry object
}
@@ -85,7 +85,7 @@ class Converters {
fun bytesToPaxcounter(bytes: ByteArray): PaxcountProtos.Paxcount = try {
PaxcountProtos.Paxcount.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToPaxcounter TypeConverter error")
Logger.e(ex) { "bytesToPaxcounter TypeConverter error" }
PaxcountProtos.Paxcount.getDefaultInstance()
}
@@ -95,7 +95,7 @@ class Converters {
fun bytesToMetadata(bytes: ByteArray): MeshProtos.DeviceMetadata = try {
MeshProtos.DeviceMetadata.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToMetadata TypeConverter error")
Logger.e(ex) { "bytesToMetadata TypeConverter error" }
MeshProtos.DeviceMetadata.getDefaultInstance()
}

View File

@@ -21,6 +21,7 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -33,7 +34,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.security.MessageDigest
import javax.inject.Inject
@@ -106,7 +106,7 @@ class DatabaseManager @Inject constructor(private val app: Application) {
// One-time cleanup: remove legacy DB if present and not active
managerScope.launch(Dispatchers.IO) { cleanupLegacyDbIfNeeded(activeDbName = dbName) }
Timber.i("Switched active DB to ${anonymizeDbName(dbName)} for address ${anonymizeAddress(address)}")
Logger.i { "Switched active DB to ${anonymizeDbName(dbName)} for address ${anonymizeAddress(address)}" }
}
/** Execute [block] with the current DB instance. */
@@ -138,26 +138,28 @@ class DatabaseManager @Inject constructor(private val app: Application) {
// Only enforce the limit over device-specific DBs; exclude legacy and default DBs
val deviceDbs =
all.filterNot { it == DatabaseConstants.LEGACY_DB_NAME || it == DatabaseConstants.DEFAULT_DB_NAME }
Timber.d(
"LRU check: limit=%d, active=%s, deviceDbs=%s",
limit,
anonymizeDbName(activeDbName),
deviceDbs.joinToString(", ") { anonymizeDbName(it) },
)
Logger.d {
"LRU check: limit=$limit, active=${anonymizeDbName(
activeDbName,
)}, deviceDbs=${deviceDbs.joinToString(", ") {
anonymizeDbName(it)
}}"
}
if (deviceDbs.size <= limit) return
val usageSnapshot = deviceDbs.associateWith { lastUsed(it) }
Timber.d(
"LRU lastUsed(ms): %s",
usageSnapshot.entries.joinToString(", ") { (name, ts) -> "${anonymizeDbName(name)}=$ts" },
)
Logger.d {
"LRU lastUsed(ms): ${usageSnapshot.entries.joinToString(", ") { (name, ts) ->
"${anonymizeDbName(name)}=$ts"
}}"
}
val victims = selectEvictionVictims(deviceDbs, activeDbName, limit, usageSnapshot)
Timber.i("LRU victims: %s", victims.joinToString(", ") { anonymizeDbName(it) })
Logger.i { "LRU victims: ${victims.joinToString(", ") { anonymizeDbName(it) }}" }
victims.forEach { name ->
runCatching { dbCache.remove(name)?.close() }
.onFailure { Timber.w(it, "Failed to close database %s", name) }
.onFailure { Logger.w(it) { "Failed to close database $name" } }
app.deleteDatabase(name)
prefs.edit().remove(lastUsedKey(name)).apply()
Timber.i("Evicted cached DB ${anonymizeDbName(name)}")
Logger.i { "Evicted cached DB ${anonymizeDbName(name)}" }
}
}
@@ -186,12 +188,12 @@ class DatabaseManager @Inject constructor(private val app: Application) {
val legacyFile = getDbFile(app, legacy)
if (legacyFile != null) {
runCatching { dbCache.remove(legacy)?.close() }
.onFailure { Timber.w(it, "Failed to close legacy database %s before deletion", legacy) }
.onFailure { Logger.w(it) { "Failed to close legacy database $legacy before deletion" } }
val deleted = app.deleteDatabase(legacy)
if (deleted) {
Timber.i("Deleted legacy DB ${anonymizeDbName(legacy)}")
Logger.i { "Deleted legacy DB ${anonymizeDbName(legacy)}" }
} else {
Timber.w("Attempted to delete legacy DB %s but deleteDatabase returned false", legacy)
Logger.w { "Attempted to delete legacy DB $legacy but deleteDatabase returned false" }
}
}
prefs.edit().putBoolean(DatabaseConstants.LEGACY_DB_CLEANED_KEY, true).apply()

View File

@@ -48,5 +48,5 @@ dependencies {
implementation(libs.androidx.datastore)
implementation(libs.androidx.datastore.preferences)
implementation(libs.kotlinx.serialization.json)
implementation(libs.timber)
implementation(libs.kermit)
}

View File

@@ -21,11 +21,11 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -43,9 +43,9 @@ class BootloaderWarningDataSource @Inject constructor(private val dataStore: Dat
runCatching { Json.decodeFromString<List<String>>(jsonString).toSet() }
.onFailure { e ->
if (e is IllegalArgumentException || e is SerializationException) {
Timber.w(e, "Failed to parse dismissed bootloader warning addresses, resetting preference")
Logger.w(e) { "Failed to parse dismissed bootloader warning addresses, resetting preference" }
} else {
Timber.w(e, "Unexpected error while parsing dismissed bootloader warning addresses")
Logger.w(e) { "Unexpected error while parsing dismissed bootloader warning addresses" }
}
}
.getOrDefault(emptySet())

View File

@@ -18,13 +18,13 @@
package org.meshtastic.core.datastore
import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import org.meshtastic.proto.ChannelProtos.Channel
import org.meshtastic.proto.ChannelProtos.ChannelSettings
import org.meshtastic.proto.ConfigProtos
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@@ -36,7 +36,7 @@ class ChannelSetDataSource @Inject constructor(private val channelSetStore: Data
channelSetStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Timber.e("Error reading DeviceConfig settings: ${exception.message}")
Logger.e { "Error reading DeviceConfig settings: ${exception.message}" }
emit(ChannelSet.getDefaultInstance())
} else {
throw exception

View File

@@ -18,11 +18,11 @@
package org.meshtastic.core.datastore
import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import org.meshtastic.proto.ConfigProtos.Config
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@@ -34,7 +34,7 @@ class LocalConfigDataSource @Inject constructor(private val localConfigStore: Da
localConfigStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Timber.e("Error reading LocalConfig settings: ${exception.message}")
Logger.e { "Error reading LocalConfig settings: ${exception.message}" }
emit(LocalConfig.getDefaultInstance())
} else {
throw exception
@@ -53,7 +53,7 @@ class LocalConfigDataSource @Inject constructor(private val localConfigStore: Da
if (localField != null) {
builder.setField(localField, value)
} else {
Timber.e("Error writing LocalConfig settings: ${config.payloadVariantCase}")
Logger.e { "Error writing LocalConfig settings: ${config.payloadVariantCase}" }
}
}
builder.build()

View File

@@ -18,11 +18,11 @@
package org.meshtastic.core.datastore
import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import org.meshtastic.proto.LocalOnlyProtos.LocalModuleConfig
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@@ -34,7 +34,7 @@ class ModuleConfigDataSource @Inject constructor(private val moduleConfigStore:
moduleConfigStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Timber.e("Error reading LocalModuleConfig settings: ${exception.message}")
Logger.e { "Error reading LocalModuleConfig settings: ${exception.message}" }
emit(LocalModuleConfig.getDefaultInstance())
} else {
throw exception
@@ -53,7 +53,7 @@ class ModuleConfigDataSource @Inject constructor(private val moduleConfigStore:
if (localField != null) {
builder.setField(localField, value)
} else {
Timber.e("Error writing LocalModuleConfig settings: ${config.payloadVariantCase}")
Logger.e { "Error writing LocalModuleConfig settings: ${config.payloadVariantCase}" }
}
}
builder.build()

View File

@@ -21,6 +21,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@@ -29,7 +30,6 @@ import kotlinx.serialization.json.Json
import org.json.JSONArray
import org.json.JSONObject
import org.meshtastic.core.datastore.model.RecentAddress
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -46,11 +46,11 @@ class RecentAddressesDataSource @Inject constructor(private val dataStore: DataS
try {
Json.decodeFromString<List<RecentAddress>>(jsonString)
} catch (e: IllegalArgumentException) {
Timber.w("Could not parse recent addresses, falling back to legacy parsing: ${e.message}")
Logger.w { "Could not parse recent addresses, falling back to legacy parsing: ${e.message}" }
// Fallback to legacy parsing
parseLegacyRecentAddresses(jsonString)
} catch (e: SerializationException) {
Timber.w("Could not parse recent addresses, falling back to legacy parsing: ${e.message}")
Logger.w { "Could not parse recent addresses, falling back to legacy parsing: ${e.message}" }
// Fallback to legacy parsing
parseLegacyRecentAddresses(jsonString)
}
@@ -73,7 +73,7 @@ class RecentAddressesDataSource @Inject constructor(private val dataStore: DataS
}
else -> {
// Unknown format, log or handle as an error if necessary
Timber.w("Unknown item type in recent IP addresses: $item")
Logger.w { "Unknown item type in recent IP addresses: $item" }
null
}
}

View File

@@ -55,7 +55,7 @@ dependencies {
implementation(libs.androidx.annotation)
implementation(libs.kotlinx.serialization.json)
implementation(libs.timber)
implementation(libs.kermit)
implementation(libs.zxing.android.embedded) { isTransitive = false }
implementation(libs.zxing.core)

View File

@@ -2,30 +2,6 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MagicNumber:Channel.kt$0xff</ID>
<ID>MagicNumber:ChannelOption.kt$.03125f</ID>
<ID>MagicNumber:ChannelOption.kt$.0625f</ID>
<ID>MagicNumber:ChannelOption.kt$.203125f</ID>
<ID>MagicNumber:ChannelOption.kt$.40625f</ID>
<ID>MagicNumber:ChannelOption.kt$.8125f</ID>
<ID>MagicNumber:ChannelOption.kt$1.6250f</ID>
<ID>MagicNumber:ChannelOption.kt$1000f</ID>
<ID>MagicNumber:ChannelOption.kt$1600</ID>
<ID>MagicNumber:ChannelOption.kt$200</ID>
<ID>MagicNumber:ChannelOption.kt$3.25f</ID>
<ID>MagicNumber:ChannelOption.kt$31</ID>
<ID>MagicNumber:ChannelOption.kt$400</ID>
<ID>MagicNumber:ChannelOption.kt$5</ID>
<ID>MagicNumber:ChannelOption.kt$62</ID>
<ID>MagicNumber:ChannelOption.kt$800</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_MODERATE$.125f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_SLOW$.125f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_SLOW$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.SHORT_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.SHORT_SLOW$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.VERY_LONG_SLOW$.0625f</ID>
<ID>MagicNumber:ChannelSet.kt$40</ID>
<ID>MagicNumber:ChannelSet.kt$960</ID>
<ID>SwallowedException:ChannelSet.kt$ex: Throwable</ID>

View File

@@ -17,7 +17,7 @@
package org.meshtastic.core.model
import timber.log.Timber
import co.touchlab.kermit.Logger
/** Provide structured access to parse and compare device version strings */
data class DeviceVersion(val asString: String) : Comparable<DeviceVersion> {
@@ -28,7 +28,7 @@ data class DeviceVersion(val asString: String) : Comparable<DeviceVersion> {
try {
verStringToInt(asString)
} catch (e: Exception) {
Timber.w("Exception while parsing version '$asString', assuming version 0")
Logger.w { "Exception while parsing version '$asString', assuming version 0" }
0
}

View File

@@ -20,14 +20,13 @@ package org.meshtastic.core.model.util
import android.graphics.Bitmap
import android.net.Uri
import android.util.Base64
import co.touchlab.kermit.Logger
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder
import org.meshtastic.core.model.Channel
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import timber.log.Timber
import java.net.MalformedURLException
import kotlin.jvm.Throws
private const val MESHTASTIC_HOST = "meshtastic.org"
private const val CHANNEL_PATH = "/e/"
@@ -86,6 +85,6 @@ fun ChannelSet.qrCode(shouldAdd: Boolean): Bitmap? = try {
val barcodeEncoder = BarcodeEncoder()
barcodeEncoder.createBitmap(bitMatrix)
} catch (ex: Throwable) {
Timber.e("URL was too complex to render as barcode")
Logger.e { "URL was too complex to render as barcode" }
null
}

View File

@@ -46,5 +46,5 @@ dependencies {
implementation(projects.core.proto)
implementation(libs.javax.inject)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.timber)
implementation(libs.kermit)
}

View File

@@ -17,6 +17,7 @@
package org.meshtastic.core.service
import co.touchlab.kermit.Logger
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -25,7 +26,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.MeshProtos.MeshPacket
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -66,7 +66,7 @@ class ServiceRepository @Inject constructor() {
get() = _clientNotification
fun setClientNotification(notification: MeshProtos.ClientNotification?) {
Timber.e(notification?.message.orEmpty())
Logger.e { notification?.message.orEmpty() }
_clientNotification.value = notification
}
@@ -80,7 +80,7 @@ class ServiceRepository @Inject constructor() {
get() = _errorMessage
fun setErrorMessage(text: String) {
Timber.e(text)
Logger.e { text }
_errorMessage.value = text
}

View File

@@ -62,7 +62,7 @@ dependencies {
implementation(libs.guava)
implementation(libs.zxing.core)
implementation(libs.zxing.android.embedded)
implementation(libs.timber)
implementation(libs.kermit)
debugImplementation(libs.androidx.compose.ui.test.manifest)

View File

@@ -2,18 +2,6 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComposableParamOrder:AlertDialogs.kt$SimpleAlertDialog</ID>
<ID>ComposableParamOrder:EditBase64Preference.kt$EditBase64Preference</ID>
<ID>ComposableParamOrder:EditTextPreference.kt$EditTextPreference</ID>
<ID>ComposableParamOrder:MainAppBar.kt$MainAppBar</ID>
<ID>ComposableParamOrder:MaterialBatteryInfo.kt$MaterialBatteryInfo</ID>
<ID>ComposableParamOrder:NodeChip.kt$NodeChip</ID>
<ID>ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon</ID>
<ID>ComposableParamOrder:SignalInfo.kt$SignalInfo</ID>
<ID>ComposableParamOrder:SwitchPreference.kt$SwitchPreference</ID>
<ID>ContentSlotReused:AdaptiveTwoPane.kt$second</ID>
<ID>LambdaParameterEventTrailing:ContactSharing.kt$onSharedContactRequested</ID>
<ID>LambdaParameterEventTrailing:MainAppBar.kt$onClickChip</ID>
<ID>MagicNumber:EditIPv4Preference.kt$0xff</ID>
<ID>MagicNumber:EditIPv4Preference.kt$16</ID>
<ID>MagicNumber:EditIPv4Preference.kt$24</ID>
@@ -22,47 +10,5 @@
<ID>MagicNumber:EditListPreference.kt$12345</ID>
<ID>MagicNumber:EditListPreference.kt$67890</ID>
<ID>MagicNumber:LazyColumnDragAndDropDemo.kt$50</ID>
<ID>ModifierMissing:AdaptiveTwoPane.kt$AdaptiveTwoPane</ID>
<ID>ModifierMissing:ChannelItem.kt$ChannelItem</ID>
<ID>ModifierMissing:ChannelSelection.kt$ChannelSelection</ID>
<ID>ModifierMissing:ContactSharing.kt$SharedContactDialog</ID>
<ID>ModifierMissing:EmojiPicker.kt$EmojiPicker</ID>
<ID>ModifierMissing:EmojiPicker.kt$EmojiPickerDialog</ID>
<ID>ModifierMissing:IndoorAirQuality.kt$IndoorAirQuality</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$LoraSignalIndicator</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$Rssi</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$Snr</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$SnrAndRssi</ID>
<ID>ModifierMissing:PreferenceDivider.kt$PreferenceDivider</ID>
<ID>ModifierMissing:SecurityIcon.kt$SecurityIcon</ID>
<ID>ModifierMissing:SharedContactDialog.kt$SharedContactDialog</ID>
<ID>ModifierMissing:SimpleAlertDialog.kt$SimpleAlertDialog</ID>
<ID>ModifierMissing:SlidingSelector.kt$OptionLabel</ID>
<ID>ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier.fillMaxWidth().padding(all = 16.dp)</ID>
<ID>ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End)</ID>
<ID>ModifierReused:PreferenceCategory.kt$Card(modifier = modifier.padding(bottom = 8.dp)) { Column( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { ProvideTextStyle(MaterialTheme.typography.bodyLarge) { content() } } }</ID>
<ID>ModifierReused:PreferenceCategory.kt$Text( text, modifier = modifier.padding(start = 16.dp, top = 24.dp, bottom = 8.dp, end = 16.dp), style = MaterialTheme.typography.titleLarge, )</ID>
<ID>ModifierReused:TextDividerPreference.kt$Card(modifier = modifier.fillMaxWidth()) { Row(modifier = modifier.fillMaxWidth().padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically) { Text( text = title, style = MaterialTheme.typography.bodyLarge, color = if (!enabled) { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } else { Color.Unspecified }, ) if (trailingIcon != null) { Icon(trailingIcon, "trailingIcon", modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) } } }</ID>
<ID>ModifierReused:TextDividerPreference.kt$Icon(trailingIcon, "trailingIcon", modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End))</ID>
<ID>ModifierReused:TextDividerPreference.kt$Row(modifier = modifier.fillMaxWidth().padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically) { Text( text = title, style = MaterialTheme.typography.bodyLarge, color = if (!enabled) { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } else { Color.Unspecified }, ) if (trailingIcon != null) { Icon(trailingIcon, "trailingIcon", modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) } }</ID>
<ID>MultipleEmitters:PreferenceCategory.kt$PreferenceCategory</ID>
<ID>ParameterNaming:BitwisePreference.kt$onItemSelected</ID>
<ID>ParameterNaming:ChannelSelection.kt$onSelected</ID>
<ID>ParameterNaming:ContactSharing.kt$onSharedContactRequested</ID>
<ID>ParameterNaming:DropDownPreference.kt$onItemSelected</ID>
<ID>ParameterNaming:EditIPv4Preference.kt$onValueChanged</ID>
<ID>ParameterNaming:EditListPreference.kt$onValuesChanged</ID>
<ID>ParameterNaming:EditPasswordPreference.kt$onValueChanged</ID>
<ID>ParameterNaming:EditTextPreference.kt$onValueChanged</ID>
<ID>ParameterNaming:PositionPrecisionPreference.kt$onValueChanged</ID>
<ID>ParameterNaming:PreferenceFooter.kt$onNegativeClicked</ID>
<ID>ParameterNaming:PreferenceFooter.kt$onPositiveClicked</ID>
<ID>ParameterNaming:SlidingSelector.kt$onOptionSelected</ID>
<ID>PreviewPublic:IndoorAirQuality.kt$IAQScalePreview</ID>
<ID>PreviewPublic:LazyColumnDragAndDropDemo.kt$LazyColumnDragAndDropDemo</ID>
<ID>PreviewPublic:MaterialBatteryInfo.kt$MaterialBatteryInfoPreview</ID>
<ID>PreviewPublic:SignalInfo.kt$SignalInfoPreview</ID>
<ID>PreviewPublic:SignalInfo.kt$SignalInfoSelfPreview</ID>
<ID>PreviewPublic:SignalInfo.kt$SignalInfoSimplePreview</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -43,6 +43,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -65,7 +66,6 @@ import org.meshtastic.core.ui.R
import org.meshtastic.core.ui.share.SharedContactDialog
import org.meshtastic.proto.AdminProtos
import org.meshtastic.proto.MeshProtos
import timber.log.Timber
import java.net.MalformedURLException
/**
@@ -90,7 +90,7 @@ fun AddContactFAB(
try {
uri.toSharedContact()
} catch (ex: MalformedURLException) {
Timber.e("URL was malformed: ${ex.message}")
Logger.e { "URL was malformed: ${ex.message}" }
null
}
if (sharedContact != null) {
@@ -102,7 +102,7 @@ fun AddContactFAB(
sharedContact?.let { SharedContactDialog(sharedContact = it, onDismiss = { onSharedContactRequested(null) }) }
fun zxingScan() {
Timber.d("Starting zxing QR code scanner")
Logger.d { "Starting zxing QR code scanner" }
val zxingScan = ScanOptions()
zxingScan.setCameraId(CAMERA_ID)
zxingScan.setPrompt("")
@@ -115,9 +115,9 @@ fun AddContactFAB(
LaunchedEffect(cameraPermissionState.status) {
if (cameraPermissionState.status.isGranted) {
Timber.d("Camera permission granted")
Logger.d { "Camera permission granted" }
} else {
Timber.d("Camera permission denied")
Logger.d { "Camera permission denied" }
}
}
@@ -193,7 +193,7 @@ val Uri.qrCode: Bitmap?
val barcodeEncoder = BarcodeEncoder()
barcodeEncoder.createBitmap(bitMatrix)
} catch (ex: WriterException) {
Timber.e("URL was too complex to render as barcode: ${ex.message}")
Logger.e { "URL was too complex to render as barcode: ${ex.message}" }
null
}

View File

@@ -20,6 +20,7 @@ package org.meshtastic.core.ui.qr
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.RadioConfigRepository
@@ -32,7 +33,6 @@ import org.meshtastic.proto.ConfigProtos.Config
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import org.meshtastic.proto.channelSet
import org.meshtastic.proto.config
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
@@ -61,7 +61,7 @@ constructor(
try {
serviceRepository.meshService?.setChannel(channel.toByteArray())
} catch (ex: RemoteException) {
Timber.e(ex, "Set channel error")
Logger.e(ex) { "Set channel error" }
}
}
@@ -70,7 +70,7 @@ constructor(
try {
serviceRepository.meshService?.setConfig(config.toByteArray())
} catch (ex: RemoteException) {
Timber.e(ex, "Set config error")
Logger.e(ex) { "Set config error" }
}
}
}

View File

@@ -64,7 +64,7 @@ dependencies {
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.timber)
implementation(libs.kermit)
implementation(libs.nordic)
implementation(libs.nordic.dfu)

View File

@@ -19,6 +19,7 @@ package org.meshtastic.feature.firmware
import android.content.Context
import android.net.Uri
import co.touchlab.kermit.Logger
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
@@ -27,7 +28,6 @@ import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.meshtastic.core.model.DeviceHardware
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
@@ -57,7 +57,7 @@ constructor(
}
tempDir.mkdirs()
}
.onFailure { e -> Timber.w(e, "Failed to cleanup temp directory") }
.onFailure { e -> Logger.w(e) { "Failed to cleanup temp directory" } }
}
suspend fun checkUrlExists(url: String): Boolean = withContext(Dispatchers.IO) {
@@ -65,7 +65,7 @@ constructor(
try {
client.newCall(request).execute().use { response -> response.isSuccessful }
} catch (e: IOException) {
Timber.w(e, "Failed to check URL existence: $url")
Logger.w(e) { "Failed to check URL existence: $url" }
false
}
}
@@ -77,12 +77,12 @@ constructor(
try {
client.newCall(request).execute()
} catch (e: IOException) {
Timber.w(e, "Download failed for $url")
Logger.w(e) { "Download failed for $url" }
return@withContext null
}
if (!response.isSuccessful) {
Timber.w("Download failed: ${response.code} for $url")
Logger.w { "Download failed: ${response.code} for $url" }
return@withContext null
}
@@ -167,7 +167,7 @@ constructor(
}
}
} catch (e: IOException) {
Timber.w(e, "Failed to extract firmware from URI")
Logger.w(e) { "Failed to extract firmware from URI" }
return@withContext null
}
matchingEntries.minByOrNull { it.first.name.length }?.second

View File

@@ -26,6 +26,7 @@ import android.net.Uri
import android.os.Build
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
@@ -72,7 +73,6 @@ import org.meshtastic.core.strings.firmware_update_starting_service
import org.meshtastic.core.strings.firmware_update_unknown_hardware
import org.meshtastic.core.strings.firmware_update_updating
import org.meshtastic.core.strings.unknown
import timber.log.Timber
import java.io.File
import javax.inject.Inject
@@ -169,7 +169,7 @@ constructor(
}
.onFailure { e ->
if (e is CancellationException) throw e
Timber.e(e)
Logger.e(e) { "Error checking for updates" }
_state.value = FirmwareUpdateState.Error(e.message ?: "Unknown error")
}
}
@@ -224,13 +224,13 @@ constructor(
_state.value = FirmwareUpdateState.Processing(getString(Res.string.firmware_update_flashing))
withTimeoutOrNull(DEVICE_DETACH_TIMEOUT) { waitForDeviceDetach(context).first() }
?: Timber.w("Timed out waiting for device to detach, assuming success")
?: Logger.w { "Timed out waiting for device to detach, assuming success" }
_state.value = FirmwareUpdateState.Success
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Timber.e(e)
Logger.e(e) { "Error saving DFU file" }
_state.value = FirmwareUpdateState.Error(e.message ?: getString(Res.string.firmware_update_failed))
} finally {
cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
@@ -279,7 +279,7 @@ constructor(
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Timber.e(e)
Logger.e(e) { "Error starting update from file" }
_state.value = FirmwareUpdateState.Error(e.message ?: "Local update failed")
}
}
@@ -345,7 +345,7 @@ private fun cleanupTemporaryFiles(fileHandler: FirmwareFileHandler, tempFirmware
tempFirmwareFile?.takeIf { it.exists() }?.delete()
fileHandler.cleanupAllTemporaryFiles()
}
.onFailure { e -> Timber.w(e, "Failed to cleanup temp files") }
.onFailure { e -> Logger.w(e) { "Failed to cleanup temp files" } }
return null
}

View File

@@ -19,6 +19,7 @@ package org.meshtastic.feature.firmware
import android.content.Context
import android.net.Uri
import co.touchlab.kermit.Logger
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
@@ -26,7 +27,6 @@ import no.nordicsemi.android.dfu.DfuServiceInitiator
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import java.io.File
import javax.inject.Inject
@@ -81,7 +81,7 @@ class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFil
return it
}
} catch (e: Exception) {
Timber.w(e, "Direct download for $filename failed, falling back to release zip")
Logger.w(e) { "Direct download for $filename failed, falling back to release zip" }
}
}
@@ -141,7 +141,7 @@ constructor(
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Timber.e(e)
Logger.e(e) { "OTA Update failed" }
updateState(FirmwareUpdateState.Error(e.message ?: "OTA Update failed"))
null
}
@@ -214,7 +214,7 @@ constructor(
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Timber.e(e)
Logger.e(e) { "USB Update failed" }
updateState(FirmwareUpdateState.Error(e.message ?: "USB Update failed"))
null
}

View File

@@ -1,8 +1,5 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComposableParamOrder:PermissionScreenLayout.kt$PermissionScreenLayout</ID>
<ID>ParameterNaming:WelcomeScreen.kt$onGetStarted</ID>
</CurrentIssues>
<CurrentIssues/>
</SmellBaseline>

View File

@@ -67,7 +67,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.common)
implementation(libs.material)
implementation(libs.timber)
implementation(libs.kermit)
fdroidImplementation(libs.osmbonuspack)
fdroidImplementation(libs.osmdroid.android)

View File

@@ -76,6 +76,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi // Added for Accompanist
import com.google.accompanist.permissions.rememberMultiplePermissionsState // Added for Accompanist
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -156,7 +157,6 @@ import org.osmdroid.views.overlay.Polygon
import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.infowindow.InfoWindow
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import timber.log.Timber
import java.io.File
import java.text.DateFormat
import kotlin.math.abs
@@ -170,7 +170,7 @@ private fun MapView.updateMarkers(
waypointMarkers: List<MarkerWithLabel>,
nodeClusterer: RadiusMarkerClusterer,
) {
Timber.d("Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints")
Logger.d { "Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints" }
overlays.removeAll { it is MarkerWithLabel }
// overlays.addAll(nodeMarkers + waypointMarkers)
overlays.addAll(waypointMarkers)
@@ -271,7 +271,7 @@ fun MapView(
fun loadOnlineTileSourceBase(): ITileSource {
val id = mapViewModel.mapStyleId
Timber.d("mapStyleId from prefs: $id")
Logger.d { "mapStyleId from prefs: $id" }
return CustomTileSource.getTileSource(id).also {
zoomLevelMax = it.maximumZoomLevel.toDouble()
showDownloadButton = if (it is OnlineTileSourceBase) it.tileSourcePolicy.acceptsBulkDownload() else false
@@ -295,11 +295,11 @@ fun MapView(
fun MapView.toggleMyLocation() {
if (context.gpsDisabled()) {
Timber.d("Telling user we need location turned on for MyLocationNewOverlay")
Logger.d { "Telling user we need location turned on for MyLocationNewOverlay" }
scope.launch { context.showToast(Res.string.location_disabled) }
return
}
Timber.d("user clicked MyLocationNewOverlay ${myLocationOverlay == null}")
Logger.d { "user clicked MyLocationNewOverlay ${myLocationOverlay == null}" }
if (myLocationOverlay == null) {
myLocationOverlay =
MyLocationNewOverlay(this).apply {
@@ -454,15 +454,15 @@ fun MapView(
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(com.meshtastic.core.strings.getString(Res.string.waypoint_delete))
builder.setNeutralButton(com.meshtastic.core.strings.getString(Res.string.cancel)) { _, _ ->
Timber.d("User canceled marker delete dialog")
Logger.d { "User canceled marker delete dialog" }
}
builder.setNegativeButton(com.meshtastic.core.strings.getString(Res.string.delete_for_me)) { _, _ ->
Timber.d("User deleted waypoint ${waypoint.id} for me")
Logger.d { "User deleted waypoint ${waypoint.id} for me" }
mapViewModel.deleteWaypoint(waypoint.id)
}
if (waypoint.lockedTo in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
builder.setPositiveButton(com.meshtastic.core.strings.getString(Res.string.delete_for_everyone)) { _, _ ->
Timber.d("User deleted waypoint ${waypoint.id} for everyone")
Logger.d { "User deleted waypoint ${waypoint.id} for everyone" }
mapViewModel.sendWaypoint(waypoint.copy { expire = 1 })
mapViewModel.deleteWaypoint(waypoint.id)
}
@@ -485,7 +485,7 @@ fun MapView(
fun showMarkerLongPressDialog(id: Int) {
performHapticFeedback()
Timber.d("marker long pressed id=$id")
Logger.d { "marker long pressed id=$id" }
val waypoint = waypoints[id]?.data?.waypoint ?: return
// edit only when unlocked or lockedTo myNodeNum
if (waypoint.lockedTo in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
@@ -691,9 +691,9 @@ fun MapView(
),
)
} catch (ex: TileSourcePolicyException) {
Timber.d("Tile source does not allow archiving: ${ex.message}")
Logger.d { "Tile source does not allow archiving: ${ex.message}" }
} catch (ex: Exception) {
Timber.d("Tile source exception: ${ex.message}")
Logger.d { "Tile source exception: ${ex.message}" }
}
}
@@ -897,7 +897,7 @@ fun MapView(
EditWaypointDialog(
waypoint = showEditWaypointDialog ?: return, // Safe call
onSendClicked = { waypoint ->
Timber.d("User clicked send waypoint ${waypoint.id}")
Logger.d { "User clicked send waypoint ${waypoint.id}" }
showEditWaypointDialog = null
mapViewModel.sendWaypoint(
waypoint.copy {
@@ -910,12 +910,12 @@ fun MapView(
)
},
onDeleteClicked = { waypoint ->
Timber.d("User clicked delete waypoint ${waypoint.id}")
Logger.d { "User clicked delete waypoint ${waypoint.id}" }
showEditWaypointDialog = null
showDeleteMarkerDialog(waypoint)
},
onDismissRequest = {
Timber.d("User clicked cancel marker edit dialog")
Logger.d { "User clicked cancel marker edit dialog" }
showEditWaypointDialog = null
},
)

View File

@@ -33,6 +33,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import co.touchlab.kermit.Logger
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.ITileSource
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
@@ -40,7 +41,6 @@ import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView
import timber.log.Timber
@SuppressLint("WakelockTimeout")
private fun PowerManager.WakeLock.safeAcquire() {
@@ -48,9 +48,9 @@ private fun PowerManager.WakeLock.safeAcquire() {
try {
acquire()
} catch (e: SecurityException) {
Timber.e("WakeLock permission exception: ${e.message}")
Logger.e { "WakeLock permission exception: ${e.message}" }
} catch (e: IllegalStateException) {
Timber.e("WakeLock acquire() exception: ${e.message}")
Logger.e { "WakeLock acquire() exception: ${e.message}" }
}
}
}
@@ -60,7 +60,7 @@ private fun PowerManager.WakeLock.safeRelease() {
try {
release()
} catch (e: IllegalStateException) {
Timber.e("WakeLock release() exception: ${e.message}")
Logger.e { "WakeLock release() exception: ${e.message}" }
}
}
}

View File

@@ -32,12 +32,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import co.touchlab.kermit.Logger
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.LocationSettingsRequest
import com.google.android.gms.location.Priority
import timber.log.Timber
private const val INTERVAL_MILLIS = 10000L
@@ -66,11 +66,11 @@ fun LocationPermissionsHandler(onPermissionResult: (Boolean) -> Unit) {
val locationSettingsLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartIntentSenderForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
Timber.d("Location settings changed by user.")
Logger.d { "Location settings changed by user." }
// User has enabled location services or improved accuracy.
onPermissionResult(true) // Settings are now adequate, and permission was already granted.
} else {
Timber.d("Location settings change cancelled by user.")
Logger.d { "Location settings change cancelled by user." }
// User chose not to change settings. The permission itself is still granted,
// but the experience might be degraded. For the purpose of enabling map features,
// we consider this as success if the core permission is there.
@@ -111,7 +111,7 @@ fun LocationPermissionsHandler(onPermissionResult: (Boolean) -> Unit) {
val task = client.checkLocationSettings(builder.build())
task.addOnSuccessListener {
Timber.d("Location settings are satisfied.")
Logger.d { "Location settings are satisfied." }
onPermissionResult(true) // Permission granted and settings are good
}
@@ -122,11 +122,11 @@ fun LocationPermissionsHandler(onPermissionResult: (Boolean) -> Unit) {
locationSettingsLauncher.launch(intentSenderRequest)
// Result of this launch will be handled by locationSettingsLauncher's callback
} catch (sendEx: ActivityNotFoundException) {
Timber.d("Error launching location settings resolution ${sendEx.message}.")
Logger.d { "Error launching location settings resolution ${sendEx.message}." }
onPermissionResult(true) // Permission is granted, but settings dialog failed. Proceed.
}
} else {
Timber.d("Location settings are not satisfiable.${exception.message}")
Logger.d { "Location settings are not satisfiable.${exception.message}" }
onPermissionResult(true) // Permission is granted, but settings not ideal. Proceed.
}
}

View File

@@ -62,6 +62,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.graphics.createBitmap
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
@@ -124,7 +125,6 @@ import org.meshtastic.proto.MeshProtos.Position
import org.meshtastic.proto.MeshProtos.Waypoint
import org.meshtastic.proto.copy
import org.meshtastic.proto.waypoint
import timber.log.Timber
import java.text.DateFormat
import kotlin.math.abs
import kotlin.math.max
@@ -219,7 +219,7 @@ fun MapView(
try {
cameraPositionState.animate(cameraUpdate)
} catch (e: IllegalStateException) {
Timber.d("Error animating camera to location: ${e.message}")
Logger.d { "Error animating camera to location: ${e.message}" }
}
}
}
@@ -239,14 +239,14 @@ fun MapView(
try {
@Suppress("MissingPermission")
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null)
Timber.d("Started location tracking")
Logger.d { "Started location tracking" }
} catch (e: SecurityException) {
Timber.d("Location permission not available: ${e.message}")
Logger.d { "Location permission not available: ${e.message}" }
isLocationTrackingEnabled = false
}
} else {
fusedLocationClient.removeLocationUpdates(locationCallback)
Timber.d("Stopped location tracking")
Logger.d { "Stopped location tracking" }
}
}
@@ -412,7 +412,7 @@ fun MapView(
cameraPositionState.animate(cameraUpdate)
hasCenteredTraceroute = true
} catch (e: IllegalStateException) {
Timber.d("Error centering traceroute overlay: ${e.message}")
Logger.d { "Error centering traceroute overlay: ${e.message}" }
}
}
}
@@ -548,7 +548,7 @@ fun MapView(
CameraUpdateFactory.newLatLngBounds(bounds.build(), 100),
)
}
Timber.d("Cluster clicked! $cluster")
Logger.d { "Cluster clicked! $cluster" }
}
true
},
@@ -679,9 +679,9 @@ fun MapView(
val currentPosition = cameraPositionState.position
val newCameraPosition = CameraPosition.Builder(currentPosition).bearing(0f).build()
cameraPositionState.animate(CameraUpdateFactory.newCameraPosition(newCameraPosition))
Timber.d("Oriented map to north")
Logger.d { "Oriented map to north" }
} catch (e: IllegalStateException) {
Timber.d("Error orienting map to north: ${e.message}")
Logger.d { "Error orienting map to north: ${e.message}" }
}
}
}
@@ -715,7 +715,7 @@ fun MapView(
internal fun convertIntToEmoji(unicodeCodePoint: Int): String = try {
String(Character.toChars(unicodeCodePoint))
} catch (e: IllegalArgumentException) {
Timber.w(e, "Invalid unicode code point: $unicodeCodePoint")
Logger.w(e) { "Invalid unicode code point: $unicodeCodePoint" }
"\uD83D\uDCCD"
}

View File

@@ -21,6 +21,7 @@ import android.app.Application
import android.net.Uri
import androidx.core.net.toFile
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
@@ -56,7 +57,6 @@ import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ConfigProtos
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@@ -200,7 +200,7 @@ constructor(
fun selectCustomTileProvider(config: CustomTileProviderConfig?) {
if (config != null) {
if (!isValidTileUrlTemplate(config.urlTemplate)) {
Timber.tag("MapViewModel").w("Attempted to select invalid URL template: ${config.urlTemplate}")
Logger.withTag("MapViewModel").w("Attempted to select invalid URL template: ${config.urlTemplate}")
_selectedCustomTileProviderUrl.value = null
googleMapsPrefs.selectedCustomTileUrl = null
return
@@ -224,7 +224,8 @@ constructor(
fun createUrlTileProvider(urlString: String): TileProvider? {
if (!isValidTileUrlTemplate(urlString)) {
Timber.tag("MapViewModel").e("Tile URL does not contain valid {x}, {y}, and {z} placeholders: $urlString")
Logger.withTag("MapViewModel")
.e("Tile URL does not contain valid {x}, {y}, and {z} placeholders: $urlString")
return null
}
return object : UrlTileProvider(TILE_SIZE, TILE_SIZE) {
@@ -237,7 +238,7 @@ constructor(
return try {
URL(formattedUrl)
} catch (e: MalformedURLException) {
Timber.tag("MapViewModel").e(e, "Malformed URL: $formattedUrl")
Logger.withTag("MapViewModel").e(e) { "Malformed URL: $formattedUrl" }
null
}
}
@@ -290,7 +291,7 @@ constructor(
try {
_selectedGoogleMapType.value = MapType.valueOf(savedGoogleMapTypeName ?: MapType.NORMAL.name)
} catch (e: IllegalArgumentException) {
Timber.e(e, "Invalid saved Google Map type: $savedGoogleMapTypeName")
Logger.e(e) { "Invalid saved Google Map type: $savedGoogleMapTypeName" }
_selectedGoogleMapType.value = MapType.NORMAL // Fallback in case of invalid stored name
googleMapsPrefs.selectedGoogleMapType = null
}
@@ -335,14 +336,14 @@ constructor(
}
_mapLayers.value = loadedItems
if (loadedItems.isNotEmpty()) {
Timber.tag("MapViewModel").i("Loaded ${loadedItems.size} persisted map layers.")
Logger.withTag("MapViewModel").i("Loaded ${loadedItems.size} persisted map layers.")
}
}
} else {
Timber.tag("MapViewModel").i("Map layers directory does not exist. No layers loaded.")
Logger.withTag("MapViewModel").i("Map layers directory does not exist. No layers loaded.")
}
} catch (e: Exception) {
Timber.tag("MapViewModel").e(e, "Error loading persisted map layers")
Logger.withTag("MapViewModel").e(e) { "Error loading persisted map layers" }
_mapLayers.value = emptyList()
}
}
@@ -367,7 +368,7 @@ constructor(
}
if (layerType == null) {
Timber.tag("MapViewModel").e("Unsupported map layer file type: $extension")
Logger.withTag("MapViewModel").e("Unsupported map layer file type: $extension")
return@launch
}
@@ -384,7 +385,7 @@ constructor(
val newItem = MapLayerItem(name = layerName, uri = localFileUri, layerType = layerType)
_mapLayers.value = _mapLayers.value + newItem
} else {
Timber.tag("MapViewModel").e("Failed to copy file to internal storage.")
Logger.withTag("MapViewModel").e("Failed to copy file to internal storage.")
}
}
}
@@ -402,7 +403,7 @@ constructor(
inputStream?.use { input -> outputStream.use { output -> input.copyTo(output) } }
Uri.fromFile(outputFile)
} catch (e: IOException) {
Timber.tag("MapViewModel").e(e, "Error copying file to internal storage")
Logger.withTag("MapViewModel").e(e) { "Error copying file to internal storage" }
null
}
}
@@ -453,7 +454,7 @@ constructor(
file.delete()
}
} catch (e: Exception) {
Timber.tag("MapViewModel").e(e, "Error deleting file from internal storage")
Logger.withTag("MapViewModel").e(e) { "Error deleting file from internal storage" }
}
}
}
@@ -465,7 +466,7 @@ constructor(
try {
application.contentResolver.openInputStream(uriToLoad)
} catch (_: Exception) {
Timber.d("MapViewModel: Error opening InputStream from URI: $uriToLoad")
Logger.d { "MapViewModel: Error opening InputStream from URI: $uriToLoad" }
null
}
}
@@ -480,7 +481,7 @@ constructor(
LayerType.GEOJSON -> loadGeoJsonLayerIfNeeded(layerItem, map)
}
} catch (e: Exception) {
Timber.tag("MapViewModel").e(e, "Error loading map layer for ${layerItem.uri}")
Logger.withTag("MapViewModel").e(e) { "Error loading map layer for ${layerItem.uri}" }
}
}

View File

@@ -20,6 +20,7 @@ package org.meshtastic.feature.map
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -44,7 +45,6 @@ import org.meshtastic.core.strings.two_days
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.proto.MeshProtos
import timber.log.Timber
import java.util.concurrent.TimeUnit
@Suppress("MagicNumber")
@@ -150,7 +150,7 @@ abstract class BaseMapViewModel(
return try {
serviceRepository.meshService?.packetId
} catch (ex: RemoteException) {
Timber.e("RemoteException: ${ex.message}")
Logger.e { "RemoteException: ${ex.message}" }
return null
}
}
@@ -170,7 +170,7 @@ abstract class BaseMapViewModel(
try {
serviceRepository.meshService?.send(p)
} catch (ex: RemoteException) {
Timber.e("Send DataPacket error: ${ex.message}")
Logger.e { "Send DataPacket error: ${ex.message}" }
}
}

View File

@@ -58,7 +58,7 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.androidx.paging.compose)
implementation(libs.timber)
implementation(libs.kermit)
debugImplementation(libs.androidx.compose.ui.test.manifest)

View File

@@ -1,26 +1,5 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComposableParamOrder:Message.kt$MessageScreen</ID>
<ID>ComposableParamOrder:Message.kt$QuickChatRow</ID>
<ID>ComposableParamOrder:MessageActions.kt$MessageActions</ID>
<ID>ComposableParamOrder:MessageActions.kt$MessageStatusButton</ID>
<ID>ComposableParamOrder:MessageItem.kt$MessageItem</ID>
<ID>ComposableParamOrder:MessageList.kt$DeliveryInfo</ID>
<ID>ComposableParamOrder:MessageList.kt$MessageList</ID>
<ID>ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onClick</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onSendMessage</ID>
<ID>LambdaParameterEventTrailing:MessageList.kt$onReply</ID>
<ID>LambdaParameterEventTrailing:QuickChat.kt$onNavigateUp</ID>
<ID>LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged</ID>
<ID>LongParameterList:MessageViewModel.kt$MessageViewModel$( private val nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, quickChatActionRepository: QuickChatActionRepository, private val serviceRepository: ServiceRepository, private val packetRepository: PacketRepository, private val uiPrefs: UiPrefs, private val meshServiceNotifications: MeshServiceNotifications, )</ID>
<ID>ModifierMissing:Message.kt$MessageScreen</ID>
<ID>ModifierNotUsedAtRoot:QuickChat.kt$modifier = modifier.fillMaxSize().padding(innerPadding)</ID>
<ID>MutableStateParam:MessageList.kt$selectedIds</ID>
<ID>ParameterNaming:MessageList.kt$onUnreadChanged</ID>
<ID>TooManyFunctions:MessageViewModel.kt$MessageViewModel : ViewModel</ID>
<ID>Wrapping:Message.kt${ event -&gt; when (event) { is MessageScreenEvent.SendMessage -&gt; { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -&gt; viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -&gt; { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -&gt; viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -&gt; navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -&gt; viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -&gt; navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -&gt; navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -&gt; onNavigateBack() is MessageScreenEvent.CopyToClipboard -&gt; { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }</ID>
</CurrentIssues>
<CurrentIssues/>
</SmellBaseline>

View File

@@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@@ -50,7 +51,6 @@ import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ConfigProtos.Config.DeviceConfig.Role
import org.meshtastic.proto.channelSet
import org.meshtastic.proto.sharedContact
import timber.log.Timber
import javax.inject.Inject
private const val VERIFIED_CONTACT_FIRMWARE_CUTOFF = "2.7.12"
@@ -198,7 +198,7 @@ constructor(
try {
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
} catch (ex: RemoteException) {
Timber.e(ex, "Favorite node error")
Logger.e(ex) { "Favorite node error" }
}
}
@@ -211,7 +211,7 @@ constructor(
}
serviceRepository.onServiceAction(ServiceAction.SendContact(contact = contact))
} catch (ex: RemoteException) {
Timber.e(ex, "Send shared contact error")
Logger.e(ex) { "Send shared contact error" }
}
}
@@ -219,7 +219,7 @@ constructor(
try {
serviceRepository.meshService?.send(p)
} catch (ex: RemoteException) {
Timber.e("Send DataPacket error: ${ex.message}")
Logger.e { "Send DataPacket error: ${ex.message}" }
}
}
}

View File

@@ -62,7 +62,7 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.common)
implementation(libs.timber)
implementation(libs.kermit)
implementation(libs.coil)
implementation(libs.markdown.renderer.android)
implementation(libs.markdown.renderer.m3)

View File

@@ -3,86 +3,9 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CommentWrapping:SignalMetrics.kt$Metric.SNR$/* Selected 12 as the max to get 4 equal vertical sections. */</ID>
<ID>ComposableParamOrder:DeviceMetrics.kt$DeviceMetricsChart</ID>
<ID>ComposableParamOrder:ElevationInfo.kt$ElevationInfo</ID>
<ID>ComposableParamOrder:EnvironmentCharts.kt$ChartContent</ID>
<ID>ComposableParamOrder:EnvironmentCharts.kt$EnvironmentMetricsChart</ID>
<ID>ComposableParamOrder:EnvironmentCharts.kt$MetricPlottingCanvas</ID>
<ID>ComposableParamOrder:HostMetricsLog.kt$HostMetricsItem</ID>
<ID>ComposableParamOrder:HostMetricsLog.kt$LogLine</ID>
<ID>ComposableParamOrder:LastHeardInfo.kt$LastHeardInfo</ID>
<ID>ComposableParamOrder:NodeFilterTextField.kt$NodeFilterTextField</ID>
<ID>ComposableParamOrder:NodeItem.kt$NodeItem</ID>
<ID>ComposableParamOrder:PaxMetrics.kt$PaxMetricsChart</ID>
<ID>ComposableParamOrder:PowerMetrics.kt$PowerMetricsChart</ID>
<ID>ComposableParamOrder:SatelliteCountInfo.kt$SatelliteCountInfo</ID>
<ID>ComposableParamOrder:SignalMetrics.kt$SignalMetricsChart</ID>
<ID>ComposableParamOrder:TracerouteButton.kt$TracerouteButton</ID>
<ID>LambdaParameterEventTrailing:TracerouteLog.kt$onNavigateUp</ID>
<ID>LongMethod:EnvironmentMetrics.kt$@Composable fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -&gt; Unit)</ID>
<ID>LongMethod:NodeDetailsSection.kt$@Composable private fun MainNodeDetails(node: Node)</ID>
<ID>MagicNumber:MetricsViewModel.kt$MetricsViewModel$1000L</ID>
<ID>MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-5</ID>
<ID>MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-7</ID>
<ID>ModifierMissing:CommonCharts.kt$ChartHeader</ID>
<ID>ModifierMissing:CommonCharts.kt$Legend</ID>
<ID>ModifierMissing:CommonCharts.kt$TimeLabels</ID>
<ID>ModifierMissing:DeviceMetrics.kt$DeviceMetricsScreen</ID>
<ID>ModifierMissing:EnvironmentMetrics.kt$EnvironmentMetricsScreen</ID>
<ID>ModifierMissing:HostMetricsLog.kt$HostMetricsLogScreen</ID>
<ID>ModifierMissing:NodeListScreen.kt$NodeListScreen</ID>
<ID>ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons</ID>
<ID>ModifierMissing:PaxMetrics.kt$PaxMetricsItem</ID>
<ID>ModifierMissing:PaxMetrics.kt$PaxMetricsScreen</ID>
<ID>ModifierMissing:PositionLog.kt$PositionItem</ID>
<ID>ModifierMissing:PositionLog.kt$PositionLogScreen</ID>
<ID>ModifierMissing:PowerMetrics.kt$PowerMetricsScreen</ID>
<ID>ModifierMissing:SignalMetrics.kt$SignalMetricsScreen</ID>
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:EnvironmentCharts.kt$modifier = modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:EnvironmentCharts.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:PaxMetrics.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
<ID>ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:SignalMetrics.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:TracerouteLog.kt$modifier = modifier.fillMaxSize().padding(innerPadding)</ID>
<ID>ModifierReused:DeviceMetrics.kt$Canvas(modifier = modifier.width(dp)) { val height = size.height val width = size.width for (i in telemetries.indices) { val telemetry = telemetries[i] /* x-value time */ val xRatio = (telemetry.time - oldest.time).toFloat() / timeDiff val x = xRatio * width /* Channel Utilization */ plotPoint( drawContext = drawContext, color = Device.CH_UTIL.color, x = x, value = telemetry.deviceMetrics.channelUtilization, divisor = MAX_PERCENT_VALUE, ) /* Air Utilization Transmit */ plotPoint( drawContext = drawContext, color = Device.AIR_UTIL.color, x = x, value = telemetry.deviceMetrics.airUtilTx, divisor = MAX_PERCENT_VALUE, ) } /* Battery Line */ var index = 0 while (index &lt; telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -&gt; val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = Device.BATTERY.color, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } }</ID>
<ID>ModifierReused:DeviceMetrics.kt$HorizontalLinesOverlay( modifier.width(dp), lineColors = listOf(graphColor, Color.Yellow, Color.Red, graphColor, graphColor), )</ID>
<ID>ModifierReused:DeviceMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = oldest.time, newest = newest.time, selectedTime.lineInterval())</ID>
<ID>ModifierReused:EnvironmentCharts.kt$Box( contentAlignment = Alignment.TopStart, modifier = modifier.horizontalScroll(state = scrollState, reverseScrolling = true), ) { HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor }) TimeAxisOverlay(modifier = modifier.width(dp), oldest = oldest, newest = newest, selectedTime.lineInterval()) MetricPlottingCanvas( modifier = modifier.width(dp), telemetries = telemetries, graphData = graphData, selectedTime = selectedTime, oldest = oldest, timeDiff = timeDiff, rightMin = rightMin, rightMax = rightMax, ) }</ID>
<ID>ModifierReused:EnvironmentCharts.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor })</ID>
<ID>ModifierReused:EnvironmentCharts.kt$MetricPlottingCanvas( modifier = modifier.width(dp), telemetries = telemetries, graphData = graphData, selectedTime = selectedTime, oldest = oldest, timeDiff = timeDiff, rightMin = rightMin, rightMax = rightMax, )</ID>
<ID>ModifierReused:EnvironmentCharts.kt$TimeAxisOverlay(modifier = modifier.width(dp), oldest = oldest, newest = newest, selectedTime.lineInterval())</ID>
<ID>ModifierReused:PaxMetrics.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { Color.LightGray })</ID>
<ID>ModifierReused:PaxMetrics.kt$Row(modifier = modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f)) { YAxisLabels( modifier = Modifier.weight(Y_AXIS_WEIGHT).fillMaxHeight().padding(start = 8.dp), labelColor = MaterialTheme.colorScheme.onSurface, minValue = minValue, maxValue = maxValue, ) Box( contentAlignment = Alignment.TopStart, modifier = Modifier.horizontalScroll(state = scrollState, reverseScrolling = true).weight(CHART_WEIGHT), ) { HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { Color.LightGray }) TimeAxisOverlay(modifier.width(dp), oldest = minTime, newest = maxTime, timeFrame.lineInterval()) Canvas(modifier = Modifier.width(dp).fillMaxHeight()) { val width = size.width val height = size.height fun xForTime(t: Int): Float = if (maxTime == minTime) width / 2 else (t - minTime).toFloat() / (maxTime - minTime) * width fun yForValue(v: Int): Float = height - (v - minValue) / (maxValue - minValue) * height fun drawLine(series: List&lt;Pair&lt;Int, Int&gt;&gt;, color: Color) { for (i in 1 until series.size) { drawLine( color = color, start = Offset(xForTime(series[i - 1].first), yForValue(series[i - 1].second)), end = Offset(xForTime(series[i].first), yForValue(series[i].second)), strokeWidth = 2.dp.toPx(), ) } } drawLine(bleSeries, PaxSeries.BLE.color) drawLine(wifiSeries, PaxSeries.WIFI.color) drawLine(totalSeries, PaxSeries.PAX.color) } } YAxisLabels( modifier = Modifier.weight(Y_AXIS_WEIGHT).fillMaxHeight().padding(end = 8.dp), labelColor = MaterialTheme.colorScheme.onSurface, minValue = minValue, maxValue = maxValue, ) }</ID>
<ID>ModifierReused:PaxMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = minTime, newest = maxTime, timeFrame.lineInterval())</ID>
<ID>ModifierReused:PowerMetrics.kt$Canvas(modifier = modifier.width(dp)) { val width = size.width val height = size.height /* Voltage */ var index = 0 while (index &lt; telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -&gt; val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = (retrieveVoltage(selectedChannel, telemetry) - voltageMin) / voltageDiff val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = VOLTAGE_COLOR, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } /* Current */ index = 0 while (index &lt; telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -&gt; val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = (retrieveCurrent(selectedChannel, telemetry) - Power.CURRENT.min) / currentDiff val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = Power.CURRENT.color, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } }</ID>
<ID>ModifierReused:PowerMetrics.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor })</ID>
<ID>ModifierReused:PowerMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = oldest.time, newest = newest.time, selectedTime.lineInterval())</ID>
<ID>ModifierReused:PowerMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), Power.CURRENT.color, minValue = Power.CURRENT.min, maxValue = Power.CURRENT.max, )</ID>
<ID>ModifierReused:PowerMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), VOLTAGE_COLOR, minValue = voltageMin, maxValue = voltageMax, )</ID>
<ID>ModifierReused:SignalMetrics.kt$Canvas(modifier = modifier.width(dp)) { val width = size.width /* Plot */ for (packet in meshPackets) { val xRatio = (packet.rxTime - oldest.rxTime).toFloat() / timeDiff val x = xRatio * width /* SNR */ plotPoint( drawContext = drawContext, color = Metric.SNR.color, x = x, value = packet.rxSnr - Metric.SNR.min, divisor = snrDiff, ) /* RSSI */ plotPoint( drawContext = drawContext, color = Metric.RSSI.color, x = x, value = packet.rxRssi - Metric.RSSI.min, divisor = rssiDiff, ) } }</ID>
<ID>ModifierReused:SignalMetrics.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor })</ID>
<ID>ModifierReused:SignalMetrics.kt$TimeAxisOverlay( modifier.width(dp), oldest = oldest.rxTime, newest = newest.rxTime, selectedTime.lineInterval(), )</ID>
<ID>ModifierReused:SignalMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), Metric.RSSI.color, minValue = Metric.RSSI.min, maxValue = Metric.RSSI.max, )</ID>
<ID>ModifierReused:SignalMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), Metric.SNR.color, minValue = Metric.SNR.min, maxValue = Metric.SNR.max, )</ID>
<ID>ModifierWithoutDefault:CommonCharts.kt$modifier</ID>
<ID>ModifierWithoutDefault:EnvironmentCharts.kt$modifier</ID>
<ID>MultipleEmitters:CommonCharts.kt$LegendLabel</ID>
<ID>MultipleEmitters:DeviceMetrics.kt$DeviceMetricsChart</ID>
<ID>MultipleEmitters:EnvironmentCharts.kt$EnvironmentMetricsChart</ID>
<ID>MultipleEmitters:NodeDetailsSection.kt$MainNodeDetails</ID>
<ID>MultipleEmitters:PaxMetrics.kt$PaxMetricsChart</ID>
<ID>MultipleEmitters:PowerMetrics.kt$PowerMetricsChart</ID>
<ID>MultipleEmitters:RemoteDeviceActions.kt$RemoteDeviceActions</ID>
<ID>MultipleEmitters:SignalMetrics.kt$SignalMetricsChart</ID>
<ID>ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored</ID>
<ID>PreviewPublic:NodeItem.kt$NodeInfoPreview</ID>
<ID>PreviewPublic:NodeItem.kt$NodeInfoSimplePreview</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import co.touchlab.kermit.Logger
import com.mikepenz.markdown.m3.Markdown
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
@@ -50,7 +51,6 @@ import org.meshtastic.core.strings.download
import org.meshtastic.core.strings.error_no_app_to_handle_link
import org.meshtastic.core.strings.view_release
import org.meshtastic.core.ui.util.showToast
import timber.log.Timber
@Composable
fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease, modifier: Modifier = Modifier) {
@@ -72,7 +72,7 @@ fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease, modifier: Modi
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
scope.launch { context.showToast(Res.string.error_no_app_to_handle_link) }
Timber.e(e)
Logger.e(e) { "Failed to handle release page URL" }
}
},
modifier = Modifier.weight(1f),
@@ -88,7 +88,7 @@ fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease, modifier: Modi
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
scope.launch { context.showToast(Res.string.error_no_app_to_handle_link) }
Timber.e(e)
Logger.e(e) { "Failed to handle release zip URL" }
}
},
modifier = Modifier.weight(1f),

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.core.net.toUri
import co.touchlab.kermit.Logger
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
@@ -47,7 +48,6 @@ import org.meshtastic.core.ui.component.icon
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig.DisplayUnits
import timber.log.Timber
import java.net.URLEncoder
@OptIn(ExperimentalFoundationApi::class)
@@ -82,7 +82,7 @@ fun LinkedCoordinatesItem(node: Node, displayUnits: DisplayUnits = DisplayUnits.
coroutineScope.launch { context.showToast("No application available to open this location!") }
}
} catch (ex: ActivityNotFoundException) {
Timber.d("Failed to open geo intent: $ex")
Logger.d { "Failed to open geo intent: $ex" }
}
},
onLongClick = {

View File

@@ -20,6 +20,7 @@ package org.meshtastic.feature.node.detail
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -33,7 +34,6 @@ import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.feature.node.component.NodeMenuAction
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
@@ -77,20 +77,20 @@ constructor(
try {
nodeRepository.setNodeNotes(nodeNum, notes)
} catch (ex: java.io.IOException) {
Timber.e("Set node notes IO error: ${ex.message}")
Logger.e { "Set node notes IO error: ${ex.message}" }
} catch (ex: java.sql.SQLException) {
Timber.e("Set node notes SQL error: ${ex.message}")
Logger.e { "Set node notes SQL error: ${ex.message}" }
}
}
private fun removeNode(nodeNum: Int) = viewModelScope.launch(Dispatchers.IO) {
Timber.i("Removing node '$nodeNum'")
Logger.i { "Removing node '$nodeNum'" }
try {
val packetId = serviceRepository.meshService?.packetId ?: return@launch
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
nodeRepository.deleteNode(nodeNum)
} catch (ex: RemoteException) {
Timber.e("Remove node error: ${ex.message}")
Logger.e { "Remove node error: ${ex.message}" }
}
}
@@ -98,7 +98,7 @@ constructor(
try {
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
} catch (ex: RemoteException) {
Timber.e(ex, "Ignore node error")
Logger.e(ex) { "Ignore node error" }
}
}
@@ -106,55 +106,55 @@ constructor(
try {
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
} catch (ex: RemoteException) {
Timber.e(ex, "Favorite node error")
Logger.e(ex) { "Favorite node error" }
}
}
private fun requestUserInfo(destNum: Int) {
Timber.i("Requesting UserInfo for '$destNum'")
Logger.i { "Requesting UserInfo for '$destNum'" }
try {
serviceRepository.meshService?.requestUserInfo(destNum)
} catch (ex: RemoteException) {
Timber.e("Request NodeInfo error: ${ex.message}")
Logger.e { "Request NodeInfo error: ${ex.message}" }
}
}
private fun requestNeighborInfo(destNum: Int) {
Timber.i("Requesting NeighborInfo for '$destNum'")
Logger.i { "Requesting NeighborInfo for '$destNum'" }
try {
val packetId = serviceRepository.meshService?.packetId ?: return
serviceRepository.meshService?.requestNeighborInfo(packetId, destNum)
} catch (ex: RemoteException) {
Timber.e("Request NeighborInfo error: ${ex.message}")
Logger.e { "Request NeighborInfo error: ${ex.message}" }
}
}
private fun requestPosition(destNum: Int, position: Position = Position(0.0, 0.0, 0)) {
Timber.i("Requesting position for '$destNum'")
Logger.i { "Requesting position for '$destNum'" }
try {
serviceRepository.meshService?.requestPosition(destNum, position)
} catch (ex: RemoteException) {
Timber.e("Request position error: ${ex.message}")
Logger.e { "Request position error: ${ex.message}" }
}
}
private fun requestTelemetry(destNum: Int, type: TelemetryType) {
Timber.i("Requesting telemetry for '$destNum'")
Logger.i { "Requesting telemetry for '$destNum'" }
try {
val packetId = serviceRepository.meshService?.packetId ?: return
serviceRepository.meshService?.requestTelemetry(packetId, destNum, type.ordinal)
} catch (ex: RemoteException) {
Timber.e("Request telemetry error: ${ex.message}")
Logger.e { "Request telemetry error: ${ex.message}" }
}
}
private fun requestTraceroute(destNum: Int) {
Timber.i("Requesting traceroute for '$destNum'")
Logger.i { "Requesting traceroute for '$destNum'" }
try {
val packetId = serviceRepository.meshService?.packetId ?: return
serviceRepository.meshService?.requestTraceroute(packetId, destNum)
} catch (ex: RemoteException) {
Timber.e("Request traceroute error: ${ex.message}")
Logger.e { "Request traceroute error: ${ex.message}" }
}
}
}

View File

@@ -18,13 +18,13 @@
package org.meshtastic.feature.node.list
import android.os.RemoteException
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import javax.inject.Inject
class NodeActions
@@ -37,7 +37,7 @@ constructor(
try {
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
} catch (ex: RemoteException) {
Timber.e(ex, "Favorite node error")
Logger.e(ex) { "Favorite node error" }
}
}
@@ -45,18 +45,18 @@ constructor(
try {
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
} catch (ex: RemoteException) {
Timber.e(ex, "Ignore node error")
Logger.e(ex) { "Ignore node error" }
}
}
suspend fun removeNode(nodeNum: Int) = withContext(Dispatchers.IO) {
Timber.i("Removing node '$nodeNum'")
Logger.i { "Removing node '$nodeNum'" }
try {
val packetId = serviceRepository.meshService?.packetId ?: return@withContext
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
nodeRepository.deleteNode(nodeNum)
} catch (ex: RemoteException) {
Timber.e("Remove node error: ${ex.message}")
Logger.e { "Remove node error: ${ex.message}" }
}
}
}

View File

@@ -17,8 +17,8 @@
package org.meshtastic.feature.node.metrics
import co.touchlab.kermit.Logger
import org.meshtastic.proto.MeshProtos
import timber.log.Timber
/**
* Safely extracts the hardware model number from a HardwareModel enum.
@@ -34,6 +34,6 @@ import timber.log.Timber
fun MeshProtos.HardwareModel.safeNumber(fallbackValue: Int = -1): Int = try {
this.number
} catch (e: IllegalArgumentException) {
Timber.w("Unknown hardware model enum value: $this, using fallback value: $fallbackValue")
Logger.w { "Unknown hardware model enum value: $this, using fallback value: $fallbackValue" }
fallbackValue
}

View File

@@ -23,6 +23,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
@@ -63,7 +64,6 @@ import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.MeshProtos.MeshPacket
import org.meshtastic.proto.Portnums
import org.meshtastic.proto.Portnums.PortNum
import timber.log.Timber
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
@@ -343,16 +343,16 @@ constructor(
}
}
Timber.d("MetricsViewModel created")
Logger.d { "MetricsViewModel created" }
} else {
Timber.d("MetricsViewModel: destNum is null, skipping metrics flows initialization.")
Logger.d { "MetricsViewModel: destNum is null, skipping metrics flows initialization." }
}
}
}
override fun onCleared() {
super.onCleared()
Timber.d("MetricsViewModel cleared")
Logger.d { "MetricsViewModel cleared" }
}
fun setTimeFrame(timeFrame: TimeFrame) {
@@ -395,7 +395,7 @@ constructor(
}
}
} catch (ex: FileNotFoundException) {
Timber.e(ex, "Can't write file error")
Logger.e(ex) { "Can't write file error" }
}
}
}

View File

@@ -65,7 +65,7 @@ dependencies {
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.timber)
implementation(libs.kermit)
implementation(libs.zxing.android.embedded)
androidTestImplementation(libs.androidx.compose.ui.test.junit4)

View File

@@ -2,28 +2,18 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComposableParamOrder:ChannelConfigScreen.kt$ChannelConfigScreen</ID>
<ID>ComposableParamOrder:Debug.kt$DecodedPayloadBlock</ID>
<ID>ComposableParamOrder:DebugSearch.kt$DebugSearchState</ID>
<ID>ComposableParamOrder:DebugSearch.kt$DebugSearchStateviewModelDefaults</ID>
<ID>ComposableParamOrder:MapReportingPreference.kt$MapReportingPreference</ID>
<ID>ComposableParamOrder:NodeActionButton.kt$NodeActionButton</ID>
<ID>ComposableParamOrder:WarningDialog.kt$WarningDialog</ID>
<ID>CyclomaticComplexMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>CyclomaticComplexMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>LambdaParameterEventTrailing:NodeActionButton.kt$onClick</ID>
<ID>LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>LongMethod:DeviceConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>LongMethod:LoRaConfigItemList.kt$@Composable fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -&gt; Unit)</ID>
<ID>LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>LongMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>LongMethod:RadioConfigScreenList.kt$@Composable fun &lt;T : MessageLite&gt; RadioConfigScreenList( title: String, onBack: () -&gt; Unit, responseState: ResponseState&lt;Any&gt;, onDismissPacketResponse: () -&gt; Unit, configState: ConfigState&lt;T&gt;, enabled: Boolean, onSave: (T) -&gt; Unit, content: LazyListScope.() -&gt; Unit, )</ID>
<ID>LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>LongMethod:SecurityConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
<ID>LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -&gt; Unit)</ID>
@@ -34,35 +24,10 @@
<ID>MagicNumber:EditChannelDialog.kt$16</ID>
<ID>MagicNumber:EditChannelDialog.kt$32</ID>
<ID>MagicNumber:PacketResponseStateDialog.kt$100</ID>
<ID>ModifierMissing:CleanNodeDatabaseScreen.kt$CleanNodeDatabaseScreen</ID>
<ID>ModifierMissing:Debug.kt$DebugScreen</ID>
<ID>ModifierMissing:DeviceConfigItemList.kt$DeviceConfigScreen</ID>
<ID>ModifierMissing:MapReportingPreference.kt$MapReportingPreference</ID>
<ID>ModifierMissing:NetworkConfigItemList.kt$NetworkConfigScreen</ID>
<ID>ModifierMissing:PositionConfigItemList.kt$PositionConfigScreen</ID>
<ID>ModifierMissing:RadioConfig.kt$RadioConfigItemList</ID>
<ID>ModifierMissing:RadioConfigScreenList.kt$RadioConfigScreenList</ID>
<ID>ModifierMissing:SecurityConfigItemList.kt$SecurityConfigScreen</ID>
<ID>ModifierMissing:SettingsScreen.kt$SettingsScreen</ID>
<ID>ModifierNotUsedAtRoot:EditChannelDialog.kt$modifier = modifier.weight(1f)</ID>
<ID>ModifierNotUsedAtRoot:EditDeviceProfileDialog.kt$modifier = modifier.weight(1f)</ID>
<ID>MultipleEmitters:CleanNodeDatabaseScreen.kt$NodesDeletionPreview</ID>
<ID>MultipleEmitters:RadioConfig.kt$RadioConfigItemList</ID>
<ID>NestedBlockDepth:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>ParameterNaming:ChannelConfigScreen.kt$onPositiveClicked</ID>
<ID>ParameterNaming:CleanNodeDatabaseScreen.kt$onCheckedChanged</ID>
<ID>ParameterNaming:CleanNodeDatabaseScreen.kt$onDaysChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onMapReportingEnabledChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onPositionPrecisionChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onPublishIntervalSecsChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onShouldReportLocationChanged</ID>
<ID>PreviewPublic:MapReportingPreference.kt$MapReportingPreview</ID>
<ID>ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>TooGenericExceptionCaught:LanguageUtils.kt$LanguageUtils$e: Exception</ID>
<ID>TooGenericExceptionCaught:RadioConfigViewModel.kt$RadioConfigViewModel$ex: Exception</ID>
<ID>TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModel</ID>
<ID>UnusedParameter:ChannelConfigScreen.kt$onBack: () -&gt; Unit</ID>
<ID>UnusedParameter:ChannelConfigScreen.kt$title: String</ID>
<ID>ViewModelInjection:DebugSearch.kt$viewModel</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -21,6 +21,7 @@ import android.app.Application
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -57,7 +58,6 @@ import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.Portnums
import timber.log.Timber
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
@@ -287,7 +287,7 @@ constructor(
}
}
} catch (ex: FileNotFoundException) {
Timber.e("Can't write file error: ${ex.message}")
Logger.e { "Can't write file error: ${ex.message}" }
}
}
}

View File

@@ -75,6 +75,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -96,7 +97,6 @@ import org.meshtastic.core.ui.theme.AnnotationColor
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog
import timber.log.Timber
import java.io.IOException
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
@@ -408,7 +408,7 @@ private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: L
withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_success, logs.size) }
} catch (e: IOException) {
withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, e.message ?: "") }
Timber.w(e, "Error:IOException ")
Logger.w(e) { "Error:IOException" }
}
}

View File

@@ -20,6 +20,7 @@ package org.meshtastic.feature.settings.debugging
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import com.google.protobuf.InvalidProtocolBufferException
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
@@ -44,7 +45,6 @@ import org.meshtastic.proto.PaxcountProtos
import org.meshtastic.proto.Portnums.PortNum
import org.meshtastic.proto.StoreAndForwardProtos
import org.meshtastic.proto.TelemetryProtos
import timber.log.Timber
import java.text.DateFormat
import java.util.Date
import java.util.Locale
@@ -237,7 +237,7 @@ constructor(
}
init {
Timber.d("DebugViewModel created")
Logger.d { "DebugViewModel created" }
viewModelScope.launch {
combine(searchManager.searchText, filterManager.filteredLogs) { searchText, logs ->
searchManager.findSearchMatches(searchText, logs)
@@ -250,7 +250,7 @@ constructor(
override fun onCleared() {
super.onCleared()
Timber.d("DebugViewModel cleared")
Logger.d { "DebugViewModel cleared" }
}
private fun toUiState(databaseLogs: List<MeshLog>) = databaseLogs

View File

@@ -31,6 +31,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import co.touchlab.kermit.Logger
import com.google.protobuf.MessageLite
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -80,7 +81,6 @@ import org.meshtastic.proto.Portnums
import org.meshtastic.proto.config
import org.meshtastic.proto.deviceProfile
import org.meshtastic.proto.moduleConfig
import timber.log.Timber
import java.io.FileOutputStream
import javax.inject.Inject
@@ -179,7 +179,7 @@ constructor(
}
.launchIn(viewModelScope)
Timber.d("RadioConfigViewModel created")
Logger.d { "RadioConfigViewModel created" }
}
private val myNodeInfo: StateFlow<MyNodeEntity?>
@@ -205,7 +205,7 @@ constructor(
override fun onCleared() {
super.onCleared()
Timber.d("RadioConfigViewModel cleared")
Logger.d { "RadioConfigViewModel cleared" }
}
private fun request(destNum: Int, requestAction: suspend (IMeshService, Int, Int) -> Unit, errorMessage: String) =
@@ -227,7 +227,7 @@ constructor(
}
}
} catch (ex: RemoteException) {
Timber.e("$errorMessage: ${ex.message}")
Logger.e { "$errorMessage: ${ex.message}" }
}
}
}
@@ -422,7 +422,7 @@ constructor(
try {
meshService?.setFixedPosition(destNum, position)
} catch (ex: RemoteException) {
Timber.e("Set fixed position error: ${ex.message}")
Logger.e { "Set fixed position error: ${ex.message}" }
}
}
@@ -436,7 +436,7 @@ constructor(
onResult(protobuf)
}
} catch (ex: Exception) {
Timber.e("Import DeviceProfile error: ${ex.message}")
Logger.e { "Import DeviceProfile error: ${ex.message}" }
sendError(ex.customMessage)
}
}
@@ -452,7 +452,7 @@ constructor(
}
setResponseStateSuccess()
} catch (ex: Exception) {
Timber.e("Can't write file error: ${ex.message}")
Logger.e { "Can't write file error: ${ex.message}" }
sendError(ex.customMessage)
}
}
@@ -491,7 +491,7 @@ constructor(
setResponseStateSuccess()
} catch (ex: Exception) {
val errorMessage = "Can't write security keys JSON error: ${ex.message}"
Timber.e(errorMessage)
Logger.e { errorMessage }
sendError(ex.customMessage)
}
}
@@ -514,7 +514,7 @@ constructor(
try {
setChannels(channelUrl)
} catch (ex: Exception) {
Timber.e(ex, "DeviceProfile channel import error")
Logger.e(ex) { "DeviceProfile channel import error" }
sendError(ex.customMessage)
}
}
@@ -656,7 +656,7 @@ constructor(
if (data?.portnumValue == Portnums.PortNum.ROUTING_APP_VALUE) {
val parsed = MeshProtos.Routing.parseFrom(data.payload)
Timber.d(debugMsg.format(parsed.errorReason.name))
Logger.d { debugMsg.format(parsed.errorReason.name) }
if (parsed.errorReason != MeshProtos.Routing.Error.NONE) {
sendError(getStringResFrom(parsed.errorReasonValue))
} else if (packet.from == destNum && route.isEmpty()) {
@@ -670,7 +670,7 @@ constructor(
}
if (data?.portnumValue == Portnums.PortNum.ADMIN_APP_VALUE) {
val parsed = AdminProtos.AdminMessage.parseFrom(data.payload)
Timber.d(debugMsg.format(parsed.payloadVariantCase.name))
Logger.d { debugMsg.format(parsed.payloadVariantCase.name) }
if (destNum != packet.from) {
sendError("Unexpected sender: ${packet.from.toUInt()} instead of ${destNum.toUInt()}.")
return
@@ -744,7 +744,7 @@ constructor(
incrementCompleted()
}
else -> Timber.d("No custom processing needed for ${parsed.payloadVariantCase}")
else -> Logger.d { "No custom processing needed for ${parsed.payloadVariantCase}" }
}
if (AdminRoute.entries.any { it.name == route }) {

View File

@@ -44,6 +44,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.advanced
@@ -79,7 +80,6 @@ import org.meshtastic.feature.settings.util.gpioPins
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
import timber.log.Timber
import java.io.File
private const val MAX_RINGTONE_SIZE = 230
@@ -116,7 +116,7 @@ fun ExternalNotificationConfigScreen(
}
}
} catch (e: Exception) {
Timber.e(e, "Error importing ringtone")
Logger.e(e) { "Error importing ringtone" }
Toast.makeText(context, "Error importing: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
@@ -308,7 +308,7 @@ fun ExternalNotificationConfigScreen(
tempFile.delete()
}
} catch (e: Exception) {
Timber.e(e, "Failed to play ringtone")
Logger.e(e) { "Failed to play ringtone" }
}
},
enabled = state.connected,

View File

@@ -22,6 +22,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalResources
import androidx.core.os.LocaleListCompat
import co.touchlab.kermit.Logger
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.fr_HT
@@ -30,7 +31,6 @@ import org.meshtastic.core.strings.pt_BR
import org.meshtastic.core.strings.zh_CN
import org.meshtastic.core.strings.zh_TW
import org.xmlpull.v1.XmlPullParser
import timber.log.Timber
import java.util.Locale
object LanguageUtils {
@@ -69,7 +69,7 @@ object LanguageUtils {
}
}
} catch (e: Exception) {
Timber.e("Error parsing locale_config.xml: ${e.message}")
Logger.e { "Error parsing locale_config.xml: ${e.message}" }
}
}
}

View File

@@ -164,7 +164,8 @@ osmbonuspack = { module = "com.github.MKergall:osmbonuspack", version = "6.9.0"
osmdroid-android = { module = "org.osmdroid:osmdroid-android", version.ref = "osmdroid-android" }
osmdroid-geopackage = { module = "org.osmdroid:osmdroid-geopackage", version.ref = "osmdroid-android" }
streamsupport-minifuture = { module = "net.sourceforge.streamsupport:streamsupport-minifuture", version = "1.7.4" }
timber = { module = "com.jakewharton.timber:timber", version = "5.0.1" }
kermit = { module = "co.touchlab:kermit", version = "2.0.5" }
usb-serial-android = { module = "com.github.mik3y:usb-serial-for-android", version = "3.10.0" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version = "4.3.0" }