/*
GasSensor.ino is used to display and transmi
conentration of CO,CH4 and LPG.
It generates visual and audible alarms if
excessive concentrations are detected.
It includes the following items:
Spark Fum Arduino Pro Mini microcontroller.
LCD display with parallel interface (4-bit)
DS3231 Real-time clock with I2C interface.
MQ-9 gas sensor with switched heater voltages.
TMP036 temperature sensor.
LM385-2.5 reference voltage diode.
Backlight control from CDS photocell
with quasi-log compressor
which includes LM385-2.5 & LM385-1.2 diodes.
LM317 voltage regulator using a resistor pack.
PCF8574 8-port I2C expander connected to a r-g-b LED
and Adafruit FX OGG Audio Codec
LM 2040 Audio power amplifier.
FS1000A 433MHZ RF transmitter module,
requires adapter circuit fot Arduino.
Push buttons with analog encoder using a resistor pack.
RTC Hour and Minute adjustments
John Saunders 12/4/2021, 6/4/2022 leading zero restored
*/
#include <LiquidCrystal.h>
#include <SoftwareSerial.h> //For the transmitter
#include <ds3231.h> //RTC
#include <Wire.h> //For the RTC and the port expander
#include "jm_PCF8574.h" //Port expander
#define PCF_ADDR 0x27
jm_PCF8574 pcf(PCF_ADDR);
#define INIT_TIMEIN 10 //Into Date/Time setting page
#define INIT_TIMEOUT 10 //Exit from Date/Time setting page
// pins
const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
const int pc = A6, gas = A3, temp = A2, ref = A1, but = A0;
const int bl = 10, htr = 13, txDr = 6, txPulse = 7, Mute = 8;
// measured values
const float refVoltsC = 2472; // for degree C , mv, measured ref voltage
const float refVoltsF = 4445.6; // for degree F, mv
const float Rl = 8660.0; // Load resistor measured value
const float Ro = 1777.0; // At end of 2 days cycling the
// gas sensor heater using 1.5V value to get 10.
// The measured value has been divided by 10 according to online instructions
// *************** Adjust RTC minutes and Hours *************************
ts t; //ts is a struct findable in ds3231.h
void setup() {
pinMode(htr, OUTPUT);
digitalWrite(htr, HIGH); //5V
pinMode(txDr, OUTPUT);
digitalWrite(txDr, HIGH);
pinMode(txPulse, OUTPUT);
pinMode(Mute,INPUT_PULLUP);
digitalWrite(txPulse, LOW);
lcd.begin(20, 2);
Wire.begin(); //start i2c (required for connection to the clock chip)
DS3231_init(DS3231_INTCN);
serial1.begin(2400);
delay(400);
lcd.createChar(0, degSym);
pcf.begin(); // returns false if not connected
for (int i = 0; i < 8; i ++) {
pcf.pinMode(i, OUTPUT);
}
for (int i = 0; i < 3; i ++) {
pcf.digitalWrite(i, 1);
}
pcf.digitalWrite(3, 0);
for (int i = 4; i < 8; i ++) {
pcf.digitalWrite(i, 1);
}
pcf.digitalWrite(2, 0);
DS3231_get(&t);
// t.wday = 0;
// DS3231_set(t);
}
void loop() {
int tempValF, gasVal, pcVal, intensity;
// int tempValC; // Only the F value is transmitted
static int secs = 0;
static int oldSec = 1;
static int timeIn = INIT_TIMEIN;
static int timeOut = INIT_TIMEOUT;
static int page = 0;
static boolean tempC = false;
float tempDegF, tempDegC;
float hiGasRatio ;
float scratch;
static float loGasRatio = 10.0;
byte butCode;
byte nextLoc;
char alarmLevel;
butCode = getButID(); // Buttons
DS3231_get(&t); // Current time & date
if (oldSec != t.sec) { // Items run at 1 Hz
oldSec = t.sec;
if (timeIn > 0) { // Adjustment page in and out delay
timeIn--;
}
if (timeOut > 0) {
timeOut--;
}
tempDegF = getTemp('F'); // Temperature measurement
tempValF = (int)tempDegF;
tempDegC = getTemp('C');
// tempValC = (int)tempDegC; Only the F value is transmitted
pcVal = analogRead(pc); // Light measurement
intensity = 375 - (5 * pcVal / 8);
if (intensity < 0) { // PWM generation for thedisplay backlight
analogWrite(bl, 0);
}
else {
if (intensity > 250) { // Upper limit not reached
analogWrite(bl, 250);
}
else {
analogWrite(bl, intensity);
}
}
// Operation is cyclic at 150 seconds period
alarmLevel = (char)(execAlarm(loGasRatio) + 'A');
switch (secs) {
case 1:
digitalWrite(htr, LOW); // 5V
break;
case 59:
hiGasRatio = getRsDivRo();
case 60:
digitalWrite(htr, HIGH); // 1.5V
break;
case 75:
nextLoc = storeVal(tempValF, 11, 1);
pcVal = analogRead(pc);
nextLoc = storeVal(pcVal, nextLoc, 0);
transmit('z', nextLoc);
break;
case 149:
loGasRatio = getRsDivRo();
break;
case 150:
secs = 0;
scratch = 1000 * hiGasRatio;
gasVal = (unsigned int)(scratch);
nextLoc = storeVal(gasVal, 13, 3);
scratch = 1000 * loGasRatio;
gasVal = (unsigned int)(scratch);
nextLoc = storeVal(gasVal, nextLoc, 3);
charBuffer[11] = alarmLevel;
charBuffer[12] = ',';
transmit('n', nextLoc);
break;
}
secs++;
}
// Page view control
switch (page) {
case 0: // Time&Date, Gas Sensor Rs/Ro
lcd.clear();
if (butCode == 3) {
page = 1;
}
if (butCode == 1) {
page = 2;
timeIn = INIT_TIMEIN;
}
lcd.setCursor(0, 0);
displayTimeDate();
lcd.setCursor(0, 1);
lcd.print("Low=");
lcd.print(loGasRatio, 3);
lcd.print(" High=");
lcd.print(hiGasRatio, 3);
break;
case 1: //Temperature and Light-Sec
if (butCode == 1) {
page = 2;
timeIn = INIT_TIMEIN;
}
if (butCode == 2) {
tempC = true;
}
if (butCode == 3) {
tempC = false;
}
if (butCode == 4) {
page = 0;
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" temp=");
if (tempC == false) {
lcd.print((tempDegF / 10), 1);
lcd.write(byte(0));
lcd.print('F');
}
else {
lcd.print((tempDegC / 10), 1);
lcd.write(byte(0));
lcd.print('C');
}
lcd.setCursor(0, 1);
lcd.print("Light=");
lcd.print(pcVal, DEC);
lcd.print(" secs=");
lcd.print(secs, DEC);
break;
case 2:
if (butCode == 3) {
page = 1;
}
if (butCode == 4) {
page = 0;
}
if ((butCode == 0) && (timeIn == 0)) {
page = 3;
timeOut = INIT_TIMEOUT;
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Settings Countdown");
lcd.setCursor(0, 1);
lcd.print(" Abort ");
lcd.print(timeIn, DEC);
break;
case 3: // Minute and hour adjustments
if ((butCode == 0) && (timeOut == 0)) {
page = 0;
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Hour=");
lcd.print(t.hour, DEC);
lcd.print(" Min=");
lcd.print(t.min, DEC);
lcd.setCursor(0, 1);
lcd.print("Up-Down Up-Down ");
lcd.print(timeOut);
if (butCode > 0) {
adjustTD(butCode);
timeOut = INIT_TIMEOUT;
}
break;
}
delay(100);
}
void adjustTD(int code) {
switch (code) {
case 4:
if (t.hour < 23) {
t.hour++;
}
else {
t.hour = 0;
}
break;
case 3:
if (t.hour > 0) {
t.hour--;
}
else {
t.hour = 23;
}
break;
case 2:
if (t.min < 59) {
t.min++;
}
else {
t.min = 0;
}
break;
case 1:
if (t.min > 0) {
t.min--;
}
else {
t.min = 59;
}
break;
}
DS3231_set(t);
while (getButID() > 0) {
delay(1);
}
}
// ********************** Displaying *****************************
const char weekdays[25] = {"SUNMONTUEWEDTHUFRISATSUN"}; // 0(SUN) through 6(SAT,SUN is also 7)
void displayTimeDate(void) {
lcd.print(t.hour);
lcd.print(":");
lcd.print(t.min);
lcd.print(":");
lcd.print(t.sec);
lcd.print(" ");
displayWeekday();
lcd.print(" ");
displayMonth();
lcd.print(" ");
lcd.print(t.mday);
}
void displayWeekday(void) {
char c;
byte indx;
for (byte i = 0; i < 3; i++) {
indx = (3 * (t.wday)) + i;
c = weekdays[indx];
lcd.print(c);
}
}
const char months[40] = {"DECJANFEBMARAPRMAYJUNJLYAUGSEPOCTNOVDEC"}; // 1 - 12
void displayMonth(void) {
char c;
byte indx;
for (byte i = 0; i < 3; i++) {
indx = (3 * t.mon) + i;
c = months[indx];
lcd.print(c);
}
}
byte degSym[8] = {
B11100,
B10100,
B11100,
B00000,
B00000,
B00000,
B00000,
};
// ******************* Hardware and Sensors *****************************
byte getButID(void) {
int butVal;
int testVal = 127;
byte code = 0;
butVal = analogRead(but);
while (butVal > testVal) {
code++;
testVal += 255;
}
return code;
}
int getTemp(char mode) {
float deg;
int refCt = analogRead(ref);
int tempCt = analogRead(temp);
if (mode == 'C') {
deg = ((refVoltsC * tempCt) / refCt) - 500;
}
else {
deg = ((refVoltsF * tempCt) / refCt) - 480;
}
return (int)deg;
}
float getRsDivRo(void) { // returns rs/ro
int gasVal;
float gasRatio;
gasVal = analogRead(gas);
gasRatio = (Rl * ((1024.0 / (float)gasVal) - 1.0)) / Ro;
return gasRatio;
}
/********************** Alarms ******************
*
Hardware Mapping
PCF8571 Code 0 1 2 3 4 5 6 7
PCF8571 Active level 0 0 0 1 0 0 0 0
PCF8571 Pin 4 5 6 16 11 12 13 14
LED Color greeen blue red
FX OGG File nn T02 01 04 03 00.ogg
*/
typedef struct {
float testLevel; // Rs/Ro value to compare the measurement against
byte colorLED; // 0-2 if on
byte flash; // LED flashes if 1
byte portID; // port 3-7 if on
byte period; // continues at this rate
} levels_t;
// This is the heart of this project.
// The FX board is mountable in OSX, files need renaming to T(##),ogg
const levels_t levelList[7] = {
// > 2.3 1.8-2.5 1.5-1.8
{2.5, 3, 1, 0, 0}, {2.5, 0, 1, 0, 0}, {1.8, 0, 0, 3, 4},
// 1.1-1.5 0.8-1.1 0.6-0.8 0.4-0.6
{ 1.5, 1, 1, 7, 3}, {1.1, 1, 0, 6, 9}, {0.8, 2, 1, 4, 4}, {0.6, 2, 0, 5, 9}
};
const byte colorAction[12] = {0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1};
byte execAlarm(float level) {
int index = 0;
byte portC, portF;
static int ct = 0;
byte blinkLED;
while (level < levelList[index].testLevel) {
index++;
}
blinkLED = levelList[index].flash & (ct & 1);
// Operates the rgb LED colorLED
portC = levelList[index].colorLED;
if (portC > 2) {
portC = 3;
}
pcf.digitalWrite(0, colorAction[portC] | blinkLED);
pcf.digitalWrite(1, colorAction[portC + 4] | blinkLED);
pcf.digitalWrite(2, colorAction[portC + 8] | blinkLED);
// Operates the sound codec
if (ct < levelList[index].period) {
ct++;
}
else { // Makes sounds
ct = 0;
portF = levelList[index].portID;
if ((portF > 2) && (portF < 8) && (digitalRead(Mute) != 0)) {
pcf.digitalWrite(portF, (portF == 3)); // port 3 = pin 16; is inverted
delay(125);
pcf.digitalWrite(portF, (portF != 3));
}
}
return index;
}
char charBuffer[47] = {"14L1776n,F,?, "};
// ************************** Storing ****************
byte storeVal(unsigned int txData, int loc, int dp) {
int rem = txData / 10000;
// if (rem > 0) {
charBuffer[loc++] = rem + '0';
// }
rem = txData % 10000;
charBuffer[loc++] = rem / 1000 + '0';
if (dp == 3) {
charBuffer[loc++] = '.';
}
txData = rem % 1000;
charBuffer[loc++] = txData / 100 + '0';
if (dp == 2) {
charBuffer[loc++] = '.';
}
rem = txData % 100;
charBuffer[loc++] = rem / 10 + '0';
if (dp == 1) {
charBuffer[loc++] = '.';
}
charBuffer[loc++] = rem % 10 + '0';
charBuffer[loc++] = ',';
return loc;
}
// ********************* Transmitting ****************
SoftwareSerial serial1 = SoftwareSerial(8, txDr); // The 8 is jjust a dummy
void transmit(char key, byte len) { // Points to the first checksum character
byte cksum = 0;
byte ckhex;
charBuffer[7] = key;
charBuffer[9] = '<' + len - 12;
for (int i = 11; i < (len - 1); i++) {
cksum += charBuffer[i];
}
ckhex = cksum / 16;
if (ckhex < 10) {
charBuffer[len++] = ckhex + '0';
}
else {
charBuffer[len++] = ckhex + '7';
}
ckhex = cksum & 0x0F;
if (ckhex < 10) {
charBuffer[len++] = ckhex + '0';
}
else {
charBuffer[len++] = ckhex + '7';
}
charBuffer[len++] = 0x0d;
charBuffer[len++] = 0x0a;
charBuffer[len++] = 0;
digitalWrite(txPulse, HIGH);
delay(20);
digitalWrite(txPulse, LOW);
delay(10);
serial1.write(charBuffer, len);
}