mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-04 19:47:22 +00:00
fix: correct AMSAT live filter satellite lists
- Resolve ISS station modules (ZARYA/UNITY/ZVEZDA/DESTINY/NAUKA) to the primary 25544 instead of whichever module appears first in the TLE source - Match OSCAR designators to local names spelled 'OSCAR N'/'AMSAT-OSCAR N' (AO-7 -> OSCAR 7) so Linear lists keep AO-7 on SatNOGS-sourced data - SSTV virtual type now uses the transceiver service class (Amateur only) plus launcher-debris name exclusion, dropping retired weather birds (TIROS) and rocket bodies (ARIANE 6 R/B) while keeping ISS/CSS/TEIKYOSAT - Persist the AMSAT active-frequency whitelist (palewire mirror) on sync; used only to disambiguate multi-match names, never as a hard filter
This commit is contained in:
1 parent
732412deb2
commit
522cac25e8
20 files changed
+174
-27
No files matched your search
@@ -33,6 +33,10 @@ data class SatRadio(
|
||||
@SerialName("uplink_mode") val uplinkMode: String?,
|
||||
@SerialName("invert") val isInverted: Boolean,
|
||||
@SerialName("norad_cat_id") val catnum: Int?,
|
||||
/** Service class from the transceiver source ("Amateur"/"Unknown" etc.).
|
||||
* Used by the SSTV virtual filter to tell amateur satellites apart from
|
||||
* debris / weather birds (TIROS) / launcher stages (Ariane 6 R/B). */
|
||||
@SerialName("service") val service: String? = null,
|
||||
/** Set for manually imported transceivers, which remote updates must not replace. */
|
||||
val isCustom: Boolean = false
|
||||
)
|
||||
+5
@@ -75,4 +75,9 @@ interface ISatelliteRepo {
|
||||
* (real transponders), excluding downlink-only beacons/telemetry that share
|
||||
* the same mode label. Used by the FM/Linear fallback filter. */
|
||||
suspend fun getSatelliteIdsWithModesAndUplink(modes: List<String>): List<Int>
|
||||
|
||||
/** Like [getSatelliteIdsWithModes] but only matches records whose service
|
||||
* class is "Amateur", so SSTV never matches weather birds (TIROS),
|
||||
* launcher debris or other non-amateur transmitters. */
|
||||
suspend fun getSatelliteIdsWithModesAndAmateur(modes: List<String>): List<Int>
|
||||
}
|
||||
+4
@@ -85,6 +85,10 @@ interface ISettingsRepo {
|
||||
/** Catalog numbers of satellites listed on AMSAT's "Live Linear Satellites" page. */
|
||||
fun getAmSatLinearCatnums(): Set<Int>
|
||||
fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>)
|
||||
/** Catalog numbers of active *amateur* satellites from AMSAT's
|
||||
* active-transponder table (whitelist for the virtual type filters). */
|
||||
fun getAmSatActiveCatnums(): Set<Int>
|
||||
fun setAmSatActiveCatnums(catnums: Set<Int>)
|
||||
//endregion
|
||||
|
||||
//region # Radio control settings
|
||||
|
||||
@@ -30,6 +30,7 @@ interface ILocalSource {
|
||||
suspend fun deleteEntries()
|
||||
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
||||
suspend fun getIdsWithModesAndUplink(modes: List<String>): List<Int>
|
||||
suspend fun getIdsWithModesAndAmateur(modes: List<String>): List<Int>
|
||||
suspend fun getRadiosTotal(): Int
|
||||
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
||||
suspend fun insertRadios(radios: List<SatRadio>, isCustom: Boolean = false)
|
||||
|
||||
@@ -63,6 +63,15 @@ object Sources {
|
||||
"Linear" to "https://www.amsat.org/live-linear-satellites/"
|
||||
)
|
||||
|
||||
/** Machine-readable AMSAT active-transponder table (palewire mirror of
|
||||
* amsat.org's active-frequency pages): one CSV row per active amateur
|
||||
* transponder with its NORAD catnum. Used as the authoritative whitelist
|
||||
* of *amateur* satellites, so the virtual filters never match
|
||||
* non-amateur debris (Ariane 6 R/B), retired weather sats (TIROS), or
|
||||
* ISS station-module aliases (ISS (DESTINY) etc.). */
|
||||
const val amSatActiveUrl =
|
||||
"https://raw.githubusercontent.com/palewire/amateur-satellite-database/main/data/amsat-active-frequencies.csv"
|
||||
|
||||
/** Virtual satellite-selection types: transponder/activity filters shown
|
||||
* at the top of the type picker. They resolve to live lists (AMSAT pages
|
||||
* or mode=SSTV radios) instead of persisted per-type IDs, and are
|
||||
|
||||
@@ -40,6 +40,21 @@ class DataParser(private val dispatcher: CoroutineDispatcher) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse AMSAT's active-transponder CSV (palewire mirror) and return the
|
||||
* set of NORAD catnums of active *amateur* satellites. Header row has a
|
||||
* norad_id column; some rows carry alphanumeric IDs (Bluebird A0241 etc.)
|
||||
* which are skipped. */
|
||||
suspend fun parseAmSatActiveCatnums(stream: InputStream): Set<Int> = withContext(dispatcher) {
|
||||
stream.bufferedReader().useLines { lines ->
|
||||
val header = lines.firstOrNull() ?: return@useLines emptySet()
|
||||
val noradIdx = header.split(",").indexOfFirst { it.trim().equals("norad_id", true) }
|
||||
if (noradIdx < 0) return@useLines emptySet()
|
||||
lines.mapNotNull { line ->
|
||||
line.split(",").getOrNull(noradIdx)?.trim()?.toIntOrNull()
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun parseTLEStream(stream: InputStream): List<OrbitalData> = withContext(dispatcher) {
|
||||
stream.bufferedReader().readLines()
|
||||
.chunked(3)
|
||||
@@ -131,14 +146,25 @@ class DataParser(private val dispatcher: CoroutineDispatcher) {
|
||||
/** True if a local entry name matches any of the AMSAT normalized keys.
|
||||
* Token-based exact match (split on spaces/brackets/slashes), so a key
|
||||
* like "ISS" does not substring-match "AISSAT-1" — the key must equal a
|
||||
* whole name token (case-insensitive). */
|
||||
* whole name token (case-insensitive). OSCAR designators are expanded so
|
||||
* that "AO-7" also matches local names spelled "OSCAR 7" / "AMSAT-OSCAR 7"
|
||||
* (the form used by CelesTrak/SatNOGS TLE sources). */
|
||||
fun matchesAmSatName(localName: String, amSatKeys: List<String>): Boolean {
|
||||
val localTokens = localName.uppercase()
|
||||
val localUpper = localName.uppercase()
|
||||
val localTokens = localUpper
|
||||
.split(Regex("[\\s()\\[\\]/]+"))
|
||||
.filter { it.isNotBlank() }
|
||||
.toSet()
|
||||
return amSatKeys.any { key ->
|
||||
key.uppercase() in localTokens
|
||||
val k = key.uppercase()
|
||||
if (k in localTokens) return@any true
|
||||
// "AO-7" -> also match local "OSCAR 7" or "AMSAT-OSCAR 7".
|
||||
val oscar = Regex("^([A-Z]{1,3})-(\\d+)$").find(k)
|
||||
if (oscar != null) {
|
||||
val num = oscar.groupValues[2]
|
||||
if ("OSCAR $num" in localUpper || "AMSAT-OSCAR $num" in localUpper) return@any true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -283,6 +283,11 @@ class DataParserTest {
|
||||
assert(dataParser.matchesAmSatName("ISS (ZARYA)", listOf("ISS")))
|
||||
assert(!dataParser.matchesAmSatName("AISSAT-1", listOf("ISS")))
|
||||
assert(!dataParser.matchesAmSatName("TESS", listOf("ISS")))
|
||||
// OSCAR designators match local names spelled "OSCAR N"/"AMSAT-OSCAR N".
|
||||
assert(dataParser.matchesAmSatName("OSCAR 7", listOf("AO-7")))
|
||||
assert(dataParser.matchesAmSatName("AMSAT-OSCAR 7", listOf("AO-7")))
|
||||
assert(dataParser.matchesAmSatName("CATSAT", listOf("CATSAT")))
|
||||
assert(dataParser.matchesAmSatName("RS-44 & BREEZE-KM R/B", listOf("RS-44")))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
@@ -286,6 +286,8 @@ class LoTWGridSyncTest {
|
||||
override fun getAmSatFmCatnums(): Set<Int> = emptySet()
|
||||
override fun getAmSatLinearCatnums(): Set<Int> = emptySet()
|
||||
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) = Unit
|
||||
override fun getAmSatActiveCatnums(): Set<Int> = emptySet()
|
||||
override fun setAmSatActiveCatnums(catnums: Set<Int>) = Unit
|
||||
override fun updateRadioControlSettings(settings: RadioControlSettings) = TODO()
|
||||
override fun getSatelliteOffset(catnum: Int): String = ""
|
||||
override fun setSatelliteOffset(catnum: Int, offset: String) = TODO()
|
||||
|
||||
Reference in new issue
Block a user