mckero
aac3aee4bb
fix(radar): QRZ 网格回填后刷新列表显示
...
问题:QRZ 爬虫异步回填网格后,列表里对应行不会立即显示网格(要等下次保存/删除操作才看到)。
根因:updateGridsquare 成功后没有触发 refreshTick++,UI 的 entries 列表不会重新计算。
修复:回填成功后调用 onSaved() 触发刷新。
影响:网格回填立即可见,改善用户体验。
2026-08-14 09:22:05 +00:00
mckero
d749c5ab48
fix(domain): WavelogQueue.updateGridsquare 添加同步锁
...
问题:updateGridsquare 是唯一没有 @Synchronized 的修改方法,与 add() 并发(LogTab 主线程 add + 后台协程 updateGridsquare)会触发 read-modify-write 竞态,导致新 QSO 丢失。
修复:给 updateGridsquare 加 @Synchronized,与其他修改方法保持一致。
影响:消除数据丢失风险。
2026-08-14 09:19:41 +00:00
mckero
082e0c5bff
fix(radar): 防止 WaveLog 重复提交 QSO
...
问题:键盘 onDone 连击会存两条相同的 QSO(时间戳/呼号/频率完全一致)。
修复:添加 submitting 状态变量,提交期间拒绝重复调用。
影响:误操作不再造成重复记录。
2026-08-14 09:14:47 +00:00
mckero
e69d6dee4a
fix(radar): 修复 offset slider 拖动时多普勒计算用旧值
...
问题:onValueChange 回调里 offsetHz 是派生状态的旧快照,拖动时计算的频率滞后一帧。
修复:在回调内立即从 newVal 计算 newOffsetHz,传给多普勒计算器。
影响:拖动 offset 时频率显示实时正确。
2026-08-14 09:11:45 +00:00
mckero
cf93ca9f71
fix(ui): 修复菜单布局的三个严重 bug
...
Bug #1 : moveToMain 误驱逐页面
- 根因:每次调用 moveToMain 都执行驱逐逻辑,即使页面本来就在主菜单
- 场景:拖拽主菜单内部顺序 → SettingsViewModel 遍历新顺序逐个调 moveToMain
→ 每次都判断 main.size > 5 → 误驱逐最后一个页面
- 修复:只在真正从 More 移到主菜单时才驱逐(加 wasInMore 标志位)
Bug #2 : onReorder 传 resolve 输出污染状态
- 根因:SettingsScreen 拖拽回调传的是 resolve 输出(mainItems/subItems)
而不是持久化输入(screenOrder/subMenuOrder)
- 场景:拖拽主菜单 → onReorder 传 [Radar, ..., Settings](完整列表)
→ Settings 被显式存入 screenOrder → 下次 resolve 当作用户手动放置 → 参与驱逐逻辑
- 修复:
1. 拖拽主菜单时过滤掉 Settings(锁定页面用默认位置,不存持久化)
2. 另一个菜单用输入的 screenOrder/subMenuOrder,不用 resolve 输出
Bug #3 : onReorder More 菜单传错参数
- 根因:拖拽 More 菜单时传的是 mainItems(resolve 输出),不是 screenOrder
- 修复:改用输入的 screenOrder
影响:修复前,拖拽主菜单会丢页面,移页面到主菜单可能导致 Settings 消失
2026-08-14 00:43:23 +00:00
mckero
7cadded6ca
fix(data): compute the OMM epoch day fraction numerically, not by string surgery
...
排查页面顺序问题时顺带审计发现: OMM/CSV 历元在子夜后约 86 秒内会被解析成
完全错误的值, 且不抛异常 —— 静默的错误数据。
## 根因
DataParser.parseCSV 用字符串拼接构造历元:
val frac = ((hour + min + sec + ms) / 86400000.0).toString().substring(1)
val epoch = "${year.substring(2)}$day$frac".toDouble()
substring(1) 的意图是切掉 "0.123" 的前导 0。但 Double.toString() 在数值
小于 1e-3 时切换为科学计数法, 于是被切掉的是【有效数字】, 剩下的指数后缀
让整个字符串重新变成一个合法但语义完全错误的 double。
Kotlin 侧实测(单测失败信息):
00:00:01.000 期望 25001.000011574073 实得 0.2500115740740741
00:01:00.000 期望 25001.000694444443 实得 2.5001944444444444
与 JDK 侧独立验证逐位一致。00:01:26.4 之后 frac >= 0.001, 不再用科学计数法,
所以这个 bug 只在每天前 86.4 秒的历元上出现(约占 0.1%), Celestrak OMM 数据
里整分历元并不罕见。
## 影响
runCatching 抓不到(没有异常), 该卫星的 juliandDateOfEpoch 会推出 year=2000
day≈0, tsince 偏差约 26 年 —— 方位/仰角/过境预报彻底失效, 不是精度下降。
且用户无从察觉。
## 修复
改为数值相加, 不经过字符串:
val dayFraction = (hour + min + sec + ms) / 86400000.0
val epoch = "${year.substring(2)}$day".toDouble() + dayFraction
"25001".toDouble() + 0.0000115 = 25001.0000115, 无科学计数法风险。
## 验证
DataParserTest 新增 5 个历元回归测试(子夜整点/子夜后 1 秒/子夜后 1 分钟/
正午/当日最后一毫秒)。先确认前两个在旧实现下失败(failures=2), 修复后:
- DataParserTest 24 个测试全绿(原有 19 个无回归)
- :core:domain:test 全量 105 个测试 0 失败 0 错误
2026-08-13 16:20:07 +00:00
mckero
6fc2f560b7
fix(nav): resolve the menu layout in core:domain so page order actually applies
...
用户报告"把 AMSAT 从更多菜单移到主菜单没生效"。排查后发现这是两个互相
掩盖的 bug, 其中一个会让用户永久无法进入设置页。
## Bug 1 (致命): 移任意页进主菜单 -> 设置入口从所有菜单消失
对"主菜单上限 5"的定义两处不一致:
- SettingsScreen.kt:892 判断 mainItems.filter { it != "Settings" }.size >= 5,
上限是【不含 Settings 的 5 个】, 于是认为 [Satellites,Passes,Radar,Map]
还有空位, 直接追加 -> 共 6 项
- MainScreen.kt:165 的 .take(5) 上限是【含 Settings 的前 5 个】, 而 Settings
排在最后 -> 被截断丢弃
结果 Settings 既不在底栏也不在更多菜单(它不在 subMenuOrder 里), 用户再也
进不去设置页, 无法自行改回, 只能清数据或重装。
## Bug 2 (用户报告): AMSAT / WavelogLog 移到主菜单被静默撤销
MainScreen.kt:161-163 给老用户补新页面的迁移逻辑【无条件执行】:
.let { list -> if ("AMSAT" in list) list else list + "AMSAT" }
用户把 AMSAT 移出子菜单后 subMenuOrder 里没有它, 这段又加回去, 第 165 行
filter { it.screenId !in subOrder } 于是永远过滤掉 AMSAT。
副作用: 这个 bug 恰好把主菜单拉回 5 项内, 反而掩盖了 Bug 1 —— 所以用户只
看到"AMSAT 移不过去", 没触发"设置锁死"。
## Bug 3: 溢出页面凭空消失而非落入更多菜单
.take(5) 直接丢弃超出的页面, 它们既不在底栏也不在更多菜单。
## Bug 4: 设置页展示顺序与底栏实际顺序不一致 (WYSIWYG 失效)
两处各自实现排序: 设置页不做 take(5)、用 sortedWith 把 Settings 钉最后;
MainScreen 做 take(5)、Settings 位置由 screenOrder 决定。
## Bug 5: 更多菜单里 AMSAT / Roaming 当前页不高亮
MoreMenuPopup.kt:69-79 的 when (currentKey) 缺 AmSat 与 Roaming 分支,
MainScreen.kt:188-198 缺 Roaming 分支, 靠 else -> false 兜底, 新增页面时
不会有编译错误提醒。Screen 子类都是 data object, 直接用 currentKey == screen
即可, 新增页面自动生效。
## Bug 6: 设置页主菜单区不过滤 hiddenScreens
MainScreen 会过滤隐藏页, 设置页不过滤, 于是隐藏的页面仍占据设置页的位置且
"移出主菜单"按钮可点, 与底栏实际情况不符。
## 改动
新增 core/domain/navigation/MenuLayout.kt (纯 Kotlin, 为 KMP 就绪) 作为菜单
布局的唯一入口:
- resolve(): 持久化偏好 -> 底栏 + 更多菜单。先给 Settings 预留名额再截断,
溢出页面落入更多菜单而非丢弃; 两个列表都没提到的页面按默认归位(升级不丢页)
- moveToMain() / moveToMore(): 移动语义集中一处, 拒绝把 Settings 移出
MainScreen 与 SettingsScreen 改为共用它, 删除双份实现与无条件迁移逻辑。
## 死代码清理 (每项已 grep 全仓库确认零引用)
- SettingsAction.ResetScreenOrder: 无任何派发点, 仅定义与 when 分支
- UiSettingsCard 的 onReorder 参数: 函数体内两个 DragOrderList 的 onReorder
实参都调 onUpdateMenu, 从未调用该参数
- SettingsAction.ReorderScreens: 唯一引用是上面那个死参数
- MainScreen 的 navigateToRadar 参数: 函数体内唯一出现是注释掉的一行
- Navigation.kt 的 defaultScreenOrder / defaultSubMenuOrder: 迁移后零引用,
默认值已在 MenuLayout 内
保留 entry<RadarDestination> 分支不动 —— RadarDestination NavKey 被
PassDetailsMatcher deeplink 使用。
## 验证
- MenuLayoutTest 13 个新测试全绿, 每个对应上面一个 bug 场景
- :core:domain:test 全量 100 个测试 0 失败 0 错误
(DataParser 19 / Doppler 17 / Qth 8 / Transponder 7 / CwCtc 7 /
CwDeepBuffer 13 / CwGolden 3 / CwSpectrogram 8 / MenuLayout 13 /
WaveLogApi 5)
- :core:domain:compileKotlin + :core:presentation + :app +
:feature:settings compileDebugKotlin => BUILD SUCCESSFUL
- 用 Python 复刻新规则重跑当初失败的全部场景: 5 个页面逐一移入主菜单,
设置入口全部保住、页面无丢失; 隐藏页 + 移动组合无页面丢失; 幂等性通过
2026-08-13 16:10:41 +00:00
mckero
8adf861ed7
chore(release): refresh 4.5.7 whats-new and bump versionCode to 461
...
版本名保持 4.5.7 不变(用户要求覆盖同一个发行版, APK 文件名仍为
Look4Sat-Pro-4.5.7.apk)。
versionCode 460 -> 461: 手机上已安装 versionCode 460 的 4.5.7, 若 code
不变则覆盖安装会被系统拒绝。versionCode 对用户不可见。
pass_whatsnew_message 五语(en/zh/tr/in/id)补全本次全部改动, 之前只写了
DeepCW 那批, 本轮 UI 改动与 fp32 模型未包含:
- 内置完整版 fp32 模型
- 更多菜单改靠右窄面板
- CW 页移除无效返回键与误导性状态提示
2026-08-13 14:49:36 +00:00
mckero
f4f6ec7db5
feat(cw): ship the full fp32 DeepCW model instead of the int8 build
...
用户要求内置完整版模型, 不要量化版。
assets/deepcw/model.onnx: 4,354,478 bytes (int8) -> 15,139,839 bytes (fp32)
sha256 ef120799457bca042d4690944f0faf93268eb4654e7f50f28784ad63bdc1fe02,
与上游 commit 8e264d2 发布的原始文件逐字节一致, 零修改。
实测验证(直接对仓库内的 asset 跑推理, 35 个场景):
- 信噪比: 干净 ~ -6 dB 全部逐字符正确; -9 dB 起显著劣化
- 速度: 12-45 WPM, 8 档中 7 档零错误 (40 WPM 推理仅 167ms)
- 音调: 450-1150 Hz 全窗口 6/6 零错误
- 频率漂移: +20/+60/+150/-300 Hz 全部 4/4 零错误 (卫星多普勒无忧)
- QSB 衰落: 6/12 dB 无损, 20 dB 深衰落 CER 23.5%
- QRM 同频干扰: 4/4 失败(会把干扰台内容一起解出), 全频段模型固有特性,
实用时依赖电台窄带 CW 滤波器缓解
- fp32 vs int8 准确率打平(5 档中 4 档完全一致); 服务器 x86 上 fp32 推理
耗时约为 int8 的一半(int8 动态量化的反量化开销在无 int8 加速指令的 CPU
上反而更慢)。手机 ARM 侧表现待装机确认。
NOTICE.md / DEEPCW.md / README.md 同步更新: 移除 int8 量化派生的记录与复现
步骤, 改为声明未修改照搬上游。
代价: APK 体积约 59MB -> 70MB, 运行内存峰值上升。此前真机闪退的根因是 R8
缺 -keep ai.onnxruntime.** 规则(已修), 与模型大小无关。
2026-08-13 14:48:56 +00:00
mckero
5d89190348
ui: slim the More menu and drop dead controls from the CW page
...
用户反馈三处 UI 问题, 一并处理。
1. 更多菜单风格不符 + 遮盖感重
- 去掉全屏 scrim 遮罩(0.35 alpha 压暗整页), 改为透明点击层, 点外部仍可关闭
- Card 限宽 232dp 靠右下角, 从"全宽卡片"变成竖长条
- 容器色 surfaceContainerHigh -> surfaceContainer, 与导航栏一致; 加 1dp 细边框
- 动画从全屏 expandVertically + spring 弹跳改为右下角原点 150ms scaleIn
- Card 加 clickable(enabled=false) 吞掉点击, 避免点卡片空白区误触关闭
2. CW 页左上角退出键点击无效 -> 删除
根因: CwDecodeScreen(navigateUp: () -> Unit = {}) 是默认空实现, 而
MainScreen 的 entry<Screen.CwDecode> 调用 CwDecodeScreen() 从未传入
navigateUp, 所以点击必然无反应。按 AGENTS.md 无死代码原则删除按钮 +
navigateUp 参数 + cw_back 字符串(五语)。
3. CW 页"正在监听"状态行在停止解码后仍显示 -> 删除
estimatedPitch 在暂停后保留上次值, 状态行不会消失, 属误导。删除该 Text
后 estimatedPitch / lastInferenceMs 两个 collector 成为死代码, 一并清理;
cw_status_listening / cw_status_tone 字符串(五语)同步删除。
signalStrength(瀑布图) 与 errorMessage(错误提示) 仍在用, 保留。
验证:
- :app:compileDebugKotlin + :feature:cw:compileDebugKotlin => BUILD SUCCESSFUL
- :core:domain:test => 31 个 CW 测试全绿 (7+13+3+8)
2026-08-13 14:48:08 +00:00
mckero
f9fd7a2cfa
Move the LICENSING NOTICE to a separate NOTICE file so GitHub detects AGPL-3.0
...
GitHub 的 license 检测器(licensee gem)要求 LICENSE 文件是纯许可证原文,
顶部加说明块会导致检测失败(显示 NOASSERTION/Other)。
根 LICENSE 恢复为纯 AGPL-3.0 原文(从 DeepCW-AGPL-3.0.txt 复制),
说明块移到根目录 NOTICE 文件(GitHub 也识别 NOTICE 文件)。
2026-08-13 13:40:45 +00:00
mckero
21f14848da
Merge origin/main (resolve CW fldigi vs DeepCW conflicts, keep DeepCW)
2026-08-13 13:39:13 +00:00
mckero
d1668f1c27
docs(cw): update in-app "what's new" for the DeepCW release
...
发版漏的一步: App 内检测到新版本时弹出的更新内容
(pass_whatsnew_message) 还停留在上一版 AMSAT/WaveLog 的说明,没写进本次
DeepCW CW 解码器的改动。
补上五语 (en/zh/tr/in/id) 的更新说明:
- DeepCW 神经网络 CW 解码 (弱信号解码率大幅提升)
- CW 解码历史永久保留
- CW 瀑布图 inferno 配色 + 平滑渐变
- 恢复 64 位 (arm64)
- 暂停/恢复误报修复
2026-08-13 10:20:30 +00:00
mckero
d6b61eaa4b
license: switch the root LICENSE to AGPL-3.0 for the combined work
...
用户指出 GitHub 上显示的许可证仍是 GPL-3.0,未反映合并 AGPL 组件的事实。
根因: 之前只在 README 加了 License 章节,根 LICENSE 文件仍是 GPL-3.0 原文,
GitHub 的自动检测(只扫 LICENSE 文件)自然还是 GPL-3.0。
修复: 合并作品按 GPL-3.0 §13 / AGPL-3.0 §13 处理,根 LICENSE 改为:
- 顶部 LICENSING NOTICE 说明构成 (Look4Sat 代码 GPL-3.0 + DeepCW 模型
AGPL-3.0-only, 合并作品按 AGPL-3.0 分发)
- 正文为 AGPL-3.0 原文 (从 DeepCW-AGPL-3.0.txt 复制)
原 GPL-3.0 原文保留在 feature/cw/licenses/Look4Sat-GPL-3.0.txt。
README License 章节同步更新,与新 LICENSE 一致。
此后 GitHub 侧边栏将识别为 AGPL-3.0。
2026-08-13 10:19:57 +00:00
mckero
26c714fc24
fix(cw): stop swallowing coroutine cancellation on pause/resume
...
用户反馈: 暂停再恢复时经常弹出 "CW decode failed: The coroutine scope
left the composition"。
根因: 暂停 (isListening=false) 使 LaunchedEffect 重启、旧的采集协程被
取消。若此刻 decodeWindow 正在 withContext(Dispatchers.Default) 里做 ONNX
推理, 取消传播时会抛出 LeftCompositionCancellationException
(message 即 "The coroutine scope left the composition", 是 CancellationException
的子类)。processBuffer 的 catch (Throwable) 把它当成解码失败吞掉, 既误报了
错误横幅, 又破坏了协程取消的正常传播。
修复:
- processBuffer 的 decodeWindow catch 里, CancellationException 直接
rethrow (暂停导致的中断是正常流程, 不是错误)。
- archiveDecode 调用同样包 try-catch, CancellationException rethrow,
其余异常只记日志不崩溃。
这是协程的标准纪律: 永远不要把 CancellationException 当业务异常吞掉。
版本: 4.5.5 -> 4.5.7 (versionCode 460)。此前多次删 v4.5.5 tag 重打导致
release 页面累积 12 个 draft 草稿, 已全部清除。
2026-08-13 09:55:15 +00:00
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
v4.5.5
2026-08-13 08:04:05 +00:00
mckero
f42d1f6c57
docs(cw): add DEEPCW.md recording DeepCW architecture, pitfalls, verification
...
记录 feature:cw 模块的完整集成知识, 供后续维护者免于重走弯路:
- 架构分层 (core:domain 纯 Kotlin 前后处理 / core:data ONNX 推理 / feature:cw UI)
- 模型规格与 int8/fp32 双版本取舍 (APK 内置 int8, fp32 作为 release 资产)
- 真机 release-only 的五个坑 (R8 keep / 惰性加载 / 线程 / 跨线程状态 / 双录音)
- 验证结果 (golden vector 79 测试 / 服务器端到端逐字符一致 / 真机 40WPM)
该文档与 skill 的 §7.55 根因坑互为参照: skill 记方法论, 这里记本模块实况。
2026-08-13 04:14:01 +00:00
mckero
3e25e1aa2f
fix(cw): keep ai.onnxruntime.** through R8 to stop native crash
...
根因定位(确定性): release 构建开启 R8 混淆 (convention 插件的
PluginSetupUtils 设 isMinifyEnabled=true), 但 onnxruntime-android 的 AAR
不含 proguard consumer rules (仅 aar-metadata.properties)。于是 R8 把
ai.onnxruntime.* 的类重命名/裁剪, 而 ONNX Runtime 的 Java 绑定走 JNI、
native 侧按原始类名/方法名解析 —— 类名对不上即 native 崩溃, 不产生
任何 Java 堆栈, 正好解释"闪退回桌面、无任何日志"。
此前的所有内存/线程修复都无效, 因为崩溃点根本不在 Java 层。
修复:
1. app/proguard-rules.pro 新增 (ONNX Runtime 官方文档要求):
-keep class ai.onnxruntime.** { *; }
-dontwarn ai.onnxruntime.**
2. app/build.gradle.kts 的 release buildType 引用该规则文件。
3. onnxruntime 1.28.0 -> 1.25.1: 1.28.0 为 2026-07-25 发布(仅三周),
1.25.1 更成熟; 同时排除 SIGILL 类骁龙 bug (该 bug 在 1.24.4 修复,
Android AAR 对应 1.25.0+)。
验证: 服务器完整管线 (真实20s音频->频谱->ONNX->CTC) 解码逐字符正确,
与手机端同一份代码逻辑。R8 keep 规则为官方文档明确要求项。
2026-08-13 03:46:14 +00:00
mckero
e0405b541f
fix(cw): prevent double AudioRecord when toggling capture
...
CwDecodeScreen 的 LaunchedEffect 原先用内层 launch{} 启动音频采集。
isListening 在权限授予后由 false->true->(授予回调再次置 true), 加上
LaunchedEffect(isListening, permissionGranted) 的双 key 重启, 会造成
前一个 audioFlow().collect 尚未取消、新的又启动 —— 两个 AudioRecord
同时录音, 内存翻倍、音频混叠, 是低内存机 OOM 崩溃的实锤级嫌疑。
改为在 LaunchedEffect 协程体内直接 collect (取消时随父协程及时停止),
并保持 (isListening, permissionGranted) 双 key 以正确处理权限授予后
需要重启采集的情形。
同时清理不再使用的 kotlinx.coroutines.launch import。
验证: :feature:cw:compileDebugKotlin => BUILD SUCCESSFUL
2026-08-13 03:16:36 +00:00
mckero
c374058e1d
perf(cw): ship int8-quantized DeepCW model (15MB -> 4MB)
...
用户真机闪退回桌面且无任何 Java 日志 => 高度怀疑进程被系统 LMK 杀掉
(内存不足) 或 native 层崩溃。模型加载 + ONNX 引擎初始化存在瞬时内存峰值,
fp32 模型 15MB 会放大这个峰值。
用 onnxruntime.quantization.quantize_dynamic 把模型权重动态量化为 QUInt8
(激活保持 float32):
- 体积: 15,139,839 -> 4,248,808 字节 (缩小 71%)
- 输入/输出名、形状、dtype 完全不变 -> Kotlin 代码无需改动
- 实测 CER (合成 CW 音频, fp32 vs int8 逐字符对比):
SNR>=0dB 完全一致; -4dB 起两者一同劣化, 无显著差异
- onnx.checker 校验通过
AGPL 合规: NOTICE.md 记录派生信息 (原文件 SHA/量化后 SHA/转换步骤),
量化属模型修改, 按 AGPL 要求如实声明。复现命令已更新。
验证: :core:domain:test => 79 tests 全绿 (golden vector 测试锁定的是
频谱图前处理, 与模型文件无关)
2026-08-13 01:02:04 +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
37300fb2a6
revert: remove build-check workflow, use release.yml for testing
2026-08-12 13:18:08 +00:00
mckero
fc8096bdd5
ci: add build-check workflow asserting DeepCW packaging facts
...
本机 2GB 内存带不动 assembleRelease, 构建移交 GitHub Actions。
现有 release.yml 只在打 tag 时触发, 补一个可手动触发 + CW 分支推送即跑的
工作流, 便于装机实测而无需发布 tag。
断言的打包事实 (任一不满足即失败):
- lib/arm64-v8a/ 存在 —— 验证 abiFilters 解除生效, 应用恢复 64 位
- libnativedecoderjni 不存在 —— 验证被删的逆向 JNI 库未残留
- assets/deepcw/model.onnx 打包尺寸恰为 15139839 字节 —— 验证 noCompress
生效, 未被压缩 (压缩会导致 ONNX Runtime mmap 失败)
- values-in / values-id 存在 —— 验证印尼语未被资源压缩丢弃
同时跑 :core:domain:test 与 assembleRelease (R8 检查), 产物上传为 artifact,
配置了签名 secrets 时对 release APK 签名。
2026-08-12 13:12:15 +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
86ab1b26b7
feat(cw): vendor DeepCW ONNX model with full AGPL-3.0 compliance
...
背景:
DeepCW (e04/deepcw-engine) 是基于 CTC 的神经网络 CW 解码模型。本地实测
弱信号解码率显著优于传统 DSP 方案: SNR<0dB 时 CER 16.0% vs ggmorse 52.6%;
SNR>=0dB 时两者均接近 0%。模型 400-1200Hz 固定频窗自带定频, 无需频谱峰值
跟踪与噪声门。
改动:
- assets/deepcw/model.onnx 15,139,839 字节, 原样收录未作修改
- assets/deepcw/model.onnx.json 模型元数据 (采样率 3200, FFT 256, hop 48,
65 个频段, log1p 归一化, 42 类含 CTC blank)
- licenses/DeepCW-AGPL-3.0.txt AGPL-3.0 许可证全文
- licenses/NOTICE.md 来源仓库/作者/取用 commit/SHA-256/
许可证兼容性说明/复现步骤
- app/build.gradle.kts: androidResources.noCompress += "onnx"
关于 noCompress:
ONNX Runtime 通过 mmap 直接读取 assets, 压缩后无法内存映射, createSession
会失败。该配置只在打包 APK 的 app 模块生效, 在 feature 库模块声明无效。
许可证:
Look4Sat 为 GPL-3.0-or-later, DeepCW 模型为 AGPL-3.0-only。GPL v3 第 13 条
明确允许两者合并分发。推理完全在本地设备进行, 不通过网络向远程用户提供
模型功能, 故 AGPL 第 13 条的网络源码提供义务不被触发。本仓库公开, 完整
对应源码提供义务已满足。
上游取用 commit: 8e264d243bbd4467bd19f3f28292219405b47e0e
模型 SHA-256: ef120799457bca042d4690944f0faf93268eb4654e7f50f28784ad63bdc1fe02
2026-08-12 11:47:02 +00:00
mckero
de6c964136
build!: drop armeabi-v7a abiFilters, restore 64-bit builds
...
背景:
abiFilters=armeabi-v7a 的唯一成因是 Morse Expert 的 32 位
libnativedecoderjni.so, 该 .so 已在上一提交删除。此前整个应用被迫以
32 位运行: 64 位设备走兼容层、寻址受限, 且不满足应用市场的 64 位要求。
原注释称"用户设备为 32 位软件"已与实际不符 (测试机为 64 位)。
改动:
- app/build.gradle.kts: 移除 defaultConfig.ndk.abiFilters 及相关注释
- feature/cw/build.gradle.kts: 移除 abiFilters; 移除 constraintlayout 依赖
(仅被已删除的 activity_main.xml 使用); 移除 UTF-8 JavaCompile 配置
(模块已无 Java 源码)
- feature/cw 改为依赖 core:domain + core:data (DeepCW 前后处理落在 core)
验证:
grep -rn 'abiFilters|jniLibs' --include=*.kts . (排除 build/) => 无匹配
APK ABI 覆盖将在 CI 产物中验证 (预期含 arm64-v8a)
2026-08-12 11:44:54 +00:00
mckero
e477851adf
refactor(cw)!: remove reverse-engineered Morse Expert engine and 32-bit JNI blob
...
背景:
feature/cw 内含 Morse Expert 1.15 (VE3NEA, 闭源免费软件) 的 jadx 反编译产物,
外加仅 armeabi-v7a 的 libnativedecoderjni.so (1.4MB)。该 .so 无 64 位版本,
迫使 app 模块设置 abiFilters=armeabi-v7a, 把整个应用锁死在 32 位。
闭源软件的反编译产物不具备任何分发授权。
改动 (共 58 项):
- 删除 pas/* (JNI 入口 nativedecoder/decoder/cwstru/system)
- 删除 com/ve3nea/morse_expert/* (MainActivity/DecodedTextView/ScaleView/
WaterfallSurfaceView)
- 删除混淆包 B B0 D E2 F2 H2 I0 I2 J2 K1 d1 g3 i3 j1 j3 k3 s
- 删除 suncompat/* (sun.misc.Unsafe/Cleaner 存根)
- 删除 jniLibs/armeabi-v7a/libnativedecoderjni.so
- 删除配套 layout (activity_main, cw_panel_main)、menu、4 个 ic_baseline_* 图标
- proguard-rules.pro 清空 keep 规则 (已无 JNI 按类名注册依赖)
验证:
find feature/cw/src -name '*.java' | wc -l => 0
find feature/cw/src -name '*.so' | wc -l => 0
git ls-files feature/cw 仅剩 build.gradle.kts, proguard-rules.pro,
CwDecodeScreen.kt, CwSettingsDialog.kt 及 5 份 app_values.xml
注意: 本提交后 CwDecodeScreen.kt 暂时无法编译 (它 inflate 了已删除的
R.layout.activity_main), 将在 DeepCW 内核接入后重写为纯 Compose。
后续: DeepCW ONNX 内核接入 (纯 Kotlin 前后处理), abiFilters 解除恢复 arm64。
2026-08-12 11:42:53 +00:00
mckero
d2d4a6fe86
chore(git): ignore .keystore files and the Hermes workspace
...
*.jks 已被忽略, 补上 *.keystore 覆盖另一种签名库扩展名。
.hermes/ 是 agent 的本地计划目录, 不属于项目产物。
背景: BG7NTA.jks 位于仓库根目录, 此前仅依赖 *.jks 规则; 补充规则以防
其他扩展名的签名材料被误提交进公开仓库。
2026-08-12 11:40:54 +00:00
mckero
c42e1d7b4e
fix(wavelog): send real ADIF band + sat_mode, not the illegal "SAT"
...
Satellite QSOs uploaded with BAND=SAT, which is not a legal ADIF Band
enumeration value (the legal values are concrete bands: 160M/80M/.../
2M/70CM/23CM...). Loggers that fail to parse an unknown band fall back
to a default — observed as QSOs landing in 160m. SAT is only legal as
PROP_MODE (propagation mode), which is already sent for v1.
Changes (WaveLogApi):
- bandFromHz(): map TX frequency to the real ADIF band (2M for VHF,
70CM for UHF, etc.)
- satModeFrom(): derive the ADIF SAT_MODE convention string from TX/RX
bands ("V/U" = VHF up / UHF down, "U/V", "V/S", "U/S"...; empty for
same-band links)
- v2 JSON: band=<real band>, add sat_mode when non-empty
- v1 ADIF: <band:> real band, add <sat_mode:> when non-empty;
PROP_MODE=SAT kept
Verification:
- New tests: SO-50 (145.850 up / 436.795 down) -> band 2M, sat_mode V/U;
AO-73 (435.150 up / 145.950 down) -> band 70CM, sat_mode U/V;
same-band -> empty sat_mode; satellite freqs never map to 160M.
- All wavelog payload tests + full domain suite green.
v4.5.6
2026-08-09 07:48:15 +00:00
mckero
09ebf1f39a
feat(cw): add spectral auto-tune so the decoder finds the CW tone
...
The fldigi port ran a fixed 600 Hz NCO, so any real signal not inside
600±75 Hz (the 150 Hz filter passband) decoded nothing — the decode rate
was effectively zero unless the tone happened to be on frequency. This
mirrors the behaviour of the removed channelTracker: a sliding spectral
peak detector now steers the NCO to the strongest tone.
Changes:
- Collect raw input, run a 512-pt Hann-windowed FFT every frame, find
the strongest bin in 300..1500 Hz (CW range), smooth-track it.
- First strong peak locks immediately (no RX reset, so the triggering
element survives); later large jumps (>120 Hz) retune and reset the
fldigi state machine; small drifts are eased at 20%.
- Absolute energy floor (peak < 30) so silence/noise never steers.
- estimatedPitch now reflects the tracked tone frequency.
Verification:
- New unit test: 900 Hz "CQ" with decoder initialized at 600 Hz decodes
correctly and pitch moves to ~900 Hz.
- All 9 decoder tests pass; full domain/cw/radar test suites green.
2026-08-09 05:04:48 +00:00
mckero
ec40f29f28
fix(cw): make the waterfall redraw as new spectra arrive
...
The waterfall backed its pixels with a plain FloatArray and never
signalled Compose, so the Canvas drew once (empty) and stayed frozen —
no spectrum ever appeared. Add a monotonic frame-counter State that
pushSamples bumps per FFT frame; the Canvas reads it in composition to
trigger redraws. Also switch to log-ish intensity scaling so quiet bins
stay dark while strong CW tones pop, matching the DeepCW look.
Applies to both the CW decode screen and the radar transceiver panel.
2026-08-08 15:04:12 +00:00
mckero
5dd7a35a23
fix(cw): remove unused legacy drawables that fail release resource linking
...
ic_baseline_delete/pause/save/share_24.xml were leftovers from the
Morse Expert View-based UI. They reference ?attr/colorControlNormal
which does not resolve in the release variant (no Material dependency
in feature:cw), breaking assembleRelease. The new Compose UI uses
icons from core:presentation, so these files are dead code.
2026-08-08 14:45:10 +00:00
mckero
4b835bac5c
feat(cw): replace reversed Morse Expert engine with a pure-Kotlin fldigi port
...
Background:
The CW decoder previously shipped a decompiled copy of the proprietary
Morse Expert 1.15 (com/ve3nea/morse_expert + obfuscated classes,
libnativedecoderjni.so, suncompat black-magic) — a copyright liability.
This removes all of it and reimplements the decoder on the open-source
fldigi (GPL v3) CW engine as a faithful pure-Kotlin port with no JNI.
Changes:
- Delete all Morse Expert reverse-engineered code: MainActivity,
obfuscated packages (B/B0/D/E2/...), suncompat/, pas/nativedecoder,
armeabi-v7a libnativedecoderjni.so, and the original View-based layouts
(activity_main, cw_panel_main, options_menu).
- Add a full fldigi CW pipeline in core/domain/cw:
- CwFldigiDsp: NCO down-conversion, FFT filter, movavg constants
- CwFftFilt: overlap-add FFT band-pass filter (fftfilt port)
- MorseTable + SomTable: full Morse code table + SOM codebook
- CwFldigiDecoder: decode_stream AGC + hysteresis, state machine,
adaptive speed tracking (5-55 WPM), SOM winner/normalize matching
- Rewrite CwDecodeScreen as pure Compose (DeepCW-style waterfall,
live decode line, history, status cards) and CwSettingsDialog
(speed/bandwidth/SOM) with no View interop.
- Replace the Morse Expert panel in TransceiversPage with a Compose
panel driving the same decoder; mic capture at 8000 Hz.
- Drop the forced armeabi-v7a abiFilters now that no native lib exists.
Verification:
- 8 unit tests pass (CQ/HELLO at 18-20 wpm, A-J at 30 wpm with
adaptive tracking, dot/dash/Farnsworth edge cases) — all decode
correctly from synthesized CW.
- :feature:cw and :feature:radar compile; app assembleDebug succeeds.
- APK contains no ve3nea/nativedecoder/morse_expert classes.
2026-08-08 13:58:19 +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
fb014a1183
chore(release): bump versionCode to 458 (4.5.5 upload fixes)
2026-08-05 11:28:53 +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
90bb584813
fix(status): drop appcompat tint attr from ic_refresh (library module has no such attr)
2026-08-05 08:53:05 +00:00