Last Updated: February 7, 2026
Author: Trepo Engineering
Status: Production Ready ✅
The HALO device consists of two ESP32 boards that can be updated independently over-the-air:
| Board | Chip | Primary Function | Binary Size |
|---|---|---|---|
| LCD | ESP32-S3 | Display, UI, User Interaction | ~3.9 MB |
| Sense | XIAO ESP32-S3 | Sensors, Scanning, Data Collection | ~1.4 MB |
Key Features: - ✅ Automatic updates during maintenance windows - ✅ Manual trigger via MQTT - ✅ Version comparison (only downloads if newer) - ✅ Retry logic (3 manifest attempts, 3 download attempts) - ✅ Chunked download with progress reporting - ✅ Automatic rollback on boot failure
┌─────────────────────────────────────────────────────────────────┐
│ AWS Cloud │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ S3 Bucket │ │
│ │ halo-ota-dev.s3.amazonaws.com │ │
│ │ │ │
│ │ /halo/ota/dev/ │ │
│ │ ├── manifest_latest.json ← Sense manifest │ │
│ │ ├── sense_v6.0.46.bin ← Sense firmware │ │
│ │ └── lcd/ │ │
│ │ ├── manifest_latest.json ← LCD manifest │ │
│ │ └── lcd_v1.2.3.bin ← LCD firmware │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ │ HTTPS GET │
│ ▼ │
└─────────────────────────────────────────────────────────────────┘
│
│
┌──────────┴──────────┐
│ WiFi Network │
└──────────┬──────────┘
│
┌────────────────┴────────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ LCD Board │◄────UART────────►│ Sense Board │
│ (ESP32-S3) │ │(XIAO ESP32-S3)│
└──────────────┘ └──────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ OTA UPDATE FLOW │
└─────────────────────────────────────────────────────────────────┘
Device S3 Bucket
│ │
│ 1. Check if in maintenance window │
│ (or MQTT force command received) │
│ │
│ 2. GET manifest_latest.json ───────────────────────►
│ │
│ ◄─────────────────────────────── 3. Return manifest│
│ │
│ 4. Compare versions: │
│ - If remote > local: proceed │
│ - If remote <= local: skip (up to date) │
│ │
│ 5. GET firmware.bin ───────────────────────────────►
│ (chunked, 2KB buffer) │
│ │
│ ◄─────────────────────────────── 6. Stream binary │
│ (progress: 10%, 20%, ... 100%) │
│ │
│ 7. Write to OTA partition │
│ │
│ 8. Verify checksum │
│ │
│ 9. Set boot partition & reboot │
│ │
│ 10. Boot into new firmware │
│ - Mark as valid after successful boot │
│ - Or rollback if boot fails │
│ │
ota/sched/setota/force_nowhalo-ota-dev/
└── halo/
└── ota/
├── dev/ ← Development environment
│ ├── manifest_latest.json ← SENSE manifest
│ ├── sense_v6.0.46.bin ← SENSE firmware
│ └── lcd/
│ ├── manifest_latest.json ← LCD manifest
│ └── lcd_v1.2.3.bin ← LCD firmware
│
├── staging/ ← Staging environment
│ ├── manifest_latest.json
│ ├── sense_vX.X.X.bin
│ └── lcd/
│ ├── manifest_latest.json
│ └── lcd_vX.X.X.bin
│
└── prod/ ← Production environment
├── manifest_latest.json
├── sense_vX.X.X.bin
└── lcd/
├── manifest_latest.json
└── lcd_vX.X.X.bin
| Board | Environment | Manifest URL |
|---|---|---|
| Sense | Dev | https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/dev/manifest_latest.json |
| Sense | Prod | https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/prod/manifest_latest.json |
| LCD | Dev | https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/dev/lcd/manifest_latest.json |
| LCD | Prod | https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/prod/lcd/manifest_latest.json |
The manifest tells the device what version is available and where to download it.
{
"version": "6.0.46",
"url": "https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/dev/sense_v6.0.46.bin",
"size": 1482736,
"sha256": "a1b2c3d4e5f6..."
}| Field | Type | Required | Description |
|---|---|---|---|
version |
string | ✅ | Semantic version (e.g., “6.0.46”) |
url |
string | ✅ | Full HTTPS URL to the binary file |
size |
integer | ⚠️ | Binary size in bytes (for progress calculation) |
sha256 |
string | ⚠️ | SHA-256 hash for integrity verification |
Versions are compared as semantic versions: -
6.0.46 > 6.0.45 → Update -
6.0.46 = 6.0.46 → Skip (already up to date) -
6.0.46 < 6.0.47 → Update
trepo-dev
profileFor Sense Board:
cd /path/to/firmware/sense
# Compile
arduino-cli compile \
--fqbn "esp32:esp32:XIAO_ESP32S3:USBMode=hwcdc,CDCOnBoot=default" \
--output-dir ./build \
.
# The binary will be at: ./build/sense.ino.binFor LCD Board:
cd /path/to/firmware/lcd
# Compile
arduino-cli compile \
--fqbn "esp32:esp32:esp32s3:FlashSize=16M,PartitionScheme=default_8MB,PSRAM=opi,CDCOnBoot=cdc" \
--output-dir ./build \
.
# The binary will be at: ./build/lcd.ino.binUse a consistent naming convention:
# Sense
mv ./build/sense.ino.bin ./build/sense_v6.0.47.bin
# LCD
mv ./build/lcd.ino.bin ./build/lcd_v1.2.4.bin# Sense (dev environment)
aws s3 cp ./build/sense_v6.0.47.bin \
s3://halo-ota-dev/halo/ota/dev/sense_v6.0.47.bin \
--profile trepo-dev
# LCD (dev environment)
aws s3 cp ./build/lcd_v1.2.4.bin \
s3://halo-ota-dev/halo/ota/dev/lcd/lcd_v1.2.4.bin \
--profile trepo-dev# Get file size in bytes
ls -l ./build/sense_v6.0.47.bin | awk '{print $5}'
# Example output: 1482736Create manifest_latest.json:
{
"version": "6.0.47",
"url": "https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/dev/sense_v6.0.47.bin",
"size": 1482736
}# Sense
aws s3 cp manifest_latest.json \
s3://halo-ota-dev/halo/ota/dev/manifest_latest.json \
--profile trepo-dev \
--content-type "application/json"
# LCD
aws s3 cp manifest_latest.json \
s3://halo-ota-dev/halo/ota/dev/lcd/manifest_latest.json \
--profile trepo-dev \
--content-type "application/json"# Check manifest is accessible
curl -s https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/dev/manifest_latest.json | jq .
# Check binary is accessible (just headers)
curl -I https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/dev/sense_v6.0.47.binEither wait for the maintenance window, or force immediately via MQTT:
# Using mosquitto_pub (replace with your device's topic)
mosquitto_pub \
-h your-mqtt-broker.amazonaws.com \
-p 8883 \
--cafile AmazonRootCA1.pem \
--cert device-cert.pem \
--key device-key.pem \
-t "halo/device/OWNER_ID/DEVICE_ID/cmd" \
-m '{"cmd":"ota/force_now"}'Here’s a script that does everything:
#!/bin/bash
# deploy_ota.sh - Deploy OTA update for Sense or LCD
BOARD=$1 # "sense" or "lcd"
VERSION=$2 # e.g., "6.0.47"
BINARY=$3 # path to compiled .bin file
ENV=${4:-dev} # "dev", "staging", or "prod"
BUCKET="halo-ota-dev"
PROFILE="trepo-dev"
if [ "$BOARD" = "sense" ]; then
S3_PATH="halo/ota/$ENV"
BINARY_NAME="sense_v${VERSION}.bin"
elif [ "$BOARD" = "lcd" ]; then
S3_PATH="halo/ota/$ENV/lcd"
BINARY_NAME="lcd_v${VERSION}.bin"
else
echo "Usage: $0 <sense|lcd> <version> <binary_path> [env]"
exit 1
fi
SIZE=$(ls -l "$BINARY" | awk '{print $5}')
URL="https://${BUCKET}.s3.us-east-1.amazonaws.com/${S3_PATH}/${BINARY_NAME}"
echo "📦 Deploying $BOARD v$VERSION to $ENV"
echo " Binary: $BINARY ($SIZE bytes)"
echo " URL: $URL"
echo ""
# Upload binary
echo "⬆️ Uploading binary..."
aws s3 cp "$BINARY" "s3://${BUCKET}/${S3_PATH}/${BINARY_NAME}" --profile $PROFILE
# Create and upload manifest
echo "📝 Creating manifest..."
cat > /tmp/manifest_latest.json << EOF
{
"version": "$VERSION",
"url": "$URL",
"size": $SIZE
}
EOF
aws s3 cp /tmp/manifest_latest.json \
"s3://${BUCKET}/${S3_PATH}/manifest_latest.json" \
--profile $PROFILE \
--content-type "application/json"
# Verify
echo "✅ Verifying..."
curl -s "https://${BUCKET}.s3.us-east-1.amazonaws.com/${S3_PATH}/manifest_latest.json" | jq .
echo ""
echo "🚀 Deploy complete! Devices will update during next maintenance window."
echo " To force immediate update, send MQTT command: ota/force_now"Usage:
./deploy_ota.sh sense 6.0.47 ./build/sense.ino.bin dev
./deploy_ota.sh lcd 1.2.4 ./build/lcd.ino.bin prodControl OTA behavior via MQTT commands sent to:
halo/device/{owner_id}/{device_id}/cmd
| Command | Payload | Description |
|---|---|---|
ota/force_now |
{"cmd":"ota/force_now"} |
Force immediate OTA check |
ota/sched/set |
{"cmd":"ota/sched/set","enable":true,"start_min":120,"window_min":30} |
Set maintenance window |
ota/sched/get |
{"cmd":"ota/sched/get"} |
Get current schedule |
ota/status/get |
{"cmd":"ota/status/get"} |
Get OTA status |
ota_check |
{"cmd":"ota_check"} |
Request OTA check (respects cooldowns) |
The start_min is minutes past midnight
UTC:
| PST Time | UTC Time | start_min |
|---|---|---|
| 6:00 PM | 2:00 AM (+1 day) | 120 |
| 2:00 AM | 10:00 AM | 600 |
| 12:00 PM | 8:00 PM | 1200 |
Example: Set maintenance window to 2-3 AM PST (10-11 AM UTC):
{
"cmd": "ota/sched/set",
"enable": true,
"start_min": 600,
"window_min": 60
}Connect to the device via USB serial to see OTA progress:
# Sense board
screen /dev/cu.usbmodem21101 115200
# LCD board
screen /dev/cu.usbmodem21201 115200Expected output during OTA:
[OTA] Checking for updates...
[OTA] Manifest attempt 1/3...
[OTA] Current: 6.0.45, Available: 6.0.47
[OTA] Update available! Downloading...
[OTA] Download attempt 1/3...
[OTA] Progress: 10% (148273/1482736)
[OTA] Progress: 20% (296547/1482736)
...
[OTA] Progress: 100% (1482736/1482736)
[OTA] Download complete. Applying update...
[OTA] Update successful! Rebooting...
OTA events are logged to CloudWatch (if configured): - Update started - Update completed - Update failed (with error)
# Sense
curl -s https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/dev/manifest_latest.json | jq .
# LCD
curl -s https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/dev/lcd/manifest_latest.json | jq .The ESP32 has built-in rollback protection:
To rollback to a previous version:
6.0.48)Important: The version number must be HIGHER than the current version for the device to accept it.
| Symptom | Cause | Solution |
|---|---|---|
| “Already up to date” | Version in manifest ≤ device version | Increment version number |
| “Manifest fetch failed” | Network issue or wrong URL | Check WiFi, verify S3 URL |
| “Download failed at X%” | WiFi instability | Device will retry automatically (3 attempts) |
| No OTA attempt | Outside maintenance window | Send ota/force_now command |
| “Partition too small” | Binary too large for OTA partition | Check partition scheme |
Error: HTTP 403 Forbidden - S3 bucket
permissions issue - Check bucket policy allows public read
Error: SSL handshake failed -
Certificate issue - Ensure device has correct root CA
Error: Not enough space - Binary too
large - Check OTA partition size (should be ~1.9MB for Sense, ~3.9MB for
LCD)
# Should return 200
curl -I https://halo-ota-dev.s3.us-east-1.amazonaws.com/halo/ota/dev/manifest_latest.json
# Check bucket policy
aws s3api get-bucket-policy --bucket halo-ota-dev --profile trepo-devBefore deploying to production:
| Step | Command |
|---|---|
| 1. Compile | arduino-cli compile --fqbn <FQBN> <sketch> |
| 2. Upload binary | aws s3 cp firmware.bin s3://bucket/path/ |
| 3. Update manifest | aws s3 cp manifest.json s3://bucket/path/ |
| 4. Verify | curl <manifest_url> |
| 5. Trigger | MQTT ota/force_now or wait for window |
| 6. Monitor | Serial output or CloudWatch |
/Users/mikehunt/Documents/Arduino/HALOMAIN_rev1//Users/mikehunt/.openclaw/workspace/ota_test_results/Questions? Contact the Trepo engineering team.