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 // ----------------------------------------------