Thermoscope
A 16x2 LCD thermometer with S/M/H history graphs (Instructable).
A 16x2 LCD thermometer with S/M/H history graphs (Instructable).
| /* | |
| datalogger.cpp - yet another buffer object. | |
| Created by thedod[.github.io]. | |
| Released into the public domain. | |
| */ | |
| #include "datalogger.h" | |
| DataLogger::DataLogger(float *buff, unsigned int buffsize) { | |
| _buffsize = buffsize; | |
| _buff = buff; | |
| nsamples = _next_push = 0; | |
| } | |
| void DataLogger::push(float value) { | |
| _buff[_next_push] = value; | |
| _next_push = (_next_push+1)%_buffsize; | |
| if (nsamples < _buffsize) nsamples++; | |
| } | |
| void DataLogger::popReset(void) { | |
| _pop_next = (_next_push+_buffsize-1)%_buffsize; | |
| _pop_depth = nsamples; | |
| } | |
| float DataLogger::pop(void) { | |
| if (!_pop_depth) return NOTHING; | |
| float value = _buff[_pop_next]; | |
| _pop_depth--; | |
| _pop_next = (_pop_next+_buffsize-1)%_buffsize; | |
| return value; | |
| } | |
| void DataLogger::aggregate(unsigned int n) { | |
| if (nsamples<n) n = nsamples; | |
| if (n) { | |
| agg_min = MUCH; agg_max = NOTHING; agg_avg = 0; | |
| popReset(); | |
| for (int i = 0 ; i< n ; i++) { | |
| float value = pop(); | |
| if (value<agg_min) agg_min = value; | |
| if (value>agg_max) agg_max = value; | |
| agg_avg += value/n; | |
| } | |
| } else { | |
| agg_min = agg_max = agg_avg = NOTHING; | |
| } | |
| } |
| /* | |
| datalogger.cpp - yet another buffer object. | |
| Created by thedod[.github.io]. | |
| Released into the public domain. | |
| */ | |
| #ifndef Datalogger_h | |
| #define Datalogger_h | |
| #define NOTHING -3E38; // "almost -infinity" ;) | |
| #define MUCH 3E38; // "almost infinity" | |
| class DataLogger { | |
| public: | |
| DataLogger(float *buff, unsigned int buffsize); | |
| int nsamples; | |
| void push(float value); | |
| void popReset(); // pop() would start popping from latest push. | |
| float pop(); // May return NOTHING if buffer's finished. | |
| void aggregate(unsigned int n); // compute agg_* (below) for latest [max] n | |
| float agg_min, agg_max, agg_avg; // results of aggregate() undefined if not called | |
| private: | |
| int _buffsize, _next_push, _pop_next, _pop_depth; | |
| float *_buff; | |
| }; | |
| #endif |
| /* | |
| rgb_lcd.cpp | |
| 2013 Copyright (c) Seeed Technology Inc. All right reserved. | |
| Author:Loovee | |
| 2013-9-18 | |
| This library is free software; you can redistribute it and/or | |
| modify it under the terms of the GNU Lesser General Public | |
| License as published by the Free Software Foundation; either | |
| version 2.1 of the License, or (at your option) any later version. | |
| This library is distributed in the hope that it will be useful, | |
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| Lesser General Public License for more details. | |
| You should have received a copy of the GNU Lesser General Public | |
| License along with this library; if not, write to the Free Software | |
| Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | |
| */ | |
| #include <Arduino.h> | |
| #include <stdio.h> | |
| #include <string.h> | |
| #include <inttypes.h> | |
| #include <Wire.h> | |
| #include "rgb_lcd.h" | |
| void i2c_send_byte(unsigned char dta) | |
| { | |
| Wire.beginTransmission(LCD_ADDRESS); // transmit to device #4 | |
| Wire.write(dta); // sends five bytes | |
| Wire.endTransmission(); // stop transmitting | |
| } | |
| void i2c_send_byteS(unsigned char *dta, unsigned char len) | |
| { | |
| Wire.beginTransmission(LCD_ADDRESS); // transmit to device #4 | |
| for(int i=0; i<len; i++) | |
| { | |
| Wire.write(dta[i]); | |
| } | |
| Wire.endTransmission(); // stop transmitting | |
| } | |
| rgb_lcd::rgb_lcd() | |
| { | |
| } | |
| void rgb_lcd::begin(uint8_t cols, uint8_t lines, uint8_t dotsize) | |
| { | |
| Wire.begin(); | |
| if (lines > 1) { | |
| _displayfunction |= LCD_2LINE; | |
| } | |
| _numlines = lines; | |
| _currline = 0; | |
| // for some 1 line displays you can select a 10 pixel high font | |
| if ((dotsize != 0) && (lines == 1)) { | |
| _displayfunction |= LCD_5x10DOTS; | |
| } | |
| // SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION! | |
| // according to datasheet, we need at least 40ms after power rises above 2.7V | |
| // before sending commands. Arduino can turn on way befer 4.5V so we'll wait 50 | |
| delayMicroseconds(50000); | |
| // this is according to the hitachi HD44780 datasheet | |
| // page 45 figure 23 | |
| // Send function set command sequence | |
| command(LCD_FUNCTIONSET | _displayfunction); | |
| delayMicroseconds(4500); // wait more than 4.1ms | |
| // second try | |
| command(LCD_FUNCTIONSET | _displayfunction); | |
| delayMicroseconds(150); | |
| // third go | |
| command(LCD_FUNCTIONSET | _displayfunction); | |
| // finally, set # lines, font size, etc. | |
| command(LCD_FUNCTIONSET | _displayfunction); | |
| // turn the display on with no cursor or blinking default | |
| _displaycontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF; | |
| display(); | |
| // clear it off | |
| clear(); | |
| // Initialize to default text direction (for romance languages) | |
| _displaymode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT; | |
| // set the entry mode | |
| command(LCD_ENTRYMODESET | _displaymode); | |
| // backlight init | |
| setReg(0, 0); | |
| setReg(1, 0); | |
| setReg(0x08, 0xAA); // all led control by pwm | |
| setColorWhite(); | |
| } | |
| /********** high level commands, for the user! */ | |
| void rgb_lcd::clear() | |
| { | |
| command(LCD_CLEARDISPLAY); // clear display, set cursor position to zero | |
| delayMicroseconds(2000); // this command takes a long time! | |
| } | |
| void rgb_lcd::home() | |
| { | |
| command(LCD_RETURNHOME); // set cursor position to zero | |
| delayMicroseconds(2000); // this command takes a long time! | |
| } | |
| void rgb_lcd::setCursor(uint8_t col, uint8_t row) | |
| { | |
| col = (row == 0 ? col|0x80 : col|0xc0); | |
| unsigned char dta[2] = {0x80, col}; | |
| i2c_send_byteS(dta, 2); | |
| } | |
| // Turn the display on/off (quickly) | |
| void rgb_lcd::noDisplay() | |
| { | |
| _displaycontrol &= ~LCD_DISPLAYON; | |
| command(LCD_DISPLAYCONTROL | _displaycontrol); | |
| } | |
| void rgb_lcd::display() { | |
| _displaycontrol |= LCD_DISPLAYON; | |
| command(LCD_DISPLAYCONTROL | _displaycontrol); | |
| } | |
| // Turns the underline cursor on/off | |
| void rgb_lcd::noCursor() | |
| { | |
| _displaycontrol &= ~LCD_CURSORON; | |
| command(LCD_DISPLAYCONTROL | _displaycontrol); | |
| } | |
| void rgb_lcd::cursor() { | |
| _displaycontrol |= LCD_CURSORON; | |
| command(LCD_DISPLAYCONTROL | _displaycontrol); | |
| } | |
| // Turn on and off the blinking cursor | |
| void rgb_lcd::noBlink() | |
| { | |
| _displaycontrol &= ~LCD_BLINKON; | |
| command(LCD_DISPLAYCONTROL | _displaycontrol); | |
| } | |
| void rgb_lcd::blink() | |
| { | |
| _displaycontrol |= LCD_BLINKON; | |
| command(LCD_DISPLAYCONTROL | _displaycontrol); | |
| } | |
| // These commands scroll the display without changing the RAM | |
| void rgb_lcd::scrollDisplayLeft(void) | |
| { | |
| command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVELEFT); | |
| } | |
| void rgb_lcd::scrollDisplayRight(void) | |
| { | |
| command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVERIGHT); | |
| } | |
| // This is for text that flows Left to Right | |
| void rgb_lcd::leftToRight(void) | |
| { | |
| _displaymode |= LCD_ENTRYLEFT; | |
| command(LCD_ENTRYMODESET | _displaymode); | |
| } | |
| // This is for text that flows Right to Left | |
| void rgb_lcd::rightToLeft(void) | |
| { | |
| _displaymode &= ~LCD_ENTRYLEFT; | |
| command(LCD_ENTRYMODESET | _displaymode); | |
| } | |
| // This will 'right justify' text from the cursor | |
| void rgb_lcd::autoscroll(void) | |
| { | |
| _displaymode |= LCD_ENTRYSHIFTINCREMENT; | |
| command(LCD_ENTRYMODESET | _displaymode); | |
| } | |
| // This will 'left justify' text from the cursor | |
| void rgb_lcd::noAutoscroll(void) | |
| { | |
| _displaymode &= ~LCD_ENTRYSHIFTINCREMENT; | |
| command(LCD_ENTRYMODESET | _displaymode); | |
| } | |
| // Allows us to fill the first 8 CGRAM locations | |
| // with custom characters | |
| void rgb_lcd::createChar(uint8_t location, uint8_t charmap[]) | |
| { | |
| location &= 0x7; // we only have 8 locations 0-7 | |
| command(LCD_SETCGRAMADDR | (location << 3)); | |
| unsigned char dta[9]; | |
| dta[0] = 0x40; | |
| for(int i=0; i<8; i++) | |
| { | |
| dta[i+1] = charmap[i]; | |
| } | |
| i2c_send_byteS(dta, 9); | |
| } | |
| /*********** mid level commands, for sending data/cmds */ | |
| // send command | |
| inline void rgb_lcd::command(uint8_t value) | |
| { | |
| unsigned char dta[2] = {0x80, value}; | |
| i2c_send_byteS(dta, 2); | |
| } | |
| // send data | |
| inline size_t rgb_lcd::write(uint8_t value) | |
| { | |
| unsigned char dta[2] = {0x40, value}; | |
| i2c_send_byteS(dta, 2); | |
| return 1; // assume sucess | |
| } | |
| void rgb_lcd::setReg(unsigned char addr, unsigned char dta) | |
| { | |
| Wire.beginTransmission(RGB_ADDRESS); // transmit to device #4 | |
| Wire.write(addr); | |
| Wire.write(dta); | |
| Wire.endTransmission(); // stop transmitting | |
| } | |
| void rgb_lcd::setRGB(unsigned char r, unsigned char g, unsigned char b) | |
| { | |
| setReg(REG_RED, r); | |
| setReg(REG_GREEN, g); | |
| setReg(REG_BLUE, b); | |
| } | |
| const unsigned char color_define[4][3] = | |
| { | |
| {255, 255, 255}, // white | |
| {255, 0, 0}, // red | |
| {0, 255, 0}, // green | |
| {0, 0, 255}, // blue | |
| }; | |
| void rgb_lcd::setColor(unsigned char color) | |
| { | |
| if(color > 3)return ; | |
| setRGB(color_define[color][0], color_define[color][1], color_define[color][2]); | |
| } |
| /* | |
| rgb_lcd.h | |
| 2013 Copyright (c) Seeed Technology Inc. All right reserved. | |
| Author:Loovee | |
| 2013-9-18 | |
| add rgb backlight fucnction @ 2013-10-15 | |
| This library is free software; you can redistribute it and/or | |
| modify it under the terms of the GNU Lesser General Public | |
| License as published by the Free Software Foundation; either | |
| version 2.1 of the License, or (at your option) any later version. | |
| This library is distributed in the hope that it will be useful, | |
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| Lesser General Public License for more details. | |
| You should have received a copy of the GNU Lesser General Public | |
| License along with this library; if not, write to the Free Software | |
| Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | |
| */ | |
| #ifndef __RGB_LCD_H__ | |
| #define __RGB_LCD_H__ | |
| #include <inttypes.h> | |
| #include "Print.h" | |
| // Device I2C Arress | |
| #define LCD_ADDRESS (0x7c>>1) | |
| #define RGB_ADDRESS (0xc4>>1) | |
| // color define | |
| #define WHITE 0 | |
| #define RED 1 | |
| #define GREEN 2 | |
| #define BLUE 3 | |
| #define REG_RED 0x04 // pwm2 | |
| #define REG_GREEN 0x03 // pwm1 | |
| #define REG_BLUE 0x02 // pwm0 | |
| #define REG_MODE1 0x00 | |
| #define REG_MODE2 0x01 | |
| #define REG_OUTPUT 0x08 | |
| // commands | |
| #define LCD_CLEARDISPLAY 0x01 | |
| #define LCD_RETURNHOME 0x02 | |
| #define LCD_ENTRYMODESET 0x04 | |
| #define LCD_DISPLAYCONTROL 0x08 | |
| #define LCD_CURSORSHIFT 0x10 | |
| #define LCD_FUNCTIONSET 0x20 | |
| #define LCD_SETCGRAMADDR 0x40 | |
| #define LCD_SETDDRAMADDR 0x80 | |
| // flags for display entry mode | |
| #define LCD_ENTRYRIGHT 0x00 | |
| #define LCD_ENTRYLEFT 0x02 | |
| #define LCD_ENTRYSHIFTINCREMENT 0x01 | |
| #define LCD_ENTRYSHIFTDECREMENT 0x00 | |
| // flags for display on/off control | |
| #define LCD_DISPLAYON 0x04 | |
| #define LCD_DISPLAYOFF 0x00 | |
| #define LCD_CURSORON 0x02 | |
| #define LCD_CURSOROFF 0x00 | |
| #define LCD_BLINKON 0x01 | |
| #define LCD_BLINKOFF 0x00 | |
| // flags for display/cursor shift | |
| #define LCD_DISPLAYMOVE 0x08 | |
| #define LCD_CURSORMOVE 0x00 | |
| #define LCD_MOVERIGHT 0x04 | |
| #define LCD_MOVELEFT 0x00 | |
| // flags for function set | |
| #define LCD_8BITMODE 0x10 | |
| #define LCD_4BITMODE 0x00 | |
| #define LCD_2LINE 0x08 | |
| #define LCD_1LINE 0x00 | |
| #define LCD_5x10DOTS 0x04 | |
| #define LCD_5x8DOTS 0x00 | |
| class rgb_lcd : public Print | |
| { | |
| public: | |
| rgb_lcd(); | |
| void begin(uint8_t cols, uint8_t rows, uint8_t charsize = LCD_5x8DOTS); | |
| void clear(); | |
| void home(); | |
| void noDisplay(); | |
| void display(); | |
| void noBlink(); | |
| void blink(); | |
| void noCursor(); | |
| void cursor(); | |
| void scrollDisplayLeft(); | |
| void scrollDisplayRight(); | |
| void leftToRight(); | |
| void rightToLeft(); | |
| void autoscroll(); | |
| void noAutoscroll(); | |
| void createChar(uint8_t, uint8_t[]); | |
| void setCursor(uint8_t, uint8_t); | |
| virtual size_t write(uint8_t); | |
| void command(uint8_t); | |
| // color control | |
| void setRGB(unsigned char r, unsigned char g, unsigned char b); // set rgb | |
| void setPWM(unsigned char color, unsigned char pwm){setReg(color, pwm);} // set pwm | |
| void setColor(unsigned char color); | |
| void setColorAll(){setRGB(0, 0, 0);} | |
| void setColorWhite(){setRGB(255, 255, 255);} | |
| using Print::write; | |
| private: | |
| void send(uint8_t, uint8_t); | |
| void setReg(unsigned char addr, unsigned char dta); | |
| uint8_t _displayfunction; | |
| uint8_t _displaycontrol; | |
| uint8_t _displaymode; | |
| uint8_t _initialized; | |
| uint8_t _numlines,_currline; | |
| }; | |
| #endif |
| /* Thermoscope - a 16x2 LCD thermometer with S/M/H history graphs. See | |
| http://www.instructables.com/id/Thermoscope-16x2-LCD-thermometer-with-SMH-graphs | |
| Done using[1]: | |
| * arduino[2] | |
| * grove shield | |
| * Grove RGB LCD (goes to I2C) | |
| * Grove Temperature sensor (goes to A0) | |
| * Grove button (goes to D4) | |
| Button toggles between Second/Minute/Hour modes. | |
| Current temperature (top left) is updated every second (regardless of mode). | |
| Graph shows normalized average temperatures for last [up to] 8 H/M/S. | |
| Second line shows minimum-maximum values for the normalized graph to give | |
| a sense of scale (e.g. if they're 0.1 degrees apart, what you see is noise). | |
| This code (and the datalogger library) can also be useful as a | |
| "poor people's scope" for other kinds of sensors. | |
| ---- | |
| [1] All the components come from a seestudio starter pack. Tips for alternatives: | |
| * LCD: code should work with any i2c or parallel 16x2 lcd display out there. | |
| Once you find the right class, they have very similar methods. | |
| * Temperature sensor: I'm sure there's something similar out there, | |
| but analog2celsius might be different depending on the thermistor. | |
| * Button: if you're using a "raw" momentary switch, also connect its output | |
| to gnd via a 10K-ohm resistor (pull-down) to make sure we get a LOW | |
| [and not noise] when the button isn't pressed. | |
| There are also buttons with a built-in resistor (they have 3 pins: | |
| gnd, vcc, and signal). | |
| [2] Note that if you have an old arduino without the sda and scl pins | |
| you'll need patch them to a4 and a5 on the grove shield's headers. See photo at | |
| http://www.instructables.com/id/Thermoscope-16x2-LCD-thermometer-with-SMH-graphs | |
| */ | |
| #include <Wire.h> | |
| #include "rgb_lcd.h" | |
| #include "datalogger.h" | |
| rgb_lcd lcd; | |
| const unsigned char DEGSYMBOL = 7; | |
| byte degSymbol[8] = { | |
| 0b00000, | |
| 0b01100, | |
| 0b10010, | |
| 0b10010, | |
| 0b01100, | |
| 0b00000, | |
| 0b00000, | |
| 0b00000 | |
| }; | |
| byte barSymbol[8]; // temporary for generating bar chars | |
| const int SECONDS = 0; | |
| const int MINUTES = 1; | |
| const int HOURS = 2; | |
| float seconds[60]; | |
| float minutes[60]; | |
| float hours[24]; // Actually, we could do with 8, but 24 is a sensible number ;) | |
| DataLogger loggers[] = { DataLogger(seconds,60), DataLogger(minutes,60), DataLogger(hours,24) }; | |
| char *logger_names[] = { "S","M","H" }; | |
| int current_logger; | |
| unsigned long sample; | |
| const int BUTTON_PIN = 4; | |
| bool button_was_on; | |
| void setup() | |
| { | |
| initLcd(lcd); | |
| lcd.clear(); | |
| sample = 0; | |
| current_logger = SECONDS; | |
| button_was_on = false; | |
| } | |
| void loop() { | |
| int a = analogRead(0); | |
| float t = analog2Celsius(a); | |
| sample++; | |
| lcd.setCursor(0, 0); | |
| lcd.print(t); | |
| lcd.write(DEGSYMBOL); | |
| lcd.print(" "); // Voodoo against 10->9 ;) | |
| loggers[SECONDS].push(t); | |
| if (!(sample%60)) { | |
| loggers[SECONDS].aggregate(60); | |
| loggers[MINUTES].push(loggers[SECONDS].agg_avg); | |
| if (!(sample%3600)) { | |
| loggers[MINUTES].aggregate(60); | |
| loggers[HOURS].push(loggers[MINUTES].agg_avg); | |
| } | |
| } | |
| lcd.setCursor(7,0); | |
| lcd.print(logger_names[current_logger]); | |
| lcd.setCursor(8,0); | |
| if (loggers[current_logger].nsamples) { | |
| // Find min/max for graph normalization | |
| loggers[current_logger].aggregate(8); | |
| long min1k = 1000*loggers[current_logger].agg_min; | |
| long max1k = 1000*loggers[current_logger].agg_max; | |
| // Draw graph | |
| loggers[current_logger].popReset(); | |
| for (int i = 0 ; i<8 ; i++) { | |
| float value = loggers[current_logger].pop(); | |
| lcd.setCursor(15-i, 0); | |
| if (value>0) { | |
| lcd.write((unsigned char)map((long)(1000*value),min1k,max1k,0,6)); | |
| } else { // We've started less than 8 S/M/H ago. This one's empty. | |
| lcd.print('-'); | |
| } | |
| } | |
| lcd.setCursor(0, 1); | |
| lcd.print(loggers[current_logger].agg_min); | |
| lcd.print("-"); | |
| lcd.print(loggers[current_logger].agg_max); | |
| lcd.write(DEGSYMBOL); | |
| lcd.print(" "); // Voodoo against 10->9 ;) | |
| } else { | |
| lcd.setCursor(8,0); | |
| lcd.print("--------"); | |
| lcd.setCursor(0,1); | |
| lcd.print("No data yet..."); | |
| } | |
| // Countdown for M/H modes | |
| lcd.setCursor(14, 1); | |
| if (current_logger) { | |
| int unit = pow(60,current_logger-1); | |
| int countdown = 60-(sample/unit)%60; | |
| if (countdown<10) lcd.print(" "); | |
| lcd.print(countdown); | |
| } else { | |
| lcd.print(" "); | |
| } | |
| for (int i=0; i<10; i++) { | |
| checkButton(); | |
| delay(100); | |
| } | |
| } | |
| float analog2Celsius(int a) { | |
| // Based on http://www.instructables.com/id/grove-minimal-thermometer/ | |
| const int B=3975; // Value of the thermistor | |
| float resistance=(float)(1023-a)*10000/a; | |
| return 1/(log(resistance/10000)/B+1/298.15)-273.15; | |
| } | |
| void initLcd(rgb_lcd lcd) { | |
| lcd.begin(16, 2); | |
| //lcd.setRGB(63, 63, 63); // Don't burn photos ;) | |
| lcd.createChar(DEGSYMBOL, degSymbol); | |
| for (int i = 0 ; i<8 ; i++) barSymbol[i] = 0b00000; | |
| lcd.createChar(0,barSymbol); | |
| for (int level = 0 ; level<7 ; level++) { | |
| barSymbol[7-level] = 0b01110; | |
| lcd.createChar(level,barSymbol); | |
| } | |
| } | |
| void checkButton(void) { | |
| int b = digitalRead(BUTTON_PIN); | |
| if (b) { | |
| if (!button_was_on) current_logger = (current_logger+1)%3; | |
| button_was_on = true; | |
| } else { | |
| button_was_on = false; | |
| } | |
| } |