Unihiker K10 OTA

Data & APIs

Add HTTP OTA (Over-The-Air) firmware update capability to Unihiker K10 Arduino projects, including AP/STA projects and ESP-NOW projects that need a safe OTA maintenance mode. Use when you need wireless firmware updates without USB cable, when ArduinoOTA fails, or when an ESP-NOW sketch must keep an OTA recovery/update path.

Install

openclaw skills install unihiker-k10-ota

Unihiker K10 - HTTP OTA

Overview

Enable wireless firmware updates for K10 Arduino projects via HTTP POST.

Core principle: K10's default partition table has no OTA partitions. You must switch to a custom partition table with ota_0 + ota_1 before Update.begin() can work.

Why not ArduinoOTA? The standard ArduinoOTA library (UDP-based) requires the ESP32 to connect back to the host computer on a random port, which is often blocked by Windows Firewall. HTTP OTA uses a simple host→device upload direction and works reliably on all networks.

When to Use

  • Your K10 is installed in a location difficult to reach with USB
  • You want to update firmware without opening the enclosure
  • You need a scriptable/automated deployment pipeline
  • ArduinoOTA network port upload fails with "No response from device"

Prerequisites

  • Existing K10 Arduino project with WebServer running
  • arduino-cli installed and K10 BSP (UNIHIKER:esp32:k10) available
  • Device and computer on the same network (or connected to K10's AP)

ESP-NOW Compatibility Rule

ESP-NOW sketches can support OTA, but ordinary HTTP OTA requires temporary IP networking through WIFI_AP, WIFI_STA, or WIFI_AP_STA. ESP-NOW itself is not an IP transport, so do not claim that the standard /ota HTTP endpoint works over pure ESP-NOW packets.

When adding OTA to an ESP-NOW program, use this policy:

  1. Prefer a maintenance OTA mode: normal runtime uses ESP-NOW; a button, serial command, saved flag, or received command enters OTA mode, starts AP or STA networking, registers /ota, and services server.handleClient().
  2. Keep ESP-NOW and WiFi on the same channel if they run together. If STA connects to a router, the router determines the channel; ESP-NOW peers must use that channel or peer channel 0.
  3. For reliability, pause ESP-NOW sends and time-critical control loops while an OTA upload is active.
  4. Keep an AP fallback such as K10-OTA-<id> available in OTA mode so updates still work when STA credentials are missing or the router changes.
  5. Treat pure ESP-NOW firmware transfer as an advanced separate design. It needs packet chunking, acknowledgements, image validation, and writes to OTA partitions; do not replace HTTP OTA with it unless the user explicitly asks for ESP-NOW-only OTA.

See references/ota-implementation.md for the ESP-NOW maintenance-mode code pattern.

Quick Start

Step 1: Add Custom Partition Table

Create partitions.csv in your sketch directory:

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x640000,
app1,     app,  ota_1,   0x650000,0x640000,
spiffs,   data, spiffs,  0xc90000,0x370000,

Compile with the custom partition:

arduino-cli compile --fqbn UNIHIKER:esp32:k10 . \
  --output-dir build \
  --build-property "build.partitions=custom"

Optional speed-up for repeated compiles:

# Use all CPU cores and keep build artifacts in stable project-local folders.
arduino-cli compile --fqbn UNIHIKER:esp32:k10 . \
  --build-path .arduino-build \
  --output-dir build \
  --build-property "build.partitions=custom" \
  -j 0

Arduino CLI already has a built-in build_cache. To use a longer-lived cache, configure the official build_cache.* keys rather than compiler.cache.*:

arduino-cli config set build_cache.path ~/.cache/arduino-build-cache
arduino-cli config set build_cache.compilations_before_purge 0

On Windows PowerShell:

arduino-cli config set build_cache.path "$env:LOCALAPPDATA\arduino\build-cache"
arduino-cli config set build_cache.compilations_before_purge 0

Step 2: Add OTA Endpoint to Firmware

Include the Update library and add a POST handler:

#include <Update.h>

void handleOta() {
  server.sendHeader("Connection", "close");
  server.send(200, "text/plain", Update.hasError() ? "FAIL" : "OK");
  if (!Update.hasError()) {
    ESP.restart();  // or schedule a delayed restart
  }
}

void handleOtaUpload() {
  HTTPUpload &upload = server.upload();
  if (upload.status == UPLOAD_FILE_START) {
    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
      Update.printError(Serial);
    }
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
      Update.printError(Serial);
    }
  } else if (upload.status == UPLOAD_FILE_END) {
    if (Update.end(true)) {
      Serial.printf("OTA Success: %u bytes\n", upload.totalSize);
    } else {
      Update.printError(Serial);
    }
  }
}

// In setup() or startNetwork():
server.on("/ota", HTTP_POST, handleOta, handleOtaUpload);

For ESP-NOW sketches, do not leave OTA as an afterthought. Add an explicit OTA mode gate:

bool otaMode = false;
bool otaUploadActive = false;

void enterOtaMode() {
  otaMode = true;
  WiFi.mode(WIFI_AP_STA);  // AP fallback plus optional STA
  WiFi.softAP("K10-OTA", "12345678");
  // Optional: WiFi.begin(savedSsid, savedPassword);
  server.on("/ota", HTTP_POST, handleOta, handleOtaUpload);
  server.begin();
}

void loop() {
  if (otaMode) {
    server.handleClient();
    return;  // keep ESP-NOW/control traffic paused during OTA maintenance
  }

  // normal ESP-NOW runtime
}

Step 3: First USB Upload (Required Once)

The first upload must be via USB to flash the new partition table:

arduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .

Step 4: Update Over WiFi

After the first USB upload, use any of these methods:

curl:

curl -F "file=@build/your_sketch.ino.bin" http://192.168.9.42/ota

Python script (works on Windows, macOS, and Linux):

python scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42

PowerShell 7+ (works on Windows, macOS, and Linux):

pwsh ./scripts/ota_upload.ps1 -Bin build/your_sketch.ino.bin -Ip 192.168.9.42

Important Notes

  • Partition change erases flash layout. The first USB upload after adding partitions.csv will reformat the flash partition table. Preferences / NVS data may be lost.
  • Every OTA-enabled sketch must include the OTA code. If you upload a sketch without /ota handler, you lose OTA capability and must return to USB.
  • Do not use delay() in loop() for long periods. Use non-blocking millis() patterns so the WebServer can process the upload request.
  • Content-Length: Arduino WebServer's server.header("Content-Length") does not work in POST handlers. Use server.clientContentLength() instead if you need the raw body size.
  • Compile cache: Use Arduino CLI's official build_cache.* settings and --build-path for repeat builds. Do not document compiler.cache.enable, compiler.cache.path, or ccache as required OTA setup because they are not part of the current Arduino CLI configuration reference.
  • ESP-NOW: HTTP OTA needs AP/STA networking. If the program uses ESP-NOW, add an OTA maintenance mode, manage WiFi channel alignment, and pause ESP-NOW traffic while flashing.

Files

unihiker-k10-ota/
├── SKILL.md                           # This file
├── references/
│   └── ota-implementation.md          # Detailed implementation guide
└── scripts/
    ├── ota_upload.py                  # Python OTA uploader
    └── ota_upload.ps1                 # PowerShell OTA uploader

Troubleshooting

IssueCauseSolution
BEGIN_FAILNo OTA partitions in partition tableAdd partitions.csv with ota_0 + ota_1 and reflash via USB
FAIL after uploadUpdate.write() failed mid-streamCheck serial log; likely flash write error or insufficient space
NO_CONTENTContent-Length header missingEnsure client sends valid multipart/form-data with file data
Device does not restartESP.restart() called before response sentUse scheduleRestart() with a small delay instead
Network port not foundmDNS/ArduinoOTA not runningHTTP OTA does not need network port detection; use the device's IP directly
ESP-NOW works until STA startsSTA changed the radio channel to the router channelPut peers on the same channel or use peer channel 0 after STA connects
OTA page unreachable in ESP-NOW sketchSketch never entered AP/STA maintenance modeAdd a button/serial/command path that calls enterOtaMode() and starts the WebServer
ESP-NOW packets drop during OTAFlashing and HTTP handling are competing with runtime trafficPause ESP-NOW sends/control loops while otaUploadActive or otaMode is true