17 KiB
QuiBot Project — Agent Guide
Overview
QuiBot is an educational robotics platform (UPC/UNE collaboration) consisting of a programmable robot with color-block recognition, gesture control, stepper motors, RGB LED eyes, and multiple input methods (web dashboard, Android voice app). The codebase comprises four independent application layers communicating via HTTP JSON APIs.
[quibot-web Nuxt SPA] ──HTTP──> [backend Express] ──HTTP──> [raspi FastAPI (Pi)]
│ │
▼ ▼
[apk Expo RN app] ──HTTP──> (same backend) [Python hardware drivers]
Tech stack: Python (pigpio, FastAPI) | TypeScript/Express | Nuxt 4/Vue 3 | Expo/React Native
No database: All state is in-memory, file-based (/tmp/quibot-audio/), or localStorage.
Directory Structure
quibot/
├── raspi/ # Raspberry Pi brain — Python, controls hardware
│ ├── main.py # FastAPI server (port 8000) on the Pi
│ ├── quibot.py # Main program: block/gesture threads (like Arduino QuiBot.ino)
│ ├── motion.py # Stepper class, homing, line-following, high-level tasks
│ ├── gesture.py # PAJ7620U2 gesture sensor (I2C, polled at 50ms)
│ ├── blocks.py # TCS34725 color sensor + servo block ejection
│ ├── eyes.py # WS2811 LED matrix (128 LEDs, pigpio waveforms, breathing animation)
│ ├── pins.py # BCM GPIO pin map for all hardware
│ └── tests/ # Manual diagnostic scripts (not automated)
├── backend/ # Local Express server (port 3000), proxy to Pi
│ ├── src/
│ │ ├── index.ts # Express entry: CORS, JSON parser, /health
│ │ ├── config.ts # Env config: RASPBERRY_PI_HOST, PORT, QUIBOT_TOKEN
│ │ ├── routes/router.ts # Mounts all controllers
│ │ ├── services/raspi.service.ts # Axios proxy layer to Pi FastAPI
│ │ └── controllers/
│ │ ├── motor.controller.ts # Motor step/stop/upload
│ │ ├── audio.controller.ts # Audio file lifecycle (incoming/locked/processed)
│ │ ├── command.controller.ts # POST /commands proxy to raspi /run
│ │ └── settings.controller.ts # GET/PUT /settings runtime config
│ └── dist/ # Compiled output (generated)
├── quibot-web/ # Nuxt 4 dashboard SPA
│ ├── app/app.vue # Single-page control panel: block queue, D-pad, eye controls, gesture log
│ ├── server/api/ # Nitro server routes proxying to raspi
│ │ ├── motor/step/[direction].post.ts
│ │ └── motor/stop.post.ts
│ ├── nuxt.config.ts # Runtime config: QUIBOT_BASE_URL, QUIBOT_TOKEN
│ └── .output/ # Built Nitro output
├── apk/ # Expo React Native voice recorder ("VoiceDrop")
│ ├── app/index.tsx # Recording screen + upload
│ ├── app/settings.tsx # Backend URL/token configuration (AsyncStorage)
│ └── lib/recorder-settings.ts # AsyncStorage wrapper
├── .gitea/workflows/ # CI/CD pipelines
│ ├── build.yml # Web + backend → zip artifacts + Gitea release
│ └── build-apk.yml # Expo prebuild + signed APK → Gitea release
├── build.sh # Placeholder
└── README.md # Project overview (Catalan)
Raspberry Pi Layer (raspi/)
Hardware target: Raspberry Pi Zero 2W controlling a robot with:
- 5 NEMA-style stepper motors (wheels x2, arms x2, syringe) via A4988/TB600 drivers in STEP/DIR mode
- VL53L0X ToF distance sensor (I2C bus 3)
- PAJ7620U2 gesture sensor (I2C bus 3, polled at 50ms)
- TCS34725 color sensor (bit-banged I2C on GPIO22/27)
- ADS1115 ADC for TCRT5000 line-following IR sensors
- WS2811 RGB LED matrix (2x 8x8 = 128 LEDs, GPIO26, pigpio waveforms at -s 1)
- Servo motor (GPIO10 PWM) for block ejection
- Hall-effect endstops on GPIOs 12, 16, 17
- Optional: I2S audio amp (MAX98357A) + mic (SPH0645)
Key files
pins.py— BCM GPIO pin numbering for every component (STEP, DIR, EN pins, I2C lines, endstops, LED_DATA on GPIO26)motion.py—Stepperclass with AccelStepper-style acceleration profiling viapigpio.gpio_trigger(). 5 motor instances (wheel_R,wheel_L,arm_R,arm_L,syringe). Continuous stepper daemon thread (_stepper_loop) at ~100Hz. Homing routines read Hall-effect endstops. Line-following with proportional correction on TCRT5000 values via ADS1115.gesture.py— Raw I2C viasmbus2(bus 3). Two-register-bank init (~240 total register writes). Polls gesture result registers 0x43/0x44 every 50ms. Returns: GS_NONE, GS_FORWARD, GS_LEFT, GS_RIGHT, GS_UP, GS_DOWN, GS_CLOCKWISE, GS_ANTICLOCKWISE, GS_WAVEblocks.py— TCS34725 RGB reads classified via Manhattan distance against calibrated reference table (BK/RD/GN/BU/YE/OG/VT). Smooth servo movement with 3us micro-steps.eyes.py— 128 WS2811 LEDs via pigpio waveforms at 1us resolution. Pre-defined shapes: EYES_OPEN, EYES_FW, EYES_DOWN, EYES_GESTURE. Breathing thread oscillates brightness 80-170 at 50ms intervals.quibot.py— Main program (equivalent to original Arduino QuiBot.ino). Two threading tasks via FreeRTOS-style pattern:task_read_blocks()(color→action mapping) andtask_read_gestures()(WAVE toggles between block/gesture mode).threading.Lockprevents concurrent motor movements. Handles SIGINT/SIGTERM for graceful shutdown.main.py— FastAPI server on port 8000 with CORS. File-based state management using/tmp/quibot-audio/(incoming/locked/processed directories). Token auth via query parameter matchingQUIBOT_TOKEN.
Color→Action Mapping (in quibot.py)
| Color | Action |
|---|---|
| RED | Advance forward |
| GREEN | Turn right |
| BLUE | Turn left |
| YELLOW | Take/block pick-up |
| ORANGE | Leave/eject |
| VIOLET | Idle |
| BLACK | Reference / no block |
Motor Position Tracking
Stepper class in motion.py tracks absolute position in steps via _pos and _target. Endstops provide physical reference during homing. The stepper loop evaluates acceleration profiles to generate STEP pulses at correct intervals.
Backend Layer (backend/)
Role: Express.js HTTP proxy sitting between frontend/mobile and the Raspberry Pi's FastAPI server. Token passthrough, no business logic.
Configuration (.env, loaded by config.ts)
| Variable | Default | Purpose |
|---|---|---|
RASPBERRY_PI_HOST |
http://raspberrypi.local |
Pi API URL |
RASPBERRY_PI_PORT |
8000 |
Pi API port |
QUIBOT_TOKEN |
MY_SECRET_TOKEN |
Auth token for all Pi endpoints |
PORT |
3000 |
Backend listen port |
Architecture
index.ts → Express app, CORS, JSON parser, /health endpoint
routes/router.ts → Mounts all controllers under /motor, /audio, /commands, /settings
config.ts → Mutable getter/setter env vars (runtime update via PUT /settings)
raspi.service.ts → Axios proxy methods for every Pi endpoint + multipart file upload handling
Controllers
motor.controller.ts—POST /motor/step/forward,/motor/step/backward,/motor/stop. AlsoPOST /motor/upload(multer multipart → proxied as FormData to Pi).audio.controller.ts—GET /audio/incoming,POST /audio/lock/:filename,/unlock/:filename,/cancel/:filename,/process/:filename. All proxy to raspi audio file lifecycle endpoints.command.controller.ts—POST /commands { task }→ proxied to raspi/run?task=...&token=...settings.controller.ts—GET /settingsreturns config;PUT /settingsupdatesraspberryPi.host,raspberryPi.port,tokenat runtime.
Build/Run
cd backend
npm install # Installs express, axios, multer, dotenv, cors, typescript
npx tsc # Compiles to dist/
node dist/index.js # Or use tsx/nodemon for dev
Web Frontend (quibot-web/)
Stack: Nuxt 4 (Nitro) + Vue 3. Single-page dashboard SPA.
Runtime config (nuxt.config.ts)
| Key | Default | Purpose |
|---|---|---|
QUIBOT_BASE_URL |
http://quibot:8000 |
Base URL for raspi FastAPI |
QUIBOT_TOKEN |
MY_SECRET_TOKEN |
Auth token |
UI Panels (app/app.vue — single-file SPA, 1369 lines)
- Block Queue Panel — Displays color blocks in a queue (localStorage persistence + demo fallback). Shows action descriptions per color.
- Motion Controls — D-pad grid: up=forward, down=back, left/right=turns, center=stop. Sends
$fetch('/api/motor/step/forward')etc. - Eye Controls — Shape selector (open/forward/down/gesture), 8-color picker. Calls
POST /api/eye/shape,/api/eye/color,/api/eye/on,/api/eye/off. - Gesture Sensor Panel — Toggle between Block Mode / Gesture Mode. Gesture detection history log. Reference table of all 8 gestures.
State & Styling
- Dark/light theme via CSS custom properties, persisted in localStorage.
- Block queue data stored in localStorage with demo fallback.
- Toast notifications for success/error feedback.
- Responsive layout with CSS Grid (mobile-adaptive).
Server Routes (server/api/)
| Method | Path | Description |
|---|---|---|
| POST | /api/motor/stop |
Proxies to raspi /motor/stop |
| POST | /api/motor/step/:direction |
Proxies to raspi /motor/step/forward|backwards |
Note: The frontend also calls POST /api/eye/shape, /api/eye/color, /api/eye/on, /api/eye/off, /api/gesture/on, /api/gesture/off — server routes for these may need to be created (frontend references them but they don't have explicit server handlers yet).
Build/Run
cd quibot-web
npm install
npx nuxt build # Produces .output/
# Dev mode:
npx nuxt dev
CI builds the Nuxt output and zips it for distribution.
Android APK (apk/)
Stack: Expo SDK ~54 + React Native 0.81.5 + Expo Router ~6.0. App name: "VoiceDrop".
Features
- Records audio via
expo-av(Audio.Recording— produces .m4a) - Auto-uploads on stop as multipart form data to configurable backend URL
- Displays recording timer, status messages, raw server response (first 400 chars)
Screens
app/index.tsx— Recorder screen: start/stop recording, upload, statusapp/settings.tsx— Backend URL, Bearer token, form field name. Saved to AsyncStorage underrecorder.*namespace.
Persistence
Settings stored in @react-native-async-storage/async-storage (keys: recorder.backendUrl, recorder.authToken, recorder.fieldName).
Build/Run
cd apk
npm install
npx expo start # Dev mode
# Production APK (local):
./build.sh # expo prebuild → generates android/ → gradle assembleRelease
CI: build-apk.yml runs expo prebuild, decodes keystore from secrets, builds signed release APK.
Complete API Reference
Raspberry Pi FastAPI (raspi/main.py) — port 8000
| Method | Path | Params/Body | Description |
|---|---|---|---|
| POST | /run |
query: task, token |
Runs whitelisted system commands (restart_nginx, uptime, update) |
| POST | /motor/step/forward |
query: token |
Starts motor forward (daemon thread) |
| POST | /motor/step/backwards |
query: token |
Starts motor backward (daemon thread) |
| POST | /motor/stop |
query: token |
Disables motor driver (GPIO EN HIGH) |
| POST | /audio/upload |
multipart: file, query: format |
Saves to /tmp/quibot-audio/incoming/, returns filename + lock_url |
| GET | /audio/incoming |
— | Lists files with size and modified time |
| POST | /audio/lock/{filename} |
— | incoming → locked (claim for processing) |
| POST | /audio/unlock/{filename} |
— | locked → incoming (release) |
| POST | /audio/cancel/{filename} |
— | locked → incoming (cancel) |
| POST | /audio/process/{filename} |
— | locked → processed |
Auth: Query parameter token matching QUIBOT_TOKEN env var (default: MY_SECRET_TOKEN).
Backend Express (backend/) — port 3000
| Method | Path | Body | Description |
|---|---|---|---|
| GET | /health |
— | Returns settings object |
| POST | /commands |
{ task } |
Proxy to raspi /run |
| POST | /motor/step/forward |
— | Motor forward proxy |
| POST | /motor/step/backward |
— | Motor backward proxy (maps to raspi /backwards) |
| POST | /motor/stop |
— | Motor stop proxy |
| POST | /motor/upload |
multipart file | Audio upload via multer in-memory buffer |
| GET | /audio/incoming |
— | List incoming audio files |
| POST | /audio/lock/:filename |
— | Lock audio file |
| POST | /audio/unlock/:filename |
— | Unlock audio file |
| POST | /audio/cancel/:filename |
— | Cancel locked audio |
| POST | /audio/process/:filename |
— | Mark processed |
| GET | /settings |
— | Returns config |
| PUT | /settings |
{ raspberryPi: { host, port }, token } |
Update runtime config |
Command Flow Examples
Web → Motion Control
User clicks "Forward" in D-pad
→ $fetch('/api/motor/step/forward', { method: 'POST' })
→ Nuxt Nitro route: server/api/motor/step/[direction].post.ts
→ $fetch(config.quibotBaseUrl + '/motor/step/forward', { query: { token } })
→ raspi FastAPI /motor/step/forward
→ motor_step("forward") in daemon thread
→ step_motor(200, DIR, 1ms pulses)
Block Processing (internal to Pi)
Child inserts colored block → quibot.py task_read_blocks() polls distance sensor
→ When detected <80mm: read_block_color() via TCS34725
→ Manhattan distance classification against color lookup table
→ RED: eyes_turn_on(EYES_FW, DARK_RED, 2)
_execute_action(task_move_to, CROSSING)
→ enable_wheels(ON) → follow_line_loop(speed) (proportional on TCRT5000)
→ After action: servo_move_to(EJECT_POSITION)
APK → Audio Upload
User stops recording in VoiceDrop → expo-av .m4a file
→ POST {backendUrl} with FormData {fieldName: "file"} + Bearer auth
→ raspi FastAPI saves to /tmp/quibot-audio/incoming/{uuid}.wav
→ Returns: { status: "received", filename, lock_url }
Gesture Mode Toggle
User toggles mode in web UI
→ _execute_action() locks mutex
→ If gesture mode: eyes_gesture_mode_on() (double cyan flash on 128-LED matrix)
→ Eyes breathing thread at MAX_BR(170) brightness
CI/CD (/.gitea/workflows/)
build.yml — Web + Backend
- Triggers: Push to
master - Builds web:
npm install && npx nuxt build, zips.output/ - Builds backend: Zips entire
backend/directory - Creates Gitea release "latest" with both zip artifacts
build-apk.yml — Mobile
- Triggers: Push to
master - expo prebuild → decode keystore from secrets →
./gradlew assembleRelease - Creates Gitea release with APK artifact
Testing
All tests in raspi/tests/ are manual diagnostic scripts (not automated frameworks). Each test is run independently by uncommenting the desired function call at the bottom of the file. Requirements:
sudo pigpiod -s 1daemon running- Python venv activated with hardware dependencies installed
- Pi connected to robot hardware
| Test | What it verifies |
|---|---|
test_simple_motor.py |
Low-level motor driver via direct GPIO writes |
test_motion.py |
Wheel steppers, arm/syringe homing, VL53L0X distance, ADS1115 line sensors |
test_blocks.py |
Servo sweep (open/eject/open), color sensor readings, raw RGB calibration |
test_gesture.py |
PAJ7620U2 I2C connection + 30-second gesture capture |
test_eyes.py |
LED shape/color rendering, animation repeat/direction, gesture animations, breathing |
Key Conventions
- Token auth is always a query parameter matching
QUIBOT_TOKEN(default:MY_SECRET_TOKEN) - No database — use filesystem for persistence, localStorage for web client state
- Backend is a dumb proxy — no business logic, just forwards HTTP requests with token passthrough
- Motor commands are fire-and-forget — motor runs in daemon thread until
/motor/stop - Audio lifecycle: incoming → locked (claim) → processed OR unlocked (release) / cancelled
- Eyes breathing runs continuously at MIN_BR(80)-MAX_BR(170) brightness in background
quibot.pyowns block/gesture autonomy — blocks are processed internally on the Pi without backend/web involvement- All paths use forward slashes in URLs; kebab-case for params