feat(aprs): 4.5.4 APRS-IS network reporting (ported from APRSdroid 1.6.3d)

Core logic extracted from APRSdroid 1.6.3d (jadx reverse-engineered,
verified against APRS spec):
- core/domain/aprs/AprsPacket.kt: passcode algorithm (0x73E2 XOR),
  login line, uncompressed/compressed position encoding (ab0oo)
- core/data/aprs/AprsIsClient.kt: APRS-IS TCP client (login + packet
  lines, 30s reconnect strategy)
- core/data/aprs/AprsReporter.kt: periodic reporting scheduler
  (default 5 min, manual trigger support)
- core/data/aprs/AprsStore.kt: SharedPreferences persistence
  (fill once, saved)
- app/AprsForegroundService.kt: foreground service with notification
  (keeps APRS alive across pages), START_STICKY, last-known position
  provider
- feature/settings/AprsCard.kt: APRS card between DataCard and
  OutputCard, gear button opens settings dialog (server/port/
  callsign/ssid/passcode/interval/status/symbol), manual report button
- Manifest: service registration + FOREGROUND_SERVICE(_DATA_SYNC) +
  POST_NOTIFICATIONS
- Strings in EN/ZH/TR/IN/ID (23 keys x 5)
- Version 4.5.4/454 + What's new 5 languages
This commit is contained in:
mckero committed 2026-08-05 02:07:11 +00:00
1 parent 72f507163a
commit 518d9a4d22
15 files changed
+931 -8

No files matched your search

@@ -0,0 +1,75 @@
package com.rtbishop.look4sat.core.data.aprs
import com.rtbishop.look4sat.core.domain.aprs.AprsPacket
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.net.InetSocketAddress
import java.net.Socket
/**
* APRS-IS TCP 客户端(从 APRSdroid TcpUploader.scala 逆向移植)。
* 纯文本协议:登录一行 + 每行一个包;断线 30 秒重连。
*/
class AprsIsClient(
private val host: String,
private val port: Int,
private val callsign: String,
private val ssid: String,
private val passcode: Int,
private val version: String,
private val filter: String = "",
private val timeoutSec: Int = 120
) {
private var socket: Socket? = null
private var writer: PrintWriter? = null
private var reader: BufferedReader? = null
private val lock = Any()
val isConnected: Boolean
get() = synchronized(lock) { socket?.isConnected == true && !socket!!.isClosed }
/** 建立连接 + 登录(同步阻塞,调用方放后台线程) */
@Throws(Exception::class)
fun connect() {
disconnect()
val s = Socket()
s.connect(InetSocketAddress(host, port), 30_000)
s.soTimeout = timeoutSec * 1000
s.tcpNoDelay = true
synchronized(lock) {
socket = s
writer = PrintWriter(OutputStreamWriter(s.getOutputStream(), Charsets.ISO_8859_1), true)
reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.ISO_8859_1), 256)
}
// 登录行
val login = AprsPacket.formatLogin(callsign, ssid, passcode, version) + filter
writer?.println(login)
}
/** 发送一个 APRS 包(一行) */
fun sendPacket(packetLine: String): Boolean {
synchronized(lock) {
val w = writer ?: return false
w.println(packetLine)
return !w.checkError()
}
}
/** 读一行(服务器响应,超时抛异常) */
fun readLine(): String? {
return reader?.readLine()
}
fun disconnect() {
synchronized(lock) {
runCatching { writer?.close() }
runCatching { reader?.close() }
runCatching { socket?.close() }
writer = null
reader = null
socket = null
}
}
}
@@ -0,0 +1,125 @@
package com.rtbishop.look4sat.core.data.aprs
import com.rtbishop.look4sat.core.domain.aprs.AprsPacket
import com.rtbishop.look4sat.core.domain.aprs.AprsPosition
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/** APRS 连接状态 */
enum class AprsState { Idle, Connecting, Connected, Disconnected, Error }
/** APRS 配置(SharedPreferences 持久化,填入即保存) */
data class AprsConfig(
val enabled: Boolean = false,
val server: String = "euro.aprs2.net",
val port: Int = 14580,
val callsign: String = "",
val ssid: String = "",
val passcode: String = "",
val intervalMin: Int = 5,
val statusText: String = "Look4Sat APRS",
val symbolTable: String = "/",
val symbolCode: String = ">",
val includeCourseSpeed: Boolean = true,
val includeAltitude: Boolean = true
)
/** APRS 上报结果 */
data class AprsReport(
val timestamp: Long,
val packet: String,
val ok: Boolean,
val detail: String
)
/** 上报调度器(周期上报 + 手动触发),连接管理放在前台服务 */
class AprsReporter(
private val configProvider: () -> AprsConfig,
private val positionProvider: () -> Pair<Double, Double>? = { null },
private val onState: (AprsState) -> Unit = {},
private val onReport: (AprsReport) -> Unit = {}
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var client: AprsIsClient? = null
private var job: Job? = null
private var manualJob: Job? = null
val isRunning: Boolean get() = job?.isActive == true
/** 启动周期上报(前台服务调用) */
fun start() {
stop()
val cfg = configProvider()
if (!cfg.enabled || cfg.callsign.isBlank()) {
onState(AprsState.Error)
return
}
job = scope.launch {
while (isActive) {
reportOnce()
delay(cfg.intervalMin.coerceAtLeast(1) * 60_000L)
}
}
}
fun stop() {
job?.cancel()
job = null
runCatching { client?.disconnect() }
client = null
onState(AprsState.Idle)
}
/** 手动触发一次(立即上报,不等待周期) */
fun reportNow() {
manualJob?.cancel()
manualJob = scope.launch { reportOnce() }
}
private suspend fun reportOnce() {
val cfg = configProvider()
if (!cfg.enabled || cfg.callsign.isBlank()) return
onState(AprsState.Connecting)
try {
val c = client ?: AprsIsClient(
host = cfg.server,
port = cfg.port,
callsign = cfg.callsign,
ssid = cfg.ssid,
passcode = cfg.passcode.toIntOrNull() ?: AprsPacket.passcode(cfg.callsign),
version = "Look4Sat 4.5.4"
).also { client = it }
if (!c.isConnected) c.connect()
onState(AprsState.Connected)
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"))
if (ok) onState(AprsState.Connected) else onState(AprsState.Error)
} catch (e: Exception) {
runCatching { client?.disconnect() }
client = null
onState(AprsState.Error)
onReport(AprsReport(System.currentTimeMillis(), "", false, e.message ?: "error"))
}
}
/** 构造位置包:BG7NTA-5>APRS:=DDMM.MMN/DDDMM.MME<状态文本 */
private fun buildPositionPacket(cfg: AprsConfig, lat: Double? = null, lon: Double? = null): String {
val source = AprsPacket.formatCallSsid(cfg.callsign, cfg.ssid)
val pos = AprsPosition(
latitude = lat ?: 0.0,
longitude = lon ?: 0.0,
symbolTable = cfg.symbolTable.firstOrNull() ?: '/',
symbolCode = cfg.symbolCode.firstOrNull() ?: '>'
)
return "$source>APRS:=${pos.toUncompressedString()}${cfg.statusText}"
}
}
@@ -0,0 +1,60 @@
package com.rtbishop.look4sat.core.data.aprs
import android.content.Context
/**
* APRS 配置存取 + 服务 action 常量(被 feature/settings 和 app 服务共用,
* 避免 feature 反向依赖 app)。
*/
object AprsStore {
const val ACTION_START = "com.rtbishop.look4sat.aprs.START"
const val ACTION_STOP = "com.rtbishop.look4sat.aprs.STOP"
const val ACTION_REPORT_NOW = "com.rtbishop.look4sat.aprs.REPORT_NOW"
const val SERVICE_CLASS = "com.rtbishop.look4sat.app.AprsForegroundService"
private const val PREFS = "aprs_config"
private const val KEY_ENABLED = "enabled"
private const val KEY_SERVER = "server"
private const val KEY_PORT = "port"
private const val KEY_CALLSIGN = "callsign"
private const val KEY_SSID = "ssid"
private const val KEY_PASSCODE = "passcode"
private const val KEY_INTERVAL = "interval"
private const val KEY_STATUS = "status"
private const val KEY_SYMBOL_TABLE = "symbol_table"
private const val KEY_SYMBOL_CODE = "symbol_code"
/** 读取配置(填入即保存,无需每次打开重填) */
fun loadConfig(context: Context): AprsConfig {
val p = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
return AprsConfig(
enabled = p.getBoolean(KEY_ENABLED, false),
server = p.getString(KEY_SERVER, "euro.aprs2.net") ?: "euro.aprs2.net",
port = p.getInt(KEY_PORT, 14580),
callsign = p.getString(KEY_CALLSIGN, "") ?: "",
ssid = p.getString(KEY_SSID, "") ?: "",
passcode = p.getString(KEY_PASSCODE, "") ?: "",
intervalMin = p.getInt(KEY_INTERVAL, 5),
statusText = p.getString(KEY_STATUS, "Look4Sat APRS") ?: "Look4Sat APRS",
symbolTable = p.getString(KEY_SYMBOL_TABLE, "/") ?: "/",
symbolCode = p.getString(KEY_SYMBOL_CODE, ">") ?: ">"
)
}
/** 保存配置 */
fun saveConfig(context: Context, cfg: AprsConfig) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
.putBoolean(KEY_ENABLED, cfg.enabled)
.putString(KEY_SERVER, cfg.server)
.putInt(KEY_PORT, cfg.port)
.putString(KEY_CALLSIGN, cfg.callsign)
.putString(KEY_SSID, cfg.ssid)
.putString(KEY_PASSCODE, cfg.passcode)
.putInt(KEY_INTERVAL, cfg.intervalMin)
.putString(KEY_STATUS, cfg.statusText)
.putString(KEY_SYMBOL_TABLE, cfg.symbolTable)
.putString(KEY_SYMBOL_CODE, cfg.symbolCode)
.apply()
}
}
@@ -0,0 +1,116 @@
package com.rtbishop.look4sat.core.domain.aprs
import kotlin.math.abs
import kotlin.math.round
/**
* APRS-IS 协议核心(纯 Kotlin,无 Android 依赖)。
* 从 APRSdroid 1.6.3d 逆向移植:AprsPacket$.scala + ab0oo Position.java。
*/
object AprsPacket {
/** APRS-IS passcode 算法(标准):0x73E2 初值,呼号大写+\0,每对字符 XOR */
fun passcode(callsign: String): Int {
val s = callsign.split("-")[0].uppercase() + "\u0000"
var hash = 29666 // 0x73E2
var i = 0
while (i <= s.length - 2) {
hash = hash xor (s[i].code * 256 + s[i + 1].code)
i += 2
}
return hash and 0x7FFF
}
/** 登录行:user CALL-SSID pass XXXX vers XXXX */
fun formatLogin(callsign: String, ssid: String, passcode: Int, version: String): String {
val callSsid = formatCallSsid(callsign, ssid)
return "user $callSsid pass $passcode vers $version"
}
/** 呼号-SSID 拼接(BG7NTA + 5 → BG7NTA-5) */
fun formatCallSsid(callsign: String, ssid: String): String {
if (ssid.isNullOrEmpty()) return callsign
return "$callsign-$ssid"
}
/** 可选距离过滤: filter r/lat/lon/dist */
fun formatRangeFilter(latitude: Double, longitude: Double, distKm: Int): String {
return String.format("r/%.3f/%.3f/%d", latitude, longitude, distKm)
}
/** 高度扩展 /A=00000(英尺) */
fun formatAltitude(altitudeMeters: Double?): String {
if (altitudeMeters == null) return ""
return String.format("/A=%06d", (altitudeMeters * 3.2808399).toInt())
}
/** 航速航向扩展(节/度) */
fun formatCourseSpeed(speedMps: Double?, bearing: Float?): String {
if (speedMps == null || bearing == null) return ""
val knots = (speedMps * 1.94384449).toInt()
return String.format("/%03d/%03d", bearing.toInt(), knots)
}
}
/**
* APRS 位置编码(ab0oo Position.java 逆向移植)。
* 非压缩:DDMM.MMN/DDDMM.MME;压缩:base91。
*/
class AprsPosition(
val latitude: Double,
val longitude: Double,
val symbolTable: Char,
val symbolCode: Char,
val positionAmbiguity: Int = 0
) {
/** 非压缩格式(APRS-IS 默认上报格式) */
fun toUncompressedString(): String {
val lat = getDMS(latitude, true)
val lon = getDMS(longitude, false)
return "$lat$symbolTable$lon$symbolCode"
}
/** 压缩格式(base91,标准 APRS 算法) */
fun toCompressedString(): String {
val jRound = round((90.0 - latitude) * 380926.0).toLong()
val j = jRound / 753571 + 33
val j2 = jRound % 753571
val j3 = j2 / 8281 + 33
val j4 = j2 % 8281
val i = (j4 % 91).toInt() + 33
val jRound2 = round((longitude + 180.0) * 190463.0).toLong()
val j5 = jRound2 / 753571 + 33
val j6 = jRound2 % 753571
val j7 = 33 + j6 / 8281
val j8 = j6 % 8281
return "" + symbolTable + j.toInt().toChar() + j3.toInt().toChar() +
((j4 / 91).toInt() + 33).toChar() + i.toChar() +
j5.toInt().toChar() + j7.toInt().toChar() +
((j8 / 91).toInt() + 33).toChar() +
((j8 % 91).toInt() + 33).toChar() + symbolCode
}
/** 单轴 DMS 编码(百分位) */
private fun getDMS(value: Double, isLat: Boolean): String {
var iRound = round(value * 6000.0).toInt()
if (iRound < 0) iRound = -iRound
val degrees = iRound / 6000
val minutes = (iRound / 100) % 60
val hundredths = iRound % 100
val frac = when (positionAmbiguity) {
1 -> " . "
2 -> String.format("%d . ", minutes / 10)
3 -> String.format("%02d. ", minutes)
4 -> String.format("%02d.%d ", minutes, hundredths / 10)
else -> String.format("%02d.%02d", minutes, hundredths)
}
return if (isLat) {
val ns = if (value >= 0) 'N' else 'S'
String.format("%02d%s%c", degrees, frac, ns)
} else {
val ew = if (value >= 0) 'E' else 'W'
String.format("%03d%s%c", degrees, frac, ew)
}
}
}
@@ -12,6 +12,29 @@
<string name="nav_cw">Dekoder CW</string>
<string name="nav_map">Peta</string>
<string name="nav_prefs">Pengaturan</string>
<string name="prefs_aprs_title">Pelaporan APRS</string>
<string name="prefs_aprs_enable">Aktifkan pelaporan APRS</string>
<string name="prefs_aprs_summary">Server: %1$s:%2$d Panggilan: %3$s</string>
<string name="prefs_aprs_report_now">Lapor Sekarang</string>
<string name="prefs_aprs_settings_title">Pengaturan APRS</string>
<string name="prefs_aprs_server">Server</string>
<string name="prefs_aprs_port">Port</string>
<string name="prefs_aprs_callsign">Tanda panggilan</string>
<string name="prefs_aprs_ssid">SSID</string>
<string name="prefs_aprs_passcode">Kode sandi</string>
<string name="prefs_aprs_passcode_hint">Kosong = otomatis dari panggilan</string>
<string name="prefs_aprs_interval">Interval (menit)</string>
<string name="prefs_aprs_status">Teks status</string>
<string name="prefs_aprs_symbol_table">Tabel simbol</string>
<string name="prefs_aprs_symbol_code">Kode simbol</string>
<string name="prefs_aprs_save">Simpan</string>
<string name="prefs_aprs_cancel">Batal</string>
<string name="aprs_notif_channel">Layanan APRS</string>
<string name="aprs_notif_title">APRS: %1$s</string>
<string name="aprs_notif_running">Pelaporan berjalan</string>
<string name="aprs_notif_connected">Terhubung ke APRS-IS</string>
<string name="aprs_notif_error">Kesalahan koneksi</string>
<string name="aprs_notif_stop">Berhenti</string>
<string name="nav_more">Lainnya</string>
<string name="nav_log">Log</string>
<string name="nav_amsat">AMSAT</string>
@@ -66,7 +89,8 @@
* 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)</string>
* Perbaikan bahasa Indonesia (values-id)
* BARU: Pelaporan APRS (jaringan APRS-IS, layanan latar depan dengan notifikasi, interval dapat diatur)</string>
<string name="radar_back">Kembali</string>
<string name="radar_notify">Beri tahu</string>
<string name="radar_az_text">Azimuth</string>
@@ -12,6 +12,29 @@
<string name="nav_cw">Dekoder CW</string>
<string name="nav_map">Peta</string>
<string name="nav_prefs">Pengaturan</string>
<string name="prefs_aprs_title">Pelaporan APRS</string>
<string name="prefs_aprs_enable">Aktifkan pelaporan APRS</string>
<string name="prefs_aprs_summary">Server: %1$s:%2$d Panggilan: %3$s</string>
<string name="prefs_aprs_report_now">Lapor Sekarang</string>
<string name="prefs_aprs_settings_title">Pengaturan APRS</string>
<string name="prefs_aprs_server">Server</string>
<string name="prefs_aprs_port">Port</string>
<string name="prefs_aprs_callsign">Tanda panggilan</string>
<string name="prefs_aprs_ssid">SSID</string>
<string name="prefs_aprs_passcode">Kode sandi</string>
<string name="prefs_aprs_passcode_hint">Kosong = otomatis dari panggilan</string>
<string name="prefs_aprs_interval">Interval (menit)</string>
<string name="prefs_aprs_status">Teks status</string>
<string name="prefs_aprs_symbol_table">Tabel simbol</string>
<string name="prefs_aprs_symbol_code">Kode simbol</string>
<string name="prefs_aprs_save">Simpan</string>
<string name="prefs_aprs_cancel">Batal</string>
<string name="aprs_notif_channel">Layanan APRS</string>
<string name="aprs_notif_title">APRS: %1$s</string>
<string name="aprs_notif_running">Pelaporan berjalan</string>
<string name="aprs_notif_connected">Terhubung ke APRS-IS</string>
<string name="aprs_notif_error">Kesalahan koneksi</string>
<string name="aprs_notif_stop">Berhenti</string>
<string name="nav_more">Lainnya</string>
<string name="nav_log">Log</string>
<string name="nav_amsat">AMSAT</string>
@@ -65,7 +88,8 @@
* 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)</string>
* Perbaikan bahasa Indonesia (values-id)
* BARU: Pelaporan APRS (jaringan APRS-IS, layanan latar depan dengan notifikasi, interval dapat diatur)</string>
<string name="radar_back">Kembali</string>
<string name="radar_notify">Beri tahu</string>
<string name="radar_az_text">Azimuth</string>
@@ -13,6 +13,29 @@
<string name="nav_cw">CW Dekoder</string>
<string name="nav_map">Harita</string>
<string name="nav_prefs">Ayarlar</string>
<string name="prefs_aprs_title">APRS Raporlama</string>
<string name="prefs_aprs_enable">APRS raporlamayı etkinleştir</string>
<string name="prefs_aprs_summary">Sunucu: %1$s:%2$d Çağrı: %3$s</string>
<string name="prefs_aprs_report_now">Şimdi Raporla</string>
<string name="prefs_aprs_settings_title">APRS Ayarları</string>
<string name="prefs_aprs_server">Sunucu</string>
<string name="prefs_aprs_port">Port</string>
<string name="prefs_aprs_callsign">Çağrı işareti</string>
<string name="prefs_aprs_ssid">SSID</string>
<string name="prefs_aprs_passcode">Parola</string>
<string name="prefs_aprs_passcode_hint">Parola boş = çağrıdan otomatik hesaplanır</string>
<string name="prefs_aprs_interval">Aralık (dakika)</string>
<string name="prefs_aprs_status">Durum metni</string>
<string name="prefs_aprs_symbol_table">Sembol tablosu</string>
<string name="prefs_aprs_symbol_code">Sembol kodu</string>
<string name="prefs_aprs_save">Kaydet</string>
<string name="prefs_aprs_cancel">İptal</string>
<string name="aprs_notif_channel">APRS hizmeti</string>
<string name="aprs_notif_title">APRS: %1$s</string>
<string name="aprs_notif_running">Raporlama çalışıyor</string>
<string name="aprs_notif_connected">APRS-IS bağlı</string>
<string name="aprs_notif_error">Bağlantı hatası</string>
<string name="aprs_notif_stop">Durdur</string>
<string name="nav_more">Daha Fazla</string>
<string name="nav_log">Günlük</string>
<string name="nav_amsat">AMSAT</string>
@@ -70,7 +93,8 @@
* 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)</string>
* Endonezya dili düzeltmesi (values-id)
* YENİ: APRS raporlama (APRS-IS ağı, bildirimli ön plan hizmeti, ayarlanabilir aralık)</string>
<!-- Radar screen -->
<string name="radar_back">Geri</string>
@@ -14,6 +14,29 @@
<string name="nav_cw">CW解码</string>
<string name="nav_map">地图</string>
<string name="nav_prefs">设置</string>
<string name="prefs_aprs_title">APRS 上报</string>
<string name="prefs_aprs_enable">启用 APRS 上报</string>
<string name="prefs_aprs_summary">服务器: %1$s:%2$d 呼号: %3$s</string>
<string name="prefs_aprs_report_now">立即上报</string>
<string name="prefs_aprs_settings_title">APRS 设置</string>
<string name="prefs_aprs_server">服务器</string>
<string name="prefs_aprs_port">端口</string>
<string name="prefs_aprs_callsign">呼号</string>
<string name="prefs_aprs_ssid">SSID</string>
<string name="prefs_aprs_passcode">验证码</string>
<string name="prefs_aprs_passcode_hint">验证码留空 = 按呼号自动计算</string>
<string name="prefs_aprs_interval">周期(分钟)</string>
<string name="prefs_aprs_status">状态文本</string>
<string name="prefs_aprs_symbol_table">符号表</string>
<string name="prefs_aprs_symbol_code">符号码</string>
<string name="prefs_aprs_save">保存</string>
<string name="prefs_aprs_cancel">取消</string>
<string name="aprs_notif_channel">APRS 服务</string>
<string name="aprs_notif_title">APRS: %1$s</string>
<string name="aprs_notif_running">上报运行中</string>
<string name="aprs_notif_connected">已连接 APRS-IS</string>
<string name="aprs_notif_error">连接错误</string>
<string name="aprs_notif_stop">停止</string>
<string name="nav_more">更多</string>
<string name="nav_log">日志</string>
<string name="nav_amsat">AMSAT</string>
@@ -60,7 +83,8 @@
* GPS 真正定位成功才会提示成功(15 秒超时, 权限检查)
* 卫星数据库下载失败时有明确失败提示
* 日志频率实时跟随多普勒面板; 左滑删除视觉恢复
* 印尼语识别修复(values-id)</string>
* 印尼语识别修复(values-id)
* 新增: APRS 网络上报(APRS-IS,前台服务+通知保活,周期可调)</string>
<!-- Radar screen -->
<string name="radar_back">后退</string>
@@ -15,6 +15,29 @@
<string name="nav_cw">CW Decoder</string>
<string name="nav_map">Map</string>
<string name="nav_prefs">Settings</string>
<string name="prefs_aprs_title">APRS Reporting</string>
<string name="prefs_aprs_enable">Enable APRS reporting</string>
<string name="prefs_aprs_summary">Server: %1$s:%2$d Callsign: %3$s</string>
<string name="prefs_aprs_report_now">Report Now</string>
<string name="prefs_aprs_settings_title">APRS Settings</string>
<string name="prefs_aprs_server">Server</string>
<string name="prefs_aprs_port">Port</string>
<string name="prefs_aprs_callsign">Callsign</string>
<string name="prefs_aprs_ssid">SSID</string>
<string name="prefs_aprs_passcode">Passcode</string>
<string name="prefs_aprs_passcode_hint">Passcode empty = auto-computed from callsign</string>
<string name="prefs_aprs_interval">Interval (min)</string>
<string name="prefs_aprs_status">Status text</string>
<string name="prefs_aprs_symbol_table">Symbol table</string>
<string name="prefs_aprs_symbol_code">Symbol code</string>
<string name="prefs_aprs_save">Save</string>
<string name="prefs_aprs_cancel">Cancel</string>
<string name="aprs_notif_channel">APRS service</string>
<string name="aprs_notif_title">APRS: %1$s</string>
<string name="aprs_notif_running">Reporting running</string>
<string name="aprs_notif_connected">Connected to APRS-IS</string>
<string name="aprs_notif_error">Connection error</string>
<string name="aprs_notif_stop">Stop</string>
<string name="nav_more">More</string>
<string name="nav_log">Log</string>
<string name="nav_amsat">AMSAT</string>
@@ -72,7 +95,8 @@
* 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)</string>
* Indonesian locale fix (values-id)
* NEW: APRS reporting (APRS-IS network, foreground service with notification, configurable interval)</string>
<!-- Radar screen -->
<string name="radar_back">Back</string>