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
+6
View File
@@ -0,0 +1,6 @@
.gradle/
build/
local.properties
.idea/
*.iml
captures/
+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
+5
View File
@@ -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
}
+4
View File
@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.caching=true
android.useAndroidX=true
kotlin.code.style=official
+26
View File
@@ -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" }
Binary file not shown.
+7
View File
@@ -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
+251
View File
@@ -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" "$@"
+94
View File
@@ -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
+16
View File
@@ -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")