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.
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.
One reading, five hops, no polling from a nurse's station required.
Load cell weighs the bag
An HX711 amplifier samples ten readings per loop under the catheter bag; weight converts directly to volume.
ARDUINOCODE.inoESP32 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()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.
WiFiClientSecureServerless 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.pyDecision 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.pklBuilt 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 averageA 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 · joblibAlerts 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.
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
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
Bill of materials.
- ESP32 dev board WiFi + TLS
- HX711 load cell amp DT/SCK
- On-chip EEPROM state persistence
- Python 3.9 Flask, local
- Vercel Functions @vercel/python
- google-cloud-firestore SDK
- Decision tree regression scikit-learn
- Feature scaler joblib
- Inputs volume + flow
- Vanilla HTML / CSS / JS no framework
- Space Mono + DM Sans type system
- Firebase Firestore live store