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.