/* OLEDLogger,1/20/2025 pairs with OLEDLogger.bas 12/6/2021 John Saunders
Version of OLEDLoggerSS with sprintf() for filename.
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);
SDClass MySD;
File dataFile;
const byte rxPin = 9;
const byte txPin = 6;
SoftwareSerial picaxeLink(rxPin, txPin);
const int chipSelect = 10; // MySD
unsigned int countUp[4] = {0, 0, 0, 0}; // for the measurements and ageing
byte dispMode = 0;
char keyHold;
char cmdChar;
// --------------------------- Setup ------------------------------
void setup() {
char SDId[2];
pinMode(rs, OUTPUT);
pinMode(en, OUTPUT);
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 ");
}
else {
oled.print(" Pixel Link Not OK! ");
}
MySD.begin(chipSelect);
dataFile = MySD.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);
}
dataFile.close();
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", " "
};
uint8_t ohmSym[8] = {
B01110,
B10001,
B10001,
B10001,
B01010,
B01010,
B11011,
};
uint8_t degSym[8] = {
B11100,
B10100,
B11100,
B00000,
B00000,
B00000,
B00000,
};
byte currMsg[SDBufSize];
// n o p q r s t u v w x y z
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';
}
DateTime now = rtc.readTime();
sprintf(fBuf, "%02u/%02u/%s%02u.CSV", (now.year() % 1000), now.month(), msgName[prefixIndex],now.day());
File dataFile = MySD.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, 1);
oled.print(fBuf);
delay(3000);
}
while (picaxeLink.available() > 0) {
fBuf[0] = picaxeLink.read();
}
delay(200);
}
// --------------------------- Setting ------------------------------
const char *adjHdgs[] = {"Hours = ",
"Minutes = ",
"Green/Blue = ",
"Orange/White = "
};
uint8_t adjVals[4], oldVals[4];
void showAdjStrings(void) {
DateTime now = rtc.readTime();
oled.clear();
for (int row = 0; row < 4; row++) {
oled.setCursor(0, row);
oled.print(adjHdgs[row]);
}
adjVals[0] = now.hour();
oldVals[0] = adjVals[0];
adjVals[1] = now.minute();
oldVals[1] = adjVals[1];
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) {
DateTime now = rtc.readTime();
if (adjVals[0] != oldVals[0]) {
rtc.setTime(DateTime(now.year(), now.month(), now.day(), adjVals[0], now.minute(), now.second()));
}
if (adjVals[1] != oldVals[1]) {
rtc.setTime(DateTime(now.year(), now.month(), now.day(), now.hour(), adjVals[1], now.second()));
}
if (adjVals[2] != oldVals[2]) {
BGLimit = adjVals[2] ;
EEPROM.write(0, BGLimit);
}
if (adjVals[3] != oldVals[3]) {
OWLimit = adjVals[3] ;
EEPROM.write(1, OWLimit);
}
}
byte newModeFlag;