fix(aprs): upload feedback, manual report reliability, station position

User testing round 3 (4.5.4): no success feedback on upload, aprs.fi
shows nothing, passcode calculator OK, notification present.

- Manual report now works even when service not running: ACTION_REPORT_NOW
  starts the service first (Toast "not configured" if missing callsign)
- Upload result feedback guaranteed: Toast always shows (short OK /
  long fail+reason), last result persisted (time/ok/detail) and shown
  in the settings card "Last report: HH:mm:ss OK/failed - detail"
- Position source: station position from settingsRepo (user decision)
  with live GPS last-known as fallback
- sendPacket reads the server confirmation line (short 3s timeout);
  server error text (Invalid/error) surfaces in Toast + card
- Strings EN/ZH/TR/IN/ID +4 keys
Verified: compileDebugKotlin all modules BUILD SUCCESSFUL,
check_strings 9 files OK.
This commit is contained in:
mckero committed 2026-08-05 03:29:36 +00:00
1 parent 012ea1eeb6
commit c9d87be289
10 files changed
+131 -19

No files matched your search

@@ -59,13 +59,33 @@ class AprsIsClient(
}
}
/** 发送一个 APRS 包(一行) */
fun sendPacket(packetLine: String): Boolean {
/**
* 发送一个 APRS 包(一行)并尝试读服务器确认。
* 返回 null=发送失败;Pair(ok, detail)=发送结果(服务器错误原文在 detail)
*/
fun sendPacket(packetLine: String): Pair<Boolean, String>? {
synchronized(lock) {
val w = writer ?: return false
val w = writer ?: return null
w.println(packetLine)
return !w.checkError()
if (w.checkError()) return Pair(false, "write failed")
}
// 尝试读服务器响应(短超时 3s;APRS-IS 对格式错误会回错误行)
return runCatching {
val s = socket ?: return@runCatching Pair(true, "OK")
val oldTimeout = s.soTimeout
s.soTimeout = 3000
try {
val resp = reader?.readLine()
if (resp != null && (resp.contains("Invalid", ignoreCase = true) ||
resp.contains("error", ignoreCase = true))) {
Pair(false, resp.trim())
} else {
Pair(true, if (resp.isNullOrBlank()) "OK" else resp.trim())
}
} finally {
s.soTimeout = oldTimeout
}
}.getOrElse { Pair(true, "OK") }
}
/** 读一行(服务器响应,超时抛异常) */
@@ -99,9 +99,10 @@ class AprsReporter(
val pos = positionProvider()
val packetLine = buildPositionPacket(cfg, pos?.first, pos?.second)
val ok = c.sendPacket(packetLine)
onReport(AprsReport(System.currentTimeMillis(), packetLine, ok,
if (ok) "OK" else "send failed"))
val result = c.sendPacket(packetLine)
val ok = result?.first == true
val detail = result?.second ?: "no connection"
onReport(AprsReport(System.currentTimeMillis(), packetLine, ok, detail))
if (ok) onState(AprsState.Connected) else onState(AprsState.Error)
} catch (e: Exception) {
runCatching { client?.disconnect() }
@@ -24,6 +24,9 @@ object AprsStore {
private const val KEY_STATUS = "status"
private const val KEY_SYMBOL_TABLE = "symbol_table"
private const val KEY_SYMBOL_CODE = "symbol_code"
private const val KEY_LAST_TIME = "last_report_time"
private const val KEY_LAST_OK = "last_report_ok"
private const val KEY_LAST_DETAIL = "last_report_detail"
/** 读取配置(填入即保存,无需每次打开重填) */
fun loadConfig(context: Context): AprsConfig {
@@ -42,6 +45,26 @@ object AprsStore {
)
}
/** 上次上报结果(供设置卡片显示) */
data class LastReport(val time: Long = 0L, val ok: Boolean = false, val detail: String = "")
fun loadLastReport(context: Context): LastReport {
val p = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
return LastReport(
time = p.getLong(KEY_LAST_TIME, 0L),
ok = p.getBoolean(KEY_LAST_OK, false),
detail = p.getString(KEY_LAST_DETAIL, "") ?: ""
)
}
fun saveLastReport(context: Context, ok: Boolean, detail: String) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
.putLong(KEY_LAST_TIME, System.currentTimeMillis())
.putBoolean(KEY_LAST_OK, ok)
.putString(KEY_LAST_DETAIL, detail)
.apply()
}
/** 保存配置 */
fun saveConfig(context: Context, cfg: AprsConfig) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()