Compare commits

..
3 Commits
16 changed files with 179 additions and 179 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
# GitHub Copilot Instructions
Read `AGENTS.md` first, then `CLAUDE.md`.
`AGENTS.md` contains the architecture, module boundaries, implementation details, conventions, and gotchas.
+113
View File
@@ -0,0 +1,113 @@
# Look4Sat AI Agent Instructions
This is the canonical project guide for all AI assistants working on Look4Sat.
All assistant-specific files (`CLAUDE.md`, `.github/copilot-instructions.md`) point here.
---
## Project Overview
Look4Sat is an open-source, fully offline Android satellite tracker and pass predictor. It tracks 9000+ active
satellites using TLE/OMM data from Celestrak/SatNOGS, calculates orbital positions via SGP4/SDP4 models, and displays
passes relative to the user's location. Features include polar radar visualization, SSTV image decoding, satellite
ground track mapping, and pass predictions up to 10 days ahead. No ads, no tracking, no network required after initial
data download.
## Architecture
**MVI (Model-View-Intent)** with unidirectional data flow:
- `State` data class → exposed via `StateFlow` from ViewModel
- `Action` sealed interface → user intents dispatched to ViewModel's `onAction()`
- Jetpack Compose UI observes state and recomposes reactively
**Clean Architecture layers:**
| Module | Responsibility |
|----------------------|---------------------------------------------------------------------|
| `app` | Entry point. Aggregates all modules |
| `core:data` | Android library. Room DB, OkHttp networking, repo implementations |
| `core:domain` | Pure Kotlin (JVM). Orbital math (SGP4/SDP4), models, repo contracts |
| `core:presentation` | Android library. Compose theme, shared UI components, NavKeys |
| `feature:map` | OSMDroid map with ground tracks |
| `feature:passes` | Pass predictions and upcoming events |
| `feature:radar` | Polar radar view of satellite positions, SSTV image decoding |
| `feature:satellites` | Satellite list, filtering, selection |
| `feature:settings` | User preferences |
- `feature:*` modules depend only on `core:domain` + `core:presentation`. Features never depend on each other.
## Build & Run
```shell
# Debug build
./gradlew assembleDebug
# Release build (minified, shrunk resources)
./gradlew assembleRelease
# Run tests
./gradlew test
```
- **Min SDK**: 24 | **Target SDK**: 36 | **JDK**: 17
- **Gradle**: Uses version catalog (`gradle/libs.versions.toml`) + convention plugins in `build-logic/`
## Key Libraries
- **Compose** (BOM 2026.05.01) + Material3 Adaptive
- **Navigation3** (type-safe, uses `@Serializable` NavKeys)
- **Room** (KSP code generation) for local satellite/TLE storage
- **OkHttp** 5.x for TLE downloads
- **OSMDroid** for map rendering
- **Kotlin Serialization** for navigation args and data parsing
- **Coroutines** + `StateFlow` for async/reactive patterns
## Conventions
- **Minimal dependencies**: Avoid adding libraries when a simple manual solution exists. Fewer deps = less maintenance.
- **DI**: Manual — ViewModels use companion `factory()` methods with `IMainContainer` interface.
- **Navigation**: Type-safe Compose Navigation3 with `@Serializable` data classes as nav keys.
- **State naming**: `<Feature>State` data class + `<Feature>Action` sealed interface per feature.
- **No feature-to-feature deps**: All cross-feature communication goes through core layers.
- **Localization**: 7 languages (en, es, ru, si, tr, uk, zh).
## Data Formats & Migration
**TLE vs. OMM/CSV format:**
Look4Sat supports both TLE and OMM (Orbit Mean-Elements Message) formats for backward compatibility:
- **TLE format**: Traditional 3-line element format (deprecated). NORAD catalog numbers are 5-digit integers, which
are running out of space. Celestrak has signaled that TLE format will eventually be phased out.
- **OMM/CSV format**: The future standard. CSV files contain the same orbital parameters as TLE but use ISO 8601
timestamps and support larger NORAD IDs. Celestrak and SatNOGS already provide OMM data in CSV format.
**Current implementation:**
- `DataParser.kt` handles both `parseTLEStream()` and `parseCSVStream()` seamlessly
- TLE data is downloaded from configured sources and stored in Room database
- When downloading satellite data, the app automatically detects format and parses accordingly
- Both formats produce identical `OrbitalData` objects, ensuring transparent format switching
**Migration path:**
As NORAD catalog space becomes constrained, OMM/CSV will become the primary format. Look4Sat is already positioned
to handle this transition without code changes — existing users can continue using TLE files while new sources
transition to OMM/CSV automatically.
## Code Style
- Prefer **short, focused functions** — single responsibility, easy to read.
- **Exceptions**: Composable functions and math-heavy algorithms (SGP4/SDP4) may be longer.
- Strict code style — no dead code, no unused imports, consistent formatting.
## Roadmap
- **KMP migration**: `core:domain` is to become a fully shareable KMM module. Keep it pure Kotlin/JVM.
## Gotchas
- Orbital math lives in `core:domain/predict/` — it's dense vector math (SGP4/SDP4). Tread carefully.
- TLE/OMM data must be refreshed weekly for accurate predictions (satellite orbits decay). TLE format is legacy and
will eventually be deprecated in favor of OMM/CSV as NORAD catalog numbers approach the 5-digit limit.
- SSTV decoding in `feature:radar` is experimental; image quality depends on signal strength during satellite pass.
- `build-logic/convention/` contains all shared Gradle configuration — edit there, not in individual modules.
- ProGuard is enabled for release builds — don't add reflection-based libs or any other dependencies without asking.
+2 -107
View File
@@ -1,110 +1,5 @@
# CLAUDE.md # CLAUDE.md
## Project Overview Read `AGENTS.md` first, then follow the instructions there.
Look4Sat is an open-source, fully offline Android satellite tracker and pass predictor. It tracks 9000+ active `AGENTS.md` contains the architecture, module boundaries, implementation details, conventions, and gotchas.
satellites using TLE/OMM data from Celestrak/SatNOGS, calculates orbital positions via SGP4/SDP4 models, and displays
passes relative to the user's location. Features include polar radar visualization, SSTV image decoding, satellite
ground track mapping, and pass predictions up to 10 days ahead. No ads, no tracking, no network required after initial
data download.
## Architecture
**MVI (Model-View-Intent)** with unidirectional data flow:
- `State` data class → exposed via `StateFlow` from ViewModel
- `Action` sealed interface → user intents dispatched to ViewModel's `onAction()`
- Jetpack Compose UI observes state and recomposes reactively
**Clean Architecture layers:**
| ------------------------ | --------------------------------------------------------------------- |
| Module | Responsibility |
|--------------------------|-----------------------------------------------------------------------|
| `app` | Entry point. Aggregates all modules |
| `core:data` | Android library. Room DB, OkHttp networking, repo implementations |
| `core:domain` | Pure Kotlin (JVM). Orbital math (SGP4/SDP4), models, repo contracts |
| `core:presentation` | Android library. Compose theme, shared UI components, NavKeys |
| `feature:map` | OSMDroid map with ground tracks |
| `feature:passes` | Pass predictions and upcoming events |
| `feature:radar` | Polar radar view of satellite positions, SSTV image decoding |
| `feature:satellites` | Satellite list, filtering, selection |
| `feature:settings` | User preferences |
| ------------------------ | --------------------------------------------------------------------- |
- `feature:*` modules depend only on `core:domain` + `core:presentation`. Features never depend on each other.
## Build & Run
```shell
# Debug build
./gradlew assembleDebug
# Release build (minified, shrunk resources)
./gradlew assembleRelease
# Run tests
./gradlew test
```
- **Min SDK**: 24 | **Target SDK**: 36 | **JDK**: 17
- **Gradle**: Uses version catalog (`gradle/libs.versions.toml`) + convention plugins in `build-logic/`
## Key Libraries
- **Compose** (BOM 2026.05.01) + Material3 Adaptive
- **Navigation3** (type-safe, uses `@Serializable` NavKeys)
- **Room** (KSP code generation) for local satellite/TLE storage
- **OkHttp** 5.x for TLE downloads
- **OSMDroid** for map rendering
- **Kotlin Serialization** for navigation args and data parsing
- **Coroutines** + `StateFlow` for async/reactive patterns
## Conventions
- **Minimal dependencies**: Avoid adding libraries when a simple manual solution exists. Fewer deps = less maintenance.
- **DI**: Manual — ViewModels use companion `factory()` methods with `IMainContainer` interface.
- **Navigation**: Type-safe Compose Navigation3 with `@Serializable` data classes as nav keys.
- **State naming**: `<Feature>State` data class + `<Feature>Action` sealed interface per feature.
- **No feature-to-feature deps**: All cross-feature communication goes through core layers.
- **Localization**: 7 languages (en, es, ru, si, tr, uk, zh).
## Data Formats & Migration
**TLE vs. OMM/CSV format:**
Look4Sat supports both TLE and OMM (Orbit Mean-Elements Message) formats for backward compatibility:
- **TLE format**: Traditional 3-line element format (deprecated). NORAD catalog numbers are 5-digit integers, which
are running out of space. Celestrak has signaled that TLE format will eventually be phased out.
- **OMM/CSV format**: The future standard. CSV files contain the same orbital parameters as TLE but use ISO 8601
timestamps and support larger NORAD IDs. Celestrak and SatNOGS already provide OMM data in CSV format.
**Current implementation:**
- `DataParser.kt` handles both `parseTLEStream()` and `parseCSVStream()` seamlessly
- TLE data is downloaded from configured sources and stored in Room database
- When downloading satellite data, the app automatically detects format and parses accordingly
- Both formats produce identical `OrbitalData` objects, ensuring transparent format switching
**Migration path:**
As NORAD catalog space becomes constrained, OMM/CSV will become the primary format. Look4Sat is already positioned
to handle this transition without code changes—existing users can continue using TLE files while new sources transition
to OMM/CSV automatically.
## Code Style
- Prefer **short, focused functions** — single responsibility, easy to read.
- **Exceptions**: Composable functions and math-heavy algorithms (SGP4/SDP4) may be longer.
- Strict code style — no dead code, no unused imports, consistent formatting.
## Roadmap
- **KMP migration**: `core:domain` is to become a fully shareable KMM module. Keep it pure Kotlin/JVM.
## Gotchas
- Orbital math lives in `core:domain/predict/` — it's dense vector math (SGP4/SDP4). Tread carefully.
- TLE/OMM data must be refreshed weekly for accurate predictions (satellite orbits decay). TLE format is legacy and
will eventually be deprecated in favor of OMM/CSV as NORAD catalog numbers approach the 5-digit limit.
- SSTV decoding in `feature:radar` is experimental; image quality depends on signal strength during satellite pass.
- `build-logic/convention/` contains all shared Gradle configuration — edit there, not in individual modules.
- ProGuard is enabled for release builds — don't add reflection-based libs or any other dependencies without asking.
+1
View File
@@ -8,5 +8,6 @@ plugins {
} }
tasks.register("clean", Delete::class.java) { tasks.register("clean", Delete::class.java) {
description = "Cleans the build directory"
delete(rootProject.layout.buildDirectory) delete(rootProject.layout.buildDirectory)
} }
@@ -19,10 +19,9 @@ package com.rtbishop.look4sat.core.data.injection
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.content.Context import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorManager import android.hardware.SensorManager
import android.hardware.display.DisplayManager
import android.location.LocationManager import android.location.LocationManager
import android.view.WindowManager
import androidx.room.Room import androidx.room.Room
import com.rtbishop.look4sat.core.data.database.Look4SatDb import com.rtbishop.look4sat.core.data.database.Look4SatDb
import com.rtbishop.look4sat.core.data.framework.BluetoothReporter import com.rtbishop.look4sat.core.data.framework.BluetoothReporter
@@ -120,9 +119,8 @@ class MainContainer(private val context: Context) : IMainContainer {
override fun provideSensorsRepo(): ISensorsRepo { override fun provideSensorsRepo(): ISensorsRepo {
val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) val displayManager = context.getSystemService(DisplayManager::class.java)
val window = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager return SensorsRepo(manager, displayManager)
return SensorsRepo(manager,sensor,window)
} }
private fun provideDatabaseRepo(): IDatabaseRepo { private fun provideDatabaseRepo(): IDatabaseRepo {
@@ -22,8 +22,9 @@ import android.hardware.Sensor
import android.hardware.SensorEvent import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
import android.hardware.SensorManager import android.hardware.SensorManager
import android.hardware.display.DisplayManager
import android.view.Display
import android.view.Surface import android.view.Surface
import android.view.WindowManager
import com.rtbishop.look4sat.core.domain.predict.GeoPos import com.rtbishop.look4sat.core.domain.predict.GeoPos
import com.rtbishop.look4sat.core.domain.predict.RAD2DEG import com.rtbishop.look4sat.core.domain.predict.RAD2DEG
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
@@ -31,13 +32,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlin.math.round import kotlin.math.round
private const val SMOOTHING_FACTOR = 0.15f
private const val SENSOR_RATE_US = 16_000
class SensorsRepo( class SensorsRepo(
private val sensorManager: SensorManager, private val sensorManager: SensorManager,
private val sensor: Sensor?, private val displayManager: DisplayManager?
private val windowManager: WindowManager ) : ISensorsRepo, SensorEventListener {
) : SensorEventListener, ISensorsRepo {
private val _orientation = MutableStateFlow(Pair(0f, 0f)) private val _sensorData = MutableStateFlow(Pair(0f, 0f))
private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
private val rotationMatrix = FloatArray(9) private val rotationMatrix = FloatArray(9)
private val tempMatrix = FloatArray(9) private val tempMatrix = FloatArray(9)
private val orientationValues = FloatArray(3) private val orientationValues = FloatArray(3)
@@ -45,11 +49,7 @@ class SensorsRepo(
private var smoothPitch = 0f private var smoothPitch = 0f
private var hasInitialReading = false private var hasInitialReading = false
companion object { override val sensorData: StateFlow<Pair<Float, Float>> = _sensorData
private const val SMOOTHING_FACTOR = 0.15f
}
override val orientation: StateFlow<Pair<Float, Float>> = _orientation
override fun getMagDeclination(geoPos: GeoPos, time: Long): Float { override fun getMagDeclination(geoPos: GeoPos, time: Long): Float {
return GeomagneticField( return GeomagneticField(
@@ -62,7 +62,7 @@ class SensorsRepo(
override fun enableSensor() { override fun enableSensor() {
hasInitialReading = false hasInitialReading = false
sensor?.let { sensorManager.registerListener(this, it, 8000) } sensor?.let { sensorManager.registerListener(this, it, SENSOR_RATE_US) }
} }
override fun disableSensor() = sensorManager.unregisterListener(this) override fun disableSensor() = sensorManager.unregisterListener(this)
@@ -70,16 +70,11 @@ class SensorsRepo(
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit
override fun onSensorChanged(event: SensorEvent) { override fun onSensorChanged(event: SensorEvent) {
if (event.sensor == sensor) updateOrientation(event.values) if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR) handleSensorEvent(event)
} }
private fun getDisplayRotation(): Int { private fun getDisplayRotation(): Int {
return try { return displayManager?.getDisplay(Display.DEFAULT_DISPLAY)?.rotation ?: Surface.ROTATION_0
@Suppress("DEPRECATION")
windowManager.defaultDisplay.rotation
} catch (_: Exception) {
Surface.ROTATION_0
}
} }
private fun remapForRotation(rotation: Int) { private fun remapForRotation(rotation: Int) {
@@ -101,30 +96,23 @@ class SensorsRepo(
if (remapped) System.arraycopy(tempMatrix, 0, rotationMatrix, 0, 9) if (remapped) System.arraycopy(tempMatrix, 0, rotationMatrix, 0, 9)
} }
private fun updateOrientation(rotationVector: FloatArray) { private fun handleSensorEvent(event: SensorEvent) {
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector) SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
remapForRotation(getDisplayRotation()) remapForRotation(getDisplayRotation())
SensorManager.getOrientation(rotationMatrix, orientationValues) SensorManager.getOrientation(rotationMatrix, orientationValues)
val azimuth = (orientationValues[0] * RAD2DEG).toFloat() val azimuth = normalizeAzimuth((orientationValues[0] * RAD2DEG).toFloat())
val pitch = (orientationValues[1] * RAD2DEG).toFloat() val pitch = (orientationValues[1] * RAD2DEG).toFloat()
val magneticAzimuth = (azimuth + 360f) % 360f
if (!hasInitialReading) { if (!hasInitialReading) {
smoothAzimuth = magneticAzimuth smoothAzimuth = azimuth
smoothPitch = pitch smoothPitch = pitch
hasInitialReading = true hasInitialReading = true
} else { } else {
smoothAzimuth = lowPassAngle(smoothAzimuth, magneticAzimuth) smoothAzimuth = lowPassAngle(smoothAzimuth, azimuth)
smoothPitch = lowPass(smoothPitch, pitch) smoothPitch = lowPass(smoothPitch, pitch)
} }
_sensorData.value = Pair(round(smoothAzimuth * 10) / 10, round(smoothPitch * 10) / 10)
_orientation.value = Pair(
round(smoothAzimuth * 10) / 10,
round(smoothPitch * 10) / 10
)
} }
/** Standard exponential low-pass filter. */
private fun lowPass(previous: Float, current: Float): Float { private fun lowPass(previous: Float, current: Float): Float {
return previous + SMOOTHING_FACTOR * (current - previous) return previous + SMOOTHING_FACTOR * (current - previous)
} }
@@ -135,9 +123,10 @@ class SensorsRepo(
*/ */
private fun lowPassAngle(previous: Float, current: Float): Float { private fun lowPassAngle(previous: Float, current: Float): Float {
var delta = current - previous var delta = current - previous
// Normalise delta into the range (-180, 180]
while (delta > 180f) delta -= 360f while (delta > 180f) delta -= 360f
while (delta <= -180f) delta += 360f while (delta <= -180f) delta += 360f
return (previous + SMOOTHING_FACTOR * delta + 360f) % 360f return normalizeAzimuth(previous + SMOOTHING_FACTOR * delta)
} }
private fun normalizeAzimuth(value: Float): Float = (value + 360f) % 360f
} }
@@ -113,8 +113,8 @@ class SettingsRepo(
} }
private fun getSelectedTypes(): List<String> { private fun getSelectedTypes(): List<String> {
val typesString = preferences.getString(keySelectedTypes, null) val typesString = preferences.getString(keySelectedTypes, "Amateur")
if (typesString.isNullOrEmpty()) return listOf("Amateur") if (typesString.isNullOrEmpty()) return emptyList()
return typesString.split(separatorComma) return typesString.split(separatorComma)
} }
//endregion //endregion
@@ -74,7 +74,6 @@ class LocalSource(private val look4SatDao: Look4SatDao) : ILocalSource {
} }
override suspend fun insertRadios(radios: List<SatRadio>) { override suspend fun insertRadios(radios: List<SatRadio>) {
look4SatDao.deleteRadios()
look4SatDao.insertRadios(radios.toFrameworkRadios()) look4SatDao.insertRadios(radios.toFrameworkRadios())
} }
@@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
class RemoteSource( class RemoteSource(
@@ -45,7 +46,10 @@ class RemoteSource(
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) { override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
try { try {
val networkRequest = Request.Builder().url(url).build() val networkRequest = Request.Builder().url(url).build()
httpClient.newCall(networkRequest).execute().body.byteStream() httpClient.newCall(networkRequest).execute().use { response ->
if (!response.isSuccessful) return@withContext null
ByteArrayInputStream(response.body.bytes())
}
} catch (exception: Exception) { } catch (exception: Exception) {
println("RemoteSource network stream exception: $exception") println("RemoteSource network stream exception: $exception")
null null
@@ -21,7 +21,7 @@ import com.rtbishop.look4sat.core.domain.predict.GeoPos
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface ISensorsRepo { interface ISensorsRepo {
val orientation: StateFlow<Pair<Float, Float>> val sensorData: StateFlow<Pair<Float, Float>>
fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float
fun enableSensor() fun enableSensor()
fun disableSensor() fun disableSensor()
@@ -49,9 +49,9 @@
\n\nPlease update the database at least weekly to get accurate predictions.</string> \n\nPlease update the database at least weekly to get accurate predictions.</string>
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string> <string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false"> <string name="pass_whatsnew_message" translatable="false">
* Enabled continuous SSTV decoding for manually selected types (Play button enables/disables decoding) * Fixed transceivers retention on a failed fetch from server
\n\n* Added selected transceiver\'s frequency display to SSTV page \n\n* Fixed SSTV frequency display, no transceiver selected hint
\n\n* Fixed Reset/Save/Play buttons behavior and states on SSTV page \n\n* Fixed All satellite category retention on Satellites screen
</string> </string>
<!-- Radar screen --> <!-- Radar screen -->
@@ -1,3 +1,3 @@
* Enabled continuous SSTV decoding for manually selected types (Play button enables/disables decoding) * Fixed transceivers retention on a failed fetch from server
* Added selected transceiver\'s frequency display to SSTV page * Fixed SSTV frequency display, no transceiver selected hint
* Fixed Reset/Save/Play buttons behavior and states on SSTV page * Fixed All satellite category retention on Satellites screen
@@ -78,7 +78,7 @@ class RadarViewModel(
private val _uiState = MutableStateFlow( private val _uiState = MutableStateFlow(
RadarState( RadarState(
isUtc = settingsRepo.otherSettings.value.stateOfUtc, isUtc = settingsRepo.otherSettings.value.stateOfUtc,
orientationValues = sensorsRepo.orientation.value, orientationValues = sensorsRepo.sensorData.value,
shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep, shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep,
shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors, shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors,
sstv = SstvSubState(selectedMode = settingsRepo.otherSettings.value.sstvMode) sstv = SstvSubState(selectedMode = settingsRepo.otherSettings.value.sstvMode)
@@ -97,7 +97,7 @@ class RadarViewModel(
if (!settingsRepo.otherSettings.value.stateOfSensors) return if (!settingsRepo.otherSettings.value.stateOfSensors) return
viewModelScope.launch { viewModelScope.launch {
sensorsRepo.enableSensor() sensorsRepo.enableSensor()
sensorsRepo.orientation.collect { data -> sensorsRepo.sensorData.collect { data ->
val orientationValues = (data.first + magDeclination) to data.second val orientationValues = (data.first + magDeclination) to data.second
_uiState.update { it.copy(orientationValues = orientationValues) } _uiState.update { it.copy(orientationValues = orientationValues) }
} }
@@ -320,10 +320,8 @@ class RadarViewModel(
private suspend fun processRadios(radios: List<SatRadio>, orbitalObject: OrbitalObject, time: Long) { private suspend fun processRadios(radios: List<SatRadio>, orbitalObject: OrbitalObject, time: Long) {
val transmitters = satelliteRepo.getRadios(orbitalObject, stationPos, radios, time) val transmitters = satelliteRepo.getRadios(orbitalObject, stationPos, radios, time)
val isFreqEnabled =
settingsRepo.rcSettings.value.frequencyState || settingsRepo.rcSettings.value.bluetoothFrequencyState
_uiState.update { state -> _uiState.update { state ->
val freq = if (isFreqEnabled && state.transceivers.selectedUuid != null) { val freq = if (state.transceivers.selectedUuid != null) {
val selectedRadio = transmitters.firstOrNull { it.uuid == state.transceivers.selectedUuid } val selectedRadio = transmitters.firstOrNull { it.uuid == state.transceivers.selectedUuid }
selectedRadio?.let { radio -> selectedRadio?.let { radio ->
val low = radio.downlinkLow val low = radio.downlinkLow
@@ -190,16 +190,14 @@ internal fun SstvPage(
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
// Doppler-corrected downlink frequency hint // Doppler-corrected downlink frequency hint
if (dopplerFrequency != null) { OutlinedText(
OutlinedText( text = dopplerFrequency?.let { "RX: $it Hz" } ?: "No transceiver selected",
text = "RX: $dopplerFrequency Hz", fontSize = 18.sp,
fontSize = 18.sp, fontWeight = FontWeight.Bold,
fontWeight = FontWeight.Bold, fillColor = MaterialTheme.colorScheme.primary,
fillColor = MaterialTheme.colorScheme.primary, outlineColor = MaterialTheme.colorScheme.background,
outlineColor = MaterialTheme.colorScheme.background, modifier = Modifier.align(Alignment.CenterHorizontally)
modifier = Modifier.align(Alignment.CenterHorizontally) )
)
}
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
+6 -6
View File
@@ -1,8 +1,8 @@
[versions] [versions]
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionCode = "441" appVersionCode = "442"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionName = "4.4.1" appVersionName = "4.4.2"
#noinspection GradleDependency,UnusedVersionCatalogEntry #noinspection GradleDependency,UnusedVersionCatalogEntry
compileSdk = "36" compileSdk = "36"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
@@ -18,18 +18,18 @@ androidx-core-ktx = "1.18.0"
androidx-core-splashscreen = "1.2.0" androidx-core-splashscreen = "1.2.0"
androidx-room = "2.8.4" androidx-room = "2.8.4"
compose-bom = "2026.05.01" compose-bom = "2026.06.00"
compose-activity = "1.13.0" compose-activity = "1.13.0"
compose-lifecycle = "2.10.0" compose-lifecycle = "2.10.0"
compose-navigation3 = "1.1.2" compose-navigation3 = "1.1.3"
google-ksp = "2.3.7" google-ksp = "2.3.8"
kotlin = "2.4.0" kotlin = "2.4.0"
kotlin-coroutines = "1.11.0" kotlin-coroutines = "1.11.0"
kotlin-serialization = "1.11.0" kotlin-serialization = "1.11.0"
other-okhttp = "5.3.2" other-okhttp = "5.4.0"
other-osmdroid = "6.1.20" other-osmdroid = "6.1.20"
test-junit4 = "4.13.2" test-junit4 = "4.13.2"
+2 -2
View File
@@ -1,7 +1,7 @@
#Mon May 25 11:20:48 BST 2026 #Sun Jun 21 14:57:04 BST 2026
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME