/* MkI 4 Channel Controller - Version 6 rev1.1
Developed January 2025 -
Based on Code Written by Malcolm Crabbe & Brian Helterline
Uses upto 4 x DS18B20 temp sensors
DS1307 RTC
Arduino Uno
ILI9341 colour TFT screen
Build Dates 13-1-25 to
Version B - updated Serial processing to skip other updates and transmit() wraps data in '<' and '>'
Version C - added auto-reset of EEPROM when blank (65535), and full screen clear each frame
*/
#include "SparkFunDS1307RTC.h"
#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#include <DS1307RTC.h>
#include <OneWire.h>
#include <Wire.h>
#include <DallasTemperature.h>
#include <EEPROM.h>
#include <TimeLib.h>
// TFT connections
#define TFT_DC 9
#define TFT_RST 8
#define TFT_CS 10
#define TFT_MOSI 11
#define TFT_CLK 13
#define TFT_MISO 12
// Use hardware SPI (on Uno, #13, #12, #11)
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST, TFT_MISO);
#define BLACK 0x0000
#define NAVY 0x000F
#define DARKGREEN 0x03E0
#define DARKCYAN 0x03EF
#define MAROON 0x7800
#define PURPLE 0x780F
#define OLIVE 0x7BE0
#define LIGHTGREY 0xC618
#define DARKGREY 0x7BEF
#define BLUE 0x001F
#define GREEN 0x07E0
#define CYAN 0x07FF
#define RED 0xF800
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#define WHITE 0xFFFF
#define ORANGE 0xFD20
#define GREENYELLOW 0xAFE5
#define PINK 0xF81F
//-----------------------------------------------------------------
// Number of vivariums required
const byte VIVARIUMS = 4; // // Default 4 channels (varible defines how many are in use)
// all temperatures are in tenths of degrees (e.g. 243 = 24.3)
// so we can use integer math
// structure to hold all the configuration information for a vivarium
// this information is stored in EEPROM
typedef struct {
unsigned int dayTemp;
unsigned int nightTemp;
unsigned int alarmLowTemp;
unsigned int alarmHighTemp;
bool alarmEnabled;
bool vivEnabled;
unsigned int dayTime; // minutes from midnight
unsigned int nightTime; // minutes from midnight
} VivariumConfig;
VivariumConfig config[VIVARIUMS]; // create the array of configs
// structure to hold all the control information for a vivarium
// this is dynamic information for heat control
enum states { OKAY_STATE, ERROR_STATE, ALARM_STATE };
typedef struct {
unsigned int setPoint; // either dayTemp or nightTemp depending on time
unsigned int temperature; // current temperature
unsigned long pulseTime; // time to leave heater relay ON (msec)
bool fullOn; // true when way below temperature and heater is always on
enum states state; // current state of vivarium
OneWire wire; // onewire bus for this sensor
DallasTemperature sensor; // sensor
DeviceAddress addr; // address of sensor on the bus
byte retries; // number of retries attempted when sensor reads faulty
} VivariumCotrol;
VivariumCotrol vivarium[VIVARIUMS]; // create the array of vivariums
const byte maxRetries = 5; // number of temperature read attempts until error
unsigned long sensorDelay; // amount of time to wait for sensors to do temperature conversion
// structure to hold all the control information for a light
typedef struct {
unsigned int onTime; // minutes from midnight
unsigned int offTime; // minutes from midnight
} LightConfig;
const byte LIGHTS = 1;
LightConfig light[LIGHTS];
//--------------------------- Hardware ---------------------------
// Pin Dassignments (Uno)
const byte outputPin[VIVARIUMS] = { 4, 5, 6, 7 };
const byte sensorPin[VIVARIUMS] = { 2, 3, 16, 17 }; // Confirmed DS18B20 works fine on these pins
const byte alarmPin = 14; // Connected to Piezo sounder for alarm
const byte lightPin[LIGHTS] = {15};
//--------------------------- Serial buffers ---------------------------
const byte numChars = 101; // the maximum number of characters we can receive in a single message
char receivedChars[numChars]; // the array of incoming bytes
boolean newData = false; // true when new, complete message received
//-----------------------------------------------------------------
void setup () {
for ( int i = 0; i < VIVARIUMS; ++i ) {
pinMode(outputPin[i], OUTPUT );
}
pinMode(alarmPin, OUTPUT); // sets the digital pin as output
for ( int i = 0; i < LIGHTS; ++i ) {
pinMode(lightPin[i], OUTPUT );
}
rtc.begin();
//-----------------------------------------------------------------
Serial.begin (9600); // Default serial port set at 9600 baud
loadConfig(); // read in configuration from EEPROM
sensorInit();
DSread();
tft.begin();
tft.setCursor(0, 0);
tft.fillScreen(ILI9341_BLACK); // blank the screen
uint8_t rotation = 1;
tft.setRotation(rotation); // set TFT to landscape
tft.setTextSize(2);
about();
}
//======================================================================
// Main Program Loop
//======================================================================
void loop () {
timeit("start of loop");
rtc.update();
checkTime(); // check for switch from day->night or night->day
// check to see if we have any Serial input
if (Serial.available() > 0) {
// we have a command, so act on it
char cmd = Serial.read();
switch ( cmd ) {
case 'S': // send all data to PC
transmit();
break;
case 'R': // receive all data from PC
readConfig();
break;
/*
The follwoing is used for testing
*/
case 'T': // test the sensors
DSread();
break;
case 'D': // display temperature values
showTemperatures();
break;
case 'C': // display all Config values
displayConfig();
break;
case 'X': // reset all config
resetConfig();
break;
case 'A': // display build and file name
about();
break;
}
return; // if we have any Serial input, restar
}
DSread(); // read sensors
timeit("post read");
updateStates();
Screen(); // update display
timeit("post screen");
controlHeat(); // do any heating required
}
//======================================================================
// Subroutines
//======================================================================
void sensorInit() {
const byte bitResolution = 11;
for ( int i = 0; i < VIVARIUMS; ++i ) {
if ( config[i].vivEnabled == true ) {
// only configure enabled vivariums
vivarium[i].wire.begin((sensorPin[i]));
vivarium[i].sensor.setOneWire(&(vivarium[i].wire));
vivarium[i].sensor.begin();
vivarium[i].sensor.getAddress(vivarium[i].addr, 0);
vivarium[i].sensor.setResolution(vivarium[i].addr, bitResolution);
vivarium[i].sensor.setWaitForConversion(false);
vivarium[i].sensor.requestTemperaturesByAddress(vivarium[i].addr);
vivarium[i].retries = 0;
sensorDelay = vivarium[i].sensor.millisToWaitForConversion(bitResolution);
}
}
delay(sensorDelay); // make sure all sensors have finished temperature conversion
}
//--------- Read DS18B20 Sensors ----------
void DSread() {
static unsigned long lastTempRequest = 0;
if (millis() - lastTempRequest >= sensorDelay) {
// time to read the temperatures
for ( int i = 0; i < VIVARIUMS; ++i ) {
unsigned int vivTemperature = 0;
if ( config[i].vivEnabled == true ) {
// only read enabled vivariums
float temperature = vivarium[i].sensor.getTempC(vivarium[i].addr);
if ( temperature < 0.0 ) {
// sensor out of range so ignore unless too many retries
if ( vivarium[i].retries++ >= maxRetries ) {
vivarium[i].retries--; // keep at max
vivTemperature = 0;
}
} else {
vivTemperature = int(temperature * 10.0 + 0.5); // round to nearest tenth
vivarium[i].retries = 0;
}
vivarium[i].sensor.requestTemperaturesByAddress(vivarium[i].addr); // start a new temperature request
}
vivarium[i].temperature = vivTemperature;
}
lastTempRequest = millis(); // reset timer
}
}
//--------- Display Sensor Tempertaures ----------
void showTemperatures() {
for ( int i = 0; i < VIVARIUMS; ++i ) {
Serial.print( "temperature " );
Serial.print( i + 1 ); // make output 1..8 not 0..7
Serial.print( " is " );
Serial.print( vivarium[i].temperature / 10.0, 1 ); // print as degrees to one decimal point
Serial.println();
}
}
//---------Check the state Ok or in error or alarm ----------
void updateStates() {
// check all the enabled vivariums and determine if
// they are in an error state
// or an alarm state
// or okay
for ( int i = 0; i < VIVARIUMS; ++i ) {
if (config[i].vivEnabled == true) {
// assume we are good
vivarium[i].state = OKAY_STATE;
if ( vivarium[i].temperature == 0 ) {
// error state
vivarium[i].state = ERROR_STATE;
}
else if ( config[i].alarmEnabled == true &&
((vivarium[i].temperature >= config[i].alarmHighTemp)
|| (vivarium[i].temperature <= config[i].alarmLowTemp))) {
// alarm state
vivarium[i].state = ALARM_STATE;
}
}
}
}
//--------- Process serial data received ----------
void recvWithStartEndMarkers() {
static boolean recvInProgress = false;
static byte ndx = 0;
char startMarker = '<';
char endMarker = '>';
char rc;
while (Serial.available() > 0 && newData == false) {
rc = Serial.read();
if (recvInProgress == true) {
if (rc != endMarker) {
receivedChars[ndx] = rc;
ndx++;
if (ndx >= numChars) {
ndx = numChars - 1;
}
}
else {
receivedChars[ndx] = '\0'; // terminate the string
recvInProgress = false;
ndx = 0;
newData = true;
}
}
else if (rc == startMarker) {
recvInProgress = true;
}
}
}
//--------- build data to send to PC application ----------
void sendNumber( int val ) {
// always send as 3 digits
if ( val < 100 ) Serial.print( "0" );
if ( val < 10 ) Serial.print( "0" );
Serial.print(val);
}
void sendTime( int val ) {
// always send time as 4 digits (HHMM)
int h = val / 60; // convert to hours only
int m = val % 60; // convert to minutes
if ( h < 10 ) Serial.print( "0" );
Serial.print(h);
if ( m < 10 ) Serial.print( "0" );
Serial.print(m);
}
//--------- Send data to PC ----------
void transmit() {
int val;
Serial.print('<'); // send start marker
// send current temperatures
for (int i = 0; i < VIVARIUMS; ++i) {
val = vivarium[i].temperature;
if (config[i].vivEnabled == false ) {
// vivarium is disabled so send 0
val = 0;
}
sendNumber(val);
}
// send daytime setPoint temperature values
for (int i = 0; i < VIVARIUMS; ++i) {
val = config[i].dayTemp;
if (config[i].vivEnabled == false ) {
// vivarium is disabled so send 0
val = 0;
}
sendNumber(val);
}
// send alarmLow values
for (int i = 0; i < VIVARIUMS; ++i) {
val = config[i].alarmLowTemp;
if (config[i].vivEnabled == false ) {
// vivarium is disabled so send 0
val = 0;
}
sendNumber(val);
}
// send alarmHigh values
for (int i = 0; i < VIVARIUMS; ++i) {
val = config[i].alarmHighTemp;
if (config[i].vivEnabled == false ) {
// vivarium is disabled so send 0
val = 0;
}
sendNumber(val);
}
// send night time
for (int i = 0; i < VIVARIUMS; ++i) {
val = config[i].nightTime;
if (config[i].vivEnabled == false ) {
// vivarium is disabled so send 0
val = 0;
}
sendTime(val);
}
// send day time
for (int i = 0; i < VIVARIUMS; ++i) {
val = config[i].dayTime;
if (config[i].vivEnabled == false ) {
// vivarium is disabled so send 0
val = 0;
}
sendTime(val);
}
// send nighttime setPoint temperature values
for (int i = 0; i < VIVARIUMS; ++i) {
val = config[i].nightTemp;
if (config[i].vivEnabled == false ) {
// vivarium is disabled so send 0
val = 0;
}
sendNumber(val);
}
// send lights on time
for (int i = 0; i < LIGHTS; ++i) {
val = light[i].onTime;
sendTime(val);
}
// send lights off time
for (int i = 0; i < LIGHTS; ++i) {
val = light[i].offTime;
sendTime(val);
}
Serial.print(VIVARIUMS);
Serial.print('>'); // send end marker
}
//--------- Read data from PC application ----------
void readConfig() {
//communications between PC application and Mega
// all values are enclosed in '<' and '>'
// order:
// dayTemp x 8
// alarmLowTemp x 8
// alarmHighTemp x 8
// nightTime <HH><MM> x 8
// dayTime <HH><MM> x 8
// nightTemp x 8
// lights onTime <HH><MM> x 2
// lights offTime <HH><MM> x 2
// current time <HH><MM><SS> x 2
// numer of vivariums
/*
When we get here, the R command has already been
received. There is a '#' preceeding the first <number>
but recvWithStartEndMarkers() will skip it
*/
// day temperatures
for ( int i = 0; i < VIVARIUMS; ++i ) {
while ( newData == false ) {
recvWithStartEndMarkers();
}
config[i].dayTemp = atoi(receivedChars);
newData = false;
}
// alarm low temperatures
for ( int i = 0; i < VIVARIUMS; ++i ) {
while ( newData == false ) {
recvWithStartEndMarkers();
}
config[i].alarmLowTemp = atoi(receivedChars);
newData = false;
}
// alarm high temperatures
for ( int i = 0; i < VIVARIUMS; ++i ) {
while ( newData == false ) {
recvWithStartEndMarkers();
}
config[i].alarmHighTemp = atoi(receivedChars);
newData = false;
}
// night time <HH><MM>
for ( int i = 0; i < VIVARIUMS; ++i ) {
while ( newData == false ) {
recvWithStartEndMarkers();
}
config[i].nightTime = atoi(receivedChars) * 60; // hours converted to minutes
newData = false;
while ( newData == false ) {
recvWithStartEndMarkers();
}
config[i].nightTime += atoi(receivedChars); // add it the minutes
newData = false;
}
// day time <HH><MM>
for ( int i = 0; i < VIVARIUMS; ++i ) {
while ( newData == false ) {
recvWithStartEndMarkers();
}
config[i].dayTime = atoi(receivedChars) * 60; // hours converted to minutes
newData = false;
while ( newData == false ) {
recvWithStartEndMarkers();
}
config[i].dayTime += atoi(receivedChars); // add it the minutes
newData = false;
}
// night temperatures
for ( int i = 0; i < VIVARIUMS; ++i ) {
while ( newData == false ) {
recvWithStartEndMarkers();
}
config[i].nightTemp = atoi(receivedChars);
newData = false;
}
// light on time <HH><MM>
for ( int i = 0; i < LIGHTS; ++i ) {
while ( newData == false ) {
recvWithStartEndMarkers();
}
light[i].onTime = atoi(receivedChars) * 60; // hours converted to minutes
newData = false;
while ( newData == false ) {
recvWithStartEndMarkers();
}
light[i].onTime += atoi(receivedChars); // add it the minutes
newData = false;
}
// light off time <HH><MM>
for ( int i = 0; i < LIGHTS; ++i ) {
while ( newData == false ) {
recvWithStartEndMarkers();
}
light[i].offTime = atoi(receivedChars) * 60; // hours converted to minutes
newData = false;
while ( newData == false ) {
recvWithStartEndMarkers();
}
light[i].offTime += atoi(receivedChars); // add it the minutes
newData = false;
}
// current time <HH><MM><SS>
int hour, minute, second;
while ( newData == false ) {
recvWithStartEndMarkers();
}
hour = atoi(receivedChars);
newData = false;
while ( newData == false ) {
recvWithStartEndMarkers();
}
minute = atoi(receivedChars);
newData = false;
while ( newData == false ) {
recvWithStartEndMarkers();
}
second = atoi(receivedChars);
newData = false;
// number of vivariums
while ( newData == false ) {
recvWithStartEndMarkers();
}
// just ignore this
newData = false;
// all the data is now in so process it
rtc.setTime(second, minute, hour, 5, 22, 2, 18); // adjust RTC time to match PC
saveConfig();
}
//--------- Save current values to EEPROM ----------
void saveConfig() {
// write all the current configuration data into EEPROM
int EEAddr = 0;
for ( int i = 0; i < VIVARIUMS; ++i ) {
// do a bit of sanity checking
// if dayTemp is 0, vivarium is disabled
// if alarmLowTemp is 0, alarms are disabled
config[i].vivEnabled = true; // assume it is enabled
if ( config[i].dayTemp == 0 ) {
config[i].vivEnabled = false;
}
config[i].alarmEnabled = true;
if ( config[i].alarmLowTemp == 0 ) {
config[i].alarmEnabled = false;
}
EEPROM.put( EEAddr, config[i] );
EEAddr += sizeof(VivariumConfig);
}
for ( int i = 0; i < LIGHTS; ++i ) {
EEPROM.put( EEAddr, light[i] );
EEAddr += sizeof(LightConfig);
}
}
//--------- Read current values from EEPROM ----------
void loadConfig() {
// load all the current configuration data from EEPROM
int EEAddr = 0;
for ( int i = 0; i < VIVARIUMS; ++i ) {
EEPROM.get( EEAddr, config[i] );
EEAddr += sizeof(VivariumConfig);
}
for ( int i = 0; i < LIGHTS; ++i ) {
EEPROM.get( EEAddr, light[i] );
EEAddr += sizeof(LightConfig);
}
// Wykryj pustą/nieskonfigurowaną pamięć EEPROM (typowe przy świeżym
// starcie symulacji Wokwi, gdzie EEPROM zawsze startuje jako 0xFFFF)
// i automatycznie ustaw wartości domyślne, bez potrzeby wysyłania 'X' ręcznie.
if ( config[0].dayTemp == 65535 || config[0].alarmLowTemp == 65535 ) {
resetConfig();
}
}
//--------- Reset to defaults ----------
void resetConfig() {
// reset all the configurations
config[0].vivEnabled = true;
config[0].dayTemp = 320;
config[0].nightTemp = 300;
config[0].alarmLowTemp = 150;
config[0].alarmHighTemp = 420;
config[0].alarmEnabled = false;
config[0].dayTime = 8 * 60;
config[0].nightTime = 22 * 60;
light[0].onTime = 14 * 60 + 0;
light[0].offTime = 22 * 60 + 0;
for ( int i = 1; i < VIVARIUMS; ++i ) {
config[i] = config[0];
}
for ( int i = 1; i < LIGHTS; ++i ) {
light[i] = light[0];
}
saveConfig();
sensorInit();
}
//--------- Display current values ----------
void displayConfig() {
// print out all the configuration values
rtc.update();
Serial.print( "Current Time: " );
Serial.print(rtc.hour()); Serial.print( ":");
if (rtc.minute() < 10) Serial.print('0');
Serial.print(rtc.minute()); Serial.print( ":");
if (rtc.second() < 10) Serial.print('0');
Serial.println(rtc.second());
Serial.println("#: enabled,dayTemp,nightTemp,alarmEnabled,alarmLow,alarmHigh,dayTime,nightTime" );
for ( int i = 0; i < VIVARIUMS; ++i ) {
Serial.print( i + 1 );
Serial.print( ": " );
Serial.print( config[i].vivEnabled );
Serial.print( "," );
sendNumber( config[i].dayTemp );
Serial.print( "," );
sendNumber( config[i].nightTemp );
Serial.print( "," );
Serial.print( config[i].alarmEnabled );
Serial.print( "," );
sendNumber( config[i].alarmLowTemp );
Serial.print( "," );
sendNumber( config[i].alarmHighTemp );
Serial.print( "," );
sendTime( config[i].dayTime );
Serial.print( "," );
sendTime( config[i].nightTime );
Serial.println();
}
Serial.println( "Lights" );
Serial.println("#: onTime,offTime" );
for ( int i = 0; i < LIGHTS; ++i ) {
Serial.print( i + 1 );
Serial.print( ": " );
sendTime( light[i].onTime );
Serial.print( "," );
sendTime( light[i].offTime );
Serial.println();
}
Serial.println();
Serial.print( "Varible light[1].onTime value " );
Serial.println(light[0].onTime);
Serial.print( "Varible light[1].offTime value " );
Serial.println(light[0].offTime);
Serial.println();
Serial.print( "Varible light[2].onTime value " );
Serial.println(light[1].onTime);
Serial.print( "Varible light[2].offTime value " );
Serial.println(light[1].offTime);
Serial.println();
unsigned int currentTime = rtc.hour() * 60 + rtc.minute(); // current minutes since midnight
Serial.print( "Varible currentTime : " );
Serial.print(currentTime);
}
//--------- Check time ----------
void checkTime() {
unsigned int currentTime = rtc.hour() * 60 + rtc.minute(); // current minutes since midnight
// check to see if the current time falls between the times set for a night time temperature
// or a daytime temperature
for ( int i = 0; i < VIVARIUMS; ++i ) {
// only check enabled vivariums, skip disabled ones
if ( config[i].vivEnabled == true ) {
vivarium[i].setPoint = config[i].dayTemp; // assume we are in daylight
if ( currentTime < config[i].dayTime || currentTime >= config[i].nightTime ) {
// we are early morning or late night so adjust to night time temperature
vivarium[i].setPoint = config[i].nightTemp;
}
}
}
// check to see if the current time requires lights on or off
for ( int i = 0; i < LIGHTS; ++i ) {
if ( currentTime < light[i].onTime || currentTime >= light[i].offTime ) {
digitalWrite(lightPin[i], LOW); // lights off
}
else {
digitalWrite(lightPin[i], HIGH); // lights on
}
}
}
//--------- Display data on TFT screen ----------
void Screen() {
bool needAlarm = false;
static bool alarmBlink = true; // toggle between "alarm" and temperature every time
tft.fillScreen(ILI9341_BLACK); // pełne czyszczenie ekranu przed rysowaniem nowej klatki,
// zapobiega "resztkom" starego tekstu gdy nowy tekst jest krótszy
tft.setCursor(0, 0); // home top left hand corner
tft.setTextColor(ILI9341_MAGENTA, ILI9341_BLACK); // magenta text on black background
tft.println(" Quad-Therm V1.0"); // Version of firmware
tft.println();
tft.println();
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK); // yellow text on black background
tft.println(" TEMP SET TEMP SET");
tft.println();
for ( int i = 0; i < VIVARIUMS; ++i ) {
if (i == 2 || i == 4 || i == 6) {
tft.println();
tft.println();
}
tft.setTextColor(ILI9341_MAGENTA, ILI9341_BLACK); // yellow text on black background for viv number
tft.print(i + 1); tft.print(" "); // viv number
if (config[i].vivEnabled == false ) {
// disabled, so blank current temperature
tft.setTextColor(ILI9341_OLIVE, ILI9341_BLACK);
tft.print("Disabled "); // Channel disabled message
}
else {
// display the current temperature or an error or alarm
if (vivarium[i].state == ERROR_STATE ) {
tft.setTextColor(ILI9341_RED, ILI9341_BLACK); // red text on black background
tft.print("ERROR"); // to display a message indicating a sensor error
needAlarm = true;
}
else if (vivarium[i].state == ALARM_STATE) {
// current tempertaure is outside the high and low thresholds
needAlarm = true;
tft.setTextColor(ILI9341_ORANGE, ILI9341_BLACK); // orange text on black background
if (alarmBlink == true) {
// just report alarm
tft.print("ALARM");
}
else {
// report actual temperature
if ( vivarium[i].temperature < 100 ) tft.print(" "); // make sure it lines up correctly if less than 10.0
tft.print(vivarium[i].temperature / 10.0, 1); // display the tempertaure of channel one on the screen
tft.print(" ");
}
}
else {
// everything is okay, display current temperature
tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK); // green text on black background
if ( vivarium[i].temperature < 100 ) tft.print(" "); // make sure it lines up correctly if less than 10.0
tft.print(vivarium[i].temperature / 10.0, 1); // display the tempertaure of channel one on the screen
tft.print(" ");
}
// now display the setpoint value
tft.setTextColor(ILI9341_CYAN, ILI9341_BLACK);
tft.print(" ");
if ( vivarium[i].setPoint < 100 ) tft.print(" "); // make sure it lines up correctly if less than 10.0
tft.print(vivarium[i].setPoint / 10.0, 1); tft.print(" ");
}
}
// all done with update, turn on alarm if needed
if (needAlarm == true ) {
tone(alarmPin, 1000); // generuje słyszalny ton 1000 Hz - działa też z buzzerem pasywnym
}
else {
noTone(alarmPin); // wyłącza dźwięk
}
tft.setTextColor(ILI9341_MAGENTA, ILI9341_BLACK);
tft.println();
tft.println();
tft.println();
tft.println();
tft.println();
tft.print("TIME ");
tft.setTextColor(ILI9341_CYAN, ILI9341_BLACK);
rtc.update();
if (rtc.hour() < 10) tft.print("0");
tft.print(rtc.hour());
tft.print(":");
if (rtc.minute() < 10) tft.print("0");
tft.print(rtc.minute());
alarmBlink = !alarmBlink;
}
//--------- Clear Screen ----------
void blankScreen()
{
tft.setCursor(0, 0);
tft.fillScreen(ILI9341_BLACK); // blank the screen
}
//--------- Display version on reboot ----------
void about()
{
tft.setCursor(0, 0);
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
tft.println();
tft.println();
tft.println(" Software - Ver 6 rev 1.1"); // Version of firmware
tft.println();
tft.println(" Release Build");
tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
tft.println();
tft.println(" 22 - 1 - 25");
tft.println();
tft.println();
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.println();
tft.println("File: New_4_ch_stat_REV_A");
delay(5000);
tft.fillScreen(ILI9341_BLACK);
}
//--------- Outputs to SSRs ----------
void controlHeat() {
const unsigned long pulseDuration = 2000; // msec (1000)
for ( int i = 0; i < VIVARIUMS; ++i ) {
// Serial.print("vivarium "); Serial.print( i + 1 );
if (config[i].vivEnabled == false ) {
// vivarium is disabled so set pulse to 0 and not full ON
vivarium[i].pulseTime = 0;
vivarium[i].fullOn = false;
// Serial.println( " disabled" );
}
else {
// get temperature error and calculate length of pulsewidth
// NOTE: error is in tenths of a degree
int error = vivarium[i].setPoint + 5 - vivarium[i].temperature;
vivarium[i].fullOn = false;
// Serial.print(" delta=" ); Serial.print(error / 10.0, 1);
if (error > 10) {
// if the difference is more than a degree, full ON
vivarium[i].pulseTime = pulseDuration;
vivarium[i].fullOn = true;
}
else if (error < 0) {
// or if the value is below zero, no heating
vivarium[i].pulseTime = 0;
vivarium[i].fullOn = false;
}
else {
// or if the value is between 0.0 and 1.0 degrees
// set the value of the duty cycle proportionally
vivarium[i].pulseTime = (unsigned long)(pulseDuration * error / 10.0);
vivarium[i].fullOn = false;
}
// override the calculations if in an error or alarm state to keep heater off
if (vivarium[i].state == ERROR_STATE || vivarium[i].state == ALARM_STATE) {
vivarium[i].pulseTime = 0;
vivarium[i].fullOn = false;
}
}
}
// done calculating pulses so turn them on and wait until they all finish
// unless they are full ON
// disabled vivariums have a pulseTime of 0 so they won't get turned on
// and we don't have to do anything special
unsigned long startTime = millis();
for ( int i = 0; i < VIVARIUMS; ++i ) {
if ( vivarium[i].pulseTime > 0 ) {
digitalWrite(outputPin[i], HIGH );
}
else {
digitalWrite(outputPin[i], LOW );
}
}
bool done = false;
while ( !done ) {
done = true; // assume we are done
for ( int i = 0; i < VIVARIUMS; ++i ) {
if ( vivarium[i].fullOn == false ) {
// not full ON so check pulse time and turn off
if ( millis() - startTime >= vivarium[i].pulseTime ) {
digitalWrite(outputPin[i], LOW); // turn off heater
} else {
done = false; // timer still running so we can't be done
}
}
}
}
while ( millis() - startTime < pulseDuration ) {
// all vivariums are off except those that are full on
// wait until we have consumed the entire pulseDuration time slot before returning
delay(1);
}
}
void timeit(const char *msg) {
#if 0
static unsigned long lastTime;
Serial.print(msg);
Serial.print( " " );
Serial.println( millis() - lastTime );
lastTime = millis();
#endif
}