diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt
index 22aed236..dfa84c2a 100644
--- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt
+++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt
@@ -30,6 +30,7 @@ import com.rtbishop.look4sat.core.data.framework.Ic705Controller
import com.rtbishop.look4sat.core.data.framework.NetworkReporter
import com.rtbishop.look4sat.core.data.framework.RadioTrackingService
import com.rtbishop.look4sat.core.data.repository.AmSatRepository
+import com.rtbishop.look4sat.core.domain.amsat.AmSatApiClient
import com.rtbishop.look4sat.core.data.repository.DatabaseRepo
import com.rtbishop.look4sat.core.data.repository.SatelliteRepo
import com.rtbishop.look4sat.core.data.repository.SelectionRepo
@@ -82,7 +83,7 @@ class MainContainer(private val context: Context) : IMainContainer {
override val selectionRepo = provideSelectionRepo()
override val satelliteRepo = provideSatelliteRepo()
override val databaseRepo = provideDatabaseRepo()
- override val amSatRepo = AmSatRepository(remoteSource)
+ override val amSatRepo = AmSatRepository(com.rtbishop.look4sat.core.domain.amsat.AmSatApiClient())
override val radioTrackingService: IRadioTrackingService by lazy {
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
RadioTrackingService(appScope, manager, satelliteRepo, settingsRepo)
diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt
index 52c131d6..3b92bdbb 100644
--- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt
+++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt
@@ -1,21 +1,91 @@
package com.rtbishop.look4sat.core.data.repository
-import com.rtbishop.look4sat.core.data.source.AmSatParser
+import com.rtbishop.look4sat.core.domain.amsat.AmSatApiClient
+import com.rtbishop.look4sat.core.domain.amsat.ApiReport
+import com.rtbishop.look4sat.core.domain.model.SatDay
+import com.rtbishop.look4sat.core.domain.model.SatReport
+import com.rtbishop.look4sat.core.domain.model.SatSlot
+import com.rtbishop.look4sat.core.domain.model.SatStatus
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
import com.rtbishop.look4sat.core.domain.repository.IAmSatRepository
-import com.rtbishop.look4sat.core.domain.source.IRemoteSource
+import java.util.Calendar
+import java.util.TimeZone
-/** AMSAT status repository: fetch HTML -> parse -> SatStatusPage */
-class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepository {
+/** AMSAT status repository: official API v1 -> SatStatusPage (replaces the HTML parser). */
+class AmSatRepository(private val apiClient: AmSatApiClient) : IAmSatRepository {
override suspend fun fetchStatus(): SatStatusPage? {
- val html = remoteSource.getStatusHtml() ?: return null
- if (html.isBlank()) return null
- return try {
- AmSatParser.parse(html, System.currentTimeMillis())
- } catch (exception: Exception) {
- println("AmSatRepository parse exception: $exception")
- null
+ val nowSec = System.currentTimeMillis() / 1000
+ val names = apiClient.fetchCatalog()
+ val reports = apiClient.fetchAllReports(hours = 168)
+ if (names.isEmpty() && reports.isEmpty()) return null
+ val statuses = buildStatuses(names, reports, nowSec)
+ val reportMap = reports.associate { it.id to toSatReport(it) }
+ return SatStatusPage(System.currentTimeMillis(), statuses, reportMap)
+ }
+
+ /** Build one SatStatus (6 days x 12 slots) per catalog satellite, slotting reports by age. */
+ private fun buildStatuses(names: List, reports: List, nowSec: Long): List {
+ val byName = reports.groupBy { it.name }
+ val monthAbbr = arrayOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
+ val utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
+ val labels = (0 until 6).map { d ->
+ utc.timeInMillis = (nowSec - d * 86400L) * 1000
+ "${monthAbbr[utc.get(Calendar.MONTH)]} ${utc.get(Calendar.DAY_OF_MONTH)}"
+ }
+ return names.map { name ->
+ val slots = (0 until 72).map { slotIdx ->
+ val slotStart = nowSec - (slotIdx + 1) * 7200L
+ val slotEnd = nowSec - slotIdx * 7200L
+ val inSlot = byName[name].orEmpty().filter { it.reportedTimeUtcSec in slotStart until slotEnd }
+ if (inSlot.isEmpty()) {
+ SatSlot(statusColor = NoReportGray, count = 0)
+ } else {
+ val newest = inSlot.maxByOrNull { it.reportedTimeUtcSec }!!
+ SatSlot(
+ statusColor = statusColorOf(newest.report),
+ count = inSlot.size,
+ reportIds = inSlot.map { it.id }
+ )
+ }
+ }
+ val days = (0 until 6).map { d ->
+ SatDay(dateLabel = labels[d], slots = slots.subList(d * 12, (d + 1) * 12))
+ }
+ SatStatus(name = name, days = days)
}
}
+
+ private fun toSatReport(r: ApiReport): SatReport {
+ val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
+ cal.timeInMillis = r.reportedTimeUtcSec * 1000
+ val hh = cal.get(Calendar.HOUR_OF_DAY).toString().padStart(2, '0')
+ val mm = cal.get(Calendar.MINUTE).toString().padStart(2, '0')
+ val y = cal.get(Calendar.YEAR)
+ val mo = (cal.get(Calendar.MONTH) + 1).toString().padStart(2, '0')
+ val d = cal.get(Calendar.DAY_OF_MONTH).toString().padStart(2, '0')
+ return SatReport(
+ id = r.id,
+ statusText = r.report,
+ call = r.callsign,
+ grid = r.gridSquare,
+ dateUtc = "$y-$mo-$d",
+ timeUtc = "$hh:$mm UTC"
+ )
+ }
+
+ private fun statusColorOf(report: String): Long = when (report.lowercase()) {
+ "heard", "crew active" -> ActiveBlue
+ "telemetry only" -> TlmOrange
+ "not heard" -> NotHeardPink
+ else -> ConflictDeepOrange
+ }
+
+ companion object {
+ private const val ActiveBlue = 0xFF648FFF
+ private const val TlmOrange = 0xFFFFB000
+ private const val NotHeardPink = 0xFFDC267F
+ private const val ConflictDeepOrange = 0xFFFE6100
+ private const val NoReportGray = 0xFFC0C0C0
+ }
}
diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/amsat/AmSatApiClient.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/amsat/AmSatApiClient.kt
new file mode 100644
index 00000000..787e73ec
--- /dev/null
+++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/amsat/AmSatApiClient.kt
@@ -0,0 +1,98 @@
+/* AmSatApiClient.kt - AMSAT official status API v1 client (pure JVM).
+ * Endpoints (verified 2026-08):
+ * GET https://www.amsat.org/status/api/v1/catalog.php -> satellite list
+ * GET https://www.amsat.org/status/api/v1/reports.php?hours=N&limit=500 -> reports
+ * Report fields: id, name ("SO-50_[FM]"), callsign, report, grid_square, reported_time (ISO 8601 UTC).
+ * Status values: Heard / Telemetry Only / Not Heard / Crew Active.
+ */
+package com.rtbishop.look4sat.core.domain.amsat
+
+import org.json.JSONObject
+import org.json.JSONArray
+import java.io.BufferedReader
+import java.net.HttpURLConnection
+import java.net.URL
+
+/** One report from the AMSAT API. */
+data class ApiReport(
+ val id: String,
+ val name: String, // "SO-50_[FM]" (API name, includes mode suffix)
+ val callsign: String,
+ val report: String, // Heard / Telemetry Only / Not Heard / Crew Active
+ val gridSquare: String,
+ val reportedTimeUtcSec: Long
+)
+
+/** AMSAT official satellite status API v1 client. */
+class AmSatApiClient(private val baseUrl: String = "https://www.amsat.org/status/api/v1") {
+
+ /** Fetch the full satellite catalog. Returns API names (e.g. "SO-50_[FM]"). */
+ fun fetchCatalog(): List {
+ val body = httpGet("$baseUrl/catalog.php") ?: return emptyList()
+ return try {
+ val arr = JSONObject(body).getJSONArray("data")
+ (0 until arr.length()).map { arr.getJSONObject(it).getString("name") }
+ } catch (e: Exception) {
+ emptyList()
+ }
+ }
+
+ /** Fetch reports for a rolling UTC window. Empty on failure. */
+ fun fetchAllReports(hours: Int = 168): List {
+ val body = httpGet("$baseUrl/reports.php?hours=$hours&limit=500") ?: return emptyList()
+ return try {
+ val arr = JSONObject(body).getJSONArray("data")
+ (0 until arr.length()).mapNotNull { i ->
+ val o = arr.getJSONObject(i)
+ val iso = o.optString("reported_time", "")
+ if (iso.isEmpty()) null else ApiReport(
+ id = o.optString("id", ""),
+ name = o.optString("name", ""),
+ callsign = o.optString("callsign", ""),
+ report = o.optString("report", ""),
+ gridSquare = o.optString("grid_square", ""),
+ reportedTimeUtcSec = parseIsoUtcSec(iso)
+ )
+ }
+ } catch (e: Exception) {
+ emptyList()
+ }
+ }
+
+ /** Parse "2026-08-05T07:30:00Z" to epoch seconds (minSdk 24: no java.time). */
+ fun parseIsoUtcSec(iso: String): Long {
+ val m = Regex("""(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})""").find(iso) ?: return 0L
+ val (y, mo, d, h, mi, s) = m.destructured
+ val days = daysFromCivil(y.toInt(), mo.toInt(), d.toInt())
+ return days * 86400L + h.toInt() * 3600L + mi.toInt() * 60L + s.toInt()
+ }
+
+ /** Days since 1970-01-01 (civil calendar, proleptic Gregorian). */
+ private fun daysFromCivil(year: Int, month: Int, day: Int): Long {
+ val y = if (month <= 2) year - 1 else year
+ val era = (if (y >= 0) y else y - 399) / 400
+ val yoe = y - era * 400
+ val doy = (153 * (if (month > 2) month - 3 else month + 9) + 2) / 5 + day - 1
+ val doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
+ return era * 146097L + doe - 719468
+ }
+
+ private fun httpGet(url: String): String? {
+ return try {
+ val conn = URL(url).openConnection() as HttpURLConnection
+ conn.requestMethod = "GET"
+ conn.connectTimeout = 15000
+ conn.readTimeout = 20000
+ conn.setRequestProperty("User-Agent", "Look4Sat/4.5.5")
+ if (conn.responseCode !in 200..299) {
+ conn.disconnect()
+ return null
+ }
+ val text = conn.inputStream.bufferedReader().use(BufferedReader::readText)
+ conn.disconnect()
+ text
+ } catch (e: Exception) {
+ null
+ }
+ }
+}
diff --git a/core/presentation/src/main/res/values-id/strings.xml b/core/presentation/src/main/res/values-id/strings.xml
index 096d2c29..93acfc05 100644
--- a/core/presentation/src/main/res/values-id/strings.xml
+++ b/core/presentation/src/main/res/values-id/strings.xml
@@ -91,13 +91,12 @@
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.
Apa yang baru di Look4Sat
- * BARU: Halaman status satelit AMSAT - tabel status langsung (warna resmi), jumlah laporan, laporan detail per satelit/hari
- * Log dikelompokkan per lintasan satelit (sesi = satelit + waktu AOS), garis pemisah antar sesi
- * GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
- * Pesan kegagalan jelas saat database satelit gagal diunduh
- * Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
- * Perbaikan bahasa Indonesia (values-id)
- * BARU: Pelaporan APRS (jaringan APRS-IS, layanan latar depan dengan notifikasi, interval dapat diatur)
+ * BARU: Status satelit AMSAT kini menggunakan API resmi AMSAT (v1 JSON, lebih cepat dan andal)
+ * Halaman status AMSAT kini mendukung layar penuh (tombol refresh dan waktu selalu dapat diakses)
+ * WaveLog: pencarian grid lawan QRZ (dialog ⚙ tempel Cookie + uji query)
+ * WaveLog: tombol perbarui daftar satelit LoTW (112 satelit bawaan)
+ * WaveLog: laporan sinyal RST 59/59 untuk QSO (v1 dan v2)
+
Kembali
Beri tahu
Azimuth
diff --git a/core/presentation/src/main/res/values-in/strings.xml b/core/presentation/src/main/res/values-in/strings.xml
index beb2715e..bedaa6c5 100644
--- a/core/presentation/src/main/res/values-in/strings.xml
+++ b/core/presentation/src/main/res/values-in/strings.xml
@@ -90,13 +90,12 @@
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.
Apa yang baru di Look4Sat
- * BARU: Halaman status satelit AMSAT - tabel status langsung (warna resmi), jumlah laporan, laporan detail per satelit/hari
- * Log dikelompokkan per lintasan satelit (sesi = satelit + waktu AOS), garis pemisah antar sesi
- * GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
- * Pesan kegagalan jelas saat database satelit gagal diunduh
- * Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
- * Perbaikan bahasa Indonesia (values-id)
- * BARU: Pelaporan APRS (jaringan APRS-IS, layanan latar depan dengan notifikasi, interval dapat diatur)
+ * BARU: Status satelit AMSAT kini menggunakan API resmi AMSAT (v1 JSON, lebih cepat dan andal)
+ * Halaman status AMSAT kini mendukung layar penuh (tombol refresh dan waktu selalu dapat diakses)
+ * WaveLog: pencarian grid lawan QRZ (dialog ⚙ tempel Cookie + uji query)
+ * WaveLog: tombol perbarui daftar satelit LoTW (112 satelit bawaan)
+ * WaveLog: laporan sinyal RST 59/59 untuk QSO (v1 dan v2)
+
Kembali
Beri tahu
Azimuth
diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml
index 1e53690f..93aa4127 100644
--- a/core/presentation/src/main/res/values-tr/strings.xml
+++ b/core/presentation/src/main/res/values-tr/strings.xml
@@ -95,13 +95,12 @@
\n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin.
What\'s new in Look4Sat
- * YENİ: AMSAT uydu durum sayfası - canlı durum tablosu (resmi renkler), rapor sayıları, uydu/gün bazında detaylı raporlar
- * Günlükler uydu geçişine göre gruplanır (oturum = uydu + AOS zamanı), oturumlar arasında ayırıcı çizgi
- * GPS artık yalnızca gerçek konum alındığında başarı gösterir (15 sn zaman aşımı, izin kontrolü)
- * Uydu veritabanı indirilemediğinde net hata mesajı gösterilir
- * Günlük frekansları Doppler panelini gerçek zamanlı takip eder; kaydırarak silme görselleri düzeltildi
- * Endonezya dili düzeltmesi (values-id)
- * YENİ: APRS raporlama (APRS-IS ağı, bildirimli ön plan hizmeti, ayarlanabilir aralık)
+ * YENİ: AMSAT uydu durumu resmi AMSAT API kullanıyor (v1 JSON, daha hızlı ve güvenilir)
+ * AMSAT durum sayfası tam ekran cihazlara uyumlu (yenileme düğmesi ve saat her zaman erişilebilir)
+ * WaveLog: QRZ karşı grid sorgulama (⚙ diyalogda Cookie yapıştır + test sorgusu)
+ * WaveLog: LoTW uydu listesi güncelleme düğmesi (112 uydu yerleşik)
+ * WaveLog: RST 59/59 sinyal raporu (v1 ve v2)
+
Geri
diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml
index fd2caeb3..5a19f412 100644
--- a/core/presentation/src/main/res/values-zh/strings.xml
+++ b/core/presentation/src/main/res/values-zh/strings.xml
@@ -85,13 +85,12 @@
请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性
Look4Sat Pro 更新内容
- * 新增: AMSAT 卫星状态页 - 实时状态表格(官网配色)、报告数量、逐卫星/逐日详细报告
- * 日志按场次分组(场次 = 卫星 + AOS 时间), 组间横线分隔
- * GPS 真正定位成功才会提示成功(15 秒超时, 权限检查)
- * 卫星数据库下载失败时有明确失败提示
- * 日志频率实时跟随多普勒面板; 左滑删除视觉恢复
- * 印尼语识别修复(values-id)
- * 新增: APRS 网络上报(APRS-IS,前台服务+通知保活,周期可调)
+ * 新: AMSAT 卫星状态改用官方 AMSAT API(v1 JSON,更快更稳)
+ * AMSAT 状态页适配全面屏(刷新按钮和更新时间始终可点)
+ * WaveLog: QRZ 对方网格查询(⚙ 弹窗粘贴 Cookie + 测试查询)
+ * WaveLog: LoTW 卫星列表更新按钮(内置 112 颗卫星)
+ * WaveLog: 信号报告 59/59(v1 和 v2)
+
后退
diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml
index 80d774eb..adb749cd 100644
--- a/core/presentation/src/main/res/values/strings.xml
+++ b/core/presentation/src/main/res/values/strings.xml
@@ -97,13 +97,12 @@
\n\nPlease update the database at least weekly to get accurate predictions.
What\'s new in Look4Sat
- * NEW: AMSAT satellite status page - live status grid (official colors), report counts, detailed reports per satellite/day
- * Log entries grouped by satellite pass (session ID = satellite + AOS time), divider lines between sessions
- * GPS now only reports success when a real fix is acquired (15s timeout, permission check)
- * Data update shows a clear failure message when the satellite database cannot be downloaded
- * Log frequencies follow the Doppler panel in real time; swipe-to-delete visuals restored
- * Indonesian locale fix (values-id)
- * NEW: APRS reporting (APRS-IS network, foreground service with notification, configurable interval)
+ * NEW: AMSAT satellite status now uses the official AMSAT API (v1 JSON, faster and more reliable)
+ * AMSAT status page fits edge-to-edge screens (refresh button and update time always reachable)
+ * WaveLog: QRZ counterpart grid lookup (gear dialog > paste QRZ cookies > test query)
+ * WaveLog: LoTW satellite list refresh button (112 satellites built in)
+ * WaveLog: RST 59/59 signal reports for QSOs (v1 and v2)
+
Back
diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt
index e0b6dc07..71bd74b6 100644
--- a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt
+++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt
@@ -15,6 +15,9 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -72,7 +75,13 @@ fun SatStatusScreen(container: IMainContainer) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var selectedDay by remember { mutableStateOf?>(null) }
- Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp)) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ .navigationBarsPadding()
+ .padding(horizontal = 8.dp)
+ ) {
// Top: legend + refresh
StatusHeader(
fetchedAtUtcMs = uiState.fetchedAtUtcMs,
@@ -100,7 +109,10 @@ fun SatStatusScreen(container: IMainContainer) {
HeaderRow(statuses = uiState.statuses)
HorizontalDivider(thickness = 1.dp)
// Row
- LazyColumn(modifier = Modifier.fillMaxSize()) {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(bottom = 16.dp)
+ ) {
items(uiState.statuses, key = { it.name }) { status ->
StatusRow(
status = status,
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index afe393a3..687c5ecd 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,8 +1,8 @@
[versions]
#noinspection UnusedVersionCatalogEntry
-appVersionCode = "454"
+appVersionCode = "455"
#noinspection UnusedVersionCatalogEntry
-appVersionName = "4.5.4"
+appVersionName = "4.5.5"
#noinspection UnusedVersionCatalogEntry
compileSdk = "37"
#noinspection UnusedVersionCatalogEntry