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.
AI model rule: K10 built-in AI support files live in fixed flash regions beginning at 0x510000. OTA partitions must end before that address if the project uses voice recognition, TTS, face recognition, or other built-in AI features.
TTS firmware rule: Speech synthesis ( ASR::setAsrSpeed() / ASR::speak()) exists only in the Chinese K10 firmware. Preserving or restoring voice_data/CN model partitions does not add the TTS API to another firmware variant. Only deploy TTS OTA images to boards whose Chinese firmware support has been confirmed.
Screen refresh rule: OTA status pages, progress indicators, connection state, and voice status should use partial redraws. Full-screen clearing or full-background redraw causes visible flicker on K10; use it only for initialization, page switches, exit cleanup, or when measured full-screen refresh is above 30 fps.
WebServer runningarduino-cli installed and K10 BSP ( UNIHIKER:esp32:k10) availableESP-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:
/ota, and services server.handleClient().0.K10-OTA-<id> available in OTA mode so updates still work when STA credentials are missing or the router changes.See references/ota-implementation.md for the ESP-NOW maintenance-mode code pattern.
Create partitions.csv in your sketch directory:
csv
# K10 OTA partition table that preserves speech-recognition model regions.
# Keep model/voice_data/fr offsets aligned with the DFRobot K10 factory table.
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x280000,
app1, app, ota_1, 0x290000, 0x280000,
model, data, spiffs, 0x510000, 4563K,
voice_data, data, fat, 0x985000, 2542K,
fr, data, , 0xC01000, 100K,
coredump, data, coredump,, 1K,
spiffs, data, spiffs, 0xC1B000, 0x3E5000,
Compile with the custom partition:
bash
arduino-cli compile --fqbn UNIHIKER:esp32:k10 . \
--output-dir build \
--build-property "build.partitions=custom"
Optional speed-up for repeated compiles:
bash
# 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.*:
bash
arduino-cli config set build_cache.path ~/.cache/arduino-build-cache
arduino-cli config set build_cache.compilations_before_purge 0
On Windows PowerShell:
powershell
arduino-cli config set build_cache.path "$env:LOCALAPPDATA\arduino\build-cache"
arduino-cli config set build_cache.compilations_before_purge 0
Include the Update library and add a POST handler:
cpp
#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:
cpp
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
}
The first upload must be via USB to flash the new partition table:
bash
arduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .
After the first USB upload, use any of these methods:
curl:
bash
curl -F "file=@build/your_sketch.ino.bin" http://192.168.9.42/ota
Python script (works on Windows, macOS, and Linux):
bash
python scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42
PowerShell 7+ (works on Windows, macOS, and Linux):
powershell
pwsh ./scripts/ota_upload.ps1 -Bin build/your_sketch.ino.bin -Ip 192.168.9.42
partitions.csv will reformat the flash partition table. Preferences / NVS data may be lost./ota handler, you lose OTA capability and must return to USB.delay() in loop() for long periods. Use non-blocking millis() patterns so the WebServer can process the upload request.server.header("Content-Length") does not work in POST handlers. Use server.clientContentLength() instead if you need the raw body size.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.model at 0x510000, voice_data at 0x985000, or fr at 0xC01000. A generic large OTA layout can erase AI support data.Restore Initial Settings or a one-time USB upload with the Arduino/PlatformIO CN/EN model refresh option. A full erase plus Model=None does not restore model files.loop() just to update OTA/WiFi progress. Draw static labels once, then overwrite only changed values or progress areas before one display update call.text
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
IssueCauseSolutionBEGIN_FAILNo OTA partitions in partition tableAdd partitions.csv with ota_0 + ota_1 and reflash via USBFAIL after uploadUpdate.write() failed mid-streamCheck serial log; likely flash write error or insufficient spaceNO_CONTENT``Content-Length header missingEnsure client sends valid multipart/form-data with file dataDevice does not restartESP.restart() called before response sentUse scheduleRestart() with a small delay insteadNetwork port not foundmDNS/ArduinoOTA not runningHTTP OTA does not need network port detection; use the device's IP directlyESP-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 connectsOTA page unreachable in ESP-NOW sketchSketch never entered AP/STA maintenance modeAdd a button/serial/command path that calls enterOtaMode() and starts the WebServerESP-NOW packets drop during OTAFlashing and HTTP handling are competing with runtime trafficPause ESP-NOW sends/control loops while otaUploadActive or otaMode is true