Commit Graph
225 Commits
Author SHA1 Message Date
mckero 2e1d8b9c00 feat(cw): archive decoded history, polish the waterfall, document licensing
三个用户反馈一并解决:

1. 解码文字不再消失 (核心)
   旧实现: 20 秒环形缓冲满了就静默覆盖最旧样本, 文字随之从屏幕消失。
   新实现: CwDeepBuffer 新增 overflow —— 满时被覆盖的旧样本先进 overflow,
   解码器累积到 15 秒就单独解码一次, 结果追加到 historyText (只增不减)。
   UI 记录区显示 historyText + decodedText (历史稳定 + 当前窗口实时)。
   ICwDecoder 接口新增 historyText StateFlow。

   归档音频已离开主窗口, 不再被 CTC 修正, 故其文本是"最终版", 追加安全。
   归档窗口 15 秒: 内容已在 20 秒窗口里解过多次, 短一点几乎无损, 且推理
   开销小。

2. 瀑布图更好看
   配色从"深蓝->青->黄"换成 matplotlib inferno (黑->紫->品红->橙->黄),
   与静态频谱图保持一致。相邻 bin 之间用水平渐变做线性插值, 消除 65 列
   离散方块的像素感。

3. AGPL 合规补漏 (用户提醒: 仓库许可证没体现 AGPL 组件)
   README 新增 License 章节: 声明项目主体 GPL-3.0 + feature/cw 的 DeepCW
   模型 AGPL-3.0-only, 并说明合并作品按 GPL-3.0 §13 / AGPL-3.0 §13 处理。

验证:
- :core:domain:test => 31 个 CW 测试全绿 (CwDeepBufferTest 新增 3 个
  overflow 归档测试: 顺序/清空/reset)
- :core:domain:compileKotlin + :core:data + :feature:cw:compileDebugKotlin
  => BUILD SUCCESSFUL
2026-08-13 08:04:05 +00:00
mckero ac5cbbc49a fix(cw): add crash probes to localize adb-less flash-crash
闪回桌面且无任何 Java 日志 => 崩溃发生在 native 层 (ONNX 库内部段错误)
或进程被系统 LMK 直接杀掉 (内存不足) —— 两种情况 Java 的
uncaughtExceptionHandler 都不会触发, crash_log.txt 自然为空。

新增 CwProbe: 在关键节点向 files/probe_cw.txt 追加时间戳行:
  decoder_constructed -> load_begin -> load_session_ok / load_failed:<类名>
  -> infer_begin -> infer_done
进程若中途死亡, 最后一行的节点就是崩溃/被杀位置, 无需 adb 即可定位。

验证: :core:data:compileDebugKotlin => BUILD SUCCESSFUL
2026-08-13 00:46:56 +00:00
mckero cf49dcfa01 fix(cw): persist decoder failures to files for adb-less diagnostics
在服务器上完整复现了手机端流程 (真实 20s 含噪音频 -> 重采样 -> 频谱 ->
ONNX -> 贪心 CTC), 解码逐字符正确:
  decoded: [CQ CQ DE BG7NTA BG7NTA K 5NN TU 73]   (与参考完全一致)
整条链路逻辑无问题, 闪退属环境/平台层。

应用内已有全局崩溃捕获器 (MainApplication.installCrashHandler) 会把堆栈
写入 files/crash_log.txt —— 无需 adb 即可取到崩溃原因。

本提交为加载与推理两处异常补充落盘:
- 模型加载失败 -> files/deepcw_load_error.txt
- 推理异常 -> files/deepcw_infer_error.txt

用户可在文件管理器按
Android/data/com.rtbishop.look4sat.bg7nta/files/ 路径取回这些日志。

验证: :core:data:compileDebugKotlin => BUILD SUCCESSFUL
2026-08-12 16:43:52 +00:00
mckero 5f297f9233 fix(cw): stop the waterfall crashing and cut APK size by 87MB
三个真机实测暴露的问题:

1. 闪退 (给权限后 3-4 秒必崩, 且注入日志抓不到堆栈)
   CwWaterfallState 用 mutableIntStateOf 记录重绘版本号, 却从音频采集线程
   写入。Compose 快照状态只能在合成线程修改, 从后台线程写会在运行时崩溃 ——
   崩在 Compose 内部, 所以业务类的日志注入抓不到。
   改用 MutableStateFlow (本身线程安全), Canvas 侧 collectAsState 读取。

2. 同一状态的跨线程数据竞争
   pushSamples 在采集线程写 rows/pending, snapshot() 在绘制线程读, 而
   ArrayDeque 非线程安全 —— 并发 removeFirst()/toList() 会抛
   ConcurrentModificationException 或 IndexOutOfBoundsException。
   两侧统一加锁; FFT 计算放在锁外, 只有队列追加持锁。

3. APK 从 8.4MB 暴涨到 135MB
   onnxruntime-android 的 AAR 自带 4 个架构原生库: arm64 28M + armv7 20M +
   x86 33M + x86_64 34M = 115MB。上次提交移除 abiFilters 时把 x86 系列也
   打包了进去 (仅模拟器需要)。
   重新加上 abiFilters, 保留 arm64-v8a + armeabi-v7a 两个真机 ABI,
   预计降至约 43MB。注意这与上次"恢复 64 位"不冲突: 那次删的是只留
   armeabi-v7a 的限制, 这次是排除 x86 系列。

另外两处加固:
- CwDeepDecoder 的模型加载从 init{} 移入惰性 ensureLoaded(): 加载会触发
  ONNX Runtime 原生库装载, 失败时抛 UnsatisfiedLinkError; 在构造函数中抛出
  会连带崩掉创建它的 composable, try-catch 也救不回来。移到首次使用时执行,
  失败经 errorMessage 上报给 UI。
- ONNX 会话限制 intraOp 线程数为 (核数-1) 且上限 4, 给音频采集和 UI 留出
  余量, 默认行为会铺满所有核心。

验证:
./gradlew :feature:cw:compileDebugKotlin :core:data:compileDebugKotlin => 通过
./gradlew :core:domain:test => 79 个测试全绿
2026-08-12 13:55:03 +00:00
mckero d93e3b4028 feat(settings): credit DeepCW (AGPL-3.0) in the about section
AGPL-3.0 要求署名。在设置页"关于"卡片的许可说明下方显示 DeepCW 模型的
作者 (e04)、许可证 (AGPL-3.0-only) 与上游仓库链接。

字符串标 translatable="false": 署名与许可证名称不应翻译。
完整声明见 feature/cw/licenses/NOTICE.md。

验证: ./gradlew :feature:settings:compileDebugKotlin => BUILD SUCCESSFUL
2026-08-12 13:03:59 +00:00
mckero 9b0d543f12 refactor(cw)!: replace legacy CW engine with DeepCW in both entry points
DeepCW 成为唯一 CW 解码内核, 不保留旧引擎作兜底 (用户决定: 完全移植)。

删除旧内核 (1298 行):
- CwDecoder.kt / CwBayesianDecoder.kt / CwChannelTracker.kt / CwSpectrogram.kt
- CwDsp / CwFFT / CwSTFFT / CwGoertzel / CwFilter / CwResampler.kt
- CwDecoderTest.kt
旧内核的自动定频与 squelch 门控由 DeepCW 的 400-1200Hz 固定频窗替代 ——
模型自带定频, 不再需要频谱峰值跟踪和噪声门。

两个 CW 入口都改接 DeepCW:
1. feature:cw 独立整页 CwDecodeScreen.kt 重写为纯 Compose (瀑布图 -> 实时行
   -> 历史区), 移除 AndroidView/LayoutInflater 和被删的 activity_main 布局;
   删除 CwSettingsDialog.kt (旧引擎的手动音调/带宽面板, DeepCW 全自动无需)
2. feature:radar 内嵌可折叠面板改走 ViewModel 的 CW state/action, 移除
   Morse Expert 控制器与 cw_panel_main 布局
   (该面板此前注释写明 "no longer feeds it", ViewModel 里的 CW 通路是死代码)

新增 CwWaterfall.kt: 复用 CwDeepSpectrogram 绘制瀑布图, 显示的正是模型
分析的 400-1200Hz 频段与同一批幅值。

接口接线:
- IMainContainer.provideCwDecoder() 提供 ICwDecoder (实现需 Context 读 assets)
- RadarViewModel 构造新增 cwDecoderFactory, onCleared() 中 close() 释放
  OrtSession 避免原生内存泄漏
- 删除已无调用方的 RadarAction.CwSetToneFreq (DeepCW 定频固定, 无参数可调);
  CwSubState.cwToneFreq 语义改为只读显示检测到的音调

依赖清理:
- feature:radar 不再依赖 feature:cw 与 constraintlayout
- feature:cw 不再依赖 constraintlayout

字符串: feature:cw 清理 Morse Expert 遗留串 (premium/rate/help/
tap_back_again_to_close/purple_500), 按 en/zh/tr/in/id 五语补全新串;
core:presentation 补 radar_cw_tone / radar_cw_waiting 五语。

验证:
./gradlew :core:domain:test => 4 个测试类全绿
./gradlew :feature:cw:compileDebugKotlin => BUILD SUCCESSFUL
./gradlew :feature:radar:compileDebugKotlin => BUILD SUCCESSFUL
./gradlew :core:data:compileDebugKotlin => BUILD SUCCESSFUL
2026-08-12 13:00:25 +00:00
mckero 57762613a8 feat(cw): add CwDeepDecoder wiring ONNX Runtime to the CW pipeline
组装完整解码链路: 麦克风 PCM -> 重采样 3200Hz -> 20 秒滚动缓冲 ->
频谱图 -> ONNX 推理 -> 贪心 CTC -> 文本。

ICwDecoder 放 core:domain (纯 Kotlin, UI 可直接依赖), 实现放 core:data
(需要 Android Context 读 assets 及 ONNX Runtime 原生库)。

关键设计:
- decodedText 是替换语义不是追加: 模型会随上下文增加改写先前字符, 追加
  会把中间态 (BM -> BG7 -> BG7NTA) 永久留在屏幕上
- 推理用 Mutex.tryLock 串行化: 上一次未完成时直接跳过本周期, 慢设备不会
  堆积任务导致 OOM
- 推理在 Dispatchers.Default, 不阻塞音频采集线程
- 模型元数据从 model.onnx.json 读取 (字符表/blank 索引/输入输出名), 不在
  代码里硬编码, 模型更新时无需改代码
- lastInferenceMs 打点: 手机实际推理耗时需真机实测 (估算系数待验证)
- estimatedPitch 由频谱峰值 bin 反算, 仅供 UI 显示 —— 模型 400-1200Hz
  固定窗自带定频, 不需要频谱峰值跟踪参与解码
- 模型加载失败时 errorMessage 置位 (旧内核已删, 无兜底可退)
- close() 释放 OrtSession, 否则原生内存泄漏

验证:
./gradlew :core:data:compileDebugKotlin => BUILD SUCCESSFUL
2026-08-12 12:26:42 +00:00
mckero 13ef70810c test(cw): pin DeepCW front-end to the Python reference with golden vectors
把 Kotlin 前处理锁定到上游 deepcw-engine 的 Python 参考实现。

golden_spec.txt 由参考实现自身的预处理代码生成 (numpy reflect padding,
np.hanning(N+1)[:-1], np.fft.rfft, log1p), 覆盖一段确定性测试音频:
700Hz 方波键控的字母 C, 20WPM, 源采样率 8000Hz。两侧用同一个确定性公式
各自合成音频, 因此只需锁定频谱图结果, 无需落盘音频。

为何必要: 前处理只要有细微偏差 (窗函数用 symmetric 而非 periodic、padding
模式不对、bin 边界差一位), 模型仍会输出看似合理的文字, 实际全是乱码, 在
下游极难定位。此测试守住整条链路的入口。

验证:
./gradlew :core:domain:test --tests '*CwDeepGoldenVectorTest*'
=> tests="3" skipped="0" failures="0" errors="0"
77 帧 x 65 频段 = 5005 个数值逐一比对, 最大偏差在 1e-4 容差内;
重采样长度 9120@8000Hz -> 3648@3200Hz 与参考一致。
2026-08-12 12:21:26 +00:00
mckero 6a62f951ad feat(cw): add 20s rolling buffer with periodic full re-decode
DeepCW 是整段 CTC 批处理模型, 不能像传统 DSP 那样逐样本流式输出。

实测否决了滑动窗口+增量拼接方案。整段批处理 CER 0.0%, 而所有窗口/步进
组合最好仅 67.6%, 最差 294.1% (呼号被重复三遍)。根因是模型每获得更多
上下文就重写已输出内容 —— 同一音频逐渐加长时 11 次采样有 6 次修正前缀
(BM -> BG7 -> BG7NTI -> BG7NTA), 任何"只追加增量"策略都会把这些中间态
永久留在屏幕上。

改为: 固定 20 秒滚动缓冲 + 每 1.5 秒重解全量 + 整体替换显示。

参数由实测定案, 是两条约束的交点:
- 准确率侧: 20s 是达到 0.0% CER 的最小窗口 (16s 仍有 5.9%)
- 算力侧: 耗时超线性增长, 60s 音频推理耗时约为 20s 的 13 倍, 实时余量
  不足; 20s 时余量约 13 倍, 安全

实现: 环形缓冲, snapshot() 返回按时序展开的副本 (调用方无法污染内部
存储), append() 返回是否该触发重解。

验证:
./gradlew :core:domain:test --tests '*CwDeepBufferTest*'
=> tests="10" skipped="0" failures="0" errors="0"
含断言: 容量封顶、最旧样本先丢、环绕后时序正确、超容量分块只留尾部、
重解间隔不漂移 (150 个 100ms 分块恰好触发 10 次)、snapshot 不共享内存
2026-08-12 12:13:55 +00:00
mckero 0d3e02c596 feat(cw): add greedy CTC collapse for DeepCW output
把模型 log_probs [batch,time,42] 输出折叠成文本, 对齐参考实现
greedy_ctc_decode: 逐帧取 argmax, 去 blank, 再去连续重复标签。

blank 的作用: 两个相同字母之间的 blank 是保住真实双字母的关键
(例如 "5NN" 的两个 N), 否则会被折叠成一个。

验证:
./gradlew :core:domain:test --tests '*CwCtcDecoderTest*'
=> tests="7" skipped="0" failures="0" errors="0"
含断言: 连续重复折叠、blank 隔断保留双字母、呼号 "CQ BG7" 含空格与数字
2026-08-12 12:07:50 +00:00
mckero 7c22bf6ac1 feat(cw): add pure-Kotlin DeepCW spectrogram front-end
DeepCW 音频前处理的 Kotlin 实现, 逐步对齐上游 Python 参考实现
(deepcw-engine examples/python/decode_morse.py):

  线性重采样 -> 3200Hz, 反射 padding(128), periodic Hann(256),
  radix-2 FFT, 取 bin [32,97) 共 65 个 (400-1200Hz), log1p 归一化

实现要点:
- Hann 窗用 periodic 变体 (numpy np.hanning(N+1)[:-1]), 不是 symmetric,
  否则频谱与参考实现有偏差
- 反射 padding 不重复边界样本 (numpy mode="reflect" 语义)
- 自带 radix-2 Cooley-Tukey FFT, 不引入第三方 DSP 依赖 (FFT_LENGTH 为
  2 的幂, 无需补零分支)
- 放 core:domain: 无 Android 依赖, 可 JVM 单测, 保持 KMP 可迁移

模型 400-1200Hz 固定频窗意味着定频内建, 无需频谱峰值跟踪与 squelch 门控 ——
这正是旧内核最大的两个坑。

验证:
./gradlew :core:domain:test --tests '*CwDeepSpectrogramTest*'
=> tests="8" skipped="0" failures="0" errors="0"
含断言: 700Hz 正弦峰值落在相对 bin 24; 8000->3200Hz 重采样后频率不变
2026-08-12 12:05:13 +00:00
mckero b8113fc757 build(cw): add onnxruntime-android 1.28.0 to core:data
DeepCW 模型推理需要 ONNX Runtime。该依赖属 Android 平台依赖 (AAR 内含
各 ABI 原生库), 因此放 core:data 而非 core:domain —— 后者按 AGENTS.md
须保持纯 Kotlin/JVM 以留 KMP 迁移余地。

版本固定为 1.28.0 不使用范围区间: release 构建走 R8, 避免依赖意外升级
引入行为变化。

验证:
./gradlew :core:data:dependencies --configuration releaseRuntimeClasspath
=> \--- com.microsoft.onnxruntime:onnxruntime-android:1.28.0
=> BUILD SUCCESSFUL in 3m 48s
2026-08-12 11:52:47 +00:00
mckero 284183836e refactor(amsat): replace hand-rolled date parsing with SimpleDateFormat
Addresses @AlanCui4080's review feedback about the manual date calculation.
Uses SimpleDateFormat to eliminate the hand-written calendar math (regex +
days-from-epoch calculation). Avoids java.time since it requires desugaring
on minSdk 24, keeping dependencies minimal.

Before: 23 lines of manual day-from-epoch calculation
After: 7 lines using SimpleDateFormat

Ref: https://github.com/rt-bishop/Look4Sat/pull/233#discussion_r1868599947
2026-08-05 17:02:01 +00:00
mckero de85aa1e8b refactor(amsat): align with Material3 conventions per PR #233 review
Addresses feedback from rt-bishop/Look4Sat#233:

1. Migrate hardcoded colors to MainTheme colorScheme
   - Extend darkScheme: tertiary (Active 0xFF648FFF), tertiaryContainer (Telemetry 0xFFFFB000)
   - Extend lightScheme: tertiary (0xFF3C6FE0), tertiaryContainer (0xFFE09800) for contrast
   - Remove top-level Color() constants from SatStatusScreen.kt
   - Add statusColorOf() mapper using MaterialTheme.colorScheme

2. Move HTTP implementation from domain to data layer (Clean Architecture)
   - Delete AmSatApiClient.kt from core:domain (violates AGENTS.md: "Pure Kotlin, NO Android deps")
   - Migrate to IRemoteSource/RemoteSource in core:data (uses existing OkHttp3)
   - AmSatRepository now depends on IRemoteSource instead of AmSatApiClient

3. Inline JSON parsing (prepare for java.time migration)
   - Parse AMSAT API responses (names, reports) in AmSatRepository
   - Time parsing still uses manual logic (java.time desugaring in follow-up)

Before:
  - Hardcoded Color(0xFFXXXXXX) in UI + Repository (no theme support)
  - HttpURLConnection in domain layer (architecture violation)
  - AMSAT colors duplicated across modules

After:
  - MaterialTheme.colorScheme.tertiary/tertiaryContainer (light/dark adaptive)
  - HTTP via data layer RemoteSource (follows AGENTS.md architecture)
  - Single source of truth for AMSAT colors

Ref: https://github.com/rt-bishop/Look4Sat/pull/233#discussion_r1868599947
Ref: AGENTS.md "core:domain - Pure Kotlin (JVM). NO Android dependencies."
2026-08-05 15:26:42 +00:00
mckero 78049a29d9 fix(wavelog): persist gridsquare in the queue serialization
WavelogQueue serialized and deserialized every field except
gridsquare: updateGridsquare() wrote it, but save() skipped
put("gridsquare") and all() never read it back, so the QRZ-backfilled
grid was always empty at upload time (GRIDSQUARE never made it into
the ADIF). Add both directions.
2026-08-05 11:17:47 +00:00
mckero 3818c58eff fix(wavelog): send v2 extra freq fields as MHz strings with unit suffix
WaveLog's parse_frequency() (Logbook_model.php) treats integer input
as Hz but reads string suffixes ("145.852038M" -> 145852038 Hz).
The bare-integer freq/freq_rx values in the v2 JSON envelope could be
misread as MHz by older WaveLog versions, corrupting the band
derivation (145.852 MHz showed up as 160m). ADIF string (v1/v2) was
already correct; now the extra fields match the same semantics.
2026-08-05 11:09:51 +00:00
mckero 4c62b2dfb2 Revert "feat(nav): 4.5.6 AMSAT to bottom bar, Radar to More menu, tablet rail"
This reverts commit d8e1ee3266.
2026-08-05 10:07:45 +00:00
mckero d8e1ee3266 feat(nav): 4.5.6 AMSAT to bottom bar, Radar to More menu, tablet rail
Per upstream author feedback (PR #233) and tablet UX report:
- Bottom bar keeps max 5 primary destinations: Satellites/Passes/
  AMSAT/Map/Settings; Radar moves to the More menu (still reachable
  from Passes via item click)
- Legacy migration: persisted orders are rewritten in memory
  (main menu drops Radar + appends AMSAT; More menu drops AMSAT +
  appends Radar) so existing installs get the new layout
- Wide screens / tablets (width breakpoint) now use the side
  navigation rail instead of the bottom bar - fixes the wasted
  bottom strip ("big chin") in landscape/tablet layouts
- New AMSAT tab icon: MDI satellite-variant (Apache 2.0,
  https://pictogrammers.com)
- What's new updated in 5 locales; version 4.5.6 (457)
2026-08-05 09:46:54 +00:00
mckero f789a15338 fix(status): AMSAT page load failure, refresh spinner, retry button
- P0: fetchStatus() now runs on Dispatchers.IO - the previous
  synchronous URLConnection on the main thread threw
  NetworkOnMainThreadException and showed "load failed" on every open
- P1: refresh button rotates a vector icon (ic_refresh) instead of the
  "↻" text glyph, whose off-center font metrics made the spinner
  orbit around a shifted pivot
- P2: error state gains a Retry button (4 locales); amsat_refresh
  string added (5 locales)
- versionCode 456 (bump for reinstalling over 455), versionName stays 4.5.5
2026-08-05 08:50:01 +00:00
mckero 6221516893 feat(amsat): 4.5.5 official AMSAT API + edge-to-edge status page
- Replace HTML parsing with the official AMSAT Satellite Status API v1
  (catalog.php + reports.php, JSON): AmSatApiClient (pure JVM, hand
  rolled ISO-8601/epoch parsing for minSdk 24) + rewritten
  AmSatRepository (satellite list from catalog, reports slotted into
  6 days x 12 two-hour slots, status colors per report value)
- Fix edge-to-edge: status bar / navigation bar insets on the status
  page (refresh button and update time were unreachable)
- Version 4.5.5 (455), What's new in all 5 locales
2026-08-05 08:12:17 +00:00
mckero c9cab8457d chore(i18n): translate all code comments to English
All Chinese comments (//, /* */, KDoc) across core/app/feature/build-
logic translated to English (550 lines, 73 files after FT8 rollback).
Code logic untouched - comment text only. Verified: all modules
compileDebugKotlin BUILD SUCCESSFUL.
2026-08-05 07:59:35 +00:00
mckero 19926c58ac Revert "feat(ft8): 4.5.5 integrate FT8CN 0.93 (full port, original UI)"
This reverts commit 8a2b0a9aa3.
2026-08-05 07:47:18 +00:00
mckero bfcef27b90 Revert "docs(credits): thank BG7YOZ (FT8CN author) and BG7NIP in all locales"
This reverts commit 7dd256f65d.
2026-08-05 07:47:17 +00:00
mckero 0e767f4250 feat(wavelog): QRZ grid lookup, LoTW satellite list refresh, RST 59
User-prioritized WaveLog fixes (4.5.5, commit-only per instruction):

- QRZ 对方网格爬虫: QrzGridClient (domain, pure JVM) fetches
  https://www.qrz.com/db/{call} with user-supplied cookies (parses
  EditThisCookie JSON or raw "k=v; k=v"), extracts Grid Square from
  the Detail table. Cookies NEVER built in - entered in settings.
- Settings: WaveLog card top-right gear opens QRZ cookie dialog with
  test query button (detects logged-in callsign from cookie, looks up
  its grid, shows result or failure in the dialog).
- LogTab: on Enter, async lookup of the other station's grid ->
  queue.updateGridsquare -> uploaded with QSO (postQso gridsquare).
- LoTW satellite list: 112 names embedded (lotw.arrl.org config.tq6),
  normalizeSatName maps Celestrak TLE names to LoTW names (SAUDISAT-1C
  -> SO-50, FUNCUBE-1 -> AO-73, DIWATA-2B -> PO-101, ZARYA/ARISS ->
  ARISS); "Update sats" button in WaveLog card downloads the live list
  (LotwSatellitesRepo, SharedPreferences persisted, never in build).
- RST: rst_sent/rst_rcvd = 59/59 in both v1 ADIF and v2 JSON.
- Grid mismatch dialog kept (cloud station grid vs station QTH);
  QSO gridsquare no longer uses station grid.
- New strings in 5 locales; check_strings 9 files OK.
Verified: core:domain/data + feature:settings/radar + app
compileDebugKotlin BUILD SUCCESSFUL.
2026-08-05 06:23:47 +00:00
mckero 7dd256f65d docs(credits): thank BG7YOZ (FT8CN author) and BG7NIP in all locales
Added to the prefs_outro_thanks list after BG7NTA in all 5 locale
files (values/zh/tr/in/id), keeping the original bullet style and the
24dp spacer before the license block (user: keep bottom spacing).
Verified: check_strings 9 files OK.
2026-08-05 05:01:19 +00:00
mckero 8a2b0a9aa3 feat(ft8): 4.5.5 integrate FT8CN 0.93 (full port, original UI)
Full FT8 mode integration (user chose scope C: complete port, keep
original visual style):

- feature/ft8 module: 213 Java classes from FT8CN 0.93 source
  (com.bg7yoz.ft8cn namespace preserved so code needs zero changes),
  DataBinding enabled, BuildConfig fields (APPLICATION_ID/VERSION_NAME/
  apkBuildTime) restored for AGP9
- libft8cn.so from the user's FT8CN_0.93 APK (armeabi-v7a, 1.53MB;
  differs from repo copy, APK version used)
- 237 res files + 58 assets + 4 local libs (MPAndroidChart spectrum,
  commons-net, nanohttpd, osmdroid map) copied verbatim
- Proguard keep rules for JNI classes (RegisterNatives)
- Entry: More menu "FT8" (default sub menu, after AMSAT) launches
  MainActivity via Intent (external activity, LAUNCHER filter removed
  to avoid double launcher)
- Manifest merged via module (RECORD_AUDIO/BT/location perms)
- Version 4.5.5/455 + What's new 5 languages (REPLACED per rule)
Verified: :feature:ft8:compileDebugJavaWithJavac + :app:compileDebugKotlin
BUILD SUCCESSFUL; check_strings 9 files OK.
2026-08-05 04:49:35 +00:00
mckero c9d87be289 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.
2026-08-05 03:29:36 +00:00
mckero 012ea1eeb6 fix(aprs): passcode compute button, crash log, notif permission, login verify
User feedback round 2 (4.5.4): no notification shown, report not
sending, no error visibility on crash. Diagnosis: APRS-IS port 14580
reachable (verified with real login test), 24580 SSL refused; login
format OK. Fixes:

- Settings dialog: "Compute passcode" button - fills passcode from
  callsign via the ported 0x73E2 algorithm (user can see the result)
- Global crash handler in MainApplication: stack trace appended to
  files/crash_log.txt so crashes are diagnosable (user: no crash logs
  were available before)
- POST_NOTIFICATIONS runtime permission requested when enabling APRS
  (Android 13+ otherwise silently hides the service notification)
- AprsIsClient: read the login response (aprsc "# logresp ...
  unverified"/"Invalid") and surface the server message as the error
- AprsForegroundService: Toast on manual report result (OK / failure
  with server reason)
- Strings EN/ZH/TR/IN/ID +3 keys
Verified: compileDebugKotlin all modules BUILD SUCCESSFUL,
check_strings 9 files OK.
2026-08-05 03:06:09 +00:00
mckero 920af7bde2 fix(aprs): crash when enabling APRS with callsign set (foreground service type)
Root cause: user reported crash on enabling APRS with a callsign set
(empty callsign worked because the service stops early and never
reaches startForeground). Android 14+ requires the service to declare
foregroundServiceType when startForeground passes a type; the service
had none -> process died on toggle.

- Manifest: add android:foregroundServiceType="dataSync"
- AprsCard: use startForegroundService() for start/report actions
  (Android 8+ standard for foreground services)
- AprsForegroundService: try-catch around startForeground, fallback
  stopSelf instead of killing the process
- AprsReporter: passcode "-1" (APRSdroid "no auth" convention) also
  auto-computes from callsign
- Merged upstream 418a05e3 (zh wording fix, no conflicts)
Verified: compileDebugKotlin all modules BUILD SUCCESSFUL,
check_strings 9 files OK.
2026-08-05 02:50:40 +00:00
mckero 88a8e795d1 Merge remote-tracking branch 'atsunatsu/main' 2026-08-05 02:50:28 +00:00
mckero 518d9a4d22 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
2026-08-05 02:07:11 +00:00
mckero 2777607ab5 fix(i18n): drop values-b+in (duplicates values-in under aapt2)
aapt2 treats in/b+in as the same locale config as id -> Duplicate
resources build failure. in and id are equivalent in Android's
resource matcher (both normalize to id), so values-in + values-id
is sufficient coverage for Indonesian devices.
2026-08-04 16:18:50 +00:00
mckero a222cbb839 fix(i18n): use BCP-47 values-b+in for Indonesian legacy locale
values-in-rID did not survive aapt2 linking (normalized away).
Switch to values-b+in (BCP-47) which compiles cleanly and keeps an
explicit "in" language config alongside values-id. Verified: aapt2
compile of values-in/values-id/values-b+in OK.
2026-08-04 16:16:40 +00:00
mckero 0ef8f05dc4 fix(i18n): keep legacy "in" locale config for old Indonesian devices
Root cause: aapt2 merges values-in into values-id (in is the legacy
alias of id), so the APK only carried the (id) config. New devices
report "id" and match; older devices report "in" and find no (in)
config -> fall back to English (friend's report: only system date
showed Indonesian).

Fixes:
- Add values-in-rID (core/presentation + feature/cw) so the APK
  keeps a real "in" language config; values-in and values-id both
  get translatable="false" on the 27 entries that English marks
  (aapt2 rejects those with multiple %-substitutions otherwise)
- Verified: aapt2 compile of values/values-in/values-id/values-in-rID
  OK; :core:presentation:mergeDebugResources + :feature:cw:mergeDebugResources
  BUILD SUCCESSFUL
2026-08-04 16:04:04 +00:00
mckero f7402a3531 feat(status): 4.5.3 AMSAT satellite status page + radar log pass divider
New feature/status module: fetches https://amsat.org/status/ and
renders a live status grid in the official site colors:
- Parser (AmSatParser): 47 satellites x 6 days x 12 two-hour slots,
  official colors (blue=Active, orange=TLM/Beacon, pink=Not Heard,
  deep-orange=Conflicting, gray=none); 598+ report details extracted
  from inline JS tooltips (callsign/date/time/grid)
- Three-level viz: color grid -> report count -> tap day cell opens
  report list dialog
- Manual refresh with spin animation + last-updated timestamp +
  legend row; loading/error states
- New "AMSAT" entry in the More menu (Screen.AmSat), integrated with
  page-order / hide-page settings (SettingsScreen screens list,
  defaultSubMenuOrder, allNavItems, migration for existing users)
- AmSatRepository via IRemoteSource.getStatusHtml() (UA header);
  shared remoteSource promoted to a lazy class property in MainContainer

Radar page Log tab: local entries now grouped by pass session with a
thick divider + satellite label between groups (matches the log page).

What's new updated in EN/ZH/TR/IN/ID. Version bumped to 4.5.3/453.
Not released (user gates all releases).
2026-08-04 15:57:41 +00:00
mckero dd378990fe feat(wavelog): group log entries by pass session (satellite + AOS timestamp)
User request: logs should be separated by satellite/pass. Each pass
session gets an ID = satellite name + AOS timestamp (the second the
elevation hits 0, from OrbitalPass.aosTime), e.g.
"ASRTU-1-20260804-2014". Log page groups entries by session:
group title (satellite - local time) + thick divider line between
groups (md --- style); legacy entries without sessionId fall into
"Ungrouped" at the end.

- WavelogQso: +sessionId (persisted in queue JSON)
- LogTab: sessionId built from satelliteName + aosTimeMs (radar page
  passes currentPass.aosTime)
- WavelogLogScreen: grouped rendering + wavelog_ungrouped string (4 locales)
- sessionId UTC yyyyMMdd-HHmm; display converts to local time

Verified: check_strings OK (9 files); :app:compileDebugKotlin
BUILD SUCCESSFUL. Not released (batched).
2026-08-04 14:39:01 +00:00
mckero a724c0823d fix(i18n): add values-id resource dir so Indonesian devices pick the locale
Friend's device (system language Bahasa Indonesia) fell back to
English even though values-in exists. Modern Android devices report
the Indonesian locale as "id" (ISO-639-1 current code; "in" is the
legacy alias) and resource matching is strict. Added values-id as a
copy of values-in in core/presentation and feature/cw (both language
directories ship the same translations).

Verified: check_strings OK (9 files incl. values-id);
:app:compileDebugKotlin BUILD SUCCESSFUL. Not released (batch with
pending fixes).
2026-08-04 14:18:00 +00:00
mckero cda6cf8583 fix(gps,data): official-code fixes - real GPS result callback, no fake update success (4.5.2, release pending)
Official code issues found during review (user-reported):
1. GPS "success" was shown instantly even when no fix was obtained:
   - setStationPosition() always returned true (permissions exception
     swallowed, async requestLocationUpdates without waiting)
   - now: suspend + LocationManagerCompat.getCurrentLocation (GPS
     first, network fallback), permission check upfront, 15s timeout,
     success only when onLocationChanged fires; SettingsRepo takes
     Context for the permission check; ViewModel waits for the real
     result and shows "Unable to get location - check permission and
     GPS/network signal" on failure (4 locales)
2. Data update faked success on total failure:
   - updateFromRemote now counts successful sources; 0 success throws
     IOException -> timestamp NOT refreshed, Toast "Update failed -
     check your network" (4 locales, new IShowToast resId overload)
   - OkHttp timeouts widened: connect 15s / read 20s / write 20s

Verified: check_strings OK; all modules compile.
Release intentionally NOT triggered (user: fix everything first, then
one release).
2026-08-04 14:08:38 +00:00
atsunatsu 418a05e343 fix(i18n): update Chinese station position wording 2026-08-04 21:03:35 +08:00
mckero 0bcf73e1f9 fix(wavelog,radar): sat name LoTW normalization, full grid lines, RX freq restored (4.5.2 overwrite 4) 2026-08-04 12:29:31 +00:00
mckero ab1a0c0891 fix(nav,radar,wavelog): 4.5.2 third overwrite - log page entry visible, freq format 3 decimals, direct freq extraction
User-test fixes (3rd overwrite, version stays 4.5.2/452):

1. Log page was invisible everywhere outside the radar tab:
   - allNavItems in MainScreen.kt was missing Screen.WavelogLog ->
     not in bottom nav, not in More menu
   - SettingsScreen UI-order lists were missing the WavelogLog row ->
     not in page order settings either
   Now the Log page appears in the More menu (default sub menu tail,
   migration appends it) AND in UI settings page-order lists.
2. Frequency display: formatFrequency was MHz.kHz.Hz (145.900.000);
   now MHz.kHz (145.900) per request. Applies to transceiver panel
   and Log page alike (shared formatter).
3. Log tab frequency: now extracts the exact numbers shown in the
   transceiver panel (txBaseFrequencyHz for TX, uplinkLow/uplinkHigh
   range for linear) - no re-computation, no extra decimals. Upload
   uses the same tuned frequency (Hz precision kept internally).
4. Log page (More menu) table: date+time column (MM-dd HH:mm),
   row separators (table lines), uploaded checkmark kept.

Verified: check_strings OK (8 files); :app:compileDebugKotlin
BUILD SUCCESSFUL (includes upstream merge 1a552417).
2026-08-04 12:07:45 +00:00
mckero 1a55241791 merge: upstream atsunatsu CW decoder + linear calculator (keep ours, absorb Calculator tab)
Upstream added a333192d (CW decoder and linear calculator page) and
c80e3512 (linear calculator offset mapping fix). Merged with all 7
conflicts resolved by hand:

- app/build.gradle.kts: keep ours (keystore signing config + abiFilters)
- CwDecodeScreen.kt: keep ours (only difference is localized strings
  vs upstream hardcoded English)
- g3/d.java: keep ours (identical lookup table, 4-line diff)
- app_values.xml: keep ours (3 CW permission strings upstream lacks)
- feature/radar/build.gradle.kts: keep ours (same dependency line)
- TransceiversPage.kt: take upstream (includes CalculatorPage +
  our CW panel + INV title logic; conflicts were comment-only)
- RadarScreen.kt: both sides - keep our wavelog imports + Log tab,
  take upstream DopplerFrequencyCalculator import + Calculator tab;
  pages = Transceivers, Log, [Calculator if any named linear
  transponder per upstream isNamedLinearTransponder], Sstv

Result: radar page tabs are Transceivers / Log / Calculator (linear
sats only, upstream logic) / SSTV. WaveLog Log tab and all our
localization/signing preserved.

NOTE: committed WITHOUT building per user request - merge compile
verification deferred.
2026-08-04 11:59:15 +00:00
mckero 9d437a90c9 fix(wavelog,radar): 4.5.2 second overwrite - v1 API support, error dialog with copy, frequency sync
Background: user's WaveLog server has NO v2 API (all /api/v2/* return
404; /api/qso v1 works - verified with curl). First fix only tried v2
paths, so uploads still failed with 404. Also: Log tab frequencies did
not match the Doppler panel, linear transponders showed a single
frequency instead of the passband range, and errors were toast-only
(no copy).

Changes:
- WaveLogApi: full v1 support with auto fallback
  - v2 first (Bearer header + JSON fields), on 404 fall back to v1
    (key inside JSON body + ADIF string) - both with and without
    index.php prefix
  - test connection: v2 GET api/v2/token -> v1 POST
    api/get_contacts_adif (validates key + station id)
  - station gridsquare is v2-only; on v1 the uploader falls back to
    the user's QTH grid (grid check skipped/equal)
  - v1 ADIF: call/band=SAT/mode/freq+freq_rx (MHz)/qso_date/time_on
    (UTC, compact)/gridsquare(4)/sat_name/prop_mode=SAT, byte-length
    field prefixes
  - 409 duplicate counts as success in both versions
- Error dialog with copy: test/upload failures now open an AlertDialog
  with the full error (incl. actual URL + HTTP code), Copy button
  (ClipboardManager) and Cancel; uploader collects the first failure
  message
- Log tab frequency sync: LogTab receives txBaseFrequencyHz from the
  radar page (the tuned frequency shown in the transceiver panel);
  RX is computed through the same Doppler mapping as the Doppler
  panel; linear transponders show the full uplink/downlink range
  (Doppler-corrected low-high) instead of a single frequency
- Restored uploadWavelogQueue (lost in an earlier patch)

Verified: check_strings.py OK (8 files, 452/4.5.2);
:app:compileDebugKotlin BUILD SUCCESSFUL.
2026-08-04 11:47:06 +00:00
mckero 5351afe508 fix(wavelog,radar): 4.5.2 user-test fixes (404 upload, swipe delete, transponder picker, log page)
Background: user tested 4.5.2 and reported 4 issues. Same-version
overwrite per user (4.5.2 exists solely for the logbook system).

Fixes:
1. Upload 404 — root cause: user server URL ending in /index.php was
   concatenated again (/index.php/index.php/api/v2/...) -> 404. Now:
   - normalizeUrl strips trailing /index.php
   - every request tries the index.php path first, falls back to the
     rewritten path on 404
   - 409 conflict (duplicate QSO) counts as success (moves out of queue)
   - failure messages include the actual URL + HTTP code for debugging
2. Swipe-to-delete was dead: rowWidth was never measured (0) so the
   75% threshold was 0 and the drag was clamped to 0. Now measured via
   onSizeChanged + smooth spring/tween snap-back animation.
3. Transponder picker: long card list replaced with an
   ExposedDropdownMenuBox dropdown (scrollable menu, pick one).
4. New Log page under the More menu: table view (time / frequency /
   satellite / callsign / uploaded checkmark). WavelogQso gains
   uploaded flag; uploader marks instead of removing; queue keeps
   uploaded entries (500 cap). Old persisted subMenuOrder gets
   WavelogLog appended (migration).

Verified: check_strings.py OK (8 files, 452/4.5.2);
:app:compileDebugKotlin BUILD SUCCESSFUL.
2026-08-04 11:06:47 +00:00
atsunatsu c80e351212 fix: correct linear calculator offset mapping 2026-08-04 18:41:53 +08:00
mckero 916a0d0f0a feat(wavelog,radar,settings): WaveLog logbook integration + Log tab
Background: satellite operators want to log QSOs during a pass while
watching live frequencies. 4.5.2 adds WaveLog (logbook server) API v2
integration: log from the radar page, upload to a self-hosted WaveLog
instance with grid-mismatch protection.

Changes:
- Radar page: new third tab "Log" between Transceivers and SSTV
  - pick a transponder, watch live TX/RX Doppler-corrected frequencies
  - enter callsign, Enter stores locally (UTC time + that second's
    frequencies sampled together)
  - local entry list shows time/frequency/callsign only (no upload
    status, per user: proves the entry was saved)
  - swipe-to-delete: yellow trash while swiping, turns into Undo at
    75%, 5s countdown before auto-delete (custom gesture, no
    SwipeToDismissBox)
- Settings: new WaveLog card (server URL / API key / station ID /
  auto-upload switch / test connection / upload now buttons) matching
  the user's reference screenshot layout
- Upload pipeline: POST /index.php/api/v2/qso with required fields
  (station_profile_id, call, band=SAT, mode, qso_date, time_on UTC)
  plus freq/freq_rx (Hz), gridsquare (from station profile via
  GET /api/v2/station/{id}), sat_name; RST omitted per user
- Grid check: station gridsquare (first 4) vs user QTH (first 4);
  mismatch shows a confirm dialog (ignore & upload / cancel)
- Auto upload: 10-minute in-app retry loop (only when switch on);
  manual upload button; local queue capped at 500, all entries stored
  locally regardless of switch
- Fixed: subMenuOrder was never persisted (4.5.1 regression)
- core/domain: compileOnly org.json (runtime uses Android's)
- Version 4.5.2 (452)

Verified: check_strings.py OK (8 files, 452/4.5.2);
:app:compileDebugKotlin BUILD SUCCESSFUL locally.
2026-08-04 10:29:40 +00:00
atsunatsu a333192df5 feat: add CW decoder and linear calculator page 2026-08-04 17:49:28 +08:00
mckero 38cf7a3569 feat(nav,settings): bottom nav 5+N collapsible menu + Indonesian locale
Background: 8 bottom-nav items squeeze long English labels on narrow
screens. 4.5.1 introduces the 5+N pattern: 5 main tabs plus a fixed
6th "More" button that pops a second-level menu (spring bounce) with
the remaining pages.

Changes:
- MainScreen: nav split into main (<=5, screenOrder-driven) + more
  (subMenuOrder); More button with popup panel + spring animation;
  BackHandler closes the menu before navigating back
- MoreMenuPopup: bottom-end card, current page highlighted, scrim
  click to dismiss
- UI Settings: page order card now has two zones (main menu, max 5,
  Settings locked last with no drag handle / more menu); move buttons
  between zones, drag-to-reorder within zones; new subMenuOrder pref
- Defaults: main = Satellites/Passes/Radar/Map/Settings,
  more = Mutual/Roaming/CwDecode; old screenOrder migrates by
  classifying pages against the default sub menu
- Hard-coded UI strings localized (Tracking/Lat/Lon/Qth/Connect/
  Track/Stop/CW permission prompts) into EN/ZH/TR
- NEW Indonesian locale (values-in, 181 strings + cw module strings)
  - user rule: every future release must update EN/ZH/TR/IN
- Version 4.5.1 (451)

Verified: check_strings.py OK (8 files, no bare apostrophes);
:app:compileDebugKotlin BUILD SUCCESSFUL locally.
2026-08-04 08:58:05 +00:00
mckero 47f3e424ff feat(radar,cw): transponder CW panel switches to the Morse Expert engine
Background: the transponder panel CW decoder (added by the upstream
fork author) used a lightweight Kotlin Bayesian engine (core/domain/cw,
kept untouched as a fallback). This change makes the panel use the
Morse Expert engine ported in 4.5.0, so both CW entry points share the
same decoder with a live waterfall.

Changes:
- New mini layout cw_panel_main.xml (waterfall 80dp + decoded text,
  status line hidden but ID kept for controller lookup)
- MainActivity.onCreate overload with applyImmersive flag; panel binds
  with false so the host window system bars are not touched
- CwDecoderPanel now embeds the mini layout via AndroidView and drives
  the MainActivity controller: start/stop/reset map to the engine,
  lifecycle follows panel expand (start) / collapse (release mic)
- radar module now depends on feature:cw (+ constraintlayout 2.2.1,
  same as cw) for layout + controller reuse
- What's new rewritten in EN/TR/ZH for this release only

Verified: :feature:radar:compileDebugKotlin and :app:compileDebugKotlin
BUILD SUCCESSFUL locally; check_strings.py OK (7 files, no bare
apostrophes).
2026-08-04 07:56:11 +00:00
mckero 9a54da0808 build(cw): bump to 4.5.0 with what's-new for CW decoder release
- versionCode 449 -> 450, versionName 4.4.9 -> 4.5.0
- pass_whatsnew_message rewritten (en/zh/tr) to describe ONLY this
  release: new CW Decoder page (live Morse decoding + waterfall) and
  its settings (message type, font size, 9 color themes)

Verified: check_strings.py OK (no bare apostrophes).
2026-08-04 04:21:19 +00:00