mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-28 08:52:45 +00:00
fix(android): stabilize motion sampling and gate pedometer command
This commit is contained in:
@@ -140,7 +140,8 @@ class NodeRuntime(context: Context) {
|
|||||||
cameraEnabled = { cameraEnabled.value },
|
cameraEnabled = { cameraEnabled.value },
|
||||||
locationMode = { locationMode.value },
|
locationMode = { locationMode.value },
|
||||||
voiceWakeMode = { VoiceWakeMode.Off },
|
voiceWakeMode = { VoiceWakeMode.Off },
|
||||||
motionAvailable = { motionHandler.isAvailable() },
|
motionActivityAvailable = { motionHandler.isActivityAvailable() },
|
||||||
|
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
|
||||||
smsAvailable = { sms.canSendSms() },
|
smsAvailable = { sms.canSendSms() },
|
||||||
hasRecordAudioPermission = { hasRecordAudioPermission() },
|
hasRecordAudioPermission = { hasRecordAudioPermission() },
|
||||||
manualTls = { manualTls.value },
|
manualTls = { manualTls.value },
|
||||||
@@ -165,7 +166,6 @@ class NodeRuntime(context: Context) {
|
|||||||
isForeground = { _isForeground.value },
|
isForeground = { _isForeground.value },
|
||||||
cameraEnabled = { cameraEnabled.value },
|
cameraEnabled = { cameraEnabled.value },
|
||||||
locationEnabled = { locationMode.value != LocationMode.Off },
|
locationEnabled = { locationMode.value != LocationMode.Off },
|
||||||
motionAvailable = { motionHandler.isAvailable() },
|
|
||||||
smsAvailable = { sms.canSendSms() },
|
smsAvailable = { sms.canSendSms() },
|
||||||
debugBuild = { BuildConfig.DEBUG },
|
debugBuild = { BuildConfig.DEBUG },
|
||||||
refreshNodeCanvasCapability = { nodeSession.refreshNodeCanvasCapability() },
|
refreshNodeCanvasCapability = { nodeSession.refreshNodeCanvasCapability() },
|
||||||
@@ -175,6 +175,8 @@ class NodeRuntime(context: Context) {
|
|||||||
_canvasRehydrateErrorText.value = null
|
_canvasRehydrateErrorText.value = null
|
||||||
},
|
},
|
||||||
onCanvasA2uiReset = { _canvasA2uiHydrated.value = false },
|
onCanvasA2uiReset = { _canvasA2uiHydrated.value = false },
|
||||||
|
motionActivityAvailable = { motionHandler.isActivityAvailable() },
|
||||||
|
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GatewayTrustPrompt(
|
data class GatewayTrustPrompt(
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class ConnectionManager(
|
|||||||
private val cameraEnabled: () -> Boolean,
|
private val cameraEnabled: () -> Boolean,
|
||||||
private val locationMode: () -> LocationMode,
|
private val locationMode: () -> LocationMode,
|
||||||
private val voiceWakeMode: () -> VoiceWakeMode,
|
private val voiceWakeMode: () -> VoiceWakeMode,
|
||||||
private val motionAvailable: () -> Boolean,
|
private val motionActivityAvailable: () -> Boolean,
|
||||||
|
private val motionPedometerAvailable: () -> Boolean,
|
||||||
private val smsAvailable: () -> Boolean,
|
private val smsAvailable: () -> Boolean,
|
||||||
private val hasRecordAudioPermission: () -> Boolean,
|
private val hasRecordAudioPermission: () -> Boolean,
|
||||||
private val manualTls: () -> Boolean,
|
private val manualTls: () -> Boolean,
|
||||||
@@ -79,7 +80,8 @@ class ConnectionManager(
|
|||||||
locationEnabled = locationMode() != LocationMode.Off,
|
locationEnabled = locationMode() != LocationMode.Off,
|
||||||
smsAvailable = smsAvailable(),
|
smsAvailable = smsAvailable(),
|
||||||
voiceWakeEnabled = voiceWakeMode() != VoiceWakeMode.Off && hasRecordAudioPermission(),
|
voiceWakeEnabled = voiceWakeMode() != VoiceWakeMode.Off && hasRecordAudioPermission(),
|
||||||
motionAvailable = motionAvailable(),
|
motionActivityAvailable = motionActivityAvailable(),
|
||||||
|
motionPedometerAvailable = motionPedometerAvailable(),
|
||||||
debugBuild = BuildConfig.DEBUG,
|
debugBuild = BuildConfig.DEBUG,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ data class NodeRuntimeFlags(
|
|||||||
val locationEnabled: Boolean,
|
val locationEnabled: Boolean,
|
||||||
val smsAvailable: Boolean,
|
val smsAvailable: Boolean,
|
||||||
val voiceWakeEnabled: Boolean,
|
val voiceWakeEnabled: Boolean,
|
||||||
val motionAvailable: Boolean,
|
val motionActivityAvailable: Boolean,
|
||||||
|
val motionPedometerAvailable: Boolean,
|
||||||
val debugBuild: Boolean,
|
val debugBuild: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +30,8 @@ enum class InvokeCommandAvailability {
|
|||||||
CameraEnabled,
|
CameraEnabled,
|
||||||
LocationEnabled,
|
LocationEnabled,
|
||||||
SmsAvailable,
|
SmsAvailable,
|
||||||
MotionAvailable,
|
MotionActivityAvailable,
|
||||||
|
MotionPedometerAvailable,
|
||||||
DebugBuild,
|
DebugBuild,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,11 +181,11 @@ object InvokeCommandRegistry {
|
|||||||
),
|
),
|
||||||
InvokeCommandSpec(
|
InvokeCommandSpec(
|
||||||
name = OpenClawMotionCommand.Activity.rawValue,
|
name = OpenClawMotionCommand.Activity.rawValue,
|
||||||
availability = InvokeCommandAvailability.MotionAvailable,
|
availability = InvokeCommandAvailability.MotionActivityAvailable,
|
||||||
),
|
),
|
||||||
InvokeCommandSpec(
|
InvokeCommandSpec(
|
||||||
name = OpenClawMotionCommand.Pedometer.rawValue,
|
name = OpenClawMotionCommand.Pedometer.rawValue,
|
||||||
availability = InvokeCommandAvailability.MotionAvailable,
|
availability = InvokeCommandAvailability.MotionPedometerAvailable,
|
||||||
),
|
),
|
||||||
InvokeCommandSpec(
|
InvokeCommandSpec(
|
||||||
name = OpenClawSmsCommand.Send.rawValue,
|
name = OpenClawSmsCommand.Send.rawValue,
|
||||||
@@ -213,7 +215,7 @@ object InvokeCommandRegistry {
|
|||||||
NodeCapabilityAvailability.LocationEnabled -> flags.locationEnabled
|
NodeCapabilityAvailability.LocationEnabled -> flags.locationEnabled
|
||||||
NodeCapabilityAvailability.SmsAvailable -> flags.smsAvailable
|
NodeCapabilityAvailability.SmsAvailable -> flags.smsAvailable
|
||||||
NodeCapabilityAvailability.VoiceWakeEnabled -> flags.voiceWakeEnabled
|
NodeCapabilityAvailability.VoiceWakeEnabled -> flags.voiceWakeEnabled
|
||||||
NodeCapabilityAvailability.MotionAvailable -> flags.motionAvailable
|
NodeCapabilityAvailability.MotionAvailable -> flags.motionActivityAvailable || flags.motionPedometerAvailable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map { it.name }
|
.map { it.name }
|
||||||
@@ -227,7 +229,8 @@ object InvokeCommandRegistry {
|
|||||||
InvokeCommandAvailability.CameraEnabled -> flags.cameraEnabled
|
InvokeCommandAvailability.CameraEnabled -> flags.cameraEnabled
|
||||||
InvokeCommandAvailability.LocationEnabled -> flags.locationEnabled
|
InvokeCommandAvailability.LocationEnabled -> flags.locationEnabled
|
||||||
InvokeCommandAvailability.SmsAvailable -> flags.smsAvailable
|
InvokeCommandAvailability.SmsAvailable -> flags.smsAvailable
|
||||||
InvokeCommandAvailability.MotionAvailable -> flags.motionAvailable
|
InvokeCommandAvailability.MotionActivityAvailable -> flags.motionActivityAvailable
|
||||||
|
InvokeCommandAvailability.MotionPedometerAvailable -> flags.motionPedometerAvailable
|
||||||
InvokeCommandAvailability.DebugBuild -> flags.debugBuild
|
InvokeCommandAvailability.DebugBuild -> flags.debugBuild
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,12 +34,13 @@ class InvokeDispatcher(
|
|||||||
private val isForeground: () -> Boolean,
|
private val isForeground: () -> Boolean,
|
||||||
private val cameraEnabled: () -> Boolean,
|
private val cameraEnabled: () -> Boolean,
|
||||||
private val locationEnabled: () -> Boolean,
|
private val locationEnabled: () -> Boolean,
|
||||||
private val motionAvailable: () -> Boolean,
|
|
||||||
private val smsAvailable: () -> Boolean,
|
private val smsAvailable: () -> Boolean,
|
||||||
private val debugBuild: () -> Boolean,
|
private val debugBuild: () -> Boolean,
|
||||||
private val refreshNodeCanvasCapability: suspend () -> Boolean,
|
private val refreshNodeCanvasCapability: suspend () -> Boolean,
|
||||||
private val onCanvasA2uiPush: () -> Unit,
|
private val onCanvasA2uiPush: () -> Unit,
|
||||||
private val onCanvasA2uiReset: () -> Unit,
|
private val onCanvasA2uiReset: () -> Unit,
|
||||||
|
private val motionActivityAvailable: () -> Boolean,
|
||||||
|
private val motionPedometerAvailable: () -> Boolean,
|
||||||
) {
|
) {
|
||||||
suspend fun handleInvoke(command: String, paramsJson: String?): GatewaySession.InvokeResult {
|
suspend fun handleInvoke(command: String, paramsJson: String?): GatewaySession.InvokeResult {
|
||||||
val spec =
|
val spec =
|
||||||
@@ -241,13 +242,22 @@ class InvokeDispatcher(
|
|||||||
message = "LOCATION_DISABLED: enable Location in Settings",
|
message = "LOCATION_DISABLED: enable Location in Settings",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
InvokeCommandAvailability.MotionAvailable ->
|
InvokeCommandAvailability.MotionActivityAvailable ->
|
||||||
if (motionAvailable()) {
|
if (motionActivityAvailable()) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
GatewaySession.InvokeResult.error(
|
GatewaySession.InvokeResult.error(
|
||||||
code = "MOTION_UNAVAILABLE",
|
code = "MOTION_UNAVAILABLE",
|
||||||
message = "MOTION_UNAVAILABLE: motion sensors not available",
|
message = "MOTION_UNAVAILABLE: accelerometer not available",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
InvokeCommandAvailability.MotionPedometerAvailable ->
|
||||||
|
if (motionPedometerAvailable()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
GatewaySession.InvokeResult.error(
|
||||||
|
code = "PEDOMETER_UNAVAILABLE",
|
||||||
|
message = "PEDOMETER_UNAVAILABLE: step counter not available",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
InvokeCommandAvailability.SmsAvailable ->
|
InvokeCommandAvailability.SmsAvailable ->
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import kotlin.math.abs
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
private const val ACCELEROMETER_SAMPLE_TARGET = 20
|
||||||
|
private const val ACCELEROMETER_SAMPLE_TIMEOUT_MS = 6_000L
|
||||||
|
|
||||||
internal data class MotionActivityRequest(
|
internal data class MotionActivityRequest(
|
||||||
val startISO: String?,
|
val startISO: String?,
|
||||||
val endISO: String?,
|
val endISO: String?,
|
||||||
@@ -57,7 +60,11 @@ internal data class PedometerRecord(
|
|||||||
)
|
)
|
||||||
|
|
||||||
internal interface MotionDataSource {
|
internal interface MotionDataSource {
|
||||||
fun isAvailable(context: Context): Boolean
|
fun isActivityAvailable(context: Context): Boolean
|
||||||
|
|
||||||
|
fun isPedometerAvailable(context: Context): Boolean
|
||||||
|
|
||||||
|
fun isAvailable(context: Context): Boolean = isActivityAvailable(context) || isPedometerAvailable(context)
|
||||||
|
|
||||||
fun hasPermission(context: Context): Boolean
|
fun hasPermission(context: Context): Boolean
|
||||||
|
|
||||||
@@ -67,11 +74,14 @@ internal interface MotionDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private object SystemMotionDataSource : MotionDataSource {
|
private object SystemMotionDataSource : MotionDataSource {
|
||||||
override fun isAvailable(context: Context): Boolean {
|
override fun isActivityAvailable(context: Context): Boolean {
|
||||||
val sensorManager = context.getSystemService(SensorManager::class.java)
|
val sensorManager = context.getSystemService(SensorManager::class.java)
|
||||||
val hasAccelerometer = sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null
|
return sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null
|
||||||
val hasStepCounter = sensorManager?.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) != null
|
}
|
||||||
return hasAccelerometer || hasStepCounter
|
|
||||||
|
override fun isPedometerAvailable(context: Context): Boolean {
|
||||||
|
val sensorManager = context.getSystemService(SensorManager::class.java)
|
||||||
|
return sensorManager?.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) != null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasPermission(context: Context): Boolean {
|
override fun hasPermission(context: Context): Boolean {
|
||||||
@@ -169,7 +179,7 @@ private object SystemMotionDataSource : MotionDataSource {
|
|||||||
sensor: Sensor,
|
sensor: Sensor,
|
||||||
): AccelerometerSample? {
|
): AccelerometerSample? {
|
||||||
val sample =
|
val sample =
|
||||||
withTimeoutOrNull(2200L) {
|
withTimeoutOrNull(ACCELEROMETER_SAMPLE_TIMEOUT_MS) {
|
||||||
suspendCancellableCoroutine<AccelerometerSample?> { cont ->
|
suspendCancellableCoroutine<AccelerometerSample?> { cont ->
|
||||||
var count = 0
|
var count = 0
|
||||||
var sumDelta = 0.0
|
var sumDelta = 0.0
|
||||||
@@ -187,7 +197,7 @@ private object SystemMotionDataSource : MotionDataSource {
|
|||||||
).toDouble()
|
).toDouble()
|
||||||
sumDelta += abs(magnitude - SensorManager.GRAVITY_EARTH.toDouble())
|
sumDelta += abs(magnitude - SensorManager.GRAVITY_EARTH.toDouble())
|
||||||
count += 1
|
count += 1
|
||||||
if (count >= 20 && !resumed) {
|
if (count >= ACCELEROMETER_SAMPLE_TARGET && !resumed) {
|
||||||
resumed = true
|
resumed = true
|
||||||
sensorManager.unregisterListener(this)
|
sensorManager.unregisterListener(this)
|
||||||
cont.resume(
|
cont.resume(
|
||||||
@@ -318,6 +328,10 @@ class MotionHandler private constructor(
|
|||||||
|
|
||||||
fun isAvailable(): Boolean = dataSource.isAvailable(appContext)
|
fun isAvailable(): Boolean = dataSource.isAvailable(appContext)
|
||||||
|
|
||||||
|
fun isActivityAvailable(): Boolean = dataSource.isActivityAvailable(appContext)
|
||||||
|
|
||||||
|
fun isPedometerAvailable(): Boolean = dataSource.isPedometerAvailable(appContext)
|
||||||
|
|
||||||
private fun parseActivityRequest(paramsJson: String?): MotionActivityRequest? {
|
private fun parseActivityRequest(paramsJson: String?): MotionActivityRequest? {
|
||||||
if (paramsJson.isNullOrBlank()) {
|
if (paramsJson.isNullOrBlank()) {
|
||||||
return MotionActivityRequest(startISO = null, endISO = null, limit = 200)
|
return MotionActivityRequest(startISO = null, endISO = null, limit = 200)
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ class InvokeCommandRegistryTest {
|
|||||||
locationEnabled = false,
|
locationEnabled = false,
|
||||||
smsAvailable = false,
|
smsAvailable = false,
|
||||||
voiceWakeEnabled = false,
|
voiceWakeEnabled = false,
|
||||||
motionAvailable = false,
|
motionActivityAvailable = false,
|
||||||
|
motionPedometerAvailable = false,
|
||||||
debugBuild = false,
|
debugBuild = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -52,7 +53,8 @@ class InvokeCommandRegistryTest {
|
|||||||
locationEnabled = true,
|
locationEnabled = true,
|
||||||
smsAvailable = true,
|
smsAvailable = true,
|
||||||
voiceWakeEnabled = true,
|
voiceWakeEnabled = true,
|
||||||
motionAvailable = true,
|
motionActivityAvailable = true,
|
||||||
|
motionPedometerAvailable = true,
|
||||||
debugBuild = false,
|
debugBuild = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -79,7 +81,8 @@ class InvokeCommandRegistryTest {
|
|||||||
locationEnabled = false,
|
locationEnabled = false,
|
||||||
smsAvailable = false,
|
smsAvailable = false,
|
||||||
voiceWakeEnabled = false,
|
voiceWakeEnabled = false,
|
||||||
motionAvailable = false,
|
motionActivityAvailable = false,
|
||||||
|
motionPedometerAvailable = false,
|
||||||
debugBuild = false,
|
debugBuild = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -117,7 +120,8 @@ class InvokeCommandRegistryTest {
|
|||||||
locationEnabled = true,
|
locationEnabled = true,
|
||||||
smsAvailable = true,
|
smsAvailable = true,
|
||||||
voiceWakeEnabled = false,
|
voiceWakeEnabled = false,
|
||||||
motionAvailable = true,
|
motionActivityAvailable = true,
|
||||||
|
motionPedometerAvailable = true,
|
||||||
debugBuild = true,
|
debugBuild = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -145,4 +149,23 @@ class InvokeCommandRegistryTest {
|
|||||||
assertTrue(commands.contains("debug.ed25519"))
|
assertTrue(commands.contains("debug.ed25519"))
|
||||||
assertTrue(commands.contains("app.update"))
|
assertTrue(commands.contains("app.update"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun advertisedCommands_onlyIncludesSupportedMotionCommands() {
|
||||||
|
val commands =
|
||||||
|
InvokeCommandRegistry.advertisedCommands(
|
||||||
|
NodeRuntimeFlags(
|
||||||
|
cameraEnabled = false,
|
||||||
|
locationEnabled = false,
|
||||||
|
smsAvailable = false,
|
||||||
|
voiceWakeEnabled = false,
|
||||||
|
motionActivityAvailable = true,
|
||||||
|
motionPedometerAvailable = false,
|
||||||
|
debugBuild = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(commands.contains(OpenClawMotionCommand.Activity.rawValue))
|
||||||
|
assertFalse(commands.contains(OpenClawMotionCommand.Pedometer.rawValue))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ class MotionHandlerTest {
|
|||||||
|
|
||||||
private class FakeMotionDataSource(
|
private class FakeMotionDataSource(
|
||||||
private val hasPermission: Boolean,
|
private val hasPermission: Boolean,
|
||||||
private val available: Boolean = true,
|
private val activityAvailable: Boolean = true,
|
||||||
|
private val pedometerAvailable: Boolean = true,
|
||||||
private val activityRecord: MotionActivityRecord =
|
private val activityRecord: MotionActivityRecord =
|
||||||
MotionActivityRecord(
|
MotionActivityRecord(
|
||||||
startISO = "2026-02-28T00:00:00Z",
|
startISO = "2026-02-28T00:00:00Z",
|
||||||
@@ -117,7 +118,9 @@ private class FakeMotionDataSource(
|
|||||||
private val activityError: Throwable? = null,
|
private val activityError: Throwable? = null,
|
||||||
private val pedometerError: Throwable? = null,
|
private val pedometerError: Throwable? = null,
|
||||||
) : MotionDataSource {
|
) : MotionDataSource {
|
||||||
override fun isAvailable(context: Context): Boolean = available
|
override fun isActivityAvailable(context: Context): Boolean = activityAvailable
|
||||||
|
|
||||||
|
override fun isPedometerAvailable(context: Context): Boolean = pedometerAvailable
|
||||||
|
|
||||||
override fun hasPermission(context: Context): Boolean = hasPermission
|
override fun hasPermission(context: Context): Boolean = hasPermission
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user