Close

Modular Display Function(s)

A project log for ESPresso Scale

Fully open source scale with ADS1232 24-bit ADC, ESP32 MCU and some coffee related software features.

JohnJohn 03/22/2019 at 23:560 Comments

Big code update coming soon plus the new v3 PRO PCB with many changes.

My main issue with all my projects is the display. I develop using 128*64 SSD1306 but then I decide to go with 96*64 full color OLED. Managing the code with all those display.setTextSize,display.update and designing the display layout is also a pain.

A Long time ago I wrote a wrapper function that manages the draw/refresh/fill of any display and supports left/middle/right justify (hor/vert) for each segment. After a nice refresh I implemented into this project.

First, I decide and design the sections (you can draw a simple mockup on excel) and the libraries/fonts, I can easily swap displays anytime and only need to change 2-3 parameters (width/height/cols for each font size and maybe some offsets).

I think this is a very elegant way to handle any project with a dynamic and static segments in one display.

More info on the actual .ino on gitlab (very soon)

void drawSingleRowSection(String leftColText, byte leftColJustify, uint32_t leftColColor, uint32_t leftColBgColor, String rightColText, byte rightColJustify, uint32_t rightColColor, uint32_t rightColBgColor, byte vJustify, byte textMaxSize, byte &lastSectionTextSize, uint8_t sectionYOffset, byte sectionHeight, bool fullFill, bool sectionFill, bool doUpdate) {
  byte leftColTextLength = leftColText.length();
  byte rightColTextLength = rightColText.length();
  
  byte colDivider = 2;
  
  byte textSize = 4; //
  byte maxCols=DISPLAY_COLS_TS1/4; //4 is the max supported size, and DISPLAY_COLS_TS1/4 are its max columns
  byte xOffset = DISPLAY_XOFFSET;
  uint8_t yOffset = sectionYOffset;
  uint8_t yOffsetAdjust = 0;
  
  byte leftPrefixLength = 0;
  byte rightPrefixLength = 0;


  if (rightColTextLength == 0) {
    //full width top section
    colDivider = 1;
  }
  

  if ((leftColTextLength <= DISPLAY_COLS_TS1/(colDivider*4)) && (rightColTextLength <= DISPLAY_COLS_TS1/(colDivider*4)) && textMaxSize > 3) {
    textSize=4;
    xOffset+=DISPLAY_XOFFSET_TS4;
  } else if ((leftColTextLength <= DISPLAY_COLS_TS1/(colDivider*3)) && (rightColTextLength <= DISPLAY_COLS_TS1/(colDivider*3)) && textMaxSize > 2) {
    textSize=3;
    xOffset+=DISPLAY_XOFFSET_TS3;    
  } else if ((leftColTextLength <= DISPLAY_COLS_TS1/(colDivider*2)) && (rightColTextLength <= DISPLAY_COLS_TS1/(colDivider*2)) && textMaxSize > 1) {
    textSize=2;
    xOffset+=DISPLAY_XOFFSET_TS2;
  } else {
    textSize=1;
    xOffset+=DISPLAY_XOFFSET_TS1;
  }

  if (lastSectionTextSize != textSize) { 
    sectionFill=true; 
  }
  lastSectionTextSize = textSize;
  maxCols=DISPLAY_COLS_TS1/textSize;
  yOffsetAdjust=(sectionHeight-DISPLAY_FONT_HEIGHT_TS1*textSize)/2;
  yOffset += yOffsetAdjust*vJustify;

  
  
  if (leftColJustify == 1) {
    //center
    leftPrefixLength = (maxCols/colDivider - leftColTextLength)/2;
  } else if (leftColJustify == 2) {
    //right
    leftPrefixLength = maxCols/colDivider - leftColTextLength;
  }

  
  if ((rightColTextLength > 0) && (maxCols/colDivider > rightColTextLength)) {
    if (rightColJustify == 1) {
      //center
      rightPrefixLength = (maxCols/colDivider - rightColTextLength)/2;
    } else if (rightColJustify == 2) {
      //right
      rightPrefixLength = (maxCols/colDivider - rightColTextLength);
    }  
  }

  if (fullFill) { 
    clearDisplay();
  }

  if (sectionFill) {
      display.fillRect(0,sectionYOffset,DISPLAY_WIDTH/2,sectionHeight,convertRG888toRGB565(leftColBgColor));
      display.fillRect(DISPLAY_WIDTH/2,sectionYOffset,DISPLAY_WIDTH,sectionHeight,convertRG888toRGB565(rightColBgColor));
  }
  
  display.setTextSize(textSize);   
  display.setTextWrap(false);
  
  //left
  display.setCursor(xOffset, yOffset);
  display.setTextColor(convertRG888toRGB565(leftColColor),convertRG888toRGB565(leftColBgColor));
  for (int i = 0;i<leftPrefixLength;i++) {
    display.print(" ");
  }
  display.print(leftColText.substring(0,maxCols/colDivider));  

  if (rightColTextLength > 0) {
    display.setCursor(DISPLAY_WIDTH/2 + xOffset, yOffset);  
    display.setTextColor(convertRG888toRGB565(rightColColor),convertRG888toRGB565(rightColBgColor));
    for (int i = 0;i<rightPrefixLength;i++) {
      display.print(" ");
    }
    display.print(rightColText.substring(0,maxCols/colDivider));
  }

  #if defined(SSD1306) || defined(SSD1331)
    if (doUpdate) { display.display(); }
  #endif
  
}

And apart from the above function, I must write an even higher level wrapper function that minimizes the fillrect/clearDisplay calls (very expensive calls) and updates only the sections that change.

This function is different for each project but is very easy to change.

The idea is to call drawFullDisplay as fast as I want (limited by my desired max framerate) and it will handle the update calls to the display. If all data are the same as the last call, it won't even bother to do anything.

void drawFullDisplay(String grams, String voltage, String resolution, String rateOfChange, bool bleConnected, String lastTare, bool sectionFill) {
  //we need to hold previous values so we can only update sections with changes. Display update is expensive, no need to do it for static sections
  static String _grams = "";
  static String _voltage = "";
  static String _resolution = "";
  static String _rateOfChange = "";
  static bool _bleConnected = false;
  static String _lastTare = "";
  
  uint8_t yOffset = 0;


  if (sectionFill) {
    drawSingleRowSection("",0,colorTop,colorTopBackground,"",0,colorTop,colorTopBackground,1,DISPLAY_TOP_MAX_TEXT_SIZE,displayTopSectionLastTextSize,yOffset,DISPLAY_TOP_SECTION_HEIGHT,false,true,false);
    yOffset += DISPLAY_TOP_SECTION_HEIGHT;
    drawSingleRowSection("",0,colorMain,colorMainBackground,"",0,colorMain,colorMainBackground,1,DISPLAY_MAIN_MAX_TEXT_SIZE,displayTopSectionLastTextSize,yOffset,DISPLAY_MAIN_SECTION_HEIGHT,false,true,false);
    yOffset += DISPLAY_MAIN_SECTION_HEIGHT;
    drawSingleRowSection("",0,colorBottom,colorBottomBackground,"",0,colorBottom,colorBottomBackground,1,DISPLAY_BOTTOM_MAX_TEXT_SIZE,displayTopSectionLastTextSize,yOffset,DISPLAY_BOTTOM_SECTION_HEIGHT,false,true,true);    
    sectionFill = false; 
  }
  
  if (resolution != _resolution || voltage != _voltage || bleConnected != _bleConnected) {
    //update top section
    yOffset=0;
    if (bleConnected != _bleConnected) {
      //refill background before (not)drawing icon
      sectionFill = true;
    }
    drawSingleRowSection(" " + resolution + " " + voltage,0,colorTop,colorTopBackground,"",2,colorTop,colorTopBackground,1,DISPLAY_TOP_MAX_TEXT_SIZE,displayTopSectionLastTextSize,yOffset,DISPLAY_TOP_SECTION_HEIGHT,false,sectionFill,true);
    if (bleConnected) {
      display.drawBitmap(DISPLAY_WIDTH-16, yOffset+2, bluetooth_icon16x12, 16, 12, convertRG888toRGB565(colorTop));
    }
    _resolution = resolution;
    _voltage = voltage;
    _bleConnected = bleConnected;
  }

  if (grams != _grams) {
    //update main section
    yOffset=DISPLAY_TOP_SECTION_HEIGHT;
    drawSingleRowSection(grams,2,colorMain,colorMainBackground,"",2,colorMain,colorMainBackground,1,DISPLAY_MAIN_MAX_TEXT_SIZE,displayMainSectionLastTextSize,yOffset,DISPLAY_MAIN_SECTION_HEIGHT,false,false,true);
    _grams = grams;    
  }
  
  if (rateOfChange != _rateOfChange || lastTare != _lastTare) {
    //update bottom section
    yOffset=DISPLAY_TOP_SECTION_HEIGHT+DISPLAY_MAIN_SECTION_HEIGHT;
    drawSingleRowSection(" " + lastTare,0,colorBottom,colorBottomBackground,rateOfChange,2,colorBottom,colorBottomBackground,1,DISPLAY_BOTTOM_MAX_TEXT_SIZE,displayBottomSectionLastTextSize,yOffset,DISPLAY_BOTTOM_SECTION_HEIGHT,false,false,true);
    _rateOfChange = rateOfChange;
    _lastTare = lastTare;
  }
  
}

Discussions