331 lines
16 KiB
Markdown
331 lines
16 KiB
Markdown
# 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.
|