Firmware for an ESP32-S3 that acts as a USB host for a Megatec/Q1-protocol UPS, parses its telemetry, and relays it to a remote server over a secure WebSocket connection.
The ESP32 sits between the UPS and the network: it polls the UPS over USB every two seconds, decodes the response, and forwards each reading as JSON over wss://. Wi-Fi credentials are configured at runtime through a captive portal, and each unit's identity is provisioned into NVS flash at the factory.
| Item | Value |
|---|---|
| Board | 4D Systems ESP32-S3 gen4 R8N16 (8 MB PSRAM, 16 MB flash) |
| UPS | Voltronic-based (Cypress/Megatec chipset) |
| USB Vendor ID | 0x0665 |
| USB Product ID | 0x5161 |
| Protocol | Megatec Q1 status query over a USB HID control pipe |
The board must supply USB host power to the UPS. Vendor and product IDs are set in
src/include/config.h; other Megatec-compatible units usually work by changing those two values.
Requires PlatformIO. Dependencies (ArduinoJson, WebSockets,
WiFiManager) are declared in platformio.ini and fetched automatically.
pio run # build
pio run --target upload # build and flash
pio device monitor # serial console at 115200 baudEach unit needs a device secret written to its NVS partition before it can authenticate with the server. provision.py generates a key, compiles it into an NVS image, flashes it, and registers it with the relay server.
Install the tooling and configure your admin secret:
pip install -r requirements.txt
cp .env.example .env
# then fill in UPS_ADMIN_SECRET and UPS_SERVER_HOSTRun the tool against the board's serial port:
python3 provision.py --port /dev/cu.usbmodemXXXXXXXUseful flags:
| Flag | Description |
|---|---|
--port |
Serial port of the target board (required) |
--host |
Relay server hostname (overrides UPS_SERVER_HOST) |
--key |
Use a specific secret key instead of generating one |
--admin-secret |
Overrides UPS_ADMIN_SECRET from the environment |
The script writes the NVS image to flash offset 0x9000. It prints the generated key on
completion — record it, because it is not recoverable afterwards. If no admin secret is
available the device is still flashed, but server-side registration is skipped.
The generated
nvs_config.csvandnvs_partition.bincontain the device secret in plain text. The script deletes them after a successful run, and both are gitignored.
On first boot (or whenever saved credentials fail) the firmware opens a captive portal access point named SkyByte-UPS-Setup. Connect to it and choose a network. The portal times out after 180 seconds and the device reboots to retry.
The device ID is derived from the last three bytes of the Wi-Fi MAC address, formatted as
UPS-XXXXXX. Authentication happens via query parameters on the WebSocket handshake to
/ws/ingest. Each reading is sent as a JSON frame:
{
"device_id": "UPS-A1B2C3",
"input_v": 230.0,
"output_v": 229.5,
"load_pct": 34.0,
"freq_hz": 50.0,
"batt_v": 13.6,
"temp_c": 27.4,
"grid_ok": true,
"batt_low": false,
"timestamp": 128394
}timestamp is the device's uptime in milliseconds from millis(), not a wall-clock time, so the server should apply its own receive timestamp.
- Device secrets live only in NVS flash and are never committed to this repository.
- The admin secret is read from the environment and must not be hardcoded.
.envis gitignored; use.env.exampleas the template for required variables.
Released under the MIT License.