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.

This guide was first published on Mar 27, 2020. It was last updated on Mar 27, 2020.

This page (The Main Loop) was last updated on Mar 26, 2021.

Text editor powered by tinymce.