OLED Portable Display Arduino Sketch

This device is completely self-contained and battery-powered. Since it is to be used hand-held it turns itself off if not in use. When off, the battery current is negligible, so it uses 2 AA non-rechargeable batteries which may last for years.

It has 16 "pages" with defined transitions between them controlled by two push buttons and by timeouts from inactivity. The power is turned on by pressing either button until the display shows. If using the yellow button, the initial page shows time and date. If the red button, the page shows the sensor outputs. 

There are 4 sensor pages, depending on the pressure and units preferences.

The remaining 11 pages are for settings and preferences, whose values are stored in the RTC or EEPROM. These pages are accessed by pressing the yellow button from the time/date page, then red.


All pressure altitude measurements need adjustment for weather-induced pressure changes. Conversely, Barometer pressure values are converted to sea level using an official formula and value (baroBase) for sea level pressure in pascals. 

The library makes these changes, but cannot know the base value, which is entered by the user. To avoid storing a long in EEPROM, the library value has been edited to 400 less. The differences is stored in EEPROM as baroDelta. The baro correction page however shows the altitude. The user should make this adjustment at a location of known altitude.

To avoid excessive EEPROM writes this is done only once on exit by timeout.


The compass angle from the magnetometer was compared to an orienteering compass by itself outdoors and showed remarkably good agreement, considering the difficulty in reading the small compass.

However, it failed miserably when installed in the project, which problem was alleviated by relocating the sensor as far as possible from the batteries. It now seems to work, but has not been calibrated.


The code is written for true north, using the hard-coded declination value for San Diego. On re-location, there is the choice of re-compiling with a new local declination value, or removing the correction code and defining the project as showing magnetic north.

OLED Portable Display Declarations

/*

   Mobile_Display This is the source code for a 2x4x10" wood box with a recessed 2x16 blue OLED character display

   It is powered by 2 alkaline AA batteries and a CR1220 for the RTC, which is the temperature-compensated DS3231

   It has a yellow and a red push-button, which are the sole controls. Pressing either turns it on.

   The yellow gets you time&data, red the BME280 data and HMC5883L compass points

   Both together gets AA battery voltage and access to several Setup pages to correct time and make display preferences

   It turns itself off after a period of no buttons pressing. Then it consumes no power from the AA batteries

   To get inside, remove the screws at the back and slide the bottom out.

   If changing the CR1220, be sure to keep a button pressed.

   To program it, you need a USB to 6-pin adapter with 5V output such as Adafruit's FTDI friend.

   The controller is an Arduino Mini Pro, 3.3V, 8 MHz. There are no more instructions!

    11/25/2018

*/

#include <SparkFunBME280.h>    // Pressure, Temperature, Humidity breakout board, I2C interface

#define bme280Addr 0x77        // I2C

#include <HMC5883L.h>          // 3-axis magnetometer Spark Fun breakout board, I2C interface

#include <EEPROM.h>            // To store non-RTC adjustable Setup parametrs

#define baroAddr 20            // First of four bytes used to store Setup parametrs

#define dispAddr 0x3D          // I2C

#define rtcAddr  0x68          // I2C

#include "Mobile_Display.h"    // Location of typedefs and fixed data

#define OLED_Reset 7           // Digital port

#define turnOn 2               // Digital port to maintain power from battery when HIGH

#define redButton 6            // Digital port, LOW when pressed

#define yellowButton 5         // Digital port, LOW when pressed

#define sqWave 4               // Digital port, 1 Hz signal from the RTC used to decrement countdown timer

#define baroInc 15             // This increments the sea level barometric pressure per button press

#define baroBase 101325        // This is the official value (Pascals), and is hard coded into SparkFunBME280.h


// Writing the display is a two-step process, first through these mirroring buffer array

char uRow[] = {"Made in 2018 by"};  //Data in here will appear on the top line of the display

char lRow[] = {" John Saunders "};  //Data in here will appear on the lower line of the display


// The RTC output is stored in this intermediary array, 2 entriies per RTC byte,high-low

// 0,1 secs,2,3 mins,4,5 hrs, 6,7 day,7,8 date, 9, 10 mon, 11, 12 year

byte rtcInts[14];                   //The RTC registers 0-6 split up into 2 bytes each,


/***************************************************

   Data ready to display these are written into values.val by the xxGet() functions

   Format Code   Index      Content

   200             0        Altitude in feet

   201             1        Temperature in deg F

   202             2        Humidity

   203             3        Timeout

   204             4        Battery Volts

   205             5        Altitude in metres

   206             6        Temperature in deg C

   207             7        Barometric pressure in in of Hg

   208             8        Barometric pressure in units of 100 kiloPascals

   209             9        Baro delta

 */  

values valInts[] = {      //data value {int),digits,places, index is fmt - 200

  //   0         1          2          3          4          5          6          7          8         9          

  {0, 5, 0}, {0, 5, 1}, {0, 2, 0}, {0, 3, 0}, {0, 3, 2}, {0, 4, 0}, {0, 5, 1}, {0, 4, 2}, {0, 5, 2},{0, 5, 0}

};


int page = 1;

int baroDelta = 0;

byte unitType;         // Select US(138) or Metric(139), in EEPROM address baroAddr + 2

byte pressType;        // Select altitude(130) or pressure(140),in EEPROM address baroAddr + 3

byte TO;               // Timeout count


BME280 altChip;

HMC5883L compass;

OLED Portable Display Functions

/***************************************************

    Functions for reading inputs

    buttonGet   Combines the presses of the two buttons into one byte

    batteryGet  Reads the voltage of the 2 main AA alkaline batteries and corrects for the 3.3V

    rtcGet      Reads the BCD vak\lues frim the RTC and converts each into two bytes in an array

    BME280Get   Reads pressure,temperature and humidity into nalInts, 6 places

    compassGet  Reads the magnetometer and outputs as an angle in radians

*/


byte buttonGet() {

  int temp = 0;

  if (digitalRead(yellowButton) == LOW) {

    temp = temp | 1;

  }

  if (digitalRead(redButton) == LOW) {

    temp = temp | 2;

  }

  return temp;

}


void batteryGet() {

  /*

     Reference volts = 3.304, Power switch drop = 0.141

  */

  float rawV;

  rawV = (analogRead(1) * 0.3227) + 14.1;

  valInts[4].val = int(rawV);

}


void rtcGet() {

  int j;

  byte bcdVal, bcdNibble;

  Wire.beginTransmission(rtcAddr);

  Wire.write(0);

  Wire.endTransmission();

  Wire.requestFrom(rtcAddr, 7);

  for (int i = 0; i < 7; i++) {

    j = 2 * i;

    bcdVal = Wire.read();

    bcdNibble = bcdVal / 16;

    rtcInts[j] = bcdNibble;

    j = (2 * i) + 1;

    bcdNibble = bcdVal & 0x0F;

    rtcInts[j] = bcdNibble;

  }

/*   posESC(2,0);

  for (int indx = 0; indx < 14;indx++) {

    Serial.print(rtcInts[indx]);

}

*/

}


void BME280Get() {

  float alt = 0;

  float deg = 32;

  float hum = 50;

  float pmetric;

  float pus;

  alt = altChip.readFloatAltitudeFeet();

  deg = altChip.readTempF();

  hum = altChip.readFloatHumidity();

  valInts[0].val = int(alt);

  deg = 10 * deg;

  valInts[1].val = int(deg);

  valInts[2].val = int(hum);

  alt = altChip.readFloatAltitudeMeters();

  deg = altChip.readTempC();

  valInts[5].val = int(alt);

  deg = 10 * deg;

  valInts[6].val = int(deg);

  alt = altChip.readFloatPressure();

  pus = 0.029529983 * alt;      //In 0.01 in mercury

  valInts[7].val = int(pus);

  pmetric = alt / 10;           // in tens of pascals

  valInts[8].val = int(pmetric);

 ;

}


float compassGet() {

  Vector norm = compass.readNormalize();

  float heading = atan2(norm.YAxis, norm.XAxis);

  float declinationAngle = (11.0 + (26.0 / 60.0)) / (180 / M_PI);

  heading += declinationAngle;

  // Correct for heading < 0 deg and heading > 360deg

  if (heading < 0)

  {

    heading += 2 * PI;

  }

  if (heading > 2 * PI)

  {

    heading -= 2 * PI;

  }

  return heading;

}


/*****************************************************************

   Functions for setup

   rtcAdj     Limited to changing hours (for DST) and minutes

   baroAdj    Sets the sea-level reference pressure into the BME280 and also EEPROM on page exit

   pressAdj   Changes the readout from altitude to barometric pressure, stores in EEPROM on page exit

   unitsAdj   Changes the readout from US to Metric values, stores in EEPROM on page exit

   toGet      Utility to read the timeout value from the jumpTable into the global variable TO

   toPut      Ulility to put the global variable TO into the umpTable

*/


void rtcAdj(int bt,int ad) {          //button and RTC address

  int rtcVal;

  byte BCD;

  int addr = 2 * ad;

  toPut();           // Reset timeout

  rtcVal = 10 * rtcInts[addr];

  addr = addr + 1;

  rtcVal = rtcVal + rtcInts[addr] + (2 * bt) - 3;

  posESC(2,0);

  Serial.print(rtcVal);

  BCD = byte((16 * (rtcVal / 10)) + (rtcVal % 10));

  Wire.beginTransmission(rtcAddr);

  Wire.write(ad);

  Wire.write((byte)BCD);

  Wire.write(0);

  Wire.endTransmission();

  return true;

}


void baroRefresh() {

  long refPressure;

  refPressure =  baroBase + baroDelta;

  altChip.setReferencePressure((float)refPressure);

  valInts[9].val = baroDelta;

}


void baroAdj(int bt) {

  long refPressure;

  toPut();           // Reset timeout

  baroDelta = baroDelta + (2 * baroInc * bt) - (3 * baroInc);

  baroRefresh();

  if (TO == 5) {

      int addr = baroAddr;

      EEPROM.write(addr, highByte(baroDelta));

      addr = addr + 1;

      EEPROM.write(addr, lowByte(baroDelta));

      toPut();

    } 

}


void pressAdj(int bt) {

  if (bt == 1) {

    pressType = 130;

  }

  if (bt == 2) {

    pressType = 140;

  }

   // write the new value of pressType into the EEPROM

  int addr = baroAddr + 2;

  EEPROM.write(addr, pressType);

  page = modeGet();

  toPut();           // Reset timeout

}


void unitAdj(int bt) {

  if (bt == 1) {

    unitType = 138;

  }

  if (bt  == 2) {

   unitType = 139;

  }

  // write the new value of pressType into the EEPROM

  int addr = baroAddr + 3;

  EEPROM.write(addr, unitType);

  page = modeGet();

  toPut();           // Reset timeout

}


byte toGet(int pg) {

  jump_t  jp = jumpTable[pg];

  return jp.toVal;

}


void toPut() {

  TO = jumpTable[page].toVal;

  valInts[3].val = TO;

}


byte modeGet(void) {

  byte altPage = 0;

  if (unitType == 139) {

    altPage += 1;

  };

  if (pressType == 140) {

    altPage += 2;

  };

  return altPage;

}


/*****************************************************************

     Functions to control the State Diagram - controls changing pages

     pageJump     Called on timeout or button press to display a different page

     modeGet      Assists pageJump fot the pages which vary according to pressType and unitType

*/


bool pageJump(int bt) {  // Returns true if a jump has ocurred

  bool jumped = false;

  int newPage;

  if (TO == 0) {            // Every page has a timeout

    page = jumpTable[page].toJump;

    toPut();

    jumped = true;

    if (page == 84) {

      digitalWrite(turnOn, LOW);

      page = 4;

    }

  }

  if (bt == 3) {       // Both buttons are needed

    page = 4;

    jumped = true;

  }

  if (bt == 2)  {

    newPage = jumpTable[page].redJump;

    if (newPage < 20) {

      page = newPage;

      jumped = true;

    }

    else if (newPage == 66)  {

      page = modeGet();

     jumped = true;

    }

  }

  else if (bt== 1)  {

    newPage = jumpTable[page].yelJump;

    if (newPage < 20) {

      page = newPage;

      jumped = true;

    }

  }

  if (jumped) {

    toPut();

    posESC(3,0);

    Serial.write(strDiag,16);

    posESC(3,5);

    Serial.print(page,DEC);

    Serial.print("  ");

  }

  return jumped;

}


/*****************************************************************

     Functions to write fields into the buffers uRow and lRow

     pageDisp    Top of the calling tree for displaying. Scans current entery in pages to end marker

     pageCpy     Called repeatdly by pageDisp with the contents of each field

     dispDec     Called from pageCpy to put a numerical value into the buffer

     strCpy      Called from pageCpy to put fixed string into the buffer

     putDisp     Called from pageCpy to put character into the buffer

*/


void pageDisp(byte pg) {

  byte rw, cl, fmt;

  int indx = 0;

  page_t *pp = pages[pg];

  do {

    rw = pp[indx].row;

    cl = pp[indx].column;

    fmt = pp[indx].format;

    if ((rw > 1) || (cl > 15) || (fmt > 252)) {

      indx = 0;

      break;

    }

    else {

      pageCpy(rw, cl, fmt);

      indx++;

    }

  }

  while (1);

}


void pageCpy(byte r, byte c, byte f) {

  char Alpha;

  int indx;

  if (f < 14) {

    Alpha = rtcInts[f] + '0';

    putDisp(r, c, Alpha);

  }

  else if (f == 14) {

    indx = 3 * (rtcInts[7] - 1);

    for (int col = 0; col < 3; col++) {

      Alpha = strDays[indx];

      putDisp(r, c, Alpha);

      c++;

      indx++;

    }

  }

  else if (f == 15) {

    indx = (10 * (rtcInts[10] & 1) ) + rtcInts[11] - 1;

    indx = 3 * indx;

    for (int col = 0; col < 3; col++) {

      Alpha = strMonths[indx];

      putDisp(r, c, Alpha);

      c++;

      indx++;

    }

  }

  else if (f == 16) {

    float normalized = (4 * compassGet() / PI) - 0.29;

    if (normalized < 0.5) {

      normalized = normalized + 8;

    }

    normalized = normalized - 0.5;

    indx = int(normalized);

    strCpy(r, c, strOctant[indx]);

  }

  else if (f == 17) {       //Display type of pressure readout

    indx = pressType - 130;

    putDisp(r,c,*strPages[indx]);

  }

  else if (f == 18) {       //Display whether using US or Metric units

    indx = unitType - 130;

    putDisp(r,c,*strPages[indx]);

  }

  else if ((f >= 31) && (f < 130)) {

    putDisp(r, c, f);

  }

  else if ((f >= 130) && (f < 149)) {

    f = f - 130;

    strCpy(r, c, strPages[f]);

  }

  else if ((f >= 200) && (f < 250)) {

    f = f - 200;

    dispDec(r, c, valInts[f].val, valInts[f].digits, valInts[f].places);

  }

}


void dispDec(int row, int col , int val, int nums, int dps) {

  int temp;

  if (val < 0) {

    val = abs(val);

    putDisp(row, col, '-');

    col++;

  }

  temp = val / 10000 + '0';

  if ((nums == 5) && (temp > '0')) {

    putDisp(row, col, temp);

    col++;

    if (dps == 4) {

      putDisp(row, col, '.');

      col++;

    }

  }

  val = val % 10000;

  temp = val / 1000 + '0';

  if (nums > 3) {

    putDisp(row, col, temp);

    col++;

    if (dps == 3) {

      putDisp(row, col, '.');

      col++;

    }

  }

  val = val % 1000;

  temp = val / 100 + '0';

  if (nums > 2) {

    putDisp(row, col, temp);

    col++;

    if (dps == 2) {

      putDisp(row, col, '.');

      col++;

    }

  }

  val = val % 100;

  temp = val / 10 + '0';

  putDisp(row, col, temp);

  col++;

  if (dps == 1) {

    putDisp(row, col, '.');

    col++;

  }

  temp = val % 10 + '0';

  putDisp(row, col, temp);

}


void strCpy(int row, int col, char *strng) {

  char val;

  int loc = col;

  int indx = 0;

  val = strng[indx];

  while (val > 0) {

    if (row == 0) {

      uRow[loc] = val;

    }

    else {

      lRow[loc] = val;

    }

    loc++;

    indx++;

    val = strng[indx];

  }

}


/*****************************************************************

    posESC writes an escape sequence into the SmartMarrix for cursor positioning

*/

void posESC(int Erow,int Ecol) {

  char r = Erow+49;

  char c = (Ecol & 0xFF) + 1;

  Serial.write((char)27);

  Serial.write('[');

  Serial.write(r);

  Serial.print(';');

  Serial.print(c,DEC);

  Serial.print('H');

}


/*****************************************************************

    colorESC writes an escape sequence into the SmartMarrix for cursor positioning

    

*/

void colorESC(int Ecolor) {

  Serial.write(27);

  Serial.print('[');

  Serial.print(Ecolor);

  Serial.print('m');

}


void clearESC(void) {

  Serial.write(27);

  Serial.print('[');

  Serial.print(2);

  Serial.print('J');

}


inline void putDisp(int r, int c, char d) {

  if (r == 0)  {

    uRow[c] = d;

  }

   if (r == 1)  {

    lRow[c] = d;

  }

}



/*****************************************************************

    dispRow writes the contents of uRow and lRow into the display

*/


void dispRow(int row) {

  int loc = (0x40 * row) + 0x80;

  Wire.beginTransmission((uint8_t)dispAddr);

  Wire.write((byte)0);

  Wire.write((uint8_t) loc);

  Wire.endTransmission();

  Wire.beginTransmission((uint8_t)dispAddr);

  Wire.write((byte)0x40);

  if (row == 0) {

    Wire.write(uRow, 16);

    posESC(0,0);

    Serial.write(uRow, 16);

  }

  if (row == 1)  {

    Wire.write(lRow, 16);

    posESC(1,0);

    Serial.write(lRow, 16);

  }

  Wire.endTransmission();

  for (int i=0; i < 16; i ++) {

    if (row == 0 ) {    

      uRow[i] = ' ';

    }

    if (row == 1 ) {

      lRow[i] = ' ';

    }

  }

}

OLED Portable Display Setup and Loop

void setup () {

  if (buttonGet() > 0) {

    digitalWrite(turnOn, HIGH);

  }

  page=4;

  Serial.begin(9600);

  clearESC();

  delay(100);

  Serial.write(strDiag,16);

  posESC(3,5);

  Serial.print(page,DEC);

  Serial.print("  ");

  pinMode(turnOn, OUTPUT);            //Keep-Alive

  pinMode(13, OUTPUT);            //built-in LED

  digitalWrite(13, LOW);

  pinMode(OLED_Reset, OUTPUT);

  digitalWrite(OLED_Reset, HIGH);

  Wire.begin();

  byte pindx, pval;

  pindx = 0;

  do {

    Wire.beginTransmission((uint8_t)dispAddr);

    pval = highByte(initParams[pindx]);

    Wire.write((byte)pval);

    pval = lowByte(initParams[pindx]);

    Wire.write((byte)pval);

    Wire.endTransmission();

    pindx++;

  }

  while (pval != 0x0C);

  dispRow(0);

  dispRow(1);

  delay(1000);

  byte BMEconst = altChip.begin();

  int addr = baroAddr;

  baroDelta = 16 * EEPROM.read(addr);

  addr = addr + 1;

  baroDelta = baroDelta  +  EEPROM.read(addr);

  altChip.setI2CAddress(bme280Addr);

  addr =  baroAddr + 2;

  pressType = EEPROM.read(addr);

  if ((pressType != 130) || (pressType != 140))  {

    EEPROM.write(addr, (byte)130);

  }

  addr++;

  unitType = EEPROM.read(addr);

  if ((unitType < 138) || (unitType > 139)) {

    EEPROM.write(addr, (byte)138);

  }

  baroRefresh();

  compass.begin();

  TO = toGet(page);


  // Set measurement range

  compass.setRange(HMC5883L_RANGE_1_3GA);


  // Set measurement mode

  compass.setMeasurementMode(HMC5883L_CONTINOUS);


  // Set data rate

  compass.setDataRate(HMC5883L_DATARATE_30HZ);


  // Set number of samples averaged

  compass.setSamples(HMC5883L_SAMPLES_8);


  // Set calibration offset. See HMC5883L_calibration.ino

  compass.setOffset(0, 0);

  /*

     Wire.beginTransmission(0x68);  // Open I2C line in write mode

    Wire.write((byte)3);

    Wire.write((byte)7);  //enable 1 Hx square wave    lcd.setCursor(0, 0);

    Wire.endTransmission(); 

*/

}


byte phase = 0;



void loop() {

  int button;

  bool jp;

  if (digitalRead(sqWave) != phase) {

    phase = digitalRead(sqWave);

    rtcGet();

    batteryGet();

    BME280Get();

    button = buttonGet();

    jp = pageJump(button);

    pageDisp(page);

    dispRow(0);

    dispRow(1);

    if (((button == 1 ) || (button == 2)) && !jp) {

      // Check for adjustments

      switch (page) {

         case 6:

          pressAdj(button);

          break;      

        case 8:

          baroAdj(button);

          break;

        case 10:

          rtcAdj(button,2);     //hours

          break;

        case 12:

          rtcAdj(button,1);     //minutes

          break;

        case 14:

          unitAdj(button);

          break;

      }

    }

    if (TO > 0) {             // Timeout

      TO--;

    }

    valInts[3].val = TO;

    posESC(3,10);

    Serial.print(TO,DEC);

    Serial.print("  ");

     posESC(3,16);

    Serial.print(baroDelta,DEC);

    while (buttonGet() > 0) {         // Wait to release buttons

    };

  }

}