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 }
Text editor powered by tinymce.