Raspberry Pi 5 (rasp5)¶
The rasp5/ directory contains everything that runs on each greenhouse node: Docker Compose services, the microcontroller API, the local React dashboard, hardware drivers, and shared Python packages.
Repository Structure¶
rasp5/
├── greenthumb-models/ # Shared SQLModel + Pydantic package (Pi + Cloud)
├── greenthumb-rpi5/ # Pi hardware drivers package
├── greenthumb-core/ # Compatibility shim (re-exports above two)
├── microcontroller-api/ # FastAPI service — hardware control + REST API
│ ├── app.py # FastAPI application
│ ├── mock_app.py # Mock app for testing without hardware
│ ├── routes/
│ │ ├── state_routes.py # /state/ — Sense-Think-Act state endpoint
│ │ ├── data_routes.py # /data/ — measurement history
│ │ ├── camera_routes.py # /camera/ — photo capture
│ │ ├── video_routes.py # /video/ — MJPEG stream
│ │ └── settings_routes.py # /settings/ — dashboard settings + cultivation API
│ └── utils/
│ ├── lifespan.py # Startup/shutdown + background task launch
│ ├── config_builder.py # Builds DeviceConfig from local DB
│ ├── config_sync.py # Applies cloud config to local DB
│ ├── db.py # SQLAlchemy engine helpers
│ ├── db_utils.py # Generic CRUD helpers
│ └── sync/
│ ├── client.py # HTTP client for Pi→Cloud communication (SyncClient)
│ └── tasks.py # sensor_persist, photo_capture, cloud_sync tasks
├── local-dashboard/ # React + Vite + Tailwind SPA for local display
│ ├── src/
│ │ ├── api/piApi.js # TanStack Query hooks for all Pi API routes
│ │ └── pages/ # Dashboard, Graphs, Thresholds, Settings, Cultivation
│ └── vite.config.js # Dev proxy → localhost:8080
├── microcontroller-api-client/ # Standalone controller (Sense-Think-Act loop)
├── db/
│ ├── 01_schema.sql # Fallback DDL (tables also created by SQLAlchemy)
│ └── 02_seed.sql # Seed data: units, variables, sensor/actuator models
├── resources/
│ └── implementation_plan.md # Phase planning doc
├── compose.yaml # Docker Compose for Pi deployment
├── Makefile
└── .env.example
Services¶
Database (db)¶
PostgreSQL 17 with automatic schema initialization.
- Port: 5432 (container-internal only)
- Data: Persisted in
postgres_datanamed volume - Init:
db/01_schema.sql+db/02_seed.sqlon first run
API (api)¶
FastAPI microcontroller API — the central hub for all hardware and data access.
- Port: 8080
- Docs:
http://<pi-ip>:8080/docs - Responsibilities:
- Read sensors in real time (via
DeviceManager) - Command actuators with per-actuator locking
- Serve MJPEG live video stream
- Run three asyncio background tasks (sensor persist, photo capture, cloud sync)
Background Tasks¶
Three coroutines are started with asyncio.create_task() in the FastAPI lifespan and cancelled cleanly on shutdown:
| Task | Interval env var | Default | What it does |
|---|---|---|---|
sensor_persist_task |
SENSOR_INTERVAL |
300 s | Calls dm.save_data(), writes Measurement rows |
photo_capture_task |
PHOTO_INTERVAL |
14400 s (4 h) | Captures JPEG via camera actuator, creates Photo row, attempts immediate cloud upload |
cloud_sync_task |
SYNC_INTERVAL |
86400 s (24 h) | Pushes unsynced measurements + photos, pulls latest config, updates last_seen_at |
Startup Sequence (utils/lifespan.py)¶
- Create DB engine + tables (
create_db_and_tables()) - Create
SyncClient(readsCLOUD_API_URL,DEVICE_TOKENfrom env) - Try
sync_client.pull_config()→ if successful, callapply_cloud_config_to_local_db()to update local DB, recordlast_config_syncinsync_metadata build_config_from_db()— assembleDeviceConfigDTO from (updated) local DBDeviceManager.init_from_config(config)— instantiates all sensor/actuator drivers- Launch three background tasks (
sensor_persist,photo_capture,cloud_sync) - On shutdown: cancel all tasks,
device_manager.shutdown(),engine.dispose()
Controller (controller)¶
Implements the Sense-Think-Act loop, calling the API every CONTROL_INTERVAL seconds (default 15 s):
GET /state/— fetch sensor readings + active thresholds + variable names- Evaluate each threshold; determine actuator commands
POST /state/actuators/{id}/commandfor each actionPOST /state/heartbeat— reset safety-mode timer
If the heartbeat is missed for > 2× CONTROL_INTERVAL, the API enters safety mode (all actuators commanded to safe state).
Local Dashboard (local-dashboard)¶
React SPA served by nginx on port 80, proxying API calls to api:8080.
| Page | Route | Description |
|---|---|---|
| Dashboard | / |
Live sensor cards (real variable names + units), sparklines, MJPEG camera feed, safety mode badge, active phase indicator |
| Graphs | /graphs |
Historical charts (recharts), time range selector, variable filter using real names |
| Thresholds | /thresholds |
Inline threshold editing, is_dirty pending badges |
| Settings | /settings |
Device mode, sync status, manual sync trigger |
| Cultivation | /cultivation |
Active cultivation view with phase timeline and advance-phase form; Begin cultivation form (species picker) and End cultivation button when no active cultivation |
Watchtower¶
Polls Docker Hub every 1 hour (WATCHTOWER_POLL_INTERVAL: 3600), pulls updated images, and restarts affected containers.
Environment Variables¶
All variables come from rasp5/.env (copy from .env.example).
Required¶
| Variable | Description |
|---|---|
DEVICE_ID |
Device ID registered in the cloud admin dashboard |
DEVICE_TOKEN |
Bearer token for cloud sync — shown once at device creation |
DB_PASSWORD |
PostgreSQL password |
Database¶
| Variable | Default | Description |
|---|---|---|
DB_USER |
greenthumb |
PostgreSQL user |
DB_NAME |
greenthumb |
Database name |
DATABASE_URL |
auto-built | Full SQLAlchemy URL (overrides individual DB_* vars) |
Cloud Sync¶
| Variable | Default | Description |
|---|---|---|
CLOUD_API_URL |
https://api.greenthumb.io |
Cloud gateway base URL |
DEVICE_TOKEN |
— | Pi bearer token (also in Required above) |
Background Task Intervals¶
| Variable | Default | Description |
|---|---|---|
SENSOR_INTERVAL |
300 | Seconds between sensor persist cycles |
PHOTO_INTERVAL |
14400 | Seconds between photo captures (4 h) |
SYNC_INTERVAL |
86400 | Seconds between cloud sync cycles (24 h) |
PERSIST_INTERVAL |
300 | Kept for backwards compatibility — use SENSOR_INTERVAL |
Camera¶
| Variable | Default | Description |
|---|---|---|
CAMERA_SRC |
/dev/video0 |
Camera device path |
CAMERA_WIDTH |
1280 | Capture width in pixels |
CAMERA_HEIGHT |
720 | Capture height in pixels |
PHOTOS_DIR |
/data/photos |
Local directory for captured photos |
Controller¶
| Variable | Default | Description |
|---|---|---|
API_BASE_URL |
http://api:8080 |
microcontroller-api base URL (controller reads this) |
CONTROL_INTERVAL |
15 | Sense-Think-Act loop cadence in seconds |
HYSTERESIS_PCT |
0.1 | Threshold dead-band fraction (10%) |
CI/CD¶
| Variable | Description |
|---|---|
DOCKERHUB_USERNAME |
Docker Hub username for image pushes |
Makefile Commands¶
| Command | Description |
|---|---|
make up |
Start all services (docker compose up -d) |
make down |
Stop all services |
make logs |
Follow logs from all services |
make logs-api |
Follow API logs |
make update |
git pull + docker compose pull + make up |
make status |
Container health + last sync times |
make backup |
Dump local PostgreSQL to /data/backups/ |
make db-shell |
Open psql shell |
make clean |
Remove all containers + volumes (destructive!) |
Hardware Access¶
The api container has direct device access via Docker bind-mounts and privileged: true:
| Device | Path | Purpose |
|---|---|---|
| Camera | /dev/video0 |
USB camera (MJPEG stream + photo capture) |
| I2C bus | /dev/i2c-1 |
AHT10, BMP280, TSL2561 sensors |
| GPIO | /dev/gpiochip0 |
RGB LED, Water Pump actuators |
CI/CD¶
On push to main, GitHub Actions builds ARM64 Docker images for api and controller, pushes them to Docker Hub. Watchtower on the Pi picks up the new images within 1 hour and restarts the affected containers.