android: 统一APP工程骨架 + 官方渲染管线Kotlin移植(与C参考逐字节一致)
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate Kotlin tables from csdk C headers (palette256, t2e)."""
|
||||||
|
import re, sys, io
|
||||||
|
|
||||||
|
CSDK = r"C:\Project\MAG160C\csdk\src"
|
||||||
|
OUT = r"C:\Project\MAG160C\android\app\src\main\kotlin\com\mag160c\thermal\core\OfficialTables.kt"
|
||||||
|
|
||||||
|
def read(fn):
|
||||||
|
with open(fn, encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def parse_matrix(text, rows=256):
|
||||||
|
vals = []
|
||||||
|
for m in re.finditer(r"\{\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\}", text):
|
||||||
|
vals.append((int(m.group(3)), int(m.group(2)), int(m.group(1)))) # R,G,B from (B,G,R,0)
|
||||||
|
if len(vals) == rows:
|
||||||
|
break
|
||||||
|
return vals
|
||||||
|
|
||||||
|
def parse_int32(text):
|
||||||
|
body = text[text.index("{"):]
|
||||||
|
return [int(x) for x in re.findall(r"-?\d+", body)]
|
||||||
|
|
||||||
|
pal_txt = read(rf"{CSDK}\mag160c_official_palette256.h")
|
||||||
|
t2e_txt = read(rf"{CSDK}\mag160c_official_t2e.h")
|
||||||
|
tables_txt = read(rf"{CSDK}\mag160c_tables.h")
|
||||||
|
|
||||||
|
pal = parse_matrix(pal_txt)
|
||||||
|
assert len(pal) == 256, f"palette entries: {len(pal)}"
|
||||||
|
t2e = parse_int32(t2e_txt)
|
||||||
|
assert len(t2e) == 646, f"t2e entries: {len(t2e)}"
|
||||||
|
|
||||||
|
# mag160c_t2e (274 entries, hex) + mag160c_e2t_acc_q10 (274 entries)
|
||||||
|
def parse_hex_array(text, name):
|
||||||
|
m = re.search(rf"static const uint32_t {name}\[MAG160C_TEMP_CURVE_ENTRIES\] = \{{(.*?)\}};", text, re.S)
|
||||||
|
assert m, name
|
||||||
|
return [int(x, 16) for x in re.findall(r"0x[0-9a-fA-F]+", m.group(1))]
|
||||||
|
|
||||||
|
t2e274 = parse_hex_array(tables_txt, "mag160c_t2e")
|
||||||
|
assert len(t2e274) == 274, f"t2e274: {len(t2e274)}"
|
||||||
|
e2t = parse_hex_array(tables_txt, "mag160c_e2t_acc_q10")
|
||||||
|
assert len(e2t) == 274, f"e2t: {len(e2t)}"
|
||||||
|
|
||||||
|
# palette ARGB int per gray index
|
||||||
|
lines = []
|
||||||
|
lines.append("package com.mag160c.thermal.core")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("/** Generated from csdk C headers by analysis/gen_kotlin_tables.py. DO NOT EDIT. */")
|
||||||
|
lines.append("object OfficialTables {")
|
||||||
|
lines.append(" /** Official default palette (256 gray levels -> ARGB int), source: CoreSDKLib dev+0xb18. */")
|
||||||
|
lines.append(" val PALETTE256_ARGB = intArrayBuilder {")
|
||||||
|
for i, (r, g, b) in enumerate(pal):
|
||||||
|
argb = (0xFF << 24) | (r << 16) | (g << 8) | b
|
||||||
|
lines.append(f" add({argb}) // {i}")
|
||||||
|
lines.append(" }")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(" /** Official T2E table (646 int32), temp = slope*diff>>12 + (i<<12) - 0x249f0. */")
|
||||||
|
lines.append(" val T2E = intArrayBuilder {")
|
||||||
|
for i in range(0, len(t2e), 8):
|
||||||
|
chunk = t2e[i:i+8]
|
||||||
|
lines.append(" add(" + ", ".join(str(v) for v in chunk) + (")" if i + 8 >= len(t2e) else ""))
|
||||||
|
lines.append(" }")
|
||||||
|
lines.append("}")
|
||||||
|
kt = "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
# intArrayBuilder trick is overkill; use direct arrays with chunked lines instead.
|
||||||
|
argb_lines = []
|
||||||
|
def signed(v):
|
||||||
|
return v if v < 2**31 else v - 2**32
|
||||||
|
for i in range(0, len(pal), 6):
|
||||||
|
row = ", ".join(str(signed((0xFF << 24) | (r << 16) | (g << 8) | b)) for (r, g, b) in pal[i:i+6])
|
||||||
|
argb_lines.append(" " + row + ",")
|
||||||
|
t2e_lines = []
|
||||||
|
for i in range(0, len(t2e), 8):
|
||||||
|
row = ", ".join(str(v) for v in t2e[i:i+8])
|
||||||
|
t2e_lines.append(" " + row + ("," if i + 8 < len(t2e) else ""))
|
||||||
|
|
||||||
|
t2e274_lines = []
|
||||||
|
for i in range(0, len(t2e274), 8):
|
||||||
|
row = ", ".join(f"0x{v:08x}" for v in t2e274[i:i+8])
|
||||||
|
t2e274_lines.append(" " + row + ("," if i + 8 < len(t2e274) else ""))
|
||||||
|
|
||||||
|
e2t_lines = []
|
||||||
|
for i in range(0, len(e2t), 8):
|
||||||
|
row = ", ".join(f"0x{v:08x}" for v in e2t[i:i+8])
|
||||||
|
e2t_lines.append(" " + row + ("," if i + 8 < len(e2t) else ""))
|
||||||
|
|
||||||
|
kt = f"""package com.mag160c.thermal.core
|
||||||
|
|
||||||
|
/** Generated from csdk C headers by analysis/gen_kotlin_tables.py. DO NOT EDIT. */
|
||||||
|
object OfficialTables {{
|
||||||
|
/** Official default palette: 256 gray levels -> ARGB int (from CoreSDKLib dev+0xb18, B,G,R order swapped). */
|
||||||
|
val PALETTE256_ARGB = intArrayOf(
|
||||||
|
{chr(10).join(argb_lines)}
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Official T2E table (646 int32): temp = slope*diff>>12 + (i<<12) - 0x249f0. */
|
||||||
|
val T2E = intArrayOf(
|
||||||
|
{chr(10).join(t2e_lines)}
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Vendor T2E curve from libcoresdk.so ARM64 @0x402010 (274 uint32 entries). */
|
||||||
|
val T2E274 = intArrayOf(
|
||||||
|
{chr(10).join(t2e274_lines)}
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Vendor E2TAccQ10 curve from libcoresdk.so ARM64 @0x40245c (274 uint32 entries). */
|
||||||
|
val E2T_ACC_Q10 = intArrayOf(
|
||||||
|
{chr(10).join(e2t_lines)}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(kt)
|
||||||
|
print(f"written {OUT}: palette {len(pal)} entries, t2e {len(t2e)} entries")
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/* render_offline.c - PC reference runner for the official mag160c pipeline.
|
||||||
|
* Usage: render_offline.exe <ddt> <frames.bin> <out.rgb> <stats.txt>
|
||||||
|
* frames.bin = sequence of complete USB frames (0x38 + 38400 bytes each).
|
||||||
|
* Writes per-frame 320x240x3 RGB24 + one stats line per frame.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "mag160c/mag160c_render.h"
|
||||||
|
|
||||||
|
static unsigned rd32(const unsigned char *p) {
|
||||||
|
return p[0] | (p[1] << 8) | (p[2] << 16) | ((unsigned)p[3] << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ffc_cb(void *user, int param) { (void)user; (void)param; }
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 5) { fprintf(stderr, "usage: %s ddt frames out.rgb stats\n", argv[0]); return 1; }
|
||||||
|
FILE *ff = fopen(argv[2], "rb");
|
||||||
|
if (!ff) { fprintf(stderr, "open frames fail\n"); return 1; }
|
||||||
|
fseek(ff, 0, SEEK_END);
|
||||||
|
long fsz = ftell(ff);
|
||||||
|
fseek(ff, 0, SEEK_SET);
|
||||||
|
unsigned char *buf = malloc(fsz);
|
||||||
|
if (fread(buf, 1, fsz, ff) != (size_t)fsz) { fclose(ff); return 1; }
|
||||||
|
fclose(ff);
|
||||||
|
|
||||||
|
const unsigned frame_len = 38400;
|
||||||
|
const unsigned total = 0x38 + frame_len;
|
||||||
|
if (fsz < (long)total) { fprintf(stderr, "frames too small\n"); return 1; }
|
||||||
|
|
||||||
|
FILE *out = fopen(argv[3], "wb");
|
||||||
|
FILE *st = fopen(argv[4], "w");
|
||||||
|
if (!out || !st) { fprintf(stderr, "open out fail\n"); return 1; }
|
||||||
|
|
||||||
|
mag160c_render_t *r = NULL;
|
||||||
|
mag160c_render_cfg_t cfg;
|
||||||
|
memset(&cfg, 0, sizeof(cfg));
|
||||||
|
cfg.width = 160; cfg.height = 120;
|
||||||
|
cfg.ddt_path = argv[1];
|
||||||
|
cfg.ffc_cb = ffc_cb;
|
||||||
|
cfg.startup_force_ffc = 1;
|
||||||
|
cfg.cdf_pivot_75 = 1;
|
||||||
|
if (mag160c_render_init(&r, &cfg) != MAG160C_OK) {
|
||||||
|
fprintf(stderr, "render init fail\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char *rgb = malloc(320 * 240 * 3);
|
||||||
|
int nframes = fsz / total;
|
||||||
|
for (int f = 0; f < nframes; ++f) {
|
||||||
|
unsigned char *fr = buf + (long)f * total;
|
||||||
|
/* sanity: marker + len */
|
||||||
|
if (rd32(fr) != 0x1bb1b11b) { fprintf(stderr, "bad marker at frame %d\n", f); return 1; }
|
||||||
|
if (rd32(fr + 8) != frame_len) { fprintf(stderr, "bad len at frame %d\n", f); return 1; }
|
||||||
|
if (rd32(fr + 0x1c + frame_len) != 0x1bb1b11c) { fprintf(stderr, "bad trailer at frame %d\n", f); return 1; }
|
||||||
|
mag160c_error_t rc = mag160c_render_frame(r, fr, 1, rgb);
|
||||||
|
if (rc == MAG160C_OK) {
|
||||||
|
fwrite(rgb, 1, 320 * 240 * 3, out);
|
||||||
|
int lo, hi, mn, mx, mean, sd;
|
||||||
|
mag160c_render_get_stats(r, &lo, &hi, &mn, &mx, &mean, &sd);
|
||||||
|
fprintf(st, "%d %d %d %d %d %d %d\n", f, lo, hi, mn, mx, mean, sd);
|
||||||
|
} else {
|
||||||
|
fprintf(st, "%d HIDDEN\n", f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(out); fclose(st);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
0 HIDDEN
|
||||||
|
1 HIDDEN
|
||||||
|
2 HIDDEN
|
||||||
|
3 HIDDEN
|
||||||
|
4 HIDDEN
|
||||||
|
5 HIDDEN
|
||||||
|
6 HIDDEN
|
||||||
|
7 HIDDEN
|
||||||
|
8 HIDDEN
|
||||||
|
9 HIDDEN
|
||||||
|
10 HIDDEN
|
||||||
|
11 HIDDEN
|
||||||
|
12 HIDDEN
|
||||||
|
13 HIDDEN
|
||||||
|
14 HIDDEN
|
||||||
|
15 HIDDEN
|
||||||
|
16 HIDDEN
|
||||||
|
17 HIDDEN
|
||||||
|
18 HIDDEN
|
||||||
|
19 HIDDEN
|
||||||
|
20 HIDDEN
|
||||||
|
21 HIDDEN
|
||||||
|
22 HIDDEN
|
||||||
|
23 HIDDEN
|
||||||
|
24 HIDDEN
|
||||||
|
25 HIDDEN
|
||||||
|
26 HIDDEN
|
||||||
|
27 HIDDEN
|
||||||
|
28 HIDDEN
|
||||||
|
29 HIDDEN
|
||||||
|
30 HIDDEN
|
||||||
|
31 HIDDEN
|
||||||
|
32 6961 7961 7118 7668 7461 103
|
||||||
|
33 6962 7962 7098 7663 7462 103
|
||||||
|
34 6962 7962 7116 7675 7462 103
|
||||||
|
35 6961 7961 7106 7672 7461 103
|
||||||
|
36 6961 7961 7106 7659 7461 103
|
||||||
|
37 6961 7961 7116 7664 7461 103
|
||||||
|
38 6961 7961 7121 7671 7461 103
|
||||||
|
39 6962 7962 7117 7673 7462 103
|
||||||
|
40 6962 7962 7132 7667 7462 103
|
||||||
|
41 6962 7962 7105 7664 7462 103
|
||||||
|
42 6961 7961 7118 7671 7461 103
|
||||||
|
43 6961 7961 7130 7671 7461 103
|
||||||
|
44 6962 7962 7122 7661 7462 103
|
||||||
|
45 6961 7961 7096 7669 7461 103
|
||||||
|
46 6961 7961 7096 7671 7461 103
|
||||||
|
47 6962 7962 7122 7675 7462 103
|
||||||
|
48 6961 7961 7094 7667 7461 103
|
||||||
|
49 6962 7962 7099 7664 7462 103
|
||||||
|
50 6962 7962 7134 7669 7462 103
|
||||||
|
51 6962 7962 7133 7662 7462 103
|
||||||
|
52 6961 7961 7129 7671 7461 103
|
||||||
|
53 6962 7962 7126 7671 7462 103
|
||||||
|
54 6961 7961 7119 7669 7461 103
|
||||||
|
55 6961 7961 7096 7673 7461 103
|
||||||
|
56 6961 7961 7135 7661 7461 103
|
||||||
|
57 6962 7962 7099 7662 7462 103
|
||||||
|
58 6961 7961 7122 7659 7461 103
|
||||||
|
59 6962 7962 7116 7669 7462 103
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
local.properties
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
captures/
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.mag160c.thermal"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.mag160c.thermal"
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
packaging {
|
||||||
|
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(platform(libs.compose.bom))
|
||||||
|
implementation(libs.compose.ui)
|
||||||
|
implementation(libs.compose.ui.graphics)
|
||||||
|
implementation(libs.compose.ui.tooling.preview)
|
||||||
|
implementation(libs.compose.material3)
|
||||||
|
implementation(libs.activity.compose)
|
||||||
|
implementation(libs.core.ktx)
|
||||||
|
implementation(libs.lifecycle.runtime.ktx)
|
||||||
|
debugImplementation(libs.compose.ui.tooling)
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.usb.host" android:required="true" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:label="MAG160C"
|
||||||
|
android:theme="@android:style/Theme.Material.Light.NoActionBar"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:allowBackup="false">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||||
|
android:resizeableActivity="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.mag160c.thermal
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent { App() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun App() {
|
||||||
|
Text("MAG160C build check")
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.mag160c.thermal.core
|
||||||
|
|
||||||
|
/** Little-endian byte helpers used by the recovered vendor parsers. */
|
||||||
|
object ByteReader {
|
||||||
|
fun rd32(b: ByteArray, off: Int): Int =
|
||||||
|
(b[off].toInt() and 0xFF) or
|
||||||
|
((b[off + 1].toInt() and 0xFF) shl 8) or
|
||||||
|
((b[off + 2].toInt() and 0xFF) shl 16) or
|
||||||
|
((b[off + 3].toInt() and 0xFF) shl 24)
|
||||||
|
|
||||||
|
fun rd16(b: ByteArray, off: Int): Int =
|
||||||
|
(b[off].toInt() and 0xFF) or ((b[off + 1].toInt() and 0xFF) shl 8)
|
||||||
|
|
||||||
|
/** Decode `count` little-endian uint16 values into an IntArray. */
|
||||||
|
fun decodeU16(b: ByteArray, off: Int, count: Int, out: IntArray, outOff: Int = 0) {
|
||||||
|
for (i in 0 until count) {
|
||||||
|
out[outOff + i] = rd16(b, off + i * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.mag160c.thermal.core
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streaming frame assembler, ported from csdk/src/mag160c_frame.c (vendor
|
||||||
|
* libmagcore reader thread). Feed every bulk read from EP 0x81 into
|
||||||
|
* [push]; when a complete valid frame is buffered it is copied to
|
||||||
|
* [outFrame] and the returned length is > 0.
|
||||||
|
*/
|
||||||
|
class FrameStream(maxFrameLen: Int) {
|
||||||
|
private val cap = maxFrameLen * 2 + 0x470
|
||||||
|
private val buf = ByteArray(cap)
|
||||||
|
private var len = 0
|
||||||
|
private var aligned = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append bulk-read data. Returns the total frame size when a complete
|
||||||
|
* frame has been copied into [outFrame] (capacity must be >= 0x38 + len),
|
||||||
|
* otherwise 0.
|
||||||
|
*/
|
||||||
|
fun push(data: ByteArray, size: Int, outFrame: ByteArray): Int {
|
||||||
|
if (cap == 0 || size > cap - len) {
|
||||||
|
len = 0
|
||||||
|
aligned = false
|
||||||
|
}
|
||||||
|
if (size > cap - len) {
|
||||||
|
len = 0
|
||||||
|
}
|
||||||
|
System.arraycopy(data, 0, buf, len, size)
|
||||||
|
len += size
|
||||||
|
|
||||||
|
if (!aligned) {
|
||||||
|
alignToMarker()
|
||||||
|
if (!aligned) return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len < FRAME_OVERHEAD) return 0
|
||||||
|
val dataLen = ByteReader.rd32(buf, 8)
|
||||||
|
if (dataLen < 0 || dataLen > cap - FRAME_OVERHEAD) {
|
||||||
|
len = 0 // bogus length; resync
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (len < FRAME_OVERHEAD + dataLen) return 0
|
||||||
|
|
||||||
|
if (!isValidFrame(buf, len)) {
|
||||||
|
len = 0
|
||||||
|
aligned = false
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
val total = FRAME_OVERHEAD + dataLen
|
||||||
|
System.arraycopy(buf, 0, outFrame, 0, total)
|
||||||
|
|
||||||
|
// consume frame
|
||||||
|
System.arraycopy(buf, total, buf, 0, len - total)
|
||||||
|
len -= total
|
||||||
|
if (len == 0) aligned = false
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun alignToMarker() {
|
||||||
|
var i = 0
|
||||||
|
while (i + 4 <= len) {
|
||||||
|
if (ByteReader.rd32(buf, i) == FRAME_MARKER) break
|
||||||
|
i += 4
|
||||||
|
}
|
||||||
|
if (i + 4 > len) {
|
||||||
|
len = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (i != 0) {
|
||||||
|
System.arraycopy(buf, i, buf, 0, len - i)
|
||||||
|
len -= i
|
||||||
|
}
|
||||||
|
aligned = true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val FRAME_MARKER = 0x1BB1B11B
|
||||||
|
const val FRAME_TRAILING_MARKER = 0x1BB1B11C
|
||||||
|
const val DATA_OFFSET = 0x1C
|
||||||
|
const val FRAME_OVERHEAD = 0x38
|
||||||
|
|
||||||
|
/** Validate a buffered frame: marker, type <= 1, length and trailer. */
|
||||||
|
fun isValidFrame(buf: ByteArray, size: Int): Boolean {
|
||||||
|
if (size < DATA_OFFSET + 4) return false
|
||||||
|
if (ByteReader.rd32(buf, 0) != FRAME_MARKER) return false
|
||||||
|
if (ByteReader.rd32(buf, 12) > 1) return false
|
||||||
|
val dataLen = ByteReader.rd32(buf, 8)
|
||||||
|
if (size < FRAME_OVERHEAD + dataLen) return false
|
||||||
|
return ByteReader.rd32(buf, DATA_OFFSET + dataLen) == FRAME_TRAILING_MARKER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package com.mag160c.thermal.core
|
||||||
|
|
||||||
|
/** Generated from csdk C headers by analysis/gen_kotlin_tables.py. DO NOT EDIT. */
|
||||||
|
object OfficialTables {
|
||||||
|
/** Official default palette: 256 gray levels -> ARGB int (from CoreSDKLib dev+0xb18, B,G,R order swapped). */
|
||||||
|
val PALETTE256_ARGB = intArrayOf(
|
||||||
|
-16777216, -16777211, -16777206, -16777201, -16777195, -16777190,
|
||||||
|
-16777185, -16777179, -16777174, -16777169, -16777163, -16777158,
|
||||||
|
-16777153, -16777147, -16777142, -16777137, -16777131, -16777126,
|
||||||
|
-16777121, -16777115, -16777110, -16777105, -16777099, -16646027,
|
||||||
|
-16449418, -16318346, -16121737, -15925129, -15794056, -15597448,
|
||||||
|
-15400839, -15269767, -15073158, -14876550, -14745477, -14548869,
|
||||||
|
-14352260, -14221188, -14024579, -13827971, -13696898, -13500290,
|
||||||
|
-13369217, -13172609, -12976000, -12844928, -12648319, -12451710,
|
||||||
|
-12320638, -12124029, -11927421, -11796348, -11599740, -11403131,
|
||||||
|
-11272059, -11075450, -10878842, -10747769, -10551161, -10420088,
|
||||||
|
-10223480, -10026871, -9895799, -9699190, -9502582, -9371509,
|
||||||
|
-9174901, -8978292, -8847220, -8650611, -8454002, -8322930,
|
||||||
|
-8126321, -7929713, -7798640, -7602032, -7470959, -7274351,
|
||||||
|
-7077742, -6946670, -6750061, -6553453, -6422380, -6225772,
|
||||||
|
-6029163, -5898091, -5701482, -5504874, -5373801, -5177193,
|
||||||
|
-4980584, -4849512, -4652903, -4456294, -4390504, -4324458,
|
||||||
|
-4192876, -4127086, -4061040, -3929458, -3863412, -3797622,
|
||||||
|
-3666040, -3599994, -3534204, -3402622, -3336576, -3270530,
|
||||||
|
-3139204, -3073158, -3007112, -2875530, -2809740, -2743694,
|
||||||
|
-2612112, -2546322, -2480276, -2348694, -2282648, -2216858,
|
||||||
|
-2085276, -2019230, -1887905, -1887397, -1821353, -1755310,
|
||||||
|
-1689522, -1623479, -1557435, -1491392, -1425604, -1359561,
|
||||||
|
-1293517, -1227729, -1161686, -1095642, -1029599, -963811,
|
||||||
|
-963304, -897260, -831473, -765425, -699377, -633329,
|
||||||
|
-567537, -501489, -435441, -369393, -303601, -237553,
|
||||||
|
-171505, -105713, -39665, -39153, -38641, -38385,
|
||||||
|
-37873, -37361, -37105, -36593, -36081, -35569,
|
||||||
|
-35313, -34801, -34289, -33777, -33521, -33009,
|
||||||
|
-32497, -32241, -31729, -31217, -30705, -30449,
|
||||||
|
-29937, -29425, -28913, -28657, -28145, -27633,
|
||||||
|
-27377, -26865, -26353, -25841, -25585, -25073,
|
||||||
|
-24561, -24305, -23793, -23281, -22769, -22513,
|
||||||
|
-22001, -21489, -20977, -20721, -20209, -19697,
|
||||||
|
-19441, -18929, -18417, -17905, -17649, -17137,
|
||||||
|
-16625, -16369, -15857, -15345, -14833, -14577,
|
||||||
|
-14065, -13553, -13041, -12785, -12273, -11761,
|
||||||
|
-11505, -10993, -10481, -9969, -9713, -9201,
|
||||||
|
-8689, -8433, -7914, -7394, -6875, -6611,
|
||||||
|
-6092, -5572, -5053, -4789, -4270, -3750,
|
||||||
|
-3487, -2967, -2448, -1928, -1665, -1145,
|
||||||
|
-626, -106, -99, -91, -84, -76,
|
||||||
|
-69, -61, -54, -46, -39, -31,
|
||||||
|
-24, -16, -9, -1,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Official T2E table (646 int32): temp = slope*diff>>12 + (i<<12) - 0x249f0. */
|
||||||
|
val T2E = intArrayOf(
|
||||||
|
51, 70, 94, 125, 162, 208, 264, 331,
|
||||||
|
410, 503, 612, 737, 881, 1045, 1231, 1440,
|
||||||
|
1675, 1937, 2227, 2547, 2899, 3285, 3705, 4162,
|
||||||
|
4658, 5192, 5768, 6386, 7047, 7754, 8506, 9305,
|
||||||
|
10153, 11050, 11997, 12995, 14045, 15147, 16303, 17513,
|
||||||
|
18778, 20098, 21473, 22905, 24393, 25938, 27540, 29199,
|
||||||
|
30916, 32690, 34522, 36412, 38360, 40366, 42430, 44552,
|
||||||
|
46731, 48968, 51262, 53613, 56022, 58487, 61008, 63586,
|
||||||
|
66220, 68910, 71654, 74454, 77308, 80217, 83179, 86195,
|
||||||
|
89263, 92385, 95558, 98783, 102059, 105387, 108764, 112191,
|
||||||
|
115668, 119194, 122768, 126391, 130060, 133777, 137541, 141350,
|
||||||
|
145205, 149105, 153050, 157039, 161072, 165147, 169266, 173426,
|
||||||
|
177629, 181873, 186157, 190482, 194847, 199251, 203694, 208175,
|
||||||
|
212695, 217252, 221847, 226478, 231145, 235849, 240587, 245361,
|
||||||
|
250169, 255012, 259888, 264797, 269740, 274715, 279722, 284761,
|
||||||
|
289831, 294933, 300064, 305227, 310419, 315640, 320891, 326170,
|
||||||
|
331478, 336814, 342178, 347569, 352988, 358433, 363904, 369402,
|
||||||
|
374925, 380474, 386048, 391647, 397271, 402919, 408591, 414286,
|
||||||
|
420005, 425747, 431512, 437300, 443109, 448941, 454795, 460670,
|
||||||
|
466566, 472484, 478422, 484381, 490360, 496359, 502378, 508416,
|
||||||
|
514474, 520551, 526647, 532761, 538894, 545045, 551215, 557402,
|
||||||
|
563606, 569828, 576068, 582324, 588597, 594887, 601193, 607516,
|
||||||
|
613854, 620209, 626579, 632965, 639366, 645782, 652213, 658660,
|
||||||
|
665120, 671596, 678086, 684589, 691108, 697639, 704185, 710744,
|
||||||
|
717317, 723903, 730502, 737114, 743740, 750377, 757028, 763691,
|
||||||
|
770366, 777053, 783753, 790464, 797187, 803922, 810669, 817427,
|
||||||
|
824196, 830977, 837768, 844571, 851384, 858209, 865043, 871889,
|
||||||
|
878745, 885611, 892487, 899374, 906270, 913177, 920093, 927019,
|
||||||
|
933954, 940899, 947854, 954818, 961791, 968773, 975764, 982764,
|
||||||
|
989773, 996791, 1003818, 1010853, 1017897, 1024949, 1032009, 1039078,
|
||||||
|
1046155, 1053240, 1060333, 1067435, 1074544, 1081661, 1088785, 1095918,
|
||||||
|
1103058, 1110205, 1117360, 1124523, 1131692, 1138869, 1146053, 1153245,
|
||||||
|
1160443, 1167648, 1174860, 1182080, 1189305, 1196538, 1203777, 1211023,
|
||||||
|
1218276, 1225535, 1232800, 1240072, 1247350, 1254635, 1261925, 1269222,
|
||||||
|
1276525, 1283834, 1291149, 1298470, 1305797, 1313129, 1320468, 1327812,
|
||||||
|
1335162, 1342517, 1349878, 1357245, 1364617, 1371995, 1379378, 1386766,
|
||||||
|
1394160, 1401559, 1408963, 1416372, 1423786, 1431206, 1438631, 1446060,
|
||||||
|
1453495, 1460934, 1468379, 1475828, 1483282, 1490741, 1498204, 1505672,
|
||||||
|
1513145, 1520622, 1528104, 1535591, 1543082, 1550578, 1558077, 1565582,
|
||||||
|
1573090, 1580603, 1588121, 1595642, 1603168, 1610698, 1618232, 1625770,
|
||||||
|
1633312, 1640859, 1648409, 1655963, 1663522, 1671084, 1678650, 1686220,
|
||||||
|
1693794, 1701371, 1708953, 1716538, 1724127, 1731719, 1739315, 1746915,
|
||||||
|
1754519, 1762126, 1769736, 1777350, 1784968, 1792589, 1800214, 1807842,
|
||||||
|
1815473, 1823108, 1830746, 1838387, 1846032, 1853680, 1861331, 1868986,
|
||||||
|
1876643, 1884304, 1891968, 1899635, 1907306, 1914979, 1922655, 1930335,
|
||||||
|
1938017, 1945703, 1953391, 1961082, 1968777, 1976474, 1984174, 1991877,
|
||||||
|
1999583, 2007292, 2015003, 2022718, 2030435, 2038155, 2045877, 2053602,
|
||||||
|
2061330, 2069061, 2076794, 2084530, 2092269, 2100010, 2107754, 2115500,
|
||||||
|
2123249, 2131001, 2138755, 2146511, 2154270, 2162031, 2169795, 2177562,
|
||||||
|
2185330, 2193101, 2200875, 2208651, 2216429, 2224210, 2231993, 2239778,
|
||||||
|
2247565, 2255355, 2263147, 2270941, 2278738, 2286537, 2294338, 2302141,
|
||||||
|
2309946, 2317754, 2325563, 2333375, 2341189, 2349005, 2356823, 2364643,
|
||||||
|
2372465, 2380290, 2388116, 2395944, 2403775, 2411607, 2419441, 2427278,
|
||||||
|
2435116, 2442956, 2450798, 2458642, 2466488, 2474336, 2482186, 2490038,
|
||||||
|
2497891, 2505747, 2513604, 2521463, 2529324, 2537187, 2545051, 2552918,
|
||||||
|
2560786, 2568656, 2576527, 2584401, 2592276, 2600153, 2608031, 2615911,
|
||||||
|
2623793, 2631677, 2639562, 2647449, 2655338, 2663228, 2671120, 2679014,
|
||||||
|
2686909, 2694806, 2702704, 2710604, 2718505, 2726408, 2734313, 2742219,
|
||||||
|
2750127, 2758036, 2765947, 2773859, 2781773, 2789688, 2797605, 2805524,
|
||||||
|
2813443, 2821364, 2829287, 2837211, 2845137, 2853064, 2860992, 2868922,
|
||||||
|
2876853, 2884786, 2892720, 2900655, 2908592, 2916530, 2924470, 2932411,
|
||||||
|
2940353, 2948296, 2956241, 2964187, 2972135, 2980084, 2988034, 2995985,
|
||||||
|
3003938, 3011892, 3019847, 3027803, 3035761, 3043720, 3051680, 3059642,
|
||||||
|
3067605, 3075569, 3083534, 3091500, 3099468, 3107436, 3115406, 3123378,
|
||||||
|
3131350, 3139323, 3147298, 3155274, 3163251, 3171229, 3179208, 3187189,
|
||||||
|
3195170, 3203153, 3211137, 3219121, 3227107, 3235095, 3243083, 3251072,
|
||||||
|
3259062, 3267054, 3275046, 3283040, 3291035, 3299030, 3307027, 3315025,
|
||||||
|
3323024, 3331023, 3339024, 3347026, 3355029, 3363033, 3371038, 3379044,
|
||||||
|
3387051, 3395059, 3403068, 3411078, 3419089, 3427101, 3435113, 3443127,
|
||||||
|
3451142, 3459158, 3467174, 3475192, 3483211, 3491230, 3499250, 3507272,
|
||||||
|
3515294, 3523317, 3531341, 3539366, 3547392, 3555419, 3563447, 3571475,
|
||||||
|
3579505, 3587535, 3595566, 3603598, 3611631, 3619665, 3627700, 3635735,
|
||||||
|
3643772, 3651809, 3659847, 3667886, 3675926, 3683966, 3692008, 3700050,
|
||||||
|
3708093, 3716137, 3724182, 3732227, 3740273, 3748321, 3756368, 3764417,
|
||||||
|
3772467, 3780517, 3788568, 3796620, 3804672, 3812726, 3820780, 3828835,
|
||||||
|
3836891, 3844947, 3853004, 3861062, 3869121, 3877180, 3885240, 3893301,
|
||||||
|
3901363, 3909425, 3917488, 3925552, 3933617, 3941682, 3949748, 3957814,
|
||||||
|
3965882, 3973950, 3982019, 3990088, 3998158, 4006229, 4014301, 4022373,
|
||||||
|
4030446, 4038519, 4046594, 4054669, 4062744, 4070820, 4078897, 4086975,
|
||||||
|
4095053, 4103132, 4111212, 4119292, 4127373, 4135454, 4143536, 4151619,
|
||||||
|
4159703, 4167787, 4175871, 4183957, 4192042, 4200129
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Vendor T2E curve from libcoresdk.so ARM64 @0x402010 (274 uint32 entries). */
|
||||||
|
val T2E274 = intArrayOf(
|
||||||
|
0x000003e8, 0x000004d3, 0x000005e1, 0x00000714, 0x0000086e, 0x000009f1, 0x00000b9e, 0x00000d76,
|
||||||
|
0x00000f7b, 0x000011ae, 0x0000140e, 0x0000169d, 0x0000195c, 0x00001c49, 0x00001f67, 0x000022b3,
|
||||||
|
0x0000262f, 0x000029db, 0x00002db5, 0x000031bd, 0x000035f4, 0x00003a58, 0x00003ee9, 0x000043a7,
|
||||||
|
0x00004890, 0x00004da4, 0x000052e2, 0x0000584a, 0x00005ddb, 0x00006394, 0x00006974, 0x00006f7a,
|
||||||
|
0x000075a6, 0x00007bf7, 0x0000826c, 0x00008904, 0x00008fbf, 0x0000969b, 0x00009d98, 0x0000a4b6,
|
||||||
|
0x0000abf3, 0x0000b34e, 0x0000bac8, 0x0000c25f, 0x0000ca12, 0x0000d1e2, 0x0000d9cc, 0x0000e1d1,
|
||||||
|
0x0000e9f0, 0x0000f229, 0x0000fa7a, 0x000102e3, 0x00010b64, 0x000113fb, 0x00011ca9, 0x0001256d,
|
||||||
|
0x00012e46, 0x00013734, 0x00014036, 0x0001494c, 0x00015276, 0x00015bb2, 0x00016501, 0x00016e62,
|
||||||
|
0x000177d4, 0x00018158, 0x00018aec, 0x00019490, 0x00019e45, 0x0001a809, 0x0001b1dd, 0x0001bbbf,
|
||||||
|
0x0001c5b0, 0x0001cfaf, 0x0001d9bc, 0x0001e3d7, 0x0001edff, 0x0001f833, 0x00020275, 0x00020cc3,
|
||||||
|
0x0002171d, 0x00022183, 0x00022bf5, 0x00023672, 0x000240fa, 0x00024b8c, 0x0002562a, 0x000260d2,
|
||||||
|
0x00026b84, 0x00027640, 0x00028106, 0x00028bd5, 0x000296ae, 0x0002a190, 0x0002ac7b, 0x0002b76f,
|
||||||
|
0x0002c26b, 0x0002cd70, 0x0002d87d, 0x0002e392, 0x0002eeaf, 0x0002f9d4, 0x00030500, 0x00031034,
|
||||||
|
0x00031b6f, 0x000326b1, 0x000331fb, 0x00033d4b, 0x000348a2, 0x00035400, 0x00035f64, 0x00036acf,
|
||||||
|
0x00037640, 0x000381b7, 0x00038d34, 0x000398b7, 0x0003a440, 0x0003afcf, 0x0003bb63, 0x0003c6fd,
|
||||||
|
0x0003d29c, 0x0003de40, 0x0003e9ea, 0x0003f599, 0x0004014d, 0x00040d05, 0x000418c3, 0x00042486,
|
||||||
|
0x0004304d, 0x00043c18, 0x000447e9, 0x000453bd, 0x00045f96, 0x00046b74, 0x00047756, 0x0004833b,
|
||||||
|
0x00048f25, 0x00049b13, 0x0004a705, 0x0004b2fb, 0x0004bef4, 0x0004caf2, 0x0004d6f3, 0x0004e2f8,
|
||||||
|
0x0004ef00, 0x0004fb0c, 0x0005071b, 0x0005132e, 0x00051f44, 0x00052b5d, 0x0005377a, 0x0005439a,
|
||||||
|
0x00054fbd, 0x00055be3, 0x0005680c, 0x00057439, 0x00058068, 0x00058c9a, 0x000598cf, 0x0005a507,
|
||||||
|
0x0005b142, 0x0005bd7f, 0x0005c9bf, 0x0005d602, 0x0005e247, 0x0005ee90, 0x0005fada, 0x00060727,
|
||||||
|
0x00061377, 0x00061fc9, 0x00062c1e, 0x00063875, 0x000644ce, 0x00065129, 0x00065d87, 0x000669e7,
|
||||||
|
0x0006764a, 0x000682ae, 0x00068f15, 0x00069b7e, 0x0006a7e9, 0x0006b456, 0x0006c0c5, 0x0006cd36,
|
||||||
|
0x0006d9a9, 0x0006e61e, 0x0006f295, 0x0006ff0e, 0x00070b89, 0x00071805, 0x00072484, 0x00073104,
|
||||||
|
0x00073d86, 0x00074a0a, 0x0007568f, 0x00076317, 0x00076fa0, 0x00077c2a, 0x000788b7, 0x00079545,
|
||||||
|
0x0007a1d4, 0x0007ae66, 0x0007baf8, 0x0007c78d, 0x0007d423, 0x0007e0ba, 0x0007ed53, 0x0007f9ed,
|
||||||
|
0x00080689, 0x00081327, 0x00081fc5, 0x00082c65, 0x00083907, 0x000845aa, 0x0008524e, 0x00085ef4,
|
||||||
|
0x00086b9b, 0x00087843, 0x000884ed, 0x00089197, 0x00089e44, 0x0008aaf1, 0x0008b7a0, 0x0008c44f,
|
||||||
|
0x0008d100, 0x0008ddb3, 0x0008ea66, 0x0008f71b, 0x000903d0, 0x00091087, 0x00091d3f, 0x000929f9,
|
||||||
|
0x000936b3, 0x0009436e, 0x0009502b, 0x00095ce8, 0x000969a7, 0x00097666, 0x00098327, 0x00098fe9,
|
||||||
|
0x00099cab, 0x0009a96f, 0x0009b634, 0x0009c2f9, 0x0009cfc0, 0x0009dc88, 0x0009e950, 0x0009f61a,
|
||||||
|
0x000a02e4, 0x000a0faf, 0x000a1c7c, 0x000a2949, 0x000a3617, 0x000a42e6, 0x000a4fb5, 0x000a5c86,
|
||||||
|
0x000a6958, 0x000a762a, 0x000a82fd, 0x000a8fd1, 0x000a9ca6, 0x000aa97c, 0x000ab652, 0x000ac329,
|
||||||
|
0x000ad001, 0x000adcda, 0x000ae9b4, 0x000af68e, 0x000b0369, 0x000b1045, 0x000b1d22, 0x000b29ff,
|
||||||
|
0x000b36dd, 0x000b43bc
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Vendor E2TAccQ10 curve from libcoresdk.so ARM64 @0x40245c (274 uint32 entries). */
|
||||||
|
val E2T_ACC_Q10 = intArrayOf(
|
||||||
|
0x00008b70, 0x0000795d, 0x00006abc, 0x00005eb5, 0x000054ac, 0x00004c62, 0x0000456c, 0x00003f62,
|
||||||
|
0x00003a34, 0x000035e5, 0x00003207, 0x00002e9d, 0x00002bc0, 0x00002910, 0x000026d3, 0x000024bc,
|
||||||
|
0x000022dc, 0x0000213c, 0x00001fc0, 0x00001e5e, 0x00001d27, 0x00001c08, 0x00001afe, 0x00001a12,
|
||||||
|
0x00001935, 0x0000186b, 0x000017ad, 0x000016ff, 0x0000165e, 0x000015ca, 0x00001540, 0x000014bd,
|
||||||
|
0x00001444, 0x000013d3, 0x0000136a, 0x00001305, 0x000012a9, 0x00001251, 0x000011fc, 0x000011af,
|
||||||
|
0x00001167, 0x0000111f, 0x000010dd, 0x000010a0, 0x00001062, 0x0000102c, 0x00000ff6, 0x00000fc3,
|
||||||
|
0x00000f91, 0x00000f64, 0x00000f38, 0x00000f0d, 0x00000ee7, 0x00000ebf, 0x00000e9a, 0x00000e78,
|
||||||
|
0x00000e56, 0x00000e36, 0x00000e16, 0x00000df8, 0x00000ddc, 0x00000dc0, 0x00000da6, 0x00000d8d,
|
||||||
|
0x00000d74, 0x00000d5d, 0x00000d47, 0x00000d30, 0x00000d1b, 0x00000d06, 0x00000cf4, 0x00000ce0,
|
||||||
|
0x00000cce, 0x00000cbc, 0x00000cab, 0x00000c9a, 0x00000c8c, 0x00000c7a, 0x00000c6c, 0x00000c5e,
|
||||||
|
0x00000c4f, 0x00000c41, 0x00000c34, 0x00000c28, 0x00000c1c, 0x00000c0e, 0x00000c03, 0x00000bf8,
|
||||||
|
0x00000bed, 0x00000be2, 0x00000bd8, 0x00000bcd, 0x00000bc3, 0x00000bb9, 0x00000bb0, 0x00000ba7,
|
||||||
|
0x00000b9e, 0x00000b95, 0x00000b8d, 0x00000b85, 0x00000b7c, 0x00000b75, 0x00000b6d, 0x00000b66,
|
||||||
|
0x00000b5f, 0x00000b57, 0x00000b51, 0x00000b4a, 0x00000b43, 0x00000b3d, 0x00000b36, 0x00000b30,
|
||||||
|
0x00000b2a, 0x00000b24, 0x00000b1e, 0x00000b19, 0x00000b13, 0x00000b0e, 0x00000b08, 0x00000b04,
|
||||||
|
0x00000aff, 0x00000af9, 0x00000af5, 0x00000af0, 0x00000aec, 0x00000ae7, 0x00000ae2, 0x00000ade,
|
||||||
|
0x00000adb, 0x00000ad5, 0x00000ad2, 0x00000ace, 0x00000ac9, 0x00000ac6, 0x00000ac3, 0x00000abe,
|
||||||
|
0x00000abb, 0x00000ab7, 0x00000ab4, 0x00000ab1, 0x00000aac, 0x00000aaa, 0x00000aa6, 0x00000aa4,
|
||||||
|
0x00000aa0, 0x00000a9d, 0x00000a9a, 0x00000a97, 0x00000a95, 0x00000a91, 0x00000a8f, 0x00000a8c,
|
||||||
|
0x00000a89, 0x00000a87, 0x00000a83, 0x00000a82, 0x00000a7f, 0x00000a7c, 0x00000a7a, 0x00000a77,
|
||||||
|
0x00000a76, 0x00000a73, 0x00000a70, 0x00000a6f, 0x00000a6b, 0x00000a6a, 0x00000a68, 0x00000a65,
|
||||||
|
0x00000a64, 0x00000a61, 0x00000a5f, 0x00000a5e, 0x00000a5c, 0x00000a5a, 0x00000a58, 0x00000a55,
|
||||||
|
0x00000a55, 0x00000a52, 0x00000a50, 0x00000a4f, 0x00000a4d, 0x00000a4b, 0x00000a4a, 0x00000a48,
|
||||||
|
0x00000a46, 0x00000a45, 0x00000a43, 0x00000a42, 0x00000a41, 0x00000a3e, 0x00000a3d, 0x00000a3c,
|
||||||
|
0x00000a3a, 0x00000a39, 0x00000a37, 0x00000a36, 0x00000a35, 0x00000a33, 0x00000a32, 0x00000a31,
|
||||||
|
0x00000a2f, 0x00000a2f, 0x00000a2c, 0x00000a2c, 0x00000a2b, 0x00000a29, 0x00000a28, 0x00000a27,
|
||||||
|
0x00000a25, 0x00000a25, 0x00000a23, 0x00000a22, 0x00000a21, 0x00000a20, 0x00000a1f, 0x00000a1e,
|
||||||
|
0x00000a1d, 0x00000a1b, 0x00000a1b, 0x00000a19, 0x00000a19, 0x00000a17, 0x00000a17, 0x00000a16,
|
||||||
|
0x00000a14, 0x00000a14, 0x00000a13, 0x00000a13, 0x00000a11, 0x00000a10, 0x00000a0f, 0x00000a0f,
|
||||||
|
0x00000a0e, 0x00000a0c, 0x00000a0c, 0x00000a0b, 0x00000a0b, 0x00000a09, 0x00000a08, 0x00000a08,
|
||||||
|
0x00000a07, 0x00000a06, 0x00000a06, 0x00000a05, 0x00000a04, 0x00000a04, 0x00000a02, 0x00000a02,
|
||||||
|
0x00000a01, 0x00000a00, 0x00000a00, 0x000009ff, 0x000009fe, 0x000009fe, 0x000009fd, 0x000009fc,
|
||||||
|
0x000009fc, 0x000009fb, 0x000009fa, 0x000009fa, 0x000009f9, 0x000009f9, 0x000009f8, 0x000009f7,
|
||||||
|
0x000009f7, 0x000009f6, 0x000009f6, 0x000009f5, 0x000009f4, 0x000009f3, 0x000009f3, 0x000009f3,
|
||||||
|
0x000009f2, 0x000009f1
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,686 @@
|
|||||||
|
package com.mag160c.thermal.core
|
||||||
|
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Official MAG160C rendering pipeline, ported from csdk/src/mag160c_render.c
|
||||||
|
* (pixel-verified against CoreSDKLib.dll).
|
||||||
|
*
|
||||||
|
* raw frame -> shutter extract -> FFC state machine -> endpoint select
|
||||||
|
* + Q12 table interp -> ref (4x type-1 mean) -> NUC lookup -> blind
|
||||||
|
* compensation -> stats/window -> LUT1024 rebuild -> gray -> 2x upscale
|
||||||
|
* -> palette -> ARGB output.
|
||||||
|
*
|
||||||
|
* Feed USB frames; get a 320x240 ARGB IntArray ready for Bitmap.setPixels.
|
||||||
|
* FFC commands are emitted through [onFfc] so the caller owns the transport.
|
||||||
|
*/
|
||||||
|
class RenderPipeline(
|
||||||
|
private val w: Int = 160,
|
||||||
|
private val h: Int = 120,
|
||||||
|
private val ffcPeriod: Int = 1800,
|
||||||
|
private val ffcDrift: Int = 250,
|
||||||
|
private val warmFrames: Int = 20,
|
||||||
|
private val cdfPivot75: Boolean = true,
|
||||||
|
private val force75: Boolean = true,
|
||||||
|
/** FFC command callback: param 0 = FFC(0), 1 = FFC(1). */
|
||||||
|
private val onFfc: ((param: Int) -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
private val npix = w * h
|
||||||
|
|
||||||
|
// ---- DDT state ----
|
||||||
|
private var epCount = 0
|
||||||
|
private var nsegs = 0
|
||||||
|
private var c5c = 0
|
||||||
|
private var c60 = 0
|
||||||
|
private var dev24 = 5
|
||||||
|
private var dev4cShift = 3
|
||||||
|
private var dev48 = 0
|
||||||
|
private val epTemps = IntArray(64)
|
||||||
|
private val blindCnt = IntArray(64)
|
||||||
|
private val epThr = arrayOfNulls<ShortArray>(64)
|
||||||
|
private val epGain = arrayOfNulls<IntArray>(64)
|
||||||
|
private val epBlind = arrayOfNulls<IntArray>(64)
|
||||||
|
private var sel = 0
|
||||||
|
private var thrWork = ShortArray(0)
|
||||||
|
private var gainWork = IntArray(0)
|
||||||
|
private var blindWork: IntArray? = null
|
||||||
|
private var blindCount = 0
|
||||||
|
|
||||||
|
// ---- frame/FFC state ----
|
||||||
|
private var ffcIdx = -1
|
||||||
|
private var globalFrames = 0
|
||||||
|
private var shutter = 0
|
||||||
|
private var shutterAtFfc = 0
|
||||||
|
private var manualFfc = false
|
||||||
|
private var hasRef = false
|
||||||
|
private var refCnt = 0
|
||||||
|
private var startupFfcDone = false
|
||||||
|
|
||||||
|
private val ref = IntArray(npix)
|
||||||
|
private val refAcc = IntArray(npix)
|
||||||
|
private val nuc = IntArray(npix)
|
||||||
|
private val gray160 = ByteArray(npix)
|
||||||
|
private val gray320 = ByteArray(npix * 4)
|
||||||
|
|
||||||
|
// ---- render state ----
|
||||||
|
private val hist1024 = IntArray(1024)
|
||||||
|
private val cdf = IntArray(1024)
|
||||||
|
private val lut = ByteArray(1024)
|
||||||
|
private val curve = IntArray(1024)
|
||||||
|
private var winLo = 0
|
||||||
|
private var winHi = 0
|
||||||
|
private var statMin = 0
|
||||||
|
private var statMax = 0
|
||||||
|
private var statMean = 0
|
||||||
|
private var statStd = 0
|
||||||
|
private var centerPrev = 341
|
||||||
|
private var meanPrev = 0
|
||||||
|
|
||||||
|
// ---- output ----
|
||||||
|
val outWidth: Int = w * 2
|
||||||
|
val outHeight: Int = h * 2
|
||||||
|
|
||||||
|
private val lock = Any()
|
||||||
|
|
||||||
|
/** Load the official DDT calibration data (raw file bytes). */
|
||||||
|
fun loadDdt(bytes: ByteArray): Boolean = synchronized(lock) {
|
||||||
|
if (bytes.size < 0x98) return false
|
||||||
|
val magic = ByteReader.rd32(bytes, 0)
|
||||||
|
if (magic != 0x5AA50003 && magic != 0x5AA50004) return false
|
||||||
|
if (ByteReader.rd32(bytes, 4) != w || ByteReader.rd32(bytes, 8) != h) return false
|
||||||
|
epCount = ByteReader.rd32(bytes, 12)
|
||||||
|
nsegs = ByteReader.rd32(bytes, 16)
|
||||||
|
if (epCount < 2 || epCount > 64 || nsegs < 1 || nsegs > 8) return false
|
||||||
|
val v4 = magic == 0x5AA50004
|
||||||
|
dev24 = ByteReader.rd32(bytes, 20)
|
||||||
|
dev4cShift = 3
|
||||||
|
if (!v4) {
|
||||||
|
c5c = ByteReader.rd32(bytes, 28)
|
||||||
|
c60 = ByteReader.rd32(bytes, 32)
|
||||||
|
if (c5c > 0x10000) c5c = 0
|
||||||
|
} else {
|
||||||
|
c5c = ByteReader.rd32(bytes, 0x2C + 0x120)
|
||||||
|
c60 = ByteReader.rd32(bytes, 0x30 + 0x120)
|
||||||
|
}
|
||||||
|
if (c5c and 1 == 1) c5c = c5c and 1.inv()
|
||||||
|
val arro = if (v4) 0x120 + 36 else 36
|
||||||
|
var off = arro
|
||||||
|
if (off + epCount * 12 > bytes.size) return false
|
||||||
|
for (e in 0 until epCount) {
|
||||||
|
epTemps[e] = ByteReader.rd32(bytes, off + e * 4)
|
||||||
|
}
|
||||||
|
off += epCount * 4
|
||||||
|
off += epCount * 4 // skip array 2
|
||||||
|
off += epCount * 4 // skip array 3
|
||||||
|
if (off + (epCount - 1) * 4 > bytes.size) return false
|
||||||
|
for (e in 0 until epCount - 1) {
|
||||||
|
blindCnt[e] = ByteReader.rd32(bytes, off + e * 4)
|
||||||
|
if (blindCnt[e] > 4096) blindCnt[e] = 4096
|
||||||
|
}
|
||||||
|
blindCnt[epCount - 1] = 0
|
||||||
|
off += (epCount - 1) * 4
|
||||||
|
|
||||||
|
val thrSz = (nsegs - 1) * npix + c5c / 2
|
||||||
|
val gainSz = nsegs * npix * 2
|
||||||
|
for (e in 0 until epCount) {
|
||||||
|
if (off + thrSz * 2 + gainSz * 2 > bytes.size) return false
|
||||||
|
val thr = ShortArray(thrSz)
|
||||||
|
for (i in 0 until thrSz) {
|
||||||
|
thr[i] = ByteReader.rd16(bytes, off + i * 2).toShort()
|
||||||
|
}
|
||||||
|
off += thrSz * 2
|
||||||
|
val gain = IntArray(gainSz)
|
||||||
|
ByteReader.decodeU16(bytes, off, gainSz, gain)
|
||||||
|
off += gainSz * 2
|
||||||
|
epThr[e] = thr
|
||||||
|
epGain[e] = gain
|
||||||
|
}
|
||||||
|
for (e in 0 until epCount - 1) {
|
||||||
|
val n = blindCnt[e]
|
||||||
|
if (n > 0) {
|
||||||
|
if (off + n * 40 > bytes.size) return false
|
||||||
|
val rec = IntArray(n * 10)
|
||||||
|
for (i in 0 until n * 10) {
|
||||||
|
rec[i] = ByteReader.rd32(bytes, off + i * 4)
|
||||||
|
}
|
||||||
|
off += n * 40
|
||||||
|
epBlind[e] = rec
|
||||||
|
} else {
|
||||||
|
epBlind[e] = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thrWork = ShortArray(thrSz)
|
||||||
|
gainWork = IntArray(gainSz)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectEndpoint(shutter: Int): Int {
|
||||||
|
var s = 0
|
||||||
|
if (epCount != 2) {
|
||||||
|
while (s < epCount - 2 && shutter > epTemps[s + 1]) s++
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rebuildTables(shutter: Int) {
|
||||||
|
sel = selectEndpoint(shutter)
|
||||||
|
val t = (((shutter - epTemps[sel]) shl 12) / (epTemps[sel + 1] - epTemps[sel]))
|
||||||
|
.coerceIn(-0x3FFF, 0x3FFF)
|
||||||
|
val a = epThr[sel]!!
|
||||||
|
val b = epThr[sel + 1]!!
|
||||||
|
val nThr = (nsegs - 1) * npix + c5c / 2
|
||||||
|
for (i in 0 until nThr) {
|
||||||
|
val av = a[i].toInt()
|
||||||
|
thrWork[i] = (av + (((b[i].toInt() - av) * t) shr 12)).toShort()
|
||||||
|
}
|
||||||
|
val ga = epGain[sel]!!
|
||||||
|
val gb = epGain[sel + 1]!!
|
||||||
|
val nGain = nsegs * npix * 2
|
||||||
|
for (i in 0 until nGain) {
|
||||||
|
gainWork[i] = ga[i] + (((gb[i] - ga[i]) * t) shr 12)
|
||||||
|
}
|
||||||
|
blindWork = epBlind[sel]
|
||||||
|
blindCount = blindCnt[sel]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refPush(frame: IntArray) {
|
||||||
|
if (refCnt == 0) {
|
||||||
|
for (i in 0 until npix) refAcc[i] = frame[i]
|
||||||
|
} else {
|
||||||
|
for (i in 0 until npix) refAcc[i] += frame[i]
|
||||||
|
}
|
||||||
|
refCnt++
|
||||||
|
if (refCnt >= 4) {
|
||||||
|
for (i in 0 until npix) ref[i] = refAcc[i] shr 2
|
||||||
|
refCnt = 0
|
||||||
|
hasRef = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nucAndBlind(f20: IntArray, out: IntArray) {
|
||||||
|
val thr = thrWork
|
||||||
|
val gg = gainWork
|
||||||
|
for (i in 0 until npix) {
|
||||||
|
val d2 = (f20[i] - ref[i]) shr 1
|
||||||
|
var seg = 0
|
||||||
|
if (d2 > thr[i * 2].toInt()) {
|
||||||
|
seg = 1
|
||||||
|
if (d2 > thr[i * 2 + 1].toInt()) seg = 2
|
||||||
|
}
|
||||||
|
val base = (seg * npix + i) * 2
|
||||||
|
var v = gg[base + 1] + ((gg[base] * d2) shr 12)
|
||||||
|
if (v < 0) v = 0 else if (v > 65535) v = 65535
|
||||||
|
out[i] = v
|
||||||
|
}
|
||||||
|
val bw = blindWork
|
||||||
|
if (bw != null && blindCount > 0) {
|
||||||
|
for (k in 0 until blindCount) {
|
||||||
|
val base = k * 10
|
||||||
|
val target = bw[base]
|
||||||
|
val typ = bw[base + 1]
|
||||||
|
if (target >= npix || typ < 3 || typ > 8) continue
|
||||||
|
var sum = 0L
|
||||||
|
for (j in 0 until typ) sum += out[bw[base + 2 + j]]
|
||||||
|
when (typ) {
|
||||||
|
8 -> out[target] = (sum shr 3).toInt()
|
||||||
|
4 -> out[target] = (sum shr 2).toInt()
|
||||||
|
else -> out[target] = (sum / typ).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun statsWindow() {
|
||||||
|
var ssum = 0L
|
||||||
|
var s2sum = 0L
|
||||||
|
var mn = 0xFFFF
|
||||||
|
var mx = 0
|
||||||
|
for (i in 0 until npix) {
|
||||||
|
val v = nuc[i]
|
||||||
|
ssum += v
|
||||||
|
s2sum += v.toLong() * v
|
||||||
|
if (v < mn) mn = v
|
||||||
|
if (v > mx) mx = v
|
||||||
|
}
|
||||||
|
statMin = mn
|
||||||
|
statMax = mx
|
||||||
|
statMean = (ssum / npix).toInt()
|
||||||
|
val dmean = ssum.toDouble() / npix
|
||||||
|
val variance = s2sum.toDouble() / npix - dmean * dmean
|
||||||
|
statStd = sqrt(if (variance > 0) variance else 0.0).toInt()
|
||||||
|
if (mn == mx) {
|
||||||
|
winLo = mn
|
||||||
|
winHi = mn
|
||||||
|
lut.fill(0x80.toByte())
|
||||||
|
gray160.fill(0x80.toByte())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var base = (dev24 * 1000) shr dev4cShift
|
||||||
|
if (base < 0x80) base = 0x80
|
||||||
|
val half = base shr 1
|
||||||
|
var lo = statMean - half
|
||||||
|
if (lo > statMin) lo = statMin
|
||||||
|
if (lo < 0) lo = 0
|
||||||
|
var hi = statMean + half
|
||||||
|
if (hi < statMax) hi = statMax
|
||||||
|
if (hi > 65535) hi = 65535
|
||||||
|
winLo = lo
|
||||||
|
winHi = hi
|
||||||
|
if (winHi <= winLo) winHi = winLo + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lutRebuild() {
|
||||||
|
// The vendor code is 32-bit unsigned arithmetic; products/subtractions
|
||||||
|
// wrap mod 2^32. u32() replicates that so results match bit-for-bit.
|
||||||
|
val span = (winHi - winLo).coerceAtLeast(1)
|
||||||
|
val s = 0xFFC00000L / span
|
||||||
|
hist1024.fill(0)
|
||||||
|
for (i in 0 until npix) {
|
||||||
|
val idx = u32((nuc[i] - winLo).toLong() * s) shr 0x16
|
||||||
|
if (idx < 1024) hist1024[idx.toInt()]++
|
||||||
|
}
|
||||||
|
var i = 0
|
||||||
|
while (i < 1023) {
|
||||||
|
hist1024[i] = (hist1024[i + 1] + hist1024[i]) shr 1
|
||||||
|
hist1024[i + 1] = (hist1024[i + 2] + hist1024[i + 1]) shr 1
|
||||||
|
hist1024[i + 2] = (hist1024[i + 3] + hist1024[i + 2]) shr 1
|
||||||
|
i += 3
|
||||||
|
}
|
||||||
|
var total = 0L
|
||||||
|
for (k in 0 until 1024) total += hist1024[k]
|
||||||
|
var u21 = total shr 12
|
||||||
|
if (u21 == 0L) u21 = 1
|
||||||
|
var lres = ((statMean - winLo).toLong() * 0x3FF / span).toInt()
|
||||||
|
if (lres < 0) lres = 0
|
||||||
|
if (lres > 1023) lres = 1023
|
||||||
|
if (cdfPivot75) {
|
||||||
|
var acc = 0L
|
||||||
|
var k = 0
|
||||||
|
while (k < 1024) {
|
||||||
|
acc += hist1024[k]
|
||||||
|
if (acc >= (total * 3 shr 2)) break
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
if (k < 1024 && k > lres) lres = k
|
||||||
|
}
|
||||||
|
var u11 = 0L
|
||||||
|
var u13 = 0L
|
||||||
|
var k = lres
|
||||||
|
while (k >= 0) {
|
||||||
|
u11 += hist1024[k]
|
||||||
|
if (u21 <= u11) {
|
||||||
|
u13 += 0x100
|
||||||
|
u11 = 0
|
||||||
|
}
|
||||||
|
cdf[k] = (((0x10000L / u21) * u11 shr 8) + u13).toInt()
|
||||||
|
k--
|
||||||
|
}
|
||||||
|
u11 = 0
|
||||||
|
u13 = 0
|
||||||
|
k = lres + 1
|
||||||
|
while (k < 1024) {
|
||||||
|
u11 += hist1024[k]
|
||||||
|
if (u11 < u21 * 8) {
|
||||||
|
if (u21 <= u11) {
|
||||||
|
u13 += 0x100
|
||||||
|
u11 = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
u13 += 0x200
|
||||||
|
u11 = 0
|
||||||
|
}
|
||||||
|
cdf[k] = (((0x10000L / u21) * u11 shr 8) + u13).toInt()
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
for (j in 0 until 1024) {
|
||||||
|
val v = ((0x300000L + j * 0x800L) shr 0xD).toInt()
|
||||||
|
curve[j] = if (v != 0) v else 1
|
||||||
|
}
|
||||||
|
var iv6 = dev48 * 0x155 / 200
|
||||||
|
var iv7 = iv6 + 0x155
|
||||||
|
var iv26 = iv7
|
||||||
|
if (iv6 + 0x156 > 0x331) iv26 = 0x330
|
||||||
|
var iv10 = 0x332
|
||||||
|
var b3 = true
|
||||||
|
val iv22 = 0xFFC00000L / span
|
||||||
|
while (iv26 + 1 < iv10) {
|
||||||
|
iv7 = (iv10 + iv26) / 2
|
||||||
|
if (b3) {
|
||||||
|
b3 = false
|
||||||
|
iv7 = iv26
|
||||||
|
}
|
||||||
|
var u5 = cdf[0].toLong()
|
||||||
|
if (u5 != 0L) {
|
||||||
|
val denom = (iv7 * 0x40000L + 0x40000L) / u5
|
||||||
|
val base = iv7 * 0x10L + 0x10L
|
||||||
|
var u21v = base
|
||||||
|
var j = lres
|
||||||
|
while (j >= 0) {
|
||||||
|
val shifted = u32(cdf[j].toLong() * denom) shr 0xE
|
||||||
|
var u12 = u32(base - shifted)
|
||||||
|
val over = u32(u21v - u12)
|
||||||
|
if (curve[j] < over) u12 = u32(u21v - curve[j])
|
||||||
|
lut[j] = (u12 shr 6).toByte()
|
||||||
|
u21v = u12
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u5 = cdf[1023].toLong()
|
||||||
|
if (u5 != 0L) {
|
||||||
|
val denom = u32(0xFFC0000L - iv7 * 0x40000L) / u5
|
||||||
|
val base = iv7 * 0x10L
|
||||||
|
var u21v = base
|
||||||
|
var j = lres + 1
|
||||||
|
while (j < 1024) {
|
||||||
|
val shifted = u32(cdf[j].toLong() * denom) shr 0xE
|
||||||
|
var u12 = u32(shifted + base)
|
||||||
|
val over = u32(u12 - u21v)
|
||||||
|
if (curve[j] < over) u12 = u32(curve[j] + u21v)
|
||||||
|
lut[j] = (u12 shr 6).toByte()
|
||||||
|
u21v = u12
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var iv4 = iv7
|
||||||
|
val bfmin = u32(u32((statMin - winLo).toLong()) * iv22) shr 0x16
|
||||||
|
val bfmax = u32(u32((statMax - winLo).toLong()) * iv22) shr 0x16
|
||||||
|
if (bfmin < 1024 && bfmax < 1024 &&
|
||||||
|
(lut[bfmin.toInt()].toInt() and 0xFF) == 0 && (lut[bfmax.toInt()].toInt() and 0xFF) != 0xFF
|
||||||
|
) {
|
||||||
|
iv4 = iv10
|
||||||
|
iv26 = iv7
|
||||||
|
}
|
||||||
|
iv10 = iv4
|
||||||
|
}
|
||||||
|
var dm = statMean - meanPrev
|
||||||
|
if (dm < 0) dm = -dm
|
||||||
|
meanPrev = statMean
|
||||||
|
var iv18 = iv10 + ((dm and 0xFFFF) shl dev4cShift) * 2 + statMean
|
||||||
|
if (iv18 < 0x9C4) {
|
||||||
|
if (iv18 < 100) iv18 = 100
|
||||||
|
iv7 = (iv18 * iv7 + centerPrev * 500) / (iv18 + 500)
|
||||||
|
centerPrev = iv7
|
||||||
|
var u5 = cdf[0].toLong()
|
||||||
|
if (u5 != 0L) {
|
||||||
|
val denom = (iv7 * 0x40000L + 0x40000L) / u5
|
||||||
|
val base = iv7 * 0x10L + 0x10L
|
||||||
|
var u21v = base
|
||||||
|
var j = lres
|
||||||
|
while (j >= 0) {
|
||||||
|
val shifted = u32(cdf[j].toLong() * denom) shr 0xE
|
||||||
|
var u12 = u32(base - shifted)
|
||||||
|
val over = u32(u21v - u12)
|
||||||
|
if (curve[j] < over) u12 = u32(u21v - curve[j])
|
||||||
|
lut[j] = (u12 shr 6).toByte()
|
||||||
|
u21v = u12
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u5 = cdf[1023].toLong()
|
||||||
|
if (u5 != 0L) {
|
||||||
|
val denom = u32(0xFFC0000L - iv7 * 0x40000L) / u5
|
||||||
|
val base = iv7 * 0x10L
|
||||||
|
var u21v = base
|
||||||
|
var j = lres + 1
|
||||||
|
while (j < 1024) {
|
||||||
|
val shifted = u32(cdf[j].toLong() * denom) shr 0xE
|
||||||
|
var u12 = u32(shifted + base)
|
||||||
|
val over = u32(u12 - u21v)
|
||||||
|
if (curve[j] < over) u12 = u32(curve[j] + u21v)
|
||||||
|
lut[j] = (u12 shr 6).toByte()
|
||||||
|
u21v = u12
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun grayMap() {
|
||||||
|
val s = 0xFFC00000L / (winHi - winLo).coerceAtLeast(1)
|
||||||
|
for (i in 0 until npix) {
|
||||||
|
val v = nuc[i]
|
||||||
|
val idx = when {
|
||||||
|
v <= winLo -> 0L
|
||||||
|
v >= winHi -> 1023L
|
||||||
|
else -> u32((v - winLo).toLong() * s) shr 0x16
|
||||||
|
}
|
||||||
|
gray160[i] = lut[idx.coerceAtMost(1023L).toInt()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 32-bit unsigned wrap (C unsigned int semantics). */
|
||||||
|
private fun u32(x: Long): Long = x and 0xFFFFFFFFL
|
||||||
|
|
||||||
|
private fun upscale2x() {
|
||||||
|
val W = w
|
||||||
|
val H = h
|
||||||
|
val W2 = W * 2
|
||||||
|
val s = gray160
|
||||||
|
val o = gray320
|
||||||
|
for (y in 0 until H - 1) {
|
||||||
|
val r0 = y * W
|
||||||
|
val r1 = r0 + W
|
||||||
|
val o0 = y * W2 * 2
|
||||||
|
val o1 = o0 + W2
|
||||||
|
var x = 0
|
||||||
|
while (x < W - 2) {
|
||||||
|
val a0 = s[r0 + x].toInt() and 0xFF
|
||||||
|
val a1 = s[r0 + x + 1].toInt() and 0xFF
|
||||||
|
val a2 = s[r0 + x + 2].toInt() and 0xFF
|
||||||
|
val b0 = s[r1 + x].toInt() and 0xFF
|
||||||
|
val b1 = s[r1 + x + 1].toInt() and 0xFF
|
||||||
|
val b2 = s[r1 + x + 2].toInt() and 0xFF
|
||||||
|
val c = x * 2
|
||||||
|
o[o0 + c] = a0.toByte()
|
||||||
|
o[o0 + c + 1] = ((a0 + a1) shr 1).toByte()
|
||||||
|
o[o0 + c + 2] = a1.toByte()
|
||||||
|
o[o0 + c + 3] = ((a2 + a1) shr 1).toByte()
|
||||||
|
o[o1 + c] = ((b0 + a0) shr 1).toByte()
|
||||||
|
o[o1 + c + 1] = ((b0 + b1 + a0 + a1) shr 2).toByte()
|
||||||
|
o[o1 + c + 2] = ((b1 + a1) shr 1).toByte()
|
||||||
|
o[o1 + c + 3] = ((b2 + b1 + a2 + a1) shr 2).toByte()
|
||||||
|
x += 2
|
||||||
|
}
|
||||||
|
val a0 = s[r0 + W - 2].toInt() and 0xFF
|
||||||
|
val a1 = s[r0 + W - 1].toInt() and 0xFF
|
||||||
|
val b0 = s[r1 + W - 2].toInt() and 0xFF
|
||||||
|
val b1 = s[r1 + W - 1].toInt() and 0xFF
|
||||||
|
var c = (W - 2) * 2
|
||||||
|
o[o0 + c] = a0.toByte()
|
||||||
|
o[o0 + c + 1] = ((a1 + a0) shr 1).toByte()
|
||||||
|
o[o1 + c] = ((b0 + a0) shr 1).toByte()
|
||||||
|
o[o1 + c + 1] = ((a1 + b1 + b0 + a0) shr 2).toByte()
|
||||||
|
c = (W - 1) * 2
|
||||||
|
o[o0 + c] = a1.toByte()
|
||||||
|
o[o0 + c + 1] = (((3 * a1) shr 2) + (a0 shr 2)).toByte()
|
||||||
|
o[o1 + c] = ((b1 + a1) shr 1).toByte()
|
||||||
|
o[o1 + c + 1] = ((3 * (b1 + a1) + b0 + a0) shr 3).toByte()
|
||||||
|
}
|
||||||
|
val y = H - 1
|
||||||
|
val r0 = y * W
|
||||||
|
val r1 = r0 - W
|
||||||
|
val o0 = y * W2 * 2
|
||||||
|
val o1 = o0 + W2
|
||||||
|
var x = 0
|
||||||
|
while (x < W - 2) {
|
||||||
|
val a0 = s[r0 + x].toInt() and 0xFF
|
||||||
|
val a1 = s[r0 + x + 1].toInt() and 0xFF
|
||||||
|
val a2 = s[r0 + x + 2].toInt() and 0xFF
|
||||||
|
val u0 = s[r1 + x].toInt() and 0xFF
|
||||||
|
val u1 = s[r1 + x + 1].toInt() and 0xFF
|
||||||
|
val u2 = s[r1 + x + 2].toInt() and 0xFF
|
||||||
|
val c = x * 2
|
||||||
|
o[o0 + c] = a0.toByte()
|
||||||
|
o[o0 + c + 1] = ((a0 + a1) shr 1).toByte()
|
||||||
|
o[o0 + c + 2] = a1.toByte()
|
||||||
|
o[o0 + c + 3] = ((a2 + a1) shr 1).toByte()
|
||||||
|
o[o1 + c] = (((3 * a0) shr 2) + (u0 shr 2)).toByte()
|
||||||
|
o[o1 + c + 1] = ((3 * (a0 + a1) + u0 + u1) shr 3).toByte()
|
||||||
|
o[o1 + c + 2] = (((3 * a1) shr 2) + (u1 shr 2)).toByte()
|
||||||
|
o[o1 + c + 3] = ((3 * (a2 + a1) + u2 + u1) shr 3).toByte()
|
||||||
|
x += 2
|
||||||
|
}
|
||||||
|
val a0 = s[r0 + W - 2].toInt() and 0xFF
|
||||||
|
val a1 = s[r0 + W - 1].toInt() and 0xFF
|
||||||
|
val u0 = s[r1 + W - 2].toInt() and 0xFF
|
||||||
|
val u1 = s[r1 + W - 1].toInt() and 0xFF
|
||||||
|
var c = (W - 2) * 2
|
||||||
|
o[o0 + c] = a0.toByte()
|
||||||
|
o[o0 + c + 1] = ((a1 + a0) shr 1).toByte()
|
||||||
|
o[o1 + c] = (((3 * a0) shr 2) + (u0 shr 2)).toByte()
|
||||||
|
o[o1 + c + 1] = ((3 * (a0 + a1) + u0 + u1) shr 3).toByte()
|
||||||
|
c = (W - 1) * 2
|
||||||
|
o[o0 + c] = a1.toByte()
|
||||||
|
o[o0 + c + 1] = ((3 * a1 + a0) shr 2).toByte()
|
||||||
|
o[o1 + c] = ((3 * a1 + u1) shr 2).toByte()
|
||||||
|
o[o1 + c + 1] = (((o[o0 + c + 1].toInt() and 0xFF) + (o[o1 + c].toInt() and 0xFF) + a1) / 3).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ffcStep(shutterIn: Int): Int {
|
||||||
|
if (ffcIdx < 0) {
|
||||||
|
onFfc?.invoke(0)
|
||||||
|
ffcIdx = 0
|
||||||
|
shutterAtFfc = shutterIn
|
||||||
|
}
|
||||||
|
ffcIdx++
|
||||||
|
val idx = ffcIdx
|
||||||
|
if (idx <= 5) {
|
||||||
|
if (idx == 1) rebuildTables(shutterAtFfc)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if (idx <= 9) {
|
||||||
|
if (idx == 9) onFfc?.invoke(1)
|
||||||
|
if (idx == 6) {
|
||||||
|
refCnt = 0
|
||||||
|
hasRef = false
|
||||||
|
}
|
||||||
|
return 2 // ref collection window
|
||||||
|
}
|
||||||
|
if (idx <= 13) return 1
|
||||||
|
// normal frame: FFC condition
|
||||||
|
var drift = shutterIn - shutterAtFfc
|
||||||
|
if (drift < 0) drift = -drift
|
||||||
|
val cool = 13
|
||||||
|
var doFfc = false
|
||||||
|
if (manualFfc) {
|
||||||
|
manualFfc = false
|
||||||
|
doFfc = true
|
||||||
|
}
|
||||||
|
if (force75 && !startupFfcDone && idx == 75) {
|
||||||
|
startupFfcDone = true
|
||||||
|
doFfc = true
|
||||||
|
}
|
||||||
|
if (idx >= 4 + ffcPeriod) {
|
||||||
|
doFfc = true
|
||||||
|
} else if (idx >= cool + 30 && drift > ffcDrift) {
|
||||||
|
doFfc = true
|
||||||
|
}
|
||||||
|
if (doFfc) {
|
||||||
|
onFfc?.invoke(0)
|
||||||
|
ffcIdx = 0
|
||||||
|
shutterAtFfc = shutterIn
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process one raw frame.
|
||||||
|
* @param frame USB frame buffer (with or without the 0x38-byte header)
|
||||||
|
* @param hasHdr true when the 28-byte header is at frame[0]
|
||||||
|
* @param outArgb 320x240 ARGB IntArray output (outW*outH entries)
|
||||||
|
* @return true when outArgb was written; false while in the startup/FFC window.
|
||||||
|
*/
|
||||||
|
fun frame(frame: ByteArray, hasHdr: Boolean, outArgb: IntArray): Boolean = synchronized(lock) {
|
||||||
|
globalFrames++
|
||||||
|
if (globalFrames < warmFrames) return false
|
||||||
|
|
||||||
|
var shutterValue = shutter
|
||||||
|
if (hasHdr) {
|
||||||
|
val len = ByteReader.rd32(frame, 8)
|
||||||
|
if (len == npix * 2) {
|
||||||
|
shutterValue = ByteReader.rd32(frame, 0x1C + len + 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shutter = shutterValue
|
||||||
|
|
||||||
|
val st = ffcStep(shutterValue)
|
||||||
|
if (st != 0) {
|
||||||
|
if (st == 2) {
|
||||||
|
val off = if (hasHdr) 0x1C else 0
|
||||||
|
ByteReader.decodeU16(frame, off, npix, nuc)
|
||||||
|
refPush(nuc)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val off = if (hasHdr) 0x1C else 0
|
||||||
|
ByteReader.decodeU16(frame, off, npix, nuc)
|
||||||
|
if (!hasRef) return false
|
||||||
|
|
||||||
|
nucAndBlind(nuc, nuc)
|
||||||
|
statsWindow()
|
||||||
|
lutRebuild()
|
||||||
|
grayMap()
|
||||||
|
upscale2x()
|
||||||
|
// palette colorize 320x240 -> ARGB
|
||||||
|
val pal = OfficialTables.PALETTE256_ARGB
|
||||||
|
var i = 0
|
||||||
|
while (i < npix * 4) {
|
||||||
|
outArgb[i] = pal[gray320[i].toInt() and 0xFF]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setShutter(value: Int) {
|
||||||
|
synchronized(lock) { shutter = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Manual FFC trigger (official FFC button). */
|
||||||
|
fun requestFfc() {
|
||||||
|
synchronized(lock) { manualFfc = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Stats(
|
||||||
|
val winLo: Int, val winHi: Int,
|
||||||
|
val min: Int, val max: Int,
|
||||||
|
val mean: Int, val std: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun stats(): Stats = synchronized(lock) {
|
||||||
|
Stats(winLo, winHi, statMin, statMax, statMean, statStd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasReference(): Boolean = synchronized(lock) { hasRef }
|
||||||
|
fun frameIndex(): Int = synchronized(lock) { ffcIdx }
|
||||||
|
fun endpoint(): Int = synchronized(lock) { sel }
|
||||||
|
|
||||||
|
/** Temperature probe on the current NUC output, in millidegrees C. */
|
||||||
|
fun probeTemp(x: Int, y: Int): Int = synchronized(lock) {
|
||||||
|
TempMath.countsToTempMc(nuc[y * w + x])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read the current NUC counts buffer (already blind-compensated). */
|
||||||
|
fun copyNuc(out: IntArray) = synchronized(lock) {
|
||||||
|
System.arraycopy(nuc, 0, out, 0, minOf(npix, out.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Debug: full intermediate state, same layout as analysis/debug_lut.c. */
|
||||||
|
internal fun debugState(): String = synchronized(lock) {
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append("win $winLo $winHi\n")
|
||||||
|
sb.append("hist")
|
||||||
|
for (v in hist1024) sb.append(' ').append(v)
|
||||||
|
sb.append("\ncdf")
|
||||||
|
for (v in cdf) sb.append(' ').append(v)
|
||||||
|
sb.append("\nlut")
|
||||||
|
for (v in lut) sb.append(' ').append(v.toInt() and 0xFF)
|
||||||
|
sb.append("\ngray")
|
||||||
|
for (v in gray160) sb.append(' ').append(v.toInt() and 0xFF)
|
||||||
|
sb.append("\ngray320")
|
||||||
|
for (v in gray320) sb.append(' ').append(v.toInt() and 0xFF)
|
||||||
|
sb.append('\n')
|
||||||
|
sb.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.mag160c.thermal.core
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temperature math ported from csdk/src/mag160c_render.c counts_to_temp_mc
|
||||||
|
* and csdk/src/mag160c_temp.c t2e_interp. All temperatures are integer
|
||||||
|
* millidegrees Celsius (0.001 C units).
|
||||||
|
*/
|
||||||
|
object TempMath {
|
||||||
|
private const val T2E_OFFSET = 0x249F0
|
||||||
|
private const val TEMP_C = 5797
|
||||||
|
private val T2E = OfficialTables.T2E
|
||||||
|
private val T2E274 = OfficialTables.T2E274
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert NUC counts to millidegrees C via the official T2E curve
|
||||||
|
* (646-entry inverse map, Q12 interpolation).
|
||||||
|
*/
|
||||||
|
fun countsToTempMc(counts: Int): Int {
|
||||||
|
var x = counts.toLong() * 3 - TEMP_C
|
||||||
|
if (x < 0) x = 0
|
||||||
|
var lo = 0
|
||||||
|
var hi = 645
|
||||||
|
while (lo < hi) {
|
||||||
|
val mid = (lo + hi + 1) shr 1
|
||||||
|
if (T2E[mid] <= x) lo = mid else hi = mid - 1
|
||||||
|
}
|
||||||
|
var i = lo
|
||||||
|
if (i > 644) i = 644
|
||||||
|
val diff = x - T2E[i]
|
||||||
|
val t2 = (T2E[i + 1] - T2E[i]).toLong()
|
||||||
|
val slope = if (t2 != 0L) (0x1000000L + t2 / 2) / t2 else 0L
|
||||||
|
val temp = ((slope * diff) shr 12) + (i.toLong() shl 12) - T2E_OFFSET
|
||||||
|
return temp.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* T2E piecewise-linear evaluation with Q13 band selection
|
||||||
|
* (vendor ReviseTemperature/CorrectTemperature core, 274-entry curve).
|
||||||
|
*/
|
||||||
|
fun t2eInterp(value: Int): Int {
|
||||||
|
val v = value + 0xC350
|
||||||
|
var idx = v shr 13
|
||||||
|
if (idx < 0) idx = 0 else if (idx > T2E274.size - 2) idx = T2E274.size - 2
|
||||||
|
val t0 = T2E274[idx]
|
||||||
|
val t1 = T2E274[idx + 1]
|
||||||
|
val d = v - (idx shl 13)
|
||||||
|
val prod = d * (t1 - t0)
|
||||||
|
var interp = prod shr 13
|
||||||
|
if (interp < 0 && (prod and 0x1FFF) != 0) interp += 1
|
||||||
|
return t0 + interp
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.mag160c.thermal.core
|
||||||
|
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Differential test: the Kotlin render pipeline must produce byte-identical
|
||||||
|
* output to the pixel-verified C implementation (csdk/src/mag160c_render.c),
|
||||||
|
* which was validated against CoreSDKLib.dll. Reference files were produced
|
||||||
|
* by analysis/render_offline.c with the official DDT + a synthetic frame
|
||||||
|
* sequence (warm=20, ffc_period=1800, ffc_drift=250, cdf pivot 75, forced
|
||||||
|
* frame-75 startup FFC, FFC callback noop).
|
||||||
|
*/
|
||||||
|
class RenderPipelineTest {
|
||||||
|
private fun res(name: String): ByteArray =
|
||||||
|
javaClass.classLoader.getResourceAsStream(name)!!.readBytes()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matchesCReferencePixelExact() {
|
||||||
|
val ddt = res("mag160c_official.ddt")
|
||||||
|
val frames = res("test_frames.bin")
|
||||||
|
val refRgb = res("test_ref.rgb")
|
||||||
|
val refStats = res("test_ref_stats.txt").decodeToString()
|
||||||
|
|
||||||
|
val pipeline = RenderPipeline(
|
||||||
|
w = 160, h = 120,
|
||||||
|
ffcPeriod = 1800, ffcDrift = 250, warmFrames = 20,
|
||||||
|
cdfPivot75 = true, force75 = true,
|
||||||
|
onFfc = {},
|
||||||
|
)
|
||||||
|
assertTrue("DDT must load", pipeline.loadDdt(ddt))
|
||||||
|
|
||||||
|
val frameSize = 0x38 + 38400
|
||||||
|
val nframes = frames.size / frameSize
|
||||||
|
val out = IntArray(320 * 240)
|
||||||
|
val renderedFrames = ArrayList<IntArray>()
|
||||||
|
for (f in 0 until nframes) {
|
||||||
|
val fr = frames.copyOfRange(f * frameSize, (f + 1) * frameSize)
|
||||||
|
if (pipeline.frame(fr, true, out)) {
|
||||||
|
renderedFrames.add(out.copyOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reference: hidden frames end with "HIDDEN", rendered frames have 6 ints
|
||||||
|
val refRendered = refStats.trim().lines().count { !it.endsWith("HIDDEN") }
|
||||||
|
assertEquals("rendered frame count", refRendered, renderedFrames.size)
|
||||||
|
|
||||||
|
// per-pixel comparison (C writes RGB24; Kotlin produces the same ARGB ints)
|
||||||
|
val rgb = res("test_ref.rgb")
|
||||||
|
var off = 0
|
||||||
|
for (fi in renderedFrames.indices) {
|
||||||
|
val expect = IntArray(320 * 240)
|
||||||
|
for (i in 0 until 320 * 240) {
|
||||||
|
val r = refRgb[off].toInt() and 0xFF
|
||||||
|
val g = refRgb[off + 1].toInt() and 0xFF
|
||||||
|
val b = refRgb[off + 2].toInt() and 0xFF
|
||||||
|
expect[i] = (0xFF shl 24) or (r shl 16) or (g shl 8) or b
|
||||||
|
off += 3
|
||||||
|
}
|
||||||
|
assertArrayEquals("frame $fi pixels", expect, renderedFrames[fi])
|
||||||
|
}
|
||||||
|
assertEquals("consumed all reference bytes", refRgb.size, off)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun tempConversionIsMonotonicAndSane() {
|
||||||
|
// protocol_spec: hand vs background ~1322 counts delta; 0x1000-ish counts
|
||||||
|
// are ambient-scale. Sanity: monotonic and plausible millidegrees.
|
||||||
|
var prev = Int.MIN_VALUE
|
||||||
|
for (counts in 6000..8000 step 10) {
|
||||||
|
val t = TempMath.countsToTempMc(counts)
|
||||||
|
assertTrue("monotonic at $counts", t > prev)
|
||||||
|
prev = t
|
||||||
|
}
|
||||||
|
// ~20-40 C around the 7000-count region (millidegrees)
|
||||||
|
val t20 = TempMath.countsToTempMc(7000)
|
||||||
|
assertTrue("temp in plausible range: $t20", t20 in 0..60_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun frameStreamReassemblesSplitReads() {
|
||||||
|
val frames = res("test_frames.bin")
|
||||||
|
val frameSize = 0x38 + 38400
|
||||||
|
val stream = FrameStream(38400)
|
||||||
|
val out = ByteArray(frameSize)
|
||||||
|
var got = 0
|
||||||
|
var pos = 0
|
||||||
|
var seed = 7
|
||||||
|
while (pos < frames.size) {
|
||||||
|
seed = (seed * 1103515245 + 12345) and 0x7FFFFFFF
|
||||||
|
val n = minOf(1 + (seed % 3000), frames.size - pos)
|
||||||
|
val len = stream.push(frames.copyOfRange(pos, pos + n), n, out)
|
||||||
|
if (len > 0) {
|
||||||
|
for (i in 0 until frameSize) {
|
||||||
|
assertEquals("byte $i at frame $got", frames[got * frameSize + i], out[i])
|
||||||
|
}
|
||||||
|
got++
|
||||||
|
}
|
||||||
|
pos += n
|
||||||
|
}
|
||||||
|
assertEquals("all frames reassembled", frames.size / frameSize, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,60 @@
|
|||||||
|
0 HIDDEN
|
||||||
|
1 HIDDEN
|
||||||
|
2 HIDDEN
|
||||||
|
3 HIDDEN
|
||||||
|
4 HIDDEN
|
||||||
|
5 HIDDEN
|
||||||
|
6 HIDDEN
|
||||||
|
7 HIDDEN
|
||||||
|
8 HIDDEN
|
||||||
|
9 HIDDEN
|
||||||
|
10 HIDDEN
|
||||||
|
11 HIDDEN
|
||||||
|
12 HIDDEN
|
||||||
|
13 HIDDEN
|
||||||
|
14 HIDDEN
|
||||||
|
15 HIDDEN
|
||||||
|
16 HIDDEN
|
||||||
|
17 HIDDEN
|
||||||
|
18 HIDDEN
|
||||||
|
19 HIDDEN
|
||||||
|
20 HIDDEN
|
||||||
|
21 HIDDEN
|
||||||
|
22 HIDDEN
|
||||||
|
23 HIDDEN
|
||||||
|
24 HIDDEN
|
||||||
|
25 HIDDEN
|
||||||
|
26 HIDDEN
|
||||||
|
27 HIDDEN
|
||||||
|
28 HIDDEN
|
||||||
|
29 HIDDEN
|
||||||
|
30 HIDDEN
|
||||||
|
31 HIDDEN
|
||||||
|
32 6961 7961 7118 7668 7461 103
|
||||||
|
33 6962 7962 7098 7663 7462 103
|
||||||
|
34 6962 7962 7116 7675 7462 103
|
||||||
|
35 6961 7961 7106 7672 7461 103
|
||||||
|
36 6961 7961 7106 7659 7461 103
|
||||||
|
37 6961 7961 7116 7664 7461 103
|
||||||
|
38 6961 7961 7121 7671 7461 103
|
||||||
|
39 6962 7962 7117 7673 7462 103
|
||||||
|
40 6962 7962 7132 7667 7462 103
|
||||||
|
41 6962 7962 7105 7664 7462 103
|
||||||
|
42 6961 7961 7118 7671 7461 103
|
||||||
|
43 6961 7961 7130 7671 7461 103
|
||||||
|
44 6962 7962 7122 7661 7462 103
|
||||||
|
45 6961 7961 7096 7669 7461 103
|
||||||
|
46 6961 7961 7096 7671 7461 103
|
||||||
|
47 6962 7962 7122 7675 7462 103
|
||||||
|
48 6961 7961 7094 7667 7461 103
|
||||||
|
49 6962 7962 7099 7664 7462 103
|
||||||
|
50 6962 7962 7134 7669 7462 103
|
||||||
|
51 6962 7962 7133 7662 7462 103
|
||||||
|
52 6961 7961 7129 7671 7461 103
|
||||||
|
53 6962 7962 7126 7671 7462 103
|
||||||
|
54 6961 7961 7119 7669 7461 103
|
||||||
|
55 6961 7961 7096 7673 7461 103
|
||||||
|
56 6961 7961 7135 7661 7461 103
|
||||||
|
57 6962 7962 7099 7662 7462 103
|
||||||
|
58 6961 7961 7122 7659 7461 103
|
||||||
|
59 6962 7962 7116 7669 7462 103
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
org.gradle.caching=true
|
||||||
|
android.useAndroidX=true
|
||||||
|
kotlin.code.style=official
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[versions]
|
||||||
|
agp = "8.11.1"
|
||||||
|
kotlin = "2.2.0"
|
||||||
|
composeBom = "2025.06.01"
|
||||||
|
activityCompose = "1.10.1"
|
||||||
|
coreKtx = "1.16.0"
|
||||||
|
lifecycle = "2.9.1"
|
||||||
|
junit = "4.13.2"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
|
compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
|
compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||||
|
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||||
|
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
|
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||||
|
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
|
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||||
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
Vendored
+251
@@ -0,0 +1,251 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
Vendored
+94
@@ -0,0 +1,94 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "MAG160C"
|
||||||
|
include(":app")
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# MAG160C 官方 APP 逆向功能总表(2026-09-06)
|
||||||
|
|
||||||
|
> 用 jadx 1.5.1 反编译 4 个 APK,IDA(MCP)分析 pro 版 libcoresdk.so(v7a),
|
||||||
|
> Ghidra 12.1.2 headless 分析普通版 libcxsdk.so。反编译工作副本在
|
||||||
|
> `%TEMP%\opencode\apkwork\jadx_*`(临时目录,已删可重跑 jadx 命令见本文末尾)。
|
||||||
|
|
||||||
|
## 1. 四个 APP 身份
|
||||||
|
|
||||||
|
| 包名 | 版本 | 名称 | 原生库 | 定位 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| cn.com.magnity.magnitycx | 1.1.4 (114) | MAG-Cx 普通版 | libcxsdk + FFmpeg (仅 armeabi) | USB 实时拍摄/录像 |
|
||||||
|
| cn.com.magnity.magnitymx | 2.0.7 (27) | MAG-Mx 专业版 | libcoresdk(新版libusb) + libthermogroupsdk (仅 armeabi-v7a) | USB+网络设备、任务巡检、云 |
|
||||||
|
| cn.com.magnity.thermoscope | 1.0.2 (2) | Thermo Scope | libthermogroupsdk (仅 armeabi-v7a) | MDT 热像文件离线分析+PDF 报告 |
|
||||||
|
| cn.com.magnity.coresdksample | 1.0 | CoreSdkSample | libcoresdk (旧) | SDK 示例(无独立功能) |
|
||||||
|
|
||||||
|
- 全部 targetSdk 25(Android 7.1 时代);专业版/普通版 MainActivity 锁定 landscape。
|
||||||
|
- USB:VID 0x833C。普通版 PID=1;专业版同时支持 PID=1(160Core/coresdk) 与 PID=2(Mx 网络机芯)。
|
||||||
|
|
||||||
|
## 2. 普通版 MAG-Cx 功能(jadx_normal/sources/cn/com/magnity/magnitycx)
|
||||||
|
|
||||||
|
**实时画面**:SurfaceView 软件渲染(lockCanvas→drawBitmap),IR 160×120→640×480 合成画布
|
||||||
|
- 12 调色板(白热/黑热/铁虹/彩虹/琥珀/金秋/寒冬/热金属/喷射/红饱和/高对比/红热)DialogFragmentPalette.java
|
||||||
|
- 中心点测温、全屏最高/最低温追踪(无/最高/最低/全部)、矩形 ROI(最值+均值,MAX_ROI_NUM=1)ImageViewer.java
|
||||||
|
- PIP 可见光小窗(手机 Camera API 预览,可拖动/三档尺寸)ImageViewerVisible.java;无像素级融合
|
||||||
|
- 数码变倍 1/2/4/8/16x(160C 仅 1/2/4 循环);双击=手动 FFC;Fling 切拍照/录像
|
||||||
|
- 录像 MP4(FFmpeg avcodec-57 + libcxsdk doRecording,640×480,2048kbps/20fps)
|
||||||
|
- 拍照 JPEG q100 640×480 含 OSD(十字/最值/水印/激光);仅 C3P 另存 .ddt 伴随文件
|
||||||
|
- 横竖屏 OrientationEventListener 实时切 native 旋转角;来电自动断 USB;退后台 500ms deinit
|
||||||
|
|
||||||
|
**USB/协议**(UsbCommunication.java,端点 0x03/0x82/0x81/0x84,28B 帧头尾协议同 csdk)
|
||||||
|
- 连接握手:GetParameter1/2 → GetCaliInfo → 无本地标定则 GetCaliFile 下载(others\{产品}.{SN}.{日期})
|
||||||
|
- 启动:native startProcess(...) + StartTransferImg;ThreadImgRecv/Process 双线程;16 帧一次查开机时长并按时间表 FFC
|
||||||
|
- 命令集 P2D_*(0x6BB6B6xx)与 D2P_*(0x5BB5B5xx),与 csdk/protocol_spec.md 完全一致
|
||||||
|
|
||||||
|
**设置项**:可见光尺寸、温度追踪目标、矩形测温开关、H/V 翻转、变倍、辐射率 4 档(1.00/0.90/0.80/0.70)、帧率高/低、移动光标
|
||||||
|
**媒体库**:GridView+LRU、多选分享/删除、ViewPager 浏览、视频外部播放器
|
||||||
|
**升级**:http://www.magnity.com.cn/APPs/UpgradeInfo(产品键 MAG-Cx)
|
||||||
|
|
||||||
|
## 3. 专业版 MAG-Mx 功能(jadx_pro/sources/cn/com/magnity/magnitymx)
|
||||||
|
|
||||||
|
**双设备模型**:CXDeviceModel(USB coresdk,PID1)+ DeviceModel(网络/云/USB-Mx thermogroup,PID2;命令口 33596、图像口 33597,云中继 121.43.190.114,账密 magnity/any123、admin/admin123)
|
||||||
|
|
||||||
|
**实时画面**(LiveFragment+MainActivity,竖直 LinearLayout)
|
||||||
|
- 工具栏:任务面板、logo 点击=FFC、设置、调参条(tunebar)、PIP 切换(长按=可见光窗口调整)、最高温追踪、缩略图、ROI、拍照(长按=MGS 录像)、暂停、设备列表
|
||||||
|
- tunebar 8 项:手动拉伸/等温线/发射率/调色板/温度报警/任务/红外基准图/扫码/码流
|
||||||
|
- 12 调色板;发射率 35 种材料表+自定义(DialogFragmentEmissivity)
|
||||||
|
- ROI:点/线/框,区域 ROI 弹名称+报警上下限(DialogFragmentRoiAlarmTempAddEdit);报警声+红色闪烁
|
||||||
|
- 可见光 Camera2 融合(alpha 混合、拖动/双指缩放、拍照取 JPEG 写入 MDT);闪光灯
|
||||||
|
- 图像增强:手动两点拉伸/自动拉伸(range5)/等温线;图像旋转 0/90/180/270
|
||||||
|
- 参考图叠加(基准图 alpha 调整,拍照时 blendBitmap 融合)
|
||||||
|
- 码流类型:温度流2/视频流4/混合流6(网络设备)
|
||||||
|
- 拍照 = 640×480 JPG + saveDDT2Buffer + ROI + 可见光 + GPS(EXIF) → **MDT 容器** saveMDT,文件名 01-yyMMddHHmmss-<任务名/扫码结果>.jpg
|
||||||
|
- **录像 = SDK .mgs 专有格式**(仅长按触发,视频流禁用);VideoEncoder MP4 通道是死代码
|
||||||
|
- 拼接全景:仅 CX/USB 通道实现(startStitching)
|
||||||
|
- 云台:WiFi SSID 含 HERO-RC → UDP 8484 协议遥控拍照/模式切换(UDPHelper)
|
||||||
|
- 扫码:ZXing,结果作文件名标记
|
||||||
|
- 音量键拍照;GPS 写 EXIF;云设备 10 分钟闲置断流
|
||||||
|
|
||||||
|
**任务巡检系统**(task/ + assets/tasks)
|
||||||
|
- SqliteTaskParser:.sqlite 库(省-市-工区-电压-变电站五级 + task 树 + capture_status/order)
|
||||||
|
- XmlTaskParser:旧 XML 任务;选库后侧滑两级树,点选→参考图叠加→拍照自动跳下一项
|
||||||
|
- 任务库放 /sdcard/magnity/mx/tasks(首拷自 assets;云下载 v1/account/taskfiles/{taskId})
|
||||||
|
|
||||||
|
**媒体库**(media/):宫格、多选(上传/分享/删除/全选)、NetworkManager 上传队列、单张浏览、"设为参考图"、"分析"= 调起 ThermoScope(intent thermoscope://路径)、GPS 跳地图
|
||||||
|
**云平台**:Retrofit REST(cloudapi.magnity.com.cn):login/logout/getDevices/upload(task_id)/taskfiles;本地 magnity_mx_files.db 记 MD5 上传状态;CloudSyncTask 云同步;MagnityNetworkService 前台服务上传
|
||||||
|
**设置**:账户/相机(仅图像旋转)/语言(自动/简/繁/EN)/主题(占位)/检查更新(magnity.com.cn/APPs/mx)/关于
|
||||||
|
**数据库**:Room cloud_device/local_device + SQLite magnity_mx_files.db
|
||||||
|
|
||||||
|
## 4. ThermoScope 功能(jadx_thermoscope/sources/cn/com/magnity/thermoscope)
|
||||||
|
|
||||||
|
- **MDT 容器格式**(types/):尾部 152B Tail{0x5BB5B57B + 5 段绝对偏移};段序 JPG 缩略图→DDT(136B头+38400B 原始 u16 帧)→VIS 可见光→LAB(每 ROI 256B:type/最值/均值/位置/坐标/色/发射率/报警上下限/名字)→TXT(编码 GB18030/GBK/UTF-8)→AUD;段间 4 字节对齐;支持裸 DDT
|
||||||
|
- 媒体库:扫 DCIM/Magnity/*.jpg(内容实为 MDT),同目录打开模式,按文件名内嵌时间排序
|
||||||
|
- 单张分析(SingleMediaActivity + ScopeSurfaceView 软件渲染):
|
||||||
|
- 11 调色板切换(回写 DDT);参数滑杆:环境温度 -40~80℃ / 发射率 0~1 / 窗口透过率 0~1(native 重算+回写)
|
||||||
|
- 点测温 Pt*(3x3 探针+发射率修正)、框测温 Rt*(min/max/ave+minPos/maxPos)、垂直滑动切红外/可见光
|
||||||
|
- 双指缩放 1~4x、平移;max/min 追踪标记;色标条+6 刻度;保存 640×480 PNG 快照+水印
|
||||||
|
- 相机信息(SN/分辨率/版本/时间/海拔/经纬度);文字备注;语音段透传
|
||||||
|
- PDF 报告(PdfCreator,tejpratapsingh PDFUtil):标题、7×4 基本信息、热像图、9 级色标、整体+ROI 分析表、结果/建议、测试员;输出 DCIM/Magnity/reports/;ReportSettingActivity 15 项报告参数
|
||||||
|
- RelayActivity:外部打开 .mdt/.ddt/.jpg/pdf 分发
|
||||||
|
|
||||||
|
## 5. SDK(cn/com/magnity/coresdk + sdk)
|
||||||
|
|
||||||
|
三代演进:coresdk(USB-only, PID1) → thermogroupsdk 基础版(thermoscope: +网络/云/PTZ/人脸) → 完整版(pro: +GetCameraInfoEx/ObjReco/visPlay_no_decode H.264)
|
||||||
|
- 帧回调只发节拍(newFrame(camState,streamType)),数据用 getOutputImage()/getOutputTempImage(int[])/getTemperatureData(int[]) 拉取
|
||||||
|
- 温度单位:毫度 int(×0.001℃);位图 ARGB_8888(IDA 证实 GetOutputImage |0xFF000000 直写、行倒序)
|
||||||
|
- nativeCopyBitmap:格式/宽高/stride 校验后 qmemcpy
|
||||||
|
- libcxsdk.so(普通版图像核心)导出 C++ API:CFunctions/CAccumulator/CSmoother(与 ARM64 libcoresdk 同族,JNI_OnLoad 动态注册)
|
||||||
|
|
||||||
|
## 6. 花屏(Android 16 上专业版)根因分析
|
||||||
|
|
||||||
|
可确认的事实:
|
||||||
|
1. 专业版仅带 **armeabi-v7a 32 位库**(libcoresdk/libthermogroupsdk),无 arm64。
|
||||||
|
2. targetSdk 25 + MainActivity 锁 landscape + 全部旧版 support/自定义 SurfaceView 渲染。
|
||||||
|
3. 渲染链:native 位图(ARGB_8888,行倒序)→ Bitmap.createBitmap 拷贝 → lockCanvas/drawBitmap → unlockCanvasAndPost;SurfaceHolder 格式 -2(TRANSPARENT),与格式无关的 Canvas 软渲染。
|
||||||
|
4. 现役 2025~2026 旗舰(多数已停 32 位支持或启用 16KB 页内核)上,32 位 v7a 库+4KB 对齐 ELF+legacy 渲染路径极易出现兼容层渲染异常(花屏/缩放模糊/偶发撕裂)。
|
||||||
|
|
||||||
|
结论:不是单一 bug,而是"32 位专属库 + targetSdk25 时代渲染栈"在 Android 16 设备上的整体不兼容。重写为 targetSdk 36 + arm64(16KB 对齐) + 现代渲染即可根治,无需逐条修旧代码。
|
||||||
|
|
||||||
|
## 7. 反编译复现命令
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$w='C:\Users\ZXC\AppData\Local\Temp\opencode\apkwork'
|
||||||
|
& 'C:\Tools\jadx\bin\jadx.bat' -d "$w\jadx_pro" --show-bad-code -j 8 "$w\orig\pro.apk"
|
||||||
|
# Ghidra: & 'C:\Tools\ghidra_12.1.2_PUBLIC\support\analyzeHeadless.bat' C:\Tools\ghidra_proj Mag160C -import <lib> -scriptPath C:\Tools\ghidra_scripts_user -postScript DumpJni.java
|
||||||
|
```
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# MAG160C 安卓统一 APP — 会话状态与心跳锚点
|
||||||
|
|
||||||
|
> 本文件是本任务("整合 4 个官方 APP 重写为 Android 16 现代化 APP")的心跳锚点。
|
||||||
|
> 每完成一个里程碑更新勾选;会话中断先读本文件 + `reverse_apk_features.md`,
|
||||||
|
> 再按"下一步"继续。约定:不弹询问窗,按计划自主推进,重大分歧点才停下来问。
|
||||||
|
|
||||||
|
## 任务目标
|
||||||
|
|
||||||
|
1. 逆向 C:\Project\MAG160C\app 4 个官方 APP(普通版/专业版/ThermoScope/demo)→ 已完成
|
||||||
|
2. 整合全部功能开发一款安卓 APP:适配 Android 16(API 36)、现代 UI、横竖屏、
|
||||||
|
无流氓文件夹、干爽简洁 → **进行中**
|
||||||
|
3. 背景问题:专业版在用户 Android 16 手机上花屏(根因见 reverse_apk_features.md §6)
|
||||||
|
|
||||||
|
## 已完成的里程碑
|
||||||
|
|
||||||
|
- [x] 工具链:jadx 1.5.1(C:\Tools\jadx)、Ghidra 12.1.2(C:\Tools)、IDA MCP 可用
|
||||||
|
- [x] 4 APK 解包 + jadx 全量反编译(%TEMP%\opencode\apkwork\jadx_*)
|
||||||
|
- [x] 普通版/专业版/ThermoScope/demo 功能清单 → docs/android_app/reverse_apk_features.md
|
||||||
|
- [x] IDA:pro libcoresdk.so GetOutputImage/copyBitmap 位图格式证实(ARGB_8888)
|
||||||
|
- [x] Ghidra:libcxsdk.so 导出 C++ API 清单(CFunctions 族,与 ARM64 coresdk 同源)
|
||||||
|
- [x] 花屏根因分析(32 位专属库 + targetSdk25 legacy 渲染栈)
|
||||||
|
|
||||||
|
## 关键事实速查(开发时直接用)
|
||||||
|
|
||||||
|
- USB:VID 0x833C PID 1,EP 0x03/0x82/0x81/0x84,帧 28B 头+38400B+尾,15fps
|
||||||
|
- 协议/渲染/温度算法全部已逆向:analysis/protocol_spec.md + csdk/(逐像素验证过的官方管线)
|
||||||
|
- csdk 的 mag160c_render 无平台依赖:USB 帧 → 320×240 RGB24 + 探针测温(毫度)
|
||||||
|
- DDT 标定文件:可从设备经 0x6BB6B66D/E 下载(官方 prepareProcessImage 同款),仓库有
|
||||||
|
build-artifacts/mag160c_official.ddt(1.8MB,SN 160043865)
|
||||||
|
- MDT 文件格式:reverse_apk_features.md §4(152B Tail + 5 段布局 + ROI 256B 记录)
|
||||||
|
- 温度:毫度 int;MDT 解码走 ConvertResponse2Temperature + T2E(Revise) + CorrectTemperature
|
||||||
|
|
||||||
|
## 用户已确认的范围(2026-09-06)
|
||||||
|
|
||||||
|
- 云功能:预留 Retrofit 接口模块,默认关闭
|
||||||
|
- 扫码打标 + 任务巡检(sqlite/xml) + 红外基准图叠加:v1 全部实现
|
||||||
|
- **网络互连(用户新增需求)**:主机手机插 USB 热像仪,局域网内另一台手机
|
||||||
|
远程实时预览——UDP 广播自动发现 + 手动添加内网 IP;自定义轻量协议
|
||||||
|
(控制通道 JSON + 图像流),不走厂商 33596/33597 协议
|
||||||
|
|
||||||
|
## 当前阶段
|
||||||
|
|
||||||
|
**阶段 2/8 完成:纯Kotlin核心管线移植并逐字节验证(2026-09-06)**
|
||||||
|
|
||||||
|
## 已完成里程碑(本任务)
|
||||||
|
|
||||||
|
- [x] 阶段1:轻量工具链定案——NDK/CMake/zig 全部卸载(-2.4GB),纯 Kotlin 方案:
|
||||||
|
渲染管线+温度算法+帧解析从 C 移植为 Kotlin,Gradle 8.14.3 + SDK 已有组件
|
||||||
|
- [x] 阶段2:项目骨架(android/,AGP 8.11.1 + Kotlin 2.2.0 + Compose BOM,
|
||||||
|
compileSdk/targetSdk 36,minSdk 26),最小APK可构建
|
||||||
|
- [x] 阶段2:表数据自动生成 analysis/gen_kotlin_tables.py → OfficialTables.kt
|
||||||
|
(palette256 ARGB / T2E 646 / T2E274 / E2T_ACC_Q10)
|
||||||
|
- [x] 阶段2:核心移植 FrameStream / RenderPipeline / TempMath(毫度测温)
|
||||||
|
- [x] 阶段2:PC端参考工具 analysis/render_offline.c(gcc 编译)+ 差分测试:
|
||||||
|
**Kotlin 输出与C参考逐字节一致**(60帧序列,首渲染帧32,NUC/LUT/灰度/输出全同)
|
||||||
|
- [x] 阶段2:FrameStream 分块重组测试 + 温度换算单调性测试全过
|
||||||
|
|
||||||
|
## 移植陷阱记录(教训)
|
||||||
|
|
||||||
|
- C 无符号32位回绕:乘积/减法必须 u32() 掩码(cdf*denom、0xffc0000-iv7*0x40000、
|
||||||
|
(v-win_lo)*S 等)
|
||||||
|
- lutRebuild 的 u12 基址是常量 iv7*0x10+0x10,**不是**链式 u21v;u21v 只用于曲线钳制比较
|
||||||
|
- Kotlin ByteArray 存 255 = -1:lut[bfmax] != 0xFF 判断必须 (b.toInt() and 0xFF)
|
||||||
|
- 暖机窗口内 FFC 状态机暂停(warm check 在 ffc_step 之前)——移植时保持顺序
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
阶段3:USB层(UsbManager + Kotlin bulk 传输 + 命令协议)+ 实时画面 UI + 拍照/录像。
|
||||||
|
参考:analysis/protocol_spec.md;文件存储 → MediaStore(DCIM/MAG160C)。
|
||||||
|
|
||||||
|
## 开发计划(待确认后逐阶段执行)
|
||||||
|
|
||||||
|
- 阶段 1:环境(cmdline-tools/NDK r27+/gradle 8.x + 16KB 对齐配置 + AGP 8.x)
|
||||||
|
- 阶段 2:新仓库 app-android(Kotlin + Compose M3 + NDK 移植 csdk 渲染/温度 C 代码 + Kotlin USB 层)
|
||||||
|
- 阶段 3:实时画面(USB 流+官方管线+OSD+ROI+调色板+FFC+增强+PIP 可见光+拍照 MDT+MP4 录像)
|
||||||
|
- 阶段 4:媒体库(MediaStore,DCIM/MAG160C,无流氓文件夹)
|
||||||
|
- 阶段 5:MDT 离线分析(缩放/ROI/参数/PDF 报告/备注)
|
||||||
|
- 阶段 6:设置/任务巡检/可选云(Retrofit opt-in)
|
||||||
|
- 阶段 7:Android 16 专项适配验收(16KB 对齐/横竖屏/深色/预测性返回)
|
||||||
|
- 阶段 8:打包安装到用户手机实测
|
||||||
|
|
||||||
|
## 心跳约定
|
||||||
|
|
||||||
|
- 每完成一个阶段:更新本文件勾选 + git commit(不 push)。
|
||||||
|
- 中断恢复:读本文件 → 按"下一步"继续 → 环境检查命令:
|
||||||
|
```powershell
|
||||||
|
Test-Path C:\Users\ZXC\AppData\Local\Android\Sdk\ndk\<ver>
|
||||||
|
Get-Command java; Get-Command gradle
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user