From 5c22bfe78ce6d7bcfd70363651e8629804ed1848 Mon Sep 17 00:00:00 2001 From: ZXCLI Date: Fri, 14 Aug 2026 17:08:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B9=B3=E5=8F=B0=E6=97=A0?= =?UTF-8?q?=E5=85=B3=E6=B8=B2=E6=9F=93=E6=A8=A1=E5=9D=97=20mag160c=5Frende?= =?UTF-8?q?r=20+=20Linux=20=E5=8F=82=E8=80=83=E7=A8=8B=E5=BA=8F=20+=20?= =?UTF-8?q?=E7=A7=BB=E6=A4=8D=E8=AF=84=E4=BC=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - csdk/include/mag160c/mag160c_render.h + csdk/src/mag160c_render.c: 官方渲染管线(快门提取/FFC 状态机/ref 采集/表重建/NUC/盲元/窗口/ LUT(0x8c)/gray/2x/调色板)封装为无平台依赖 API,FFC 命令经回调发出 - csdk/tools/mag160c_linux_demo.c:Linux 风格参考程序(libusb + render API + 显示后端抽象 fbdev/DRM/LVGL),Windows 可编译验证(BMP 输出) - 修复帧组装:第二段 bulk 读 38428B(像素+尾),尾部含快门字 实测 shutter=31492 sel=2 stats 正常 - docs/linux_port_plan.md:芯片选型(推荐 T113-S3 QFP128 非 BGA ~20元)、 动画库(LVGL+rlottie 参考 LiThermal)、显示方案、接口抽象设计 - csdk/README 增加 Linux 移植章节 --- build-artifacts/mag160c_demo3.exe | 2 +- csdk/README.md | 35 +- csdk/include/mag160c/mag160c_render.h | 88 ++++ csdk/src/mag160c_render.c | 665 ++++++++++++++++++++++++++ csdk/tools/mag160c_linux_demo.c | 217 +++++++++ docs/linux_port_plan.md | 149 ++++++ 6 files changed, 1154 insertions(+), 2 deletions(-) create mode 100644 csdk/include/mag160c/mag160c_render.h create mode 100644 csdk/src/mag160c_render.c create mode 100644 csdk/tools/mag160c_linux_demo.c create mode 100644 docs/linux_port_plan.md diff --git a/build-artifacts/mag160c_demo3.exe b/build-artifacts/mag160c_demo3.exe index f151375..e65867b 100644 --- a/build-artifacts/mag160c_demo3.exe +++ b/build-artifacts/mag160c_demo3.exe @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:204a8e37fa0ce7059e0108c1cde276a28a109e4be9e601f0a39a18a1f9d88489 +oid sha256:a85b8a2164ee379b6eda7237976f694d584004d848dfc0000a5159f8b32899c2 size 139578 diff --git a/csdk/README.md b/csdk/README.md index 10a849a..070afeb 100644 --- a/csdk/README.md +++ b/csdk/README.md @@ -207,7 +207,40 @@ Start-Process "C:\Project\MAG160C\build-artifacts\mag160c_demo3.exe" -WorkingDir - 输出日志:`demo3_diag.txt`(每帧统计)、`demo3_ffc.txt`(FFC 事件)、 `demo3_auto_*.bmp`(每 30 帧快照,320×240) -### 5. 官方抓帧工具(tsdk_pair2 / tsdk_pair3) +### 5. Linux 移植(mag160c_render API) + +`include/mag160c/mag160c_render.h` + `src/mag160c_render.c` 是平台无关的 +**官方渲染管线模块**:喂原始 USB 帧 → 输出 320x240 RGB24。内部完整实现 +官方链路(快门提取→FFC 状态机→ref 采集→表重建→NUC→盲元→窗口→LUT→ +gray→2x→调色板),FFC 命令通过回调发给调用方(调用方持有 USB 传输)。 + +```c +mag160c_render_cfg_t cfg = { .width = 160, .height = 120, + .ddt_path = "mag160c_official.ddt", .ffc_period = 1800, + .ffc_drift = 250, .startup_force_ffc = 1, .cdf_pivot_75 = 1, + .ffc_cb = my_ffc_sender, .ffc_user = NULL }; +mag160c_render_init(&r, &cfg); +/* 每帧: */ +mag160c_render_frame(r, full_frame_with_header, 1, rgb320x240); +``` + +`tools/mag160c_linux_demo.c` 是 Linux 风格参考程序(libusb + render API + +display 后端抽象,Windows 上也可编译运行,输出 thermal_*.bmp 验证)。 +Linux 构建: + +```bash +gcc -O2 -Wall csdk/tools/mag160c_linux_demo.c csdk/src/mag160c_render.c \ + csdk/src/mag160c_ir.c csdk/src/mag160c_frame.c csdk/src/mag160c_temp.c \ + csdk/src/mag160c_tcm.c csdk/src/mag160c_error.c csdk/src/mag160c_display.c \ + -lusb-1.0 -lpthread -lm -o mag160c_linux_demo +``` + +显示后端(display_rgb 中)可按目标平台实现:fbdev / DRM-KMS / LVGL image。 +UI 与动画建议:LVGL(界面/交互动画)+ rlottie(Lottie 特效),参考 +LiThermal 项目;主控建议全志 T113-S3(QFP128,非 BGA,~20 元,G2D 2D +加速,Tina Linux)。完整评估见 `../docs/linux_port_plan.md`。 + +### 6. 官方抓帧工具(tsdk_pair2 / tsdk_pair3) 用途:对照验证 demo3 与官方管线。先停 demo3,再运行(同一设备独占): diff --git a/csdk/include/mag160c/mag160c_render.h b/csdk/include/mag160c/mag160c_render.h new file mode 100644 index 0000000..583ecfe --- /dev/null +++ b/csdk/include/mag160c/mag160c_render.h @@ -0,0 +1,88 @@ +/* + * mag160c_render.h - platform-independent official rendering pipeline. + * + * Mirrors the official CoreSDKLib.dll frame path (verified pixel-exact): + * raw frame -> shutter extract -> FFC state machine -> endpoint select + * + Q12 table interp -> ref (4x type-1 mean) -> NUC lookup -> blind + * compensation -> stats/window -> LUT1024 rebuild -> gray -> 2x + * upscale -> palette -> RGB24 output. + * + * No OS/display dependencies: feed it USB frames, get 320x240 RGB24. + * FFC commands are emitted through a callback so the caller owns the + * transport (libusb on Linux, bulk transfers on Windows, ...). + */ +#ifndef MAG160C_RENDER_H +#define MAG160C_RENDER_H + +#include +#include +#include "mag160c.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Default output geometry (official: 2x of the 160x120 sensor). */ +#define MAG160C_RENDER_OUT_W 320 +#define MAG160C_RENDER_OUT_H 240 + +/* FFC command callback: param 0 = FFC(0), 1 = FFC(1). */ +typedef void (*mag160c_render_ffc_cb_t)(void *user, int param); + +typedef struct mag160c_render_t mag160c_render_t; + +typedef struct mag160c_render_cfg_t { + uint16_t width; /* sensor width, 160 */ + uint16_t height; /* sensor height, 120 */ + const char *ddt_path; /* official DDT calibration file */ + int ffc_period; /* frames between FFC pairs, official 1800 */ + int ffc_drift; /* shutter drift threshold, official 250 */ + int startup_force_ffc; /* official frame-75 forced FFC, 1 = on */ + int warm_frames; /* frames dropped after start, official 20 */ + int cdf_pivot_75; /* 0x8c: CDF pivot at 75% cumulative, 1 = on */ + mag160c_render_ffc_cb_t ffc_cb; + void *ffc_user; +} mag160c_render_cfg_t; + +MAG160C_API mag160c_error_t +mag160c_render_init(mag160c_render_t **out, const mag160c_render_cfg_t *cfg); + +/* Process one raw frame. + * frame : USB frame buffer. If has_hdr, the 28-byte header is at + * frame[0] (marker 0x1bb1b11b, counter, len, type, ...) and + * the shutter word is extracted from the tail; otherwise + * frame is the bare 38400-byte pixel buffer and shutter + * must be supplied via mag160c_render_set_shutter(). + * out_rgb : 320x240x3 RGB24 (palette-colored). + * Returns MAG160C_OK when out_rgb was written, MAG160C_ERR_NOT_READY + * while the startup/FFC window is running (image frozen by caller). */ +MAG160C_API mag160c_error_t +mag160c_render_frame(mag160c_render_t *r, const uint8_t *frame, + int has_hdr, uint8_t *out_rgb); + +/* Bare-pixel mode: set the current shutter (device temperature proxy) + * before calling mag160c_render_frame(). */ +MAG160C_API void mag160c_render_set_shutter(mag160c_render_t *r, int shutter); + +/* Per-frame diagnostics for UI overlays. */ +MAG160C_API void mag160c_render_get_stats(mag160c_render_t *r, + int *win_lo, int *win_hi, + int *fmin, int *fmax, + int *mean, int *stddev); +MAG160C_API int mag160c_render_get_shutter(mag160c_render_t *r); +MAG160C_API int mag160c_render_get_endpoint(mag160c_render_t *r); +MAG160C_API int mag160c_render_get_frame_index(mag160c_render_t *r); +MAG160C_API int mag160c_render_has_reference(mag160c_render_t *r); + +/* Temperature probe on the current NUC output (millidegrees C). */ +MAG160C_API mag160c_error_t +mag160c_render_probe_temp(mag160c_render_t *r, uint32_t x, uint32_t y, + int32_t *out_temp_mc); + +MAG160C_API void mag160c_render_destroy(mag160c_render_t *r); + +#ifdef __cplusplus +} +#endif + +#endif /* MAG160C_RENDER_H */ diff --git a/csdk/src/mag160c_render.c b/csdk/src/mag160c_render.c new file mode 100644 index 0000000..58235b9 --- /dev/null +++ b/csdk/src/mag160c_render.c @@ -0,0 +1,665 @@ +/* mag160c_render.c - official pipeline, platform-independent. + * Migrated from csdk/tools/mag160c_demo3.c (pixel-verified vs + * CoreSDKLib.dll). Feed it USB frames; get 320x240 RGB24. */ +#include "mag160c/mag160c_render.h" +#include "mag160c_official_palette256.h" +#include "mag160c_official_t2e.h" + +#include +#include +#include +#include + +#define MAG_T2E_OFFSET 0x249f0 +#define TEMP_C 5797 +#define OFF_N0 4 +#define OFF_N1 5 +#define OFF_N2 4 + +struct mag160c_render_t { + uint16_t w, h; + int npix; + + /* DDT state */ + int ep_count, nsegs, c5c, c60; + int dev24, dev4c_shift, dev48, dev8c, dev47950, dev47948; + int shift; + int ep_temps[64]; + int blind_cnt[64]; + short *ep_thr[64]; + unsigned short *ep_gain[64]; + unsigned int *ep_blind[64]; + int sel; + short *thr_work; + unsigned short *gain_work; + unsigned int *blind_work; + int blind_count; + + /* frame/FFC state */ + int ffc_idx, global_frames, shutter, shutter_at_ffc; + int has_ref, ref_cnt, startup_ffc_done, ref_reset; + unsigned short *ref; + unsigned int *ref_acc; + unsigned short *nuc; + unsigned char *gray160, *gray320; + + /* render state */ + unsigned int hist1024[1024]; + unsigned int cdf[1024]; + unsigned char lut[1024]; + int win_lo, win_hi; + int stat_min, stat_max, stat_mean, stat_std; + int center_prev, mean_prev; + + /* config */ + mag160c_render_ffc_cb_t ffc_cb; + void *ffc_user; + int ffc_period, ffc_drift, warm_frames, cdf_pivot_75, force75; +}; + +static int counts_to_temp_mc(int counts) { + int64_t x = (int64_t)counts * 3 - TEMP_C; + if (x < 0) x = 0; + int lo = 0, hi = 645; + while (lo < hi) { + int mid = (lo + hi + 1) >> 1; + if (mag160c_official_t2e[mid] <= x) lo = mid; + else hi = mid - 1; + } + int i = lo; + if (i > 644) i = 644; + int64_t diff = x - mag160c_official_t2e[i]; + int64_t t2 = mag160c_official_t2e[i + 1] - mag160c_official_t2e[i]; + int64_t slope = t2 ? (0x1000000 + t2 / 2) / t2 : 0; + int64_t temp = ((slope * diff) >> 12) + ((int64_t)i << 12) - MAG_T2E_OFFSET; + return (int)temp; +} + +/* ---------------- DDT loading (official 0x18000f130 v3) ---------------- */ + +static int load_ddt(mag160c_render_t *r, const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) return 0; + unsigned char hdr[256]; + if (fread(hdr, 1, 0x100, f) < 0x98) { fclose(f); return 0; } + unsigned magic = *(unsigned *)(hdr + 0); + unsigned w = *(unsigned *)(hdr + 4), hh = *(unsigned *)(hdr + 8); + if (magic != 0x5aa50003 && magic != 0x5aa50004) { fclose(f); return 0; } + if (w != r->w || hh != r->h) { fclose(f); return 0; } + r->ep_count = *(int *)(hdr + 12); + r->nsegs = *(int *)(hdr + 16); + if (r->ep_count < 2 || r->ep_count > 64 || r->nsegs < 1 || r->nsegs > 8) { + fclose(f); return 0; + } + int v4 = (magic == 0x5aa50004); + r->dev24 = *(int *)(hdr + 20); + r->dev4c_shift = 3; + if (!v4) { + r->c5c = *(int *)(hdr + 28); + r->c60 = *(int *)(hdr + 32); + if (r->c5c > 0x10000) r->c5c = 0; + } else { + r->c5c = *(int *)(hdr + 0x2c + 0x120); + r->c60 = *(int *)(hdr + 0x30 + 0x120); + } + if (r->c5c & 1) r->c5c &= ~1; + int arro = v4 ? 0x120 + 36 : 36; + if (fseek(f, arro, SEEK_SET)) { fclose(f); return 0; } + if (fread(r->ep_temps, 4, r->ep_count, f) != (size_t)r->ep_count) { fclose(f); return 0; } + unsigned tmp[64]; + if (fread(tmp, 4, r->ep_count, f) != (size_t)r->ep_count) { fclose(f); return 0; } + if (fread(tmp, 4, r->ep_count, f) != (size_t)r->ep_count) { fclose(f); return 0; } + if (fread(r->blind_cnt, 4, r->ep_count - 1, f) != (size_t)(r->ep_count - 1)) { fclose(f); return 0; } + r->blind_cnt[r->ep_count - 1] = 0; + for (int i = 0; i < r->ep_count; ++i) + if (r->blind_cnt[i] > 4096) r->blind_cnt[i] = 4096; + + size_t thr_sz = (size_t)(r->nsegs - 1) * r->npix + r->c5c / 2; + size_t gain_sz = (size_t)r->nsegs * r->npix * 2; + for (int e = 0; e < r->ep_count; ++e) { + r->ep_thr[e] = malloc(thr_sz * 2); + r->ep_gain[e] = malloc(gain_sz * 2); + r->ep_blind[e] = malloc((size_t)r->blind_cnt[e] * 40); + if (!r->ep_thr[e] || !r->ep_gain[e] || + (r->blind_cnt[e] && !r->ep_blind[e])) { fclose(f); return 0; } + if (fread(r->ep_thr[e], 2, thr_sz, f) != thr_sz) { fclose(f); return 0; } + if (fread(r->ep_gain[e], 2, gain_sz, f) != gain_sz) { fclose(f); return 0; } + } + for (int e = 0; e < r->ep_count - 1; ++e) { + if (r->blind_cnt[e] && + fread(r->ep_blind[e], 40, r->blind_cnt[e], f) != (size_t)r->blind_cnt[e]) { + fclose(f); return 0; + } + } + fclose(f); + r->shift = 12; + return 1; +} + +static int select_endpoint(mag160c_render_t *r, int shutter) { + int sel = 0; + if (r->ep_count != 2) { + while (sel < r->ep_count - 2 && shutter > r->ep_temps[sel + 1]) sel++; + } + return sel; +} + +static void interp_i16(const short *a, const short *b, short *dst, int n, int t) { + for (int i = 0; i < n; ++i) + dst[i] = (short)((int)a[i] + (((int)b[i] - (int)a[i]) * t >> 12)); +} +static void interp_u16(const unsigned short *a, const unsigned short *b, + unsigned short *dst, int n, int t) { + for (int i = 0; i < n; ++i) + dst[i] = (unsigned short)((int)a[i] + (((int)b[i] - (int)a[i]) * t >> 12)); +} + +static void rebuild_tables(mag160c_render_t *r, int shutter) { + r->sel = select_endpoint(r, shutter); + int sel = r->sel; + int t = ((shutter - r->ep_temps[sel]) << 12) / (r->ep_temps[sel + 1] - r->ep_temps[sel]); + if (t > 0x3fff) t = 0x3fff; + if (t < -0x3fff) t = -0x3fff; + interp_i16(r->ep_thr[sel], r->ep_thr[sel + 1], r->thr_work, + (r->nsegs - 1) * r->npix + r->c5c / 2, t); + interp_u16(r->ep_gain[sel], r->ep_gain[sel + 1], r->gain_work, + r->nsegs * r->npix * 2, t); + r->blind_work = r->ep_blind[sel]; + r->blind_count = r->blind_cnt[sel]; +} + +/* ---------------- ref smoother (mode = 4-frame mean) ---------------- */ + +static void ref_push(mag160c_render_t *r, const unsigned short *frame) { + if (r->ref_cnt == 0) { + for (int i = 0; i < r->npix; ++i) r->ref_acc[i] = frame[i]; + } else { + for (int i = 0; i < r->npix; ++i) r->ref_acc[i] += frame[i]; + } + r->ref_cnt++; + if (r->ref_cnt >= OFF_N0) { + for (int i = 0; i < r->npix; ++i) + r->ref[i] = (unsigned short)(r->ref_acc[i] >> 2); + r->ref_cnt = 0; + r->has_ref = 1; + } +} + +/* ---------------- NUC + blind (0x180017200 + 0x180017330) ---------------- */ + +static void nuc_and_blind(mag160c_render_t *r, const unsigned short *f20, + unsigned short *out) { + const short *thr = r->thr_work; + const unsigned short *gg = r->gain_work; + for (int i = 0; i < r->npix; ++i) { + int d2 = ((int)f20[i] - (int)r->ref[i]) >> 1; + int seg = 0; + if (d2 > thr[i * 2]) { + seg = 1; + if (d2 > thr[i * 2 + 1]) seg = 2; + } + const unsigned short *p = gg + (seg * r->npix + i) * 2; + int v = (int)p[1] + ((int)p[0] * d2 >> 12); + if (v < 0) v = 0; + if (v > 65535) v = 65535; + out[i] = (unsigned short)v; + } + if (r->blind_work && r->blind_count > 0) { + for (int k = 0; k < r->blind_count; ++k) { + const unsigned int *rec = r->blind_work + k * 10; + unsigned target = rec[0], typ = rec[1]; + const unsigned int *ng = rec + 2; + if (target >= (unsigned)r->npix || typ < 3 || typ > 8) continue; + long sum = 0; + for (unsigned j = 0; j < typ; ++j) sum += out[ng[j]]; + if (typ == 8) out[target] = (unsigned short)(sum >> 3); + else if (typ == 4) out[target] = (unsigned short)(sum >> 2); + else out[target] = (unsigned short)(sum / typ); + } + } +} + +/* ---------------- stats + window (0x180010780 + 0x1800109c0) ---------------- */ + +static void stats_window(mag160c_render_t *r, const unsigned short *nuc) { + unsigned long long ssum = 0, s2sum = 0; + unsigned mn = 0xffff, mx = 0; + for (int i = 0; i < r->npix; ++i) { + unsigned v = nuc[i]; + ssum += v; + s2sum += (unsigned long long)v * v; + if (v < mn) mn = v; + if (v > mx) mx = v; + } + r->stat_min = (int)mn; + r->stat_max = (int)mx; + r->stat_mean = (int)(ssum / r->npix); + double dmean = (double)ssum / r->npix; + double var = (double)s2sum / r->npix - dmean * dmean; + r->stat_std = (int)sqrt(var > 0 ? var : 0); + if (mn == mx) { + r->win_lo = r->win_hi = (int)mn; + memset(r->lut, 0x80, sizeof(r->lut)); + memset(r->gray160, 0x80, r->npix); + return; + } + int base = (r->dev24 * 1000) >> r->dev4c_shift; + if (base < 0x80) base = 0x80; + int half = base >> 1; + int lo = r->stat_mean - half; + if ((unsigned)lo > (unsigned)r->stat_min) lo = r->stat_min; + if (lo < 0) lo = 0; + int hi = r->stat_mean + half; + if ((unsigned)hi < (unsigned)r->stat_max) hi = r->stat_max; + if (hi > 65535) hi = 65535; + r->win_lo = lo; + r->win_hi = hi; + if (r->win_hi <= r->win_lo) r->win_hi = r->win_lo + 1; +} + +/* ---------------- LUT1024 rebuild (0x180011950 + 0x180011ee0) ---------------- */ + +static void lut_rebuild(mag160c_render_t *r, const unsigned short *nuc) { + int span = r->win_hi - r->win_lo; + unsigned S = 0xFFC00000u / (unsigned)span; + memset(r->hist1024, 0, sizeof(r->hist1024)); + for (int i = 0; i < r->npix; ++i) { + unsigned idx = ((unsigned)nuc[i] - (unsigned)r->win_lo) * S >> 0x16; + if (idx < 1024) r->hist1024[idx]++; + } + for (int i = 0; i < 1023; i += 3) { + r->hist1024[i] = (r->hist1024[i + 1] + r->hist1024[i]) >> 1; + r->hist1024[i + 1] = (r->hist1024[i + 2] + r->hist1024[i + 1]) >> 1; + r->hist1024[i + 2] = (r->hist1024[i + 3] + r->hist1024[i + 2]) >> 1; + } + unsigned total = 0; + for (int i = 0; i < 1024; ++i) total += r->hist1024[i]; + unsigned u21 = total >> 12; + if (u21 == 0) u21 = 1; + int lres = ((r->stat_mean - r->win_lo) * 0x3ff) / span; + if (lres < 0) lres = 0; + if (lres > 1023) lres = 1023; + if (r->cdf_pivot_75) { + unsigned acc = 0; + int k = 0; + while (k < 1024) { + acc += r->hist1024[k]; + if (acc >= (total * 3 >> 2)) break; + k++; + } + if (k < 1024 && k > lres) lres = k; + } + unsigned u11 = 0, u13 = 0; + for (int k = lres; k >= 0; --k) { + u11 += r->hist1024[k]; + if (u21 <= u11) { u13 += 0x100; u11 = 0; } + r->cdf[k] = ((0x10000 / u21) * u11 >> 8) + u13; + } + u11 = 0; u13 = 0; + for (int k = lres + 1; k < 1024; ++k) { + u11 += r->hist1024[k]; + if (u11 < u21 * 8) { + if (u21 <= u11) { u13 += 0x100; u11 = 0; } + } else { + u13 += 0x200; + u11 = 0; + } + r->cdf[k] = ((0x10000 / u21) * u11 >> 8) + u13; + } + static unsigned curve[1024]; + for (int k = 0; k < 1024; ++k) { + unsigned v = (0x300000u + (unsigned)k * 0x800) >> 0xd; + curve[k] = v ? v : 1; + } + int iv6 = (r->dev48 * 0x155) / 200; + int iv7 = iv6 + 0x155; + int iv26 = iv7; + if (iv6 + 0x156 > 0x331) iv26 = 0x330; + int iv10 = 0x332; + int b3 = 1; + unsigned iv22 = 0xFFC00000u / (unsigned)span; + while (iv26 + 1 < iv10) { + iv7 = (iv10 + iv26) / 2; + if (b3) { b3 = 0; iv7 = iv26; } + unsigned u5 = r->cdf[0]; + if (u5 != 0) { + unsigned u21v = (unsigned)(iv7 * 0x10 + 0x10); + for (int k = lres; k >= 0; --k) { + unsigned u12 = (unsigned)(iv7 * 0x10 + 0x10) - + (r->cdf[k] * ((iv7 * 0x40000u + 0x40000u) / u5) >> 0xe); + if (curve[k] < u21v - u12) u12 = u21v - curve[k]; + r->lut[k] = (unsigned char)(u12 >> 6); + u21v = u12; + } + } + u5 = r->cdf[1023]; + if (u5 != 0) { + unsigned u21v = (unsigned)(iv7 * 0x10); + for (int k = lres + 1; k < 1024; ++k) { + unsigned u12 = (r->cdf[k] * ((0xffc0000u - iv7 * 0x40000u) / u5) >> 0xe) + + (unsigned)(iv7 * 0x10); + if (curve[k] < u12 - u21v) u12 = curve[k] + u21v; + r->lut[k] = (unsigned char)(u12 >> 6); + u21v = u12; + } + } + int iv4 = iv7; + unsigned bfmin = ((unsigned)r->stat_min - (unsigned)r->win_lo) * iv22 >> 0x16; + unsigned bfmax = ((unsigned)r->stat_max - (unsigned)r->win_lo) * iv22 >> 0x16; + if (bfmin < 1024 && bfmax < 1024 && + r->lut[bfmin] == 0 && r->lut[bfmax] != 0xff) { + iv4 = iv10; + iv26 = iv7; + } + iv10 = iv4; + } + int dm = r->stat_mean - r->mean_prev; + if (dm < 0) dm = -dm; + r->mean_prev = r->stat_mean; + int iv18 = iv10 + ((dm & 0xffff) << r->dev4c_shift) * 2 + r->stat_mean; + if (iv18 < 0x9c4) { + if (iv18 < 100) iv18 = 100; + iv7 = (iv18 * iv7 + r->center_prev * 500) / (iv18 + 500); + r->center_prev = iv7; + unsigned u5 = r->cdf[0]; + if (u5 != 0) { + unsigned u21v = (unsigned)(iv7 * 0x10 + 0x10); + for (int k = lres; k >= 0; --k) { + unsigned u12 = (unsigned)(iv7 * 0x10 + 0x10) - + (r->cdf[k] * ((iv7 * 0x40000u + 0x40000u) / u5) >> 0xe); + if (curve[k] < u21v - u12) u12 = u21v - curve[k]; + r->lut[k] = (unsigned char)(u12 >> 6); + u21v = u12; + } + } + u5 = r->cdf[1023]; + if (u5 != 0) { + unsigned u21v = (unsigned)(iv7 * 0x10); + for (int k = lres + 1; k < 1024; ++k) { + unsigned u12 = (r->cdf[k] * ((0xffc0000u - iv7 * 0x40000u) / u5) >> 0xe) + + (unsigned)(iv7 * 0x10); + if (curve[k] < u12 - u21v) u12 = curve[k] + u21v; + r->lut[k] = (unsigned char)(u12 >> 6); + u21v = u12; + } + } + } +} + +/* ---------------- gray + 2x upscale (0x180019740) ---------------- */ + +static void gray_map(mag160c_render_t *r, const unsigned short *nuc) { + unsigned S = 0xFFC00000u / (unsigned)(r->win_hi - r->win_lo); + for (int i = 0; i < r->npix; ++i) { + unsigned v = nuc[i]; + unsigned idx; + if (v <= (unsigned)r->win_lo) idx = 0; + else if (v >= (unsigned)r->win_hi) idx = 1023; + else idx = (v - (unsigned)r->win_lo) * S >> 0x16; + if (idx > 1023) idx = 1023; + r->gray160[i] = r->lut[idx]; + } +} + +static void upscale2x(mag160c_render_t *r) { + const unsigned char *s = r->gray160; + unsigned char *o = r->gray320; + int W = r->w, H = r->h, W2 = W * 2; + for (int y = 0; y < H - 1; ++y) { + const unsigned char *r0 = s + y * W, *r1 = r0 + W; + unsigned char *o0 = o + y * W2 * 2, *o1 = o0 + W2; + int x; + for (x = 0; x < W - 2; x += 2) { + unsigned a0 = r0[x], a1 = r0[x + 1], a2 = r0[x + 2]; + unsigned b0 = r1[x], b1 = r1[x + 1], b2 = r1[x + 2]; + int c = x * 2; + o0[c] = (unsigned char)a0; + o0[c + 1] = (unsigned char)((a0 + a1) >> 1); + o0[c + 2] = (unsigned char)a1; + o0[c + 3] = (unsigned char)((a2 + a1) >> 1); + o1[c] = (unsigned char)((b0 + a0) >> 1); + o1[c + 1] = (unsigned char)((b0 + b1 + a0 + a1) >> 2); + o1[c + 2] = (unsigned char)((b1 + a1) >> 1); + o1[c + 3] = (unsigned char)((b2 + b1 + a2 + a1) >> 2); + } + unsigned a0 = r0[W - 2], a1 = r0[W - 1]; + unsigned b0 = r1[W - 2], b1 = r1[W - 1]; + int c = (W - 2) * 2; + o0[c] = (unsigned char)a0; + o0[c + 1] = (unsigned char)((a1 + a0) >> 1); + o1[c] = (unsigned char)((b0 + a0) >> 1); + o1[c + 1] = (unsigned char)((a1 + b1 + b0 + a0) >> 2); + c = (W - 1) * 2; + o0[c] = (unsigned char)a1; + o0[c + 1] = (unsigned char)((3 * a1 >> 2) + (a0 >> 2)); + o1[c] = (unsigned char)((b1 + a1) >> 1); + o1[c + 1] = (unsigned char)((3 * (b1 + a1) + b0 + a0) >> 3); + } + { + int y = H - 1; + const unsigned char *r0 = s + y * W, *r1 = r0 - W; + unsigned char *o0 = o + y * W2 * 2, *o1 = o0 + W2; + int x; + for (x = 0; x < W - 2; x += 2) { + unsigned a0 = r0[x], a1 = r0[x + 1], a2 = r0[x + 2]; + unsigned u0 = r1[x], u1 = r1[x + 1], u2 = r1[x + 2]; + int c = x * 2; + o0[c] = (unsigned char)a0; + o0[c + 1] = (unsigned char)((a0 + a1) >> 1); + o0[c + 2] = (unsigned char)a1; + o0[c + 3] = (unsigned char)((a2 + a1) >> 1); + o1[c] = (unsigned char)((3 * a0 >> 2) + (u0 >> 2)); + o1[c + 1] = (unsigned char)((3 * (a0 + a1) + u0 + u1) >> 3); + o1[c + 2] = (unsigned char)((3 * a1 >> 2) + (u1 >> 2)); + o1[c + 3] = (unsigned char)((3 * (a2 + a1) + u2 + u1) >> 3); + } + unsigned a0 = r0[W - 2], a1 = r0[W - 1]; + unsigned u0 = r1[W - 2], u1 = r1[W - 1]; + int c = (W - 2) * 2; + o0[c] = (unsigned char)a0; + o0[c + 1] = (unsigned char)((a1 + a0) >> 1); + o1[c] = (unsigned char)((3 * a0 >> 2) + (u0 >> 2)); + o1[c + 1] = (unsigned char)((3 * (a0 + a1) + u0 + u1) >> 3); + c = (W - 1) * 2; + o0[c] = (unsigned char)a1; + o0[c + 1] = (unsigned char)((3 * a1 + a0) >> 2); + o1[c] = (unsigned char)((3 * a1 + u1) >> 2); + o1[c + 1] = (unsigned char)((o0[c + 1] + o1[c] + a1) / 3); + } +} + +/* ---------------- FFC state machine (0x180009ee0) ---------------- + * Returns: 0 = render this frame + * 1 = hidden frame (startup/rebuild window), freeze display + * 2 = ref collection frame (type-1 window): caller must push + * the decoded pixels through ref_push before freezing. */ + +static int ffc_step(mag160c_render_t *r, int shutter) { + if (r->ffc_idx < 0) { + if (r->ffc_cb) r->ffc_cb(r->ffc_user, 0); + r->ffc_idx = 0; + r->shutter_at_ffc = shutter; + } + r->ffc_idx++; + int idx = r->ffc_idx; + if (idx <= OFF_N1) { + if (idx == 1) rebuild_tables(r, r->shutter_at_ffc); + return 1; + } + if (idx <= OFF_N0 + OFF_N1) { + if (idx == OFF_N0 + OFF_N1) { + if (r->ffc_cb) r->ffc_cb(r->ffc_user, 1); + } + if (idx == OFF_N1 + 1) { + r->ref_cnt = 0; + r->has_ref = 0; + } + return 2; /* ref collection window */ + } + if (idx <= OFF_N0 + OFF_N2 + OFF_N1) return 1; + /* normal frame: FFC condition */ + { + int drift = shutter - r->shutter_at_ffc; + if (drift < 0) drift = -drift; + int cool = OFF_N0 + OFF_N2 + OFF_N1; + int do_ffc = 0; + if (r->force75 && !r->startup_ffc_done && idx == 75) { + r->startup_ffc_done = 1; + do_ffc = 1; + } + if (idx >= OFF_N0 + r->ffc_period) + do_ffc = 1; + else if (idx >= cool + 30 && drift > r->ffc_drift) + do_ffc = 1; + if (do_ffc) { + if (r->ffc_cb) r->ffc_cb(r->ffc_user, 0); + r->ffc_idx = 0; + r->shutter_at_ffc = shutter; + return 1; + } + } + return 0; +} + +/* ---------------- public API ---------------- */ + +mag160c_error_t mag160c_render_init(mag160c_render_t **out, + const mag160c_render_cfg_t *cfg) { + if (!out || !cfg || !cfg->ddt_path) return MAG160C_ERR_INVALID_ARGUMENT; + mag160c_render_t *r = calloc(1, sizeof(*r)); + if (!r) return MAG160C_ERR_NO_MEMORY; + r->w = cfg->width ? cfg->width : 160; + r->h = cfg->height ? cfg->height : 120; + r->npix = r->w * r->h; + r->ffc_cb = cfg->ffc_cb; + r->ffc_user = cfg->ffc_user; + r->ffc_period = cfg->ffc_period ? cfg->ffc_period : 1800; + r->ffc_drift = cfg->ffc_drift ? cfg->ffc_drift : 250; + r->warm_frames = cfg->warm_frames ? cfg->warm_frames : 20; + r->cdf_pivot_75 = cfg->cdf_pivot_75; + r->force75 = cfg->startup_force_ffc; + r->dev24 = 5; + r->dev4c_shift = 3; + r->dev48 = 0; + r->dev47950 = 0; + r->dev47948 = -50000; + r->ffc_idx = -1; + r->center_prev = 341; + r->mean_prev = 0; + + r->thr_work = malloc((r->npix * 2 + 4096) * sizeof(short)); + r->gain_work = malloc((size_t)r->npix * 6 * sizeof(unsigned short)); + r->ref = malloc((size_t)r->npix * 2); + r->ref_acc = malloc((size_t)r->npix * 4); + r->nuc = malloc((size_t)r->npix * 2); + r->gray160 = malloc((size_t)r->npix); + r->gray320 = malloc((size_t)r->npix * 4); + if (!r->thr_work || !r->gain_work || !r->ref || !r->ref_acc || + !r->nuc || !r->gray160 || !r->gray320) { + mag160c_render_destroy(r); + return MAG160C_ERR_NO_MEMORY; + } + if (!load_ddt(r, cfg->ddt_path)) { + mag160c_render_destroy(r); + return MAG160C_ERR_NOT_INITIALIZED; + } + *out = r; + return MAG160C_OK; +} + +mag160c_error_t mag160c_render_frame(mag160c_render_t *r, const uint8_t *frame, + int has_hdr, uint8_t *out_rgb) { + if (!r || !frame || !out_rgb) return MAG160C_ERR_INVALID_ARGUMENT; + r->global_frames++; + if (r->global_frames < r->warm_frames) return MAG160C_ERR_NOT_READY; + + int shutter = r->shutter; + if (has_hdr) { + unsigned len = (unsigned)frame[8] | ((unsigned)frame[9] << 8) | + ((unsigned)frame[10] << 16) | ((unsigned)frame[11] << 24); + if (len == (unsigned)(r->npix * 2)) { + const uint8_t *tail = frame + 0x1c + len; + shutter = (int)((unsigned)tail[8] | ((unsigned)tail[9] << 8) | + ((unsigned)tail[10] << 16) | ((unsigned)tail[11] << 24)); + } + } + r->shutter = shutter; + + int st = ffc_step(r, shutter); + if (st != 0) { + if (st == 2) { + /* ref window: push decoded pixels (type-1 calibration frames) */ + const uint8_t *px = has_hdr ? frame + 0x1c : frame; + for (int i = 0; i < r->npix; ++i) + r->nuc[i] = (unsigned short)(px[i * 2] | ((unsigned)px[i * 2 + 1] << 8)); + ref_push(r, r->nuc); + } + return MAG160C_ERR_NOT_READY; + } + + /* decode pixels (frame + 0x1c when header present) */ + const uint8_t *px = has_hdr ? frame + 0x1c : frame; + unsigned short *live = r->nuc; + for (int i = 0; i < r->npix; ++i) + live[i] = (unsigned short)(px[i * 2] | ((unsigned)px[i * 2 + 1] << 8)); + + if (!r->has_ref) return MAG160C_ERR_NOT_READY; + + nuc_and_blind(r, live, r->nuc); + stats_window(r, r->nuc); + lut_rebuild(r, r->nuc); + gray_map(r, r->nuc); + upscale2x(r); + /* palette colorize 320x240 */ + for (int i = 0; i < r->npix * 4; ++i) { + unsigned char gv = r->gray320[i]; + out_rgb[i * 3 + 0] = mag160c_official_palette256[gv][0]; + out_rgb[i * 3 + 1] = mag160c_official_palette256[gv][1]; + out_rgb[i * 3 + 2] = mag160c_official_palette256[gv][2]; + } + return MAG160C_OK; +} + +void mag160c_render_set_shutter(mag160c_render_t *r, int shutter) { + if (r) r->shutter = shutter; +} + +void mag160c_render_get_stats(mag160c_render_t *r, int *win_lo, int *win_hi, + int *fmin, int *fmax, int *mean, int *stddev) { + if (!r) return; + if (win_lo) *win_lo = r->win_lo; + if (win_hi) *win_hi = r->win_hi; + if (fmin) *fmin = r->stat_min; + if (fmax) *fmax = r->stat_max; + if (mean) *mean = r->stat_mean; + if (stddev) *stddev = r->stat_std; +} + +int mag160c_render_get_shutter(mag160c_render_t *r) { return r ? r->shutter : 0; } +int mag160c_render_get_endpoint(mag160c_render_t *r) { return r ? r->sel : 0; } +int mag160c_render_get_frame_index(mag160c_render_t *r) { return r ? r->ffc_idx : 0; } +int mag160c_render_has_reference(mag160c_render_t *r) { return r ? r->has_ref : 0; } + +mag160c_error_t mag160c_render_probe_temp(mag160c_render_t *r, uint32_t x, uint32_t y, + int32_t *out_temp_mc) { + if (!r || !out_temp_mc || x >= r->w || y >= r->h) return MAG160C_ERR_INVALID_ARGUMENT; + *out_temp_mc = counts_to_temp_mc(r->nuc[y * r->w + x]); + return MAG160C_OK; +} + +void mag160c_render_destroy(mag160c_render_t *r) { + if (!r) return; + for (int e = 0; e < r->ep_count; ++e) { + free(r->ep_thr[e]); + free(r->ep_gain[e]); + free(r->ep_blind[e]); + } + free(r->thr_work); + free(r->gain_work); + free(r->ref); + free(r->ref_acc); + free(r->nuc); + free(r->gray160); + free(r->gray320); + free(r); +} diff --git a/csdk/tools/mag160c_linux_demo.c b/csdk/tools/mag160c_linux_demo.c new file mode 100644 index 0000000..30e5183 --- /dev/null +++ b/csdk/tools/mag160c_linux_demo.c @@ -0,0 +1,217 @@ +/* mag160c_linux_demo.c - Linux-oriented reference app. + * + * Uses the platform-independent mag160c_render API. On Windows this + * builds/runs too (no GDI): output is written to thermal.bmp every 30 + * frames. On Linux the same code path is used; only the display + * backend differs (fbdev/DRM/LVGL - see DISPLAY_BACKEND below). + * + * Build (Windows, same toolchain as demo3): + * gcc -O2 -w -DMAG160C_STATIC -I"csdk\third_party\libusb\win64" + * -I"csdk\include" -I"csdk\src" -o build-artifacts\mag160c_linux_demo.exe + * csdk\tools\mag160c_linux_demo.c csdk\src\mag160c_render.c + * csdk\src\mag160c_ir.c csdk\src\mag160c_frame.c csdk\src\mag160c_temp.c + * csdk\src\mag160c_tcm.c csdk\src\mag160c_error.c csdk\src\mag160c_display.c + * csdk\third_party\libusb\win64\libusb-1.0.x64.a + * Build (Linux): + * gcc -O2 -Wall csdk/tools/mag160c_linux_demo.c csdk/src/mag160c_render.c + * csdk/src/mag160c_ir.c csdk/src/mag160c_frame.c csdk/src/mag160c_temp.c + * csdk/src/mag160c_tcm.c csdk/src/mag160c_error.c csdk/src/mag160c_display.c + * -lusb-1.0 -lpthread -lm -o mag160c_linux_demo + */ +#define _WIN32_WINNT 0x0601 +#include +#include +#include +#include +#ifdef _WIN32 +#include +#define SLEEP_MS(ms) Sleep(ms) +#else +#include +#include +#define SLEEP_MS(ms) usleep((ms) * 1000) +#endif +#include +#include "mag160c/mag160c.h" +#include "mag160c/mag160c_render.h" +#include "mag160c/mag160c_display.h" + +#define W 160 +#define H 120 +#define OUT_W 320 +#define OUT_H 240 + +static libusb_context *g_ctx; +static libusb_device_handle *g_h; + +static int sendcmd(unsigned magic, unsigned param, int len) { + 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_h, 0x03, cmd, len, &xfer, 2000)) return -1; + unsigned char resp[0x1000]; + if (libusb_bulk_transfer(g_h, 0x82, resp, sizeof(resp), &xfer, 2000)) return -1; + return 0; +} + +static void ffc_cb(void *user, int param) { + (void)user; + if (g_h) sendcmd(0x6bb6b672, (unsigned)param, 8); +} + +/* ---- display backend abstraction ---- + * Windows: no-op (terminal stats). Linux: implement one of: + * A. fbdev: open("/dev/fb0"), mmap, memcpy RGB (convert to RGB565) + * B. DRM/KMS: drmModeSetCrtc + dumb buffer + * C. LVGL: lv_img_set_src + lv_obj_invalidate (best for UI/animation) + */ +static void display_rgb(const uint8_t *rgb, int w, int h) { + (void)rgb; (void)w; (void)h; + /* Linux example (fbdev RGB565): + * static int fd = -1; static unsigned char *fb; static int fbw, fbh; + * ... open/mmap once, then: + * for (y) for (x) { u16 v = ((rgb[y*w*3+x*3]>>3)<<11) | + * ((rgb[y*w*3+x*3+1]>>2)<<5) | + * (rgb[y*w*3+x*3+2]>>3); + * *(u16*)(fb + y*fbw*2 + x*2) = v; } + */ +} + +static void save_bmp(const char *path, const uint8_t *rgb, int w, int h) { + uint32_t sz = 54 + (uint32_t)w * h * 3; + uint8_t bmp[54]; + memset(bmp, 0, 54); + bmp[0] = 'B'; bmp[1] = 'M'; + bmp[2] = (uint8_t)sz; bmp[3] = (uint8_t)(sz >> 8); + bmp[4] = (uint8_t)(sz >> 16); bmp[5] = (uint8_t)(sz >> 24); + bmp[10] = 54; bmp[14] = 40; + bmp[18] = (uint8_t)w; bmp[19] = (uint8_t)(w >> 8); + bmp[20] = (uint8_t)(w >> 16); bmp[21] = (uint8_t)(w >> 24); + bmp[22] = (uint8_t)h; bmp[23] = (uint8_t)(h >> 8); + bmp[24] = (uint8_t)(h >> 16); bmp[25] = (uint8_t)(h >> 24); + bmp[26] = 1; bmp[28] = 24; + FILE *f = fopen(path, "wb"); + if (!f) return; + fwrite(bmp, 1, 54, f); + for (int y = h - 1; y >= 0; --y) + fwrite(rgb + (size_t)y * w * 3, 1, (size_t)w * 3, f); + fclose(f); +} + +int main(int argc, char **argv) { + const char *ddt = argc > 1 ? argv[1] : "mag160c_official.ddt"; + libusb_init(&g_ctx); + + mag160c_render_cfg_t cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.width = W; + cfg.height = H; + cfg.ddt_path = ddt; + cfg.ffc_period = 1800; + cfg.ffc_drift = 250; + cfg.startup_force_ffc = 1; + cfg.warm_frames = 20; + cfg.cdf_pivot_75 = 1; + cfg.ffc_cb = ffc_cb; + mag160c_render_t *render = NULL; + if (mag160c_render_init(&render, &cfg) != MAG160C_OK) { + fprintf(stderr, "render init failed (need %s)\n", ddt); + return 1; + } + + int ok = 0; + for (int attempt = 0; attempt < 4 && !ok; ++attempt) { + if (attempt > 0) { + if (g_h) libusb_reset_device(g_h); + if (g_h) { libusb_close(g_h); g_h = NULL; } + SLEEP_MS(2500); + } + libusb_device **list = NULL; + ssize_t cnt = libusb_get_device_list(g_ctx, &list); + for (ssize_t i = 0; i < cnt && !g_h; ++i) { + struct libusb_device_descriptor d; + libusb_get_device_descriptor(list[i], &d); + if (d.idVendor == 0x833c) libusb_open(list[i], &g_h); + } + libusb_free_device_list(list, 1); + if (!g_h) continue; + libusb_set_configuration(g_h, 2); + libusb_set_configuration(g_h, 1); + if (libusb_claim_interface(g_h, 0) != 0) { libusb_close(g_h); g_h = NULL; continue; } + if (sendcmd(0x6bb6b66b, 0, 4)) continue; + if (sendcmd(0x6bb6b66c, 0, 4)) continue; + if (sendcmd(0x6bb6b66f, 0, 4)) continue; + if (sendcmd(0x6bb6b672, 0, 8)) continue; + SLEEP_MS(100); + if (sendcmd(0x6bb6b672, 0, 8)) continue; + SLEEP_MS(300); + if (sendcmd(0x6bb6b673, 0, 4)) continue; + SLEEP_MS(700); + ok = 1; + } + if (!ok) { + fprintf(stderr, "camera init failed\n"); + mag160c_render_destroy(render); + libusb_exit(g_ctx); + return 1; + } + + static uint8_t rgb[OUT_W * OUT_H * 3]; + static uint8_t frame[40000]; + unsigned prev = 0xffffffff; + unsigned long nframe = 0; + for (;;) { + uint8_t hdr[64]; + int xfer = 0; + if (libusb_bulk_transfer(g_h, 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 == prev) continue; + prev = c; + if (libusb_bulk_transfer(g_h, 0x81, frame, sizeof(frame), &xfer, 500) || xfer < 38400) + continue; + nframe++; + + /* assemble the full frame (28B header + pixels + tail) for the + * renderer: the second bulk read carries 38400 px + 28B tail */ + static uint8_t full[28 + 40000]; + memcpy(full, hdr, 28); + memcpy(full + 28, frame, 38400); + if (xfer > 38400) + memcpy(full + 28 + 38400, frame + 38400, (size_t)(xfer - 38400)); + /* feed the platform-independent pipeline */ + mag160c_error_t rc = mag160c_render_frame(render, full, 1, rgb); + if (rc != MAG160C_OK) continue; /* FFC window / startup: frozen */ + + display_rgb(rgb, OUT_W, OUT_H); + if (nframe % 30 == 0) { + char path[64]; + snprintf(path, sizeof(path), "thermal_%05lu.bmp", nframe); + save_bmp(path, rgb, OUT_W, OUT_H); + int lo, hi, fmin, fmax, mean, std; + mag160c_render_get_stats(render, &lo, &hi, &fmin, &fmax, &mean, &std); + printf("f=%lu shutter=%d sel=%d win=[%d,%d] stats=(%d..%d m%d s%d) ref=%d\n", + nframe, mag160c_render_get_shutter(render), + mag160c_render_get_endpoint(render), lo, hi, + fmin, fmax, mean, std, mag160c_render_has_reference(render)); + fflush(stdout); + } + } + mag160c_render_destroy(render); + libusb_close(g_h); + libusb_exit(g_ctx); + return 0; +} diff --git a/docs/linux_port_plan.md b/docs/linux_port_plan.md new file mode 100644 index 0000000..0c52c13 --- /dev/null +++ b/docs/linux_port_plan.md @@ -0,0 +1,149 @@ +# MAG160C Linux 移植方案(参考 LiThermal) + +> 目标:把 MAG160C 热像仪(160x120,USB)移植到 Linux 嵌入式设备, +> 参考 LiThermal 项目(https://github.com/diylxy/LiThermal)的硬件与软件架构。 +> 日期:2026-08-14 + +## 1. LiThermal 架构(参考基线) + +| 项目 | LiThermal | +|---|---| +| 热像模组 | 海康 TB4117(290 元,网络/RTSP 取流,USB-NCM)| +| 主控 | 全志 T113-S3(Tina Linux)| +| 封装 | QFP128(0.3mm 引脚,手焊可行)| +| UI | LVGL | +| 动画 | rlottie(Lottie 动画)+ LVGL 内置动画 | +| 显示 | RGB 屏(86 屏/4.3 寸 TFT)| +| 2D 加速 | G2D(全志 2D 引擎)| + +MAG160C 与 LiThermal 的差异:MAG160C 是 **USB 原生协议**(EP 0x81 帧流, +非 UVC/网络),数据量极小(160x120x2B = 38.4KB/帧,15fps ≈ 576KB/s)。 +对主控的要求比 4117 的网络流更低。 + +## 2. Linux 主控芯片选型 + +要求:非 BGA、价格便宜、能跑图形显示和动画(LVGL/2D 加速,无 3D 需求)。 + +| 芯片 | 封装 | 价格 | 核心 | 显示接口 | 2D/GPU | 生态 | 结论 | +|---|---|---|---|---|---|---|---| +| **全志 T113-S3** | QFP128 | ~15-25 元 | 双 A7 1.2G + RISC-V | RGB/MIPI-DSI | G2D 2D | Tina Linux 成熟 | **首选**(LiThermal 验证)| +| 全志 V3s | QFP128 | ~12-18 元 | 单 A7 1.2G | RGB | G2D | Tina Linux | 备选(单核跑 LVGL+渲染有余)| +| 全志 F1C200s | QFP88 | ~8-12 元 | ARM926EJ-S 400M | RGB | 无 | 老 | 太弱(动画吃力)| +| 瑞芯微 RV1103/RV1106 | QFN88 | ~10-15 元 | 单 A7 + NPU | MIPI-DSI | RGA 2D | buildroot | QFN 手焊难,DSI 屏贵 | +| 君正 X2000 | LQFP176 | ~20-30 元 | 双 XBurst2 | RGB | 无 | 弱 | 生态差 | +| 瑞芯微 RK3128/3288 | BGA | — | — | — | — | — | **排除(BGA)** | + +**推荐:T113-S3(QFP128)**。理由: +- LiThermal 已在同平台跑通 LVGL + rlottie 动画 + 热像显示,风险最低 +- 双 A7 1.2GHz:渲染管线(NUC 查表 19200 像素 + LUT + 2x 升采样) + 每帧 <2ms(C 整数运算),15fps 毫无压力,CPU 余量给 LVGL 动画 +- G2D 提供 2D 加速(blit/旋转/缩放),适合灰度帧上屏 +- QFP128 手焊可行(LiThermal README 确认 0.3mm QFP) +- 价格 ~20 元,与 TB4117(290 元)对比,MAG160C 整机成本可压到很低 + +备选:V3s(更低成本,单核 A7 够用);若需 MIPI 摄像头级方案再考虑 RV1106。 + +## 3. 显示方案 + +| 方案 | 接口 | 适合 | +|---|---|---| +| RGB 并口 TFT(4.3/5 寸)| RGB565/888 + 背光 | 首选,便宜,T113-S3 原生支持 | +| MIPI-DSI | DSI | 分辨率更高,屏贵 | +| HDMI | — | 桌面/工业屏 | + +T113-S3 + 4.3 寸 RGB 屏(800x480)+ 电容触摸 ≈ 全套 60-80 元。 + +## 4. 动画渲染库选型 + +| 库 | 类型 | 资源占用 | 适合场景 | 结论 | +|---|---|---|---|---| +| **LVGL** | 嵌入式 UI 框架 | 极低(~100KB RAM)| UI + 基础动画(lv_anim、转场、进度环)| **主力** | +| **rlottie** | Lottie 矢量动画 | 低(渲染 json)| 开机动画、扫描动画、图标动画 | LiThermal 已用,直接参考 | +| ThorVG | 矢量图形 | 低 | rlottie 替代(更活跃)| 备选 | +| Qt/Flutter | 重型 | 高 | — | 排除(T113-S3 无 3D GPU)| + +**推荐组合:LVGL(UI/交互动画)+ rlottie(Lottie 特效动画)**, +与 LiThermal 完全一致。热像显示用 LVGL image 对象每帧 flush RGB +帧(G2D blit 加速),帧率独立于 UI 动画。 + +## 5. 现有 csdk 的 Linux 适配点 + +### 5.1 平台无关(直接可用) +- `mag160c_ir.c`:libusb-1.0 本身就是跨平台(Linux 原生支持)✓ +- 帧解析/流组装、温度换算、TCM:纯 C 无平台依赖 ✓ +- **渲染管线(NUC/窗口/LUT/gray/2x/调色板/FFC 状态机)**:demo3 内实现, + 目前与 Win32 窗口耦合 → **需要抽取为平台无关模块(见 §6)** + +### 5.2 需要适配 +| 项目 | Windows(demo3)| Linux | +|---|---|---| +| USB 传输 | libusb bulk_transfer(单线程轮询)| 同 API;建议改用 libusb 异步/线程 + condvar | +| 帧线程 | 主循环轮询 | 独立线程 + 条件变量(csdk 已有线程模型)| +| 显示后端 | Win32 GDI + HALFTONE | fbdev / DRM-KMS / LVGL image | +| 输入 | Win32 消息 | touch(LVGL input device)| +| 日志 | demo3_diag.txt | syslog / stdout | + +### 5.3 FFC 调度器 +`mag160c_ffc_scheduler_t`(mag160c_display.h)已平台无关 ✓,Linux 侧 +接线程 tick 即可。 + +## 6. 接口抽象设计(渲染管线抽取) + +目标:`mag160c_render.h` —— 输入 raw 帧 + 设备状态,输出 RGB 帧, +零平台依赖,Windows/Linux 共用。 + +```c +/* mag160c_render.h(新) */ +typedef struct mag160c_render_t mag160c_render_t; + +/* 初始化:加载 DDT 表(路径),分配缓冲 */ +mag160c_error_t mag160c_render_init(mag160c_render_t **out, const char *ddt_path, + uint16_t width, uint16_t height); + +/* 每帧驱动(等价官方 0x9ee0 + 0xca30): + * frame : USB 原始帧(0x1c 头 + 像素,或裸像素) + * has_hdr : 1=带帧头(自动提取快门/类型),0=裸像素(需外部给 shutter) + * out_rgb : 输出 320x240x3 RGB24(调色板着色后) + * 内部完成:快门提取→FFC 状态机→ref 采集→表重建→NUC→盲元→ + * 窗口→LUT→gray→2x→palette */ +mag160c_error_t mag160c_render_frame(mag160c_render_t *r, + const uint8_t *frame, int has_hdr, + uint8_t *out_rgb); + +/* 查询(供 UI 显示) */ +void mag160c_render_get_stats(mag160c_render_t *r, int *win_lo, int *win_hi, + int *mean, int *max, int *min); +int mag160c_render_get_shutter(mag160c_render_t *r); +int mag160c_render_get_ffc_pending(mag160c_render_t *r); /* 供 FFC 调度 */ + +void mag160c_render_destroy(mag160c_render_t *r); +``` + +迁移步骤: +1. 把 demo3 的管线函数(load_ddt/select_endpoint/interp/ref_push/ + nuc_and_blind/stats_window/lut_rebuild/gray_map/upscale2x/palette/ + ffc 状态机)迁入 `csdk/src/mag160c_render.c` +2. demo3.c 改为调用 render API(Win32 侧只保留取帧 + 显示) +3. Linux 侧:`mag160c_render_frame()` + LVGL image flush + +## 7. Linux 参考实现(最终形态) + +``` +[USB 帧线程] → mag160c_ir(libusb) + ↓ raw 帧 +[mag160c_render_frame] → 320x240 RGB + ↓ +[LVGL image 对象 + G2D blit] → 屏幕 + ↑ +[LVGL UI:菜单/温度显示/FFC 按钮 + rlottie 开机动画] +``` + +整机成本估算(MAG160C 模组 ~200-300 元 + T113-S3 板 ~60-80 元)远低于 +LiThermal(4117 290 元 + 板卡)。 + +## 8. 行动项 + +- [ ] 抽取 mag160c_render 模块(§6 接口) +- [ ] demo3 迁移到 render API(验证无回归) +- [ ] 编写 Linux 示例:fbdev 后端 + LVGL 集成(后续在 T113-S3 上验证) +- [ ] 文档:csdk/README 增加 Linux 移植章节