/* xsns_43_hre.ino - Badger HR-E Water Meter Encoder interface Copyright (C) 2021 Jon Little This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_HRE /*********************************************************************************************\ * HR-E LCD Water meter register interface * * https://www.badgermeter.com/business-lines/utility/high-resolution-lcd-encoders-hr-e-lcd/ * Source: Jon Little, https://github.com/burundiocibu/particle/blob/master/water_meter/src/HRE_Reader.cpp * * This code marches the bits out the data line as ASCII characters with the form * KG44?Q45484=0444444V;RB000000022;IB018435683 * where the RB...; is the miligalons used * * Note that this sensor takes a _long_ time to read. 62 bits * 4 ms/bit for the * sync sequence plus 46 bytes * 40 ms/byte = 2088 ms minimum. If we aren't alligned * to the sync sequence, it could be almost twice that. * To keep from bogging the kernel down, we read 8 bits at a time on the 50 ms callback. * It will take seconds to discover if the device is there. * * In lieu of an actual schematic to describe the electrical interface, here is a description: * * hre_clock_pin: drives the power/clock for the water meter through a 1k resister to * the base of a pnp transistor * hre_data_pin: is the data and has a 1 k pulldown * * The pnp transitor has the collector connected to the power/clock and is pulled up * to +5 via a 1 k resistor. * The emitter is connected to ground * \*********************************************************************************************/ #define XSNS_43 43 enum hre_states { hre_idle, // Initial state, hre_sync, // Start search for sync sequence hre_syncing, // Searching for sync sequence hre_read, // Start reading data block hre_reading, // Reading data hre_sleep, // Start sleeping hre_sleeping // pausing before reading again }; hre_states hre_state = hre_idle; float hre_usage = 0; // total water usage, in gal float hre_rate = 0; // flow rate, in gal/min uint32_t hre_usage_time = 0; // uptime associated with hre_usage and hre_rate int hre_read_errors = 0; // total number of read errors since boot bool hre_good = false; // The settling times here were determined using a single unit hooked to a scope int hreReadBit() { digitalWrite(Pin(GPIO_HRE_CLOCK), HIGH); delay(1); int bit = digitalRead(Pin(GPIO_HRE_DATA)); digitalWrite(Pin(GPIO_HRE_CLOCK), LOW); delay(1); return bit; } // With the times in the HreReadBit routine, a characer will take // 20 ms plus io time. char hreReadChar(int &parity_errors) { // start bit hreReadBit(); unsigned ch=0; int sum=0; for (uint32_t i=0; i<7; i++) { int b = hreReadBit(); ch |= b << i; sum += b; } // parity if ( (sum & 0x1) != hreReadBit()) parity_errors++; // stop bit hreReadBit(); return ch; } void hreInit(void) { hre_read_errors = 0; hre_good = false; pinMode(Pin(GPIO_HRE_CLOCK), OUTPUT); pinMode(Pin(GPIO_HRE_DATA), INPUT); // Note that the level shifter inverts this line and we want to leave it // high when not being read. digitalWrite(Pin(GPIO_HRE_CLOCK), LOW); hre_state = hre_sync; } void hreEvery50ms(void) { static int sync_counter = 0; // Number of sync bit reads static int sync_run = 0; // Number of consecutive '1's read static uint32_t curr_start = 0; // uptime when entered hre_reading for current read static int read_counter = 0; // number of bytes in the current read static int parity_errors = 0; // Number of parity errors in current read static char buff[46]; // 8 char and a term static char ch; static size_t i; switch (hre_state) { case hre_sync: if (TasmotaGlobal.uptime < 10) break; sync_run = 0; sync_counter = 0; hre_state = hre_syncing; AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HRE "hre_state:hre_syncing")); break; case hre_syncing: // Find the header, a string of 62 '1's // Since each bit taks 2 ms, we just read 20 bits at a time for (uint32_t i=0; i<20; i++) { if (hreReadBit()) sync_run++; else sync_run = 0; if (sync_run == 62) { hre_state = hre_read; break; } sync_counter++; } // If the meter doesn't get in sync within 1000 bits, give up for now if (sync_counter > 1000) { hre_state = hre_sleep; AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HRE D_ERROR)); } break; // Start reading the data block case hre_read: AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HRE "sync_run:%d, sync_counter:%d"), sync_run, sync_counter); read_counter = 0; parity_errors = 0; curr_start = TasmotaGlobal.uptime; memset(buff, 0, sizeof(buff)); hre_state = hre_reading; AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HRE "hre_state:hre_reading")); // So this is intended to fall through to the hre_reading section. // it seems that if there is much of a delay between getting the sync // bits and starting the read, the HRE won't output the message we // are looking for... case hre_reading: // Read two characters at a time... buff[read_counter++] = hreReadChar(parity_errors); buff[read_counter++] = hreReadChar(parity_errors); if (read_counter == 46) { AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HRE "pe:%d, re:%d, buff:%s"), parity_errors, hre_read_errors, buff); if (parity_errors == 0) { float curr_usage; curr_usage = 0.01f * atol(buff+24); // useage in gal if (hre_usage_time) { double dt = 1.666e-2 * (curr_start - hre_usage_time); // dt in minutes hre_rate = (curr_usage - hre_usage)/dt; // gallons/min } hre_usage = curr_usage; hre_usage_time = curr_start; hre_good = true; hre_state = hre_sleep; } else { hre_read_errors++; hre_state = hre_sleep; } } break; case hre_sleep: hre_usage_time = curr_start; hre_state = hre_sleeping; AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HRE "hre_state:hre_sleeping")); case hre_sleeping: // If there isn't some delay between readings, rate calculations // aren't as accurate. 27 seconds will give about a 30 second refresh rate if (TasmotaGlobal.uptime - hre_usage_time >= 27) hre_state = hre_sync; } } void hreShow(boolean json) { if (!hre_good) return; const char *id = "HRE"; char usage[16]; char rate[16]; dtostrfd(hre_usage, 2, usage); dtostrfd(hre_rate, 3, rate); if (json) { ResponseAppend_P(JSON_SNS_GNGPM, id, usage, rate); #ifdef USE_WEBSERVER } else { WSContentSend_PD(HTTP_SNS_GALLONS, id, usage); WSContentSend_PD(HTTP_SNS_GPM, id, rate); #endif // USE_WEBSERVER } } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xsns43(uint32_t function) { // If we don't have pins assigned give up quickly. if (!PinUsed(GPIO_HRE_CLOCK) || !PinUsed(GPIO_HRE_DATA)) { return false; } switch (function) { case FUNC_INIT: hreInit(); break; case FUNC_EVERY_50_MSECOND: hreEvery50ms(); break; case FUNC_EVERY_SECOND: break; case FUNC_JSON_APPEND: hreShow(1); break; #ifdef USE_WEBSERVER case FUNC_WEB_SENSOR: hreShow(0); break; #endif // USE_WEBSERVER } return false; } #endif // USE_HRE