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.
/*
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;
/***************************************************
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] = ' ';
}
}
}
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
};
}
}