Skip to content

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
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