Add CAT radio control for full-duplex satellite operation (#207)
This commit is contained in:
1 parent
868155c532
commit
2df0b9ba3d
28 files changed
+2126
-8
No files matched your search
@@ -63,3 +63,20 @@ data class DataSourcesSettings(
|
||||
val tleUrl: String,
|
||||
val transceiversUrl: String
|
||||
)
|
||||
|
||||
data class RadioControlSettings(
|
||||
val enabled: Boolean,
|
||||
val radioModel: String,
|
||||
val txRadioAddress: String,
|
||||
val rxRadioAddress: String,
|
||||
val txRadioName: String,
|
||||
val rxRadioName: String,
|
||||
val baudRate: Int
|
||||
) {
|
||||
companion object {
|
||||
val SUPPORTED_RADIOS = listOf(
|
||||
"Yaesu FT-817/818",
|
||||
"Yaesu FT-857/897"
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
@@ -15,6 +15,9 @@ interface IMainContainer {
|
||||
fun provideBluetoothReporter(): IReporter
|
||||
fun provideNetworkReporter(): IReporter
|
||||
fun provideSensorsRepo(): ISensorsRepo
|
||||
fun provideTxRadioController(): IRadioController
|
||||
fun provideRxRadioController(): IRadioController
|
||||
val radioTrackingService: IRadioTrackingService
|
||||
}
|
||||
|
||||
interface IContainerProvider {
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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 com.rtbishop.look4sat.core.domain.repository
|
||||
|
||||
interface IRadioController {
|
||||
|
||||
val isConnected: Boolean
|
||||
|
||||
suspend fun connect(): Boolean
|
||||
|
||||
suspend fun disconnect()
|
||||
|
||||
suspend fun setFrequency(frequencyHz: Long): Boolean
|
||||
|
||||
suspend fun setMode(mode: String): Boolean
|
||||
|
||||
suspend fun setCtcssMode(enabled: Boolean): Boolean
|
||||
|
||||
suspend fun setCtcssTone(toneHz: Double): Boolean
|
||||
|
||||
suspend fun readFrequencyAndMode(): Pair<Long, String>?
|
||||
|
||||
suspend fun pttOn(): Boolean
|
||||
|
||||
suspend fun pttOff(): Boolean
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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 com.rtbishop.look4sat.core.domain.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
data class RadioTrackingState(
|
||||
val isActive: Boolean = false,
|
||||
val txConnected: Boolean = false,
|
||||
val rxConnected: Boolean = false,
|
||||
val txFrequencyHz: Long? = null,
|
||||
val rxFrequencyHz: Long? = null,
|
||||
val txMode: String? = null,
|
||||
val rxMode: String? = null,
|
||||
val ctcssTone: Double? = null,
|
||||
val txBaseFrequencyHz: Long? = null,
|
||||
val selectedTransponder: SatRadio? = null,
|
||||
val currentPass: OrbitalPass? = null,
|
||||
val azimuth: Double = 0.0,
|
||||
val elevation: Double = 0.0,
|
||||
val distance: Double = 0.0,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
interface IRadioTrackingService {
|
||||
val state: StateFlow<RadioTrackingState>
|
||||
|
||||
suspend fun connectRadios()
|
||||
suspend fun disconnectRadios()
|
||||
fun startTracking(pass: OrbitalPass, transponder: SatRadio, txBaseFreqHz: Long?)
|
||||
fun stopTracking()
|
||||
fun setTransponder(transponder: SatRadio)
|
||||
fun setTxBaseFrequency(frequencyHz: Long)
|
||||
fun adjustTxBaseFrequency(deltaHz: Long)
|
||||
fun setCtcssTone(toneHz: Double?)
|
||||
fun setMode(txMode: String, rxMode: String)
|
||||
}
|
||||
+6
@@ -22,6 +22,7 @@ import com.rtbishop.look4sat.core.domain.model.DatabaseState
|
||||
import com.rtbishop.look4sat.core.domain.model.OtherSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.PassesSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -71,4 +72,9 @@ interface ISettingsRepo {
|
||||
val dataSourcesSettings: StateFlow<DataSourcesSettings>
|
||||
fun updateDataSourcesSettings(settings: DataSourcesSettings)
|
||||
//endregion
|
||||
|
||||
//region # Radio control settings
|
||||
val radioControlSettings: StateFlow<RadioControlSettings>
|
||||
fun updateRadioControlSettings(settings: RadioControlSettings)
|
||||
//endregion
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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 com.rtbishop.look4sat.core.domain.utility
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
|
||||
object TransponderMapper {
|
||||
|
||||
fun mapUplinkToDownlink(txFreqHz: Long, transponder: SatRadio): Long? {
|
||||
val uplinkLow = transponder.uplinkLow ?: return null
|
||||
val downlinkLow = transponder.downlinkLow ?: return null
|
||||
|
||||
// Single-frequency transponder (FM repeater) - just return the downlink freq
|
||||
val uplinkHigh = transponder.uplinkHigh
|
||||
val downlinkHigh = transponder.downlinkHigh
|
||||
if (uplinkHigh == null || downlinkHigh == null
|
||||
|| uplinkLow == uplinkHigh || downlinkLow == downlinkHigh
|
||||
) {
|
||||
return downlinkLow
|
||||
}
|
||||
|
||||
// Passband transponder (linear) - map within the band
|
||||
val offset = txFreqHz - uplinkLow
|
||||
return if (transponder.isInverted) {
|
||||
downlinkHigh - offset
|
||||
} else {
|
||||
downlinkLow + offset
|
||||
}
|
||||
}
|
||||
|
||||
fun mapDownlinkToUplink(rxFreqHz: Long, transponder: SatRadio): Long? {
|
||||
val uplinkLow = transponder.uplinkLow ?: return null
|
||||
val downlinkLow = transponder.downlinkLow ?: return null
|
||||
|
||||
val uplinkHigh = transponder.uplinkHigh
|
||||
val downlinkHigh = transponder.downlinkHigh
|
||||
if (uplinkHigh == null || downlinkHigh == null
|
||||
|| uplinkLow == uplinkHigh || downlinkLow == downlinkHigh
|
||||
) {
|
||||
return uplinkLow
|
||||
}
|
||||
|
||||
return if (transponder.isInverted) {
|
||||
uplinkLow + (downlinkHigh - rxFreqHz)
|
||||
} else {
|
||||
uplinkLow + (rxFreqHz - downlinkLow)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapUplinkModeToDownlinkMode(txMode: String, isInverted: Boolean): String {
|
||||
if (!isInverted) return txMode
|
||||
return when (txMode.uppercase()) {
|
||||
"USB" -> "LSB"
|
||||
"LSB" -> "USB"
|
||||
else -> txMode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.rtbishop.look4sat.core.domain
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.utility.TransponderMapper
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class TransponderMapperTest {
|
||||
|
||||
private fun makeRadio(
|
||||
uplinkLow: Long? = 435000000L,
|
||||
uplinkHigh: Long? = 435100000L,
|
||||
downlinkLow: Long? = 145800000L,
|
||||
downlinkHigh: Long? = 145900000L,
|
||||
isInverted: Boolean = true
|
||||
) = SatRadio(
|
||||
uuid = "test",
|
||||
info = "Test",
|
||||
isAlive = true,
|
||||
downlinkLow = downlinkLow,
|
||||
downlinkHigh = downlinkHigh,
|
||||
downlinkMode = "USB",
|
||||
uplinkLow = uplinkLow,
|
||||
uplinkHigh = uplinkHigh,
|
||||
uplinkMode = "USB",
|
||||
isInverted = isInverted,
|
||||
catnum = 0
|
||||
)
|
||||
|
||||
@Test
|
||||
fun invertedTransponder_mapsFrequencyCorrectly() {
|
||||
val radio = makeRadio(isInverted = true)
|
||||
// TX at uplinkLow → RX should be downlinkHigh
|
||||
assertEquals(145900000L, TransponderMapper.mapUplinkToDownlink(435000000L, radio))
|
||||
// TX at uplinkHigh → RX should be downlinkLow
|
||||
assertEquals(145800000L, TransponderMapper.mapUplinkToDownlink(435100000L, radio))
|
||||
// TX at center → RX at center
|
||||
assertEquals(145850000L, TransponderMapper.mapUplinkToDownlink(435050000L, radio))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonInvertedTransponder_mapsFrequencyCorrectly() {
|
||||
val radio = makeRadio(isInverted = false)
|
||||
// TX at uplinkLow → RX should be downlinkLow
|
||||
assertEquals(145800000L, TransponderMapper.mapUplinkToDownlink(435000000L, radio))
|
||||
// TX at uplinkHigh → RX should be downlinkHigh
|
||||
assertEquals(145900000L, TransponderMapper.mapUplinkToDownlink(435100000L, radio))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullFrequencies_returnsNull() {
|
||||
assertNull(TransponderMapper.mapUplinkToDownlink(435000000L, makeRadio(uplinkLow = null)))
|
||||
assertNull(TransponderMapper.mapUplinkToDownlink(435000000L, makeRadio(downlinkLow = null)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleFrequencyTransponder_returnsDownlinkLow() {
|
||||
// FM repeater style: no passband, just single frequencies
|
||||
val radio = makeRadio(
|
||||
uplinkLow = 145990000L, uplinkHigh = null,
|
||||
downlinkLow = 436300000L, downlinkHigh = null,
|
||||
isInverted = false
|
||||
)
|
||||
assertEquals(436300000L, TransponderMapper.mapUplinkToDownlink(145990000L, radio))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun equalLowHighTransponder_returnsDownlinkLow() {
|
||||
val radio = makeRadio(
|
||||
uplinkLow = 145990000L, uplinkHigh = 145990000L,
|
||||
downlinkLow = 436300000L, downlinkHigh = 436300000L,
|
||||
isInverted = false
|
||||
)
|
||||
assertEquals(436300000L, TransponderMapper.mapUplinkToDownlink(145990000L, radio))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invertedMode_swapsUsbLsb() {
|
||||
assertEquals("LSB", TransponderMapper.mapUplinkModeToDownlinkMode("USB", true))
|
||||
assertEquals("USB", TransponderMapper.mapUplinkModeToDownlinkMode("LSB", true))
|
||||
assertEquals("FM", TransponderMapper.mapUplinkModeToDownlinkMode("FM", true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonInvertedMode_keepsSame() {
|
||||
assertEquals("USB", TransponderMapper.mapUplinkModeToDownlinkMode("USB", false))
|
||||
assertEquals("LSB", TransponderMapper.mapUplinkModeToDownlinkMode("LSB", false))
|
||||
assertEquals("FM", TransponderMapper.mapUplinkModeToDownlinkMode("FM", false))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user