Next come the additional functions for actions that happen often, or that might be useful in other sketches.
This one is loadPalette(), and it fills a 16-bit array with 256 color values computed using various formulas. It's the most math-y part of the whole sketch, but it's not nasty math. The arrangement of the 16 bits in a color value is described more fully in the Adafruit_GFX tutorial (which describes many of the Arcada GFX functions as well).
The sharp-eyed reader might notice that some of the lines appear in pairs, one commented out, the other one not. Why? It's due to an odd characteristic of the TFT screens on my PyBadges. It turns out that the colors appear in gradual, even steps as their value increases from black to full color...except for the last step. The brightest color has more contrast than other values, and makes harsh contours appear where there should be smooth gradients on my screens, so the math stops short of using the maximum values of RGB, and the smoothness is preserved. If you wish to use the full range of color on your screen, just reverse which line is commented out for each pair. Your hardware may vary.
// 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;
}
}
Here are two more functions. setColorRange() just receives an integer and changes two floating point values. They define the low and high temperatures for several preset color ranges.
setBackdrop() clears the screen and draws the parts of the display that don't change from frame to frame, like the color palette and the button labels.
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;
}
}
The next two functions should have been swapped in order, making the program's flow a little clearer. Sorry about that.
Anyway, the second one, newDirectory(), is used before recording a sequence. Each new BMP sequence gets a freshly minted flash directory all its own, and its name is built using a serial number. This function takes a serial number and converts it back and forth between an array of characters and a string object to construct the directory name. With that, it tries to create a subdirectory, and returns a true value if successful.
prepForSave() simply expands the 8-bit values in the output array to 24 grayscale bits and calls another function, writeBMP(), to complete the process of... well, writing a BMP file. If the write fails, an alert message pops up on the screen.
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
}
The Augmented BMP Files
This function is what makes captured images fun. It starts by building a string containing a path for saving the image under an unused serial file name. Then the bytes of a normal BMP get written next, two headers, the array of pixel values, and two pad bytes. But keep reading. The extra data comes next.
// 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.
Next, five extra floating point values are stored in a peculiar way. All temperatures are temporarily made positive by adding 1000. The integer portion becomes a 16-bit integer, and the fractional portion gets multiplied by 49152 to produce another 16-bit integer, and these are copied byte by byte into another small array... 20 bytes to hold 5 floating point temperatures. It's not elegant, but it allows other sketches to access these numbers and reconstruct nearly every pixel temperature in the image.
Three more useful numbers get broken into bytes and added, the positions of the coldest and hottest pixels, and the elapsed time in milliseconds.
// 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;
}
}
recallLastBMP() is a bit of a cheap hack, but useful. The byte array that's used when saving BMPs gets overwritten with every new frame displayed. That is, parts of it get overwritten and parts of it don't. Some bytes still persist unchanged from the last image capture, and this function can clear the screen and redisplay those bytes as pixels again. Not as good as recalling an image from flash storage, but handy in its own way.
It requires a button click to exit, so the debounce instructions from the main loop appear at the end here as well.
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
}
}
The function availableFileNumber() is small, but I use it a lot when I need a name for saving a new file. It takes a number and a character string and combines them into a numbered filename, then checks whether a file by that name already exists in storage. If so, it tries the next number, and keeps counting until an unoccupied filename is found, or the value MAX_SERIAL is exceeded. The number of the first free filename is returned, or a zero if none is found.
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
}
Page last edited March 08, 2024
Text editor powered by tinymce.