High-performance Go relay that ingests JPEG frames, depth values, and GPS coordinates from ESP32 sensors, fuses them by timestamp, and forwards to the ML backend for inference.
ESP32-CAM Sensor-Relayer Flask Backend
| | |
|--POST /api/frame----------->| (buffers frame) |
| (JPEG binary) | |
| | |
|--POST /api/depth----------->| (fuses frame+depth) |
| (JSON: distance) (within 300ms) |
| | |
| |--POST /api/stream/frame--->|
| | (multipart: image, |
| | depth_mm, lat, lng) |
| | |
| |<--204 No Content-----------|
| | |
|--POST /api/gps------------->| (enriches with location) |
| (JSON: lat/lng) | |
make dev-relayer # Run in development mode
make build-relayer # Build binary
make docker-relayer # Build Docker image
make simulate-relayer # Run test simulatorinternal/
├── config/ Configuration loading from environment
├── models/ Data structures (SensorFrame, DepthSample, etc)
├── store/ State management and fusion logic
├── metrics/ Performance metrics
├── handler/ HTTP endpoint handlers
├── worker/ Forwarding workers and pool management
└── server/ HTTP server setup
web/
├── static/ Dashboard HTML
└── embed.go Embeds HTML at compile time
pkg/utils/ Shared utilities
Set environment variables (or copy .env.example):
RELAYER_ADDR=:5001 # Listen address
BACKEND_URL=http://localhost:5000/api/stream/frame # ML backend endpoint
FUSE_WINDOW=300ms # Max time delta for fusion
MAX_FRAME_AGE=2s # Drop old frames
FORWARD_TIMEOUT=5s # HTTP timeout
WORKER_COUNT=4 # Parallel forwarders
QUEUE_SIZE=64 # Internal queue depth
PORT=<auto-set by Railway> # Override listen portFrames and depth samples are fused if they arrive within FUSE_WINDOW (default 300ms). The relayer:
- Stores the latest frame, depth reading, and GPS coordinates in memory
- Attempts fusion on every new frame or depth POST
- Succeeds if both frame and depth exist and their timestamps are within the window
- Forwards the fused payload (image + depth + GPS + metadata) to the ML backend
- Drops the frame after fusion to avoid duplication
The window constraint ensures that frame and depth capture the same defect moment.
POST /api/frame
- Raw JPEG body or multipart form-data with "image" field
- Returns 204 No Content
POST /api/depth
- JSON:
{"distance": 123.4}(millimeters) - Returns 204 No Content
POST /api/gps
- JSON:
{"lat": 28.6, "lng": 77.2} - Returns 204 No Content
GET /api/metrics
- Returns JSON with counters and queue status
GET /api/config / POST /api/config
- View or update backend URL live
GET /api/preview
- Returns latest JPEG frame
GET /
- Dashboard UI
GET /health
- Health-check for load balancers/Docker
Frames and depth samples are fused if they arrive within FUSE_WINDOW (default 300ms). Once fused, the frame is consumed and forwarded as multipart/form-data to the backend with:
image— JPEG binarydepth_mm— Depth in millimeterslat,lng— GPS coordinates (if available)relayed_at— Unix millisecond timestamptime_diff_ms— Frame-depth timestamp difference
# Run with live reload (requires entr or similar)
go run ./...
# Build
go build -o sensor-relayer
# Test with simulator
go run . &
python simulator.py
# Metrics endpoint
curl http://localhost:5001/api/metricsdocker build -t sensor-relayer:latest .
docker run -p 5001:5001 \
-e BACKEND_URL=http://host.docker.internal:5000/api/stream/frame \
sensor-relayer:latest- Parallel worker pool (configurable, default 4)
- Lock-free atomic metrics
- Buffered channels with backpressure
- Connection pooling for backend forwarding