Raspberry Pi 5 (rasp5)¶
Each greenhouse node is a Raspberry Pi 5 running a self-contained Docker Compose stack: a local database, the hardware API, the controller loop, a local dashboard, and Watchtower. The node keeps working when the cloud is unreachable.
rasp5 is a superproject. Its five components are git submodules (branch prod):
rasp5/
├── microcontroller-api/ # FastAPI service: hardware, REST API, background tasks
├── microcontroller-api-client/ # Controller: Sense-Think-Act loop
├── greenthumb-models/ # Shared `greenthumb` package (Pi + cloud)
├── greenthumb-rpi5/ # Raspberry Pi hardware drivers
├── local-dashboard/ # React + Vite + Tailwind SPA
├── db/ # Generated by schema-sync from the database repo (schema only, never edit)
├── deploy/ # Pi production stack (pull-only)
├── tests/vectors/ # Shared schedule test vectors
├── shell-scripts/ # pi-status, restore-on-boot watcher
├── compose.yaml # Workstation build/dev stack
├── Makefile # Workstation targets
└── .env.example
Network access
The node's dashboard and API are meant for a trusted local network or the VPN. Never port-forward them.
Services¶
| Service | Port | Role |
|---|---|---|
db |
5432 | PostgreSQL 17. Schema only, no seed: the catalog arrives from the cloud on the first sync. |
api |
8080 | microcontroller-api (Swagger at /docs). Holds the DeviceManager and runs six background tasks. |
controller |
— | microcontroller-api-client, the Sense-Think-Act loop. |
local-dashboard |
80 | nginx serving the SPA: Dashboard, Actuators, Cultivation, Settings. |
watchtower |
— | Checks for new images hourly. Updates local-dashboard automatically. api and controller images are pulled but only applied with make promote. |
microcontroller-api¶
The FastAPI service that owns the hardware. At startup it builds the DeviceManager from the local database, which instantiates the drivers from greenthumb-rpi5 for every component on the device. It serves these route groups, plus generic CRUD routes:
| Prefix | Purpose |
|---|---|
/state |
Current system state for the controller, heartbeat, safety mode |
/actuator |
Actuator list, commands, release, fail-safe flag |
/data |
Measurement history and latest values |
/camera |
Photo capture and MJPEG stream |
/settings |
Device mode, control rules, cultivation, calibration, runtime settings, manual sync |
Swagger is at http://<raspberry-pi-ip>:8080/docs. The API Reference lists the endpoints.
Background tasks¶
Six asyncio tasks run inside the API process. Their intervals are runtime settings on the device row, not environment variables. Read and change them with GET/PATCH /settings/runtime.
| Task | Interval (device column, default) | What it does |
|---|---|---|
| sensor-persist | sensor_interval_s (300 s) |
Writes sensor readings to the local database |
| photo-capture | photo_interval_s (4 h) |
Captures a photo and uploads it through the cloud API |
| cloud-sync | sync_interval_s (3600 s) |
Pulls config, then pushes measurements, device state, actuator and calibration logs, photos. POST /settings/sync runs one now. |
| liveness | control_interval_s (15 s) |
Pings the cloud |
| data-prune | once a day | Deletes synced local data older than the retention window |
| heartbeat-watchdog | heartbeat_timeout_s (60 s) |
Puts actuators in a safe state when the controller stops sending heartbeats |
Controller¶
The controller reads GET /state, evaluates the control rules, commands actuators with POST /actuator/{id}/command and sends POST /state/heartbeat. Its cadence (control_interval_s, default 15 s) comes from the device's runtime settings. See Controller Client.
local-dashboard¶
A React + Vite + Tailwind SPA, served by nginx on port 80. nginx proxies /settings, /state, /data, /camera, /measurement and /actuator to api:8080.
| Page | Route | Contents |
|---|---|---|
| Dashboard | / |
Live sensor readings, with a History tab for charts |
| Cultivation | /cultivation |
Active cultivation, growth phases and control rules |
| Actuators | /actuators |
Actuator controls and the camera |
| Settings | /settings |
Operating mode, cloud sync, device operations, calibration |
The old routes /graphs, /thresholds and /rules redirect to the Dashboard History tab and the Cultivation page.
Environment variables¶
The Pi reads deploy/.env (copy it from .env.example). Names only:
| Variable | Default | Description |
|---|---|---|
DEVICE_ID |
— | Device ID registered in the cloud |
DEVICE_TOKEN |
— | Device token for cloud sync, shown once when the device is created |
CLOUD_API_URL |
(empty) | Cloud sync base URL; empty disables sync |
DB_USER, DB_PASSWORD, DB_NAME |
— | PostgreSQL credentials |
PHOTOS_DIR |
— | Local photo directory inside the api container |
RESTORE_ON_BOOT |
— | Re-assert schedule-driven loads when the api container starts |
API_BASE_URL |
— | microcontroller-api base URL used by the controller |
LOG_LEVEL |
— | Log level for api and controller |
IMAGE_TAG |
— | Image tag to run (rollback pin) |
DOCKERHUB_USERNAME |
— | Docker Hub namespace for the images |
Operating a node¶
On the Pi (Linux), from the production stack directory:
cd ~/Documents/greenthumb/rasp5/deploy
make pull && make up # pull new images and reconcile the stack
make promote # apply pulled api + controller images (relays may switch)
make status # Pi health: temperature, throttling, load
make logs-api # follow one service's logs
make db-shell # psql shell
On a workstation (PowerShell), from rasp5/:
cmd /c make schema-sync # refresh db/ from the database repo
cmd /c make dev-reset # DEV ONLY: deletes the database and photo volumes, then rebuilds
Warning
The root Makefile is for workstations only. On the Pi, use deploy/Makefile.
Hardware access¶
The api container is given /dev/i2c-1, /dev/video0 and the GPIO chip and runs privileged (see rasp5/compose.yaml).
CI/CD¶
A push to prod makes GitHub Actions build arm64 images for microcontroller-api, microcontroller-api-client and local-dashboard, tagged :prod and :<sha>, and push them to Docker Hub. Watchtower on the Pi pulls them within the hour: the dashboard updates itself, while api and controller wait for make promote.