diff --git a/README.md b/README.md index 559bda2..58a9785 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Step Detector | ✅️ | rememberStepDetectorSensorState() Step Counter | ✅️ | rememberStepCounterSensorState() Geomagnetic Rotation Vector | ✅️️ | rememberGeomagneticRotationVectorSensorState() Heart Rate | ✅️ | rememberHeartRateSensorState() -Pose6DOF | — | N/A +Pose6DOF | ✅ | rememberPose6DOFSensorState() Stationary Detect | ✅️ | rememberStationaryDetectSensorState() Motion Detect | ✅️ | rememberMotionDetectSensorState() Heart Beat | ✅ | rememberHeartBeatSensorState() @@ -78,7 +78,7 @@ Head Tracker | ✅️ | rememberHeadTrackerSensorState() Accelerometer Limited Axes | ✅️ | rememberLimitedAxesAccelerometerSensorState() Gyroscope Limited Axes | ✅️️ | rememberLimitedAxesGyroscopeSensorState() Accelerometer Limited Axes (Uncalibrated) | ✅ | rememberUncalibratedLimitedAxesAccelerometerSensorState() -Gyroscope Limited Axes (Uncalibrated) | — | N/A +Gyroscope Limited Axes (Uncalibrated) | ✅ | rememberUncalibratedLimitedAxesGyroscopeSensorState() Heading | ✅ | rememberHeadingSensorState() ## License 🔖 diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/Pose6DOFSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/Pose6DOFSensorState.kt new file mode 100644 index 0000000..f198155 --- /dev/null +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/Pose6DOFSensorState.kt @@ -0,0 +1,185 @@ +package com.mutualmobile.composesensors + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +/** + * A Pose6DOF event consists of a rotation expressed as a quaternion and a translation expressed in + * SI units. The event also contains a delta rotation and translation that show how the device's + * pose has changed since the previous sequence numbered pose. The event uses the canonical Android + * Sensor axes. + * + * For more info, please refer the [Android Documentation Reference](https://developer.android.com/reference/android/hardware/SensorEvent#sensor.type_pose_6dof) + * @param xScaledSinValue Represents the value x*sin(θ/2) + * @param yScaledSinValue Represents the value y*sin(θ/2) + * @param zScaledSinValue Represents the value z*sin(θ/2) + * @param cosValue Represents the value cos(θ/2) + * @param xTranslation Translation along x-axis from an arbitrary origin + * @param yTranslation Translation along y-axis from an arbitrary origin + * @param zTranslation Translation along z-axis from an arbitrary origin + * @param xScaledSinDeltaRotation Represents the delta quaternion rotation x*sin(θ/2) + * @param yScaledSinDeltaRotation Represents the delta quaternion rotation y*sin(θ/2) + * @param zScaledSinDeltaRotation Represents the delta quaternion rotation z*sin(θ/2) + * @param cosDeltaRotation Represents the delta quaternion rotation cos(θ/2) + * @param xDeltaTranslation Delta translation along x-axis + * @param yDeltaTranslation Delta translation along y-axis + * @param zDeltaTranslation Delta translation along z-axis + * @param sequenceNumber Represents the Sequence number + * @param isAvailable Whether the current device has a Pose6DOF sensor. Defaults to false. + * @param accuracy Accuracy factor of the Pose6DOF sensor. Defaults to 0. + */ +@RequiresApi(Build.VERSION_CODES.N) +@Immutable +class Pose6DOFSensorState internal constructor( + val xScaledSinValue: Float = 0f, + val yScaledSinValue: Float = 0f, + val zScaledSinValue: Float = 0f, + val cosValue: Float = 0f, + val xTranslation: Float = 0f, + val yTranslation: Float = 0f, + val zTranslation: Float = 0f, + val xScaledSinDeltaRotation: Float = 0f, + val yScaledSinDeltaRotation: Float = 0f, + val zScaledSinDeltaRotation: Float = 0f, + val cosDeltaRotation: Float = 0f, + val xDeltaTranslation: Float = 0f, + val yDeltaTranslation: Float = 0f, + val zDeltaTranslation: Float = 0f, + val sequenceNumber: Float = 0f, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Pose6DOFSensorState) return false + + if (xScaledSinValue != other.xScaledSinValue) return false + if (yScaledSinValue != other.yScaledSinValue) return false + if (zScaledSinValue != other.zScaledSinValue) return false + if (cosValue != other.cosValue) return false + if (xTranslation != other.xTranslation) return false + if (yTranslation != other.yTranslation) return false + if (zTranslation != other.zTranslation) return false + if (xScaledSinDeltaRotation != other.xScaledSinDeltaRotation) return false + if (yScaledSinDeltaRotation != other.yScaledSinDeltaRotation) return false + if (zScaledSinDeltaRotation != other.zScaledSinDeltaRotation) return false + if (cosDeltaRotation != other.cosDeltaRotation) return false + if (xDeltaTranslation != other.xDeltaTranslation) return false + if (yDeltaTranslation != other.yDeltaTranslation) return false + if (zDeltaTranslation != other.zDeltaTranslation) return false + if (sequenceNumber != other.sequenceNumber) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + + return true + } + + override fun hashCode(): Int { + var result = xScaledSinValue.hashCode() + result = 31 * result + yScaledSinValue.hashCode() + result = 31 * result + zScaledSinValue.hashCode() + result = 31 * result + cosValue.hashCode() + result = 31 * result + xTranslation.hashCode() + result = 31 * result + yTranslation.hashCode() + result = 31 * result + zTranslation.hashCode() + result = 31 * result + xScaledSinDeltaRotation.hashCode() + result = 31 * result + yScaledSinDeltaRotation.hashCode() + result = 31 * result + zScaledSinDeltaRotation.hashCode() + result = 31 * result + cosDeltaRotation.hashCode() + result = 31 * result + xDeltaTranslation.hashCode() + result = 31 * result + yDeltaTranslation.hashCode() + result = 31 * result + zDeltaTranslation.hashCode() + result = 31 * result + sequenceNumber.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } + + override fun toString(): String { + return "Pose6DOFSensorState(xScaledSinValue=$xScaledSinValue," + + " yScaledSinValue=$yScaledSinValue, zScaledSinValue=$zScaledSinValue, " + + " cosValue=$cosValue, xTranslation=$xTranslation, yTranslation=$yTranslation," + + " zTranslation=$zTranslation, xScaledSinDeltaRotation=$xScaledSinDeltaRotation," + + " yScaledSinDeltaRotation=$yScaledSinDeltaRotation," + + " zScaledSinDeltaRotation=$zScaledSinDeltaRotation," + + " cosDeltaRotation=$cosDeltaRotation, xDeltaTranslation=$xDeltaTranslation," + + " yDeltaTranslation=$yDeltaTranslation, zDeltaTranslation=$zDeltaTranslation," + + " sequenceNumber=$sequenceNumber, isAvailable=$isAvailable, " + + "accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } +} + +/** + * Creates and [remember]s an instance of [Pose6DOFSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. + * @param sensorDelay The rate at which the raw sensor data should be received. + * Defaults to [SensorDelay.Normal]. + * @param onError Callback invoked on every error state. + */ +@RequiresApi(Build.VERSION_CODES.N) +@Composable +fun rememberPose6DOFSensorState( + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} +): Pose6DOFSensorState { + val sensorState = rememberSensorState( + sensorType = SensorType.Pose6DOF, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) + val pose6DOFSensorState = remember { mutableStateOf(Pose6DOFSensorState()) } + + LaunchedEffect( + key1 = sensorState, + block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + pose6DOFSensorState.value = Pose6DOFSensorState( + xScaledSinValue = sensorStateValues[0], + yScaledSinValue = sensorStateValues[1], + zScaledSinValue = sensorStateValues[2], + cosValue = sensorStateValues[3], + xTranslation = sensorStateValues[4], + yTranslation = sensorStateValues[5], + zTranslation = sensorStateValues[6], + xScaledSinDeltaRotation = sensorStateValues[7], + yScaledSinDeltaRotation = sensorStateValues[8], + zScaledSinDeltaRotation = sensorStateValues[9], + cosDeltaRotation = sensorStateValues[10], + xDeltaTranslation = sensorStateValues[11], + yDeltaTranslation = sensorStateValues[12], + zDeltaTranslation = sensorStateValues[13], + sequenceNumber = sensorStateValues[14], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } + } + ) + + return pose6DOFSensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedLimitedAxesGyroscopeSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedLimitedAxesGyroscopeSensorState.kt new file mode 100644 index 0000000..162f96d --- /dev/null +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedLimitedAxesGyroscopeSensorState.kt @@ -0,0 +1,159 @@ +package com.mutualmobile.composesensors + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +/** + * Measures a device's rate of rotation in rad/s around each of the three + * physical axes (x, y, and z). Uncalibrated gyroscope sensor estimates an + * extra drift on x, y, z axes, to the effect that uncalibrated_value = + * calibrated_value + drift in that particular axis. + * + * Equivalent to [UncalibratedGyroscopeSensorState], but supporting cases where one or two axes are + * not supported. The last three values represent whether the angular speed value for a given axis + * is supported. The supported axes should be determined at build time and these values do not + * change during runtime. The angular speed values and drift values for axes that are not supported + * are set to 0. + * + * For more info, please refer the [Android Documentation Reference](https://developer.android.com/reference/android/hardware/SensorEvent#sensor.type_gyroscope_limited_axes_uncalibrated:) + * + * @param xRotation Angular speed (w/o drift compensation) around the X axis (if supported). + * Defaults to 0f. + * @param yRotation Angular speed (w/o drift compensation) around the Y axis (if supported). + * Defaults to 0f. + * @param zRotation Angular speed (w/o drift compensation) around the Z axis (if supported). + * Defaults to 0f. + * @param xBias estimated drift around X axis (if supported). Defaults to 0f. + * @param yBias estimated drift around Y axis (if supported). Defaults to 0f. + * @param zBias estimated drift around Z axis (if supported). Defaults to 0f. + * @param xAxisSupported Whether angular speed supported for x-axis. Defaults to false. + * @param yAxisSupported Whether angular speed supported for y-axis. Defaults to false. + * @param zAxisSupported Whether angular speed supported for z-axis. Defaults to false. + * @param isAvailable Whether the current device has a gyroscope sensor. + * Defaults to false. + * @param accuracy Accuracy factor of the gyroscope sensor. Defaults to 0. + */ +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +@Immutable +class UncalibratedLimitedAxesGyroscopeSensorState internal constructor( + val xRotation: Float = 0f, + val yRotation: Float = 0f, + val zRotation: Float = 0f, + val xBias: Float = 0f, + val yBias: Float = 0f, + val zBias: Float = 0f, + val xAxisSupported: Boolean = false, + val yAxisSupported: Boolean = false, + val zAxisSupported: Boolean = false, + val isAvailable: Boolean = false, + val accuracy: Int = 0, + private val startListeningEvents: (() -> Unit)? = null, + private val stopListeningEvents: (() -> Unit)? = null +) : SensorStateListener { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is UncalibratedLimitedAxesGyroscopeSensorState) return false + + if (xRotation != other.xRotation) return false + if (yRotation != other.yRotation) return false + if (zRotation != other.zRotation) return false + if (xBias != other.xBias) return false + if (yBias != other.yBias) return false + if (zBias != other.zBias) return false + if (xAxisSupported != other.xAxisSupported) return false + if (yAxisSupported != other.yAxisSupported) return false + if (zAxisSupported != other.zAxisSupported) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + if (startListeningEvents != other.startListeningEvents) return false + if (stopListeningEvents != other.stopListeningEvents) return false + + return true + } + + override fun hashCode(): Int { + var result = xRotation.hashCode() + result = 31 * result + yRotation.hashCode() + result = 31 * result + zRotation.hashCode() + result = 31 * result + xBias.hashCode() + result = 31 * result + yBias.hashCode() + result = 31 * result + zBias.hashCode() + result = 31 * result + xAxisSupported.hashCode() + result = 31 * result + yAxisSupported.hashCode() + result = 31 * result + zAxisSupported.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + startListeningEvents.hashCode() + result = 31 * result + stopListeningEvents.hashCode() + return result + } + + override fun toString(): String { + return "UncalibratedGyroscopeSensorState(xRotation=$xRotation, yRotation=$yRotation," + + " zRotation=$zRotation," + "xBias=$xBias," + "yBias=$yBias," + "zBias=$zBias," + + " xAxisSupported=$xAxisSupported, yAxisSupported=$yAxisSupported," + + " zAxisSupported=$zAxisSupported, isAvailable=$isAvailable, accuracy=$accuracy)" + } + + override fun startListening() { + startListeningEvents?.invoke() + } + + override fun stopListening() { + stopListeningEvents?.invoke() + } +} + +/** + * Creates and [remember]s an instance of [UncalibratedLimitedAxesGyroscopeSensorState]. + * @param autoStart Start listening to sensor events as soon as sensor state is initialised. + * Defaults to true. + * @param sensorDelay The rate at which the raw sensor data should be received. Defaults to + * [SensorDelay.Normal]. + * @param onError Callback invoked on every error state. + */ +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +@Composable +fun rememberUncalibratedLimitedAxesGyroscopeSensorState( + autoStart: Boolean = true, + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} +): UncalibratedLimitedAxesGyroscopeSensorState { + val sensorState = rememberSensorState( + sensorType = SensorType.GyroscopeLimitedAxesUncalibrated, + sensorDelay = sensorDelay, + autoStart = autoStart, + onError = onError + ) + val uncalibratedLimitedAxesGyroscopeSensorState = + remember { mutableStateOf(UncalibratedLimitedAxesGyroscopeSensorState()) } + + LaunchedEffect( + key1 = sensorState, + block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + uncalibratedLimitedAxesGyroscopeSensorState.value = + UncalibratedLimitedAxesGyroscopeSensorState( + xRotation = sensorStateValues[0], + yRotation = sensorStateValues[1], + zRotation = sensorStateValues[2], + xBias = sensorStateValues[3], + yBias = sensorStateValues[4], + zBias = sensorStateValues[5], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy, + startListeningEvents = sensorState::startListening, + stopListeningEvents = sensorState::stopListening + ) + } + } + ) + + return uncalibratedLimitedAxesGyroscopeSensorState.value +}