mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(analytics)!: modularize analytics - remove Logging (#3256)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
9aa0cf9335
commit
cad88d277b
72 changed files with 1219 additions and 1426 deletions
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
60
app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
Normal file
60
app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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") ||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 ")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue