Files

331 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.