mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-05 20:17:29 +00:00
fix: broaden linear transponder matching and deduplicate same-range entries
- isNamedLinearTransponder: match ' Lin' prefix (AO-7 style) and add fallback branch (hasLinearName && hasLinearMode) for entries with 'Linear'/'Lin' but no 'transponder' word (AO-7, AO-73) - add deduplicateTransponders: merge entries sharing the same uplink/downlink frequency range, preferring non-CW entry (AO-7 Mode A had separate SSB and CW entries; JO-97 had separate CW and SSB Transponder entries) - apply dedup in CalculatorPage filter chain - 6 new tests, 23 total, all passing
This commit is contained in:
1 parent
10eb84690e
commit
bccc2ccfcc
3 files changed
+106
-3
No files matched your search
+21
-2
@@ -112,11 +112,30 @@ object DopplerFrequencyCalculator {
|
|||||||
val modes = listOfNotNull(transponder.downlinkMode, transponder.uplinkMode)
|
val modes = listOfNotNull(transponder.downlinkMode, transponder.uplinkMode)
|
||||||
.joinToString(separator = " ")
|
.joinToString(separator = " ")
|
||||||
.lowercase(Locale.ENGLISH)
|
.lowercase(Locale.ENGLISH)
|
||||||
val hasLinearName = info.contains("linear")
|
val hasLinearName = info.contains("linear") || info.contains(" lin") || info.startsWith("lin")
|
||||||
val hasTransponderName = info.contains("transponder") || info.contains("transp") ||
|
val hasTransponderName = info.contains("transponder") || info.contains("transp") ||
|
||||||
info.contains("xponder") || info.contains("xpdr")
|
info.contains("xponder") || info.contains("xpdr")
|
||||||
val hasLinearMode = listOf("ssb", "usb", "lsb", "cw").any { modes.contains(it) }
|
val hasLinearMode = listOf("ssb", "usb", "lsb", "cw").any { modes.contains(it) }
|
||||||
|
|
||||||
return (hasLinearName && hasTransponderName) || (hasTransponderName && hasLinearMode)
|
return (hasLinearName && hasTransponderName) || (hasTransponderName && hasLinearMode) ||
|
||||||
|
(hasLinearName && hasLinearMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes duplicate transponder entries that describe the same physical
|
||||||
|
* transponder with different mode labels (e.g. SatNOGS lists AO-7's Mode A
|
||||||
|
* as both "Lin SSB" and "Lin CW", and JO-97's U/V transponder as both
|
||||||
|
* "CW Transponder" and "SSB Transponder").
|
||||||
|
*
|
||||||
|
* Entries sharing the same uplink/downlink frequency range are considered
|
||||||
|
* the same transponder. The non-CW entry is preferred because its invert
|
||||||
|
* flag is more reliable (e.g. JO-97's CW entry wrongly has invert=false).
|
||||||
|
*/
|
||||||
|
fun deduplicateTransponders(radios: List<SatRadio>): List<SatRadio> {
|
||||||
|
return radios.groupBy { radio ->
|
||||||
|
listOf(radio.uplinkLow, radio.uplinkHigh, radio.downlinkLow, radio.downlinkHigh)
|
||||||
|
}.values.map { group ->
|
||||||
|
group.firstOrNull { it.downlinkMode?.equals("CW", ignoreCase = true) != true } ?: group.first()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+84
-1
@@ -13,6 +13,7 @@ import org.junit.Test
|
|||||||
class DopplerFrequencyCalculatorTest {
|
class DopplerFrequencyCalculatorTest {
|
||||||
|
|
||||||
private fun linearTransponder(
|
private fun linearTransponder(
|
||||||
|
uuid: String = "linear",
|
||||||
upLow: Long = 145_000_000L,
|
upLow: Long = 145_000_000L,
|
||||||
upHigh: Long = 145_500_000L,
|
upHigh: Long = 145_500_000L,
|
||||||
downLow: Long = 435_000_000L,
|
downLow: Long = 435_000_000L,
|
||||||
@@ -22,7 +23,7 @@ class DopplerFrequencyCalculatorTest {
|
|||||||
downlinkMode: String? = "USB",
|
downlinkMode: String? = "USB",
|
||||||
uplinkMode: String? = "LSB"
|
uplinkMode: String? = "LSB"
|
||||||
) = SatRadio(
|
) = SatRadio(
|
||||||
uuid = "linear", info = info, isAlive = true,
|
uuid = uuid, info = info, isAlive = true,
|
||||||
downlinkLow = downLow, downlinkHigh = downHigh,
|
downlinkLow = downLow, downlinkHigh = downHigh,
|
||||||
downlinkMode = downlinkMode, uplinkLow = upLow, uplinkHigh = upHigh,
|
downlinkMode = downlinkMode, uplinkLow = upLow, uplinkHigh = upHigh,
|
||||||
uplinkMode = uplinkMode, isInverted = inverted, catnum = 12345
|
uplinkMode = uplinkMode, isInverted = inverted, catnum = 12345
|
||||||
@@ -72,11 +73,93 @@ class DopplerFrequencyCalculatorTest {
|
|||||||
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(driftingRangeEntry))
|
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(driftingRangeEntry))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isNamedLinearTransponder_returnsTrueForAbbreviatedLinName() {
|
||||||
|
// AO-7 style: "Mode V/A (A) Lin SSB" — "Lin" abbreviation, no "transponder" word
|
||||||
|
val ao7Entry = linearTransponder(info = "Mode V/A (A) Lin SSB", downlinkMode = "USB", uplinkMode = "USB")
|
||||||
|
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(ao7Entry))
|
||||||
|
val ao7CwEntry = linearTransponder(info = "Mode V/A (A) Lin CW", downlinkMode = "CW", uplinkMode = "CW")
|
||||||
|
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(ao7CwEntry))
|
||||||
|
val ao7ModeBEntry = linearTransponder(info = "Mode U/V (B) Lin", downlinkMode = "USB", uplinkMode = "LSB")
|
||||||
|
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(ao7ModeBEntry))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isNamedLinearTransponder_returnsTrueForLinearWithoutTransponderWord() {
|
||||||
|
// AO-73 style: "Mode U/V Linear" — has "Linear" but no "transponder"
|
||||||
|
val ao73Entry = linearTransponder(info = "Mode U/V Linear", downlinkMode = "USB", uplinkMode = "LSB")
|
||||||
|
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(ao73Entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isNamedLinearTransponder_returnsFalseForDownlinkContainingLinInsideWord() {
|
||||||
|
// "Downlink" contains "lin" but is not a linear-transponder name
|
||||||
|
val downlinkEntry = linearTransponder(info = "Mode U Downlink", downlinkMode = "FM", uplinkMode = "FM")
|
||||||
|
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(downlinkEntry))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun isNamedLinearTransponder_returnsFalseForFmRepeater() {
|
fun isNamedLinearTransponder_returnsFalseForFmRepeater() {
|
||||||
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(fmTransponder()))
|
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(fmTransponder()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deduplicateTransponders_mergesSameFrequencyRange() {
|
||||||
|
// AO-7's Mode A: same range, SSB and CW entries
|
||||||
|
val ssb = linearTransponder(
|
||||||
|
uuid = "ssb-uuid", info = "Mode V/A (A) Lin SSB",
|
||||||
|
downlinkMode = "USB", uplinkMode = "USB"
|
||||||
|
)
|
||||||
|
val cw = linearTransponder(
|
||||||
|
uuid = "cw-uuid", info = "Mode V/A (A) Lin CW",
|
||||||
|
downlinkMode = "CW", uplinkMode = "CW"
|
||||||
|
)
|
||||||
|
val modeB = linearTransponder(
|
||||||
|
uuid = "modeb-uuid", info = "Mode U/V (B) Lin",
|
||||||
|
upLow = 432_125_000L, upHigh = 432_175_000L,
|
||||||
|
downLow = 145_925_000L, downHigh = 145_975_000L,
|
||||||
|
downlinkMode = "USB", uplinkMode = "LSB"
|
||||||
|
)
|
||||||
|
val result = DopplerFrequencyCalculator.deduplicateTransponders(listOf(ssb, cw, modeB))
|
||||||
|
assertEquals(2, result.size)
|
||||||
|
// SSB entry should be preferred over CW (same range)
|
||||||
|
assertEquals("ssb-uuid", result[0].uuid)
|
||||||
|
assertEquals("modeb-uuid", result[1].uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deduplicateTransponders_prefersNonCwEntry() {
|
||||||
|
// JO-97: CW entry has invert=false (wrong), SSB has invert=true (correct)
|
||||||
|
val cw = linearTransponder(
|
||||||
|
uuid = "cw-uuid", info = "U/V CW Transponder",
|
||||||
|
downlinkMode = "CW", uplinkMode = "CW",
|
||||||
|
upLow = 435_100_000L, upHigh = 435_120_000L,
|
||||||
|
downLow = 145_855_000L, downHigh = 145_875_000L
|
||||||
|
)
|
||||||
|
val ssb = linearTransponder(
|
||||||
|
uuid = "ssb-uuid", info = "U/V SSB Transponder",
|
||||||
|
downlinkMode = "USB", uplinkMode = "LSB",
|
||||||
|
upLow = 435_100_000L, upHigh = 435_120_000L,
|
||||||
|
downLow = 145_855_000L, downHigh = 145_875_000L,
|
||||||
|
inverted = true
|
||||||
|
)
|
||||||
|
val result = DopplerFrequencyCalculator.deduplicateTransponders(listOf(cw, ssb))
|
||||||
|
assertEquals(1, result.size)
|
||||||
|
assertEquals("ssb-uuid", result[0].uuid)
|
||||||
|
// Verify the correct invert flag is preserved
|
||||||
|
assertTrue(result[0].isInverted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deduplicateTransponders_preservesUniqueEntries() {
|
||||||
|
val t1 = linearTransponder(uuid = "t1", upLow = 145_000_000L, upHigh = 145_500_000L,
|
||||||
|
downLow = 435_000_000L, downHigh = 435_500_000L)
|
||||||
|
val t2 = linearTransponder(uuid = "t2", upLow = 435_000_000L, upHigh = 435_500_000L,
|
||||||
|
downLow = 145_000_000L, downHigh = 145_500_000L)
|
||||||
|
val result = DopplerFrequencyCalculator.deduplicateTransponders(listOf(t1, t2))
|
||||||
|
assertEquals(2, result.size)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun computeUplinkFromDownlink_linear_noDoppler() {
|
fun computeUplinkFromDownlink_linear_noDoppler() {
|
||||||
val xpdr = linearTransponder()
|
val xpdr = linearTransponder()
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ fun CalculatorPage(
|
|||||||
) {
|
) {
|
||||||
val calculatorTransceivers = remember(transceivers) {
|
val calculatorTransceivers = remember(transceivers) {
|
||||||
transceivers.filter(DopplerFrequencyCalculator::isNamedLinearTransponder)
|
transceivers.filter(DopplerFrequencyCalculator::isNamedLinearTransponder)
|
||||||
|
.let(DopplerFrequencyCalculator::deduplicateTransponders)
|
||||||
}
|
}
|
||||||
val selectedTransceiver = remember(calculatorTransceivers, selectedUuid) {
|
val selectedTransceiver = remember(calculatorTransceivers, selectedUuid) {
|
||||||
calculatorTransceivers.firstOrNull { it.uuid == selectedUuid }
|
calculatorTransceivers.firstOrNull { it.uuid == selectedUuid }
|
||||||
|
|||||||
Reference in new issue
Block a user