From fe8595f747d0687c8289ee29ecd29c39d05d7fe0 Mon Sep 17 00:00:00 2001 From: ZXCLI Date: Wed, 9 Sep 2026 19:23:19 +0800 Subject: [PATCH] android: remove demo synthetic thermal image (real-device display test); rebuild toolchain+APK on this machine --- .../mag160c/thermal/ui/live/LiveViewModel.kt | 126 +----------------- build-artifacts/mag160c-app-debug.apk | 4 +- docs/android_app/HANDOFF_DEVELOPMENT.md | 47 ++++--- docs/android_app/session_state.md | 17 +++ 4 files changed, 52 insertions(+), 142 deletions(-) diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveViewModel.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveViewModel.kt index 0c31e06..83e9b74 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveViewModel.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveViewModel.kt @@ -2,13 +2,10 @@ package com.mag160c.thermal.ui.live import android.app.Application import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope import com.mag160c.thermal.core.TempMath import com.mag160c.thermal.usb.IrSession import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch /** * Live view state machine: USB permission -> link -> stream -> OSD stats. @@ -96,13 +93,17 @@ class LiveViewModel(app: Application) : AndroidViewModel(app) { } } - /** Begin USB permission flow, then start streaming. No device -> demo mode. */ + /** Begin USB permission flow, then start streaming. No device -> idle notice. */ fun connect() { val context = getApplication() val transport = com.mag160c.thermal.usb.UsbTransport(context) val dev = transport.findDevice() if (dev == null) { - startDemo() + _state.value = _state.value.copy( + connected = false, + streaming = false, + status = "no_device", + ) return } transport.requestPermission { ok -> @@ -116,87 +117,6 @@ class LiveViewModel(app: Application) : AndroidViewModel(app) { } } - private var demoPipeline: com.mag160c.thermal.core.RenderPipeline? = null - private var demoJob: kotlinx.coroutines.Job? = null - - /** - * Demo mode: feed synthetic frames through the REAL render pipeline - * (official DDT bundled) so the UI is fully explorable without hardware. - */ - private fun startDemo() { - if (demoJob?.isActive == true) return - _state.value = _state.value.copy(connected = true, streaming = true, status = "demo") - val frames = buildDemoFrames() - demoJob = viewModelScope.launch(kotlinx.coroutines.Dispatchers.Default) { - val ddt = runCatching { - getApplication().assets.open("mag160c.ddt").readBytes() - }.getOrDefault(ByteArray(0)) - val pipe = com.mag160c.thermal.core.RenderPipeline(160, 120, onFfc = {}) - if (!pipe.loadDdt(ddt)) return@launch - demoPipeline = pipe - val out = IntArray(320 * 240) - var count = 0 - while (demoPipeline != null) { - for (f in frames.indices) { - val p = demoPipeline ?: return@launch - if (p.frame(frames[f], true, out)) { - latestFrame = out.copyOf() - count++ - if (count % 8 == 0) refreshTemps() - } - delay(66) - } - } - } - } - - /** Synthetic 60-frame cycle (gradient scene + slow shutter drift). */ - private fun buildDemoFrames(): ArrayList { - val frameSize = 0x38 + 38400 - val frames = ArrayList(60) - var seed = 20260906 - for (f in 0 until 60) { - val buf = ByteArray(frameSize) - var acc = seed - fun next(): Int { - acc = (acc * 1103515245 + 12345) and 0x7FFFFFFF - return acc % 97 - 48 - } - seed = acc - for (i in 0 until 19200) { - val x = i % 160 - val y = i / 160 - var base = x * 4 + y * 3 + 14000 - // warm block (like a hand in frame), upper-right area - if (x in 96..144 && y in 28..76) base += 620 - if (x in 104..136 && y in 36..68) base += 180 - val v = (base + next()).coerceIn(0, 65535) - val off = 0x1C + i * 2 - buf[off] = (v and 0xFF).toByte() - buf[off + 1] = ((v shr 8) and 0xFF).toByte() - } - frames.add(buf) - } - // headers + trailers - for (f in frames.indices) { - val buf = frames[f] - put32(buf, 0, 0x1BB1B11B) - put32(buf, 4, f) - put32(buf, 8, 38400) - put32(buf, 12, if (f in 6..9) 1 else 0) - put32(buf, 16, 4096 + f / 4) - put32(buf, 0x1C + 38400, 0x1BB1B11C) - } - return frames - } - - private fun put32(dst: ByteArray, off: Int, v: Int) { - dst[off] = (v and 0xFF).toByte() - dst[off + 1] = ((v shr 8) and 0xFF).toByte() - dst[off + 2] = ((v shr 16) and 0xFF).toByte() - dst[off + 3] = ((v ushr 24) and 0xFF).toByte() - } - fun disconnect() = session.stop() fun triggerFfc() = session.triggerFfc() @@ -333,37 +253,6 @@ class LiveViewModel(app: Application) : AndroidViewModel(app) { /** Update per-frame temperature stats (called on a slow timer). */ fun refreshTemps() { - val demo = demoPipeline - if (demo != null) { - // Demo-only display mapping (true calibration needs hardware). - demo.copyNuc(demoNuc) - var mean = 0L - var mn = Int.MAX_VALUE - var mx = -1 - var mnPos = -1 - var mxPos = -1 - for (i in demoNuc.indices) { - val v = demoNuc[i] - mean += v - if (v < mn) { mn = v; mnPos = i } - if (v > mx) { mx = v; mxPos = i } - } - mean /= demoNuc.size - val mv = mean.toFloat() - val probes = _state.value.probes.map { p -> - val v = demoNuc[p.y * 160 + p.x] - p.copy(tempC = 24f + (v - mean) * 0.02f) - } - _state.value = _state.value.copy( - centerTempC = 24f + (demoNuc[60 * 160 + 80] - mean) * 0.02f, - maxTempC = 24f + (mx - mean) * 0.02f, - minTempC = 24f + (mn - mean) * 0.02f, - maxPos = mxPos, - minPos = mnPos, - probes = probes, - ) - return - } if (!session.isStreaming()) return val center = session.probeTemp(80, 60) val nuc = IntArray(19200) @@ -371,8 +260,6 @@ class LiveViewModel(app: Application) : AndroidViewModel(app) { updateTemps(center, nuc) } - private val demoNuc = IntArray(19200) - private fun updateTemps(center: Int?, nuc: IntArray) { var mn = Int.MAX_VALUE var mx = -1 @@ -403,7 +290,6 @@ class LiveViewModel(app: Application) : AndroidViewModel(app) { } override fun onCleared() { - demoPipeline = null session.destroy() super.onCleared() } diff --git a/build-artifacts/mag160c-app-debug.apk b/build-artifacts/mag160c-app-debug.apk index a0b73e1..8ce6814 100644 --- a/build-artifacts/mag160c-app-debug.apk +++ b/build-artifacts/mag160c-app-debug.apk @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9069b65f5822d9bdca4bbbdf0a28ad05d0891b344149009f49dad546caab598 -size 12232282 +oid sha256:7f8b898035e65a02d75a8e23828e1716ed6aeaea1f959cd087a6173fe91272f9 +size 11806269 diff --git a/docs/android_app/HANDOFF_DEVELOPMENT.md b/docs/android_app/HANDOFF_DEVELOPMENT.md index 860b6ba..dabeeb5 100644 --- a/docs/android_app/HANDOFF_DEVELOPMENT.md +++ b/docs/android_app/HANDOFF_DEVELOPMENT.md @@ -28,10 +28,16 @@ ## 2. 技术栈与工具链(已部署) - **纯 Kotlin + Jetpack Compose (Material 3)**,无 NDK、无 .so、无 native。 -- Gradle 8.14.3(`C:\Tools\gradle-8.14.3\bin\gradle.bat`),JDK Zulu 21(`C:\Program Files\Zulu\zulu-21`)。 -- Android SDK:`C:\Users\ZXC\AppData\Local\Android\Sdk`(platforms/android-36、build-tools 36.1.0、 - platform-tools/adb、模拟器 system-images android-36.1 google_apis_playstore x86_64、AVD `Medium_Phone_API_36.1`)。 -- 工程:`C:\Project\MAG160C\android\`;版本目录 `gradle\libs.versions.toml` +- **工具链(2026-09-09 本机重装;交接文档旧机器路径 C:\Tools\gradle-8.14.3、 + Zulu、C:\Users\ZXC\AppData\Local\Android\Sdk 均已失效)**: + - JDK Temurin 21:`C:\Tools\jdk-21` + - Android SDK:`C:\Tools\android-sdk`(cmdline-tools\latest、platforms/android-36、 + build-tools 36.0.0、platform-tools/adb;许可已接受;无模拟器/AVD/system-images) + - Gradle:用工程 wrapper `android\gradlew.bat`(8.14.3 自动下载) + - `android\local.properties` 已写 `sdk.dir`(已 gitignore) + - 构建需 `JAVA_HOME=C:\Tools\jdk-21`;国内下载慢可给 JVM 挂本机代理 + (Clash 7890:`GRADLE_OPTS="-Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=7890 ..."`) +- 工程版本目录 `gradle\libs.versions.toml` (AGP 8.11.1 / Kotlin 2.2.0 / Compose BOM 2025.06.01 / lifecycle 2.9.1 / junit 4.13.2)。 - **磁盘轻量约定**:不装 NDK/CMake/zig(曾占用 2.4GB+,已卸载);全 Kotlin。 - 模拟器实测命令(无窗口): @@ -126,8 +132,9 @@ res/drawable/*.xml 自绘矢量图标(双弧圆等可靠几何图形 即包含挖孔安全区高度,否则真机热像顶端会插进顶栏背后);底导航高度经 `UiInsets.navPx`、快门区高度经其 `onSizeChanged`→`vm.uiBottomPx`(= 导航+快门区), 渲染器据此留白。 -- 演示模式:无设备时用真实管线+DDT渲染合成帧(热块场景),demo温度用局部线性显示映射 - (真机温度走管线数学;绝对温度标定待真机)。 +- **演示模式已撤下**(2026-09-09 用户要求,接真机实测显示,勿加回来):无设备时 + `connect()` 置 `status="no_device"`,LiveScreen 显示占位文案"未检测到热像仪, + 请插入MAG160C",渲染器无帧黑底;startDemo/合成帧代码已删除。 ### 4.4 温度 - 温度 = 毫度 int(÷1000 = ℃)。`counts_to_temp_mc`(NUC域→T2E逆映射)用于探针/OSD, @@ -137,12 +144,10 @@ res/drawable/*.xml 自绘矢量图标(双弧圆等可靠几何图形 ## 5. 构建 / 测试 / 提交 ```powershell -# 构建(无 --daemon 单次,约1分钟) +# 构建 + 单元测试(无 --daemon 单次,约1分钟;首次在本机需下载依赖较久) cd C:\Project\MAG160C\android -$env:JAVA_HOME="C:\Program Files\Zulu\zulu-21" -& "C:\Tools\gradle-8.14.3\bin\gradle.bat" :app:assembleDebug --no-daemon -# 单元测试(管线差分验证) -& "C:\Tools\gradle-8.14.3\bin\gradle.bat" test --no-daemon +$env:JAVA_HOME="C:\Tools\jdk-21" +.\gradlew.bat :app:assembleDebug test --no-daemon # 产物 Copy-Item app\build\outputs\apk\debug\app-debug.apk C:\Project\MAG160C\build-artifacts\mag160c-app-debug.apk # 提交(每里程碑一个 commit,不 push) @@ -166,9 +171,10 @@ git -C C:\Project\MAG160C commit -m "android: ..." **已完成**:核心管线移植(字节级验证)、USB 层、实时画面(相机式布局: 顶栏4控制项 FFC/变倍/追踪/调色板 + 底部快门区 相册/拍照/录像 + 底导航)、 -图标文字按物理持机朝向补偿旋转(加速度计)、演示模式、拍照 MDT 容器→MediaStore、 +图标文字按物理持机朝向补偿旋转(加速度计)、拍照 MDT 容器→MediaStore、 MP4 录像、媒体库、离线分析查看器、PDF 报告、任务解析、设置页、Android16 沉浸 (状态栏隐藏+竖屏锁定+挖孔安全区)、乱码多次修复。 +(演示模式 2026-09-09 按用户要求撤下,见 §4.3。) **布局约定(最终,勿回退)**: - Activity **锁定竖屏**(`portrait`),构图钉死手机竖屏框架:顶栏/热像区域/快门区/底导航 @@ -204,10 +210,12 @@ MP4 录像、媒体库、离线分析查看器、PDF 报告、任务解析、设 > `com.mag160c.thermal`)。Android 16 已适配,目标是一个 MAG160C 热像仪(160×120, 15fps, > USB VID 0x833C PID 1)统一 APP。 > -> 当前已完成到第 9 轮反馈修复:核心渲染管线 Kotlin 移植(逐字节验证)、USB 层、 +> 当前已完成到第 10 轮反馈修复:核心渲染管线 Kotlin 移植(逐字节验证)、USB 层、 > 相机式实时布局(顶栏4控制项+底部快门区+底导航)、图标文字持机朝向补偿旋转、 -> 演示模式(无设备用真实管线+DDT渲染合成帧)、拍照MDT存MediaStore、MP4录像、 -> 媒体库、离线分析、PDF报告、任务解析、设置页、Android16沉浸(竖屏锁定+挖孔适配)。 +> 拍照MDT存MediaStore、MP4录像、媒体库、离线分析、PDF报告、任务解析、设置页、 +> Android16沉浸(竖屏锁定+挖孔适配)。 +> 第 10 轮(2026-09-09):按用户要求撤下演示模式合成热像图(无设备只显示 +> "未检测到热像仪"占位),接真机+热像仪实测显示。 > > 硬性约定(勿回退):① Activity 竖屏锁定 `portrait`,构图钉死手机竖屏框架, > 屏显方向必须始终对应镜头方向;图标/文字用 `ui/DeviceOrientation`(加速度计, @@ -224,11 +232,10 @@ MP4 录像、媒体库、离线分析查看器、PDF 报告、任务解析、设 > > 构建/提交: > ```powershell -> cd C:\Project\MAG160C\android; $env:JAVA_HOME="C:\Program Files\Zulu\zulu-21" -> & "C:\Tools\gradle-8.14.3\bin\gradle.bat" :app:assembleDebug --no-daemon -> & "C:\Tools\gradle-8.14.3\bin\gradle.bat" test --no-daemon +> cd C:\Project\MAG160C\android; $env:JAVA_HOME="C:\Tools\jdk-21" +> .\gradlew.bat :app:assembleDebug test --no-daemon > git -C C:\Project\MAG160C add -A; git -C C:\Project\MAG160C commit -m "android: ..." > ``` -> 环境:`C:\Tools\gradle-8.14.3`、JDK Zulu 21、SDK `C:\Users\ZXC\AppData\Local\Android\Sdk`、 -> AVD `Medium_Phone_API_36.1`。注意:git 工作树里有大量与本任务无关的脏文件 +> 环境(2026-09-09 本机重装):JDK Temurin 21 `C:\Tools\jdk-21`、SDK `C:\Tools\android-sdk` +> (无模拟器/AVD)、Gradle 用工程 wrapper。注意:git 工作树里有大量与本任务无关的脏文件 > (`.tools/`、`IR_Camera_SDK/`、`csdk/`、`analysis/` 等),**不要提交它们**,只提交你的改动。 \ No newline at end of file diff --git a/docs/android_app/session_state.md b/docs/android_app/session_state.md index 8da5ff8..5277cb7 100644 --- a/docs/android_app/session_state.md +++ b/docs/android_app/session_state.md @@ -190,6 +190,23 @@ (相册/录像 42dp、快门 60dp、纵向 padding 6dp、间距 44dp)。 - [x] 模拟器复测布局正常,单元测试全过;APK 已更新。 +## 用户反馈修复 第十轮(2026-09-09,撤下演示热像图) + +- [x] **用户要求**:撤下用于测试的合成热像图(演示模式),准备接真机+热像仪实测显示。 +- [x] LiveViewModel:`connect()` 无设备时不再启动演示渲染,改为 `status="no_device"` + (LiveScreen 既有占位文案"未检测到热像仪,请插入MAG160C",渲染器无帧即黑底); + 删除 startDemo/buildDemoFrames/demoPipeline/demoNuc 与 refreshTemps 演示分支 + (连带消除"演示循环与真机流同时写 latestFrame、演示温度覆盖真机温度"的隐患)。 + 勿把演示模式加回来。 +- [x] 工具链在本机重装并重建 APK(2026-09-09):JDK Temurin 21 `C:\Tools\jdk-21`、 + SDK `C:\Tools\android-sdk`(platform 36 / build-tools 36.0.0 / platform-tools,许可已接受)、 + Gradle 用工程 wrapper(8.14.3);交接文档旧路径 C:\Tools\gradle-8.14.3 / Zulu / + AppData SDK 属另一台开发机已失效(HANDOFF §2/§5 已改为本机路径)。 + assembleDebug+test 全过;dex 抽查中文 UTF-8 正常、无 startDemo 残留; + build-artifacts/mag160c-app-debug.apk 已更新(11.8MB),可直接装真机。 +- 待办不变(真机 USB 实测温度标定、网络互连远程预览、离线MDT温度解码、 + 调色板精确提取、PIP/云模块)。 + ## 待办 - 真机USB实测(温度绝对值标定、FFC/录像/MDT保存端到端)