feat(train): elevation from the barometer, its pressure kept raw (ADR 0030, D49)
11c: where a recording has GPS and the phone a barometer, the service averages its readings over 5 s into the file's `b` lines and the workout's new `pressure` series (tenths of a pascal). The page, Health Connect's totals and the exported files take the fixes' altitudes from it: the standard atmosphere's height at the pressure, levelled by the median of GPS's altitudes less those; climbs count above 1 m. Workouts recorded before keep GPS's altitudes; an older app skips the series. Not tried on a phone yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
128ba1f6c8
commit
57cbd97fd1
3 changed files with 320 additions and 11 deletions
|
|
@ -0,0 +1,26 @@
|
|||
package app.halkyon.train
|
||||
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
/**
|
||||
* The barometer's readings (hPa, several a second) averaged over [windowMs] (ADR 0030, D49):
|
||||
* each window's mean, in tenths of a pascal, as the reading that closes it comes.
|
||||
*/
|
||||
class PressureAverage(private val windowMs: Long = 5_000) {
|
||||
private var from: Long? = null
|
||||
private var sum = 0.0
|
||||
private var count = 0
|
||||
|
||||
/** Takes the reading [hPa] at [at]; the window's mean where it closed. */
|
||||
fun add(at: Long, hPa: Float): Long? {
|
||||
val start = from ?: at.also { from = it }
|
||||
sum += hPa
|
||||
count++
|
||||
if (at - start < windowMs) return null
|
||||
val mean = (sum / count * 1_000).roundToLong()
|
||||
from = null
|
||||
sum = 0.0
|
||||
count = 0
|
||||
return mean
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ import org.json.JSONObject
|
|||
// The recording's side of the system (ADR 0030, D13, D16, D18, D19): a
|
||||
// foreground service, started from the open app, that takes GPS fixes from
|
||||
// Android's LocationManager (no Play services) and steps from the step
|
||||
// detector, reads the heart-rate strap and the rower where the person chose
|
||||
// detector and the air's pressure from the barometer (D49), reads the heart-rate strap and the rower where the person chose
|
||||
// them (D26 to D28), and the Loop's heart rate where no strap gives one (D41),
|
||||
// pauses by itself, and appends each sample as a line to the
|
||||
// recording file, flushed every few seconds, so a recording survives the app
|
||||
|
|
@ -174,6 +174,9 @@ class RecorderService : Service(), LocationListener, SensorEventListener {
|
|||
private var usualCadence = 165
|
||||
private var lastMusicAt = 0L
|
||||
|
||||
/** The barometer's readings, averaged every 5 s (D49). */
|
||||
private val pressure = PressureAverage()
|
||||
|
||||
/** What tells each distance passed (D49), and its words. */
|
||||
private var announcer: Announcer? = null
|
||||
private var announceWords: AnnounceWords? = null
|
||||
|
|
@ -272,6 +275,15 @@ class RecorderService : Service(), LocationListener, SensorEventListener {
|
|||
getSystemService(LocationManager::class.java)
|
||||
.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1_000L, 0f, this, Looper.getMainLooper())
|
||||
}
|
||||
// The air's pressure where the phone has a barometer: the elevation
|
||||
// GPS gives poorly (D49).
|
||||
if (located) {
|
||||
getSystemService(SensorManager::class.java).let { sensors ->
|
||||
sensors.getDefaultSensor(Sensor.TYPE_PRESSURE)?.let {
|
||||
sensors.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stepping) {
|
||||
getSystemService(SensorManager::class.java).let { sensors ->
|
||||
sensors.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR)?.let {
|
||||
|
|
@ -595,6 +607,11 @@ class RecorderService : Service(), LocationListener, SensorEventListener {
|
|||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
if (event.sensor.type == Sensor.TYPE_PRESSURE) {
|
||||
val at = wallTime(event.timestamp)
|
||||
pressure.add(at, event.values[0])?.let { decipascals -> write("b $at $decipascals") }
|
||||
return
|
||||
}
|
||||
if (event.sensor.type != Sensor.TYPE_STEP_DETECTOR) return
|
||||
val at = wallTime(event.timestamp)
|
||||
write("s $at")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package app.halkyon.train
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
/** The barometer's readings averaged every 5 s (ADR 0030, D49). */
|
||||
class PressureAverageTest {
|
||||
@Test
|
||||
fun `a window's readings come out as their mean in tenths of a pascal, once it closes`() {
|
||||
val average = PressureAverage()
|
||||
assertNull(average.add(0, 1000.0f))
|
||||
assertNull(average.add(2_000, 1000.2f))
|
||||
assertEquals(1_000_100L, average.add(5_000, 1000.1f))
|
||||
assertNull(average.add(5_200, 990.0f))
|
||||
assertEquals(990_000L, average.add(10_200, 990.0f))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue