Skip to content

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.