android: remove demo synthetic thermal image (real-device display test); rebuild toolchain+APK on this machine

This commit is contained in:
ZXCLI
2026-09-09 19:23:19 +08:00
parent ca7e29a647
commit fe8595f747
4 changed files with 52 additions and 142 deletions
@@ -2,13 +2,10 @@ package com.mag160c.thermal.ui.live
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.mag160c.thermal.core.TempMath import com.mag160c.thermal.core.TempMath
import com.mag160c.thermal.usb.IrSession import com.mag160c.thermal.usb.IrSession
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/** /**
* Live view state machine: USB permission -> link -> stream -> OSD stats. * 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() { fun connect() {
val context = getApplication<Application>() val context = getApplication<Application>()
val transport = com.mag160c.thermal.usb.UsbTransport(context) val transport = com.mag160c.thermal.usb.UsbTransport(context)
val dev = transport.findDevice() val dev = transport.findDevice()
if (dev == null) { if (dev == null) {
startDemo() _state.value = _state.value.copy(
connected = false,
streaming = false,
status = "no_device",
)
return return
} }
transport.requestPermission { ok -> 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<Application>().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<ByteArray> {
val frameSize = 0x38 + 38400
val frames = ArrayList<ByteArray>(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 disconnect() = session.stop()
fun triggerFfc() = session.triggerFfc() fun triggerFfc() = session.triggerFfc()
@@ -333,37 +253,6 @@ class LiveViewModel(app: Application) : AndroidViewModel(app) {
/** Update per-frame temperature stats (called on a slow timer). */ /** Update per-frame temperature stats (called on a slow timer). */
fun refreshTemps() { 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 if (!session.isStreaming()) return
val center = session.probeTemp(80, 60) val center = session.probeTemp(80, 60)
val nuc = IntArray(19200) val nuc = IntArray(19200)
@@ -371,8 +260,6 @@ class LiveViewModel(app: Application) : AndroidViewModel(app) {
updateTemps(center, nuc) updateTemps(center, nuc)
} }
private val demoNuc = IntArray(19200)
private fun updateTemps(center: Int?, nuc: IntArray) { private fun updateTemps(center: Int?, nuc: IntArray) {
var mn = Int.MAX_VALUE var mn = Int.MAX_VALUE
var mx = -1 var mx = -1
@@ -403,7 +290,6 @@ class LiveViewModel(app: Application) : AndroidViewModel(app) {
} }
override fun onCleared() { override fun onCleared() {
demoPipeline = null
session.destroy() session.destroy()
super.onCleared() super.onCleared()
} }
Binary file not shown.
+27 -20
View File
@@ -28,10 +28,16 @@
## 2. 技术栈与工具链(已部署) ## 2. 技术栈与工具链(已部署)
- **纯 Kotlin + Jetpack Compose (Material 3)**,无 NDK、无 .so、无 native。 - **纯 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`)。 - **工具链(2026-09-09 本机重装;交接文档旧机器路径 C:\Tools\gradle-8.14.3、
- Android SDK`C:\Users\ZXC\AppData\Local\Android\Sdk`platforms/android-36、build-tools 36.1.0、 Zulu、C:\Users\ZXC\AppData\Local\Android\Sdk 均已失效)**
platform-tools/adb、模拟器 system-images android-36.1 google_apis_playstore x86_64、AVD `Medium_Phone_API_36.1`)。 - JDK Temurin 21`C:\Tools\jdk-21`
- 工程:`C:\Project\MAG160C\android\`;版本目录 `gradle\libs.versions.toml` - 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)。 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。 - **磁盘轻量约定**:不装 NDK/CMake/zig(曾占用 2.4GB+,已卸载);全 Kotlin。
- 模拟器实测命令(无窗口): - 模拟器实测命令(无窗口):
@@ -126,8 +132,9 @@ res/drawable/*.xml 自绘矢量图标(双弧圆等可靠几何图形
即包含挖孔安全区高度,否则真机热像顶端会插进顶栏背后);底导航高度经 即包含挖孔安全区高度,否则真机热像顶端会插进顶栏背后);底导航高度经
`UiInsets.navPx`、快门区高度经其 `onSizeChanged`→`vm.uiBottomPx`= 导航+快门区), `UiInsets.navPx`、快门区高度经其 `onSizeChanged`→`vm.uiBottomPx`= 导航+快门区),
渲染器据此留白。 渲染器据此留白。
- 演示模式:无设备时用真实管线+DDT渲染合成帧(热块场景),demo温度用局部线性显示映射 - **演示模式已撤下**(2026-09-09 用户要求,接真机实测显示,勿加回来):无设备时
(真机温度走管线数学;绝对温度标定待真机)。 `connect()` 置 `status="no_device"`LiveScreen 显示占位文案"未检测到热像仪,
请插入MAG160C",渲染器无帧黑底;startDemo/合成帧代码已删除。
### 4.4 温度 ### 4.4 温度
- 温度 = 毫度 int(÷1000 = ℃)。`counts_to_temp_mc`(NUC域→T2E逆映射)用于探针/OSD, - 温度 = 毫度 int(÷1000 = ℃)。`counts_to_temp_mc`(NUC域→T2E逆映射)用于探针/OSD,
@@ -137,12 +144,10 @@ res/drawable/*.xml 自绘矢量图标(双弧圆等可靠几何图形
## 5. 构建 / 测试 / 提交 ## 5. 构建 / 测试 / 提交
```powershell ```powershell
# 构建(无 --daemon 单次,约1分钟) # 构建 + 单元测试(无 --daemon 单次,约1分钟;首次在本机需下载依赖较久
cd C:\Project\MAG160C\android cd C:\Project\MAG160C\android
$env:JAVA_HOME="C:\Program Files\Zulu\zulu-21" $env:JAVA_HOME="C:\Tools\jdk-21"
& "C:\Tools\gradle-8.14.3\bin\gradle.bat" :app:assembleDebug --no-daemon .\gradlew.bat :app:assembleDebug test --no-daemon
# 单元测试(管线差分验证)
& "C:\Tools\gradle-8.14.3\bin\gradle.bat" test --no-daemon
# 产物 # 产物
Copy-Item app\build\outputs\apk\debug\app-debug.apk C:\Project\MAG160C\build-artifacts\mag160c-app-debug.apk Copy-Item app\build\outputs\apk\debug\app-debug.apk C:\Project\MAG160C\build-artifacts\mag160c-app-debug.apk
# 提交(每里程碑一个 commit,不 push # 提交(每里程碑一个 commit,不 push
@@ -166,9 +171,10 @@ git -C C:\Project\MAG160C commit -m "android: ..."
**已完成**:核心管线移植(字节级验证)、USB 层、实时画面(相机式布局: **已完成**:核心管线移植(字节级验证)、USB 层、实时画面(相机式布局:
顶栏4控制项 FFC/变倍/追踪/调色板 + 底部快门区 相册/拍照/录像 + 底导航)、 顶栏4控制项 FFC/变倍/追踪/调色板 + 底部快门区 相册/拍照/录像 + 底导航)、
图标文字按物理持机朝向补偿旋转(加速度计)、演示模式、拍照 MDT 容器→MediaStore、 图标文字按物理持机朝向补偿旋转(加速度计)、拍照 MDT 容器→MediaStore、
MP4 录像、媒体库、离线分析查看器、PDF 报告、任务解析、设置页、Android16 沉浸 MP4 录像、媒体库、离线分析查看器、PDF 报告、任务解析、设置页、Android16 沉浸
(状态栏隐藏+竖屏锁定+挖孔安全区)、乱码多次修复。 (状态栏隐藏+竖屏锁定+挖孔安全区)、乱码多次修复。
(演示模式 2026-09-09 按用户要求撤下,见 §4.3。)
**布局约定(最终,勿回退)** **布局约定(最终,勿回退)**
- Activity **锁定竖屏**`portrait`),构图钉死手机竖屏框架:顶栏/热像区域/快门区/底导航 - Activity **锁定竖屏**`portrait`),构图钉死手机竖屏框架:顶栏/热像区域/快门区/底导航
@@ -204,10 +210,12 @@ MP4 录像、媒体库、离线分析查看器、PDF 报告、任务解析、设
> `com.mag160c.thermal`)。Android 16 已适配,目标是一个 MAG160C 热像仪(160×120, 15fps, > `com.mag160c.thermal`)。Android 16 已适配,目标是一个 MAG160C 热像仪(160×120, 15fps,
> USB VID 0x833C PID 1)统一 APP。 > USB VID 0x833C PID 1)统一 APP。
> >
> 当前已完成到第 9 轮反馈修复:核心渲染管线 Kotlin 移植(逐字节验证)、USB 层、 > 当前已完成到第 10 轮反馈修复:核心渲染管线 Kotlin 移植(逐字节验证)、USB 层、
> 相机式实时布局(顶栏4控制项+底部快门区+底导航)、图标文字持机朝向补偿旋转、 > 相机式实时布局(顶栏4控制项+底部快门区+底导航)、图标文字持机朝向补偿旋转、
> 演示模式(无设备用真实管线+DDT渲染合成帧)、拍照MDT存MediaStore、MP4录像、 > 拍照MDT存MediaStore、MP4录像、媒体库、离线分析、PDF报告、任务解析、设置页
> 媒体库、离线分析、PDF报告、任务解析、设置页、Android16沉浸(竖屏锁定+挖孔适配)。 > Android16沉浸(竖屏锁定+挖孔适配)。
> 第 10 轮(2026-09-09):按用户要求撤下演示模式合成热像图(无设备只显示
> "未检测到热像仪"占位),接真机+热像仪实测显示。
> >
> 硬性约定(勿回退):① Activity 竖屏锁定 `portrait`,构图钉死手机竖屏框架, > 硬性约定(勿回退):① Activity 竖屏锁定 `portrait`,构图钉死手机竖屏框架,
> 屏显方向必须始终对应镜头方向;图标/文字用 `ui/DeviceOrientation`(加速度计, > 屏显方向必须始终对应镜头方向;图标/文字用 `ui/DeviceOrientation`(加速度计,
@@ -224,11 +232,10 @@ MP4 录像、媒体库、离线分析查看器、PDF 报告、任务解析、设
> >
> 构建/提交: > 构建/提交:
> ```powershell > ```powershell
> cd C:\Project\MAG160C\android; $env:JAVA_HOME="C:\Program Files\Zulu\zulu-21" > cd C:\Project\MAG160C\android; $env:JAVA_HOME="C:\Tools\jdk-21"
> & "C:\Tools\gradle-8.14.3\bin\gradle.bat" :app:assembleDebug --no-daemon > .\gradlew.bat :app:assembleDebug test --no-daemon
> & "C:\Tools\gradle-8.14.3\bin\gradle.bat" test --no-daemon
> git -C C:\Project\MAG160C add -A; git -C C:\Project\MAG160C commit -m "android: ..." > 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` > 环境2026-09-09 本机重装):JDK Temurin 21 `C:\Tools\jdk-21`、SDK `C:\Tools\android-sdk`
> AVD `Medium_Phone_API_36.1`。注意:git 工作树里有大量与本任务无关的脏文件 > (无模拟器/AVD)、Gradle 用工程 wrapper。注意:git 工作树里有大量与本任务无关的脏文件
> `.tools/``IR_Camera_SDK/``csdk/``analysis/` 等),**不要提交它们**,只提交你的改动。 > `.tools/``IR_Camera_SDK/``csdk/``analysis/` 等),**不要提交它们**,只提交你的改动。
+17
View File
@@ -190,6 +190,23 @@
(相册/录像 42dp、快门 60dp、纵向 padding 6dp、间距 44dp)。 (相册/录像 42dp、快门 60dp、纵向 padding 6dp、间距 44dp)。
- [x] 模拟器复测布局正常,单元测试全过;APK 已更新。 - [x] 模拟器复测布局正常,单元测试全过;APK 已更新。
## 用户反馈修复 第十轮(2026-09-09,撤下演示热像图)
- [x] **用户要求**:撤下用于测试的合成热像图(演示模式),准备接真机+热像仪实测显示。
- [x] LiveViewModel`connect()` 无设备时不再启动演示渲染,改为 `status="no_device"`
LiveScreen 既有占位文案"未检测到热像仪,请插入MAG160C",渲染器无帧即黑底);
删除 startDemo/buildDemoFrames/demoPipeline/demoNuc 与 refreshTemps 演示分支
(连带消除"演示循环与真机流同时写 latestFrame、演示温度覆盖真机温度"的隐患)。
勿把演示模式加回来。
- [x] 工具链在本机重装并重建 APK2026-09-09):JDK Temurin 21 `C:\Tools\jdk-21`
SDK `C:\Tools\android-sdk`platform 36 / build-tools 36.0.0 / platform-tools,许可已接受)、
Gradle 用工程 wrapper8.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保存端到端) - 真机USB实测(温度绝对值标定、FFC/录像/MDT保存端到端)