I'm want to use the MLX90640 Thermal Camera module from Adafruit with my Wio Terminal to make a diagnostic tool that I can use for thermal measurements. I tried out a module with a 55 degree FOV lens with Jupyter Notebooks on my PC Using the MLX90640 Thermal Camera with Jupyter Notebooks .
Now I'm going to try out a module with a wide angle 110 degree FOV with the Wio Terminal. I'm using a STEMMA-QT to Grove cable to connect the modules, so I can easily swap modules to change the FOV. I also need to 3D print a holder so that I can attach the module to the back of the Wio Terminal (I'm going to create a dummy header to plug into the RPi connector).
First, I need to try out some software (I'm using the Arduino IDE). It seems that each board manufacturer has their own variant of the sensor library and different examples are included. I had used the Adafruit MLX90640 library with the example that I used with Jupyter Notebooks. I decided to try the SparkFun library next. That library has an example that sends the sensor data as a serial stream over USB to a Processing app to visualize the data.
Here is base setup (I still need to figure out how to mount the camera module, but it's easier to take selfies with the camera loose).
And a short video of the data visualization. I wearing my glasses in this one.
Of course, the whole purpose of using the Wio Terminal is to show the data on its built-in LCD display. So, I switched to using the Seeed Studio library which supports both the MLX90640 and MLX90641. The MLX90640 has a 32x24 pixel array and the MLX90641 has a 16x12 pixel array. The MLX90640 is spec'ed for a commercial temperature range and the MLX90641 for industrial temperature range.
The Seeed library has an example of using a Grove MLX90641 module with the Wio Terminal. I needed to adjust array sizes to handle the larger sensor of the MLX90640 and also corrected a mistake in the Celsius to Fahrenheit conversion. The one thing that I could not get working was using sprites with the display. I assume the code did work with an MLX90641. It could be that I'm encountering a memory issue with the larger arrays. Anyway, I'm doing it without using sprites - reduced the refresh rate and inserted a little delay in the loop. I'll try to figure this out later.
Wio_Terminal_MLX90640_noSprite.ino
#include <Wire.h> #include "MLX90640_API.h" #include "MLX9064X_I2C_Driver.h" #include <TFT_eSPI.h> // Include the graphics library (this includes the sprite functions) const byte MLX90640_address = 0x33; //Default 7-bit unshifted address of the MLX90640 #define TA_SHIFT 8 //Default shift for MLX90640 in open air #define debug Serial uint16_t eeMLX90640[832]; float MLX90640To[768]; uint16_t MLX90640Frame[834]; paramsMLX90640 MLX90640; int errorno = 0; TFT_eSPI tft = TFT_eSPI(); //TFT_eSprite Display = TFT_eSprite(&tft); // Create Sprite object "img" with pointer to "tft" object // the pointer is used by pushSprite() to push it onto the TFT unsigned long CurTime; uint16_t TheColor; // start with some initial colors uint16_t MinTemp = 25; uint16_t MaxTemp = 38; // variables for interpolated colors byte red, green, blue; // variables for row/column interpolation byte i, j, k, row, col, incr; float intPoint, val, a, b, c, d, ii; byte aLow, aHigh; // size of a display "pixel" byte BoxWidth = 3; byte BoxHeight = 3; int x, y; char buf[20]; // variable to toggle the display grid int ShowGrid = -1; // array for the interpolated array float HDTemp[6400]; void setup() { Wire.begin(); Wire.setClock(2000000); //Increase I2C clock speed to 2M debug.begin(115200); //Fast debug as possible // while (!debug); // wait for terminal // start the display and set the background to black if (isConnected() == false) { debug.println("MLX90640 not detected at default I2C address. Please check wiring. Freezing."); while (1); } //Get device parameters - We only have to do this once int status; status = MLX90640_DumpEE(MLX90640_address, eeMLX90640); errorno = status;//MLX90640_CheckEEPROMValid(eeMLX90640);//eeMLX90640[10] & 0x0040;// if (status != 0) { debug.println("Failed to load system parameters"); while(1); } status = MLX90640_ExtractParameters(eeMLX90640, &MLX90640); //errorno = status; if (status != 0) { debug.println("Parameter extraction failed"); while(1); } //Once params are extracted, we can release eeMLX90640 array // MLX90640_SetRefreshRate(MLX90640_address, 0x05); //Set rate to 16Hz MLX90640_SetRefreshRate(MLX90640_address, 0x02); //Set rate to 2Hz tft.begin(); tft.setRotation(3); tft.fillScreen(TFT_BLACK); // Display.createSprite(TFT_HEIGHT, TFT_WIDTH); // Display.fillSprite(TFT_BLACK); // get the cutoff points for the color interpolation routines // note this function called when the temp scale is changed Getabcd(); // draw a legend with the scale that matches the sensors max and min DrawLegend(); tft.fillRect(15, 15, 210, 210, TFT_BLUE); } void loop() { // draw a large white border for the temperature area // tft.fillRect(10, 10, 210, 210, TFT_BLUE); for (byte x = 0 ; x < 2 ; x++) { int status = MLX90640_GetFrameData(MLX90640_address, MLX90640Frame); float vdd = MLX90640_GetVdd(MLX90640Frame, &MLX90640); float Ta = MLX90640_GetTa(MLX90640Frame, &MLX90640); float tr = Ta - TA_SHIFT; //Reflected temperature based on the sensor ambient temperature float emissivity = 0.95; MLX90640_CalculateTo(MLX90640Frame, &MLX90640, emissivity, tr, MLX90640To); } interpolate_image(MLX90640To,24,32,HDTemp,80,80); //display the 80 x 80 array DisplayGradient(); //Crosshair in the middle of the screen tft.drawCircle(115, 115, 5, TFT_WHITE); tft.drawFastVLine(115, 105, 20, TFT_WHITE); tft.drawFastHLine(105, 115, 20, TFT_WHITE); //Displaying the temp at the middle of the Screen //Push the Sprite to the screen // Display.pushSprite(0, 0); tft.setRotation(3); tft.setTextColor(TFT_WHITE); tft.drawFloat(HDTemp[35 * 80 + 35], 2, 90, 20); delay(500); } //Returns true if the MLX90640 is detected on the I2C bus boolean isConnected() { Wire.beginTransmission((uint8_t)MLX90640_address); if (Wire.endTransmission() != 0) { return (false); //Sensor did not ACK } return (true); } // function to display the results void DisplayGradient() { tft.setRotation(4); // rip through 70 rows for (row = 0; row < 70; row ++) { // fast way to draw a non-flicker grid--just make every 10 MLX90640To 2x2 as opposed to 3x3 // drawing lines after the grid will just flicker too much if (ShowGrid < 0) { BoxWidth = 3; } else { if ((row % 10 == 9) ) { BoxWidth = 2; } else { BoxWidth = 3; } } // then rip through each 70 cols for (col = 0; col < 70; col++) { // fast way to draw a non-flicker grid--just make every 10 MLX90640To 2x2 as opposed to 3x3 if (ShowGrid < 0) { BoxHeight = 3; } else { if ( (col % 10 == 9)) { BoxHeight = 2; } else { BoxHeight = 3; } } // finally we can draw each the 70 x 70 points, note the call to get interpolated color tft.fillRect((row * 3) + 15, (col * 3) + 15, BoxWidth, BoxHeight, GetColor(HDTemp[row * 80 + col])); } } } // my fast yet effective color interpolation routine uint16_t GetColor(float val) { /* pass in value and figure out R G B several published ways to do this I basically graphed R G B and developed simple linear equations again a 5-6-5 color display will not need accurate temp to R G B color calculation equations based on http://web-tech.ga-usa.com/2012/05/creating-a-custom-hot-to-cold-temperature-color-gradient-for-use-with-rrdtool/index.html */ red = constrain(255.0 / (c - b) * val - ((b * 255.0) / (c - b)), 0, 255); if ((val > MinTemp) & (val < a)) { green = constrain(255.0 / (a - MinTemp) * val - (255.0 * MinTemp) / (a - MinTemp), 0, 255); } else if ((val >= a) & (val <= c)) { green = 255; } else if (val > c) { green = constrain(255.0 / (c - d) * val - (d * 255.0) / (c - d), 0, 255); } else if ((val > d) | (val < a)) { green = 0; } if (val <= b) { blue = constrain(255.0 / (a - b) * val - (255.0 * b) / (a - b), 0, 255); } else if ((val > b) & (val <= d)) { blue = 0; } else if (val > d) { blue = constrain(240.0 / (MaxTemp - d) * val - (d * 240.0) / (MaxTemp - d), 0, 240); } // use the displays color mapping function to get 5-6-5 color palet (R=5 bits, G=6 bits, B-5 bits) return tft.color565(red, green, blue); } // function to get the cutoff points in the temp vs RGB graph void Getabcd() { a = MinTemp + (MaxTemp - MinTemp) * 0.2121; b = MinTemp + (MaxTemp - MinTemp) * 0.3182; c = MinTemp + (MaxTemp - MinTemp) * 0.4242; d = MinTemp + (MaxTemp - MinTemp) * 0.8182; } float get_point(float *p, uint8_t rows, uint8_t cols, int8_t x, int8_t y) { if (x < 0) { x = 0; } if (y < 0) { y = 0; } if (x >= cols) { x = cols - 1; } if (y >= rows) { y = rows - 1; } return p[y * cols + x]; } void set_point(float *p, uint8_t rows, uint8_t cols, int8_t x, int8_t y, float f) { if ((x < 0) || (x >= cols)) { return; } if ((y < 0) || (y >= rows)) { return; } p[y * cols + x] = f; } // src is a grid src_rows * src_cols // dest is a pre-allocated grid, dest_rows*dest_cols void interpolate_image(float *src, uint8_t src_rows, uint8_t src_cols, float *dest, uint8_t dest_rows, uint8_t dest_cols) { float mu_x = (src_cols - 1.0) / (dest_cols - 1.0); float mu_y = (src_rows - 1.0) / (dest_rows - 1.0); float adj_2d[16]; // matrix for storing adjacents for (uint8_t y_idx = 0; y_idx < dest_rows; y_idx++) { for (uint8_t x_idx = 0; x_idx < dest_cols; x_idx++) { float x = x_idx * mu_x; float y = y_idx * mu_y; get_adjacents_2d(src, adj_2d, src_rows, src_cols, x, y); float frac_x = x - (int)x; // we only need the ~delta~ between the points float frac_y = y - (int)y; // we only need the ~delta~ between the points float out = bicubicInterpolate(adj_2d, frac_x, frac_y); set_point(dest, dest_rows, dest_cols, x_idx, y_idx, out); } } } // p is a list of 4 points, 2 to the left, 2 to the right float cubicInterpolate(float p[], float x) { float r = p[1] + (0.5 * x * (p[2] - p[0] + x * (2.0 * p[0] - 5.0 * p[1] + 4.0 * p[2] - p[3] + x * (3.0 * (p[1] - p[2]) + p[3] - p[0])))); return r; } // p is a 16-point 4x4 array of the 2 rows & columns left/right/above/below float bicubicInterpolate(float p[], float x, float y) { float arr[4] = {0, 0, 0, 0}; arr[0] = cubicInterpolate(p + 0, x); arr[1] = cubicInterpolate(p + 4, x); arr[2] = cubicInterpolate(p + 8, x); arr[3] = cubicInterpolate(p + 12, x); return cubicInterpolate(arr, y); } // src is rows*cols and dest is a 4-point array passed in already allocated! void get_adjacents_1d(float *src, float *dest, uint8_t rows, uint8_t cols, int8_t x, int8_t y) { // pick two items to the left dest[0] = get_point(src, rows, cols, x - 1, y); dest[1] = get_point(src, rows, cols, x, y); // pick two items to the right dest[2] = get_point(src, rows, cols, x + 1, y); dest[3] = get_point(src, rows, cols, x + 2, y); } // src is rows*cols and dest is a 16-point array passed in already allocated! void get_adjacents_2d(float *src, float *dest, uint8_t rows, uint8_t cols, int8_t x, int8_t y) { float arr[4]; for (int8_t delta_y = -1; delta_y < 3; delta_y++) { // -1, 0, 1, 2 float *row = dest + 4 * (delta_y + 1); // index into each chunk of 4 for (int8_t delta_x = -1; delta_x < 3; delta_x++) { // -1, 0, 1, 2 row[delta_x + 1] = get_point(src, rows, cols, x + delta_x, y + delta_y); } } } // function to draw a legend void DrawLegend() { //color legend with max and min text j = 0; float inc = (MaxTemp - MinTemp ) / 160.0; for (ii = MinTemp; ii < MaxTemp; ii += inc) { tft.drawFastHLine(260, 200 - j++, 30, GetColor(ii)); } tft.setTextSize(2); tft.setCursor(245, 20); tft.setTextColor(TFT_WHITE, TFT_BLACK); sprintf(buf, "%2d/%2d", MaxTemp, (int) (MaxTemp * 1.8) + 32); tft.print(buf); tft.setTextSize(2); tft.setCursor(245, 210); tft.setTextColor(TFT_WHITE, TFT_BLACK); sprintf(buf, "%2d/%2d", MinTemp, (int) (MinTemp * 1.8) + 32); tft.print(buf); }
And another short video. The temperature displayed at the top is the temperature of the pixels in the crosshairs in the center of the image. In this video I don't have my glasses on.
Next, I need to add the battery pack and mount the camera to the side opposite the LCD and I will have a standalone thermal camera . I'll post when that's done.
Here is the link to the finished project: Wio Terminal Thermal Camera