From f46f9fdae6b90e35035022bf9ef12e8cb9562e71 Mon Sep 17 00:00:00 2001 From: Leandro Lupori Date: Wed, 18 May 2022 10:53:54 -0300 Subject: [PATCH 1/2] Add support for FLIR ONE G3 camera This model uses a sensor with 80x60 resolution --- .gitignore | 2 +- Makefile | 21 +- load_module.sh | 1 + src/flirone.c | 1210 +++++++++++++++++++++--------------------------- 4 files changed, 547 insertions(+), 687 deletions(-) create mode 100755 load_module.sh diff --git a/.gitignore b/.gitignore index 7383431..f0c4306 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -src/flirone.o +flirone.o flirone diff --git a/Makefile b/Makefile index 6e8d450..3403baa 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,20 @@ -CC = gcc -I/usr/include/libusb-1.0 -GXX = g++ -CXXFLAGS = -pipe -O2 -Wall -W -D_REENTRANT -lusb-1.0 -lm -INCPATH = -I. -I/usr/include/libusb-1.0 +SANITIZERS ?= -fsanitize=address -fsanitize=undefined \ + -fsanitize=pointer-compare -fsanitize=pointer-subtract \ + -fsanitize=leak -fno-sanitize-recover=all \ + -fsanitize-address-use-after-scope \ + -fstack-protector-all \ + -fstack-protector-strong -all: flirone.o flirone +INCPATH = -I/usr/include/libusb-1.0 +CFLAGS = $(INCPATH) -pipe -O2 -Wall -D_REENTRANT -g $(SANITIZERS) + +all: flirone flirone.o: src/flirone.c src/plank.h + $(CC) $(CFLAGS) -c $< -o $@ -flirone: src/flirone.o - ${CC} -o flirone src/flirone.o -lusb-1.0 -lm -Wall +flirone: flirone.o + $(CC) -o $@ $< $(SANITIZERS) -lusb-1.0 -lm -g clean: rm -f flirone.o flirone - diff --git a/load_module.sh b/load_module.sh new file mode 100755 index 0000000..045f1f1 --- /dev/null +++ b/load_module.sh @@ -0,0 +1 @@ +sudo modprobe v4l2loopback exclusive_caps=0,0 video_nr=2,3,4 diff --git a/src/flirone.c b/src/flirone.c index b37627d..ebabe9b 100644 --- a/src/flirone.c +++ b/src/flirone.c @@ -1,5 +1,6 @@ /* * Copyright (C) 2015-2016 Thomas + * Copyright (C) 2022 Instituto de Pesquisas Eldorado * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,787 +17,640 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +#include #include -#include +#include +#include +#include +#include #include #include -#include -#include +#include +#include +#include #include - -#include -#include +#include -#include "jpeglib.h" +#include +#include "font5x7.h" +#include "jpeglib.h" #include "plank.h" -// -- define v4l2 --------------- -#include -#include -#include -#include -#include +/* defines */ -#define VIDEO_DEVICE0 "/dev/video1" // gray scale thermal image -#define FRAME_WIDTH0 160 -#define FRAME_HEIGHT0 120 - -#define VIDEO_DEVICE1 "/dev/video2" // color visible image -#define FRAME_WIDTH1 640 -#define FRAME_HEIGHT1 480 - -#define VIDEO_DEVICE2 "/dev/video3" // colorized thermal image -#define FRAME_WIDTH2 160 -#define FRAME_HEIGHT2 128 - -#define FRAME_FORMAT0 V4L2_PIX_FMT_GREY -#define FRAME_FORMAT1 V4L2_PIX_FMT_MJPEG -#define FRAME_FORMAT2 V4L2_PIX_FMT_RGB24 - -struct v4l2_capability vid_caps0; -struct v4l2_capability vid_caps1; -struct v4l2_capability vid_caps2; - -struct v4l2_format vid_format0; -struct v4l2_format vid_format1; -struct v4l2_format vid_format2; - -size_t framesize0; -size_t linewidth0; - -size_t framesize1; -size_t linewidth1; - -size_t framesize2; -size_t linewidth2; - - -const char *video_device0=VIDEO_DEVICE0; -const char *video_device1=VIDEO_DEVICE1; -const char *video_device2=VIDEO_DEVICE2; - -int fdwr0 = 0; -int fdwr1 = 0; -int fdwr2 = 0; - -// -- end define v4l2 --------------- - - #define VENDOR_ID 0x09cb - #define PRODUCT_ID 0x1996 - - static struct libusb_device_handle *devh = NULL; - int filecount=0; - struct timeval t1, t2; - long long fps_t; - - int FFC = 0; // detect FFC - -// -- buffer for EP 0x85 chunks --------------- - #define BUF85SIZE 1048576 // size got from android app - int buf85pointer = 0; - unsigned char buf85[BUF85SIZE]; - - void print_format(struct v4l2_format*vid_format) { - printf(" vid_format->type =%d\n", vid_format->type ); - printf(" vid_format->fmt.pix.width =%d\n", vid_format->fmt.pix.width ); - printf(" vid_format->fmt.pix.height =%d\n", vid_format->fmt.pix.height ); - printf(" vid_format->fmt.pix.pixelformat =%d\n", vid_format->fmt.pix.pixelformat); - printf(" vid_format->fmt.pix.sizeimage =%u\n", vid_format->fmt.pix.sizeimage ); - printf(" vid_format->fmt.pix.field =%d\n", vid_format->fmt.pix.field ); - printf(" vid_format->fmt.pix.bytesperline=%d\n", vid_format->fmt.pix.bytesperline ); - printf(" vid_format->fmt.pix.colorspace =%d\n", vid_format->fmt.pix.colorspace ); -} +// color visible image +#define FRAME_WIDTH1 640 +#define FRAME_HEIGHT1 480 -//#include "font.h" -#include "font5x7.h" -void font_write(unsigned char *fb, int x, int y, const char *string) -{ - int rx, ry; - while (*string) { - for (ry = 0; ry < 5; ++ry) { - for (rx = 0; rx < 7; ++rx) { - int v = (font5x7_basic[((*string) & 0x7F) - CHAR_OFFSET][ry] >> (rx)) & 1; -// fb[(y+ry) * 160 + (x + rx)] = v ? 0 : 0xFF; // black / white -// fb[(y+rx) * 160 + (x + ry)] = v ? 0 : 0xFF; // black / white - - fb[(y+rx) * 160 + (x + ry)] = v ? 0 : fb[(y+rx) * 160 + (x + ry)]; // transparent - } - } - string++; - x += 6; - } -} +// colorized thermal image +#define FRAME_WIDTH2 frame_width2 +#define FRAME_HEIGHT2 frame_height2 +// original width/height +#define FRAME_OWIDTH2 frame_owidth2 +#define FRAME_OHEIGHT2 frame_oheight2 -double raw2temperature(unsigned short RAW) -{ - // mystery correction factor - RAW *=4; - // calc amount of radiance of reflected objects ( Emissivity < 1 ) - double RAWrefl=PlanckR1/(PlanckR2*(exp(PlanckB/(TempReflected+273.15))-PlanckF))-PlanckO; - // get displayed object temp max/min - double RAWobj=(RAW-(1-Emissivity)*RAWrefl)/Emissivity; - // calc object temperature - return PlanckB/log(PlanckR1/(PlanckR2*(RAWobj+PlanckO))+PlanckF)-273.15; -} +// max chars in line +#define MAX_CHARS2 (FRAME_WIDTH2 / 6 + (flirone_pro ? 0 : 1)) +#define FRAME_FORMAT1 V4L2_PIX_FMT_MJPEG +#define FRAME_FORMAT2 V4L2_PIX_FMT_RGB24 -void startv4l2() -{ - int ret_code = 0; +#define FONT_COLOR_DFLT 0xff - int i; - int k=1; -/* -//open video_device0 - printf("using output device: %s\n", video_device0); - - fdwr0 = open(video_device0, O_RDWR); - assert(fdwr0 >= 0); +/* USB defs */ +#define VENDOR_ID 0x09cb +#define PRODUCT_ID 0x1996 - ret_code = ioctl(fdwr0, VIDIOC_QUERYCAP, &vid_caps0); - assert(ret_code != -1); +/* These are just to make USB requests easier to read */ +#define REQ_TYPE 1 +#define REQ 0xb +#define V_STOP 0 +#define V_START 1 +#define INDEX(i) (i) +#define LEN(l) (l) - memset(&vid_format0, 0, sizeof(vid_format0)); +// buffer for EP 0x85 chunks +#define BUF85SIZE 1048576 // size got from android app - ret_code = ioctl(fdwr0, VIDIOC_G_FMT, &vid_format0); +/* global data */ - linewidth0=FRAME_WIDTH0; - framesize0=FRAME_WIDTH0*FRAME_HEIGHT0*1; // 8 Bit +static char video_device1[64]; +static char video_device2[64]; +static int frame_width2 = 80; +static int frame_height2 = 80; +static int frame_owidth2 = 80; +static int frame_oheight2 = 60; - vid_format0.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; - vid_format0.fmt.pix.width = FRAME_WIDTH0; - vid_format0.fmt.pix.height = FRAME_HEIGHT0; - vid_format0.fmt.pix.pixelformat = FRAME_FORMAT0; - vid_format0.fmt.pix.sizeimage = framesize0; - vid_format0.fmt.pix.field = V4L2_FIELD_NONE; - vid_format0.fmt.pix.bytesperline = linewidth0; - vid_format0.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB; +static char flirone_pro = 0; +static char pal_inverse = 0; +static char pal_colors = 0; - // set data format - ret_code = ioctl(fdwr0, VIDIOC_S_FMT, &vid_format0); - assert(ret_code != -1); +static int FFC = 0; // detect FFC - print_format(&vid_format0); -*/ -//open video_device1 - printf("using output device: %s\n", video_device1); - - fdwr1 = open(video_device1, O_RDWR); - assert(fdwr1 >= 0); +static int fdwr1 = 0; +static int fdwr2 = 0; +static struct libusb_device_handle *devh = NULL; +static unsigned buf85pointer = 0; +static unsigned char buf85[BUF85SIZE]; - ret_code = ioctl(fdwr1, VIDIOC_QUERYCAP, &vid_caps1); - assert(ret_code != -1); +/* functions */ - memset(&vid_format1, 0, sizeof(vid_format1)); +void print_format(struct v4l2_format*vid_format) +{ + printf(" vid_format->type =%d\n", vid_format->type ); + printf(" vid_format->fmt.pix.width =%d\n", vid_format->fmt.pix.width ); + printf(" vid_format->fmt.pix.height =%d\n", vid_format->fmt.pix.height ); + printf(" vid_format->fmt.pix.pixelformat =%d\n", vid_format->fmt.pix.pixelformat); + printf(" vid_format->fmt.pix.sizeimage =%u\n", vid_format->fmt.pix.sizeimage ); + printf(" vid_format->fmt.pix.field =%d\n", vid_format->fmt.pix.field ); + printf(" vid_format->fmt.pix.bytesperline=%d\n", vid_format->fmt.pix.bytesperline ); + printf(" vid_format->fmt.pix.colorspace =%d\n", vid_format->fmt.pix.colorspace ); +} - ret_code = ioctl(fdwr1, VIDIOC_G_FMT, &vid_format1); +void font_write(unsigned char *fb, int x, int y, const char *string, + unsigned char color) +{ + int rx, ry, pos; + + while (*string) { + for (ry = 0; ry < 7; ry++) { + for (rx = 0; rx < 5; rx++) { + int v = (font5x7_basic[(*string & 0x7F) - CHAR_OFFSET][rx] >> (ry)) & 1; + pos = (y + ry) * FRAME_WIDTH2 + (x + rx); + fb[pos] = v ? color : fb[pos]; // transparent + } + } + string++; + x += 6; + } +} - linewidth1=FRAME_WIDTH1; - framesize1=FRAME_WIDTH1*FRAME_HEIGHT1*1; // 8 Bit ?? +static double raw2temperature(unsigned short RAW) +{ + // mystery correction factor + RAW *= 4; + // calc amount of radiance of reflected objects ( Emissivity < 1 ) + double RAWrefl=PlanckR1/(PlanckR2*(exp(PlanckB/(TempReflected+273.15))-PlanckF))-PlanckO; + // get displayed object temp max/min + double RAWobj=(RAW-(1-Emissivity)*RAWrefl)/Emissivity; + // calc object temperature + return PlanckB/log(PlanckR1/(PlanckR2*(RAWobj+PlanckO))+PlanckF)-273.15; +} - vid_format1.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; - vid_format1.fmt.pix.width = FRAME_WIDTH1; - vid_format1.fmt.pix.height = FRAME_HEIGHT1; - vid_format1.fmt.pix.pixelformat = FRAME_FORMAT1; - vid_format1.fmt.pix.sizeimage = framesize1; - vid_format1.fmt.pix.field = V4L2_FIELD_NONE; - vid_format1.fmt.pix.bytesperline = linewidth1; - vid_format1.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB; +static void startv4l2() +{ + int ret_code = 0; + struct v4l2_capability vid_caps1 = {}, vid_caps2 = {}; + struct v4l2_format vid_format1 = {}, vid_format2 = {}; + size_t linewidth1 = 0, framesize1 = 0; + size_t linewidth2 = 0, framesize2 = 0; - // set data format - ret_code = ioctl(fdwr1, VIDIOC_S_FMT, &vid_format1); - assert(ret_code != -1); + //open video_device1 + printf("using output device: %s\n", video_device1); - print_format(&vid_format1); + fdwr1 = open(video_device1, O_RDWR); + assert(fdwr1 >= 0); + ret_code = ioctl(fdwr1, VIDIOC_QUERYCAP, &vid_caps1); + assert(ret_code != -1); -//open video_device2 - printf("using output device: %s\n", video_device2); - - fdwr2 = open(video_device2, O_RDWR); - assert(fdwr2 >= 0); + memset(&vid_format1, 0, sizeof(vid_format1)); - ret_code = ioctl(fdwr2, VIDIOC_QUERYCAP, &vid_caps2); - assert(ret_code != -1); + ret_code = ioctl(fdwr1, VIDIOC_G_FMT, &vid_format1); - memset(&vid_format2, 0, sizeof(vid_format2)); + linewidth1 = FRAME_WIDTH1; + framesize1 = FRAME_WIDTH1 * FRAME_HEIGHT1; - ret_code = ioctl(fdwr2, VIDIOC_G_FMT, &vid_format2); + vid_format1.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; + vid_format1.fmt.pix.width = FRAME_WIDTH1; + vid_format1.fmt.pix.height = FRAME_HEIGHT1; + vid_format1.fmt.pix.pixelformat = FRAME_FORMAT1; + vid_format1.fmt.pix.sizeimage = framesize1; + vid_format1.fmt.pix.field = V4L2_FIELD_NONE; + vid_format1.fmt.pix.bytesperline = linewidth1; + vid_format1.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB; - linewidth2=FRAME_WIDTH2; - framesize2=FRAME_WIDTH2*FRAME_HEIGHT2*3; // 8x8x8 Bit + // set data format + ret_code = ioctl(fdwr1, VIDIOC_S_FMT, &vid_format1); + assert(ret_code != -1); - vid_format2.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; - vid_format2.fmt.pix.width = FRAME_WIDTH2; - vid_format2.fmt.pix.height = FRAME_HEIGHT2; - vid_format2.fmt.pix.pixelformat = FRAME_FORMAT2; - vid_format2.fmt.pix.sizeimage = framesize2; - vid_format2.fmt.pix.field = V4L2_FIELD_NONE; - vid_format2.fmt.pix.bytesperline = linewidth2; - vid_format2.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB; + print_format(&vid_format1); - // set data format - ret_code = ioctl(fdwr2, VIDIOC_S_FMT, &vid_format2); - assert(ret_code != -1); + //open video_device2 + printf("using output device: %s\n", video_device2); - print_format(&vid_format2); -} + fdwr2 = open(video_device2, O_RDWR); + assert(fdwr2 >= 0); + ret_code = ioctl(fdwr2, VIDIOC_QUERYCAP, &vid_caps2); + assert(ret_code != -1); -// unused -void closev4l2() -{ -// close(fdwr0); - close(fdwr1); - close(fdwr2); + memset(&vid_format2, 0, sizeof(vid_format2)); + + ret_code = ioctl(fdwr2, VIDIOC_G_FMT, &vid_format2); + + linewidth2 = FRAME_WIDTH2; + framesize2 = FRAME_WIDTH2 * FRAME_HEIGHT2 * 3; // 8x8x8 Bit + vid_format2.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; + vid_format2.fmt.pix.width = FRAME_WIDTH2; + vid_format2.fmt.pix.height = FRAME_HEIGHT2; + vid_format2.fmt.pix.pixelformat = FRAME_FORMAT2; + vid_format2.fmt.pix.sizeimage = framesize2; + vid_format2.fmt.pix.field = V4L2_FIELD_NONE; + vid_format2.fmt.pix.bytesperline = linewidth2; + vid_format2.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB; + + // set data format + ret_code = ioctl(fdwr2, VIDIOC_S_FMT, &vid_format2); + assert(ret_code != -1); + + print_format(&vid_format2); } -void vframe(char ep[],char EP_error[], int r, int actual_length, unsigned char buf[], unsigned char *colormap) +static void vframe(char ep[], char EP_error[], int r, int actual_length, + unsigned char buf[], unsigned char *colormap) { - // error handler - time_t now1; - now1 = time(NULL); - if (r < 0) { - if (strcmp (EP_error, libusb_error_name(r))!=0) - { - strcpy(EP_error, libusb_error_name(r)); - fprintf(stderr, "\n: %s >>>>>>>>>>>>>>>>>bulk transfer (in) %s:%i %s\n", ctime(&now1), ep , r, libusb_error_name(r)); - sleep(1); - } - return; - } - - // reset buffer if the new chunk begins with magic bytes or the buffer size limit is exceeded - unsigned char magicbyte[4]={0xEF,0xBE,0x00,0x00}; - - if ((strncmp (buf, magicbyte,4)==0 ) || ((buf85pointer + actual_length) >= BUF85SIZE)) - { - //printf(">>>>>>>>>>>begin of new frame<<<<<<<<<<<<<\n"); - buf85pointer=0; + time_t now1; + char magicbyte[4] = { 0xEF, 0xBE, 0x00, 0x00 }; + uint32_t FrameSize, ThermalSize, JpgSize; + int v, x, y, pos, disp; + unsigned short pix[FRAME_OWIDTH2 * FRAME_OHEIGHT2]; // original Flir 16 Bit RAW + unsigned char *fb_proc, *fb_proc2; + size_t framesize2 = FRAME_WIDTH2 * FRAME_HEIGHT2 * 3; // 8x8x8 Bit + int min = 0x10000, max = 0; + int maxx = -1, maxy = -1; + int delta, scale, med; + int hw, hh; + char st1[100]; + char st2[100]; + struct tm *loctime; + + now1 = time(NULL); + if (r < 0) { + if (strcmp(EP_error, libusb_error_name(r)) != 0) { + strcpy(EP_error, libusb_error_name(r)); + fprintf(stderr, "\n: %s >>>>>>>>>>>>>>>>>bulk transfer (in) %s:%i %s\n", + ctime(&now1), ep, r, libusb_error_name(r)); + sleep(1); + } + return; } - - //printf("actual_length %d !!!!!\n", actual_length); - - memmove(buf85+buf85pointer, buf, actual_length); - buf85pointer=buf85pointer+actual_length; - - if ((strncmp (buf85, magicbyte,4)!=0 )) - { + + // reset buffer if the new chunk begins with magic bytes or the buffer size limit is exceeded + if (strncmp((char *)buf, magicbyte, 4) == 0 || + buf85pointer + actual_length >= BUF85SIZE) + buf85pointer = 0; + + memmove(buf85 + buf85pointer, buf, actual_length); + buf85pointer += actual_length; + + if (strncmp((char *)buf85, magicbyte, 4) != 0) { //reset buff pointer - buf85pointer=0; + buf85pointer = 0; printf("Reset buffer because of bad Magic Byte!\n"); return; } - - // a quick and dirty job for gcc - uint32_t FrameSize = buf85[ 8] + (buf85[ 9] << 8) + (buf85[10] << 16) + (buf85[11] << 24); - uint32_t ThermalSize = buf85[12] + (buf85[13] << 8) + (buf85[14] << 16) + (buf85[15] << 24); - uint32_t JpgSize = buf85[16] + (buf85[17] << 8) + (buf85[18] << 16) + (buf85[19] << 24); - uint32_t StatusSize = buf85[20] + (buf85[21] << 8) + (buf85[22] << 16) + (buf85[23] << 24); - - //printf("FrameSize= %d (+28=%d), ThermalSize %d, JPG %d, StatusSize %d, Pointer %d\n",FrameSize,FrameSize+28, ThermalSize, JpgSize,StatusSize,buf85pointer); - - if ( (FrameSize+28) > (buf85pointer) ) - { - // wait for next chunk - return; - } - - int i,v; - // get a full frame, first print the status - t1=t2; - gettimeofday(&t2, NULL); - // fps as moving average over last 20 frames -// fps_t = (19*fps_t+10000000/(((t2.tv_sec * 1000000) + t2.tv_usec) - ((t1.tv_sec * 1000000) + t1.tv_usec)))/20; - - filecount++; -// printf("#%08i %lld/10 fps:",filecount,fps_t); -// for (i = 0; i < StatusSize; i++) { -// v=28+ThermalSize+JpgSize+i; -// if(buf85[v]>31) {printf("%c", buf85[v]);} -// } -// printf("\n"); - - buf85pointer=0; - - unsigned short pix[160*120]; // original Flir 16 Bit RAW - int x, y; - unsigned char *fb_proc,*fb_proc2; - - fb_proc = malloc(160 * 128); // 8 Bit gray buffer really needs only 160 x 120 - memset(fb_proc, 128, 160*128); // sizeof(fb_proc) doesn't work, value depends from LUT - - fb_proc2 = malloc(160 * 128 * 3 ); // 8x8x8 Bit RGB buffer - - int min = 0x10000, max = 0; - float rms = 0; - -// Make a unsigned short array from what comes from the thermal frame -// find the max, min and RMS (not used yet) values of the array - int maxx, maxy; - for (y = 0; y < 120; ++y) - { - for (x = 0; x < 160; ++x) { - if (x<80) - v = buf85[2*(y * 164 + x) +32]+256*buf85[2*(y * 164 + x) +33]; - else - v = buf85[2*(y * 164 + x) +32+4]+256*buf85[2*(y * 164 + x) +33+4]; - pix[y * 160 + x] = v; // unsigned char!! - - if (v < min) min = v; - if (v > max) { max = v; maxx = x; maxy = y; } - rms += v * v; + + // a quick and dirty job for gcc + FrameSize = buf85[ 8] + (buf85[ 9] << 8) + (buf85[10] << 16) + (buf85[11] << 24); + ThermalSize = buf85[12] + (buf85[13] << 8) + (buf85[14] << 16) + (buf85[15] << 24); + JpgSize = buf85[16] + (buf85[17] << 8) + (buf85[18] << 16) + (buf85[19] << 24); + + if (FrameSize + 28 > buf85pointer) + // wait for next chunk + return; + + /* + printf("actual_len=%d, buf85pointer=%d, FrameSize=%d, ThermalSize=%d, JpgSize=%d\n", + actual_length, buf85pointer, FrameSize, ThermalSize, JpgSize); + */ + + // get a full frame, first print the status + buf85pointer = 0; + + fb_proc = malloc(FRAME_WIDTH2 * FRAME_HEIGHT2); + memset(fb_proc, 128, FRAME_WIDTH2 * FRAME_HEIGHT2); + assert(fb_proc); + + fb_proc2 = malloc(FRAME_WIDTH2 * FRAME_HEIGHT2 * 3); // 8x8x8 Bit RGB buffer + assert(fb_proc2); + + if (pal_colors) { + for (y = 0; y < FRAME_HEIGHT2; ++y) + for (x = 0; x < FRAME_WIDTH2; ++x) + for (disp = 0; disp < 3; disp++) + fb_proc2[3 * y * FRAME_WIDTH2 + 3 * x + disp] = + colormap[3 * (y * 256 / FRAME_HEIGHT2) + disp]; + goto render; } - } - - // RMS used later -// rms /= 160 * 120; -// rms = sqrtf(rms); - -// scale the data in the array - int delta = max - min; - if (!delta) delta = 1; // if max = min we have divide by zero - int scale = 0x10000 / delta; - - for (y = 0; y < 120; ++y) //120 - { - for (x = 0; x < 160; ++x) { //160 - int v = (pix[y * 160 + x] - min) * scale >> 8; - -// fb_proc is the gray scale frame buffer - fb_proc[y * 160 + x] = v; // unsigned char!! + // Make a unsigned short array from what comes from the thermal frame + // find the max and min values of the array + + hw = FRAME_OWIDTH2 / 2; + hh = FRAME_OHEIGHT2 / 2; + + for (y = 0; y < FRAME_OHEIGHT2; y++) { + for (x = 0; x < FRAME_OWIDTH2; x++) { + if (flirone_pro) { + pos = 2 * (y * (FRAME_OWIDTH2 + 4) + x) + 32; + if (x > hw) + pos += 4; + } else { + /* + * 32 - seems to be the header size + * +2 - for some reason 2 16-bit values must be skipped at the end + * of each line + */ + pos = 2 * (y * (FRAME_OWIDTH2 + 2) + x) + 32; + } + + v = buf85[pos] | buf85[pos + 1] << 8; + pix[y * FRAME_OWIDTH2 + x] = v; + + if (v < min) + min = v; + if (v > max) { + max = v; + maxx = x; + maxy = y; + } + } } - } - - char st1[100]; - char st2[100]; - struct tm *loctime; - // Convert it to local time and Print it out in a nice format. - loctime = localtime (&now1); - strftime (st1, 60, "%H:%M:%S", loctime); - - // calc medium of 2x2 center pixels - int med = (pix[59 * 160 + 79]+pix[59 * 160 + 80]+pix[60 * 160 + 79]+pix[60 * 160 + 80])/4; - sprintf(st2," %.1f/%.1f/%.1f'C", raw2temperature(min),raw2temperature(med),raw2temperature(max)); - strcat(st1, st2); - - #define MAX 26 // max chars in line 160/6=26,6 - strncpy(st2, st1, MAX); - // write zero to string !! - st2[MAX-1] = '\0'; - font_write(fb_proc, 1, 120, st2); - - // show crosshairs, remove if required - font_write(fb_proc, 80-2, 60-3, "+"); - - maxx -= 4; - maxy -= 4; - - if (maxx < 0) maxx = 0; - if (maxy < 0) maxy = 0; - if (maxx > 150) maxx = 150; - if (maxy > 110) maxy = 110; - - font_write(fb_proc, 160-6, maxy, "<"); - font_write(fb_proc, maxx, 120-8, "|"); - - for (y = 0; y < 128; ++y) - { - for (x = 0; x < 160; ++x) { - -// fb_proc is the gray scale frame buffer - v=fb_proc[y * 160 + x] ; // unsigned char!! - -// fb_proc2 is an 24bit RGB buffer - - fb_proc2[3*y * 160 + x*3] = colormap[3 * v]; // unsigned char!! - fb_proc2[(3*y * 160 + x*3)+1] = colormap[3 * v + 1]; // unsigned char!! - fb_proc2[(3*y * 160 + x*3)+2] = colormap[3 * v + 2]; // unsigned char!! + + assert(maxx != -1); + assert(maxy != -1); + /* printf("min=%d max=%d x=%d y=%d\n", min, max, maxx, maxy); */ + + // scale the data in the array + delta = max - min; + if (delta == 0) + delta = 1; // if max = min we have divide by zero + scale = 0x10000 / delta; + + for (y = 0; y < FRAME_OHEIGHT2; y++) { + for (x = 0; x < FRAME_OWIDTH2; x++) { + int v = (pix[y * FRAME_OWIDTH2 + x] - min) * scale >> 8; + + // fb_proc is the gray scale frame buffer + fb_proc[y * FRAME_OWIDTH2 + x] = v; // unsigned char!! + } } - } - - - - // write video to v4l2loopback(s) -// write(fdwr0, fb_proc, framesize0); // gray scale Thermal Image - write(fdwr1, &buf85[28+ThermalSize], JpgSize); // jpg Visual Image - - if (strncmp (&buf85[28+ThermalSize+JpgSize+17],"FFC",3)==0) - { - FFC=1; // drop all FFC frames - } else - { - if (FFC==1) - { - FFC=0; // drop first frame after FFC + + // calc medium of 2x2 center pixels + med = pix[(hh - 1) * FRAME_OWIDTH2 + hw - 1] + + pix[(hh - 1) * FRAME_OWIDTH2 + hw] + + pix[hh * FRAME_OWIDTH2 + hw - 1] + + pix[hh * FRAME_OWIDTH2 + hw]; + med /= 4; + + // Print temperatures and time + loctime = localtime (&now1); + + sprintf(st1,"'C %.1f/%.1f/", + raw2temperature(min), raw2temperature(med)); + sprintf(st2, "%.1f ", raw2temperature(max)); + strftime(&st2[strlen(st2)], 60, "%H:%M:%S", loctime); + + if (flirone_pro) { + strcat(st1, st2); + st1[MAX_CHARS2 - 1] = 0; + font_write(fb_proc, 1, FRAME_OHEIGHT2, st1, FONT_COLOR_DFLT); + } else { + // Print in 2 lines for FLIR ONE G3 + st1[MAX_CHARS2 - 1] = 0; + st2[MAX_CHARS2 - 1] = 0; + font_write(fb_proc, 1, FRAME_OHEIGHT2 + 2, st1, FONT_COLOR_DFLT); + font_write(fb_proc, 1, FRAME_OHEIGHT2 + 12, st2, FONT_COLOR_DFLT); } - else - { - write(fdwr2, fb_proc2, framesize2); // colorized RGB Thermal Image + + // show crosshairs, remove if required + font_write(fb_proc, hw - 2, hh - 3, "+", FONT_COLOR_DFLT); + + maxx -= 4; + maxy -= 4; + + if (maxx < 0) + maxx = 0; + if (maxy < 0) + maxy = 0; + if (maxx > FRAME_OWIDTH2 - 10) + maxx = FRAME_OWIDTH2 - 10; + if (maxy > FRAME_OHEIGHT2 - 10) + maxy = FRAME_OHEIGHT2 - 10; + + font_write(fb_proc, FRAME_OWIDTH2 - 6, maxy, "<", FONT_COLOR_DFLT); + font_write(fb_proc, maxx, FRAME_OHEIGHT2 - 8, "|", FONT_COLOR_DFLT); + + // build RGB image + for (y = 0; y < FRAME_HEIGHT2; y++) { + for (x = 0; x < FRAME_WIDTH2; x++) { + // fb_proc is the gray scale frame buffer + v = fb_proc[y * FRAME_OWIDTH2 + x]; + if (pal_inverse) + v = 255 - v; + + for (disp = 0; disp < 3; disp++) + // fb_proc2 is a 24bit RGB buffer + fb_proc2[3 * y * FRAME_OWIDTH2 + 3 * x + disp] = + colormap[3 * v + disp]; + } } + +render: + // jpg Visual Image + if (write(fdwr1, &buf85[28 + ThermalSize], JpgSize) != JpgSize) { + perror("write visual image failed"); + exit(1); } + if (strncmp((char *)&buf85[28 + ThermalSize + JpgSize + 17], "FFC", 3) == 0) { + printf("drop FFC frame\n"); + FFC = 1; // drop all FFC frames + } else { + if (FFC == 1) { + printf("drop first frame after FFC\n"); + FFC = 0; // drop first frame after FFC + } else { + // colorized RGB Thermal Image + if (write(fdwr2, fb_proc2, framesize2) != (ssize_t)framesize2) { + perror("write thermal image failed"); + exit(1); + } + } + } + // free memory + free(fb_proc); // thermal RAW + free(fb_proc2); // visible jpg +} - // free memory - free(fb_proc); // thermal RAW - free(fb_proc2); // visible jpg - +static int find_lvr_flirusb(void) +{ + devh = libusb_open_device_with_vid_pid(NULL, VENDOR_ID, PRODUCT_ID); + return devh ? 0 : -EIO; } - static int find_lvr_flirusb(void) - { - devh = libusb_open_device_with_vid_pid(NULL, VENDOR_ID, PRODUCT_ID); - return devh ? 0 : -EIO; - } - - void print_bulk_result(char ep[],char EP_error[], int r, int actual_length, unsigned char buf[]) - { - time_t now1; - int i; - - now1 = time(NULL); - if (r < 0) { - if (strcmp (EP_error, libusb_error_name(r))!=0) - { - strcpy(EP_error, libusb_error_name(r)); - fprintf(stderr, "\n: %s >>>>>>>>>>>>>>>>>bulk transfer (in) %s:%i %s\n", ctime(&now1), ep , r, libusb_error_name(r)); - sleep(1); - } - //return 1; - } else - { - printf("\n: %s bulk read EP %s, actual length %d\nHEX:\n",ctime(&now1), ep ,actual_length); - // write frame to file - /* - char filename[100]; - sprintf(filename, "EP%s#%05i.bin",ep,filecount); - filecount++; - FILE *file = fopen(filename, "wb"); - fwrite(buf, 1, actual_length, file); - fclose(file); - */ - // hex print of first byte - for (i = 0; i < (((200)<(actual_length))?(200):(actual_length)); i++) { - printf(" %02x", buf[i]); - } - - printf("\nSTRING:\n"); - for (i = 0; i < (((200)<(actual_length))?(200):(actual_length)); i++) { - if(buf[i]>31) {printf("%c", buf[i]);} - } - printf("\n"); - - } - } - - int EPloop(unsigned char *colormap) - { - int i,r = 1; - r = libusb_init(NULL); - if (r < 0) { - fprintf(stderr, "failed to initialise libusb\n"); - exit(1); - } - - r = find_lvr_flirusb(); - if (r < 0) { - fprintf(stderr, "Could not find/open device\n"); - goto out; - } - printf("Successfully find the Flir One G2 device\n"); - +static void usb_exit(void) +{ + //close the device + libusb_reset_device(devh); + libusb_close(devh); + libusb_exit(NULL); +} + +static int usb_init(void) +{ + int r; + + r = libusb_init(NULL); + if (r < 0) { + fprintf(stderr, "failed to initialise libusb\n"); + exit(1); + } + + r = find_lvr_flirusb(); + if (r < 0) { + fprintf(stderr, "Could not find/open device\n"); + goto out; + } + printf("Successfully find the Flir One G2/G3/Pro device\n"); r = libusb_set_configuration(devh, 3); if (r < 0) { - fprintf(stderr, "libusb_set_configuration error %d\n", r); + fprintf(stderr, "libusb_set_configuration error %d\n", r); + goto out; + } + printf("Successfully set usb configuration 3\n"); + + // Claiming of interfaces is a purely logical operation; + // it does not cause any requests to be sent over the bus. + r = libusb_claim_interface(devh, 0); + if (r < 0) { + fprintf(stderr, "libusb_claim_interface 0 error %d\n", r); + goto out; + } + r = libusb_claim_interface(devh, 1); + if (r < 0) { + fprintf(stderr, "libusb_claim_interface 1 error %d\n", r); + goto out; + } + r = libusb_claim_interface(devh, 2); + if (r < 0) { + fprintf(stderr, "libusb_claim_interface 2 error %d\n", r); goto out; } - printf("Successfully set usb configuration 3\n"); - - - // Claiming of interfaces is a purely logical operation; - // it does not cause any requests to be sent over the bus. - r = libusb_claim_interface(devh, 0); - if (r <0) { - fprintf(stderr, "libusb_claim_interface 0 error %d\n", r); - goto out; - } - r = libusb_claim_interface(devh, 1); - if (r < 0) { - fprintf(stderr, "libusb_claim_interface 1 error %d\n", r); - goto out; - } - r = libusb_claim_interface(devh, 2); - if (r < 0) { - fprintf(stderr, "libusb_claim_interface 2 error %d\n", r); - goto out; - } - printf("Successfully claimed interface 0,1,2\n"); - - - unsigned char buf[1048576]; + printf("Successfully claimed interface 0, 1, 2\n"); + return 0; + +out: + usb_exit(); + return -1; +} + +static int EPloop(unsigned char *colormap) +{ + int r = 0; + unsigned char buf[BUF85SIZE]; int actual_length; + time_t now; + char EP85_error[50] = ""; + unsigned char data[2] = { 0, 0 }; // only a bad dummy + int state, timeout; - time_t now; - // save last error status to avoid clutter the log - char EP81_error[50]="", EP83_error[50]="",EP85_error[50]=""; - unsigned char data[2]={0,0}; // only a bad dummy - - // don't forget: $ sudo modprobe v4l2loopback video_nr=0,1 - startv4l2(); - - int state = 1; - int ct=0; + if (usb_init() < 0) + return -1; - while (1) - { - - switch(state) { - - case 1: - /* Flir config - 01 0b 01 00 01 00 00 00 c4 d5 - 0 bmRequestType = 01 - 1 bRequest = 0b - 2 wValue 0001 type (H) index (L) stop=0/start=1 (Alternate Setting) - 4 wIndex 01 interface 1/2 - 5 wLength 00 - 6 Data 00 00 - - libusb_control_transfer (*dev_handle, bmRequestType, bRequest, wValue, wIndex, *data, wLength, timeout) - */ - + // save last error status to avoid clutter the log + startv4l2(); + + state = 1; + + // don't change timeout=100ms !! + timeout = 100; + while (1) { + switch(state) { + case 1: printf("stop interface 2 FRAME\n"); - r = libusb_control_transfer(devh,1,0x0b,0,2,data,0,100); + r = libusb_control_transfer(devh, REQ_TYPE, REQ, V_STOP, INDEX(2), + data, LEN(0), timeout); if (r < 0) { fprintf(stderr, "Control Out error %d\n", r); return r; } printf("stop interface 1 FILEIO\n"); - r = libusb_control_transfer(devh,1,0x0b,0,1,data,0,100); + r = libusb_control_transfer(devh, REQ_TYPE, REQ, V_STOP, INDEX(1), + data, LEN(0), timeout); if (r < 0) { fprintf(stderr, "Control Out error %d\n", r); return r; - } - - printf("\nstart interface 1 FILEIO\n"); - r = libusb_control_transfer(devh,1,0x0b,1,1,data,0,100); - if (r < 0) { - fprintf(stderr, "Control Out error %d\n", r); - return r; - } - now = time(0); // Get the system time - printf("\n:xx %s",ctime(&now)); - state = 3; // jump over wait stait 2. Not really using any data from CameraFiles.zip - break; - - - case 2: - printf("\nask for CameraFiles.zip on EP 0x83:\n"); - now = time(0); // Get the system time - printf("\n: %s",ctime(&now)); - - int transferred = 0; - char my_string[128]; - - //--------- write string: {"type":"openFile","data":{"mode":"r","path":"CameraFiles.zip"}} - int length = 16; - unsigned char my_string2[16]={0xcc,0x01,0x00,0x00,0x01,0x00,0x00,0x00,0x41,0x00,0x00,0x00,0xF8,0xB3,0xF7,0x00}; - printf("\nEP 0x02 to be sent Hexcode: %i Bytes[",length); - int i; - for (i = 0; i < length; i++) { - printf(" %02x", my_string2[i]); - - } - printf(" ]\n"); - - r = libusb_bulk_transfer(devh, 2, my_string2, length, &transferred, 0); - if(r == 0 && transferred == length) - { - printf("\nWrite successful!"); } - else - printf("\nError in write! res = %d and transferred = %d\n", r, transferred); - - strcpy( my_string,"{\"type\":\"openFile\",\"data\":{\"mode\":\"r\",\"path\":\"CameraFiles.zip\"}}"); - - length = strlen(my_string)+1; - printf("\nEP 0x02 to be sent: %s", my_string); - - // avoid error: invalid conversion from ‘char*’ to ‘unsigned char*’ [-fpermissive] - unsigned char *my_string1 = (unsigned char*)my_string; - //my_string1 = (unsigned char*)my_string; - - r = libusb_bulk_transfer(devh, 2, my_string1, length, &transferred, 0); - if(r == 0 && transferred == length) - { - printf("\nWrite successful!"); - printf("\nSent %d bytes with string: %s\n", transferred, my_string); - } - else - printf("\nError in write! res = %d and transferred = %d\n", r, transferred); - - //--------- write string: {"type":"readFile","data":{"streamIdentifier":10}} - length = 16; - unsigned char my_string3[16]={0xcc,0x01,0x00,0x00,0x01,0x00,0x00,0x00,0x33,0x00,0x00,0x00,0xef,0xdb,0xc1,0xc1}; - printf("\nEP 0x02 to be sent Hexcode: %i Bytes[",length); - for (i = 0; i < length; i++) { - printf(" %02x", my_string3[i]); + printf("\nstart interface 1 FILEIO\n"); + r = libusb_control_transfer(devh, REQ_TYPE, REQ, V_START, + INDEX(1), data, LEN(0), timeout); + if (r < 0) { + fprintf(stderr, "Control Out error %d\n", r); + return r; } - printf(" ]\n"); - - r = libusb_bulk_transfer(devh, 2, my_string3, length, &transferred, 0); - if(r == 0 && transferred == length) - { - printf("\nWrite successful!"); - } - else - printf("\nError in write! res = %d and transferred = %d\n", r, transferred); - - - //strcpy( my_string, "{\"type\":\"setOption\",\"data\":{\"option\":\"autoFFC\",\"value\":true}}"); - strcpy( my_string,"{\"type\":\"readFile\",\"data\":{\"streamIdentifier\":10}}"); - length = strlen(my_string)+1; - printf("\nEP 0x02 to be sent %i Bytes: %s", length, my_string); - - // avoid error: invalid conversion from ‘char*’ to ‘unsigned char*’ [-fpermissive] - my_string1 = (unsigned char*)my_string; - - r = libusb_bulk_transfer(devh, 2, my_string1, length, &transferred, 0); - if(r == 0 && transferred == length) - { - printf("\nWrite successful!"); - printf("\nSent %d bytes with string: %s\n", transferred, my_string); - } - else - printf("\nError in write! res = %d and transferred = %d\n", r, transferred); - - - // go to next state - now = time(0); // Get the system time - printf("\n: %s",ctime(&now)); - //sleep(1); - state = 3; + now = time(0); // Get the system time + printf("\n:xx %s", ctime(&now)); + state = 2; break; - - case 3: - printf("\nAsk for video stream, start EP 0x85:\n"); - - r = libusb_control_transfer(devh,1,0x0b,1,2,data, 2,200); + case 2: + printf("\nAsk for video stream, start EP 0x85:\n"); + r = libusb_control_transfer(devh, REQ_TYPE, REQ, V_START, + INDEX(2), data, LEN(2), timeout * 2); if (r < 0) { fprintf(stderr, "Control Out error %d\n", r); return r; - }; + } - state = 4; + state = 3; break; - case 4: - // endless loop - // poll Frame Endpoints 0x85 - // don't change timeout=100ms !! - r = libusb_bulk_transfer(devh, 0x85, buf, sizeof(buf), &actual_length, 100); + case 3: + // endless loop + // poll Frame Endpoints 0x85 + r = libusb_bulk_transfer(devh, 0x85, buf, sizeof(buf), + &actual_length, timeout); if (actual_length > 0) - vframe("0x85",EP85_error, r, actual_length, buf, colormap); - - break; - - } + vframe("0x85", EP85_error, r, actual_length, buf, colormap); + break; + } // poll Endpoints 0x81, 0x83 - r = libusb_bulk_transfer(devh, 0x81, buf, sizeof(buf), &actual_length, 10); -/* - if (actual_length > 0 && actual_length <= 101) - { - - - - char k[5]; - if (strncmp (&buf[32],"VoltageUpdate",13)==0) - { - printf("xx %d\n",actual_length); - - - char *token, *string, *tofree, *string2; -// char l; - strcpy(string,buf); -// string = buf; -// assert(string != NULL); - printf("yy\n"); - - for (i = 32; i < (((200)<(actual_length))?(200):(actual_length)); i++) - { - if(string[i]>31) - { - printf("%c", string[i]); -// printf("%d ", i); -// string2[i-32] = string[i]; - } - } - - while ((token = strsep(&string, ":")) != NULL) - { - printf("zz\n"); - printf("%s\n", token); - } - -// free(tofree); -// for (i = 32; i < (((200)<(actual_length))?(200):(actual_length)); i++) { -// if(buf[i]>31) {printf("%c", buf[i]);} -// } - - - } - } + r = libusb_bulk_transfer(devh, 0x81, buf, sizeof(buf), &actual_length, 10); + r = libusb_bulk_transfer(devh, 0x83, buf, sizeof(buf), &actual_length, 10); + if (strcmp(libusb_error_name(r), "LIBUSB_ERROR_NO_DEVICE")==0) { + fprintf(stderr, "EP 0x83 LIBUSB_ERROR_NO_DEVICE -> reset USB\n"); + goto out; + } + } + // never reached ;-) + libusb_release_interface(devh, 0); -*/ +out: + usb_exit(); + return r >= 0 ? r : -r; +} - r = libusb_bulk_transfer(devh, 0x83, buf, sizeof(buf), &actual_length, 10); - if (strcmp(libusb_error_name(r), "LIBUSB_ERROR_NO_DEVICE")==0) { - fprintf(stderr, "EP 0x83 LIBUSB_ERROR_NO_DEVICE -> reset USB\n"); - goto out; - } -// print_bulk_result("0x83",EP83_error, r, actual_length, buf); +static void usage(void) +{ + fprintf(stderr, + "Usage:\n" + "\n" + "./flirone [option]* palletes/\n" + "\n" + "Options:\n" + "\t-i\tUse inverse pallete colors\n" + "\t-p\tShow pallete colors instead of sensor data\n" + "\t--pro\tSelect FLIR ONE PRO camera (default is FLIR ONE G3)\n" + "\t-v \tUse /dev/video and /dev/video devices (default is n=1)\n"); + exit(1); } - - // never reached ;-) - libusb_release_interface(devh, 0); - - out: - //close the device - libusb_reset_device(devh); - libusb_close(devh); - libusb_exit(NULL); - return r >= 0 ? r : -r; - } int main(int argc, char **argv) { int i; + long n; + const char *arg, *palpath = NULL; unsigned char colormap[768]; + char *endptr; FILE *fp; - if(argc < 2) { - fprintf(stderr, "\nUsage:flir8o palette.raw\n"); - exit(1); - } + /* parse args */ + if (argc < 2) + usage(); + + n = 1; + for (i = 1; i < argc; i++) { + arg = argv[i]; + + if (strcmp(arg, "--pro") == 0) { + flirone_pro = 1; + frame_width2 = 160; + frame_height2 = 128; + frame_owidth2 = 160; + frame_oheight2 = 120; + + } else if (strcmp(arg, "-v") == 0) { + arg = argv[++i]; + n = strtol(arg, &endptr, 10); + if (n < 0 || n > 65535 || *endptr != 0) + usage(); + + } else if (strcmp(arg, "-i") == 0) { + pal_inverse = 1; + + } else if (strcmp(arg, "-p") == 0) { + pal_colors = 1; + + } else { + if (palpath) + usage(); + palpath = arg; + } + } + if (palpath == NULL) + usage(); - fp = fopen(argv[1], "rb"); - fread(colormap, sizeof(unsigned char), 768, fp); // read 256 rgb values - fclose(fp); + sprintf(video_device1, "/dev/video%ld", n); + sprintf(video_device2, "/dev/video%ld", n + 1); - while (1) - { - EPloop(colormap); - } + fp = fopen(palpath, "rb"); + // read 256 rgb values + if (fread(colormap, 1, 768, fp) != 768) { + perror("failed to read colormap"); + exit(1); + } + fclose(fp); - -} + while (1) + EPloop(colormap); +} From a8cbe363f4740bcf17c420b7f64185df98e5ba85 Mon Sep 17 00:00:00 2001 From: Leandro Lupori Date: Wed, 18 May 2022 14:48:28 -0300 Subject: [PATCH 2/2] Update README.md --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 941f4d2..96c1586 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,11 @@ This is a cleaned up version of code posted here: http://www.eevblog.com/forum/thermal-imaging/question-about-flir-one-for-android/ -All credit goes to tomas123, cynfab etc from that forum who did the awesome research and work to make this support. \ No newline at end of file +All credit goes to tomas123, cynfab etc from that forum who did the awesome research and work to make this support. + +### Add Support for FLIR ONE G3 Cameras + +The branch g3 of this fork adds support for FLIR ONE G3 cameras (it may also work with PRO LT models), that have a sensor with 80x60 resolution. + +It also adds some command line options, that can be displayed by running the program without any arguments. +The `--pro` flag should make it compatible with FLIR ONE G2 and PRO models, although I don't have one of those to test.