Okay, take a deep breath. Here's the latest version of the full sketch to copy and upload. See you at the other end!
// SPDX-FileCopyrightText: 2020 Anne Barela for Adafruit Industries // // SPDX-License-Identifier: MIT /* ThermalImager_009b - Collect thermal image values from a MLX90640 sensor array, display them as color-mapped pixels on a TFT screen, include data capture to flash media, and a user configuration menu. Written by Eduardo using code from these sources. Arcada and MLX90640 libraries from adafruit.com Ver. 1 - Read temps, auto-range extremes, display gray squares on TFT Ver. 2 - Add Ironbow color palette, low+center+high markers Ver. 3 - Add crude BMP image write to SD Ver. 4 - Attach interrupts to improve button response Ver. 5 - Store BMPs to SD in an orderly manner, in folders Ver. 6 - Port to Teensy 3.2, where the libraries used are suited Ver. 7 - Port to Adafruit PyBadge using Arcada library. Use simulated data while awaiting hardware release Ver. 8 - Convert menu to scrolling style and add settings for emissivity and frame rate, more if feasible. Ver. 9 - Bring in the current Adafruit library and read a real sensor. */ #include <Adafruit_MLX90640.h> #include "Adafruit_Arcada.h" Adafruit_MLX90640 mlx; Adafruit_Arcada arcada; #if !defined(USE_TINYUSB) #warning "Compile with TinyUSB selected!" #endif File myFile; float mlx90640To[768]; // Here we receive the float vals acquired from MLX90640 #define DE_BOUNCE 200 // Wait this many msec between button clicks #define MENU_LEN 12 // Number of total available menu choices #define MENU_ROWS 9 // Number of menu lines that can fit on screen #define MENU_VPOS 6 #define GRAY_33 0x528A #define BOTTOM_DIR "MLX90640" #define DIR_FORMAT "/dir%05d" #define BMP_FORMAT "/frm%05d.bmp" #define CFG_FLNAME "/config.ini" #define MAX_SERIAL 999 // BMP File Header, little end first, Photoshop ver. const PROGMEM uint8_t BmpPSPHead[14] = { 0x42, 0x4D, // "BM" in hex 0x38, 0x09, 0x00, 0x00, // File size, 2360 0x00, 0x00, // reserved for app data 1 0x00, 0x00, // reserved for app data 2 0x36, 0x00, 0x00, 0x00 // Offset of first pixel, 54 }; // BMP 24-bit DIB Header, little end first, Photoshop ver. const PROGMEM uint8_t DIBHeadPSP1[40] = { 0x28, 0x00, 0x00, 0x00, // Header size, 40 0x20, 0x00, 0x00, 0x00, // pixel width, 32 0x18, 0x00, 0x00, 0x00, // pixel height, 24 0x01, 0x00, // color planes, 1 0x18, 0x00, // bits per pixel, 24 0x00, 0x00, 0x00, 0x00, // Compression method, 0==none 0x00, 0x00, 0x00, 0x00, // Raw bitmap data size, dummy 0 0x12, 0x0B, 0x00, 0x00, // Pixels per meter H, 2834 0x12, 0x0B, 0x00, 0x00, // Pixels per meter V, 2834 0x00, 0x00, 0x00, 0x00, // Colors in palette, 0==default 2^n 0x00, 0x00, 0x00, 0x00 // Number of important colors, 0 }; // BMP file data, 2 byte padding const PROGMEM uint8_t PSPpad[2] = {0x00, 0x00}; //Byte arrays of bitmapped icons, 16 x 12 px: const PROGMEM uint8_t battIcon[] = { 0x0f, 0x00, 0x3f, 0xc0, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x3f, 0xc0}; const PROGMEM uint8_t camIcon[] = { 0x01, 0xe0, 0x61, 0x20, 0xff, 0xf0, 0x80, 0x10, 0x86, 0x10, 0x89, 0x10, 0x90, 0x90, 0x90, 0x90, 0x89, 0x10, 0x86, 0x10, 0x80, 0x10, 0xff, 0xf0}; const PROGMEM uint8_t SDicon[] = { 0x0f, 0xe0, 0x1f, 0xe0, 0x3c, 0x60, 0x78, 0x60, 0x70, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x6f, 0x60, 0x60, 0x60, 0x7f, 0xe0, 0x7f, 0xe0}; const PROGMEM uint8_t snowIcon[] = { 0x15, 0x00, 0x4E, 0x40, 0xC4, 0x60, 0x75, 0xC0, 0x9F, 0x20, 0x0E, 0x00, 0x0E, 0x00, 0x9F, 0x20, 0x75, 0xC0, 0xC4, 0x60, 0x4E, 0x40, 0x15, 0x00}; uint8_t pixelArray[2304]; // BMP image body, 32 pixels * 24 rows * 3 bytes // Some global values that several functions will use, including // 5 floats to append to the BMP pixel data: // coldest pixel, coldest color, center temp, hottest color, hottest pixel float sneakFloats[5] = {3.1415926, 0.0, -11.7, 98.6, -12.34}; // Test values that get overwritten uint16_t highAddr = 0, lowAddr = 0; // Append the pixel addresses, too uint16_t backColor, lowPixel, highPixel, buttonRfunc = 1, emissivity = 95, frameRate = 4, thermRange = 0, paletteNum = 1, colorPal[256], // Array for color palettes nextDirIndex = 0, nextBMPindex = 0, nextBMPsequence = 1; // These keep count of SD files and dirs, 0==error uint32_t deBounce = 0, buttonBits = 0; boolean mirrorFlag = false, celsiusFlag = false, markersOn = true, screenDim = false, smoothing = false, showLastCap = false, save1frame = false, recordingInProg = false, buttonActive = false; float battAverage = 0.0, colorLow = 0.0, colorHigh = 100.0; // Values for managing color range volatile boolean clickFlagMenu = false, clickFlagSelect = false; // Volatiles for timer callback handling void setup() { if (!arcada.arcadaBegin()) { // Start TFT and fill with black // Serial.print("Failed to begin"); while (1); } arcada.filesysBeginMSD(); // Set up SD or QSPI flash as an external USB drive arcada.displayBegin(); // Activate TFT screen arcada.display->setRotation(1); // wide orientation arcada.display->setTextWrap(false); arcada.setBacklight(255); // Turn on backlight battAverage = arcada.readBatterySensor(); Serial.begin(115200); // while(!Serial); // Wait for user to open terminal Serial.println("MLX90640 IR Array Example"); if(arcada.filesysBegin()){ // Initialize flash storage, begin setting up indices for saving BMPs if(!arcada.exists(BOTTOM_DIR)) { // Is base "MLX90640" directory absent? if(arcada.mkdir(BOTTOM_DIR)) // Can it be added? nextDirIndex = nextBMPindex = 1; // Success, prepare to store numbered files & dirs } else { // "MLX90640" directory exists, can we add files | directories? // Get the number of the next unused serial directory path nextDirIndex = availableFileNumber(1, BOTTOM_DIR + String(DIR_FORMAT)); // and the next unused serial BMP name nextBMPindex = availableFileNumber(1, BOTTOM_DIR + String(BMP_FORMAT)); } } // By now each global index variable is either 0 (no nums available), or the next unclaimed serial num if(!mlx.begin(MLX90640_I2CADDR_DEFAULT, &Wire)) { Serial.println("MLX90640 not found!"); arcada.haltBox("MLX90640 not found!"); while(1) delay(10); // Halt here } Serial.println("Found Adafruit MLX90640"); Serial.print("Serial number: "); Serial.print(mlx.serialNumber[0], HEX); Serial.print(mlx.serialNumber[1], HEX); Serial.println(mlx.serialNumber[2], HEX); //mlx.setMode(MLX90640_INTERLEAVED); mlx.setMode(MLX90640_CHESS); mlx.setResolution(MLX90640_ADC_18BIT); switch(frameRate) { case 0: mlx.setRefreshRate(MLX90640_0_5_HZ); break; // 6 frame rates, 0.5 to 16 FPS in powers of 2 case 1: mlx.setRefreshRate(MLX90640_1_HZ); break; case 2: mlx.setRefreshRate(MLX90640_2_HZ); break; case 3: mlx.setRefreshRate(MLX90640_4_HZ); break; case 4: mlx.setRefreshRate(MLX90640_8_HZ); break; default: mlx.setRefreshRate(MLX90640_16_HZ); break; } Wire.setClock(1000000); // max 1 MHz for(int counter01 = 0; counter01 < 2304; ++counter01) pixelArray[counter01] = counter01 / 9; // Initialize BMP pixel buffer with a gradient loadPalette(paletteNum); // Load false color palette backColor = GRAY_33; // 33% gray for BG setBackdrop(backColor, buttonRfunc); // Current BG, current button labels arcada.timerCallback(50, buttonCatcher); // Assign a 50Hz callback function to catch button presses } void loop() { static uint32_t frameCounter = 0; float scaledPix, highPix, lowPix; uint16_t markColor; // Show the battery level indicator, 3.7V to 3.3V represented by a 7 segment bar battAverage = battAverage * 0.95 + arcada.readBatterySensor() * 0.05; // *Gradually* track battery level highPix = (int)constrain((battAverage - 3.3) * 15.0, 0.0, 6.0) + 1; // Scale it to a 7-segment bar markColor = highPix > 2 ? 0x07E0 : 0xFFE0; // Is the battery level bar green or yellow? markColor = highPix > 1 ? markColor : 0xF800; // ...or even red? arcada.display->fillRect(146, 2, 12, 12, backColor); // Erase old battery icon arcada.display->drawBitmap(146, 2, battIcon, 16, 12, 0xC618); // Redraw gray battery icon arcada.display->fillRect(150, 12 - highPix, 4, highPix, markColor); // Add the level bar // Fetch 768 fresh temperature values from the MLX90640 arcada.display->drawBitmap(146, 18, camIcon, 16, 12, 0xF400); // Show orange camera icon during I2C acquisition if(mlx.getFrame(mlx90640To) != 0) { Serial.println("Failed"); return; } arcada.display->fillRect(146, 18, 12, 12, backColor); // Acquisition done, erase camera icon // First pass: Find hottest and coldest pixels highAddr = lowAddr = 0; highPix = lowPix = mlx90640To[highAddr]; for (int x = 1 ; x < 768 ; x++) { // Compare every pixel if(mlx90640To[x] > highPix) { // Hotter pixel found? highPix = mlx90640To[x]; // Record its values highAddr = x; } if(mlx90640To[x] < lowPix) { // Colder pixel found? lowPix = mlx90640To[x]; // Likewise lowAddr = x; } } if(thermRange == 0) { // Are the colors set to auto-range? colorLow = lowPix; // Then high and low color values get updated colorHigh = highPix; } sneakFloats[0] = lowPix; // Retain these five temperature values sneakFloats[1] = colorLow; // to append to the BMP file, if any sneakFloats[2] = mlx90640To[400]; sneakFloats[3] = colorHigh; sneakFloats[4] = highPix; // Second pass: Scale the float values down to 8-bit and plot colormapped pixels if(mirrorFlag) { // Mirrored display (selfie mode)? for(int y = 0; y < 24; ++y) { // Rows count from bottom up for(int x = 0 ; x < 32 ; x++) { scaledPix = constrain((mlx90640To[32 * y + x] - colorLow) / (colorHigh - colorLow) * 255.9, 0.0, 255.0); pixelArray[3 * (32 * y + x)] = (uint8_t)scaledPix; // Store as a byte in BMP buffer arcada.display->fillRect(140 - x * 4, 92 - y * 4, 4, 4, colorPal[(uint16_t)scaledPix]); // Filled rectangles, bottom up } } } else { // Not mirrored for(int y = 0; y < 24; ++y) { for(int x = 0 ; x < 32 ; x++) { scaledPix = constrain((mlx90640To[32 * y + x] - colorLow) / (colorHigh - colorLow) * 255.9, 0.0, 255.0); pixelArray[3 * (32 * y + x)] = (uint8_t)scaledPix; arcada.display->fillRect(16 + x * 4, 92 - y * 4, 4, 4, colorPal[(uint16_t)scaledPix]); } } } // Post pass: Screen print the lowest, center, and highest temperatures arcada.display->fillRect( 0, 96, 53, 12, colorPal[0]); // Contrasting mini BGs for cold temp arcada.display->fillRect(107, 96, 53, 12, colorPal[255]); // and for hot temperature texts scaledPix = constrain((mlx90640To[400] - colorLow) / (colorHigh - colorLow) * 255.9, 0.0, 255.0); arcada.display->fillRect(53, 96, 54, 12, colorPal[(uint16_t)scaledPix]); // Color coded mini BG for center temp arcada.display->setTextSize(1); arcada.display->setCursor(10, 99); arcada.display->setTextColor(0xFFFF ^ colorPal[0]); // Contrasting text color for coldest value arcada.display->print(celsiusFlag ? lowPix : lowPix * 1.8 + 32.0); // Print Celsius or Fahrenheit arcada.display->setCursor(120, 99); arcada.display->setTextColor(0xFFFF ^ colorPal[255]); // Contrast text for hottest value arcada.display->print(celsiusFlag ? highPix : highPix * 1.8 + 32.0); // Print Celsius or Fahrenheit arcada.display->setCursor(65, 99); if((mlx90640To[400] < (colorLow + colorHigh) * 0.5) == (paletteNum < 3)) arcada.display->setTextColor(0xFFFF); // A contrasting text color for center temp else arcada.display->setTextColor(0x0000); arcada.display->print(celsiusFlag ? mlx90640To[400] : mlx90640To[400] * 1.8 + 32.0); // Pixel 12 * 32 + 16 markColor = 0x0600; // Deep green color to draw onscreen cross markers if(markersOn) { // Show markers? if(mirrorFlag) { // ...over a mirrored display? arcada.display->drawFastHLine(156 - (( lowAddr % 32) * 4 + 16), 93 - 4 * ( lowAddr / 32), 4, markColor); // Color crosses mark cold pixel, arcada.display->drawFastVLine(159 - (( lowAddr % 32) * 4 + 17), 92 - 4 * ( lowAddr / 32), 4, markColor); arcada.display->drawFastHLine(156 - ((highAddr % 32) * 4 + 16), 93 - 4 * (highAddr / 32), 4, markColor); // hot pixel, arcada.display->drawFastVLine(159 - ((highAddr % 32) * 4 + 17), 92 - 4 * (highAddr / 32), 4, markColor); arcada.display->drawFastHLine(76, 45, 4, markColor); // and center pixel arcada.display->drawFastVLine(78, 44, 4, markColor); } else { // Not mirrored arcada.display->drawFastHLine(( lowAddr % 32) * 4 + 16, 93 - 4 * ( lowAddr / 32), 4, markColor); // Color crosses mark cold pixel, arcada.display->drawFastVLine(( lowAddr % 32) * 4 + 17, 92 - 4 * ( lowAddr / 32), 4, markColor); arcada.display->drawFastHLine((highAddr % 32) * 4 + 16, 93 - 4 * (highAddr / 32), 4, markColor); // hot pixel, arcada.display->drawFastVLine((highAddr % 32) * 4 + 17, 92 - 4 * (highAddr / 32), 4, markColor); arcada.display->drawFastHLine(80, 45, 4, markColor); // and center pixel arcada.display->drawFastVLine(81, 44, 4, markColor); } } // Print the frame count on the left sidebar arcada.display->setRotation(0); // Vertical printing arcada.display->setCursor(48, 4); arcada.display->setTextColor(0xFFFF, backColor); // White text, current BG arcada.display->print("FRM "); arcada.display->print(++frameCounter); arcada.display->setRotation(1); // Back to horizontal // Handle any button presses if(!buttonActive && clickFlagMenu) { // Was B:MENU button pressed? buttonActive = true; // Set button flag deBounce = millis() + DE_BOUNCE; // and start debounce timer menuLoop(backColor); // Execute menu routine until finished clickFlagSelect = recordingInProg = false; // Clear unneeded flags nextBMPsequence = 1; setBackdrop(backColor, buttonRfunc); // Repaint current BG & button labels } if(!buttonActive && clickFlagSelect) { // Was the A button pressed? buttonActive = true; // Set button flag deBounce = millis() + DE_BOUNCE; // and start debounce timer if(buttonRfunc == 0) { // Freeze requested? arcada.display->drawBitmap(146, 48, snowIcon, 16, 12, 0xC61F); // Freeze icon on while(buttonBits & ARCADA_BUTTONMASK_A) // Naive freeze: loop until button released delay(10); // Short pause deBounce = millis() + DE_BOUNCE; // Restart debounce timer arcada.display->fillRect(146, 48, 12, 12, backColor); // Freeze icon off } else if(buttonRfunc == 1) { // Capture requested? if((nextBMPindex = availableFileNumber(nextBMPindex, BOTTOM_DIR + String(BMP_FORMAT))) != 0) { // Serialized BMP filename available? save1frame = true; // Set the flag to save a BMP arcada.display->fillRect(0, 96, 160, 12, 0x0600); // Display a green strip arcada.display->setTextColor(0xFFFF); // with white capture message text arcada.display->setCursor(16, 99); arcada.display->print("Saving frame "); arcada.display->print(nextBMPindex); } } else { // Begin or halt recording a sequence of BMP files if(!recordingInProg) { // "A:START RECORDING" was pressed if((nextDirIndex = availableFileNumber(nextDirIndex, BOTTOM_DIR + String(DIR_FORMAT))) != 0) { // Serialized directory name available? // Make the directory if(newDirectory()) { // Success in making a new sequence directory? recordingInProg = true; // Set the flag for saving BMP files nextBMPsequence = 1; // ...numbered starting with 00001 setBackdrop(backColor, 3); // Show "A:STOP RECORDING" label } else // Couldn't make the new directory, so nextDirIndex = 0; // disable further sequences } } else { // "A:STOP RECORDING" was pressed recordingInProg = false; setBackdrop(backColor, 2); // Clear "A:STOP RECORDING" label } } } // Saving any BMP images to flash media happens here if(save1frame || recordingInProg) { // Write a BMP file to SD? arcada.display->drawBitmap(146, 32, SDicon, 16, 12, 0x07E0); // Flash storage activity icon on prepForSave(); // Save to flash. Use global values for parameters nextBMPsequence += recordingInProg ? 1 : 0; // If recording a series, increment frame count save1frame = false; // If one frame saved, clear the flag afterwards arcada.display->fillRect(146, 32, 12, 12, backColor); // Flash storage activity icon off } if(showLastCap) { // Redisplay the last BMP saved? buttonActive = true; // Set button flag deBounce = millis() + DE_BOUNCE; // and start debounce timer recallLastBMP(backColor); // Redisplay last bitmap from buffer until finished setBackdrop(backColor, buttonRfunc); // Repaint current BG & button labels showLastCap = false; } // Here we protect against button bounces while the function loops if(buttonActive && millis() > deBounce && (buttonBits & (ARCADA_BUTTONMASK_B | ARCADA_BUTTONMASK_A)) == 0) // Has de-bounce wait expired & all buttons released? buttonActive = false; // Clear flag to allow another button press clickFlagMenu = clickFlagSelect = false; // End of the loop, clear all interrupt flags } // Compute and fill an array with 256 16-bit color values void loadPalette(uint16_t palNumber) { uint16_t x, y; float fleX, fleK; switch(palNumber) { case 1: // Compute ironbow palette for(x = 0; x < 256; ++x) { fleX = (float)x / 255.0; // fleK = 65535.9 * (1.02 - (fleX - 0.72) * (fleX - 0.72) * 1.96); // fleK = (fleK > 65535.0) || (fleX > 0.75) ? 65535.0 : fleK; // Truncate red curve fleK = 63487.0 * (1.02 - (fleX - 0.72) * (fleX - 0.72) * 1.96); fleK = (fleK > 63487.0) || (fleX > 0.75) ? 63487.0 : fleK; // Truncate red curve colorPal[x] = (uint16_t)fleK & 0xF800; // Top 5 bits define red // fleK = fleX * fleX * 2047.9; fleK = fleX * fleX * 2015.0; colorPal[x] += (uint16_t)fleK & 0x07E0; // Middle 6 bits define green // fleK = 31.9 * (14.0 * (fleX * fleX * fleX) - 20.0 * (fleX * fleX) + 7.0 * fleX); fleK = 30.9 * (14.0 * (fleX * fleX * fleX) - 20.0 * (fleX * fleX) + 7.0 * fleX); fleK = fleK < 0.0 ? 0.0 : fleK; // Truncate blue curve colorPal[x] += (uint16_t)fleK & 0x001F; // Bottom 5 bits define blue } break; case 2: // Compute quadratic "firebow" palette for(x = 0; x < 256; ++x) { fleX = (float)x / 255.0; // fleK = 65535.9 * (1.00 - (fleX - 1.0) * (fleX - 1.0)); fleK = 63487.0 * (1.00 - (fleX - 1.0) * (fleX - 1.0)); colorPal[x] = (uint16_t)fleK & 0xF800; // Top 5 bits define red // fleK = fleX < 0.25 ? 0.0 : (fleX - 0.25) * 1.3333 * 2047.9; fleK = fleX < 0.25 ? 0.0 : (fleX - 0.25) * 1.3333 * 2015.0; colorPal[x] += (uint16_t)fleK & 0x07E0; // Middle 6 bits define green // fleK = fleX < 0.5 ? 0.0 : (fleX - 0.5) * (fleX - 0.5) * 127.9; fleK = fleX < 0.5 ? 0.0 : (fleX - 0.5) * (fleX - 0.5) * 123.0; colorPal[x] += (uint16_t)fleK & 0x001F; // Bottom 5 bits define blue } break; case 3: // Compute "alarm" palette for(x = 0; x < 256; ++x) { fleX = (float)x / 255.0; fleK = 65535.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 1.0); colorPal[x] = (uint16_t)fleK & 0xF800; // Top 5 bits define red fleK = 2047.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : (fleX - 0.875) * 8.0); colorPal[x] += (uint16_t)fleK & 0x07E0; // Middle 6 bits define green fleK = 31.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 0.0); colorPal[x] += (uint16_t)fleK & 0x001F; // Bottom 5 bits define blue } break; case 4: // Compute negative gray palette, black hot for(x = 0; x < 256; ++x) colorPal[255 - x] = (((uint16_t)x << 8) & 0xF800) + (((uint16_t)x << 3) & 0x07E0) + (((uint16_t)x >> 3) & 0x001F); break; default: // Compute gray palette, white hot for(x = 0; x < 256; ++x) colorPal[x] = (((uint16_t)x << 8) & 0xF800) + (((uint16_t)x << 3) & 0x07E0) + (((uint16_t)x >> 3) & 0x001F); break; } } void setColorRange(int presetIndex) { // Set coldest/hottest values in color range switch(presetIndex) { case 1: // Standard range, from FLIR document: 50F to 90F colorLow = 10.0; colorHigh = 32.22; break; case 2: // Cool/warm range, for detecting mammals outdoors colorLow = 5.0; colorHigh = 32.0; break; case 3: // Warm/warmer range, for detecting mammals indoors colorLow = 20.0; colorHigh = 32.0; break; case 4: // Hot spots, is anything hotter than it ought to be? colorLow = 20.0; colorHigh = 50.0; break; case 5: // Fire & ice, extreme temperatures only! colorLow = -10.0; colorHigh = 200.0; break; default: // Default is autorange, so these values will change with every frame colorLow = 0.0; colorHigh = 100.0; break; } } // Draw the stationary screen elements behind the live camera window void setBackdrop(uint16_t bgColor, uint16_t buttonFunc) { arcada.display->fillScreen(bgColor); for(int x = 0; x < 160; ++x) // Paint current palette across bottom arcada.display->drawFastVLine(x, 110, 6, colorPal[map(x, 0, 159, 0, 255)]); arcada.display->setCursor(16, 120); arcada.display->setTextColor(0xFFFF, bgColor); // White text, current BG for button labels switch(buttonFunc) { case 0: arcada.display->print("B:MENU A:FREEZE"); break; case 1: arcada.display->print("B:MENU "); if(nextBMPindex == 0) // No room to store a BMP in flash media? arcada.display->setTextColor(GRAY_33 >> 1); // Grayed button label arcada.display->print("A:CAPTURE"); break; case 2: arcada.display->print("B:MENU "); if(nextDirIndex == 0) // Has flash storage no room for a new directory? arcada.display->setTextColor(GRAY_33 >> 1); // Grayed button label arcada.display->print("A:START RECORD"); break; case 3: arcada.display->print("B:MENU "); arcada.display->setTextColor(0xFFFF, 0xF800); // White text, red BG recording indicator arcada.display->print("A:STOP RECORD"); break; case 4: arcada.display->print(" A:EXIT"); // Use for bitmap redisplay only break; } } void prepForSave() { for(int x = 0; x < 768; ++x) pixelArray[3 * x + 2] = pixelArray[3 * x + 1] = pixelArray[3 * x]; // Copy each blue byte into R & G for 256 grays in 24 bits if(!writeBMP()) { // Did BMP write to flash fail? arcada.display->fillRect(0, 96, 160, 12, 0xF800); // Red error signal arcada.display->setTextColor(0xFFFF); // with white text arcada.display->setCursor(20, 99); arcada.display->print("Storage error!"); } } boolean newDirectory() { // Create a subdirectory, converting the name between char arrays and string objects char fileArray[64]; String fullPath; sprintf(fileArray, DIR_FORMAT, nextDirIndex); // Generate subdirectory name fullPath = BOTTOM_DIR + String(fileArray); // Make a filepath out of it, then return arcada.mkdir(fullPath.c_str()); // try to make a real subdirectory from it } // Here we write the actual bytes of a BMP file (plus extras) to flash media boolean writeBMP() { uint16_t counter1, shiftedFloats[14]; // A buffer for the appended floats and uint16_t's uint32_t timeStamp; float shiftAssist; char fileArray[64]; String fullPath; // First, figure out a name and path for our new BMP fullPath = BOTTOM_DIR; // Build a filepath starting with the base subdirectory if(buttonRfunc == 2) { // BMP sequence recording in progress? sprintf(fileArray, DIR_FORMAT, nextDirIndex); // Generate subdirectory name fullPath += String(fileArray); // Add it to the path sprintf(fileArray, BMP_FORMAT, nextBMPsequence); // Generate a sequential filename fullPath += String(fileArray); // Complete the filepath string } else { // Not a sequence, solitary BMP file sprintf(fileArray, BMP_FORMAT, nextBMPindex); // Generate a serial filename fullPath += String(fileArray); // Complete the filepath string } myFile = arcada.open(fullPath.c_str(), FILE_WRITE); // Only one file can be open at a time if(myFile) { // If the file opened okay, write to it: myFile.write(BmpPSPHead, 14); // BMP header 1 myFile.write(DIBHeadPSP1, 40); // BMP header 2 myFile.write(pixelArray, 2304); // Array of 768 BGR byte triples myFile.write(PSPpad, 2); // Pad with 2 zeros 'cause Photoshop does it. // My BMP hack - append 5 fixed-point temperature values as 40 extra bytes for(counter1 = 0; counter1 < 5; ++counter1) { // Shift 5 floats shiftAssist = sneakFloats[counter1] + 1000.0; // Offset MLX90640 temps to positive shiftedFloats[counter1 * 2] = (uint16_t)shiftAssist; shiftAssist = (shiftAssist - (float)shiftedFloats[counter1 * 2]) * 49152.0; // Scale up fraction shiftedFloats[counter1 * 2 + 1] = (uint16_t)shiftAssist; } shiftedFloats[10] = lowAddr; // Two more appended numbers, the 2 extreme pixel addresses shiftedFloats[11] = highAddr; timeStamp = millis(); // Recycle this variable to append a time stamp lowAddr = timeStamp & 0xFFFF; highAddr = timeStamp >> 16; shiftedFloats[12] = lowAddr; shiftedFloats[13] = highAddr; myFile.write(shiftedFloats, 28); // Write appended uint16_t's myFile.close(); return true; } else { // The file didn't open, return error return false; } } void recallLastBMP(uint16_t bgColor) { // Display 8-bit values left in buffer from the last BMP save int counter1, counter2; boolean exitFlag = false; setBackdrop(bgColor, 4); // Clear screen, just a color palette & "A:EXIT" in the BG for(int counter1 = 0; counter1 < 24; ++counter1) { // Redraw using leftover red byte values, not yet overwritten for(int counter2 = 0 ; counter2 < 32 ; ++counter2) { arcada.display->fillRect(16 + counter2 * 4, 92 - counter1 * 4, 4, 4, colorPal[(uint16_t)pixelArray[3 * (32 * counter1 + counter2) + 2]]); } } while(!exitFlag) { // Loop here until exit button if(!buttonActive && (buttonBits & ARCADA_BUTTONMASK_A)) { // "A:EXIT" button freshly pressed? exitFlag = true; buttonActive = true; deBounce = millis() + DE_BOUNCE; } if(buttonActive && millis() > deBounce && (buttonBits & (ARCADA_BUTTONMASK_A | ARCADA_BUTTONMASK_B)) == 0) // Has de-bounce wait expired & all buttons released? buttonActive = false; // Clear flag to allow another button press } } uint16_t availableFileNumber(uint16_t startNumber, String formatBase) { // Find unclaimed serial number for file series uint16_t counter1; char nameArray[80]; for(counter1 = startNumber; counter1 % MAX_SERIAL != 0; ++counter1) { // Start counting sprintf(nameArray, formatBase.c_str(), counter1); // Generate a serialized filename if(!arcada.exists(nameArray)) // If it doesn't already exist return counter1; // return the number as available } return 0; // Loop finished, no free number found, return fail } boolean menuLoop(uint16_t bgColor) { // Lay out a menu screen, interact to change values int counter1 = 0, scrollPosition = 0; boolean exitFlag = false, settingsChanged = false; uint32_t menuButtons; arcada.display->fillScreen(bgColor); arcada.display->fillRect(0, 12 * (counter1 + scrollPosition) + MENU_VPOS - 2, 160, 12, 0x0000); // Black stripe cursor on menu arcada.display->setTextColor(0xFFFF); // White text arcada.display->setCursor(16, 120); // at screen bottom arcada.display->print("B:ADVANCE A:CHANGE"); // for button labels for(counter1 = 0; counter1 < MENU_ROWS; ++counter1) { // Display menu texts menuLines(counter1, scrollPosition); } counter1 = 0; while(!exitFlag) { // Loop until exit is activated if(!buttonActive && (buttonBits & ARCADA_BUTTONMASK_B)) { // Fresh press of B:ADVANCE button? buttonActive = true; // Set button flag deBounce = millis() + DE_BOUNCE; // and start debounce timer. arcada.display->fillRect(0, 12 * (counter1 - scrollPosition) + MENU_VPOS - 2, 160, 12, bgColor); // Erase cursor & text menuLines(counter1, scrollPosition); // Refresh menu text line counter1 = (counter1 + 1) % MENU_LEN; // Advance menu counter if(counter1 == 0) { // Have we cycled around to the menu top? scrollPosition = 0; for(int counter2 = 0; counter2 < MENU_ROWS; ++counter2) { // Redisplay all menu texts arcada.display->fillRect(0, 12 * counter2 + MENU_VPOS - 2, 160, 12, bgColor); // Erase old text menuLines(counter2 + scrollPosition, scrollPosition); // Redraw each text line } } else if((counter1 + 1 < MENU_LEN) && (counter1 - scrollPosition == MENU_ROWS - 1)) { // Should we scroll down 1 menu line? ++scrollPosition; for(int counter2 = 0; counter2 < MENU_ROWS; ++counter2) { // Redisplay all menu texts arcada.display->fillRect(0, 12 * counter2 + MENU_VPOS - 2, 160, 12, bgColor); // Erase old text menuLines(counter2 + scrollPosition, scrollPosition); // Redraw each text line } } arcada.display->fillRect(0, 12 * (counter1 - scrollPosition) + MENU_VPOS - 2, 160, 12, 0x0000); // New black cursor menuLines(counter1, scrollPosition); // Refresh text line deBounce = millis() + DE_BOUNCE; // Restart debounce timer, just for safety } if(!buttonActive && (buttonBits & ARCADA_BUTTONMASK_A)) { // Fresh press of A:CHANGE button? buttonActive = true; // Set button flag deBounce = millis() + DE_BOUNCE; // and start debounce timer. switch(counter1) { // Change whichever setting is currently hilighted case 0: showLastCap = true; // Set flag to display the last frame captured to SD exitFlag = true; // and exit break; case 1: celsiusFlag = !celsiusFlag; // Toggle Celsius/Fahrenheit break; case 2: buttonRfunc = (buttonRfunc + 1) % 3; // Step through button functions break; case 3: loadPalette(paletteNum = (paletteNum + 1) % 5); // Step through various color palettes break; case 4: thermRange = (thermRange + 1) % 6; // Step through various temp range presets break; case 5: markersOn = !markersOn; // Toggle hot/cold marker visibility break; case 6: mirrorFlag = !mirrorFlag; // Toggle mirrored display break; case 7: switch(frameRate = (frameRate + 1) % 6) { // 6 frame rates, 0.5 to 16 in powers of 2 case 0: mlx.setRefreshRate(MLX90640_0_5_HZ); break; case 1: mlx.setRefreshRate(MLX90640_1_HZ); break; case 2: mlx.setRefreshRate(MLX90640_2_HZ); break; case 3: mlx.setRefreshRate(MLX90640_4_HZ); break; case 4: mlx.setRefreshRate(MLX90640_8_HZ); break; default: mlx.setRefreshRate(MLX90640_16_HZ); break; } break; case 8: emissivity = (emissivity + 90) % 100; // Step from 95% to 5% by -10% break; case 9: smoothing = !smoothing; // Toggle pixel smoothing break; case 10: arcada.setBacklight((screenDim = !screenDim) ? 64 : 255); // Change backlight LED break; default: exitFlag = true; break; } if((counter1 > 0) && (counter1 < MENU_LEN - 1)) // Was any setting just changed? settingsChanged = true; arcada.display->fillRect(0, 12 * (counter1 - scrollPosition) + MENU_VPOS - 2, 160, 12, 0x0000); // Erase hilit menu line menuLines(counter1, scrollPosition); // Retype hilit menu line } if(buttonActive && millis() > deBounce && (buttonBits & (ARCADA_BUTTONMASK_A | ARCADA_BUTTONMASK_B)) == 0) // Has de-bounce wait expired & all buttons released? buttonActive = false; // Clear flag to allow another button press } return(settingsChanged); } void menuLines(int lineNumber, int scrollPos) { // Screen print a single line in the settings menu arcada.display->setTextColor(0xFFFF); // White text arcada.display->setCursor(10, 12 * (lineNumber - scrollPos) + MENU_VPOS); // Menu lines 12 pixels apart if(lineNumber - scrollPos == 0 && scrollPos > 0) { // Are any menu lines scrolled off screen top? arcada.display->print(" ^"); // Print a small up arrow indicator } else if(lineNumber - scrollPos == 8 && lineNumber + 1 < MENU_LEN) { // How about off the bottom? arcada.display->print(" v"); // Print a small down arrow indicator... yeah, it's a v } else { switch(lineNumber) { case 0: arcada.display->print(" Display last capture"); break; case 1: arcada.display->print(" Scale - "); arcada.display->print(celsiusFlag ? "CELSIUS" : "FAHRENHEIT"); break; case 2: arcada.display->print(" Rt button - "); switch(buttonRfunc) { case 1: arcada.display->print("CAPTURE"); break; case 2: arcada.display->print("RECORD"); break; default: arcada.display->print("FREEZE"); break; } break; case 3: arcada.display->print(" Palette - "); for(int xPos = 0; xPos < 72; ++xPos) // Display the current heat spectrum colors arcada.display->drawFastVLine(xPos + 87, (lineNumber - scrollPos) * 12 + MENU_VPOS, 8, colorPal[map(xPos, 0, 71, 0, 255)]); switch(paletteNum) { case 1: arcada.display->print("IRONBOW"); break; case 2: arcada.display->print("FIREBOW"); break; case 3: arcada.display->setTextColor(0x0000); // Black text for reverse contrast arcada.display->print("ALARM"); break; case 4: arcada.display->setTextColor(0x0000); // Black text arcada.display->print("BLACK HOT"); break; default: arcada.display->print("WHITE HOT"); break; } break; case 4: arcada.display->print("Temp range - "); setColorRange(thermRange); switch(thermRange) { case 1: arcada.display->print("STANDARD"); break; case 2: arcada.display->print("COOL/WARM"); break; case 3: arcada.display->print("WARM/WARMER"); break; case 4: arcada.display->print("HOT SPOTS"); break; case 5: arcada.display->print("FIRE & ICE"); break; default: arcada.display->print("AUTO-RANGE"); break; } break; case 5: arcada.display->print(" Markers - "); arcada.display->print(markersOn ? "ON" : "OFF"); break; case 6: arcada.display->print(" Image - "); arcada.display->print(mirrorFlag ? "MIRRORED" : "FORWARD"); break; case 7: arcada.display->print("Frame rate - "); arcada.display->print((float)(1 << frameRate) * 0.5); arcada.display->print(" FPS"); break; case 8: arcada.display->setTextColor(GRAY_33 << 1); // Grayed menu item arcada.display->print("Emissivity - "); arcada.display->print(emissivity); arcada.display->print("%"); break; case 9: arcada.display->setTextColor(GRAY_33 << 1); // Grayed menu item arcada.display->print(" Smoothing - "); arcada.display->print(smoothing ? "ON" : "OFF"); break; case 10: arcada.display->print(" Backlight - "); arcada.display->print(screenDim ? "DIM" : "FULL"); break; case 11: arcada.display->print(" Exit menu"); } } } // This is the function that substitutes for GPIO external interrupts // It will check for A and B button presses at 50Hz void buttonCatcher(void) { buttonBits = arcada.readButtons(); clickFlagMenu |= (buttonBits & ARCADA_BUTTONMASK_B) != 0; clickFlagSelect |= (buttonBits & ARCADA_BUTTONMASK_A) != 0; }
Did you make it to the finish? High five!
Next, we'll look at the sketch in operation, and peek under the hood to see what the code does.