尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Smart IV drip monitoring: an ESP32-CAM photographs each IV bag, a FastAPI + MobileNetV3 backend estimates fill level, and a live ward dashboard alerts nurses before a line runs dry.
翻譯準備中
正在載入完整 README。
檢視完整 README ↗An ESP32-CAM unit mounted on an IV stand photographs the bag on a timer. A backend scores each photo with a trained fill-level model, derives volume, flow rate and status, and pushes live updates to a web dashboard that shows nursing staff every bed and raises an alert when one goes critical.
Live: https://frontend-topaz-xi-xu5rpbpic5.vercel.app
This README is the project's only documentation file.
server/server/app/cv/ + ml-model-training/frontend/supabase/firmware/esp32-cam/frontend/ Ward dashboard - TanStack Start, React, Tailwind. -> Vercel
server/ FastAPI backend - ingest, fill-level model, storage, -> Render
exports, WebSocket feeds.
supabase/ Database schema (migrations) for staff auth, roles
and ward settings. Supabase CLI project.
firmware/esp32-cam/ ESP32-CAM firmware (PlatformIO) - capture + upload. -> serial once, then OTA
firmware/tools/ fleet.py - remote settings, photos and OTA over MQTT.
ml-model-training/ Training scripts and model artifacts.
.github/ CI workflow and Dependabot config.
docs/screenshots/ Images used in this README.
docker-compose.yml Runs the backend locally in Docker.
Each part is self-contained: its own dependencies, config and ignore rules, and its own CI job.
ESP32-CAM --HTTP POST image--> FastAPI (server/) --WebSocket--> Dashboard (frontend/)
fill-level model, /ws/bedfeed live bed cards
SQLite, CSV/XLSX export |
v
Supabase (supabase/): staff login,
roles, ward settings
POST /api/v1/frames. The server runs the
model on it, turns the fill % into a full reading (volume, flow rate from
the previous reading, status and alert), stores it and broadcasts it.server/app/bedfeed.py reshapes each reading into the
{id, bed, patient, fluid, flow, level, status, auxClass, battery, wifi}
message that frontend/src/lib/fluidwatch.ts's applyLiveUpdate() merges
into the bed list. A device the dashboard hasn't seen before becomes a new
bed card automatically, so no provisioning is needed.NORMAL → STABLE,
LOW_LEVEL → WATCH, EMPTY/OCCLUSION/ERROR → CRITICAL.ward_settings row (alert
thresholds and the backend's WebSocket URL). The backend never talks to
Supabase.Backend (Python 3.11):
cd server
python -m venv .venv
.venv\Scripts\activate # Windows; source .venv/bin/activate elsewhere
pip install -r requirements.txt
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
Or with Docker, from the repo root: API_AUTH_TOKEN=<token> docker compose up --build.
Dashboard (Node 22):
cd frontend
npm install
npm run dev # http://localhost:8080
Sign up. The first account created becomes the ward admin. Open
/dashboard and click Connect ward server. Without a live connection the
dashboard runs a built-in simulator on six demo beds, so the UI is never empty.
Firmware: see Firmware. Without hardware,
open http://localhost:8000/docs, use POST /api/v1/frames to upload any
photo, and watch it appear on the dashboard.
server/FastAPI service that ingests camera frames (and plain JSON readings), runs the fill-level model, stores readings, exports them, and pushes live updates over WebSockets.
| Path | Role |
|---|---|
app/main.py | App setup, CORS, /health |
app/config.py | Settings from env / .env (see below) |
app/db.py, app/models.py, app/schemas.py | SQLAlchemy engine and the Reading table / Pydantic shapes |
app/routers/frames.py | POST /api/v1/frames - image → model → reading; GET /latest serves the last photo |
app/routers/readings.py | JSON ingest, list/latest, CSV/XLSX export, bearer-token auth |
app/routers/devices.py | One row per device from its latest reading, with an online flag (seen in the last 90s) |
app/routers/ws.py, app/ws_manager.py | /ws/readings and /ws/bedfeed broadcast feeds |
app/bedfeed.py | Reading → dashboard bed-card message |
app/cv/ | Fill-level model and reading derivation - see Fill-level model |
app/frame_storage.py | Keeps only the latest JPEG per device (data/frames/) |
Readings are capped at 1000 per device (oldest pruned) so storage stays predictable during long demos.
| Method & path | Auth | Purpose |
|---|---|---|
POST /api/v1/frames?device_id=&bed_label=&patient_name=&fluid_label= | Bearer | Camera upload (multipart file). Runs the model; 422 if the file isn't an image |
GET /api/v1/frames/latest?device_id= | - | Last photo from a device (used by bed cards' <img>) |
POST /api/v1/readings | Bearer | JSON reading from a device that measures directly |
GET /api/v1/readings?device_id=&limit= | - | Recent readings, newest first (limit ≤ 5000) |
GET /api/v1/readings/latest?device_id= | - | Most recent reading |
GET /api/v1/readings/export.csv / .xlsx | - | Full log, oldest first, for records/EMR hand-off |
GET /api/v1/devices | - | Fleet status: battery, Wi-Fi, last seen, online |
WS /ws/bedfeed | - | Live feed in the dashboard's bed shape |
WS /ws/readings | - | Live feed with the full reading |
GET /health, GET /docs | - | Liveness; Swagger UI |
| Variable | Default | Notes |
|---|---|---|
API_AUTH_TOKEN | generated | Required for POSTs. If unset, one is generated on first start and written to server/.env (check the startup log). Copy it into the firmware's secrets.h. Always set it explicitly in Docker/Render, otherwise each fresh container mints a new one and locks the ESP32 out. |
DATABASE_URL | sqlite:///./data/iv_drip.db | Any SQLAlchemy URL; Postgres works without code changes |
CORS_ORIGINS | * | Comma-separated dashboard origins |
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY | unset | Turn on the training-data archive (see Collecting training data). The service-role/secret key bypasses RLS, so it belongs only in server/.env and Render's environment. |
See server/.env.example.
What runs: server/app/cv/models/fill_regression_v2_mobilenet.pth, a
MobileNetV3-Small multitask model with a continuous 0–100% regression head
and an auxiliary 4-class head (empty / 50% / 80% / full). Test MAE is about
3.8% and RMSE about 8.9% on the training team's held-out set.
How it's wired in:
app/cv/base.py defines the contract: FrameProcessor.process(device_id, frame_bytes) -> FrameResult(fill_percent, aux_class).app/cv/real_frame_processor.py loads the checkpoint once at startup and
preprocesses exactly as in training (aspect-preserving resize to 224×224,
black padding, ImageNet normalization). It returns the regression output
clamped to 0–100, with the class prediction as aux_class. The dashboard
shows aux_class as a sanity badge.app/cv/reading_builder.py derives everything else from the fill %:
volume (500 ml bag by default), flow rate from the drop since the
device's previous reading, time remaining, and status (EMPTY ≤ 0.5%,
LOW_LEVEL ≤ 15%).app/cv/pipeline.py's active_processor selects the implementation.
MockFrameProcessor (random levels) is still there for working without
the model.Dependencies: torch, torchvision and pillow, installed as
CPU-only wheels via the extra index in requirements.txt.
Caveat: training labels covered only 0/50/80/100%, so intermediate values
are interpolation. The pipeline is verified end to end, but the model has
not been validated on real IV-bag photos at scale. Run a batch of real
photos through /docs or ml-model-training/test_v2.py before trusting
specific numbers in a demo.
ml-model-training/ holds the training scripts and artifacts:
fill_regression_v2_mobilenet.pth + _info.json: the deployed model.train_continuous_v2.py: trains it.test_v2.py: the reference inference that real_frame_processor.py
mirrors.fill_classifier_mobilenet.pth / train_classifier.py: the earlier
4-class classifier (97.4% accuracy) that v2 was initialized from.fluid_line_rf.joblib / fluid_line_model.json: an earlier classical-CV
prototype, not used.To update the model: retrain with train_continuous_v2.py, copy the new
.pth into server/app/cv/models/ (same filename) and restart. Nothing else
changes.
The current model was trained on public images and doesn't transfer well to real ESP32-CAM frames, so the backend can archive every frame it receives into Supabase as a shared, labelled dataset:
training-frames, stored as
<device>/<YYYY-MM-DD>/<device>_<timestamp>.jpg.training_frames, one row per image. It holds the
device, time, size, the deployed model's prediction, and the labels
label_fill_percent, label_yaw_deg, session and notes.capture_settings (Supabase Table Editor). For example, set
label_fill_percent = 80, label_yaw_deg = 30, session = run-03.
Every frame archived from then on carries those labels, within ~10 s of
the change. Set enabled = false to pause archiving. Labels can also be
corrected per row afterwards.SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY on the
backend (server/.env locally, Environment tab on Render). The schema and
bucket come from supabase/migrations/20261026120000_training_frames.sql.
Archiving runs after the camera's upload has been answered, and a failure
is only logged, never surfaced to the device.training-frames, Table Editor → training_frames).python ml-model-training/fetch_dataset.py --email you@.... It signs in
with your own FluidWatch account, which must have the admin role, and
writes the images plus labels.csv to ml-model-training/dataset/. Only
new images are downloaded on each run.Capacity on the Supabase Free plan:
capture_settings.enabled = false between deliberate collection sessions.frontend/TanStack Start (React 19, SSR) · TypeScript · Tailwind CSS v4 · shadcn/ui ·
Supabase auth · a raw WebSocket to the backend's /ws/bedfeed.
/ (landing), /auth, and, behind login, /dashboard
(live bed cards), /analytics (trends + hardware table from the REST API)
and /admin (staff roles, alert thresholds, backend URL; admins only).
Routes are files in src/routes/; routeTree.gen.ts is generated.src/lib/liveBeds.tsx owns the WebSocket connection
for the whole signed-in session, so it survives page changes, and raises
critical alerts (toast + optional beep). The connection URL and alert
thresholds come from ward_settings (src/lib/wardSettings.ts), which
admins edit at /admin. src/lib/fluidwatch.ts holds the bed model,
deriveStatus() and applyLiveUpdate().public/manifest.webmanifest and a hand-written
public/sw.js.npm run dev | build | test | lint | typecheck | format.
The lockfile is bun.lock; npm install works too.frontend/.env is committed and points at the project's Supabase instance.
The publishable key is safe to expose: it's an RLS-protected anon key.
| Variable | Used by |
|---|---|
VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY, VITE_SUPABASE_PROJECT_ID | Browser bundle (baked in at build time) |
SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, SUPABASE_PROJECT_ID | Server-side rendering |
src/integrations/supabase/client.server.ts also has a service-role admin
client. Nothing uses it yet; if something does, it needs
SUPABASE_SERVICE_ROLE_KEY, which must never reach the browser bundle.
supabase/supabase/migrations/ creates:
profiles and user_roles (app_role enum: admin / nurse), with RLS
so only admins can change roles.ward_settings, a single row with watch_level, critical_level,
min_flow, ws_url and sound_alerts, read by the dashboard and edited
from /admin.handle_new_user trigger, which creates the profile and makes the
first signup an admin and everyone after a nurse.training_frames, capture_settings, bucket
training-frames) and remote device management (firmware_releases,
device_config, bucket firmware). See
Remote management.Using the existing project: nothing to do. frontend/.env already
points at it. When deploying, add the production URL under
Authentication → URL Configuration (see below).
Setting up your own project:
Create a project at supabase.com, then
apply the schema. Either run supabase link --project-ref <ref> and
supabase db push from the repo root, or paste each migration into the
SQL editor in filename order.
In Authentication → URL Configuration, set Site URL to the
production URL. Add http://localhost:8080/**, the production URL plus
/**, and optionally https://*.vercel.app/** (for preview deploys) to
Redirect URLs. Email confirmation is on by default; you can turn it
off under Providers → Email for testing.
Google sign-in (optional):
https://<ref>.supabase.co/auth/v1/callback).Until then, the Google button shows a "Google sign-in failed" toast. Email sign-in is unaffected.
Copy frontend/.env.example to frontend/.env and fill in the URL and
anon key (both the VITE_ and plain variants) from Settings → API.
If the backend isn't on localhost:8000, set ward_settings.ws_url,
either from /admin after signing in or in the SQL editor.
firmware/esp32-cam/Firmware for an AI-Thinker ESP32-CAM (ESP32-D0WD-V3, 4 MB PSRAM, OV5640 5 MP,
fixed-focus lens). It connects to Wi-Fi, takes a JPEG every interval_s
(10 s by default) and POSTs it as multipart to SERVER_URL with
device_id/bed_label/patient_name/fluid_label and the bearer token. It does
no image analysis itself; the server does that. Every setting can be changed,
and new firmware installed, remotely over MQTT, so a unit only needs a
programmer once.
| File | Role |
|---|---|
src/main.cpp | Schedule, upload, MQTT command handling, status reports |
src/capture_config.cpp | The settings table: every name, range, default and meaning (fleet.py reads it too) |
src/camera_control.cpp | Camera power-up / capture / power-down, applying sensor settings |
src/remote.cpp | MQTT over TLS (HiveMQ Cloud), with reconnect backoff and a last-will "offline" status |
src/ota.cpp | Over-the-air install with SHA-256 check and automatic rollback |
src/thermal.cpp | Chip-temperature monitoring and the over-temperature cutoff |
include/secrets.h | Wi-Fi, SERVER_URL, API_AUTH_TOKEN, MQTT credentials (gitignored) |
flash setting turns it on for captures.pio device monitor, 115200): boot settings, a full sensor
report, and every upload's HTTP status. Expect 201.The OV5640 is the hottest part of the board while it streams, so the firmware
keeps it powered off except for the second or two each photo takes:
power up, discard warmup_frames while exposure and white balance settle,
take the photo, power down (sensor held in hardware power-down via PWDN).
On top of that:
cpu_mhz = 160 by default (80 is coolest, 240 fastest);xclk_mhz can lower the sensor's clock further.Thermal protection. The ESP32's own die temperature is checked every 2 s (smoothed) and reported everywhere as a read-only metric:
fleet.py status;tune result;What happens at each level:
| Chip temperature | What happens |
|---|---|
below temp_cutoff_c (80 °C) | normal |
≥ temp_cutoff_c | cooling: camera off, no photos, CPU at 80 MHz, until it is 10 °C below the cutoff |
≥ temp_cutoff_c + 10 °C | critical: Wi-Fi and camera off, deep sleep for 10 minutes, then a fresh boot |
Every change is reported as a thermal event.
The sensor is uncalibrated (it can be off by about 10 °C) and measures the
silicon, not the camera, so the limits sit well below the chip's 125 °C.
On the classic ESP32 the sensor is powered by the Wi-Fi radio. While modem
sleep has the radio idle, it returns a fixed 53.33 °C. The firmware skips that
value and keeps the radio awake for a few milliseconds per reading. On the
test unit (rev 3.1) this gave real readings: 37 °C at boot, climbing about
1 °C a minute while streaming. If no real reading arrives for a minute, it
reports thermal: unavailable, and the cutoff is off until readings return.
All of these can be changed remotely (fleet.py config), are kept across
reboots, and are listed by python firmware/tools/fleet.py params:
| Setting | Range | Default | What it does |
|---|---|---|---|
interval_s | 5..3600 | 10 | Seconds between scheduled photos |
cpu_mhz | 80..240 | 160 | CPU clock: 80, 160 or 240 MHz. Lower = cooler and less power; 160 is the balance |
camera_sleep | 0..1 | 1 | 1 = power the camera off between photos (cooler, safer); 0 = keep it running |
xclk_mhz | 10..20 | 20 | Clock fed to the sensor. Lower runs the sensor cooler but slower; 20 is standard |
warmup_frames | 0..30 | 5 | Frames thrown away after power-up so exposure and white balance settle |
settle_ms | 0..5000 | 100 | Extra wait after power-up before the warm-up frames |
flash | 0..1 | 0 | 1 = white LED on while capturing (even lighting, but can glare on glossy plastic) |
temp_cutoff_c | 50..100 | 80 | Chip temperature that pauses the camera until it cools 10 C; 10 C above it the unit shuts down |
frame_size | 0..21 | 8 | Resolution: QVGA=5 (320x240), VGA=8 (640x480), SVGA=9, XGA=10, HD=11, UXGA=13, QSXGA=21 |
jpeg_quality | 4..63 | 10 | JPEG quality, lower = better and bigger. Below ~8 can overflow at large sizes |
brightness | -2..2 | 0 | Overall brightness offset |
contrast | -2..2 | 0 | Contrast. +1 makes the liquid line stand out more |
saturation | -2..2 | 0 | Colour saturation. Clear fluids need little; coloured fluids may like +1 |
sharpness | -2..2 | 0 | Edge sharpening (not every driver version supports it) |
denoise | 0..8 | 0 | Noise reduction strength (not every driver version supports it) |
Plus the text settings bed_label, patient_name and fluid_label (at most 40
characters each).
For IV bottles and bags. Start from the defaults (VGA, quality 10, auto
exposure, camera sleeping between photos), then run fleet.py tune with the
unit mounted on a real stand. That takes one photo per preset (resolution,
flash, exposure, contrast, gain, white balance, fixed exposure) without
touching the dataset, so you can compare them side by side. Things to look for:
aec=0 aec_value=... agc=0 agc_gain=...) makes every frame look
the same, which helps the model more than any single setting.contrast=1 usually makes the liquid line clearer.flash=1 evens out lighting at night, but check for glare on the plastic.gainceiling means less noise in dim wards.fleet.py (laptop) --MQTT/TLS--> HiveMQ Cloud <--MQTT/TLS-- ESP32-CAM
| |
+--- firmware images, releases, settings ---> Supabase <-+ downloads a signed URL
Topics, all under fluidwatch/<DEVICE_ID>/: config (retained settings),
cmd (capture / status / report / reboot / ota), status (retained health,
"offline" as the last will), event (results) and photo/<tag> (tuning shots).
One-time setup:
Broker:
firmware/esp32-cam/include/secrets.h (MQTT_HOST, MQTT_USERNAME,
MQTT_PASSWORD).firmware/tools/.env
(gitignored):
MQTT_HOST=xxxxxxxx.s1.eu.hivemq.cloud
MQTT_USERNAME=...
MQTT_PASSWORD=...
Supabase: run
supabase/migrations/20261109120000_firmware_ota.sql in the SQL editor.
It creates:
firmware bucket;firmware_releases, one row per version with its SHA-256;device_config, each unit's settings.fleet.py uses the service-role key from server/.env.
Install the tool: pip install -r firmware/tools/requirements.txt.
Flash this firmware over serial one last time. It switches to a
partition table with two app slots (min_spiffs.csv), which OTA needs.
Day to day (from anywhere, no programmer):
python firmware/tools/fleet.py status # online units: firmware, Wi-Fi, temperature, last photo
python firmware/tools/fleet.py config IV-CAM-01 contrast=1 bed_label="Bed 12"
python firmware/tools/fleet.py tune IV-CAM-01 # photos per preset -> tune/
python firmware/tools/fleet.py capture IV-CAM-01 --out now.jpg
python firmware/tools/fleet.py release 1.1.0 --notes "..." # build, upload to Supabase, record SHA-256
python firmware/tools/fleet.py ota IV-CAM-01 1.1.0 # install it remotely
How an update stays safe:
fleet.py ota sends the unit a one-hour signed download URL and the image's
SHA-256 over the authenticated broker.rolled back from ... in its status.The board has no USB, so use an FTDI / USB-TTL adapter (voltage jumper on 5V) and a real 5 V / 2 A+ supply. Weak power is the most common cause of resets and camera-init failures.
| FTDI | ESP32-CAM |
|---|---|
| 5V | 5V |
| GND | GND |
| TX | U0R |
| RX | U0T |
To flash:
DOWNLOAD_BOOT.cd firmware/esp32-cam
cp include/secrets.h.example include/secrets.h # Wi-Fi, SERVER_URL, API_AUTH_TOKEN, MQTT
pio run -t upload
pio device monitor
SERVER_URL is the full frames endpoint. For local testing that's
http://<backend LAN IP>:8000/api/v1/frames; for production it's
https://<render-app>.onrender.com/api/v1/frames. API_AUTH_TOKEN must
match the backend's exactly. include/secrets.h is gitignored.
The repo also has two standalone sketches, kept as .txt so they aren't
built. To use one, swap it in for main.cpp:
src/camera_bringup_test.cpp.txt: bare camera check.src/camera_web_server.cpp.txt: tuning page with a live stream. It has a
control for every setting in the table above, shows the values the sensor
reports, and generates the matching fleet.py config command.Troubleshooting:
boot:0x33 and invalid header: 0xffffffff: something
is pulling IO12 high at power-up, which switches the flash chip to
1.8 V. Nothing may touch IO12.DOWNLOAD_BOOT but uploads still fail, the
FTDI TX → U0R wire isn't connected.BOARD_HAS_PSRAM in platformio.ini
applied, then do a clean rebuild.SERVER_URL is reachable from the board's network.fleet.py status shows nothing:
[MQTT] connect failed, state=..., where state 4 or 5 means bad
credentials..github/workflows/ci.yml runs on every push and PR to main:
| Job | Checks |
|---|---|
| Backend | ruff lint (rules in server/ruff.toml), then pytest in server/ (63 tests): every endpoint, auth, both WebSocket feeds, exports, reading derivation, path sanitizing, real-model smoke tests |
| Dashboard | lint, typecheck, Vitest (37 tests for the telemetry logic), production build in frontend/ |
| Firmware | pio run compiles firmware/esp32-cam against secrets.h.example; ruff + pytest for firmware/tools/fleet.py (27 tests) |
| End-to-end | Builds the backend Docker image, runs it, then server/e2e/smoke_test.py drives it from outside: frame upload → model → stored reading → /ws/bedfeed message → latest frame, devices, CSV/XLSX |
| Security | See Security |
Vercel also builds a preview of every PR. The main branch ruleset requires
the Backend, Dashboard, Firmware, End-to-end and Vercel checks to pass before
a PR can merge, and blocks force-pushes and deletion of main.
To run the same checks locally:
cd frontend && npm test && npm run lint && npm run typecheck
cd server
pip install -r requirements-dev.txt
ruff check .
pytest # throwaway DB/frames dir; never touches data/ or .env
# from the repo root
API_AUTH_TOKEN=dev-token docker compose up --build
python server/e2e/smoke_test.py --token dev-token
GitHub's built-in code and secret scanning aren't available on this private repo's plan, so CI's Security job covers the same ground for free:
frontend/.env, which is public by design: it
ships in the browser bundle, and Row Level Security protects the data
(.gitleaks.toml). Real secrets live only in gitignored files:
server/.env and firmware/esp32-cam/include/secrets.h.pip-audit (backend) and bun audit
(dashboard; fails on high or critical). frontend/package.json
overrides pins patched versions of transitive build-tool packages.
torch/torchvision CPU wheels aren't on PyPI, so pip-audit skips them..github/dependabot.yml): as few PRs as possible.
frontend/, server/,
firmware/tools/, CI actions together, as a multi-ecosystem group).
It carries minor and patch versions only, each released at least 90 days
earlier (cooldown)..github/workflows/dependabot-auto-merge.yml; needs Settings →
General → Allow auto-merge).Backend API writes require the bearer token (API_AUTH_TOKEN); uploaded
device IDs are sanitized before they become file names; corrupt uploads are
rejected with 422.
| Part | Where | Settings |
|---|---|---|
| Dashboard | Vercel, project frontend | Root Directory frontend, framework TanStack Start. Auto-deploys on push to main. Env vars: the six SUPABASE_* values from frontend/.env. vite.config.ts builds Vercel's Build Output API (.vercel/output/) via Nitro's vercel preset. |
| Backend | Render web service (free tier) | Root Directory server. Build pip install -r requirements.txt. Start uvicorn app.main:app --host 0.0.0.0 --port $PORT. Health check /health. Env: API_AUTH_TOKEN (set explicitly), CORS_ORIGINS = the Vercel origin. server/render.yaml mirrors these settings. |
| Firmware | The device | secrets.h → SERVER_URL=https://<render-app>.onrender.com/api/v1/frames and the same token |
After deploying:
ward_settings.ws_url (from /admin) to
wss://<render-app>.onrender.com/ws/bedfeed. It has to be wss://,
because browsers block plain ws:// from an https:// page.Render free-tier notes:
DATABASE_URL.special_effect | 0..6 | 0 | 0 none, 1 negative, 2 grayscale, 3 red, 4 green, 5 blue, 6 sepia |
awb | 0..1 | 1 | Auto white balance |
awb_gain | 0..1 | 1 | Apply the auto white balance gains |
wb_mode | 0..4 | 0 | White balance preset when awb_gain is on: 0 auto, 1 sunny, 2 cloudy, 3 office, 4 home |
aec | 0..1 | 1 | Auto exposure. 0 = use aec_value (fixed exposure, most consistent frames) |
aec2 | 0..1 | 0 | DSP night mode: longer exposure in low light (slower, may blur drips) |
ae_level | -2..2 | 0 | Auto exposure target: negative = darker, positive = brighter |
aec_value | 0..1200 | 300 | Manual exposure time, used when aec = 0 |
agc | 0..1 | 1 | Auto gain. 0 = use agc_gain |
agc_gain | 0..30 | 0 | Manual gain when agc = 0. Higher = brighter but noisier |
gainceiling | 0..6 | 3 | Max auto gain: 0=2x 1=4x 2=8x 3=16x 4=32x 5=64x 6=128x. Higher = brighter in dim light, more noise |
bpc | 0..1 | 0 | Black pixel correction |
wpc | 0..1 | 1 | White pixel correction (hides hot pixels) |
raw_gma | 0..1 | 1 | Gamma correction |
lenc | 0..1 | 1 | Lens shading correction (evens out darker corners) |
dcw | 0..1 | 1 | Downsize via the sensor (keep 1 for smaller frame sizes) |
hmirror | 0..1 | 0 | Mirror the image left-right |
vflip | 0..1 | 0 | Flip the image upside down (camera mounted inverted) |
colorbar | 0..1 | 0 | Test pattern instead of the image (diagnostics only) |