Vending Almost Anything
The reference system that lets a Fab Lab "vend almost anything". It is a small set of parts you can rearrange for different deployments; this skill is the map for running it, deploying it in any topology, and changing what it sells.
Where the code lives
Everything is under xiao-vending-machine-full-code-system/ (the "code system"):
backend-full/ CSV-backed Node.js backend + operator dashboard (the brain)
src/server.mjs the official API + static dashboard, port 3000
data/*.csv products, cards, orders, inventory, devices (the state)
public/ Operate / Inventory / Config dashboard pages
wio-rfid-writer/wio_rfid_writer/ firmware for the backend RFID WRITER Wio
scripts/card_io.py USB serial card read/write (LOCAL_DEVICE_TOOLS mode)
frontend-vending-machine/
official_frontend_wio_terminal/ firmware for the customer-facing READER Wio (dispenses)
testing_phase/ bring-up sketches + the offline MCU-only sketch (phase 2)
scripts/start_backend.sh install deps + start the backend from anywhere
render.yaml / CODESPACES_SETUP.md one-click cloud hosting recipes
The three parts
| Part | Where it usually runs | Job |
|---|---|---|
Backend (backend-full) |
a PC (or cloud) | Holds all state in data/*.csv; decides every dispense, refill, balance. |
Frontend reader (official_frontend_wio_terminal) |
Wio Terminal in the machine | Reads a card, asks the backend for permission, drives the 4 servos, reports back. |
Backend writer (wio-rfid-writer) |
Wio Terminal at the operator desk | Polls the backend for write jobs and encodes RFID cards. |
Two card types (the type is written onto the card by the writer):
- direct — an operator pre-picks the exact product(s) + quantity; the machine
dispenses after backend approval.
{"type":"direct","user_name":..,"order_number":..,"v":1} - selecting — a stored-value balance card; the customer picks products on the
Wio screen. Balance lives in
cards.csv, not on the card.{"type":"selecting","user_name":..,"v":1}
The hardware (parts + build)
The code drives a physical machine whose build lives in the sibling folder
xiao-vending-machine-assemble-steps/:
- Bill of materials — what to buy: the "What you'll buy" section of
xiao-vending-machine-assemble-steps/README.md(2× Wio Terminal, 4× Feetech ST3215 bus servo, an RFID reader + M1 tags, a 12 V 10 A adapter + a 12 V→5 V buck converter, hinge / lock / M3 heat-set nuts / M3×20 + M4×20 screws, 4× PVC column, a PC front board). Every link there is an example — any equivalent works. - Assembly: ten photographed steps plus the Wio Terminal wiring diagram (servo UART on header pins 8/10; RFID on the Grove I2C pins 27/28).
- Design files: editable STL (print) and STEP (case) under
xiao-vending-machine-assemble-steps/hardware-preparatory/stl-files/.
Power: one 12 V input drives the four servos; a 12 V→5 V buck converter supplies the 5 V USB that powers the Wio Terminal, so a single adapter runs both.
Step 1 — pick a deployment topology
The same parts snap together four ways. Choose one, then follow its column. Full walkthroughs are in reference-deployment.md.
| Topology | Backend | MCUs | Network | Tracks stock/balance? | Use when |
|---|---|---|---|---|---|
| A. MCU-only (offline) | none | 1 reader Wio | none | No (card carries the recipe) | Demo, no PC, no WiFi, simplest possible |
| B. PC-on-LAN (default) | booth PC :3000 | reader + writer Wio | same WiFi | Yes | Real booth with a laptop present |
| C. One-box USB writer | booth PC :3000 (LOCAL_DEVICE_TOOLS=1) |
reader Wio only | reader on WiFi; cards written over USB | Yes | Operator desk writes cards without a second Wio |
| D. Cloud backend | Render / Codespaces | reader + writer Wio | internet | Yes, but state is ephemeral | Remote/multi-site; hosted dashboard |
The official firmware requires a backend (B/C/D). Topology A is a different, self-contained sketch (
testing_phase/2-servo-rfid-testing) that dispenses a fixed recipe with no network — see reference-deployment.md.
Step 2 — run the backend (topologies B / C / D)
From the code-system root (copy backend-full, frontend-vending-machine, and
scripts together, then):
bash scripts/start_backend.sh # installs deps, serves http://localhost:3000
# or:
cd backend-full && npm install && npm start
Open http://localhost:3000 for the three dashboard pages: Operate,
Inventory, Config. Health check: GET /api/health.
Common environment switches (details in reference-backend.md):
| Variable | Effect |
|---|---|
PORT |
Serve on a port other than 3000 (also update each Wio's backend URL). |
LOCAL_DEVICE_TOOLS=1 |
Enable USB card writing/flashing from the dashboard (topology C). |
Step 3 — configure and flash the Wio Terminals
Both Wios must reach the backend and present matching credentials. Two ways:
From the dashboard (Config page, easiest). With arduino-cli installed and a
Wio on USB, the Config page detects the PC's LAN IP, writes WiFi + backend URL
into the sketch, flags a network mismatch, and flashes with one click.
(POST /api/wio/wifi, POST /api/wio/flash; target is frontend or writer.)
By hand, edit the constants at the top of each sketch, then compile/upload
with arduino-cli (FQBN Seeeduino:samd:seeed_wio_terminal). The config points:
| Sketch | Constants to set |
|---|---|
official_frontend_wio_terminal.ino (reader) |
WIFI_SSID, WIFI_PASSWORD, BACKEND_BASE_URL (no trailing slash), DEVICE_ID="frontend-1", API_KEY="FRONTEND_1_SECRET" |
wio_rfid_writer.ino (writer) |
WIFI_SSID, WIFI_PASSWORD, API_BASE (note: not BACKEND_BASE_URL), DEVICE_ID="wio-rfid-writer", API_KEY="WIO_WRITER_SECRET" |
Each Wio's DEVICE_ID/API_KEY must match an enabled=true row in
backend-full/data/devices.csv, or every request is rejected with 401. Build,
flash, and calibration commands are in reference-firmware.md.
Change what it sells (products)
Products map 1 product → 1 servo → 1 physical column, servo ids 1..4 (four
columns). Two ways to change them:
- Dashboard (runtime): Inventory sets exact per-column counts and refills (cap 10); Operate writes order/balance cards and shows alerts. No restart.
- CSV (definition): edit
backend-full/data/and restart the backend.inventory.csvis canonical;product_meta.csvholds the extra fields:
# inventory.csv (canonical: what & where & price)
product_id,name,servo_id,stock,price
P001,XIAO ESP32-S3 Sense,1,0,15.99
# product_meta.csv (display + rules, joined on product_id)
product_id,slot_id,low_stock_threshold,max_capacity,active,tag,description,feature_1,feature_2,feature_3,image_path,product_url
P001,A1,3,10,true,Vision + Voice AI,...,...,...,...,/assets/products/xiao-esp32-s3-sense.jpg,https://...
Keep these in sync (the invariants that break dispensing if ignored):
servo_idmust be a unique value in 1..4. The on-screen selecting menu and the dispense plan are indexed by servo id 1..4 — a product on servo 5, or two products sharing a servo, will not appear/behave correctly.- Physical column order must match
servo_id(product on servo 2 sits in the column driven by servo id 2). max_capacity(default 10) is the mechanical column limit; refills are capped to it.low_stock_thresholddrives the refill/needs_refillalerts.active=falsehides a product from selling without deleting its history.
Full CSV schema for all files (orders, cards, ledger, devices, logs) is in reference-backend.md.
Vend something else (make it your own)
"Vend almost anything" means adapting this reference, not rebuilding from zero.
The 1 product → 1 servo → 1 column model is deliberately generic:
- Different product: print
dispenser-specific.stlinstead of the commondispenser.stl, and/or resize the printeddispenser/dispenser armand the PVC column for the new item (editable STL/STEP in the assembly guide), then recapture servo travel withtesting_phase/1-a+1-b. - Everything else is unchanged: the backend, card types, dashboard, and REST
API do not care what a column holds — only the
product_meta.csvlabels/prices and the servo plan change.
Reuse the architecture; swap the physical front end.
Change the parameters
| Parameter | Where | Notes |
|---|---|---|
| Servo travel (home/open) | ZERO_POS[4] / MAX_POS[4] in the reader .ino |
Re-capture with testing_phase/1-a+1-b; one dispense = ZERO→MAX→ZERO. |
| Servo count / ids | SERVO_NUM, ID[] in the reader .ino |
Bound to 4 columns; changing this ripples into the backend's 1..4 arrays. |
| WiFi / backend URL | sketch constants or Config page | URL has no trailing slash; localhost on a Wio means the Wio itself. |
| Device credentials | devices.csv + sketch DEVICE_ID/API_KEY |
Must match; enabled=true. |
| Capacity / low-stock | max_capacity / low_stock_threshold in product_meta.csv |
|
| Prices | price in inventory.csv (or dashboard) |
Used for selecting-card checkout math. |
| RFID key / data blocks | DATA_BLOCKS {4,5,6,8,9,10}, key FF*6 in both sketches |
Writer and reader must agree, or reads fail. |
| Backend port | PORT env |
Update every Wio's backend URL to match. |
Smoke test the backend (no hardware)
curl -s http://localhost:3000/api/health
curl -s http://localhost:3000/api/products
# frontend device call needs the matching headers:
curl -s -X POST http://localhost:3000/api/frontend/verify-card \
-H 'Content-Type: application/json' \
-H 'x-device-id: frontend-1' -H 'x-api-key: FRONTEND_1_SECRET' \
-d '{"type":"selecting","user_name":"Alice"}'
More scripted checks and the full request/response contract are in reference-backend.md.
Reference files
- reference-backend.md — run/env, complete REST API, every CSV schema, the order + stock/balance lifecycle, and hosting (LAN / Render / Codespaces / USB) with the persistence caveat.
- reference-firmware.md — the two Wio firmwares in detail, servo calibration workflow + numbers, RFID payload/blocks/key, build & flash, controls, and hardware troubleshooting.
- reference-deployment.md — the four topologies A–D end to end, including the offline MCU-only sketch, and how changing products / parameters differs per topology.
The physical build — bill of materials, Wio Terminal wiring diagram, ten assembly
steps, dispense test videos, and editable STL/STEP design files — lives in the
sibling folder xiao-vending-machine-assemble-steps/ (see its README.md).