Skip to Content
DevelopersImplementation plan

Gardyn Liberation — Implementation Plan (Phase 0 → Done)

Draft. This page describes intended behaviour that has not been verified on a real Gardyn yet.

Status: v3 — Phases 0, 3, 4, 5 and the software half of 7/8 built (2026-10-31); Phase 1 (hardware recon) and the Rust driver validation in Phase 2 need a real unit · Date: 2026-10-29 · Author: @ng (drafted with Claude) Name: seedypod (ADR-0001; drafted as leafypod). Repo: seedypod/core.

Sibling project to sleepypod : a sealed consumer appliance (Gardyn hydroponic tower) whose embedded Linux controller (a Raspberry Pi) is re-flashed to run a local, cloud-free control stack. This plan mirrors sleepypod’s conventions exactly (branching, CI, Codecov, Stryker, semantic-release, Renovate, Vercel demo, Nextra docs on Pages, ADRs, OKF knowledge bundle) so tooling and muscle memory carry over.

Legend: ∥ = can run in parallel with the preceding item(s) in the same phase · ⛔ = hard dependency · ⏱ = rough effort for one person.


0. Ground truth (what we know going in)

Everything below comes from iot-root/garden-of-eden (config.py, app/lib/hardware.py, docs/design.md, mqtt.py) and its issue tracker. Treat as hypotheses to verify on our unit in Phase 1.

0.1 Hardware map (BCM numbering)

FunctionInterfacePin / addrNotes
Grow lightPWMGPIO188 kHz; hardware PWM channel on every Pi
PumpPWMGPIO2450 Hz; not a hardware PWM pin → software PWM; upstream caps duty at 30 %
Water levelUltrasonic DYP-A01trig GPIO26 / echo GPIO19µs-timing sensitive; take 5-sample median
ButtonGPIO inGPIO13single = light, double = pump
Over-temp alertGPIO inGPIO25from PCB thermal monitor
PCB tempI²C PCT20750x48all models
Pump powerI²C INA2190x40V/I/W
Air temp/humidityI²C0x5C AM2320 (Gardyn 1.0/2.0) · 0x38 DHT20 (3.0 / Studio)AM2320 needs a wake-up write before it ACKs (#142)
CamerasUSB UVC/dev/video0 upper, /dev/video2 lowervia on-board hub; fswebcam needs ≥10 skip frames (#143), exits 0 when unplugged (#144)
ControllerPi Zero W (early) / Zero 2 WmicroSDheader soldered in mirrored orientation

Model detection: DHT20 present ⇒ 3.0+; else AM2320 ⇒ 1.0/2.0; Studio is 3.0-class with a ~4 gal tank (Home ≈ 5 gal, 30 pods).

0.2 Safety invariants (lifted from upstream’s bug list — these are the spec, not nice-to-haves)

  1. Single owner of the pump. Max run (900 s upstream default), dry-run interlock (water distance > low threshold ⇒ refuse), and over-temp cutoff are enforced inside the pump driver. No caller — REST, MQTT, cron, CLI, MCP — can bypass them (#108, #109, #114).
  2. One long-lived daemon owns the pins. No per-invocation scripts; starting a process must never glitch an output (#131).
  3. Median-of-N ultrasonic in the driver, used identically by guards and alerts (#111).
  4. Serialized camera access; validate output size, not exit code (#117, #143, #144).
  5. Per-unit identity derived from /proc/cpuinfo serial, never a shared default (#130).
  6. Fail-safe on fault = stay off until a human acknowledges; auto-recovery is opt-in (sleepypod  ADR-0022 pattern).
  7. Power-loss recovery: persisted actuator + grow-cycle state restored on boot (#3).

0.3 Upstream roadmap we are superseding

6 open milestones (0–53 % complete, no due dates), 63 open issues, two “product” milestones (Plant Guidance, Web UI) never received an issue. All of it is table-stakes IoT; the only non-trivial items are model detection (#72) and the safety bugs above. We take the pin map and bug list; we fork nothing (GPL-3 — a clean-room reimplementation keeps our license choice open; see ADR-0002).


1. Architecture

┌───────────────── core (TypeScript: Hono server + Vite/React SPA, TanStack Router) ─────────┐ │ Web UI (React) tRPC routers MCP server Scheduler Grow-cycle Alerts SQLite │ │ HA bridge (MQTT discovery) HomeKit (hap-nodejs, later) Demo mode (simulated HAL) │ └──────────────────────────────▲──────────────────────────────────────────────────────────────┘ │ HAL contract: MQTT (telemetry/commands/HA discovery) + HTTP (stills, health) ┌──────────────────────────────┴──── hal (Rust, one static binary, systemd) ─────────────────┐ │ Sensor/Actuator trait registry · safety interlocks · mDNS · simulator backend · OTA-able │ │ rppal (GPIO/PWM/I²C) · embedded-hal driver crates · v4l2 capture · rumqttc · axum │ └──────────────────────────────▲──────────────────────────────────────────────────────────────┘ │ Gardyn PCB (lights, pump, sensors, cameras, button)

Deployment topologies (pick per unit; both supported from day one):

TopologyPi in the unitRuns HALRuns coreWhen
A. Single-box on Zero 2 WZero 2 W (drop-in, ~$15)yesyes — Hono + static SPA idles ~60–90 MB, fits 512 MBDefault. Whole stack inside the unit, no extra hardware
B. SplitZero 2 Wyesno — core on a LAN box (home server / HA host)Multiple units under one core, or if A proves tight
C. Pi 5 single-boxPi 5 (+ ribbon, own PSU)yesyesOnly when on-device CV (Phase 7) is wanted

The lightweight core is what makes A possible: sleepypod ’s Next.js standalone needed the Pod’s 2 GB and a post-processed build (strip-turbopack-external-hashes, hand-copied .next/static, rm instrumentation.ts for the Vercel demo). See ADR-0003. The HAL contract is identical in all three, so a unit can move between topologies by changing one URL.

1.1 Repository layout (single repo, mirrors sleepypod/core + its modules/ multi-language precedent)

core/ ├── server/ # Hono (Node): mounts tRPC (@hono/trpc-server), OpenAPI (trpc-to-openapi fetch handler), │ # MCP (WebStandardStreamableHTTPServerTransport, Origin guard), serves web/dist, starts instrumentation ├── web/ # Vite + React SPA; TanStack Router file routes in web/src/routes/; globals.css tokens; ds/ ui/ AppShell/ ├── src/ │ ├── server/ # tRPC routers, context; src/mcp/ tools (framework-agnostic, ported from sleepypod) │ ├── hardware/ # HAL client (MQTT + HTTP), capability probing, simulator │ ├── scheduler/ # cron-parser + node-schedule jobs (lights, pump, captures) │ ├── grow/ # grow-cycle state machine, reminders │ ├── alerts/ # persisted, dismissible alerts (water_low, pump_fault, …) │ ├── db/ # drizzle schema + migrations (sqlite) │ ├── mqtt/ # HA bridge (discovery, state, commands) — ADR-0019 pattern │ ├── lib/ # shared pure logic │ └── hooks/, components/ ├── hal/ # Rust workspace (cargo), cross-compiled in CI │ ├── crates/hal-core # traits, registry, safety, config │ ├── crates/hal-drivers# ina219, pct2075, am2320, dht20, dyp-a01, pwm-light, pwm-pump, uvc │ ├── crates/hal-sim # simulator backend (feature-gated) │ └── crates/hald # the daemon binary (axum + rumqttc + mdns-sd) ├── docs/ │ ├── adr/ # NNNN-slug.md, sleepypod template │ ├── hardware/ # board photos, pin map, teardown notes │ ├── DEPLOYMENT.md INSTALLATION.md DEBUGGING.md PRIVACY.md │ └── testing/ ├── knowledge/ # OKF v0.2 bundle (horticulture evidence, calibration, CV eval) ├── scripts/ # install, deploy, flash, probe ├── tests/ # top-level: instrumentation/proxy tests, integration/, browser/ (Playwright *.e2e.ts via run.mjs) │ # unit tests are COLOCATED: src/<area>/tests/<file>.test.ts (sleepypod convention); hal has cargo tests ├── codecov.yml .releaserc.json renovate.json vercel.json stryker.config.mjs ├── vitest.config.mts eslint.config.ts .githooks/ .node-version AGENTS.md

2. Phases

Phase 0 — Repo & conventions bootstrap ⏱ 1 day

Goal: an empty-but-complete repo where dev → main already releases, docs deploy, demo deploys, and coverage reports — before any product code.

#Task∥Notes
0.1Create GitHub org + repos core, <name>.github.io; org profile README (.github/profile)gh repo create <org>/core --public --license <see ADR-0002>
0.2ADR-0001 New repository & naming, ADR-0002 License∥ 0.1Clean-room ⇒ MIT/Apache-2 possible; GPL only if we vendor upstream code (we won’t)
0.3Branch model: dev (integration) → main (release). Protect both; merge queue on main; squash on main, merge-commit on dev⛔ 0.1identical to sleepypod 
0.4Scaffold Hono (Node) + Vite/React 19 + TanStack Router (file-based routes via @tanstack/router-plugin), TS strict, pnpm, .node-version 24.x; Tailwind 4; copy eslint, editorconfig, .githooks (lint-staged pre-commit; tsc+eslint+drizzle-check+changed-tests pre-push), prepare hook. Dev: vite proxies /api → Hono∥ 0.3ADR-0003 records Next.js as rejected: needs ≥2 GB, build post-processing, SSR unused
0.5CI test.yml (name: CI): matrix {Lint (changed files only), Typecheck, Unit (vitest run --coverage, Codecov upload), Browser (NODE_ENV=production CI=1 pnpm build && pnpm test:browser, Playwright chromium)}; concurrency cancel-in-progress; triggers push dev, pull_request, merge_group, workflow_dispatch; CI=1 disables hardware instrumentation at startup⛔ 0.4no coverage thresholds in vitest — Codecov gates
0.6CI rust.yml: fmt, clippy -D warnings, test, cross builds for arm-unknown-linux-gnueabihf (Zero W), aarch64-unknown-linux-gnu (Zero 2 / Pi 4 / Pi 5); upload hald-<target> artifacts; cargo-llvm-cov → Codecov flag rust∥ 0.5path-filtered to hal/**; Codecov carryforward: true so TS-only PRs don’t zero it
0.7codecov.yml with flags javascript (src/, app/, lib/) and rust (hal/); project target auto ±1 %, patch ±8 %∥ 0.5
0.8release.yml on main: semantic-release (conventionalcommits, sleepypod releaseRules), build, pin .git-info, tarball core.tar.gz + attach hald-* binaries to the GitHub Release⛔ 0.5, 0.6
0.9pr-title-gate.yml (regex ^(feat∣fix∣perf∣revert)(\(…\))?!?: on PRs to main), back-merge.yml (workflow_run after “Analyze & Release”, --no-ff merge main→dev pushed directly with BACKMERGE_TOKEN, opens a “needs a hand” issue on conflict), build.yml (tarball on every branch for gp-update-style installs), dev-release.yml (push to dev ⇒ re-tag rolling dev-latest prerelease), ref-collision-check.yml (fail if a branch name equals a tag), openapi.yml (boot the built app in CI, poll /api/openapi.json, upload artifact)∥ 0.8all copied from sleepypod/core
0.10mutation.yml: Stryker, weekly Sat 06:00 UTC + mutation-test label; shards by module∥ 0.8can land empty-config now, shards in Phase 3
0.11renovate.json: automerge minor/patch, 3-day min age, Monday schedule, major = manual, conventional prefixes; add cargo manager∥ 0.8
0.12vercel.json: deploy only dev, VITE_DEMO=1 pnpm --filter web build → static SPA with the in-browser tRPC demoLink (sleepypod src/demo/ pattern); no instrumentation stripping needed⛔ 0.4live demo URL goes in README like sleepypod.vercel.app
0.13Docs site repo: Nextra/MDX, content/{getting-started,core,hal,hardware,developers,troubleshooting}, ci.yml (build + playwright), pages.yml (GitHub Pages)∥ 0.4copy sleepypod.github.io skeleton
0.14knowledge/ OKF v0.2 bundle: index.md, log.md, references/okf.md pinned to the same upstream spec revision, scripts/validate_knowledge.py + CI step∥ 0.13starts nearly empty; Phase 6/7 fill it
0.15AGENTS.md (+ .claude/): copy sleepypod rules; add HAL-specific ones (never bypass safety module, never hardcode pins outside hal/config)∥ 0.4
0.16.github/SUPPORT.md (model/version, how to grab + share logs); labels created by hand: mutation-test, dependencies, hardware, safety. Sleepypod has no issue/PR templates, CODEOWNERS, commitlint or labeler — don’t add them∥ 0.13
0.17ADR-0003 Core stack (TS/React/Next/Lingui), ADR-0004 Two-tier HAL/core split, ADR-0005 tRPC, ADR-0006 Rust for the HAL, ADR-0007 HAL contract: MQTT + HTTP∥ 0.2sleepypod ADR format: docs/adr/NNNN-kebab-title.md, no frontmatter, # ADR NNNN: Title, **Status:** Proposed / **Date:** YYYY-MM-DD, sections Context / Decision / (Rationale, Alternatives Considered) / Consequences (Positive/Negative/Neutral) / References, amendments appended as ## Amendment (date); status Proposed until Phase 2 proves them
0.0Spike (do first, ~1 h): does Next.js fit? Build sleepypod/core unchanged, run next start under systemd-run -p MemoryMax=512M on aarch64 (or on the Zero 2 W after Phase 1.4); record idle RSS, boot-to-ready, first-request latency, and the same for a hello-world Hono + Vite SPA. Decide ADR-0003 on the numbers: Next fits ⇒ port app/ verbatim and keep sleepypod’s build; else Hono/Vite. The rest of this plan is written for the Hono/Vite outcome; swapping back touches only 0.4, 0.12, 0.18, 3.7, 3.8, 3.11, 4.3reversible; nothing downstream depends on the answer until 0.4
0.18OpenAPI + trpc-panel like sleepypod : trpc-to-openapi fetch handler mounted in Hono at /api/*, spec at /api/openapi.json, docs at /api/docs, panel at /panel∥ 0.4lands with the first router in Phase 3 but the CI artifact job is Phase 0

Exit criteria: a feat: PR dev→main cuts v0.1.0 with a Release containing an (empty) core tarball and hald binaries for both targets; Codecov comments on PRs; dev auto-deploys to Vercel; docs site live on Pages.

Phase 1 — Hardware reconnaissance & root ⏱ ½ day hands-on ∥ Phase 0 (fully independent)

#Task∥Notes
1.1Open the unit; photograph the PCB (both sides), the Pi, header orientation, camera USB routing, PSU rating. Commit to docs/hardware/This decides Topology B feasibility
1.2dd the stock microSD to an image; store off-repo (it’s Gardyn’s proprietary firmware). Note card size⛔ 1.1rollback path — never skip
1.3Optional: mount the image read-only, inventory their services/pins/cloud endpoints into docs/hardware/stock-firmware-notes.md (no code copied)∥ 1.4pure recon; useful for confirming pin map on our revision
1.4Flash Raspberry Pi OS Lite (64-bit if Zero 2) with SSH + Wi-Fi + hostname preconfigured in Imager; boot; ssh in⛔ 1.2this is “root” — owning the boot media
1.5Probe: i2cdetect -y 1; run upstream’s Python drivers as throwaway probes only (distance.py, light.py --on, pump.py --on --speed 20 for 5 s with water present, both cameras) to confirm every row of §0.1 on our model⛔ 1.4record actual addresses/model; AM2320 won’t show in i2cdetect without wake
1.6Record findings: docs/hardware/pin-map.md (authoritative), update §0.1, file ADR-0008 Target controller (stay on Zero W vs Zero 2 W vs Pi 5)⛔ 1.5
1.7If upgrading: order Zero 2 W (no header) + header, or Pi 5 + 40-pin ribbon + USB-C PSU; for Pi 5 plan to cut 5 V on the ribbon (keep GND) and re-route cameras∥ 1.6

Exit criteria: we can toggle light and pump and read every sensor from a shell on our own OS, with the pin map confirmed and the stock image backed up.

Phase 2 — HAL (Rust daemon) ⏱ 1–2 weeks ⛔ Phase 1.6 (pin map) · Phase 0.6 (CI)

#Task∥Notes
2.1ADR-0007 finalize HAL contract (freeze before 2.4 so core can start): MQTT topic tree, payload schemas (JSON, versioned v1), HTTP endpoints, HA discovery entity set, mDNS service _seedypod-hal._tcp with TXT id,model,vthe one artifact that unblocks Phase 3 in parallel
2.2hal-core: Sensor/Actuator traits, config-driven registry (hal.toml: [[sensor]] kind="ina219" addr=0x40), unit types, Reading{ts,value,unit,quality}, reprobe-on-failure⛔ 2.1pH/EC/RTD later are just more registry entries
2.3hal-core::safety: pump max-run timer, dry-run interlock (distance median vs threshold), over-temp cutoff (GPIO25 + PCT2075 hysteresis 34/36 °C), light sanity; fault ⇒ latched off until acknowledge; every actuator command passes through here⛔ 2.2§0.2 items 1, 3, 6; unit-test exhaustively; ADR-0009 Pump safety guard (sleepypod ADR-0022 shape)
2.4hal-sim backend: deterministic fake hardware (tank drains when pump runs, temp drifts, cameras serve fixtures). Used by hald --sim, by core’s demo mode, and by CI∥ 2.3
2.5Drivers (one PR each, all ∥ after 2.2): pwm-light (rppal hw PWM, 8 kHz) · pwm-pump (thread soft-PWM 50 Hz, duty cap configurable, default 30 %) · ina219 · pct2075 · dht20 · am2320 (wake write) · dyp-a01 (trig/echo, 5-sample median, timeout) · button (debounce, single/double/long) · uvc (v4l2 or fswebcam shell-out; mutex; skip ≥10 frames; validate JPEG size)∥ each othereach ships with a loopback/sim test
2.6hald: tokio runtime; rumqttc client with LWT …/availability; publish telemetry at configurable cadence + on change; subscribe commands; axum GET /health, GET /camera/{upper,lower} (JPEG), GET /system; mDNS advertise; persisted state file for power-loss recovery; SIGTERM ⇒ pump off⛔ 2.3, 2.5
2.7HA discovery published by the HAL itself (light, fan-as-pump w/ percentage, sensors with proper device_class, binary_sensor water_low, event entity for button) so HA works with zero core∥ 2.6upstream’s entity set is the starting list
2.8Per-unit identity (/proc/cpuinfo Serial) + model detection (I²C fingerprint)∥ 2.6
2.9Packaging: scripts/install-hal.sh (systemd unit, hal.toml, user hal in gpio,i2c,video), OTA-able self-update from GitHub Release assets, hald --check config lint⛔ 2.6
2.10On-device soak: 72 h on the real unit with scheduled runs from mosquitto_pub only; watch for PWM glitches, I²C dropouts, camera lockups⛔ 2.9
2.11ADR-0006/0007/0009 → Accepted; docs content/hal/⛔ 2.10

Exit criteria: hald runs as a service on the unit, HA sees the device via discovery alone, all §0.2 invariants have tests, CI builds both targets, coverage flag rust reporting.

Phase 3 — Core (tRPC / scheduler / DB / MCP) ⏱ 1–2 weeks ⛔ Phase 2.1 (contract) · ∥ Phase 2.2–2.10

Core is built against hal-sim from day one; real hardware only enters at 3.9.

#Task∥Notes
3.1src/hardware/halClient: MQTT subscribe/publish + HTTP; typed by the contract (zod schemas generated from a shared JSON Schema in hal/contract/); capability probing from mDNS//system not from config (sleepypod probe-first pattern)
3.2Drizzle + SQLite schema: device_settings, device_state, telemetry (downsampled), schedules (light/pump/capture), grow_cycles, pods (30 slots: plant, planted_at, notes), alerts, captures, light_hours (cumulative, for LED aging)∥ 3.1ADR-0010 Drizzle/SQLite (copy)
3.3tRPC routers: device, light, pump, sensors, schedule, grow, pods, camera, alerts, system; event bus → mutation broadcast (ADR-0015 pattern) delivered to the UI as tRPC subscriptions over SSE (httpSubscriptionLink) — no WebSocket server/port; add @hono/node-ws only if a high-rate stream (live camera preview) ever needs it⛔ 3.1, 3.2the Pi provides nothing here; transports are purely the server’s
3.4Scheduler: cron-parser + node-schedule; light curves (sunrise/sunset ramps), pump cycles (duration not on/off pairs), capture intervals; schedule compiler clamps to HAL limits∥ 3.3
3.5Grow-cycle state machine + reminders (thinning d14, root-check d21, harvest d35, nutrients every 7 d — all settings); acknowledge from UI/HA/MCP∥ 3.4
3.6Alerts module: persisted, dismissible (water_low, pump_fault, over_temp, camera_missing, hal_offline)∥ 3.4
3.7MCP server on top of tRPC, sleepypod  shape exactly: Hono route /api/mcp stateless Streamable HTTP (fresh McpServer + WebStandardStreamableHTTPServerTransport({sessionIdGenerator: undefined, enableJsonResponse: true}) per request, Origin≠Host rejected as DNS-rebinding guard, GET/POST/DELETE), implementation in src/mcp/{server,caller,schemas,units}.ts + tools/{status,control,schedules,grow,maintenance,settings}.ts; tools = read state, set light/pump within limits, run-once pump, capture still, schedule CRUD, grow actions; resources = latest telemetry, captures; MHS-shaped: every capability a named read/write primitive with units + declared limits (ADR-0011 MCP & MHS alignment)⛔ 3.3
3.8Demo mode (VITE_DEMO=1): tRPC demoLink with in-browser handlers over hal-sim-equivalent TS fakes + seeded history; powers Vercel⛔ 3.1port sleepypod src/demo/
3.9Integration against real hald on the bench (Topology A); contract tests in tests/⛔ 2.10, 3.3
3.10Stryker shards: server, scheduler, hardware, grow, alerts, db∥ 3.9
3.11Install/deploy: scripts/install (systemd core.service :3000 running node server/dist/index.js + web/dist), scripts/deploy (build locally, archive over SSH), update-from-Release via tRPC (sleepypod  sp-update pattern, gp-* bin scripts); LAN-only iptables policy; measure RSS on Zero 2 W (target < 120 MB)∥ 3.9ADR-0012 Deployment

Exit criteria: core schedules and drives a real unit via hald; MCP client (Claude) can read sensors and run a bounded pump cycle; Vercel demo works without hardware.

Phase 4 — Web UI ⏱ 1 week ⛔ 3.3 (routers) · ∥ 3.4–3.11

#Task∥Notes
4.1Port the sleepypod design system (ADR-0016 Shared design language): copy app/globals.css (Tailwind 4 @theme, ~250 semantic vars, IBM Plex Sans/Mono), src/components/ds/ (core/layout/forms/feedback/notice/toast/charts), src/ui/ (shadcn base-vega on base-ui, lucide, CVA), src/components/AppShell/ (Sidebar/TabBar/navItems/footerStatus/PullToRefresh). Replace the 5 next/link + next/navigation imports with TanStack Link/useLocation/useSearch. Rename domain tokens: --color-cool/warm → --color-water/light, --side-left/right → --pod-ok/--pod-attention, --stage-*/--chart-vital-* → growth/telemetry series. Lingui from the start~1 day; do NOT extract a shared @org/ds package yet — copy first, extract after the second consumer stabilizes
4.1bReuse screen patterns: Schedule/ (CurveEditor, CurveChart, SetPointEditor, DaySelector, TimeInput, ConfirmDialog) → light curves + pump schedules; TempControl dial → brightness; Sensors/Environment cards → telemetry; status/+diagnostics/ → HAL health⛔ 4.1
4.2Screens: Dashboard (live sensors, water gauge, light/pump state), Lights (brightness + curve editor), Water (run-once with countdown, history, tank calibration), Schedule editor, Grow cycle + 30-pod grid, Cameras (latest + timelapse scrubber), Alerts banner, Settings (HAL URL, MQTT, HA, limits, units)∥ each other after 4.1
4.3Browser tests (playwright *.e2e.ts via tests/browser/run.mjs, webServer: node server/dist/index.js with CI=1, temp DBs seeded) in CI “Browser Tests” lane⛔ 4.2
4.4Wall-display mode (kiosk route, large type, dark) — the “extending displays” lesson∥ 4.3
4.5Docs content/core/ user guide, screenshots captured via capture/ scripts like the sleepypod  site⛔ 4.2

Exit criteria: everything controllable from the UI; demo on Vercel; docs with screenshots.

Phase 5 — Integrations ⏱ 3–5 days ⛔ Phase 3 · tasks ∥ each other

#TaskNotes
5.1HA: core-level bridge adds what HAL discovery can’t (grow-cycle reminders as events, alerts, schedule enable switches, run-once service); docs/homeassistant/lovelace-example.yaml; ADR-0013 MQTT bridge (copy ADR-0019)HAL-only HA already works from Phase 2
5.2HomeKit via hap-nodejs (light as Lightbulb, pump as Switch/Fan, sensors) — reuse sleepypod  identity derivation ADR-0020optional
5.3iOS: evaluate forking sleepypod/ios shell (tRPC client, networking, mock client pattern) → seedypod/iosoptional; separate repo
5.4M5 Dial / small physical controller for light+pump (sleepypod  m5-rotary-dial pattern, mDNS discovery)optional
5.5Notifications: HA is the delivery channel; no push service of our owndecision in ADR-0013

Phase 6 — Hardware expansion ⏱ 1–2 weekends ⛔ Phase 2 · ∥ Phases 4–5

#Task∥Notes
6.1Pi 5 swap (Topology B): ribbon, cut 5 V, own PSU, cameras to Pi USB; hald on aarch64; note pigpio is irrelevant to us (rppal works on RP1)ADR-0008 update
6.2Water chemistry: Atlas EZO-pH (I²C 0x63), EZO-EC (0x64), EZO-RTD (0x66) on the shared bus with Atlas isolated carrier (pH/EC interfere in one reservoir otherwise); drivers in hal-drivers; calibration flows in UI; HA device_class ph/conductivity/temperature∥ 6.1registry entries + 3 small drivers
6.3Dosing: 2–3 peristaltic pumps (pH-down, nutrient A/B) on spare GPIO via a MOSFET board; Actuator entries with hard daily volume caps in safety; dosing controller in core (target bands, hysteresis, lockout after N doses) — off by default, ADR-0014 Auto-dosing safety⛔ 6.2the feature Gardyn doesn’t sell
6.4Light-hours counter → LED aging estimate + replace reminder (upstream’s “Maintenance” milestone idea)∥ 6.2trivial once 3.2 exists
6.5knowledge/ OKF: concepts for nutrient/pH target bands by crop, calibration protocol, tank geometry — each with sources∥ 6.2

Phase 7 — Plant intelligence (CV) ⏱ open-ended ⛔ 6.1 (compute) or off-device inference · ∥ Phase 5

#TaskNotes
7.1Capture pipeline: hourly stills → per-pod crops (fixed camera, fixed pod geometry ⇒ static crop map per model) → timelapsecore
7.2Germination / growth detection per pod: start with classical CV (green-pixel area over time) before any model; evaluation protocol + metrics as OKF concepts (sleepypod  training pattern)
7.3training/ repo (Python, uv, OKF knowledge base) if/when a learned model is justified; vendor pinned core files with checksums like sleepypodseparate repo
7.4MCP: expose “which pods look unhealthy” as a resource; Claude as the agronomist over MCP

Phase 8 — Hardening & 1.0 ⏱ 1 week ⛔ everything above

#Task∥Notes
8.130-day unattended soak on the primary unit; alert on every fault class at least once (inject via hald --sim-fault)
8.2Security: LAN-only default, optional API key for tRPC/MCP, MQTT TLS opt-in flags (two-flag split from ADR-0019); PRIVACY.md (cameras never leave LAN)∥ 8.1
8.3Coverage audit doc (docs/testing/coverage-audit-<date>.md), Stryker baseline ≥ sleepypod’s∥ 8.1
8.4Docs complete: getting-started (flash → install → first schedule in 15 min), hardware guides per Gardyn model, troubleshooting, migrating-from-garden-of-eden∥ 8.1
8.5Release v1.0.0 via feat!: PR to main; announce; link MHS waitlist/alignment note in README⛔ 8.1–8.4

Done = a second Gardyn (or a Studio) can be liberated by someone else from the docs alone, HA + MCP + UI all work, and every safety invariant has a test and an alert.


3. Critical path & parallelism map

Phase 0 ──────────────────────────┐ Phase 1 (independent) ────────────┤ ├─► 2.1 contract ─┬─► Phase 2 (HAL) ───┐ │ └─► Phase 3 (core) ──┼─► 3.9 integrate ─► Phase 4 (UI) ─┐ │ │ ├─► Phase 8 │ ├─► Phase 5 (integrations) ────────┤ │ └─► Phase 6 (hardware) ─► Phase 7 ─┘

Two people could run Phase 2 and Phase 3 fully in parallel after 2.1; one person should do 2.1–2.4 first, then alternate.


4. ADR index (sleepypod template: Status/Date, Context, Decision, Consequences, References, Authors)

#TitlePhaseSeed
0001New repository & naming0
0002License (clean-room, no GPL code vendored)0
0003Core application stack: TS, React, Tailwind 4, Lingui; Hono + Vite + TanStack Router vs Next.js decided by the 0.0 spike (hypothesis: Next needs ≥2 GB and build post-processing; SSR unused)0new — supersedes sleepypod 0003/0004
0004Two-tier split: HAL daemon ↔ core0new
0005tRPC as the primary control plane; MCP and MQTT are additive surfaces0copy + extend
0006Rust for the HAL (vs Python/gpiozero)0→2new
0007HAL contract: MQTT + HTTP, versioned schemas, HA discovery from the HAL2new
0008Target controller per topology (Zero 2 W split / Pi 5 single-box)1new
0009Pump safety guard & fault latching2adapt 0022
0010Drizzle ORM + SQLite3copy
0011MCP tool design & Model Hardware Standard alignment3new
0012Deployment (tarball releases, update-from-Release, LAN-only)3adapt
0013MQTT bridge for Home Assistant (core-level)5adapt 0019
0014Auto-dosing safety (caps, lockouts, off by default)6new
0015Event bus & mutation broadcast3copy
0016Shared design language: port sleepypod tokens/ds/ui/AppShell; extraction to a shared package deferred4new

5. HAL contract v1 (draft for ADR-0007)

Prefix seedypod/<unit-id>/. Retained where marked. Payloads JSON; numbers SI; ts ISO-8601.

TopicDirRetainedPayload
availability→yesonline / offline (LWT)
system→yes{model, hal_version, serial, caps:[…], limits:{pump_max_s, pump_max_duty}}
sensor/<id>→no{ts, value, unit, quality} — ids: air_temp, humidity, pcb_temp, water_distance, water_level_pct, pump_v, pump_a, pump_w, ph, ec, water_temp
actuator/<id>/state→yes{on, level, since, fault?} — ids: light, pump, dose_<n>
actuator/<id>/set←—{on?, level?, duration_s?} — HAL clamps and may refuse (fault in state)
event/button→no{press: single ∣ double ∣ long}
alert/<kind>→yes{active, ts, detail} — water_low, pump_fault, over_temp, camera_missing
alert/<kind>/ack←—{} clears a latched fault
HTTP GET /health, /system, /camera/{upper ∣ lower}.jpg
HA discovery homeassistant/<component>/<unit-id>_<id>/config→yesstandard MQTT discovery; device.identifiers=[unit-id]

6. OKRs (first quarter) — new for this project; sleepypod has no OKR docs, only ADRs + knowledge/ (OKF)

O1 — Own the hardware. KR1.1 Stock firmware imaged and replaced on unit #1; pin map confirmed for our model (Phase 1). KR1.2 hald runs 30 days unattended with zero manual interventions (Phase 8.1). KR1.3 100 % of §0.2 invariants covered by tests; 0 bypass paths (Phase 2.3).

O2 — Ship the sleepypod-grade stack. KR2.1 v0.1.0 cut from CI within 1 day of repo creation (Phase 0). KR2.0 Core + HAL run together on the Zero 2 W inside the unit, core RSS < 120 MB (Topology A). KR2.2 Codecov project ≥ 85 % JS, ≥ 80 % Rust; Stryker score ≥ 70 % on server/scheduler/safety. KR2.3 Vercel demo + Pages docs live by end of Phase 4.

O3 — Exceed Gardyn. KR3.1 HA, MCP, and web UI control with no cloud account (Phase 3–5). KR3.2 pH/EC/water-temp telemetry with calibration (Phase 6.2). KR3.3 One other person liberates their unit from the docs alone (Phase 8 “done”).


7. Open questions (answer before the phase that needs them)

  1. Name. Settled as seedypod (ADR-0001 amendment); still needs a domain + App Store check if an iOS app follows. (Phase 0, ADR-0001)
  2. Which Gardyn do we have? 1.0/2.0 (AM2320) vs 3.0/Studio (DHT20). Decides driver priority. (Phase 1.5)
  3. Zero W or Zero 2 W inside? Zero W ⇒ must cross-compile armv6 and skip any on-device core. (Phase 1.1)
  4. Single repo vs hal split out? Plan assumes single repo (mirrors modules/ precedent); revisit if Rust CI time dominates. (Phase 2.11)
  5. Does Zero 2 W actually host the core comfortably? Measure in 3.11; if RSS or SQLite I/O on microSD disappoints, Topology B is a one-line change. (Phase 3)
  6. MHS. When the spec opens, reconcile ADR-0011 primitive names with it. (Phase 3.7)

Sources: sleepypod/core (conventions and ADR precedents)  · iot-root/garden-of-eden (reference implementation, GPL-3; no code copied)  · Model Hardware Standard research preview 

Imported from docs/implementation-plan.md in seedypod/core. Edit it there, not here.

Last updated on