docs+analysis: independent verification guide for phases A-F (calibrated scanners, self-tested commands)
This commit is contained in:
@@ -6,8 +6,28 @@
|
||||
|
||||
**`libcxsdk.so` 里没有静态调色板表**。12 个调色板全部由
|
||||
`CFunctions::SetColorPalette`(Ghidra 偏移 `0x00026c70`)在**运行时用算术生成**。
|
||||
对全文件扫描"连续 256 个 u32LE、alpha=0xFF 且 RGB 非零"的候选 **命中 0 个**
|
||||
(alpha=0x00 形式、BGR/RGB 三元组形式同样为 0)。
|
||||
|
||||
两层证据(2026-09-11 复核时补强,见"否证的最强形式"):
|
||||
|
||||
1. **正对照(决定性)**:项目持有的唯一官方表——铁虹——
|
||||
(csdk `src/mag160c_official_palette256.h`,源自厂商运行时内存
|
||||
`CoreSDKLib dev+0xb18`)在库里**以任何编码都搜不到**:
|
||||
- `B,G,R,0`(官方文档布局)→ 无
|
||||
- `B,G,R,0xFF`(alpha 强制)→ 无
|
||||
- **忽略第 4 字节的 `B,G,R` 宽松匹配 → 也无**
|
||||
|
||||
⚠️ **踩坑记录**:该表在内存里是 `(B, G, R, 0)` —— **第 4 字节是 0**,
|
||||
而 `OfficialTables.kt` 里的 ARGB int 被生成脚本强制 `alpha=0xFF`。
|
||||
因此"直接拿 Kotlin 表去二进制里搜"**永远搜不到**,会得出假的"未找到"。
|
||||
正确的针必须从 csdk 头文件(真实内存布局)构造。核查时务必用
|
||||
`analysis/tools/PalScan.java`(已按此修正)而非手工拼的 ARGB 针。
|
||||
2. **结构性证据**:Ghidra 反编译显示该函数用算术逐条生成全部 12 张表
|
||||
(`libcxsdk_decomp.txt` 的 case 0..10/12/13),所以静态数据里本就不该有。
|
||||
|
||||
> 另:早期"扫描 256 个连续 u32LE、alpha=0xFF"的判据命中 0,这一条仍成立但
|
||||
> **说服力弱**——它只说明"没有 alpha=0xFF 形式",与布局无关。
|
||||
> 通用"渐变"启发式(`PalScan.java` 的 [C] 段)在本库上误报约 11% 的位置
|
||||
> (全是指令/整数数组),**不能作为证据**,工具里已明确如此标注。
|
||||
|
||||
因此本阶段改用**移植生成函数**的路线并成功恢复:
|
||||
**UI 索引 0..10 共 11 个调色板已为官方精确表**(索引 11 见下),
|
||||
@@ -62,22 +82,38 @@
|
||||
| `analysis/sdk_re/android_app/palette_candidates.json` | 扫描结论(静态候选为空)+ 全部恢复表 |
|
||||
| `analysis/sdk_re/android_app/palette_candidates.png` | 12 组「官方预览图 + 恢复色条」对照图 |
|
||||
| `analysis/sdk_re/android_app/palette_match_report.txt` | 覆盖率证据表 |
|
||||
| `analysis/tools/extract_palettes.py` | 计划要求的纯标准库扫描脚本(可复现"无静态表"这一否定结论) |
|
||||
| `analysis/tools/extract_palettes.py` | 计划要求的纯标准库扫描脚本(本机无 python3,改用下面的 JDK 版) |
|
||||
| `analysis/tools/PalScan.java` | **JDK 版否证复核器**:正对照(铁虹在 3 种编码下的精确匹配)+ 自校准 + 通用扫描(明确标注不可作证据)。已实测:3 种编码全部 not found |
|
||||
| `analysis/tools/PalIdentify.java` | 13 个生成体的机械移植(含写地址校验) |
|
||||
| `analysis/tools/PalExport2.java` | 导出 Kotlin/JSON/PNG/报告 |
|
||||
| `analysis/tools/PalExport2.java` | 导出 Kotlin/JSON/PNG/报告(须与 PalIdentify 一起编译) |
|
||||
| `analysis/tools/CheckStrings.java` | 核对真机清单里的日志串是否真能由源码产生 |
|
||||
| `android/.../core/VendorPalettes.kt` | 生成的 Kotlin 表(0..10 为精确表) |
|
||||
|
||||
重跑(本机无 python3,Java 版单文件即可运行):
|
||||
重跑(本机无 python3;两个命令都已实测通过):
|
||||
|
||||
```bash
|
||||
java analysis/tools/PalExport2.java \
|
||||
android/app/src/main/kotlin/com/mag160c/thermal/core/OfficialTables.kt \
|
||||
<解包APK的 res/mipmap-hdpi-v4 目录> \
|
||||
android/app/src/main/kotlin/com/mag160c/thermal/core \
|
||||
analysis/sdk_re/android_app
|
||||
# 否证复核:铁虹是否真的不在库里(决定性证据)
|
||||
java analysis/tools/PalScan.java \
|
||||
analysis/sdk_re/android_app/bin/libcxsdk.so \
|
||||
csdk/src/mag160c_official_palette256.h
|
||||
|
||||
# 生成物可复现:重新生成后与仓库文件 diff 应为空
|
||||
mkdir -p /c/Users/zxc/AppData/Local/Temp/regen_build && \
|
||||
cd /c/Users/zxc/AppData/Local/Temp/regen_build && \
|
||||
cp "C:\Project\MAG160C\analysis\tools\PalIdentify.java" . && \
|
||||
cp "C:\Project\MAG160C\analysis\tools\PalExport2.java" . && \
|
||||
"C:\Tools\jdk-21\bin\javac" -d out PalIdentify.java PalExport2.java && \
|
||||
"C:\Tools\jdk-21\bin\java" -cp out PalExport2 \
|
||||
"C:\Project\MAG160C\android\app\src\main\kotlin\com\mag160c\thermal\core\OfficialTables.kt" \
|
||||
"C:\Users\zxc\AppData\Local\Temp\cxres\res\mipmap-hdpi-v4" \
|
||||
"C:\Users\zxc\AppData\Local\Temp\regen_core" \
|
||||
"C:\Users\zxc\AppData\Local\Temp\regen_out"
|
||||
diff "C:\Users\zxc\AppData\Local\Temp\regen_core\VendorPalettes.kt" \
|
||||
"C:\Project\MAG160C\android\app\src\main\kotlin\com\mag160c\thermal\core\VendorPalettes.kt"
|
||||
# 实测:diff 为空;铁虹锚点 256/256
|
||||
```
|
||||
|
||||
有 python3 的机器上:
|
||||
有 python3 的机器上(可选,仅证明"无静态 256 项 alpha=0xFF 表"这个弱结论):
|
||||
|
||||
```bash
|
||||
python3 analysis/tools/extract_palettes.py \
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.regex.*;
|
||||
import java.util.stream.*;
|
||||
|
||||
/**
|
||||
* Cross-check the quoted DebugLog lines in docs/android_app/real_device_checklist.md
|
||||
* against the Kotlin sources, so a verifier can confirm the checklist describes
|
||||
* logs the code can actually produce.
|
||||
*
|
||||
* WHY NOT A PLAIN SEARCH: the sources build messages by interpolation
|
||||
* ("$name write=$n/${packet.size}") while the checklist shows instantiated
|
||||
* examples ("GetParameter1 write=4/4"). A literal search therefore produces
|
||||
* false negatives. This tool instead (1) tries the literal with <placeholders>
|
||||
* removed, then (2) falls back to requiring the remaining identifier-ish words
|
||||
* to appear somewhere in the sources.
|
||||
*
|
||||
* A "needs review" line means the whole message is instance data (the template
|
||||
* lives in one string literal and the command name is a call-site argument) —
|
||||
* those must be traced by hand to the call site. Both categories were manually
|
||||
* resolved for the 2026-09-11 run (see verification_guide.md §3 Phase A).
|
||||
*
|
||||
* Usage:
|
||||
* java CheckStrings.java docs/android_app/real_device_checklist.md android/app/src/main
|
||||
*/
|
||||
public class CheckStrings {
|
||||
public static void main(String[] a) throws Exception {
|
||||
String md = Files.readString(Paths.get(a[0]), java.nio.charset.StandardCharsets.UTF_8);
|
||||
StringBuilder src = new StringBuilder();
|
||||
try (Stream<Path> s = Files.walk(Paths.get(a[1]))) {
|
||||
List<Path> kt = s.filter(x -> x.toString().endsWith(".kt")).collect(Collectors.toList());
|
||||
for (Path p : kt) src.append(Files.readString(p, java.nio.charset.StandardCharsets.UTF_8)).append('\n');
|
||||
}
|
||||
String all = src.toString();
|
||||
|
||||
// only lines that show a DebugLog tag: [usb] [vm] [session] [cmd] [stream] [pip] [remote] [crash]
|
||||
Matcher m = Pattern.compile("`(\\[(?:usb|vm|session|cmd|stream|pip|remote|crash)\\][^`]*)`").matcher(md);
|
||||
int exact = 0, skeleton = 0, miss = 0;
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
List<String> misses = new ArrayList<>();
|
||||
|
||||
while (m.find()) {
|
||||
String line = m.group(1);
|
||||
if (!seen.add(line)) continue;
|
||||
// the tag is a separate DebugLog argument in the source
|
||||
String msg = line.replaceFirst("^\\[[a-z]+\\]\\s*", "");
|
||||
|
||||
String literal = msg.replaceAll("<[^>]*>", "").trim();
|
||||
if (literal.length() >= 6 && all.contains(literal)) { exact++; continue; }
|
||||
|
||||
String skel = msg
|
||||
.replaceAll("<[^>]*>", " ")
|
||||
.replaceAll("0x[0-9A-Fa-f]+", " ")
|
||||
.replaceAll("\\b\\d+(\\.\\d+)?\\b", " ")
|
||||
.replaceAll("[A-Za-z]*\\d[A-Za-z0-9]*", " ")
|
||||
.replaceAll("[^\\x20-\\x7E\\u4e00-\\u9fff]+", " ");
|
||||
List<String> keep = new ArrayList<>();
|
||||
for (String w : skel.trim().split("\\s+")) {
|
||||
if (w.length() >= 3 && w.matches("[A-Za-z_:.\\-\\[\\]()/]+")) keep.add(w);
|
||||
}
|
||||
boolean found = !keep.isEmpty();
|
||||
for (String w : keep) if (!all.contains(w)) found = false;
|
||||
if (found) skeleton++;
|
||||
else { miss++; misses.add(line + " [probe: " + String.join(" ", keep) + "]"); }
|
||||
}
|
||||
|
||||
System.out.println("exact literal match : " + exact);
|
||||
System.out.println("skeleton match : " + skeleton);
|
||||
System.out.println("needs human review : " + miss);
|
||||
if (!misses.isEmpty()) {
|
||||
System.out.println();
|
||||
for (String s : misses) System.out.println(" ? " + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Independent check of the Phase C negative claim: "libcxsdk.so contains no
|
||||
* static palette tables."
|
||||
*
|
||||
* The canonical scanner is analysis/tools/extract_palettes.py, but this machine
|
||||
* has no working python3, so this JDK equivalent lets a verifier re-run the scan.
|
||||
*
|
||||
* The tool reports three things and is explicit about how much each one proves.
|
||||
*
|
||||
* [A] DETECTOR SANITY -- the known iron-bow table is planted into a synthetic
|
||||
* buffer and the broad scan must find it. This only proves the scan is not
|
||||
* blind; it proves nothing about the library.
|
||||
*
|
||||
* [B] POSITIVE CONTROL (the decisive part) -- the same table is searched for
|
||||
* verbatim in the library under the DOCUMENTED byte layout. This matters:
|
||||
* csdk/src/mag160c_official_palette256.h records the vendor table as
|
||||
* "256 x 4 bytes (B, G, R, 0)" — the 4th byte is ZERO, while the ARGB ints
|
||||
* in OfficialTables.kt have alpha forced to 0xFF by the generator, so
|
||||
* searching the Kotlin ints directly can never match the binary. The tool
|
||||
* therefore rebuilds the needle from the csdk header (ground truth) and
|
||||
* additionally tries an RGB-only match that ignores the 4th byte.
|
||||
* Iron-bow is the one vendor palette the project holds (captured at runtime
|
||||
* from CoreSDKLib dev+0xb18, verified pixel-exact against the official
|
||||
* renderer); if this library stored static tables of that family, it would
|
||||
* be among them. Absence in every encoding is therefore strong evidence
|
||||
* for the negative claim.
|
||||
*
|
||||
* [C] BROAD SCAN (reported, but NOT usable as evidence) -- a generic "256-entry
|
||||
* colour ramp" search. Measured on this library it flags ~11% of all
|
||||
* positions, all of them ARM32 code where the 4th byte of each word happens
|
||||
* to be constant and adjacent words differ little. A generic ramp test
|
||||
* cannot separate palettes from integer/address arrays, so this count is
|
||||
* deliberately reported with its false-positive density instead of being
|
||||
* presented as a verdict.
|
||||
*
|
||||
* The negative claim therefore rests on [B] plus the independent structural
|
||||
* evidence: the Ghidra listing shows CFunctions::SetColorPalette COMPUTING all
|
||||
* twelve tables with arithmetic (analysis/sdk_re/android_app/libcxsdk_decomp.txt),
|
||||
* which is why no static data can be found.
|
||||
*
|
||||
* Usage:
|
||||
* java PalScan.java analysis/sdk_re/android_app/bin/libcxsdk.so \
|
||||
* csdk/src/mag160c_official_palette256.h
|
||||
*/
|
||||
public class PalScan {
|
||||
static final int N = 256;
|
||||
static final int STEP_LIMIT = 0x200000; // max per-step change
|
||||
static final int CHANNEL_SPAN = 64; // a colour scale spans a channel
|
||||
static final int MIN_SPANNING_CHANNELS = 2;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String libPath = args[0];
|
||||
String headerPath = args.length > 1 ? args[1] : null;
|
||||
byte[] lib = Files.readAllBytes(Paths.get(libPath));
|
||||
System.out.println("file: " + libPath + " (" + lib.length + " bytes)");
|
||||
|
||||
if (headerPath == null) {
|
||||
System.out.println("(pass csdk/src/mag160c_official_palette256.h as the 2nd argument)");
|
||||
return;
|
||||
}
|
||||
// Ground truth for the in-binary layout: the csdk header holds the table
|
||||
// exactly as it appears in memory, (B, G, R, 0) per entry.
|
||||
int[][] rgb = readHeader(headerPath);
|
||||
if (rgb.length != N) {
|
||||
System.out.println("header parsed " + rgb.length + " entries, expected " + N + " - aborting");
|
||||
return;
|
||||
}
|
||||
System.out.println("ground-truth table: " + N + " entries (B,G,R,0), first 3 = "
|
||||
+ rgb[0][0] + "," + rgb[0][1] + "," + rgb[0][2] + " "
|
||||
+ rgb[1][0] + "," + rgb[1][1] + "," + rgb[1][2] + " "
|
||||
+ rgb[2][0] + "," + rgb[2][1] + "," + rgb[2][2]);
|
||||
|
||||
// ---------- [A] detector sanity ----------
|
||||
byte[] planted = encodeHeader(rgb, false);
|
||||
int plantedHits = scan(planted).size();
|
||||
System.out.println();
|
||||
System.out.println("[A] detector sanity: planted the known table into a 1024-byte buffer");
|
||||
System.out.println(" detected: " + plantedHits + " candidate(s)"
|
||||
+ (plantedHits == 0 ? " <-- SCAN IS BLIND, [C] is meaningless" : " (scan is not blind)"));
|
||||
|
||||
// ---------- [B] positive control ----------
|
||||
System.out.println();
|
||||
System.out.println("[B] POSITIVE CONTROL - is the known vendor table stored verbatim?");
|
||||
int exact = indexOf(lib, encodeHeader(rgb, false), 0);
|
||||
System.out.println(" B,G,R,0 (documented layout) : "
|
||||
+ (exact < 0 ? "not found" : "*** FOUND at 0x" + Integer.toHexString(exact) + " ***"));
|
||||
int exactFF = indexOf(lib, encodeHeader(rgb, true), 0);
|
||||
System.out.println(" B,G,R,0xFF (alpha forced) : "
|
||||
+ (exactFF < 0 ? "not found" : "*** FOUND at 0x" + Integer.toHexString(exactFF) + " ***"));
|
||||
int rgbOnly = matchRgbIgnoringFourth(lib, rgb);
|
||||
System.out.println(" B,G,R ignoring 4th byte : "
|
||||
+ (rgbOnly < 0 ? "not found" : "*** FOUND at 0x" + Integer.toHexString(rgbOnly) + " ***"));
|
||||
|
||||
// ---------- [C] broad scan ----------
|
||||
System.out.println();
|
||||
System.out.println("[C] broad scan for any 256-entry colour ramp (supporting only):");
|
||||
List<Integer> hits = scan(lib);
|
||||
int positions = (lib.length - N * 4) / 4;
|
||||
System.out.printf(" candidates: %d of %d positions (%.1f%%)%n",
|
||||
hits.size(), positions, 100.0 * hits.size() / Math.max(1, positions));
|
||||
if (!hits.isEmpty()) {
|
||||
System.out.println(" These are ARM32 code regions: the ramp test cannot tell a colour");
|
||||
System.out.println(" scale from a run of small integers, so this count is NOT evidence.");
|
||||
}
|
||||
|
||||
// ---------- verdict ----------
|
||||
System.out.println();
|
||||
boolean found = exact >= 0 || exactFF >= 0 || rgbOnly >= 0;
|
||||
if (found) {
|
||||
System.out.println("VERDICT: a static copy of the vendor table IS present -> the negative claim is WRONG.");
|
||||
} else {
|
||||
System.out.println("VERDICT: the known vendor table is absent in every encoding tested.");
|
||||
System.out.println(" Together with the Ghidra listing (CFunctions::SetColorPalette computes");
|
||||
System.out.println(" the tables arithmetically) this upholds the negative claim:");
|
||||
System.out.println(" libcxsdk.so stores no static palette tables.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse csdk/src/mag160c_official_palette256.h into {B,G,R} triples. */
|
||||
static int[][] readHeader(String path) throws Exception {
|
||||
List<int[]> rows = new ArrayList<>();
|
||||
for (String line : Files.readAllLines(Paths.get(path), java.nio.charset.StandardCharsets.UTF_8)) {
|
||||
if (!line.contains("{") || !line.contains("}")) continue;
|
||||
String body = line.substring(line.indexOf('{') + 1, line.indexOf('}'));
|
||||
String[] parts = body.split(",");
|
||||
if (parts.length < 3) continue;
|
||||
try {
|
||||
rows.add(new int[]{
|
||||
Integer.parseInt(parts[0].trim()),
|
||||
Integer.parseInt(parts[1].trim()),
|
||||
Integer.parseInt(parts[2].trim()),
|
||||
});
|
||||
} catch (NumberFormatException e) {
|
||||
// header/trailer lines
|
||||
}
|
||||
}
|
||||
return rows.toArray(new int[0][]);
|
||||
}
|
||||
|
||||
static byte[] encodeHeader(int[][] rgb, boolean alphaFF) {
|
||||
byte[] out = new byte[rgb.length * 4];
|
||||
for (int i = 0; i < rgb.length; i++) {
|
||||
out[i*4] = (byte) rgb[i][0];
|
||||
out[i*4+1] = (byte) rgb[i][1];
|
||||
out[i*4+2] = (byte) rgb[i][2];
|
||||
out[i*4+3] = (byte) (alphaFF ? 0xFF : 0x00);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Match all 256 (B,G,R) triples at 4-byte stride, ignoring the 4th byte. */
|
||||
static int matchRgbIgnoringFourth(byte[] data, int[][] rgb) {
|
||||
for (int o = 0; o + rgb.length * 4 <= data.length; o += 4) {
|
||||
boolean ok = true;
|
||||
for (int i = 0; i < rgb.length && ok; i++) {
|
||||
int p = o + i * 4;
|
||||
if ((data[p] & 0xFF) != rgb[i][0]
|
||||
|| (data[p+1] & 0xFF) != rgb[i][1]
|
||||
|| (data[p+2] & 0xFF) != rgb[i][2]) ok = false;
|
||||
}
|
||||
if (ok) return o;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** All offsets whose 1024-byte window passes the ramp test. */
|
||||
static List<Integer> scan(byte[] b) {
|
||||
List<Integer> out = new ArrayList<>();
|
||||
for (int o = 0; o + N * 4 <= b.length; o += 4) if (isPaletteRun(b, o)) out.add(o);
|
||||
return out;
|
||||
}
|
||||
|
||||
static boolean isPaletteRun(byte[] b, int o) {
|
||||
int alpha = b[o + 3] & 0xFF;
|
||||
int prev = -1, distinct = 0;
|
||||
int[] mn = {255, 255, 255}, mx = {0, 0, 0};
|
||||
for (int i = 0; i < N; i++) {
|
||||
int p = o + i * 4;
|
||||
if ((b[p + 3] & 0xFF) != alpha) return false; // unused byte constant
|
||||
int bl = b[p] & 0xFF, g = b[p + 1] & 0xFF, r = b[p + 2] & 0xFF;
|
||||
int v = bl | (g << 8) | (r << 16);
|
||||
if (prev >= 0 && Math.abs(v - prev) > STEP_LIMIT) return false;
|
||||
if (v != prev) distinct++;
|
||||
if (bl < mn[0]) mn[0] = bl; if (bl > mx[0]) mx[0] = bl;
|
||||
if (g < mn[1]) mn[1] = g; if (g > mx[1]) mx[1] = g;
|
||||
if (r < mn[2]) mn[2] = r; if (r > mx[2]) mx[2] = r;
|
||||
prev = v;
|
||||
}
|
||||
if (distinct < 32) return false;
|
||||
int spanning = 0;
|
||||
for (int c = 0; c < 3; c++) if (mx[c] - mn[c] >= CHANNEL_SPAN) spanning++;
|
||||
return spanning >= MIN_SPANNING_CHANNELS;
|
||||
}
|
||||
|
||||
static byte[] encode(int[] anchor, int order) {
|
||||
byte[] out = new byte[anchor.length * 4];
|
||||
for (int i = 0; i < anchor.length; i++) {
|
||||
int v = anchor[i];
|
||||
int r = (v >> 16) & 0xFF, g = (v >> 8) & 0xFF, bl = v & 0xFF, al = (v >>> 24) & 0xFF;
|
||||
switch (order) {
|
||||
case 0: putLE(out, i * 4, v); break;
|
||||
case 1: putBE(out, i * 4, v); break;
|
||||
case 2: out[i*4]=(byte) bl; out[i*4+1]=(byte) g; out[i*4+2]=(byte) r; out[i*4+3]=(byte) al; break;
|
||||
case 3: out[i*4]=(byte) r; out[i*4+1]=(byte) g; out[i*4+2]=(byte) bl; out[i*4+3]=(byte) al; break;
|
||||
case 4: out[i*4]=0; out[i*4+1]=(byte) bl; out[i*4+2]=(byte) g; out[i*4+3]=(byte) r; break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static void putLE(byte[] o, int p, int v) {
|
||||
o[p]=(byte)(v&0xFF); o[p+1]=(byte)((v>>8)&0xFF); o[p+2]=(byte)((v>>16)&0xFF); o[p+3]=(byte)((v>>>24)&0xFF);
|
||||
}
|
||||
|
||||
static void putBE(byte[] o, int p, int v) {
|
||||
o[p]=(byte)((v>>>24)&0xFF); o[p+1]=(byte)((v>>16)&0xFF); o[p+2]=(byte)((v>>8)&0xFF); o[p+3]=(byte)(v&0xFF);
|
||||
}
|
||||
|
||||
static int indexOf(byte[] hay, byte[] needle, int from) {
|
||||
outer:
|
||||
for (int i = from; i + needle.length <= hay.length; i++) {
|
||||
for (int j = 0; j < needle.length; j++) if (hay[i + j] != needle[j]) continue outer;
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int[] readAnchor(String ktPath) throws Exception {
|
||||
String src = Files.readString(Paths.get(ktPath), java.nio.charset.StandardCharsets.UTF_8);
|
||||
int k = src.indexOf("val PALETTE256_ARGB = intArrayOf(");
|
||||
int start = k + "val PALETTE256_ARGB = intArrayOf(".length();
|
||||
int end = src.indexOf(")", start);
|
||||
List<Integer> vals = new ArrayList<>();
|
||||
StringBuilder num = new StringBuilder();
|
||||
for (char c : src.substring(start, end).toCharArray()) {
|
||||
if (c == '-' || (c >= '0' && c <= '9')) num.append(c);
|
||||
else if (num.length() > 0) { vals.add(Integer.parseInt(num.toString())); num.setLength(0); }
|
||||
}
|
||||
return vals.stream().mapToInt(Integer::intValue).toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
# 核查指南 —— Phase A→F 执行轮(交给核查 AI 的入口)
|
||||
|
||||
> **核查对象**:commit `06c1f30..8e1312a`(7 个提交,基线 `087e15c`),
|
||||
> 位于 `C:\Project\MAG160C`,源码在 `android\`(Gradle,包 `com.mag160c.thermal`)。
|
||||
>
|
||||
> **第一原则:不要相信执行模型的总结与文档叙述。** 所有结论必须来自
|
||||
> ① git 提交内容 ② 仓库内的官方逆向产物(可独立复核)③ 亲自重跑的脚本/构建
|
||||
> ④ 硬件实测。执行模型(下称"执行者")的文档只当"待验证的声明"处理。
|
||||
>
|
||||
> 本文件给出一条可直接粘贴的起始指令、阅读顺序、三道必过关口、
|
||||
> 逐阶段核查方法、**执行者主动交底的高风险项**,以及本机无法验证的边界。
|
||||
|
||||
---
|
||||
|
||||
## 0. 可直接粘贴给核查 AI 的起始指令
|
||||
|
||||
```
|
||||
你是本项目的独立核查者。项目根 C:\Project\MAG160C(git 仓库),
|
||||
核查范围是提交 06c1f30..8e1312a(7 个提交,基线 087e15c),
|
||||
任务是把 docs/android_app/execution_plan.md 中 Phase A→F 的每项声明
|
||||
独立复核一遍并对每条给出 证实/证伪/无法判定 的结论。
|
||||
|
||||
铁律:
|
||||
1. 不要采信执行者写的总结、session_state、checklist 的任何叙述,
|
||||
它们只是待验证声明;结论必须来自代码、官方逆向产物、可复现命令。
|
||||
2. 先读 docs/android_app/verification_guide.md(核查指南),按它的
|
||||
三道关口顺序执行;第一道不通过就停止并报告。
|
||||
3. 不要修改任何源码或提交;你是只读核查者。发现问题只报告,不修复。
|
||||
4. 无法在本机验证的(需真机/双设备)必须显式标注"无法判定",不要默认通过。
|
||||
5. 执行者的构建命令(Windows Git Bash):
|
||||
cd /c/Project/MAG160C/android && export JAVA_HOME="C:\\Tools\\jdk-21" && \
|
||||
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:assembleDebug test --no-daemon"
|
||||
(本机无 python3、无模拟器、无连接设备;JDK/SDK 见 android/local.properties)
|
||||
最后按"阶段 → 声明 → 方法 → 结论 → 反例(若证伪)"输出报告。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 阅读顺序(先建立判据,再看实现)
|
||||
|
||||
1. `docs/android_app/execution_plan.md` —— **验收判据来源**。特别是 §0 全局约束
|
||||
(禁改清单、构建命令、提交格式)与各阶段的"精确规格"。核查就是"拿计划对实现"。
|
||||
2. `docs/android_app/verification_guide.md`(本文件)—— 核查路线。
|
||||
3. `git log --oneline 087e15c..HEAD` 与每个提交的 `git show --stat` —— 改动边界。
|
||||
4. `docs/android_app/session_state.md` 的"执行计划进度""本轮总结""待办"——
|
||||
执行者的自述(**待验证**)。
|
||||
5. `docs/android_app/real_device_checklist.md` —— 24 步真机清单(**待验证**,
|
||||
见 §4.9:其中第 15 步的预期与实现可能不符)。
|
||||
6. 官方逆向产物(判据,非执行者产出):
|
||||
- `analysis/magcx_official_flow.md`(协议权威)
|
||||
- `analysis/sdk_re/android_app/jadx_magcx/`(官方普通版 Java 源码)
|
||||
- `analysis/sdk_re/android_app/libcxsdk_decomp.txt`(Ghidra 伪代码)
|
||||
- `analysis/sdk_re/android_app/palette_extraction_findings.md`(Phase C 结论)
|
||||
|
||||
判据优先级:**官方产物 > 计划文档 > 执行者文档**。三者冲突时以官方产物为准,
|
||||
并指出冲突本身。
|
||||
|
||||
---
|
||||
|
||||
## 2. 第一关:硬门槛(不通过则停止核查)
|
||||
|
||||
### 2.1 git 完整性
|
||||
|
||||
```bash
|
||||
cd /c/Project/MAG160C
|
||||
git status --short # 期望:无输出(工作树干净)
|
||||
git log --oneline 087e15c..HEAD # 期望:恰好 7 条,见下表
|
||||
git log --oneline -1 origin/main # 期望:087e15c(**未 push**,符合用户指令)
|
||||
```
|
||||
|
||||
期望的提交与消息(消息须与执行计划 §各阶段"提交信息"**逐字一致**):
|
||||
|
||||
| 序 | hash | 消息(计划要求) | 阶段 |
|
||||
|---|---|---|---|
|
||||
| 1 | 06c1f30 | `android: lifetime query + cali consistency check + real-device checklist` | A |
|
||||
| 2 | 656d419 | `android: MDT temperature decode + analyzer probe UI` | B |
|
||||
| 3 | b7a928e | `android: vendor palette extraction tooling (+exact tables if found)` | C |
|
||||
| 4 | c34940e | `android: cloud module scaffold (retrofit, opt-in, disabled by default)` | D |
|
||||
| 5 | f8b3200 | `android: visible-light PIP overlay (camera2, draggable, 3 sizes)` | E |
|
||||
| 6 | 512508e | `android: LAN remote preview (UDP discovery + raw-frame TCP stream, client-side rendering)` | F |
|
||||
| 7 | 8e1312a | `android: phase Z wrap-up (...)`(Z 无强制消息,内容须仅为文档/清单/APK) | Z |
|
||||
|
||||
**反例**:条数≠7;消息被改写;Z 里混入功能代码改动。
|
||||
|
||||
### 2.2 禁改清单(执行计划 §0 第 1 条)
|
||||
|
||||
逐条独立验证,不要相信"执行者说没改":
|
||||
|
||||
```bash
|
||||
# ① 竖屏锁定:源码 + 已构建 APK 双重确认
|
||||
git diff 087e15c..HEAD -- android/app/src/main/AndroidManifest.xml | grep -i orientation
|
||||
# 期望:无 '-' 开头的行(只有新增注释/权限行)
|
||||
"C:\Tools\android-sdk\build-tools\36.0.0\aapt2.exe" dump xmltree \
|
||||
build-artifacts/mag160c-app-debug.apk --file AndroidManifest.xml | grep -i screenOrientation
|
||||
# 期望:screenOrientation(0x0101001e)=1 (1 = portrait)
|
||||
|
||||
# ② 命令字节序锁:MagProtocolTest 与 cmd4/cmd8 实现都不得变
|
||||
git diff 087e15c..HEAD --stat -- android/app/src/test/kotlin/com/mag160c/thermal/usb/MagProtocolTest.kt
|
||||
git diff 087e15c..HEAD -- android/app/src/main/kotlin/com/mag160c/thermal/usb/MagProtocol.kt
|
||||
# 期望:测试文件无 diff;MagProtocol 仅新增 RSP_SEND_LIFETIME 常量
|
||||
|
||||
# ③ LiveRenderer 构图逻辑
|
||||
git diff 087e15c..HEAD -- android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveRenderer.kt
|
||||
# 期望:空
|
||||
|
||||
# ④ analysis/ 只读参考:已存在文件不得被改(新增文件允许)
|
||||
git diff 087e15c..HEAD --stat --diff-filter=M -- analysis/
|
||||
# 期望:空
|
||||
|
||||
# ⑤ IrSession 握手序列(A 阶段按要求加了代码,需人工确认序列未被改)
|
||||
git diff 087e15c..HEAD -- android/app/src/main/kotlin/com/mag160c/thermal/usb/IrSession.kt
|
||||
# 核查点:66b(GetParameter1)→66c→66f→[670]→673 顺序、800ms 超时、
|
||||
# 前半段失败即 abort/回退 的行为均未被改动;删除行应仅有两处
|
||||
# (cache.readBytes() 改为先比 MD5;stats 行末尾追加 lifetime=)
|
||||
```
|
||||
|
||||
**反例**:APK 里 screenOrientation≠1;MagProtocolTest 有改动;
|
||||
LiveRenderer 有 diff;`--diff-filter=M` 的 analysis/ 输出非空。
|
||||
|
||||
### 2.3 构建与测试(全绿是计划的硬性验收)
|
||||
|
||||
```bash
|
||||
cd /c/Project/MAG160C/android && export JAVA_HOME="C:\\Tools\\jdk-21"
|
||||
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:assembleDebug :app:assembleRelease test --no-daemon"
|
||||
# 期望:BUILD SUCCESSFUL(debug + release(R8) + 单测)
|
||||
```
|
||||
|
||||
单测计数须可复现(执行者报告为 44/0/0):
|
||||
|
||||
```bash
|
||||
cd /c/Project/MAG160C
|
||||
grep -h -o 'tests="[0-9]*" skipped="[0-9]*" failures="[0-9]*" errors="[0-9]*"' \
|
||||
android/app/build/test-results/testDebugUnitTest/*.xml \
|
||||
| awk -F'"' '{t+=$2;f+=$6;e+=$8} END {print "tests="t" failures="f" errors="e}'
|
||||
```
|
||||
|
||||
期望的逐类分布(缺一类即说明有用例被静默跳过):
|
||||
|
||||
| 测试类 | 用例数 | 覆盖 |
|
||||
|---|---|---|
|
||||
| usb.MagProtocolTest | 3 | 命令字节序(既有,未改) |
|
||||
| core.RenderPipelineTest | 3 | 与 C 参考逐字节一致(既有) |
|
||||
| core.TempMathTest | 5 | Phase B 温度图 |
|
||||
| core.PalettesTest | 8 | Phase C 调色板 |
|
||||
| media.MdtTest | 6 | Phase B MDT 容器 |
|
||||
| cloud.CloudClientTest | 4 | Phase D 默认关闭契约 |
|
||||
| net.RemoteContractTest | 12 | Phase F 协议/粘包/截断 |
|
||||
| net.RemoteLoopbackTest | 3 | Phase F **真实 TCP 回环端到端** |
|
||||
|
||||
**重要**:`RenderPipelineTest.matchesCReferencePixelExact` 必须仍然通过——
|
||||
它是整个渲染管线正确性的地基;若它挂了,后面所有阶段都无意义。
|
||||
|
||||
---
|
||||
|
||||
## 3. 第二关:逐阶段声明 → 核查方法
|
||||
|
||||
### Phase A(06c1f30)
|
||||
|
||||
| 声明 | 核查方法 | 通过判据 / 反例 |
|
||||
|---|---|---|
|
||||
| `RSP_SEND_LIFETIME = 0x5BB5B561` 等于官方 `D2P_SendLifeTime` | 读 `jadx_magcx/\cn\com\magnity\magnitycx\sdk\D2PCmd.java` 的十进制值,手算十六进制 | =1538635105;反例:数值不符 |
|
||||
| `CMD_GET_LIFETIME` 未被改动 | 同上(P2DCmd.java)对照 | =1807136373=0x6BB6B675 |
|
||||
| lifetime 失败不阻断连接 | 读 `IrSession.startInternal` 第 3b 步 | 失败仅 `DebugLog.log`,无 return/abort |
|
||||
| cali 缓存 MD5 对照**不改变**返回值 | 读 `obtainCali` cache-hit 分支 | 仍 return cached;日志文案 `cali cache vs bundled DDT: identical/differ` |
|
||||
| 心跳 stats 追加 lifetime | 读 `streamLoop` | 行尾 ` lifetime=${deviceLifetimeMs}` |
|
||||
| checklist 的日志串与代码一致 | 跑 `analysis/tools/CheckStrings.java`(见下),再人工追溯它标出的 7 条 | 无凭空编造的日志行 |
|
||||
|
||||
**checklist 日志串机械核对**(执行者已自测,核查者须独立重跑):
|
||||
|
||||
```bash
|
||||
cd /c/Project/MAG160C && \
|
||||
"C:\Tools\jdk-21\bin\java" analysis/tools/CheckStrings.java \
|
||||
docs/android_app/real_device_checklist.md android/app/src/main
|
||||
# 执行者 2026-09-11 实测:exact=16, skeleton=30, needs-review=7
|
||||
```
|
||||
|
||||
那 7 条**不是缺陷**,而是"整行都是实例数据"的情形(模板在一个字符串字面量里,
|
||||
命令名由调用点传入),工具无法自动匹配,必须人工追溯。执行者已逐条追溯完毕:
|
||||
|
||||
| 标出的行 | 源码出处(已核对) |
|
||||
|---|---|
|
||||
| `[cmd] GetParameter1 write=4/4` | `"$name write=$n/${packet.size}"` (IrSession.kt:418) + 调用点 `"GetParameter1"` (:185) |
|
||||
| `[cmd] GetParameter1 resp=0x5BB5B55B len=60 head=…` | `"$name resp=0x%08X len=%d head=%s"` (:433) + 同调用点 |
|
||||
| `[cmd] GetParameter2 resp=0x5BB5B55C len=…` | 同上模板 + 66c 调用点 (:192) |
|
||||
| `[cmd] BasePara1: serial=… devType=3 160x120 @15fps` | `"BasePara1: serial=$serial devType=$devType ${width}x$height @${fps}fps"` (:292) |
|
||||
| `[usb] ep 0x81 fail#… halted=…` | `"ep 0x%02X fail#$failureCount get_status rc=%d halted=%d clear_halt rc=%d"` (:408) |
|
||||
| `[cmd]`(空)与 `[crash]` | 正则产物,非清单条目 |
|
||||
|
||||
**核查者要做的是独立确认这张对应表**(尤其模板与调用点是否真能拼出清单示例),
|
||||
而不是重跑得到同样的 7 条后就放行。
|
||||
|
||||
> A 的 `obtainCali` 无单测(需 `UsbDeviceConnection`)。核查者应显式标注
|
||||
> "仅静态审查,无运行时证据"。
|
||||
|
||||
### Phase B(656d419)
|
||||
|
||||
| 声明 | 核查方法 | 通过判据 / 反例 |
|
||||
|---|---|---|
|
||||
| `Mdt.parse` 与 `compose` 互逆 | 读代码 + 跑 `MdtTest`;**另写独立用例**:构造含 EXIF 缩略图的 JPEG(内部含 `FF D9`)再 compose/parse | jpg 字节往返一致(`extractJpg` 反向搜索最后一个 FFD9,应取主图 EOI,不被缩略图干扰) |
|
||||
| text 去 NUL 填充 | 读 parse | `trimEnd('\u0000')` |
|
||||
| 温度图 = u16LE → `countsToTempMc` | 读 `TempMath.tempMapFromPixels`;**独立复算**:用 `csdk/src/mag160c_temp.c` 或已提交的 C 参考对同一 counts 算温度 | 与 `TempMathTest.mapMatchesScalarConversionPerPixel` 同值 |
|
||||
| 分析页探针映射为"直接映射"而非 90° 逆映射 | 读 `AnalyzeViewer`:画布 `aspectRatio(4/3)` 且 `drawImage` **无 canvas.rotate** | 判断成立则直接映射正确;反例:若实际绘制有旋转,则探针温度取错像素 |
|
||||
| 温度条固定在未缩放 fitRect 顶部 | 读 `drawTemperatureOsd`(传 `imageRect(size,1f,Offset.Zero)`) | 属**规格歧义**(计划原文"随 fitRect 走,不随手势矩阵"),核查者需判定取舍是否可接受 |
|
||||
|
||||
### Phase C(b7a928e)——本轮最需要深度复核的一相
|
||||
|
||||
| 声明 | 核查方法 | 通过判据 / 反例 |
|
||||
|---|---|---|
|
||||
| `libcxsdk.so` 无静态调色板表 | 跑 `analysis/tools/PalScan.java`(**决定性**:正对照);python 版仅作弱旁证 | 三种编码(`B,G,R,0` / `B,G,R,0xFF` / 忽略第 4 字节的 `B,G,R`)全部 not found |
|
||||
| 索引↔case 映射(0..10) | ①读 `libcxsdk_decomp.txt` 的 `CFunctions::SetColorPalette`,确认是 `switch(param_2)` 且 case 标签即调色板序号;②锚点:case 2 → `OfficialTables.PALETTE256_ARGB` 256/256;③预览图交叉验证 | ①②为结构性证据;③仅作旁证 |
|
||||
| 索引 11 未解决 | 检查 `VendorPalettes.SOURCE_CASE[11] == -1` 且 `Palettes.buildAll()[11]` 为近似曲线 | 与 findings 文档一致 |
|
||||
| 铁虹仍是官方表 | 读 `Palettes.officialIronbow()` 与 `PalettesTest.ironbowMatchesOfficialTables` | 返回 `OfficialTables.PALETTE256_ARGB` 本身 |
|
||||
|
||||
**否证复核 + 生成物可复现**(执行者已自测,核查者须独立重跑;完整命令见
|
||||
`analysis/sdk_re/android_app/palette_extraction_findings.md` 的"可复现产物"节):
|
||||
|
||||
```bash
|
||||
cd /c/Project/MAG160C
|
||||
# 决定性:官方铁虹表是否真的不在库里(3 种编码都要 not found)
|
||||
"C:\Tools\jdk-21\bin\java" analysis/tools/PalScan.java \
|
||||
analysis/sdk_re/android_app/bin/libcxsdk.so \
|
||||
csdk/src/mag160c_official_palette256.h
|
||||
|
||||
# 生成物可复现(两文件一起编译;重新生成后 diff 应为空)
|
||||
# 实测:VendorPalettes.kt 与 palette_candidates.json diff 均为空,锚点 256/256
|
||||
```
|
||||
|
||||
> ⚠️ **核查者必须避开的坑**:铁虹表在内存里的布局是 `(B, G, R, 0)`——**第 4 字节
|
||||
> 是 0**,而 `OfficialTables.kt` 的 ARGB int 被生成脚本强制 `alpha=0xFF`。
|
||||
> 所以"直接把 Kotlin 表当针去二进制里搜"**必然搜不到**,会得到假的"未找到"。
|
||||
> 针必须从 `csdk/src/mag160c_official_palette256.h`(真实内存布局)构造,
|
||||
> 或用已修正的 `PalScan.java`。执行者在第一版工具里就犯过这个错,已修正。
|
||||
>
|
||||
> 另:通用"256 项渐变"启发式扫描在本库上误报约 11% 的位置(全是指令/整数
|
||||
> 数组),`PalScan.java` 的 [C] 段已明确标注**不可作为证据**,不要引用该计数。
|
||||
|
||||
|
||||
|
||||
**重新生成并比对**(Phase C 最强核查手段;执行者已自测通过,**核查者须独立重跑**):
|
||||
|
||||
> 注意:`PalExport2.java` **不是**单文件可直跑——它依赖同一目录
|
||||
> `PalIdentify.java` 中的 `PalBody`(13 个生成体)。必须两文件一起编译。
|
||||
> 另外 Windows 版 java 不认 Git Bash 的 `/tmp` 路径,必须用 `C:\...` 形式。
|
||||
|
||||
```bash
|
||||
# ① 解包官方普通版的调色板预览图(交叉验证用)
|
||||
mkdir -p /c/Users/zxc/AppData/Local/Temp/cxres && \
|
||||
cd /c/Users/zxc/AppData/Local/Temp/cxres && \
|
||||
"C:\Tools\jdk-21\bin\jar" xf "C:\Project\MAG160C\app\【普通版】MAG-Cx.apk" res/mipmap-hdpi-v4/
|
||||
|
||||
# ② 两文件编译
|
||||
mkdir -p /c/Users/zxc/AppData/Local/Temp/regen_build && \
|
||||
cd /c/Users/zxc/AppData/Local/Temp/regen_build && \
|
||||
cp "C:\Project\MAG160C\analysis\tools\PalIdentify.java" . && \
|
||||
cp "C:\Project\MAG160C\analysis\tools\PalExport2.java" . && \
|
||||
"C:\Tools\jdk-21\bin\javac" -d out PalIdentify.java PalExport2.java
|
||||
|
||||
# ③ 重新生成(输出到临时目录,勿覆盖仓库)
|
||||
"C:\Tools\jdk-21\bin\java" -cp out PalExport2 \
|
||||
"C:\Project\MAG160C\android\app\src\main\kotlin\com\mag160c\thermal\core\OfficialTables.kt" \
|
||||
"C:\Users\zxc\AppData\Local\Temp\cxres\res\mipmap-hdpi-v4" \
|
||||
"C:\Users\zxc\AppData\Local\Temp\regen_core" \
|
||||
"C:\Users\zxc\AppData\Local\Temp\regen_out"
|
||||
# 期望最后一行:iron_bow vs OfficialTables anchor: 256/256
|
||||
|
||||
# ④ 与仓库文件逐字节比对(两条都必须为空)
|
||||
diff "C:\Users\zxc\AppData\Local\Temp\regen_core\VendorPalettes.kt" \
|
||||
"C:\Project\MAG160C\android\app\src\main\kotlin\com\mag160c\thermal\core\VendorPalettes.kt"
|
||||
diff "C:\Users\zxc\AppData\Local\Temp\regen_out\palette_candidates.json" \
|
||||
"C:\Project\MAG160C\analysis\sdk_re\android_app\palette_candidates.json"
|
||||
```
|
||||
|
||||
**执行者自测结果(2026-09-11,供核查者对照)**:上述四条命令全部成功,
|
||||
铁虹锚点 256/256,两份生成物 diff 均为空,即**从 .so 伪代码 → 生成体移植 →
|
||||
Kotlin 表**这条链完全可复现。这证明的是"移植实现与提交物一致",**不**证明
|
||||
"移植的语义与厂商二进制一致"(后者只有 case 2 锚点能证明)。
|
||||
|
||||
**执行者主动交底(务必复核)**:index 4/8/9(琥珀/喷射/红饱和)的映射在
|
||||
**两种独立度量下结论不一致**——像素加权的最近色距离倾向把"红饱和"的预览图
|
||||
判给灰阶表(case 0),而颜色集合覆盖率倾向 case 9。原因是该预览图近乎无彩
|
||||
(执行者测得 meanChroma≈9.7)。执行者最终采用了"结构性证据(`switch(param_2)`
|
||||
的 case 标签即调色板序号)+ case 2 锚点"的论证,预览图仅作旁证。**核查者应
|
||||
独立判定索引 9(以及 4、8)的映射是否成立**;更强证据需反解 ARM32 跳表
|
||||
(文件偏移 0x26cd2 附近,执行者尝试未成功)或真机抓帧比对。
|
||||
|
||||
可复现的"红热预览图是占位副本"检查(支撑索引 11 未解决的结论):
|
||||
|
||||
```bash
|
||||
# palette_red_hot.png 与 palette_white_hot.png:11914 个不透明像素中 11777 个完全相同
|
||||
# 可用任意图像工具逐像素比对(执行者用自写 Java 工具测得 meanAbsDiff=0.5/255)
|
||||
```
|
||||
|
||||
### Phase D(c34940e)
|
||||
|
||||
| 声明 | 核查方法 | 通过判据 / 反例 |
|
||||
|---|---|---|
|
||||
| Retrofit 2.11.0 已接入 | 读 `gradle/libs.versions.toml`、`app/build.gradle.kts` | 两处均在 |
|
||||
| 默认关闭 | 读 `AppSettings.cloudEnabled`(默认 false);跑 `CloudClientTest` | 4 项全绿 |
|
||||
| **任何生产代码路径都不会静默联网** | `grep -rn "CloudClient" android/app/src/main` | 只应有 AppSettings 的 setEnabled 与声明;**反例**:任何 `api()` 调用点 |
|
||||
| release 混淆不破 | 跑 `assembleRelease` | BUILD SUCCESSFUL |
|
||||
| PROGUARD 规则存在 | 读 `app/proguard-rules.pro` | 含 `-keep class com.mag160c.thermal.cloud.**` |
|
||||
|
||||
### Phase E(f8b3200)
|
||||
|
||||
| 声明 | 核查方法 | 通过判据 / 反例 |
|
||||
|---|---|---|
|
||||
| CAMERA 权限已加且**相机可选** | `aapt2 dump badging` 看 uses-feature | `camera`/`camera.any`/`autofocus` 均为 not-required;usb.host 仍为 required |
|
||||
| 顶栏第 5 项 + 图标 | 读 `LiveScreen`、`res/drawable/ic_pip.xml` | 双弧圆 + 右下实心矩形(符合计划图标约定) |
|
||||
| 三档尺寸 96/128/160dp、高=宽×3/4 | 读 `LiveViewModel.PIP_WIDTHS_DP` 与 `PipOverlay` | 一致 |
|
||||
| 右侧留色标条位 | `PIP_RIGHT_MARGIN_DP = 32` | 一致 |
|
||||
| 异常不崩溃 | 读 `PipCameraView`:所有回调 try/catch + release | 无未捕获路径 |
|
||||
| 相机释放(关闭/离页/ON_STOP) | 读 `PipOverlay.DisposableEffect` 与 `LiveScreen` 生命周期观察者 | 三处均有 release |
|
||||
|
||||
**执行者主动交底(务必真机验证)**:`ON_STOP` 释放相机后,**是否有代码自动重新
|
||||
打开未经证实**。实现中 `PipCameraEngine.released` 标志被置位后不再复位,重新
|
||||
打开依赖 TextureView surface 重建路径(`onSurfaceTextureAvailable`)。
|
||||
`real_device_checklist.md` 第 15 步写了"返回后若 PIP 仍为开启态,画面重新出现",
|
||||
这是**推断而非实测**;若真机不符,应判定为该步预期有误(清单过强),
|
||||
而非功能必须修复——请按实测修正文档结论。
|
||||
|
||||
**另一处待验证**:PIP 浮层用了两个并列 `pointerInput`(一个拖拽、一个点按/双击)。
|
||||
Compose 中多 detector 并存的行为需真机确认(单击换档、双击关闭、拖拽是否互不干扰)。
|
||||
|
||||
### Phase F(512508e)
|
||||
|
||||
| 声明 | 核查方法 | 通过判据 / 反例 |
|
||||
|---|---|---|
|
||||
| 端口/魔数/帧长与计划一致 | 读 `RemoteContract`:47510/47511/0x1BB1B11B/38400/38412/3s/10s | 逐个对表 |
|
||||
| 封包格式 | 读 `encodeFramePacket` + 跑 `RemoteContractTest` | `[magic][counter][len][38400B]` |
|
||||
| 粘包/截断/坏长度/重同步 | 跑 `net.RemoteContractTest`(12 项) | 全绿 |
|
||||
| 端到端可用 | 跑 `net.RemoteLoopbackTest`(真实 TCP) | 全绿;核查者亦可**自写独立回环测试**(契约见 `RemoteContract` 注释) |
|
||||
| 渲染在客户端本地 | 读 `RemoteViewerViewModel`:本地 `RenderPipeline`+内置 DDT;`setPalette/setZoom` 只动本地 | 调色板/变倍无网络调用 |
|
||||
| 主机侧不拖慢相机 | 读 `RemoteHost`:帧队列 DROP_OLDEST、单协程写 socket | 一致 |
|
||||
| 默认不启动 | 读设置页与 `setRemoteHostEnabled` | 默认关闭;开启需活跃 USB 会话,否则返回 false 弹"先连接热像仪" |
|
||||
| INTERNET 权限 | `aapt2 dump badging` | 已声明 |
|
||||
|
||||
**执行者主动交底(潜在缺陷,建议构造用例)**:
|
||||
`RemoteSession.send()` 每条命令 `launch` 一个新协程写同一 socket,**两条相邻命令
|
||||
(如 `hello` 后紧跟 `startStream`)理论上可能交错写入**(小包在 TCP 上通常原子,
|
||||
但无保证)。建议核查者写一个高频命令用例(如连续多次 `requestFfc()`)观察主机侧
|
||||
是否出现解析不了的半行 JSON。若复现,属真实缺陷(应串行化写协程)。
|
||||
|
||||
**未验证边界**:UDP `255.255.255.255` 广播在真机/真实 Wi-Fi 上是否可达
|
||||
(部分网络/设备会过滤广播,且息屏策略可能影响收包)——仅回环测试通过。
|
||||
|
||||
---
|
||||
|
||||
## 4. 第三关:执行者主动交底的可疑点汇总(按风险排序)
|
||||
|
||||
| # | 事项 | 风险 | 建议动作 |
|
||||
|---|---|---|---|
|
||||
| 1 | Phase C 索引 4/8/9 的映射在两种度量下不一致 | 高(可能装错表) | 独立复验;必要时反解跳表/真机比对 |
|
||||
| 2 | Phase C 静态扫描判据仅覆盖 alpha=0xFF | 中 | 补扫 alpha=0x00 / RGB888 / BGR 等编码 |
|
||||
| 3 | PIP 从后台返回能否自动恢复相机 | 中 | 真机验证;不符则修正清单预期 |
|
||||
| 4 | PIP 双 `pointerInput` 手势并存 | 中 | 真机验证三种手势 |
|
||||
| 5 | Phase F 命令写入未串行化 | 中 | 高频命令用例压测 |
|
||||
| 6 | Phase B 探针映射方向(直接 vs 旋转) | 中 | 真机点已知位置比对温度 |
|
||||
| 7 | Phase B 温度条"随 fitRect"的规格歧义 | 低 | 判定取舍 |
|
||||
| 8 | Phase A lifetime 负载接受两种布局的"对冲" | 低 | 判定是否应只留官方一种(官方 Java 为 `[magic][ms]`,共 8B) |
|
||||
| 9 | checklist 第 15 步预期与实现可能不符 | 低 | 见 #3 |
|
||||
| 10 | `build-artifacts` 内 APK 非可复现字节(时间戳) | 低 | 以"重建并验证字符串/安装行为"替代字节比对 |
|
||||
| 11 | 超出计划的一处主动改动:camera feature 显式设为 required=false | 低 | 判定是否可接受(不影响 USB 主机功能) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 核查边界:本机无法验证的事项(须标注"无法判定")
|
||||
|
||||
本机(Windows,`C:\Tools\android-sdk`)**没有模拟器组件、没有连接的真机**,
|
||||
`adb devices` 为空。因此以下只能在用户真机上判定:
|
||||
|
||||
1. USB 出流与温度绝对值标定(`real_device_checklist.md` 第 1-12 步)。
|
||||
2. 可见光 PIP 的全部运行时行为(第 13-15 步)。
|
||||
3. 局域网远程预览双机行为(第 16-24 步)。
|
||||
4. 分析页温度条/探针的真机显示与取温正确性(第 11 步)。
|
||||
5. 任何"画面/像素"层面的确认(执行者无视觉能力,只能保证编译与单测)。
|
||||
|
||||
核查者不应把"未实测"一律记为失败;应记为**无法判定**,并保留执行者的
|
||||
真机清单作为待用户执行的验证脚本(但清单本身的日志串与预期须先按 §3A/§4
|
||||
机械核对一遍)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 复现命令速查
|
||||
|
||||
```bash
|
||||
# 构建 + 全量单测(Windows Git Bash)
|
||||
cd /c/Project/MAG160C/android && export JAVA_HOME="C:\\Tools\\jdk-21" && \
|
||||
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:assembleDebug :app:assembleRelease test --no-daemon"
|
||||
|
||||
# 单类测试
|
||||
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:testDebugUnitTest --tests 'com.mag160c.thermal.net.*' --no-daemon"
|
||||
|
||||
# 单测计数
|
||||
grep -h -o 'tests="[0-9]*"' /c/Project/MAG160C/android/app/build/test-results/testDebugUnitTest/*.xml | ...
|
||||
|
||||
# APK 清单核验(竖屏 + 权限 + feature)
|
||||
"C:\Tools\android-sdk\build-tools\36.0.0\aapt2.exe" dump badging build-artifacts/mag160c-app-debug.apk
|
||||
"C:\Tools\android-sdk\build-tools\36.0.0\aapt2.exe" dump xmltree build-artifacts/mag160c-app-debug.apk --file AndroidManifest.xml
|
||||
|
||||
# dex 中文串(防乱码回归;无需脚本)
|
||||
unzip -p build-artifacts/mag160c-app-debug.apk 'classes*.dex' | grep -ac '画中画'
|
||||
unzip -p build-artifacts/mag160c-app-debug.apk 'classes*.dex' | grep -ac '正在扫描局域网主机'
|
||||
|
||||
# checklist 日志串核对(执行者已跑通:exact=16 skeleton=30 review=7)
|
||||
"C:\Tools\jdk-21\bin\java" analysis/tools/CheckStrings.java \
|
||||
docs/android_app/real_device_checklist.md android/app/src/main
|
||||
|
||||
# Phase C 重新生成并比对(见 §3 Phase C 完整命令)
|
||||
```
|
||||
|
||||
**可用工具**:`C:\Tools\jdk-21`(java/javac/jar)、
|
||||
`C:\Tools\android-sdk\build-tools\36.0.0`(aapt2)、`C:\Tools\jadx-1.5.1`。
|
||||
|
||||
**不可用 / 易踩的坑**(执行者已实测,核查者省得重踩):
|
||||
|
||||
| 事项 | 实际 |
|
||||
|---|---|
|
||||
| python3 | **不可用**(Microsoft Store 桩,退出码 49)。`extract_palettes.py` 需改用 JDK 复写或安装 python |
|
||||
| node | 未安装 |
|
||||
| 模拟器 / 连接设备 | 无(`adb devices` 为空,SDK 无 emulator 组件) |
|
||||
| `analysis/tools/PalExport2.java` | **不能单文件直跑**:依赖 `PalIdentify.java` 的 `PalBody`,须两文件一起 `javac -d out` |
|
||||
| Git Bash 的 `/tmp` 路径 | Windows 版 java **不认**,必须写 `C:\Users\zxc\AppData\Local\Temp\...` |
|
||||
| `analysis/tools/CheckStrings.java` | 可单文件直跑(无外部依赖) |
|
||||
Reference in New Issue
Block a user