/* OLEDLogger,12/7/2021 pairs with OLEDLogger.bas 12/6/2021 John Saunders
The OLED Logger performs display and logging of RF messages and analog inputs.
It consists of an external (to avoid interference) RF receiver with a wood base and
three sub-assemblies which are housed in a metal clamshell case with a sloping cream-colored top.
This has a 20-column by 4 row blue OLED character display, and a 5-button rosette,
which are connected to the brown base by a ribbon cable.
The base has three sub-assemblies: Power, Signal Conditioning, and Logging/Display.
A. The Power sub-assembly has a 3400 MAH Li-Ion cell which has a switch-mode charger.
The recommended charging power supply is the Challenger 12V, 3A with 2 pigtails.
When the battery is fully charged, the power consumption is 1.2W.
It also has an on-off switch and an attenuator to measure the charging voltage.
The power must be on to charge. The battery connects to the Logging/Display when on.
The Logging/Display provides +5V power to the Signal Conditioner from an internal booster,
B. The Signal Conditioner sub-assembly used a Picaxe 28X2. It converts analog inputs to ASCII in volts.
There are three sets of inputs:
1. Power Monitoring: Charging voltage and Battery voltage.
2. Rear panel: 3 jacks Green, Blue and Orange have 3 switched ranges: Up=28V,Center=52V,Down=12V.
Scaling is corrected in the conversion. A 4.096V reference is used.
A White jack is direct 2V using a 2.048V reference.
Analog inputs are digitized and transferred to the Logging/Display when it sends a command.
3. RF signals via 9-pin male jack.
These are already digitized and verified messages are immediately transferred to the Logging/Display unmodified.
C: The Logging/Display uses an Pololu A-Star 32u4 Prime LV. compatible.
The 32u4 was selected over the 328P because its additional 512 bytes SRAM was needed.
The Pololu A-Star was selected over the Arduino Leonardo because it runs directly from the battery.
The A-Star is covered by full-size SD logger shield,
which also has a real-time clock-calendar and an optional serial EEPROM.
The hard-wiring on the shield and A-star peculiarities severely constrained the choice of pins.
The I2C RTC is hard-wired to pins 2&3 on the A-Star.
The SD and the EEPROM are SPI hard-wired on the shield to pins 11-13. 10 is hard wired for the SD select.
Pin 8 was home-wired to 8 for the EEPROM select.
Serial input on the 32u4 needs 8 and up, so 9 must be used. There are additional pins on the A-Star
but the shield makes them physically inaccessible. A0 and A1 are used for the buttons,
This results in using 5,7 and A2-A5 for the display and 6 for the serial output. 4 is unused.
Modified 12/21/2021 to provide custom characters for ohm and degree
12/24/21 holdCode = txBuf[0] not keyCode; clear end of TimeDate
12/25/21 Show manual comand on timeDate line end and wait for response for another command
10/10/22 New measurements codes. Modified incoming. 10/11/2022 Mode changes
This is the program running prior to attaching the Feather.
*/
// include the library code:
#include <PololuHD44780.h> // Needed to get setCursor() to work right
#include <SoftwareSerial.h>
#include <EEPROM.h>
#include <SD.h> // I hate micro-SD. There is one on the A-Star.
#include <PCF8523.h> // Not DS1307 compatible, 2020 version
#define numPages 14
PCF8523 rtc;
// initialize the library by associating any needed LCD interface pin
// with the arduino pin number it is connected to
const byte rs = 7, en = 5, d4 = A2, d5 = A3, d6 = A4, d7 = A5;
PololuHD44780 oled(rs, en, d4, d5, d6, d7);
const byte rxPin = 9;
const byte txPin = 6;
SoftwareSerial picaxeLink(rxPin, txPin);
const int chipSelect = 10; // SD
unsigned int countUp[4] = {0, 0, 0, 0}; // for the measurements and ageing
byte dispMode = 0;
char keyHold;
char cmdChar;
byte newModeFlag;
// --------------------------- Setup ------------------------------
void setup() {
char SDId[2];
pinMode(rs, OUTPUT);
pinMode(en, OUTPUT);
// oled.begin(20, 4);
oled.initPins();
oled.clear();
delay(10);
oled.display();
delay(10);
pinMode(rxPin, INPUT);
pinMode(txPin, OUTPUT);
picaxeLink.begin(57600);
oled.setCursor(0, 3); dispMode = 0;
delay(10);
oled.clear();
delay(10);
if (picaxeLink.isListening() == true) {
oled.print("RxPin ");
oled.print(rxPin, DEC);
oled.print("=OK ");
}
SD.begin(chipSelect);
File dataFile = SD.open("ID.TXT", FILE_READ);
dataFile.read(SDId, 1);
if (SDId > 0) {
delay(10);
oled.print("SD is ");
oled.print(SDId);
delay(1000);
}
else {
oled.print("No SD present");
delay(200);
}
rtc.begin();
rtc.setBatterySwitchover();
oled.createChar(0, degSym);
oled.createChar(1, ohmSym);
BGLimit = EEPROM.read(0);
OWLimit = EEPROM.read(1);
dispMode = 0;
keyHold = 0;
newModeFlag = 0;
cmdChar = ' ';
}
// --------------------------- Loop ------------------------------
void loop() {
char picChar, keyCode = ' ';
byte colCount = 0;
//byte lastCol = 0;
byte newMsgFlag = 0;
byte buttVal = getButtons();
autoCommand();
// --------------------------- Input from Picaxe ------------------------------
colCount = 0;
// lastCol = 0;
while ((picaxeLink.available() > 0) && (keyCode == ' ')) { // 130 ms, including 76 for record
picChar = picaxeLink.read();
if (picChar == '<') {
picChar = picaxeLink.read(); // Should be Key Code
if ((picChar >= 'n') && (picChar <= 'z')) {
keyCode = picChar;
if (picChar == 't') {
cmdChar = ' '; // Enable another command
}
SDBuf[0] = picChar;
colCount = 1;
}
}
delay(3);
}
while ((picaxeLink.available() > 0) && (keyCode != ' ')) {
picChar = picaxeLink.read(); // skip over a comma
if ((picChar <= 'z') && (colCount < 21) && (picChar != '>')) {
SDBuf[colCount++] = picChar;
}
else { //10/10/2022
SDBuf[colCount] = 0;
//lastCol = colCount - 1;
// SDBuf[lastCol] = 0;
newMsgFlag = 1;
cmdChar = ' ';
}
delay(3);
}
// if (lastCol > 0) {
// SDBuf[lastCol] = 0;
// }
// --------------------------- Mode State Machine ------------------------------
switch (dispMode) {
case 0:
newModeFlag = 0;
manualCommand(buttVal);
showIncoming(newMsgFlag);
switch (buttVal) {
case 4: // left button
dispMode = 2;
newModeFlag = 1;
oled.clear();
break;
case 16: //center button
dispMode = 1;
newModeFlag = 1;
keyHold = 0;
break;
}
break;
case 1:
switch (buttVal) {
case 1: // Up button
keyHold = 0;
break;
case 3: // Down button
keyHold = SDBuf[0];
break;
case 4: // left button
dispMode = 0;
newModeFlag = 1;
oled.clear();
break;
}
if ((newModeFlag == 1) || ((newMsgFlag == 1) && (keyHold == 0)) || ((newMsgFlag == 1) && (keyCode == keyHold))) {
for (byte col = 0; col < SDBufSize; col++) {
picChar = SDBuf[col];
currMsg[col] = picChar;
if (picChar == 0) {
break;
}
}
countUp[3] = 0;
newModeFlag = 0; //moved 10/11/2022
}
showPage(newMsgFlag);
break;
case 2:
if (newModeFlag == 1) {
showAdjStrings();
}
newModeFlag = 0;
adjustments(buttVal);
switch (buttVal) {
case 16: //center button
adjChange();
dispMode = 0;
newModeFlag = 1;
oled.clear();
break;
}
break;
}
if ((dispMode < 2) && (newMsgFlag == 1)) {
record();
newMsgFlag = 0;
// oled.setCursor(0, 3);
// oled.print("End of loop");
}
while (getButtons() > 0); {
delay(10);
}
delay(10);
}
// --------------------------- Displaying ------------------------------
void showTimeDate(void) {
byte tenSec;
static byte oldTenSec = 0;
DateTime now = rtc.readTime();
tenSec = (rtc.rtcReadReg(PCF8523_SECONDS)) & 0x70; // Tens of seconds
if (oldTenSec != tenSec) {
oldTenSec = tenSec;
for (int i = 0; i < 4; i++) {
++countUp[i];
}
}
if (dispMode < 2) {
oled.setCursor(17, 0);
oled.print(" " );
oled.print(cmdChar);
oled.setCursor(0, 0);
oled.print(now.hour());
oled.print(':');
oled.print(now.minute());
oled.print(':');
oled.print(now.second());
oled.print(' ');
oled.print(now.month());
oled.print('/');
oled.print(now.day());
oled.print('/');
oled.print(now.year());
}
}
#define SDBufSize 21
char SDBuf[SDBufSize];
void showIncoming(byte newMsg) {
static String row1 = " ", row2 = " ", row3 = " ";
if (newMsg == 1) {
oled.clear();
row3 = row2;
row2 = row1;
row1 = SDBuf;
}
oled.setCursor(0, 3);
oled.print(row3);
oled.setCursor(0, 2);
oled.print(row2);
oled.setCursor(0, 1);
oled.print(row1);
oled.setCursor(0, 0);
showTimeDate();
}
const char*units[] = {
// n o p
" ", "$", "$", " ", " ", " ", " ", " ", "MA", "V", " ", " ",
// q r s
" ", " ", "`F", "`F", "inHg", "V", "MAH", "MAH", "`F", "%", "MA", "V",
// t u v
" ", "V", " ", "V", "`F", " ", " ", " ", " ", "V", " ", "V",
// w x y
" ", "W", " ", " ", " ", "V", " ", " ", " ", "V", " ", "V",
// z
"`F", " ", "V", " "
};
byte ohmSym[8] = {
B01110,
B10001,
B10001,
B10001,
B01010,
B01010,
B11011,
};
byte degSym[8] = {
B11100,
B10100,
B11100,
B00000,
B00000,
B00000,
B00000,
};
byte currMsg[SDBufSize];
const char *msgName[13] = {"gasAir", "event", "test", "brFan", "baro", "env", "meas", "gFan", "volts", "watts", "radar", "dau", "gasTl"};
void printUnits(byte index) {
char unitsChar = units[index][0];
oled.print(' ');
switch (unitsChar) {
case'`':
oled.write(byte(0));
oled.write('F');
break;
case'$':
oled.write(byte(1));
break;
default:
oled.print(units[index]);
break;
}
}
void showPage(byte flag) {
byte id = currMsg[0] - 'n';
byte commaCt = 0;
byte unitsIndex;
char msgChar;
if (id > 12) {
return;
}
if (flag == 1) {
oled.clear();
}
oled.setCursor(0, 2);
for (int col = 2; col <= SDBufSize; col++) {
unitsIndex = (4 * id) + commaCt;
msgChar = currMsg[col];
if (msgChar == ',') {
printUnits(unitsIndex);
switch (commaCt) {
case 0:
oled.setCursor(10, 2);
commaCt++;
break;
case 1:
oled.setCursor(0, 3);
commaCt++;
break;
case 2:
oled.setCursor(10, 3);
break;
}
}
else {
if (msgChar == 0) {
printUnits(unitsIndex);
break;
}
else {
oled.write(msgChar);
}
}
}
oled.setCursor(0, 0);
showTimeDate();
oled.setCursor(0, 1);
oled.print(msgName[id]);
if (keyHold > 0) {
oled.setCursor(14, 1);
oled.print(countUp[3], DEC);
oled.print(' ');
oled.print(keyHold);
}
}
// --------------------------- Control ------------------------------
int getButtons(void) {
static int threshold[] = {885, 649, 410, 180, 0};
int arrowsVal = 0; // variable to store the value read for the Arrow pins
int centerVal = 0; // variable to store the value read for the Center pin
int arrowPins = A0;
int centerPin = A1;
int pinDir = 0;
centerVal = digitalRead(centerPin) ^ 1;
arrowsVal = analogRead(arrowPins);
while (threshold[pinDir] > arrowsVal) {
pinDir++;
}
return ((16 * centerVal) + pinDir);
}
byte BGLimit = 30;
byte OWLimit = 30;
const byte powerLimit = 60;
byte manualCommand(byte buttonId) {
char sendCode = ' ';
switch (buttonId) {
case 1: //Up button
sendCode = 'm';
break;
case 2: //right button
sendCode = 'n';
break;
case 3: //down button
sendCode = 'o';
break;
}
if ((sendCode != ' ') && (cmdChar == ' ')) { // Inhibit until respone received
picaxeLink.print(sendCode);
cmdChar = sendCode; // Display command code sent
}
while (getButtons() != 0) {
delay(10);;
}
return sendCode;
}
byte autoCommand(void) {
char sendCode = ' ';
if (countUp[0] > BGLimit) {
countUp[0] = 0;
sendCode = 'm';
}
if (countUp[1] > OWLimit) {
countUp[1] = 0;
sendCode = 'o';
}
if (countUp[2] > powerLimit) {
countUp[2] = 0;
sendCode = 'n';
}
if (sendCode != ' ') {
picaxeLink.print(sendCode);
}
while (getButtons() != 0) {
delay(10);;
}
return sendCode;
}
// --------------------------- Logging ------------------------------
void record() {
char dir = SDBuf[0];
uint8_t prefixIndex;
char fBuf[19];
if ((dir < 'n') || (dir > 'z')) {
prefixIndex = 2;
}
else {
prefixIndex = dir - 'n';
}
digitalWrite(4, HIGH);
DateTime now = rtc.readTime();
// The previous code stopped open() after 10/2024
sprintf(fBuf, "%02u/%02u/%s%02u.CSV", (now.year() % 1000), now.month(), msgName[prefixIndex],now.day());
File dataFile = SD.open(fBuf, FILE_WRITE);
if (dataFile) {
dataFile.print(now.hour());
dataFile.print(':');
dataFile.print(now.minute());
dataFile.print(':');
dataFile.print(now.second());
dataFile.print(',');
for (int i = 0; i < SDBufSize; i++) {
if ((SDBuf[i] > 42) && (SDBuf[i] < 127)) {
dataFile.print(SDBuf[i]);
}
SDBuf[i] = 0;
}
dataFile.write(13);
dataFile.write(10);
dataFile.close();
}
else {
oled.clear();
oled.setCursor(0, 0);
oled.print("Open file failure:");
oled.setCursor(0, 2);
oled.print(filename);
while (picaxeLink.available() > 0) {
fBuf[0] = picaxeLink.read();
}
}
delay(200);
}
// --------------------------- Setting ------------------------------
const char *adjHdgs[] = {"Hours = ",
"Minutes = ",
"Green/Blue = ",
"Orange/White = "
};
byte adjVals[4], oldVals[4];
void showAdjStrings(void) {
oled.clear();
for (int row = 0; row < 4; row++) {
oled.setCursor(0, row);
oled.print(adjHdgs[row]);
}
adjVals[0] = rtc.rtcReadReg(PCF8523_HOURS);
adjVals[1] = rtc.rtcReadReg(PCF8523_MINUTES);
adjVals[2] = BGLimit;
adjVals[3] = OWLimit;
for (int i = 0; i < 4; i++) {
oldVals[i] = adjVals[i];
}
}
void adjustments(byte buttonId) {
unsigned int intRate;
static byte adjIndex = 3;
if (buttonId == 2) {
adjIndex++;
if (adjIndex > 3) {
adjIndex = 0;
}
}
if (buttonId == 4) {
if (adjIndex > 1) {
adjIndex--;
}
else {
adjIndex = 3;
}
}
for (int row = 0; row < 4; row++) {
oled.setCursor(17, row);
if (row > 1) {
intRate = 10 * adjVals[row];
oled.print(intRate, DEC);
}
else {
oled.print(adjVals[row], DEC);
}
oled.setCursor(15, row);
if (row == adjIndex) {
oled.print('*');
if (buttonId == 1) {
adjVals[row]++;
}
if ((buttonId == 3) && (adjVals[row] > 1)) {
adjVals[row]--;
}
}
else {
oled.print(' ');
}
}
}
void adjChange(void) {
if (adjVals[0] != oldVals[0]) {
rtc.rtcWriteReg(PCF8523_HOURS, adjVals[0]);
}
if (adjVals[1] != oldVals[1]) {
rtc.rtcWriteReg(PCF8523_MINUTES, adjVals[1]);
}
if (adjVals[2] != oldVals[2]) {
BGLimit = adjVals[2] ;
EEPROM.write(0, BGLimit);
}
if (adjVals[3] != oldVals[3]) {
OWLimit = adjVals[3] ;
EEPROM.write(1, OWLimit);
}
}