Scale Sense — Architecture
System overview
Bar location (local) Pi gateway AWS Customer
────────────────────── ────────────────── ────────────── ──────────────
ESP32 / Scale Mosquitto IoT Core Web dashboard
MQTT over WiFi ──WiFi──► Local MQTT + buffer ──► Device registry (React)
WiFi + BLE │ │
Greengrass Lambda Push alerts
ESP32 / Scale ──────────► Edge runtime + auth Ingest + alerts (SNS/FCM)
│ │
· · · OTA server ◄────────── S3 firmware Phone app
Local cache Versioned .bin (React Native)
ESP32 / Scale ──────────► │
WireGuard ──────────── WireGuard peer
VPN tunnel AWS VPC
│
Watchtower ◄─────────── ECR
Auto container Pi images
updates
Phone (BLE) ◄──────────────── ESP32 (BLE advertising)
Maintenance UI Identify flash + stream
Communication paths
Path
Protocol
Purpose
ESP32 → Pi
MQTT over WiFi
Scale readings, status
Pi → IoT Core
MQTT over cellular (TLS)
Cloud data pipeline
IoT Core → Lambda
Rule engine trigger
Ingest + alerts
Lambda → WebSocket
API Gateway WS
Real-time dashboard
Phone → API
HTTPS REST
Business data
Phone ↔ ESP32
BLE GATT
Maintenance / tare / calibrate
IoT Core → Pi → ESP32
MQTT downlink
Commands, OTA
WireGuard
UDP tunnel
Remote SSH into Pi
Key architectural decisions
Why Raspberry Pi gateway (not direct to cloud)
Bars don't have IT departments — we own the network entirely
Cellular SIM in Pi = no bar WiFi dependency, no IT gatekeeping
Pi buffers readings locally when cellular drops (Mosquitto persistence)
One Pi per location = one certificate, not 50 ESP32 certificates
Pi runs local OTA server = faster firmware updates, less cellular data
Pi is a full Linux box = remote diagnostics via WireGuard SSH
Why Mosquitto on Pi (not direct MQTT to IoT Core)
Local buffer when cellular is intermittent
Scales publish to 192.168.x.x — never need to know cloud endpoint
Mosquitto bridge handles TLS + cert auth to IoT Core transparently
Per-location SIM data stays minimal
Why Greengrass (added once Pi was in the plan)
Pi registers as Greengrass core device — IoT Core sees Pi, not 50 ESP32s
Fleet management: deploy software to Pi from AWS console
OTA job delivery to ESP32s via Greengrass leaf device model
Component model replaces Watchtower for Pi software updates long-term
Why modem sleep (not deep sleep)
ESP32 must stay commandable: tare, maintenance mode, BLE, OTA
18650 battery gives plenty of capacity without aggressive power saving
Deep sleep breaks MQTT session, BLE advertising, and command reception
Modem sleep reduces WiFi draw between transmissions with no connectivity loss
Why BLE as secondary (not primary) transport
BLE range limited: walk-in coolers are Faraday cages, WiFi penetrates better
BLE is bidirectional but MQTT/WiFi handles reliable cloud reporting
BLE excellent for local maintenance: no cloud roundtrip, works offline
ESP32 runs both simultaneously (small power penalty, acceptable)
BLE primary use: identify flash, live weight stream during maintenance
Networking — bar's WiFi never touched
Pi has cellular SIM (Hologram / Twilio Super SIM / T-Mobile IoT)
Scales connect to Pi's own WiFi AP (separate SSID)
Bar staff never need to configure anything networking-related
WireGuard VPN gives us SSH access to any Pi for remote support
AWS services used
Service
Purpose
IoT Core
MQTT broker, device registry, rule engine
Greengrass
Edge runtime on Pi, component deployment
Lambda
Ingest, alert logic, API handlers, report generation
DynamoDB
Location/tap/keg/device/beer data (single-table)
Timestream
Scale readings time series
API Gateway (REST)
Customer REST API
API Gateway (WS)
Real-time WebSocket for dashboard
Cognito
Auth — Staff / Manager / Admin groups
SNS
Push notification fan-out
S3
Firmware binaries, export PDFs/CSVs
ECR
Pi Docker container images
EventBridge
Scheduled checks (offline scale detection)
SES
Email alerts and auto-export
CloudWatch
Logs and metrics
Scale hardware
MCU: ESP32-C3-MINI-1U (single-core RISC-V, WiFi + BLE, U.FL external antenna)
Load cell amp: HX711
Battery: LiPo pouch cell (5000mAh, JST PH 2.0mm) with TP4056 charger
Voltage regulator: HT7333 (3.3V LDO — NOT AMS1117)
Battery ADC: GPIO4 (ADC1_CH4, safe during WiFi), external 470kΩ/470kΩ divider → 0.5 ratio. Do NOT use GPIO5 (ADC2, disabled during WiFi)
Display: SSD1306 OLED 128x32 (I2C: SDA=GPIO7, SCL=GPIO3)
Status LED: GPIO8
HX711 DOUT: GPIO0, CLK: GPIO10
Raspberry Pi hardware
Raspberry Pi 4 (2GB)
Sixfab 4G HAT or USB cellular modem (Huawei E3372)
IoT SIM: Hologram / Twilio Super SIM
OS: Raspberry Pi OS Lite (headless)
Approx BOM per location: ~$125
Data flows — readings
ESP32 reads HX711 every 60s (modem sleep, WiFi active between readings)
→ publishes JSON to keg/readings/SCALE-0042 on Pi Mosquitto
→ Mosquitto bridge forwards to IoT Core
→ IoT Core rule triggers Lambda
→ Lambda: update DynamoDB tap state + write Timestream + check alerts
→ Lambda: fan out to WebSocket connections for that location
→ Dashboard updates live
Data flows — commands (downlink)
Dashboard/app sends POST /taps/{id}/command
→ API Gateway → Lambda validates auth
→ Lambda publishes retained MQTT to keg/command/SCALE-0042/<cmd>
→ IoT Core → Pi Mosquitto → ESP32 receives on wake
→ ESP32 executes, publishes response to keg/response/SCALE-0042
→ Lambda receives response, pushes WebSocket event
Data flows — OTA
Bob uploads new .bin to S3 firmware bucket (versioned)
→ Creates IoT Job targeting device group
→ IoT Core delivers job notification to Pi (Greengrass)
→ Pi OTA server fetches .bin from S3 presigned URL
→ Pi serves .bin locally to ESP32 over WiFi
→ ESP32 fetches, flashes, reboots
→ Reports success/failure to IoT Core
→ Staged rollout: one scale first, wait 24h, then rest
Security
ESP32 ↔ Pi: plain MQTT on local WiFi (trusted network we own)
Pi ↔ IoT Core: mutual TLS with X.509 certificates (Pi holds private key in /greengrass/v2/certs/)
Dashboard ↔ API: JWT from Cognito, validated by API Gateway authorizer
Phone ↔ API: same JWT flow
Phone ↔ ESP32 BLE: no pairing, service UUID whitelist (Scale Sense devices only)
SSH into Pi: WireGuard VPN only, no public port exposure
OTA: S3 presigned URLs (15 min expiry), SHA256 checksum validation