fix(service): correct phone position units and omit missing readings (#7437)

This commit is contained in:
James Rich authored and GitHub committed 2026-09-29 11:08:35 +00:00
1 parent df33cad51c
commit c7d7bc766d
3 files changed
+154 -19

No files matched your search

@@ -26,10 +26,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koin.core.annotation.Single
import org.meshtastic.core.common.hasLocationPermission
import org.meshtastic.core.model.Position
import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.MeshLocationManager
import kotlin.time.Duration.Companion.milliseconds
import org.meshtastic.proto.Position as ProtoPosition
@Single
@@ -51,23 +49,20 @@ class AndroidMeshLocationManager(private val context: Application, private val l
.getLocations()
.onEach { location ->
sendPositionFn(
ProtoPosition.Builder()
.also { wb ->
wb.latitude_i = Position.degI(location.latitude)
wb.longitude_i = Position.degI(location.longitude)
wb.altitude =
if (LocationCompat.hasMslAltitude(location)) {
LocationCompat.getMslAltitudeMeters(location).toInt()
} else {
null
}
wb.altitude_hae = location.altitude.toInt()
wb.time = (location.time.milliseconds.inWholeSeconds).toInt()
wb.ground_speed = location.speed.toInt()
wb.ground_track = location.bearing.toInt()
wb.location_source = ProtoPosition.LocSource.LOC_EXTERNAL
}
.build(),
phonePosition(
latitude = location.latitude,
longitude = location.longitude,
timeMillis = location.time,
mslAltitudeMeters =
if (LocationCompat.hasMslAltitude(location)) {
LocationCompat.getMslAltitudeMeters(location)
} else {
null
},
haeAltitudeMeters = location.altitude.takeIf { location.hasAltitude() },
speedMetersPerSecond = location.speed.takeIf { location.hasSpeed() },
bearingDegrees = location.bearing.takeIf { location.hasBearing() },
),
)
}
.launchIn(scope)
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.service
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.util.GeoConstants
import org.meshtastic.core.model.util.UnitConversions
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.milliseconds
import org.meshtastic.proto.Position as ProtoPosition
/**
* The position the phone reports for its node. A reading the fix lacks stays unset rather than going out as 0, and
* speed and track use the units firmware's own GPS sends: km/h and 1e-5 degrees.
*/
internal fun phonePosition(
latitude: Double,
longitude: Double,
timeMillis: Long,
mslAltitudeMeters: Double?,
haeAltitudeMeters: Double?,
speedMetersPerSecond: Float?,
bearingDegrees: Float?,
): ProtoPosition = ProtoPosition.Builder()
.also { wb ->
wb.latitude_i = Position.degI(latitude)
wb.longitude_i = Position.degI(longitude)
wb.altitude = mslAltitudeMeters?.toInt()
wb.altitude_hae = haeAltitudeMeters?.toInt()
wb.time = timeMillis.milliseconds.inWholeSeconds.toInt()
wb.ground_speed = speedMetersPerSecond?.let { UnitConversions.metersPerSecondToKph(it).roundToInt() }
wb.ground_track = bearingDegrees?.let { (it / GeoConstants.HEADING_DEG).roundToInt() }
wb.location_source = ProtoPosition.LocSource.LOC_EXTERNAL
}
.build()
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.service
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import org.meshtastic.proto.Position as ProtoPosition
class PhonePositionTest {
private fun fix(
mslAltitudeMeters: Double? = null,
haeAltitudeMeters: Double? = null,
speedMetersPerSecond: Float? = null,
bearingDegrees: Float? = null,
) = phonePosition(
latitude = 1.5,
longitude = -2.25,
timeMillis = 1_700_000_000_500,
mslAltitudeMeters = mslAltitudeMeters,
haeAltitudeMeters = haeAltitudeMeters,
speedMetersPerSecond = speedMetersPerSecond,
bearingDegrees = bearingDegrees,
)
@Test
fun `readings the fix lacks stay unset`() {
val position = fix()
assertNull(position.altitude)
assertNull(position.altitude_hae)
assertNull(position.ground_speed)
assertNull(position.ground_track)
}
@Test
fun `zero readings are sent as zero`() {
val position =
fix(mslAltitudeMeters = 0.0, haeAltitudeMeters = 0.0, speedMetersPerSecond = 0f, bearingDegrees = 0f)
assertEquals(0, position.altitude)
assertEquals(0, position.altitude_hae)
assertEquals(0, position.ground_speed)
assertEquals(0, position.ground_track)
}
@Test
fun `speed goes out in km per hour`() {
assertEquals(36, fix(speedMetersPerSecond = 10f).ground_speed)
assertEquals(5, fix(speedMetersPerSecond = 1.4f).ground_speed)
}
@Test
fun `track goes out in hundred-thousandths of a degree`() {
assertEquals(9_000_000, fix(bearingDegrees = 90f).ground_track)
assertEquals(35_950_000, fix(bearingDegrees = 359.5f).ground_track)
}
@Test
fun `each altitude keeps its own datum`() {
val position = fix(mslAltitudeMeters = 12.9, haeAltitudeMeters = -20.4)
assertEquals(12, position.altitude)
assertEquals(-20, position.altitude_hae)
}
@Test
fun `coordinates and time use the wire scale`() {
val position = fix()
assertEquals(15_000_000, position.latitude_i)
assertEquals(-22_500_000, position.longitude_i)
assertEquals(1_700_000_000, position.time)
assertEquals(ProtoPosition.LocSource.LOC_EXTERNAL, position.location_source)
}
}