← Back to Projects
EmbeddedIterating

Vend-A-Shoe

Remote vending controller that lets a user pick a bin in a Next.js dashboard and trigger a physical dispense through a Supabase command queue and Raspberry Pi worker.

A cyber-physical vending pipeline instead of a dashboard-only app.; Frontend writes `device_commands` rows to Supabase with bin-specific actions (`dispense_bin_1..4`).; Raspberry Pi worker claims pending commands, executes servo movement script, then marks command status.; Servo control supports per-bin calibration; Bin 3 needed reduced duty cycle to prevent over-rotation.

Date
2026
Signal
Embedded + Systems
Build stage
Cloud-to-hardware prototype running; calibration and reliability iteration
Stack
Next.js, TypeScript
embeddedfullstackiotsystemsintake
IMG_1485.MOV

IMG_1485.MOV

Project notes

Highlights

What I built

  • Queue-based cloud-to-hardware architecture avoids exposing Pi directly to internet.
  • Command claiming flow reduces duplicate execution risk under polling.
  • Status lifecycle gives observability and post-failure diagnosis.
  • Includes deployment/runtime details; not just code that runs in dev.
  • `app/page.tsx`: command UI and Supabase insert flow for bin-specific dispense actions.
  • `pi_worker.py`: queue consumer loop, command claiming, action parsing, command status updates.

Architecture

How the system works

  • Frontend (`app/page.tsx`): creates queue command rows in Supabase.
  • Queue table (`supabase.sql`): tracks `pending`, `running`, `completed`, `failed`; indexed by status/date.
  • Worker (`pi_worker.py`): polls oldest pending command, claims atomically via status update, executes servo script, persists outcome.
  • Actuation (`servo_motor.py`): PWM sweep from home to target duty and back; target can vary by bin.
  • Runtime (`pi-worker.service`): persistent process with restart policy and env-based config.
  • UI writes command rows into `device_commands` through Supabase client.

Challenges

What made it hard

  • Reliable internet-triggered actuation with safe state transitions.
  • Servo power and motion tuning under real mechanical constraints.
  • Keeping remote command UX clear while hardware work happens asynchronously.

Lessons

What I learned

  • Physical systems punish "one size fits all" assumptions quickly.
  • Queue states matter; `running`/`failed` are not optional if you want to debug real hardware behavior.
  • Calibration hooks in software save time when mechanical behavior shifts.
  • Mechanical variance forces software-level calibration hooks.
  • State transitions make hardware workflows debuggable when remote.

Stack / materials

Next.jsTypeScriptSupabasePostgreSQLRaspberry Pi 4PythonRPi.GPIOsystemdReactTailwind CSSLucide iconsSupabase JS clientSupabase Postgres schema + policiesVercel deploy config
  • Milestone 1: single-bin command flow (`dispense`) from web app to Pi script.
  • Milestone 2: upgraded to four-bin routing with GPIO mapping in `pi_worker.py`.
  • Milestone 3: calibrated Bin 3 with per-bin target duty override after over-travel.
  • Milestone 4: UI pass for responsive bin cards, status badges, and clearer loading/command feedback.
  • Decision: queue-based cloud-to-device architecture through Supabase
  • Why: avoids exposing Raspberry Pi directly to internet; keeps command history durable.
  • Tradeoff: introduces polling delay and status reconciliation complexity.
  • Decision: status transitions (`pending`, `running`, `completed`, `failed`)

What I would improve next

  • Add push-based updates instead of pure polling for faster UX.
  • Harden RLS and command auth model beyond MVP open policies.
  • Add sensor feedback to verify successful dispense actions.

Build log

  • Milestone 1: single-bin command flow (`dispense`) from web app to Pi script.
  • Milestone 2: upgraded to four-bin routing with GPIO mapping in `pi_worker.py`.
  • Milestone 3: calibrated Bin 3 with per-bin target duty override after over-travel.
  • Milestone 4: UI pass for responsive bin cards, status badges, and clearer loading/command feedback.
  • Decision: queue-based cloud-to-device architecture through Supabase
  • Why: avoids exposing Raspberry Pi directly to internet; keeps command history durable.
  • Tradeoff: introduces polling delay and status reconciliation complexity.

What broke; how I fixed it

  • Bin 3 over-rotated and caused unreliable dispense movement.
  • Root cause: one motion profile across all bins ignored mechanical variation.
  • Fix: added `BIN_TO_TARGET_DUTY` in worker; passed `--target-duty` into servo script for per-bin tuning.
  • Another issue: command model started single-lane; this broke scaling to multiple bins.
  • Fix: switched action parsing to `dispense_bin_n` and mapped bins to dedicated GPIO pins.
  • Bin 3 over-rotation was addressed with per-bin target duty override in worker + servo invocation.
  • System evolved from single `dispense` action to `dispense_bin_1..4` command routing with GPIO mapping.

What changed from v1 to now

Current prototype supports browser-triggered, bin-specific dispense commands with observable queue states and Pi-based execution; it is a working cloud-to-hardware control loop with clear next steps around auth hardening, sensor feedback, and push updates.

Video gallery

Video 1

Video 1

Video 2

Video 2

Video 3

Video 3

Video 4

Video 4

Video 5

Video 5

Video 6

Video 6

Video 7

Video 7

Video 8

Video 8

Video 9

Video 9

Video 10

Video 10

Video 11

Video 11

Video 12

Video 12

Media timeline

Build photos, clips, and process visuals. The goal is to show how the project evolved, not just the final screenshot.

IMG_1487.mov

IMG_1487.mov

IMG_4757.MOV

IMG_4757.MOV

IMG_4840.MOV

IMG_4840.MOV

IMG_4841.MOV

IMG_4841.MOV

IMG_5122.MOV

IMG_5122.MOV

IMG_5123.MOV

IMG_5123.MOV

IMG_5124.MOV

IMG_5124.MOV

IMG_5125.MOV

IMG_5125.MOV

IMG_5146.MOV

IMG_5146.MOV

IMG_5147.MOV

IMG_5147.MOV

IMG_5411.MOV

IMG_5411.MOV