From 129df287e01b571ca99d4e438ed37aec0b3eb1d7 Mon Sep 17 00:00:00 2001 From: Nikita Kulikov Date: Thu, 23 Jan 2025 17:01:18 +0000 Subject: [PATCH 01/10] Search delegate now can have many sources --- ...odel.kt => BLESearchConnectionDelegate.kt} | 20 ++++++++++++---- .../search/ConnectionSearchDelegate.kt | 14 +++++++++++ .../search/ConnectionSearchViewModel.kt | 19 ++++++++++++--- ...earchViewModel.kt => USBSearchDelegate.kt} | 23 +++++++++++++------ 4 files changed, 61 insertions(+), 15 deletions(-) rename components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/{BLESearchViewModel.kt => BLESearchConnectionDelegate.kt} (81%) create mode 100644 components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDelegate.kt rename components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/{USBSearchViewModel.kt => USBSearchDelegate.kt} (81%) diff --git a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchViewModel.kt b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt similarity index 81% rename from components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchViewModel.kt rename to components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt index 216c8836be..20adc0caaa 100644 --- a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchViewModel.kt +++ b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt @@ -7,29 +7,33 @@ import com.flipperdevices.bridge.connection.config.api.FDevicePersistedStorage import com.flipperdevices.bridge.connection.config.api.model.FDeviceFlipperZeroBleModel import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.preference.pb.FlipperZeroBle -import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import me.gulya.anvil.assisted.ContributesAssistedFactory import no.nordicsemi.android.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanMode import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerSettings import no.nordicsemi.android.kotlin.ble.scanner.BleScanner import no.nordicsemi.android.kotlin.ble.scanner.aggregator.BleScanResultAggregator -import javax.inject.Inject @SuppressLint("MissingPermission") -@ContributesBinding(AppGraph::class, ConnectionSearchViewModel::class) -class BLESearchViewModel @Inject constructor( +class BLESearchConnectionDelegate @AssistedInject constructor( + @Assisted viewModelScope: CoroutineScope, context: Context, persistedStorage: FDevicePersistedStorage -) : ConnectionSearchViewModel(persistedStorage) { +) : ConnectionSearchDelegate { private val aggregator = BleScanResultAggregator() private val devicesFlow = MutableStateFlow>( persistentListOf() @@ -62,6 +66,12 @@ class BLESearchViewModel @Inject constructor( } override fun getDevicesFlow() = devicesFlow.asStateFlow() + + @AssistedFactory + @ContributesMultibinding(AppGraph::class, ConnectionSearchDelegate.Factory::class) + fun interface Factory : ConnectionSearchDelegate.Factory { + override fun invoke(scope: CoroutineScope): BLESearchConnectionDelegate + } } private fun ServerDevice.toFDeviceFlipperZeroBleModel() = FDeviceFlipperZeroBleModel( diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDelegate.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDelegate.kt new file mode 100644 index 0000000000..d547eebcbc --- /dev/null +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDelegate.kt @@ -0,0 +1,14 @@ +package com.flipperdevices.bridge.connection.screens.search + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow + +interface ConnectionSearchDelegate { + fun getDevicesFlow(): StateFlow> + fun interface Factory { + operator fun invoke( + scope: CoroutineScope, + ): ConnectionSearchDelegate + } +} \ No newline at end of file diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt index 86347267ac..c50c203757 100644 --- a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt @@ -3,13 +3,26 @@ package com.flipperdevices.bridge.connection.screens.search import com.flipperdevices.bridge.connection.config.api.FDevicePersistedStorage import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject -abstract class ConnectionSearchViewModel( - private val persistedStorage: FDevicePersistedStorage +class ConnectionSearchViewModel @Inject constructor( + private val persistedStorage: FDevicePersistedStorage, + searchDelegatesFactories: MutableSet ) : DecomposeViewModel() { - abstract fun getDevicesFlow(): StateFlow> + private val searchDelegates = searchDelegatesFactories.map { it(viewModelScope) } + private val combinedFlow = combine(searchDelegates.map { it.getDevicesFlow() }) { flows -> + flows.toList().flatten().toImmutableList() + }.stateIn(viewModelScope, SharingStarted.Lazily, persistentListOf()) + + fun getDevicesFlow() = combinedFlow + fun onDeviceClick(searchItem: ConnectionSearchItem) { viewModelScope.launch { if (searchItem.isAdded) { diff --git a/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchViewModel.kt b/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt similarity index 81% rename from components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchViewModel.kt rename to components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt index 68dc2443a2..9354ac3ba4 100644 --- a/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchViewModel.kt +++ b/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt @@ -6,25 +6,27 @@ import com.flipperdevices.bridge.connection.config.api.model.FDeviceFlipperZeroU import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.info -import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.delay +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds private val FLIPPER_NAME_REGEXP = "Flipper ([A-Za-z]+)".toRegex() -@ContributesBinding(AppGraph::class, ConnectionSearchViewModel::class) -class USBSearchViewModel @Inject constructor( +class USBSearchDelegate @AssistedInject constructor( + @Assisted viewModelScope: CoroutineScope, private val persistedStorage: FDevicePersistedStorage -) : ConnectionSearchViewModel(persistedStorage), LogTagProvider { +) : ConnectionSearchDelegate, LogTagProvider { override val TAG = "USBSearchViewModel" private val searchItems = @@ -36,7 +38,7 @@ class USBSearchViewModel @Inject constructor( flow { while (true) { emit(Unit) - delay(1.seconds) + kotlinx.coroutines.delay(1.seconds) } }, persistedStorage.getAllDevices() @@ -69,6 +71,13 @@ class USBSearchViewModel @Inject constructor( } override fun getDevicesFlow() = searchItems.asStateFlow() + + + @AssistedFactory + @ContributesMultibinding(AppGraph::class, ConnectionSearchDelegate.Factory::class) + fun interface Factory : ConnectionSearchDelegate.Factory { + override fun invoke(scope: CoroutineScope): USBSearchDelegate + } } private fun SerialPort.toFDeviceFlipperZeroUSBModel(): FDeviceFlipperZeroUsbModel { From 80c720f8e28843ca812b50b79259b901d7b3def8 Mon Sep 17 00:00:00 2001 From: Nikita Kulikov Date: Thu, 23 Jan 2025 17:21:31 +0000 Subject: [PATCH 02/10] Add search usb --- .../connection/sample/shared/build.gradle.kts | 1 + .../screens/search/USBSearchDelegate.kt | 102 ++++++++++++++++++ gradle/libs.versions.toml | 4 +- 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt diff --git a/components/bridge/connection/sample/shared/build.gradle.kts b/components/bridge/connection/sample/shared/build.gradle.kts index 23aa59996c..d5f58a1a08 100644 --- a/components/bridge/connection/sample/shared/build.gradle.kts +++ b/components/bridge/connection/sample/shared/build.gradle.kts @@ -133,4 +133,5 @@ androidDependencies { api(libs.ble.kotlin.client) api(libs.compose.activity) + implementation(libs.usb.android) } diff --git a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt new file mode 100644 index 0000000000..15b6b17125 --- /dev/null +++ b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt @@ -0,0 +1,102 @@ +package com.flipperdevices.bridge.connection.screens.search + +import android.content.Context +import android.hardware.usb.UsbDevice +import android.hardware.usb.UsbManager +import com.flipperdevices.bridge.connection.config.api.FDevicePersistedStorage +import com.flipperdevices.bridge.connection.config.api.model.FDeviceFlipperZeroUsbModel +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.info +import com.hoho.android.usbserial.driver.UsbSerialProber +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +private val FLIPPER_NAME_REGEXP = "Flipper ([A-Za-z]+)".toRegex() + +class USBSearchDelegate @AssistedInject constructor( + @Assisted viewModelScope: CoroutineScope, + private val context: Context, + private val persistedStorage: FDevicePersistedStorage +) : ConnectionSearchDelegate, LogTagProvider { + override val TAG = "USBSearchViewModel" + + private val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager + + private val searchItems = + MutableStateFlow>(persistentListOf()) + + init { + viewModelScope.launch { + combine( + flow { + while (true) { + emit(Unit) + kotlinx.coroutines.delay(1.seconds) + } + }, + persistedStorage.getAllDevices() + ) { _, savedDevices -> + UsbSerialProber + .getDefaultProber() + .findAllDrivers(usbManager) + .map { it.device } to savedDevices + }.collect { (searchDevices, savedDevices) -> + val existedDescriptors = savedDevices + .filterIsInstance() + .associateBy { it.portPath } + + info { searchDevices.joinToString(",") { "${it}" } } + + searchItems.emit( + searchDevices.map { it.toFDeviceFlipperZeroUSBModel() }.map { usbDevice -> + ConnectionSearchItem( + address = usbDevice.portPath, + deviceModel = existedDescriptors[usbDevice.portPath] + ?: usbDevice, + isAdded = existedDescriptors.containsKey(usbDevice.portPath) + ) + }.distinctBy { it.address } + .toImmutableList() + + ) + } + } + } + + override fun getDevicesFlow() = searchItems.asStateFlow() + + + @AssistedFactory + @ContributesMultibinding(AppGraph::class, ConnectionSearchDelegate.Factory::class) + fun interface Factory : ConnectionSearchDelegate.Factory { + override fun invoke(scope: CoroutineScope): USBSearchDelegate + } +} + +private fun UsbDevice.toFDeviceFlipperZeroUSBModel(): FDeviceFlipperZeroUsbModel { + return FDeviceFlipperZeroUsbModel( + name = productName?.extractFlipperName() ?: deviceName, + portPath = deviceName, + humanReadableName = deviceName, + ) +} + +private fun String.extractFlipperName(): String { + val regexFind = FLIPPER_NAME_REGEXP.find(this) + val groups = regexFind?.groupValues + return groups?.getOrNull(index = 1) + ?: this +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f59a402bc..e24456b010 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,8 +100,9 @@ buildkonfig = "5.5.1" wire = "5.1.0" okio = "3.9.1" -jserial = "2.11.0" +jserial = "2.11.0" +usb-android = "3.8.1" [libraries] # Gradle - Core @@ -248,6 +249,7 @@ ble-kotlin-client = { module = "no.nordicsemi.android.kotlin.ble:client", versio # USB jserial = { module = "com.fazecast:jSerialComm", version.ref = "jserial" } +usb-android = { module = "com.github.mik3y:usb-serial-for-android", version.ref = "usb-android" } # Images image-lottie = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie" } From 71889d4b59998e31e0b371ad2869d5bc0ab42912 Mon Sep 17 00:00:00 2001 From: Nikita Kulikov Date: Thu, 23 Jan 2025 22:18:39 +0000 Subject: [PATCH 03/10] Base implementation of USB serial --- .../sample/android/build.gradle.kts | 1 + .../connection/ConnectionTestApplication.kt | 3 + .../connection/sample/shared/build.gradle.kts | 5 + .../screens/search/USBSearchDelegate.kt | 4 +- .../drawable/material_ic_bluetooth.xml | 9 ++ .../drawable/material_ic_usb.xml | 9 ++ .../ConnectionSearchDecomposeComponent.kt | 17 ++ .../screens/search/ConnectionSearchItem.kt | 2 +- .../transport/usb/impl/build.gradle.kts | 9 +- .../usb/impl/model/USBAndroidDevice.kt | 90 +++++++++++ .../usb/impl/model/USBAndroidDeviceFactory.kt | 36 +++++ .../usb/impl/model/USBSerialListener.kt | 74 +++++++++ .../usb/impl/model/USBDesktopDevice.kt | 34 ++++ .../usb/impl/model/USBDesktopDeviceFactory.kt | 18 +++ .../usb/impl/USBDeviceConnectionApiImpl.kt | 20 ++- .../usb/impl/di/BleDeviceConnectionModule.kt | 10 +- .../usb/impl/model/USBPlatformDevice.kt | 26 +++ .../impl/model/USBPlatformDeviceFactory.kt | 11 ++ .../usb/impl/serial/FUSBSerialDeviceApi.kt | 6 +- components/bridge/pbutils/src/main/proto | 1 - .../pbutils/src/main/proto/.github/CODEOWNERS | 3 + .../main/proto/.github/workflows/generate.yml | 30 ++++ .../bridge/pbutils/src/main/proto/.gitignore | 8 + .../bridge/pbutils/src/main/proto/Changelog | 105 ++++++++++++ .../src/main/proto/application.options | 8 + .../pbutils/src/main/proto/application.proto | 58 +++++++ .../pbutils/src/main/proto/desktop.proto | 20 +++ .../pbutils/src/main/proto/flipper.options | 6 + .../pbutils/src/main/proto/flipper.proto | 151 ++++++++++++++++++ .../pbutils/src/main/proto/gpio.options | 0 .../bridge/pbutils/src/main/proto/gpio.proto | 73 +++++++++ .../bridge/pbutils/src/main/proto/gui.options | 2 + .../bridge/pbutils/src/main/proto/gui.proto | 52 ++++++ .../pbutils/src/main/proto/property.options | 3 + .../pbutils/src/main/proto/property.proto | 13 ++ .../pbutils/src/main/proto/storage.options | 26 +++ .../pbutils/src/main/proto/storage.proto | 99 ++++++++++++ .../pbutils/src/main/proto/system.options | 16 ++ .../pbutils/src/main/proto/system.proto | 95 +++++++++++ 39 files changed, 1133 insertions(+), 20 deletions(-) create mode 100644 components/bridge/connection/sample/shared/src/commonMain/composeResources/drawable/material_ic_bluetooth.xml create mode 100644 components/bridge/connection/sample/shared/src/commonMain/composeResources/drawable/material_ic_usb.xml create mode 100644 components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt delete mode 160000 components/bridge/pbutils/src/main/proto create mode 100644 components/bridge/pbutils/src/main/proto/.github/CODEOWNERS create mode 100644 components/bridge/pbutils/src/main/proto/.github/workflows/generate.yml create mode 100644 components/bridge/pbutils/src/main/proto/.gitignore create mode 100644 components/bridge/pbutils/src/main/proto/Changelog create mode 100644 components/bridge/pbutils/src/main/proto/application.options create mode 100644 components/bridge/pbutils/src/main/proto/application.proto create mode 100644 components/bridge/pbutils/src/main/proto/desktop.proto create mode 100644 components/bridge/pbutils/src/main/proto/flipper.options create mode 100644 components/bridge/pbutils/src/main/proto/flipper.proto create mode 100644 components/bridge/pbutils/src/main/proto/gpio.options create mode 100644 components/bridge/pbutils/src/main/proto/gpio.proto create mode 100644 components/bridge/pbutils/src/main/proto/gui.options create mode 100644 components/bridge/pbutils/src/main/proto/gui.proto create mode 100644 components/bridge/pbutils/src/main/proto/property.options create mode 100644 components/bridge/pbutils/src/main/proto/property.proto create mode 100644 components/bridge/pbutils/src/main/proto/storage.options create mode 100644 components/bridge/pbutils/src/main/proto/storage.proto create mode 100644 components/bridge/pbutils/src/main/proto/system.options create mode 100644 components/bridge/pbutils/src/main/proto/system.proto diff --git a/components/bridge/connection/sample/android/build.gradle.kts b/components/bridge/connection/sample/android/build.gradle.kts index e76cdf6766..8847caa970 100644 --- a/components/bridge/connection/sample/android/build.gradle.kts +++ b/components/bridge/connection/sample/android/build.gradle.kts @@ -20,6 +20,7 @@ commonDependencies { androidDependencies { implementation(projects.components.core.di) + implementation(projects.components.core.activityholder) } dependencies { diff --git a/components/bridge/connection/sample/android/src/androidMain/kotlin/com/flipperdevices/bridge/connection/ConnectionTestApplication.kt b/components/bridge/connection/sample/android/src/androidMain/kotlin/com/flipperdevices/bridge/connection/ConnectionTestApplication.kt index b639086e88..cde6615fd2 100644 --- a/components/bridge/connection/sample/android/src/androidMain/kotlin/com/flipperdevices/bridge/connection/ConnectionTestApplication.kt +++ b/components/bridge/connection/sample/android/src/androidMain/kotlin/com/flipperdevices/bridge/connection/ConnectionTestApplication.kt @@ -3,6 +3,7 @@ package com.flipperdevices.bridge.connection import android.app.Application import com.flipperdevices.bridge.connection.di.AppComponent import com.flipperdevices.bridge.connection.di.DaggerMergedAndroidAppComponent +import com.flipperdevices.core.activityholder.CurrentActivityHolder import com.flipperdevices.core.di.ApplicationParams import com.flipperdevices.core.di.ComponentHolder import timber.log.Timber @@ -27,5 +28,7 @@ class ConnectionTestApplication : Application() { ComponentHolder.components += appComponent Timber.plant(Timber.DebugTree()) + + CurrentActivityHolder.register(this) } } diff --git a/components/bridge/connection/sample/shared/build.gradle.kts b/components/bridge/connection/sample/shared/build.gradle.kts index d5f58a1a08..63db20a19e 100644 --- a/components/bridge/connection/sample/shared/build.gradle.kts +++ b/components/bridge/connection/sample/shared/build.gradle.kts @@ -72,6 +72,8 @@ commonDependencies { api(projects.components.bridge.connection.feature.screenstreaming.impl) api(projects.components.bridge.connection.feature.update.api) api(projects.components.bridge.connection.feature.update.impl) + api(projects.components.bridge.connection.feature.emulate.api) + api(projects.components.bridge.connection.feature.emulate.impl) api(projects.components.filemngr.main.api) api(projects.components.filemngr.main.impl) @@ -117,6 +119,9 @@ androidDependencies { api(projects.components.bridge.connection.transport.ble.api) api(projects.components.bridge.connection.transport.ble.impl) + api(projects.components.bridge.connection.transport.usb.api) + api(projects.components.bridge.connection.transport.usb.impl) + api(projects.components.firstpair.connection.api) api(projects.components.keyparser.api) diff --git a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt index 15b6b17125..436168a9c6 100644 --- a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt +++ b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt @@ -89,8 +89,8 @@ class USBSearchDelegate @AssistedInject constructor( private fun UsbDevice.toFDeviceFlipperZeroUSBModel(): FDeviceFlipperZeroUsbModel { return FDeviceFlipperZeroUsbModel( name = productName?.extractFlipperName() ?: deviceName, - portPath = deviceName, - humanReadableName = deviceName, + portPath = deviceId.toString(), + humanReadableName = productName ?: deviceName, ) } diff --git a/components/bridge/connection/sample/shared/src/commonMain/composeResources/drawable/material_ic_bluetooth.xml b/components/bridge/connection/sample/shared/src/commonMain/composeResources/drawable/material_ic_bluetooth.xml new file mode 100644 index 0000000000..65a1155542 --- /dev/null +++ b/components/bridge/connection/sample/shared/src/commonMain/composeResources/drawable/material_ic_bluetooth.xml @@ -0,0 +1,9 @@ + + + diff --git a/components/bridge/connection/sample/shared/src/commonMain/composeResources/drawable/material_ic_usb.xml b/components/bridge/connection/sample/shared/src/commonMain/composeResources/drawable/material_ic_usb.xml new file mode 100644 index 0000000000..5ac270e279 --- /dev/null +++ b/components/bridge/connection/sample/shared/src/commonMain/composeResources/drawable/material_ic_usb.xml @@ -0,0 +1,9 @@ + + + diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDecomposeComponent.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDecomposeComponent.kt index 9667f41277..330a146067 100644 --- a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDecomposeComponent.kt +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDecomposeComponent.kt @@ -32,6 +32,9 @@ import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import javax.inject.Provider import flipperapp.components.core.ui.res.generated.resources.Res as SharedRes +import com.flipperdevices.bridge.connection.config.api.FDeviceType +import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_bluetooth +import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_usb class ConnectionSearchDecomposeComponent @AssistedInject constructor( @Assisted componentContext: ComponentContext, @@ -69,6 +72,20 @@ class ConnectionSearchDecomposeComponent @AssistedInject constructor( key = { device -> device.address } ) { searchItem -> Row { + Icon( + modifier = Modifier + .padding(16.dp) + .size(24.dp), + painter = painterResource( + when(searchItem.deviceModel.type) { + FDeviceType.FLIPPER_ZERO_BLE -> Res.drawable.material_ic_bluetooth + FDeviceType.FLIPPER_ZERO_USB -> Res.drawable.material_ic_usb + } + ), + contentDescription = null, + tint = LocalPallet.current.text100 + ) + Text( modifier = Modifier .weight(1f) diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItem.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItem.kt index 73ca96e4e2..b8bacce30c 100644 --- a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItem.kt +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItem.kt @@ -6,4 +6,4 @@ data class ConnectionSearchItem( val address: String, val deviceModel: FDeviceBaseModel, val isAdded: Boolean -) +) \ No newline at end of file diff --git a/components/bridge/connection/transport/usb/impl/build.gradle.kts b/components/bridge/connection/transport/usb/impl/build.gradle.kts index 0968b876b9..1cede1d956 100644 --- a/components/bridge/connection/transport/usb/impl/build.gradle.kts +++ b/components/bridge/connection/transport/usb/impl/build.gradle.kts @@ -21,6 +21,13 @@ commonDependencies { implementation(libs.kotlin.coroutines) } -jvmSharedDependencies { +desktopDependencies { implementation(libs.jserial) } + +androidDependencies { + implementation(projects.components.core.activityholder) + + implementation(libs.fastutil) + implementation(libs.usb.android) +} diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt new file mode 100644 index 0000000000..bbb1ab27a1 --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt @@ -0,0 +1,90 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +import android.app.PendingIntent +import android.content.Intent +import android.hardware.usb.UsbManager +import android.util.Log +import com.flipperdevices.core.activityholder.CurrentActivityHolder +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.hoho.android.usbserial.driver.UsbSerialDriver +import com.hoho.android.usbserial.util.SerialInputOutputManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +private const val RW_TIMEOUT = 0 + +class USBAndroidDevice( + private val serialDriver: UsbSerialDriver, + private val usbManager: UsbManager, + private val scope: CoroutineScope +) : USBPlatformDevice, LogTagProvider { + override val TAG = "USBAndroidDevice" + + private val serialPort = serialDriver.ports.first() + private val serialListener = USBSerialListener(scope) + + override fun connect(baudRate: Int, dataBits: Int, stopBits: Int, parity: Int): Boolean { + val connection = runCatching { usbManager.openDevice(serialPort.device) } + .onFailure { error(it) { "Fail open connection" } } + .getOrNull() + if (connection == null) { + requestPermission() + error("Connection is null, request permission") + } + + serialPort.open(connection) + serialPort.setParameters(baudRate, dataBits, stopBits, parity) + serialPort.dtr = true + serialPort.rts = true + + val ioManager = SerialInputOutputManager(serialPort, serialListener) + scope.launch { + try { + awaitCancellation() + } finally { + withContext(NonCancellable) { + ioManager.stop() + } + } + } + ioManager.start() + return true + } + + private fun requestPermission() { + if (!usbManager.hasPermission(serialDriver.device)) { + + val intent = Intent("Test") + + val activity = CurrentActivityHolder.getCurrentActivity() + ?: error("Failed get current activity") + intent.setPackage(activity.packageName) + val usbPermissionIntent = + PendingIntent.getBroadcast(activity, 0, intent, PendingIntent.FLAG_MUTABLE) + usbManager.requestPermission(serialDriver.device, usbPermissionIntent) + return + } + } + + override fun closePort() { + serialPort.close() + } + + override fun writeBytes(buffer: ByteArray, bytesToWrite: Int, offset: Int): Int { + val subBuffer = if (offset == 0) { + buffer + } else { + buffer.copyOfRange(offset, buffer.size) + } + serialPort.write(subBuffer, bytesToWrite, RW_TIMEOUT) + return bytesToWrite + } + + override fun readBytes(buffer: ByteArray, bytesToRead: Int): Int { + return serialListener.readBytes(buffer, bytesToRead) + } +} diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt new file mode 100644 index 0000000000..ed20fb3183 --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt @@ -0,0 +1,36 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +import android.content.Context +import android.hardware.usb.UsbManager +import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig +import com.flipperdevices.core.di.AppGraph +import com.hoho.android.usbserial.driver.UsbSerialProber +import com.squareup.anvil.annotations.ContributesBinding +import kotlinx.coroutines.CoroutineScope +import javax.inject.Inject + + +@ContributesBinding(AppGraph::class, USBPlatformDeviceFactory::class) +class USBAndroidDeviceFactory @Inject constructor( + private val context: Context +) : USBPlatformDeviceFactory { + override fun getUSBPlatformDevice( + config: FUSBDeviceConnectionConfig, + scope: CoroutineScope + ): USBPlatformDevice { + val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager + val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager) + val device = availableDrivers.filter { + it.device.deviceId.toString() == config.path + }.firstOrNull() + if (device == null) { + error("Failed find device with id ${config.path}") + } + + return USBAndroidDevice( + serialDriver = device, + usbManager = manager, + scope = scope + ) + } +} \ No newline at end of file diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt new file mode 100644 index 0000000000..e629b88733 --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt @@ -0,0 +1,74 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.log.info +import com.hoho.android.usbserial.util.SerialInputOutputManager +import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class USBSerialListener( + scope: CoroutineScope +) : SerialInputOutputManager.Listener, LogTagProvider { + override val TAG = "USBSerialListener" + + private val queue = ByteArrayFIFOQueue() + + init { + scope.launch { + try { + awaitCancellation() + } finally { + withContext(NonCancellable) { + synchronized(queue) { + queue.notifyAll() + } + } + } + } + } + + fun readBytes( + buffer: ByteArray, + bytesToRead: Int + ): Int { + var index = 0 + synchronized(queue) { + while (queue.isEmpty) { + queue.wait() + } + while (index < bytesToRead && !queue.isEmpty) { + buffer[index++] = queue.dequeueByte() + } + } + return index + } + + override fun onNewData(data: ByteArray?) { + info { "Receive $data" } + synchronized(queue) { + data?.forEach { + queue.enqueue(it) + } + queue.notifyAll() + } + } + + override fun onRunError(e: Exception?) { + error(e) { "Failed in usb serial" } + } +} + +@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") +private fun Any.wait() { + (this as Object).wait() +} + +@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") +private fun Any.notifyAll() { + (this as Object).notifyAll() +} \ No newline at end of file diff --git a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt new file mode 100644 index 0000000000..f1c8d351fc --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt @@ -0,0 +1,34 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +import com.fazecast.jSerialComm.SerialPort +import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig + +private const val OPEN_PORT_TIME_MS = 1000 + +class USBDesktopDevice( + private val serialPort: SerialPort +) : USBPlatformDevice { + override fun connect(baudRate: Int, dataBits: Int, stopBits: Int, parity: Int): Boolean { + serialPort.setComPortParameters( + baudRate, + dataBits, + SerialPort.ONE_STOP_BIT, + SerialPort.NO_PARITY + ) + serialPort.openPort(OPEN_PORT_TIME_MS) + serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 0, 0) + return serialPort.openPort() + } + + override fun closePort() { + serialPort.closePort() + } + + override fun writeBytes(buffer: ByteArray, bytesToWrite: Int, offset: Int): Int { + return serialPort.writeBytes(buffer, bytesToWrite, offset) + } + + override fun readBytes(buffer: ByteArray, bytesToRead: Int): Int { + return serialPort.readBytes(buffer, bytesToRead) + } +} diff --git a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt new file mode 100644 index 0000000000..662eba66cf --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt @@ -0,0 +1,18 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +import com.fazecast.jSerialComm.SerialPort +import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig +import com.flipperdevices.core.di.AppGraph +import com.squareup.anvil.annotations.ContributesBinding +import kotlinx.coroutines.CoroutineScope +import javax.inject.Inject + +@ContributesBinding(AppGraph::class, USBPlatformDeviceFactory::class) +class USBDesktopDeviceFactory @Inject constructor() : USBPlatformDeviceFactory { + override fun getUSBPlatformDevice( + config: FUSBDeviceConnectionConfig, + scope: CoroutineScope + ): USBPlatformDevice { + return USBDesktopDevice(SerialPort.getCommPort(config.path)) + } +} \ No newline at end of file diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/USBDeviceConnectionApiImpl.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/USBDeviceConnectionApiImpl.kt index e3c8fc4228..86184b34b3 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/USBDeviceConnectionApiImpl.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/USBDeviceConnectionApiImpl.kt @@ -1,12 +1,13 @@ package com.flipperdevices.bridge.connection.transport.usb.impl -import com.fazecast.jSerialComm.SerialPort import com.flipperdevices.bridge.connection.feature.actionnotifier.api.FlipperActionNotifier import com.flipperdevices.bridge.connection.transport.common.api.FInternalTransportConnectionStatus import com.flipperdevices.bridge.connection.transport.common.api.FTransportConnectionStatusListener import com.flipperdevices.bridge.connection.transport.usb.api.FUSBApi import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig import com.flipperdevices.bridge.connection.transport.usb.api.USBDeviceConnectionApi +import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDevice +import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDeviceFactory import com.flipperdevices.bridge.connection.transport.usb.impl.serial.FUSBSerialDeviceApi import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.info @@ -20,10 +21,10 @@ private val FLOOD_END_STRING = "\r\n\r\n>: ".toByteArray() private val COMMAND = "start_rpc_session\r".toByteArray() private const val BAUD_RATE = 230400 private const val DATA_BITS = 8 -private const val OPEN_PORT_TIME_MS = 1000 class USBDeviceConnectionApiImpl( - private val actionNotifierFactory: FlipperActionNotifier.Factory + private val actionNotifierFactory: FlipperActionNotifier.Factory, + private val usbPlatformDeviceFactory: USBPlatformDeviceFactory ) : USBDeviceConnectionApi, LogTagProvider { override val TAG = "USBDeviceConnectionApi" @@ -33,16 +34,13 @@ class USBDeviceConnectionApiImpl( listener: FTransportConnectionStatusListener ): Result = runCatching { listener.onStatusUpdate(FInternalTransportConnectionStatus.Connecting) - val serialPort = SerialPort.getCommPort(config.path) - serialPort.setComPortParameters( + val serialPort = usbPlatformDeviceFactory.getUSBPlatformDevice(config, scope) + val portOpened = serialPort.connect( BAUD_RATE, DATA_BITS, - SerialPort.ONE_STOP_BIT, - SerialPort.NO_PARITY + USBPlatformDevice.ONE_STOP_BIT, + USBPlatformDevice.NO_PARITY ) - serialPort.openPort(OPEN_PORT_TIME_MS) - serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 0, 0) - val portOpened = serialPort.openPort() info { "Read port is: $portOpened" } @@ -80,7 +78,7 @@ class USBDeviceConnectionApiImpl( } @OptIn(ExperimentalStdlibApi::class) - private fun skipFlood(serialPort: SerialPort, floodBytes: ByteArray) { + private fun skipFlood(serialPort: USBPlatformDevice, floodBytes: ByteArray) { info { "Start wait flood" } var floodCurrentIndex = 0 val buffer = ByteArray(size = 1) diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt index 049e6e18c9..4cbf56cc13 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt @@ -5,6 +5,8 @@ import com.flipperdevices.bridge.connection.transport.common.api.di.DeviceConnec import com.flipperdevices.bridge.connection.transport.common.api.di.toHolder import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig import com.flipperdevices.bridge.connection.transport.usb.impl.USBDeviceConnectionApiImpl +import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDevice +import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDeviceFactory import com.flipperdevices.core.di.AppGraph import com.squareup.anvil.annotations.ContributesTo import dagger.Module @@ -20,6 +22,10 @@ class BleDeviceConnectionModule { @IntoMap @ClassKey(FUSBDeviceConnectionConfig::class) fun provideBleDeviceConnectionApi( - actionNotifierFactory: FlipperActionNotifier.Factory - ): DeviceConnectionApiHolder = USBDeviceConnectionApiImpl(actionNotifierFactory).toHolder() + actionNotifierFactory: FlipperActionNotifier.Factory, + platformDeviceFactory: USBPlatformDeviceFactory + ): DeviceConnectionApiHolder = USBDeviceConnectionApiImpl( + actionNotifierFactory, + platformDeviceFactory + ).toHolder() } diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt new file mode 100644 index 0000000000..e9b8db8cee --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt @@ -0,0 +1,26 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig + +interface USBPlatformDevice { + fun connect(baudRate: Int, dataBits: Int, stopBits: Int, parity: Int): Boolean + fun closePort() + + fun writeBytes(buffer: ByteArray, bytesToWrite: Int = buffer.size, offset: Int = 0): Int + + fun readBytes(buffer: ByteArray, bytesToRead: Int = buffer.size): Int + + companion object { + // Parity Values + const val NO_PARITY: Int = 0 + const val ODD_PARITY: Int = 1 + const val EVEN_PARITY: Int = 2 + const val MARK_PARITY: Int = 3 + const val SPACE_PARITY: Int = 4 + + // Number of Stop Bits + const val ONE_STOP_BIT: Int = 1 + const val ONE_POINT_FIVE_STOP_BITS: Int = 2 + const val TWO_STOP_BITS: Int = 3 + } +} \ No newline at end of file diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt new file mode 100644 index 0000000000..75037b37aa --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt @@ -0,0 +1,11 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig +import kotlinx.coroutines.CoroutineScope + +interface USBPlatformDeviceFactory { + fun getUSBPlatformDevice( + config: FUSBDeviceConnectionConfig, + scope: CoroutineScope + ): USBPlatformDevice +} \ No newline at end of file diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/serial/FUSBSerialDeviceApi.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/serial/FUSBSerialDeviceApi.kt index e97910e368..fe93fbaf16 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/serial/FUSBSerialDeviceApi.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/serial/FUSBSerialDeviceApi.kt @@ -1,12 +1,12 @@ package com.flipperdevices.bridge.connection.transport.usb.impl.serial -import com.fazecast.jSerialComm.SerialPort import com.flipperdevices.bridge.connection.feature.actionnotifier.api.FlipperActionNotifier import com.flipperdevices.bridge.connection.transport.common.api.meta.FTransportMetaInfoApi import com.flipperdevices.bridge.connection.transport.common.api.serial.FSerialDeviceApi import com.flipperdevices.bridge.connection.transport.common.api.serial.FSerialRestartApi import com.flipperdevices.bridge.connection.transport.common.api.serial.FlipperSerialSpeed import com.flipperdevices.bridge.connection.transport.usb.api.FUSBApi +import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDevice import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.info import kotlinx.coroutines.CoroutineScope @@ -19,7 +19,7 @@ import kotlinx.coroutines.launch class FUSBSerialDeviceApi( private val scope: CoroutineScope, - private val serialPort: SerialPort, + private val serialPort: USBPlatformDevice, private val actionNotifier: FlipperActionNotifier ) : FUSBApi, FSerialDeviceApi, @@ -40,6 +40,7 @@ class FUSBSerialDeviceApi( while (result > 0) { result = serialPort.readBytes(buffer, buffer.size) val readBytes = buffer.take(result).toByteArray() + rxSpeed.onReceiveBytes(result) receiverByteFlow.emit(readBytes) } error("End loop with result $result") @@ -65,6 +66,7 @@ class FUSBSerialDeviceApi( val writtenBytes = serialPort.writeBytes(data, data.size - writtenBytesOffset, writtenBytesOffset) info { "Write $writtenBytes" } + txSpeed.onReceiveBytes(writtenBytes) if (writtenBytes == -1) { error("Failed to write bytes") } diff --git a/components/bridge/pbutils/src/main/proto b/components/bridge/pbutils/src/main/proto deleted file mode 160000 index ee5b6a22fd..0000000000 --- a/components/bridge/pbutils/src/main/proto +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ee5b6a22fd6aaf9075a2b7bd373309592e6627c5 diff --git a/components/bridge/pbutils/src/main/proto/.github/CODEOWNERS b/components/bridge/pbutils/src/main/proto/.github/CODEOWNERS new file mode 100644 index 0000000000..dfe56a07f4 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Who owns all the code by default + +* @DrZlo13 @skotopes @gornekich @nminaylov @hedger diff --git a/components/bridge/pbutils/src/main/proto/.github/workflows/generate.yml b/components/bridge/pbutils/src/main/proto/.github/workflows/generate.yml new file mode 100644 index 0000000000..16d34a769c --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/.github/workflows/generate.yml @@ -0,0 +1,30 @@ +name: 'Generate protobuf sources' + +on: + push: + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - name: Checkout flipperzero-protobuf + uses: actions/checkout@v2 + + - name: Checkout nanopb + uses: actions/checkout@v2 + with: + repository: nanopb/nanopb + path: nanopb + + - name: Setup python + uses: actions/setup-python@v3 + with: + python-version: '3.9' + + - name: Setup protobuf + run: | + sudo apt -y install protobuf-compiler + python3 -m pip install --upgrade python3-protobuf==2.5.0 protobuf==3.20.1 grpcio-tools==1.47.0 grpcio==1.47.0 + + - name: Generate sources + run: python3 nanopb/generator/nanopb_generator.py -q -I . -D /tmp *.proto diff --git a/components/bridge/pbutils/src/main/proto/.gitignore b/components/bridge/pbutils/src/main/proto/.gitignore new file mode 100644 index 0000000000..34165a8706 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/.gitignore @@ -0,0 +1,8 @@ +# JetBrains IDEs +.idea + +# MacOS +.DS_Store + +# Visual Studio Code +.vscode/ \ No newline at end of file diff --git a/components/bridge/pbutils/src/main/proto/Changelog b/components/bridge/pbutils/src/main/proto/Changelog new file mode 100644 index 0000000000..be5fa9c29f --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/Changelog @@ -0,0 +1,105 @@ +# Changelog + +## [0.21] +### Added +- Application AppButtonPressRequest now supports button indices in addition to names. + +## [0.20] +### Added +- Storage ListRequest message now has a file size filter. + +## [0.19] +### Added +- Storage ListRequest message now supports md5sum calculation for each file + +## [0.18] +### Added +- Loader is now able to load apps from SD card + +## [0.17] +### Added +- Desktop service subscription API + +## [0.16] +### Added +- Desktop service API + +## [0.15] +### Modified +- ScreenFrame: additional orientation field + +## [0.14] +### Added +- New subsystem: Property +- Property messages: GetRequest, GetResponse +- App messages: GetErrorRequest, GetErrorResponse, DataExchangeRequest + +## [0.13] +### Added +- Storage: timestamp + +## [0.12] +### Added +- Region message + +## [0.11] +### Added +- App messages: AppStateResponse + +## [0.10] +### Added +- GPIO message: SetPinMode, SetInputPull, GetPinMode, GetPinModeResponse, ReadPin, ReadPinResponse, WritePin + +## [0.9] +### Added +- System message: UpdateResponse, enum UpdateResultCode: new entries + OutdatedManifestVersion, IntFull, UnspecifiedError + +## [0.8] +### Added +- App messages: AppExitRequest, AppLoadFileRequest, AppButtonPressRequest, AppButtonReleaseRequest + +## [0.7] +### Added +- System message: UpdateResponse: passing update preparation detailed status + +## [0.6] +### Added +- System message: RebootRequest: added UPDATE restart mode +### Fixed +- System message: UpdateRequest: renamed manifest field + +## [0.5] +### Added +- System message: PowerInfo + +## [0.4] +### Added +- System message: UpdateRequest +- Storage messages: BackupCreateRequest, BackupRestoreRequest + +## [0.3] +### Fixed +- BLE disconnection after writing / erasing FLASH + +## [0.2] +### Added +- System messages: ProtobufVersion +- Support parallel RPC sessions +- Screen streaming optimization + +## [0.1] +### Added +- Code owners +- CI setup +- System messages: Ping, Reboot, DeviceInfo, FactoryReset, DateTime, + AudiovisualAlert +- Storage messages: Info, Stat, Read, Write, Delete, Mkdir, Md5sum, + Rename +- App messages: Start, LockStatus +- Gui messages: ScreenStream, ScreenFrame, SendInput, VirtualDisplay + +## [0.0] +### Added +- Introduced Flipper Zero Protobuf repository +- StorageList messages diff --git a/components/bridge/pbutils/src/main/proto/application.options b/components/bridge/pbutils/src/main/proto/application.options new file mode 100644 index 0000000000..338bb67d61 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/application.options @@ -0,0 +1,8 @@ +PB_App.StartRequest.args type:FT_POINTER +PB_App.StartRequest.args max_length:512 +PB_App.StartRequest.name type:FT_POINTER +PB_App.StartRequest.name max_length:512 +PB_App.AppLoadFileRequest.path max_length:512 +PB_App.AppButtonPressRequest.args max_length:512 +PB_App.GetErrorResponse.text type:FT_POINTER +PB_App.DataExchangeRequest.data type:FT_POINTER diff --git a/components/bridge/pbutils/src/main/proto/application.proto b/components/bridge/pbutils/src/main/proto/application.proto new file mode 100644 index 0000000000..4c82cdf91f --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/application.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package PB_App; +option java_package = "com.flipperdevices.protobuf.app"; + +message StartRequest { + string name = 1; + string args = 2; +} + +message LockStatusRequest { +} + +message LockStatusResponse { + bool locked = 1; +} + +message AppExitRequest { +} + +message AppLoadFileRequest { + string path = 1; +} + +message AppButtonPressRequest { + string args = 1; + int32 index = 2; +} + +message AppButtonReleaseRequest { +} + +message AppButtonPressReleaseRequest { + string args = 1; + int32 index = 2; +} + +enum AppState { + APP_CLOSED = 0; + APP_STARTED = 1; +}; + +message AppStateResponse { + AppState state = 1; +} + +message GetErrorRequest { +} + +message GetErrorResponse { + uint32 code = 1; + string text = 2; +} + +message DataExchangeRequest { + bytes data = 1; +} + diff --git a/components/bridge/pbutils/src/main/proto/desktop.proto b/components/bridge/pbutils/src/main/proto/desktop.proto new file mode 100644 index 0000000000..cc8212d40c --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/desktop.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package PB_Desktop; +option java_package = "com.flipperdevices.protobuf.desktop"; + +message IsLockedRequest { +} + +message UnlockRequest { +} + +message StatusSubscribeRequest { +} + +message StatusUnsubscribeRequest { +} + +message Status { + bool locked = 1; +} diff --git a/components/bridge/pbutils/src/main/proto/flipper.options b/components/bridge/pbutils/src/main/proto/flipper.options new file mode 100644 index 0000000000..0301133444 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/flipper.options @@ -0,0 +1,6 @@ +PB.Main submsg_callback:true +PB.Region.country_code type:FT_POINTER +PB.Region.country_code max_size:2 +PB.Region.Band.power_limit int_size:IS_8 +PB.Region.Band.duty_cycle int_size:IS_8 + diff --git a/components/bridge/pbutils/src/main/proto/flipper.proto b/components/bridge/pbutils/src/main/proto/flipper.proto new file mode 100644 index 0000000000..2ba75dadce --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/flipper.proto @@ -0,0 +1,151 @@ +syntax = "proto3"; +import "storage.proto"; +import "system.proto"; +import "application.proto"; +import "gui.proto"; +import "gpio.proto"; +import "property.proto"; +import "desktop.proto"; + +package PB; +option java_package = "com.flipperdevices.protobuf"; + +enum CommandStatus { + OK = 0; + + /**< Common Errors */ + ERROR = 1; /**< Unknown error */ + ERROR_DECODE = 2; /**< Command can't be decoded successfully - command_id in response may be wrong! */ + ERROR_NOT_IMPLEMENTED = 3; /**< Command successfully decoded, but not implemented (deprecated or not yet implemented) */ + ERROR_BUSY = 4; /**< Somebody took global lock, so not all commands are available */ + ERROR_CONTINUOUS_COMMAND_INTERRUPTED = 14; /**< Not received has_next == 0 */ + ERROR_INVALID_PARAMETERS = 15; /**< not provided (or provided invalid) crucial parameters to perform RPC */ + + /**< Storage Errors */ + ERROR_STORAGE_NOT_READY = 5; /**< FS not ready */ + ERROR_STORAGE_EXIST = 6; /**< File/Dir already exist */ + ERROR_STORAGE_NOT_EXIST = 7; /**< File/Dir does not exist */ + ERROR_STORAGE_INVALID_PARAMETER = 8; /**< Invalid API parameter */ + ERROR_STORAGE_DENIED = 9; /**< Access denied */ + ERROR_STORAGE_INVALID_NAME = 10; /**< Invalid name/path */ + ERROR_STORAGE_INTERNAL = 11; /**< Internal error */ + ERROR_STORAGE_NOT_IMPLEMENTED = 12; /**< Function is not implemented */ + ERROR_STORAGE_ALREADY_OPEN = 13; /**< File/Dir already opened */ + ERROR_STORAGE_DIR_NOT_EMPTY = 18; /**< Directory, you're going to remove is not empty */ + + /**< Application Errors */ + ERROR_APP_CANT_START = 16; /**< Can't start app - internal error */ + ERROR_APP_SYSTEM_LOCKED = 17; /**< Another app is running */ + ERROR_APP_NOT_RUNNING = 21; /**< App is not running or doesn't support RPC commands */ + ERROR_APP_CMD_ERROR = 22; /**< Command execution error */ + + /**< Virtual Display Errors */ + ERROR_VIRTUAL_DISPLAY_ALREADY_STARTED = 19; /**< Virtual Display session can't be started twice */ + ERROR_VIRTUAL_DISPLAY_NOT_STARTED = 20; /**< Virtual Display session can't be stopped when it's not started */ + + /**< GPIO Errors */ + ERROR_GPIO_MODE_INCORRECT = 58; + ERROR_GPIO_UNKNOWN_PIN_MODE = 59; +} + +/* There are Server commands (e.g. Storage_write), which have no body message + * in response. But 'oneof' obligate to have at least 1 encoded message + * in scope. For this needs Empty message is implemented. + */ +message Empty { +} + +message StopSession { +} + +message Main { + uint32 command_id = 1; + CommandStatus command_status = 2; + bool has_next = 3; + oneof content { + Empty empty = 4; + StopSession stop_session = 19; + .PB_System.PingRequest system_ping_request = 5; + .PB_System.PingResponse system_ping_response = 6; + .PB_System.RebootRequest system_reboot_request = 31; + .PB_System.DeviceInfoRequest system_device_info_request = 32; + .PB_System.DeviceInfoResponse system_device_info_response = 33; + .PB_System.FactoryResetRequest system_factory_reset_request = 34; + .PB_System.GetDateTimeRequest system_get_datetime_request = 35; + .PB_System.GetDateTimeResponse system_get_datetime_response = 36; + .PB_System.SetDateTimeRequest system_set_datetime_request = 37; + .PB_System.PlayAudiovisualAlertRequest system_play_audiovisual_alert_request = 38; + .PB_System.ProtobufVersionRequest system_protobuf_version_request = 39; + .PB_System.ProtobufVersionResponse system_protobuf_version_response = 40; + .PB_System.UpdateRequest system_update_request = 41; + .PB_System.UpdateResponse system_update_response = 46; + .PB_System.PowerInfoRequest system_power_info_request = 44; + .PB_System.PowerInfoResponse system_power_info_response = 45; + .PB_Storage.InfoRequest storage_info_request = 28; + .PB_Storage.InfoResponse storage_info_response = 29; + .PB_Storage.TimestampRequest storage_timestamp_request = 59; + .PB_Storage.TimestampResponse storage_timestamp_response = 60; + .PB_Storage.StatRequest storage_stat_request = 24; + .PB_Storage.StatResponse storage_stat_response = 25; + .PB_Storage.ListRequest storage_list_request = 7; + .PB_Storage.ListResponse storage_list_response = 8; + .PB_Storage.ReadRequest storage_read_request = 9; + .PB_Storage.ReadResponse storage_read_response = 10; + .PB_Storage.WriteRequest storage_write_request = 11; + .PB_Storage.DeleteRequest storage_delete_request = 12; + .PB_Storage.MkdirRequest storage_mkdir_request = 13; + .PB_Storage.Md5sumRequest storage_md5sum_request = 14; + .PB_Storage.Md5sumResponse storage_md5sum_response = 15; + .PB_Storage.RenameRequest storage_rename_request = 30; + .PB_Storage.BackupCreateRequest storage_backup_create_request = 42; + .PB_Storage.BackupRestoreRequest storage_backup_restore_request = 43; + .PB_Storage.TarExtractRequest storage_tar_extract_request = 71; + .PB_App.StartRequest app_start_request = 16; + .PB_App.LockStatusRequest app_lock_status_request = 17; + .PB_App.LockStatusResponse app_lock_status_response = 18; + .PB_App.AppExitRequest app_exit_request = 47; + .PB_App.AppLoadFileRequest app_load_file_request = 48; + .PB_App.AppButtonPressRequest app_button_press_request = 49; + .PB_App.AppButtonReleaseRequest app_button_release_request = 50; + .PB_App.AppButtonPressReleaseRequest app_button_press_release_request = 75; + .PB_App.GetErrorRequest app_get_error_request = 63; + .PB_App.GetErrorResponse app_get_error_response = 64; + .PB_App.DataExchangeRequest app_data_exchange_request = 65; + .PB_Gui.StartScreenStreamRequest gui_start_screen_stream_request = 20; + .PB_Gui.StopScreenStreamRequest gui_stop_screen_stream_request = 21; + .PB_Gui.ScreenFrame gui_screen_frame = 22; + .PB_Gui.SendInputEventRequest gui_send_input_event_request = 23; + .PB_Gui.StartVirtualDisplayRequest gui_start_virtual_display_request = 26; + .PB_Gui.StopVirtualDisplayRequest gui_stop_virtual_display_request = 27; + .PB_Gpio.SetPinMode gpio_set_pin_mode = 51; + .PB_Gpio.SetInputPull gpio_set_input_pull = 52; + .PB_Gpio.GetPinMode gpio_get_pin_mode = 53; + .PB_Gpio.GetPinModeResponse gpio_get_pin_mode_response = 54; + .PB_Gpio.ReadPin gpio_read_pin = 55; + .PB_Gpio.ReadPinResponse gpio_read_pin_response = 56; + .PB_Gpio.WritePin gpio_write_pin = 57; + .PB_Gpio.GetOtgMode gpio_get_otg_mode = 72; + .PB_Gpio.GetOtgModeResponse gpio_get_otg_mode_response = 73; + .PB_Gpio.SetOtgMode gpio_set_otg_mode = 74; + .PB_App.AppStateResponse app_state_response = 58; + .PB_Property.GetRequest property_get_request = 61; + .PB_Property.GetResponse property_get_response = 62; + .PB_Desktop.IsLockedRequest desktop_is_locked_request = 66; + .PB_Desktop.UnlockRequest desktop_unlock_request = 67; + .PB_Desktop.StatusSubscribeRequest desktop_status_subscribe_request = 68; + .PB_Desktop.StatusUnsubscribeRequest desktop_status_unsubscribe_request = 69; + .PB_Desktop.Status desktop_status = 70; + } +} + +message Region { + message Band { + uint32 start = 1; + uint32 end = 2; + int32 power_limit = 3; + uint32 duty_cycle = 4; + } + + bytes country_code = 1; + repeated Band bands = 2; +} diff --git a/components/bridge/pbutils/src/main/proto/gpio.options b/components/bridge/pbutils/src/main/proto/gpio.options new file mode 100644 index 0000000000..e69de29bb2 diff --git a/components/bridge/pbutils/src/main/proto/gpio.proto b/components/bridge/pbutils/src/main/proto/gpio.proto new file mode 100644 index 0000000000..236767be02 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/gpio.proto @@ -0,0 +1,73 @@ +syntax = "proto3"; + +package PB_Gpio; +option java_package = "com.flipperdevices.protobuf.gpio"; + +enum GpioPin { + PC0 = 0; + PC1 = 1; + PC3 = 2; + PB2 = 3; + PB3 = 4; + PA4 = 5; + PA6 = 6; + PA7 = 7; +}; + +enum GpioPinMode { + OUTPUT = 0; + INPUT = 1; +}; + +enum GpioInputPull { + NO = 0; + UP = 1; + DOWN = 2; +}; + +enum GpioOtgMode { + OFF = 0; + ON = 1; +}; + +message SetPinMode { + GpioPin pin = 1; + GpioPinMode mode = 2; +} + +message SetInputPull { + GpioPin pin = 1; + GpioInputPull pull_mode = 2; +} + +message GetPinMode { + GpioPin pin = 1; +} + +message GetPinModeResponse { + GpioPinMode mode = 1; +} + +message ReadPin { + GpioPin pin = 1; +} + +message ReadPinResponse { + uint32 value = 2; +} + +message WritePin { + GpioPin pin = 1; + uint32 value = 2; +} + +message GetOtgMode { +}; + +message GetOtgModeResponse { + GpioOtgMode mode = 1; +}; + +message SetOtgMode { + GpioOtgMode mode = 1; +}; diff --git a/components/bridge/pbutils/src/main/proto/gui.options b/components/bridge/pbutils/src/main/proto/gui.options new file mode 100644 index 0000000000..a5efc3e3f9 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/gui.options @@ -0,0 +1,2 @@ +PB_Gui.ScreenFrame.data type:FT_POINTER +PB_Gui.ScreenFrame.data max_size:1024 \ No newline at end of file diff --git a/components/bridge/pbutils/src/main/proto/gui.proto b/components/bridge/pbutils/src/main/proto/gui.proto new file mode 100644 index 0000000000..dbd5144fc8 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/gui.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +package PB_Gui; +option java_package = "com.flipperdevices.protobuf.screen"; + +enum InputKey { + UP = 0; + DOWN = 1; + RIGHT = 2; + LEFT = 3; + OK = 4; + BACK = 5; +}; + +enum InputType { + PRESS = 0; /**< Press event, emitted after de-bounce */ + RELEASE = 1; /**< Release event, emitted after de-bounce */ + SHORT = 2; /**< Short event, emitted after InputTypeRelease done withing INPUT_LONG_PRESS interval */ + LONG = 3; /**< Long event, emitted after INPUT_LONG_PRESS interval, asynchronous to InputTypeRelease */ + REPEAT = 4; /**< Repeat event, emitted with INPUT_REPEATE_PRESS period after InputTypeLong event */ +} + +enum ScreenOrientation { + HORIZONTAL = 0; /**< Horizontal */ + HORIZONTAL_FLIP = 1; /**< Horizontal flipped (180) */ + VERTICAL = 2; /**< Vertical (90) */ + VERTICAL_FLIP = 3; /**< Vertical flipped (270) */ +} + +message ScreenFrame { + bytes data = 1; + ScreenOrientation orientation = 2; +} + +message StartScreenStreamRequest { +} + +message StopScreenStreamRequest { +} + +message SendInputEventRequest { + InputKey key = 1; + InputType type = 2; +} + +message StartVirtualDisplayRequest { + ScreenFrame first_frame = 1; /**< Optional: screen frame to show */ + bool send_input = 2; /**< Optional: send flipper input */ +} + +message StopVirtualDisplayRequest { +} \ No newline at end of file diff --git a/components/bridge/pbutils/src/main/proto/property.options b/components/bridge/pbutils/src/main/proto/property.options new file mode 100644 index 0000000000..3cfad35b51 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/property.options @@ -0,0 +1,3 @@ +PB_Property.GetRequest.key type:FT_POINTER +PB_Property.GetResponse.key type:FT_POINTER +PB_Property.GetResponse.value type:FT_POINTER diff --git a/components/bridge/pbutils/src/main/proto/property.proto b/components/bridge/pbutils/src/main/proto/property.proto new file mode 100644 index 0000000000..2f164ce048 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/property.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package PB_Property; +option java_package = "com.flipperdevices.protobuf.property"; + +message GetRequest { + string key = 1; +} + +message GetResponse { + string key = 1; + string value = 2; +} diff --git a/components/bridge/pbutils/src/main/proto/storage.options b/components/bridge/pbutils/src/main/proto/storage.options new file mode 100644 index 0000000000..8e99754fdd --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/storage.options @@ -0,0 +1,26 @@ +PB_Storage.File.name type:FT_POINTER +PB_Storage.File.data type:FT_POINTER +PB_Storage.InfoRequest.path type:FT_POINTER +PB_Storage.TimestampRequest.path type:FT_POINTER +PB_Storage.StatRequest.path type:FT_POINTER +PB_Storage.ListRequest.path type:FT_POINTER +PB_Storage.ReadRequest.path type:FT_POINTER +PB_Storage.WriteRequest.path type:FT_POINTER +PB_Storage.DeleteRequest.path type:FT_POINTER +PB_Storage.MkdirRequest.path type:FT_POINTER +PB_Storage.Md5sumRequest.path type:FT_POINTER +PB_Storage.RenameRequest.old_path type:FT_POINTER +PB_Storage.RenameRequest.new_path type:FT_POINTER +PB_Storage.BackupCreateRequest.archive_path type:FT_POINTER +PB_Storage.BackupRestoreRequest.archive_path type:FT_POINTER +PB_Storage.TarExtractRequest.tar_path type:FT_POINTER +PB_Storage.TarExtractRequest.out_path type:FT_POINTER + +PB_Storage.ListResponse.file max_count:8 + +// not affected by nanopb, so server & client should keep in mind these max values +PB_Storage.File.data max_size:512 +PB_Storage.Md5sumResponse.md5sum max_length:32 +PB_Storage.File.md5sum max_length:32 +PB_Storage.*.path max_length:255 + diff --git a/components/bridge/pbutils/src/main/proto/storage.proto b/components/bridge/pbutils/src/main/proto/storage.proto new file mode 100644 index 0000000000..eb6269bfb0 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/storage.proto @@ -0,0 +1,99 @@ +syntax = "proto3"; + +package PB_Storage; +option java_package = "com.flipperdevices.protobuf.storage"; + +message File { + enum FileType { + FILE = 0; // default value + DIR = 1; + } + FileType type = 1; + string name = 2; + uint32 size = 3; + bytes data = 4; + string md5sum = 5; +} + +message InfoRequest { + string path = 1; +} + +message InfoResponse { + uint64 total_space = 1; + uint64 free_space = 2; +} + +message TimestampRequest { + string path = 1; +} + +message TimestampResponse { + uint32 timestamp = 1; +} + +message StatRequest { + string path = 1; +} + +message StatResponse { + File file = 1; +} + +message ListRequest { + string path = 1; + bool include_md5 = 2; + uint32 filter_max_size = 3; +} + +message ListResponse { + repeated File file = 1; +} + +message ReadRequest { + string path = 1; +} + +message ReadResponse { + File file = 1; +} + +message WriteRequest { + string path = 1; + File file = 2; +} + +message DeleteRequest { + string path = 1; + bool recursive = 2; +} + +message MkdirRequest { + string path = 1; +} + +message Md5sumRequest { + string path = 1; +} + +message Md5sumResponse { + string md5sum = 1; +} + +message RenameRequest { + string old_path = 1; + string new_path = 2; +} + +message BackupCreateRequest { + string archive_path = 1; +} + +message BackupRestoreRequest { + string archive_path = 1; +} + +message TarExtractRequest { + string tar_path = 1; + string out_path = 2; +} diff --git a/components/bridge/pbutils/src/main/proto/system.options b/components/bridge/pbutils/src/main/proto/system.options new file mode 100644 index 0000000000..b400c3eeb2 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/system.options @@ -0,0 +1,16 @@ +PB_System.PingRequest.data type:FT_POINTER +PB_System.PingRequest.data max_size:1024 +PB_System.PingResponse.data type:FT_POINTER +PB_System.PingResponse.data max_size:1024 +PB_System.DeviceInfoResponse.key type:FT_POINTER +PB_System.DeviceInfoResponse.value type:FT_POINTER +PB_System.DateTime.hour int_size:IS_8 +PB_System.DateTime.minute int_size:IS_8 +PB_System.DateTime.second int_size:IS_8 +PB_System.DateTime.day int_size:IS_8 +PB_System.DateTime.month int_size:IS_8 +PB_System.DateTime.year int_size:IS_16 +PB_System.DateTime.weekday int_size:IS_8 +PB_System.UpdateRequest.update_manifest type:FT_POINTER +PB_System.PowerInfoResponse.key type:FT_POINTER +PB_System.PowerInfoResponse.value type:FT_POINTER diff --git a/components/bridge/pbutils/src/main/proto/system.proto b/components/bridge/pbutils/src/main/proto/system.proto new file mode 100644 index 0000000000..90238e8f34 --- /dev/null +++ b/components/bridge/pbutils/src/main/proto/system.proto @@ -0,0 +1,95 @@ +syntax = "proto3"; + +package PB_System; +option java_package = "com.flipperdevices.protobuf.system"; + +message PingRequest { + bytes data = 1; +} + +message PingResponse { + bytes data = 1; +} + +message RebootRequest { + enum RebootMode { + OS = 0; // default value + DFU = 1; + UPDATE = 2; + } + RebootMode mode = 1; +} + +message DeviceInfoRequest { +} + +message DeviceInfoResponse { + string key = 1; + string value = 2; +} + +message FactoryResetRequest { +} + +message GetDateTimeRequest { +} + +message GetDateTimeResponse { + DateTime datetime = 1; +} + +message SetDateTimeRequest { + DateTime datetime = 1; +} + +message DateTime { + // Time + uint32 hour = 1; /**< Hour in 24H format: 0-23 */ + uint32 minute = 2; /**< Minute: 0-59 */ + uint32 second = 3; /**< Second: 0-59 */ + // Date + uint32 day = 4; /**< Current day: 1-31 */ + uint32 month = 5; /**< Current month: 1-12 */ + uint32 year = 6; /**< Current year: 2000-2099 */ + uint32 weekday = 7; /**< Current weekday: 1-7 */ +} + +message PlayAudiovisualAlertRequest { +} + +message ProtobufVersionRequest { +} + +message ProtobufVersionResponse { + uint32 major = 1; + uint32 minor = 2; +} + +message UpdateRequest { + string update_manifest = 1; +} + +message UpdateResponse { + enum UpdateResultCode { + OK = 0; + ManifestPathInvalid = 1; + ManifestFolderNotFound = 2; + ManifestInvalid = 3; + StageMissing = 4; + StageIntegrityError = 5; + ManifestPointerError = 6; + TargetMismatch = 7; + OutdatedManifestVersion = 8; + IntFull = 9; + UnspecifiedError = 10; + } + UpdateResultCode code = 1; +} + +message PowerInfoRequest { +} + +message PowerInfoResponse { + string key = 1; + string value = 2; +} From 47b35fc816b18cc5e1cbaf131e8d71a54e746712 Mon Sep 17 00:00:00 2001 From: Nikita Kulikov Date: Fri, 24 Jan 2025 21:30:47 +0000 Subject: [PATCH 04/10] Fix lint --- CHANGELOG.md | 1 + .../search/BLESearchConnectionDelegate.kt | 1 - .../screens/search/USBSearchDelegate.kt | 3 +- .../ConnectionSearchDecomposeComponent.kt | 48 ++------------ .../search/ConnectionSearchDelegate.kt | 2 +- .../screens/search/ConnectionSearchItem.kt | 2 +- .../search/ConnectionSearchItemComposable.kt | 66 +++++++++++++++++++ .../search/ConnectionSearchViewModel.kt | 2 - .../screens/search/USBSearchDelegate.kt | 1 - .../usb/impl/model/USBAndroidDevice.kt | 2 - .../usb/impl/model/USBAndroidDeviceFactory.kt | 3 +- .../usb/impl/model/USBSerialListener.kt | 2 +- .../usb/impl/model/USBDesktopDevice.kt | 1 - .../usb/impl/model/USBDesktopDeviceFactory.kt | 2 +- .../usb/impl/di/BleDeviceConnectionModule.kt | 1 - .../usb/impl/model/USBPlatformDevice.kt | 4 +- .../impl/model/USBPlatformDeviceFactory.kt | 2 +- 17 files changed, 79 insertions(+), 64 deletions(-) create mode 100644 components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItemComposable.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d0fded8c..4fdc3c0a77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ # 1.8.2 - In Progress +- [Feature] Add USB connection option to sample # 1.8.1 diff --git a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt index 20adc0caaa..d6002324c5 100644 --- a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt +++ b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import me.gulya.anvil.assisted.ContributesAssistedFactory import no.nordicsemi.android.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanMode import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerSettings diff --git a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt index 436168a9c6..ebabe1dc94 100644 --- a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt +++ b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt @@ -58,7 +58,7 @@ class USBSearchDelegate @AssistedInject constructor( .filterIsInstance() .associateBy { it.portPath } - info { searchDevices.joinToString(",") { "${it}" } } + info { searchDevices.joinToString(",") { "$it" } } searchItems.emit( searchDevices.map { it.toFDeviceFlipperZeroUSBModel() }.map { usbDevice -> @@ -78,7 +78,6 @@ class USBSearchDelegate @AssistedInject constructor( override fun getDevicesFlow() = searchItems.asStateFlow() - @AssistedFactory @ContributesMultibinding(AppGraph::class, ConnectionSearchDelegate.Factory::class) fun interface Factory : ConnectionSearchDelegate.Factory { diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDecomposeComponent.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDecomposeComponent.kt index 330a146067..16da3c754e 100644 --- a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDecomposeComponent.kt +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDecomposeComponent.kt @@ -25,16 +25,11 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import flipperapp.components.bridge.connection.sample.shared.generated.resources.Res import flipperapp.components.bridge.connection.sample.shared.generated.resources.connection_search_title -import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_add_box -import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_delete import flipperapp.components.core.ui.res.generated.resources.material_ic_close import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import javax.inject.Provider import flipperapp.components.core.ui.res.generated.resources.Res as SharedRes -import com.flipperdevices.bridge.connection.config.api.FDeviceType -import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_bluetooth -import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_usb class ConnectionSearchDecomposeComponent @AssistedInject constructor( @Assisted componentContext: ComponentContext, @@ -71,45 +66,10 @@ class ConnectionSearchDecomposeComponent @AssistedInject constructor( devices, key = { device -> device.address } ) { searchItem -> - Row { - Icon( - modifier = Modifier - .padding(16.dp) - .size(24.dp), - painter = painterResource( - when(searchItem.deviceModel.type) { - FDeviceType.FLIPPER_ZERO_BLE -> Res.drawable.material_ic_bluetooth - FDeviceType.FLIPPER_ZERO_USB -> Res.drawable.material_ic_usb - } - ), - contentDescription = null, - tint = LocalPallet.current.text100 - ) - - Text( - modifier = Modifier - .weight(1f) - .padding(16.dp), - text = searchItem.deviceModel.humanReadableName, - color = LocalPallet.current.text100 - ) - - Icon( - modifier = Modifier - .clickableRipple { searchViewModel.onDeviceClick(searchItem) } - .padding(16.dp) - .size(24.dp), - painter = painterResource( - if (searchItem.isAdded) { - Res.drawable.material_ic_delete - } else { - Res.drawable.material_ic_add_box - } - ), - contentDescription = null, - tint = LocalPallet.current.text100 - ) - } + ConnectionSearchItemComposable( + searchItem, + onDeviceClick = { searchViewModel.onDeviceClick(searchItem) } + ) } } } diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDelegate.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDelegate.kt index d547eebcbc..81af8bd32f 100644 --- a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDelegate.kt +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchDelegate.kt @@ -11,4 +11,4 @@ interface ConnectionSearchDelegate { scope: CoroutineScope, ): ConnectionSearchDelegate } -} \ No newline at end of file +} diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItem.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItem.kt index b8bacce30c..73ca96e4e2 100644 --- a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItem.kt +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItem.kt @@ -6,4 +6,4 @@ data class ConnectionSearchItem( val address: String, val deviceModel: FDeviceBaseModel, val isAdded: Boolean -) \ No newline at end of file +) diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItemComposable.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItemComposable.kt new file mode 100644 index 0000000000..17c5057526 --- /dev/null +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchItemComposable.kt @@ -0,0 +1,66 @@ +package com.flipperdevices.bridge.connection.screens.search + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flipperdevices.bridge.connection.config.api.FDeviceType +import com.flipperdevices.core.ui.ktx.clickableRipple +import com.flipperdevices.core.ui.theme.LocalPallet +import flipperapp.components.bridge.connection.sample.shared.generated.resources.Res +import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_add_box +import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_bluetooth +import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_delete +import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_usb +import org.jetbrains.compose.resources.painterResource + +@Composable +fun ConnectionSearchItemComposable( + searchItem: ConnectionSearchItem, + onDeviceClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row(modifier) { + Icon( + modifier = Modifier + .padding(16.dp) + .size(24.dp), + painter = painterResource( + when (searchItem.deviceModel.type) { + FDeviceType.FLIPPER_ZERO_BLE -> Res.drawable.material_ic_bluetooth + FDeviceType.FLIPPER_ZERO_USB -> Res.drawable.material_ic_usb + } + ), + contentDescription = null, + tint = LocalPallet.current.text100 + ) + + Text( + modifier = Modifier + .weight(1f) + .padding(16.dp), + text = searchItem.deviceModel.humanReadableName, + color = LocalPallet.current.text100 + ) + + Icon( + modifier = Modifier + .clickableRipple(onClick = onDeviceClick) + .padding(16.dp) + .size(24.dp), + painter = painterResource( + if (searchItem.isAdded) { + Res.drawable.material_ic_delete + } else { + Res.drawable.material_ic_add_box + } + ), + contentDescription = null, + tint = LocalPallet.current.text100 + ) + } +} diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt index c50c203757..08424dce04 100644 --- a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt @@ -2,11 +2,9 @@ package com.flipperdevices.bridge.connection.screens.search import com.flipperdevices.bridge.connection.config.api.FDevicePersistedStorage import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch diff --git a/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt b/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt index 9354ac3ba4..185828e656 100644 --- a/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt +++ b/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt @@ -72,7 +72,6 @@ class USBSearchDelegate @AssistedInject constructor( override fun getDevicesFlow() = searchItems.asStateFlow() - @AssistedFactory @ContributesMultibinding(AppGraph::class, ConnectionSearchDelegate.Factory::class) fun interface Factory : ConnectionSearchDelegate.Factory { diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt index bbb1ab27a1..887fe0bb90 100644 --- a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt @@ -3,7 +3,6 @@ package com.flipperdevices.bridge.connection.transport.usb.impl.model import android.app.PendingIntent import android.content.Intent import android.hardware.usb.UsbManager -import android.util.Log import com.flipperdevices.core.activityholder.CurrentActivityHolder import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.error @@ -57,7 +56,6 @@ class USBAndroidDevice( private fun requestPermission() { if (!usbManager.hasPermission(serialDriver.device)) { - val intent = Intent("Test") val activity = CurrentActivityHolder.getCurrentActivity() diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt index ed20fb3183..e21a0324ce 100644 --- a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt @@ -9,7 +9,6 @@ import com.squareup.anvil.annotations.ContributesBinding import kotlinx.coroutines.CoroutineScope import javax.inject.Inject - @ContributesBinding(AppGraph::class, USBPlatformDeviceFactory::class) class USBAndroidDeviceFactory @Inject constructor( private val context: Context @@ -33,4 +32,4 @@ class USBAndroidDeviceFactory @Inject constructor( scope = scope ) } -} \ No newline at end of file +} diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt index e629b88733..db8b93ee3f 100644 --- a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt @@ -71,4 +71,4 @@ private fun Any.wait() { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") private fun Any.notifyAll() { (this as Object).notifyAll() -} \ No newline at end of file +} diff --git a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt index f1c8d351fc..6b97f9ea1f 100644 --- a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt +++ b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt @@ -1,7 +1,6 @@ package com.flipperdevices.bridge.connection.transport.usb.impl.model import com.fazecast.jSerialComm.SerialPort -import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig private const val OPEN_PORT_TIME_MS = 1000 diff --git a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt index 662eba66cf..8526a35e54 100644 --- a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt +++ b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt @@ -15,4 +15,4 @@ class USBDesktopDeviceFactory @Inject constructor() : USBPlatformDeviceFactory { ): USBPlatformDevice { return USBDesktopDevice(SerialPort.getCommPort(config.path)) } -} \ No newline at end of file +} diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt index 4cbf56cc13..5c6ddd87e5 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt @@ -5,7 +5,6 @@ import com.flipperdevices.bridge.connection.transport.common.api.di.DeviceConnec import com.flipperdevices.bridge.connection.transport.common.api.di.toHolder import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig import com.flipperdevices.bridge.connection.transport.usb.impl.USBDeviceConnectionApiImpl -import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDevice import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDeviceFactory import com.flipperdevices.core.di.AppGraph import com.squareup.anvil.annotations.ContributesTo diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt index e9b8db8cee..8b2d67fe35 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt @@ -1,7 +1,5 @@ package com.flipperdevices.bridge.connection.transport.usb.impl.model -import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig - interface USBPlatformDevice { fun connect(baudRate: Int, dataBits: Int, stopBits: Int, parity: Int): Boolean fun closePort() @@ -23,4 +21,4 @@ interface USBPlatformDevice { const val ONE_POINT_FIVE_STOP_BITS: Int = 2 const val TWO_STOP_BITS: Int = 3 } -} \ No newline at end of file +} diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt index 75037b37aa..e06e7090eb 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt @@ -8,4 +8,4 @@ interface USBPlatformDeviceFactory { config: FUSBDeviceConnectionConfig, scope: CoroutineScope ): USBPlatformDevice -} \ No newline at end of file +} From ad9866da6b8999c8f7f71f22c2ae004ddeae70ef Mon Sep 17 00:00:00 2001 From: Nikita Kulikov Date: Fri, 24 Jan 2025 21:38:11 +0000 Subject: [PATCH 05/10] Revert submodule bad changes --- .../pbutils/src/main/proto/.github/CODEOWNERS | 3 - .../main/proto/.github/workflows/generate.yml | 30 ---- .../bridge/pbutils/src/main/proto/.gitignore | 8 - .../bridge/pbutils/src/main/proto/Changelog | 105 ------------ .../src/main/proto/application.options | 8 - .../pbutils/src/main/proto/application.proto | 58 ------- .../pbutils/src/main/proto/desktop.proto | 20 --- .../pbutils/src/main/proto/flipper.options | 6 - .../pbutils/src/main/proto/flipper.proto | 151 ------------------ .../pbutils/src/main/proto/gpio.options | 0 .../bridge/pbutils/src/main/proto/gpio.proto | 73 --------- .../bridge/pbutils/src/main/proto/gui.options | 2 - .../bridge/pbutils/src/main/proto/gui.proto | 52 ------ .../pbutils/src/main/proto/property.options | 3 - .../pbutils/src/main/proto/property.proto | 13 -- .../pbutils/src/main/proto/storage.options | 26 --- .../pbutils/src/main/proto/storage.proto | 99 ------------ .../pbutils/src/main/proto/system.options | 16 -- .../pbutils/src/main/proto/system.proto | 95 ----------- 19 files changed, 768 deletions(-) delete mode 100644 components/bridge/pbutils/src/main/proto/.github/CODEOWNERS delete mode 100644 components/bridge/pbutils/src/main/proto/.github/workflows/generate.yml delete mode 100644 components/bridge/pbutils/src/main/proto/.gitignore delete mode 100644 components/bridge/pbutils/src/main/proto/Changelog delete mode 100644 components/bridge/pbutils/src/main/proto/application.options delete mode 100644 components/bridge/pbutils/src/main/proto/application.proto delete mode 100644 components/bridge/pbutils/src/main/proto/desktop.proto delete mode 100644 components/bridge/pbutils/src/main/proto/flipper.options delete mode 100644 components/bridge/pbutils/src/main/proto/flipper.proto delete mode 100644 components/bridge/pbutils/src/main/proto/gpio.options delete mode 100644 components/bridge/pbutils/src/main/proto/gpio.proto delete mode 100644 components/bridge/pbutils/src/main/proto/gui.options delete mode 100644 components/bridge/pbutils/src/main/proto/gui.proto delete mode 100644 components/bridge/pbutils/src/main/proto/property.options delete mode 100644 components/bridge/pbutils/src/main/proto/property.proto delete mode 100644 components/bridge/pbutils/src/main/proto/storage.options delete mode 100644 components/bridge/pbutils/src/main/proto/storage.proto delete mode 100644 components/bridge/pbutils/src/main/proto/system.options delete mode 100644 components/bridge/pbutils/src/main/proto/system.proto diff --git a/components/bridge/pbutils/src/main/proto/.github/CODEOWNERS b/components/bridge/pbutils/src/main/proto/.github/CODEOWNERS deleted file mode 100644 index dfe56a07f4..0000000000 --- a/components/bridge/pbutils/src/main/proto/.github/CODEOWNERS +++ /dev/null @@ -1,3 +0,0 @@ -# Who owns all the code by default - -* @DrZlo13 @skotopes @gornekich @nminaylov @hedger diff --git a/components/bridge/pbutils/src/main/proto/.github/workflows/generate.yml b/components/bridge/pbutils/src/main/proto/.github/workflows/generate.yml deleted file mode 100644 index 16d34a769c..0000000000 --- a/components/bridge/pbutils/src/main/proto/.github/workflows/generate.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: 'Generate protobuf sources' - -on: - push: - -jobs: - generate: - runs-on: ubuntu-latest - steps: - - name: Checkout flipperzero-protobuf - uses: actions/checkout@v2 - - - name: Checkout nanopb - uses: actions/checkout@v2 - with: - repository: nanopb/nanopb - path: nanopb - - - name: Setup python - uses: actions/setup-python@v3 - with: - python-version: '3.9' - - - name: Setup protobuf - run: | - sudo apt -y install protobuf-compiler - python3 -m pip install --upgrade python3-protobuf==2.5.0 protobuf==3.20.1 grpcio-tools==1.47.0 grpcio==1.47.0 - - - name: Generate sources - run: python3 nanopb/generator/nanopb_generator.py -q -I . -D /tmp *.proto diff --git a/components/bridge/pbutils/src/main/proto/.gitignore b/components/bridge/pbutils/src/main/proto/.gitignore deleted file mode 100644 index 34165a8706..0000000000 --- a/components/bridge/pbutils/src/main/proto/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# JetBrains IDEs -.idea - -# MacOS -.DS_Store - -# Visual Studio Code -.vscode/ \ No newline at end of file diff --git a/components/bridge/pbutils/src/main/proto/Changelog b/components/bridge/pbutils/src/main/proto/Changelog deleted file mode 100644 index be5fa9c29f..0000000000 --- a/components/bridge/pbutils/src/main/proto/Changelog +++ /dev/null @@ -1,105 +0,0 @@ -# Changelog - -## [0.21] -### Added -- Application AppButtonPressRequest now supports button indices in addition to names. - -## [0.20] -### Added -- Storage ListRequest message now has a file size filter. - -## [0.19] -### Added -- Storage ListRequest message now supports md5sum calculation for each file - -## [0.18] -### Added -- Loader is now able to load apps from SD card - -## [0.17] -### Added -- Desktop service subscription API - -## [0.16] -### Added -- Desktop service API - -## [0.15] -### Modified -- ScreenFrame: additional orientation field - -## [0.14] -### Added -- New subsystem: Property -- Property messages: GetRequest, GetResponse -- App messages: GetErrorRequest, GetErrorResponse, DataExchangeRequest - -## [0.13] -### Added -- Storage: timestamp - -## [0.12] -### Added -- Region message - -## [0.11] -### Added -- App messages: AppStateResponse - -## [0.10] -### Added -- GPIO message: SetPinMode, SetInputPull, GetPinMode, GetPinModeResponse, ReadPin, ReadPinResponse, WritePin - -## [0.9] -### Added -- System message: UpdateResponse, enum UpdateResultCode: new entries - OutdatedManifestVersion, IntFull, UnspecifiedError - -## [0.8] -### Added -- App messages: AppExitRequest, AppLoadFileRequest, AppButtonPressRequest, AppButtonReleaseRequest - -## [0.7] -### Added -- System message: UpdateResponse: passing update preparation detailed status - -## [0.6] -### Added -- System message: RebootRequest: added UPDATE restart mode -### Fixed -- System message: UpdateRequest: renamed manifest field - -## [0.5] -### Added -- System message: PowerInfo - -## [0.4] -### Added -- System message: UpdateRequest -- Storage messages: BackupCreateRequest, BackupRestoreRequest - -## [0.3] -### Fixed -- BLE disconnection after writing / erasing FLASH - -## [0.2] -### Added -- System messages: ProtobufVersion -- Support parallel RPC sessions -- Screen streaming optimization - -## [0.1] -### Added -- Code owners -- CI setup -- System messages: Ping, Reboot, DeviceInfo, FactoryReset, DateTime, - AudiovisualAlert -- Storage messages: Info, Stat, Read, Write, Delete, Mkdir, Md5sum, - Rename -- App messages: Start, LockStatus -- Gui messages: ScreenStream, ScreenFrame, SendInput, VirtualDisplay - -## [0.0] -### Added -- Introduced Flipper Zero Protobuf repository -- StorageList messages diff --git a/components/bridge/pbutils/src/main/proto/application.options b/components/bridge/pbutils/src/main/proto/application.options deleted file mode 100644 index 338bb67d61..0000000000 --- a/components/bridge/pbutils/src/main/proto/application.options +++ /dev/null @@ -1,8 +0,0 @@ -PB_App.StartRequest.args type:FT_POINTER -PB_App.StartRequest.args max_length:512 -PB_App.StartRequest.name type:FT_POINTER -PB_App.StartRequest.name max_length:512 -PB_App.AppLoadFileRequest.path max_length:512 -PB_App.AppButtonPressRequest.args max_length:512 -PB_App.GetErrorResponse.text type:FT_POINTER -PB_App.DataExchangeRequest.data type:FT_POINTER diff --git a/components/bridge/pbutils/src/main/proto/application.proto b/components/bridge/pbutils/src/main/proto/application.proto deleted file mode 100644 index 4c82cdf91f..0000000000 --- a/components/bridge/pbutils/src/main/proto/application.proto +++ /dev/null @@ -1,58 +0,0 @@ -syntax = "proto3"; - -package PB_App; -option java_package = "com.flipperdevices.protobuf.app"; - -message StartRequest { - string name = 1; - string args = 2; -} - -message LockStatusRequest { -} - -message LockStatusResponse { - bool locked = 1; -} - -message AppExitRequest { -} - -message AppLoadFileRequest { - string path = 1; -} - -message AppButtonPressRequest { - string args = 1; - int32 index = 2; -} - -message AppButtonReleaseRequest { -} - -message AppButtonPressReleaseRequest { - string args = 1; - int32 index = 2; -} - -enum AppState { - APP_CLOSED = 0; - APP_STARTED = 1; -}; - -message AppStateResponse { - AppState state = 1; -} - -message GetErrorRequest { -} - -message GetErrorResponse { - uint32 code = 1; - string text = 2; -} - -message DataExchangeRequest { - bytes data = 1; -} - diff --git a/components/bridge/pbutils/src/main/proto/desktop.proto b/components/bridge/pbutils/src/main/proto/desktop.proto deleted file mode 100644 index cc8212d40c..0000000000 --- a/components/bridge/pbutils/src/main/proto/desktop.proto +++ /dev/null @@ -1,20 +0,0 @@ -syntax = "proto3"; - -package PB_Desktop; -option java_package = "com.flipperdevices.protobuf.desktop"; - -message IsLockedRequest { -} - -message UnlockRequest { -} - -message StatusSubscribeRequest { -} - -message StatusUnsubscribeRequest { -} - -message Status { - bool locked = 1; -} diff --git a/components/bridge/pbutils/src/main/proto/flipper.options b/components/bridge/pbutils/src/main/proto/flipper.options deleted file mode 100644 index 0301133444..0000000000 --- a/components/bridge/pbutils/src/main/proto/flipper.options +++ /dev/null @@ -1,6 +0,0 @@ -PB.Main submsg_callback:true -PB.Region.country_code type:FT_POINTER -PB.Region.country_code max_size:2 -PB.Region.Band.power_limit int_size:IS_8 -PB.Region.Band.duty_cycle int_size:IS_8 - diff --git a/components/bridge/pbutils/src/main/proto/flipper.proto b/components/bridge/pbutils/src/main/proto/flipper.proto deleted file mode 100644 index 2ba75dadce..0000000000 --- a/components/bridge/pbutils/src/main/proto/flipper.proto +++ /dev/null @@ -1,151 +0,0 @@ -syntax = "proto3"; -import "storage.proto"; -import "system.proto"; -import "application.proto"; -import "gui.proto"; -import "gpio.proto"; -import "property.proto"; -import "desktop.proto"; - -package PB; -option java_package = "com.flipperdevices.protobuf"; - -enum CommandStatus { - OK = 0; - - /**< Common Errors */ - ERROR = 1; /**< Unknown error */ - ERROR_DECODE = 2; /**< Command can't be decoded successfully - command_id in response may be wrong! */ - ERROR_NOT_IMPLEMENTED = 3; /**< Command successfully decoded, but not implemented (deprecated or not yet implemented) */ - ERROR_BUSY = 4; /**< Somebody took global lock, so not all commands are available */ - ERROR_CONTINUOUS_COMMAND_INTERRUPTED = 14; /**< Not received has_next == 0 */ - ERROR_INVALID_PARAMETERS = 15; /**< not provided (or provided invalid) crucial parameters to perform RPC */ - - /**< Storage Errors */ - ERROR_STORAGE_NOT_READY = 5; /**< FS not ready */ - ERROR_STORAGE_EXIST = 6; /**< File/Dir already exist */ - ERROR_STORAGE_NOT_EXIST = 7; /**< File/Dir does not exist */ - ERROR_STORAGE_INVALID_PARAMETER = 8; /**< Invalid API parameter */ - ERROR_STORAGE_DENIED = 9; /**< Access denied */ - ERROR_STORAGE_INVALID_NAME = 10; /**< Invalid name/path */ - ERROR_STORAGE_INTERNAL = 11; /**< Internal error */ - ERROR_STORAGE_NOT_IMPLEMENTED = 12; /**< Function is not implemented */ - ERROR_STORAGE_ALREADY_OPEN = 13; /**< File/Dir already opened */ - ERROR_STORAGE_DIR_NOT_EMPTY = 18; /**< Directory, you're going to remove is not empty */ - - /**< Application Errors */ - ERROR_APP_CANT_START = 16; /**< Can't start app - internal error */ - ERROR_APP_SYSTEM_LOCKED = 17; /**< Another app is running */ - ERROR_APP_NOT_RUNNING = 21; /**< App is not running or doesn't support RPC commands */ - ERROR_APP_CMD_ERROR = 22; /**< Command execution error */ - - /**< Virtual Display Errors */ - ERROR_VIRTUAL_DISPLAY_ALREADY_STARTED = 19; /**< Virtual Display session can't be started twice */ - ERROR_VIRTUAL_DISPLAY_NOT_STARTED = 20; /**< Virtual Display session can't be stopped when it's not started */ - - /**< GPIO Errors */ - ERROR_GPIO_MODE_INCORRECT = 58; - ERROR_GPIO_UNKNOWN_PIN_MODE = 59; -} - -/* There are Server commands (e.g. Storage_write), which have no body message - * in response. But 'oneof' obligate to have at least 1 encoded message - * in scope. For this needs Empty message is implemented. - */ -message Empty { -} - -message StopSession { -} - -message Main { - uint32 command_id = 1; - CommandStatus command_status = 2; - bool has_next = 3; - oneof content { - Empty empty = 4; - StopSession stop_session = 19; - .PB_System.PingRequest system_ping_request = 5; - .PB_System.PingResponse system_ping_response = 6; - .PB_System.RebootRequest system_reboot_request = 31; - .PB_System.DeviceInfoRequest system_device_info_request = 32; - .PB_System.DeviceInfoResponse system_device_info_response = 33; - .PB_System.FactoryResetRequest system_factory_reset_request = 34; - .PB_System.GetDateTimeRequest system_get_datetime_request = 35; - .PB_System.GetDateTimeResponse system_get_datetime_response = 36; - .PB_System.SetDateTimeRequest system_set_datetime_request = 37; - .PB_System.PlayAudiovisualAlertRequest system_play_audiovisual_alert_request = 38; - .PB_System.ProtobufVersionRequest system_protobuf_version_request = 39; - .PB_System.ProtobufVersionResponse system_protobuf_version_response = 40; - .PB_System.UpdateRequest system_update_request = 41; - .PB_System.UpdateResponse system_update_response = 46; - .PB_System.PowerInfoRequest system_power_info_request = 44; - .PB_System.PowerInfoResponse system_power_info_response = 45; - .PB_Storage.InfoRequest storage_info_request = 28; - .PB_Storage.InfoResponse storage_info_response = 29; - .PB_Storage.TimestampRequest storage_timestamp_request = 59; - .PB_Storage.TimestampResponse storage_timestamp_response = 60; - .PB_Storage.StatRequest storage_stat_request = 24; - .PB_Storage.StatResponse storage_stat_response = 25; - .PB_Storage.ListRequest storage_list_request = 7; - .PB_Storage.ListResponse storage_list_response = 8; - .PB_Storage.ReadRequest storage_read_request = 9; - .PB_Storage.ReadResponse storage_read_response = 10; - .PB_Storage.WriteRequest storage_write_request = 11; - .PB_Storage.DeleteRequest storage_delete_request = 12; - .PB_Storage.MkdirRequest storage_mkdir_request = 13; - .PB_Storage.Md5sumRequest storage_md5sum_request = 14; - .PB_Storage.Md5sumResponse storage_md5sum_response = 15; - .PB_Storage.RenameRequest storage_rename_request = 30; - .PB_Storage.BackupCreateRequest storage_backup_create_request = 42; - .PB_Storage.BackupRestoreRequest storage_backup_restore_request = 43; - .PB_Storage.TarExtractRequest storage_tar_extract_request = 71; - .PB_App.StartRequest app_start_request = 16; - .PB_App.LockStatusRequest app_lock_status_request = 17; - .PB_App.LockStatusResponse app_lock_status_response = 18; - .PB_App.AppExitRequest app_exit_request = 47; - .PB_App.AppLoadFileRequest app_load_file_request = 48; - .PB_App.AppButtonPressRequest app_button_press_request = 49; - .PB_App.AppButtonReleaseRequest app_button_release_request = 50; - .PB_App.AppButtonPressReleaseRequest app_button_press_release_request = 75; - .PB_App.GetErrorRequest app_get_error_request = 63; - .PB_App.GetErrorResponse app_get_error_response = 64; - .PB_App.DataExchangeRequest app_data_exchange_request = 65; - .PB_Gui.StartScreenStreamRequest gui_start_screen_stream_request = 20; - .PB_Gui.StopScreenStreamRequest gui_stop_screen_stream_request = 21; - .PB_Gui.ScreenFrame gui_screen_frame = 22; - .PB_Gui.SendInputEventRequest gui_send_input_event_request = 23; - .PB_Gui.StartVirtualDisplayRequest gui_start_virtual_display_request = 26; - .PB_Gui.StopVirtualDisplayRequest gui_stop_virtual_display_request = 27; - .PB_Gpio.SetPinMode gpio_set_pin_mode = 51; - .PB_Gpio.SetInputPull gpio_set_input_pull = 52; - .PB_Gpio.GetPinMode gpio_get_pin_mode = 53; - .PB_Gpio.GetPinModeResponse gpio_get_pin_mode_response = 54; - .PB_Gpio.ReadPin gpio_read_pin = 55; - .PB_Gpio.ReadPinResponse gpio_read_pin_response = 56; - .PB_Gpio.WritePin gpio_write_pin = 57; - .PB_Gpio.GetOtgMode gpio_get_otg_mode = 72; - .PB_Gpio.GetOtgModeResponse gpio_get_otg_mode_response = 73; - .PB_Gpio.SetOtgMode gpio_set_otg_mode = 74; - .PB_App.AppStateResponse app_state_response = 58; - .PB_Property.GetRequest property_get_request = 61; - .PB_Property.GetResponse property_get_response = 62; - .PB_Desktop.IsLockedRequest desktop_is_locked_request = 66; - .PB_Desktop.UnlockRequest desktop_unlock_request = 67; - .PB_Desktop.StatusSubscribeRequest desktop_status_subscribe_request = 68; - .PB_Desktop.StatusUnsubscribeRequest desktop_status_unsubscribe_request = 69; - .PB_Desktop.Status desktop_status = 70; - } -} - -message Region { - message Band { - uint32 start = 1; - uint32 end = 2; - int32 power_limit = 3; - uint32 duty_cycle = 4; - } - - bytes country_code = 1; - repeated Band bands = 2; -} diff --git a/components/bridge/pbutils/src/main/proto/gpio.options b/components/bridge/pbutils/src/main/proto/gpio.options deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/components/bridge/pbutils/src/main/proto/gpio.proto b/components/bridge/pbutils/src/main/proto/gpio.proto deleted file mode 100644 index 236767be02..0000000000 --- a/components/bridge/pbutils/src/main/proto/gpio.proto +++ /dev/null @@ -1,73 +0,0 @@ -syntax = "proto3"; - -package PB_Gpio; -option java_package = "com.flipperdevices.protobuf.gpio"; - -enum GpioPin { - PC0 = 0; - PC1 = 1; - PC3 = 2; - PB2 = 3; - PB3 = 4; - PA4 = 5; - PA6 = 6; - PA7 = 7; -}; - -enum GpioPinMode { - OUTPUT = 0; - INPUT = 1; -}; - -enum GpioInputPull { - NO = 0; - UP = 1; - DOWN = 2; -}; - -enum GpioOtgMode { - OFF = 0; - ON = 1; -}; - -message SetPinMode { - GpioPin pin = 1; - GpioPinMode mode = 2; -} - -message SetInputPull { - GpioPin pin = 1; - GpioInputPull pull_mode = 2; -} - -message GetPinMode { - GpioPin pin = 1; -} - -message GetPinModeResponse { - GpioPinMode mode = 1; -} - -message ReadPin { - GpioPin pin = 1; -} - -message ReadPinResponse { - uint32 value = 2; -} - -message WritePin { - GpioPin pin = 1; - uint32 value = 2; -} - -message GetOtgMode { -}; - -message GetOtgModeResponse { - GpioOtgMode mode = 1; -}; - -message SetOtgMode { - GpioOtgMode mode = 1; -}; diff --git a/components/bridge/pbutils/src/main/proto/gui.options b/components/bridge/pbutils/src/main/proto/gui.options deleted file mode 100644 index a5efc3e3f9..0000000000 --- a/components/bridge/pbutils/src/main/proto/gui.options +++ /dev/null @@ -1,2 +0,0 @@ -PB_Gui.ScreenFrame.data type:FT_POINTER -PB_Gui.ScreenFrame.data max_size:1024 \ No newline at end of file diff --git a/components/bridge/pbutils/src/main/proto/gui.proto b/components/bridge/pbutils/src/main/proto/gui.proto deleted file mode 100644 index dbd5144fc8..0000000000 --- a/components/bridge/pbutils/src/main/proto/gui.proto +++ /dev/null @@ -1,52 +0,0 @@ -syntax = "proto3"; - -package PB_Gui; -option java_package = "com.flipperdevices.protobuf.screen"; - -enum InputKey { - UP = 0; - DOWN = 1; - RIGHT = 2; - LEFT = 3; - OK = 4; - BACK = 5; -}; - -enum InputType { - PRESS = 0; /**< Press event, emitted after de-bounce */ - RELEASE = 1; /**< Release event, emitted after de-bounce */ - SHORT = 2; /**< Short event, emitted after InputTypeRelease done withing INPUT_LONG_PRESS interval */ - LONG = 3; /**< Long event, emitted after INPUT_LONG_PRESS interval, asynchronous to InputTypeRelease */ - REPEAT = 4; /**< Repeat event, emitted with INPUT_REPEATE_PRESS period after InputTypeLong event */ -} - -enum ScreenOrientation { - HORIZONTAL = 0; /**< Horizontal */ - HORIZONTAL_FLIP = 1; /**< Horizontal flipped (180) */ - VERTICAL = 2; /**< Vertical (90) */ - VERTICAL_FLIP = 3; /**< Vertical flipped (270) */ -} - -message ScreenFrame { - bytes data = 1; - ScreenOrientation orientation = 2; -} - -message StartScreenStreamRequest { -} - -message StopScreenStreamRequest { -} - -message SendInputEventRequest { - InputKey key = 1; - InputType type = 2; -} - -message StartVirtualDisplayRequest { - ScreenFrame first_frame = 1; /**< Optional: screen frame to show */ - bool send_input = 2; /**< Optional: send flipper input */ -} - -message StopVirtualDisplayRequest { -} \ No newline at end of file diff --git a/components/bridge/pbutils/src/main/proto/property.options b/components/bridge/pbutils/src/main/proto/property.options deleted file mode 100644 index 3cfad35b51..0000000000 --- a/components/bridge/pbutils/src/main/proto/property.options +++ /dev/null @@ -1,3 +0,0 @@ -PB_Property.GetRequest.key type:FT_POINTER -PB_Property.GetResponse.key type:FT_POINTER -PB_Property.GetResponse.value type:FT_POINTER diff --git a/components/bridge/pbutils/src/main/proto/property.proto b/components/bridge/pbutils/src/main/proto/property.proto deleted file mode 100644 index 2f164ce048..0000000000 --- a/components/bridge/pbutils/src/main/proto/property.proto +++ /dev/null @@ -1,13 +0,0 @@ -syntax = "proto3"; - -package PB_Property; -option java_package = "com.flipperdevices.protobuf.property"; - -message GetRequest { - string key = 1; -} - -message GetResponse { - string key = 1; - string value = 2; -} diff --git a/components/bridge/pbutils/src/main/proto/storage.options b/components/bridge/pbutils/src/main/proto/storage.options deleted file mode 100644 index 8e99754fdd..0000000000 --- a/components/bridge/pbutils/src/main/proto/storage.options +++ /dev/null @@ -1,26 +0,0 @@ -PB_Storage.File.name type:FT_POINTER -PB_Storage.File.data type:FT_POINTER -PB_Storage.InfoRequest.path type:FT_POINTER -PB_Storage.TimestampRequest.path type:FT_POINTER -PB_Storage.StatRequest.path type:FT_POINTER -PB_Storage.ListRequest.path type:FT_POINTER -PB_Storage.ReadRequest.path type:FT_POINTER -PB_Storage.WriteRequest.path type:FT_POINTER -PB_Storage.DeleteRequest.path type:FT_POINTER -PB_Storage.MkdirRequest.path type:FT_POINTER -PB_Storage.Md5sumRequest.path type:FT_POINTER -PB_Storage.RenameRequest.old_path type:FT_POINTER -PB_Storage.RenameRequest.new_path type:FT_POINTER -PB_Storage.BackupCreateRequest.archive_path type:FT_POINTER -PB_Storage.BackupRestoreRequest.archive_path type:FT_POINTER -PB_Storage.TarExtractRequest.tar_path type:FT_POINTER -PB_Storage.TarExtractRequest.out_path type:FT_POINTER - -PB_Storage.ListResponse.file max_count:8 - -// not affected by nanopb, so server & client should keep in mind these max values -PB_Storage.File.data max_size:512 -PB_Storage.Md5sumResponse.md5sum max_length:32 -PB_Storage.File.md5sum max_length:32 -PB_Storage.*.path max_length:255 - diff --git a/components/bridge/pbutils/src/main/proto/storage.proto b/components/bridge/pbutils/src/main/proto/storage.proto deleted file mode 100644 index eb6269bfb0..0000000000 --- a/components/bridge/pbutils/src/main/proto/storage.proto +++ /dev/null @@ -1,99 +0,0 @@ -syntax = "proto3"; - -package PB_Storage; -option java_package = "com.flipperdevices.protobuf.storage"; - -message File { - enum FileType { - FILE = 0; // default value - DIR = 1; - } - FileType type = 1; - string name = 2; - uint32 size = 3; - bytes data = 4; - string md5sum = 5; -} - -message InfoRequest { - string path = 1; -} - -message InfoResponse { - uint64 total_space = 1; - uint64 free_space = 2; -} - -message TimestampRequest { - string path = 1; -} - -message TimestampResponse { - uint32 timestamp = 1; -} - -message StatRequest { - string path = 1; -} - -message StatResponse { - File file = 1; -} - -message ListRequest { - string path = 1; - bool include_md5 = 2; - uint32 filter_max_size = 3; -} - -message ListResponse { - repeated File file = 1; -} - -message ReadRequest { - string path = 1; -} - -message ReadResponse { - File file = 1; -} - -message WriteRequest { - string path = 1; - File file = 2; -} - -message DeleteRequest { - string path = 1; - bool recursive = 2; -} - -message MkdirRequest { - string path = 1; -} - -message Md5sumRequest { - string path = 1; -} - -message Md5sumResponse { - string md5sum = 1; -} - -message RenameRequest { - string old_path = 1; - string new_path = 2; -} - -message BackupCreateRequest { - string archive_path = 1; -} - -message BackupRestoreRequest { - string archive_path = 1; -} - -message TarExtractRequest { - string tar_path = 1; - string out_path = 2; -} diff --git a/components/bridge/pbutils/src/main/proto/system.options b/components/bridge/pbutils/src/main/proto/system.options deleted file mode 100644 index b400c3eeb2..0000000000 --- a/components/bridge/pbutils/src/main/proto/system.options +++ /dev/null @@ -1,16 +0,0 @@ -PB_System.PingRequest.data type:FT_POINTER -PB_System.PingRequest.data max_size:1024 -PB_System.PingResponse.data type:FT_POINTER -PB_System.PingResponse.data max_size:1024 -PB_System.DeviceInfoResponse.key type:FT_POINTER -PB_System.DeviceInfoResponse.value type:FT_POINTER -PB_System.DateTime.hour int_size:IS_8 -PB_System.DateTime.minute int_size:IS_8 -PB_System.DateTime.second int_size:IS_8 -PB_System.DateTime.day int_size:IS_8 -PB_System.DateTime.month int_size:IS_8 -PB_System.DateTime.year int_size:IS_16 -PB_System.DateTime.weekday int_size:IS_8 -PB_System.UpdateRequest.update_manifest type:FT_POINTER -PB_System.PowerInfoResponse.key type:FT_POINTER -PB_System.PowerInfoResponse.value type:FT_POINTER diff --git a/components/bridge/pbutils/src/main/proto/system.proto b/components/bridge/pbutils/src/main/proto/system.proto deleted file mode 100644 index 90238e8f34..0000000000 --- a/components/bridge/pbutils/src/main/proto/system.proto +++ /dev/null @@ -1,95 +0,0 @@ -syntax = "proto3"; - -package PB_System; -option java_package = "com.flipperdevices.protobuf.system"; - -message PingRequest { - bytes data = 1; -} - -message PingResponse { - bytes data = 1; -} - -message RebootRequest { - enum RebootMode { - OS = 0; // default value - DFU = 1; - UPDATE = 2; - } - RebootMode mode = 1; -} - -message DeviceInfoRequest { -} - -message DeviceInfoResponse { - string key = 1; - string value = 2; -} - -message FactoryResetRequest { -} - -message GetDateTimeRequest { -} - -message GetDateTimeResponse { - DateTime datetime = 1; -} - -message SetDateTimeRequest { - DateTime datetime = 1; -} - -message DateTime { - // Time - uint32 hour = 1; /**< Hour in 24H format: 0-23 */ - uint32 minute = 2; /**< Minute: 0-59 */ - uint32 second = 3; /**< Second: 0-59 */ - // Date - uint32 day = 4; /**< Current day: 1-31 */ - uint32 month = 5; /**< Current month: 1-12 */ - uint32 year = 6; /**< Current year: 2000-2099 */ - uint32 weekday = 7; /**< Current weekday: 1-7 */ -} - -message PlayAudiovisualAlertRequest { -} - -message ProtobufVersionRequest { -} - -message ProtobufVersionResponse { - uint32 major = 1; - uint32 minor = 2; -} - -message UpdateRequest { - string update_manifest = 1; -} - -message UpdateResponse { - enum UpdateResultCode { - OK = 0; - ManifestPathInvalid = 1; - ManifestFolderNotFound = 2; - ManifestInvalid = 3; - StageMissing = 4; - StageIntegrityError = 5; - ManifestPointerError = 6; - TargetMismatch = 7; - OutdatedManifestVersion = 8; - IntFull = 9; - UnspecifiedError = 10; - } - UpdateResultCode code = 1; -} - -message PowerInfoRequest { -} - -message PowerInfoResponse { - string key = 1; - string value = 2; -} From 4ec575af77ade51b0b40c6a257879532708b0ce7 Mon Sep 17 00:00:00 2001 From: Nikita Kulikov Date: Fri, 24 Jan 2025 21:54:12 +0000 Subject: [PATCH 06/10] Add submodule back --- components/bridge/pbutils/src/main/proto | 1 + 1 file changed, 1 insertion(+) create mode 160000 components/bridge/pbutils/src/main/proto diff --git a/components/bridge/pbutils/src/main/proto b/components/bridge/pbutils/src/main/proto new file mode 160000 index 0000000000..ee5b6a22fd --- /dev/null +++ b/components/bridge/pbutils/src/main/proto @@ -0,0 +1 @@ +Subproject commit ee5b6a22fd6aaf9075a2b7bd373309592e6627c5 From 76f431a214a791c7ccc17b24b4be28f0b5054544 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Wed, 8 Jul 2026 17:15:02 +0300 Subject: [PATCH 07/10] Fix gradle wrapper --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index be2dc79a8a..70fe02a536 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 46f292a0d74527ed52d33736fafa70bdab73ff4c Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Thu, 9 Jul 2026 08:55:49 +0300 Subject: [PATCH 08/10] Fix build --- .../search/BLESearchConnectionDelegate.kt | 21 ++++++++++------ .../screens/search/USBSearchDelegate.kt | 15 ++++++------ .../search/ConnectionSearchViewModel.kt | 4 ++-- .../screens/search/USBSearchDelegate.kt | 24 ++++++++++++------- .../usb/impl/model/USBAndroidDeviceFactory.kt | 7 +++--- .../usb/impl/model/USBDesktopDeviceFactory.kt | 7 +++--- 6 files changed, 47 insertions(+), 31 deletions(-) diff --git a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt index 85a396ae59..6f21b79c35 100644 --- a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt +++ b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/BLESearchConnectionDelegate.kt @@ -7,7 +7,11 @@ import com.flipperdevices.bridge.connection.config.api.FDevicePersistedStorage import com.flipperdevices.bridge.connection.config.api.model.FDeviceFlipperZeroBleModel import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.preference.pb.FlipperZeroBle -import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.binding import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -23,13 +27,10 @@ import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanMode import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerSettings import no.nordicsemi.android.kotlin.ble.scanner.BleScanner import no.nordicsemi.android.kotlin.ble.scanner.aggregator.BleScanResultAggregator -import dev.zacsweers.metro.Inject -import dev.zacsweers.metro.binding @SuppressLint("MissingPermission") -@ContributesBinding(AppGraph::class, binding()) -class BLESearchViewModel @Inject constructor( - @Assisted viewModelScope: CoroutineScope, +class BLESearchConnectionDelegate @AssistedInject constructor( + @Assisted scope: CoroutineScope, context: Context, persistedStorage: FDevicePersistedStorage ) : ConnectionSearchDelegate { @@ -61,10 +62,16 @@ class BLESearchViewModel @Inject constructor( ) } }.onEach { devicesFlow.emit(it.toPersistentList()) } - .launchIn(viewModelScope) + .launchIn(scope) } override fun getDevicesFlow() = devicesFlow.asStateFlow() + + @AssistedFactory + @ContributesIntoSet(AppGraph::class, binding()) + fun interface Factory : ConnectionSearchDelegate.Factory { + override fun invoke(scope: CoroutineScope): BLESearchConnectionDelegate + } } private fun ServerDevice.toFDeviceFlipperZeroBleModel() = FDeviceFlipperZeroBleModel( diff --git a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt index ebabe1dc94..9a1d2f1511 100644 --- a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt +++ b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt @@ -9,10 +9,11 @@ import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.info import com.hoho.android.usbserial.driver.UsbSerialProber -import com.squareup.anvil.annotations.ContributesMultibinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.binding import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -27,7 +28,7 @@ import kotlin.time.Duration.Companion.seconds private val FLIPPER_NAME_REGEXP = "Flipper ([A-Za-z]+)".toRegex() class USBSearchDelegate @AssistedInject constructor( - @Assisted viewModelScope: CoroutineScope, + @Assisted scope: CoroutineScope, private val context: Context, private val persistedStorage: FDevicePersistedStorage ) : ConnectionSearchDelegate, LogTagProvider { @@ -39,7 +40,7 @@ class USBSearchDelegate @AssistedInject constructor( MutableStateFlow>(persistentListOf()) init { - viewModelScope.launch { + scope.launch { combine( flow { while (true) { @@ -79,7 +80,7 @@ class USBSearchDelegate @AssistedInject constructor( override fun getDevicesFlow() = searchItems.asStateFlow() @AssistedFactory - @ContributesMultibinding(AppGraph::class, ConnectionSearchDelegate.Factory::class) + @ContributesIntoSet(AppGraph::class, binding()) fun interface Factory : ConnectionSearchDelegate.Factory { override fun invoke(scope: CoroutineScope): USBSearchDelegate } diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt index 08424dce04..a990a0fcc4 100644 --- a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt @@ -7,12 +7,12 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import dev.zacsweers.metro.Inject import kotlinx.coroutines.launch -import javax.inject.Inject class ConnectionSearchViewModel @Inject constructor( private val persistedStorage: FDevicePersistedStorage, - searchDelegatesFactories: MutableSet + searchDelegatesFactories: Set ) : DecomposeViewModel() { private val searchDelegates = searchDelegatesFactories.map { it(viewModelScope) } private val combinedFlow = combine(searchDelegates.map { it.getDevicesFlow() }) { flows -> diff --git a/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt b/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt index 0faa0d8106..2de53eb7e8 100644 --- a/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt +++ b/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt @@ -6,28 +6,28 @@ import com.flipperdevices.bridge.connection.config.api.model.FDeviceFlipperZeroU import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.info -import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.binding import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch -import kotlin.time.Duration.Companion.seconds -import dev.zacsweers.metro.Inject -import dev.zacsweers.metro.Assisted import kotlin.time.DurationUnit import kotlin.time.toDuration -import dev.zacsweers.metro.binding private val FLIPPER_NAME_REGEXP = "Flipper ([A-Za-z]+)".toRegex() -@ContributesBinding(AppGraph::class, binding()) -class USBSearchDelegate @Inject constructor( - @Assisted viewModelScope: CoroutineScope, +class USBSearchDelegate @AssistedInject constructor( + @Assisted scope: CoroutineScope, private val persistedStorage: FDevicePersistedStorage ) : ConnectionSearchDelegate, LogTagProvider { override val TAG = "USBSearchViewModel" @@ -36,7 +36,7 @@ class USBSearchDelegate @Inject constructor( MutableStateFlow>(persistentListOf()) init { - viewModelScope.launch { + scope.launch { combine( flow { while (true) { @@ -74,6 +74,12 @@ class USBSearchDelegate @Inject constructor( } override fun getDevicesFlow() = searchItems.asStateFlow() + + @AssistedFactory + @ContributesIntoSet(AppGraph::class, binding()) + fun interface Factory : ConnectionSearchDelegate.Factory { + override fun invoke(scope: CoroutineScope): USBSearchDelegate + } } private fun SerialPort.toFDeviceFlipperZeroUSBModel(): FDeviceFlipperZeroUsbModel { diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt index e21a0324ce..29b82acc21 100644 --- a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt @@ -5,11 +5,12 @@ import android.hardware.usb.UsbManager import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig import com.flipperdevices.core.di.AppGraph import com.hoho.android.usbserial.driver.UsbSerialProber -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.binding import kotlinx.coroutines.CoroutineScope -import javax.inject.Inject -@ContributesBinding(AppGraph::class, USBPlatformDeviceFactory::class) +@ContributesBinding(AppGraph::class, binding()) class USBAndroidDeviceFactory @Inject constructor( private val context: Context ) : USBPlatformDeviceFactory { diff --git a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt index 8526a35e54..df7fa0db79 100644 --- a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt +++ b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt @@ -3,11 +3,12 @@ package com.flipperdevices.bridge.connection.transport.usb.impl.model import com.fazecast.jSerialComm.SerialPort import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig import com.flipperdevices.core.di.AppGraph -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.binding import kotlinx.coroutines.CoroutineScope -import javax.inject.Inject -@ContributesBinding(AppGraph::class, USBPlatformDeviceFactory::class) +@ContributesBinding(AppGraph::class, binding()) class USBDesktopDeviceFactory @Inject constructor() : USBPlatformDeviceFactory { override fun getUSBPlatformDevice( config: FUSBDeviceConnectionConfig, From 4aa82af70def3b599843249341adb695e60b6c26 Mon Sep 17 00:00:00 2001 From: Roman Makeev <57789105+makeevrserg@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:10:08 +0300 Subject: [PATCH 09/10] USB Fixes (#1089) **Background** Hotfixes for usb-for-mobile --- .../connection/live-tests/build.gradle.kts | 25 +++ .../connection/livetests/LiveTestsSuite.kt | 24 +++ .../connection/livetests/RootLiveTest.kt | 38 +++++ .../connection/livetests/RpcLiveTests.kt | 19 +++ .../connection/livetests/StorageLiveTests.kt | 16 ++ .../connection/livetests/api/LiveTest.kt | 17 ++ .../livetests/model/LiveTestError.kt | 11 ++ .../livetests/model/LiveTestReport.kt | 9 ++ .../livetests/model/LiveTestStatus.kt | 6 + .../livetests/runner/LiveTestReporter.kt | 10 ++ .../livetests/runner/LiveTestSuiteRunner.kt | 42 +++++ .../livetests/runner/LogLiveTestReporter.kt | 38 +++++ .../livetests/tests/DeviceInfoLiveTest.kt | 31 ++++ .../livetests/tests/FileTransferLiveTest.kt | 29 ++++ .../tests/LargeFileTransferLiveTest.kt | 29 ++++ .../livetests/tests/PingLiveTest.kt | 42 +++++ .../livetests/tests/PingStressLiveTest.kt | 54 +++++++ .../livetests/tests/ReconnectLiveTest.kt | 76 +++++++++ .../livetests/tests/SoundLiveTest.kt | 24 +++ .../livetests/tests/StorageTransferChecks.kt | 47 ++++++ .../sample/android/build.gradle.kts | 1 + .../android/src/main/AndroidManifest.xml | 4 +- .../connection/ConnectionTestApplication.kt | 18 ++- .../flipperdevices/bridge/connection/Main.kt | 5 + .../connection/sample/shared/build.gradle.kts | 2 + .../bridge/connection/di/AppComponent.kt | 3 + .../screens/search/USBSearchDelegate.kt | 52 ++++--- .../search/ConnectionSearchViewModel.kt | 8 +- .../bridge/connection/di/AppComponent.kt | 2 + .../screens/search/USBSearchDelegate.kt | 5 +- .../transport/usb/impl/build.gradle.kts | 4 +- .../usb/impl/model/USBAndroidDevice.kt | 147 +++++++++++------- .../usb/impl/model/USBAndroidDeviceFactory.kt | 34 ++-- .../usb/impl/model/USBPermissionReceiver.kt | 13 ++ .../usb/impl/model/USBPermissionRequester.kt | 87 +++++++++++ .../usb/impl/model/USBSerialListener.kt | 65 +------- .../usb/impl/model/USBDesktopDevice.kt | 128 ++++++++++++--- .../usb/impl/model/USBDesktopDeviceFactory.kt | 19 ++- .../usb/impl/USBDeviceConnectionApiImpl.kt | 144 +++++++++-------- ...Module.kt => USBDeviceConnectionModule.kt} | 6 +- .../usb/impl/handshake/ByteSequenceMatcher.kt | 56 +++++++ .../usb/impl/handshake/FlipperRpcHandshake.kt | 58 +++++++ .../usb/impl/model/USBPlatformDevice.kt | 38 +++-- .../impl/model/USBPlatformDeviceFactory.kt | 2 +- .../usb/impl/model/USBReceiveBuffer.kt | 38 +++++ .../usb/impl/model/USBSerialParity.kt | 9 ++ .../usb/impl/model/USBSerialPortParams.kt | 8 + .../usb/impl/model/USBSerialStopBits.kt | 7 + .../usb/impl/model/USBTransportException.kt | 29 ++++ .../usb/impl/serial/FUSBSerialDeviceApi.kt | 61 +++++--- .../app_package/api/FeatureEntryImpl.kt.ftl | 4 +- settings.gradle.kts | 1 + 52 files changed, 1349 insertions(+), 296 deletions(-) create mode 100644 components/bridge/connection/live-tests/build.gradle.kts create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/LiveTestsSuite.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/RootLiveTest.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/RpcLiveTests.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/StorageLiveTests.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/api/LiveTest.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestError.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestReport.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestStatus.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LiveTestReporter.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LiveTestSuiteRunner.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LogLiveTestReporter.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/DeviceInfoLiveTest.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/FileTransferLiveTest.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/LargeFileTransferLiveTest.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/PingLiveTest.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/PingStressLiveTest.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/ReconnectLiveTest.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/SoundLiveTest.kt create mode 100644 components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/StorageTransferChecks.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPermissionReceiver.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPermissionRequester.kt rename components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/{BleDeviceConnectionModule.kt => USBDeviceConnectionModule.kt} (82%) create mode 100644 components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/handshake/ByteSequenceMatcher.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/handshake/FlipperRpcHandshake.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBReceiveBuffer.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialParity.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialPortParams.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialStopBits.kt create mode 100644 components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBTransportException.kt diff --git a/components/bridge/connection/live-tests/build.gradle.kts b/components/bridge/connection/live-tests/build.gradle.kts new file mode 100644 index 0000000000..6d05354f31 --- /dev/null +++ b/components/bridge/connection/live-tests/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("flipper.multiplatform") + id("flipper.multiplatform-dependencies") + id("flipper.anvil") +} + +android.namespace = "com.flipperdevices.bridge.connection.livetests" + +commonDependencies { + implementation(projects.components.core.log) + + implementation(projects.components.bridge.connection.pbutils) + implementation(projects.components.bridge.connection.config.api) + implementation(projects.components.bridge.connection.orchestrator.api) + implementation(projects.components.bridge.connection.feature.common.api) + implementation(projects.components.bridge.connection.feature.provider.api) + implementation(projects.components.bridge.connection.feature.rpc.api) + implementation(projects.components.bridge.connection.feature.rpc.model) + implementation(projects.components.bridge.connection.feature.getinfo.api) + implementation(projects.components.bridge.connection.feature.storage.api) + implementation(projects.components.bridge.connection.feature.alarm.api) + + implementation(libs.kotlin.coroutines) + implementation(libs.okio) +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/LiveTestsSuite.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/LiveTestsSuite.kt new file mode 100644 index 0000000000..7cbf673b63 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/LiveTestsSuite.kt @@ -0,0 +1,24 @@ +package com.flipperdevices.bridge.connection.livetests + +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.tests.ReconnectLiveTest +import com.flipperdevices.bridge.connection.livetests.tests.SoundLiveTest +import dev.zacsweers.metro.Inject + +/** + * Defines which live tests run and in which order. + * + * The reconnect test goes last because it churns the connection + * the other tests rely on. + */ +class LiveTestsSuite @Inject constructor( + rpcLiveTests: RpcLiveTests, + storageLiveTests: StorageLiveTests, + soundLiveTest: SoundLiveTest, + reconnectLiveTest: ReconnectLiveTest +) { + val orderedTests: List = + rpcLiveTests.orderedTests + + storageLiveTests.orderedTests + + listOf(soundLiveTest, reconnectLiveTest) +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/RootLiveTest.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/RootLiveTest.kt new file mode 100644 index 0000000000..b90e742091 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/RootLiveTest.kt @@ -0,0 +1,38 @@ +package com.flipperdevices.bridge.connection.livetests + +import com.flipperdevices.bridge.connection.livetests.model.LiveTestReport +import com.flipperdevices.bridge.connection.livetests.runner.LiveTestSuiteRunner +import com.flipperdevices.bridge.connection.livetests.runner.LogLiveTestReporter +import com.flipperdevices.bridge.connection.orchestrator.api.FDeviceOrchestrator +import com.flipperdevices.bridge.connection.orchestrator.api.model.FDeviceConnectStatus +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.info +import kotlinx.coroutines.flow.first +import dev.zacsweers.metro.Inject +import kotlin.time.TimeSource + +/** + * Entry point of the live test suite. + * + * Waits until a Flipper Zero is connected via the orchestrator, + * then runs every live test sequentially and reports results to logs. + */ +class RootLiveTest @Inject constructor( + private val orchestrator: FDeviceOrchestrator, + liveTestsSuite: LiveTestsSuite +) : LogTagProvider { + override val TAG = "RootLiveTest" + + private val suiteRunner = LiveTestSuiteRunner( + tests = liveTestsSuite.orderedTests, + reporter = LogLiveTestReporter(), + timeSource = TimeSource.Monotonic + ) + + suspend fun awaitDeviceAndRunAll(): List { + info { "Waiting for a connected Flipper to start live tests" } + orchestrator.getState().first { status -> status is FDeviceConnectStatus.Connected } + info { "Device connected, starting live tests" } + return suiteRunner.runAll() + } +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/RpcLiveTests.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/RpcLiveTests.kt new file mode 100644 index 0000000000..d90d363c05 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/RpcLiveTests.kt @@ -0,0 +1,19 @@ +package com.flipperdevices.bridge.connection.livetests + +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.tests.DeviceInfoLiveTest +import com.flipperdevices.bridge.connection.livetests.tests.PingLiveTest +import com.flipperdevices.bridge.connection.livetests.tests.PingStressLiveTest +import dev.zacsweers.metro.Inject + +class RpcLiveTests @Inject constructor( + pingLiveTest: PingLiveTest, + deviceInfoLiveTest: DeviceInfoLiveTest, + pingStressLiveTest: PingStressLiveTest +) { + val orderedTests: List = listOf( + pingLiveTest, + deviceInfoLiveTest, + pingStressLiveTest + ) +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/StorageLiveTests.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/StorageLiveTests.kt new file mode 100644 index 0000000000..e3003eeb35 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/StorageLiveTests.kt @@ -0,0 +1,16 @@ +package com.flipperdevices.bridge.connection.livetests + +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.tests.FileTransferLiveTest +import com.flipperdevices.bridge.connection.livetests.tests.LargeFileTransferLiveTest +import dev.zacsweers.metro.Inject + +class StorageLiveTests @Inject constructor( + fileTransferLiveTest: FileTransferLiveTest, + largeFileTransferLiveTest: LargeFileTransferLiveTest +) { + val orderedTests: List = listOf( + fileTransferLiveTest, + largeFileTransferLiveTest + ) +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/api/LiveTest.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/api/LiveTest.kt new file mode 100644 index 0000000000..6ed4a3041f --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/api/LiveTest.kt @@ -0,0 +1,17 @@ +package com.flipperdevices.bridge.connection.livetests.api + +/** + * Single scenario executed against a real Flipper Zero device. + * + * Implementations must be safe to run sequentially inside one connection + * session and must clean up everything they create on the device. + */ +interface LiveTest { + val name: String + val description: String + + /** + * @return human-readable details on success or the failure reason + */ + suspend fun execute(): Result +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestError.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestError.kt new file mode 100644 index 0000000000..aef27dea69 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestError.kt @@ -0,0 +1,11 @@ +package com.flipperdevices.bridge.connection.livetests.model + +sealed class LiveTestError(message: String) : Exception(message) + +class FeatureNotSupportedError( + featureName: String +) : LiveTestError("Feature $featureName is not supported by the connected device") + +class LiveTestAssertionError( + message: String +) : LiveTestError(message) diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestReport.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestReport.kt new file mode 100644 index 0000000000..e60189d6cc --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestReport.kt @@ -0,0 +1,9 @@ +package com.flipperdevices.bridge.connection.livetests.model + +import kotlin.time.Duration + +data class LiveTestReport( + val testName: String, + val status: LiveTestStatus, + val duration: Duration +) diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestStatus.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestStatus.kt new file mode 100644 index 0000000000..2a5dea452a --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/model/LiveTestStatus.kt @@ -0,0 +1,6 @@ +package com.flipperdevices.bridge.connection.livetests.model + +sealed interface LiveTestStatus { + data class Passed(val details: String) : LiveTestStatus + data class Failed(val error: Throwable) : LiveTestStatus +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LiveTestReporter.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LiveTestReporter.kt new file mode 100644 index 0000000000..d639ccf40d --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LiveTestReporter.kt @@ -0,0 +1,10 @@ +package com.flipperdevices.bridge.connection.livetests.runner + +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.model.LiveTestReport + +interface LiveTestReporter { + fun onTestStarted(test: LiveTest, testNumber: Int, totalCount: Int) + fun onTestFinished(report: LiveTestReport) + fun onSuiteFinished(reports: List) +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LiveTestSuiteRunner.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LiveTestSuiteRunner.kt new file mode 100644 index 0000000000..50f9e297bc --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LiveTestSuiteRunner.kt @@ -0,0 +1,42 @@ +package com.flipperdevices.bridge.connection.livetests.runner + +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.model.LiveTestReport +import com.flipperdevices.bridge.connection.livetests.model.LiveTestStatus +import kotlin.time.TimeSource + +/** + * Executes live tests strictly one after another, so tests never + * compete for the single serial connection to the device. + */ +class LiveTestSuiteRunner( + private val tests: List, + private val reporter: LiveTestReporter, + private val timeSource: TimeSource +) { + private suspend fun runSingleTest(test: LiveTest): LiveTestReport { + val startMark = timeSource.markNow() + val executionResult = runCatching { test.execute() } + .getOrElse { unexpectedError -> Result.failure(unexpectedError) } + val status = executionResult.fold( + onSuccess = { details -> LiveTestStatus.Passed(details) }, + onFailure = { testError -> LiveTestStatus.Failed(testError) } + ) + return LiveTestReport( + testName = test.name, + status = status, + duration = startMark.elapsedNow() + ) + } + + suspend fun runAll(): List { + val reports = tests.mapIndexed { index, test -> + reporter.onTestStarted(test, index + 1, tests.size) + val report = runSingleTest(test) + reporter.onTestFinished(report) + report + } + reporter.onSuiteFinished(reports) + return reports + } +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LogLiveTestReporter.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LogLiveTestReporter.kt new file mode 100644 index 0000000000..2be3f3885f --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/runner/LogLiveTestReporter.kt @@ -0,0 +1,38 @@ +package com.flipperdevices.bridge.connection.livetests.runner + +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.model.LiveTestReport +import com.flipperdevices.bridge.connection.livetests.model.LiveTestStatus +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.log.info +import kotlin.time.Duration + +class LogLiveTestReporter : LiveTestReporter, LogTagProvider { + override val TAG = "LiveTests" + + override fun onTestStarted(test: LiveTest, testNumber: Int, totalCount: Int) { + info { "[$testNumber/$totalCount] ${test.name}: ${test.description}" } + } + + override fun onTestFinished(report: LiveTestReport) { + when (val status = report.status) { + is LiveTestStatus.Passed -> + info { "PASS '${report.testName}' in ${report.duration}: ${status.details}" } + + is LiveTestStatus.Failed -> + error(status.error) { "FAIL '${report.testName}' in ${report.duration}" } + } + } + + override fun onSuiteFinished(reports: List) { + val passedCount = reports.count { report -> report.status is LiveTestStatus.Passed } + val totalDuration = reports.fold(Duration.ZERO) { accumulated, report -> + accumulated + report.duration + } + info { "Live tests finished: $passedCount/${reports.size} passed in $totalDuration" } + reports + .filter { report -> report.status is LiveTestStatus.Failed } + .forEach { report -> info { "Failed test: ${report.testName}" } } + } +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/DeviceInfoLiveTest.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/DeviceInfoLiveTest.kt new file mode 100644 index 0000000000..efdec61db1 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/DeviceInfoLiveTest.kt @@ -0,0 +1,31 @@ +package com.flipperdevices.bridge.connection.livetests.tests + +import com.flipperdevices.bridge.connection.feature.getinfo.api.FGetInfoFeatureApi +import com.flipperdevices.bridge.connection.feature.getinfo.model.FGetInfoApiProperty +import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider +import com.flipperdevices.bridge.connection.feature.provider.api.getSync +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.model.FeatureNotSupportedError +import dev.zacsweers.metro.Inject + +class DeviceInfoLiveTest @Inject constructor( + private val featureProvider: FFeatureProvider +) : LiveTest { + override val name = "Device info" + override val description = "Read device name and firmware information over RPC" + + override suspend fun execute(): Result { + val getInfoApi = featureProvider.getSync() + ?: return Result.failure(FeatureNotSupportedError("GET_INFO")) + return getInfoApi.get(FGetInfoApiProperty.DeviceInfo.DEVICE_NAME) + .mapCatching { deviceName -> + val hardwareVersion = getInfoApi + .get(FGetInfoApiProperty.DeviceInfo.HARDWARE_VERSION) + .getOrThrow() + val firmwareBranch = getInfoApi + .get(FGetInfoApiProperty.DeviceInfo.FIRMWARE_BRANCH) + .getOrThrow() + "Device '$deviceName', hardware v$hardwareVersion, firmware branch '$firmwareBranch'" + } + } +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/FileTransferLiveTest.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/FileTransferLiveTest.kt new file mode 100644 index 0000000000..e7661899e8 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/FileTransferLiveTest.kt @@ -0,0 +1,29 @@ +package com.flipperdevices.bridge.connection.livetests.tests + +import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider +import com.flipperdevices.bridge.connection.feature.provider.api.getSync +import com.flipperdevices.bridge.connection.feature.storage.api.FStorageFeatureApi +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.model.FeatureNotSupportedError +import dev.zacsweers.metro.Inject + +class FileTransferLiveTest @Inject constructor( + private val featureProvider: FFeatureProvider +) : LiveTest { + override val name = "File transfer" + override val description = + "Upload a small random file to the SD card, verify md5, download it back and compare" + + override suspend fun execute(): Result { + val storageApi = featureProvider.getSync() + ?: return Result.failure(FeatureNotSupportedError("STORAGE")) + return runCatching { + storageApi.transferAndVerify(FILE_SIZE_BYTES, FLIPPER_FILE_PATH) + } + } + + companion object { + private const val FILE_SIZE_BYTES = 4096 + private const val FLIPPER_FILE_PATH = "/ext/flipper-live-test.bin" + } +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/LargeFileTransferLiveTest.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/LargeFileTransferLiveTest.kt new file mode 100644 index 0000000000..93b765c5f1 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/LargeFileTransferLiveTest.kt @@ -0,0 +1,29 @@ +package com.flipperdevices.bridge.connection.livetests.tests + +import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider +import com.flipperdevices.bridge.connection.feature.provider.api.getSync +import com.flipperdevices.bridge.connection.feature.storage.api.FStorageFeatureApi +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.model.FeatureNotSupportedError +import dev.zacsweers.metro.Inject + +class LargeFileTransferLiveTest @Inject constructor( + private val featureProvider: FFeatureProvider +) : LiveTest { + override val name = "Large file transfer" + override val description = + "Upload a 64 KiB random file to stress chunked reads/writes, verify md5 and content" + + override suspend fun execute(): Result { + val storageApi = featureProvider.getSync() + ?: return Result.failure(FeatureNotSupportedError("STORAGE")) + return runCatching { + storageApi.transferAndVerify(FILE_SIZE_BYTES, FLIPPER_FILE_PATH) + } + } + + companion object { + private const val FILE_SIZE_BYTES = 65536 + private const val FLIPPER_FILE_PATH = "/ext/flipper-live-test-large.bin" + } +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/PingLiveTest.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/PingLiveTest.kt new file mode 100644 index 0000000000..5491272e6f --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/PingLiveTest.kt @@ -0,0 +1,42 @@ +package com.flipperdevices.bridge.connection.livetests.tests + +import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider +import com.flipperdevices.bridge.connection.feature.provider.api.getSync +import com.flipperdevices.bridge.connection.feature.rpc.api.FRpcFeatureApi +import com.flipperdevices.bridge.connection.feature.rpc.model.wrapToRequest +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.model.FeatureNotSupportedError +import com.flipperdevices.bridge.connection.livetests.model.LiveTestAssertionError +import com.flipperdevices.protobuf.Main +import com.flipperdevices.protobuf.system.PingRequest +import okio.ByteString.Companion.toByteString +import dev.zacsweers.metro.Inject +import kotlin.random.Random + +class PingLiveTest @Inject constructor( + private val featureProvider: FFeatureProvider +) : LiveTest { + override val name = "Ping" + override val description = "Send system ping and verify the payload is echoed back" + + override suspend fun execute(): Result { + val rpcApi = featureProvider.getSync() + ?: return Result.failure(FeatureNotSupportedError("RPC")) + val payload = Random.nextBytes(PING_PAYLOAD_SIZE_BYTES).toByteString() + val pingRequest = Main(system_ping_request = PingRequest(data_ = payload)) + return rpcApi.requestOnce(pingRequest.wrapToRequest()).mapCatching { response -> + val echoedPayload = response.system_ping_response?.data_ + ?: throw LiveTestAssertionError("Response has no ping payload: $response") + if (echoedPayload != payload) { + throw LiveTestAssertionError( + "Ping payload mismatch: sent ${payload.hex()}, received ${echoedPayload.hex()}" + ) + } + "Flipper echoed ${echoedPayload.size} bytes back" + } + } + + companion object { + private const val PING_PAYLOAD_SIZE_BYTES = 16 + } +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/PingStressLiveTest.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/PingStressLiveTest.kt new file mode 100644 index 0000000000..cce533d95b --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/PingStressLiveTest.kt @@ -0,0 +1,54 @@ +package com.flipperdevices.bridge.connection.livetests.tests + +import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider +import com.flipperdevices.bridge.connection.feature.provider.api.getSync +import com.flipperdevices.bridge.connection.feature.rpc.api.FRpcFeatureApi +import com.flipperdevices.bridge.connection.feature.rpc.model.wrapToRequest +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.model.FeatureNotSupportedError +import com.flipperdevices.bridge.connection.livetests.model.LiveTestAssertionError +import com.flipperdevices.protobuf.Main +import com.flipperdevices.protobuf.system.PingRequest +import okio.ByteString.Companion.toByteString +import dev.zacsweers.metro.Inject +import kotlin.random.Random + +class PingStressLiveTest @Inject constructor( + private val featureProvider: FFeatureProvider +) : LiveTest { + override val name = "Ping stress" + override val description = "Send sequential pings with payload sizes from empty to 1 KiB" + + private suspend fun pingWithPayload(rpcApi: FRpcFeatureApi, payloadSize: Int) { + val payload = Random.nextBytes(payloadSize).toByteString() + val response = rpcApi + .requestOnce(Main(system_ping_request = PingRequest(data_ = payload)).wrapToRequest()) + .getOrThrow() + val echoedPayload = response.system_ping_response?.data_ + ?: throw LiveTestAssertionError("No ping payload in response for size $payloadSize") + if (echoedPayload != payload) { + throw LiveTestAssertionError("Ping payload mismatch for size $payloadSize") + } + } + + override suspend fun execute(): Result { + val rpcApi = featureProvider.getSync() + ?: return Result.failure(FeatureNotSupportedError("RPC")) + return runCatching { + var totalPayloadBytes = 0 + for (payloadSize in PAYLOAD_SIZES) { + repeat(ROUNDS_PER_SIZE) { + pingWithPayload(rpcApi, payloadSize) + totalPayloadBytes += payloadSize + } + } + "Completed ${PAYLOAD_SIZES.size * ROUNDS_PER_SIZE} pings, " + + "echoed $totalPayloadBytes payload bytes" + } + } + + companion object { + private val PAYLOAD_SIZES = listOf(0, 1, 16, 256, 1024) + private const val ROUNDS_PER_SIZE = 4 + } +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/ReconnectLiveTest.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/ReconnectLiveTest.kt new file mode 100644 index 0000000000..3543d922b4 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/ReconnectLiveTest.kt @@ -0,0 +1,76 @@ +package com.flipperdevices.bridge.connection.livetests.tests + +import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider +import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureStatus +import com.flipperdevices.bridge.connection.feature.provider.api.get +import com.flipperdevices.bridge.connection.feature.rpc.api.FRpcFeatureApi +import com.flipperdevices.bridge.connection.feature.rpc.model.wrapToRequest +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.model.LiveTestAssertionError +import com.flipperdevices.bridge.connection.orchestrator.api.FDeviceOrchestrator +import com.flipperdevices.bridge.connection.orchestrator.api.model.FDeviceConnectStatus +import com.flipperdevices.protobuf.Main +import com.flipperdevices.protobuf.system.PingRequest +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout +import dev.zacsweers.metro.Inject +import kotlin.time.Duration.Companion.seconds + +class ReconnectLiveTest @Inject constructor( + private val orchestrator: FDeviceOrchestrator, + private val featureProvider: FFeatureProvider +) : LiveTest { + override val name = "Reconnect" + override val description = + "Disconnect and reconnect the device several times, pinging after each cycle" + + private suspend fun awaitStatus(predicate: (FDeviceConnectStatus) -> Boolean) { + withTimeout(STATUS_TIMEOUT) { + orchestrator.getState().first(predicate) + } + } + + private suspend fun awaitRpcFeature(): FRpcFeatureApi { + return withTimeout(STATUS_TIMEOUT) { + featureProvider.get() + .filterIsInstance>() + .first() + .featureApi + } + } + + private suspend fun pingOnce() { + val response = awaitRpcFeature() + .requestOnce(Main(system_ping_request = PingRequest()).wrapToRequest()) + .getOrThrow() + if (response.system_ping_response == null) { + throw LiveTestAssertionError("No ping response after reconnect: $response") + } + } + + override suspend fun execute(): Result { + val startStatus = orchestrator.getState().value + if (startStatus !is FDeviceConnectStatus.Connected) { + return Result.failure( + LiveTestAssertionError("Expected connected device, but status is $startStatus") + ) + } + val device = startStatus.device + return runCatching { + repeat(RECONNECT_CYCLES) { + orchestrator.disconnectCurrent() + awaitStatus { status -> status is FDeviceConnectStatus.Disconnected } + orchestrator.connect(device) + awaitStatus { status -> status is FDeviceConnectStatus.Connected } + pingOnce() + } + "Survived $RECONNECT_CYCLES disconnect/connect cycles with ping after each" + } + } + + companion object { + private const val RECONNECT_CYCLES = 3 + private val STATUS_TIMEOUT = 30.seconds + } +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/SoundLiveTest.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/SoundLiveTest.kt new file mode 100644 index 0000000000..b764c1bad2 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/SoundLiveTest.kt @@ -0,0 +1,24 @@ +package com.flipperdevices.bridge.connection.livetests.tests + +import com.flipperdevices.bridge.connection.feature.alarm.api.FAlarmFeatureApi +import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider +import com.flipperdevices.bridge.connection.feature.provider.api.getSync +import com.flipperdevices.bridge.connection.livetests.api.LiveTest +import com.flipperdevices.bridge.connection.livetests.model.FeatureNotSupportedError +import dev.zacsweers.metro.Inject + +class SoundLiveTest @Inject constructor( + private val featureProvider: FFeatureProvider +) : LiveTest { + override val name = "Sound" + override val description = "Ask the Flipper to play an audiovisual alert" + + override suspend fun execute(): Result { + val alarmApi = featureProvider.getSync() + ?: return Result.failure(FeatureNotSupportedError("ALARM")) + return runCatching { + alarmApi.makeSound() + "Alert command sent, the Flipper should have made a sound" + } + } +} diff --git a/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/StorageTransferChecks.kt b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/StorageTransferChecks.kt new file mode 100644 index 0000000000..ca08dfde22 --- /dev/null +++ b/components/bridge/connection/live-tests/src/commonMain/kotlin/com/flipperdevices/bridge/connection/livetests/tests/StorageTransferChecks.kt @@ -0,0 +1,47 @@ +package com.flipperdevices.bridge.connection.livetests.tests + +import com.flipperdevices.bridge.connection.feature.storage.api.FStorageFeatureApi +import com.flipperdevices.bridge.connection.livetests.model.LiveTestAssertionError +import kotlinx.coroutines.coroutineScope +import okio.ByteString.Companion.toByteString +import okio.buffer +import okio.use +import kotlin.random.Random + +/** + * Uploads [payloadSizeBytes] random bytes to [flipperFilePath], verifies the md5 + * on the device, downloads the file back, compares content and deletes the file. + */ +internal suspend fun FStorageFeatureApi.transferAndVerify( + payloadSizeBytes: Int, + flipperFilePath: String +): String { + val payload = Random.nextBytes(payloadSizeBytes) + uploadApi().sink(flipperFilePath).buffer().use { bufferedSink -> + bufferedSink.write(payload) + } + + val expectedMd5 = payload.toByteString().md5().hex() + val flipperMd5 = md5Api().md5(flipperFilePath).getOrThrow() + if (flipperMd5 != expectedMd5) { + throw LiveTestAssertionError( + "MD5 mismatch after upload: expected $expectedMd5, flipper reports $flipperMd5" + ) + } + + val downloadedPayload = coroutineScope { + downloadApi() + .source(flipperFilePath, this) + .buffer() + .use { bufferedSource -> bufferedSource.readByteArray() } + } + if (!downloadedPayload.contentEquals(payload)) { + throw LiveTestAssertionError( + "Downloaded content differs from uploaded " + + "(${downloadedPayload.size} vs ${payload.size} bytes)" + ) + } + + deleteApi().delete(flipperFilePath).getOrThrow() + return "Transferred and verified ${payload.size} bytes (md5 $expectedMd5)" +} diff --git a/components/bridge/connection/sample/android/build.gradle.kts b/components/bridge/connection/sample/android/build.gradle.kts index d7936f5279..bc776259e9 100644 --- a/components/bridge/connection/sample/android/build.gradle.kts +++ b/components/bridge/connection/sample/android/build.gradle.kts @@ -17,4 +17,5 @@ dependencies { implementation(projects.components.bridge.connection.sample.shared) implementation(projects.components.core.di) implementation(projects.components.core.activityholder) + implementation(projects.components.core.ktx) } diff --git a/components/bridge/connection/sample/android/src/main/AndroidManifest.xml b/components/bridge/connection/sample/android/src/main/AndroidManifest.xml index 8fc9bb4f45..2a0b4d0506 100644 --- a/components/bridge/connection/sample/android/src/main/AndroidManifest.xml +++ b/components/bridge/connection/sample/android/src/main/AndroidManifest.xml @@ -1,3 +1,5 @@ - \ No newline at end of file + + + diff --git a/components/bridge/connection/sample/android/src/main/kotlin/com/flipperdevices/bridge/connection/ConnectionTestApplication.kt b/components/bridge/connection/sample/android/src/main/kotlin/com/flipperdevices/bridge/connection/ConnectionTestApplication.kt index 1a04d134bf..0d82861dab 100644 --- a/components/bridge/connection/sample/android/src/main/kotlin/com/flipperdevices/bridge/connection/ConnectionTestApplication.kt +++ b/components/bridge/connection/sample/android/src/main/kotlin/com/flipperdevices/bridge/connection/ConnectionTestApplication.kt @@ -7,12 +7,16 @@ import dev.zacsweers.metro.createGraphFactory import com.flipperdevices.core.activityholder.CurrentActivityHolder import com.flipperdevices.core.di.ApplicationParams import com.flipperdevices.core.di.ComponentHolder +import com.flipperdevices.core.ktx.jre.FlipperDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import timber.log.Timber class ConnectionTestApplication : Application() { - companion object { - lateinit var appComponent: AppComponent - } + private val applicationScope = CoroutineScope( + SupervisorJob() + FlipperDispatchers.workStealingDispatcher + ) override fun onCreate() { super.onCreate() @@ -31,5 +35,13 @@ class ConnectionTestApplication : Application() { Timber.plant(Timber.DebugTree()) CurrentActivityHolder.register(this) + + applicationScope.launch { + appComponent.rootLiveTest.awaitDeviceAndRunAll() + } + } + + companion object { + lateinit var appComponent: AppComponent } } diff --git a/components/bridge/connection/sample/desktop/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/Main.kt b/components/bridge/connection/sample/desktop/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/Main.kt index fb2a9e30ec..637c0fa161 100644 --- a/components/bridge/connection/sample/desktop/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/Main.kt +++ b/components/bridge/connection/sample/desktop/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/Main.kt @@ -5,6 +5,7 @@ import dev.zacsweers.metro.createGraphFactory import com.flipperdevices.core.ktx.jre.FlipperDispatchers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch fun main() { val applicationScope = CoroutineScope( @@ -16,5 +17,9 @@ fun main() { scope = applicationScope ) + applicationScope.launch { + appComponent.rootLiveTest.awaitDeviceAndRunAll() + } + launch(appComponent) } diff --git a/components/bridge/connection/sample/shared/build.gradle.kts b/components/bridge/connection/sample/shared/build.gradle.kts index 8367e576ba..35f3af0f32 100644 --- a/components/bridge/connection/sample/shared/build.gradle.kts +++ b/components/bridge/connection/sample/shared/build.gradle.kts @@ -96,6 +96,8 @@ commonDependencies { api(projects.components.analytics.metric.api) api(projects.components.analytics.metric.noop) + api(projects.components.bridge.connection.liveTests) + api(libs.kotlin.immutable.collections) implementation(projects.components.core.ui.res) diff --git a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/di/AppComponent.kt b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/di/AppComponent.kt index 28c518e764..28f5ce722b 100644 --- a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/di/AppComponent.kt +++ b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/di/AppComponent.kt @@ -1,7 +1,10 @@ package com.flipperdevices.bridge.connection.di import com.flipperdevices.bridge.connection.ConnectionTestActivity +import com.flipperdevices.bridge.connection.livetests.RootLiveTest interface AppComponent { + val rootLiveTest: RootLiveTest + fun inject(activity: ConnectionTestActivity) } diff --git a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt index 9a1d2f1511..5fc72821e7 100644 --- a/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt +++ b/components/bridge/connection/sample/shared/src/androidMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt @@ -18,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine @@ -26,15 +27,17 @@ import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds private val FLIPPER_NAME_REGEXP = "Flipper ([A-Za-z]+)".toRegex() +private const val FLIPPER_USB_MANUFACTURER = "Flipper Devices Inc." class USBSearchDelegate @AssistedInject constructor( @Assisted scope: CoroutineScope, private val context: Context, private val persistedStorage: FDevicePersistedStorage ) : ConnectionSearchDelegate, LogTagProvider { - override val TAG = "USBSearchViewModel" + override val TAG = "USBSearchDelegate" private val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager + private val usbSerialProber = UsbSerialProber.getDefaultProber() private val searchItems = MutableStateFlow>(persistentListOf()) @@ -45,35 +48,36 @@ class USBSearchDelegate @AssistedInject constructor( flow { while (true) { emit(Unit) - kotlinx.coroutines.delay(1.seconds) + delay(1.seconds) } }, persistedStorage.getAllDevices() - ) { _, savedDevices -> - UsbSerialProber - .getDefaultProber() - .findAllDrivers(usbManager) - .map { it.device } to savedDevices - }.collect { (searchDevices, savedDevices) -> - val existedDescriptors = savedDevices - .filterIsInstance() - .associateBy { it.portPath } + ) { _, savedDevices -> savedDevices } + .collect { savedDevices -> + val existedDescriptors = savedDevices + .filterIsInstance() + .associateBy { savedDevice -> savedDevice.portPath } - info { searchDevices.joinToString(",") { "$it" } } + val searchDevices = usbSerialProber + .findAllDrivers(usbManager) + .map { serialDriver -> serialDriver.device } + .filter { usbDevice -> usbDevice.manufacturerName == FLIPPER_USB_MANUFACTURER } - searchItems.emit( - searchDevices.map { it.toFDeviceFlipperZeroUSBModel() }.map { usbDevice -> - ConnectionSearchItem( - address = usbDevice.portPath, - deviceModel = existedDescriptors[usbDevice.portPath] - ?: usbDevice, - isAdded = existedDescriptors.containsKey(usbDevice.portPath) - ) - }.distinctBy { it.address } - .toImmutableList() + info { searchDevices.joinToString(",") { usbDevice -> "$usbDevice" } } - ) - } + searchItems.emit( + searchDevices.map { usbDevice -> + val usbModel = usbDevice.toFDeviceFlipperZeroUSBModel() + ConnectionSearchItem( + address = usbModel.portPath, + deviceModel = existedDescriptors[usbModel.portPath] + ?: usbModel, + isAdded = existedDescriptors.containsKey(usbModel.portPath) + ) + }.distinctBy { searchItem -> searchItem.address } + .toImmutableList() + ) + } } } diff --git a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt index a990a0fcc4..1e384da166 100644 --- a/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt +++ b/components/bridge/connection/sample/shared/src/commonMain/kotlin/com/flipperdevices/bridge/connection/screens/search/ConnectionSearchViewModel.kt @@ -14,9 +14,11 @@ class ConnectionSearchViewModel @Inject constructor( private val persistedStorage: FDevicePersistedStorage, searchDelegatesFactories: Set ) : DecomposeViewModel() { - private val searchDelegates = searchDelegatesFactories.map { it(viewModelScope) } - private val combinedFlow = combine(searchDelegates.map { it.getDevicesFlow() }) { flows -> - flows.toList().flatten().toImmutableList() + private val searchDelegates = searchDelegatesFactories.map { factory -> factory(viewModelScope) } + private val combinedFlow = combine( + searchDelegates.map { delegate -> delegate.getDevicesFlow() } + ) { deviceLists -> + deviceLists.flatMap { deviceList -> deviceList }.toImmutableList() }.stateIn(viewModelScope, SharingStarted.Lazily, persistentListOf()) fun getDevicesFlow() = combinedFlow diff --git a/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/di/AppComponent.kt b/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/di/AppComponent.kt index 35fb322ac6..34217ca942 100644 --- a/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/di/AppComponent.kt +++ b/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/di/AppComponent.kt @@ -1,5 +1,6 @@ package com.flipperdevices.bridge.connection.di +import com.flipperdevices.bridge.connection.livetests.RootLiveTest import com.flipperdevices.bridge.connection.screens.ConnectionRootDecomposeComponent import com.flipperdevices.core.ui.theme.viewmodel.ThemeViewModel import dev.zacsweers.metro.Provider @@ -7,4 +8,5 @@ import dev.zacsweers.metro.Provider interface AppComponent { val themeViewModelProvider: Provider val rootComponentFactory: ConnectionRootDecomposeComponent.Factory + val rootLiveTest: RootLiveTest } diff --git a/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt b/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt index 2de53eb7e8..0265e8a6bd 100644 --- a/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt +++ b/components/bridge/connection/sample/shared/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/screens/search/USBSearchDelegate.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlin.time.DurationUnit import kotlin.time.toDuration +import kotlin.time.Duration.Companion.seconds private val FLIPPER_NAME_REGEXP = "Flipper ([A-Za-z]+)".toRegex() @@ -30,7 +31,7 @@ class USBSearchDelegate @AssistedInject constructor( @Assisted scope: CoroutineScope, private val persistedStorage: FDevicePersistedStorage ) : ConnectionSearchDelegate, LogTagProvider { - override val TAG = "USBSearchViewModel" + override val TAG = "USBSearchDelegate" private val searchItems = MutableStateFlow>(persistentListOf()) @@ -41,7 +42,7 @@ class USBSearchDelegate @AssistedInject constructor( flow { while (true) { emit(Unit) - delay(1.toDuration(DurationUnit.SECONDS)) + delay(1.seconds) } }, persistedStorage.getAllDevices() diff --git a/components/bridge/connection/transport/usb/impl/build.gradle.kts b/components/bridge/connection/transport/usb/impl/build.gradle.kts index d34753ddff..caa70051af 100644 --- a/components/bridge/connection/transport/usb/impl/build.gradle.kts +++ b/components/bridge/connection/transport/usb/impl/build.gradle.kts @@ -26,8 +26,6 @@ desktopDependencies { } androidDependencies { - implementation(projects.components.core.activityholder) - - implementation(libs.fastutil) + implementation(libs.ktx) implementation(libs.usb.android) } diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt index 887fe0bb90..e141cefd45 100644 --- a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDevice.kt @@ -1,88 +1,123 @@ package com.flipperdevices.bridge.connection.transport.usb.impl.model -import android.app.PendingIntent -import android.content.Intent import android.hardware.usb.UsbManager -import com.flipperdevices.core.activityholder.CurrentActivityHolder import com.flipperdevices.core.log.LogTagProvider -import com.flipperdevices.core.log.error +import com.flipperdevices.core.log.info import com.hoho.android.usbserial.driver.UsbSerialDriver +import com.hoho.android.usbserial.driver.UsbSerialPort import com.hoho.android.usbserial.util.SerialInputOutputManager -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.launch +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import java.io.IOException +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds -private const val RW_TIMEOUT = 0 +private val WRITE_TIMEOUT = 5.seconds +private val DTR_RESET_PULSE = 100.milliseconds class USBAndroidDevice( private val serialDriver: UsbSerialDriver, private val usbManager: UsbManager, - private val scope: CoroutineScope + private val permissionRequester: USBPermissionRequester, + private val receiveBuffer: USBReceiveBuffer, + private val serialListener: USBSerialListener ) : USBPlatformDevice, LogTagProvider { override val TAG = "USBAndroidDevice" private val serialPort = serialDriver.ports.first() - private val serialListener = USBSerialListener(scope) + private val closed = AtomicBoolean(false) + private var ioManager: SerialInputOutputManager? = null - override fun connect(baudRate: Int, dataBits: Int, stopBits: Int, parity: Int): Boolean { - val connection = runCatching { usbManager.openDevice(serialPort.device) } - .onFailure { error(it) { "Fail open connection" } } - .getOrNull() - if (connection == null) { - requestPermission() - error("Connection is null, request permission") - } - - serialPort.open(connection) - serialPort.setParameters(baudRate, dataBits, stopBits, parity) - serialPort.dtr = true - serialPort.rts = true + private fun toAndroidStopBits(stopBits: USBSerialStopBits): Int = when (stopBits) { + USBSerialStopBits.ONE -> UsbSerialPort.STOPBITS_1 + USBSerialStopBits.ONE_POINT_FIVE -> UsbSerialPort.STOPBITS_1_5 + USBSerialStopBits.TWO -> UsbSerialPort.STOPBITS_2 + } - val ioManager = SerialInputOutputManager(serialPort, serialListener) - scope.launch { - try { - awaitCancellation() - } finally { - withContext(NonCancellable) { - ioManager.stop() - } - } - } - ioManager.start() - return true + private fun toAndroidParity(parity: USBSerialParity): Int = when (parity) { + USBSerialParity.NONE -> UsbSerialPort.PARITY_NONE + USBSerialParity.ODD -> UsbSerialPort.PARITY_ODD + USBSerialParity.EVEN -> UsbSerialPort.PARITY_EVEN + USBSerialParity.MARK -> UsbSerialPort.PARITY_MARK + USBSerialParity.SPACE -> UsbSerialPort.PARITY_SPACE } - private fun requestPermission() { - if (!usbManager.hasPermission(serialDriver.device)) { - val intent = Intent("Test") + private fun configureAndStartIo(params: USBSerialPortParams) { + serialPort.setParameters( + params.baudRate, + params.dataBits, + toAndroidStopBits(params.stopBits), + toAndroidParity(params.parity) + ) + serialPort.dtr = false + serialPort.rts = false + val manager = SerialInputOutputManager(serialPort, serialListener) + ioManager = manager + manager.start() + } - val activity = CurrentActivityHolder.getCurrentActivity() - ?: error("Failed get current activity") - intent.setPackage(activity.packageName) - val usbPermissionIntent = - PendingIntent.getBroadcast(activity, 0, intent, PendingIntent.FLAG_MUTABLE) - usbManager.requestPermission(serialDriver.device, usbPermissionIntent) - return + private fun openPortBlocking(params: USBSerialPortParams) { + val connection = usbManager.openDevice(serialDriver.device) + ?: throw USBPortOpenException( + "System denied access to USB device ${serialDriver.device.deviceName}" + ) + serialPort.open(connection) + try { + configureAndStartIo(params) + } catch (configurationError: IOException) { + serialPort.close() + throw configurationError } } - override fun closePort() { - serialPort.close() + /** + * The port is opened with DTR/RTS de-asserted and the lines are raised + * only after [DTR_RESET_PULSE]. The Flipper treats the DTR drop as a + * terminal disconnect and resets its CLI session, so a session left in + * RPC mode by a killed process cannot break the next handshake. + */ + override suspend fun open(params: USBSerialPortParams): Result { + permissionRequester.ensurePermission(serialDriver.device) + .getOrElse { permissionError -> return Result.failure(permissionError) } + return withContext(Dispatchers.IO) { + runCatching { openPortBlocking(params) } + .mapCatching { + delay(DTR_RESET_PULSE) + serialPort.dtr = true + serialPort.rts = true + } + } } - override fun writeBytes(buffer: ByteArray, bytesToWrite: Int, offset: Int): Int { - val subBuffer = if (offset == 0) { - buffer - } else { - buffer.copyOfRange(offset, buffer.size) + override suspend fun write(data: ByteArray): Result = withContext(Dispatchers.IO) { + runCatching { + serialPort.write(data, WRITE_TIMEOUT.inWholeMilliseconds.toInt()) + }.recoverCatching { writeError -> + throw USBPortClosedException("Failed to write ${data.size} bytes", writeError) } - serialPort.write(subBuffer, bytesToWrite, RW_TIMEOUT) - return bytesToWrite } - override fun readBytes(buffer: ByteArray, bytesToRead: Int): Int { - return serialListener.readBytes(buffer, bytesToRead) + override suspend fun read(): Result = receiveBuffer.awaitNextChunk() + + override suspend fun close() { + if (!closed.compareAndSet(false, true)) { + return + } + withContext(NonCancellable + Dispatchers.IO) { + ioManager?.stop() + receiveBuffer.close(cause = null) + // De-assert control lines so the Flipper detects the terminal + // disconnect and resets its CLI session on the next open. + // Desktop's jSerialComm does this implicitly on close. + runCatching { + serialPort.dtr = false + serialPort.rts = false + } + runCatching { serialPort.close() } + .onFailure { closeError -> info { "Port close skipped: $closeError" } } + } } } diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt index 29b82acc21..8f942fe08b 100644 --- a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBAndroidDeviceFactory.kt @@ -17,20 +17,28 @@ class USBAndroidDeviceFactory @Inject constructor( override fun getUSBPlatformDevice( config: FUSBDeviceConnectionConfig, scope: CoroutineScope - ): USBPlatformDevice { - val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager - val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager) - val device = availableDrivers.filter { - it.device.deviceId.toString() == config.path - }.firstOrNull() - if (device == null) { - error("Failed find device with id ${config.path}") + ): Result { + val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager + val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager) + val serialDriver = availableDrivers.firstOrNull { driver -> + driver.device.deviceId.toString() == config.path + } ?: return Result.failure( + USBDeviceNotFoundException("No USB device with id ${config.path}") + ) + if (serialDriver.ports.isEmpty()) { + return Result.failure( + USBDeviceNotFoundException("USB device ${config.path} has no serial ports") + ) } - - return USBAndroidDevice( - serialDriver = device, - usbManager = manager, - scope = scope + val receiveBuffer = USBReceiveBuffer() + return Result.success( + USBAndroidDevice( + serialDriver = serialDriver, + usbManager = usbManager, + permissionRequester = USBPermissionRequester(context, usbManager), + receiveBuffer = receiveBuffer, + serialListener = USBSerialListener(receiveBuffer) + ) ) } } diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPermissionReceiver.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPermissionReceiver.kt new file mode 100644 index 0000000000..1741ec08d8 --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPermissionReceiver.kt @@ -0,0 +1,13 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class USBPermissionReceiver( + private val onPermissionResponse: () -> Unit +) : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + onPermissionResponse() + } +} diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPermissionRequester.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPermissionRequester.kt new file mode 100644 index 0000000000..72aee5a1dc --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPermissionRequester.kt @@ -0,0 +1,87 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +import android.app.PendingIntent +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.content.ContextCompat +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.info +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.time.Duration.Companion.seconds + +private const val ACTION_USB_PERMISSION = "com.flipperdevices.bridge.connection.USB_PERMISSION" +private val PERMISSION_TIMEOUT = 30.seconds + +/** + * Shows the system USB permission dialog and suspends until the user + * responds or [PERMISSION_TIMEOUT] expires. + * + * The response broadcast is only a wake-up signal; [UsbManager.hasPermission] + * is the source of truth, so a malformed broadcast can never produce + * a false "not granted" result. + */ +class USBPermissionRequester( + private val context: Context, + private val usbManager: UsbManager +) : LogTagProvider { + override val TAG = "USBPermissionRequester" + + private fun registerPermissionReceiver(receiver: USBPermissionReceiver) { + ContextCompat.registerReceiver( + context, + receiver, + IntentFilter(ACTION_USB_PERMISSION), + ContextCompat.RECEIVER_NOT_EXPORTED + ) + } + + private fun buildPermissionPendingIntent(): PendingIntent { + val intent = Intent(ACTION_USB_PERMISSION) + intent.setPackage(context.packageName) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } + + private suspend fun awaitPermissionResponse(device: UsbDevice) { + val responseArrived = CompletableDeferred() + val receiver = USBPermissionReceiver { responseArrived.complete(Unit) } + registerPermissionReceiver(receiver) + try { + info { "Requesting USB permission" } + usbManager.requestPermission(device, buildPermissionPendingIntent()) + val response = withTimeoutOrNull(PERMISSION_TIMEOUT) { responseArrived.await() } + if (response == null) { + info { "USB permission request timed out" } + } + } finally { + context.unregisterReceiver(receiver) + } + } + + suspend fun ensurePermission(device: UsbDevice): Result { + if (usbManager.hasPermission(device)) { + return Result.success(Unit) + } + info { "No USB permission, showing system dialog" } + awaitPermissionResponse(device) + return if (usbManager.hasPermission(device)) { + info { "USB permission granted" } + Result.success(Unit) + } else { + info { "USB permission was not granted" } + Result.failure( + USBDeviceAccessDeniedException( + "USB permission was not granted for device ${device.deviceName}" + ) + ) + } + } +} diff --git a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt index db8b93ee3f..6c9be7ac20 100644 --- a/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt +++ b/components/bridge/connection/transport/usb/impl/src/androidMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialListener.kt @@ -2,73 +2,24 @@ package com.flipperdevices.bridge.connection.transport.usb.impl.model import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.error -import com.flipperdevices.core.log.info import com.hoho.android.usbserial.util.SerialInputOutputManager -import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class USBSerialListener( - scope: CoroutineScope + private val receiveBuffer: USBReceiveBuffer ) : SerialInputOutputManager.Listener, LogTagProvider { override val TAG = "USBSerialListener" - private val queue = ByteArrayFIFOQueue() - - init { - scope.launch { - try { - awaitCancellation() - } finally { - withContext(NonCancellable) { - synchronized(queue) { - queue.notifyAll() - } - } - } - } - } - - fun readBytes( - buffer: ByteArray, - bytesToRead: Int - ): Int { - var index = 0 - synchronized(queue) { - while (queue.isEmpty) { - queue.wait() - } - while (index < bytesToRead && !queue.isEmpty) { - buffer[index++] = queue.dequeueByte() - } - } - return index - } - override fun onNewData(data: ByteArray?) { - info { "Receive $data" } - synchronized(queue) { - data?.forEach { - queue.enqueue(it) - } - queue.notifyAll() + if (data == null || data.isEmpty()) { + return } + // SerialInputOutputManager allocates a fresh array per callback, + // so the chunk can be handed over without a defensive copy. + receiveBuffer.append(data) } override fun onRunError(e: Exception?) { - error(e) { "Failed in usb serial" } + error(e) { "USB serial I/O thread failed" } + receiveBuffer.close(e) } } - -@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") -private fun Any.wait() { - (this as Object).wait() -} - -@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") -private fun Any.notifyAll() { - (this as Object).notifyAll() -} diff --git a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt index 6b97f9ea1f..3b11b4d38c 100644 --- a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt +++ b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDevice.kt @@ -1,33 +1,125 @@ package com.flipperdevices.bridge.connection.transport.usb.impl.model import com.fazecast.jSerialComm.SerialPort +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.info +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration.Companion.milliseconds -private const val OPEN_PORT_TIME_MS = 1000 +private const val OPEN_PORT_SAFETY_SLEEP_MS = 1000 +private const val READ_BUFFER_SIZE = 4096 +private val DTR_RESET_PULSE = 100.milliseconds class USBDesktopDevice( - private val serialPort: SerialPort -) : USBPlatformDevice { - override fun connect(baudRate: Int, dataBits: Int, stopBits: Int, parity: Int): Boolean { + private val serialPort: SerialPort, + private val scope: CoroutineScope, + private val receiveBuffer: USBReceiveBuffer +) : USBPlatformDevice, LogTagProvider { + override val TAG = "USBDesktopDevice" + + private val closed = AtomicBoolean(false) + + private fun toDesktopStopBits(stopBits: USBSerialStopBits): Int = when (stopBits) { + USBSerialStopBits.ONE -> SerialPort.ONE_STOP_BIT + USBSerialStopBits.ONE_POINT_FIVE -> SerialPort.ONE_POINT_FIVE_STOP_BITS + USBSerialStopBits.TWO -> SerialPort.TWO_STOP_BITS + } + + private fun toDesktopParity(parity: USBSerialParity): Int = when (parity) { + USBSerialParity.NONE -> SerialPort.NO_PARITY + USBSerialParity.ODD -> SerialPort.ODD_PARITY + USBSerialParity.EVEN -> SerialPort.EVEN_PARITY + USBSerialParity.MARK -> SerialPort.MARK_PARITY + USBSerialParity.SPACE -> SerialPort.SPACE_PARITY + } + + private fun readIntoBufferBlocking() { + val buffer = ByteArray(READ_BUFFER_SIZE) + while (true) { + val readCount = serialPort.readBytes(buffer, buffer.size) + if (readCount < 0) { + receiveBuffer.close( + USBPortClosedException("Serial port read failed with code $readCount") + ) + return + } + if (readCount > 0) { + receiveBuffer.append(buffer.copyOf(readCount)) + } + } + } + + private fun openPortBlocking(params: USBSerialPortParams) { serialPort.setComPortParameters( - baudRate, - dataBits, - SerialPort.ONE_STOP_BIT, - SerialPort.NO_PARITY + params.baudRate, + params.dataBits, + toDesktopStopBits(params.stopBits), + toDesktopParity(params.parity) + ) + if (!serialPort.openPort(OPEN_PORT_SAFETY_SLEEP_MS)) { + throw USBPortOpenException("Failed to open serial port ${serialPort.systemPortName}") + } + serialPort.clearDTR() + serialPort.clearRTS() + // Reads block until at least one byte arrives and writes block until + // the whole buffer is flushed, so `write` is all-or-nothing and the + // read loop never spins on empty results. + serialPort.setComPortTimeouts( + SerialPort.TIMEOUT_READ_SEMI_BLOCKING or SerialPort.TIMEOUT_WRITE_BLOCKING, + 0, + 0 ) - serialPort.openPort(OPEN_PORT_TIME_MS) - serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 0, 0) - return serialPort.openPort() } - override fun closePort() { - serialPort.closePort() - } + /** + * The port is opened with DTR/RTS de-asserted and the lines are raised + * only after [DTR_RESET_PULSE]. The Flipper treats the DTR drop as a + * terminal disconnect and resets its CLI session, so a session left in + * RPC mode by a killed process cannot break the next handshake. + */ + override suspend fun open(params: USBSerialPortParams): Result = + withContext(Dispatchers.IO) { + runCatching { openPortBlocking(params) } + .mapCatching { + delay(DTR_RESET_PULSE) + if (!serialPort.setDTR() || !serialPort.setRTS()) { + throw USBPortOpenException( + "Failed to assert DTR/RTS on ${serialPort.systemPortName}" + ) + } + } + .onSuccess { + scope.launch(Dispatchers.IO) { readIntoBufferBlocking() } + } + } - override fun writeBytes(buffer: ByteArray, bytesToWrite: Int, offset: Int): Int { - return serialPort.writeBytes(buffer, bytesToWrite, offset) + override suspend fun write(data: ByteArray): Result = withContext(Dispatchers.IO) { + runCatching { + val writtenBytes = serialPort.writeBytes(data, data.size) + if (writtenBytes != data.size) { + throw USBPortClosedException( + "Serial port write returned $writtenBytes of ${data.size} bytes" + ) + } + } } - override fun readBytes(buffer: ByteArray, bytesToRead: Int): Int { - return serialPort.readBytes(buffer, bytesToRead) + override suspend fun read(): Result = receiveBuffer.awaitNextChunk() + + override suspend fun close() { + if (!closed.compareAndSet(false, true)) { + return + } + withContext(NonCancellable + Dispatchers.IO) { + info { "Closing serial port ${serialPort.systemPortName}" } + serialPort.closePort() + receiveBuffer.close(cause = null) + } } } diff --git a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt index df7fa0db79..d74781ab11 100644 --- a/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt +++ b/components/bridge/connection/transport/usb/impl/src/desktopMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBDesktopDeviceFactory.kt @@ -13,7 +13,22 @@ class USBDesktopDeviceFactory @Inject constructor() : USBPlatformDeviceFactory { override fun getUSBPlatformDevice( config: FUSBDeviceConnectionConfig, scope: CoroutineScope - ): USBPlatformDevice { - return USBDesktopDevice(SerialPort.getCommPort(config.path)) + ): Result { + val serialPort = runCatching { SerialPort.getCommPort(config.path) } + .getOrElse { portLookupError -> + return Result.failure( + USBDeviceNotFoundException( + "No serial port at path ${config.path}", + portLookupError + ) + ) + } + return Result.success( + USBDesktopDevice( + serialPort = serialPort, + scope = scope, + receiveBuffer = USBReceiveBuffer() + ) + ) } } diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/USBDeviceConnectionApiImpl.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/USBDeviceConnectionApiImpl.kt index 86184b34b3..7ed6aa48e6 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/USBDeviceConnectionApiImpl.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/USBDeviceConnectionApiImpl.kt @@ -6,103 +6,113 @@ import com.flipperdevices.bridge.connection.transport.common.api.FTransportConne import com.flipperdevices.bridge.connection.transport.usb.api.FUSBApi import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig import com.flipperdevices.bridge.connection.transport.usb.api.USBDeviceConnectionApi +import com.flipperdevices.bridge.connection.transport.usb.impl.handshake.FlipperRpcHandshake +import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBHandshakeTimeoutException import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDevice import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDeviceFactory +import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBSerialParity +import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBSerialPortParams +import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBSerialStopBits import com.flipperdevices.bridge.connection.transport.usb.impl.serial.FUSBSerialDeviceApi import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.info import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration.Companion.seconds -private val FLOOD_END_STRING = "\r\n\r\n>: ".toByteArray() -private val COMMAND = "start_rpc_session\r".toByteArray() -private const val BAUD_RATE = 230400 -private const val DATA_BITS = 8 +private val HANDSHAKE_TIMEOUT = 10.seconds +private val SERIAL_PORT_PARAMS = USBSerialPortParams( + baudRate = 230400, + dataBits = 8, + stopBits = USBSerialStopBits.ONE, + parity = USBSerialParity.NONE +) class USBDeviceConnectionApiImpl( private val actionNotifierFactory: FlipperActionNotifier.Factory, - private val usbPlatformDeviceFactory: USBPlatformDeviceFactory + private val usbPlatformDeviceFactory: USBPlatformDeviceFactory, + private val rpcHandshake: FlipperRpcHandshake ) : USBDeviceConnectionApi, LogTagProvider { override val TAG = "USBDeviceConnectionApi" - override suspend fun connect( - scope: CoroutineScope, - config: FUSBDeviceConnectionConfig, - listener: FTransportConnectionStatusListener - ): Result = runCatching { - listener.onStatusUpdate(FInternalTransportConnectionStatus.Connecting) - val serialPort = usbPlatformDeviceFactory.getUSBPlatformDevice(config, scope) - val portOpened = serialPort.connect( - BAUD_RATE, - DATA_BITS, - USBPlatformDevice.ONE_STOP_BIT, - USBPlatformDevice.NO_PARITY - ) - - info { "Read port is: $portOpened" } - - if (!portOpened) { - error("Fail to open port") - } - - info { "Port opened, start reading flood" } - skipFlood(serialPort, FLOOD_END_STRING) - info { "Flood skipped, send start_rpc_session command" } - serialPort.writeBytes(COMMAND, COMMAND.size) - skipFlood(serialPort, "\n".toByteArray()) - info { "Flood skipped. Now we are in RPC mode" } - - val deviceApi = FUSBSerialDeviceApi( - scope = scope, - serialPort = serialPort, - actionNotifier = actionNotifierFactory(scope) - ) - info { "Finish create device API" } - + private fun closeDeviceOnScopeCancellation(scope: CoroutineScope, device: USBPlatformDevice) { scope.launch { try { awaitCancellation() } finally { withContext(NonCancellable) { - info { "Closing port..." } - serialPort.closePort() + info { "Connection scope cancelled, closing port" } + device.close() } } } - listener.onStatusUpdate(FInternalTransportConnectionStatus.Connected(scope, deviceApi)) + } - return@runCatching deviceApi + private suspend fun runHandshakeWithTimeout(device: USBPlatformDevice): Result { + return try { + withTimeout(HANDSHAKE_TIMEOUT) { rpcHandshake.enterRpcMode(device) } + } catch (_: TimeoutCancellationException) { + Result.failure( + USBHandshakeTimeoutException( + "Flipper did not enter RPC mode within $HANDSHAKE_TIMEOUT" + ) + ) + } } - @OptIn(ExperimentalStdlibApi::class) - private fun skipFlood(serialPort: USBPlatformDevice, floodBytes: ByteArray) { - info { "Start wait flood" } - var floodCurrentIndex = 0 - val buffer = ByteArray(size = 1) - while (!Thread.interrupted()) { - if (serialPort.readBytes(buffer, buffer.size) == 0) { - info { "Exit from skipFlood because buffer is empty" } - continue - } + private fun createDeviceApi( + scope: CoroutineScope, + device: USBPlatformDevice, + listener: FTransportConnectionStatusListener, + initialData: ByteArray + ): FUSBSerialDeviceApi = FUSBSerialDeviceApi( + scope = scope, + device = device, + actionNotifier = actionNotifierFactory(scope), + initialData = initialData, + onTransportClosed = { + listener.onStatusUpdate(FInternalTransportConnectionStatus.Disconnected) + } + ) - info { - "#skipFlood Read ${buffer.toHexString()} (${ - buffer.joinToString { - it.toInt().toChar().toString() - } - })" - } - if (floodBytes[floodCurrentIndex] == buffer.first()) { - floodCurrentIndex++ - } else { - floodCurrentIndex = 0 + private suspend fun connectToDevice( + scope: CoroutineScope, + device: USBPlatformDevice, + listener: FTransportConnectionStatusListener + ): Result { + closeDeviceOnScopeCancellation(scope, device) + device.open(SERIAL_PORT_PARAMS).getOrElse { openError -> + device.close() + return Result.failure(openError) + } + return runHandshakeWithTimeout(device) + .map { leftoverBytes -> createDeviceApi(scope, device, listener, leftoverBytes) } + .onSuccess { deviceApi -> + info { "USB transport connected" } + listener.onStatusUpdate( + FInternalTransportConnectionStatus.Connected(scope, deviceApi) + ) } - if (floodCurrentIndex == floodBytes.size) { - return + .onFailure { handshakeError -> + info { "USB connect failed: $handshakeError" } + device.close() } - } + } + + override suspend fun connect( + scope: CoroutineScope, + config: FUSBDeviceConnectionConfig, + listener: FTransportConnectionStatusListener + ): Result { + listener.onStatusUpdate(FInternalTransportConnectionStatus.Connecting) + return usbPlatformDeviceFactory.getUSBPlatformDevice(config, scope).fold( + onSuccess = { device -> connectToDevice(scope, device, listener) }, + onFailure = { deviceLookupError -> Result.failure(deviceLookupError) } + ) } } diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/USBDeviceConnectionModule.kt similarity index 82% rename from components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt rename to components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/USBDeviceConnectionModule.kt index f6e751797f..d2a8c4556b 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/BleDeviceConnectionModule.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/di/USBDeviceConnectionModule.kt @@ -5,6 +5,7 @@ import com.flipperdevices.bridge.connection.transport.common.api.di.DeviceConnec import com.flipperdevices.bridge.connection.transport.common.api.di.toHolder import com.flipperdevices.bridge.connection.transport.usb.api.FUSBDeviceConnectionConfig import com.flipperdevices.bridge.connection.transport.usb.impl.USBDeviceConnectionApiImpl +import com.flipperdevices.bridge.connection.transport.usb.impl.handshake.FlipperRpcHandshake import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDeviceFactory import com.flipperdevices.core.di.AppGraph import dev.zacsweers.metro.ContributesTo @@ -22,7 +23,8 @@ interface BleDeviceConnectionModule { actionNotifierFactory: FlipperActionNotifier.Factory, platformDeviceFactory: USBPlatformDeviceFactory ): DeviceConnectionApiHolder = USBDeviceConnectionApiImpl( - actionNotifierFactory, - platformDeviceFactory + actionNotifierFactory = actionNotifierFactory, + usbPlatformDeviceFactory = platformDeviceFactory, + rpcHandshake = FlipperRpcHandshake() ).toHolder() } diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/handshake/ByteSequenceMatcher.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/handshake/ByteSequenceMatcher.kt new file mode 100644 index 0000000000..31770680eb --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/handshake/ByteSequenceMatcher.kt @@ -0,0 +1,56 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.handshake + +private fun buildPrefixFunction(pattern: ByteArray): IntArray { + val prefixFunction = IntArray(pattern.size) + var matchedLength = 0 + for (index in 1 until pattern.size) { + while (matchedLength > 0 && pattern[index] != pattern[matchedLength]) { + matchedLength = prefixFunction[matchedLength - 1] + } + if (pattern[index] == pattern[matchedLength]) { + matchedLength++ + } + prefixFunction[index] = matchedLength + } + return prefixFunction +} + +/** + * Incremental Knuth-Morris-Pratt matcher for finding a byte sequence in + * a stream consumed chunk by chunk. + * + * Unlike naive matching it correctly handles patterns with repeated + * prefixes (e.g. finding "\r\n\r\n>: " in a stream containing + * "\r\n\r\n\r\n>: ") and matches that span chunk boundaries. + */ +class ByteSequenceMatcher( + private val pattern: ByteArray +) { + private val prefixFunction = buildPrefixFunction(pattern) + private var matchedLength = 0 + + init { + require(pattern.isNotEmpty()) { "Pattern must not be empty" } + } + + /** + * Feeds [chunk] into the matcher. Returns the index right after the + * end of the first match, or -1 if the pattern is not fully matched yet. + */ + fun indexAfterMatch(chunk: ByteArray): Int { + for (index in chunk.indices) { + val currentByte = chunk[index] + while (matchedLength > 0 && pattern[matchedLength] != currentByte) { + matchedLength = prefixFunction[matchedLength - 1] + } + if (pattern[matchedLength] == currentByte) { + matchedLength++ + } + if (matchedLength == pattern.size) { + matchedLength = 0 + return index + 1 + } + } + return -1 + } +} diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/handshake/FlipperRpcHandshake.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/handshake/FlipperRpcHandshake.kt new file mode 100644 index 0000000000..6c4ba0ba87 --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/handshake/FlipperRpcHandshake.kt @@ -0,0 +1,58 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.handshake + +import com.flipperdevices.bridge.connection.transport.usb.impl.model.USBPlatformDevice +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.info + +private val CLI_PROMPT = "\r\n\r\n>: ".toByteArray() +private val START_RPC_SESSION_COMMAND = "start_rpc_session\r".toByteArray() +private val COMMAND_ECHO_TERMINATOR = "\n".toByteArray() + +/** + * Switches a Flipper CLI session into RPC mode: waits for the CLI prompt + * after the welcome banner, sends `start_rpc_session` and waits for the + * command echo to finish. + */ +class FlipperRpcHandshake : LogTagProvider { + override val TAG = "FlipperRpcHandshake" + + /** + * Reads from [device] until [terminator] is found, starting with the + * already-read [initialChunk]. Returns the bytes that arrived in the + * same chunk right after the terminator - they belong to the next + * protocol phase and must not be dropped. + */ + private suspend fun skipUntilTerminator( + device: USBPlatformDevice, + terminator: ByteArray, + initialChunk: ByteArray + ): Result { + val matcher = ByteSequenceMatcher(terminator) + var chunk = initialChunk + while (true) { + if (chunk.isNotEmpty()) { + val indexAfterMatch = matcher.indexAfterMatch(chunk) + if (indexAfterMatch >= 0) { + return Result.success(chunk.copyOfRange(indexAfterMatch, chunk.size)) + } + } + chunk = device.read() + .getOrElse { readError -> return Result.failure(readError) } + } + } + + /** + * On success returns the bytes that arrived right after the handshake + * finished - the first bytes of the RPC stream. + */ + suspend fun enterRpcMode(device: USBPlatformDevice): Result { + info { "Waiting for CLI prompt" } + val afterPrompt = skipUntilTerminator(device, CLI_PROMPT, ByteArray(size = 0)) + .getOrElse { promptError -> return Result.failure(promptError) } + info { "CLI prompt received, requesting RPC session" } + device.write(START_RPC_SESSION_COMMAND) + .getOrElse { writeError -> return Result.failure(writeError) } + return skipUntilTerminator(device, COMMAND_ECHO_TERMINATOR, afterPrompt) + .onSuccess { info { "RPC session started" } } + } +} diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt index 8b2d67fe35..8cf5f8d6c6 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDevice.kt @@ -1,24 +1,28 @@ package com.flipperdevices.bridge.connection.transport.usb.impl.model +/** + * Platform-specific serial port for a single USB connection session. + * + * Instances are single-use: [open] once, then [close] once. + * [close] is idempotent and safe to call concurrently from any state. + */ interface USBPlatformDevice { - fun connect(baudRate: Int, dataBits: Int, stopBits: Int, parity: Int): Boolean - fun closePort() + /** + * Opens the port with [params] and starts collecting incoming bytes + * into an internal buffer consumed via [read]. + */ + suspend fun open(params: USBSerialPortParams): Result - fun writeBytes(buffer: ByteArray, bytesToWrite: Int = buffer.size, offset: Int = 0): Int + /** + * Writes [data] fully or returns a failure. Partial writes never succeed. + */ + suspend fun write(data: ByteArray): Result - fun readBytes(buffer: ByteArray, bytesToRead: Int = buffer.size): Int + /** + * Suspends until the next chunk of incoming bytes is available. + * Returns [USBPortClosedException] failure when the port is closed. + */ + suspend fun read(): Result - companion object { - // Parity Values - const val NO_PARITY: Int = 0 - const val ODD_PARITY: Int = 1 - const val EVEN_PARITY: Int = 2 - const val MARK_PARITY: Int = 3 - const val SPACE_PARITY: Int = 4 - - // Number of Stop Bits - const val ONE_STOP_BIT: Int = 1 - const val ONE_POINT_FIVE_STOP_BITS: Int = 2 - const val TWO_STOP_BITS: Int = 3 - } + suspend fun close() } diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt index e06e7090eb..2f2d83276d 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBPlatformDeviceFactory.kt @@ -7,5 +7,5 @@ interface USBPlatformDeviceFactory { fun getUSBPlatformDevice( config: FUSBDeviceConnectionConfig, scope: CoroutineScope - ): USBPlatformDevice + ): Result } diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBReceiveBuffer.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBReceiveBuffer.kt new file mode 100644 index 0000000000..9ec1c8aa77 --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBReceiveBuffer.kt @@ -0,0 +1,38 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.channels.Channel + +/** + * Buffer between a platform serial reader (callback or blocking thread) + * and coroutine consumers. + * + * The buffer is unbounded: serial throughput is tiny compared to consumer + * speed, so backpressure would only add failure modes without real benefit. + */ +class USBReceiveBuffer { + private val incomingChunks = Channel(Channel.UNLIMITED) + + fun append(chunk: ByteArray) { + incomingChunks.trySend(chunk) + } + + fun close(cause: Throwable?) { + incomingChunks.close(cause) + } + + /** + * Cancellation is rethrown so a cancelled consumer is never confused + * with a closed port. + */ + @Suppress("TooGenericExceptionCaught") + suspend fun awaitNextChunk(): Result { + return try { + Result.success(incomingChunks.receive()) + } catch (cancellation: CancellationException) { + throw cancellation + } catch (portClosed: Exception) { + Result.failure(USBPortClosedException("USB port is closed", portClosed)) + } + } +} diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialParity.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialParity.kt new file mode 100644 index 0000000000..5d02ed248e --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialParity.kt @@ -0,0 +1,9 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +enum class USBSerialParity { + NONE, + ODD, + EVEN, + MARK, + SPACE +} diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialPortParams.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialPortParams.kt new file mode 100644 index 0000000000..3f6ea348e4 --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialPortParams.kt @@ -0,0 +1,8 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +data class USBSerialPortParams( + val baudRate: Int, + val dataBits: Int, + val stopBits: USBSerialStopBits, + val parity: USBSerialParity +) diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialStopBits.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialStopBits.kt new file mode 100644 index 0000000000..0263e825df --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBSerialStopBits.kt @@ -0,0 +1,7 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +enum class USBSerialStopBits { + ONE, + ONE_POINT_FIVE, + TWO +} diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBTransportException.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBTransportException.kt new file mode 100644 index 0000000000..33be071b9a --- /dev/null +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/model/USBTransportException.kt @@ -0,0 +1,29 @@ +package com.flipperdevices.bridge.connection.transport.usb.impl.model + +sealed class USBTransportException( + message: String, + cause: Throwable? = null +) : Exception(message, cause) + +class USBDeviceNotFoundException( + message: String, + cause: Throwable? = null +) : USBTransportException(message, cause) + +class USBDeviceAccessDeniedException( + message: String +) : USBTransportException(message) + +class USBPortOpenException( + message: String, + cause: Throwable? = null +) : USBTransportException(message, cause) + +class USBPortClosedException( + message: String, + cause: Throwable? = null +) : USBTransportException(message, cause) + +class USBHandshakeTimeoutException( + message: String +) : USBTransportException(message) diff --git a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/serial/FUSBSerialDeviceApi.kt b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/serial/FUSBSerialDeviceApi.kt index fe93fbaf16..68cc9ef5a2 100644 --- a/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/serial/FUSBSerialDeviceApi.kt +++ b/components/bridge/connection/transport/usb/impl/src/jvmSharedMain/kotlin/com/flipperdevices/bridge/connection/transport/usb/impl/serial/FUSBSerialDeviceApi.kt @@ -14,13 +14,16 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch class FUSBSerialDeviceApi( private val scope: CoroutineScope, - private val serialPort: USBPlatformDevice, - private val actionNotifier: FlipperActionNotifier + private val device: USBPlatformDevice, + private val actionNotifier: FlipperActionNotifier, + private val initialData: ByteArray, + private val onTransportClosed: () -> Unit ) : FUSBApi, FSerialDeviceApi, FSerialRestartApi by NoopRestartApi(), @@ -34,17 +37,7 @@ class FUSBSerialDeviceApi( private val speedFlowState = MutableStateFlow(FlipperSerialSpeed()) init { - scope.launch { - val buffer = ByteArray(size = 1024) - var result = 1 - while (result > 0) { - result = serialPort.readBytes(buffer, buffer.size) - val readBytes = buffer.take(result).toByteArray() - rxSpeed.onReceiveBytes(result) - receiverByteFlow.emit(readBytes) - } - error("End loop with result $result") - } + scope.launch { readUntilPortClosed() } combine( rxSpeed.getSpeed(), txSpeed.getSpeed() @@ -56,25 +49,43 @@ class FUSBSerialDeviceApi( }.launchIn(scope) } + private suspend fun emitReceivedBytes(chunk: ByteArray) { + rxSpeed.onReceiveBytes(chunk.size) + receiverByteFlow.emit(chunk) + } + + private suspend fun readUntilPortClosed() { + if (initialData.isNotEmpty()) { + // The shared flow drops emissions that happen before the first + // subscriber appears, so leftover handshake bytes must wait for it. + receiverByteFlow.subscriptionCount.first { subscribers -> subscribers > 0 } + emitReceivedBytes(initialData) + } + while (true) { + val chunk = device.read().getOrElse { readError -> + info { "USB port closed, stopping read loop: $readError" } + onTransportClosed() + return + } + emitReceivedBytes(chunk) + } + } + override suspend fun getSpeed() = speedFlowState.asStateFlow() + override suspend fun getReceiveBytesFlow() = receiverByteFlow + override fun getActionNotifier() = actionNotifier override suspend fun sendBytes(data: ByteArray) { - var writtenBytesOffset = 0 - do { - val writtenBytes = - serialPort.writeBytes(data, data.size - writtenBytesOffset, writtenBytesOffset) - info { "Write $writtenBytes" } - txSpeed.onReceiveBytes(writtenBytes) - if (writtenBytes == -1) { - error("Failed to write bytes") - } - writtenBytesOffset += writtenBytes - } while (writtenBytesOffset < data.size) + // FSerialDeviceApi contract has no Result channel, so a write + // failure intentionally propagates as an exception. + device.write(data) + .onSuccess { txSpeed.onReceiveBytes(data.size) } + .getOrThrow() } override suspend fun disconnect() { - serialPort.closePort() + device.close() } } diff --git a/config/geminio/modules_templates/Navigaton Feature Module/root/src/app_package/api/FeatureEntryImpl.kt.ftl b/config/geminio/modules_templates/Navigaton Feature Module/root/src/app_package/api/FeatureEntryImpl.kt.ftl index fee34716f8..a785b07c55 100644 --- a/config/geminio/modules_templates/Navigaton Feature Module/root/src/app_package/api/FeatureEntryImpl.kt.ftl +++ b/config/geminio/modules_templates/Navigaton Feature Module/root/src/app_package/api/FeatureEntryImpl.kt.ftl @@ -12,7 +12,7 @@ import com.flipperdevices.core.ui.navigation.ComposableFeatureEntry import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesMultibinding import tangle.viewmodel.compose.tangleViewModel -import javax.inject.Inject +import dev.zacsweers.metro.Inject @ContributesBinding(AppGraph::class, ${__formattedModuleName}FeatureEntry::class) @ContributesMultibinding(AppGraph::class, ComposableFeatureEntry::class) @@ -38,7 +38,7 @@ import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.ui.navigation.AggregateFeatureEntry import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesMultibinding -import javax.inject.Inject +import dev.zacsweers.metro.Inject import tangle.viewmodel.compose.tangleViewModel @ContributesBinding(AppGraph::class, ${__formattedModuleName}FeatureEntry::class) diff --git a/settings.gradle.kts b/settings.gradle.kts index 97839c23c2..d115d0edb6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,6 +49,7 @@ include( ":components:bridge:connection:sample:shared", ":components:bridge:connection:sample:android", ":components:bridge:connection:sample:desktop", + ":components:bridge:connection:live-tests", ":components:bridge:connection:config:api", ":components:bridge:connection:config:impl", ":components:bridge:connection:transportconfigbuilder:api", From 4d4dc2e29d7b651724f9b4ade8b81cbbfc6f31e8 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Thu, 9 Jul 2026 13:09:39 +0300 Subject: [PATCH 10/10] Remove livetests namespace --- components/bridge/connection/live-tests/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/bridge/connection/live-tests/build.gradle.kts b/components/bridge/connection/live-tests/build.gradle.kts index 6d05354f31..7cb179bd71 100644 --- a/components/bridge/connection/live-tests/build.gradle.kts +++ b/components/bridge/connection/live-tests/build.gradle.kts @@ -4,8 +4,6 @@ plugins { id("flipper.anvil") } -android.namespace = "com.flipperdevices.bridge.connection.livetests" - commonDependencies { implementation(projects.components.core.log)