android: 统一APP工程骨架 + 官方渲染管线Kotlin移植(与C参考逐字节一致)

This commit is contained in:
ZXCLI
2026-09-06 18:44:02 +08:00
parent dd52469377
commit 5b41a6aa9b
27 changed files with 2184 additions and 0 deletions
+52
View File
@@ -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)
}
+23
View File
@@ -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