ESP32 Ethernet firmware updates
Status: normative updater API; software implementation and compilation are verified separately from the NOT RUN hardware acceptance checks below. See ADR 0019.
firmware/esp32 is an ESP-IDF reference app for the Waveshare ESP32-S3-ETH
with its PoE module (ESP32-S3-POE-ETH, SKU 28771; verify the purchased board
matches this model). It contains a reusable yoke_ota component. It brings
up wired Ethernet and a firmware updater only. IR receive/transmit,
routing, and YWP/1 are not implemented by this app. The Python agent
remains a separate Linux program using only the standard library.
No manager, Internet connection, broker, discovery, pairing lease, external CA, or NTP service is required. A locally chosen DHCP server is the default address source; a static IPv4 configuration is also available in Kconfig. The serial log prints the address. No network scanning or automatic trust enrollment occurs. Third-party programs have exactly the same HTTPS API as curl or any management UI.
Trust and storage
USB commissioning writes the otatrust NVS partition with a unique server
private key, self-signed server certificate, the exact self-signed operator
client certificate authorized for this device, and a local Unix-time floor.
Private keys are generated locally using OpenSSL, never committed or baked
into an application image. The client key stays on the operator machine.
Missing or malformed trust fails closed; firmware never erases or creates
trust automatically. TLS requires the client certificate; the client must
verify the provisioned server certificate and hostname/IP SAN. No insecure
HTTP listener is created and verification must not be disabled.
The physical provisioning operation is the authority boundary. The pinned
operator certificate is full firmware-administrator authority, not an IR
pairing credential. Rotation/revocation requires USB reprovisioning in this
version. Protect trust images as secrets and retain a backup outside this
disposable workspace. TLS uses the provisioned local time floor, advances
with uptime, and saves it before restart and on boot. Time spent powered off
is unknowable without an RTC; expiration is therefore not a strong offline
revocation mechanism. clock_source: provisioned-floor exposes this limit.
Use a correct local clock when provisioning; certificate failures require
physical recovery, never an unauthenticated network clock-setting API.
OTA changes only ota_0/ota_1, otadata, and the updater's bounded status
journal in otatrust. nvs is reserved for application configuration,
identity and pairing and is never erased by the updater. Application schema
changes must remain readable by both slots during rollback; this updater
does not migrate schemas. The 16 MiB layout reserves two 6 MiB app slots,
64 KiB application NVS, and 64 KiB trust NVS; firmware upload is limited to
the inactive slot size. Bootloader/partition-table/NVS uploads are rejected
as app images. Network updates cannot change partition geometry.
API contract
All paths require TLS client authentication. Unknown methods/paths fail. Request bodies use an exact positive Content-Length (no chunked uploads). Only one upload is processed at a time. Body reception has an independent 120-second absolute watchdog which shuts down its socket even while TLS is blocked inside a partial record. A 5-second socket idle timeout also applies; it alone would not bound a slowly dribbled TLS record. Hash/image validation after body reception adds bounded local processing time. Failed/incomplete uploads abort the inactive slot and leave the selected boot image unchanged.
The original decimal Content-Length header is checked against the slot limit and parsed length. This prevents ESP-IDF's 64-bit parser length from being silently narrowed to a smaller 32-bit target length.
TLS handshakes have a 10-second absolute deadline. The pinned ESP-IDF v5.5.6
server wraps a blocking TLS call with a timeout that cannot interrupt a
dribbled record. The project therefore generates a build-local replacement
of that one SDK function: a nonblocking handshake loop with a monotonic
deadline, a scheduler yield, and restoration of the original socket flags.
prepare_tls.py checks the entire upstream source's SHA-256 before creating
the override; changing SDK source fails configuration until reviewed. The
SDK checkout and HTTPD's TLS transport hooks remain unchanged. The same
handshake_deadline.h code is tested against real mbedTLS on loopback.
The wired-only reference app enables bootloader_random_enable() before
any certificate/key validation or TLS operation, and retains that SAR ADC
entropy source for all TLS seeds/reseeds. It owns the ADC for its full
lifetime. An application integrating the reusable updater must establish
and retain an Espressif-supported true entropy source before
yoke_ota_init() and throughout server operation; ADC sampling cannot run
concurrently with the reference entropy setup. Do not silently disable the
source when adding IR analog input. See Espressif's
random-number guidance.
| Request | Result |
|---|---|
GET /v1/status |
JSON protocol version, running app/version/slot, selected slot, image limits, state, last error, last attempted digest/version, local clock source |
PUT /v1/firmware, Content-Type: application/octet-stream, X-Image-SHA256: <64 lowercase hex> |
Stream raw yoke-ir-updater.bin; require complete digest match, valid ESP32-S3 app, matching project name; stage inactive slot, select boot slot, reply 202, restart |
Successful 202 means pending-reboot, not application health. A lost response
has an unknown outcome: reconnect and inspect status/version before retrying.
Reboot is scheduled after accepting a valid image even if the response is
lost. HTTP 400 means invalid metadata, 409 means unconfirmed current image
or update already pending, 413 oversized, 415 wrong content type, and 500
means validation/storage/receive error; bounded JSON error gives detail.
Errors close the connection so unread bodies cannot become later requests.
There is no arbitrary URL fetch, command execution, or remote health-confirm
endpoint. A digest checks transport/content intent; the authenticated client
authorizes installation. No separate publisher signature is claimed.
States are idle, receiving, verifying, pending-reboot,
pending-verify, healthy, rolled-back, and error. A durable journal
records selected image digest/version and target slot before boot selection;
after restart, running/target slot and ESP-IDF image state distinguish a
pending verification from rollback. Errors retain the previous good image.
Status and serial logs show versions and errors. Upload processing is
synchronous, so a second status request waits for upload completion; transient
progress states are visible in serial logs. This is not a progress stream.
Application integration and rollback
Call yoke_ota_init() early in boot, then start the HTTPS server with
yoke_ota_start() after network setup. On ESP_OTA_IMG_PENDING_VERIFY,
the component starts a 60-second health deadline independently of network
availability. Call yoke_ota_confirm_health() only after loading existing
configuration/identity/pairing, initializing application workers and local
IR hardware, and passing local self-tests. A missing peer or unplugged
Ethernet cable must not prevent a locally healthy application passing.
Do not call confirmation merely because app_main ran. Fatal boot errors
may call yoke_ota_fail_health(); otherwise timeout reboots into the last
confirmed image. A reset/crash before confirmation also triggers ESP-IDF
bootloader rollback. A confirmed healthy image is not automatically rolled
back for an unrelated later runtime fault.
The minimal app confirms its narrow updater-only health after validating trust, initializing preserved application NVS, installing W5500 and starting the HTTPS listener. It does not wait for DHCP/link/manager and does not claim IR hardware health. The component owns no IR pairing or application tasks.
Primary sources
The Waveshare product documentation describes the 16 MiB flash, W5500 Ethernet and optional PoE module. The vendor Ethernet examples give MOSI 11, MISO 12, SCLK 13, CS 14, INT 10 and RESET 9. These are isolated in Kconfig and bench-unverified here; other similarly named Waveshare boards use different pins. The implementation uses Espressif's HTTPS server and OTA/rollback APIs.
Hardware acceptance — NOT RUN
Compile and host tests cannot prove Ethernet wiring, PoE supply behavior or flash recovery. Before installing an actual IR application, bench-test:
- USB first flash and unique trust provisioning; missing/wrong certificate rejection, server identity rejection, valid authenticated status/upload.
- Successful version change over W5500/PoE, confirmation after real local health checks, and preservation of identity/configuration/pairing bytes.
- Truncated upload, wrong digest, wrong chip/project, oversize and power interruption during write: previous image still boots.
- New image crash, timer timeout, and power interruption before health confirmation: last confirmed image boots and status shows rollback.
- Unplugged Ethernet/absent manager: local application health can still confirm; normal operation does not acquire a pairing lease.
- USB recovery with both app slots damaged, and operator certificate
rotation without erasing application NVS. Never run
erase-flashas a normal update step.