OLED display "flair" (degrees, decimals)

@ken830 I really like how you displayed:

  1. CO2 subscript
  2. temperature degree symbol
  3. single decimal point

in your display. Would you share your updateOLED code here for all to steal?

I went down the path of u8g2 documentation and dtostrf and round functions, but could not figure either of these niceties out.

Thanks! Haha! Boyā€¦ Skip to the end if you donā€™t want to read this book.

The only reason Iā€™m even here is because I bought a pair of those air quality monitors from China many years ago that was basically a PMS5003 with a small board/LCD mounted in a clear acrylic sandwich. I looked up the sensor and thought I could someday interface to it directly and get that data on the network and logged to my home automation system. I never got around to it. I never had the time. Recently, with COVID and always being indoors, I started to get concerned about CO2 levels and so I started to think more seriously about it. I never looked into Arduino before and thought I was going to use a Pi Pico W or something. Well, I had a friend who just happened to mention to me in an e-mail that he got an Air Gradient and upgrade kit. I looked it up.: open source, off-the-shelf sensors, etcā€¦ it was exactly what I was looking to implement! Plus, I really liked having the injection molded case. I have a 3D printer, but I would spend a million years perfecting the casing design and STILL it would look 3D printed. And the real benefit, I thought, was that everything would just work and I wouldnā€™t have to spend any time building or debugging it. hahaā€¦ boy has that not gone as plannedā€¦ but Iā€™m having fun debugging, so itā€™s all good!

When I first got the kit up and running, the display bothered me because no one needed the temperature down to 1/100įµ—Ź° of a degreeā€¦ Thatā€™s needless precision without supporting accuracy. And CO2 looked weird. And I wanted to add in the NOx readings and needed to make some space to fit everything. So, I figured I would spend 5 minutes tops figuring out where the print statements were and just fix them the way I liked. Boy was I wrong.

For the single decimal place:

At first I thought of trying something like a C print statement ( %.1f), or a truncation (I didnā€™t care about lack of rounding), but it took a bit of time to finally find the Arduino documentation on String() located here: https://www.arduino.cc/reference/en/language/variables/data-types/stringobject/. I didnā€™t really understand why it was under Variables > Data types, but :man_shrugging:.

In the Syntax section, I noticed String(val, decimalPlaces) and the parameter definition is given as:

decimalPlaces: only if val is float or double. The desired decimal places.

So, just put in the number 1 there and it worked!

Degree symbol (Ā°):

Unicode: U+00B0
UTF-8: C2 B0

This, I thought was easy, just use the Ā° character, which you can literally paste in, but (my memory of this is a bit fuzzy) I think it didnā€™t work. I then learned about u8g2 and dug into the documentation. I tried a bunch of stuff like u8g2.enableUTF8Print(); chose a different font, and use the escape hex code of the UTF-8 code "\xC2B0" to finally get it to work! But I will change it in a bit when I get to the subscript 2 below.

Subscript 2 (ā‚‚):

Unicode: U+2082
UTF-8: E2 82 82

Now, I feel like I was home freeā€¦ the subscript 2 in COā‚‚ was going to be the same way. If pasting the unicode character in the string didnā€™t work, I would do the same escape hex code trick with the UTF-8 codeā€¦ but subscript 2 sits further down the unicode map and is a 3-byte UTF-8 character and so nothing I tried worked. I then started digging into the included fonts and looking at all of the glyph sets of all of the fonts only to discover that none of the fonts cover that remote area of the character mapā€¦ The only character nearby that area is the Euro currency symbol sitting alone in a sea of blankness at U+20AC (UTF-8: E2 82 AC). So close!!

Well, I was left with no choice but to create my own font. I then looked into all of the font creation and conversion tools. Luckily, in my research, I found someone just two weeks earlier had created a web-based tool that can create custom fonts! No installation needed. It was meant for CJK characters that are missing, but it just used the GNU Unifont library to put into your own set of glyphs, so it would work. I put in my two characters (degree symbol and subscript 2) and clicked the ā€œGenerate nowā€ button and I was presented with the font. I then saved that into a .h and did a #include at the top of my sketch, set u8g2 to the custom font, and walla! my print statements with the unicode strings just magically work!.

All of this is still a work-in-progress because before I finished, I started working on the IĀ²C stability issue and then the exception 0 issue. Part of the reason is that my hack-and-slash code to add in the TVOC/NOx sensor and mess with the display somehow made the reboots happen as frequently as every few minutes, whereas a full-stock FW seems to only reboot after many minutes or several hours.

TL;DR & Final Results:

Hereā€™s the custom font generated by the web-tool:

/*
  Fontname: -gnu-Unifont-Medium-R-Normal-Sans-16-160-75-75-c-80-iso10646-1
  Copyright: Copyright (C) 1998-2019 Roman Czyborra, Paul Hardy,  Qianqian Fang, Andrew Miller, Johnnie Weaver, David Corbett, et al.  License GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl.html>  with the GNU Font Embedding Exception.
  Glyphs: 99/57086
  BBX Build Mode: 0
*/
const uint8_t u8g2_font_unifont_myfonts[1331] U8G2_FONT_SECTION("u8g2_font_unifont_myfonts") = 
  "c\0\3\2\5\5\4\5\6\20\20\0\376\12\376\13\377\1\233\3\63\5\11 \6\0\240G\1!\10A"
  "\61DqH\4\42\10\205(F\221\271\5#\17F%D\325\323\60$Q\313\60D=\1$\22G%"
  "D\27\16J\24I\341<FRe\20\63\0%\24G%D\243II)\211\224\64N\23)\211\222\222"
  "\246\0&\22G%D\265\225\262J(&\221\226\210I&M\1'\7\201\60F\61\4(\14\203\355C"
  "\225DI\324[\224\5)\15\203\351C\221EY\324K\224D\0*\15\347dDW\252\264mIS-"
  "\3+\14\347dD\27\327\206!\213k\0,\11\202\254C\241$\12\0-\7$(E\61\4.\7B"
  ",D\61\4/\14F%D[\254\206iXM\1\60\22F%D\245EI\250M\211\22mb\22e"
  "\22\0\61\13E)D\225II\330\247A\62\17F%D\63$\241\230fZXM\207\1\63\20F%"
  "D\63$\241\230Fs*\212\311\220\0\64\20F%D\31jIT\311\222,\31\306\264\2\65\17F%"
  "DqH\253\203\234\246b\62$\0\66\17F%D\65\205i:(\241c\62$\0\67\13F%Dq"
  "-\246\305\264\11\70\20F%D\63$\241\61\31\222\320\61\31\22\0\71\16F%D\63$\241\61\31\324"
  "\306h\2:\11\342lD\61\304C\0;\12\42\355C\61\304J\242\0<\11%)D\231u\355\0="
  "\11\246\244Dq'\16\3>\11%%D\221v\353\10\77\17F%D\63$\241\230\206\325\34L#\0"
  "@\22F%D\65eR\242$K\244DJ$-\361\20A\16F%D\245E-\241\70\14\242c\0"
  "B\16F%D\61(\241qXB\307a\1C\16F%D\63$\241\265\243\230\14\11\0D\16F%"
  "D\61DY\22\372-\31\42\0E\15F%DqH\253\203\222\266\16\3F\14F%DqH\253\203"
  "\222v\5G\16F%D\63$\241\265\64\204\66e\11H\13F%D\21:\16\203\350\61I\13E)"
  "D\61Ha\77\15\2J\16G%D\65\210qOY\224e\33\0K\21F%D\21jIT\311D"
  "\61\311\242Z\22\6L\11F%D\221\366\327aM\15F%D\21\212\323\20-\36\35\3N\20F%"
  "D\21n\233\22)\221\224H\211v\14O\14F%D\63$\241\77&C\2P\15F%D\61(\241"
  "qX\322\256\0Q\26g\345C\63Da\22&a\22&a\22&\211\222H\322\220\13R\20F%D"
  "\61(\241qX\242Z\222%\241\30S\15F%D\63$\241\331QL\206\4T\12G%Dq\310\342"
  "\376\6U\13F%D\21\372\307dH\0V\21G%D\221Z\223,\312\242\254\22&i\234\1W\15"
  "F%D\21zq\231\206h\24\3X\17F%D\21\212I\324&jQK(\6Y\16G%D\221"
  "\252I\26e\225\64\356\6Z\13F%Dq-\366\232\16\3[\12\203\361C\61D\375\323\0\134\14F"
  "%D\221\306\325\70\215\253\1]\12\203\345C\61\365OC\0^\11fdF\245EI\30_\7'\344"
  "Cq\10`\7c\250F\221\25a\16\6%D\63$a\232\14\243MY\2b\16f%D\221\266,"
  "\232\350\270)\13\0c\15\6%D\63$\241\332\61\31\22\0d\14f%D\333\262h\243\67e\11e"
  "\17\6%D\63$\241\70\14j\61\31\22\0f\14e%D'\205\245A\12{\2g\23f\245C\233"
  ",Z\222%Y\264\245C\22\212\311\220\0h\14f%D\221\266,\232\350c\0i\13e)D\25\346"
  "\210\330\247Aj\14\245\245CYG\304>J\221\4k\21f%D\221\266%Q%\23\223,\252%a"
  "\0l\12e)D#\366O\203\0m\22\7%D\261(Q$ER$ER$ER\1n\13\6"
  "%D\221,\232\350c\0o\14\6%D\63$\241\217\311\220\0p\16F\245C\221,\232\350\270)K"
  "\232\2q\14F\245C\263h\243\67eI\13r\13\6%D\221,\232\250v\5s\15\6%D\63$"
  "\241\354\230\14\11\0t\13E%D\25\226\6)\354*u\12\6%D\21\372MY\2v\14\6%D"
  "\21\32\223\250\67Q\2w\21\7%D\221J\221\24I\221\24I\221T\261\0x\17\6%D\21\212I"
  "\224\211Z\224\204b\0y\16F\245C\21zL\42KZ\31\22\0z\12\6%Dq\15{\35\6{"
  "\16\244\251C\245da\26\25kQ\26\12|\7\301\261C\361A}\17\244\251C!fQ\26\226ja"
  "\226H\0~\12g$F\243I\221\246\0\177#\20\242\203\221\364;I\347\244\223\16I\66%a\32%"
  "C\222MI\230NC\62\354\234tN:)\351\7\200$\20\242\203\221\364;I\347\244\223\66i\231\222"
  "(\211\242C\22\15a\224D\305(YtN:'\235\224\364\3\260\12\204\250E\243DR\242\0\0\0"
  "\0\4\377\377 \202\15\345\344C\263da$e\341 \0";

That can go in itā€™s own .h file (mine is named u8g2_font_107b527caaa4ad2133465f74776dcb44.h) and then you will put in a #include at the top of your sketch code. Or, alternatively, you can just paste in the font definition at the top of your sketch code as well.

For me, near the top of my script, I have:

#include "u8g2_font_107b527caaa4ad2133465f74776dcb44.h"

char spin = ' ';
unsigned int Hr, Min = 0;

Then, hereā€™s my updateOLED() and updateOLED2() functions:

void updateOLED() {
   if (currentMillis - previousOled >= oledInterval) {
     previousOled += oledInterval;

    String ln4;
    String ln1 = "PM:" + String(pm25) +  " AQI:" + String(PM_TO_AQI_US(pm25));
    String ln2 = "COā‚‚:" + String(Co2); 
    String ln3 = "VOC:" + String(TVOC) + " NOx:" + String(NOX);
    if (inF) {
      ln4 = String((temp* 9 / 5) + 32, 1) + "Ā°F " + String(hum)+"%RH"; 
    } else {
      ln4 = String(temp, 1) + "Ā°C RH:" + String(hum)+"%"; 
    }
    
    if (spin == '-') {
      spin = '\\';
    }
    else if (spin == '\\') {
      spin = '|';
    }
    else if (spin == '|') {
      spin = '/';
    }
    else {
      spin = '-';
    }
    
    Hr = (currentMillis/3600000);
    Min = ((currentMillis/60000)-(Hr*60));

    String ln5 = String( String(Hr) + "h" + String(Min) + "m " + spin);

    updateOLED2(ln1, ln2, ln3, ln4, ln5);
   }
}

void updateOLED2(String ln1, String ln2, String ln3, String ln4, String ln5) {
      char buf[9];
          u8g2.firstPage();
          u8g2.firstPage();
          do {
          u8g2.setFont(u8g2_font_unifont_myfonts);
      
          u8g2.drawUTF8(1, 10, ln1.c_str());
          u8g2.drawUTF8(1, 25, ln2.c_str());
          u8g2.drawUTF8(1, 40, ln3.c_str());
          u8g2.drawUTF8(1, 55, ln4.c_str());
          
          u8g2.setFont(u8g2_font_6x10_mf);
          int ln4pos = u8g2.getDisplayWidth() - u8g2.getUTF8Width(ln5.c_str());
          u8g2.drawUTF8(ln4pos, 64, ln5.c_str());
          Serial.println(ln5);
          } while ( u8g2.nextPage() );
}

Let me know if it doesnā€™t work. I may have missed something when extracting it out as code snippets and cleaning some of the commented-out stuff.

3 Likes

Oh! I forgot. While I was doing all of that, I also opened an issue on the u8g2 repository and olikraus responded and decided to expand the extended fonts to include the subscript characters. He put in the milestone task list and I see he already committed the changes last week! So, in the next release, all the standard fonts should now include all the subscript glyphs!!! This should make things easier for everyone from now on! Not just the Air Gradient community.

https://github.com/olikraus/u8g2/issues/2079

https://github.com/olikraus/u8g2/milestone/40

https://github.com/olikraus/u8g2/commit/f303687122560300331e4025ff77ecb31b267afe

3 Likes

DUDE.

Yes, I read the ā€œbookā€. :slight_smile:

I will try it Monday when I get my double end USB-C cable (more on that in the painterā€™s tape thread) but:

you should change your name to Ken1010. For 10 outta 10!!

1 Like

Also, the more I think about it, itā€™s more than flair. When that small display has a lot of items on it, these little changes make it much more readable at a glance, because they help distiguish all the otherwise mostly all caps labels. Function from form.

So yeah. Thanks for getting sucked down the rabbit hole! Iā€™ve been there many times myselfā€¦I probably didnā€™t go knowing youā€™d already been!!

LOL! Thanks!

After we close out the Exception 0 issue, I have big plans (aspirations) for the display. Thereā€™s still a lot of room left in the D1 Miniā€™s flash for code, so we could get fancy.

I want a mode where it would cycle through dedicated screens for each type of reading with large font that you can see from across the room (or at least a bit further). For v3.7 boards and those that re-work a button, we can use that to switch modes between the everything-screen and the cycling-screen.

I also wanted a mode that can show a graph/plot of each readingā€™s 5-min/1-hr/etc. history because with air quality, short-term trends are sometimes more useful than real-time readings.

Even without the button, it could be an option during build-time. And donā€™t we have Bluetooth on the D1 Mini? We should be able to use that to control it. Iā€™ve never done anything remotely close to what Iā€™m proposing here, so everything will have to be learned from scratch. And I will inevitably hit obstacles and dead-ends. I donā€™t know if I should pursue this next or look into the OTA option.

Iā€™m looking at placing an order on AliExpress for a few D1 Minis, sensors, and displays to help speed up some of the work I have planned.

Hmm. I was able to get the degree symbol but not the subscript.
I even tried re-creating the font using the web tool you pointed to. No luck. Any ideas?

Hereā€™s what my creation window looks like (I pasted from Mac Character Viewer):

If you got the degree symbol but not the subscripts, then it sounds like youā€™re not using the custom font. Can you paste your sketch code here? And what is the filename of your custom font?

Will do this weekend. I donā€™t think itā€™s that Iā€™m not using the custom font. When I take the code pointing to that out, I get weirdcharacters in the display, just not the correct ones. With the custom code named, nothing shows up at all. COsubscript 2 (typing this on a phone) becomes just CO, not even a blank space. More later!

Hi @ken830,

Still canā€™t get this working. Hereā€™s my sketch code:

void updateOLED() {
   if (currentMillis - previousOled >= oledInterval) {
     previousOled += oledInterval;

    String ln4;
    String ln1 = "AQI:" + String(PM_TO_AQI_US(pm25)) + "  PMā‚‚ā‚…:" + String(pm25);
    String ln2 = "COā‚‚:" + String(Co2);
    String ln3 = "TVOC:" + String(TVOC) + "  NOx:" + String(NOX);
    if (inF) {
      ln4 = String((temp* 9 / 5) + 32,1) + "Ā° " + String(hum) +"%";
      } else {
      ln4 = "C:" + String(temp,1) + "Ā° H:" + String(hum) +"%";
      }
    updateOLED2(ln1, ln2, ln3, ln4);
   }
}

void updateOLED2(String ln1, String ln2, String ln3, String ln4) {
      char buf[9];
          u8g2.firstPage();
          u8g2.firstPage();
          do {
//          u8g2.setFont(u8g2_font_t0_16_tf);
          u8g2.setFont(u8g2_font_unifont_myfonts);
          u8g2.drawStr(1, 10, String(ln1).c_str());
          u8g2.drawStr(1, 27, String(ln2).c_str());
          u8g2.drawStr(1, 44, String(ln3).c_str());
          u8g2.drawStr(1, 61, String(ln4).c_str());
            } while ( u8g2.nextPage() );
}

Hereā€™s what I see when I flash that - no subscript CO2 or PM25:

If I use u8g2.setFont(u8g2_font_t0_16_tf); hereā€™s what I get:

Things I tried to fix this, none of which helped:

  1. Changed esp8266 to v3.1.1 (the one that keeps rebooting). No effect.
  2. Tried commenting out my include statement. This verified that yes, the sketch is looking for the custom code declaration.
  3. Instead of using my own custom code, pasted the custom code you supply above, directly into my sketch instead of in a .h file. No effect.

Any idea?

Assuming you got the font name and include filename correct, I think this is probably the problem:

You should use u8g2.drawUTF8() instead of u8g2.drawStr().

And the Arduino String() function is redundant here as the ln1/2/3/4 are already of type string as defined in updateOLED(). This wonā€™t prevent it from working, but it is unnecessary.

Give this a try:

void updateOLED2(String ln1, String ln2, String ln3, String ln4) {
      char buf[9];
          u8g2.firstPage();
          u8g2.firstPage();
          do {
//          u8g2.setFont(u8g2_font_t0_16_tf);
          u8g2.setFont(u8g2_font_unifont_myfonts);
          u8g2.drawUTF8(1, 10, ln1.c_str());
          u8g2.drawUTF8(1, 27, ln2.c_str());
          u8g2.drawUTF8(1, 44, ln3.c_str());
          u8g2.drawUTF8(1, 61, ln4.c_str());
          } while ( u8g2.nextPage() );
}

EDIT:
BTW, I put together a quick-and-dirty uptime calculator function that counts the rollovers of millis(). I used an unsigned long, so itā€™s crazy-overkill. As long as we call the uptime function at least once every ~49.7 days, we wonā€™t rollover until (2^64)-1 milliseconds = 584,554,531 years! Had to keep it plugged in and not play with it for over a day to check I got my logic all correct:


Thank you! Fixed, you were exactly right on ā€˜ā€™ā€˜drawUTF8ā€™ā€™ā€™ substitution. I could have inspected more carefully.

Love the mutli-unit uptime calc!

Great!!

If you are interested:

// Calculate Up-time in seconds - uses millis() along with an unsigned long overflow counter. Must call at least once every (2^32)-1 milliseconds (~49.7 days)
unsigned long upTimeSec() {
  static unsigned long prev_millis = 0;
  static unsigned long rollover_count = 0;

  unsigned long uptime_sec;
  unsigned long current_millis;

  current_millis = millis();

  if (prev_millis > current_millis) {
    rollover_count++;
  }

  uptime_sec = ((rollover_count * (((2^32)-1)/1000)) + ((current_millis)/1000));

  prev_millis = current_millis;

  return uptime_sec;
}

Awesome, thank you - Iā€™ll have to add it next I make some tweaks to see if I can add any up-time observations. Mine certainly appears to be stable now.

1 Like

How I use it (one year = every 365 days, no leap year accounting):

    currentSec = upTimeSec();

    Yr = (currentSec / 31536000);
    currentSec -= (Yr * 31536000);
    Dy = (currentSec / 86400);
    currentSec -= (Dy * 86400);
    Hr = (currentSec / 3600);
    currentSec -= (Hr * 3600);
    Min = (currentSec / 60);