16 KiB
MAG160C IR Camera Protocol Specification (Recovered via Binary Reverse Engineering)
Recovered from: Linux libmagcore.so.2.1.1 (x86-64 ELF, disassembled with capstone/objdump),
Windows CoreSDKLib.dll / ThermalSDK.dll, and Android libcoresdk.so (ARM64, exported
CFunctions/CAccumulator/CMagDevice symbols). All offsets/opcodes below were verified in
the binaries listed in analysis/ (magcore_disasm.txt, coresdk_arm64_defined.txt, etc).
1. Device Identity & USB Topology
| Item | Value | Evidence |
|---|---|---|
| VID | 0x833C |
MAG_LinkCamera hard-codes 0x833c (libmagcore 0x1d880); link loop compares vid == 0x833c (0x184ed) |
| PID | 0x0001 (default; PID filter can be overridden) |
Hardware Guide; link loop compares pid only when pid!=0 (0x184f6..0x18507) |
| Config value | 2 |
libusb_set_configuration(handle, 2) at 0x1855c |
| Interface | 0 |
libusb_claim_interface(handle, 0) at 0x1857d |
| Kernel driver | auto-detach enabled | libusb_set_auto_detach_kernel_driver(handle, 1) at 0x18554 |
Link sequence (libmagcore 0x18440, called from MAG_LinkCameraEx 0x186b0):
libusb_get_device_list- For each device:
libusb_get_device_descriptor; accept whenvid==0x833cand (pid filter==0 orpid==filter) and (optional bus/addr filters match) libusb_open(handle stored at dev+0x68)libusb_set_auto_detach_kernel_driver(handle, 1)libusb_set_configuration(handle, 2)libusb_claim_interface(handle, 0)- Record timestamp (heartbeat), set open flag dev+0x80=1, pid dev+0x78
2. Endpoints & Bulk Transfer Usage
| Endpoint | Direction | Used for | Timeout | Max length |
|---|---|---|---|---|
0x03 |
OUT | command packets | 0x1f4 = 500 ms | variable (4..0x3c) |
0x82 |
IN | command responses | 0x7d0 = 2000 ms | 0x1000 = 4096 |
0x81 |
IN | frame stream (realtime) | 0x1f4 = 500 ms | 2 × frame-data length |
0x84 |
IN | bulk/large data reads | 0xea60 = 60000 ms | 0x80000 = 524288 |
3. Command / Response Protocol (endpoint pair 0x03 / 0x82)
3.1 Command packet shapes
All commands are written on EP OUT 0x03, then the SDK immediately issues one
libusb_bulk_transfer on EP IN 0x82 reading up to 0x1000 bytes with a 2000 ms timeout.
Success requires the read to return more than 3 bytes.
Two shapes observed:
- 8-byte command:
{u32 magic, u32 param}— used by all0x6bb6b6xxcommands. - 0x3c-byte command:
{u32 magic, 0x38 bytes payload}— used by DDT load/store (0x6bb6b66d/0x6bb6b66e).
Send path: libmagcore 0x189f0 (raw), 0x18ad0 (u32), 0x18b10 (two u32);
response dispatch at 0x187f0.
3.2 Command codes (all observed values)
| Magic | Purpose | Notes |
|---|---|---|
0x6BB6B66B |
prepare/calibration query 1 | sent back-to-back with 0x66c (0x195af) |
0x6BB6B66C |
prepare/calibration query 2 | (0x195c1) |
0x6BB6B66D |
write 0x38-byte payload (e.g. DDT save) | (0x19892) |
0x6BB6B66E |
read 0x38-byte payload (e.g. DDT load) | (0x198e9) |
0x6BB6B66F |
camera info query (PID etc) | response → dev+0xb00.. (0x192e0) |
0x6BB6B670 |
prepare/version query | response → dev+0xb70 (0x19a6c) |
0x6BB6B672 |
trigger FFC | param passed through (0x19250) |
0x6BB6B673 |
start streaming/processing | sent after 50 ms sleep + threads created (0x1909a) |
0x6BB6B674 |
stop | (0x18c13) |
0x6BB6B676 |
set parameter A | (0x19280) |
0x6BB6B677 |
set parameter B | (0x192b0) |
3.3 Response codes (on EP 0x82)
| Magic | Length | Meaning | Disposition |
|---|---|---|---|
0x5BB5B55B |
0x38 | camera info block | copy 0x38 bytes → dev+0xb00 (0x188e0): [0]=pid, [0x10]=width, [0x14]=height, ...; updates per-core name table (core-%d), sets dev+0xac0 count |
0x5BB5B55C |
0x38 | info block (second slot) | copy 0x38 bytes → dev+0xb38 (0x18880); [0x30] also → dev+0xb68 |
0x5BB5B55D |
0x38 | info block (third slot) | (0x15396, DDT blocks) |
0x5BB5B55E |
0x10 | two qwords | qword0 → dev+0xb70, qword1 → dev+0xb78 (0x18851); used by 0x6bb6b66f/0x6bb6b670 responses |
0x5BB5B55F |
0x38 | info block (fourth slot) | (0x154be) |
0x5BB5B57B |
— | DDT/file header marker | used in saved-file format (0x151db) |
Response 0x5BB5B55B payload layout (camera info, 0x38 bytes):
+0x00 u32 pid (compared to dev+0x78)
+0x04 u32 type
+0x08 u64 serial/other
+0x10 u32 width (0xa0 = 160)
+0x14 u32 height (0x78 = 120)
+0x18 u32 ...
+0x1c u32 ...
+0x20 u32 ...
+0x24 u32 ...
+0x28 u32 ...
+0x2c u32 ...
+0x30 u32 ...
+0x34 u32 ...
4. Frame Stream Protocol (endpoint 0x81)
Reader thread (libmagcore 0x170a4):
- loops while running:
libusb_bulk_transfer(handle, EP 0x81, buf, 2*len, &n, 500ms), appends into a2*len + 0x470byte ring buffer (dev+0x328). - Searches (4-byte aligned) for marker
0x1BB1B11B. - When
buffered >= 0x38 + frame_lenand buffer starts with the marker and*(u32*)(buf+8) == frame_lenand*(u32*)(buf+0x1c+frame_len) == 0x1BB1B11C, the frame is accepted and memcpy'd to the frame slot:memcpy(meta+0x4c, buf+0x1c, frame_len). - Signals condvar; dispatcher thread (0x16fb5) copies
frame_len+0x4cbytes to the user buffer (dev+0x50) and invokes the new-frame callbackcb(frame_index, data).
4.1 Frame layout (in-buffer)
+0x00 u32 0x1BB1B11B leading marker
+0x04 u32 frame counter (passed through to callback / stored at dev+8)
+0x08 u32 data length (== frame_len; 0x9600 = 38400 for 160x120 raw uint16)
+0x0c u32 frame type (0 or 1; must be <= 1)
+0x10 u32 period/shutter (a shutter value; SDK stores shutter = frame[0x10] + 0x1f4
when width==160, else frame[0x08])
+0x14 u32 ...
+0x18 u32 ...
+0x1c ... pixel data (frame_len bytes; for 160x120 = 38400 = 19200 × uint16 LE)
+0x1c+frame_len u32 0x1BB1B11C trailing marker
+0x20+frame_len ... tail (rest of the 0x38-byte fixed overhead)
Total frame size = 0x38 + frame_len. Width is validated against the expected value:
0xa0 (160) or 0x180 (384). The per-frame metadata block passed to the higher layer is 0x4c
bytes: {0, frame_len, 0, height, -50000, frame[0x10], shutter, 0, 0, 0, 0} + data.
4.2 Frame dispatch / processing threads
Start (MAG_StartProcessImage → 0x1d8b0 → 0x18f40):
- allocate
2*frame_len + 0x470read buffer (dev+0x328) andframe_len + 0x4cframe slot (dev+0x330) - register user callback (dev+0x48/0x50)
- create dispatcher thread 0x16fb5 (waits condvar, copies frame, calls callback)
- create reader thread 0x170a4 (bulk reads EP 0x81, frame parsing)
usleep(50000)- send command
0x6BB6B673 - set running flag (dev+0x84 = 1)
Stop: send 0x6BB6B674, join threads, free buffers (0x18c10).
5. Start / Stop / FFC / Info API ↔ command mapping
| Public API | Command(s) | Implementation |
|---|---|---|
MAG_LinkCamera* |
none (USB setup only) | 0x18440 |
MAG_PrepareProcessImage |
thread → 0x193f0 (query via 0x6bb6b66f) → 0x19a40 (0x6bb6b670) | 0x19990 |
MAG_StartProcessImage |
0x6BB6B673 |
0x18f40 |
MAG_StopProcessImage |
0x6BB6B674 |
0x18c10 |
MAG_TriggerFFC |
0x6BB6B672 + param (1) |
0x1b850 → 0x19250 |
MAG_SetFFCMode |
stored; mode affects 0x6bb6b673 behavior |
0x1b810 |
MAG_GetCamInfo |
cached copy of dev+0x60..0xb0 (0x50 bytes) | 0x1b230 |
MAG_GetCameraTemperature |
cached dev+0xb38 block | 0x1b030 |
MAG_GetCurrentCameraInnerTemperature |
cached dev+0x... | 0x1aff0 |
MAG_GetSenorTemperature |
cached | 0x1b070 |
MAG_ResetCamera |
0x6BB6B672-style reset path |
0x1bff0 |
| DDT save/load | 0x6BB6B66D / 0x6BB6B66E + 0x38-byte payload |
0x19840 |
Error codes (custom errno domain 0xE4AE0001..): 1 = invalid argument, 2 = no memory,
3 = not ready/already running, 4 = not initialized, 5 = not open/not linked,
6 = not supported, 7 = failed, 8 = ..., 9 = ..., 0x101/0x202/0x205/0x206 = link failures.
6. DDT Saved-File Format (MAG_SaveDDT / MAG_LoadBufferedDDT)
Header 0x5BB5B57B followed by 4-byte-aligned payload (padding with 0x00), then a
sequence of typed blocks, each {u32 magic, u32 aligned_len, data, padding}:
0x5BB5B55B(0x38-byte info block)0x5BB5B55C0x5BB5B55D0x5BB5B55E0x5BB5B55F
7. Temperature Conversion Pipeline (Android libcoresdk.so, ARM64)
Pipeline per frame (CFunctions object at offset base):
PushFrame(frame, srcType) 0x6a4e4
├─ CAccumulator::PushFrame (baseline) 0x62548
└─ Raw2Temperature(frame) 0x6a398 (mode from this+0x1dc4)
├─ mode==1 && flag(0x1cf4): copy baseline ptr, then Calibration(frame) @ 0x6a41c
└─ else: ConvertResponse2Temperature(frame) @ 0x73124
(if flags 0x1200/0x1208 clear → simple path)
└─ BlindCompensation(out) 0x72f30
7.1 Calibration (piecewise-linear per-pixel map) — 0x6a41c
for p in 0..n-1: n = this+0x76d0 (pixel count, e.g. 19200)
diff = (int16)(frame[p] - baseline[p]) >> 1 // baseline from this+0x1ca8 (0 if flag 0x1cc4 clear)
i = 0
while i < cnt-1 and diff > thresh[i]: i++ // thresh = this+0x1610 (per-pixel thresholds, int16)
v = table[i*n*2 + p*2 + 1] + ((diff * table[i*n*2 + p*2]) >> 12) // table = this+0x1628
v = max(v, 0); v = min(v, 0xffff)
out[p] = v // out = this+0x130
then BlindCompensation(out)
cnt = this+0x130c (band count). Threshold table is per-pixel too (advances
cnt-1 entries per pixel).
7.2 ConvertResponse2Temperature (auto-gain) — 0x73124
base = clamp((0xc350 - this+0x76e0) >> (this+0x76e4), 0, 0xffff) // 0xc350 = 50000
coeff = global [0x4028a4]
for p: v = base + (response[p]-baseline[p]) * coeff; v = clamp(v,0,0xffff)
overflow_count += (v saturated)
if overflow_count > 0x101: coeff-- (auto reduce gain, logs "AGAIN")
7.3 Probe temperature read — GetTemperature(pos, mode, lock) @ 0x7252c
- validate pos < pixel count, mode in 1..5 → radius r (1..3)
- window average over
temp[(y+dy)*width + (x+dx)](this+0x140) ReviseTemperature(avg, pos, 1)→ int- if flag this+0x58:
CorrectTemperature(v, emissivity, mode)→ int - invalid →
0xE4AE0001/ INT_MIN
7.4 ReviseTemperature(value, mode, offset) — 0x6cdb8 (PWL via T2E)
if this+0x1638 (calibrated) and emissivity(this+0x1844+ch*0x21c) in (0.01, 0.99):
x = (value << this+0x76e4) + this+0x76e0
i = clamp((x + 0xc350) >> 13, 0, 0x111) // index into T2E
v = T2E[i] + ((x - i<<13) * (T2E[i+1]-T2E[i]) >> 13)
... sensor-temp float mixing: (v_val - (1-s0)*v_sensor) / s0
binary search T2E for result → idx
out = (-0xc350 + idx<<13 - gain + (result-T2E[idx]) * slope[idx] >> 10) >> shift
else:
simple path 0x6d678: x = (value << shift) + gain (no correction)
7.5 CorrectTemperature(value, emissivity, mode) — 0x6d6b0
guard: 0 < this+0xcc <= 1, 0 < emissivity <= 1
v1 = T2E interp(value)
v2 = T2E interp(fcvtzs(this+0xb0 * 1000.0))
s = (this+0xcc) * (this+0xd0) * emissivity
res = (v1 - (1-s)*v2) / s
idx = binary search T2E for res
out = idx*8192 - 0xc350 + ((res - T2E[idx]) * slope2[idx] >> 10)
clamp out <= 0xfffbd502
k = clamp((out - 15000)/100, 0, 6) // offset segmentation
out += (this+0x88 + 4k).off0 + slope*... + fcvtzs(this+0xc4 * out)
Units: 0xc350 = 50000, 0x3a98 = 15000 → the integer result is in
millidegrees Celsius × 0.01 fixed scale (temperature ≈ out/100000 °C × 100 →
see 0x3394f4 = 1000.0f, 0x3394fc = 0.01f), consistent with invalid temp = -1.0e8 in
Java and GetTemperaturemC API.
7.6 T2E table (T2E symbol @ 0x402010)
0x112 monotonic u32 entries, first = 0x3e8 (1000), last = 0xb43bc (737212). Dumped to
analysis/t2e_table.json. A second runtime-initialized slope table is referenced via GOT
0x401f60 (inverse-map slopes per index).
7.7 CAccumulator::PushFrame — 0x62548 (baseline / FFC tracking)
State: counter this+0x20, target this+0x28 (mode), pixel count this+0x24 (>>2), buffers this+0x10 (u16 acc), this+0x18.
- mode 1: memcpy acc ← frame (2×count bytes)
- mode 2: acc[i] = (acc[i]+frame[i]) >> 1
- switch on frame count (3..0x1f states at 0x336220) handles restart; returns 1 when count == target (counter reset)
8. Windows parity notes
CoreSDKLib.dllimportslibusb0.dll(usb_bulk_write/usb_bulk_read/...), exports the sameMAG_*ABI;ThermalSDK.dllwraps it.- Windows frame thread has the same 0x1bb1b11b/0x1bb1b11c parser and the same
SendCmd(EP 0x03) + read(EP 0x82)pattern (strings: "usb error: usb_bulk_read=%d, %s"). - Android
libcoresdk.souses the same magic codes via JNICMagDevice; device open uses the native fd from Android UsbManager (bulk endpoints same).
9. Remaining unknowns
- Exact T2E slope table (GOT 0x401f60) contents — runtime-initialized, likely derived from the loaded DDT/calibration file; structure confirmed, values not in static image.
- Exact payload semantics of
0x6BB6B676/0x6BB6B677. - Frame type 0 vs 1 selection criteria and the
frame[0x10]period/shutter semantics. - Whether camera delivers baseline/DDT over EP 0x84 or via command 0x6BB6B66E.
2026-08-10 Live Hardware Verification (supplement to sections 3-4)
Measured on a real unit (VID 0x833C PID 0x0001, serial 160043865) by intercepting the official Windows demo (libusb0.dll API shim) and by direct libusb-1.0 probing. These override the earlier Linux-SDK-derived assumptions:
- Commands are 4 bytes (magic only) for 66b/66c/66f/673/674. Only FFC (672) uses an 8-byte {magic, param} packet. The Linux SDK sent 8-byte {magic, param} via SendDword, but the unit only accepts 4-byte for the plain commands (8-byte start is STALLed).
- Init sequence: 66b -> 66c -> 66f (4B each) -> FFC(0) x2 -> sleep 300 ms -> START(73). The official demo sends exactly this.
- Info block: 0x5bb5b55b (from 66b) is the camera info: +0x00 pid, +0x10 width=160, +0x14 height=120, +0x18 fps=15. 0x5bb5b55c (from 66c) is a different block; on this unit its +0x10/+0x14 are 5/4 and must not be used as dimensions.
- FFC semantics: FFC(1) switches stream frames to type=0; FFC(0) switches back to type=1. FFC must be issued after a full frame (header + payload). The demo alternates FFC(0)/FFC(1) about every 10 frames.
- Frame stream: 28-byte header + 38400-byte payload + 28-byte tail, 15 fps. Trailer marker 0x1bb1b11c confirmed. Live capture: 60 frames in 4 s.
- Temperature signal: hand vs background ~1322 counts (FOV-covering hand); ~147 counts/C under a 34C-vs-25C assumption; frame-difference rendering shows the hand clearly.
2026-08-10 (evening) - FFC cadence fully decoded (libusb0_trace.txt)
Per-line analysis of the 26092-line official trace (frame = 28B header + 38428B data):
- Init: 66b -> 66c -> 66f -> FFC(0) -> START -> [frame 1] -> FFC(0).
- FFC(1) after ~frame 10 switches the stream to type=0 (frames 11+).
- Sustained cadence: FFC(0), then exactly 9-10 frames later FFC(1), then a variable gap (34..2680 frames) before the next FFC(0). Each FFC toggles the type; type=1 only appears inside the 9-frame window.
- Full trace: 10898 frames, 10519 type=0 (96.5%), 194 read errors, stream never stalls.
- FFC is always issued after a complete frame (header + payload read).
Verified reproduction (hardware, 2026-08-10): scheduler with period 400 frames and gap 9 frames (csdk mag160c_ffc_scheduler_t) sustained 1400+ type=0 frames at 15.1 fps with zero stalls, both in the single-threaded demo loop and in the csdk threaded reader (mag160c_ir_start + scheduler). An earlier 44-frame period caused fps degradation and 17.7% type=1 frames.