From c0a22cdc3aa839c5ceb99bb7417eb019dd72ec3a Mon Sep 17 00:00:00 2001 From: andrekir Date: Tue, 17 May 2022 00:41:41 -0300 Subject: [PATCH] add network service discovery --- .../mesh/repository/nsd/NsdRepository.kt | 97 +++++++++++++++++++ .../repository/nsd/NsdRepositoryModule.kt | 20 ++++ .../mesh/repository/radio/TCPInterface.kt | 1 - .../geeksville/mesh/ui/SettingsFragment.kt | 24 ++++- 4 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/repository/nsd/NsdRepository.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/nsd/NsdRepositoryModule.kt diff --git a/app/src/main/java/com/geeksville/mesh/repository/nsd/NsdRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/nsd/NsdRepository.kt new file mode 100644 index 000000000..5352c66d4 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/nsd/NsdRepository.kt @@ -0,0 +1,97 @@ +package com.geeksville.mesh.repository.nsd + +import android.net.nsd.NsdManager +import android.net.nsd.NsdServiceInfo +import com.geeksville.android.Logging +import com.geeksville.mesh.CoroutineDispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.collections.ArrayList + +@Singleton +class NsdRepository @Inject constructor( + private val dispatchers: CoroutineDispatchers, + private val nsdManagerLazy: dagger.Lazy, +) : Logging { + + private val resolveQueue = Semaphore(1) + private var hostsList: ArrayList? = ArrayList() + + val resolvedList: List? get() = hostsList + + private val _networkDiscovery: Flow = callbackFlow { + val discoveryListener = object : NsdManager.DiscoveryListener { + override fun onDiscoveryStarted(regType: String) { + debug("Service discovery started: $regType") + hostsList?.clear() + } + + override fun onServiceFound(service: NsdServiceInfo) { + debug("Service discovery success: $service") + if (service.serviceType == SERVICE_TYPE && + service.serviceName.contains(serviceName) + ) { + val resolveListener = object : NsdManager.ResolveListener { + override fun onServiceResolved(service: NsdServiceInfo) { + debug("Resolve Succeeded: $service") + hostsList?.add(service) + trySend(service) + } + + override fun onResolveFailed(service: NsdServiceInfo, errorCode: Int) { + debug("Resolve failed: $service - Error code: $errorCode") + } + } + // one resolveService at a time to avoid: Error Code 3: Failure Already active + launch { + try { + resolveQueue.acquire() + nsdManagerLazy.get()?.resolveService(service, resolveListener) + } finally { + resolveQueue.release() + } + } + } else { + debug("Not our Service - Name: ${service.serviceName}, Type: ${service.serviceType}") + } + } + + override fun onServiceLost(service: NsdServiceInfo) { + debug("Service lost: $service") + } + + override fun onDiscoveryStopped(serviceType: String) { + debug("Discovery stopped: $serviceType") + } + + override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { + debug("Start Discovery failed: Error code: $errorCode") + nsdManagerLazy.get()?.stopServiceDiscovery(this) + } + + override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { + debug("Stop Discovery failed: Error code: $errorCode") + nsdManagerLazy.get()?.stopServiceDiscovery(this) + } + } + nsdManagerLazy.get() + ?.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener) + awaitClose { nsdManagerLazy.get()?.stopServiceDiscovery(discoveryListener) } + }.flowOn(dispatchers.default) + + fun networkDiscoveryFlow(): Flow { + return _networkDiscovery + } + + companion object { + //To find all the available networks SERVICE_TYPE = "_services._dns-sd._udp" + const val SERVICE_TYPE = "_http._tcp." + const val serviceName = "Meshtastic" + } +} diff --git a/app/src/main/java/com/geeksville/mesh/repository/nsd/NsdRepositoryModule.kt b/app/src/main/java/com/geeksville/mesh/repository/nsd/NsdRepositoryModule.kt new file mode 100644 index 000000000..2202b2984 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/nsd/NsdRepositoryModule.kt @@ -0,0 +1,20 @@ +package com.geeksville.mesh.repository.nsd + +import android.app.Application +import android.content.Context +import android.net.nsd.NsdManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class NsdRepositoryModule { + companion object { + @Provides + fun provideNsdManager(application: Application): NsdManager? { + return application.getSystemService(Context.NSD_SERVICE) as NsdManager? + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt index 0b7556618..d33f1c5a8 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt @@ -87,7 +87,6 @@ class TCPInterface(service: RadioInterfaceService, private val address: String) } else readChar(c.toByte()) } catch (ex: SocketTimeoutException) { - errormsg("SocketTimeoutException in TCP reader: $ex") // Ignore and start another read } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 61de246a7..c98c6ebe6 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -42,6 +42,7 @@ import com.geeksville.mesh.databinding.SettingsFragmentBinding import com.geeksville.mesh.model.BluetoothViewModel import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.repository.bluetooth.BluetoothRepository +import com.geeksville.mesh.repository.nsd.NsdRepository import com.geeksville.mesh.repository.radio.MockInterface import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.geeksville.mesh.repository.radio.SerialInterface @@ -59,7 +60,11 @@ import com.google.android.material.snackbar.Snackbar import com.hoho.android.usbserial.driver.UsbSerialDriver import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import java.util.regex.Pattern import javax.inject.Inject @@ -121,7 +126,7 @@ class BTScanModel @Inject constructor( private val application: Application, private val bluetoothRepository: BluetoothRepository, private val usbRepository: UsbRepository, - // private val nsdRepository: NsdRepository, + private val nsdRepository: NsdRepository, ) : ViewModel(), Logging { private val context: Context get() = application.applicationContext @@ -233,6 +238,9 @@ class BTScanModel @Inject constructor( @SuppressLint("MissingPermission") fun stopScan() { + // Stop Network Service Discovery (for TCP) + networkDiscovery?.cancel() + if (scanner != null) { debug("stopping scan") try { @@ -288,9 +296,9 @@ class BTScanModel @Inject constructor( addDeviceAssociations() // Include Network Service Discovery - // nsdRepository.resolvedList?.forEach { service -> - // addDevice(TCPDeviceListEntry(service)) - // } + nsdRepository.resolvedList?.forEach { service -> + addDevice(TCPDeviceListEntry(service)) + } val serialDevices by lazy { usbRepository.serialDevicesWithDrivers.value } serialDevices.forEach { (_, d) -> @@ -303,7 +311,13 @@ class BTScanModel @Inject constructor( } } - fun startScan () { + private var networkDiscovery: Job? = null + fun startScan() { + // Start Network Service Discovery (find TCP devices) + networkDiscovery = nsdRepository.networkDiscoveryFlow() + .onEach { addDevice(TCPDeviceListEntry(it)) } + .launchIn(CoroutineScope(Dispatchers.Main)) + if (hasCompanionDeviceApi) { startCompanionScan() } else startClassicScan()