From a42d19c548c1dc5db7a1688e567e7405b95c50e4 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 18 May 2021 02:17:46 +0200 Subject: [PATCH] Fixed gear update behavior when the device is reconnecting due to power loss. DeviceName changes, so we need to map to existing Gear via serial number. Previously we mapped against the deviceName which stays only the same if USB is reconnected, but the gear didn't loose power. --- .../main/java/eu/darken/fpv/dvca/gear/Gear.kt | 11 +++- .../eu/darken/fpv/dvca/gear/GearManager.kt | 50 +++++++++++++------ .../dvca/gear/goggles/djifpv/FpvGogglesV1.kt | 10 ++-- .../java/eu/darken/fpv/dvca/usb/HWDevice.kt | 26 +++++++--- .../fpv/dvca/usb/manager/HWDeviceManager.kt | 21 +++++--- .../fpv/dvca/videofeed/ui/VideoFeedVM.kt | 4 +- 6 files changed, 86 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/eu/darken/fpv/dvca/gear/Gear.kt b/app/src/main/java/eu/darken/fpv/dvca/gear/Gear.kt index 10ca5b6..4981427 100644 --- a/app/src/main/java/eu/darken/fpv/dvca/gear/Gear.kt +++ b/app/src/main/java/eu/darken/fpv/dvca/gear/Gear.kt @@ -9,11 +9,20 @@ interface Gear { val identifier: String get() = device.identifier + val serialNumber: HWDevice.SerialNumber? + get() = device.serialNumber + val label: String - get() = device.label + get() = gearName + + val gearName: String + get() = device.productName val isGearConnected: Boolean + val logId: String + get() = "$serialNumber ยง $identifier $gearName" + val events: Flow sealed interface Event { diff --git a/app/src/main/java/eu/darken/fpv/dvca/gear/GearManager.kt b/app/src/main/java/eu/darken/fpv/dvca/gear/GearManager.kt index 294a449..480b141 100644 --- a/app/src/main/java/eu/darken/fpv/dvca/gear/GearManager.kt +++ b/app/src/main/java/eu/darken/fpv/dvca/gear/GearManager.kt @@ -21,7 +21,7 @@ class GearManager @Inject constructor( ) { private val mutex = Mutex() - private val gearMap = mutableMapOf() + private val gearMap = mutableMapOf() private val factories = setOf( fpvGogglesV1Factory ) @@ -35,30 +35,48 @@ class GearManager @Inject constructor( Timber.tag(TAG).v("Devices changed! Updating gearmap.") updateGearMap(devices) } - .catch { Timber.tag(TAG).e("Failed to handle device status change.") } + .catch { Timber.tag(TAG).e(it, "Failed to handle device status change.") } .launchIn(appScope) } private suspend fun updateGearMap(devices: Set) = mutex.withLock { Timber.tag(TAG).v("Updating gear...") - gearMap.values.forEach { gear -> - val match = devices.singleOrNull { it.identifier == gear.identifier } - gear.updateDevice(match) + val gears = devices.mapNotNull { device -> + val gear = device.createGear() + if (gear == null) { + Timber.tag(TAG).d("Unknown device, couldn't create gear: %s", device.logId) + return@mapNotNull null + } + + Timber.tag(TAG).i("Known type of gear: %s", device.logId) + + if (!device.hasPermission) { + // Need to be able to read the serial + Timber.tag(TAG).d("Missing usb permission, requesting access now") + deviceManager.requestPermission(device) + } + + gear } - devices.forEach { device -> - // Was already updated - if (gearMap.containsKey(device.identifier)) return@forEach + gearMap.values.forEach { existing -> + val match = gears.singleOrNull { it.serialNumber == existing.serialNumber } + Timber.tag(TAG).v("Updating gear %s", existing.logId) + existing.updateDevice(match?.device) + } - val newGear = device.createGear() - if (newGear == null) { - Timber.tag(TAG).d("Unknown device, couldn't create gear: %s", device.label) + gears.forEach { gear -> + val snOfNewGear = gear.serialNumber + if (snOfNewGear == null) { + Timber.tag(TAG).w("Serial number was unavailable, no permission? Can't add %s", gear.logId) return@forEach - } else { - Timber.tag(TAG).i("Added new gear: %s", device.label) } - gearMap[newGear.identifier] = newGear + + // Was already updated + if (gearMap.containsKey(snOfNewGear)) return@forEach + + gearMap[snOfNewGear] = gear } Timber.tag(TAG).v("...gear update done.") @@ -70,11 +88,11 @@ class GearManager @Inject constructor( private fun HWDevice.createGear(): Gear? { val factory = this.findFactory() if (factory == null) { - Timber.tag(TAG).i("No gear factory to handle device: %s", this.label) + Timber.tag(TAG).i("No gear factory to handle device: %s", this.identifier) return null } return factory.create(this@GearManager, this).also { - Timber.tag(TAG).i("Used %s to create %s", factory, it.label) + Timber.tag(TAG).i("Used %s to create %s", factory, it) } } diff --git a/app/src/main/java/eu/darken/fpv/dvca/gear/goggles/djifpv/FpvGogglesV1.kt b/app/src/main/java/eu/darken/fpv/dvca/gear/goggles/djifpv/FpvGogglesV1.kt index 0ca5af4..1bb96b1 100644 --- a/app/src/main/java/eu/darken/fpv/dvca/gear/goggles/djifpv/FpvGogglesV1.kt +++ b/app/src/main/java/eu/darken/fpv/dvca/gear/goggles/djifpv/FpvGogglesV1.kt @@ -36,16 +36,20 @@ class FpvGogglesV1( val reconnect = device != null && currentDevice == null + currentDevice = device + if (device == null) { Timber.tag(TAG).w("Device disconnected!") - val wasActive = videoFeedInternal.value != null + val wasActive = videoFeedInternal.value != null || wasVideoActive if (wasActive) { Timber.tag(TAG).d("Video feed was active on disconnect, will restart after reconnect.") } stopVideoFeed() + wasVideoActive = wasActive + eventsInternal.value = Gear.Event.GearDetached(this) } else if (reconnect) { Timber.tag(TAG).w("Device reconnected!") @@ -54,10 +58,10 @@ class FpvGogglesV1( if (wasVideoActive) { Timber.tag(TAG).i("Video feed was previously active, starting again.") startVideoFeed() + } else { + Timber.tag(TAG).v("Video was previously not active.") } } - - currentDevice = device } private val videoFeedInternal = MutableStateFlow(null) diff --git a/app/src/main/java/eu/darken/fpv/dvca/usb/HWDevice.kt b/app/src/main/java/eu/darken/fpv/dvca/usb/HWDevice.kt index d114d56..3c8f0b0 100644 --- a/app/src/main/java/eu/darken/fpv/dvca/usb/HWDevice.kt +++ b/app/src/main/java/eu/darken/fpv/dvca/usb/HWDevice.kt @@ -13,24 +13,33 @@ class HWDevice( val rawDevice: UsbDevice, ) { private val mutex = Mutex() + private var storedSerial: String? = null + val serialNumber: SerialNumber? + get() { + val number = storedSerial ?: try { + rawDevice.serialNumber?.also { storedSerial = it } + } catch (e: SecurityException) { + null + } + + return number?.let { SerialNumber(it) } + } val identifier: String get() = rawDevice.deviceName - val label: String - get() = "${rawDevice.manufacturerName} \"${rawDevice.productName}\" (${rawDevice.deviceName})" + val productName: String + get() = rawDevice.productName ?: rawDevice.manufacturerName ?: rawDevice.deviceName val hasPermission: Boolean get() = hwManager.hasPermission(this) - private suspend fun requirePermission() { - if (!hasPermission) hwManager.requestPermission(this) - } + val logId: String + get() = "${rawDevice.deviceName} ($productName)" private val openConnections = mutableSetOf() suspend fun openConnection(): HWConnection = mutex.withLock { - requirePermission() val connection = hwManager.openDevice(this) openConnections.add(connection) return connection @@ -41,9 +50,12 @@ class HWDevice( openConnections.forEach { it.close() } } - override fun toString(): String = label + override fun toString(): String = logId companion object { private val TAG = App.logTag("Usb", "Device") } + + @JvmInline + value class SerialNumber(val serialNumber: String) } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/fpv/dvca/usb/manager/HWDeviceManager.kt b/app/src/main/java/eu/darken/fpv/dvca/usb/manager/HWDeviceManager.kt index 8ba7619..4a97f10 100644 --- a/app/src/main/java/eu/darken/fpv/dvca/usb/manager/HWDeviceManager.kt +++ b/app/src/main/java/eu/darken/fpv/dvca/usb/manager/HWDeviceManager.kt @@ -41,7 +41,7 @@ class HWDeviceManager @Inject constructor( ) { Timber.tag(TAG).v("onDeviceAttached(rawDevice=%s)", rawDevice.label) - val added = usbManager.find(rawDevice.identifier) ?: throw UnknownDeviceException(rawDevice) + val added = rawDevice.toHWDevice() this[rawDevice.identifier]?.let { Timber.tag(TAG).w("Double attach? Releasing previous device: %s", it) @@ -64,7 +64,7 @@ class HWDeviceManager @Inject constructor( Timber.tag(TAG).w("Failed to remove %s, already removed?", rawDevice.label) return@updateAsync this } else { - Timber.tag(TAG).i("Removing detached devices %s", toRemove.label) + Timber.tag(TAG).i("Removing detached devices %s", toRemove.logId) toRemove.release() } @@ -88,15 +88,22 @@ class HWDeviceManager @Inject constructor( return HWConnection( rawDevice = device.rawDevice, rawConnection = rawConnection - ).also { Timber.tag(TAG).d("Connection to %s opened: %s", device.label, it) } + ).also { Timber.tag(TAG).d("Connection to %s opened: %s", device.logId, it) } } - fun hasPermission(device: HWDevice): Boolean { - return usbManager.hasPermission(device.rawDevice) - } + fun hasPermission(device: HWDevice): Boolean = usbManager.hasPermission(device.rawDevice) suspend fun requestPermission(device: HWDevice): Boolean { - return usbPermissionHandler.requestPermission(device) + Timber.tag(TAG).v("Requesting permission for %s", device.logId) + + if (hasPermission(device)) { + Timber.tag(TAG).w("Unnecessary permission request, we already have it.") + return true + } + + return usbPermissionHandler.requestPermission(device).also { + Timber.tag(TAG).v("Permission request isGranted=$it for %s", device.logId) + } } companion object { diff --git a/app/src/main/java/eu/darken/fpv/dvca/videofeed/ui/VideoFeedVM.kt b/app/src/main/java/eu/darken/fpv/dvca/videofeed/ui/VideoFeedVM.kt index 1efc974..a9a9875 100644 --- a/app/src/main/java/eu/darken/fpv/dvca/videofeed/ui/VideoFeedVM.kt +++ b/app/src/main/java/eu/darken/fpv/dvca/videofeed/ui/VideoFeedVM.kt @@ -21,9 +21,9 @@ class VideoFeedVM @Inject constructor( } .filterNotNull() .onEach { - Timber.tag(TAG).d("Device available: %s", it.label) + Timber.tag(TAG).d("Device available: %s", it.logId) if (it.videoFeed.first() == null) { - Timber.tag(TAG).d("Enabling videofeed for %s", it.label) + Timber.tag(TAG).d("Enabling videofeed for %s", it.logId) it.startVideoFeed() } }