From bf492e3f8b8d7488962c9d46dc09798fbcb697a8 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Mon, 21 Sep 2026 10:42:45 +0800 Subject: [PATCH] feat(passes/radar): port pass history + compass calibration, cumulative what's new - Pass history (Iu-yang1 fork): PassesSettings.hoursBefore look-back window; second 'Time before' slider in the pass filter; completed passes stay listed (progress clamped to [0,1]) in the history window. - Compass calibration (Iu-yang1 fork): OtherSettings.compassOffsetDegrees, CompassAccuracy from the magnetic sensor, calibration dialog with figure-eight accuracy meter, corrected heading and manual offset field; RadarViewModel applies the offset. - What's New dialog now shows the cumulative 4.4.6->4.4.7 changelog and follows the system language (values/ + values-zh/). - pass_filter_history_hours localized in es/ru/si/tr/uk. --- .../core/data/repository/SatelliteRepo.kt | 6 +- .../core/data/repository/SensorsRepo.kt | 41 +++++- .../core/data/repository/SettingsRepo.kt | 9 +- .../look4sat/core/domain/model/Settings.kt | 8 +- .../core/domain/repository/ISensorsRepo.kt | 11 ++ .../src/main/res/values-es/strings.xml | 1 + .../src/main/res/values-ru/strings.xml | 1 + .../src/main/res/values-si/strings.xml | 1 + .../src/main/res/values-tr/strings.xml | 1 + .../src/main/res/values-uk/strings.xml | 1 + .../src/main/res/values-zh/strings.xml | 31 +++- .../src/main/res/values/strings.xml | 42 +++++- .../look4sat/feature/passes/PassesDialog.kt | 16 +++ .../look4sat/feature/passes/PassesScreen.kt | 2 + .../look4sat/feature/passes/PassesState.kt | 3 + .../feature/passes/PassesViewModel.kt | 21 ++- .../look4sat/feature/radar/RadarViewModel.kt | 11 +- .../feature/settings/SettingsScreen.kt | 134 +++++++++++++++++- .../feature/settings/SettingsState.kt | 8 ++ .../feature/settings/SettingsViewModel.kt | 44 +++++- 20 files changed, 363 insertions(+), 29 deletions(-) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt index 43270d13..f788849c 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt @@ -76,9 +76,11 @@ class SatelliteRepo( .collect { selectedIds -> _satellites.update { localStorage.getEntriesWithIds(selectedIds) } val settings = settingsRepo.passesSettings.value + val now = System.currentTimeMillis() + // 历史过境: 回看窗口 hoursBefore 小时, 过境列表从更早时间开始计算. calculatePasses( - time = System.currentTimeMillis(), - hoursAhead = settings.hoursAhead, + time = now - settings.hoursBefore * 60L * 60L * 1000L, + hoursAhead = settings.hoursBefore + settings.hoursAhead, minElevation = settings.minElevation, aosStartMinute = settings.aosStartMinute, aosEndMinute = settings.aosEndMinute, diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SensorsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SensorsRepo.kt index 05f94ecf..7688d9c2 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SensorsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SensorsRepo.kt @@ -27,6 +27,7 @@ import android.view.Display import android.view.Surface import com.rtbishop.look4sat.core.domain.predict.GeoPos import com.rtbishop.look4sat.core.domain.predict.RAD2DEG +import com.rtbishop.look4sat.core.domain.repository.CompassAccuracy import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -41,7 +42,12 @@ class SensorsRepo( ) : ISensorsRepo, SensorEventListener { private val _sensorData = MutableStateFlow(Pair(0f, 0f)) - private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + private val rotationSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + private val magneticSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) + private val _compassAccuracy = MutableStateFlow( + if (rotationSensor == null || magneticSensor == null) CompassAccuracy.UNAVAILABLE + else CompassAccuracy.UNRELIABLE + ) private val rotationMatrix = FloatArray(9) private val tempMatrix = FloatArray(9) private val orientationValues = FloatArray(3) @@ -50,6 +56,7 @@ class SensorsRepo( private var hasInitialReading = false override val sensorData: StateFlow> = _sensorData + override val compassAccuracy: StateFlow = _compassAccuracy override fun getMagDeclination(geoPos: GeoPos, time: Long): Float { return GeomagneticField( @@ -62,15 +69,41 @@ class SensorsRepo( override fun enableSensor() { hasInitialReading = false - sensor?.let { sensorManager.registerListener(this, it, SENSOR_RATE_US) } + if (rotationSensor == null || magneticSensor == null) { + _compassAccuracy.value = CompassAccuracy.UNAVAILABLE + return + } + _compassAccuracy.value = CompassAccuracy.UNRELIABLE + val rotationRegistered = sensorManager.registerListener(this, rotationSensor, SENSOR_RATE_US) + val magneticRegistered = sensorManager.registerListener(this, magneticSensor, SENSOR_RATE_US) + if (!rotationRegistered || !magneticRegistered) { + sensorManager.unregisterListener(this) + _compassAccuracy.value = CompassAccuracy.UNAVAILABLE + } } override fun disableSensor() = sensorManager.unregisterListener(this) - override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { + if (sensor.type == Sensor.TYPE_MAGNETIC_FIELD) { + updateAccuracy(accuracy) + } + } override fun onSensorChanged(event: SensorEvent) { - if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR) handleSensorEvent(event) + when (event.sensor.type) { + Sensor.TYPE_ROTATION_VECTOR -> handleSensorEvent(event) + Sensor.TYPE_MAGNETIC_FIELD -> updateAccuracy(event.accuracy) + } + } + + private fun updateAccuracy(accuracy: Int) { + _compassAccuracy.value = when (accuracy) { + SensorManager.SENSOR_STATUS_ACCURACY_HIGH -> CompassAccuracy.HIGH + SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM -> CompassAccuracy.MEDIUM + SensorManager.SENSOR_STATUS_ACCURACY_LOW -> CompassAccuracy.LOW + else -> CompassAccuracy.UNRELIABLE + } } private fun getDisplayRotation(): Int { diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt index e0f68c64..7efca483 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt @@ -58,6 +58,7 @@ class SettingsRepo( private val keyBluetoothFrequencyFormat = "bluetoothFrequencyFormat" private val keyFilterShowDeepSpace = "filterShowDeepSpace" private val keyFilterHoursAhead = "filterHoursAhead" + private val keyFilterHoursBefore = "filterHoursBefore" private val keyFilterMinElevation = "filterMinElevation" private val keyFilterAosStartMinute = "filterAosStartMinute" private val keyFilterAosEndMinute = "filterAosEndMinute" @@ -79,6 +80,7 @@ class SettingsRepo( private val keyStateOfAutoUpdate = "stateOfAutoUpdate" private val keyStateOfAutoLotwSync = "stateOfAutoLotwSync" private val keyStateOfSensors = "stateOfSensors" + private val keyCompassOffsetDegrees = "compassOffsetDegrees" private val keyStateOfSweep = "stateOfSweep" private val keyStateOfUtc = "stateOfUtc" private val keyStateOfLightTheme = "stateOfLightTheme" @@ -318,6 +320,7 @@ class SettingsRepo( override fun setPassesSettings(settings: PassesSettings) = preferences.edit { putBoolean(keyFilterShowDeepSpace, settings.showDeepSpace) putInt(keyFilterHoursAhead, settings.hoursAhead) + putInt(keyFilterHoursBefore, settings.hoursBefore) putLong(keyFilterMinElevation, settings.minElevation.toRawBits()) putInt(keyFilterAosStartMinute, settings.aosStartMinute) putInt(keyFilterAosEndMinute, settings.aosEndMinute) @@ -329,6 +332,7 @@ class SettingsRepo( private fun getPassesSettings(): PassesSettings { val showDeepSpace = preferences.getBoolean(keyFilterShowDeepSpace, true) val hoursAhead = preferences.getInt(keyFilterHoursAhead, 24) + val hoursBefore = preferences.getInt(keyFilterHoursBefore, 0).coerceIn(0, 240) val minElevation = Double.fromBits(preferences.getLong(keyFilterMinElevation, 16.0.toRawBits())) val aosStartMinute = preferences.getInt(keyFilterAosStartMinute, 0).coerceIn(0, 23 * 60 + 59) val aosEndMinute = preferences.getInt(keyFilterAosEndMinute, 23 * 60 + 59).coerceIn(0, 23 * 60 + 59) @@ -342,7 +346,8 @@ class SettingsRepo( aosStartMinute, aosEndMinute, invertAosTimeWindow, - selectedModes + selectedModes, + hoursBefore ) } //endregion @@ -542,6 +547,7 @@ class SettingsRepo( putBoolean(keyStateOfAutoUpdate, new.stateOfAutoUpdate) putBoolean(keyStateOfAutoLotwSync, new.stateOfAutoLotwSync) putBoolean(keyStateOfSensors, new.stateOfSensors) + putFloat(keyCompassOffsetDegrees, new.compassOffsetDegrees.coerceIn(-180f, 180f)) putBoolean(keyStateOfSweep, new.stateOfSweep) putBoolean(keyStateOfUtc, new.stateOfUtc) putBoolean(keyStateOfLightTheme, new.stateOfLightTheme) @@ -562,6 +568,7 @@ class SettingsRepo( stateOfAutoUpdate = preferences.getBoolean(keyStateOfAutoUpdate, true), stateOfAutoLotwSync = preferences.getBoolean(keyStateOfAutoLotwSync, true), stateOfSensors = preferences.getBoolean(keyStateOfSensors, true), + compassOffsetDegrees = preferences.getFloat(keyCompassOffsetDegrees, 0f).coerceIn(-180f, 180f), stateOfSweep = preferences.getBoolean(keyStateOfSweep, true), stateOfUtc = preferences.getBoolean(keyStateOfUtc, false), stateOfLightTheme = preferences.getBoolean(keyStateOfLightTheme, false), diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt index b6b50d13..d2fe269d 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt @@ -30,7 +30,9 @@ data class PassesSettings( val aosStartMinute: Int = 0, val aosEndMinute: Int = 23 * 60 + 59, val invertAosTimeWindow: Boolean = false, - val selectedModes: List + val selectedModes: List, + /** 历史过境回看窗口(小时): 过境列表从 now-hoursBefore 开始计算. */ + val hoursBefore: Int = 0 ) data class RCSettings( @@ -71,7 +73,9 @@ data class OtherSettings( val highElevation: Double = 45.0, /** Independent auto-sync toggle for LoTW confirmed grids (separate from the * ephemeris auto-update; only effective after at least one manual sync). */ - val stateOfAutoLotwSync: Boolean = true + val stateOfAutoLotwSync: Boolean = true, + /** 指南针手动航向偏置(度, -180..180): 雷达方位 = 传感器方位 + 磁偏角 + 该偏置. */ + val compassOffsetDegrees: Float = 0f ) data class DataSourcesSettings( diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISensorsRepo.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISensorsRepo.kt index f376c336..2532d260 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISensorsRepo.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISensorsRepo.kt @@ -20,8 +20,19 @@ package com.rtbishop.look4sat.core.domain.repository import com.rtbishop.look4sat.core.domain.predict.GeoPos import kotlinx.coroutines.flow.StateFlow +/** 指南针校准精度等级, 由磁场传感器 accuracy 事件映射而来. */ +enum class CompassAccuracy { + UNAVAILABLE, + UNRELIABLE, + LOW, + MEDIUM, + HIGH +} + interface ISensorsRepo { val sensorData: StateFlow> + /** 指南针当前校准精度 (磁场传感器 accuracy), 用于校准对话框进度. */ + val compassAccuracy: StateFlow fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float fun enableSensor() fun disableSensor() diff --git a/core/presentation/src/main/res/values-es/strings.xml b/core/presentation/src/main/res/values-es/strings.xml index cbb75e02..61e3a2c3 100644 --- a/core/presentation/src/main/res/values-es/strings.xml +++ b/core/presentation/src/main/res/values-es/strings.xml @@ -32,6 +32,7 @@ Filtrar pases Elevación mínima Tiempo por delante + Tiempo anterior Ventana AOS Invertir ventana AOS DeepSpace (período >225min) diff --git a/core/presentation/src/main/res/values-ru/strings.xml b/core/presentation/src/main/res/values-ru/strings.xml index e506bd88..a0b44882 100644 --- a/core/presentation/src/main/res/values-ru/strings.xml +++ b/core/presentation/src/main/res/values-ru/strings.xml @@ -31,6 +31,7 @@ Фильтровать пролеты Минимальная элевация Период расчёта + Прошедший период Окно AOS Инвертировать окно AOS DeepSpace (период >225мин) diff --git a/core/presentation/src/main/res/values-si/strings.xml b/core/presentation/src/main/res/values-si/strings.xml index 24221934..2236c969 100644 --- a/core/presentation/src/main/res/values-si/strings.xml +++ b/core/presentation/src/main/res/values-si/strings.xml @@ -32,6 +32,7 @@ පසුකිරිම් පිරිපහදුව අවම උත්තෝලනය ඉදිරි කාලය + පෙර කාලය AOS කවුළුව AOS කවුළුව ප්‍රතිවිරුද්ධ කරන්න DeepSpace (කාලය >225min) diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index 3db484d4..df091a0a 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -32,6 +32,7 @@ Geçişleri filtrele Minimum yükseklik açısı Gösterilecek zaman + Geçmiş süre AOS aralığı AOS aralığını tersine çevir DeepSpace (periyot >225dk) diff --git a/core/presentation/src/main/res/values-uk/strings.xml b/core/presentation/src/main/res/values-uk/strings.xml index c4cc57b6..fa878bd0 100644 --- a/core/presentation/src/main/res/values-uk/strings.xml +++ b/core/presentation/src/main/res/values-uk/strings.xml @@ -32,6 +32,7 @@ Фільтр прольотів Мінімальне піднесень Час вперед + Час назад Вікно AOS Інвертувати вікно AOS DeepSpace (період >225хв) diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index 59cf3fef..2955d423 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -28,6 +28,7 @@ 筛选过境 仰角值 提前时间 + 先前时间 AOS 时间段 反选 AOS 时间段 DeepSpace(周期 >225分钟) @@ -40,7 +41,22 @@ 请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性 - AMSAT + 4.4.6 以来的全部更新(4.4.7 系列): +\n• 网格模式地图:LoTW 已确认网格绿色填充、漫游网格蓝色条纹、当前网格加粗框;右上角网格/卫星模式开关。 +\n• 首通呼号(VUCC 视图):绿格显示该格最早通联的呼号,非绿格空白。 +\n• worked 网格可按操作网格筛选(含“全部”);点击网格弹出 QSO 明细;每格 QSO 计数。 +\n• 各操作网格独立 VUCC 计数;极区网格标签;反经线修复;漫游条纹密度一致。 +\n• WAPC/WAJA 地图标签随系统语言。 +\n• 网格模式视口与筛选跨页保持。 +\n• LoTW 同步:账号直连、全量/增量模式、实时进度、可取消、启动自动同步(独立开关)、设置页显示上次同步时间。 +\n• 历史过境:过境筛选可回看过去数小时(先前时间)。 +\n• 指南针校准:8 字动作精度指示 + 手动航向偏置。 +\n• AMSAT 状态页:连续相同状态报告计数。 +\n• 过境/雷达:UTC 切换重算过境;日出日落与仰角颜色修复;卫星模糊搜索;相互过境页滚动保持、跟随 UTC、雷达快捷入口。 +\n• CW 解码器支持 32/64 位设备;Android 17 (API 37) 适配。 + + +AMSAT AMSAT 卫星状态 刷新 重试 @@ -241,6 +257,19 @@ 自动同步 LoTW 已确认网格 启用雷达扫描动画 使用传感器旋转雷达视图 + 校准指南针 + 指南针校准 + 缓慢移动手机画“8”字,直到精度提高。请远离磁铁和金属物体。 + 精度:%s + 高——校准完成 + 中——请继续移动 + 低——请继续移动 + 不可靠——请开始“8”字动作 + 设备没有可用的磁场传感器 + 校正后航向:%.1f° + 手动航向偏置(°) + 范围 −180° 至 +180°;正值为顺时针偏移。 + 偏置归零 启用红色夜间模式 地图显示 Maidenhead 网格 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 3f3592a3..e9c9c58b 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -33,6 +33,7 @@ Filter passes Minimal elevation Time ahead + Time before AOS window Invert AOS window DeepSpace (period >225min) @@ -51,9 +52,31 @@ Make sure to set your position via GPS, Lat/Lon or QTH in Settings. \n\nPlease update the database at least weekly to get accurate predictions. What\'s new in Look4Sat - - * Added required tweaks to support Android 17 (API 37) - \n\n* Added ACCESS_LOCAL_NETWORK permission check to support network data output + All changes since 4.4.6 (the 4.4.7 series): + +• Grid-mode map: LoTW confirmed grids in green, roamed grids with blue stripes, current grid highlighted; mode switch pill at the map's top-right. + +• First-call labels (VUCC view): show the first callsign worked in each green grid instead of the grid code; non-worked cells stay empty. + +• Worked grids filtered by operated grid (with an All option); tap a grid to see its QSO detail dialog; per-grid QSO counts. + +• Per-operated-grid VUCC counting; polar grid labels; antimeridian fixes; consistent roamed-stripe density. + +• WAPC/WAJA map labels follow the system language. + +• Grid-mode viewport and award filter survive page switches. + +• LoTW sync: direct login, full/incremental modes, live progress, cancel button, auto-sync on app start with its own toggle, last-sync time shown. + +• Pass history: filter can look back over past passes (Time before). + +• Compass calibration: figure-eight accuracy meter plus manual heading offset. + +• AMSAT status page: consecutive same-status report counts. + +• Passes/radar: UTC toggle recalculates passes; sunrise/sunset and elevation color fixes; satellite fuzzy search; mutual page keeps scroll, follows UTC, radar shortcut. + +• CW decoder for 32/64-bit devices; Android 17 (API 37) support. @@ -275,6 +298,19 @@ Auto-sync LoTW confirmed grids Enable radar sweep animation Use sensors to rotate radar view + Calibrate compass + Compass calibration + Move the phone slowly in a figure-eight until accuracy improves. Keep it away from magnets and metal. + Accuracy: %s + High — calibration complete + Medium — keep moving + Low — keep moving + Unreliable — start the figure-eight motion + Magnetic sensor unavailable + Corrected heading: %.1f° + Manual heading offset (°) + Range −180° to +180°. Positive values rotate clockwise. + Reset offset Enable red night mode filter Show Maidenhead grid on map diff --git a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesDialog.kt b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesDialog.kt index cc1c37ef..95982618 100644 --- a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesDialog.kt +++ b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesDialog.kt @@ -73,6 +73,7 @@ private val allModes = listOf( ) private val hourSteps = listOf(1, 2, 4, 8, 12, 24, 48, 72, 96, 120, 144, 168, 192, 216, 240) +private val historyHourSteps = listOf(0) + hourSteps private const val dayMinutes = 24 * 60 private const val minuteStep = 15 private const val endOfDayMinute = dayMinutes - 1 @@ -80,6 +81,7 @@ private const val quarterHourSlots = dayMinutes / minuteStep data class PassFilterParams( val hours: Int, + val hoursBefore: Int, val elevation: Double, val lowElevation: Double, val highElevation: Double, @@ -95,6 +97,7 @@ private fun PassesDialogPreview() { MainTheme { PassesFilterDialog( hours = 24, + hoursBefore = 0, elevation = 16.0, lowElevation = 16.0, highElevation = 65.0, @@ -111,6 +114,7 @@ private fun PassesDialogPreview() { @Composable internal fun PassesFilterDialog( hours: Int, + hoursBefore: Int, elevation: Double, lowElevation: Double, highElevation: Double, @@ -122,6 +126,9 @@ internal fun PassesFilterDialog( accept: (PassFilterParams) -> Unit ) { val hoursIndex = remember { mutableIntStateOf(hourSteps.indexOfFirst { it >= hours }.coerceAtLeast(0)) } + val hoursBeforeIndex = remember { + mutableIntStateOf(historyHourSteps.indexOfFirst { it >= hoursBefore }.coerceAtLeast(0)) + } val elevationValueNew = remember { mutableDoubleStateOf(elevation) } val highlightBounds = 0f..90f var highlightRange by remember(lowElevation, highElevation) { @@ -136,6 +143,7 @@ internal fun PassesFilterDialog( accept( PassFilterParams( hours = hourSteps[hoursIndex.intValue], + hoursBefore = historyHourSteps[hoursBeforeIndex.intValue], elevation = elevationValueNew.doubleValue, lowElevation = highlightRange.start.roundToInt().toDouble(), highElevation = highlightRange.endInclusive.roundToInt().toDouble(), @@ -164,6 +172,14 @@ internal fun PassesFilterDialog( valueRange = 0f..(hourSteps.size - 1).toFloat(), steps = hourSteps.size - 2 ) { hoursIndex.intValue = it.toInt().coerceIn(0, hourSteps.size - 1) } + SliderRow( + title = stringResource(R.string.pass_filter_history_hours), + value = hoursBeforeIndex.intValue.toDouble(), + displayValue = formatHoursLabel(historyHourSteps[hoursBeforeIndex.intValue]), + valueResId = R.drawable.ic_clock, + valueRange = 0f..(historyHourSteps.size - 1).toFloat(), + steps = historyHourSteps.size - 2 + ) { hoursBeforeIndex.intValue = it.toInt().coerceIn(0, historyHourSteps.size - 1) } ToggleRow( title = stringResource(R.string.pass_filter_deep_space), checked = deepSpaceEnabled, diff --git a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt index ace6618e..b3449f41 100644 --- a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt +++ b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt @@ -103,6 +103,7 @@ private fun PassesScreen( if (uiState.isPassesDialogShown) { PassesFilterDialog( hours = uiState.hours, + hoursBefore = uiState.hoursBefore, elevation = uiState.elevation, lowElevation = uiState.lowElevation, highElevation = uiState.highElevation, @@ -115,6 +116,7 @@ private fun PassesScreen( onAction( PassesAction.FilterPasses( hoursAhead = params.hours, + hoursBefore = params.hoursBefore, minElevation = params.elevation, lowElevation = params.lowElevation, highElevation = params.highElevation, diff --git a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesState.kt b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesState.kt index 3aef6726..a93d7a96 100644 --- a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesState.kt +++ b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesState.kt @@ -28,6 +28,8 @@ data class PassesState( val nextTime: String = "00:00:00", val isNextTimeAos: Boolean = true, val hours: Int = 24, + /** 历史过境回看窗口(小时), 0 = 只看未来. */ + val hoursBefore: Int = 0, val elevation: Double = 16.0, val lowElevation: Double = 16.0, val highElevation: Double = 65.0, @@ -47,6 +49,7 @@ sealed interface PassesAction { data object DismissWhatsNew : PassesAction data class FilterPasses( val hoursAhead: Int, + val hoursBefore: Int, val minElevation: Double, val lowElevation: Double, val highElevation: Double, diff --git a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt index 14f410d3..37db5912 100644 --- a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt +++ b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt @@ -55,6 +55,7 @@ class PassesViewModel( isUtc = settingsRepo.otherSettings.value.stateOfUtc, nextPass = defaultPass, hours = settingsRepo.passesSettings.value.hoursAhead, + hoursBefore = settingsRepo.passesSettings.value.hoursBefore, elevation = settingsRepo.passesSettings.value.minElevation, lowElevation = settingsRepo.otherSettings.value.lowElevation, highElevation = settingsRepo.otherSettings.value.highElevation, @@ -123,6 +124,7 @@ class PassesViewModel( is PassesAction.FilterPasses -> applyFilter( hoursAhead = action.hoursAhead, + hoursBefore = action.hoursBefore, minElevation = action.minElevation, lowElevation = action.lowElevation, highElevation = action.highElevation, @@ -135,6 +137,7 @@ class PassesViewModel( is PassesAction.FilterRadios -> applyFilter( hoursAhead = _uiState.value.hours, + hoursBefore = _uiState.value.hoursBefore, minElevation = _uiState.value.elevation, lowElevation = _uiState.value.lowElevation, highElevation = _uiState.value.highElevation, @@ -246,7 +249,7 @@ class PassesViewModel( return ordered } - /** Computes live progress for each pass, filtering out expired ones. */ + /** Computes live progress while retaining completed passes in the requested history window. */ private fun computePassProgress(passList: List, time: Long): List { val result = ArrayList(passList.size) for (pass in passList) { @@ -254,9 +257,9 @@ class PassesViewModel( val deltaNow = time.minus(pass.aosTime).toFloat() val deltaTotal = pass.losTime.minus(pass.aosTime).toFloat() val newProgress = (deltaNow / deltaTotal).round(2) - if (newProgress >= 1.0f) continue - if (newProgress != pass.progress) { - result.add(pass.copy(progress = newProgress)) + val boundedProgress = newProgress.coerceIn(0f, 1f) + if (boundedProgress != pass.progress) { + result.add(pass.copy(progress = boundedProgress)) } else { result.add(pass) } @@ -285,6 +288,7 @@ class PassesViewModel( private fun applyFilter( hoursAhead: Int, + hoursBefore: Int, minElevation: Double, lowElevation: Double, highElevation: Double, @@ -302,13 +306,15 @@ class PassesViewModel( aosStartMinute, aosEndMinute, invertAosTimeWindow, - modes + modes, + hoursBefore ) ) settingsRepo.updateOtherSettings { it.copy(lowElevation = lowElevation, highElevation = highElevation) } _uiState.update { it.copy( hours = hoursAhead, + hoursBefore = hoursBefore, elevation = minElevation, lowElevation = lowElevation, highElevation = highElevation, @@ -319,9 +325,10 @@ class PassesViewModel( modes = modes ) } + val now = System.currentTimeMillis() satelliteRepo.calculatePasses( - time = System.currentTimeMillis(), - hoursAhead = hoursAhead, + time = now - hoursBefore * 60L * 60L * 1000L, + hoursAhead = hoursBefore + hoursAhead, minElevation = minElevation, aosStartMinute = aosStartMinute, aosEndMinute = aosEndMinute, diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt index 6f84be11..0e2354e6 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt @@ -82,7 +82,7 @@ class RadarViewModel( private val _uiState = MutableStateFlow( RadarState( isUtc = settingsRepo.otherSettings.value.stateOfUtc, - orientationValues = sensorsRepo.sensorData.value, + orientationValues = correctedOrientation(sensorsRepo.sensorData.value), shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep, shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors, sstv = SstvSubState(selectedMode = settingsRepo.otherSettings.value.sstvMode) @@ -102,12 +102,19 @@ class RadarViewModel( viewModelScope.launch { sensorsRepo.enableSensor() sensorsRepo.sensorData.collect { data -> - val orientationValues = (data.first + magDeclination) to data.second + val orientationValues = correctedOrientation(data) _uiState.update { it.copy(orientationValues = orientationValues) } } } } + /** 指南针方位 = 传感器方位 + 磁偏角 + 手动偏置(compassOffsetDegrees, 校准用). */ + private fun correctedOrientation(data: Pair): Pair { + val offset = settingsRepo.otherSettings.value.compassOffsetDegrees + val azimuth = ((data.first + magDeclination + offset) % 360f + 360f) % 360f + return azimuth to data.second + } + private fun collectSettingsChanges() { viewModelScope.launch { settingsRepo.otherSettings.collectLatest { settings -> diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt index f7fb9872..c7160fc4 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt @@ -38,8 +38,10 @@ import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -56,6 +58,8 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -66,9 +70,11 @@ import com.rtbishop.look4sat.core.domain.model.OtherSettings import com.rtbishop.look4sat.core.domain.model.RadioControlSettings import com.rtbishop.look4sat.core.domain.model.WavelogSettings import com.rtbishop.look4sat.core.domain.predict.GeoPos +import com.rtbishop.look4sat.core.domain.repository.CompassAccuracy import com.rtbishop.look4sat.core.domain.repository.IContainerProvider import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode import com.rtbishop.look4sat.core.presentation.CardButton +import com.rtbishop.look4sat.core.presentation.SharedDialog import com.rtbishop.look4sat.core.presentation.IconCard import com.rtbishop.look4sat.core.presentation.MainTheme import com.rtbishop.look4sat.core.presentation.PrimaryIconCard @@ -204,6 +210,19 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> onSave = { onAction(SettingsAction.UpdateRadioControl(it)) } ) } + if (dialogs.compassCalibration) { + LaunchedEffect(Unit) { onAction(SettingsAction.StartCompassCalibration) } + CompassCalibrationDialog( + accuracy = uiState.compassAccuracy, + headingDegrees = uiState.compassHeadingDegrees, + initialOffsetDegrees = uiState.otherSettings.compassOffsetDegrees, + onDismiss = { + onAction(SettingsAction.StopCompassCalibration) + dialogs.compassCalibration = false + }, + onSave = { onAction(SettingsAction.SetCompassOffset(it)) } + ) + } if (dialogs.lotw) { // LoTW sync failures are typed codes from the ViewModel; render them // through string resources so the dialog follows the system language. @@ -342,7 +361,13 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> showLoTWDialog = { dialogs.lotw = true } ) } - item { OtherCard(uiState.otherSettings, onAction) } + item { + OtherCard( + settings = uiState.otherSettings, + onCompassCalibration = { dialogs.compassCalibration = true }, + onAction = onAction + ) + } item { CardCredits() } item(span = { GridItemSpan(maxLineSpan) }) { CardButton( @@ -530,15 +555,19 @@ private fun OtherCardPreview() = MainTheme { shouldSeeWarning = false, shouldSeeWhatsNew = false ) - OtherCard(settings = values) {} + OtherCard(settings = values, onCompassCalibration = {}, onAction = {}) } @Composable -private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit) { +private fun OtherCard( + settings: OtherSettings, + onCompassCalibration: () -> Unit, + onAction: (SettingsAction) -> Unit +) { ElevatedCard( modifier = Modifier .fillMaxWidth() - .height(320.dp) + .height(370.dp) ) { Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { Text( @@ -560,6 +589,11 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni SwitchRow(R.string.prefs_other_switch_sensors, settings.stateOfSensors) { onAction(SettingsAction.ToggleSensor(it)) } + CardButton( + onClick = onCompassCalibration, + text = stringResource(R.string.prefs_compass_calibration_button), + modifier = Modifier.fillMaxWidth() + ) SwitchRow(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) { onAction(SettingsAction.ToggleNightMode(it)) } @@ -567,6 +601,94 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni } } +/** + * 指南针校准对话框: 8 字动作提升精度, 实时显示校正后航向, 可手动输入偏置(±180°). + */ +@Composable +private fun CompassCalibrationDialog( + accuracy: CompassAccuracy, + headingDegrees: Float, + initialOffsetDegrees: Float, + onDismiss: () -> Unit, + onSave: (Float) -> Unit +) { + var offsetText by rememberSaveable(initialOffsetDegrees) { + mutableStateOf(initialOffsetDegrees.toString()) + } + val parsedOffset = offsetText.replace(',', '.').toFloatOrNull()?.takeIf { it in -180f..180f } + val statusRes = when (accuracy) { + CompassAccuracy.HIGH -> R.string.prefs_compass_accuracy_high + CompassAccuracy.MEDIUM -> R.string.prefs_compass_accuracy_medium + CompassAccuracy.LOW -> R.string.prefs_compass_accuracy_low + CompassAccuracy.UNRELIABLE -> R.string.prefs_compass_accuracy_unreliable + CompassAccuracy.UNAVAILABLE -> R.string.prefs_compass_accuracy_unavailable + } + val progress = when (accuracy) { + CompassAccuracy.HIGH -> 1f + CompassAccuracy.MEDIUM -> 0.66f + CompassAccuracy.LOW -> 0.33f + CompassAccuracy.UNRELIABLE, CompassAccuracy.UNAVAILABLE -> 0f + } + SharedDialog( + title = stringResource(R.string.prefs_compass_calibration_title), + onDismissRequest = onDismiss, + onCancel = onDismiss, + onAccept = { + parsedOffset?.let { + onSave(it) + onDismiss() + } + } + ) { padding -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(horizontal = padding) + ) { + Text( + text = "∞", + fontSize = 92.sp, + color = MaterialTheme.colorScheme.primary, + lineHeight = 92.sp + ) + Text( + text = stringResource(R.string.prefs_compass_calibration_instruction), + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.prefs_compass_accuracy, stringResource(statusRes)), + fontWeight = FontWeight.Medium, + color = if (accuracy == CompassAccuracy.HIGH) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + } + ) + LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth()) + Text( + text = stringResource(R.string.prefs_compass_heading, headingDegrees), + fontSize = 20.sp, + fontWeight = FontWeight.Medium + ) + OutlinedTextField( + value = offsetText, + onValueChange = { offsetText = it }, + label = { Text(stringResource(R.string.prefs_compass_offset)) }, + supportingText = { Text(stringResource(R.string.prefs_compass_offset_support)) }, + isError = parsedOffset == null, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Done), + modifier = Modifier.fillMaxWidth() + ) + CardButton( + onClick = { offsetText = "0" }, + text = stringResource(R.string.prefs_compass_offset_reset), + modifier = Modifier.fillMaxWidth() + ) + } + } +} + @Composable private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { Row( @@ -741,6 +863,7 @@ private class DialogVisibility { var radioControl by mutableStateOf(false) var wavelog by mutableStateOf(false) var lotw by mutableStateOf(false) + var compassCalibration by mutableStateOf(false) } @Composable @@ -748,13 +871,14 @@ private fun rememberDialogVisibility(): DialogVisibility { return rememberSaveable(saver = run { androidx.compose.runtime.saveable.Saver( save = { - listOf(it.position, it.locator, it.dataSources, it.network, it.bluetooth, it.radioControl, it.wavelog, it.lotw) + listOf(it.position, it.locator, it.dataSources, it.network, it.bluetooth, it.radioControl, it.wavelog, it.lotw, it.compassCalibration) }, restore = { DialogVisibility().apply { position = it[0]; locator = it[1]; dataSources = it[2] network = it[3]; bluetooth = it[4]; radioControl = it[5]; wavelog = it[6] lotw = it.getOrElse(7) { false } + compassCalibration = it.getOrElse(8) { false } } } ) diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt index 818e4334..924badfc 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt @@ -24,6 +24,7 @@ import com.rtbishop.look4sat.core.domain.model.RCSettings import com.rtbishop.look4sat.core.domain.model.RadioControlSettings import com.rtbishop.look4sat.core.domain.model.WavelogSettings import com.rtbishop.look4sat.core.domain.predict.GeoPos +import com.rtbishop.look4sat.core.domain.repository.CompassAccuracy import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode import java.io.File @@ -67,6 +68,10 @@ data class SettingsState( val lotwError: LoTWError? = null, /** Epoch ms of the last successful LoTW sync (0 = never) — shown like the ephemeris update time. */ val lotwLastSyncEpochMs: Long = 0L, + /** 指南针校准精度等级 (校准对话框进度条). */ + val compassAccuracy: CompassAccuracy = CompassAccuracy.UNRELIABLE, + /** 校正后航向(度, 含磁偏角+手动偏置), 校准对话框实时显示. */ + val compassHeadingDegrees: Float = 0f, val updateChecker: UpdateCheckerState = UpdateCheckerState() ) @@ -98,6 +103,9 @@ sealed interface SettingsAction { data class ToggleAutoLotwSync(val value: Boolean) : SettingsAction data class ToggleSweep(val value: Boolean) : SettingsAction data class ToggleSensor(val value: Boolean) : SettingsAction + data object StartCompassCalibration : SettingsAction + data object StopCompassCalibration : SettingsAction + data class SetCompassOffset(val degrees: Float) : SettingsAction data class ToggleLightTheme(val value: Boolean) : SettingsAction data class ToggleNightMode(val value: Boolean) : SettingsAction diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt index fa8dc721..4bdeccb8 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo import com.rtbishop.look4sat.core.domain.model.WavelogSettings import com.rtbishop.look4sat.core.domain.repository.IMainContainer +import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo import com.rtbishop.look4sat.core.domain.repository.IUpdateRepository import com.rtbishop.look4sat.core.domain.repository.LoTWResult @@ -49,6 +50,7 @@ class SettingsViewModel( private val updateRepo: IUpdateRepository, private val wavelogRepo: IWavelogRepository, private val lotwRepo: com.rtbishop.look4sat.core.domain.repository.ILoTWRepository, + private val sensorsRepo: ISensorsRepo, private val apkFile: File, private val showToast: IShowToast ) : ViewModel() { @@ -57,6 +59,8 @@ class SettingsViewModel( private val defaultDataSettings = DataSettings(false, 0, 0, 0L) /** In-flight LoTW sync job, cancelled by CancelLoTWSync. */ private var lotwSyncJob: kotlinx.coroutines.Job? = null + /** 指南针校准: 传感器原始方位(未加磁偏角/偏置), 校准对话框实时显示校正后航向. */ + private var rawCompassHeading = sensorsRepo.sensorData.value.first private val _uiState = MutableStateFlow( SettingsState( appVersionName = settingsRepo.appVersionName, @@ -69,6 +73,8 @@ class SettingsViewModel( dataSourcesStatus = settingsRepo.dataSourcesStatus.value, wavelogSettings = settingsRepo.wavelogSettings.value, workedGridsCount = settingsRepo.getWorkedGrids().size, + compassAccuracy = sensorsRepo.compassAccuracy.value, + compassHeadingDegrees = correctedCompassHeading(), lotwLastSyncEpochMs = lotwCursorEpochMs(settingsRepo.getLastLotwSyncDate()) ) ) @@ -79,7 +85,10 @@ class SettingsViewModel( viewModelScope.launch { settingsRepo.stationPosition.collect { geoPos -> _uiState.update { - it.copy(positionSettings = it.positionSettings.copy(isUpdating = false, stationPos = geoPos)) + it.copy( + positionSettings = it.positionSettings.copy(isUpdating = false, stationPos = geoPos), + compassHeadingDegrees = correctedCompassHeading() + ) } } } @@ -104,7 +113,20 @@ class SettingsViewModel( } viewModelScope.launch { settingsRepo.otherSettings.collect { settings -> - _uiState.update { it.copy(otherSettings = settings) } + _uiState.update { + it.copy(otherSettings = settings, compassHeadingDegrees = correctedCompassHeading()) + } + } + } + viewModelScope.launch { + sensorsRepo.compassAccuracy.collect { accuracy -> + _uiState.update { it.copy(compassAccuracy = accuracy) } + } + } + viewModelScope.launch { + sensorsRepo.sensorData.collect { orientation -> + rawCompassHeading = orientation.first + _uiState.update { it.copy(compassHeadingDegrees = correctedCompassHeading()) } } } viewModelScope.launch { @@ -157,6 +179,11 @@ class SettingsViewModel( is SettingsAction.ToggleAutoLotwSync -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoLotwSync = action.value) } is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) } is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) } + SettingsAction.StartCompassCalibration -> sensorsRepo.enableSensor() + SettingsAction.StopCompassCalibration -> sensorsRepo.disableSensor() + is SettingsAction.SetCompassOffset -> settingsRepo.updateOtherSettings { + it.copy(compassOffsetDegrees = action.degrees.coerceIn(-180f, 180f)) + } is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) } is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) } // Remote control & data sources @@ -358,6 +385,18 @@ class SettingsViewModel( } } + /** 校正后航向 = 传感器原始方位 + 磁偏角 + 手动偏置. */ + private fun correctedCompassHeading(): Float { + val declination = sensorsRepo.getMagDeclination(settingsRepo.stationPosition.value) + val offset = settingsRepo.otherSettings.value.compassOffsetDegrees + return ((rawCompassHeading + declination + offset) % 360f + 360f) % 360f + } + + override fun onCleared() { + sensorsRepo.disableSensor() + super.onCleared() + } + // endregion // region Data update helpers — consolidated from 3 near-identical functions @@ -405,6 +444,7 @@ class SettingsViewModel( updateRepo = container.updateRepo, wavelogRepo = container.wavelogRepo, lotwRepo = container.lotwRepo, + sensorsRepo = container.provideSensorsRepo(), apkFile = File(context.cacheDir, "look4sat-update.apk"), showToast = container.provideShowToast() )