mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: service extraction (#4828)
This commit is contained in:
parent
0d0bdf9172
commit
807db83f53
76 changed files with 309 additions and 257 deletions
|
|
@ -50,6 +50,7 @@ kotlin {
|
|||
implementation(projects.core.service)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.ble)
|
||||
implementation(projects.core.network)
|
||||
implementation(projects.feature.settings)
|
||||
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ import org.meshtastic.core.ble.BluetoothRepository
|
|||
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.core.network.repository.UsbRepository
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.feature.connections.model.AndroidUsbDeviceData
|
||||
import org.meshtastic.feature.connections.model.DeviceListEntry
|
||||
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
|
||||
import org.meshtastic.feature.connections.repository.UsbRepository
|
||||
|
||||
@KoinViewModel
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ import org.meshtastic.core.common.database.DatabaseManager
|
|||
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
||||
import org.meshtastic.core.datastore.model.RecentAddress
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.network.repository.NetworkRepository
|
||||
import org.meshtastic.core.network.repository.NetworkRepository.Companion.toAddressString
|
||||
import org.meshtastic.core.network.repository.UsbRepository
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -38,9 +41,6 @@ import org.meshtastic.feature.connections.model.DeviceListEntry
|
|||
import org.meshtastic.feature.connections.model.DiscoveredDevices
|
||||
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
|
||||
import org.meshtastic.feature.connections.model.getMeshtasticShortName
|
||||
import org.meshtastic.feature.connections.repository.NetworkRepository
|
||||
import org.meshtastic.feature.connections.repository.NetworkRepository.Companion.toAddressString
|
||||
import org.meshtastic.feature.connections.repository.UsbRepository
|
||||
import java.util.Locale
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal fun ConnectivityManager.networkAvailable(): Flow<Boolean> =
|
||||
observeNetworks().map { activeNetworksList -> activeNetworksList.isNotEmpty() }.distinctUntilChanged()
|
||||
|
||||
internal fun ConnectivityManager.observeNetworks(
|
||||
networkRequest: NetworkRequest = NetworkRequest.Builder().build(),
|
||||
): Flow<List<Network>> = callbackFlow {
|
||||
// Keep track of the current active networks
|
||||
val activeNetworks = mutableSetOf<Network>()
|
||||
|
||||
val callback =
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
activeNetworks.add(network)
|
||||
trySend(activeNetworks.toList())
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
activeNetworks.remove(network)
|
||||
trySend(activeNetworks.toList())
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
if (activeNetworks.contains(network)) {
|
||||
trySend(activeNetworks.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerNetworkCallback(networkRequest, callback)
|
||||
|
||||
awaitClose { unregisterNetworkCallback(callback) }
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
|
||||
@Single
|
||||
class NetworkRepository(
|
||||
private val nsdManager: NsdManager,
|
||||
private val connectivityManager: ConnectivityManager,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
@Named("ProcessLifecycle") private val processLifecycle: Lifecycle,
|
||||
) {
|
||||
|
||||
val networkAvailable: Flow<Boolean> by lazy {
|
||||
connectivityManager
|
||||
.networkAvailable()
|
||||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
.shareIn(
|
||||
scope = processLifecycle.coroutineScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
replay = 1,
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
val resolvedList: Flow<List<NsdServiceInfo>> by lazy {
|
||||
nsdManager
|
||||
.serviceList(NetworkConstants.SERVICE_TYPE)
|
||||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
.shareIn(
|
||||
scope = processLifecycle.coroutineScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
replay = 1,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun NsdServiceInfo.toAddressString() = buildString {
|
||||
@Suppress("DEPRECATION")
|
||||
append(host.hostAddress)
|
||||
if (serviceType.trim('.') == NetworkConstants.SERVICE_TYPE && port != NetworkConstants.SERVICE_PORT) {
|
||||
append(":$port")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.os.Build
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal fun NsdManager.serviceList(serviceType: String): Flow<List<NsdServiceInfo>> =
|
||||
discoverServices(serviceType).mapLatest { serviceList -> serviceList.mapNotNull { resolveService(it) } }
|
||||
|
||||
private fun NsdManager.discoverServices(
|
||||
serviceType: String,
|
||||
protocolType: Int = NsdManager.PROTOCOL_DNS_SD,
|
||||
): Flow<List<NsdServiceInfo>> = callbackFlow {
|
||||
val serviceList = CopyOnWriteArrayList<NsdServiceInfo>()
|
||||
val discoveryListener =
|
||||
object : NsdManager.DiscoveryListener {
|
||||
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||
cancel("Start Discovery failed: Error code: $errorCode")
|
||||
}
|
||||
|
||||
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||
cancel("Stop Discovery failed: Error code: $errorCode")
|
||||
}
|
||||
|
||||
override fun onDiscoveryStarted(serviceType: String) {
|
||||
Logger.d { "NSD Service discovery started" }
|
||||
}
|
||||
|
||||
override fun onDiscoveryStopped(serviceType: String) {
|
||||
Logger.d { "NSD Service discovery stopped" }
|
||||
close()
|
||||
}
|
||||
|
||||
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
|
||||
Logger.d { "NSD Service found: $serviceInfo" }
|
||||
serviceList += serviceInfo
|
||||
trySend(serviceList)
|
||||
}
|
||||
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo) {
|
||||
Logger.d { "NSD Service lost: $serviceInfo" }
|
||||
serviceList.removeAll { it.serviceName == serviceInfo.serviceName }
|
||||
trySend(serviceList)
|
||||
}
|
||||
}
|
||||
trySend(emptyList()) // Emit an initial empty list
|
||||
discoverServices(serviceType, protocolType, discoveryListener)
|
||||
|
||||
awaitClose {
|
||||
try {
|
||||
stopServiceDiscovery(discoveryListener)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
// ignore if discovery is already stopped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private suspend fun NsdManager.resolveService(serviceInfo: NsdServiceInfo): NsdServiceInfo? =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val isResumed = AtomicBoolean(false)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
val callback =
|
||||
object : NsdManager.ServiceInfoCallback {
|
||||
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
|
||||
if (isResumed.compareAndSet(false, true)) {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceUpdated(updatedServiceInfo: NsdServiceInfo) {
|
||||
if (updatedServiceInfo.hostAddresses.isNotEmpty()) {
|
||||
if (isResumed.compareAndSet(false, true)) {
|
||||
continuation.resume(updatedServiceInfo)
|
||||
try {
|
||||
unregisterServiceInfoCallback(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.w(e) { "Already unregistered" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceLost() {
|
||||
if (isResumed.compareAndSet(false, true)) {
|
||||
continuation.resume(null)
|
||||
try {
|
||||
unregisterServiceInfoCallback(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.w(e) { "Already unregistered" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceInfoCallbackUnregistered() {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
registerServiceInfoCallback(serviceInfo, Dispatchers.Main.asExecutor(), callback)
|
||||
continuation.invokeOnCancellation {
|
||||
try {
|
||||
unregisterServiceInfoCallback(callback)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.w(e) { "Already unregistered" }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val listener =
|
||||
object : NsdManager.ResolveListener {
|
||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||
if (isResumed.compareAndSet(false, true)) {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||
if (isResumed.compareAndSet(false, true)) {
|
||||
continuation.resume(serviceInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
resolveService(serviceInfo, listener)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver
|
||||
import com.hoho.android.usbserial.driver.ProbeTable
|
||||
import com.hoho.android.usbserial.driver.UsbSerialProber
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
/**
|
||||
* Creates a probe table for the USB driver. This augments the default device-to-driver mappings with additional known
|
||||
* working configurations. See this package's README for more info.
|
||||
*/
|
||||
@Single
|
||||
class ProbeTableProvider {
|
||||
fun get(): ProbeTable = UsbSerialProber.getDefaultProbeTable().apply {
|
||||
// RAK 4631:
|
||||
addProduct(9114, 32809, CdcAcmSerialDriver::class.java)
|
||||
// LilyGo TBeam v1.1:
|
||||
addProduct(6790, 21972, CdcAcmSerialDriver::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
/** USB serial connection. */
|
||||
interface SerialConnection : AutoCloseable {
|
||||
/** Called to initiate the serial connection. */
|
||||
fun connect()
|
||||
|
||||
/**
|
||||
* Send data (asynchronously) to the serial device. If the connection is not presently established then the data
|
||||
* provided is ignored / dropped.
|
||||
*/
|
||||
fun sendBytes(bytes: ByteArray)
|
||||
|
||||
/**
|
||||
* Close the USB serial connection.
|
||||
*
|
||||
* @param waitForStopped if true, waits for the connection to terminate before returning
|
||||
*/
|
||||
fun close(waitForStopped: Boolean)
|
||||
|
||||
override fun close()
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
import android.hardware.usb.UsbManager
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.hoho.android.usbserial.driver.UsbSerialDriver
|
||||
import com.hoho.android.usbserial.driver.UsbSerialPort
|
||||
import com.hoho.android.usbserial.util.SerialInputOutputManager
|
||||
import org.meshtastic.core.common.util.ignoreException
|
||||
import java.nio.BufferOverflowException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
internal class SerialConnectionImpl(
|
||||
private val usbManagerLazy: Lazy<UsbManager?>,
|
||||
private val device: UsbSerialDriver,
|
||||
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)
|
||||
private val ioRef = AtomicReference<SerialInputOutputManager>()
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun sendBytes(bytes: ByteArray) {
|
||||
ioRef.get()?.let {
|
||||
Logger.d { "writing ${bytes.size} byte(s)" }
|
||||
try {
|
||||
it.writeAsync(bytes)
|
||||
} catch (e: BufferOverflowException) {
|
||||
Logger.w(e) { "Buffer overflow while writing to serial port" }
|
||||
} catch (e: Exception) {
|
||||
// USB disconnections often cause IOExceptions here; log as warning to avoid Crashlytics noise
|
||||
Logger.w(e) { "Failed to write to serial port (likely disconnected)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close(waitForStopped: Boolean) {
|
||||
if (closed.compareAndSet(false, true)) {
|
||||
ignoreException(silent = true) { ioRef.get()?.stop() }
|
||||
ignoreException(silent = true) {
|
||||
port.close() // This will cause the reader thread to exit
|
||||
}
|
||||
}
|
||||
|
||||
// Allow a short amount of time for the manager to quit (so the port can be cleanly closed)
|
||||
if (waitForStopped) {
|
||||
Logger.d { "Waiting for USB manager to stop..." }
|
||||
ignoreException(silent = true) { closedLatch.await(1, TimeUnit.SECONDS) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
close(true)
|
||||
}
|
||||
|
||||
override fun connect() {
|
||||
// We shouldn't be able to get this far without a USB subsystem so explode if that isn't true
|
||||
val usbManager = usbManagerLazy.value!!
|
||||
|
||||
val usbDeviceConnection = usbManager.openDevice(device.device)
|
||||
if (usbDeviceConnection == null) {
|
||||
listener.onMissingPermission()
|
||||
closed.set(true)
|
||||
return
|
||||
}
|
||||
|
||||
port.open(usbDeviceConnection)
|
||||
port.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
|
||||
port.dtr = true
|
||||
port.rts = true
|
||||
|
||||
Logger.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)
|
||||
// Connection is already failing, don't try to set DTR/RTS as it will just throw more
|
||||
// IOExceptions
|
||||
ignoreException(silent = true) { port.close() }
|
||||
closedLatch.countDown()
|
||||
listener.onDisconnected(e)
|
||||
}
|
||||
},
|
||||
)
|
||||
.apply {
|
||||
readTimeout = 200 // To save battery we only timeout ever so often
|
||||
ioRef.set(this)
|
||||
}
|
||||
|
||||
io.start()
|
||||
listener.onConnected()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
/** Callbacks indicating state changes in the USB serial connection. */
|
||||
interface SerialConnectionListener {
|
||||
/** Unable to initiate the connection due to missing permissions. This is a terminal state. */
|
||||
fun onMissingPermission() {}
|
||||
|
||||
/** Called when a connection has been established. */
|
||||
fun onConnected() {}
|
||||
|
||||
/** Called when serial data is received. */
|
||||
fun onDataReceived(bytes: ByteArray) {}
|
||||
|
||||
/** Called when the connection has been terminated. */
|
||||
fun onDisconnected(thrown: Exception?) {}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.exceptionReporter
|
||||
import org.meshtastic.core.common.util.getParcelableExtraCompat
|
||||
|
||||
/** A helper class to call onChanged when bluetooth is enabled or disabled or when permissions are changed. */
|
||||
@Single
|
||||
class UsbBroadcastReceiver(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)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
||||
val device: UsbDevice? = intent.getParcelableExtraCompat(UsbManager.EXTRA_DEVICE)
|
||||
val deviceName: String = device?.deviceName ?: "unknown"
|
||||
|
||||
when (intent.action) {
|
||||
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
|
||||
Logger.d { "USB device '$deviceName' was detached" }
|
||||
usbRepository.refreshState()
|
||||
}
|
||||
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
||||
Logger.d { "USB device '$deviceName' was attached" }
|
||||
usbRepository.refreshState()
|
||||
}
|
||||
UsbManager.EXTRA_PERMISSION_GRANTED -> {
|
||||
Logger.d { "USB device '$deviceName' was granted permission" }
|
||||
usbRepository.refreshState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import org.meshtastic.core.common.util.registerReceiverCompat
|
||||
|
||||
private const val ACTION_USB_PERMISSION = "com.geeksville.mesh.USB_PERMISSION"
|
||||
|
||||
internal fun UsbManager.requestPermission(context: Context, device: UsbDevice): Flow<Boolean> = callbackFlow {
|
||||
val receiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (ACTION_USB_PERMISSION == intent.action) {
|
||||
val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
|
||||
trySend(granted)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
val permissionIntent =
|
||||
PendingIntentCompat.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
Intent(ACTION_USB_PERMISSION).apply { `package` = context.packageName },
|
||||
0,
|
||||
true,
|
||||
)
|
||||
val filter = IntentFilter(ACTION_USB_PERMISSION)
|
||||
context.registerReceiverCompat(receiver, filter)
|
||||
requestPermission(device, permissionIntent)
|
||||
|
||||
awaitClose { context.unregisterReceiver(receiver) }
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
import android.app.Application
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.hoho.android.usbserial.driver.UsbSerialDriver
|
||||
import com.hoho.android.usbserial.driver.UsbSerialProber
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.registerReceiverCompat
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
|
||||
/** Repository responsible for maintaining and updating the state of USB connectivity. */
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Single
|
||||
class UsbRepository(
|
||||
private val application: Application,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
@Named("ProcessLifecycle") private val processLifecycle: Lifecycle,
|
||||
private val usbBroadcastReceiverLazy: Lazy<UsbBroadcastReceiver>,
|
||||
private val usbManagerLazy: Lazy<UsbManager?>,
|
||||
private val usbSerialProberLazy: Lazy<UsbSerialProber>,
|
||||
) {
|
||||
private val _serialDevices = MutableStateFlow(emptyMap<String, UsbDevice>())
|
||||
|
||||
val serialDevices =
|
||||
_serialDevices
|
||||
.mapLatest { serialDevices ->
|
||||
val serialProber = usbSerialProberLazy.value
|
||||
buildMap {
|
||||
serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { driver -> put(k, driver) } }
|
||||
}
|
||||
}
|
||||
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
init {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||
refreshStateInternal()
|
||||
usbBroadcastReceiverLazy.value.let { receiver ->
|
||||
application.registerReceiverCompat(receiver, receiver.intentFilter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
SerialConnectionImpl(usbManagerLazy, device, listener)
|
||||
|
||||
fun requestPermission(device: UsbDevice): Flow<Boolean> =
|
||||
usbManagerLazy.value?.requestPermission(application, device) ?: emptyFlow()
|
||||
|
||||
fun refreshState() {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) { refreshStateInternal() }
|
||||
}
|
||||
|
||||
private suspend fun refreshStateInternal() =
|
||||
withContext(dispatchers.default) { _serialDevices.emit(usbManagerLazy.value?.deviceList ?: emptyMap()) }
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* 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 org.meshtastic.feature.connections.repository
|
||||
|
||||
object NetworkConstants {
|
||||
const val SERVICE_PORT = 4403
|
||||
const val SERVICE_TYPE = "_meshtastic._tcp"
|
||||
}
|
||||
|
|
@ -50,6 +50,7 @@ import kotlinx.coroutines.launch
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.isValidAddress
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.network.repository.NetworkConstants
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add_network_device
|
||||
import org.meshtastic.core.resources.address
|
||||
|
|
@ -60,7 +61,6 @@ import org.meshtastic.core.resources.no_network_devices_found
|
|||
import org.meshtastic.core.resources.recent_network_devices
|
||||
import org.meshtastic.feature.connections.ScannerViewModel
|
||||
import org.meshtastic.feature.connections.model.DeviceListEntry
|
||||
import org.meshtastic.feature.connections.repository.NetworkConstants
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue