INTELLICATH
ESP32 · Load cell · Decision tree · Firestore

A catheter bag
that tells someone
before it's full.

INTELLICATH weighs the bag continuously with a load cell, predicts the minute it reaches capacity, and puts that number in front of a nurse before the bag has to do it for them.

Why this exists
A full catheter bag doesn't page anyone. Someone has to walk over, look, and remember to look again.

Catheter-associated urinary tract infections are one of the more preventable hospital-acquired complications, and a lot of the risk sits in the gap between checks — a bag that sat too full for too long, a blockage no one noticed until output stopped.

INTELLICATH replaces the walk-and-look with a number on a screen: a load cell under the bag, a small model that turns weight and flow into a countdown, and a dashboard that recolors before the situation does.

How it works

One reading, five hops, no polling from a nurse's station required.

01 — SENSE

Load cell weighs the bag

An HX711 amplifier samples ten readings per loop under the catheter bag; weight converts directly to volume.

ARDUINOCODE.ino
02 — COMPUTE

ESP32 derives the flow rate

Flow is recalculated on a fixed 60-second window, not an unbounded elapsed time, so a quiet period doesn't distort the rate.

calculateUrineFlowRate()
03 — POST

TLS-pinned upload

The device validates the server's certificate against a hardcoded root CA, then posts with an API key and a device ID.

WiFiClientSecure
04 — VALIDATE

Serverless function checks it

The key, the rate limit, and the clinical bounds (0–800 mL bag volume, 0–100 mL/min flow) are all checked before anything is stored.

db.py
05 — PREDICT

Decision tree, then dashboard

A scikit-learn regressor turns remaining volume and flow rate into minutes-to-full; the dashboard renders the countdown and fires alerts.

decision_tree.pkl
What it does

Built around two jobs — sense accurately, predict honestly.

Continuous weight sensing, not spot checks

The load cell samples every loop of the device's firmware. Urine output and flow rate are measured directly from bag weight, not estimated from a nurse's last visual check.

HX711 · 10-sample average

A trained countdown, not a guess

A decision tree regression model predicts minutes until the bag reaches 800 mL from remaining volume and current flow rate — retrained offline, hash-verified before it's ever loaded.

scikit-learn · joblib

Alerts you can tune

Warn threshold and refresh interval are editable per session and saved to localStorage, not hardcoded into the page.

Tells you when it's lying

If the last successful reading is more than 30 seconds old, the timestamp turns amber and counts up instead of quietly going stale.

Usable with a keyboard

Every alert toggle carries a visible focus ring, the sidebar announces its own expanded state, and toasts are built from real DOM nodes.

Engineering

The parts that don't show up on the dashboard.

Hashes the model before it's allowed to run

Loading a pickle is arbitrary code execution if the file was ever swapped. get_model() checks a SHA-256 of both decision_tree.pkl and scaler.pkl against env-stored hashes before joblib.load() touches either.

db.py

One validator, three callers

api/predict.py, api/data.py, and the local Flask app all import the same validate_sensor_data — type coercion plus clinical range bounds, instead of three copies quietly drifting apart.

db.py

Skips the write when nothing changed

save_data_to_firestore compares the incoming reading against the last stored document and drops the insert when volume, flow, and output are all within noise tolerance.

db.py

Pinned TLS on the sensor itself

The ESP32 validates the server's certificate against a hardcoded ISRG Root X1 before it ever sends a reading — WiFiClientSecure, not a bare HTTPClient trusting whatever answers.

ARDUINOCODE.ino
Security

Every hop between the sensor and the screen is checked.

Every request carries a key

X-Api-Key is checked on both API routes; the ESP32 attaches X-Device-Id alongside it so a stored reading is attributable to a specific unit.

db.py

Errors don't narrate the stack

A failed request returns a flat "an internal error occurred." The real exception, with exc_info=True, goes to the server log — never to the client that triggered it.

app.py

Rate-limited per IP

check_rate_limit rejects a second request from the same address inside a one-second window — a stopgap against a flooded endpoint filling Firestore with garbage.

db.py

No HTML in, HTML out

The toast notification system builds every node with createElement and textContent. Nothing a server response contains is ever parsed as markup.

index.html
Stack

Bill of materials.

Hardware
  • ESP32 dev board WiFi + TLS
  • HX711 load cell amp DT/SCK
  • On-chip EEPROM state persistence
Backend
  • Python 3.9 Flask, local
  • Vercel Functions @vercel/python
  • google-cloud-firestore SDK
Model
  • Decision tree regression scikit-learn
  • Feature scaler joblib
  • Inputs volume + flow
Frontend
  • Vanilla HTML / CSS / JS no framework
  • Space Mono + DM Sans type system
  • Firebase Firestore live store

See the number update.