-
-
Notifications
You must be signed in to change notification settings - Fork 390
feat: Port “Compass view” bottom sheet from Meshtastic-Apple PR #1504 #3896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
022aa84
feat(node/compass): add localized compass sheet with sensor + locatio…
jakevis dd232b4
Merge branch 'meshtastic:main' into main
jakevis a564662
Adjusting compass rose for simplicity
jakevis 1284edf
feat(compass): visualize positional uncertainty with accuracy cone an…
jakevis ff57454
Improved the compass
RCGV1 a394406
Merge pull request #1 from RCGV1/compass
jakevis 4834a71
feat(node/compass): keep cone and target aligned to heading
jakevis f3b5231
Merge branch 'main' into main
jakevis 49d229a
Update feature/node/src/main/kotlin/org/meshtastic/feature/node/compo…
jakevis 9d76fba
Update feature/node/src/main/kotlin/org/meshtastic/feature/node/compo…
jakevis 99144c1
Update feature/node/src/main/kotlin/org/meshtastic/feature/node/compo…
jakevis f260dd9
Fixing co-pilot suggested changes
jakevis e8e3754
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/H…
jakevis f8ec132
Merge branch 'main' into main
jakevis 0b956d8
Merge remote-tracking branch 'upstream/main' into jakevis/compass
jamesarich a3c51cf
spotless
jamesarich File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| /* | ||
| * Copyright (c) 2025 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.feature.node.compass | ||
|
|
||
| import android.content.Context | ||
| import android.hardware.Sensor | ||
| import android.hardware.SensorEvent | ||
| import android.hardware.SensorEventListener | ||
| import android.hardware.SensorManager | ||
| import android.hardware.GeomagneticField | ||
| import android.location.Location | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.channels.awaitClose | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.callbackFlow | ||
| import javax.inject.Inject | ||
|
|
||
| private const val ROTATION_MATRIX_SIZE = 9 | ||
| private const val ORIENTATION_SIZE = 3 | ||
| private const val DEGREES_IN_CIRCLE = 360f | ||
|
|
||
| data class HeadingState( | ||
| val heading: Float? = null, // 0..360 degrees | ||
| val hasSensor: Boolean = true, | ||
| val accuracy: Int = SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM, | ||
| ) | ||
|
|
||
| class CompassHeadingProvider @Inject constructor(@ApplicationContext private val context: Context) { | ||
|
|
||
| /** | ||
| * Emits compass heading in degrees. If a location is provided, applies true north correction. | ||
| * | ||
| * @param location Optional Location from the phone's location provider. | ||
| */ | ||
| fun headingUpdates(location: Location? = null): Flow<HeadingState> = callbackFlow { | ||
| val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager | ||
| if (sensorManager == null) { | ||
| trySend(HeadingState(hasSensor = false)) | ||
| close() | ||
| return@callbackFlow | ||
| } | ||
|
|
||
| val rotationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) | ||
| val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) | ||
| val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) | ||
|
|
||
| if (rotationSensor == null && (accelerometer == null || magnetometer == null)) { | ||
| trySend(HeadingState(hasSensor = false)) | ||
| awaitClose {} | ||
| return@callbackFlow | ||
| } | ||
|
|
||
| val rotationMatrix = FloatArray(ROTATION_MATRIX_SIZE) | ||
| val orientation = FloatArray(ORIENTATION_SIZE) | ||
| val accelValues = FloatArray(ORIENTATION_SIZE) | ||
| val magnetValues = FloatArray(ORIENTATION_SIZE) | ||
| var hasAccel = false | ||
| var hasMagnet = false | ||
|
|
||
| val listener = object : SensorEventListener { | ||
| override fun onSensorChanged(event: SensorEvent) { | ||
| when (event.sensor.type) { | ||
| Sensor.TYPE_ROTATION_VECTOR -> { | ||
| SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values) | ||
| } | ||
| Sensor.TYPE_ACCELEROMETER -> { | ||
| System.arraycopy(event.values, 0, accelValues, 0, accelValues.size) | ||
| hasAccel = true | ||
| } | ||
| Sensor.TYPE_MAGNETIC_FIELD -> { | ||
| System.arraycopy(event.values, 0, magnetValues, 0, magnetValues.size) | ||
| hasMagnet = true | ||
| } | ||
| } | ||
|
|
||
| val ready = rotationSensor != null || (hasAccel && hasMagnet) | ||
| if (ready) { | ||
| if (rotationSensor == null) { | ||
| SensorManager.getRotationMatrix(rotationMatrix, null, accelValues, magnetValues) | ||
| } | ||
|
|
||
| SensorManager.getOrientation(rotationMatrix, orientation) | ||
| var azimuth = Math.toDegrees(orientation[0].toDouble()).toFloat() | ||
| var heading = (azimuth + 360) % 360 | ||
|
|
||
| // True north correction using phone location if available | ||
| location?.let { loc -> | ||
| val geomagnetic = GeomagneticField( | ||
| loc.latitude.toFloat(), | ||
| loc.longitude.toFloat(), | ||
| loc.altitude.toFloat(), | ||
| System.currentTimeMillis() | ||
| ) | ||
| heading = (heading + geomagnetic.declination + 360) % 360 | ||
| } | ||
|
|
||
| trySend( | ||
| HeadingState( | ||
| heading = heading, | ||
| hasSensor = true, | ||
| accuracy = event.accuracy | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { | ||
| // No-op | ||
| } | ||
| } | ||
|
|
||
| rotationSensor?.let { sensorManager.registerListener(listener, it, SensorManager.SENSOR_DELAY_UI) } | ||
| if (rotationSensor == null) { | ||
| accelerometer?.let { sensorManager.registerListener(listener, it, SensorManager.SENSOR_DELAY_UI) } | ||
| magnetometer?.let { sensorManager.registerListener(listener, it, SensorManager.SENSOR_DELAY_UI) } | ||
| } | ||
|
|
||
| awaitClose { sensorManager.unregisterListener(listener) } | ||
| } | ||
| } | ||
48 changes: 48 additions & 0 deletions
48
feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /* | ||
| * Copyright (c) 2025 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.feature.node.compass | ||
|
|
||
| import androidx.compose.ui.graphics.Color | ||
| import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig.DisplayUnits | ||
|
|
||
| private const val DEFAULT_TARGET_COLOR_HEX = 0xFFFF9800 | ||
|
|
||
| enum class CompassWarning { | ||
| NO_MAGNETOMETER, | ||
| NO_LOCATION_PERMISSION, | ||
| LOCATION_DISABLED, | ||
| NO_LOCATION_FIX, | ||
| } | ||
|
|
||
| /** Render-ready state for the compass sheet (heading, bearing, distances, and warnings). */ | ||
| data class CompassUiState( | ||
| val targetName: String = "", | ||
| val targetColor: Color = Color(DEFAULT_TARGET_COLOR_HEX), | ||
| val heading: Float? = null, | ||
| val bearing: Float? = null, | ||
| val distanceText: String? = null, | ||
| val bearingText: String? = null, | ||
| val lastUpdateText: String? = null, | ||
| val positionTimeSec: Long? = null, // Epoch seconds for the target position (used for elapsed display) | ||
| val warnings: List<CompassWarning> = emptyList(), | ||
| val errorRadiusText: String? = null, | ||
| val angularErrorDeg: Float? = null, | ||
| val isAligned: Boolean = false, | ||
| val hasTargetPosition: Boolean = true, | ||
| val displayUnits: DisplayUnits = DisplayUnits.METRIC, | ||
| ) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
headingUpdatesfunction accepts alocationparameter but never uses it because it's captured once at function creation time. However, the location needs to be continuously updated for accurate true north correction as the phone moves. Consider either removing the parameter and obtaining location dynamically, or restructuring to accept a Flow<Location?> that can be combined with sensor updates.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had codex implement a suggested fix (on next push)