/* MAG160C Windows Demo - live thermal imaging viewer. * * Features: * - real-time pseudo-color thermal image (160x120 upscaled to 640x480) * - estimated temperature readout (calibrated to a start-up background * reference; ~147 counts/C measured on the reference unit) * - mouse probe temperature, max-temp hotspot marker, center temp * - manual FFC trigger, frame-difference display mode, BMP snapshot * * Build (MinGW): * gcc -std=c11 -mwindows -I mag160c_demo.c \ * -o mag160c_demo.exe -lgdi32 -luser32 -lpthread * copy libusb-1.0.dll next to the exe. */ #define _WIN32_WINNT 0x0601 #include #include #include #include #include static FILE *g_log; /* ---- protocol (verified on hardware) ------------------------------------ */ #define MAG_CMD_PREPARE1 0x6bb6b66b #define MAG_CMD_PREPARE2 0x6bb6b66c #define MAG_CMD_GET_INFO 0x6bb6b66f #define MAG_CMD_FFC 0x6bb6b672 #define MAG_CMD_START 0x6bb6b673 #define MAG_CMD_STOP 0x6bb6b674 #define IR_W 160 #define IR_H 120 #define FRAME_LEN (IR_W * IR_H * 2) /* ---- globals ------------------------------------------------------------ */ static libusb_context *g_usb; static libusb_device_handle *g_dev; static unsigned char g_frame[FRAME_LEN]; static unsigned char g_bmp[54 + IR_W * IR_H * 3]; static char g_status[512]; static volatile int g_new_frame; static volatile unsigned g_frame_count; static volatile unsigned g_frame_type; static int g_diff_mode = 1; /* diff mode by default (best hand contrast) */ static unsigned short g_reference[IR_W * IR_H]; static int g_has_reference; static int g_running; static double g_fps; /* temperature estimate: counts -> deg C, linear about the startup reference */ static double g_counts_per_c = 147.0; /* measured: hand vs background 1322 cts / ~9 C */ static int g_probe_x = -1, g_probe_y = -1; static int g_max_x = -1, g_max_y = -1; static double g_max_temp = -100.0; static double g_center_temp = -100.0; /* ---- helpers ------------------------------------------------------------ */ static int sendcmd(unsigned magic, unsigned param, int len) { if (g_log) { fprintf(g_log, "CMD %08x len=%d param=%u\n", magic, len, param); fflush(g_log); } unsigned char cmd[8] = {0}; cmd[0] = (unsigned char)(magic); cmd[1] = (unsigned char)(magic >> 8); cmd[2] = (unsigned char)(magic >> 16); cmd[3] = (unsigned char)(magic >> 24); if (len >= 8) { cmd[4] = (unsigned char)(param); cmd[5] = (unsigned char)(param >> 8); cmd[6] = (unsigned char)(param >> 16); cmd[7] = (unsigned char)(param >> 24); } int xfer = 0; if (libusb_bulk_transfer(g_dev, 0x03, cmd, len, &xfer, 2000)) return -1; unsigned char resp[0x1000]; if (libusb_bulk_transfer(g_dev, 0x82, resp, sizeof(resp), &xfer, 2000)) return -1; return 0; } static double counts_to_c(unsigned v) { return 25.0 + (v - (int)g_reference[g_probe_x >= 0 ? (g_probe_y * IR_W + g_probe_x) : 0]) / g_counts_per_c; } static void render(const unsigned char *frame) { unsigned hist[65536] = {0}; unsigned nz = 0; for (int i = 0; i < FRAME_LEN; i += 2) { unsigned v = (unsigned)frame[i] | ((unsigned)frame[i + 1] << 8); if (v == 0) continue; hist[v]++; nz++; } unsigned mn = 0, mx = 0, acc = 0; unsigned lo = (unsigned)(nz * 2 / 100); unsigned hi = (unsigned)(nz * 98 / 100); for (unsigned k = 0; k < 65536; ++k) { acc += hist[k]; if (acc >= lo && mn == 0) mn = k; if (acc >= hi && mx == 0) { mx = k; break; } } if (mx <= mn + 50) { mn = 0; mx = 65535; } int hot = 0; unsigned hotv = 0; unsigned center_sum = 0; unsigned char *px = g_bmp + 54; for (int y = 0; y < IR_H; ++y) { for (int x = 0; x < IR_W; ++x) { int o = (y * IR_W + x) * 2; unsigned v = (unsigned)frame[o] | ((unsigned)frame[o + 1] << 8); int diffv = 0; if (g_diff_mode && g_has_reference) { diffv = (int)v - (int)g_reference[y * IR_W + x]; } if (x >= 70 && x < 90 && y >= 50 && y < 70) center_sum += v; if (v > hotv && v != 0) { hotv = v; hot = y * IR_W + x; } unsigned char r, g, b; if (g_diff_mode && g_has_reference) { /* neutral gray background, hand = bright red/orange, cold = blue fixed span 2000: hand occlusion measures ~+1200 counts */ int span = 2000; if (diffv > 0) { unsigned idx = (unsigned)(diffv * 255 / span); if (idx > 255) idx = 255; r = (unsigned char)(128 + idx / 2); g = (unsigned char)(128 - idx / 2); b = (unsigned char)(128 - idx / 2); } else { unsigned idx = (unsigned)(-diffv * 255 / span); if (idx > 255) idx = 255; r = (unsigned char)(128 - idx / 2); g = (unsigned char)(128 - idx / 2); b = (unsigned char)(128 + idx / 2); } } else { unsigned idx = (v - mn) * 255 / (mx - mn + 1); if (idx > 255) idx = 255; if (idx < 64) { r = 0; g = (unsigned char)(idx * 4); b = 255; } else if (idx < 128) { r = 0; g = 255; b = (unsigned char)(255 - (idx - 64) * 4); } else if (idx < 192) { r = (unsigned char)((idx - 128) * 4); g = 255; b = 0; } else { r = 255; g = (unsigned char)(255 - (idx - 192) * 4); b = 0; } } int dst = (IR_H - 1 - y) * IR_W * 3 + x * 3; px[dst + 0] = b; px[dst + 1] = g; px[dst + 2] = r; } } if (hot >= 0) { g_max_x = hot % IR_W; g_max_y = hot / IR_W; g_max_temp = 25.0 + ((int)hotv - (int)g_reference[hot]) / g_counts_per_c; /* draw a marker: white cross on the hottest pixel */ int mx2 = g_max_x, my2 = IR_H - 1 - g_max_y; for (int k = -2; k <= 2; ++k) { if (mx2 + k >= 0 && mx2 + k < IR_W) { int d2 = my2 * IR_W * 3 + (mx2 + k) * 3; px[d2] = 255; px[d2 + 1] = 255; px[d2 + 2] = 255; } if (my2 + k >= 0 && my2 + k < IR_H) { int d2 = (my2 + k) * IR_W * 3 + mx2 * 3; px[d2] = 255; px[d2 + 1] = 255; px[d2 + 2] = 255; } } } g_center_temp = 25.0 + ((int)(center_sum / 400) - (int)g_reference[50 * IR_W + 70]) / g_counts_per_c; } /* ---- window ------------------------------------------------------------- */ static HWND g_hwnd; static HFONT g_font; static void set_status(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vsnprintf(g_status, sizeof(g_status), fmt, ap); va_end(ap); InvalidateRect(g_hwnd, NULL, TRUE); } static void save_bmp(void) { char path[MAX_PATH]; SYSTEMTIME st; GetLocalTime(&st); snprintf(path, sizeof(path), "thermal_%04d%02d%02d_%02d%02d%02d.bmp", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); FILE *f = fopen(path, "wb"); if (f) { fwrite(g_bmp, 1, sizeof(g_bmp), f); fclose(f); set_status("saved %s", path); } } static void do_ffc(void) { sendcmd(MAG_CMD_FFC, 1, 8); Sleep(300); sendcmd(MAG_CMD_FFC, 0, 8); set_status("FFC triggered"); } static LRESULT CALLBACK wndproc(HWND hw, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_PAINT: { PAINTSTRUCT ps; HDC dc = BeginPaint(hw, &ps); /* image */ HDC mem = CreateCompatibleDC(dc); HBITMAP bm = CreateCompatibleBitmap(dc, IR_W, IR_H); HGDIOBJ old = SelectObject(mem, bm); SetDIBitsToDevice(mem, 0, 0, IR_W, IR_H, 0, 0, 0, IR_H, g_bmp + 54, (BITMAPINFO *)(g_bmp + 14), DIB_RGB_COLORS); StretchBlt(dc, 10, 10, 640, 480, mem, 0, 0, IR_W, IR_H, SRCCOPY); SelectObject(mem, old); DeleteObject(bm); DeleteDC(mem); /* text panel */ SelectObject(dc, g_font); SetBkMode(dc, TRANSPARENT); SetTextColor(dc, RGB(220, 220, 220)); int y = 20; char line[256]; snprintf(line, sizeof(line), "frame : %u (type %u)", g_frame_count, g_frame_type); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; snprintf(line, sizeof(line), "fps : %.1f", g_fps); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; if (g_probe_x >= 0) { double t = counts_to_c((unsigned)(g_frame[g_probe_y * IR_W * 2 + g_probe_x * 2] | (g_frame[g_probe_y * IR_W * 2 + g_probe_x * 2 + 1] << 8))); snprintf(line, sizeof(line), "probe : (%d,%d) %.2f C", g_probe_x, g_probe_y, t); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; } if (g_max_x >= 0) { snprintf(line, sizeof(line), "max : (%d,%d) %.2f C", g_max_x, g_max_y, g_max_temp); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; } snprintf(line, sizeof(line), "center: %.2f C", g_center_temp); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; snprintf(line, sizeof(line), "mode : %s", g_diff_mode ? "DIFF (reference)" : "absolute"); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; snprintf(line, sizeof(line), "scale : %.0f counts/C", g_counts_per_c); TextOutA(dc, 670, y, line, (int)strlen(line)); y += 24; SetTextColor(dc, RGB(120, 220, 120)); TextOutA(dc, 10, 500, g_status, (int)strlen(g_status)); EndPaint(hw, &ps); break; } case WM_LBUTTONDOWN: { int x = LOWORD(lp), y = HIWORD(lp); if (x >= 10 && x < 650 && y >= 10 && y < 490) { g_probe_x = (x - 10) * IR_W / 640; g_probe_y = (y - 10) * IR_H / 480; g_probe_y = IR_H - 1 - g_probe_y; InvalidateRect(hw, NULL, TRUE); } break; } case WM_COMMAND: switch (LOWORD(wp)) { case 1001: do_ffc(); break; /* FFC */ case 1002: save_bmp(); break; /* save */ case 1003: g_diff_mode = !g_diff_mode; break; /* diff mode */ case 1004: { /* re-reference */ for (int i = 0; i < FRAME_LEN; i += 2) g_reference[i / 2] = (unsigned short)(g_frame[i] | (g_frame[i + 1] << 8)); g_has_reference = 1; g_diff_mode = 1; set_status("reference captured (press FFC then re-capture for best result)"); break; } case 1005: { /* reset probe */ g_probe_x = -1; InvalidateRect(hw, NULL, TRUE); break; } } break; case WM_ERASEBKGND: return 1; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hw, msg, wp, lp); } static int open_camera(void) { libusb_device **list = NULL; ssize_t cnt = libusb_get_device_list(g_usb, &list); for (ssize_t i = 0; i < cnt && !g_dev; ++i) { struct libusb_device_descriptor d; libusb_get_device_descriptor(list[i], &d); if (d.idVendor == 0x833c) libusb_open(list[i], &g_dev); } libusb_free_device_list(list, 1); if (!g_dev) return -1; libusb_set_configuration(g_dev, 2); libusb_set_configuration(g_dev, 1); return libusb_claim_interface(g_dev, 0); } static int init_camera(void) { for (int attempt = 0; attempt < 4; ++attempt) { if (g_log) { fprintf(g_log, "init attempt %d\n", attempt + 1); fflush(g_log); } { /* hard reset before every attempt: the unit needs a fresh session or the FFC/type switch stalls */ if (g_dev) libusb_reset_device(g_dev); if (g_dev) { libusb_close(g_dev); g_dev = NULL; } Sleep(2500); } if (open_camera() != 0) continue; if (sendcmd(MAG_CMD_PREPARE1, 0, 4)) continue; if (sendcmd(MAG_CMD_PREPARE2, 0, 4)) continue; if (sendcmd(MAG_CMD_GET_INFO, 0, 4)) continue; if (sendcmd(MAG_CMD_FFC, 0, 8)) continue; Sleep(100); if (sendcmd(MAG_CMD_FFC, 0, 8)) continue; Sleep(300); if (sendcmd(MAG_CMD_START, 0, 4)) continue; Sleep(700); unsigned char probe[64]; int xfer = 0, got = 0; for (int k = 0; k < 10; ++k) { if (libusb_bulk_transfer(g_dev, 0x81, probe, sizeof(probe), &xfer, 300) == 0 && xfer >= 28) { unsigned m = (unsigned)probe[0] | ((unsigned)probe[1] << 8) | ((unsigned)probe[2] << 16) | ((unsigned)probe[3] << 24); if (m == 0x1bb1b11b) { got = 1; break; } } } if (got) { if (g_log) { fprintf(g_log, "init ok (attempt %d)\n", attempt + 1); fflush(g_log); } return 0; } if (g_log) { fprintf(g_log, "init no frames, retry\n"); fflush(g_log); } libusb_close(g_dev); g_dev = NULL; } return -1; } int WINAPI WinMain(HINSTANCE inst, HINSTANCE hprev, LPSTR cmd, int show) { (void)hprev; (void)cmd; (void)show; g_log = fopen("C:/Users/ZXC/AppData/Local/Temp/opencode/demo_log.txt", "w"); if (g_log) { fprintf(g_log, "demo start\n"); fflush(g_log); } libusb_init(&g_usb); if (init_camera() != 0) { if (g_log) { fprintf(g_log, "init FAILED\n"); fflush(g_log); } MessageBoxA(NULL, "camera init failed (retried 4x)", "MAG160C Demo", MB_ICONERROR); return 1; } WNDCLASSA wc = {0}; wc.lpfnWndProc = wndproc; wc.hInstance = inst; wc.lpszClassName = "Mag160cDemo"; wc.hCursor = LoadCursor(NULL, IDC_CROSS); wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); RegisterClassA(&wc); g_hwnd = CreateWindowA("Mag160cDemo", "MAG160C Thermal Demo", WS_OVERLAPPEDWINDOW, 60, 40, 1000, 600, NULL, NULL, inst, NULL); g_font = CreateFontA(18, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, "Consolas"); CreateWindowA("BUTTON", "FFC", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 670, 160, 120, 32, g_hwnd, (HMENU)1001, inst, NULL); CreateWindowA("BUTTON", "Save BMP", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 800, 160, 120, 32, g_hwnd, (HMENU)1002, inst, NULL); CreateWindowA("BUTTON", "Diff mode", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, 670, 200, 140, 28, g_hwnd, (HMENU)1003, inst, NULL); CreateWindowA("BUTTON", "Set reference", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 670, 240, 140, 32, g_hwnd, (HMENU)1004, inst, NULL); CreateWindowA("BUTTON", "Clear probe", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 820, 240, 100, 32, g_hwnd, (HMENU)1005, inst, NULL); /* bmp header */ unsigned sz = 54 + IR_W * IR_H * 3; g_bmp[0] = 'B'; g_bmp[1] = 'M'; g_bmp[2] = (unsigned char)sz; g_bmp[3] = (unsigned char)(sz >> 8); g_bmp[4] = (unsigned char)(sz >> 16); g_bmp[5] = (unsigned char)(sz >> 24); g_bmp[10] = 54; g_bmp[14] = 40; g_bmp[18] = IR_W; g_bmp[19] = 0; g_bmp[22] = IR_H; g_bmp[23] = 0; g_bmp[26] = 1; g_bmp[28] = 24; g_running = 1; ShowWindow(g_hwnd, SW_SHOW); set_status("starting..."); /* capture startup background reference */ unsigned prevcnt = 0xffffffff; int frames = 0, ffc_done = 0; unsigned long long ref_sum[IR_W * IR_H] = {0}; int ref_n = 0; DWORD t0 = GetTickCount(); DWORD last_fps_t = t0; unsigned last_fps_cnt = 0; MSG msg; for (;;) { while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) goto done; TranslateMessage(&msg); DispatchMessage(&msg); } unsigned char hdr[64]; int xfer = 0; if (libusb_bulk_transfer(g_dev, 0x81, hdr, sizeof(hdr), &xfer, 500) || xfer < 28) continue; unsigned m = (unsigned)hdr[0] | ((unsigned)hdr[1] << 8) | ((unsigned)hdr[2] << 16) | ((unsigned)hdr[3] << 24); if (m != 0x1bb1b11b) continue; unsigned c = (unsigned)hdr[4] | ((unsigned)hdr[5] << 8) | ((unsigned)hdr[6] << 16) | ((unsigned)hdr[7] << 24); if (c == prevcnt) continue; prevcnt = c; if (libusb_bulk_transfer(g_dev, 0x81, g_frame, sizeof(g_frame), &xfer, 500) || xfer < FRAME_LEN) continue; frames++; if (g_log && (frames % 100) == 0) { fprintf(g_log, "frames=%d cnt=%u\n", frames, c); fflush(g_log); } g_frame_count++; g_frame_type = (unsigned)hdr[12]; /* accumulate reference over the first 30 frames */ /* wait for the stream to stabilize before capturing the reference */ if (!g_has_reference && frames >= 150 && ref_n < 30) { for (int i = 0; i < FRAME_LEN; i += 2) ref_sum[i / 2] += (unsigned short)(g_frame[i] | (g_frame[i + 1] << 8)); ref_n++; if (ref_n == 30) { unsigned long long rs = 0; for (int i = 0; i < IR_W * IR_H; ++i) { g_reference[i] = (unsigned short)(ref_sum[i] / 30); rs += g_reference[i]; } g_has_reference = 1; if (g_log) { fprintf(g_log, "reference captured: avg=%llu\n", rs / 19200); fflush(g_log); } set_status("reference captured (frame 150+) - hand shows red"); } } else if (g_has_reference) { /* slow background tracking: reference follows slow drift so that a hand (fast change) stays visible; 0.5%/frame */ for (int i = 0; i < FRAME_LEN; i += 2) { unsigned v = (unsigned)g_frame[i] | ((unsigned)g_frame[i + 1] << 8); unsigned r = g_reference[i / 2]; g_reference[i / 2] = (unsigned short)((r * 199 + v) / 200); } } if (!ffc_done && frames == 3) { sendcmd(MAG_CMD_FFC, 1, 8); /* switch to type=0 (temperature); same as verified thermal_viewer.c */ ffc_done = 1; Sleep(400); continue; } if (g_log && (g_frame_count % 50) == 0) { unsigned long long sv = 0, sd = 0; int dmax = 0, dmin = 0; for (int i = 0; i < FRAME_LEN; i += 2) { unsigned v = (unsigned)g_frame[i] | ((unsigned)g_frame[i + 1] << 8); sv += v; int d = (int)v - (int)g_reference[i / 2]; sd += (unsigned long long)(d < 0 ? -d : d); if (d > dmax) dmax = d; if (d < dmin) dmin = d; } fprintf(g_log, "frame %u: type=%u avg=%llu ref=%u dmean=%llu dmin=%d dmax=%d\n", g_frame_count, g_frame_type, sv / 19200, (unsigned)g_reference[0], sd / 19200, dmin, dmax); fflush(g_log); } render(g_frame); if ((g_frame_count % 200) == 0) { FILE *tf = fopen("C:/Users/ZXC/AppData/Local/Temp/opencode/render_test.bmp", "wb"); if (tf) { fwrite(g_bmp, 1, sizeof(g_bmp), tf); fclose(tf); } } DWORD now = GetTickCount(); if (now - last_fps_t >= 1000) { g_fps = (g_frame_count - last_fps_cnt) * 1000.0 / (now - last_fps_t); last_fps_t = now; last_fps_cnt = g_frame_count; } InvalidateRect(g_hwnd, NULL, FALSE); UpdateWindow(g_hwnd); } done: g_running = 0; libusb_clear_halt(g_dev, 0x03); libusb_clear_halt(g_dev, 0x82); Sleep(100); sendcmd(MAG_CMD_STOP, 0, 4); libusb_close(g_dev); libusb_exit(g_usb); return 0; }