The Infinity Bowl

The Infinity Bowl is a fully automated, 3D-printed in-wall feeding and hydration system for pets.
111
34
0
4110
updated June 10, 2025

Description

PDF

Veterinary studies keep ranking the humble pet bowl among the top-5 germiest spots in the average home—right up there with the kitchen sponge and the bathroom faucet. Yet surveys show barely 15% of owners scrub their pet’s water dish every day, and almost a third admit they can’t remember the last time they washed it at all. Combine warm kibble dust, back-wash, and standing tap water and you’ve got a bacterial buffet of E. coli, salmonella, and even MRSA.

Endless gadgets promise “cleaner” bowls—but they all share one flaw: they rely on a stagnant reservoir you still have to refill, rinse, and disinfect. The Infinity Bowl breaks that cycle completely. It’s the only printable system that plumbs straight into your household water line, delivering a stream of fresh municipal water the moment your pet steps up and shutting off—plus draining safely—when they walk away. If a valve ever misbehaves, overflow routes directly to a dedicated sewer line, not your kitchen floor.

Why flowing water? Cats evolved to distrust puddles—in the wild, moving water signaled safety, while stagnant pools hid parasites. Dogs aren’t picky, but they are healthier when their water’s clean and cool. Infinity Bowl gives both species what instinct craves: moving, oxygenated water on demand.

All that tech—ultrasonic presence sensing, LED-lit acrylic faucet, Wi-Fi stats dashboard, EEPROM-saved settings, redundant safety shut-offs—is tucked into a sleek pull-out drawer. You just slide it in, connect one cold-water line and one drain, and forget the daily drudgery forever.

Build it, plumb it, and watch your pets drink to their health—infinitely.

References

https://www.uspca.co.uk/danger-in-your-pets-bowl

https://phys.org/news/2018-09-life-threatening-bacteria-pets-bowls-experts.amp
 

Background

The Infinity Bowl is a fully automated, 3D-printed in-wall feeding and hydration system for pets, designed to elevate both pet wellness and home convenience.

Built with Arduino-based sensing, real plumbing integration, and IoT-style data serving, it combines modern aesthetics with everyday practicality.

I created this project to eliminate the two major pain points in pet care: dirty stagnant bowls and the tedious daily refilling and cleaning cycle.

The Infinity Bowl ensures constant access to fresh, flowing water and ready food, while minimizing manual maintenance.

Built-in safety features—including an overflow drain directly plumbed to the sewer—protect your home from valve malfunctions or water accidents.

Smart, clean, and worry-free, the Infinity Bowl blends seamlessly into any modern space with its minimalist pull-out drawer design and integrated monitoring system.



Functions

  • Water Bowl:
    • Ultrasonic sensor detects pet presence
    • Fresh water flows automatically while pet is present
    • Water is supplied directly from household plumbing
    • Custom acrylic faucet with integrated LED indicator showing active water flow
    • Overflow protection: excess water drains safely to a plumbed sewer line if valve malfunctions
  • Food Bowl:
    • Default setup supports free-feeding (ideal for dogs like mine)
    • Modular swap-out option available for timed, portion-controlled feedings (in development)
    • Quad drawer sliders effortlessly support a large kibble storage container (holds an entire 25lb bag)
  • Additional Features:
    • Mounted discreetly within a pull-out drawer for clean aesthetic minimalism, easy cleaning, and space efficiency
    • Integrated HTTP web server for real-time statistics, remote control, and configuration
    • Smart LED Feedback
      • Integrated multi-color LED lighting via FastLED library:
        • Pulsating blue / turquoise / aqua animation while pet is present (alive and active feel)
        • Quick red pulse alert when presence is no longer detected, useful for debugging presence stability
        • Fade-to-off when as pet is no longer detected for a period of time, giving clear visual feedback without abruptness
    • Measurement Smoothing & Intelligent Detection
      • Advanced measurement smoothing:
        • Averages multiple ultrasonic distance measurements while rejecting outliers (ignores min/max to remove noise)
        • Ensures stable and reliable presence detection even in noisy environments.
      • Self-calibrating idle distance detection:
        • System automatically learns what the no-presence distance is without needing manual calibration
    • Intelligent Presence Timeout
      • Timeout detection:
        • If the sensor falls out of calibration, the system turns the water off, preventing continuous run and forces a recalibration


Hardware and Software Used

Hardware:
  • Service Panel
    • Mount Arduino (3D print)
    • Mount Prototype PCB  & MOSFET (3D print)
    • Arduino Uno R4
    • MOSFET
    • Prototype PCB
    • 12V or 24V power supply
  • Presence Detection
    • HC-SR04 ultrasonic
    • Connector Cable
  • Drawer
    • CNC/Laser cut ½” MDF
    • Solenoid
      • Water Valve (potable-water rated, 12V or 24V)
      • Connector Cable
    • Food Compartment
      • CNC/Laser cut Acrylic
      • Dowel and spacers (3D print)
    • Food Bowl
      • Bowl (3D print)
      • Module Free-feed reducer  (3D print)
    • Water Bowl
      • Bowl (3D print)
      • Fresh Water Plumbing
        • 1/4 Quick-Connect Valve
        • 1/4 Quick-Connect tubing
        • Adapter 1/2" PEX for to 1/2" Male NPT
        • Adapter 1/2” NPT to 1/4 Quick-Connect
      • Drain
        • Adapter Male 3/4 NPT to Male 3/4 NPT
        • Female 3/4 NPT to flexible drain hose
      • Faucet
        • CNC/Laser cut Acrylic
        • 1/4 Quick-Connect to Male 1/2” NPT
        • 7 LED WS2812B 5050 RGB LED Ring
        • Mount (3D print)
        • Connector Cable
  • Drywall, plumbing hardware for water supply and sewer drain integration

Software:
  • Arduino IDE 
  • Fusion 360

HTTP Server / Dashboard

  • Built-in HTTP web server serving:
    • Real-time system state (presence, last trigger time, idle distance, measurement window).
    • Configuration (edit thresholds and flow rate live)
  • Statistics dashboard for:
    • Number of triggers
    • Average presence time
    • Total hydration time
    • Estimated water dispensed in milliliters
    • Breakdown of these stats across 1h, 6h, 12h, 24h, 1w, and since reboot
    • Session and Activation history with timestamped records

Wiring 

Ultrasonic Sensor:
  • VCC → 5V
  • GND → GND
  • TRIG → Digital Pin 10
  • ECHO → Digital Pin 9
Solenoid Water Valve (controlled via MOSFET):
  • MOSFET
    • Gate → Digital Pin 8
    • Source → 12V/24V power supply
    • Drain → To Solenoid
Acrylic Faucet 7 LED Round PCB:
  • LED Positive → 5V
  • LED Data → Digital Pin 7 
  • LED Negative → GND

Demo and Usage Instructions

  1. Pet approaches
    • Ultrasonic sensor measures distance change
    • Presence state is triggered
    • MOSFET is engaged, Solenoid Valve is energized
    • Fresh water flows
    • Faucet LED lights up
  2. Pet leaves
    1. Ultrasonic sensor measures distance change
    2. After 10 consecutive measurements of non-presence, returns to idle state
    3. MOSFET is disengaged, Solenoid Valve is denergized
    4. Water flow stops
    5. Faucet LED turns off.

Amazon Items

Safety

Keeping your pet safe is critical when exposing them to 3D printed parts / post-processing. Where ever food/water comes in contact, ensure the materials selected are food safe. In my build, the bowls were coated with spot putty, sanded, painted and coated with multiple layers of food safe epoxy resin. 

Coating Procedure
  1. Attach the bowls to a stationary work  surface with screws 
    • TIP: Use a thin poly sheet in between the bowls and work surface for easy removal, just incase epoxy binds them together
  2. Use a drop cloth to protect floor from any drips
  3. Lightly sand painted finish with 800 grit sand paper to ensure even an ideal epoxy bonding surface
  4. Wear appropriate safety equipment when working with epoxy — gloves, respirator, etc.
  5. Mix small batches of epoxy resin (60mL), ensure to follow manufacturer instructions exactly to ensure correct ratio and the resin will cure completely
  6. With a disposable paintbrush apply a very light coating of epoxy to the surface
    • Even out any spot which has too much
    • Do not get epoxy in ¾ drain theads
  7. Return every 10 minutes for an hour to brush out any spots where pooling might be occurring 
  8. Additional Coats: Wait 12 hours, begin over at step 4
    • Additional coats will smooth out any bumpy texture that may have formed and provide additional structure / protection
    • Ensure to target low spots or missed spots


See attached safety sheet from the epoxy manufacturer selected.

https://www.artresin.com/blogs/artresin/artresin-passes-food-safety-tests

Assembly

1. Cabinet Construction

  • Drawer Dimensions:
    • 28.5” tall × 14” wide × 6” deep
  • Framing:
    • Designed to fit between standard 16” on-center wall studs (> 6” wide walls are non-typical however)
  • Materials:
    • The pull-out drawer and housing structure are made from MDF panels for strength, durability, and ease of painting
  • Wall Integration:
    • A drywall cavity must be cut to fit the cabinet, allowing for a flush, minimalist installation

2. Plumbing Requirements

  • Municipal Water Supply:
    • Requires a cold-water line plumbed to the installation location inside the wall
    • Water connection is made via a PEX to ¼” quick-connect fitting with the purpose of –
      • Reducing max flow rate
      • Reliable/cost effective solenoid valves available
      • Simplifies downstream connections
         
  • Sewer/Drain Access:
    • Requires access to a ground-level sanitary sewer line to handle any overflow safely
    • (Note: This is typically the most complex part of installation—plan carefully and code, consult a licensed plumber)

3. 3D Printing & Post-Processing

  • Parts to Print:
    • Water bowl, food bowl, faucet structure, sensor holders, valve mounts
  • Material:
    • Any filament material you prefer (PLA, PETG, ABS, etc.)
  • Post-Processing:
    • Smooth parts using your preferred finishing techniques (sanding, vapor smoothing, etc.)
    • Paint or prime as desired for aesthetics
    • Important: Regardless of material or finish, apply multiple layers of food-safe epoxy to all surfaces that will contact water or food.
      (Food-safe epoxy coating is mandatory for hygiene and durability)

4. Faucet Fabrication

  • Faucet Design:
    • Constructed from three layers of flat acrylic sheets:
      • Two bottom clear acrylic sheets for LED light transmission / cool factor
      • One top black acrylic layer for contrast and hiding construction details
         
  • Assembly:
    • Bond the acrylic sheets using a formulated glue
    • Cut and tap ¼” NPT threads into the faucet to accommodate a ¼” quick-connect fitting for water supply attachment
       
  • LED Installation:
    • Install 7 LED PCB into the faucet base to provide active water flow indication

5. Electronics & Service Panel

  • Service Panel Design:
    • Create a separate service cubby mounted inside the wall between studs, positioned to the left or right of the pull-out drawer
    • This layout keeps all electronics and high-voltage components safely away from plumbing
  • Electrical Setup:
    • Equip the service cubby with a GFCI (Ground Fault Circuit Interrupter) outlet for maximum safety when operating near water
  • Components:
    • Arduino microcontroller, solenoid valve relay, ultrasonic rangefinder, FastLED lighting strip, power supply, and optional WiFi module
  • Maintenance Access:
    • The cubby allows quick access to critical components for upgrades, maintenance, or emergency shutoff without disturbing the drawer or bowls.

Disclaimer

 

The Infinity Bowl project involves plumbing, electrical wiring, and permanent modifications to household infrastructure.

This design is provided for educational and personal use only.

Builders are responsible for ensuring that all plumbing and electrical work complies with local building codes and safety regulations.

Proper installation of GFCI protection, backflow prevention devices, and approved drainage methods is strongly recommended.

Always consult with a licensed plumber or electrician if you are unsure about any aspect of the installation.

The designer assumes no liability for damages, injuries, or losses resulting from the use or misuse of this project.


Code

https://github.com/smysnk/infinity-bowl

#include <WiFiS3.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <WiFiServer.h>
#include <EEPROM.h>
#include <FastLED.h>
#include <stdlib.h>
#include <numeric>  // for accumulate

//── DEBUG & Logging ──────────────────────────────────────────────────────────
#define DEBUG true
void logInfo(const String &msg) {
  Serial.print("[INFO] ");
  Serial.println(msg);
}
void logDebug(const String &msg) {
  if (DEBUG) {
    Serial.print("[DEBUG] ");
    Serial.println(msg);
  }
}
void logWarn(const String &msg) {
  Serial.print("[WARN] ");
  Serial.println(msg);
}

//── Configurable pins ─────────────────────────────────────────────────────────
#define PIN_ECHO 9
#define PIN_TRIGGER 10
#define PIN_WATER 8
#define LED_PIN 7    // FastLED data pin
#define NUM_LEDS 7  // configurable number of LEDs
CRGB leds[NUM_LEDS];

//── Constants ─────────────────────────────────────────────────────────────────
#define MAX_QUEUE 10
#define WINDOW_SIZE 10
#define EEPROM_CONFIG_ADDR 0

//── Config defaults ─────────────────────────────────────────────────────────
#define DEFAULT_THRESHOLD_TRIGGER 5.0f
#define DEFAULT_THRESHOLD_SAME 0.5f
#define DEFAULT_THRESHOLD_TIMEOUT 120
#define DEFAULT_FLOW_RATE 10.0f

//── Event types ───────────────────────────────────────────────────────────────
enum EventType { EVENT_MEASUREMENT,
                 EVENT_PRESENCE_TIMEOUT };
struct Event {
  EventType type;
  float value;
};

//── Activation & Session ──────────────────────────────────────────────────────
struct Activation {
  unsigned long startTime, endTime;
};
struct Session {
  unsigned long startTime;
  Activation *activations;
  int activationCount;
};

//── Application config ────────────────────────────────────────────────────────
struct Config {
  float thresholdTrigger, thresholdSame;
  int thresholdTimeout;
  float flowRate;
};

//── Runtime state ─────────────────────────────────────────────────────────────
struct State {
  Config config;
  bool presence;
  unsigned long lastTrigger;
  float distanceIdle;
  float measurementsWindow[WINDOW_SIZE];
  Session *sessions;
  int sessionCount;
} state;

//── Event queue ───────────────────────────────────────────────────────────────
static Event eventQueue[MAX_QUEUE];
static int queueStart = 0, queueEnd = 0;

//── Networking ────────────────────────────────────────────────────────────────
const char *ssid = “SSID";
const char *password = "password";
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 60000);
WiFiServer server(80);
int status = WL_IDLE_STATUS;

//── Timing & LED globals ─────────────────────────────────────────────────────
unsigned long appStartTime = 0;
bool redPulse = false;
unsigned long redPulseStart = 0;
const unsigned long redPulseDuration = 200;

//── Prototypes ───────────────────────────────────────────────────────────────
void dispatchEvent(const Event &e);
bool hasEvent();
Event dequeueEvent();
void processEvents();
void reducer(const Event &e);
unsigned long getCurrentTime();
float measureDistance();
void initConfig();
void setupState();
void loadConfigFromEEPROM();
void saveConfigToEEPROM();

//── Setup ────────────────────────────────────────────────────────────────────
void setup() {
  Serial.begin(9600);
  logInfo("Starting Pet Fountain Controller");

  pinMode(PIN_ECHO, INPUT);
  pinMode(PIN_TRIGGER, OUTPUT);
  pinMode(PIN_WATER, OUTPUT);
  digitalWrite(PIN_WATER, LOW);

  // FastLED init
  FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, NUM_LEDS);
  FastLED.clear();
  FastLED.show();

  // Load or reset config
  loadConfigFromEEPROM();
  if (state.config.thresholdTrigger <= 0 || state.config.thresholdSame <= 0 || state.config.thresholdTimeout <= 0 || state.config.flowRate <= 0) {
    initConfig();
    saveConfigToEEPROM();
    logInfo("Config reset to defaults");
  } else {
    logInfo("Loaded config from EEPROM");
  }

  setupState();

  // Wi-Fi
  while (WiFi.status() == WL_NO_MODULE) {
    logWarn("Wi-Fi module missing!");
    delay(1000);
  }
  while (status != WL_CONNECTED) {
    logInfo("Connecting to Wi-Fi...");
    status = WiFi.begin(ssid, password);
    delay(2000);
  }
  logInfo("Wi-Fi connected");

  // NTP
  timeClient.begin();
  while (!timeClient.update()) {
    delay(500);
  }
  appStartTime = timeClient.getEpochTime();
  logInfo("NTP time = " + String(appStartTime));

  // First session
  int sc = state.sessionCount;
  state.sessions = (Session *)realloc(state.sessions, (sc + 1) * sizeof(Session));
  state.sessionCount = sc + 1;
  state.sessions[sc] = { appStartTime, nullptr, 0 };
  logInfo("Started session #" + String(sc + 1));

  server.begin();
  logInfo("HTTP server started");
}

//── Main Loop ───────────────────────────────────────────────────────────────
unsigned long lastMeasurement = 0;
unsigned long sampleBuf[WINDOW_SIZE];
int sampleCount = 0;
unsigned long lastTimeoutCheck = 0;

void loop() {
  unsigned long nowMs = millis();
  unsigned long now = getCurrentTime();

  // HC-SR04 measurement every 100 ms
  if (nowMs - lastMeasurement >= 100) {
    lastMeasurement = nowMs;
    sampleBuf[sampleCount++] = measureDistance();
    if (sampleCount >= WINDOW_SIZE) {
      float sum = 0;
      float minv = sampleBuf[0], maxv = sampleBuf[0];
      bool anyNonZero = false;
      for (int i = 0; i < WINDOW_SIZE; i++) {
        float v = sampleBuf[i];
        sum += v;
        minv = min(minv, v);
        maxv = max(maxv, v);
        if (v != 0) anyNonZero = true;
      }
      float avg = (sum - minv - maxv) / (WINDOW_SIZE - 2);
      logDebug("Measurement avg: " + String(avg, 2));
      dispatchEvent({ EVENT_MEASUREMENT, avg });
      sampleCount = 0;
    }
  }

  // Presence timeout every 60s
  if (nowMs - lastTimeoutCheck >= 60000) {
    lastTimeoutCheck = nowMs;
    if (state.presence && (now - state.lastTrigger) > state.config.thresholdTimeout) {
      dispatchEvent({ EVENT_PRESENCE_TIMEOUT, 0 });
    }
  }

  processEvents();

  // LED behavior
  if (state.presence) {
    // blue/turquoise/aqua pulsating
    uint8_t b = beatsin8(20, 128, 255);
    uint8_t h = beatsin8(10, 96, 160);
    fill_solid(leds, NUM_LEDS, CHSV(h, 255, b));
    // red pulse override
    if (redPulse && millis() - redPulseStart < redPulseDuration) {
      fill_solid(leds, NUM_LEDS, CRGB::Red);
    } else {
      redPulse = false;
    }
  } else {
    // fade off when presence lost
    fadeToBlackBy(leds, NUM_LEDS, 20);
  }
  FastLED.show();

  // HTTP handling + stats (unchanged from previous implementation)
  WiFiClient client = server.available();
  if (client) {
    // HTTP handling & stats
    WiFiClient client = server.available();
    if (client) {
      while (!client.available()) delay(1);
      String req = client.readStringUntil('\r');
      client.readStringUntil('\n');
      logDebug("HTTP request: " + req);

      // Reset Sessions
      if (req.startsWith("GET /resetSessions")) {
        for (int i = 0; i < state.sessionCount; i++) {
          free(state.sessions[i].activations);
        }
        free(state.sessions);
        state.sessions = nullptr;
        state.sessionCount = 0;

        unsigned long now2 = getCurrentTime();
        state.sessions = (Session *)malloc(sizeof(Session));
        state.sessionCount = 1;
        state.sessions[0].startTime = now2;
        state.sessions[0].activations = nullptr;
        state.sessions[0].activationCount = 0;
        logInfo("All sessions reset, new session at " + String(now2));
      }

      // Config setter
      if (req.startsWith("GET /setConfig")) {
        int qpos = req.indexOf('?');
        int end = req.indexOf(' ', qpos);
        String query = req.substring(qpos + 1, end);
        int idx = 0;
        while (idx < query.length()) {
          int amp = query.indexOf('&', idx);
          if (amp < 0) amp = query.length();
          String pair = query.substring(idx, amp);
          int eq = pair.indexOf('=');
          if (eq > 0) {
            String key = pair.substring(0, eq);
            String val = pair.substring(eq + 1);
            if (key == "thresholdTrigger") state.config.thresholdTrigger = val.toFloat();
            else if (key == "thresholdSame") state.config.thresholdSame = val.toFloat();
            else if (key == "thresholdTimeout") state.config.thresholdTimeout = val.toInt();
            else if (key == "flowRate") state.config.flowRate = val.toFloat();
          }
          idx = amp + 1;
        }
        saveConfigToEEPROM();
        logInfo("Config updated via HTTP");
      }

      // Index page
      if (req.startsWith("GET / ")) {
        unsigned long curr = getCurrentTime();
        String html = "<!DOCTYPE html><html><head><title>Infinity Bowl</title></head><body>";

        // Header & Reset button
        html += "<h1>Infinity Bowl Controller</h1>";
        html += "<form action=\"/resetSessions\" method=\"GET\">";
        html += "<button type=\"submit\">Reset Sessions</button></form>";

        // Constants
        html += "<h2>Constants</h2><ul>";
        html += "<li>PIN_ECHO: " + String(PIN_ECHO) + "</li>";
        html += "<li>PIN_TRIGGER: " + String(PIN_TRIGGER) + "</li>";
        html += "<li>PIN_WATER: " + String(PIN_WATER) + "</li>";
        html += "<li>MAX_QUEUE: " + String(MAX_QUEUE) + "</li>";
        html += "<li>WINDOW_SIZE: " + String(WINDOW_SIZE) + "</li></ul>";

        // State
        html += "<h2>State</h2><ul>";
        html += "<li>presence: " + String(state.presence ? "true" : "false") + "</li>";
        html += "<li>lastTrigger: " + String(state.lastTrigger) + "</li>";
        html += "<li>distanceIdle: " + String(state.distanceIdle, 2) + "</li>";
        html += "<li>measureWin: [";
        for (int i = 0; i < WINDOW_SIZE; i++) {
          html += String(state.measurementsWindow[i], 2);
          if (i < WINDOW_SIZE - 1) html += ",";
        }
        html += "]</li></ul>";

        // Config form
        html += "<h2>Config</h2><form action=\"/setConfig\" method=\"GET\">";
        html += "thresholdTrigger: <input name=\"thresholdTrigger\" value=\"" + String(state.config.thresholdTrigger, 2) + "\">";
        html += " thresholdSame: <input name=\"thresholdSame\" value=\"" + String(state.config.thresholdSame, 2) + "\">";
        html += " timeout: <input name=\"thresholdTimeout\" value=\"" + String(state.config.thresholdTimeout) + "\">";
        html += " flowRate: <input name=\"flowRate\" value=\"" + String(state.config.flowRate, 2) + "\">";
        html += " <button type=\"submit\">Set</button></form>";

        // Statistics (1h,6h,12h,24h,1w,since reboot)
        const int N = 6;
        unsigned long windows[N] = {
          curr - 3600UL,
          curr - 6 * 3600UL,
          curr - 12 * 3600UL,
          curr - 24 * 3600UL,
          curr - 7 * 24 * 3600UL,
          appStartTime
        };
        const char *names[N] = { "1h", "6h", "12h", "24h", "1w", "since reboot" };

        unsigned long counts[N] = { 0 }, durations[N] = { 0 };
        // Aggregate activations
        for (int i = 0; i < state.sessionCount; i++) {
          Session &s = state.sessions[i];
          for (int j = 0; j < s.activationCount; j++) {
            Activation &a = s.activations[j];
            unsigned long dur = a.endTime - a.startTime;
            for (int w = 0; w < N; w++) {
              if (a.startTime >= windows[w]) {
                counts[w]++;
                durations[w] += dur;
              }
            }
          }
        }

        // Build stats table
        html += "<h2>Statistics</h2><table border=1><tr><th>Metric</th>";
        for (int w = 0; w < N; w++) html += "<th>" + String(names[w]) + "</th>";
        html += "</tr>";

        // # triggers
        html += "<tr><td># triggers</td>";
        for (int w = 0; w < N; w++) html += "<td>" + String(counts[w]) + "</td>";
        html += "</tr>";

        // Avg presence
        html += "<tr><td>Avg presence (s)</td>";
        for (int w = 0; w < N; w++) {
          float avg = counts[w] ? (float)durations[w] / counts[w] : 0;
          html += "<td>" + String(avg, 2) + "</td>";
        }
        html += "</tr>";

        // Hydration time
        html += "<tr><td>Hydration time (s)</td>";
        for (int w = 0; w < N; w++) html += "<td>" + String(durations[w]) + "</td>";
        html += "</tr>";

        // Water dispensed
        html += "<tr><td>Water dispensed (mL)</td>";
        for (int w = 0; w < N; w++) {
          float ml = durations[w] * state.config.flowRate;
          html += "<td>" + String(ml, 2) + "</td>";
        }
        html += "</tr></table>";

        // Sessions & Activations
        html += "<h2>Sessions & Activations</h2>";
        for (int i = 0; i < state.sessionCount; i++) {
          Session &s = state.sessions[i];
          html += "<h3>Session " + String(i + 1) + ": start=" + String(s.startTime) + "</h3><ul>";
          for (int j = 0; j < s.activationCount; j++) {
            Activation &a = s.activations[j];
            unsigned long dur = a.endTime - a.startTime;
            html += "<li>Act " + String(j + 1)
                    + ": start=" + String(a.startTime)
                    + ", end=" + String(a.endTime)
                    + ", dur=" + String(dur) + "s"
                    + ", vol=" + String(dur * state.config.flowRate, 2) + "mL</li>";
          }
          html += "</ul>";
        }

        html += "</body></html>";

                client.print("HTTP/1.1 200 OK\r\n");
                client.print("Content-Type: text/html\r\n");
                client.print("Connection: close\r\n");
                client.print(html);
                logInfo("HTTP response sent");

                client.stop();
                logInfo("Client disconnected");
      }
    }
  }
}

//── Reducer ──────────────────────────────────────────────────────────────────
void reducer(const Event &e) {
  unsigned long now = getCurrentTime();

  if (e.type == EVENT_MEASUREMENT) {
    // shift window
    for (int i = WINDOW_SIZE - 1; i > 0; i--) {
      state.measurementsWindow[i] = state.measurementsWindow[i - 1];
    }
    state.measurementsWindow[0] = e.value;

    // calibrate only if non-zero
    bool stable = true, nz = false;
    for (int i = 0; i < WINDOW_SIZE; i++) {
      float v = state.measurementsWindow[i];
      if (v != 0) nz = true;
      if (fabs(v - e.value) > state.config.thresholdSame) stable = false;
    }
    if (state.distanceIdle < 0 && stable && nz) {
      state.distanceIdle = std::accumulate(state.measurementsWindow,
                                           state.measurementsWindow + WINDOW_SIZE, 0.0f)
                           / WINDOW_SIZE;
      logInfo("Calibrated idle: " + String(state.distanceIdle, 2));
      return;
    }

    // red pulse on stable distance
    if (state.presence && fabs(e.value - state.distanceIdle) <= state.config.thresholdSame) {
      redPulse = true;
      redPulseStart = millis();
    }

    // detect presence start
    if (!state.presence && e.value < state.distanceIdle - state.config.thresholdTrigger) {
      state.presence = true;
      state.lastTrigger = now;
      digitalWrite(PIN_WATER, HIGH);
    }

    // detect presence end
    if (state.presence) {
      bool gone = true;
      for (int i = 0; i < WINDOW_SIZE; i++) {
        float v = state.measurementsWindow[i];
        if (v < state.distanceIdle - state.config.thresholdTrigger || v > state.distanceIdle + state.config.thresholdTrigger) {
          gone = false;
        }
      }
      if (gone) {
        state.presence = false;
        digitalWrite(PIN_WATER, LOW);
      }
    }
  } else {
    // timeout end
    state.presence = false;
    digitalWrite(PIN_WATER, LOW);
  }
}

//── Helper functions ─────────────────────────────────────────────────────────

unsigned long getCurrentTime() {
  timeClient.update();
  return timeClient.getEpochTime();
}

float measureDistance() {
  digitalWrite(PIN_TRIGGER, LOW);
  delayMicroseconds(2);
  digitalWrite(PIN_TRIGGER, HIGH);
  delayMicroseconds(10);
  digitalWrite(PIN_TRIGGER, LOW);
  unsigned long d = pulseIn(PIN_ECHO, HIGH, 30000);
  return (d * 0.034) / 2;
}

void dispatchEvent(const Event &e) {
  int nxt = (queueEnd + 1) % MAX_QUEUE;
  if (nxt != queueStart) {
    eventQueue[queueEnd] = e;
    queueEnd = nxt;
  }
}

bool hasEvent() {
  return queueStart != queueEnd;
}

Event dequeueEvent() {
  Event e = eventQueue[queueStart];
  queueStart = (queueStart + 1) % MAX_QUEUE;
  return e;
}

void processEvents() {
  while (hasEvent()) {
    Event e = dequeueEvent();
    bool prev = state.presence;
    reducer(e);
    if (state.presence != prev) {
      logInfo(String("Water ") + (state.presence ? "ON" : "OFF"));
    }
  }
}

void initConfig() {
  state.config = { DEFAULT_THRESHOLD_TRIGGER,
                   DEFAULT_THRESHOLD_SAME,
                   DEFAULT_THRESHOLD_TIMEOUT,
                   DEFAULT_FLOW_RATE };
}

void setupState() {
  initConfig();
  state.presence = false;
  state.lastTrigger = getCurrentTime();
  state.distanceIdle = -1;
  state.sessionCount = 0;
  state.sessions = nullptr;
  for (int i = 0; i < WINDOW_SIZE; i++) {
    state.measurementsWindow[i] = 0;
  }
}

void loadConfigFromEEPROM() {
  EEPROM.get(EEPROM_CONFIG_ADDR, state.config);
}

void saveConfigToEEPROM() {
  EEPROM.put(EEPROM_CONFIG_ADDR, state.config);
}

 

Tags



Awarded in the contest


3
Smart Pet Gadgets with Arduino
137 entries | February 27 – April 27, 2025

Model origin

The author marked this model as their own original creation.

License