Project - TeensyClimate
Contents |
Project: TeensyClimate
The primary purpose of this project was to give me a reason to learn how to develop solutions using the Atmel AVR 8-bit microcontrollers.
The secondary purpose of this project is to develop a climate control system for the home.
Current Project Status
30 July 2011:
The code has been cleaned up a little bit (you should have seen versions 0.1-0.4!!!).
While I am using the uthash library, I know I'm not using it properly, but it works. While I wanted to implement a hash table, it's pretty much just creating a linked list for me and I'm iterating the list in various places. I've tried implementing a standard linked list using pointers (in version 0.6) but it's not working properly. The sensor objects are being created with the proper hardware addresses in each location, but the temperature readings are not being stored right, which is just strange. So for now I'm continuing to eat up some extra memory while I use the uthash library.
I'm trying to determine the best relay setup to use to control the attic fan. I guess I just need to get over it and by a $15-20 120-240V relay and be done with it.
To Date min's and max's
- 7/31/2011 14:11:16 - Max 131.67 @ Attic
- 8/3/2011 14:58:15 - Max: 135.16 @ Attic
Development Photos
- Teensy 2.0 Development Board
- LCD Contrast Potentiometer (10k)
- DS18B20 Programmable Resolution 1-Wire Digital Thermometer: two on board and one in attic
- CAT3 extending 1-Wire bus to sensor in attic
- HD44780 compatible LCD display
The CLI was updated to export data using a CSV format (millis,now(),each sensors last conversion...). A Python script was written to read the text from the serial interface and write it to a file. LiveGraph was used to read the file in real time and display the graphed data. The graph above begins at about 11:30 PM 7 Aug 2011 and ends about 11:30 PM 8 Aug 2011. One sample is output to the CLI every second. After roughly a 24 hour period the CSV file was 2.69MB in size.
Version
The current version is 0.5c.
Features
As of 0.5c (1 Aug 2011):
- Global min/max with sensor name and timestamps
As of 0.5b (30 July 2011):
- Dynamic sensor discovery on device power on
- Per sensor last/min/max temperatures with timestamps
- Non-blocking temperature conversions
- Sensor data display on LCD (currently limited to the first two sensors)
- Sensor data display on serial console (basic and extended)
- Serial console control
- Time update via serial console
To Do
- Air Conditional Filter Change Reminder
- Attic fan relay control
- EEPROM storage of configuration information
- Ethernet TCP/IP Interface using WizNet WIZ812MJ module
- Historical statistics other than just current/min/max
- avg/min/max per day
- long term five minute interval graphing (via external means, SNMP possible)
- Keypad for complete device control
- PCB design
- Final migration from breadboard to PCB
Source Code
/*Starting point:http://tushev.org/articles/electronics/42-how-it-works-ds18b20-and-arduino-= TODO =-A/C Filter Change Reminder-= EEPROM STORAGE MAP =-1k bytes EEPROM availablePage size is 8 bytes128 PagesReserve the first page for empty (due to slot 0 possiblity of being corrupted)Reserve pages 2-9 for configuration dataTIMEZONE_OFFSET_HOURS - charDST - daylight savings time - byteNUM_STORED_SENSORS - byteAIRFILTER REPLACEMENT DATEAIRFILTER LIFESPANPages 10 and up are reserved for stored sensorsSTORED SENSORS:ADDR - byte[8] (1 page)NAME - char[10] (1 page plus 2 bytes)CALIBRATION_OFFSET - float (2 bytes)*///#define DEBUG 0#define ACTIVITY_CHAR_INTERVAL 250#define BLINK_INTERVAL 250#define CONSOLE_UPDATE_INTERVAL 1000#define DEFAULT_MIN_TEMP 999#define DEFAULT_MAX_TEMP -99#define DEFAULT_TIME_ZONE_OFFSET -5 // CST during DST#define DS18B20_ID 0x28#define DS18B20_CONVERSION_WAIT_TIME 750#define LCD_CLEAR_INTERVAL 10000#define LCD_UPDATE_INTERVAL 1000#define LCD_STRING_BUFFER_LENGTH 21#define LED_PIN 11#define CLI_STRING_BUFFER_LENGTH 50#define NUM_CLI_LINES 10#include <avr/pgmspace.h>#include <MemoryFree.h>#include <LiquidCrystal.h>#include <Metro.h>#include <OneWire.h>#include <String.h>#include <Streaming.h>#include <Time.h>#include "C:\arduino-0022\projects\temp_03\uthash.h"prog_char cli_line_1[] PROGMEM = "CLI Options:";
prog_char cli_line_2[] PROGMEM = "B: jump to bootloader";
prog_char cli_line_3[] PROGMEM = "C: reset sensors stats";
prog_char cli_line_4[] PROGMEM = "d/D: toggle lcd display contents";
prog_char cli_line_5[] PROGMEM = "e/E: toggle the display of extended stats";
prog_char cli_line_6[] PROGMEM = "f/F: show current free memory";
prog_char cli_line_7[] PROGMEM = "G: reset global sensor stats";
prog_char cli_line_8[] PROGMEM = "h/H: display this help";
prog_char cli_line_9[] PROGMEM = "T: time sync via console";
prog_char cli_line_10[] PROGMEM = "?: display this help";
PROGMEM const char *cli_string_table[] =
{cli_line_1,
cli_line_2,
cli_line_3,
cli_line_4,
cli_line_5,
cli_line_6,
cli_line_7,
cli_line_8,
cli_line_9,
cli_line_10
};
byte tempSensorAttic[8] = {
0x28, 0xdb, 0x32, 0x5b, 0x03, 0x00, 0x00, 0x8d};
byte tempSensorInside1[8] = {
0x28, 0xb7, 0x4b, 0x5b, 0x03, 0x00, 0x00, 0xad};
byte tempSensorInside2[8] = {
0x28, 0x39, 0x44, 0x5b, 0x03, 0x00, 0x00, 0x3b};
boolean lcdDisplayAddresses = false;
boolean showExtendedStats = false;
unsigned int activityCharIndex = 0;
unsigned int ledStatus = 0;
unsigned long loopIteration = 0;
unsigned long loopStartMillis;
unsigned long lastLoopDuration = 0;
LiquidCrystal lcd(16, 17, 12, 13, 14, 15);
#define NUM_METROS 6Metro activityCharMetro(ACTIVITY_CHAR_INTERVAL, false);
Metro consoleUpdateMetro(CONSOLE_UPDATE_INTERVAL);
Metro ledMetro(BLINK_INTERVAL, false);
Metro lcdUpdateMetro(LCD_UPDATE_INTERVAL);
Metro lcdClearMetro(LCD_CLEAR_INTERVAL); // counter to clear the lcd every 10 seconds for housekeeping
Metro owBusSearchMetro(600000, false); // counter to search the bus every 10 minutes for new devices
OneWire ds(10);
float globalMin = DEFAULT_MIN_TEMP;
char * globalMinName = NULL;
time_t globalMinTimeStamp = (time_t) 0;
float globalMax = DEFAULT_MAX_TEMP;
char * globalMaxName = NULL;
time_t globalMaxTimeStamp = (time_t) 0;
struct DS18B20 {
byte addr[8]; /* key */
char name[10];
boolean active;boolean converting; /* true if a conversion has been requested */
unsigned int crcerrors;
unsigned long startConversionLI; // loop iteration of the start conversion
unsigned long liLastConversion; // number of loop iterations for the last conversion
Metro conversionTimer;float lastTemp;
time_t lastTimeStamp;
float minTemp;
time_t minTimeStamp;
float maxTemp;
time_t maxTimeStamp;
UT_hash_handle hh; /* makes this structure hashable */
};
struct DS18B20 *tempSensors = NULL;
boolean compareByteArray(byte a1[], byte a2[]) {
int arraySize = sizeof(a1)/sizeof(byte);
if (sizeof(a1) != sizeof(a2)) {
return false;
}for(int i=0; i<arraySize; i++) {
if (a1[i] != a2[i]) {
return false;
}}return true;
}void add_sensor(byte addr[]) {
struct DS18B20 *s;
s = (DS18B20 *) malloc(sizeof(struct DS18B20));
for (int i = 0; i < 8; i++) {
s->addr[i] = addr[i];
}s->active = false;
s->converting = false;
s->crcerrors = 0;
s->conversionTimer = Metro(DS18B20_CONVERSION_WAIT_TIME, false);
s->liLastConversion = 0;
s->lastTemp = 0.0;
s->lastTimeStamp = now();
s->minTemp = DEFAULT_MIN_TEMP;
s->minTimeStamp = s->lastTimeStamp;
s->maxTemp = DEFAULT_MAX_TEMP;
s->maxTimeStamp = s->lastTimeStamp;
if (compareByteArray(s->addr,tempSensorAttic)) {
strcpy(s->name, "Attic");
}else if(compareByteArray(s->addr,tempSensorInside1)) {
strcpy(s->name, "Inside1");
}else if(compareByteArray(s->addr,tempSensorInside2)) {
strcpy(s->name, "Inside2");
}else {
strcpy(s->name, "Unknown");
}HASH_ADD(hh, tempSensors, addr, sizeof(byte) * 8, s);
}struct DS18B20 *find_sensor(byte addr[]) {
struct DS18B20 *s;
#ifdef DEBUGSerial.print("find_sensor(");
Serial.print(OneWireaddrtostring(addr, false));
Serial.println(")");
#endiffor (s=tempSensors; s != NULL; s=(DS18B20 *) s->hh.next) {
#ifdef DEBUGSerial.println("Comparing:");
Serial.print(OneWireaddrtostring(s->addr, false));
Serial.print(" to ");
Serial.println(OneWireaddrtostring(addr, false));
#endifif (compareByteArray(addr, s->addr)) {
#ifdef DEBUGSerial.println(" match found... sensor already detected");
#endifreturn s;
}}#ifdef DEBUGSerial.println("no match found... returning NULL");
#endifreturn NULL;
}void print_sensors(boolean extended) {
struct DS18B20 *s;
Serial.println("Current list of sensors:");
for (s=tempSensors; s != NULL; s=(DS18B20 *) s->hh.next) {
Serial.print(OneWireaddrtostring(s->addr, false));
if (s->active) {
if (!showExtendedStats) {
// show standard informationSerial.print(" ");
Serial.print(s->lastTemp);
Serial.print("/");
Serial.print(s->minTemp);
Serial.print("/");
Serial.print(s->maxTemp);
Serial.print(" F, Location: ");
Serial.println(s->name);
}else {
// show extended statsSerial << " Location: " << s->name << " (crcerrors: " << s->crcerrors << " ) (lis: " << s->liLastConversion << ")" << endl;
Serial << " Last: " << s->lastTemp << " @ ";
serialPrintDateTime(s->lastTimeStamp);
Serial << endl;
Serial << " Min: " << s->minTemp << " @ ";
serialPrintDateTime(s->minTimeStamp);
Serial << endl;
Serial << " Max: " << s->maxTemp << " @ ";
serialPrintDateTime(s->maxTimeStamp);
Serial << endl;
}}else {
Serial.println(" is pending first read.");
}}Serial.println();
}boolean update_sensor(byte addr[], float temp) {
struct DS18B20 *s;
s = find_sensor(addr);
if (s != NULL) {
if (!s->active) {
s->active = true;
}s->lastTemp = temp;
s->lastTimeStamp = now();
if (temp < s->minTemp) {
s->minTemp = temp;
s->minTimeStamp = now();
}if (temp < globalMin) {
globalMin = temp;
globalMinName = s->name;
globalMinTimeStamp = now();
}if (temp > s->maxTemp) {
s->maxTemp = temp;
s->maxTimeStamp = now();
}if (temp > globalMax) {
globalMax = temp;
globalMaxName = s->name;
globalMaxTimeStamp = now();
}return true;
}else {
#ifdef DEBUGSerial.print("update_sensor() failed for addr ");
Serial.print(OneWireaddrtostring(addr, false));
Serial.print(" ");
Serial.print(temp);
Serial.println(" F");
#endifreturn false;
}}int count_sensors() {
struct DS18B20 *s;
int count = 0;
for (s=tempSensors; s != NULL; s=(DS18B20 *) s->hh.next, count++) {
}return count;
}void clear_sensor_stats() {
struct DS18B20 *s;
for (s=tempSensors; s != NULL; s=(DS18B20 *) s->hh.next) {
s->minTemp = DEFAULT_MIN_TEMP;
s->minTimeStamp = (time_t) 0;
s->maxTemp = DEFAULT_MAX_TEMP;
s->maxTimeStamp = (time_t) 0;
}}void clear_global_sensor_stats() {
globalMin = DEFAULT_MIN_TEMP;
globalMinName = NULL;
globalMinTimeStamp = (time_t) 0;
globalMax = DEFAULT_MAX_TEMP;
globalMaxName = NULL;
globalMaxTimeStamp = (time_t) 0;
}boolean findDS18B20Devices(OneWire & ow) {
byte addr[8];
unsigned int deviceCount = 0;
#ifdef DEBUGSerial.println("Searching bus...");
#endif//find a devicewhile (ow.search(addr)) {
if (OneWire::crc8( addr, 7) != addr[7]) {
Serial.println("Bad crc!!!");
continue;
}if (addr[0] != DS18B20_ID) {
#ifdef DEBUGSerial.print("Unknown device: ");
#endifSerial.println(OneWireaddrtostring(addr, false));
continue;
}#ifdef DEBUGSerial.print("Found a device:");
Serial.println(OneWireaddrtostring(addr, false));
#endifdeviceCount++;
if (find_sensor(addr) == NULL) {
#ifdef DEBUGSerial.print("Adding new sensor to list: ");
#endifadd_sensor(addr);
}#ifdef DEBUGelse {
Serial.print("We already know about this sensor: ");
}#endifSerial.println(OneWireaddrtostring(addr, false));
Serial.println();
}#ifdef DEBUGSerial.println("No more devices found... resetting search.");
Serial.println();
#endifow.reset_search();
}boolean requestTemperatureConversion(OneWire ow, DS18B20 *sensor) {
ow.reset();
ow.select(sensor->addr);
ow.write(0x44, 1);
return true;
}float retrieveTemperature(OneWire ow, DS18B20 *sensor) {
byte data[12];
float temp;
ow.reset();
ow.select(sensor->addr);
ow.write(0xBE);
for (int i = 0; i < 9; i++) {
data[i] = ow.read();
}temp = ( (data[1] << 8) + data[0] )*0.0625; // tempc
temp = (temp * 1.8) + 32; // tempf
if (OneWire::crc8( data, 8) != data[8]) {
temp = -9999;
}return temp;
}void toggleLED() {
switch (ledStatus) {
case 1:
digitalWrite(LED_PIN, LOW);
ledStatus = 0;
break;
default:
digitalWrite(LED_PIN, HIGH);
ledStatus = 1;
break;
}}void showActivityChar() {
lcd.setCursor(18,0);
switch (activityCharIndex) {
case 1:
activityCharIndex--;
lcd.print(count_sensors());
lcd.print(" ");
break;
default:
activityCharIndex++;
lcd.print(count_sensors());
lcd.print("*");
break;
}}String OneWireaddrtostring(byte addr[], boolean lcd) {
String toReturn;for( int i = 0; i < 8; i++) {
if (addr[i] < 16) {
toReturn += "0";
}toReturn += String(addr[i], HEX);
if (i < 7) {
if (!lcd) {
// don't print semicolons on the lcd// we don't have enough roomtoReturn += ":";
}}}return toReturn;
}void update_lcd() {
struct DS18B20 *s;
unsigned int count;
unsigned int maxCount = 2;
s = tempSensors;
if (lcdDisplayAddresses) {
maxCount = 3;
}if (lcdClearMetro.check() == 1) {
lcd.clear();
lcdClearMetro.reset();
}if (lcdDisplayAddresses) {
lcd.setCursor(0,0);
lcd.print("Uptime: ");
lcd.print(millis() / 1000);
lcd.print("s ");
}for (count = 0; count < maxCount; count++, s=(DS18B20 *) s->hh.next) {
if (s == NULL) {
break;
}if (!lcdDisplayAddresses) {
if (!s->active) {
continue;
}lcd.setCursor(count*10,0);
lcd.print(s->name);
lcd.setCursor(count*10,1);
lcd.print(s->lastTemp);
lcd.print((char)223);
lcd.print("F");
lcd.setCursor(count*10,2);
lcd.print(s->minTemp);
lcd.print((char)223);
lcd.print("F");
lcd.setCursor(count*10,3);
lcd.print(s->maxTemp);
lcd.print((char)223);
lcd.print("F");
}else {
// display addresses instead of tempslcd.setCursor(0,count + 1);
lcd.print(" ");
lcd.print(OneWireaddrtostring(s->addr, true));
lcd.setCursor(0,count + 1);
lcd.print(s->name);
lcd.print(":");
}}}void serialPrintDateTime(time_t timeStamp) {
time_t timeStampAdjusted = timeStamp + (DEFAULT_TIME_ZONE_OFFSET * 60 * 60);
Serial << month(timeStampAdjusted) << "/" << day(timeStampAdjusted) << "/" << year(timeStampAdjusted) << " " << hour(timeStampAdjusted) << ":";
if (minute(timeStampAdjusted) < 10) Serial << "0";
Serial << minute(timeStampAdjusted) << ":";
if (second(timeStampAdjusted) < 10) Serial << "0";
Serial << second(timeStampAdjusted);
}void processTimeSyncMessage() {
int count=0;
char buf[11];
boolean status = false; // did we get good data
Serial.println("Waiting for time data in @time_t format...");
Serial.flush();
while (count < 11) {
if (Serial.available()) { // receive all 11 bytes into "buf"
buf[count++] = Serial.read();
}}if (buf[0] == '@') {
time_t pctime = 0;
for(int i=1; i < 11; i++) {
char c = buf[i];
if (c >= '0' && c <= '9') {
pctime = (10 * pctime) + (c - '0') ; // convert digits to a number
}}pctime += 10;
setTime(pctime); // Sync clock to the time received
status = true;
}if (status) {
Serial.println("Sync message received and time updated.");
}else {
Serial.println("Invalid sync message received.");
}}void jumpToBootloader() {
unsigned int counter = 10;
Serial << "Jumping to bootloader in " << counter << " seconds..." << endl;
Serial << "Make sure you close your serial console!!!" << endl;
Serial << endl;
Serial << "PRESS ANY KEY TO ABORT!!!" << endl;
Serial << endl;
Serial.flush();
lcd.clear();
while (counter > 0) {
lcd.setCursor(0,0);
lcd.print(counter);
Serial << counter << " ";
if (Serial.available()) {
Serial.flush();
Serial << endl << endl << "Aborted!" << endl << endl << endl;
return;
}delay(1000);
counter--;
}Serial.println("Jumping!");
cli();
// disable watchdog, if enabled// disable all peripheralsUDCON = 1;
USBCON = (1<<FRZCLK); // disable USB
UCSR1B = 0;
delay(5);
#if defined(__AVR_AT90USB162__) // Teensy 1.0EIMSK = 0;
PCICR = 0;
SPCR = 0;
ACSR = 0;
EECR = 0;
TIMSK0 = 0;
TIMSK1 = 0;
UCSR1B = 0;
DDRB = 0;
DDRC = 0;
DDRD = 0;
PORTB = 0;
PORTC = 0;
PORTD = 0;
asm volatile("jmp 0x3E00");
#elif defined(__AVR_ATmega32U4__) // Teensy 2.0EIMSK = 0;
PCICR = 0;
SPCR = 0;
ACSR = 0;
EECR = 0;
ADCSRA = 0;
TIMSK0 = 0;
TIMSK1 = 0;
TIMSK3 = 0;
TIMSK4 = 0;
UCSR1B = 0;
TWCR = 0;
DDRB = 0;
DDRC = 0;
DDRD = 0;
DDRE = 0;
DDRF = 0;
TWCR = 0;
PORTB = 0;
PORTC = 0;
PORTD = 0;
PORTE = 0;
PORTF = 0;
asm volatile("jmp 0x7E00");
#elif defined(__AVR_AT90USB646__) // Teensy++ 1.0EIMSK = 0;
PCICR = 0;
SPCR = 0;
ACSR = 0;
EECR = 0;
ADCSRA = 0;
TIMSK0 = 0;
TIMSK1 = 0;
TIMSK2 = 0;
TIMSK3 = 0;
UCSR1B = 0;
TWCR = 0;
DDRA = 0;
DDRB = 0;
DDRC = 0;
DDRD = 0;
DDRE = 0;
DDRF = 0;
PORTA = 0;
PORTB = 0;
PORTC = 0;
PORTD = 0;
PORTE = 0;
PORTF = 0;
asm volatile("jmp 0xFC00");
#elif defined(__AVR_AT90USB1286__) // Teensy++ 2.0EIMSK = 0;
PCICR = 0;
SPCR = 0;
ACSR = 0;
EECR = 0;
ADCSRA = 0;
TIMSK0 = 0;
TIMSK1 = 0;
TIMSK2 = 0;
TIMSK3 = 0;
UCSR1B = 0;
TWCR = 0;
DDRA = 0;
DDRB = 0;
DDRC = 0;
DDRD = 0;
DDRE = 0;
DDRF = 0;
PORTA = 0;
PORTB = 0;
PORTC = 0;
PORTD = 0;
PORTE = 0;
PORTF = 0;
asm volatile("jmp 0x1FC00");
#endif}void printCLIOptions() {
char buf[CLI_STRING_BUFFER_LENGTH];
for (int i = 0; i < NUM_CLI_LINES; i++) {
strcpy_P(buf, (char*)pgm_read_word(&(cli_string_table[i])));
Serial.println(buf);
}Serial.println();
}void setup() {
// Initialize the display and tell the world we're starting to worklcd.begin(20, 4);
lcd.setCursor(0,0);
lcd.print("Setting up...");
pinMode(LED_PIN, OUTPUT);
ledMetro.reset();
Serial.begin(9600);
// displaying activity char so we know we've started the initial 1-wire searchshowActivityChar();
#ifdef DEBUGfor (int i = 0; i < 10; i++) {
Serial << i << " ";
delay(1000);
}Serial << endl;
#endiffindDS18B20Devices(ds);
#ifdef DEBUGfor (int i = 0; i < 10; i++) {
Serial << i << " ";
delay(1000);
}Serial << endl;
#endif} // end setup
void loop() {
loopIteration++;
loopStartMillis = millis();
float tmpTemp = 0;
struct DS18B20 *s;
if (activityCharMetro.check() == 1) {
showActivityChar();
activityCharMetro.reset();
}// manage the sensorsfor (s=tempSensors; s != NULL; s=(DS18B20 *) s->hh.next) {
if (s->converting) {
if (s->conversionTimer.check() == 1) {
tmpTemp = retrieveTemperature(ds, s);
if (tmpTemp != -9999) {
update_sensor(s->addr, tmpTemp);
}else {
s->crcerrors++;
}s->liLastConversion = loopIteration - s->startConversionLI;
s->converting = false;
tmpTemp = 0;
}}else {
requestTemperatureConversion(ds, s);
s->startConversionLI = loopIteration;
s->conversionTimer.reset();
s->converting = true;
}} // for tempSensors
// process command line inputif (Serial.available() > 0) {
char c = Serial.read();
switch (c) {
case 'B':
jumpToBootloader();
break;
case 'C':
clear_sensor_stats();
break;
case 'd':
case 'D':
if (lcdDisplayAddresses) {
#ifdef DEBUGSerial.println("Switching LCD to display sensor values");
#endiflcdDisplayAddresses = false;
}else {
#ifdef DEBUGSerial.println("Switching LCD to display sensor addresses");
#endiflcdDisplayAddresses = true;
}Serial.println();
Serial.flush();
lcd.clear();
break;
case 'f':
case 'F':
Serial << "Free memory: " << freeMemory() << " bytes." << endl;
#ifdef DEBUGSerial << sizeof(DS18B20) * count_sensors() << " bytes for " << count_sensors() << " DS18B20 sensors" << endl;
Serial << sizeof(LiquidCrystal) << " bytes for LiquidCrystal object" << endl;
Serial << sizeof(Metro) * NUM_METROS << " bytes for " << NUM_METROS << " Metro objects" << endl;
Serial << sizeof(OneWire) << " bytes for OneWire object" << endl;
#endifSerial << endl;
break;
case 'G':
clear_global_sensor_stats();
break;
case 'e':
case 'E':
if (showExtendedStats) {
#ifdef DEBUGSerial.println("Disabling extended stats.");
#endifshowExtendedStats = false;
}else {
#ifdef DEBUGSerial.println("Enabling extended stats.");
#endifshowExtendedStats = true;
}break;
case 't':
case 'T':
#ifdef DEBUGSerial.println("Processing time sync request...");
#endifprocessTimeSyncMessage();
#ifdef DEBUGSerial.println("Done!");
#endifSerial.println();
break;
case 'h':
case 'H':
case '?':
printCLIOptions();
break;
default:
Serial.print("Key: 0x");
Serial.println(c, HEX);
printCLIOptions();
} // testing c
}if (consoleUpdateMetro.check() == 1) {
serialPrintDateTime(now());
Serial << " (Uptime: " << millis() / 1000 << " s) (li: " << loopIteration << ") (lld: " << lastLoopDuration << " ms)" << endl;
Serial << "Global Min: " << globalMin << " @ ";
serialPrintDateTime(globalMinTimeStamp);
Serial << " (" << globalMinName << ")" << endl;
Serial << "Global Max: " << globalMax << " @ ";
serialPrintDateTime(globalMaxTimeStamp);
Serial << " (" << globalMaxName << ")" << endl << endl;
print_sensors(false);
}if (lcdUpdateMetro.check() == 1) {
update_lcd();
}if (ledMetro.check() == 1) {
toggleLED();
ledMetro.reset();
}lastLoopDuration = millis() - loopStartMillis;
} // end loop



