The project displays the water usage for a lawn irrigation system. Our new landscaping uses a Rachio Pro smart irrigation system (http://rachio.com/pro). The system is very useful and has a smart phone interface that includes a display of the water usage. This water usage is an estimate based on the time the sprinklers are on and the type of sprinklers selected during the installation portion. To get a more accurate reading we installed a Dwyer water meter inline with the irrigation. The meter provides an analog readout of the cumulative water usage directly on the meter. It also has a wired output which we connected to our project to remotely monitor the water usage.
Our initial prototype is using an Arduino Uno board and a 1.5″ OLED from Adafruit. We have a second prototype using an Adafruit Trinket Pro in place of the Uno board. Both use a DS3231 real-time clock module to keep time. This early version has some basic datalogging to the SD card. A few times a minute is writes the date, time and water measurement. On power up, it reads these values taking the last in the list as the current value.
The initial testing is showing a discrepancy between the reported water usage and what is shown on the measure dial. The circuit appears to be picking up some extra readings making for an over count.
Given it’s a prototype there is still a bit of work to do. There are also several hardcoded values, such as the previous year’s usage. It also requires the SD card and RTC to be initialized properly.
/*
Program: WaterMeter
Description: Displays the water usage. The dwyer water meter provides a grounding pulse for every 0.1 gallons of water that flows.
The circuit reads the water usage pulses and displays various cumulative values.
Circuit:
* 1.5" 128x128 OLED w/microSD holder, $39.95, https://www.adafruit.com/products/1431
* RTC: $8.99, https://www.amazon.com/gp/product/B00HF4NUSS/ref=od_aui_detailpages00?ie=UTF8&psc=1
* Adafruit Trinket Pro $9.95, https://learn.adafruit.com/introducing-pro-trinket/overview
* Dwyer Multi-Jet Water Meter w/ Pulsed Output, WMT2-A-C-04, 1" NPT, 50 GPM, Brass Body, $126,
* (4) 2.5 x 2 bolts and nuts
* (4) 1" wood screens
* Button -- used for debug to simulate meter
_r01 used SPI 128x64 Monochrome OLED screen
_r02 added RTC, 128x128 1.5" Color OLED
_r03 adjusted display position, add SD card but conflicts with GFX
_r04 added internal interrupt, glitchy
table of additional totals and previous numbers, psuedo history
_r05 removed WiFi code
_r06 added SD card, logo splash, RTC isn't working now
_r07 first attempt to write and read to SD card, writes fixed string to SD at setup, RTC fixed
_r08 initializes data from SD files and writes date
SD_r02 datalog written with date, csv read in
SD_r03 datalog written every 11 minutes
SD_r04 daily, monthly cumulatives added
SD_r05 added weekly cumulatives
TODO LIST:
- clean of differences in prototype hardware
- initial SD card write to put column headers
- clean up GUI, calculate last year averages and totals
- verify is interrupt pin debounce
- create a initial setup (time, ytd, current) currently hardcoded initial value
- how to handle power loss
- datalog info, with backup/startup; use timer to periodically record w/timestamp
- networking; current display and download SD card files
- move some code to .h file
- add display off/on button
- add power management sleep
Pin Use:
A0
A1
A2
A3
A4 RTC I2C DATA
A5 RTC I2C CLOCK
D0
D1
D2 EXTERNAL INPUT - METER
D3
D4 TFT SD CARD CS
D5 TFT DC
D6
D7
D8
D9 TFT RST
D10 TFT CS
D11 TFT MOSI
D12 TFT MISO
D13 TFT CLK
SDA
SDD
RTC module wiring - look for text on the board,
+ power (5V)
D SDA
C SDC
NC
- ground
Partial diagram here: http://www.l8ter.com/wp-content/uploads/2011/09/DS3231fritz.png
*/
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1351.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>
#define DS3231_I2C_ADDRESS 0x68
// You can use any (4 or) 5 pins
#define sclk 13
#define miso 12
#define mosi 11
#define cs 10
#define rst 9
#define dc 8 // fix this 8 for Trinket 5 for Arduino
#define SD_CS 4
// Color definitions
// 5b R, 6b G, 5b B
#define SHIRT_BLUE 0x3AB5 // Blue
#define DARK_GREEN 0x5E0 // Dark Green
//#define LT_GREY 0xCE9A // Light grey
#define DARK_GREY 0x5AEB
//#define GREY 0x7BEF
#define GREY 0x2104
#define BLACK 0x0000
#define BLUE 0x001F
#define RED 0xF800
#define GREEN 0x07E0
#define CYAN 0x07FF
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#define WHITE 0xFFFF
#define LOGGING_FREQ_SECONDS 600
#define MAX_SLEEP_ITERATIONS LOGGING_FREQ_SECONDS / 8
int sleepIterations = 0;
volatile bool watchdogActivated = false;
float CumWater;
float DailyH20;
float WeeklyH20;
float MonthlyH20;
float YTDH20;
//JPC TODO IF APPLICABLE
float LY_DailyH20 = 199.5; // last years value
float LY_WeeklyH20 = 1496.0;
float LY_MonthlyH20 = 5984;
float LY_YTDH20 = 74052.0;
int x, y = 0;
const int TableX = 3;
const int TableY = 69;
// variables for interrupt pin portion
const byte interruptPin = 2;
volatile byte state = LOW;
// variables for RTC
byte prev_second, prev_minute, prev_hour, prev_dayOfMonth, prev_month, prev_year = -1;
// display
Adafruit_SSD1351 tft = Adafruit_SSD1351(cs, dc, rst);
// bitmap logo splash screen
File bmpFile;
int bmpWidth, bmpHeight;
uint8_t bmpDepth, bmpImageoffset;
File dataFile;
int len = 3;
byte c_second, c_minute, c_hour, c_dayOfWeek, c_dayOfMonth, c_month, c_year;
byte p_second, p_minute, p_hour, p_dayOfWeek, p_dayOfMonth, p_month, p_year;
// --------------------------------------
// Watchdog timer interrupt ISR
ISR(WDT_vect)
{
watchdogActivated = true;
}
// end Watchdog ISR
// --------------------------------------
// --------------------------------------
void setup() {
// setup watchdog timer
noInterrupts();
MCUSR &= ~(1<<WDRF); // MCU status register, watchdog reset
WDTCSR |= (1<<WDCE) | (1<<WDE); // watchdog control register, WDCE WDE bits
WDTCSR = (1<<WDP0) | (1<<WDP3); // watchdog control register, presalcer 8s is largest
WDTCSR |= (1<<WDIE); // watchdog control register, enable no reset
interrupts();
Serial.begin(9600); // initialize serial communication
pinMode(cs, OUTPUT);
digitalWrite(cs, HIGH);
// setup external interrupt pin
pinMode(interruptPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(interruptPin), meter, FALLING);
Wire.begin();
// setup real time clock
//Initial use of RTC set the time, uncomment to set time, then comment out to keep RTC time
// DS3231 seconds, minutes, hours, day, date, month, year
//setDS3231time(45,59,23,7,31,12,16); //12/31/2016 Sat 12:59:45pm
//setDS3231time(15,1,9,6,6,05,17); //5/6/2017 Sat 9:01am
//setDS3231time(50,5,0,3,23,05,17); //5/23/2017 Mon 12:04:50am
// setup display
tft.begin();
tft.setRotation(2);
tft.fillScreen(BLUE);
tft.setFont();
tft.setTextSize(1);
tft.setTextColor(WHITE);
tft.setCursor(0, 0);
//tft.println(F("Initializing SD card"));
if (!SD.begin(SD_CS)) {
tft.println(F("SD failed!"));
return;
}
//tft.println(F("SD OK!"));
//hardcoded splash screen file, won't work if SD card not preloaded with image
//commented out to save space, code was getting too big for Trinket
// bmpDraw("primo128.bmp", 0, 0);
// delay(3000);
String inString = ""; // string to hold input
String dDate = "";
String dTime = "";
tft.println(F("Reading previous data"));
tft.print(F("Please wait..."));
int index = 0; // track place in .csv row: date, time, cum, daily, week, month, ytd
int records = 0;
int modulo = 0;
dataFile = SD.open("datalog.csv");
if (!dataFile) { tft.println("SD open error"); return; }
while (dataFile.available()) {
int inChar = dataFile.read();
if ((inChar != '\n') && (inChar != ',')) {
inString += (char)inChar;
}
else {
//tft.println(inString);
switch(index) {
case 0:
dDate = inString;
break;
case 1:
dTime = inString;
break;
case 2:
CumWater = inString.toFloat();
len = sizeof(inString);
break;
case 3:
DailyH20 = inString.toFloat();
break;
case 4:
WeeklyH20 = inString.toFloat();
break;
case 5:
MonthlyH20 = inString.toFloat();
break;
case 6:
YTDH20 = inString.toFloat();
index = -1; // resets back to 0
break;
}
index++;
records++;
modulo++;
if (modulo == 100) {
tft.print(F("."));
//tft.setCursor(50, 20);
//tft.print(F("___________"));
//tft.setCursor(50, 20);
//tft.print(records);
modulo = 0;
}
inString = "";
}
}
dataFile.close();
//tft.setCursor(0, 20);
tft.println("");
tft.print(F("records: "));
tft.println(records);
tft.print(F("gallons: "));
tft.println(CumWater);
delay(2000);
// print initial screen
tft.fillScreen(BLACK);
tft.setFont(&FreeSansBold9pt7b);
tft.setTextSize(1);
tft.setTextColor(BLUE,BLACK);
tft.setCursor(10, 15);
tft.print(F("Water Usage"));
tft.drawRect(TableX-2, TableY-2, 125, 45, GREY);
tft.drawFastVLine(TableX+32,TableY-2,45, GREY);
tft.drawFastVLine(TableX+77,TableY-2,45, GREY);
tft.drawFastHLine(TableX-2,TableY+9,125, GREY);
tft.drawFastHLine(TableX-2,TableY+20,125, GREY);
tft.drawFastHLine(TableX-2,TableY+30,125, GREY);
tft.setFont();
tft.setTextSize(1);
tft.setTextColor(DARK_GREY);
tft.setCursor(TableX, TableY); tft.println(F("Day"));
tft.setCursor(TableX, TableY+11); tft.println(F("Week"));
tft.setCursor(TableX, TableY+22); tft.println(F("Month"));
tft.setCursor(TableX, TableY+33); tft.println(F("Year"));
//Very first time using SD card
//CumWater = 7019.31; // initial value gallons as of 5/6/17
//CumWater = 10175.55; // initial value gallons as of 5/18/17
//CumWater = 10985.73; // initial value gallons as of 5/21/17
//CumWater = 11766.05; // initial value gallons as of 5/22/17
// initial writing of data
Datalog();
readDS3231time(&c_second, &c_minute, &c_hour, &c_dayOfWeek, &c_dayOfMonth, &c_month, &c_year);
p_second = c_second; p_minute = c_minute; p_hour = c_hour;
p_dayOfWeek = c_dayOfWeek; p_dayOfMonth = c_dayOfMonth; p_month = c_month, p_year = c_year;
RefreshScreen();
}
// ------------------------------------------------------
// ISR: when interrupt on pin increment water usage
void meter() {
state = !state;
CumWater = CumWater + 0.1;
DailyH20 = DailyH20 + 0.1;
WeeklyH20 = WeeklyH20 + 0.1;
MonthlyH20 = MonthlyH20 + 0.1;
YTDH20 = YTDH20 + 0.1;
}
// ------------------------------------------------------
// ------------------------------------------------------
void loop() {
if (watchdogActivated) {
watchdogActivated = false;
// every 8 seconds
RefreshScreen();
sleepIterations +=1;
if (sleepIterations >= MAX_SLEEP_ITERATIONS) {
sleepIterations = 0;
// every target time
readDS3231time(&c_second, &c_minute, &c_hour, &c_dayOfWeek, &c_dayOfMonth, &c_month, &c_year);
// test if day is same, if not reset DailyH20 = 0
if(c_dayOfMonth != p_dayOfMonth) {
DailyH20 = 0;
p_dayOfWeek = c_dayOfWeek;
if((c_dayOfWeek != p_dayOfWeek) && (c_dayOfWeek == 1)) {
WeeklyH20 = 0;
p_dayOfWeek = c_dayOfWeek;
}
if(c_month != p_month) {
MonthlyH20 = 0;
p_month = c_month;
if(c_year != p_year) {
YTDH20 = 0;
p_year = c_year;
}
}
}
Datalog();
}
} // watchdogActivated
} // end of main
// ------------------------------------------------------
// ------------------------------------------------------
void RefreshScreen() {
// refresh screen very eight seconds
tft.setFont();
x = 128-((len+1)*13); y = 30; // length of CumWater computed previously
tft.setCursor(x, y);
tft.setTextSize(2);
tft.setTextColor(WHITE,BLACK);
tft.print(CumWater, 1);
x= 60; y = TableY;
tft.setTextSize(1);
tft.setTextColor(DARK_GREY,BLACK);
tft.setCursor(TableX+37, TableY); tft.print(DailyH20,0);
tft.setCursor(TableX+37, TableY+11); tft.print(WeeklyH20,0);
tft.setCursor(TableX+37, TableY+22); tft.print(MonthlyH20,0);
tft.setCursor(TableX+37, TableY+33); tft.print(YTDH20,0);
tft.setTextSize(1);
tft.setTextColor(GREY,BLACK);
tft.setCursor(TableX+82, TableY); tft.print(LY_DailyH20,0);
tft.setCursor(TableX+82, TableY+11); tft.print(LY_WeeklyH20,0);
tft.setCursor(TableX+82, TableY+22); tft.print(LY_MonthlyH20,0);
tft.setCursor(TableX+82, TableY+33); tft.print(LY_YTDH20,0);
displayTime(); // put time on TFT
return;
}
// ------------------------------------------------------
// ------------------------------------------------------
void readDS3231time(byte *second, byte *minute, byte *hour, byte *dayOfWeek, byte *dayOfMonth, byte *month, byte *year)
{
Wire.beginTransmission(DS3231_I2C_ADDRESS);
Wire.write(0); // set DS3231 register pointer to 00h
Wire.endTransmission();
Wire.requestFrom(DS3231_I2C_ADDRESS, 7);
// request seven bytes of data from DS3231 starting from register 00h
*second = bcdToDec(Wire.read() & 0x7f);
*minute = bcdToDec(Wire.read());
*hour = bcdToDec(Wire.read() & 0x3f);
*dayOfWeek = bcdToDec(Wire.read());
*dayOfMonth = bcdToDec(Wire.read());
*month = bcdToDec(Wire.read());
*year = bcdToDec(Wire.read());
return;
}
// end of readDS3231time()
// ----------------------------------------------------
// ----------------------------------------------------
// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{
return( (val/10*16) + (val%10) );
}
// ----------------------------------------------------
// ----------------------------------------------------
// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{
return( (val/16*10) + (val%16) );
}
// ----------------------------------------------------
// ----------------------------------------------------
// displayTime function
void displayTime()
{
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
// retrieve data from DS3231
readDS3231time(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month,
&year);
int16_t x, y;
int16_t x1, y1;
uint16_t w, h;
if (prev_dayOfMonth != dayOfMonth) // only write if day changes
{
prev_dayOfMonth = dayOfMonth;
tft.setFont();
tft.setTextSize(1);
x = 5; y = 50;
// x = 5; y = 120;
tft.getTextBounds(F("12/30/2016^^^12:12pm"), x-5, y-5, &x1, &y1, &w, &h);
tft.fillRect(x1,y1,w,h, BLACK);
tft.setCursor(x, y);
tft.setTextColor(YELLOW,BLACK);
tft.print(" ");
tft.setCursor(x, y);
tft.print(month);
tft.print("/");
tft.print(dayOfMonth);
tft.print("/20");
tft.print(year);
}
tft.setFont();
tft.setTextColor(YELLOW,BLACK);
tft.setTextSize(1);
x = 70; //TODO MAKE THIS CLEANER, REFERENCE PRIOR
tft.setCursor(x, y);
if (hour > 12) {
tft.print(hour - 12);
}
else {
if (hour == 0) {
tft.print(12);
}
else {
tft.print(hour);
}
}
tft.print(":");
if (minute < 10) {
tft.print("0");
}
tft.print(minute);
tft.setFont();
if (hour > 11) {
tft.print(" pm");
}
else {
tft.print(" am");
}
return;
}
// END OF DISPLAY TIME
// ----------------------------------------------------------------
// ----------------------------------------------------------------
void setDS3231time(byte second, byte minute, byte hour, byte dayOfWeek, byte dayOfMonth, byte month, byte year)
{
// sets time and date data to DS3231
Wire.beginTransmission(DS3231_I2C_ADDRESS);
Wire.write(0); // set next input to start at the seconds register
Wire.write(decToBcd(second)); // set seconds
Wire.write(decToBcd(minute)); // set minutes
Wire.write(decToBcd(hour)); // set hours
Wire.write(decToBcd(dayOfWeek)); // set day of week (1=Sunday, 7=Saturday)
Wire.write(decToBcd(dayOfMonth)); // set date (1 to 31)
Wire.write(decToBcd(month)); // set month
Wire.write(decToBcd(year)); // set year (0 to 99)
Wire.endTransmission();
return;
}
// ----------------------------------------------------------------
/*
// ----------------------------------------------
// This function opens a Windows Bitmap (BMP) file and
// displays it at the given coordinates. It's sped up
// by reading many pixels worth of data at a time
// (rather than pixel by pixel). Increasing the buffer
// size takes more of the Arduino's precious RAM but
// makes loading a little faster. 20 pixels seems a
// good balance.
#define BUFFPIXEL 20
void bmpDraw(char *filename, uint8_t x, uint8_t y) {
File bmpFile;
int bmpWidth, bmpHeight; // W+H in pixels
uint8_t bmpDepth; // Bit depth (currently must be 24)
uint32_t bmpImageoffset; // Start of image data in file
uint32_t rowSize; // Not always = bmpWidth; may have padding
uint8_t sdbuffer[3*BUFFPIXEL]; // pixel buffer (R+G+B per pixel)
uint8_t buffidx = sizeof(sdbuffer); // Current position in sdbuffer
boolean goodBmp = false; // Set to true on valid header parse
boolean flip = true; // BMP is stored bottom-to-top
int w, h, row, col;
uint8_t r, g, b;
uint32_t pos = 0, startTime = millis();
if((x >= tft.width()) || (y >= tft.height())) return;
Serial.println();
Serial.print("Loading image '");
Serial.print(filename);
Serial.println('\'');
// Open requested file on SD card
if ((bmpFile = SD.open(filename)) == NULL) {
Serial.print("File not found");
return;
}
// Parse BMP header
if(read16(bmpFile) == 0x4D42) { // BMP signature
Serial.print("File size: "); Serial.println(read32(bmpFile));
(void)read32(bmpFile); // Read & ignore creator bytes
bmpImageoffset = read32(bmpFile); // Start of image data
Serial.print("Image Offset: "); Serial.println(bmpImageoffset, DEC);
// Read DIB header
Serial.print("Header size: "); Serial.println(read32(bmpFile));
bmpWidth = read32(bmpFile);
bmpHeight = read32(bmpFile);
if(read16(bmpFile) == 1) { // # planes -- must be '1'
bmpDepth = read16(bmpFile); // bits per pixel
Serial.print("Bit Depth: "); Serial.println(bmpDepth);
if((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed
goodBmp = true; // Supported BMP format -- proceed!
Serial.print("Image size: ");
Serial.print(bmpWidth);
Serial.print('x');
Serial.println(bmpHeight);
// BMP rows are padded (if needed) to 4-byte boundary
rowSize = (bmpWidth * 3 + 3) & ~3;
// If bmpHeight is negative, image is in top-down order.
// This is not canon but has been observed in the wild.
if(bmpHeight < 0) {
bmpHeight = -bmpHeight;
flip = false;
}
// Crop area to be loaded
w = bmpWidth;
h = bmpHeight;
if((x+w-1) >= tft.width()) w = tft.width() - x;
if((y+h-1) >= tft.height()) h = tft.height() - y;
for (row=0; row<h; row++) { // For each scanline...
tft.goTo(x, y+row);
// Seek to start of scan line. It might seem labor-
// intensive to be doing this on every line, but this
// method covers a lot of gritty details like cropping
// and scanline padding. Also, the seek only takes
// place if the file position actually needs to change
// (avoids a lot of cluster math in SD library).
if(flip) // Bitmap is stored bottom-to-top order (normal BMP)
pos = bmpImageoffset + (bmpHeight - 1 - row) * rowSize;
else // Bitmap is stored top-to-bottom
pos = bmpImageoffset + row * rowSize;
if(bmpFile.position() != pos) { // Need seek?
bmpFile.seek(pos);
buffidx = sizeof(sdbuffer); // Force buffer reload
}
// optimize by setting pins now
for (col=0; col<w; col++) { // For each pixel...
// Time to read more pixel data?
if (buffidx >= sizeof(sdbuffer)) { // Indeed
bmpFile.read(sdbuffer, sizeof(sdbuffer));
buffidx = 0; // Set index to beginning
}
// Convert pixel from BMP to TFT format, push to display
b = sdbuffer[buffidx++];
g = sdbuffer[buffidx++];
r = sdbuffer[buffidx++];
tft.drawPixel(x+col, y+row, tft.Color565(r,g,b));
// optimized!
//tft.pushColor(tft.Color565(r,g,b));
} // end pixel
} // end scanline
Serial.print("Loaded in ");
Serial.print(millis() - startTime);
Serial.println(" ms");
} // end goodBmp
}
}
bmpFile.close();
if(!goodBmp) Serial.println("BMP format not recognized.");
}
// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.
uint16_t read16(File f) {
uint16_t result;
((uint8_t *)&result)[0] = f.read(); // LSB
((uint8_t *)&result)[1] = f.read(); // MSB
return result;
}
uint32_t read32(File f) {
uint32_t result;
((uint8_t *)&result)[0] = f.read(); // LSB
((uint8_t *)&result)[1] = f.read();
((uint8_t *)&result)[2] = f.read();
((uint8_t *)&result)[3] = f.read(); // MSB
return result;
}
// end bmpDraw()
// ----------------------------------------------
*/
// ----------------------------------------------
void Datalog()
{
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
// retrieve data from DS3231
readDS3231time(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);
File dataFile = SD.open("datalog.csv",FILE_WRITE);
if (dataFile) {
dataFile.print(year);
dataFile.print("/");
dataFile.print(month);
dataFile.print("/");
dataFile.print(dayOfMonth);
dataFile.print(",");
dataFile.print(hour);
dataFile.print(":");
dataFile.print(minute);
dataFile.print(",");
dataFile.print(CumWater);
dataFile.print(",");
dataFile.print(DailyH20);
dataFile.print(",");
dataFile.print(WeeklyH20);
dataFile.print(",");
dataFile.print(MonthlyH20);
dataFile.print(",");
dataFile.println(YTDH20);
dataFile.close();
}
else {
Serial.println("error opening datalog.txt");
}
return;
}
// END OF DATALOG FUNCTION
// ----------------------------------------------

