Skip to content

Commit

Permalink
Single tap infrared (#992)
Browse files Browse the repository at this point in the history
**Background**

When pressing infrared buttons anywhere - the signal will be send not
once. FW added new request which allows one-time infrared signal send

**Changes**

- Change EmulateApi with new one-time send

**Test plan**

- Build firmware from
flipperdevices/flipperzero-firmware#4000
- Open flipper app
- Open some infrared key
- Press key once and see emulating on flipper now shorter
- Repeat this step for Infrared Remote Controls
  • Loading branch information
makeevrserg authored Dec 19, 2024
1 parent c2c5394 commit 796f4b3
Show file tree
Hide file tree
Showing 15 changed files with 125 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Feature] Add count subfolders for new file manager
- [Feature] Add file downloading for new file manager
- [Feature] Add move-to to new file manager
- [Feature] Single tap for infrared remotes
- [Refactor] Move rename and file create to separated modules
- [Refactor] Improve and refactor new FileManager Editor
- [FIX] Migrate url host from metric.flipperdevices.com to metric.flipp.dev
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ object Constants {
majorVersion = 0,
minorVersion = 21
)
val API_SUPPORTED_INFRARED_PRESS_RELEASE = SemVer(
majorVersion = 0,
minorVersion = 25
)
val API_SUPPORTED_GET_REQUEST = API_SUPPORTED_FLIPPER_ERROR
const val LAGS_FLIPPER_DETECT_TIMEOUT_MS = 10 * 1000L // 10 seconds

Expand Down
2 changes: 1 addition & 1 deletion components/bridge/pbutils/src/main/proto
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ private fun KeyScreenState.Ready.toEmulateConfigs(): ImmutableList<EmulateConfig
keyType = FlipperKeyType.INFRARED,
keyPath = this.flipperKey.path,
args = name,
index = index
index = index,
)
}.toImmutableList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ interface EmulateHelper {

suspend fun stopEmulate(
scope: CoroutineScope,
requestApi: FlipperRequestApi
requestApi: FlipperRequestApi,
isPressRelease: Boolean = false
)

suspend fun stopEmulateForce(
requestApi: FlipperRequestApi
requestApi: FlipperRequestApi,
isPressRelease: Boolean = false
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import com.flipperdevices.bridge.dao.api.model.FlipperKeyType
/*
Override equals and hashCode to compare emulate key, because emulate time not important
*/
/**
* @param isPressRelease one-time emulate for infrared-only
*/
@Immutable
data class EmulateConfig(
val keyType: FlipperKeyType,
val keyPath: FlipperFilePath,
val minEmulateTime: Long? = null,
val args: String? = null,
val index: Int? = null
val index: Int? = null,
val isPressRelease: Boolean = false
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ private fun ComposableActiveStateEmulateInternal(

val buttonActiveModifier = Modifier.onScrollHoldPress(
onTap = {
emulateViewModel.onSinglePress(emulateConfig)
emulateViewModel.onSinglePress(emulateConfig.copy(isPressRelease = true))
},
onLongPressStart = {
emulateViewModel.onStartEmulate(emulateConfig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ class EmulateHelperImpl @Inject constructor(

override suspend fun stopEmulate(
scope: CoroutineScope,
requestApi: FlipperRequestApi
requestApi: FlipperRequestApi,
isPressRelease: Boolean
) = withLock(mutex, "schedule_stop") {
if (stopJob != null) {
info { "Return from #stopEmulate because stop already in progress" }
Expand Down Expand Up @@ -111,17 +112,21 @@ class EmulateHelperImpl @Inject constructor(
}

override suspend fun stopEmulateForce(
requestApi: FlipperRequestApi
requestApi: FlipperRequestApi,
isPressRelease: Boolean
) = withLock(mutex, "force_stop") {
if (stopJob != null) {
stopJob?.cancelAndJoin()
stopJob = null
}
stopEmulateInternal(requestApi)
stopEmulateInternal(requestApi, isPressRelease)
}

private suspend fun stopEmulateInternal(requestApi: FlipperRequestApi) {
stopEmulateHelper.onStop(requestApi)
private suspend fun stopEmulateInternal(
requestApi: FlipperRequestApi,
isPressRelease: Boolean = false
) {
stopEmulateHelper.onStop(requestApi, isPressRelease)
currentKeyEmulating.emit(null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.flipperdevices.keyemulate.model.EmulateConfig
import com.flipperdevices.keyemulate.model.FlipperAppError
import com.flipperdevices.protobuf.Flipper
import com.flipperdevices.protobuf.app.Application
import com.flipperdevices.protobuf.app.appButtonPressReleaseRequest
import com.flipperdevices.protobuf.app.appButtonPressRequest
import com.flipperdevices.protobuf.app.appLoadFileRequest
import com.flipperdevices.protobuf.main
Expand All @@ -32,6 +33,7 @@ private const val APP_RETRY_COUNT = 3
private const val APP_RETRY_SLEEP_TIME_MS = 1 * 1000L // 1 second

interface StartEmulateHelper {
@Suppress("LongParameterList")
suspend fun onStart(
scope: CoroutineScope,
serviceApi: FlipperServiceApi,
Expand Down Expand Up @@ -97,13 +99,17 @@ class StartEmulateHelperImpl @Inject constructor(
val indexEmulateSupported =
serviceApi.flipperVersionApi.isSupported(Constants.API_SUPPORTED_INFRARED_EMULATE)

val isPressReleaseSupported =
serviceApi.flipperVersionApi.isSupported(Constants.API_SUPPORTED_INFRARED_PRESS_RELEASE)

info { "Support emulate by index: $indexEmulateSupported" }

return processButtonPress(
config = config,
onResultTime = onResultTime,
serviceApi = serviceApi,
isIndexEmulateSupport = indexEmulateSupported
isIndexEmulateSupport = indexEmulateSupported,
isPressReleaseSupported = isPressReleaseSupported
)
}

Expand All @@ -115,6 +121,7 @@ class StartEmulateHelperImpl @Inject constructor(
private suspend fun processButtonPress(
config: EmulateConfig,
isIndexEmulateSupport: Boolean,
isPressReleaseSupported: Boolean,
onResultTime: (Long) -> Unit,
serviceApi: FlipperServiceApi
): Boolean {
Expand All @@ -125,7 +132,17 @@ class StartEmulateHelperImpl @Inject constructor(
val appButtonPressResponse = serviceApi.requestApi.request(
flowOf(
main {
appButtonPressRequest = getAppButtonPressRequest(config, isIndexEmulateSupport)
if (config.isPressRelease && isPressReleaseSupported) {
appButtonPressReleaseRequest = getAppButtonPressReleaseRequest(
config,
isIndexEmulateSupport
)
} else {
appButtonPressRequest = getAppButtonPressRequest(
config,
isIndexEmulateSupport
)
}
}.wrapToRequest(FlipperRequestPriority.FOREGROUND)
)
)
Expand Down Expand Up @@ -164,6 +181,29 @@ class StartEmulateHelperImpl @Inject constructor(
}
}

private fun getAppButtonPressReleaseRequest(
config: EmulateConfig,
isIndexEmulateSupport: Boolean,
): Application.AppButtonPressReleaseRequest {
return when (config.keyType) {
FlipperKeyType.INFRARED -> if (isIndexEmulateSupport) {
val indexArgs = config.index ?: error("Index args is null")
info { "#getAppButtonPressReleaseRequest by index with $config" }
appButtonPressReleaseRequest {
index = indexArgs
}
} else {
val configArgs = config.args ?: error("Config args is null")
info { "#getAppButtonPressReleaseRequest by args with $config" }
appButtonPressReleaseRequest {
args = configArgs
}
}

else -> error("#getAppButtonPressReleaseRequest Unknown button press request with config $config")
}
}

private fun getAppButtonPressRequest(
config: EmulateConfig,
isIndexEmulateSupport: Boolean,
Expand All @@ -183,6 +223,7 @@ class StartEmulateHelperImpl @Inject constructor(
args = configArgs
}
}

else -> error("Unknown button press request with config $config")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,29 @@ import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject

interface StopEmulateHelper {
suspend fun onStop(requestApi: FlipperRequestApi)
suspend fun onStop(
requestApi: FlipperRequestApi,
isPressRelease: Boolean = false
)
}

@ContributesBinding(AppGraph::class, StopEmulateHelper::class)
class StopEmulateHelperImpl @Inject constructor() : StopEmulateHelper, LogTagProvider {
override val TAG = "StopEmulateHelper"

override suspend fun onStop(requestApi: FlipperRequestApi) {
override suspend fun onStop(requestApi: FlipperRequestApi, isPressRelease: Boolean) {
info { "stopEmulateInternal" }

val appButtonResponse = requestApi.request(
flowOf(
main {
appButtonReleaseRequest = appButtonReleaseRequest { }
}.wrapToRequest(FlipperRequestPriority.FOREGROUND)
if (!isPressRelease) {
val appButtonResponse = requestApi.request(
flowOf(
main {
appButtonReleaseRequest = appButtonReleaseRequest { }
}.wrapToRequest(FlipperRequestPriority.FOREGROUND)
)
)
)
info { "App button stop response: $appButtonResponse" }
info { "App button stop response: $appButtonResponse" }
}

val appExitResponse = requestApi.request(
flowOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.flipperdevices.keyemulate.viewmodel

import android.app.Application
import androidx.datastore.core.DataStore
import com.flipperdevices.bridge.api.utils.Constants
import com.flipperdevices.bridge.dao.api.model.FlipperKeyType
import com.flipperdevices.bridge.service.api.FlipperServiceApi
import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider
Expand Down Expand Up @@ -116,6 +117,7 @@ class InfraredViewModel @Inject constructor(
emulateHelper.stopEmulate(viewModelScope, serviceApi.requestApi)
}

@Suppress("CyclomaticComplexMethod", "LongMethod")
private suspend fun calculateTimeoutAndStartEmulate(
scope: CoroutineScope,
serviceApi: FlipperServiceApi,
Expand All @@ -125,12 +127,13 @@ class InfraredViewModel @Inject constructor(
val requestApi = serviceApi.requestApi
val timeout = config.minEmulateTime
val appStarted: Boolean?

val isPressReleaseSupported =
serviceApi.flipperVersionApi.isSupported(Constants.API_SUPPORTED_INFRARED_PRESS_RELEASE)
try {
appStarted = emulateHelper.startEmulate(
scope,
serviceApi,
config
scope = scope,
serviceApi = serviceApi,
config = config,
)
if (appStarted && timeout != null) {
if (oneTimePress) {
Expand All @@ -150,22 +153,34 @@ class InfraredViewModel @Inject constructor(
}
}
} catch (ignored: AlreadyOpenedAppException) {
emulateHelper.stopEmulateForce(requestApi)
emulateHelper.stopEmulateForce(
requestApi = requestApi,
isPressRelease = isPressReleaseSupported && oneTimePress
)
emulateButtonStateFlow.emit(EmulateButtonState.AppAlreadyOpenDialog)
return false
} catch (ignored: ForbiddenFrequencyException) {
emulateHelper.stopEmulateForce(requestApi)
emulateHelper.stopEmulateForce(
requestApi = requestApi,
isPressRelease = isPressReleaseSupported && oneTimePress
)
emulateButtonStateFlow.emit(EmulateButtonState.ForbiddenFrequencyDialog)
return false
} catch (fatal: Throwable) {
error(fatal) { "Handle fatal exception on emulate infrared" }
emulateHelper.stopEmulateForce(requestApi)
emulateHelper.stopEmulateForce(
requestApi = requestApi,
isPressRelease = isPressReleaseSupported && oneTimePress
)
emulateButtonStateFlow.emit(EmulateButtonState.Inactive())
return false
}
if (!appStarted) {
info { "Failed start emulation" }
emulateHelper.stopEmulateForce(requestApi)
emulateHelper.stopEmulateForce(
requestApi = requestApi,
isPressRelease = isPressReleaseSupported && oneTimePress
)
emulateButtonStateFlow.emit(EmulateButtonState.Inactive())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ interface DispatchSignalApi : InstanceKeeper.Instance {
fun dispatch(
config: EmulateConfig,
identifier: IfrKeyIdentifier,
isOneTime: Boolean = true,
onDispatched: () -> Unit = {}
)

Expand Down
1 change: 1 addition & 0 deletions components/remote-controls/setup/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies {
implementation(projects.components.core.di)
implementation(projects.components.core.ktx)
implementation(projects.components.core.log)
implementation(projects.components.core.data)

implementation(projects.components.core.ui.lifecycle)
implementation(projects.components.core.ui.theme)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ class SetupComponentImpl @AssistedInject constructor(
),
keyType = FlipperKeyType.INFRARED,
args = signalModel.remote.name,
index = 0
index = 0,
isPressRelease = true
)
val keyIdentifier = (loadedState.response.signalResponse?.data as? SingleKeyButtonData)
?.keyIdentifier
Expand Down
Loading

0 comments on commit 796f4b3

Please sign in to comment.