From ee6859877af03ddc9ccede0b4fa41dc09740e6b7 Mon Sep 17 00:00:00 2001 From: ZXCLI Date: Sat, 12 Sep 2026 15:36:34 +0800 Subject: [PATCH] =?UTF-8?q?android+docs:=20debug=20=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=85=B3=E9=97=AD=EF=BC=9B=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=89=8B=E5=86=8C=E5=8D=87=E7=BA=A7=E4=B8=BA=E6=8F=92=E5=9B=BE?= =?UTF-8?q?=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit debug 日志:DebugLog 增加总开关 enabled(默认 false),log() 直接返回、 init() 不装崩溃钩子、startFile() 不建文件。要在设备上排查时改成 true 重新构建 即可,其它代码不动。关闭后崩溃仍按安卓常规方式可见(logcat AndroidRuntime + 系统弹窗),只是不再有落在相册目录旁的 debug_*.log。 真机烟雾测试(小米 22041211AC,相机插着):出流正常、拍照成功(相册 19→20, 新增 MAG160C_20260912_153522.jpg)、崩溃 0、App 自身日志行数 0、无新日志文件。 插图版手册 docs/android_app/app_manual.md + manual_images/: - 02_markers:标记结构解剖(准星/读数/引线/描边字),真机照片放大 3 倍并标注 - 03/04:横屏 960x720 与竖屏 720x960 的真实照片 - 05/06/07:设置、分析(含底部测量面板)、相册 真机截图 - 01_live_schematic:实时界面改为**自绘示意图**,因为实时 OSD 文字按握持角度预旋转 (用户要求的可读性设计),手册拍摄时手机横放,截图里文字全是侧躺的无法阅读; 其余界面文字本就水平,直接用截图。 图由 manual_images/MakeFigures.java 生成(裁剪缩放/放大标注/示意图绘制), 界面改动后重跑即可;原始截图存为 JPEG(PNG→JPEG 省约 8MB)作为可再生输入。 .class 已加入 .gitignore。 README 阅读顺序加入操作手册、结构表更新、里程碑补记根因级修复。105 项测试全绿。 --- .gitignore | 3 + .../com/mag160c/thermal/MainActivity.kt | 6 +- .../com/mag160c/thermal/media/DebugLog.kt | 28 +- build-artifacts/mag160c-app-debug.apk | 2 +- docs/android_app/app_manual.md | 114 ++++---- .../manual_images/01_live_schematic.png | 3 + .../manual_images/01_settings_raw.jpg | 3 + docs/android_app/manual_images/02_markers.png | 3 + .../manual_images/03_photo_landscape.png | 3 + .../manual_images/04_photo_portrait.png | 3 + .../manual_images/05_analysis_raw.jpg | 3 + .../android_app/manual_images/05_settings.png | 3 + .../android_app/manual_images/06_analysis.png | 3 + .../manual_images/06_gallery_raw.jpg | 3 + docs/android_app/manual_images/07_gallery.png | 3 + .../manual_images/MAG160C_20260912_141530.jpg | 3 + .../manual_images/MAG160C_20260912_145010.jpg | 3 + .../manual_images/MakeFigures.java | 265 ++++++++++++++++++ docs/android_app/manual_images/RawJpeg.java | 27 ++ docs/android_app/session_state.md | 48 ++++ 20 files changed, 459 insertions(+), 70 deletions(-) create mode 100644 docs/android_app/manual_images/01_live_schematic.png create mode 100644 docs/android_app/manual_images/01_settings_raw.jpg create mode 100644 docs/android_app/manual_images/02_markers.png create mode 100644 docs/android_app/manual_images/03_photo_landscape.png create mode 100644 docs/android_app/manual_images/04_photo_portrait.png create mode 100644 docs/android_app/manual_images/05_analysis_raw.jpg create mode 100644 docs/android_app/manual_images/05_settings.png create mode 100644 docs/android_app/manual_images/06_analysis.png create mode 100644 docs/android_app/manual_images/06_gallery_raw.jpg create mode 100644 docs/android_app/manual_images/07_gallery.png create mode 100644 docs/android_app/manual_images/MAG160C_20260912_141530.jpg create mode 100644 docs/android_app/manual_images/MAG160C_20260912_145010.jpg create mode 100644 docs/android_app/manual_images/MakeFigures.java create mode 100644 docs/android_app/manual_images/RawJpeg.java diff --git a/.gitignore b/.gitignore index aca4356..b04a856 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ __pycache__/ # ZCode 会话目录 .zcode/ + +# Java build output for the manual figure generator +docs/android_app/manual_images/*.class diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/MainActivity.kt b/android/app/src/main/kotlin/com/mag160c/thermal/MainActivity.kt index 0bf791f..8d9b6fa 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/MainActivity.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/MainActivity.kt @@ -12,8 +12,10 @@ import com.mag160c.thermal.ui.theme.Mag160cTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // field-debug crash hook + log file (opened at startup so even - // early crashes are captured; logging never throws) + // Field-debug logging is OFF by default (see DebugLog.enabled): a shipping + // build writes no log file and no logcat lines. Flip that flag and rebuild to + // capture a detailed trace on a device — these two calls then behave exactly + // as they used to. com.mag160c.thermal.media.DebugLog.init(applicationContext) com.mag160c.thermal.media.DebugLog.startFile(applicationContext) // enableEdgeToEdge exists since API 21 but reaches for the modern inset diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/media/DebugLog.kt b/android/app/src/main/kotlin/com/mag160c/thermal/media/DebugLog.kt index 5cd0198..68bd18b 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/media/DebugLog.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/media/DebugLog.kt @@ -16,8 +16,20 @@ import java.util.Locale /** * Field-debug logger: mirrors every line to logcat AND appends it to a - * timestamped text file next to the photos (DCIM/MAG160C), so the user can - * hand over the file without adb (real-device bring-up, round 11). + * timestamped text file next to the photos (DCIM/MAG160C), so a problem on the + * user's phone can be diagnosed without adb. + * + * ## It is OFF by default (user decision, 2026-09-12) + * + * A shipping build has no business writing a log file beside the user's photos or + * filling logcat at 15 frames per second, so [enabled] defaults to false and every + * entry point returns immediately. To investigate something on a device, flip + * [enabled] to true and rebuild — that is the whole procedure; the file then + * appears exactly as it used to. + * + * With logging off, crashes are still reported the normal Android way (logcat + * `AndroidRuntime` plus the system's "app keeps stopping" dialog); only the + * convenient on-device copy is gone. * * Logging must NEVER crash the app: every sink operation is guarded, and * writes are capped. Sink fallback chain (scoped storage only allows media @@ -28,6 +40,13 @@ import java.util.Locale * The header line records where the file actually ended up. */ object DebugLog { + /** + * Master switch. OFF in shipping builds: writes nothing, to no sink. + * Set to true (and rebuild) to capture a detailed trace on a device. + */ + @Volatile + var enabled: Boolean = false + private val fmt = SimpleDateFormat("HH:mm:ss.SSS", Locale.ENGLISH) private val fileFmt = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH) private val lock = Any() @@ -38,6 +57,7 @@ object DebugLog { /** Install once from MainActivity.onCreate: crash hook + app context. */ fun init(context: Context) { + if (!enabled) return synchronized(lock) { if (previousHandler != null) return previousHandler = Thread.getDefaultUncaughtExceptionHandler() @@ -51,6 +71,7 @@ object DebugLog { /** Log one line (also to logcat as MAG160C/). Never throws. */ fun log(tag: String, msg: String) { + if (!enabled) return try { Log.d("MAG160C/$tag", msg) } catch (_: Exception) { @@ -80,9 +101,10 @@ object DebugLog { /** * Open a fresh debug_*.log. Called once at app start (so even startup * crashes are captured); never throws. Returns the display path for - * logging. Closes any previous file. + * logging. Closes any previous file. Does nothing when [enabled] is false. */ fun startFile(context: Context): String { + if (!enabled) return "(debug log disabled)" val name = "debug_${fileFmt.format(Date())}.log" closeFile() synchronized(lock) { diff --git a/build-artifacts/mag160c-app-debug.apk b/build-artifacts/mag160c-app-debug.apk index 8777c70..f2481eb 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:69b8ea3a8edb5c0f8a7c1e0f900223e3fa62f1625ee071d36bf13c0054644cbd +oid sha256:3332381023d485eae92a9e548ec366b1bd03a08a3a3c17257e587e86fd0099d5 size 12741424 diff --git a/docs/android_app/app_manual.md b/docs/android_app/app_manual.md index 215aa8d..2d82850 100644 --- a/docs/android_app/app_manual.md +++ b/docs/android_app/app_manual.md @@ -3,6 +3,10 @@ 适用版本:MAG160C 统一热像版 1.0.0(`com.mag160c.thermal`) 硬件:MAG160C 热像仪机芯(160×120 @15fps),USB VID `0x833C` PID `0x0001` +> 本文中的插图:**截图**取自真机(小米 22041211AC / Android 12);**示意图**用于说明 +> 布局与标记结构。图片源文件在 `manual_images/`,可用同目录的 `MakeFigures.java` +> 重新生成(界面改动后请重新出图,避免手册与实物不一致)。 + --- ## 1. 安装与首次连接 @@ -14,79 +18,62 @@ 也可以直接把 APK 拷进手机点击安装(需允许"安装未知应用")。 2. 把热像仪插到手机的 USB 口。系统会弹出 **"允许 App 访问该 USB 设备"** → 勾选"始终允许"并确定。**这个授权在手机重启后会失效,需要重新确认一次**。 -3. App 会自动连接并出图,通常 1~2 秒。画面出现即表示握手完成(内部流程: - 查设备 → 请求权限 → 握手 → 读标定文件 → 开始出流)。 +3. App 会自动连接并出图,通常 1~2 秒(内部流程:查设备 → 请求权限 → 握手 → + 读标定文件 → 开始出流)。 -> 如果顶栏下面显示"未检测到热像仪",见第 11 节。 +> 顶栏下方显示"未检测到热像仪"时,见第 11 节。 --- ## 2. 界面总览 -``` -┌─────────────────────────────────────────┐ -│ FFC 1× 追高·低 铁虹 画中画 │ ← 控制顶栏 -├─────────────────────────────────────────┤ -│ │ -│ 热成像画面(锁屏构图) │ -│ │ -├─────────────────────────────────────────┤ -│ 相册 拍照(快门) 录像 │ ← 拍摄栏 -├─────────────────────────────────────────┤ -│ 实时 相册 分析 设置 │ ← 底部导航 -└─────────────────────────────────────────┘ -``` +![实时界面示意图](manual_images/01_live_schematic.png) + +| 编号 | 位置 | 作用 | +|---|---|---| +| ① | 顶栏 · FFC | 手动做一次快门校正(画面短暂冻结属正常) | +| ② | 顶栏 · 1× | 数码变倍,点一次在 1× → 2× → 3× → 4× 循环 | +| ③ | 顶栏 · 追踪 | 全画面最高/最低温标记(最高、最低、两者、关闭) | +| ④ | 顶栏 · 铁虹 | 切换调色板(12 种) | +| — | 顶栏 · 画中画 | 可见光画面叠加,三档尺寸,可拖动 | +| ⑤ | 拍摄栏 | 相册 / **拍照** / 录像 | +| ⑥ | 底部导航 | 实时 / 相册 / 分析 / 设置 | + +画面里还有两个读数:左上角 **"中心 xx.x℃"** 是画面中心点温度;右侧竖条是 +**温度色标**,上下端标注当前画面的最高温与最低温。 App 固定竖屏显示。转动手机时**界面和画面构图都不跟着转**(见第 8 节)。 --- -## 3. 实时界面 +## 3. 实时界面操作 -| 控件 | 作用 | -|---|---| -| **FFC** | 手动做一次快门校正。画面会短暂冻结一下,属正常;校正后温度读数会刷新 | -| **1×** | 数码变倍,点一次在 1× → 2× → 3× → 4× 之间循环(从画面中心裁剪放大) | -| **追高·低** | 全画面最高温/最低温标记的开关与目标(最高、最低、两者、关闭) | -| **铁虹** | 切换调色板(12 种,点开选择) | -| **画中画** | 可见光画面叠加(首次使用需授予摄像头权限),三档尺寸,可拖动摆放 | -| **相册 / 拍照 / 录像** | 见下 | - -**测温点**:直接在画面上**点一下**就加一个测点;**点已有的测点**则删除它。 -测点会显示序号 + 温度,并随画面一起保存进照片。 - -**拍照**:立刻把当前画面存成照片(含温度数据、测点、最高/最低温标记)。 - -**录像**:点一次开始,再点一次停止并保存。录制中按钮显示"停止"。 +- **测温点**:直接在画面上**点一下**就加一个测点;**点已有的测点**则删除它。 + 测点会显示序号 + 温度,并随照片一起保存。 +- **拍照**:立刻把当前画面存成照片(含温度数据、测点、最高/最低温标记)。 +- **录像**:点一次开始,再点一次停止并保存;录制中按钮显示"停止"。 --- ## 4. 画面上的标记怎么读 -从本轮起标记改成仪表风格,含义如下: +![标记结构](manual_images/02_markers.png) -``` - │ - ┌─┴─┐ - ───┤ ├─── Pt1 25.0℃ - └─┬─┘ - │ +| 编号 | 部位 | 说明 | +|---|---|---| +| ① | 方形准星 + 四根短臂 | 指住被测量的**那一个像素**;中心镂空,不遮挡被测区域 | +| ② | 读数 | `Pt1`/`Pt2`… 是手动测点;`MAX`/`MIN` 是全画面最高/最低温 | +| ③ | 引线 | 把读数和它自己的准星连起来,测点多时不会看错 | +| ④ | 文字样式 | 白字 + 黑描边,**没有白色底板**:冷端(黑)和热端(白)上都读得清,且不挡画面 | - 方形准星 + 四根短臂:指住被测量的那一个像素,中心镂空不遮挡被测区域 - 右侧描边白字:温度读数;引线把读数和自己的准星连起来,测点多时不会看错 - 没有白色底板 —— 冷端(黑)和热端(白)上都靠描边保证可读,且不遮挡画面 -``` - -- **`MAX` / `MIN`**:全画面的最高温与最低温,显示哪一个由"追踪"设置决定 -- **`Pt1` `Pt2` …**:你手动点的测点 -- **引线**:从读数连到自己那个准星,测点多时不会看错 - -温度单位统一为 ℃,显示一位小数。 +温度单位统一 ℃,一位小数。 --- ## 5. 相册 +![相册](manual_images/07_gallery.png) + - 列出 `DCIM/MAG160C` 里的照片,按时间倒序(最新的在最前) - **点开**:全屏查看;**双指缩放**、**拖动平移**、**双击复位** - **删除**:右上角"删除" → 确认后从手机中移除(不可恢复) @@ -96,9 +83,9 @@ App 固定竖屏显示。转动手机时**界面和画面构图都不跟着转** ## 6. 分析(离线测温) -进入"分析"页会列出**带温度数据的照片**(早期没有温度数据的旧照片会被过滤掉)。 +![分析界面](manual_images/06_analysis.png) -打开一张照片后: +进入"分析"页会列出**带温度数据的照片**(早期没有温度数据的旧照片会被过滤)。 | 操作 | 说明 | |---|---| @@ -108,7 +95,7 @@ App 固定竖屏显示。转动手机时**界面和画面构图都不跟着转** | **顶部 · 备注** | 给这张照片写一段文字备注(写入文件本身) | | **顶部 · 保存** | 把当前测点烧录进图像,**另存为一张新照片**(温度数据一并保留) | -分析页显示的标记与照片里已经烧录的标记位置完全一致,不会出现"两个测点"。 +分析页显示的标记与照片里已烧录的标记位置完全一致,不会出现"两个测点"。 **注意**:分析读的是照片里存的原始温度数据,所以同一张照片任何时候打开, 读数都一样(不会因为环境变化而漂移)。 @@ -117,6 +104,8 @@ App 固定竖屏显示。转动手机时**界面和画面构图都不跟着转** ## 7. 设置 +![设置](manual_images/05_settings.png) + | 设置项 | 说明 | 是否影响画面 | |---|---|---| | **默认调色板** | 启动时使用的调色板 | ✔ | @@ -137,7 +126,7 @@ App 固定竖屏显示。转动手机时**界面和画面构图都不跟着转** 若画面上下颠倒,用"旋转USB画面"(每次 90°);若左右镜像反了,用"水平翻转"或 "竖直翻转"。这三项是**永久生效的设置**,实时、拍照、录像都会跟着变。 -**图像增强怎么选**:档位越高,纳入增强的对比度门槛越高、强边缘增强幅度越大, +**图像增强怎么选**:档位越高,纳入增强的对比度门槛越高、强边缘的增强幅度越大, 即"更锐但更挑"——弱对比区域(噪声所在处)会被排除,所以调高**不会**把噪声放大。 日常建议 2~4 级;需要看清弱纹理时再上 6~8 级。默认关闭。 @@ -145,10 +134,13 @@ App 固定竖屏显示。转动手机时**界面和画面构图都不跟着转** ## 8. 拍照 / 录像的方向(重要) -- **文件方向跟随你拿手机的方向**(和普通相机一样): - - 竖着拿 → 照片 720×960(竖幅),录像也是竖幅 - - 横着拿 → 照片 960×720(横幅),录像也是横幅 - - 无论怎么拿,**场景都是正的**,不需要事后旋转 +![竖屏握持拍出的照片](manual_images/04_photo_portrait.png) +*竖着拿手机拍 → 720×960 竖幅照片,场景是正的* + +![横屏握持拍出的照片](manual_images/03_photo_landscape.png) +*横着拿手机拍 → 960×720 横幅照片,场景同样是正的* + +- **文件方向跟随你拿手机的方向**(和普通相机一样),所以**不需要事后旋转**。 - **预览画面本身是锁定的**:横着拿手机时,屏幕里的画面看起来是侧的 (因为屏幕构图固定在竖屏框架上),但保存下来的文件是正的。 这是刻意设计:热像仪固定在手机上,锁定预览才能保证转动手机时不晃方向。 @@ -164,7 +156,6 @@ App 固定竖屏显示。转动手机时**界面和画面构图都不跟着转** | 照片 | `DCIM/MAG160C/MAG160C_yyyyMMdd_HHmmss.jpg` | | 分析另存 | `DCIM/MAG160C/MAG160C_yyyyMMdd_HHmmss_edit.jpg` | | 录像 | `DCIM/MAG160C/MAG160C_V_yyyyMMdd_HHmmss.mp4` | -| 调试日志 | `Download/MAG160C/debug_*.log`(部分机型写进 `DCIM/MAG160C/`,排查问题时取这个文件) | 照片文件是 **MDT 容器**:文件开头就是标准 JPEG(任何看图软件都能打开), 后面追加了温度数据、测点坐标、备注、渲染参数等信息。 @@ -201,17 +192,12 @@ App 固定竖屏显示。转动手机时**界面和画面构图都不跟着转** - 先做一次 FFC(环境温度变化后尤其需要) - 分析页的读数来自照片里存的原始数据,不会随时间变化;实时页读数会随环境漂移 -**录像停止时日志里有 "drain error"** -- 文件仍然完整可播放(尾部可能略短)。这是编码器在停止瞬间偶发的状态问题, - 已做保护不会闪退,文件也已保存 - **手机息屏后回来** - 息屏时渲染停止(省电)、USB 读取继续,已保存的文件不受影响 -**电脑上想取文件** +**想拿文件到电脑上** ``` adb pull /sdcard/DCIM/MAG160C/ -adb pull /sdcard/Download/MAG160C/ # 调试日志(部分机型在 DCIM/MAG160C) ``` --- diff --git a/docs/android_app/manual_images/01_live_schematic.png b/docs/android_app/manual_images/01_live_schematic.png new file mode 100644 index 0000000..8ffee30 --- /dev/null +++ b/docs/android_app/manual_images/01_live_schematic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f423b3eaa983ebaeb71c1cc3316ad2bb49402212b41f6178e947e82fab1dfa87 +size 103941 diff --git a/docs/android_app/manual_images/01_settings_raw.jpg b/docs/android_app/manual_images/01_settings_raw.jpg new file mode 100644 index 0000000..7f5febe --- /dev/null +++ b/docs/android_app/manual_images/01_settings_raw.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eca868c7c54b525a8a6148129f237493c0b22d0b37d9e67762b008ffe11336a6 +size 248861 diff --git a/docs/android_app/manual_images/02_markers.png b/docs/android_app/manual_images/02_markers.png new file mode 100644 index 0000000..898a48b --- /dev/null +++ b/docs/android_app/manual_images/02_markers.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8277c2fa54e3e923250b5607fc263f353df53898e0acb80667172c62c280682f +size 284758 diff --git a/docs/android_app/manual_images/03_photo_landscape.png b/docs/android_app/manual_images/03_photo_landscape.png new file mode 100644 index 0000000..4541e63 --- /dev/null +++ b/docs/android_app/manual_images/03_photo_landscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aff597cebe57ca905aa950fa4498813983ac96e7c3b052e0a71096a887db48d3 +size 862343 diff --git a/docs/android_app/manual_images/04_photo_portrait.png b/docs/android_app/manual_images/04_photo_portrait.png new file mode 100644 index 0000000..eaea37b --- /dev/null +++ b/docs/android_app/manual_images/04_photo_portrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:699e371ea9a5b26741c0750ee7695b457c365e75a4db10b4a172012d87473676 +size 659561 diff --git a/docs/android_app/manual_images/05_analysis_raw.jpg b/docs/android_app/manual_images/05_analysis_raw.jpg new file mode 100644 index 0000000..fa52823 --- /dev/null +++ b/docs/android_app/manual_images/05_analysis_raw.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c95810762c12a0a331061fbbd32fdd19f781db281e168cce6067dab7ddd939b +size 292039 diff --git a/docs/android_app/manual_images/05_settings.png b/docs/android_app/manual_images/05_settings.png new file mode 100644 index 0000000..3819be4 --- /dev/null +++ b/docs/android_app/manual_images/05_settings.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4cd7b2bc8891a938b8c03786df16e31fd46fea8fd7041833db7b6eef384472e3 +size 89006 diff --git a/docs/android_app/manual_images/06_analysis.png b/docs/android_app/manual_images/06_analysis.png new file mode 100644 index 0000000..f4bbdb4 --- /dev/null +++ b/docs/android_app/manual_images/06_analysis.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31a6649aeb09c45c1fbb8269ddf310a97fda43577fb997dd1a17852ad958497e +size 1002326 diff --git a/docs/android_app/manual_images/06_gallery_raw.jpg b/docs/android_app/manual_images/06_gallery_raw.jpg new file mode 100644 index 0000000..d20e418 --- /dev/null +++ b/docs/android_app/manual_images/06_gallery_raw.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7efab3c007dcebc4b1c0db4d7ff4d607b42b78459d68a20037bdcb2b62691bdc +size 682925 diff --git a/docs/android_app/manual_images/07_gallery.png b/docs/android_app/manual_images/07_gallery.png new file mode 100644 index 0000000..d365ca8 --- /dev/null +++ b/docs/android_app/manual_images/07_gallery.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:480943e6b2b0a991245a9c69e063469ca18cfc53640e0f23996a07f121ecfe08 +size 1014199 diff --git a/docs/android_app/manual_images/MAG160C_20260912_141530.jpg b/docs/android_app/manual_images/MAG160C_20260912_141530.jpg new file mode 100644 index 0000000..a7dba10 --- /dev/null +++ b/docs/android_app/manual_images/MAG160C_20260912_141530.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2460bcedaafd2b1d82275eb9c5f4c5b78a3f08014bac2b5477a8a8a29dd4b902 +size 209396 diff --git a/docs/android_app/manual_images/MAG160C_20260912_145010.jpg b/docs/android_app/manual_images/MAG160C_20260912_145010.jpg new file mode 100644 index 0000000..480e007 --- /dev/null +++ b/docs/android_app/manual_images/MAG160C_20260912_145010.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6774b7625bbbd54ee9d4b3a79ea05229e6c40cd40351f413808543b2984fde6 +size 186500 diff --git a/docs/android_app/manual_images/MakeFigures.java b/docs/android_app/manual_images/MakeFigures.java new file mode 100644 index 0000000..2354972 --- /dev/null +++ b/docs/android_app/manual_images/MakeFigures.java @@ -0,0 +1,265 @@ +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import javax.imageio.ImageIO; + +/** + * Derive the manual's figures: scale/crop real screenshots, and build an annotated + * schematic of the live screen. Kept as a tool so the figures can be regenerated + * after a UI change instead of being stale hand-edited PNGs. + * + * Usage: MakeFigures + */ +public class MakeFigures { + static File outDir; + + public static void main(String[] args) throws Exception { + outDir = new File(args[0]); + File imgs = new File(args[1]); + outDir.mkdirs(); + + // --- real screenshots: scale to a readable width and crop the chrome-free part + // cropped to the rows: the full-screen shots are mostly empty on a 9:20 display + scale(new File(imgs, "01_settings_raw.jpg"), "05_settings.png", 780, 0, 60, 1440, 1720); + scale(new File(imgs, "05_analysis_raw.jpg"), "06_analysis.png", 700, 0, 60, 1440, 3040); + scale(new File(imgs, "06_gallery_raw.jpg"), "07_gallery.png", 780, 0, 60, 1440, 1260); + + // --- reference photos (already the real saved files) + scale(new File(imgs, "MAG160C_20260912_145010.jpg"), "03_photo_landscape.png", 900, 0, 0, 0, 0); + scale(new File(imgs, "MAG160C_20260912_141530.jpg"), "04_photo_portrait.png", 560, 0, 0, 0, 0); + + // --- marker anatomy: zoom on the MAX reticle of the landscape photo + BufferedImage lp = ImageIO.read(new File(imgs, "MAG160C_20260912_141530.jpg")); + markerAnatomy(lp); + + // --- live screen schematic (the real screen cannot be screenshotted for the + // manual: its OSD text is pre-rotated to the current grip, so a landscape-held + // phone yields sideways labels in the picture) + liveSchematic(); + } + + // ---------------------------------------------------------------- helpers + + static BufferedImage scaled(BufferedImage s, int w) { + int h = Math.max(1, (int) Math.round(s.getHeight() * (w / (double) s.getWidth()))); + BufferedImage o = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + Graphics2D g = o.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.drawImage(s, 0, 0, w, h, null); + g.dispose(); + return o; + } + + static void scale(File src, String out, int w, int x, int y, int cw, int ch) throws Exception { + BufferedImage s = ImageIO.read(src); + if (cw > 0) s = s.getSubimage(x, y, cw, ch); + BufferedImage o = scaled(s, w); + ImageIO.write(o, "png", new File(outDir, out)); + System.out.println("wrote " + out + " " + o.getWidth() + "x" + o.getHeight()); + } + + static Font font(int size, int style) { + // a CJK-capable family; falls back to the platform default if absent + return new Font("Microsoft YaHei", style, size); + } + + /** Zoomed crop around the MAX reticle, with labelled callouts. */ + static void markerAnatomy(BufferedImage photo) { + // Pt1 on the portrait reference photo (720x960): the reticle sits at + // (249,147) and the label to its right, so this window holds the whole glyph + // plus its readout. + int cx = 300, cy = 150; + int cw = 420, ch = 260; + int x = Math.max(0, cx - cw / 2), y = Math.max(0, cy - ch / 2); + BufferedImage crop = photo.getSubimage(x, Math.min(y, photo.getHeight() - ch), cw, ch); + int zoom = 3; + BufferedImage big = new BufferedImage(cw * zoom, ch * zoom, BufferedImage.TYPE_INT_RGB); + Graphics2D g = big.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g.drawImage(crop, 0, 0, cw * zoom, ch * zoom, null); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setFont(font(26, Font.BOLD)); + + // callouts: the reticle, the label, the leader + int rx = (cx - x) * zoom, ry = (cy - y) * zoom; + callout(g, "① 方形准星", rx - 150, ry - 120, rx, ry, new Color(0x4FC3F7)); + callout(g, "② MAX/MIN 读数", rx + 150, ry + 120, rx + 210, ry, new Color(0x81C784)); + g.setColor(new Color(0xFFD54F)); + g.setFont(font(24, Font.BOLD)); + g.drawString("③ 引线(读数连到自己的准星)", 24, ch * zoom - 60); + g.drawString("④ 白字 + 黑描边(无底板)", 24, ch * zoom - 24); + g.dispose(); + try { + ImageIO.write(big, "png", new File(outDir, "02_markers.png")); + System.out.println("wrote 02_markers.png " + big.getWidth() + "x" + big.getHeight()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + static void callout(Graphics2D g, String text, int lx, int ly, int tx, int ty, Color c) { + g.setColor(c); + g.setStroke(new BasicStroke(3f)); + g.drawLine(lx, ly, tx, ty); + g.fillOval(tx - 7, ty - 7, 14, 14); + FontMetrics fm = g.getFontMetrics(); + int w = fm.stringWidth(text); + int bx = lx < tx ? lx - w - 12 : lx + 12; + g.setColor(new Color(0, 0, 0, 190)); + g.fillRoundRect(bx - 6, ly - fm.getAscent() - 4, w + 12, fm.getHeight(), 8, 8); + g.setColor(c); + g.drawString(text, bx, ly); + } + + /** + * The live screen as a labelled diagram. Positions mirror the real layout + * (top control bar glued to the portrait top edge, image below it, capture row, + * bottom navigation). + */ + static void liveSchematic() throws Exception { + int W = 720, H = 1280; + BufferedImage im = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB); + Graphics2D g = im.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + g.setColor(new Color(0x101014)); + g.fillRect(0, 0, W, H); + + // top bar + int barH = 96; + g.setColor(new Color(0x5A5A50)); + g.fillRect(0, 24, W, barH); + String[] top = {"FFC", "1×", "追踪", "铁虹", "画中画"}; + for (int i = 0; i < top.length; i++) { + int cx = 78 + i * 142; + g.setColor(new Color(0xD8D8C8)); + g.setStroke(new BasicStroke(4f)); + g.drawOval(cx - 16, 24 + barH / 2 - 26, 32, 32); + g.setFont(font(22, Font.PLAIN)); + FontMetrics fm = g.getFontMetrics(); + g.drawString(top[i], cx - fm.stringWidth(top[i]) / 2, 24 + barH - 8); + } + + // image area with a marker, mimicking the real look + int imgX = 8, imgY = 24 + barH + 16, imgW = W - 16, imgH = 830; + GradientPaint gp = new GradientPaint( + imgX, imgY, new Color(0x1A0733), + imgX + imgW, imgY + imgH, new Color(0xB25A00)); + g.setPaint(gp); + g.fillRect(imgX, imgY, imgW, imgH); + g.setColor(new Color(0x5E1580)); + g.fillOval(imgX + 40, imgY + 250, 380, 320); + g.setColor(new Color(0xFFE066)); + g.fillOval(imgX + 380, imgY + 520, 260, 240); + + // a probe marker drawn the way the app draws it + int mx = imgX + 420, my = imgY + 400; + g.setColor(Color.WHITE); + g.setStroke(new BasicStroke(3f)); + g.drawRect(mx - 12, my - 12, 24, 24); + g.drawLine(mx - 30, my, mx - 12, my); + g.drawLine(mx + 12, my, mx + 30, my); + g.drawLine(mx, my - 30, mx, my - 12); + g.drawLine(mx, my + 12, mx, my + 30); + g.drawLine(mx + 30, my, mx + 62, my); + g.setFont(font(30, Font.BOLD)); + drawOutlined(g, "Pt1 25.0℃", mx + 62, my + 10); + // a max marker + int ax = imgX + 90, ay = imgY + 170; + g.setStroke(new BasicStroke(3f)); + g.setColor(Color.WHITE); + g.drawRect(ax - 12, ay - 12, 24, 24); + g.drawLine(ax + 12, ay, ax + 30, ay); + g.drawLine(ax, ay - 30, ax, ay - 12); + g.drawLine(ax, ay + 12, ax, ay + 30); + g.drawLine(ax - 30, ay, ax - 12, ay); + g.drawLine(ax + 30, ay, ax + 62, ay); + g.setFont(font(30, Font.BOLD)); + drawOutlined(g, "MAX 41.5℃", ax + 62, ay + 10); + // centre readout, upper-left of the image + g.setFont(font(30, Font.BOLD)); + drawOutlined(g, "中心 26.4℃", imgX + 20, imgY + 40); + // colour bar along the right edge + int barX = imgX + imgW - 34, barW = 22; + for (int i = 0; i < 120; i++) { + float t = i / 119f; + g.setColor(new Color( + (int) (255 * Math.min(1, t * 1.6)), + (int) (255 * Math.max(0, t * 1.6 - 0.6)), + (int) (255 * Math.max(0, 1 - t * 2.2)))); + g.fillRect(barX, imgY + 40 + i * 6, barW, 7); + } + + // capture row + int rowY = imgY + imgH + 30; + g.setColor(new Color(0x1B1B20)); + g.fillRect(0, rowY - 10, W, 210); + circle(g, 200, rowY + 60, 44, new Color(0x555555), "相册"); + circle(g, 360, rowY + 60, 62, Color.WHITE, "拍照"); + circle(g, 520, rowY + 60, 44, new Color(0xE53935), "录像"); + + // bottom navigation + int navY = H - 118; + g.setColor(new Color(0x2A2A22)); + g.fillRect(0, navY, W, 118); + String[] nav = {"实时", "相册", "分析", "设置"}; + for (int i = 0; i < nav.length; i++) { + int cx = 90 + i * 180; + g.setColor(i == 0 ? new Color(0xD8C88A) : new Color(0x9A9A9A)); + g.setStroke(new BasicStroke(4f)); + g.drawOval(cx - 16, navY + 22, 32, 32); + g.setFont(font(24, Font.PLAIN)); + FontMetrics fm = g.getFontMetrics(); + g.drawString(nav[i], cx - fm.stringWidth(nav[i]) / 2, navY + 92); + } + + // numbered callouts + g.setFont(font(26, Font.BOLD)); + badge(g, 1, 100, 40); + badge(g, 2, 242, 40); + badge(g, 3, 384, 40); + badge(g, 4, 526, 40); + badge(g, 5, 420, rowY + 20); + badge(g, 6, 130, navY + 24); + g.dispose(); + ImageIO.write(im, "png", new File(outDir, "01_live_schematic.png")); + System.out.println("wrote 01_live_schematic.png " + W + "x" + H); + } + + static final String[] CIRCLED = {"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"}; + + static void badge(Graphics2D g, int n, int x, int y) { + String s = CIRCLED[n - 1]; + g.setColor(new Color(0xE53935)); + g.fillOval(x - 20, y - 20, 40, 40); + g.setColor(Color.WHITE); + FontMetrics fm = g.getFontMetrics(); + g.drawString(s, x - fm.stringWidth(s) / 2, y + 10); + } + + static void circle(Graphics2D g, int cx, int cy, int r, Color c, String label) { + g.setColor(c); + g.fillOval(cx - r, cy - r, r * 2, r * 2); + if (c == Color.WHITE) { + g.setColor(new Color(0x222222)); + g.setStroke(new BasicStroke(3f)); + g.drawOval(cx - r, cy - r, r * 2, r * 2); + } + g.setColor(Color.WHITE); + g.setFont(font(24, Font.PLAIN)); + FontMetrics fm = g.getFontMetrics(); + g.drawString(label, cx - fm.stringWidth(label) / 2, cy + r + 34); + } + + /** White text with a dark outline — the same trick the app's markers use. */ + static void drawOutlined(Graphics2D g, String s, int x, int y) { + Shape shape = g.getFont().createGlyphVector(g.getFontRenderContext(), s).getOutline(x, y); + g.setColor(new Color(0, 0, 0, 220)); + g.setStroke(new BasicStroke(5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g.draw(shape); + g.setColor(Color.WHITE); + g.fill(shape); + } +} diff --git a/docs/android_app/manual_images/RawJpeg.java b/docs/android_app/manual_images/RawJpeg.java new file mode 100644 index 0000000..8f2f94f --- /dev/null +++ b/docs/android_app/manual_images/RawJpeg.java @@ -0,0 +1,27 @@ +import java.awt.image.BufferedImage; +import java.io.File; +import javax.imageio.*; +import javax.imageio.stream.FileImageOutputStream; +/** Convert capture PNGs to JPEG (the figures' inputs do not need to be lossless). */ +public class RawJpeg { + public static void main(String[] a) throws Exception { + for (String n : a) { + BufferedImage src = ImageIO.read(new File(n)); + // screencap PNGs carry an alpha channel; JPEG cannot take that colorspace + BufferedImage im = new BufferedImage( + src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_RGB); + im.createGraphics().drawImage(src, 0, 0, null); + String out = n.replace(".png", ".jpg"); + ImageWriter w = ImageIO.getImageWritersByFormatName("jpeg").next(); + ImageWriteParam p = w.getDefaultWriteParam(); + p.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + p.setCompressionQuality(0.88f); + try (FileImageOutputStream os = new FileImageOutputStream(new File(out))) { + w.setOutput(os); + w.write(null, new IIOImage(im, null, null), p); + } + w.dispose(); + System.out.println(out + " " + new File(out).length() / 1024 + " KB"); + } + } +} diff --git a/docs/android_app/session_state.md b/docs/android_app/session_state.md index b53a2af..2da249b 100644 --- a/docs/android_app/session_state.md +++ b/docs/android_app/session_state.md @@ -489,6 +489,54 @@ "未连接"分支显示,正常出图时用户看不到任何反馈)。 - [x] 单测 66 → **76 项全绿**;debug + release(R8) 双构建通过;APK 已更新。 +## 用户反馈修复 第二十五轮(2026-09-12,关闭 debug 日志 + 带图操作手册) + +用户三点要求:把 App 的 debug 日志关掉;做一个带图的手册;push 上去。 + +### 1) debug 日志默认关闭 + +`DebugLog` 增加总开关 `enabled`,**默认 false**:`log()` 直接返回(不写 logcat 也不写文件), +`init()` 不安装崩溃钩子,`startFile()` 不建文件。要在设备上排查时把该常量改成 true 重新 +构建即可,其它代码一行不用动。 + +- 关掉后崩溃仍按安卓常规方式可见(logcat `AndroidRuntime` + 系统"应用停止运行"弹窗), + 只是不再有那份落在相册目录旁的 `debug_*.log` +- 真机烟雾测试(小米 22041211AC):出流正常、拍照成功(相册 19 → 20 张, + 新增 `MAG160C_20260912_153522.jpg`)、崩溃数 0、**App 自身日志行数 0**、 + `Download/MAG160C` 无新日志文件 +- 注意:`adb logcat | grep MAG160C` 会匹配到系统 MediaProvider 打印的**文件路径**, + 不是本 App 的日志。判断 App 是否还在打日志要按 tag 过滤: + `grep -E "MAG160C/(vm|usb|session|stream|render|rec|analyze)"` + +### 2) 带图操作手册 + +`docs/android_app/app_manual.md` 升级为插图版,图在 `manual_images/`: + +| 图 | 内容 | 来源 | +|---|---|---| +| `01_live_schematic.png` | 实时界面布局与 ①~⑥ 控件编号 | **绘制**(见下) | +| `02_markers.png` | 标记结构解剖:准星/读数/引线/描边字 | 真机照片放大 3 倍 | +| `03/04_photo_*.png` | 横屏 960×720 与竖屏 720×960 的真实照片 | 真机拍摄 | +| `05_settings.png` | 设置页 | 真机截图 | +| `06_analysis.png` | 分析页(含底部测量面板) | 真机截图 | +| `07_gallery.png` | 相册 | 真机截图 | + +**为什么实时界面用绘制图而不是截图**:实时界面的 OSD 文字会按握持角度预旋转 +(这是用户要的可读性设计),手册拍摄时手机是横放的,截图里全部文字都是侧躺的、 +顶部/底部栏也一样,放进手册无法阅读。因此实时页改为自绘示意图(控件位置与真实 +布局一致),其余界面文字本来就是水平的,直接用真机截图。 + +图由 `manual_images/MakeFigures.java` 生成(截图裁剪缩放、标记解剖放大与标注、 +示意图绘制),界面改动后重跑即可,避免手册里的图与实物不一致。原始截图存为 +JPEG(`*_raw.jpg`,PNG 转 JPEG 省下约 8MB)作为可再生的输入。 + +### 3) README 同步 + +阅读顺序加入操作手册,仓库结构表更新,里程碑补记 2026-09-11~12 的三个根因级修复 +(launchMode / SET_CONFIGURATION / 软件画布卡顿 6.5→15.1fps)与同期完成项。 + +105 项测试全绿;本轮提交后 push 到 origin(用户明确要求)。 + ## 用户反馈修复 第二十四轮(2026-09-12,横屏拍摄方向:文件跟随握持角度) 用户反馈:"切到横屏拍照片和视频方向又不对了"。