Files

16 KiB
Raw Permalink Blame History

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):

  1. libusb_get_device_list
  2. For each device: libusb_get_device_descriptor; accept when vid==0x833c and (pid filter==0 or pid==filter) and (optional bus/addr filters match)
  3. libusb_open (handle stored at dev+0x68)
  4. libusb_set_auto_detach_kernel_driver(handle, 1)
  5. libusb_set_configuration(handle, 2)
  6. libusb_claim_interface(handle, 0)
  7. 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 all 0x6bb6b6xx commands.
  • 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 a 2*len + 0x470 byte ring buffer (dev+0x328).
  • Searches (4-byte aligned) for marker 0x1BB1B11B.
  • When buffered >= 0x38 + frame_len and buffer starts with the marker and *(u32*)(buf+8) == frame_len and *(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+0x4c bytes to the user buffer (dev+0x50) and invokes the new-frame callback cb(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):

  1. allocate 2*frame_len + 0x470 read buffer (dev+0x328) and frame_len + 0x4c frame slot (dev+0x330)
  2. register user callback (dev+0x48/0x50)
  3. create dispatcher thread 0x16fb5 (waits condvar, copies frame, calls callback)
  4. create reader thread 0x170a4 (bulk reads EP 0x81, frame parsing)
  5. usleep(50000)
  6. send command 0x6BB6B673
  7. 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)
  • 0x5BB5B55C
  • 0x5BB5B55D
  • 0x5BB5B55E
  • 0x5BB5B55F

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

  1. validate pos < pixel count, mode in 1..5 → radius r (1..3)
  2. window average over temp[(y+dy)*width + (x+dx)] (this+0x140)
  3. ReviseTemperature(avg, pos, 1) → int
  4. if flag this+0x58: CorrectTemperature(v, emissivity, mode) → int
  5. 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.dll imports libusb0.dll (usb_bulk_write/usb_bulk_read/...), exports the same MAG_* ABI; ThermalSDK.dll wraps 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.so uses the same magic codes via JNI CMagDevice; 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:

  1. 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).
  2. Init sequence: 66b -> 66c -> 66f (4B each) -> FFC(0) x2 -> sleep 300 ms -> START(73). The official demo sends exactly this.
  3. 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.
  4. 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.
  5. Frame stream: 28-byte header + 38400-byte payload + 28-byte tail, 15 fps. Trailer marker 0x1bb1b11c confirmed. Live capture: 60 frames in 4 s.
  6. 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):

  1. Init: 66b -> 66c -> 66f -> FFC(0) -> START -> [frame 1] -> FFC(0).
  2. FFC(1) after ~frame 10 switches the stream to type=0 (frames 11+).
  3. 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.
  4. Full trace: 10898 frames, 10519 type=0 (96.5%), 194 read errors, stream never stalls.
  5. 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.