//FILE: MQTT_Gallon_ESP32_TM1637[Arduino]
//DATE: 01/15/2026
//AUTHORS: RFGabriel & ChatGPT
//DESCRIPTION: Counts gallons of water provided by a water meter that closes a switch
// for each gallon. The current gallon count represents gallons since
// midnight. At midnight, the gallon count is zeroed. Total gallons for
// the day are kept in MQTT which receives a gallon count value every
// x seconds. Since the gallons per minute are not expected to total more
// than 5, it would be useless to report a count more frequently than
// 60/5 or every 12 seconds.
// Two LEDs are provided, one that indicates the Keep-Alive signal and
// the other that MQTT is being reported, else showing WiFi status.
// A reset button is provided that will zero out the gallon total
// The display is a TM1637 module containing 4 7-segment LEDs
// ===== ESP32: Non-blocking WiFi+MQTT + TM1637 + pulse counter + DST midnight reset =====
#include <WiFi.h>
#include <PubSubClient.h>
#include <TM1637Display.h>
#include <time.h>
// ------------ Pins ------------
const uint8_t PIN_PULSE = 14;
const uint8_t PIN_TM_CLK = 4;
const uint8_t PIN_TM_DIO = 5;
const uint8_t PIN_LED_GREEN = 2; // active LOW keep-alive
const uint8_t PIN_LED_RED = 12; // pulse blink
const uint8_t PIN_LED_YELLOW = 13; // status blink (WiFi/MQTT)
const uint8_t PIN_BTN_USER = 16;
// ------------ WiFi / MQTT ------------
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASS = "";
// Use ONE broker variable (removed MQTT_HOST)
const char* MQTT_BROKER = "broker.mqttdashboard.com";
const uint16_t MQTT_PORT = 1883;
// If you actually use auth, fill in; otherwise leave as nullptr/"" and connect accordingly
const char* MQTT_USER = ""; // or nullptr
const char* MQTT_PASS = ""; // or nullptr
const char* MQTT_TOPIC_COUNT = "watermeter/gallons";
// ------------ Behavior tuning ------------
const uint16_t DEBOUNCE_MS = 200;
static const uint16_t LED_ON_DURATION_MS = 1000;
// ------------ Display ------------
TM1637Display display(PIN_TM_CLK, PIN_TM_DIO);
// ------------ Globals ------------
volatile bool pulseEvent = false;
volatile uint32_t lastIrqMs = 0;
volatile uint32_t gallonCount = 0;
bool redLedOn = false;
uint32_t redLedSinceMs = 0;
// ------------ MQTT client ------------
WiFiClient espClient;
PubSubClient mqtt(espClient);
// ------------ Timers (non-blocking) ------------
uint32_t tWifiAttemptMs = 0;
uint32_t tMqttAttemptMs = 0;
uint32_t tPublishMs = 0;
uint32_t tDisplayMs = 0;
uint32_t tStatusBlinkMs = 0;
bool yellowBlinkState = false;
// Backoff windows (adjust as desired)
const uint32_t WIFI_RETRY_MS = 5000;
const uint32_t MQTT_RETRY_MS = 5000;
const uint32_t PUBLISH_MS = 1000;
const uint32_t DISPLAY_MS = 200;
const uint32_t BLINK_MS = 250;
// ------------ DST-aware timezone (NYC) ------------
static const char* TZ_INFO = "EST5EDT,M3.2.0/2,M11.1.0/2";
static int lastDateKey = -1;
// --- ISR (keep it tiny) ---
void IRAM_ATTR onPulse() {
uint32_t now = millis();
if (now - lastIrqMs >= DEBOUNCE_MS) {
lastIrqMs = now;
pulseEvent = true;
}
}
// Build a unique, stable MQTT client ID for ESP32
String mqttClientId() {
uint64_t mac = ESP.getEfuseMac();
char buf[32];
snprintf(buf, sizeof(buf), "watermeter-esp32-%04X%08X",
(uint16_t)(mac >> 32), (uint32_t)mac);
return String(buf);
}
int localDateKey() {
time_t now = time(nullptr);
if (now < 1700000000) return lastDateKey; // time not valid yet; don't reset
struct tm tmLocal;
localtime_r(&now, &tmLocal);
return (tmLocal.tm_year + 1900) * 10000 + (tmLocal.tm_mon + 1) * 100 + tmLocal.tm_mday;
}
void maybeResetAtLocalMidnight() {
int dk = localDateKey();
if (dk <= 0) return;
if (lastDateKey == -1) {
lastDateKey = dk;
return;
}
if (dk != lastDateKey) {
lastDateKey = dk;
// ✅ local day changed (DST-safe)
noInterrupts();
gallonCount = 0;
interrupts();
}
}
void startTimeNYC() {
configTzTime(TZ_INFO, "pool.ntp.org", "time.nist.gov", "time.google.com");
lastDateKey = -1;
}
void beginWifiIfNeeded() {
if (WiFi.status() == WL_CONNECTED) return;
uint32_t now = millis();
if (now - tWifiAttemptMs < WIFI_RETRY_MS) return;
tWifiAttemptMs = now;
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
}
bool ensureMqttConnected() {
if (mqtt.connected()) return true;
if (WiFi.status() != WL_CONNECTED) return false;
uint32_t now = millis();
if (now - tMqttAttemptMs < MQTT_RETRY_MS) return false;
tMqttAttemptMs = now;
String cid = mqttClientId();
bool ok;
if (MQTT_USER && MQTT_USER[0] != '\0') {
ok = mqtt.connect(cid.c_str(), MQTT_USER, MQTT_PASS);
} else {
ok = mqtt.connect(cid.c_str());
}
return ok;
}
void setup() {
Serial.begin(115200);
pinMode(PIN_LED_GREEN, OUTPUT);
pinMode(PIN_LED_RED, OUTPUT);
pinMode(PIN_LED_YELLOW, OUTPUT);
digitalWrite(PIN_LED_GREEN, HIGH); // off (active LOW)
digitalWrite(PIN_LED_RED, LOW);
digitalWrite(PIN_LED_YELLOW, LOW);
pinMode(PIN_BTN_USER, INPUT_PULLUP);
pinMode(PIN_PULSE, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_PULSE), onPulse, RISING);
display.setBrightness(7);
display.clear();
display.showNumberDec(0, true);
mqtt.setServer(MQTT_BROKER, MQTT_PORT);
beginWifiIfNeeded();
startTimeNYC();
}
void loop() {
const uint32_t now = millis();
// 1) Non-blocking WiFi connect maintenance
beginWifiIfNeeded();
// 2) Non-blocking MQTT connect maintenance
ensureMqttConnected();
// 3) MQTT client housekeeping (cheap)
mqtt.loop();
// 4) Consume pulse event, increment counter (FIXED)
if (pulseEvent) {
noInterrupts();
pulseEvent = false;
interrupts();
noInterrupts();
gallonCount++;
interrupts();
// pulse blink (non-blocking)
digitalWrite(PIN_LED_RED, HIGH);
redLedOn = true;
redLedSinceMs = now;
}
// 5) Turn off red LED after duration (non-blocking)
if (redLedOn && (now - redLedSinceMs >= LED_ON_DURATION_MS)) {
digitalWrite(PIN_LED_RED, LOW);
redLedOn = false;
}
// 6) Status LED (yellow) blink pattern: blink when not connected
bool wifiOk = (WiFi.status() == WL_CONNECTED);
bool mqttOk = mqtt.connected();
bool needsAttention = (!wifiOk || !mqttOk);
if (needsAttention) {
if (now - tStatusBlinkMs >= BLINK_MS) {
tStatusBlinkMs = now;
yellowBlinkState = !yellowBlinkState;
digitalWrite(PIN_LED_YELLOW, yellowBlinkState ? HIGH : LOW);
}
} else {
digitalWrite(PIN_LED_YELLOW, LOW);
}
// 7) DST-aware local midnight reset (no blocking)
maybeResetAtLocalMidnight();
// 8) Update display periodically (non-blocking)
if (now - tDisplayMs >= DISPLAY_MS) {
tDisplayMs = now;
uint32_t g;
noInterrupts();
g = gallonCount;
interrupts();
if (g > 9999) g = 9999;
display.showNumberDec((int)g, true);
}
// 9) Publish periodically (non-blocking)
if (now - tPublishMs >= PUBLISH_MS) {
tPublishMs = now;
if (mqtt.connected()) {
uint32_t g;
noInterrupts();
g = gallonCount;
interrupts();
char payload[16];
snprintf(payload, sizeof(payload), "%u", (unsigned)g);
mqtt.publish(MQTT_TOPIC_COUNT, payload, true);
}
}
// 10) Button handling (kept non-blocking)
static bool btnPrev = true;
static uint32_t tDown = 0;
bool btnNow = digitalRead(PIN_BTN_USER); // HIGH idle, LOW pressed
if (btnPrev && !btnNow) {
tDown = now;
} else if (!btnPrev && btnNow) {
uint32_t dt = now - tDown;
if (dt >= 2000) {
noInterrupts();
gallonCount = 0;
interrupts();
} else if (dt >= 50 && dt <= 800) {
ESP.restart();
}
}
btnPrev = btnNow;
// Yield without blocking
delay(0);
}
Gallon Pulse [12]
MQTT[13] or WiFi
KeepAlive[2]
Gallon_Pulse
RESET