/* * 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 #include #if MAG160C_HAS_LIBUSB #include #endif #if MAG160C_HAS_THREADS #include #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; }