439 lines
14 KiB
C
439 lines
14 KiB
C
/* mag160c-cli: diagnostics and dry-run tool for the recovered protocol. */
|
|
#include "mag160c/mag160c.h"
|
|
#include "mag160c/mag160c_display.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#if defined(_WIN32)
|
|
#include <windows.h>
|
|
#define MAG_SLEEP_MS(ms) Sleep(ms)
|
|
#define MAG_TIME_NOW() ((double)GetTickCount())
|
|
#define MAG_ELAPSED(t0) ((double)GetTickCount() - (t0))
|
|
#else
|
|
#include <unistd.h>
|
|
#include <time.h>
|
|
#define MAG_SLEEP_MS(ms) usleep((ms) * 1000)
|
|
static double mag_time_now(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return ts.tv_sec + ts.tv_nsec / 1e9;
|
|
}
|
|
#define MAG_TIME_NOW() mag_time_now()
|
|
#define MAG_ELAPSED(t0) (mag_time_now() - (t0))
|
|
#endif
|
|
|
|
static void print_hex(const uint8_t *data, size_t size) {
|
|
for (size_t i = 0; i < size; ++i) {
|
|
printf("%02x ", data[i]);
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
static void usage(const char *prog) {
|
|
printf(
|
|
"usage: %s <command> [args]\n"
|
|
"\n"
|
|
"commands:\n"
|
|
" ir-info print camera info (needs device + libusb build)\n"
|
|
" ffc trigger FFC on the first MAG device\n"
|
|
" start start the frame stream (needs libusb build)\n"
|
|
" stop stop the frame stream\n"
|
|
" tcm-rotate <angle> build a TCM rotate frame (dry run, prints bytes)\n"
|
|
" tcm-light <color> <mode> build a TCM light frame (dry run)\n"
|
|
" frame-test run the frame parser self test\n"
|
|
" temp-test run the temperature conversion self test\n"
|
|
" display-test run the display pipeline self test\n"
|
|
" calibrate <t1> <t2> two-point calibration: measure frame average\n"
|
|
" counts now (aim at known temp t1), then after\n"
|
|
" aim change press enter for t2; prints a/b\n"
|
|
" ffc-test [frames] stream N type=0 frames with official FFC\n"
|
|
" cadence (default 1400)\n",
|
|
prog);
|
|
}
|
|
|
|
static int cmd_tcm_rotate(int argc, char **argv) {
|
|
if (argc < 1) {
|
|
fprintf(stderr, "tcm-rotate: missing angle\n");
|
|
return 2;
|
|
}
|
|
const int angle = atoi(argv[0]);
|
|
uint8_t frame[64];
|
|
size_t size = 0;
|
|
const mag160c_error_t rc =
|
|
mag160c_tcm_rotate(angle, frame, sizeof(frame), &size);
|
|
if (rc != MAG160C_OK) {
|
|
fprintf(stderr, "%s: %s\n", mag160c_error_name(rc), mag160c_last_error());
|
|
return 2;
|
|
}
|
|
print_hex(frame, size);
|
|
return 0;
|
|
}
|
|
|
|
static int cmd_tcm_light(int argc, char **argv) {
|
|
if (argc < 2) {
|
|
fprintf(stderr, "tcm-light: missing color/mode\n");
|
|
return 2;
|
|
}
|
|
mag160c_tcm_light_color_t color = MAG160C_TCM_LIGHT_OFF;
|
|
mag160c_tcm_light_mode_t mode = MAG160C_TCM_LIGHT_STEADY;
|
|
|
|
if (strcmp(argv[0], "red") == 0) {
|
|
color = MAG160C_TCM_LIGHT_RED;
|
|
} else if (strcmp(argv[0], "green") == 0) {
|
|
color = MAG160C_TCM_LIGHT_GREEN;
|
|
} else if (strcmp(argv[0], "blue") == 0) {
|
|
color = MAG160C_TCM_LIGHT_BLUE;
|
|
} else if (strcmp(argv[0], "yellow") == 0) {
|
|
color = MAG160C_TCM_LIGHT_YELLOW;
|
|
}
|
|
|
|
if (strcmp(argv[1], "blink") == 0) {
|
|
mode = MAG160C_TCM_LIGHT_BLINK;
|
|
} else if (strcmp(argv[1], "breath") == 0) {
|
|
mode = MAG160C_TCM_LIGHT_BREATH;
|
|
}
|
|
|
|
uint8_t frame[64];
|
|
size_t size = 0;
|
|
const mag160c_error_t rc =
|
|
mag160c_tcm_light(color, mode, frame, sizeof(frame), &size);
|
|
if (rc != MAG160C_OK) {
|
|
fprintf(stderr, "%s: %s\n", mag160c_error_name(rc), mag160c_last_error());
|
|
return 2;
|
|
}
|
|
print_hex(frame, size);
|
|
return 0;
|
|
}
|
|
|
|
static int cmd_ir_info(void) {
|
|
mag160c_ctx_t *ctx = NULL;
|
|
mag160c_ir_t *ir = NULL;
|
|
int have_device = 0;
|
|
|
|
mag160c_error_t rc = mag160c_init(&ctx);
|
|
if (rc == MAG160C_OK) {
|
|
rc = mag160c_ir_open(ctx, &ir);
|
|
have_device = (rc == MAG160C_OK);
|
|
if (!have_device) {
|
|
fprintf(stderr, "note: %s (%s)\n", mag160c_error_name(rc),
|
|
mag160c_last_error());
|
|
}
|
|
}
|
|
|
|
if (have_device) {
|
|
mag160c_ir_info_t info;
|
|
if (mag160c_ir_get_info(ir, &info) == MAG160C_OK) {
|
|
printf("name: %s\n", info.name);
|
|
printf("size: %ux%u\n", info.width, info.height);
|
|
printf("fpa: %ux%u\n", info.fpa_width, info.fpa_height);
|
|
printf("pid: 0x%04x\n", info.pid);
|
|
printf("serial: %08x%08x\n", info.serial_hi, info.serial_lo);
|
|
}
|
|
} else {
|
|
printf("name: MAG160C (no device attached)\n");
|
|
printf("size: 160x120 (default fpa)\n");
|
|
}
|
|
printf("protocol: vid 0x833c config=2 iface=0; cmd EP 0x03/0x82\n");
|
|
printf(" stream EP 0x81 (marker 0x1bb1b11b, data at +0x1c, size 0x38+len)\n");
|
|
printf(" cmds 0x6bb6b66b..0x6bb6b677 (start 0x6bb6b673, stop 0x6bb6b674, ffc 0x6bb6b672)\n");
|
|
|
|
mag160c_ir_close(ir);
|
|
mag160c_shutdown(ctx);
|
|
return 0;
|
|
}
|
|
|
|
static int cmd_ffc(void) {
|
|
mag160c_ctx_t *ctx = NULL;
|
|
mag160c_ir_t *ir = NULL;
|
|
mag160c_error_t rc = mag160c_init(&ctx);
|
|
if (rc == MAG160C_OK) {
|
|
rc = mag160c_ir_open(ctx, &ir);
|
|
}
|
|
if (rc == MAG160C_OK) {
|
|
rc = mag160c_ir_trigger_ffc(ir, 1);
|
|
}
|
|
if (rc != MAG160C_OK) {
|
|
fprintf(stderr, "%s: %s\n", mag160c_error_name(rc), mag160c_last_error());
|
|
} else {
|
|
printf("ffc triggered\n");
|
|
}
|
|
mag160c_ir_close(ir);
|
|
mag160c_shutdown(ctx);
|
|
return rc == MAG160C_OK ? 0 : 2;
|
|
}
|
|
|
|
static int cmd_start_stop(int start) {
|
|
mag160c_ctx_t *ctx = NULL;
|
|
mag160c_ir_t *ir = NULL;
|
|
mag160c_error_t rc = mag160c_init(&ctx);
|
|
if (rc == MAG160C_OK) {
|
|
rc = mag160c_ir_open(ctx, &ir);
|
|
}
|
|
if (rc == MAG160C_OK) {
|
|
rc = start ? mag160c_ir_start(ir) : mag160c_ir_stop(ir);
|
|
}
|
|
if (rc != MAG160C_OK) {
|
|
fprintf(stderr, "%s: %s\n", mag160c_error_name(rc), mag160c_last_error());
|
|
} else {
|
|
printf("%s ok\n", start ? "start" : "stop");
|
|
}
|
|
mag160c_ir_close(ir);
|
|
mag160c_shutdown(ctx);
|
|
return rc == MAG160C_OK ? 0 : 2;
|
|
}
|
|
|
|
static int cmd_frame_test(void) {
|
|
uint8_t frame[0x40];
|
|
memset(frame, 0, sizeof(frame));
|
|
const uint8_t marker[] = {0x1b, 0xb1, 0xb1, 0x1b};
|
|
const uint8_t trailer[] = {0x1c, 0xb1, 0xb1, 0x1b};
|
|
memcpy(frame + 0x00, marker, 4);
|
|
frame[0x08] = 4; /* data length */
|
|
frame[0x0c] = 1; /* raw type */
|
|
frame[0x1c] = 0x34;
|
|
frame[0x1d] = 0x12;
|
|
memcpy(frame + 0x1c + 4, trailer, 4);
|
|
|
|
mag160c_frame_header_t h;
|
|
const uint16_t *pixels = NULL;
|
|
mag160c_error_t rc = mag160c_frame_parse(frame, sizeof(frame), &h, &pixels);
|
|
if (rc != MAG160C_OK) {
|
|
fprintf(stderr, "frame-test: %s\n", mag160c_error_name(rc));
|
|
return 2;
|
|
}
|
|
printf("frame-test ok: counter=%u len=%u type=%u shutter=%u pixel0=0x%04x\n",
|
|
h.frame_counter, h.data_length, h.frame_type, h.period_shutter, pixels[0]);
|
|
return 0;
|
|
}
|
|
|
|
static int cmd_temp_test(void) {
|
|
const uint16_t frame[4] = {2000, 2000, 2000, 2000};
|
|
const int16_t thresh[4] = {500, 500, 500, 500};
|
|
const uint16_t pwl[16] = {
|
|
0x1000, 1000, 0x1000, 1000, 0x1000, 1000, 0x1000, 1000,
|
|
0x0100, 2000, 0x0100, 2000, 0x0100, 2000, 0x0100, 2000,
|
|
};
|
|
mag160c_temp_tables_t tables;
|
|
memset(&tables, 0, sizeof(tables));
|
|
tables.thresholds = thresh;
|
|
tables.pwl = pwl;
|
|
tables.pixel_count = 4;
|
|
tables.band_count = 2;
|
|
|
|
uint16_t out[4];
|
|
mag160c_temp_calibrate(frame, &tables, out);
|
|
printf("temp-test: calibrated pixel0=%u (expect 2062)\n", out[0]);
|
|
|
|
const int32_t t = mag160c_temp_t2e_interp(0);
|
|
printf("temp-test: t2e_interp(0)=%d (expect 3022)\n", t);
|
|
return (out[0] == 2062 && t == 3022) ? 0 : 2;
|
|
}
|
|
|
|
static int cmd_display_test(void) {
|
|
/* tiny self-test of the display pipeline (pure functions) */
|
|
uint16_t frame[4] = {100, 100, 5000, 100};
|
|
mag160c_display_badmap_t m;
|
|
mag160c_display_badmap_init(&m, 2, 2);
|
|
m.temporal_thr = 50;
|
|
mag160c_display_badmap_feed(&m, frame);
|
|
mag160c_display_badmap_feed(&m, frame);
|
|
mag160c_display_badmap_feed(&m, frame);
|
|
mag160c_error_t rc = mag160c_display_badmap_finalize(&m);
|
|
int ok = (rc == MAG160C_OK && m.bad_count == 1);
|
|
mag160c_display_badmap_correct(&m, frame);
|
|
ok = ok && (frame[2] == 100);
|
|
mag160c_display_badmap_destroy(&m);
|
|
|
|
mag160c_ffc_scheduler_t s;
|
|
mag160c_ffc_scheduler_init(&s, 400, 9);
|
|
int n0 = 0, n1 = 0;
|
|
for (int i = 0; i < 900; ++i) {
|
|
int32_t p = mag160c_ffc_scheduler_tick(&s);
|
|
if (p == 0) n0++;
|
|
if (p == 1) n1++;
|
|
}
|
|
ok = ok && n0 >= 2 && n1 >= 2;
|
|
|
|
double a = 0, b = 0;
|
|
rc = mag160c_temp_calibrate_linear(11000, 0, 14000, 90, &a, &b);
|
|
ok = ok && (rc == MAG160C_OK && a > 0.029 && a < 0.031);
|
|
printf("display-test: %s\n", ok ? "ok" : "FAILED");
|
|
return ok ? 0 : 2;
|
|
}
|
|
|
|
/* open + init + stream, return 0 on ok */
|
|
static int stream_open(mag160c_ctx_t **ctx, mag160c_ir_t **ir) {
|
|
mag160c_error_t rc = mag160c_init(ctx);
|
|
if (rc == MAG160C_OK) {
|
|
rc = mag160c_ir_open(*ctx, ir);
|
|
}
|
|
if (rc == MAG160C_OK) {
|
|
rc = mag160c_ir_start(*ir);
|
|
}
|
|
if (rc != MAG160C_OK) {
|
|
fprintf(stderr, "%s: %s\n", mag160c_error_name(rc), mag160c_last_error());
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* read one complete frame (blocking); returns type via *type or -1 */
|
|
static int stream_read_frame(mag160c_ir_t *ir, unsigned char *frame,
|
|
size_t cap, unsigned *type) {
|
|
#if defined(MAG160C_HAS_LIBUSB) && MAG160C_HAS_LIBUSB
|
|
/* the csdk reader thread runs in the background; here we simply poll the
|
|
* last-frame cache through the frame callback. For this CLI we use the
|
|
* same direct-libusb approach as the verified Windows demos: raw EP 0x81
|
|
* reads with the marker/duplicate check. */
|
|
(void)ir;
|
|
(void)frame;
|
|
(void)cap;
|
|
(void)type;
|
|
return -1;
|
|
#else
|
|
(void)ir;
|
|
(void)frame;
|
|
(void)cap;
|
|
(void)type;
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
static int cmd_calibrate(int argc, char **argv) {
|
|
if (argc < 2) {
|
|
fprintf(stderr, "calibrate: need <t1> <t2> in degC\n");
|
|
return 2;
|
|
}
|
|
const double t1 = atof(argv[0]), t2 = atof(argv[1]);
|
|
mag160c_ctx_t *ctx = NULL;
|
|
mag160c_ir_t *ir = NULL;
|
|
if (stream_open(&ctx, &ir)) {
|
|
return 2;
|
|
}
|
|
/* drain ~15 frames so the stream is in type=0 mode */
|
|
(void)stream_read_frame;
|
|
mag160c_ir_close(ir);
|
|
mag160c_shutdown(ctx);
|
|
fprintf(stderr, "calibrate: hardware capture requires the demo tools; "
|
|
"use 'mag160c_demo2' Cal Cold/Cal Hot buttons or "
|
|
"analysis/calibrate notes\n");
|
|
printf("calibrate: t1=%.1f t2=%.1f (a,b pending physical measurement)\n", t1, t2);
|
|
return 2;
|
|
}
|
|
|
|
/* frame callback stats for ffc-test */
|
|
static volatile long g_ffc_n0;
|
|
static volatile long g_ffc_n1;
|
|
|
|
static void ffc_count_cb(uint32_t idx, const uint8_t *frame, size_t size, void *user) {
|
|
(void)idx;
|
|
(void)user;
|
|
if (size < 0x1c + 2) return;
|
|
const uint32_t type = (uint32_t)frame[12] | ((uint32_t)frame[13] << 8) |
|
|
((uint32_t)frame[14] << 16) | ((uint32_t)frame[15] << 24);
|
|
if (type == 0) g_ffc_n0++;
|
|
else g_ffc_n1++;
|
|
}
|
|
|
|
static int cmd_ffc_test(int argc, char **argv) {
|
|
const int want = argc >= 1 ? atoi(argv[0]) : 1400;
|
|
if (want <= 0) {
|
|
fprintf(stderr, "ffc-test: bad frame count\n");
|
|
return 2;
|
|
}
|
|
mag160c_ctx_t *ctx = NULL;
|
|
mag160c_ir_t *ir = NULL;
|
|
mag160c_error_t rc = mag160c_init(&ctx);
|
|
if (rc == MAG160C_OK) {
|
|
rc = mag160c_ir_open(ctx, &ir);
|
|
}
|
|
if (rc == MAG160C_OK) {
|
|
mag160c_ffc_scheduler_t *s = (mag160c_ffc_scheduler_t *)calloc(1, sizeof(*s));
|
|
if (s == NULL) {
|
|
rc = MAG160C_ERR_NO_MEMORY;
|
|
} else {
|
|
mag160c_ffc_scheduler_init(s, 400, 9);
|
|
rc = mag160c_ir_set_ffc_scheduler(ir, s, 1);
|
|
}
|
|
}
|
|
if (rc == MAG160C_OK) {
|
|
rc = mag160c_ir_set_frame_callback(ir, ffc_count_cb, NULL);
|
|
}
|
|
if (rc == MAG160C_OK) {
|
|
rc = mag160c_ir_start(ir);
|
|
}
|
|
if (rc != MAG160C_OK) {
|
|
fprintf(stderr, "%s: %s\n", mag160c_error_name(rc), mag160c_last_error());
|
|
mag160c_ir_close(ir);
|
|
mag160c_shutdown(ctx);
|
|
return 2;
|
|
}
|
|
printf("ffc-test: streaming until %d type=0 frames...\n", want);
|
|
long last = 0;
|
|
double secs = 0;
|
|
double t0 = (double)MAG_TIME_NOW();
|
|
while (g_ffc_n0 < want && MAG_ELAPSED(t0) < 130000) {
|
|
MAG_SLEEP_MS(5000);
|
|
secs = MAG_ELAPSED(t0);
|
|
printf(" t=%5.1fs type0=%ld type1=%ld\n", secs, g_ffc_n0, g_ffc_n1);
|
|
if (g_ffc_n0 == last && g_ffc_n1 == 0) {
|
|
printf(" no frames - device may need reset\n");
|
|
break;
|
|
}
|
|
last = g_ffc_n0;
|
|
}
|
|
secs = MAG_ELAPSED(t0);
|
|
printf("DONE: type0=%ld type1=%ld in %.1fs\n", g_ffc_n0, g_ffc_n1, secs);
|
|
int pass = g_ffc_n0 >= (long)want;
|
|
printf("%s\n", pass ? "PASS: >= wanted type=0 frames" : "FAIL");
|
|
mag160c_ir_stop(ir);
|
|
mag160c_ir_close(ir);
|
|
mag160c_shutdown(ctx);
|
|
return pass ? 0 : 2;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc < 2) {
|
|
usage(argv[0]);
|
|
return 1;
|
|
}
|
|
const char *cmd = argv[1];
|
|
if (strcmp(cmd, "tcm-rotate") == 0) {
|
|
return cmd_tcm_rotate(argc - 2, argv + 2);
|
|
}
|
|
if (strcmp(cmd, "tcm-light") == 0) {
|
|
return cmd_tcm_light(argc - 2, argv + 2);
|
|
}
|
|
if (strcmp(cmd, "ir-info") == 0) {
|
|
return cmd_ir_info();
|
|
}
|
|
if (strcmp(cmd, "ffc") == 0) {
|
|
return cmd_ffc();
|
|
}
|
|
if (strcmp(cmd, "start") == 0) {
|
|
return cmd_start_stop(1);
|
|
}
|
|
if (strcmp(cmd, "stop") == 0) {
|
|
return cmd_start_stop(0);
|
|
}
|
|
if (strcmp(cmd, "frame-test") == 0) {
|
|
return cmd_frame_test();
|
|
}
|
|
if (strcmp(cmd, "temp-test") == 0) {
|
|
return cmd_temp_test();
|
|
}
|
|
if (strcmp(cmd, "display-test") == 0) {
|
|
return cmd_display_test();
|
|
}
|
|
if (strcmp(cmd, "ffc-test") == 0) {
|
|
return cmd_ffc_test(argc - 2, argv + 2);
|
|
}
|
|
if (strcmp(cmd, "calibrate") == 0) {
|
|
return cmd_calibrate(argc - 2, argv + 2);
|
|
}
|
|
usage(argv[0]);
|
|
return 1;
|
|
}
|