Files

599 lines
19 KiB
C

/*
* IR camera session, recovered from libmagcore.so.2.1.1:
*
* Link: enumerate VID 0x833C -> open -> set_auto_detach(1) ->
* set_configuration(2) -> claim_interface(0)
* Commands: write 8-byte {u32 magic, u32 param} on EP 0x03, then read up to
* 0x1000 bytes on EP 0x82 with 2000 ms timeout; success requires > 3
* bytes. Response magic 0x5BB5B55B carries the camera info block.
* Start: allocate 2*len+0x470 read buffer, create reader thread (EP 0x81)
* + dispatcher thread, sleep 50 ms, send 0x6BB6B673.
* Stop: send 0x6BB6B674, join threads.
* FFC: 0x6BB6B672 (+param).
*
* 2026-08-10 hardware: commands are 4-byte (magic only); only FFC is 8-byte
* {magic, param}. FFC(1) switches the stream to type=0 frames, FFC(0) back
* to type=1. The official demo alternates FFC(0)/FFC(1) (FFC(1) exactly 9
* frames after FFC(0)) to keep the type=0 stream alive; verified 1400+
* type=0 frames at 15 fps with zero stalls using the mag160c_ffc_scheduler.
*/
#include "mag160c_internal.h"
#include "mag160c/mag160c_display.h"
#include <stdlib.h>
#include <time.h>
#if MAG160C_HAS_LIBUSB
#include <libusb.h>
#endif
#if MAG160C_HAS_THREADS
#include <pthread.h>
#endif
struct mag160c_ctx_t {
int dummy;
};
struct mag160c_ir_t {
#if MAG160C_HAS_LIBUSB
libusb_context *usb; /* kept alive for the lifetime of the handle */
libusb_device_handle *handle;
#endif
uint32_t pid;
uint32_t width;
uint32_t height;
uint32_t frame_len; /* expected frame data length (bytes) */
uint32_t ffc_mode;
uint32_t info[8]; /* 0x5bb5b55b block cache */
int linked;
int running;
int prepared;
#if MAG160C_HAS_THREADS
pthread_t reader_thread;
pthread_t dispatch_thread;
int threads_up;
#endif
mag160c_frame_cb_t frame_cb;
void *frame_cb_user;
/* optional FFC scheduler: replicated official cadence keeps the type=0
* stream alive (verified 1400+ frames). When set, the reader thread
* issues FFC commands after complete frames as the scheduler dictates. */
mag160c_ffc_scheduler_t *ffc_sched;
int ffc_sched_owned;
};
mag160c_error_t mag160c_init(mag160c_ctx_t **out_ctx) {
if (out_ctx == NULL) {
mag160c_set_error("mag160c_init: out_ctx null");
return MAG160C_ERR_INVALID_ARGUMENT;
}
*out_ctx = (mag160c_ctx_t *)calloc(1, sizeof(mag160c_ctx_t));
if (*out_ctx == NULL) {
mag160c_set_error("mag160c_init: allocation failed");
return MAG160C_ERR_NO_MEMORY;
}
mag160c_clear_error();
return MAG160C_OK;
}
void mag160c_shutdown(mag160c_ctx_t *ctx) {
free(ctx);
}
#if MAG160C_HAS_LIBUSB
/* ---- command channel: EP 0x03 write + EP 0x82 read ---------------------- */
static mag160c_error_t cmd_transfer(mag160c_ir_t *ir, const uint8_t *cmd, size_t cmd_len) {
int transferred = 0;
int rc = libusb_bulk_transfer(ir->handle, MAG160C_IR_EP_CMD_OUT,
(uint8_t *)cmd, (int)cmd_len, &transferred, 500);
if (rc != 0 || (size_t)transferred != cmd_len) {
mag160c_set_error("cmd: write EP 0x03 failed (%s)", libusb_error_name(rc));
return MAG160C_ERR_USB;
}
uint8_t resp[0x1000];
rc = libusb_bulk_transfer(ir->handle, MAG160C_IR_EP_CMD_IN, resp, sizeof(resp),
&transferred, 2000);
if (rc != 0) {
mag160c_set_error("cmd: read EP 0x82 failed (%s)", libusb_error_name(rc));
return MAG160C_ERR_USB;
}
if (transferred <= 3) {
mag160c_set_error("cmd: short response (%d bytes)", transferred);
return MAG160C_ERR_BAD_FRAME;
}
const uint32_t magic = (uint32_t)resp[0] | ((uint32_t)resp[1] << 8) |
((uint32_t)resp[2] << 16) | ((uint32_t)resp[3] << 24);
const int payload_len = transferred - 4;
switch (magic) {
case MAG160C_MAG_RSP_INFO_0:
/* 0x5bb5b55b carries the camera info block (pid, width@+0x10,
height@+0x14); the 0x5bb5b55c block has a different layout and
must NOT be parsed as info */
if (payload_len >= (int)sizeof(ir->info)) {
memcpy(ir->info, resp + 4, sizeof(ir->info));
ir->width = ir->info[4]; /* +0x10 */
ir->height = ir->info[5]; /* +0x14 */
ir->frame_len = ir->width * ir->height * 2;
}
break;
default:
break;
}
mag160c_clear_error();
return MAG160C_OK;
}
static mag160c_error_t send_cmd32(mag160c_ir_t *ir, uint32_t magic, uint32_t param) {
uint8_t cmd[8];
cmd[0] = (uint8_t)(magic);
cmd[1] = (uint8_t)(magic >> 8);
cmd[2] = (uint8_t)(magic >> 16);
cmd[3] = (uint8_t)(magic >> 24);
cmd[4] = (uint8_t)(param);
cmd[5] = (uint8_t)(param >> 8);
cmd[6] = (uint8_t)(param >> 16);
cmd[7] = (uint8_t)(param >> 24);
return cmd_transfer(ir, cmd, sizeof(cmd));
}
/* Verified on hardware (WinUSB, VID 0x833C PID 0x0001):
* most commands are 4 bytes (magic only); only FFC (0x6bb6b672) carries an
* 8-byte {magic, param} payload. The official demo sends 66b/66c/66f as
* 4-byte packets and FFC before START, then FFC(0/1 alternating) after every
* ~10 frames. */
static mag160c_error_t send_cmd4(mag160c_ir_t *ir, uint32_t magic) {
uint8_t cmd[4];
cmd[0] = (uint8_t)(magic);
cmd[1] = (uint8_t)(magic >> 8);
cmd[2] = (uint8_t)(magic >> 16);
cmd[3] = (uint8_t)(magic >> 24);
return cmd_transfer(ir, cmd, sizeof(cmd));
}
/* ---- frame reader thread (recovered 0x170a4) ----------------------------- */
typedef struct reader_ctx_t {
mag160c_ir_t *ir;
uint8_t *buf; /* 2*frame_len + 0x470 */
size_t buf_cap;
size_t buf_len;
size_t aligned;
} reader_ctx_t;
static void reader_align(reader_ctx_t *r) {
size_t i = 0;
for (; i + 4 <= r->buf_len; i += 4) {
if (r->buf[i] == 0x1b && r->buf[i + 1] == 0xb1 &&
r->buf[i + 2] == 0xb1 && r->buf[i + 3] == 0x1b) {
break;
}
}
if (i == r->buf_len) {
r->buf_len = 0;
return;
}
if (i != 0) {
memmove(r->buf, r->buf + i, r->buf_len - i);
r->buf_len -= i;
}
r->aligned = 1;
}
static void *reader_thread_fn(void *arg) {
reader_ctx_t *r = (reader_ctx_t *)arg;
mag160c_ir_t *ir = r->ir;
uint8_t tmp[0x8000];
while (ir->running) {
int transferred = 0;
int rc = libusb_bulk_transfer(ir->handle, MAG160C_IR_EP_STREAM_IN, tmp,
(int)sizeof(tmp), &transferred, 500);
if (rc != 0 || transferred <= 0) {
continue;
}
if ((size_t)transferred > r->buf_cap - r->buf_len) {
r->buf_len = 0; /* buffer full: drop and resync */
r->aligned = 0;
}
memcpy(r->buf + r->buf_len, tmp, (size_t)transferred);
r->buf_len += (size_t)transferred;
if (!r->aligned) {
reader_align(r);
}
/* complete frame = 0x38 + data_length */
while (r->aligned && r->buf_len >= MAG160C_FRAME_OVERHEAD) {
const uint32_t len = (uint32_t)r->buf[8] | ((uint32_t)r->buf[9] << 8) |
((uint32_t)r->buf[10] << 16) | ((uint32_t)r->buf[11] << 24);
const size_t total = (size_t)MAG160C_FRAME_OVERHEAD + len;
if (len > r->buf_cap) {
r->buf_len = 0;
r->aligned = 0;
break;
}
if (r->buf_len < total) {
break;
}
const size_t trail = (size_t)MAG160C_FRAME_DATA_OFFSET + len;
if (r->buf[trail] == 0x1c && r->buf[trail + 1] == 0xb1 &&
r->buf[trail + 2] == 0xb1 && r->buf[trail + 3] == 0x1b) {
/* valid frame: notify dispatcher via callback */
if (ir->frame_cb != NULL) {
const uint32_t idx = (uint32_t)r->buf[4] | ((uint32_t)r->buf[5] << 8) |
((uint32_t)r->buf[6] << 16) |
((uint32_t)r->buf[7] << 24);
ir->frame_cb(idx, r->buf, total, ir->frame_cb_user);
}
/* official FFC cadence: tick after every complete frame and
* send the requested FFC command (0/1) immediately */
if (ir->ffc_sched != NULL) {
const int32_t param = mag160c_ffc_scheduler_tick(ir->ffc_sched);
if (param >= 0) {
(void)send_cmd32(ir, MAG160C_MAG_CMD_FFC, (uint32_t)param);
}
}
memmove(r->buf, r->buf + total, r->buf_len - total);
r->buf_len -= total;
if (r->buf_len == 0) {
r->aligned = 0;
}
} else {
r->buf_len = 0;
r->aligned = 0;
break;
}
}
}
return NULL;
}
#endif /* MAG160C_HAS_LIBUSB */
mag160c_error_t mag160c_ir_open(mag160c_ctx_t *ctx, mag160c_ir_t **out_ir) {
if (ctx == NULL || out_ir == NULL) {
mag160c_set_error("mag160c_ir_open: null argument");
return MAG160C_ERR_INVALID_ARGUMENT;
}
*out_ir = NULL;
#if !MAG160C_HAS_LIBUSB
mag160c_set_error("mag160c_ir_open: built without libusb support");
return MAG160C_ERR_NOT_SUPPORTED;
#else
mag160c_ir_t *ir = (mag160c_ir_t *)calloc(1, sizeof(mag160c_ir_t));
if (ir == NULL) {
mag160c_set_error("mag160c_ir_open: allocation failed");
return MAG160C_ERR_NO_MEMORY;
}
ir->pid = 0;
ir->linked = 0;
libusb_context *usb = NULL;
int rc = libusb_init(&usb);
if (rc != 0) {
mag160c_set_error("mag160c_ir_open: libusb_init failed");
free(ir);
return MAG160C_ERR_USB;
}
libusb_device **list = NULL;
const ssize_t count = libusb_get_device_list(usb, &list);
libusb_device_handle *handle = NULL;
for (ssize_t i = 0; i < count && handle == NULL; ++i) {
struct libusb_device_descriptor desc;
if (libusb_get_device_descriptor(list[i], &desc) != 0) {
continue;
}
if (desc.idVendor != MAG160C_IR_VENDOR_ID) {
continue;
}
if (libusb_open(list[i], &handle) != 0) {
continue;
}
libusb_set_auto_detach_kernel_driver(handle, 1);
/* vendor tries config 2 first, then falls back to config 1
(libmagcore 0x1856a -> 0x1865b); this unit has only config 1 */
if (libusb_set_configuration(handle, 2) != 0) {
if (libusb_set_configuration(handle, 1) != 0) {
libusb_close(handle);
handle = NULL;
continue;
}
}
if (libusb_claim_interface(handle, MAG160C_IR_INTERFACE_NUMBER) != 0) {
libusb_close(handle);
handle = NULL;
continue;
}
ir->pid = desc.idProduct;
}
libusb_free_device_list(list, 1);
if (handle == NULL) {
libusb_exit(usb);
mag160c_set_error("mag160c_ir_open: no device with VID 0x%04x",
MAG160C_IR_VENDOR_ID);
free(ir);
return MAG160C_ERR_NOT_OPEN;
}
ir->usb = usb; /* keep context alive; released in mag160c_ir_close */
ir->handle = handle;
ir->linked = 1;
/* query camera info: verified on hardware the sequence is
0x6bb6b66b (4B), 0x6bb6b66c (4B), 0x6bb6b66f (4B) */
(void)send_cmd4(ir, MAG160C_MAG_CMD_PREPARE1);
(void)send_cmd4(ir, MAG160C_MAG_CMD_PREPARE2);
(void)send_cmd4(ir, MAG160C_MAG_CMD_GET_INFO);
*out_ir = ir;
mag160c_clear_error();
return MAG160C_OK;
#endif
}
void mag160c_ir_close(mag160c_ir_t *ir) {
if (ir == NULL) {
return;
}
if (ir->running) {
(void)mag160c_ir_stop(ir);
}
#if MAG160C_HAS_LIBUSB
if (ir->handle != NULL) {
libusb_release_interface(ir->handle, MAG160C_IR_INTERFACE_NUMBER);
libusb_close(ir->handle);
ir->handle = NULL;
}
if (ir->usb != NULL) {
libusb_exit(ir->usb);
ir->usb = NULL;
}
#endif
if (ir->ffc_sched_owned && ir->ffc_sched != NULL) {
free(ir->ffc_sched);
ir->ffc_sched = NULL;
}
free(ir);
}
mag160c_error_t mag160c_ir_is_linked(mag160c_ir_t *ir) {
if (ir == NULL) {
mag160c_set_error("mag160c_ir_is_linked: ir null");
return MAG160C_ERR_INVALID_ARGUMENT;
}
return ir->linked ? MAG160C_OK : MAG160C_ERR_NOT_OPEN;
}
mag160c_error_t mag160c_ir_get_info(mag160c_ir_t *ir, mag160c_ir_info_t *out_info) {
if (ir == NULL || out_info == NULL) {
mag160c_set_error("mag160c_ir_get_info: null argument");
return MAG160C_ERR_INVALID_ARGUMENT;
}
memset(out_info, 0, sizeof(*out_info));
out_info->width = ir->width ? ir->width : 160;
out_info->height = ir->height ? ir->height : 120;
out_info->fpa_width = out_info->width;
out_info->fpa_height = out_info->height;
out_info->pid = ir->pid;
out_info->serial_lo = ir->info[1]; /* +0x08 */
out_info->serial_hi = ir->info[2]; /* +0x10 upper */
out_info->max_fps = 25;
snprintf(out_info->name, sizeof(out_info->name), "MAG-IR-0x%04x", ir->pid);
mag160c_clear_error();
return MAG160C_OK;
}
mag160c_error_t mag160c_ir_set_frame_callback(mag160c_ir_t *ir, mag160c_frame_cb_t cb,
void *user) {
if (ir == NULL) {
mag160c_set_error("mag160c_ir_set_frame_callback: ir null");
return MAG160C_ERR_INVALID_ARGUMENT;
}
ir->frame_cb = cb;
ir->frame_cb_user = user;
return MAG160C_OK;
}
mag160c_error_t mag160c_ir_prepare(mag160c_ir_t *ir) {
if (ir == NULL || !ir->linked) {
mag160c_set_error("mag160c_ir_prepare: not linked");
return MAG160C_ERR_NOT_OPEN;
}
#if !MAG160C_HAS_LIBUSB
(void)ir;
return MAG160C_ERR_NOT_SUPPORTED;
#else
mag160c_error_t e = send_cmd4(ir, MAG160C_MAG_CMD_PREPARE1);
if (e != MAG160C_OK) {
return e;
}
e = send_cmd4(ir, MAG160C_MAG_CMD_PREPARE2);
if (e != MAG160C_OK) {
return e;
}
ir->prepared = 1;
return MAG160C_OK;
#endif
}
mag160c_error_t mag160c_ir_start(mag160c_ir_t *ir) {
if (ir == NULL || !ir->linked) {
mag160c_set_error("mag160c_ir_start: not linked");
return MAG160C_ERR_NOT_OPEN;
}
if (ir->running) {
mag160c_set_error("mag160c_ir_start: already running");
return MAG160C_ERR_NOT_READY;
}
#if !MAG160C_HAS_LIBUSB
(void)ir;
return MAG160C_ERR_NOT_SUPPORTED;
#elif !MAG160C_HAS_THREADS
(void)ir;
mag160c_set_error("mag160c_ir_start: built without thread support");
return MAG160C_ERR_NOT_SUPPORTED;
#else
if (ir->frame_len == 0) {
ir->frame_len = 160 * 120 * 2; /* 38400 default */
}
reader_ctx_t *r = (reader_ctx_t *)calloc(1, sizeof(reader_ctx_t));
if (r == NULL) {
mag160c_set_error("mag160c_ir_start: allocation failed");
return MAG160C_ERR_NO_MEMORY;
}
r->ir = ir;
r->buf_cap = ir->frame_len * 2 + 0x470;
r->buf = (uint8_t *)malloc(r->buf_cap);
if (r->buf == NULL) {
free(r);
mag160c_set_error("mag160c_ir_start: allocation failed");
return MAG160C_ERR_NO_MEMORY;
}
ir->running = 1;
if (pthread_create(&ir->reader_thread, NULL, reader_thread_fn, r) != 0) {
ir->running = 0;
free(r->buf);
free(r);
mag160c_set_error("mag160c_ir_start: pthread_create failed");
return MAG160C_ERR_INTERNAL;
}
ir->threads_up = 1;
/* vendor sleeps 50 ms before issuing START */
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = 50 * 1000 * 1000;
nanosleep(&ts, NULL);
/* verified on hardware: FFC(0) twice before START switches the unit into
the live imaging mode (type=0 frames); the official demo does this */
(void)send_cmd32(ir, MAG160C_MAG_CMD_FFC, 0);
(void)send_cmd32(ir, MAG160C_MAG_CMD_FFC, 0);
ts.tv_nsec = 300 * 1000 * 1000;
nanosleep(&ts, NULL);
mag160c_error_t e = send_cmd4(ir, MAG160C_MAG_CMD_START);
if (e != MAG160C_OK) {
ir->running = 0;
pthread_join(ir->reader_thread, NULL);
ir->threads_up = 0;
free(r->buf);
free(r);
return e;
}
return MAG160C_OK;
#endif
}
mag160c_error_t mag160c_ir_stop(mag160c_ir_t *ir) {
if (ir == NULL || !ir->linked) {
mag160c_set_error("mag160c_ir_stop: not linked");
return MAG160C_ERR_NOT_OPEN;
}
#if !MAG160C_HAS_LIBUSB
(void)ir;
return MAG160C_ERR_NOT_SUPPORTED;
#else
if (ir->running) {
ir->running = 0;
(void)send_cmd4(ir, MAG160C_MAG_CMD_STOP);
#if MAG160C_HAS_THREADS
if (ir->threads_up) {
pthread_join(ir->reader_thread, NULL);
ir->threads_up = 0;
}
#endif
}
return MAG160C_OK;
#endif
}
mag160c_error_t mag160c_ir_trigger_ffc(mag160c_ir_t *ir, uint32_t param) {
if (ir == NULL || !ir->linked) {
mag160c_set_error("mag160c_ir_trigger_ffc: not linked");
return MAG160C_ERR_NOT_OPEN;
}
#if !MAG160C_HAS_LIBUSB
(void)ir;
(void)param;
return MAG160C_ERR_NOT_SUPPORTED;
#else
return send_cmd32(ir, MAG160C_MAG_CMD_FFC, param);
#endif
}
mag160c_error_t mag160c_ir_set_ffc_mode(mag160c_ir_t *ir, uint32_t mode) {
if (ir == NULL || !ir->linked) {
mag160c_set_error("mag160c_ir_set_ffc_mode: not linked");
return MAG160C_ERR_NOT_OPEN;
}
ir->ffc_mode = mode; /* ffc mode stored locally; affects start behavior */
return MAG160C_OK;
}
/* Attach an FFC scheduler. The scheduler is ticked after every complete
* frame in the reader thread; when it returns 0/1 the corresponding FFC
* command is sent (official demo cadence keeps the type=0 stream alive).
* If sched is NULL, the scheduler is disabled. A stack-allocated scheduler
* must outlive the stream; pass own=1 to let the IR session free it. */
mag160c_error_t mag160c_ir_set_ffc_scheduler(mag160c_ir_t *ir,
mag160c_ffc_scheduler_t *sched,
int own) {
if (ir == NULL || !ir->linked) {
mag160c_set_error("mag160c_ir_set_ffc_scheduler: not linked");
return MAG160C_ERR_NOT_OPEN;
}
if (ir->ffc_sched_owned && ir->ffc_sched != NULL) {
free(ir->ffc_sched);
}
ir->ffc_sched = sched;
ir->ffc_sched_owned = own;
mag160c_clear_error();
return MAG160C_OK;
}
mag160c_error_t mag160c_ir_reset(mag160c_ir_t *ir) {
if (ir == NULL || !ir->linked) {
mag160c_set_error("mag160c_ir_reset: not linked");
return MAG160C_ERR_NOT_OPEN;
}
#if !MAG160C_HAS_LIBUSB
(void)ir;
return MAG160C_ERR_NOT_SUPPORTED;
#else
return send_cmd32(ir, MAG160C_MAG_CMD_FFC, 0);
#endif
}
mag160c_error_t mag160c_ir_read_temperature(mag160c_ir_t *ir, uint32_t x, uint32_t y,
int32_t *out_temp) {
(void)x;
(void)y;
if (ir == NULL || out_temp == NULL) {
mag160c_set_error("mag160c_ir_read_temperature: null argument");
return MAG160C_ERR_INVALID_ARGUMENT;
}
*out_temp = 0;
if (!ir->linked) {
return MAG160C_ERR_NOT_OPEN;
}
/* requires the host pipeline; temperature map is maintained by the
* frame callback (mag160c_temp_calibrate). Without a live frame the
* vendor SDK returns 0xe4ae0001. */
mag160c_set_error("mag160c_ir_read_temperature: no temperature map available "
"(feed frames via mag160c_temp_calibrate first)");
return MAG160C_ERR_NOT_READY;
}