diff --git a/README.md b/README.md index e4c92b0..c90b30b 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Linear Acceleration | ✅️ | rememberLinearAccelerationSensorState() Rotation Vector | ✅️️ | rememberRotationVectorSensorState() Relative Humidity | ⚠️ | WIP Ambient Temperature | ✅️ | rememberAmbientTemperatureSensorState() -Magnetic Field (Uncalibrated) | — | N/A +Magnetic Field (Uncalibrated) | ✅️️ | rememberUncalibratedMagneticFieldSensorState() GameRotation Vector | ✅️ | rememberGameRotationVectorSensorState() Gyroscope (Uncalibrated) | ⚠️ | WIP Significant Motion | — | N/A @@ -96,16 +96,16 @@ Step Counter | ✅️ | rememberStepCounterSensorState() Geomagnetic Rotation Vector | ✅️️ | rememberGeomagneticRotationVectorSensorState() Heart Rate | ✅️ | rememberHeartRateSensorState() Pose6DOF | — | N/A -Stationary Detect | — | N/A -Motion Detect | — | N/A +Stationary Detect | ⚠️ | WIP +Motion Detect | ⚠️ | WIP Heart Beat | — | N/A Low Latency Off-Body Detect | — | N/A Accelerometer (Uncalibrated) | ⚠️ | WIP -Hinge Angle | ⚠️ | WIP +Hinge Angle | ✅️ | rememberHingeAngleSensorState() Head Tracker | — | N/A -Accelerometer Limited Axes | — | N/A -Gyroscope Limited Axes | — | N/A -Accelerometer Limited Axes (Uncalibrated) | — | N/A +Accelerometer Limited Axes | ⚠️ | WIP +Gyroscope Limited Axes | ✅️️ | rememberLimitedAxesGyroscopeSensorState() +Accelerometer Limited Axes (Uncalibrated) | ⚠️ | WIP Gyroscope Limited Axes (Uncalibrated) | — | N/A Heading | ⚠️ | WIP All | — | N/A diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/HingeAngleSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/HingeAngleSensorState.kt new file mode 100644 index 0000000..e4b2bfc --- /dev/null +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/HingeAngleSensorState.kt @@ -0,0 +1,80 @@ +package com.mutualmobile.composesensors + +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 hinge angle sensor measures the angle, in degrees, between two integral parts of the device. + * Movement of a hinge measured by this sensor type is expected to alter the ways in which the user + * can interact with the device, for example, by unfolding or revealing a display. + * @param angle Angle between two integral parts of the device (in degrees) + * @param isAvailable Whether the current device has a HingeAngle sensor. Defaults to false. + * @param accuracy Accuracy factor of the HingeAngle sensor. Defaults to 0. + * @see [rememberHingeAngleSensorState] + */ +@Immutable +class HingeAngleSensorState internal constructor( + val angle: Float = 0f, + val isAvailable: Boolean = false, + val accuracy: Int = 0 +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is HingeAngleSensorState) return false + + if (angle != other.angle) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + + return true + } + + override fun hashCode(): Int { + var result = angle.hashCode() + result = 31 * result + isAvailable.hashCode() + result = 31 * result + accuracy.hashCode() + return result + } + + override fun toString(): String { + return "HingeAngleSensorState(angle=$angle, isAvailable=$isAvailable, accuracy=$accuracy)" + } +} + +/** + * Creates and [remember]s an instance of [HingeAngleSensorState]. + * @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. + */ +@Composable +fun rememberHingeAngleSensorState( + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} +): HingeAngleSensorState { + val sensorState = rememberSensorState( + sensorType = SensorType.HingeAngle, + sensorDelay = sensorDelay, + onError = onError + ) + val hingeAngleSensorState = remember { mutableStateOf(HingeAngleSensorState()) } + + LaunchedEffect( + key1 = sensorState, + block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + hingeAngleSensorState.value = HingeAngleSensorState( + angle = sensorStateValues[0], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy + ) + } + } + ) + + return hingeAngleSensorState.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesGyroscopeSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesGyroscopeSensorState.kt new file mode 100644 index 0000000..edc2e6c --- /dev/null +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/LimitedAxesGyroscopeSensorState.kt @@ -0,0 +1,107 @@ +package com.mutualmobile.composesensors + +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 + +/** + * Equivalent to Gyroscope Sensor but supporting cases where one or two axes are not supported. + * @param xRotation Angular speed around the x-axis (if supported). Defaults to 0f. + * @param yRotation Angular speed around the y-axis (if supported). Defaults to 0f. + * @param zRotation Angular speed around the z-axis (if supported). Defaults to 0f. + * @param xAxisSupported Angular speed supported for x-axis. Defaults to 0f. + * @param yAxisSupported Angular speed supported for y-axis. Defaults to 0f. + * @param zAxisSupported Angular speed supported for z-axis. Defaults to 0f. + * @param isAvailable Whether the current device has a gyroscope sensor. Defaults to false. + * @param accuracy Accuracy factor of the gyroscope sensor. Defaults to 0. + */ +@Immutable +class LimitedAxesGyroscopeSensorState internal constructor( + val xRotation: Float = 0f, + val yRotation: Float = 0f, + val zRotation: Float = 0f, + val xAxisSupported: Float = 0f, + val yAxisSupported: Float = 0f, + val zAxisSupported: Float = 0f, + val isAvailable: Boolean = false, + val accuracy: Int = 0 +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LimitedAxesGyroscopeSensorState + + if (xRotation != other.xRotation) return false + if (yRotation != other.yRotation) return false + if (zRotation != other.zRotation) 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 + + return true + } + + override fun hashCode(): Int { + var result = xRotation.hashCode() + result = 31 * result + yRotation.hashCode() + result = 31 * result + zRotation.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 + return result + } + + override fun toString(): String { + return "LimitedAxesGyroscopeSensorState(xRotation=$xRotation, yRotation=$yRotation, " + + "zRotation=$zRotation, xAxisSupported=$xAxisSupported, " + + "yAxisSupported=$yAxisSupported, zAxisSupported=$zAxisSupported, " + + "isAvailable=$isAvailable, accuracy=$accuracy)" + } +} + +/** + * Creates and [remember]s an instance of [LimitedAxesGyroscopeSensorState]. + * @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. + */ + +@Composable +fun rememberLimitedAxesGyroscopeSensorState( + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} +): LimitedAxesGyroscopeSensorState { + val sensorState = rememberSensorState( + sensorType = SensorType.GyroscopeLimitedAxes, + sensorDelay = sensorDelay, + onError = onError + ) + + val gyroscopeLimitedAxesSensor = remember { mutableStateOf(LimitedAxesGyroscopeSensorState()) } + + LaunchedEffect(key1 = sensorState, block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + gyroscopeLimitedAxesSensor.value = LimitedAxesGyroscopeSensorState( + xRotation = sensorStateValues[0], + yRotation = sensorStateValues[1], + zRotation = sensorStateValues[2], + xAxisSupported = sensorStateValues[3], + yAxisSupported = sensorStateValues[4], + zAxisSupported = sensorStateValues[5], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy + ) + } + }) + + return gyroscopeLimitedAxesSensor.value +} diff --git a/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedMagneticFieldSensorState.kt b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedMagneticFieldSensorState.kt new file mode 100644 index 0000000..81f56c4 --- /dev/null +++ b/composesensors/src/main/java/com/mutualmobile/composesensors/UncalibratedMagneticFieldSensorState.kt @@ -0,0 +1,107 @@ +package com.mutualmobile.composesensors + +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 + +/** + * An Uncalibrated Magnetic Field sensor is similar to [SensorType.MagneticField] but the hard iron calibration is reported separately instead of being included in the measurement. Factory calibration and temperature compensation will still be applied to the "uncalibrated" measurement. Assumptions are that the magnetic field is due to the Earth's poles being avoided. + * Hard iron - These distortions arise due to the magnetized iron, steel or permanent magnets on the device. + * Soft iron - These distortions arise due to the interaction with the earth's magnetic field. + * @param xStrength The measured magnetic field in X-axis. Soft iron and temperature calibrations are applied. But the hard iron calibration is not applied. The value is calculated in micro-Tesla (uT). + * @param yStrength The measured magnetic field in Y-axis. Soft iron and temperature calibrations are applied. But the hard iron calibration is not applied. The value is calculated in micro-Tesla (uT). + * @param zStrength The measured magnetic field in Z-axis. Soft iron and temperature calibrations are applied. But the hard iron calibration is not applied. The value is calculated in micro-Tesla (uT). + * @param xBias The iron bias estimated in X-axis. It is a component of the estimated hard iron calibration. The value is calculated in micro-Tesla (uT). + * @param yBias The iron bias estimated in Y-axis. It is a component of the estimated hard iron calibration. The value is calculated in micro-Tesla (uT). + * @param zBias The iron bias estimated in Z-axis. It is a component of the estimated hard iron calibration. The value is calculated in micro-Tesla (uT). + * @param isAvailable Whether the current device has an uncalibrated magnetic field sensor. Defaults to false. + * @param accuracy Accuracy factor of the uncalibrated magnetic field sensor. Defaults to 0. + */ +@Immutable +class UncalibratedMagneticFieldSensorState internal constructor( + val xStrength: Float = 0f, + val yStrength: Float = 0f, + val zStrength: Float = 0f, + val xBias: Float = 0f, + val yBias: Float = 0f, + val zBias: Float = 0f, + val isAvailable: Boolean = false, + val accuracy: Int = 0 +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is UncalibratedMagneticFieldSensorState) return false + + if (xStrength != other.xStrength) return false + if (yStrength != other.yStrength) return false + if (zStrength != other.zStrength) return false + if (xBias != other.xBias) return false + if (yBias != other.yBias) return false + if (zBias != other.zBias) return false + if (isAvailable != other.isAvailable) return false + if (accuracy != other.accuracy) return false + + return true + } + + override fun hashCode(): Int { + var result = xStrength.hashCode() + result = 31 * result + yStrength.hashCode() + result = 31 * result + zStrength.hashCode() + result = 31 * result + xBias.hashCode() + result = 31 * result + yBias.hashCode() + result = 31 * result + zBias.hashCode() + result = 31 * result + accuracy.hashCode() + result = 31 * result + isAvailable.hashCode() + return result + } + + override fun toString(): String { + return "UncalibratedMagneticFieldSensorState(xStrength=$xStrength, yStrength=$yStrength, zStrength=$zStrength, " + + "xBias=$xBias, yBias=$yBias, zBias=$zBias, " + + "isAvailable=$isAvailable, accuracy=$accuracy)" + } +} + +/** + * Creates and [remember]s an instance of [UncalibratedMagneticFieldSensorState]. + * @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. + */ +@Composable +fun rememberUncalibratedMagneticFieldSensorState( + sensorDelay: SensorDelay = SensorDelay.Normal, + onError: (throwable: Throwable) -> Unit = {} +): UncalibratedMagneticFieldSensorState { + val sensorState = rememberSensorState( + sensorType = SensorType.MagneticFieldUncalibrated, + sensorDelay = sensorDelay, + onError = onError + ) + val uncalibratedMagneticFieldSensorState = + remember { mutableStateOf(UncalibratedMagneticFieldSensorState()) } + + LaunchedEffect( + key1 = sensorState, + block = { + val sensorStateValues = sensorState.data + if (sensorStateValues.isNotEmpty()) { + uncalibratedMagneticFieldSensorState.value = UncalibratedMagneticFieldSensorState( + xStrength = sensorStateValues[0], + yStrength = sensorStateValues[1], + zStrength = sensorStateValues[2], + xBias = sensorStateValues[3], + yBias = sensorStateValues[4], + zBias = sensorStateValues[5], + isAvailable = sensorState.isAvailable, + accuracy = sensorState.accuracy + ) + } + } + ) + + return uncalibratedMagneticFieldSensorState.value +}