This is where the display action happens.
For every cycle, after getting some variables ready, what does the loop do first? It checks the battery level! The Arcada library allows the processor to calculate the battery's voltage, and the sketch converts that into an unscientific onscreen indicator. It's still pretty good at showing when power's getting low.
After that is the main event. An array of 768 floating point numbers gets filled with temperatures from the MLX90640 sensor. Everything else that happens depends on this.
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
Now all the numbers in the array get inspected. Before they are turned into pixels, the coldest and the hottest values must be located. The center temperature gets saved, too. These are some of the numbers that get appended to the pixels in a BMP file, whenever a capture is performed.
// 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;
Then the array values are inspected again. This time each one is converted to a color, a pixel-shaped rectangle is painted on the TFT screen, and a thermal picture finally appears. At the same time, the scaled 8-bit values get stored in another array, one that can contain pixels in a form suitable for writing to a BMP file, if needed.
// 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]); } } }
After this, all the other onscreen stuff that shows up around the image gets added. That means printing the three important temperatures, the frame counter, and the three small crosses on the important pixels. All of these are shown in carefully selected contrasting colors. Pretty fancy, eh?
// 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
The Interactive Stuff
What happens when a button gets pressed? The response starts waayyy at the end of the sketch, where the buttonCatcher()
function is being called 50 times a second. If it detects either button A or B clicking, a corresponding Boolean is set. When the loop reaches this point, the sketch deals with those Boolean flags. It will store timer values for suppressing button bounces, set more Booleans to block double-presses, and even operate the camera's features.
First, the B button's Boolean is tested, and the onscreen settings menu is called up if clicked.
Then the A button is checked. It will either send the sketch into a small loop that freezes everything until all buttons are released, or it will set even more Booleans, ones that will allow other functions to write BMP images to flash.
If neither button gets clicked, none of these events happen and the loop continues showing thermal pixels.
// 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 } } }
Okay. Now that the buttons have been checked, one of them might have been clicked for an action to happen, so here's where the responses start.
The first chunk happens when a capture is triggered, either singly or in a series. Whenever prepForSave()
is called, a BMP image complete with added temperature values is written to flash storage, if available.
If Display last capture was selected from the menu, then the recallLastBMP()
function gets called, and whatever 8-bit pixel data is still lingering in the output array gets shown in color on the screen once again.
Finally, all the timer values and Booleans used for handling buttons are checked and updated, and if all conditions are met, the values are cleared to permit new button clicks. (We'll see this code block again in some of the support functions.)
// 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 }
The main loop repeats from here, but that's not even half the sketch. The heavy lifting happens in the supporting functions that follow.
Page last edited March 08, 2024
Text editor powered by tinymce.