feat: add SensorCollector for sensor data acquisition
This commit is contained in:
@@ -0,0 +1,234 @@
|
|||||||
|
package com.senstools.domain.sensor
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.hardware.Sensor
|
||||||
|
import android.hardware.SensorEvent
|
||||||
|
import android.hardware.SensorEventListener
|
||||||
|
import android.hardware.SensorManager
|
||||||
|
import android.location.Location
|
||||||
|
import android.location.LocationListener
|
||||||
|
import android.location.LocationManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.senstools.domain.model.AttitudeData
|
||||||
|
import com.senstools.domain.model.GPSData
|
||||||
|
import com.senstools.domain.model.SensorData
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
interface SensorDataCallback {
|
||||||
|
fun onGPSData(gps: GPSData)
|
||||||
|
fun onSensorData(sensor: SensorData)
|
||||||
|
fun onAttitudeData(attitude: AttitudeData)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SensorCollector {
|
||||||
|
|
||||||
|
private var sensorManager: SensorManager? = null
|
||||||
|
private var locationManager: LocationManager? = null
|
||||||
|
private var callback: SensorDataCallback? = null
|
||||||
|
private var collecting = false
|
||||||
|
|
||||||
|
// Sensors
|
||||||
|
private var accelerometer: android.hardware.Sensor? = null
|
||||||
|
private var gyroscope: android.hardware.Sensor? = null
|
||||||
|
private var magnetometer: android.hardware.Sensor? = null
|
||||||
|
private var pressureSensor: android.hardware.Sensor? = null
|
||||||
|
private var temperatureSensor: android.hardware.Sensor? = null
|
||||||
|
private var humiditySensor: android.hardware.Sensor? = null
|
||||||
|
|
||||||
|
// Sensor fusion for attitude
|
||||||
|
private val rotationMatrix = FloatArray(9)
|
||||||
|
private val orientationAngles = FloatArray(3)
|
||||||
|
private val gravity = FloatArray(3)
|
||||||
|
private val geomagnetic = FloatArray(3)
|
||||||
|
private val accelValues = FloatArray(3)
|
||||||
|
private val gyroValues = FloatArray(3)
|
||||||
|
private val magValues = FloatArray(3)
|
||||||
|
|
||||||
|
private val sensorEventListener = object : SensorEventListener {
|
||||||
|
override fun onSensorChanged(event: SensorEvent) {
|
||||||
|
val timestamp = System.currentTimeMillis()
|
||||||
|
when (event.sensor.type) {
|
||||||
|
Sensor.TYPE_ACCELEROMETER -> {
|
||||||
|
System.arraycopy(event.values, 0, accelValues, 0, 3)
|
||||||
|
computeAttitude(timestamp)
|
||||||
|
emitSensorData(timestamp)
|
||||||
|
}
|
||||||
|
Sensor.TYPE_GYROSCOPE -> {
|
||||||
|
System.arraycopy(event.values, 0, gyroValues, 0, 3)
|
||||||
|
}
|
||||||
|
Sensor.TYPE_MAGNETIC_FIELD -> {
|
||||||
|
System.arraycopy(event.values, 0, magValues, 0, 3)
|
||||||
|
System.arraycopy(event.values, 0, geomagnetic, 0, 3)
|
||||||
|
}
|
||||||
|
Sensor.TYPE_PRESSURE -> {
|
||||||
|
// Pressure to altitude approximation: h = 44330 * (1 - (p/101325)^(1/5.255))
|
||||||
|
val pressure = event.values[0]
|
||||||
|
emitSensorDataWithPressure(timestamp, pressure)
|
||||||
|
}
|
||||||
|
Sensor.TYPE_AMBIENT_TEMPERATURE -> {
|
||||||
|
emitSensorDataWithTemp(timestamp, event.values[0])
|
||||||
|
}
|
||||||
|
Sensor.TYPE_RELATIVE_HUMIDITY -> {
|
||||||
|
emitSensorDataWithHumidity(timestamp, event.values[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAccuracyChanged(sensor: android.hardware.Sensor, accuracy: Int) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracking variables for incremental sensor emission
|
||||||
|
private var lastAccelTimestamp = 0L
|
||||||
|
private var lastPressureTimestamp = 0L
|
||||||
|
private var lastTempTimestamp = 0L
|
||||||
|
private var lastHumidityTimestamp = 0L
|
||||||
|
private var pendingPressure = 0f
|
||||||
|
private var pendingTemp = 0f
|
||||||
|
private var pendingHumidity = 0f
|
||||||
|
private var hasPendingPressure = false
|
||||||
|
private var hasPendingTemp = false
|
||||||
|
private var hasPendingHumidity = false
|
||||||
|
|
||||||
|
private val locationListener = object : LocationListener {
|
||||||
|
override fun onLocationChanged(location: Location) {
|
||||||
|
val timestamp = System.currentTimeMillis()
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||||
|
val now = Date()
|
||||||
|
val gps = GPSData(
|
||||||
|
timestamp = timestamp,
|
||||||
|
longitude = location.longitude,
|
||||||
|
latitude = location.latitude,
|
||||||
|
altitude = location.altitude.toDouble(),
|
||||||
|
speed = location.speed,
|
||||||
|
bearing = location.bearing,
|
||||||
|
satellites = 0, // Not available from LocationListener directly
|
||||||
|
time = String.format("%tT", now),
|
||||||
|
date = String.format("%tF", now)
|
||||||
|
)
|
||||||
|
callback?.onGPSData(gps)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
|
||||||
|
override fun onProviderEnabled(provider: String) {}
|
||||||
|
override fun onProviderDisabled(provider: String) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startCollecting(context: Context, cb: SensorDataCallback) {
|
||||||
|
if (collecting) return
|
||||||
|
callback = cb
|
||||||
|
collecting = true
|
||||||
|
|
||||||
|
sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||||
|
locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
|
|
||||||
|
// Register sensors
|
||||||
|
sensorManager?.let { sm ->
|
||||||
|
accelerometer = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||||
|
gyroscope = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
||||||
|
magnetometer = sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
||||||
|
pressureSensor = sm.getDefaultSensor(Sensor.TYPE_PRESSURE)
|
||||||
|
temperatureSensor = sm.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE)
|
||||||
|
humiditySensor = sm.getDefaultSensor(Sensor.TYPE_RELATIVE_HUMIDITY)
|
||||||
|
|
||||||
|
// High-rate sensors: SENSOR_DELAY_GAME (~100Hz)
|
||||||
|
accelerometer?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_GAME) }
|
||||||
|
gyroscope?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_GAME) }
|
||||||
|
magnetometer?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_GAME) }
|
||||||
|
|
||||||
|
// Low-rate sensors: SENSOR_DELAY_NORMAL (~10Hz)
|
||||||
|
pressureSensor?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_NORMAL) }
|
||||||
|
temperatureSensor?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_NORMAL) }
|
||||||
|
humiditySensor?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_NORMAL) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register GPS
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
== PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
locationManager?.requestLocationUpdates(
|
||||||
|
LocationManager.GPS_PROVIDER,
|
||||||
|
200L, // 5Hz
|
||||||
|
0f,
|
||||||
|
locationListener,
|
||||||
|
Looper.getMainLooper()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopCollecting() {
|
||||||
|
if (!collecting) return
|
||||||
|
collecting = false
|
||||||
|
sensorManager?.unregisterListener(sensorEventListener)
|
||||||
|
locationManager?.removeUpdates(locationListener)
|
||||||
|
sensorManager = null
|
||||||
|
locationManager = null
|
||||||
|
callback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCollecting(): Boolean = collecting
|
||||||
|
|
||||||
|
private fun computeAttitude(timestamp: Long) {
|
||||||
|
// Simple attitude estimation from accelerometer + magnetometer
|
||||||
|
val g = FloatArray(3)
|
||||||
|
val m = FloatArray(3)
|
||||||
|
System.arraycopy(accelValues, 0, g, 0, 3)
|
||||||
|
System.arraycopy(magValues, 0, m, 0, 3)
|
||||||
|
|
||||||
|
if (!SensorManager.getRotationMatrix(rotationMatrix, null, g, m)) return
|
||||||
|
SensorManager.getOrientation(rotationMatrix, orientationAngles)
|
||||||
|
|
||||||
|
val pitch = Math.toDegrees(orientationAngles[1].toDouble()).toFloat()
|
||||||
|
val roll = Math.toDegrees(orientationAngles[2].toDouble()).toFloat()
|
||||||
|
val yaw = Math.toDegrees(orientationAngles[0].toDouble()).toFloat()
|
||||||
|
|
||||||
|
callback?.onAttitudeData(
|
||||||
|
AttitudeData(
|
||||||
|
pitch = pitch,
|
||||||
|
roll = roll,
|
||||||
|
yaw = yaw,
|
||||||
|
timestamp = timestamp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitSensorData(timestamp: Long) {
|
||||||
|
lastAccelTimestamp = timestamp
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||||
|
val now = Date()
|
||||||
|
callback?.onSensorData(
|
||||||
|
SensorData(
|
||||||
|
timestamp = timestamp,
|
||||||
|
accelerometer = accelValues.clone(),
|
||||||
|
gyroscope = gyroValues.clone(),
|
||||||
|
magnetometer = magValues.clone(),
|
||||||
|
pressureAltitude = if (hasPendingPressure) pendingPressure else 0f,
|
||||||
|
temperature = if (hasPendingTemp) pendingTemp else 0f,
|
||||||
|
humidity = if (hasPendingHumidity) pendingHumidity else 0f,
|
||||||
|
time = String.format("%tT", now),
|
||||||
|
date = String.format("%tF", now)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitSensorDataWithPressure(timestamp: Long, pressure: Float) {
|
||||||
|
hasPendingPressure = true
|
||||||
|
pendingPressure = 44330f * (1f - Math.pow((pressure / 101325f).toDouble(), 1.0 / 5.255).toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitSensorDataWithTemp(timestamp: Long, temp: Float) {
|
||||||
|
hasPendingTemp = true
|
||||||
|
pendingTemp = temp
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitSensorDataWithHumidity(timestamp: Long, humidity: Float) {
|
||||||
|
hasPendingHumidity = true
|
||||||
|
pendingHumidity = humidity
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
Vendored
+9
-5
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015 the original authors.
|
# Copyright © 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@@ -86,7 +86,8 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -114,6 +115,7 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -171,6 +173,7 @@ fi
|
|||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
@@ -203,14 +206,15 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
Vendored
+2
-1
@@ -70,10 +70,11 @@ goto fail
|
|||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
Reference in New Issue
Block a user