refactor(analytics)!: modularize analytics - remove Logging (#3256)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-09-30 18:22:22 -05:00 committed by GitHub
parent 9aa0cf9335
commit cad88d277b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 1219 additions and 1426 deletions

View file

@ -39,25 +39,20 @@ import androidx.compose.ui.platform.LocalView
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.MainScreen
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
import com.geeksville.mesh.ui.sharing.toSharedContact
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity :
AppCompatActivity(),
Logging {
class MainActivity : AppCompatActivity() {
private val model: UIViewModel by viewModels()
// This is aware of the Activity lifecycle and handles binding to the mesh service.
@ -78,15 +73,6 @@ class MainActivity :
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
lifecycleScope.launch {
val appIntroCompleted = uiPreferencesDataSource.appIntroCompleted.value
if (appIntroCompleted) {
(application as GeeksvilleApplication).askToRate(this@MainActivity)
}
}
}
setContent {
val theme by model.theme.collectAsState()
val dynamic = theme == MODE_DYNAMIC
@ -108,12 +94,7 @@ class MainActivity :
if (appIntroCompleted) {
MainScreen(uIViewModel = model)
} else {
AppIntroductionScreen(
onDone = {
model.onAppIntroCompleted()
(application as GeeksvilleApplication).askToRate(this@MainActivity)
},
)
AppIntroductionScreen(onDone = { model.onAppIntroCompleted() })
}
}
}
@ -132,22 +113,22 @@ class MainActivity :
when (appLinkAction) {
Intent.ACTION_VIEW -> {
appLinkData?.let {
debug("App link data: $it")
Timber.d("App link data: $it")
if (it.path?.startsWith("/e/") == true || it.path?.startsWith("/E/") == true) {
debug("App link data is a channel set")
Timber.d("App link data is a channel set")
model.requestChannelUrl(it)
} else if (it.path?.startsWith("/v/") == true || it.path?.startsWith("/V/") == true) {
val sharedContact = it.toSharedContact()
debug("App link data is a shared contact: ${sharedContact.user.longName}")
Timber.d("App link data is a shared contact: ${sharedContact.user.longName}")
model.setSharedContactRequested(sharedContact)
} else {
debug("App link data is not a channel set")
Timber.d("App link data is not a channel set")
}
}
}
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
debug("USB device attached")
Timber.d("USB device attached")
showSettingsPage()
}
@ -161,7 +142,7 @@ class MainActivity :
}
else -> {
warn("Unexpected action $appLinkAction")
Timber.w("Unexpected action $appLinkAction")
}
}
}

View file

@ -32,6 +32,7 @@ import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.Job
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. */
@ -56,7 +57,7 @@ constructor(
private var serviceSetupJob: Job? = null
init {
debug("Adding self as LifecycleObserver for $lifecycleOwner")
Timber.d("Adding self as LifecycleObserver for $lifecycleOwner")
lifecycleOwner.lifecycle.addObserver(this)
}
@ -67,7 +68,7 @@ constructor(
serviceSetupJob =
lifecycleOwner.lifecycleScope.handledLaunch {
serviceRepository.setMeshService(service)
debug("connected to mesh service, connectionState=${serviceRepository.connectionState.value}")
Timber.d("connected to mesh service, connectionState=${serviceRepository.connectionState.value}")
}
}
@ -82,32 +83,32 @@ constructor(
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
debug("Lifecycle: ON_START")
Timber.d("Lifecycle: ON_START")
try {
bindMeshService()
} catch (ex: BindFailedException) {
errormsg("Bind of MeshService failed: ${ex.message}")
Timber.e("Bind of MeshService failed: ${ex.message}")
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
debug("Lifecycle: ON_DESTROY")
Timber.d("Lifecycle: ON_DESTROY")
owner.lifecycle.removeObserver(this)
debug("Removed self as LifecycleObserver to $lifecycleOwner")
Timber.d("Removed self as LifecycleObserver to $lifecycleOwner")
}
// endregion
@Suppress("TooGenericExceptionCaught")
private fun bindMeshService() {
debug("Binding to mesh service!")
Timber.d("Binding to mesh service!")
try {
MeshService.startService(activity)
} catch (ex: Exception) {
errormsg("Failed to start service from activity - but ignoring because bind will work: ${ex.message}")
Timber.e("Failed to start service from activity - but ignoring because bind will work: ${ex.message}")
}
connect(activity, MeshService.createIntent(), BIND_AUTO_CREATE + BIND_ABOVE_CLIENT)

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import timber.log.Timber
import javax.inject.Inject
/**
* The main application class for Meshtastic.
*
* This class is annotated with [HiltAndroidApp] to enable Hilt for dependency injection. It initializes core
* application components, including analytics and platform-specific helpers, and manages analytics consent based on
* user preferences.
*/
@HiltAndroidApp
class MeshUtilApplication : Application() {
@Inject lateinit var platformAnalytics: PlatformAnalytics
companion object {
/**
* Provides access to the platform-specific analytics provider. Initialized via the injected [PlatformAnalytics]
* during [onCreate].
*/
lateinit var analytics: PlatformAnalytics
private set
}
override fun onCreate() {
super.onCreate()
// Initialize platform-specific features (analytics, crash reporting, etc.)
analytics = platformAnalytics
}
}
fun logAssert(executeReliableWrite: Boolean) {
if (!executeReliableWrite) {
val ex = AssertionError("Assertion failed")
Timber.e(ex)
throw ex
}
}

View file

@ -1,53 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.analytics
/**
* Created by kevinh on 12/24/14.
*/
interface AnalyticsProvider {
// Turn analytics logging on/off
fun setEnabled(on: Boolean)
/**
* Store an event
*/
fun track(event: String, vararg properties: DataPair)
/**
* Only track this event if using a cheap provider (like google)
*/
fun trackLowValue(event: String, vararg properties: DataPair)
fun endSession()
fun startSession()
/**
* Set persistent ID info about this user, as a key value pair
*/
fun setUserInfo(vararg p: DataPair)
/**
* Increment some sort of analytics counter
*/
fun increment(name: String, amount: Double = 1.0)
fun sendScreenView(name: String)
fun endScreenView()
}

View file

@ -19,13 +19,12 @@ package com.geeksville.mesh.android
import android.os.Build
/**
* Created by kevinh on 1/14/16.
*/
object BuildUtils : Logging {
/** Created by kevinh on 1/14/16. */
object BuildUtils {
// Are we running on the emulator?
val isEmulator
get() = Build.FINGERPRINT.startsWith("generic") ||
get() =
Build.FINGERPRINT.startsWith("generic") ||
Build.FINGERPRINT.startsWith("unknown") ||
Build.FINGERPRINT.contains("emulator") ||
setOf(Build.MODEL, Build.PRODUCT).contains("google_sdk") ||

View file

@ -1,53 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.android
import timber.log.Timber
interface Logging {
private fun tag(): String = this.javaClass.name
fun info(msg: String) = Timber.tag(tag()).i(msg)
fun debug(msg: String) = Timber.tag(tag()).d(msg)
fun warn(msg: String) = Timber.tag(tag()).w(msg)
/**
* Log an error message, note - we call this errormsg rather than error because error() is a stdlib function in
* kotlin in the global namespace and we don't want users to accidentally call that.
*/
fun errormsg(msg: String, ex: Throwable? = null) {
if (ex?.message != null) {
Timber.tag(tag()).e(ex, msg)
} else {
Timber.tag(tag()).e(msg)
}
}
// / Kotlin assertions are disabled on android, so instead we use this assert helper
fun logAssert(f: Boolean) {
if (!f) {
val ex = AssertionError("Assertion failed")
// if(!Debug.isDebuggerConnected())
throw ex
}
}
}

View file

@ -24,6 +24,7 @@ import android.content.ServiceConnection
import android.os.IBinder
import android.os.IInterface
import com.geeksville.mesh.util.exceptionReporter
import timber.log.Timber
import java.io.Closeable
import java.lang.IllegalArgumentException
import java.util.concurrent.locks.ReentrantLock
@ -31,11 +32,8 @@ import kotlin.concurrent.withLock
class BindFailedException : Exception("bindService failed")
/**
* A wrapper that cleans up the service binding process
*/
open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T) : Closeable,
Logging {
/** A wrapper that cleans up the service binding process */
open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T) : Closeable {
var serviceP: T? = null
@ -72,17 +70,17 @@ open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T
if (isClosed) {
isClosed = false
if (!c.bindService(intent, connection, flags)) {
// Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false. Try
// 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
errormsg("Needed to use the second bind attempt hack")
Timber.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 {
warn("Ignoring rebind attempt for service")
Timber.w("Ignoring rebind attempt for service")
}
}
@ -92,41 +90,39 @@ 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?
warn("Ignoring error in ServiceClient.close, probably harmless")
Timber.w("Ignoring error in ServiceClient.close, probably harmless")
}
serviceP = null
context = null
}
// Called when we become connected
open fun onConnected(service: T) {
}
open fun onConnected(service: T) {}
// called on loss of connection
open fun onDisconnected() {
}
open fun onDisconnected() {}
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) = exceptionReporter {
if (!isClosed) {
val s = stubFactory(binder)
serviceP = s
onConnected(s)
private val connection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) = exceptionReporter {
if (!isClosed) {
val s = stubFactory(binder)
serviceP = s
onConnected(s)
// after calling our handler, tell anyone who was waiting for this connection to complete
lock.withLock {
condition.signalAll()
// after calling our handler, tell anyone who was waiting for this connection to complete
lock.withLock { condition.signalAll() }
} else {
// 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")
}
} else {
// 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
warn("A service connected while we were closing it, ignoring")
}
override fun onServiceDisconnected(name: ComponentName?) = exceptionReporter {
serviceP = null
onDisconnected()
}
}
override fun onServiceDisconnected(name: ComponentName?) = exceptionReporter {
serviceP = null
onDisconnected()
}
}
}

View file

@ -17,31 +17,27 @@
package com.geeksville.mesh.concurrent
import com.geeksville.mesh.android.Logging
import timber.log.Timber
/**
* Sometimes when starting services we face situations where messages come in that require computation
* but we can't do that computation yet because we are still waiting for some long running init to
* complete.
* Sometimes when starting services we face situations where messages come in that require computation but we can't do
* that computation yet because we are still waiting for some long running init to complete.
*
* This class lets you queue up closures to run at a later date and later on you can call run() to
* run all the previously queued work.
* This class lets you queue up closures to run at a later date and later on you can call run() to run all the
* previously queued work.
*/
class DeferredExecution : Logging {
class DeferredExecution {
private val queue = mutableListOf<() -> Unit>()
/// Queue some new work
// / Queue some new work
fun add(fn: () -> Unit) {
queue.add(fn)
}
/// run all work in the queue and clear it to be ready to accept new work
// / run all work in the queue and clear it to be ready to accept new work
fun run() {
debug("Running deferred execution numjobs=${queue.size}")
queue.forEach {
it()
}
Timber.d("Running deferred execution numjobs=${queue.size}")
queue.forEach { it() }
queue.clear()
}
}
}

View file

@ -16,29 +16,23 @@
*/
package com.geeksville.mesh.concurrent
import com.geeksville.mesh.android.Logging
/**
* A deferred execution object (with various possible implementations)
*/
interface Continuation<in T> : Logging {
/** A deferred execution object (with various possible implementations) */
interface Continuation<in T> {
abstract fun resume(res: Result<T>)
// syntactic sugar
fun resumeSuccess(res: T) = resume(Result.success(res))
fun resumeWithException(ex: Throwable) = try {
resume(Result.failure(ex))
} catch (ex: Throwable) {
// errormsg("Ignoring $ex while resuming, because we are the ones who threw it")
// Timber.e("Ignoring $ex while resuming, because we are the ones who threw it")
throw ex
}
}
/**
* An async continuation that just calls a callback when the result is available
*/
/** An async continuation that just calls a callback when the result is available */
class CallbackContinuation<in T>(private val cb: (Result<T>) -> Unit) : Continuation<T> {
override fun resume(res: Result<T>) = cb(res)
}
@ -46,8 +40,8 @@ class CallbackContinuation<in T>(private val cb: (Result<T>) -> Unit) : Continua
/**
* This is a blocking/threaded version of coroutine Continuation
*
* A little bit ugly, but the coroutine version has a nasty internal bug that showed up
* in my SyncBluetoothDevice so I needed a quick workaround.
* A little bit ugly, but the coroutine version has a nasty internal bug that showed up in my SyncBluetoothDevice so I
* needed a quick workaround.
*/
class SyncContinuation<T> : Continuation<T> {
@ -84,8 +78,8 @@ class SyncContinuation<T> : Continuation<T> {
}
/**
* Calls an init function which is responsible for saving our continuation so that some
* other thread can call resume or resume with exception.
* Calls an init function which is responsible for saving our continuation so that some other thread can call resume or
* resume with exception.
*
* Essentially this is a blocking version of the (buggy) coroutine suspendCoroutine
*/

View file

@ -26,7 +26,6 @@ import android.os.RemoteException
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
@ -53,6 +52,7 @@ import org.meshtastic.core.datastore.model.RecentAddress
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import timber.log.Timber
import javax.inject.Inject
/**
@ -108,8 +108,7 @@ constructor(
private val networkRepository: NetworkRepository,
private val radioInterfaceService: RadioInterfaceService,
private val recentAddressesDataSource: RecentAddressesDataSource,
) : ViewModel(),
Logging {
) : ViewModel() {
private val context: Context
get() = application.applicationContext
@ -199,12 +198,12 @@ constructor(
init {
serviceRepository.statusMessage.onEach { errorText.value = it }.launchIn(viewModelScope)
debug("BTScanModel created")
Timber.d("BTScanModel created")
}
override fun onCleared() {
super.onCleared()
debug("BTScanModel cleared")
Timber.d("BTScanModel cleared")
}
fun setErrorText(text: String) {
@ -233,11 +232,11 @@ constructor(
fun stopScan() {
if (scanJob != null) {
debug("stopping scan")
Timber.d("stopping scan")
try {
scanJob?.cancel()
} catch (ex: Throwable) {
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
Timber.w("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
} finally {
scanJob = null
}
@ -252,7 +251,7 @@ constructor(
@SuppressLint("MissingPermission")
fun startScan() {
debug("starting classic scan")
Timber.d("starting classic scan")
_spinner.value = true
scanJob =
@ -281,7 +280,7 @@ constructor(
try {
serviceRepository.meshService?.let { service -> MeshService.changeDeviceAddress(context, service, address) }
} catch (ex: RemoteException) {
errormsg("changeDeviceSelection failed, probably it is shutting down", ex)
Timber.e(ex, "changeDeviceSelection failed, probably it is shutting down")
// ignore the failure and the GUI won't be updating anyways
}
}
@ -289,14 +288,14 @@ constructor(
@SuppressLint("MissingPermission")
private fun requestBonding(it: DeviceListEntry) {
val device = bluetoothRepository.getRemoteDevice(it.address) ?: return
info("Starting bonding for ${device.anonymize}")
Timber.i("Starting bonding for ${device.anonymize}")
bluetoothRepository
.createBond(device)
.onEach { state ->
debug("Received bond state changed $state")
Timber.d("Received bond state changed $state")
if (state != BluetoothDevice.BOND_BONDING) {
debug("Bonding completed, state=$state")
Timber.d("Bonding completed, state=$state")
if (state == BluetoothDevice.BOND_BONDED) {
setErrorText(context.getString(R.string.pairing_completed))
changeDeviceAddress("x${device.address}")
@ -307,7 +306,7 @@ constructor(
}
.catch { ex ->
// We ignore missing BT adapters, because it lets us run on the emulator
warn("Failed creating Bluetooth bond: ${ex.message}")
Timber.w("Failed creating Bluetooth bond: ${ex.message}")
}
.launchIn(viewModelScope)
}
@ -317,10 +316,10 @@ constructor(
.requestPermission(it.driver.device)
.onEach { granted ->
if (granted) {
info("User approved USB access")
Timber.i("User approved USB access")
changeDeviceAddress(it.fullAddress)
} else {
errormsg("USB permission denied for device ${it.address}")
Timber.e("USB permission denied for device ${it.address}")
}
}
.launchIn(viewModelScope)

View file

@ -26,7 +26,6 @@ import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.StoreAndForwardProtos
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.ui.debug.FilterMode
import com.google.protobuf.InvalidProtocolBufferException
import dagger.hilt.android.lifecycle.HiltViewModel
@ -45,6 +44,7 @@ import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.MeshLog
import timber.log.Timber
import java.text.DateFormat
import java.util.Date
import java.util.Locale
@ -203,8 +203,7 @@ class DebugViewModel
constructor(
private val meshLogRepository: MeshLogRepository,
private val nodeRepository: NodeRepository,
) : ViewModel(),
Logging {
) : ViewModel() {
val meshLog: StateFlow<ImmutableList<UiMeshLog>> =
meshLogRepository
@ -240,7 +239,7 @@ constructor(
}
init {
debug("DebugViewModel created")
Timber.d("DebugViewModel created")
viewModelScope.launch {
combine(searchManager.searchText, filterManager.filteredLogs) { searchText, logs ->
searchManager.findSearchMatches(searchText, logs)
@ -253,7 +252,7 @@ constructor(
override fun onCleared() {
super.onCleared()
debug("DebugViewModel cleared")
Timber.d("DebugViewModel cleared")
}
private fun toUiState(databaseLogs: List<MeshLog>) = databaseLogs

View file

@ -34,7 +34,6 @@ import com.geeksville.mesh.MeshProtos.Position
import com.geeksville.mesh.Portnums
import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.util.safeNumber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@ -67,6 +66,7 @@ import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import org.meshtastic.feature.map.model.CustomTileSource
import timber.log.Timber
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
@ -211,8 +211,7 @@ constructor(
private val deviceHardwareRepository: DeviceHardwareRepository,
private val firmwareReleaseRepository: FirmwareReleaseRepository,
private val mapPrefs: MapPrefs,
) : ViewModel(),
Logging {
) : ViewModel() {
private val destNum = savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum
private fun MeshLog.hasValidTraceroute(): Boolean =
@ -376,15 +375,15 @@ constructor(
.onEach { firmwareEdition -> _state.update { state -> state.copy(firmwareEdition = firmwareEdition) } }
.launchIn(viewModelScope)
debug("MetricsViewModel created")
Timber.d("MetricsViewModel created")
} else {
debug("MetricsViewModel: destNum is null, skipping metrics flows initialization.")
Timber.d("MetricsViewModel: destNum is null, skipping metrics flows initialization.")
}
}
override fun onCleared() {
super.onCleared()
debug("MetricsViewModel cleared")
Timber.d("MetricsViewModel cleared")
}
fun setTimeFrame(timeFrame: TimeFrame) {
@ -427,7 +426,7 @@ constructor(
}
}
} catch (ex: FileNotFoundException) {
errormsg("Can't write file error: ${ex.message}")
Timber.e(ex, "Can't write file error")
}
}
}

View file

@ -35,7 +35,6 @@ import com.geeksville.mesh.ConfigProtos.Config
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.channel
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.channelSettings
@ -44,7 +43,6 @@ import com.geeksville.mesh.copy
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.util.safeNumber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -71,11 +69,11 @@ import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.util.toChannelSet
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
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
@ -165,24 +163,12 @@ constructor(
firmwareReleaseRepository: FirmwareReleaseRepository,
private val uiPreferencesDataSource: UiPreferencesDataSource,
private val meshServiceNotifications: MeshServiceNotifications,
) : ViewModel(),
Logging {
) : ViewModel() {
val theme: StateFlow<Int> = uiPreferencesDataSource.theme
val firmwareVersion = myNodeInfo.mapNotNull { nodeInfo -> nodeInfo?.firmwareVersion }
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmwareEdition }
val deviceHardware: StateFlow<DeviceHardware?> =
ourNodeInfo
.mapNotNull { nodeInfo ->
nodeInfo?.user?.hwModel?.let { hwModel ->
deviceHardwareRepository.getDeviceHardwareByModel(hwModel.safeNumber()).getOrNull()
}
}
.stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = null)
val clientNotification: StateFlow<MeshProtos.ClientNotification?> = serviceRepository.clientNotification
fun clearClientNotification(notification: MeshProtos.ClientNotification) {
@ -306,7 +292,7 @@ constructor(
.onEach { channelSet -> _channels.value = channelSet }
.launchIn(viewModelScope)
debug("ViewModel created")
Timber.d("ViewModel created")
}
private val _sharedContactRequested: MutableStateFlow<AdminProtos.SharedContact?> = MutableStateFlow(null)
@ -332,7 +318,7 @@ constructor(
fun requestChannelUrl(url: Uri) = runCatching { _requestChannelSet.value = url.toChannelSet() }
.onFailure { ex ->
errormsg("Channel url error: ${ex.message}")
Timber.e(ex, "Channel url error")
showSnackBar(R.string.channel_invalid)
}
@ -361,7 +347,7 @@ constructor(
override fun onCleared() {
super.onCleared()
debug("ViewModel cleared")
Timber.d("ViewModel cleared")
}
private inline fun updateLoraConfig(crossinline body: (Config.LoRaConfig) -> Config.LoRaConfig) {
@ -374,7 +360,7 @@ constructor(
try {
meshService?.setConfig(config.toByteArray())
} catch (ex: RemoteException) {
errormsg("Set config error:", ex)
Timber.e(ex, "Set config error")
}
}
@ -382,7 +368,7 @@ constructor(
try {
meshService?.setChannel(channel.toByteArray())
} catch (ex: RemoteException) {
errormsg("Set channel error:", ex)
Timber.e(ex, "Set channel error")
}
}

View file

@ -28,7 +28,6 @@ import androidx.annotation.RequiresPermission
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hasBluetoothPermission
import com.geeksville.mesh.util.registerReceiverCompat
import kotlinx.coroutines.flow.Flow
@ -38,6 +37,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -51,7 +51,7 @@ constructor(
private val bluetoothBroadcastReceiverLazy: dagger.Lazy<BluetoothBroadcastReceiver>,
private val dispatchers: CoroutineDispatchers,
private val processLifecycle: Lifecycle,
) : Logging {
) {
private val _state =
MutableStateFlow(
BluetoothState(
@ -126,7 +126,7 @@ constructor(
} ?: BluetoothState()
_state.emit(newState)
debug("Detected our bluetooth access=$newState")
Timber.d("Detected our bluetooth access=$newState")
}
companion object {

View file

@ -27,46 +27,47 @@ import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.location.LocationRequestCompat
import androidx.core.location.altitude.AltitudeConverterCompat
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.MeshUtilApplication
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationRepository @Inject constructor(
class LocationRepository
@Inject
constructor(
private val context: Application,
private val locationManager: dagger.Lazy<LocationManager>,
) : Logging {
) {
/**
* Status of whether the app is actively subscribed to location changes.
*/
/** Status of whether the app is actively subscribed to location changes. */
private val _receivingLocationUpdates: MutableStateFlow<Boolean> = MutableStateFlow(false)
val receivingLocationUpdates: StateFlow<Boolean> get() = _receivingLocationUpdates
val receivingLocationUpdates: StateFlow<Boolean>
get() = _receivingLocationUpdates
@RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
private fun LocationManager.requestLocationUpdates() = callbackFlow {
val intervalMs = 30 * 1000L // 30 seconds
val minDistanceM = 0f
val locationRequest = LocationRequestCompat.Builder(intervalMs)
.setMinUpdateDistanceMeters(minDistanceM)
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.build()
val locationRequest =
LocationRequestCompat.Builder(intervalMs)
.setMinUpdateDistanceMeters(minDistanceM)
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.build()
val locationListener = LocationListenerCompat { location ->
if (location.hasAltitude() && !LocationCompat.hasMslAltitude(location)) {
try {
AltitudeConverterCompat.addMslAltitudeToLocation(context, location)
} catch (e: Exception) {
errormsg("addMslAltitudeToLocation() failed", e)
Timber.e(e, "addMslAltitudeToLocation() failed")
}
}
// info("New location: $location")
@ -83,9 +84,11 @@ class LocationRepository @Inject constructor(
}
}
info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")
Timber.i(
"Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m",
)
_receivingLocationUpdates.value = true
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
MeshUtilApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
try {
providerList.forEach { provider ->
@ -102,17 +105,15 @@ class LocationRepository @Inject constructor(
}
awaitClose {
info("Stopping location requests")
Timber.i("Stopping location requests")
_receivingLocationUpdates.value = false
GeeksvilleApplication.analytics.track("location_stop")
MeshUtilApplication.analytics.track("location_stop")
LocationManagerCompat.removeUpdates(this@requestLocationUpdates, locationListener)
}
}
/**
* Observable flow for location updates
*/
/** Observable flow for location updates */
@RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
fun getLocations() = locationManager.get().requestLocationUpdates()
}

View file

@ -18,7 +18,6 @@
package com.geeksville.mesh.repository.network
import com.geeksville.mesh.MeshProtos.MqttClientProxyMessage
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.mqttClientProxyMessage
import com.geeksville.mesh.util.ignoreException
import com.google.protobuf.ByteString
@ -37,6 +36,7 @@ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.util.subscribeList
import timber.log.Timber
import java.net.URI
import java.security.SecureRandom
import javax.inject.Inject
@ -50,7 +50,7 @@ class MQTTRepository
constructor(
private val radioConfigRepository: RadioConfigRepository,
private val nodeRepository: NodeRepository,
) : Logging {
) {
companion object {
/**
@ -70,7 +70,7 @@ constructor(
private var mqttClient: MqttAsyncClient? = null
fun disconnect() {
info("MQTT Disconnected")
Timber.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) {
info("MQTT connectComplete: $serverURI reconnect: $reconnect")
Timber.i("MQTT connectComplete: $serverURI reconnect: $reconnect")
channelSet.subscribeList
.ifEmpty {
return
@ -123,7 +123,7 @@ constructor(
}
override fun connectionLost(cause: Throwable) {
info("MQTT connectionLost cause: $cause")
Timber.i("MQTT connectionLost cause: $cause")
if (cause is IllegalArgumentException) close(cause)
}
@ -138,7 +138,7 @@ constructor(
}
override fun deliveryComplete(token: IMqttDeliveryToken?) {
info("MQTT deliveryComplete messageId: ${token?.messageId}")
Timber.i("MQTT deliveryComplete messageId: ${token?.messageId}")
}
}
@ -161,15 +161,15 @@ constructor(
private fun subscribe(topic: String) {
mqttClient?.subscribe(topic, DEFAULT_QOS)
info("MQTT Subscribed to topic: $topic")
Timber.i("MQTT Subscribed to topic: $topic")
}
fun publish(topic: String, data: ByteArray, retained: Boolean) {
try {
val token = mqttClient?.publish(topic, data, DEFAULT_QOS, retained)
info("MQTT Publish messageId: ${token?.messageId}")
Timber.i("MQTT Publish messageId: ${token?.messageId}")
} catch (ex: Exception) {
errormsg("MQTT Publish error: ${ex.message}")
Timber.e("MQTT Publish error: ${ex.message}")
}
}
}

View file

@ -21,7 +21,6 @@ import android.net.ConnectivityManager
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.android.Logging
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
@ -29,21 +28,19 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NetworkRepository @Inject constructor(
class NetworkRepository
@Inject
constructor(
private val nsdManagerLazy: dagger.Lazy<NsdManager>,
private val connectivityManager: dagger.Lazy<ConnectivityManager>,
private val dispatchers: CoroutineDispatchers,
) : Logging {
) {
val networkAvailable: Flow<Boolean>
get() = connectivityManager.get().networkAvailable()
.flowOn(dispatchers.io)
.conflate()
get() = connectivityManager.get().networkAvailable().flowOn(dispatchers.io).conflate()
val resolvedList: Flow<List<NsdServiceInfo>>
get() = nsdManagerLazy.get().serviceList(SERVICE_TYPE)
.flowOn(dispatchers.io)
.conflate()
get() = nsdManagerLazy.get().serviceList(SERVICE_TYPE).flowOn(dispatchers.io).conflate()
companion object {
internal const val SERVICE_PORT = 4403

View file

@ -21,7 +21,6 @@ import android.annotation.SuppressLint
import android.app.Application
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.service.BLECharacteristicNotFoundException
@ -39,6 +38,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.meshtastic.core.model.util.anonymize
import timber.log.Timber
import java.lang.reflect.Method
import java.util.UUID
@ -105,8 +105,7 @@ constructor(
bluetoothRepository: BluetoothRepository,
private val service: RadioInterfaceService,
@Assisted val address: String,
) : IRadioInterface,
Logging {
) : IRadioInterface {
companion object {
// this service UUID is publicly visible for scanning
@ -162,7 +161,7 @@ constructor(
} catch (ex: CancellationException) {
break
} catch (ex: Exception) {
debug("RSSI polling error: ${ex.message}")
Timber.d("RSSI polling error: ${ex.message}")
}
}
}
@ -193,7 +192,7 @@ constructor(
// device is off/not connected)
val device = bluetoothRepository.getRemoteDevice(address)
if (device != null) {
info("Creating radio interface service. device=${address.anonymize}")
Timber.i("Creating radio interface service. device=${address.anonymize}")
// Note this constructor also does no comm
val s = SafeBluetooth(context, device)
@ -201,7 +200,7 @@ constructor(
startConnect()
} else {
errormsg("Bluetooth adapter not found, assuming running on the emulator!")
Timber.e("Bluetooth adapter not found, assuming running on the emulator!")
}
}
@ -210,7 +209,7 @@ constructor(
try {
safe?.let { s ->
val uuid = BTM_TORADIO_CHARACTER
debug("queuing ${p.size} bytes to $uuid")
Timber.d("queuing ${p.size} bytes to $uuid")
// Note: we generate a new characteristic each time, because we are about to
// change the data and we want the data stored in the closure
@ -219,7 +218,7 @@ constructor(
s.asyncWriteCharacteristic(toRadio, p) { r ->
try {
r.getOrThrow()
debug("write of ${p.size} bytes to $uuid completed")
Timber.d("write of ${p.size} bytes to $uuid completed")
if (isFirstSend) {
isFirstSend = false
@ -241,10 +240,10 @@ constructor(
private fun scheduleReconnect(reason: String) {
stopRssiPolling()
if (reconnectJob == null) {
warn("Scheduling reconnect because $reason")
Timber.w("Scheduling reconnect because $reason")
reconnectJob = service.serviceScope.handledLaunch { retryDueToException() }
} else {
warn("Skipping reconnect for $reason")
Timber.w("Skipping reconnect for $reason")
}
}
@ -260,13 +259,13 @@ constructor(
.clone() // We clone the array just in case, I'm not sure if they keep reusing the array
if (b.isNotEmpty()) {
debug("Received ${b.size} bytes from radio")
Timber.d("Received ${b.size} bytes from radio")
service.handleFromRadio(b)
// Queue up another read, until we run out of packets
doReadFromRadio(firstRead)
} else {
debug("Done reading from radio, fromradio is empty")
Timber.d("Done reading from radio, fromradio is empty")
if (firstRead) {
// If we just finished our initial download, now we want to start listening for notifies
startWatchingFromNum()
@ -287,7 +286,7 @@ constructor(
exceptionReporter {
// If the gatt has been destroyed, skip the refresh attempt
safe?.gatt?.let { gatt ->
debug("DOING FORCE REFRESH")
Timber.d("DOING FORCE REFRESH")
val refresh: Method = gatt.javaClass.getMethod("refresh")
refresh.invoke(gatt)
}
@ -309,12 +308,12 @@ constructor(
try {
if (fromNumChanged) {
fromNumChanged = false
debug("fromNum changed, so we are reading new messages")
Timber.d("fromNum changed, so we are reading new messages")
doReadFromRadio(false)
}
} catch (e: RadioNotConnectedException) {
// Don't report autobugs for this, getting an exception here is expected behavior
errormsg("Ending FromNum read, radio not connected", e)
Timber.e(e, "Ending FromNum read, radio not connected")
}
}
}
@ -334,7 +333,7 @@ constructor(
val backoffMillis = (1000 * (1 shl reconnectAttempts.coerceAtMost(maxReconnectionAttempts))).toLong()
// Exponential backoff, capped at 64s
reconnectAttempts++
warn(
Timber.w(
"Forcing disconnect and hopefully device will comeback" +
" (disabling forced refresh). Reconnect attempt $reconnectAttempts," +
" waiting ${backoffMillis}ms.",
@ -350,18 +349,18 @@ constructor(
service.onDisconnect(false) // assume we will fail
delay(backoffMillis) // Give some nasty time for buggy BLE stacks to shutdown
reconnectJob = null // Any new reconnect requests after this will be allowed to run
warn("Attempting reconnect")
Timber.w("Attempting reconnect")
if (safe != null) {
// check again, because we just slept, and someone might have closed our interface
startConnect()
} else {
warn("Not connecting, because safe==null, someone must have closed us")
Timber.w("Not connecting, because safe==null, someone must have closed us")
}
} else {
warn("Abandoning reconnect because safe==null, someone must have closed the device")
Timber.w("Abandoning reconnect because safe==null, someone must have closed the device")
}
} catch (ex: CancellationException) {
warn("retryDueToException was cancelled")
Timber.w("retryDueToException was cancelled")
} finally {
reconnectJob = null
}
@ -377,7 +376,7 @@ constructor(
private fun doDiscoverServicesAndInit() {
val s = safe
if (s == null) {
warn("Interface is shutting down, so skipping discover")
Timber.w("Interface is shutting down, so skipping discover")
} else {
s.asyncDiscoverServices { discRes ->
try {
@ -385,7 +384,7 @@ constructor(
service.serviceScope.handledLaunch {
try {
debug("Discovered services!")
Timber.d("Discovered services!")
delay(
1000,
) // android BLE is buggy and needs a 1000ms sleep before calling getChracteristic, or you
@ -412,7 +411,7 @@ constructor(
}
} catch (ex: BLEException) {
if (s.gatt == null) {
warn("GATT was closed while discovering, assume we are shutting down")
Timber.w("GATT was closed while discovering, assume we are shutting down")
} else {
scheduleReconnect("Unexpected error discovering services, forcing disconnect $ex")
}
@ -429,7 +428,7 @@ constructor(
reconnectAttempts = 0 // Reset backoff on successful connection
service.serviceScope.handledLaunch {
info("Connected to radio!")
Timber.i("Connected to radio!")
startRssiPolling()
if (
@ -453,7 +452,7 @@ constructor(
safe?.asyncRequestMtu(512) { mtuRes ->
try {
mtuRes.getOrThrow()
debug("MTU change attempted")
Timber.d("MTU change attempted")
// throw BLEException("Test MTU set failed")
@ -474,7 +473,7 @@ constructor(
stopRssiPolling()
if (safe != null) {
info("Closing BluetoothInterface")
Timber.i("Closing BluetoothInterface")
val s = safe
safe = null // We do this first, because if we throw we still want to mark that we no longer have a valid
// connection
@ -482,10 +481,10 @@ constructor(
try {
s?.close()
} catch (_: BLEConnectionClosing) {
warn("Ignoring BLE errors while closing")
Timber.w("Ignoring BLE errors while closing")
}
} else {
debug("Radio was not connected, skipping disable")
Timber.d("Radio was not connected, skipping disable")
}
}

View file

@ -17,9 +17,9 @@
package com.geeksville.mesh.repository.radio
import com.geeksville.mesh.android.Logging
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. */
@ -28,15 +28,14 @@ class BluetoothInterfaceSpec
constructor(
private val factory: BluetoothInterfaceFactory,
private val bluetoothRepository: BluetoothRepository,
) : InterfaceSpec<BluetoothInterface>,
Logging {
) : InterfaceSpec<BluetoothInterface> {
override fun createInterface(rest: String): BluetoothInterface = factory.create(rest)
/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(rest: String): Boolean {
val allPaired = bluetoothRepository.state.value.bondedDevices.map { it.address }.toSet()
return if (!allPaired.contains(rest)) {
warn("Ignoring stale bond to ${rest.anonymize}")
Timber.w("Ignoring stale bond to ${rest.anonymize}")
false
} else {
true

View file

@ -24,7 +24,6 @@ import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.Portnums
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.channel
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.config
@ -39,6 +38,7 @@ import kotlinx.coroutines.delay
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Position
import timber.log.Timber
import kotlin.random.Random
private val defaultLoRaConfig =
@ -59,8 +59,7 @@ class MockInterface
constructor(
private val service: RadioInterfaceService,
@Assisted val address: String,
) : IRadioInterface,
Logging {
) : IRadioInterface {
companion object {
private const val MY_NODE = 0x42424242
@ -72,7 +71,7 @@ constructor(
private val packetIdSequence = generateSequence { currentPacketId++ }.iterator()
init {
info("Starting the mock interface")
Timber.i("Starting the mock interface")
service.onConnect() // Tell clients they can use the API
}
@ -87,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 -> info("Ignoring data sent to mock interface $pr")
else -> Timber.i("Ignoring data sent to mock interface $pr")
}
}
@ -109,12 +108,12 @@ constructor(
}
}
else -> info("Ignoring admin sent to mock interface $d")
else -> Timber.i("Ignoring admin sent to mock interface $d")
}
}
override fun close() {
info("Closing the mock interface")
Timber.i("Closing the mock interface")
}
// / Generate a fake text message from a node
@ -298,7 +297,7 @@ constructor(
}
private fun sendConfigResponse(configId: Int) {
debug("Sending mock config response")
Timber.d("Sending mock config response")
// / Generate a fake node info entry
@Suppress("MagicNumber")

View file

@ -18,15 +18,15 @@
package com.geeksville.mesh.repository.radio
import android.app.Application
import android.provider.Settings
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshUtilApplication
import com.geeksville.mesh.android.BinaryLogFile
import com.geeksville.mesh.android.BuildUtils
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.network.NetworkRepository
@ -49,6 +49,7 @@ import kotlinx.coroutines.launch
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.prefs.radio.RadioPrefs
import org.meshtastic.core.service.ConnectionState
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -73,7 +74,7 @@ constructor(
private val processLifecycle: Lifecycle,
private val radioPrefs: RadioPrefs,
private val interfaceFactory: InterfaceFactory,
) : Logging {
) {
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
@ -138,7 +139,7 @@ constructor(
fun keepAlive(now: Long = System.currentTimeMillis()) {
if (now - lastHeartbeatMillis > HEARTBEAT_INTERVAL_MILLIS) {
info("Sending ToRadio heartbeat")
Timber.i("Sending ToRadio heartbeat")
val heartbeat =
MeshProtos.ToRadio.newBuilder().setHeartbeat(MeshProtos.Heartbeat.getDefaultInstance()).build()
handleSendToRadio(heartbeat.toByteArray())
@ -150,7 +151,8 @@ constructor(
fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String =
interfaceFactory.toInterfaceAddress(interfaceId, rest)
fun isMockInterface(): Boolean = BuildConfig.DEBUG || (context as GeeksvilleApplication).isInTestLab
fun isMockInterface(): Boolean =
BuildConfig.DEBUG || Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true"
/**
* Determines whether to default to mock interface for device address. This keeps the decision logic separate and
@ -198,7 +200,7 @@ constructor(
}
private fun broadcastConnectionChanged(newState: ConnectionState) {
debug("Broadcasting connection state change to $newState")
Timber.d("Broadcasting connection state change to $newState")
processLifecycle.coroutineScope.launch(dispatchers.default) { _connectionState.emit(newState) }
}
@ -219,7 +221,7 @@ constructor(
keepAlive(System.currentTimeMillis())
}
// ignoreException { debug("FromRadio: ${MeshProtos.FromRadio.parseFrom(p)}") }
// ignoreException { Timber.d("FromRadio: ${MeshProtos.FromRadio.parseFrom(p)}") }
processLifecycle.coroutineScope.launch(dispatchers.io) { _receivedData.emit(p) }
emitReceiveActivity()
@ -241,13 +243,13 @@ constructor(
/** Start our configured interface (if it isn't already running) */
private fun startInterface() {
if (radioIf !is NopInterface) {
warn("Can't start interface - $radioIf is already running")
Timber.w("Can't start interface - $radioIf is already running")
} else {
val address = getBondedDeviceAddress()
if (address == null) {
warn("No bonded mesh radio, can't start interface")
Timber.w("No bonded mesh radio, can't start interface")
} else {
info("Starting radio ${address.anonymize}")
Timber.i("Starting radio ${address.anonymize}")
isStarted = true
if (logSends) {
@ -271,7 +273,7 @@ constructor(
private fun stopInterface() {
val r = radioIf
info("stopping interface $r")
Timber.i("stopping interface $r")
isStarted = false
radioIf = interfaceFactory.nopInterface
r.close()
@ -301,18 +303,18 @@ constructor(
*/
private fun setBondedDeviceAddress(address: String?): Boolean =
if (getBondedDeviceAddress() == address && isStarted) {
warn("Ignoring setBondedDevice ${address.anonymize}, because we are already using that device")
Timber.w("Ignoring setBondedDevice ${address.anonymize}, because we are already using that device")
false
} else {
// Record that this use has configured a new radio
GeeksvilleApplication.analytics.track("mesh_bond")
MeshUtilApplication.analytics.track("mesh_bond")
// Ignore any errors that happen while closing old device
ignoreException { stopInterface() }
// The device address "n" can be used to mean none
debug("Setting bonded device to ${address.anonymize}")
Timber.d("Setting bonded device to ${address.anonymize}")
// Stores the address if non-null, otherwise removes the pref
radioPrefs.devAddr = address
@ -353,14 +355,14 @@ constructor(
// Use tryEmit for SharedFlow as it's non-blocking
val emitted = _meshActivity.tryEmit(MeshActivity.Send)
if (!emitted) {
debug("MeshActivity.Send event was not emitted due to buffer overflow or no collectors")
Timber.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) {
debug("MeshActivity.Receive event was not emitted due to buffer overflow or no collectors")
Timber.d("MeshActivity.Receive event was not emitted due to buffer overflow or no collectors")
}
}
}

View file

@ -17,23 +17,23 @@
package com.geeksville.mesh.repository.radio
import com.geeksville.mesh.android.Logging
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
*/
class SerialInterface @AssistedInject constructor(
/** An interface that assumes we are talking to a meshtastic device via USB serial */
class SerialInterface
@AssistedInject
constructor(
service: RadioInterfaceService,
private val serialInterfaceSpec: SerialInterfaceSpec,
private val usbRepository: UsbRepository,
@Assisted private val address: String,
) : StreamInterface(service), Logging {
) : StreamInterface(service) {
private var connRef = AtomicReference<SerialConnection?>()
init {
@ -48,39 +48,42 @@ class SerialInterface @AssistedInject constructor(
override fun connect() {
val device = serialInterfaceSpec.findSerial(address)
if (device == null) {
errormsg("Can't find device")
Timber.e("Can't find device")
} else {
info("Opening $device")
Timber.i("Opening $device")
val onConnect: () -> Unit = { super.connect() }
usbRepository.createSerialConnection(device, object : SerialConnectionListener {
override fun onMissingPermission() {
errormsg("Need permissions for port")
}
usbRepository
.createSerialConnection(
device,
object : SerialConnectionListener {
override fun onMissingPermission() {
Timber.e("Need permissions for port")
}
override fun onConnected() {
onConnect.invoke()
}
override fun onConnected() {
onConnect.invoke()
}
override fun onDataReceived(bytes: ByteArray) {
debug("Received ${bytes.size} byte(s)")
bytes.forEach(::readChar)
}
override fun onDataReceived(bytes: ByteArray) {
Timber.d("Received ${bytes.size} byte(s)")
bytes.forEach(::readChar)
}
override fun onDisconnected(thrown: Exception?) {
thrown?.let { e ->
errormsg("Serial error: $e")
}
debug("$device disconnected")
onDeviceDisconnect(false)
override fun onDisconnected(thrown: Exception?) {
thrown?.let { e -> Timber.e("Serial error: $e") }
Timber.d("$device disconnected")
onDeviceDisconnect(false)
}
},
)
.also { conn ->
connRef.set(conn)
conn.connect()
}
}).also { conn ->
connRef.set(conn)
conn.connect()
}
}
}
override fun sendBytes(p: ByteArray) {
connRef.get()?.sendBytes(p)
}
}
}

View file

@ -17,16 +17,14 @@
package com.geeksville.mesh.repository.radio
import com.geeksville.mesh.android.Logging
import timber.log.Timber
/**
* An interface that assumes we are talking to a meshtastic device over some sort of stream connection (serial or TCP
* probably)
*/
abstract class StreamInterface(protected val service: RadioInterfaceService) :
Logging,
IRadioInterface {
companion object : Logging {
abstract class StreamInterface(protected val service: RadioInterfaceService) : IRadioInterface {
companion object {
private const val START1 = 0x94.toByte()
private const val START2 = 0xc3.toByte()
private const val MAX_TO_FROM_RADIO_SIZE = 512
@ -43,7 +41,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) :
private var packetLen = 0
override fun close() {
debug("Closing stream for good")
Timber.d("Closing stream for good")
onDeviceDisconnect(true)
}
@ -92,7 +90,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) :
when (val c = b.toChar()) {
'\r' -> {} // ignore
'\n' -> {
debug("DeviceLog: $debugLineBuf")
Timber.d("DeviceLog: $debugLineBuf")
debugLineBuf.clear()
}
else -> debugLineBuf.append(c)
@ -106,7 +104,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) :
var nextPtr = ptr + 1
fun lostSync() {
errormsg("Lost protocol sync")
Timber.e("Lost protocol sync")
nextPtr = 0
}

View file

@ -17,7 +17,6 @@
package com.geeksville.mesh.repository.radio
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.util.Exceptions
@ -26,6 +25,7 @@ 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
@ -34,10 +34,8 @@ import java.net.InetAddress
import java.net.Socket
import java.net.SocketTimeoutException
class TCPInterface @AssistedInject constructor(
service: RadioInterfaceService,
@Assisted private val address: String,
) : StreamInterface(service), Logging {
class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @Assisted private val address: String) :
StreamInterface(service) {
companion object {
const val MAX_RETRIES_ALLOWED = Int.MAX_VALUE
@ -67,7 +65,7 @@ class TCPInterface @AssistedInject constructor(
override fun onDeviceDisconnect(waitForStopped: Boolean) {
val s = socket
if (s != null) {
debug("Closing TCP socket")
Timber.d("Closing TCP socket")
s.close()
socket = null
}
@ -80,7 +78,7 @@ class TCPInterface @AssistedInject constructor(
try {
startConnect()
} catch (ex: IOException) {
errormsg("IOException in TCP reader: $ex")
Timber.e("IOException in TCP reader: $ex")
onDeviceDisconnect(false)
} catch (ex: Throwable) {
Exceptions.report(ex, "Exception in TCP reader")
@ -89,22 +87,22 @@ class TCPInterface @AssistedInject constructor(
if (retryCount > MAX_RETRIES_ALLOWED) break
debug("Reconnect attempt $retryCount in ${backoffDelay / 1000}s")
Timber.d("Reconnect attempt $retryCount in ${backoffDelay / 1000}s")
delay(backoffDelay)
retryCount++
backoffDelay = minOf(backoffDelay * 2, MAX_BACKOFF_MILLIS)
}
debug("Exiting TCP reader")
Timber.d("Exiting TCP reader")
}
}
// Create a socket to make the connection with the server
private suspend fun startConnect() = withContext(Dispatchers.IO) {
debug("TCP connecting to $address")
Timber.d("TCP connecting to $address")
val (host, port) = address.split(":", limit = 2)
.let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: SERVICE_PORT) }
val (host, port) =
address.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: SERVICE_PORT) }
Socket(InetAddress.getByName(host), port).use { socket ->
socket.tcpNoDelay = true
@ -121,18 +119,20 @@ class TCPInterface @AssistedInject constructor(
backoffDelay = MIN_BACKOFF_MILLIS
var timeoutCount = 0
while (timeoutCount < 180) try { // close after 90s of inactivity
val c = inputStream.read()
if (c == -1) {
warn("Got EOF on TCP stream")
break
} else {
timeoutCount = 0
readChar(c.toByte())
while (timeoutCount < 180) {
try { // close after 90s of inactivity
val c = inputStream.read()
if (c == -1) {
Timber.w("Got EOF on TCP stream")
break
} else {
timeoutCount = 0
readChar(c.toByte())
}
} catch (ex: SocketTimeoutException) {
timeoutCount++
// Ignore and start another read
}
} catch (ex: SocketTimeoutException) {
timeoutCount++
// Ignore and start another read
}
}
}

View file

@ -18,11 +18,11 @@
package com.geeksville.mesh.repository.usb
import android.hardware.usb.UsbManager
import com.geeksville.mesh.android.Logging
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
@ -31,8 +31,8 @@ import java.util.concurrent.atomic.AtomicReference
internal class SerialConnectionImpl(
private val usbManagerLazy: dagger.Lazy<UsbManager?>,
private val device: UsbSerialDriver,
private val listener: SerialConnectionListener
) : SerialConnection, Logging {
private val listener: SerialConnectionListener,
) : SerialConnection {
private val port = device.ports[0] // Most devices have just one port (port 0)
private val closedLatch = CountDownLatch(1)
private val closed = AtomicBoolean(false)
@ -40,7 +40,7 @@ internal class SerialConnectionImpl(
override fun sendBytes(bytes: ByteArray) {
ioRef.get()?.let {
debug("writing ${bytes.size} byte(s)")
Timber.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) {
debug("Waiting for USB manager to stop...")
Timber.d("Waiting for USB manager to stop...")
closedLatch.await(1, TimeUnit.SECONDS)
}
}
@ -80,26 +80,31 @@ internal class SerialConnectionImpl(
port.dtr = true
port.rts = true
debug("Starting serial reader thread")
val io = SerialInputOutputManager(port, object : SerialInputOutputManager.Listener {
override fun onNewData(data: ByteArray) {
listener.onDataReceived(data)
}
Timber.d("Starting serial reader thread")
val io =
SerialInputOutputManager(
port,
object : SerialInputOutputManager.Listener {
override fun onNewData(data: ByteArray) {
listener.onDataReceived(data)
}
override fun onRunError(e: Exception?) {
closed.set(true)
ignoreException {
port.dtr = false
port.rts = false
port.close()
override fun onRunError(e: Exception?) {
closed.set(true)
ignoreException {
port.dtr = false
port.rts = false
port.close()
}
closedLatch.countDown()
listener.onDisconnected(e)
}
},
)
.apply {
readTimeout = 200 // To save battery we only timeout ever so often
ioRef.set(this)
}
closedLatch.countDown()
listener.onDisconnected(e)
}
}).apply {
readTimeout = 200 // To save battery we only timeout ever so often
ioRef.set(this)
}
io.start()
listener.onConnected()

View file

@ -23,23 +23,20 @@ import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import com.geeksville.mesh.android.Logging
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.
*/
class UsbBroadcastReceiver @Inject constructor(
private val usbRepository: UsbRepository
) : BroadcastReceiver(), Logging {
/** A helper class to call onChanged when bluetooth is enabled or disabled or when permissions are changed. */
class UsbBroadcastReceiver @Inject constructor(private val usbRepository: UsbRepository) : BroadcastReceiver() {
// Can be used for registering
internal val intentFilter get() = IntentFilter().apply {
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
}
internal val intentFilter
get() =
IntentFilter().apply {
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
}
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
val device: UsbDevice? = intent.getParcelableExtraCompat(UsbManager.EXTRA_DEVICE)
@ -47,17 +44,17 @@ class UsbBroadcastReceiver @Inject constructor(
when (intent.action) {
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
debug("USB device '$deviceName' was detached")
Timber.d("USB device '$deviceName' was detached")
usbRepository.refreshState()
}
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
debug("USB device '$deviceName' was attached")
Timber.d("USB device '$deviceName' was attached")
usbRepository.refreshState()
}
UsbManager.EXTRA_PERMISSION_GRANTED -> {
debug("USB device '$deviceName' was granted permission")
Timber.d("USB device '$deviceName' was granted permission")
usbRepository.refreshState()
}
}
}
}
}

View file

@ -22,59 +22,61 @@ import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.util.registerReceiverCompat
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialProber
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* Repository responsible for maintaining and updating the state of USB connectivity.
*/
/** Repository responsible for maintaining and updating the state of USB connectivity. */
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class UsbRepository @Inject constructor(
class UsbRepository
@Inject
constructor(
private val application: Application,
private val dispatchers: CoroutineDispatchers,
private val processLifecycle: Lifecycle,
private val usbBroadcastReceiverLazy: dagger.Lazy<UsbBroadcastReceiver>,
private val usbManagerLazy: dagger.Lazy<UsbManager?>,
private val usbSerialProberLazy: dagger.Lazy<UsbSerialProber>
) : Logging {
private val usbSerialProberLazy: dagger.Lazy<UsbSerialProber>,
) {
private val _serialDevices = MutableStateFlow(emptyMap<String, UsbDevice>())
@Suppress("unused") // Retained as public API
val serialDevices = _serialDevices
.asStateFlow()
val serialDevices = _serialDevices.asStateFlow()
@Suppress("unused") // Retained as public API
val serialDevicesWithDrivers = _serialDevices
.mapLatest { serialDevices ->
val serialProber = usbSerialProberLazy.get()
buildMap {
serialDevices.forEach { (k, v) ->
serialProber.probeDevice(v)?.let { driver ->
put(k, driver)
}
val serialDevicesWithDrivers =
_serialDevices
.mapLatest { serialDevices ->
val serialProber = usbSerialProberLazy.get()
buildMap {
serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { driver -> put(k, driver) } }
}
}
}.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
@Suppress("unused") // Retained as public API
val serialDevicesWithPermission = _serialDevices
.mapLatest { serialDevices ->
usbManagerLazy.get()?.let { usbManager ->
serialDevices.filterValues { device ->
usbManager.hasPermission(device)
}
} ?: emptyMap()
}.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
val serialDevicesWithPermission =
_serialDevices
.mapLatest { serialDevices ->
usbManagerLazy.get()?.let { usbManager ->
serialDevices.filterValues { device -> usbManager.hasPermission(device) }
} ?: emptyMap()
}
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
init {
processLifecycle.coroutineScope.launch(dispatchers.default) {
@ -86,23 +88,19 @@ class UsbRepository @Inject constructor(
}
/**
* Creates a USB serial connection to the specified USB device. State changes and data arrival
* result in async callbacks on the supplied listener.
* Creates a USB serial connection to the specified USB device. State changes and data arrival result in async
* callbacks on the supplied listener.
*/
fun createSerialConnection(device: UsbSerialDriver, listener: SerialConnectionListener): SerialConnection {
return SerialConnectionImpl(usbManagerLazy, device, listener)
}
fun createSerialConnection(device: UsbSerialDriver, listener: SerialConnectionListener): SerialConnection =
SerialConnectionImpl(usbManagerLazy, device, listener)
fun requestPermission(device: UsbDevice): Flow<Boolean> =
usbManagerLazy.get()?.requestPermission(application, device) ?: emptyFlow()
fun refreshState() {
processLifecycle.coroutineScope.launch(dispatchers.default) {
refreshStateInternal()
}
processLifecycle.coroutineScope.launch(dispatchers.default) { refreshStateInternal() }
}
private suspend fun refreshStateInternal() = withContext(dispatchers.default) {
_serialDevices.emit(usbManagerLazy.get()?.deviceList ?: emptyMap())
}
private suspend fun refreshStateInternal() =
withContext(dispatchers.default) { _serialDevices.emit(usbManagerLazy.get()?.deviceList ?: emptyMap()) }
}

View file

@ -20,10 +20,8 @@ package com.geeksville.mesh.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.geeksville.mesh.android.Logging
class BootCompleteReceiver : BroadcastReceiver(), Logging {
class BootCompleteReceiver : BroadcastReceiver() {
override fun onReceive(mContext: Context, intent: Intent) {
// Verify the intent action
if (Intent.ACTION_BOOT_COMPLETED != intent.action) {
@ -32,4 +30,4 @@ class BootCompleteReceiver : BroadcastReceiver(), Logging {
// start listening for bluetooth messages from our device
MeshService.startServiceLater(mContext)
}
}
}

View file

@ -40,6 +40,8 @@ import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.FromRadio.PayloadVariantCase
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
import com.geeksville.mesh.MeshUtilApplication
import com.geeksville.mesh.MeshUtilApplication.Companion.analytics
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.Portnums
@ -47,9 +49,6 @@ import com.geeksville.mesh.StoreAndForwardProtos
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.TelemetryProtos.LocalStats
import com.geeksville.mesh.XmodemProtos
import com.geeksville.mesh.analytics.DataPair
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.copy
@ -78,6 +77,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.analytics.DataPair
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
@ -121,9 +121,7 @@ import kotlin.math.absoluteValue
* infinite recursion on some androids (because contextWrapper.getResources calls to string
*/
@AndroidEntryPoint
class MeshService :
Service(),
Logging {
class MeshService : Service() {
@Inject lateinit var dispatchers: CoroutineDispatchers
@Inject lateinit var packetRepository: Lazy<PacketRepository>
@ -156,7 +154,7 @@ class MeshService :
private val tracerouteStartTimes = ConcurrentHashMap<Int, Long>()
companion object : Logging {
companion object {
// Intents broadcast by MeshService
@ -277,7 +275,7 @@ class MeshService :
private fun stopLocationRequests() {
if (locationFlow?.isActive == true) {
info("Stopping location requests")
Timber.i("Stopping location requests")
locationFlow?.cancel()
locationFlow = null
}
@ -311,7 +309,7 @@ class MeshService :
override fun onCreate() {
super.onCreate()
info("Creating mesh service")
Timber.i("Creating mesh service")
serviceNotifications.initChannels()
// Switch to the IO thread
serviceScope.handledLaunch { radioInterfaceService.connect() }
@ -368,7 +366,7 @@ class MeshService :
val a = radioInterfaceService.getBondedDeviceAddress()
val wantForeground = a != null && a != NO_DEVICE_SELECTED
info("Requesting foreground service=$wantForeground")
Timber.i("Requesting foreground service=$wantForeground")
// We always start foreground because that's how our service is always started (if we didn't
// then android would
@ -405,7 +403,7 @@ class MeshService :
}
override fun onDestroy() {
info("Destroying mesh service")
Timber.i("Destroying mesh service")
// Make sure we aren't using the notification first
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
@ -428,7 +426,7 @@ class MeshService :
/** discard entire node db & message state - used when downloading a new db from the device */
private fun discardNodeDB() {
debug("Discarding NodeDB")
Timber.d("Discarding NodeDB")
myNodeInfo = null
nodeDBbyNodeNum.clear()
haveNodeDB = false
@ -738,7 +736,7 @@ class MeshService :
// We ignore most messages that we sent
val fromUs = myInfo.myNodeNum == packet.from
debug("Received data from $fromId, portnum=${data.portnum} ${bytes.size} bytes")
Timber.d("Received data from $fromId, portnum=${data.portnum} ${bytes.size} bytes")
dataPacket.status = MessageStatus.RECEIVED
@ -751,19 +749,19 @@ class MeshService :
when (data.portnumValue) {
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> {
if (data.replyId != 0 && data.emoji == 0) {
debug("Received REPLY from $fromId")
Timber.d("Received REPLY from $fromId")
rememberDataPacket(dataPacket)
} else if (data.replyId != 0 && data.emoji != 0) {
debug("Received EMOJI from $fromId")
Timber.d("Received EMOJI from $fromId")
rememberReaction(packet)
} else {
debug("Received CLEAR_TEXT from $fromId")
Timber.d("Received CLEAR_TEXT from $fromId")
rememberDataPacket(dataPacket)
}
}
Portnums.PortNum.ALERT_APP_VALUE -> {
debug("Received ALERT_APP from $fromId")
Timber.d("Received ALERT_APP from $fromId")
rememberDataPacket(dataPacket)
}
@ -776,9 +774,9 @@ class MeshService :
Portnums.PortNum.POSITION_APP_VALUE -> {
val u = MeshProtos.Position.parseFrom(data.payload)
// debug("position_app ${packet.from} ${u.toOneLineString()}")
// Timber.d("position_app ${packet.from} ${u.toOneLineString()}")
if (data.wantResponse && u.latitudeI == 0 && u.longitudeI == 0) {
debug("Ignoring nop position update from position request")
Timber.d("Ignoring nop position update from position request")
} else {
handleReceivedPosition(packet.from, u, dataPacket.time)
}
@ -855,7 +853,7 @@ class MeshService :
if (start != null) {
val elapsedMs = System.currentTimeMillis() - start
val seconds = elapsedMs / 1000.0
info("Traceroute $requestId complete in $seconds s")
Timber.i("Traceroute $requestId complete in $seconds s")
"$full\n\nDuration: ${"%.1f".format(seconds)} s"
} else {
full
@ -864,7 +862,7 @@ class MeshService :
}
}
else -> debug("No custom processing needed for ${data.portnumValue}")
else -> Timber.d("No custom processing needed for ${data.portnumValue}")
}
// We always tell other apps when new data packets arrive
@ -872,9 +870,9 @@ class MeshService :
serviceBroadcasts.broadcastReceivedData(dataPacket)
}
GeeksvilleApplication.analytics.track("num_data_receive", DataPair(1))
MeshUtilApplication.analytics.track("num_data_receive", DataPair("num_data_receive", 1))
GeeksvilleApplication.analytics.track(
MeshUtilApplication.analytics.track(
"data_receive",
DataPair("num_bytes", bytes.size),
DataPair("type", data.portnumValue),
@ -888,7 +886,7 @@ class MeshService :
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
if (fromNodeNum == myNodeNum) {
val response = a.getConfigResponse
debug("Admin: received config ${response.payloadVariantCase}")
Timber.d("Admin: received config ${response.payloadVariantCase}")
setLocalConfig(response)
}
}
@ -898,7 +896,7 @@ class MeshService :
val mi = myNodeInfo
if (mi != null) {
val ch = a.getChannelResponse
debug("Admin: Received channel ${ch.index}")
Timber.d("Admin: Received channel ${ch.index}")
if (ch.index + 1 < mi.maxChannels) {
handleChannel(ch)
@ -908,15 +906,15 @@ class MeshService :
}
AdminProtos.AdminMessage.PayloadVariantCase.GET_DEVICE_METADATA_RESPONSE -> {
debug("Admin: received DeviceMetadata from $fromNodeNum")
Timber.d("Admin: received DeviceMetadata from $fromNodeNum")
serviceScope.handledLaunch {
nodeRepository.insertMetadata(MetadataEntity(fromNodeNum, a.getDeviceMetadataResponse))
}
}
else -> warn("No special processing needed for ${a.payloadVariantCase}")
else -> Timber.w("No special processing needed for ${a.payloadVariantCase}")
}
debug("Admin: Received session_passkey from $fromNodeNum")
Timber.d("Admin: Received session_passkey from $fromNodeNum")
sessionPasskey = a.sessionPasskey
}
@ -931,7 +929,7 @@ class MeshService :
p
} else {
p.copy {
warn("Public key mismatch from $longName ($shortName)")
Timber.w("Public key mismatch from $longName ($shortName)")
publicKey = NodeEntity.ERROR_BYTE_STRING
}
}
@ -962,10 +960,10 @@ class MeshService :
// (only)
// we don't record these nop position updates
if (myNodeNum == fromNum && p.latitudeI == 0 && p.longitudeI == 0) {
debug("Ignoring nop position update for the local node")
Timber.d("Ignoring nop position update for the local node")
} else {
updateNodeInfo(fromNum) {
debug("update position: ${it.longName?.toPIIString()} with ${p.toPIIString()}")
Timber.d("update position: ${it.longName?.toPIIString()} with ${p.toPIIString()}")
it.setPosition(p, (defaultTime / 1000L).toInt())
}
}
@ -1036,7 +1034,7 @@ class MeshService :
}
private fun handleReceivedStoreAndForward(dataPacket: DataPacket, s: StoreAndForwardProtos.StoreAndForward) {
debug("StoreAndForward: ${s.variantCase} ${s.rr} from ${dataPacket.from}")
Timber.d("StoreAndForward: ${s.variantCase} ${s.rr} from ${dataPacket.from}")
when (s.variantCase) {
StoreAndForwardProtos.StoreAndForward.VariantCase.STATS -> {
val u =
@ -1093,7 +1091,7 @@ class MeshService :
)
onNodeDBChanged()
} else {
warn("Ignoring early received packet: ${packet.toOneLineString()}")
Timber.w("Ignoring early received packet: ${packet.toOneLineString()}")
// earlyReceivedPackets.add(packet)
// logAssert(earlyReceivedPackets.size < 128) // The max should normally be about 32,
// but if the device is
@ -1104,7 +1102,7 @@ class MeshService :
private fun sendNow(p: DataPacket) {
val packet = toMeshPacket(p)
p.time = System.currentTimeMillis() // update time to the actual time we started sending
// debug("Sending to radio: ${packet.toPIIString()}")
// Timber.d("Sending to radio: ${packet.toPIIString()}")
packetHandler.sendToRadio(packet)
}
@ -1115,7 +1113,7 @@ class MeshService :
sendNow(p)
sentPackets.add(p)
} catch (ex: Exception) {
errormsg("Error sending queued message:", ex)
Timber.e("Error sending queued message:", ex)
}
}
offlineSentPackets.removeAll(sentPackets)
@ -1150,7 +1148,7 @@ class MeshService :
// decided to pass through to us (except for broadcast packets)
// val toNum = packet.to
// debug("Received: $packet")
// Timber.d("Received: $packet")
if (packet.hasDecoded()) {
val packetToSave =
MeshLog(
@ -1232,19 +1230,12 @@ class MeshService :
/** Send in analytics about mesh connection */
private fun reportConnection() {
val radioModel = DataPair("radio_model", myNodeInfo?.model ?: "unknown")
GeeksvilleApplication.analytics.track(
MeshUtilApplication.analytics.track(
"mesh_connect",
DataPair("num_nodes", numNodes),
DataPair("num_online", numOnlineNodes),
radioModel,
)
// Once someone connects to hardware start tracking the approximate number of nodes in their
// mesh
// this allows us to collect stats on what typical mesh size is and to tell difference
// between users who just
// downloaded the app, vs has connected it to some hardware.
GeeksvilleApplication.analytics.setUserInfo(DataPair("num_nodes", numNodes), radioModel)
}
private var sleepTimeout: Job? = null
@ -1254,7 +1245,7 @@ class MeshService :
// Called when we gain/lose connection to our radio
private fun onConnectionChanged(c: ConnectionState) {
debug("onConnectionChanged: ${connectionStateHolder.getState()} -> $c")
Timber.d("onConnectionChanged: ${connectionStateHolder.getState()} -> $c")
// Perform all the steps needed once we start waiting for device sleep to complete
fun startDeviceSleep() {
@ -1266,7 +1257,10 @@ class MeshService :
val now = System.currentTimeMillis()
connectTimeMsec = 0L
GeeksvilleApplication.analytics.track("connected_seconds", DataPair((now - connectTimeMsec) / 1000.0))
MeshUtilApplication.analytics.track(
"connected_seconds",
DataPair("connected_seconds", (now - connectTimeMsec) / 1000.0),
)
}
// Have our timeout fire in the appropriate number of seconds
@ -1277,12 +1271,12 @@ class MeshService :
// wait 30 seconds
val timeout = (localConfig.power?.lsSecs ?: 0) + 30
debug("Waiting for sleeping device, timeout=$timeout secs")
Timber.d("Waiting for sleeping device, timeout=$timeout secs")
delay(timeout * 1000L)
warn("Device timeout out, setting disconnected")
Timber.w("Device timeout out, setting disconnected")
onConnectionChanged(ConnectionState.DISCONNECTED)
} catch (ex: CancellationException) {
debug("device sleep timeout cancelled")
Timber.d("device sleep timeout cancelled")
}
}
@ -1295,12 +1289,12 @@ class MeshService :
stopLocationRequests()
stopMqttClientProxy()
GeeksvilleApplication.analytics.track(
MeshUtilApplication.analytics.track(
"mesh_disconnect",
DataPair("num_nodes", numNodes),
DataPair("num_online", numOnlineNodes),
)
GeeksvilleApplication.analytics.track("num_nodes", DataPair(numNodes))
MeshUtilApplication.analytics.track("num_nodes", DataPair("num_nodes", numNodes))
// broadcast an intent with our new connection state
serviceBroadcasts.broadcastConnection()
@ -1312,12 +1306,12 @@ class MeshService :
connectTimeMsec = System.currentTimeMillis()
startConfig()
} catch (ex: InvalidProtocolBufferException) {
errormsg("Invalid protocol buffer sent by device - update device software and try again", ex)
Timber.e("Invalid protocol buffer sent by device - update device software and try again", ex)
} catch (ex: RadioNotConnectedException) {
// note: no need to call startDeviceSleep(), because this exception could only have
// reached us if it was
// already called
errormsg("Lost connection to radio during init - waiting for reconnect ${ex.message}")
Timber.e("Lost connection to radio during init - waiting for reconnect ${ex.message}")
} catch (ex: RemoteException) {
// It seems that when the ESP32 goes offline it can briefly come back for a 100ms
// ish which
@ -1437,7 +1431,7 @@ class MeshService :
// Explicitly handle default/unwanted cases to satisfy the exhaustive `when`
PayloadVariantCase.PAYLOADVARIANT_NOT_SET -> { proto ->
errormsg("Unexpected or unrecognized FromRadio variant: ${proto.payloadVariantCase}")
Timber.e("Unexpected or unrecognized FromRadio variant: ${proto.payloadVariantCase}")
}
}
}
@ -1452,7 +1446,7 @@ class MeshService :
val proto = MeshProtos.FromRadio.parseFrom(bytes)
proto.route()
} catch (ex: InvalidProtocolBufferException) {
errormsg("Invalid Protobuf from radio, len=${bytes.size}", ex)
Timber.e("Invalid Protobuf from radio, len=${bytes.size}", ex)
}
}
@ -1463,7 +1457,7 @@ class MeshService :
private val newNodes = mutableListOf<MeshProtos.NodeInfo>()
private fun handleDeviceConfig(config: ConfigProtos.Config) {
debug("Received config ${config.toOneLineString()}")
Timber.d("Received config ${config.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@ -1479,7 +1473,7 @@ class MeshService :
}
private fun handleModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
debug("Received moduleConfig ${config.toOneLineString()}")
Timber.d("Received moduleConfig ${config.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@ -1495,7 +1489,7 @@ class MeshService :
}
private fun handleChannel(ch: ChannelProtos.Channel) {
debug("Received channel ${ch.index}")
Timber.d("Received channel ${ch.index}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@ -1553,7 +1547,7 @@ class MeshService :
}
private fun handleNodeInfo(info: MeshProtos.NodeInfo) {
debug(
Timber.d(
"Received nodeinfo num=${info.num}," +
" hasUser=${info.hasUser()}," +
" hasPosition=${info.hasPosition()}," +
@ -1616,10 +1610,7 @@ class MeshService :
val mi = myNodeInfo
if (myInfo != null && mi != null) {
// Track types of devices and firmware versions in use
GeeksvilleApplication.analytics.setUserInfo(
DataPair("firmware", mi.firmwareVersion),
DataPair("hw_model", mi.model),
)
analytics.setDeviceAttributes(mi.firmwareVersion ?: "unknown", mi.model ?: "unknown")
}
}
@ -1647,7 +1638,7 @@ class MeshService :
/** Update our DeviceMetadata */
private fun handleMetadata(metadata: MeshProtos.DeviceMetadata) {
debug("Received deviceMetadata ${metadata.toOneLineString()}")
Timber.d("Received deviceMetadata ${metadata.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@ -1679,7 +1670,7 @@ class MeshService :
}
private fun handleClientNotification(notification: MeshProtos.ClientNotification) {
debug("Received clientNotification ${notification.toOneLineString()}")
Timber.d("Received clientNotification ${notification.toOneLineString()}")
serviceRepository.setClientNotification(notification)
serviceNotifications.showClientNotification(notification)
// if the future for the originating request is still in the queue, complete as unsuccessful
@ -1688,7 +1679,7 @@ class MeshService :
}
private fun handleFileInfo(fileInfo: MeshProtos.FileInfo) {
debug("Received fileInfo ${fileInfo.toOneLineString()}")
Timber.d("Received fileInfo ${fileInfo.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@ -1701,7 +1692,7 @@ class MeshService :
}
private fun handleLogReord(logRecord: MeshProtos.LogRecord) {
debug("Received logRecord ${logRecord.toOneLineString()}")
Timber.d("Received logRecord ${logRecord.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@ -1714,7 +1705,7 @@ class MeshService :
}
private fun handleRebooted(rebooted: Boolean) {
debug("Received rebooted ${rebooted.toOneLineString()}")
Timber.d("Received rebooted ${rebooted.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@ -1727,7 +1718,7 @@ class MeshService :
}
private fun handleXmodemPacket(xmodemPacket: XmodemProtos.XModem) {
debug("Received XmodemPacket ${xmodemPacket.toOneLineString()}")
Timber.d("Received XmodemPacket ${xmodemPacket.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@ -1740,7 +1731,7 @@ class MeshService :
}
private fun handleDeviceUiConfig(deviceuiConfig: DeviceUIProtos.DeviceUIConfig) {
debug("Received deviceUIConfig ${deviceuiConfig.toOneLineString()}")
Timber.d("Received deviceUIConfig ${deviceuiConfig.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@ -1768,7 +1759,7 @@ class MeshService :
private fun stopMqttClientProxy() {
if (mqttMessageFlow?.isActive == true) {
info("Stopping MqttClientProxy")
Timber.i("Stopping MqttClientProxy")
mqttMessageFlow?.cancel()
mqttMessageFlow = null
}
@ -1785,13 +1776,13 @@ class MeshService :
private fun handleConfigComplete(configCompleteId: Int) {
if (configCompleteId == configNonce) {
debug("Received config complete for config-only nonce $configNonce")
Timber.d("Received config complete for config-only nonce $configNonce")
handleConfigComplete()
}
}
private fun handleConfigComplete() {
debug("Received config only complete for nonce $configNonce")
Timber.d("Received config only complete for nonce $configNonce")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@ -1804,13 +1795,13 @@ class MeshService :
// This was our config request
if (newMyNodeInfo == null) {
errormsg("Did not receive a valid config")
Timber.e("Did not receive a valid config")
} else {
myNodeInfo = newMyNodeInfo
}
// This was our config request
if (newNodes.isEmpty()) {
errormsg("Did not receive a valid node info")
Timber.e("Did not receive a valid node info")
} else {
newNodes.forEach(::installNodeInfo)
newNodes.clear()
@ -1829,7 +1820,7 @@ class MeshService :
newMyNodeInfo = null
newNodes.clear()
debug("Starting config only nonce=$configNonce")
Timber.d("Starting config only nonce=$configNonce")
packetHandler.sendToRadio(ToRadio.newBuilder().apply { this.wantConfigId = configNonce })
}
@ -1840,7 +1831,7 @@ class MeshService :
val mi = myNodeInfo
if (mi != null) {
val idNum = destNum ?: mi.myNodeNum // when null we just send to the local node
debug("Sending our position/time to=$idNum ${Position(position)}")
Timber.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
@ -1865,7 +1856,7 @@ class MeshService :
)
}
} catch (ex: BLEException) {
warn("Ignoring disconnected radio during gps location update")
Timber.w("Ignoring disconnected radio during gps location update")
}
}
@ -1876,9 +1867,9 @@ class MeshService :
@Suppress("ComplexCondition")
if (user == old) {
debug("Ignoring nop owner change")
Timber.d("Ignoring nop owner change")
} else {
debug(
Timber.d(
"setOwner Id: $id longName: ${longName.anonymize}" +
" shortName: $shortName isLicensed: $isLicensed" +
" isUnmessagable: $isUnmessagable",
@ -1942,10 +1933,10 @@ class MeshService :
packetHandler.sendToRadio(
newMeshPacketTo(myNodeNum).buildAdminPacket {
if (node.isFavorite) {
debug("removing node ${node.num} from favorite list")
Timber.d("removing node ${node.num} from favorite list")
removeFavoriteNode = node.num
} else {
debug("adding node ${node.num} to favorite list")
Timber.d("adding node ${node.num} to favorite list")
setFavoriteNode = node.num
}
},
@ -1957,10 +1948,10 @@ class MeshService :
packetHandler.sendToRadio(
newMeshPacketTo(myNodeNum).buildAdminPacket {
if (node.isIgnored) {
debug("removing node ${node.num} from ignore list")
Timber.d("removing node ${node.num} from ignore list")
removeIgnoredNode = node.num
} else {
debug("adding node ${node.num} to ignore list")
Timber.d("adding node ${node.num} to ignore list")
setIgnoredNode = node.num
}
},
@ -1985,21 +1976,23 @@ class MeshService :
}
fun clearDatabases() = serviceScope.handledLaunch {
debug("Clearing nodeDB")
Timber.d("Clearing nodeDB")
discardNodeDB()
nodeRepository.clearNodeDB()
}
private fun updateLastAddress(deviceAddr: String?) {
val currentAddr = meshPrefs.deviceAddress
debug("setDeviceAddress: received request to change to: ${deviceAddr.anonymize}")
Timber.d("setDeviceAddress: received request to change to: ${deviceAddr.anonymize}")
if (deviceAddr != currentAddr) {
debug("SetDeviceAddress: Device address changed from ${currentAddr.anonymize} to ${deviceAddr.anonymize}")
Timber.d(
"SetDeviceAddress: Device address changed from ${currentAddr.anonymize} to ${deviceAddr.anonymize}",
)
meshPrefs.deviceAddress = deviceAddr
clearDatabases()
clearNotifications()
} else {
debug("SetDeviceAddress: Device address is unchanged, ignoring.")
Timber.d("SetDeviceAddress: Device address is unchanged, ignoring.")
}
}
@ -2011,7 +2004,7 @@ class MeshService :
object : IMeshService.Stub() {
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
debug("Passing through device change to radio service: ${deviceAddr.anonymize}")
Timber.d("Passing through device change to radio service: ${deviceAddr.anonymize}")
updateLastAddress(deviceAddr)
radioInterfaceService.setDeviceAddress(deviceAddr)
}
@ -2063,7 +2056,7 @@ class MeshService :
if (p.id == 0) p.id = generatePacketId()
val bytes = p.bytes!!
info(
Timber.i(
"sendData dest=${p.to}, id=${p.id} <- ${bytes.size} bytes" +
" (connectionState=${connectionStateHolder.getState()})",
)
@ -2083,7 +2076,7 @@ class MeshService :
try {
sendNow(p)
} catch (ex: Exception) {
errormsg("Error sending message, so enqueueing", ex)
Timber.e("Error sending message, so enqueueing", ex)
enqueueForSending(p)
}
} else {
@ -2094,13 +2087,11 @@ class MeshService :
// Keep a record of DataPackets, so GUIs can show proper chat history
rememberDataPacket(p, false)
GeeksvilleApplication.analytics.track(
MeshUtilApplication.analytics.track(
"data_send",
DataPair("num_bytes", bytes.size),
DataPair("type", p.dataType),
)
GeeksvilleApplication.analytics.track("num_data_sent", DataPair(1))
}
}
@ -2114,7 +2105,7 @@ class MeshService :
}
override fun setRemoteConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
debug("Setting new radio config!")
Timber.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) // Update our local copy
@ -2134,7 +2125,7 @@ class MeshService :
/** Send our current module config to the device */
override fun setModuleConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
debug("Setting new module config!")
Timber.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) // Update our local copy
@ -2203,14 +2194,14 @@ class MeshService :
override fun getNodes(): MutableList<NodeInfo> = toRemoteExceptions {
val r = nodeDBbyNodeNum.values.map { it.toNodeInfo() }.toMutableList()
info("in getOnline, count=${r.size}")
Timber.i("in getOnline, count=${r.size}")
// return arrayOf("+16508675309")
r
}
override fun connectionState(): String = toRemoteExceptions {
val r = connectionStateHolder.getState()
info("in connectionState=$r")
Timber.i("in connectionState=$r")
r.toString()
}
@ -2250,7 +2241,7 @@ class MeshService :
}
if (currentPosition == null) {
debug("Position request skipped - no valid position available")
Timber.d("Position request skipped - no valid position available")
return@toRemoteExceptions
}

View file

@ -25,6 +25,7 @@ import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.NodeInfo
import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -49,7 +50,7 @@ constructor(
}
fun broadcastNodeChange(info: NodeInfo) {
MeshService.debug("Broadcasting node change $info")
Timber.d("Broadcasting node change $info")
val intent = Intent(MeshService.ACTION_NODE_CHANGE).putExtra(EXTRA_NODEINFO, info)
explicitBroadcast(intent)
}
@ -58,10 +59,10 @@ constructor(
fun broadcastMessageStatus(id: Int, status: MessageStatus?) {
if (id == 0) {
MeshService.debug("Ignoring anonymous packet status")
Timber.d("Ignoring anonymous packet status")
} else {
// Do not log, contains PII possibly
// MeshService.debug("Broadcasting message status $p")
// MeshService.Timber.d("Broadcasting message status $p")
val intent =
Intent(MeshService.ACTION_MESSAGE_STATUS).apply {
putExtra(EXTRA_PACKET_ID, id)

View file

@ -26,6 +26,7 @@ import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.geeksville.mesh.BuildConfig
import timber.log.Timber
import java.util.concurrent.TimeUnit
/** Helper that calls MeshService.startService() */
@ -37,7 +38,7 @@ class ServiceStarter(appContext: Context, workerParams: WorkerParameters) : Work
// Indicate whether the task finished successfully with the Result
Result.success()
} catch (ex: Exception) {
MeshService.errormsg("failure starting service, will retry", ex)
Timber.e("failure starting service, will retry", ex)
Result.retry()
}
}
@ -48,7 +49,7 @@ class ServiceStarter(appContext: Context, workerParams: WorkerParameters) : Work
*/
fun MeshService.Companion.startServiceLater(context: Context) {
// No point in even starting the service if the user doesn't have a device bonded
info("Received boot complete announcement, starting mesh service in two minutes")
Timber.i("Received boot complete announcement, starting mesh service in two minutes")
val delayRequest =
OneTimeWorkRequestBuilder<ServiceStarter>()
.setInitialDelay(2, TimeUnit.MINUTES)
@ -69,14 +70,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.
info("Trying to start service debug=${BuildConfig.DEBUG}")
Timber.i("Trying to start service debug=${BuildConfig.DEBUG}")
val intent = createIntent()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
context.startForegroundService(intent)
} catch (ex: ForegroundServiceStartNotAllowedException) {
errormsg("Unable to start service: ${ex.message}")
Timber.e("Unable to start service: ${ex.message}")
}
} else {
context.startForegroundService(intent)

View file

@ -20,9 +20,6 @@ package com.geeksville.mesh.service
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.errormsg
import com.geeksville.mesh.android.BuildUtils.info
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.fromRadio
import com.geeksville.mesh.repository.radio.RadioInterfaceService
@ -41,6 +38,7 @@ import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.util.toOneLineString
import org.meshtastic.core.model.util.toPIIString
import org.meshtastic.core.service.ConnectionState
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.TimeUnit
@ -71,7 +69,7 @@ constructor(
*/
fun sendToRadio(p: ToRadio.Builder) {
val built = p.build()
debug("Sending to radio ${built.toPIIString()}")
Timber.d("Sending to radio ${built.toPIIString()}")
val b = built.toByteArray()
radioInterfaceService.sendToRadio(b)
@ -103,7 +101,7 @@ constructor(
fun stopPacketQueue() {
if (queueJob?.isActive == true) {
info("Stopping packet queueJob")
Timber.i("Stopping packet queueJob")
queueJob?.cancel()
queueJob = null
queuedPackets.clear()
@ -113,7 +111,7 @@ constructor(
}
fun handleQueueStatus(queueStatus: MeshProtos.QueueStatus) {
debug("queueStatus ${queueStatus.toOneLineString()}")
Timber.d("queueStatus ${queueStatus.toOneLineString()}")
val (success, isFull, requestId) = with(queueStatus) { Triple(res == 0, free == 0, meshPacketId) }
if (success && isFull) return // Queue is full, wait for free != 0
if (requestId != 0) {
@ -132,20 +130,20 @@ constructor(
if (queueJob?.isActive == true) return
queueJob =
scope.handledLaunch {
debug("packet queueJob started")
Timber.d("packet queueJob started")
while (connectionStateHolder.getState() == 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)
debug("queueJob packet id=${packet.id.toUInt()} waiting")
Timber.d("queueJob packet id=${packet.id.toUInt()} waiting")
val success = response.get(2, TimeUnit.MINUTES)
debug("queueJob packet id=${packet.id.toUInt()} success $success")
Timber.d("queueJob packet id=${packet.id.toUInt()} success $success")
} catch (e: TimeoutException) {
debug("queueJob packet id=${packet.id.toUInt()} timeout")
Timber.d("queueJob packet id=${packet.id.toUInt()} timeout")
} catch (e: Exception) {
debug("queueJob packet id=${packet.id.toUInt()} failed")
Timber.d("queueJob packet id=${packet.id.toUInt()} failed")
}
}
}
@ -182,7 +180,7 @@ constructor(
if (connectionStateHolder.getState() != ConnectionState.CONNECTED) throw RadioNotConnectedException()
sendToRadio(ToRadio.newBuilder().apply { this.packet = packet })
} catch (ex: Exception) {
errormsg("sendToRadio error:", ex)
Timber.e("sendToRadio error:", ex)
future.complete(false)
}
return future

View file

@ -31,11 +31,11 @@ import android.os.Build
import android.os.DeadObjectException
import android.os.Handler
import android.os.Looper
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.MeshUtilApplication.Companion.analytics
import com.geeksville.mesh.concurrent.CallbackContinuation
import com.geeksville.mesh.concurrent.Continuation
import com.geeksville.mesh.concurrent.SyncContinuation
import com.geeksville.mesh.logAssert
import com.geeksville.mesh.util.exceptionReporter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -43,6 +43,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.Closeable
import java.util.Random
import java.util.UUID
@ -62,9 +63,7 @@ fun longBLEUUID(hexFour: String): UUID = UUID.fromString("0000$hexFour-0000-1000
*
* This class fixes the API by using coroutines to let you safely do a series of BTLE operations.
*/
class SafeBluetooth(private val context: Context, private val device: BluetoothDevice) :
Logging,
Closeable {
class SafeBluetooth(private val context: Context, private val device: BluetoothDevice) : Closeable {
// / Timeout before we declare a bluetooth operation failed (used for synchronous API operations only)
var timeoutMsec = 20 * 1000L
@ -102,11 +101,11 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val completion: com.geeksville.mesh.concurrent.Continuation<*>,
val timeoutMillis: Long = 0, // If we want to timeout this operation at a certain time, use a non zero value
private val startWorkFn: () -> Boolean,
) : Logging {
) {
// / Start running a queued bit of work, return true for success or false for fatal bluetooth error
fun startWork(): Boolean {
debug("Starting work: $tag")
Timber.d("Starting work: $tag")
return startWorkFn()
}
@ -123,8 +122,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
private val mHandler: Handler = Handler(Looper.getMainLooper())
fun restartBle() {
GeeksvilleApplication.analytics.track("ble_restart") // record # of times we needed to use this nasty hack
errormsg("Doing emergency BLE restart")
analytics.track("ble_restart") // record # of times we needed to use this nasty hack
Timber.w("Doing emergency BLE restart")
context.bluetoothManager?.adapter?.let { adp ->
if (adp.isEnabled) {
adp.disable()
@ -168,7 +167,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
object : BluetoothGattCallback() {
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) = exceptionReporter {
info("new bluetooth connection state $newState, status $status")
Timber.i("new bluetooth connection state $newState, status $status")
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
@ -177,7 +176,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// If autoconnect is on and this connect attempt failed, hopefully some future attempt will
// succeed
if (status != BluetoothGatt.GATT_SUCCESS && autoConnect) {
errormsg("Connect attempt failed $status, not calling connect completion handler...")
Timber.e("Connect attempt failed $status, not calling connect completion handler...")
} else {
completeWork(status, Unit)
}
@ -185,9 +184,9 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
BluetoothProfile.STATE_DISCONNECTED -> {
if (gatt == null) {
errormsg("No gatt: ignoring connection state $newState, status $status")
Timber.e("No gatt: ignoring connection state $newState, status $status")
} else if (isClosing) {
info("Got disconnect because we are shutting down, closing gatt")
Timber.i("Got disconnect because we are shutting down, closing gatt")
gatt = null
g.close() // Finish closing our gatt here
} else {
@ -195,7 +194,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val oldstate = state
state = newState
if (oldstate == BluetoothProfile.STATE_CONNECTED) {
info("Lost connection - aborting current work: $currentWork")
Timber.i("Lost connection - aborting current work: $currentWork")
// If we get a disconnect, just try again otherwise fail all current operations
// Note: if no work is pending (likely) we also just totally teardown and restart the
@ -218,12 +217,12 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// you will get a callback with status=133. Then call BluetoothGatt#connect()
// to initiate a background connection.
if (autoConnect) {
warn("Failed on non-auto connect, falling back to auto connect attempt")
Timber.w("Failed on non-auto connect, falling back to auto connect attempt")
closeGatt() // Close the old non-auto connection
lowLevelConnect(true)
}
} else if (status == 147) {
info("got 147, calling lostConnection()")
Timber.i("got 147, calling lostConnection()")
lostConnection("code 147")
}
@ -261,7 +260,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val reliable = currentReliableWrite
if (reliable != null) {
if (!characteristic.value.contentEquals(reliable)) {
errormsg("A reliable write failed!")
Timber.e("A reliable write failed!")
gatt.abortReliableWrite()
completeWork(STATUS_RELIABLE_WRITE_FAILED, characteristic) // skanky code to indicate failure
} else {
@ -278,7 +277,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
// Alas, passing back an Int mtu isn't working and since I don't really care what MTU
// the device was willing to let us have I'm just punting and returning Unit
if (isSettingMtu) completeWork(status, Unit) else errormsg("Ignoring bogus onMtuChanged")
if (isSettingMtu) completeWork(status, Unit) else Timber.e("Ignoring bogus onMtuChanged")
}
/**
@ -290,7 +289,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val handler = notifyHandlers.get(characteristic.uuid)
if (handler == null) {
warn("Received notification from $characteristic, but no handler registered")
Timber.w("Received notification from $characteristic, but no handler registered")
} else {
exceptionReporter { handler(characteristic) }
}
@ -344,9 +343,9 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
if (newWork.timeoutMillis != 0L) {
activeTimeout =
serviceScope.launch {
// debug("Starting failsafe timer ${newWork.timeoutMillis}")
// Timber.d("Starting failsafe timer ${newWork.timeoutMillis}")
delay(newWork.timeoutMillis)
errormsg("Failsafe BLE timer expired!")
Timber.e("Failsafe BLE timer expired!")
completeWork(STATUS_TIMEOUT, Unit) // Throw an exception in that work
}
}
@ -356,12 +355,12 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val failThis = simFailures && !newWork.isConnect() && failRandom.nextInt(100) < failPercent
if (failThis) {
errormsg("Simulating random work failure!")
Timber.e("Simulating random work failure!")
completeWork(STATUS_SIMFAILURE, Unit)
} else {
val started = newWork.startWork()
if (!started) {
errormsg("Failed to start work, returned error status")
Timber.e("Failed to start work, returned error status")
completeWork(STATUS_NOSTART, Unit) // abandon the current attempt and try for another
}
}
@ -372,7 +371,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val btCont = BluetoothContinuation(tag, cont, timeout, initFn)
synchronized(workQueue) {
debug("Enqueuing work: ${btCont.tag}")
Timber.d("Enqueuing work: ${btCont.tag}")
workQueue.add(btCont)
// if we don't have any outstanding operations, run first item in queue
@ -409,9 +408,9 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
}
if (work == null) {
warn("wor completed, but we already killed it via failsafetimer? status=$status, res=$res")
Timber.w("wor completed, but we already killed it via failsafetimer? status=$status, res=$res")
} else {
// debug("work ${work.tag} is completed, resuming status=$status, res=$res")
// Timber.d("work ${work.tag} is completed, resuming status=$status, res=$res")
if (status != 0) {
work.completion.resumeWithException(
BLEStatusException(status, "Bluetooth status=$status while doing ${work.tag}"),
@ -426,12 +425,12 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
/** Something went wrong, abort all queued */
private fun failAllWork(ex: Exception) {
synchronized(workQueue) {
warn("Failing ${workQueue.size} works, because ${ex.message}")
Timber.w("Failing ${workQueue.size} works, because ${ex.message}")
workQueue.forEach {
try {
it.completion.resumeWithException(ex)
} catch (ex: Exception) {
errormsg("Mystery exception, why were we informed about our own exceptions?", ex)
Timber.e("Mystery exception, why were we informed about our own exceptions?", ex)
}
}
workQueue.clear()
@ -531,7 +530,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
notifyHandlers.clear()
lostConnectCallback?.let {
debug("calling lostConnect handler")
Timber.d("calling lostConnect handler")
it.invoke()
}
}
@ -543,7 +542,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// Queue a new connection attempt
val cb = connectionCallback
if (cb != null) {
debug("queuing a reconnection callback")
Timber.d("queuing a reconnection callback")
assert(currentWork == null)
if (
@ -557,7 +556,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// need)
queueWork("reconnect", CallbackContinuation(cb), 0) { true }
} else {
debug("No connectionCallback registered")
Timber.d("No connectionCallback registered")
}
}
@ -691,7 +690,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
/** Close just the GATT device but keep our pending callbacks active */
fun closeGatt() {
gatt?.let { g ->
info("Closing our GATT connection")
Timber.i("Closing our GATT connection")
isClosing = true
try {
g.disconnect()
@ -704,7 +703,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
}
gatt?.let { g2 ->
warn("Android onConnectionStateChange did not run, manually closing")
Timber.w("Android onConnectionStateChange did not run, manually closing")
gatt = null // clear gat before calling close, bcause close might throw dead object exception
g2.close()
}
@ -712,9 +711,9 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// Attempt to invoke virtual method 'com.android.bluetooth.gatt.AdvertiseClient
// com.android.bluetooth.gatt.AdvertiseManager.getAdvertiseClient(int)' on a null object reference
// com.geeksville.mesh.service.SafeBluetooth.closeGatt
warn("Ignoring NPE in close - probably buggy Samsung BLE")
Timber.w("Ignoring NPE in close - probably buggy Samsung BLE")
} catch (ex: DeadObjectException) {
warn("Ignoring dead object exception, probably bluetooth was just disabled")
Timber.w("Ignoring dead object exception, probably bluetooth was just disabled")
} finally {
isClosing = false
}
@ -748,7 +747,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// / asyncronously turn notification on/off for a characteristic
fun setNotify(c: BluetoothGattCharacteristic, enable: Boolean, onChanged: (BluetoothGattCharacteristic) -> Unit) {
debug("starting setNotify(${c.uuid}, $enable)")
Timber.d("starting setNotify(${c.uuid}, $enable)")
notifyHandlers[c.uuid] = onChanged
// c.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
gatt!!.setCharacteristicNotification(c, enable)
@ -765,6 +764,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
} else {
BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
}
asyncWriteDescriptor(descriptor) { debug("Notify enable=$enable completed") }
asyncWriteDescriptor(descriptor) { Timber.d("Notify enable=$enable completed") }
}
}

View file

@ -76,9 +76,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.android.AddNavigationTracking
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.setAttributes
import com.geeksville.mesh.MeshUtilApplication.Companion.analytics
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.channelsGraph
@ -118,6 +116,7 @@ import org.meshtastic.core.ui.icon.Nodes
import org.meshtastic.core.ui.icon.Settings
import org.meshtastic.core.ui.theme.StatusColors.StatusBlue
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import timber.log.Timber
enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, val route: Route) {
Conversations(R.string.conversations, MeshtasticIcons.Conversations, ContactsRoutes.ContactsGraph),
@ -150,14 +149,13 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
}
}
AddNavigationTracking(navController)
if (connectionState == ConnectionState.CONNECTED) {
requestChannelSet?.let { newChannelSet -> ScannedQrCodeDialog(uIViewModel, newChannelSet) }
}
VersionChecks(uIViewModel)
analytics.addNavigationTrackingEffect(navController = navController)
VersionChecks(uIViewModel)
val alertDialogState by uIViewModel.currentAlert.collectAsStateWithLifecycle()
alertDialogState?.let { state ->
if (state.choices.isNotEmpty()) {
@ -230,8 +228,6 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
val receiveColor = capturedColorScheme.StatusBlue
LaunchedEffect(uIViewModel.meshActivity, capturedColorScheme) {
uIViewModel.meshActivity.collectLatest { activity ->
debug("MeshActivity Event: $activity, Current Alpha: ${animatedGlowAlpha.value}")
val newTargetColor =
when (activity) {
is MeshActivity.Send -> sendColor
@ -416,16 +412,12 @@ private fun VersionChecks(viewModel: UIViewModel) {
val firmwareEdition by viewModel.firmwareEdition.collectAsStateWithLifecycle(null)
val currentFirmwareVersion by viewModel.firmwareVersion.collectAsStateWithLifecycle(null)
val currentDeviceHardware by viewModel.deviceHardware.collectAsStateWithLifecycle(null)
val latestStableFirmwareRelease by
viewModel.latestStableFirmwareRelease.collectAsStateWithLifecycle(DeviceVersion("2.6.4"))
LaunchedEffect(connectionState, firmwareEdition) {
if (connectionState == ConnectionState.CONNECTED) {
firmwareEdition?.let { edition ->
debug("FirmwareEdition: ${edition.name}")
Timber.d("FirmwareEdition: ${edition.name}")
when (edition) {
MeshProtos.FirmwareEdition.VANILLA -> {
// Handle any specific logic for VANILLA firmware edition if needed
@ -439,14 +431,6 @@ private fun VersionChecks(viewModel: UIViewModel) {
}
}
LaunchedEffect(connectionState, currentFirmwareVersion, currentDeviceHardware) {
if (connectionState == ConnectionState.CONNECTED) {
if (currentDeviceHardware != null && currentFirmwareVersion != null) {
setAttributes(currentFirmwareVersion!!, currentDeviceHardware!!)
}
}
}
// Check if the device is running an old app version or firmware version
LaunchedEffect(connectionState, myNodeInfo) {
if (connectionState == ConnectionState.CONNECTED) {

View file

@ -77,7 +77,6 @@ import androidx.compose.ui.unit.sp
import androidx.datastore.core.IOException
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.android.BuildUtils.warn
import com.geeksville.mesh.model.DebugViewModel
import com.geeksville.mesh.model.DebugViewModel.UiMeshLog
import kotlinx.collections.immutable.toImmutableList
@ -89,6 +88,7 @@ import org.meshtastic.core.ui.component.CopyIconButton
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.theme.AnnotationColor
import org.meshtastic.core.ui.theme.AppTheme
import timber.log.Timber
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
@ -394,7 +394,7 @@ private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: L
)
.show()
}
warn("Error:IOException: " + e.toString())
Timber.w(e, "Error:IOException ")
}
}

View file

@ -853,19 +853,19 @@ private fun MainNodeDetails(node: Node, ourNode: Node?, displayUnits: ConfigProt
icon = Icons.Default.History,
trailingText = formatAgo(node.lastHeard),
)
val distance = ourNode?.distance(node)?.toDistanceString(displayUnits)
if (node != ourNode && distance != null) {
val distance = ourNode?.distance(node)?.takeIf { it > 0 }?.toDistanceString(displayUnits)
if (distance != null && distance.isNotEmpty()) {
SettingsItemDetail(
text = stringResource(R.string.node_sort_distance),
icon = Icons.Default.SocialDistance,
trailingText = distance,
)
SettingsItemDetail(
text = stringResource(R.string.last_position_update),
icon = Icons.Default.LocationOn,
trailingText = formatAgo(node.position.time),
)
}
SettingsItemDetail(
text = stringResource(R.string.last_position_update),
icon = Icons.Default.LocationOn,
trailingText = formatAgo(node.position.time),
)
}
@Composable

View file

@ -40,11 +40,11 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.core.net.toUri
import com.geeksville.mesh.android.BuildUtils.debug
import kotlinx.coroutines.launch
import org.meshtastic.core.model.util.GPSFormat
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.HyperlinkBlue
import timber.log.Timber
import java.net.URLEncoder
@OptIn(ExperimentalFoundationApi::class)
@ -69,7 +69,7 @@ fun LinkedCoordinates(modifier: Modifier = Modifier, latitude: Double, longitude
onLongClick = {
coroutineScope.launch {
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", annotatedString)))
debug("Copied to clipboard")
Timber.d("Copied to clipboard")
}
},
),
@ -106,7 +106,7 @@ private fun handleClick(context: Context, annotatedString: AnnotatedString) {
Toast.makeText(context, "No application available to open this location!", Toast.LENGTH_LONG).show()
}
} catch (ex: ActivityNotFoundException) {
debug("Failed to open geo intent: $ex")
Timber.d("Failed to open geo intent: $ex")
}
}
}

View file

@ -232,11 +232,12 @@ fun SettingsScreen(
TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
if (state.analyticsAvailable) {
val allowed by viewModel.analyticsAllowedFlow.collectAsStateWithLifecycle(false)
SettingsItemSwitch(
text = stringResource(R.string.analytics_okay),
checked = state.analyticsEnabled,
checked = allowed,
leadingIcon = Icons.Default.BugReport,
onClick = { viewModel.toggleAnalytics() },
onClick = { viewModel.toggleAnalyticsAllowed() },
)
}

View file

@ -24,7 +24,6 @@ import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.Portnums
import com.geeksville.mesh.android.Logging
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -50,6 +49,7 @@ import org.meshtastic.core.model.util.positionToMeter
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
@ -70,8 +70,7 @@ constructor(
private val meshLogRepository: MeshLogRepository,
private val uiPrefs: UiPrefs,
private val uiPreferencesDataSource: UiPreferencesDataSource,
) : ViewModel(),
Logging {
) : ViewModel() {
val myNodeInfo: StateFlow<MyNodeEntity?> = nodeRepository.myNodeInfo
val myNodeNum
@ -254,7 +253,7 @@ constructor(
}
}
} catch (ex: FileNotFoundException) {
errormsg("Can't write file error: ${ex.message}")
Timber.e("Can't write file error: ${ex.message}")
}
}
}

View file

@ -40,9 +40,6 @@ import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.Portnums
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.isAnalyticsAvailable
import com.geeksville.mesh.config
import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.model.getChannelList
@ -80,6 +77,7 @@ import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import timber.log.Timber
import java.io.FileOutputStream
import javax.inject.Inject
@ -113,11 +111,16 @@ constructor(
private val locationRepository: LocationRepository,
private val mapConsentPrefs: MapConsentPrefs,
private val analyticsPrefs: AnalyticsPrefs,
) : ViewModel(),
Logging {
) : ViewModel() {
private val meshService: IMeshService?
get() = serviceRepository.meshService
var analyticsAllowedFlow = analyticsPrefs.getAnalyticsAllowedChangesFlow()
fun toggleAnalyticsAllowed() {
analyticsPrefs.analyticsAllowed = !analyticsPrefs.analyticsAllowed
}
private val destNum = savedStateHandle.toRoute<SettingsRoutes.Settings>().destNum
private val _destNode = MutableStateFlow<Node?>(null)
val destNode: StateFlow<Node?>
@ -169,9 +172,7 @@ constructor(
}
.launchIn(viewModelScope)
_radioConfigState.update { it.copy(analyticsAvailable = (app as GeeksvilleApplication).isAnalyticsAvailable) }
debug("RadioConfigViewModel created")
Timber.d("RadioConfigViewModel created")
}
private val myNodeInfo: StateFlow<MyNodeEntity?>
@ -197,7 +198,7 @@ constructor(
override fun onCleared() {
super.onCleared()
debug("RadioConfigViewModel cleared")
Timber.d("RadioConfigViewModel cleared")
}
private fun request(destNum: Int, requestAction: suspend (IMeshService, Int, Int) -> Unit, errorMessage: String) =
@ -219,7 +220,7 @@ constructor(
}
}
} catch (ex: RemoteException) {
errormsg("$errorMessage: ${ex.message}")
Timber.e("$errorMessage: ${ex.message}")
}
}
}
@ -387,7 +388,7 @@ constructor(
try {
meshService?.setFixedPosition(destNum, position)
} catch (ex: RemoteException) {
errormsg("Set fixed position error: ${ex.message}")
Timber.e("Set fixed position error: ${ex.message}")
}
}
@ -401,7 +402,7 @@ constructor(
onResult(protobuf)
}
} catch (ex: Exception) {
errormsg("Import DeviceProfile error: ${ex.message}")
Timber.e("Import DeviceProfile error: ${ex.message}")
sendError(ex.customMessage)
}
}
@ -417,7 +418,7 @@ constructor(
}
setResponseStateSuccess()
} catch (ex: Exception) {
errormsg("Can't write file error: ${ex.message}")
Timber.e("Can't write file error: ${ex.message}")
sendError(ex.customMessage)
}
}
@ -456,7 +457,7 @@ constructor(
setResponseStateSuccess()
} catch (ex: Exception) {
val errorMessage = "Can't write security keys JSON error: ${ex.message}"
errormsg(errorMessage)
Timber.e(errorMessage)
sendError(ex.customMessage)
}
}
@ -479,7 +480,7 @@ constructor(
try {
setChannels(channelUrl)
} catch (ex: Exception) {
errormsg("DeviceProfile channel import error", ex)
Timber.e(ex, "DeviceProfile channel import error")
sendError(ex.customMessage)
}
}
@ -617,7 +618,7 @@ constructor(
if (data?.portnumValue == Portnums.PortNum.ROUTING_APP_VALUE) {
val parsed = MeshProtos.Routing.parseFrom(data.payload)
debug(debugMsg.format(parsed.errorReason.name))
Timber.d(debugMsg.format(parsed.errorReason.name))
if (parsed.errorReason != MeshProtos.Routing.Error.NONE) {
sendError(getStringResFrom(parsed.errorReasonValue))
} else if (packet.from == destNum && route.isEmpty()) {
@ -631,7 +632,7 @@ constructor(
}
if (data?.portnumValue == Portnums.PortNum.ADMIN_APP_VALUE) {
val parsed = AdminProtos.AdminMessage.parseFrom(data.payload)
debug(debugMsg.format(parsed.payloadVariantCase.name))
Timber.d(debugMsg.format(parsed.payloadVariantCase.name))
if (destNum != packet.from) {
sendError("Unexpected sender: ${packet.from.toUInt()} instead of ${destNum.toUInt()}.")
return
@ -698,7 +699,7 @@ constructor(
incrementCompleted()
}
else -> debug("No custom processing needed for ${parsed.payloadVariantCase}")
else -> Timber.d("No custom processing needed for ${parsed.payloadVariantCase}")
}
if (AdminRoute.entries.any { it.name == route }) {
@ -707,9 +708,4 @@ constructor(
requestIds.update { it.apply { remove(data.requestId) } }
}
}
fun toggleAnalytics() {
analyticsPrefs.analyticsAllowed = !analyticsPrefs.analyticsAllowed
_radioConfigState.update { it.copy(analyticsEnabled = analyticsPrefs.analyticsAllowed) }
}
}

View file

@ -89,10 +89,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.analytics.DataPair
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.errormsg
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.MeshUtilApplication.Companion.analytics
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.UIViewModel
@ -107,6 +104,7 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import kotlinx.coroutines.launch
import org.meshtastic.core.analytics.DataPair
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.util.getChannelUrl
import org.meshtastic.core.model.util.qrCode
@ -116,6 +114,7 @@ import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.AdaptiveTwoPane
import org.meshtastic.core.ui.component.PreferenceFooter
import timber.log.Timber
/**
* Composable screen for managing and sharing Meshtastic channels. Allows users to view, edit, and share channel
@ -184,7 +183,7 @@ fun ChannelScreen(
}
fun zxingScan() {
debug("Starting zxing QR code scanner")
Timber.d("Starting zxing QR code scanner")
val zxingScan = ScanOptions()
zxingScan.setCameraId(0)
zxingScan.setPrompt("")
@ -211,7 +210,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) {
errormsg("ignoring channel problem", ex)
Timber.e("ignoring channel problem", ex)
channelSet = channels // Throw away user edits
@ -239,7 +238,7 @@ fun ChannelScreen(
confirmButton = {
TextButton(
onClick = {
debug("Switching back to default channel")
Timber.d("Switching back to default channel")
installSettings(
Channel.default.settings,
Channel.default.loraConfig.copy {
@ -383,7 +382,7 @@ private fun EditChannelUrl(enabled: Boolean, channelUrl: Uri, modifier: Modifier
else -> {
// track how many times users share channels
GeeksvilleApplication.analytics.track("share", DataPair("content_type", "channel"))
analytics.track("share", DataPair("content_type", "channel"))
coroutineScope.launch {
clipboardManager.setClipEntry(
ClipEntry(ClipData.newPlainText(label, valueState.toString())),

View file

@ -48,8 +48,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.errormsg
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@ -94,7 +92,7 @@ fun AddContactFAB(
try {
uri.toSharedContact()
} catch (ex: MalformedURLException) {
errormsg("URL was malformed: ${ex.message}")
Timber.e("URL was malformed: ${ex.message}")
null
}
if (sharedContact != null) {
@ -136,7 +134,7 @@ fun AddContactFAB(
}
fun zxingScan() {
debug("Starting zxing QR code scanner")
Timber.d("Starting zxing QR code scanner")
val zxingScan = ScanOptions()
zxingScan.setCameraId(CAMERA_ID)
zxingScan.setPrompt("")
@ -229,7 +227,7 @@ val Uri.qrCode: Bitmap?
val barcodeEncoder = BarcodeEncoder()
barcodeEncoder.createBitmap(bitMatrix)
} catch (ex: WriterException) {
errormsg("URL was too complex to render as barcode: ${ex.message}")
Timber.e("URL was too complex to render as barcode: ${ex.message}")
null
}

View file

@ -19,13 +19,10 @@ package com.geeksville.mesh.util
import android.os.RemoteException
import android.util.Log
import android.view.View
import com.geeksville.mesh.android.Logging
import com.google.android.material.snackbar.Snackbar
import timber.log.Timber
object Exceptions : Logging {
/// Set in Application.onCreate
object Exceptions {
// / Set in Application.onCreate
var reporter: ((Throwable, String?, String?) -> Unit)? = null
/**
@ -34,19 +31,17 @@ object Exceptions : Logging {
* After reporting return
*/
fun report(exception: Throwable, tag: String? = null, message: String? = null) {
errormsg(
Timber.e(
exception,
"Exceptions.report: $tag $message",
exception
) // print the message to the log _before_ telling the crash reporter
reporter?.let { r ->
r(exception, tag, message)
}
reporter?.let { r -> r(exception, tag, message) }
}
}
/**
* This wraps (and discards) exceptions, but first it reports them to our bug tracking system and prints
* a message to the log.
* This wraps (and discards) exceptions, but first it reports them to our bug tracking system and prints a message to
* the log.
*/
fun exceptionReporter(inner: () -> Unit) {
try {
@ -57,40 +52,24 @@ fun exceptionReporter(inner: () -> Unit) {
}
}
/**
* If an exception occurs, show the message in a snackbar and continue
*/
fun exceptionToSnackbar(view: View, inner: () -> Unit) {
try {
inner()
} catch (ex: Throwable) {
Snackbar.make(view, ex.message ?: "An exception occurred", Snackbar.LENGTH_LONG).show()
}
}
/**
* This wraps (and discards) exceptions, but it does output a log message
*/
/** This wraps (and discards) exceptions, but it does output a log message */
fun ignoreException(silent: Boolean = false, inner: () -> Unit) {
try {
inner()
} catch (ex: Throwable) {
// DO NOT THROW users expect we have fully handled/discarded the exception
if(!silent)
Exceptions.errormsg("ignoring exception", ex)
if (!silent) Timber.e("ignoring exception", ex)
}
}
/// Convert any exceptions in this service call into a RemoteException that the client can
/// then handle
// / Convert any exceptions in this service call into a RemoteException that the client can
// / then handle
fun <T> toRemoteExceptions(inner: () -> T): T = try {
inner()
} catch (ex: Throwable) {
Log.e("toRemoteExceptions", "Uncaught exception, returning to remote client", ex)
when(ex) { // don't double wrap remote exceptions
when (ex) { // don't double wrap remote exceptions
is RemoteException -> throw ex
else -> throw RemoteException(ex.message)
}
}

View file

@ -18,7 +18,7 @@
package com.geeksville.mesh.util
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.android.BuildUtils.warn
import timber.log.Timber
/**
* Safely extracts the hardware model number from a HardwareModel enum.
@ -34,7 +34,7 @@ import com.geeksville.mesh.android.BuildUtils.warn
fun MeshProtos.HardwareModel.safeNumber(fallbackValue: Int = -1): Int = try {
this.number
} catch (e: IllegalArgumentException) {
warn("Unknown hardware model enum value: $this, using fallback value: $fallbackValue")
Timber.w("Unknown hardware model enum value: $this, using fallback value: $fallbackValue")
fallbackValue
}

View file

@ -20,12 +20,12 @@ package com.geeksville.mesh.util
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import com.geeksville.mesh.android.Logging
import org.meshtastic.core.strings.R
import org.xmlpull.v1.XmlPullParser
import timber.log.Timber
import java.util.Locale
object LanguageUtils : Logging {
object LanguageUtils {
const val SYSTEM_DEFAULT = "zz"
@ -57,7 +57,7 @@ object LanguageUtils : Logging {
}
}
} catch (e: Exception) {
errormsg("Error parsing locale_config.xml: ${e.message}")
Timber.e("Error parsing locale_config.xml: ${e.message}")
}
}