Spaces:
Sleeping
Architecture Notes
Long-Poll Pattern (ESP32 ↔ Server)
The ESP32 polls GET /api/elevator/command when IDLE. Instead of returning immediately with "no command", the server holds the HTTP connection open for up to 10 seconds using asyncio.wait_for:
ESP32 Server
|---GET /api/elevator/command-->|
| | await event.wait() (up to 10s)
| |
| (dashboard sends POST) |
| | event.set() → command available
|<------{"cmd":"CALL_3"}---------| returns immediately
| |
|---GET /api/elevator/command-->| (next cycle after move completes)
| | no command → timeout after 10s
|<------{"cmd":"NONE"}-----------| returns after 10s
Why this matters: The naive approach polls every 500 ms — 120 requests/minute. Long-poll reduces this to ~6 requests/minute (one per timeout cycle), and delivers commands with near-zero latency when they do arrive.
Implementation detail: A single asyncio.Event guards a single command slot. After the ESP32 reads the command, both the slot and the Event are cleared so the next poll cycle starts clean. The ESP32 sets http.setTimeout(15000) to ensure the socket stays open past the 10-second server hold.
SSE Pattern (Server → Dashboard)
The dashboard opens one persistent HTTP connection to GET /api/events. The server keeps this connection alive and pushes JSON-encoded events whenever elevator or robot state changes:
Browser Server
|---GET /api/events------------>|
| | registers asyncio.Queue
|<--data:{"type":"elevator"...}-| on ESP32 POST /status
|<--data:{"type":"robot"...}----| on Jetson POST /status
| |
| (client disconnects) |
| | queue removed from set (finally block)
Why this matters: Polling every 500 ms from the browser creates 120 requests/minute regardless of whether anything changed. SSE pushes updates only when state changes, with zero polling overhead and no perceptible latency.
Implementation detail: Each connected browser gets its own asyncio.Queue. The broadcast() function in state.py pushes a copy of every state change to every registered queue. When the browser disconnects (tab closed, network drop), the finally block in the async generator removes the queue from the set, preventing memory leaks.
Component Interaction Summary
[ESP32] [Jetson / ROS2] [Browser]
| | |
|--POST /api/elevator/status--> SSE <--|
|<--GET /api/elevator/command-- (long-poll) |
| |--POST /api/robot/status-->
| |<--GET /api/robot/goal-- (long-poll)
| POST /api/robot/goal <--|
|<-- (elevator_cmd_event set by robot/goal handler) |
POST /api/robot/goal is the single mission-dispatch entry point. It sets both the robot nav goal and the elevator call command in one atomic operation, ensuring the elevator is already summoned when the robot arrives.