{"id":"aa37e2ba-4967-4eea-a24f-4fc67b0f2c1d","entityType":"agent","slug":"clawhub-rockets-cn-unihiker-k10-ota","name":"Unihiker K10 Ota","canonicalUrl":"https://www.xpersona.co/agent/clawhub-rockets-cn-unihiker-k10-ota","canonicalPath":"/agent/clawhub-rockets-cn-unihiker-k10-ota","generatedAt":"2026-10-11T07:43:47.692Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-11T04:14:19.560Z","emptyReason":null},"description":"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... Skill: Unihiker K10 Ota Owner: rockets-cn Summary: 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... Tags: arduino:1.0.0, esp32:1.0.0, k10:1.0.0, latest:1.0.4, ota:1.0.0, unihiker:1.0.0 Version history: v1.0.4 | 2026-07-22T07:43:03.837Z | user Recommend the DFRobot Gravity ADS1115 for safe K10 external analog","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.2K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s172xg1ay5cxz756fkf8n3qtmd87evvj:unihiker-k10-ota","sourceUrl":"https://clawhub.ai/rockets-cn/unihiker-k10-ota","homepage":"https://clawhub.ai/rockets-cn/skills/unihiker-k10-ota","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/rockets-cn/unihiker-k10-ota","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/rockets-cn/skills/unihiker-k10-ota","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":61,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"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... "},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T04:14:19.560Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T04:14:19.560Z","emptyReason":null},"stars":null,"forks":null,"downloads":1163,"packageName":null,"latestVersion":"1.0.4","tractionLabel":"1.2K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T04:14:19.489Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T04:14:19.560Z","lastCrawledAt":"2026-10-11T04:14:19.489Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T04:14:19.489Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.4","createdAt":"2026-07-22T07:43:03.837Z","changelog":"Recommend the DFRobot Gravity ADS1115 for safe K10 external analog expansion and publish the latest synchronized K10 skill updates.","fileCount":6,"zipByteSize":14444},{"version":"1.0.3","createdAt":"2026-07-21T13:18:58.376Z","changelog":"Document Chinese-firmware-only K10 speech synthesis APIs and add verified reference code.","fileCount":6,"zipByteSize":14426},{"version":"1.0.2","createdAt":"2026-06-12T09:21:01.150Z","changelog":"Document partial redraw requirements for OTA progress and K10 status screens.","fileCount":6,"zipByteSize":14284},{"version":"1.0.1","createdAt":"2026-06-12T08:42:20.121Z","changelog":"Use model-preserving OTA partition guidance and document recovery when K10 AI model data is missing or damaged.","fileCount":6,"zipByteSize":13879},{"version":"1.0.0","createdAt":"2026-06-10T03:24:56.359Z","changelog":"Initial Clawhub release for HTTP OTA update guidance on Unihiker K10 Arduino projects.","fileCount":5,"zipByteSize":12117}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s172xg1ay5cxz756fkf8n3qtmd87evvj:unihiker-k10-ota","setupComplexity":"low","setupSteps":["Setup complexity is LOW. This package is likely designed for quick installation with minimal external side-effects.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-rockets-cn-unihiker-k10-ota/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-rockets-cn-unihiker-k10-ota/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-rockets-cn-unihiker-k10-ota/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-rockets-cn-unihiker-k10-ota/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-rockets-cn-unihiker-k10-ota/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-rockets-cn-unihiker-k10-ota/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T07:43:47.689Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-rockets-cn-unihiker-k10-ota/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-rockets-cn-unihiker-k10-ota/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-rockets-cn-unihiker-k10-ota/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-rockets-cn-unihiker-k10-ota/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-11T04:14:19.560Z","emptyReason":null},"readme":"Skill: Unihiker K10 Ota\n\nOwner: rockets-cn\n\nSummary: 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...\n\nTags: arduino:1.0.0, esp32:1.0.0, k10:1.0.0, latest:1.0.4, ota:1.0.0, unihiker:1.0.0\n\nVersion history:\n\nv1.0.4 | 2026-07-22T07:43:03.837Z | user\n\nRecommend the DFRobot Gravity ADS1115 for safe K10 external analog expansion and publish the latest synchronized K10 skill updates.\n\nv1.0.3 | 2026-07-21T13:18:58.376Z | user\n\nDocument Chinese-firmware-only K10 speech synthesis APIs and add verified reference code.\n\nv1.0.2 | 2026-06-12T09:21:01.150Z | user\n\nDocument partial redraw requirements for OTA progress and K10 status screens.\n\nv1.0.1 | 2026-06-12T08:42:20.121Z | user\n\nUse model-preserving OTA partition guidance and document recovery when K10 AI model data is missing or damaged.\n\nv1.0.0 | 2026-06-10T03:24:56.359Z | user\n\nInitial Clawhub release for HTTP OTA update guidance on Unihiker K10 Arduino projects.\n\nArchive index:\n\nArchive v1.0.4: 6 files, 14444 bytes\n\nFiles: references/ota-implementation.md (14475b), scripts/ota_upload.ps1 (2097b), scripts/ota_upload.py (3022b), skill-card.md (2165b), SKILL.md (11239b), _meta.json (135b)\n\nFile v1.0.4:SKILL.md\n\n---\nname: unihiker-k10-ota\ndescription: 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.\n---\n\n# Unihiker K10 - HTTP OTA\n\n## Overview\n\nEnable wireless firmware updates for K10 Arduino projects via HTTP POST.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n## When to Use\n\n- Your K10 is installed in a location difficult to reach with USB\n- You want to update firmware without opening the enclosure\n- You need a scriptable/automated deployment pipeline\n- ArduinoOTA network port upload fails with \"No response from device\"\n\n## Prerequisites\n\n- Existing K10 Arduino project with `WebServer` running\n- `arduino-cli` installed and K10 BSP (`UNIHIKER:esp32:k10`) available\n- Device and computer on the same network (or connected to K10's AP)\n\n## ESP-NOW Compatibility Rule\n\nESP-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.\n\nWhen adding OTA to an ESP-NOW program, use this policy:\n\n1. 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()`.\n2. 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`.\n3. For reliability, pause ESP-NOW sends and time-critical control loops while an OTA upload is active.\n4. 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.\n5. 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.\n\nSee `references/ota-implementation.md` for the ESP-NOW maintenance-mode code pattern.\n\n## Quick Start\n\n### Step 1: Add Custom Partition Table\n\nCreate `partitions.csv` in your sketch directory:\n\n```csv\n# K10 OTA partition table that preserves speech-recognition model regions.\n# Keep model/voice_data/fr offsets aligned with the DFRobot K10 factory table.\n# Name,     Type, SubType, Offset,   Size,     Flags\nnvs,        data, nvs,     0x9000,   0x5000,\notadata,    data, ota,     0xe000,   0x2000,\napp0,       app,  ota_0,   0x10000,  0x280000,\napp1,       app,  ota_1,   0x290000, 0x280000,\nmodel,      data, spiffs,  0x510000, 4563K,\nvoice_data, data, fat,     0x985000, 2542K,\nfr,         data, ,        0xC01000, 100K,\ncoredump,   data, coredump,,         1K,\nspiffs,     data, spiffs,  0xC1B000, 0x3E5000,\n```\n\nCompile with the custom partition:\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\"\n```\n\nOptional speed-up for repeated compiles:\n\n```bash\n# Use all CPU cores and keep build artifacts in stable project-local folders.\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0\n```\n\nArduino CLI already has a built-in `build_cache`. To use a longer-lived cache, configure the official `build_cache.*` keys rather than `compiler.cache.*`:\n\n```bash\narduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\nOn Windows PowerShell:\n\n```powershell\narduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\n### Step 2: Add OTA Endpoint to Firmware\n\nInclude the `Update` library and add a POST handler:\n\n```cpp\n#include <Update.h>\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    ESP.restart();  // or schedule a delayed restart\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n\n// In setup() or startNetwork():\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n```\n\nFor ESP-NOW sketches, do not leave OTA as an afterthought. Add an explicit OTA mode gate:\n\n```cpp\nbool otaMode = false;\nbool otaUploadActive = false;\n\nvoid enterOtaMode() {\n  otaMode = true;\n  WiFi.mode(WIFI_AP_STA);  // AP fallback plus optional STA\n  WiFi.softAP(\"K10-OTA\", \"12345678\");\n  // Optional: WiFi.begin(savedSsid, savedPassword);\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n}\n\nvoid loop() {\n  if (otaMode) {\n    server.handleClient();\n    return;  // keep ESP-NOW/control traffic paused during OTA maintenance\n  }\n\n  // normal ESP-NOW runtime\n}\n```\n\n### Step 3: First USB Upload (Required Once)\n\nThe first upload must be via USB to flash the new partition table:\n\n```bash\narduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .\n```\n\n### Step 4: Update Over WiFi\n\nAfter the first USB upload, use any of these methods:\n\n**curl:**\n```bash\ncurl -F \"file=@build/your_sketch.ino.bin\" http://192.168.9.42/ota\n```\n\n**Python script (works on Windows, macOS, and Linux):**\n```bash\npython scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42\n```\n\n**PowerShell 7+ (works on Windows, macOS, and Linux):**\n```powershell\npwsh ./scripts/ota_upload.ps1 -Bin build/your_sketch.ino.bin -Ip 192.168.9.42\n```\n\n## Important Notes\n\n- **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.\n- **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.\n- **Do not use `delay()` in `loop()` for long periods.** Use non-blocking `millis()` patterns so the WebServer can process the upload request.\n- **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.\n- **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.\n- **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.\n- **AI model regions:** Do not let OTA app partitions overlap `model` at `0x510000`, `voice_data` at `0x985000`, or `fr` at `0xC01000`. A generic large OTA layout can erase AI support data.\n- **TTS compatibility:** TTS is Chinese-firmware-only. OTA preserves or replaces the application image; it must not be described as converting an English/international firmware into a TTS-capable Chinese firmware merely because CN model data remains present.\n- **Model recovery:** If AI functions reboot or model data is suspected damaged, use Mind+ `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.\n- **Display refresh:** Do not clear and redraw the whole K10 screen in `loop()` just to update OTA/WiFi progress. Draw static labels once, then overwrite only changed values or progress areas before one display update call.\n\n## Files\n\n```\nunihiker-k10-ota/\n├── SKILL.md                           # This file\n├── references/\n│   └── ota-implementation.md          # Detailed implementation guide\n└── scripts/\n    ├── ota_upload.py                  # Python OTA uploader\n    └── ota_upload.ps1                 # PowerShell OTA uploader\n```\n\n## Troubleshooting\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| `BEGIN_FAIL` | No OTA partitions in partition table | Add `partitions.csv` with `ota_0` + `ota_1` and reflash via USB |\n| `FAIL` after upload | `Update.write()` failed mid-stream | Check serial log; likely flash write error or insufficient space |\n| `NO_CONTENT` | `Content-Length` header missing | Ensure client sends valid `multipart/form-data` with file data |\n| Device does not restart | `ESP.restart()` called before response sent | Use `scheduleRestart()` with a small delay instead |\n| Network port not found | mDNS/ArduinoOTA not running | HTTP OTA does not need network port detection; use the device's IP directly |\n| ESP-NOW works until STA starts | STA changed the radio channel to the router channel | Put peers on the same channel or use peer channel `0` after STA connects |\n| OTA page unreachable in ESP-NOW sketch | Sketch never entered AP/STA maintenance mode | Add a button/serial/command path that calls `enterOtaMode()` and starts the WebServer |\n| ESP-NOW packets drop during OTA | Flashing and HTTP handling are competing with runtime traffic | Pause ESP-NOW sends/control loops while `otaUploadActive` or `otaMode` is true |\n\nFile v1.0.4:_meta.json\n\n{\n  \"ownerId\": \"kn7bn6y6610bpcvzaykr1h2m1d83e2rm\",\n  \"slug\": \"unihiker-k10-ota\",\n  \"version\": \"1.0.4\",\n  \"publishedAt\": 1784706183837\n}\n\nFile v1.0.4:references/ota-implementation.md\n\n# K10 HTTP OTA Implementation Guide\n\nScreen refresh rule: if the firmware displays OTA progress, WiFi state, IP addresses, voice state, or other status on the K10 screen, update only changed regions. Do not clear and redraw the whole screen in `loop()` unless full-screen refresh is measured above 30 fps; otherwise the display will visibly flicker.\n\nTTS compatibility rule: K10 speech synthesis is available only in the Chinese firmware. Keeping the `voice_data` partition intact is necessary for compatible TTS builds but does not make `ASR::setAsrSpeed()` / `ASR::speak()` available in English/international firmware.\n\n## Table of Contents\n\n1. [Why HTTP OTA Instead of ArduinoOTA](#why-http-ota)\n2. [Partition Table Requirements](#partition-table)\n3. [Firmware Code Changes](#firmware-code)\n4. [ESP-NOW Projects](#esp-now-projects)\n5. [Build and Upload Workflow](#build-upload)\n6. [OTA Update Workflow](#ota-update)\n7. [Reference: Complete Minimal Example](#minimal-example)\n\n---\n\n## Why HTTP OTA Instead of ArduinoOTA\n\n`ArduinoOTA` uses a UDP-based protocol:\n1. Host sends an authentication challenge to the device (port 3232)\n2. Device verifies password\n3. **Device opens a TCP connection back to the host** on a random port\n4. Host streams the firmware over this reverse connection\n\nStep 3 is the failure point on Windows because:\n- Windows Defender Firewall blocks inbound connections from the ESP32\n- No admin privileges available to add firewall rules\n- `arduino-cli` network upload cannot pass the password non-interactively\n\nHTTP OTA flips the direction:\n- Host opens a TCP connection **to** the device (outbound — always allowed)\n- Host POSTs the firmware as `multipart/form-data`\n- Device receives and writes to flash using the `Update` library\n\n---\n\n## Partition Table Requirements\n\nThe K10 BSP (`UNIHIKER:esp32` v0.0.3) ships with `large_spiffs_16MB.csv`:\n\n```csv\n# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\nfactory,  app,  factory, 0x10000, 0x500000,\nmodel,    data, spiffs,  0x510000,4563k,\nvoice_data,data, fat,    0x985000,2542k,\nfr,       data, ,        0xC01000,100K,\ncoredump, data, coredump,,        1K,\n```\n\n**Problem:** There is no `ota_0` / `ota_1` / `otadata` partition. `Update.begin()` fails immediately because it cannot find an inactive OTA slot to write to.\n\n**Solution for K10 AI projects:** Create `partitions.csv` in your sketch directory with OTA partitions that stop before the model region:\n\n```csv\n# Name,     Type, SubType, Offset,   Size,     Flags\nnvs,        data, nvs,     0x9000,   0x5000,\notadata,    data, ota,     0xe000,   0x2000,\napp0,       app,  ota_0,   0x10000,  0x280000,\napp1,       app,  ota_1,   0x290000, 0x280000,\nmodel,      data, spiffs,  0x510000, 4563K,\nvoice_data, data, fat,     0x985000, 2542K,\nfr,         data, ,        0xC01000, 100K,\ncoredump,   data, coredump,,         1K,\nspiffs,     data, spiffs,  0xC1B000, 0x3E5000,\n```\n\n- `app0` and `app1` are each 2.5 MB and end before `0x510000`\n- `otadata` is required for the bootloader to know which app partition to boot from\n- `model`, `voice_data`, and `fr` keep the factory offsets used by the K10 AI libraries\n- `spiffs` is moved after the model regions\n\n**Compile with the custom partition:**\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\"\n```\n\nThe first USB upload after this change will write the new partition table to flash. This is a one-time operation.\n\nIf the firmware image no longer fits in 2.5 MB, do not expand `app0` or `app1` over the model regions in an AI project. Reduce firmware size, drop OTA, or explicitly decide that the program will not use built-in AI model data.\n\n---\n\n## Optional: Speed Up Repeated Compiles\n\nArduino CLI has its own build cache. The reliable, current configuration keys are under `build_cache.*`:\n\n```bash\n# Linux/macOS\narduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\n```powershell\n# Windows PowerShell\narduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\nFor project-local repeat builds, keep the intermediate build folder stable and let Arduino CLI use all CPU cores:\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0\n```\n\nUse `--clean` only when you need a full rebuild; it deliberately bypasses cached build artifacts.\n\n`ccache` can help in some C/C++ toolchains, but it is not a documented Arduino CLI configuration path in current releases. Avoid treating these as standard Arduino CLI settings:\n\n```bash\narduino-cli config set compiler.cache.enable true\narduino-cli config set compiler.cache.path /path/to/ccache\n```\n\nThe current Arduino CLI command reference also does not list `compile --build-cache-path`, so prefer persistent `build_cache.path` configuration plus a stable `--build-path`.\n\n---\n\n## Firmware Code Changes\n\n### 1. Include Update Library\n\n```cpp\n#include <Update.h>\n```\n\n### 2. Add Upload Handler\n\nUse the **four-argument** `server.on()` overload to register both a final handler and an upload-progress handler:\n\n```cpp\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    scheduleRestart(\"OTA update done\");  // or ESP.restart()\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.printf(\"OTA: %s\\n\", upload.filename.c_str());\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  }\n  else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  }\n  else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n```\n\n### 3. Register the Route\n\n```cpp\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n```\n\n### 4. Restart Scheduling (Recommended)\n\nRestarting immediately inside the handler can cut off the HTTP response. Use a delayed restart:\n\n```cpp\nbool restartPending = false;\nuint32_t restartAtMs = 0;\n\nvoid scheduleRestart(const String &message) {\n  restartPending = true;\n  restartAtMs = millis() + 1200;  // 1.2s delay\n}\n\nvoid loop() {\n  // ... existing loop code ...\n  if (restartPending && millis() >= restartAtMs) {\n    ESP.restart();\n  }\n}\n```\n\n---\n\n## ESP-NOW Projects\n\nHTTP OTA can coexist with ESP-NOW, but not as a pure ESP-NOW transport. The OTA web endpoint needs an IP interface, so an ESP-NOW sketch must temporarily enable AP, STA, or AP+STA networking while accepting the firmware upload.\n\n### Recommended Pattern: OTA Maintenance Mode\n\nUse normal runtime for ESP-NOW. Enter OTA mode only when needed:\n\n- Button long press during boot or runtime\n- Serial command such as `ota`\n- Saved `Preferences` flag set by a previous command\n- Trusted ESP-NOW command from a controller node\n- Local web/admin command if the sketch already has a WebServer\n\nIn OTA mode:\n\n1. Stop or pause periodic ESP-NOW sends.\n2. Start `WIFI_AP` or `WIFI_AP_STA`.\n3. Start the WebServer and register `/ota`.\n4. Call `server.handleClient()` frequently.\n5. Mark `otaUploadActive = true` during upload writes.\n6. Restart after a successful update.\n\nMinimal pattern:\n\n```cpp\n#include <WiFi.h>\n#include <WebServer.h>\n#include <Update.h>\n#include <esp_now.h>\n\nWebServer server(80);\n\nbool otaMode = false;\nbool otaUploadActive = false;\nbool restartPending = false;\nuint32_t restartAtMs = 0;\n\nvoid scheduleRestart() {\n  restartPending = true;\n  restartAtMs = millis() + 1200;\n}\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    scheduleRestart();\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n\n  if (upload.status == UPLOAD_FILE_START) {\n    otaUploadActive = true;\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (!Update.end(true)) {\n      Update.printError(Serial);\n    }\n    otaUploadActive = false;\n  } else if (upload.status == UPLOAD_FILE_ABORTED) {\n    Update.abort();\n    otaUploadActive = false;\n  }\n}\n\nvoid enterOtaMode() {\n  otaMode = true;\n\n  // Prefer an AP fallback so OTA still works when router credentials are wrong.\n  WiFi.mode(WIFI_AP_STA);\n  WiFi.softAP(\"K10-OTA\", \"12345678\");\n\n  // Optional: also connect to infrastructure WiFi.\n  // WiFi.begin(savedSsid, savedPassword);\n\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n\n  Serial.print(\"OTA AP IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  // Example gate: hold a button at boot, read Preferences, or parse Serial.\n  bool requestedOtaMode = false;\n\n  if (requestedOtaMode) {\n    enterOtaMode();\n    return;\n  }\n\n  WiFi.mode(WIFI_STA);\n  // Set channel before esp_now_init() if the deployment uses a fixed ESP-NOW channel.\n  // esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);\n  esp_now_init();\n}\n\nvoid loop() {\n  if (otaMode) {\n    server.handleClient();\n    if (restartPending && millis() >= restartAtMs) {\n      ESP.restart();\n    }\n    return;\n  }\n\n  if (!otaUploadActive) {\n    // Normal ESP-NOW runtime here.\n  }\n}\n```\n\n### Channel Rules\n\nESP-NOW and WiFi share one 2.4 GHz radio:\n\n- If the device is only in `WIFI_STA` and does not connect to a router, set a fixed channel before `esp_now_init()`.\n- If STA connects to a router, the router decides the channel. ESP-NOW peers must use that same channel.\n- Peer channel `0` means \"use the current WiFi channel\" and is useful when the local device follows the AP/STA channel.\n- Avoid hidden channel changes while ESP-NOW peers are active; reconnecting STA may move the radio and break peers on the old channel.\n\nFor K10 OTA work, prefer this practical rule: in normal ESP-NOW mode use a known channel; in OTA maintenance mode pause ESP-NOW and allow AP/STA networking to own the radio.\n\n### Pure ESP-NOW OTA\n\nPure ESP-NOW OTA is possible but should be treated as a separate advanced feature, not the default for this skill. It requires:\n\n- Firmware chunking small enough for ESP-NOW payload limits\n- Sequence numbers and acknowledgements\n- Retry, resume, and timeout handling\n- Image size and checksum validation before boot switch\n- Writes through `Update` or ESP-IDF OTA APIs into the inactive OTA partition\n- A secure authorization model so arbitrary peers cannot flash the device\n\nUse pure ESP-NOW OTA only when the user explicitly needs updates without AP/STA IP networking. Otherwise, use HTTP OTA maintenance mode.\n\n---\n\n## Build and Upload Workflow\n\n### Initial Setup (USB Required)\n\n```bash\n# Compile with custom partition table\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  -j 0 \\\n  --build-property \"build.partitions=custom\"\n\n# Upload via USB (also flashes the new partition table)\narduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .\n```\n\nUse the serial port name for your operating system:\n\n| OS | Example port |\n|----|--------------|\n| Windows | `COM4` |\n| macOS | `/dev/cu.usbmodem1101` |\n| Linux | `/dev/ttyACM0` |\n\n### Subsequent Updates (WiFi OTA)\n\n```bash\n# Compile only\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . --build-path .arduino-build --output-dir build -j 0\n\n# Upload via HTTP with curl\ncurl -F \"file=@build/your_sketch.ino.bin\" http://192.168.9.42/ota\n\n# Or use the cross-platform Python uploader\npython scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42\n```\n\n---\n\n## OTA Update Workflow\n\n1. **Ensure the device is running an OTA-enabled sketch** (has `/ota` endpoint)\n2. **Get the device IP** from the web UI, serial output, or router\n3. **Compile** the new firmware\n4. **POST the `.bin` file** to `http://<ip>/ota`\n5. **Wait for `OK` response** (~5–10 seconds for a 1.1 MB firmware)\n6. **Device restarts automatically** after a short delay\n7. **Verify** by checking the web UI or JSON status endpoint\n\n---\n\n## Reference: Complete Minimal Example\n\n```cpp\n#include <WiFi.h>\n#include <WebServer.h>\n#include <Update.h>\n\nWebServer server(80);\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    delay(100);\n    ESP.restart();\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (!Update.end(true)) {\n      Update.printError(Serial);\n    }\n  }\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.softAP(\"K10-OTA-Test\", \"12345678\");\n\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n\n  Serial.print(\"IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nvoid loop() {\n  server.handleClient();\n}\n```\n\n---\n\n## Common Pitfalls\n\n| Pitfall | Why It Happens |\n|---------|---------------|\n| `Update.begin(size)` with exact size fails | ESP32 flash requires 4 KB alignment. Use `UPDATE_SIZE_UNKNOWN` instead. |\n| `Update.writeStream(server.client())` hangs | `writeStream()` waits for the client to close the connection, but the client waits for the HTTP response. Deadlock. Use chunked `client.read()` with a known `Content-Length` instead. |\n| `server.header(\"Content-Length\")` returns empty | Arduino WebServer stores `Content-Length` in `_clientContentLength`, not the headers map. Use `server.clientContentLength()` (public in ESP32 core). |\n| `FAIL` after full upload | `Update.end(true)` failed. Most common cause: firmware size exceeds OTA partition size. Ensure partition is large enough. |\n\nFile v1.0.4:skill-card.md\n\n## Description:\n\nAdd 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.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[rockets-cn](https://clawhub.ai/user/rockets-cn)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and engineers use this skill to add and maintain HTTP OTA firmware updates for Unihiker K10 Arduino projects, including AP/STA setups and ESP-NOW projects that need a temporary OTA maintenance mode.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The OTA pattern can expose an unauthenticated HTTP firmware update endpoint to anyone with network access.\n\nMitigation: Add per-device authentication, keep OTA mode temporary and locally triggered, and use signed firmware or an equivalent integrity check before using it on a real network.\n\nRisk: Shared default access point credentials can make OTA maintenance mode easier to reach than intended.\n\nMitigation: Avoid shared default AP passwords and provision unique device credentials.\n\nRisk: Changing the K10 partition table can erase local data or damage built-in AI model regions if offsets are wrong.\n\nMitigation: Review the partition table before the first USB flash and keep OTA application partitions from overlapping the model, voice_data, and fr regions.\n\n## Reference(s):\n\n- [K10 HTTP OTA Implementation Guide](references/ota-implementation.md)\n\n## Skill Output:\n\n**Output Type(s):** [Markdown, Code, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown guidance with inline code, command examples, and helper upload scripts]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Includes Python and PowerShell helper upload scripts.]\n\n## Skill Version(s):\n\n1.0.4 (source: server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.0.3: 6 files, 14426 bytes\n\nFiles: references/ota-implementation.md (14475b), scripts/ota_upload.ps1 (2036b), scripts/ota_upload.py (3022b), skill-card.md (2231b), SKILL.md (11239b), _meta.json (135b)\n\nFile v1.0.3:SKILL.md\n\n---\nname: unihiker-k10-ota\ndescription: 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.\n---\n\n# Unihiker K10 - HTTP OTA\n\n## Overview\n\nEnable wireless firmware updates for K10 Arduino projects via HTTP POST.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n## When to Use\n\n- Your K10 is installed in a location difficult to reach with USB\n- You want to update firmware without opening the enclosure\n- You need a scriptable/automated deployment pipeline\n- ArduinoOTA network port upload fails with \"No response from device\"\n\n## Prerequisites\n\n- Existing K10 Arduino project with `WebServer` running\n- `arduino-cli` installed and K10 BSP (`UNIHIKER:esp32:k10`) available\n- Device and computer on the same network (or connected to K10's AP)\n\n## ESP-NOW Compatibility Rule\n\nESP-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.\n\nWhen adding OTA to an ESP-NOW program, use this policy:\n\n1. 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()`.\n2. 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`.\n3. For reliability, pause ESP-NOW sends and time-critical control loops while an OTA upload is active.\n4. 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.\n5. 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.\n\nSee `references/ota-implementation.md` for the ESP-NOW maintenance-mode code pattern.\n\n## Quick Start\n\n### Step 1: Add Custom Partition Table\n\nCreate `partitions.csv` in your sketch directory:\n\n```csv\n# K10 OTA partition table that preserves speech-recognition model regions.\n# Keep model/voice_data/fr offsets aligned with the DFRobot K10 factory table.\n# Name,     Type, SubType, Offset,   Size,     Flags\nnvs,        data, nvs,     0x9000,   0x5000,\notadata,    data, ota,     0xe000,   0x2000,\napp0,       app,  ota_0,   0x10000,  0x280000,\napp1,       app,  ota_1,   0x290000, 0x280000,\nmodel,      data, spiffs,  0x510000, 4563K,\nvoice_data, data, fat,     0x985000, 2542K,\nfr,         data, ,        0xC01000, 100K,\ncoredump,   data, coredump,,         1K,\nspiffs,     data, spiffs,  0xC1B000, 0x3E5000,\n```\n\nCompile with the custom partition:\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\"\n```\n\nOptional speed-up for repeated compiles:\n\n```bash\n# Use all CPU cores and keep build artifacts in stable project-local folders.\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0\n```\n\nArduino CLI already has a built-in `build_cache`. To use a longer-lived cache, configure the official `build_cache.*` keys rather than `compiler.cache.*`:\n\n```bash\narduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\nOn Windows PowerShell:\n\n```powershell\narduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\n### Step 2: Add OTA Endpoint to Firmware\n\nInclude the `Update` library and add a POST handler:\n\n```cpp\n#include <Update.h>\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    ESP.restart();  // or schedule a delayed restart\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n\n// In setup() or startNetwork():\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n```\n\nFor ESP-NOW sketches, do not leave OTA as an afterthought. Add an explicit OTA mode gate:\n\n```cpp\nbool otaMode = false;\nbool otaUploadActive = false;\n\nvoid enterOtaMode() {\n  otaMode = true;\n  WiFi.mode(WIFI_AP_STA);  // AP fallback plus optional STA\n  WiFi.softAP(\"K10-OTA\", \"12345678\");\n  // Optional: WiFi.begin(savedSsid, savedPassword);\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n}\n\nvoid loop() {\n  if (otaMode) {\n    server.handleClient();\n    return;  // keep ESP-NOW/control traffic paused during OTA maintenance\n  }\n\n  // normal ESP-NOW runtime\n}\n```\n\n### Step 3: First USB Upload (Required Once)\n\nThe first upload must be via USB to flash the new partition table:\n\n```bash\narduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .\n```\n\n### Step 4: Update Over WiFi\n\nAfter the first USB upload, use any of these methods:\n\n**curl:**\n```bash\ncurl -F \"file=@build/your_sketch.ino.bin\" http://192.168.9.42/ota\n```\n\n**Python script (works on Windows, macOS, and Linux):**\n```bash\npython scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42\n```\n\n**PowerShell 7+ (works on Windows, macOS, and Linux):**\n```powershell\npwsh ./scripts/ota_upload.ps1 -Bin build/your_sketch.ino.bin -Ip 192.168.9.42\n```\n\n## Important Notes\n\n- **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.\n- **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.\n- **Do not use `delay()` in `loop()` for long periods.** Use non-blocking `millis()` patterns so the WebServer can process the upload request.\n- **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.\n- **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.\n- **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.\n- **AI model regions:** Do not let OTA app partitions overlap `model` at `0x510000`, `voice_data` at `0x985000`, or `fr` at `0xC01000`. A generic large OTA layout can erase AI support data.\n- **TTS compatibility:** TTS is Chinese-firmware-only. OTA preserves or replaces the application image; it must not be described as converting an English/international firmware into a TTS-capable Chinese firmware merely because CN model data remains present.\n- **Model recovery:** If AI functions reboot or model data is suspected damaged, use Mind+ `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.\n- **Display refresh:** Do not clear and redraw the whole K10 screen in `loop()` just to update OTA/WiFi progress. Draw static labels once, then overwrite only changed values or progress areas before one display update call.\n\n## Files\n\n```\nunihiker-k10-ota/\n├── SKILL.md                           # This file\n├── references/\n│   └── ota-implementation.md          # Detailed implementation guide\n└── scripts/\n    ├── ota_upload.py                  # Python OTA uploader\n    └── ota_upload.ps1                 # PowerShell OTA uploader\n```\n\n## Troubleshooting\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| `BEGIN_FAIL` | No OTA partitions in partition table | Add `partitions.csv` with `ota_0` + `ota_1` and reflash via USB |\n| `FAIL` after upload | `Update.write()` failed mid-stream | Check serial log; likely flash write error or insufficient space |\n| `NO_CONTENT` | `Content-Length` header missing | Ensure client sends valid `multipart/form-data` with file data |\n| Device does not restart | `ESP.restart()` called before response sent | Use `scheduleRestart()` with a small delay instead |\n| Network port not found | mDNS/ArduinoOTA not running | HTTP OTA does not need network port detection; use the device's IP directly |\n| ESP-NOW works until STA starts | STA changed the radio channel to the router channel | Put peers on the same channel or use peer channel `0` after STA connects |\n| OTA page unreachable in ESP-NOW sketch | Sketch never entered AP/STA maintenance mode | Add a button/serial/command path that calls `enterOtaMode()` and starts the WebServer |\n| ESP-NOW packets drop during OTA | Flashing and HTTP handling are competing with runtime traffic | Pause ESP-NOW sends/control loops while `otaUploadActive` or `otaMode` is true |\n\nFile v1.0.3:_meta.json\n\n{\n  \"ownerId\": \"kn7bn6y6610bpcvzaykr1h2m1d83e2rm\",\n  \"slug\": \"unihiker-k10-ota\",\n  \"version\": \"1.0.3\",\n  \"publishedAt\": 1784639938376\n}\n\nFile v1.0.3:references/ota-implementation.md\n\n# K10 HTTP OTA Implementation Guide\n\nScreen refresh rule: if the firmware displays OTA progress, WiFi state, IP addresses, voice state, or other status on the K10 screen, update only changed regions. Do not clear and redraw the whole screen in `loop()` unless full-screen refresh is measured above 30 fps; otherwise the display will visibly flicker.\n\nTTS compatibility rule: K10 speech synthesis is available only in the Chinese firmware. Keeping the `voice_data` partition intact is necessary for compatible TTS builds but does not make `ASR::setAsrSpeed()` / `ASR::speak()` available in English/international firmware.\n\n## Table of Contents\n\n1. [Why HTTP OTA Instead of ArduinoOTA](#why-http-ota)\n2. [Partition Table Requirements](#partition-table)\n3. [Firmware Code Changes](#firmware-code)\n4. [ESP-NOW Projects](#esp-now-projects)\n5. [Build and Upload Workflow](#build-upload)\n6. [OTA Update Workflow](#ota-update)\n7. [Reference: Complete Minimal Example](#minimal-example)\n\n---\n\n## Why HTTP OTA Instead of ArduinoOTA\n\n`ArduinoOTA` uses a UDP-based protocol:\n1. Host sends an authentication challenge to the device (port 3232)\n2. Device verifies password\n3. **Device opens a TCP connection back to the host** on a random port\n4. Host streams the firmware over this reverse connection\n\nStep 3 is the failure point on Windows because:\n- Windows Defender Firewall blocks inbound connections from the ESP32\n- No admin privileges available to add firewall rules\n- `arduino-cli` network upload cannot pass the password non-interactively\n\nHTTP OTA flips the direction:\n- Host opens a TCP connection **to** the device (outbound — always allowed)\n- Host POSTs the firmware as `multipart/form-data`\n- Device receives and writes to flash using the `Update` library\n\n---\n\n## Partition Table Requirements\n\nThe K10 BSP (`UNIHIKER:esp32` v0.0.3) ships with `large_spiffs_16MB.csv`:\n\n```csv\n# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\nfactory,  app,  factory, 0x10000, 0x500000,\nmodel,    data, spiffs,  0x510000,4563k,\nvoice_data,data, fat,    0x985000,2542k,\nfr,       data, ,        0xC01000,100K,\ncoredump, data, coredump,,        1K,\n```\n\n**Problem:** There is no `ota_0` / `ota_1` / `otadata` partition. `Update.begin()` fails immediately because it cannot find an inactive OTA slot to write to.\n\n**Solution for K10 AI projects:** Create `partitions.csv` in your sketch directory with OTA partitions that stop before the model region:\n\n```csv\n# Name,     Type, SubType, Offset,   Size,     Flags\nnvs,        data, nvs,     0x9000,   0x5000,\notadata,    data, ota,     0xe000,   0x2000,\napp0,       app,  ota_0,   0x10000,  0x280000,\napp1,       app,  ota_1,   0x290000, 0x280000,\nmodel,      data, spiffs,  0x510000, 4563K,\nvoice_data, data, fat,     0x985000, 2542K,\nfr,         data, ,        0xC01000, 100K,\ncoredump,   data, coredump,,         1K,\nspiffs,     data, spiffs,  0xC1B000, 0x3E5000,\n```\n\n- `app0` and `app1` are each 2.5 MB and end before `0x510000`\n- `otadata` is required for the bootloader to know which app partition to boot from\n- `model`, `voice_data`, and `fr` keep the factory offsets used by the K10 AI libraries\n- `spiffs` is moved after the model regions\n\n**Compile with the custom partition:**\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\"\n```\n\nThe first USB upload after this change will write the new partition table to flash. This is a one-time operation.\n\nIf the firmware image no longer fits in 2.5 MB, do not expand `app0` or `app1` over the model regions in an AI project. Reduce firmware size, drop OTA, or explicitly decide that the program will not use built-in AI model data.\n\n---\n\n## Optional: Speed Up Repeated Compiles\n\nArduino CLI has its own build cache. The reliable, current configuration keys are under `build_cache.*`:\n\n```bash\n# Linux/macOS\narduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\n```powershell\n# Windows PowerShell\narduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\nFor project-local repeat builds, keep the intermediate build folder stable and let Arduino CLI use all CPU cores:\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0\n```\n\nUse `--clean` only when you need a full rebuild; it deliberately bypasses cached build artifacts.\n\n`ccache` can help in some C/C++ toolchains, but it is not a documented Arduino CLI configuration path in current releases. Avoid treating these as standard Arduino CLI settings:\n\n```bash\narduino-cli config set compiler.cache.enable true\narduino-cli config set compiler.cache.path /path/to/ccache\n```\n\nThe current Arduino CLI command reference also does not list `compile --build-cache-path`, so prefer persistent `build_cache.path` configuration plus a stable `--build-path`.\n\n---\n\n## Firmware Code Changes\n\n### 1. Include Update Library\n\n```cpp\n#include <Update.h>\n```\n\n### 2. Add Upload Handler\n\nUse the **four-argument** `server.on()` overload to register both a final handler and an upload-progress handler:\n\n```cpp\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    scheduleRestart(\"OTA update done\");  // or ESP.restart()\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.printf(\"OTA: %s\\n\", upload.filename.c_str());\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  }\n  else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  }\n  else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n```\n\n### 3. Register the Route\n\n```cpp\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n```\n\n### 4. Restart Scheduling (Recommended)\n\nRestarting immediately inside the handler can cut off the HTTP response. Use a delayed restart:\n\n```cpp\nbool restartPending = false;\nuint32_t restartAtMs = 0;\n\nvoid scheduleRestart(const String &message) {\n  restartPending = true;\n  restartAtMs = millis() + 1200;  // 1.2s delay\n}\n\nvoid loop() {\n  // ... existing loop code ...\n  if (restartPending && millis() >= restartAtMs) {\n    ESP.restart();\n  }\n}\n```\n\n---\n\n## ESP-NOW Projects\n\nHTTP OTA can coexist with ESP-NOW, but not as a pure ESP-NOW transport. The OTA web endpoint needs an IP interface, so an ESP-NOW sketch must temporarily enable AP, STA, or AP+STA networking while accepting the firmware upload.\n\n### Recommended Pattern: OTA Maintenance Mode\n\nUse normal runtime for ESP-NOW. Enter OTA mode only when needed:\n\n- Button long press during boot or runtime\n- Serial command such as `ota`\n- Saved `Preferences` flag set by a previous command\n- Trusted ESP-NOW command from a controller node\n- Local web/admin command if the sketch already has a WebServer\n\nIn OTA mode:\n\n1. Stop or pause periodic ESP-NOW sends.\n2. Start `WIFI_AP` or `WIFI_AP_STA`.\n3. Start the WebServer and register `/ota`.\n4. Call `server.handleClient()` frequently.\n5. Mark `otaUploadActive = true` during upload writes.\n6. Restart after a successful update.\n\nMinimal pattern:\n\n```cpp\n#include <WiFi.h>\n#include <WebServer.h>\n#include <Update.h>\n#include <esp_now.h>\n\nWebServer server(80);\n\nbool otaMode = false;\nbool otaUploadActive = false;\nbool restartPending = false;\nuint32_t restartAtMs = 0;\n\nvoid scheduleRestart() {\n  restartPending = true;\n  restartAtMs = millis() + 1200;\n}\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    scheduleRestart();\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n\n  if (upload.status == UPLOAD_FILE_START) {\n    otaUploadActive = true;\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (!Update.end(true)) {\n      Update.printError(Serial);\n    }\n    otaUploadActive = false;\n  } else if (upload.status == UPLOAD_FILE_ABORTED) {\n    Update.abort();\n    otaUploadActive = false;\n  }\n}\n\nvoid enterOtaMode() {\n  otaMode = true;\n\n  // Prefer an AP fallback so OTA still works when router credentials are wrong.\n  WiFi.mode(WIFI_AP_STA);\n  WiFi.softAP(\"K10-OTA\", \"12345678\");\n\n  // Optional: also connect to infrastructure WiFi.\n  // WiFi.begin(savedSsid, savedPassword);\n\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n\n  Serial.print(\"OTA AP IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  // Example gate: hold a button at boot, read Preferences, or parse Serial.\n  bool requestedOtaMode = false;\n\n  if (requestedOtaMode) {\n    enterOtaMode();\n    return;\n  }\n\n  WiFi.mode(WIFI_STA);\n  // Set channel before esp_now_init() if the deployment uses a fixed ESP-NOW channel.\n  // esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);\n  esp_now_init();\n}\n\nvoid loop() {\n  if (otaMode) {\n    server.handleClient();\n    if (restartPending && millis() >= restartAtMs) {\n      ESP.restart();\n    }\n    return;\n  }\n\n  if (!otaUploadActive) {\n    // Normal ESP-NOW runtime here.\n  }\n}\n```\n\n### Channel Rules\n\nESP-NOW and WiFi share one 2.4 GHz radio:\n\n- If the device is only in `WIFI_STA` and does not connect to a router, set a fixed channel before `esp_now_init()`.\n- If STA connects to a router, the router decides the channel. ESP-NOW peers must use that same channel.\n- Peer channel `0` means \"use the current WiFi channel\" and is useful when the local device follows the AP/STA channel.\n- Avoid hidden channel changes while ESP-NOW peers are active; reconnecting STA may move the radio and break peers on the old channel.\n\nFor K10 OTA work, prefer this practical rule: in normal ESP-NOW mode use a known channel; in OTA maintenance mode pause ESP-NOW and allow AP/STA networking to own the radio.\n\n### Pure ESP-NOW OTA\n\nPure ESP-NOW OTA is possible but should be treated as a separate advanced feature, not the default for this skill. It requires:\n\n- Firmware chunking small enough for ESP-NOW payload limits\n- Sequence numbers and acknowledgements\n- Retry, resume, and timeout handling\n- Image size and checksum validation before boot switch\n- Writes through `Update` or ESP-IDF OTA APIs into the inactive OTA partition\n- A secure authorization model so arbitrary peers cannot flash the device\n\nUse pure ESP-NOW OTA only when the user explicitly needs updates without AP/STA IP networking. Otherwise, use HTTP OTA maintenance mode.\n\n---\n\n## Build and Upload Workflow\n\n### Initial Setup (USB Required)\n\n```bash\n# Compile with custom partition table\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  -j 0 \\\n  --build-property \"build.partitions=custom\"\n\n# Upload via USB (also flashes the new partition table)\narduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .\n```\n\nUse the serial port name for your operating system:\n\n| OS | Example port |\n|----|--------------|\n| Windows | `COM4` |\n| macOS | `/dev/cu.usbmodem1101` |\n| Linux | `/dev/ttyACM0` |\n\n### Subsequent Updates (WiFi OTA)\n\n```bash\n# Compile only\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . --build-path .arduino-build --output-dir build -j 0\n\n# Upload via HTTP with curl\ncurl -F \"file=@build/your_sketch.ino.bin\" http://192.168.9.42/ota\n\n# Or use the cross-platform Python uploader\npython scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42\n```\n\n---\n\n## OTA Update Workflow\n\n1. **Ensure the device is running an OTA-enabled sketch** (has `/ota` endpoint)\n2. **Get the device IP** from the web UI, serial output, or router\n3. **Compile** the new firmware\n4. **POST the `.bin` file** to `http://<ip>/ota`\n5. **Wait for `OK` response** (~5–10 seconds for a 1.1 MB firmware)\n6. **Device restarts automatically** after a short delay\n7. **Verify** by checking the web UI or JSON status endpoint\n\n---\n\n## Reference: Complete Minimal Example\n\n```cpp\n#include <WiFi.h>\n#include <WebServer.h>\n#include <Update.h>\n\nWebServer server(80);\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    delay(100);\n    ESP.restart();\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (!Update.end(true)) {\n      Update.printError(Serial);\n    }\n  }\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.softAP(\"K10-OTA-Test\", \"12345678\");\n\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n\n  Serial.print(\"IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nvoid loop() {\n  server.handleClient();\n}\n```\n\n---\n\n## Common Pitfalls\n\n| Pitfall | Why It Happens |\n|---------|---------------|\n| `Update.begin(size)` with exact size fails | ESP32 flash requires 4 KB alignment. Use `UPDATE_SIZE_UNKNOWN` instead. |\n| `Update.writeStream(server.client())` hangs | `writeStream()` waits for the client to close the connection, but the client waits for the HTTP response. Deadlock. Use chunked `client.read()` with a known `Content-Length` instead. |\n| `server.header(\"Content-Length\")` returns empty | Arduino WebServer stores `Content-Length` in `_clientContentLength`, not the headers map. Use `server.clientContentLength()` (public in ESP32 core). |\n| `FAIL` after full upload | `Update.end(true)` failed. Most common cause: firmware size exceeds OTA partition size. Ensure partition is large enough. |\n\nFile v1.0.3:skill-card.md\n\n## Description: <br>\nAdd 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. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[rockets-cn](https://clawhub.ai/user/rockets-cn) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and engineers use this skill to add HTTP firmware upload paths, partition-table guidance, and OTA maintenance-mode patterns to Unihiker K10 Arduino projects, including projects that also use ESP-NOW. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: Unauthenticated OTA endpoints can allow nearby or LAN users to replace device firmware. <br>\nMitigation: Require strong per-device authentication or a physical maintenance-mode gate before accepting firmware uploads. <br>\nRisk: A fixed AP password can expose the maintenance network to unauthorized users. <br>\nMitigation: Replace the example password with unique device credentials and restrict OTA sessions to trusted networks. <br>\nRisk: Firmware accepted without integrity checks can permanently flash unintended or tampered images. <br>\nMitigation: Add firmware integrity or signature checks before flashing updates. <br>\n\n\n## Reference(s): <br>\n- [K10 HTTP OTA Implementation Guide](references/ota-implementation.md) <br>\n- [ClawHub skill page](https://clawhub.ai/rockets-cn/skills/unihiker-k10-ota) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Markdown, Code, Shell commands, Configuration] <br>\n**Output Format:** [Markdown guidance with inline C++, shell, PowerShell, CSV, and Python examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Includes Python and PowerShell uploader scripts for HTTP OTA firmware uploads.] <br>\n\n## Skill Version(s): <br>\n1.0.3 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.0.2: 6 files, 14284 bytes\n\nFiles: references/ota-implementation.md (14204b), scripts/ota_upload.ps1 (2097b), scripts/ota_upload.py (3022b), skill-card.md (2830b), SKILL.md (10659b), _meta.json (135b)\n\nFile v1.0.2:SKILL.md\n\n---\nname: unihiker-k10-ota\ndescription: 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.\n---\n\n# Unihiker K10 - HTTP OTA\n\n## Overview\n\nEnable wireless firmware updates for K10 Arduino projects via HTTP POST.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n## When to Use\n\n- Your K10 is installed in a location difficult to reach with USB\n- You want to update firmware without opening the enclosure\n- You need a scriptable/automated deployment pipeline\n- ArduinoOTA network port upload fails with \"No response from device\"\n\n## Prerequisites\n\n- Existing K10 Arduino project with `WebServer` running\n- `arduino-cli` installed and K10 BSP (`UNIHIKER:esp32:k10`) available\n- Device and computer on the same network (or connected to K10's AP)\n\n## ESP-NOW Compatibility Rule\n\nESP-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.\n\nWhen adding OTA to an ESP-NOW program, use this policy:\n\n1. 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()`.\n2. 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`.\n3. For reliability, pause ESP-NOW sends and time-critical control loops while an OTA upload is active.\n4. 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.\n5. 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.\n\nSee `references/ota-implementation.md` for the ESP-NOW maintenance-mode code pattern.\n\n## Quick Start\n\n### Step 1: Add Custom Partition Table\n\nCreate `partitions.csv` in your sketch directory:\n\n```csv\n# K10 OTA partition table that preserves speech-recognition model regions.\n# Keep model/voice_data/fr offsets aligned with the DFRobot K10 factory table.\n# Name,     Type, SubType, Offset,   Size,     Flags\nnvs,        data, nvs,     0x9000,   0x5000,\notadata,    data, ota,     0xe000,   0x2000,\napp0,       app,  ota_0,   0x10000,  0x280000,\napp1,       app,  ota_1,   0x290000, 0x280000,\nmodel,      data, spiffs,  0x510000, 4563K,\nvoice_data, data, fat,     0x985000, 2542K,\nfr,         data, ,        0xC01000, 100K,\ncoredump,   data, coredump,,         1K,\nspiffs,     data, spiffs,  0xC1B000, 0x3E5000,\n```\n\nCompile with the custom partition:\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\"\n```\n\nOptional speed-up for repeated compiles:\n\n```bash\n# Use all CPU cores and keep build artifacts in stable project-local folders.\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0\n```\n\nArduino CLI already has a built-in `build_cache`. To use a longer-lived cache, configure the official `build_cache.*` keys rather than `compiler.cache.*`:\n\n```bash\narduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\nOn Windows PowerShell:\n\n```powershell\narduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\n### Step 2: Add OTA Endpoint to Firmware\n\nInclude the `Update` library and add a POST handler:\n\n```cpp\n#include <Update.h>\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    ESP.restart();  // or schedule a delayed restart\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n\n// In setup() or startNetwork():\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n```\n\nFor ESP-NOW sketches, do not leave OTA as an afterthought. Add an explicit OTA mode gate:\n\n```cpp\nbool otaMode = false;\nbool otaUploadActive = false;\n\nvoid enterOtaMode() {\n  otaMode = true;\n  WiFi.mode(WIFI_AP_STA);  // AP fallback plus optional STA\n  WiFi.softAP(\"K10-OTA\", \"12345678\");\n  // Optional: WiFi.begin(savedSsid, savedPassword);\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n}\n\nvoid loop() {\n  if (otaMode) {\n    server.handleClient();\n    return;  // keep ESP-NOW/control traffic paused during OTA maintenance\n  }\n\n  // normal ESP-NOW runtime\n}\n```\n\n### Step 3: First USB Upload (Required Once)\n\nThe first upload must be via USB to flash the new partition table:\n\n```bash\narduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .\n```\n\n### Step 4: Update Over WiFi\n\nAfter the first USB upload, use any of these methods:\n\n**curl:**\n```bash\ncurl -F \"file=@build/your_sketch.ino.bin\" http://192.168.9.42/ota\n```\n\n**Python script (works on Windows, macOS, and Linux):**\n```bash\npython scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42\n```\n\n**PowerShell 7+ (works on Windows, macOS, and Linux):**\n```powershell\npwsh ./scripts/ota_upload.ps1 -Bin build/your_sketch.ino.bin -Ip 192.168.9.42\n```\n\n## Important Notes\n\n- **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.\n- **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.\n- **Do not use `delay()` in `loop()` for long periods.** Use non-blocking `millis()` patterns so the WebServer can process the upload request.\n- **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.\n- **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.\n- **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.\n- **AI model regions:** Do not let OTA app partitions overlap `model` at `0x510000`, `voice_data` at `0x985000`, or `fr` at `0xC01000`. A generic large OTA layout can erase AI support data.\n- **Model recovery:** If AI functions reboot or model data is suspected damaged, use Mind+ `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.\n- **Display refresh:** Do not clear and redraw the whole K10 screen in `loop()` just to update OTA/WiFi progress. Draw static labels once, then overwrite only changed values or progress areas before one display update call.\n\n## Files\n\n```\nunihiker-k10-ota/\n├── SKILL.md                           # This file\n├── references/\n│   └── ota-implementation.md          # Detailed implementation guide\n└── scripts/\n    ├── ota_upload.py                  # Python OTA uploader\n    └── ota_upload.ps1                 # PowerShell OTA uploader\n```\n\n## Troubleshooting\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| `BEGIN_FAIL` | No OTA partitions in partition table | Add `partitions.csv` with `ota_0` + `ota_1` and reflash via USB |\n| `FAIL` after upload | `Update.write()` failed mid-stream | Check serial log; likely flash write error or insufficient space |\n| `NO_CONTENT` | `Content-Length` header missing | Ensure client sends valid `multipart/form-data` with file data |\n| Device does not restart | `ESP.restart()` called before response sent | Use `scheduleRestart()` with a small delay instead |\n| Network port not found | mDNS/ArduinoOTA not running | HTTP OTA does not need network port detection; use the device's IP directly |\n| ESP-NOW works until STA starts | STA changed the radio channel to the router channel | Put peers on the same channel or use peer channel `0` after STA connects |\n| OTA page unreachable in ESP-NOW sketch | Sketch never entered AP/STA maintenance mode | Add a button/serial/command path that calls `enterOtaMode()` and starts the WebServer |\n| ESP-NOW packets drop during OTA | Flashing and HTTP handling are competing with runtime traffic | Pause ESP-NOW sends/control loops while `otaUploadActive` or `otaMode` is true |\n\nFile v1.0.2:_meta.json\n\n{\n  \"ownerId\": \"kn7bn6y6610bpcvzaykr1h2m1d83e2rm\",\n  \"slug\": \"unihiker-k10-ota\",\n  \"version\": \"1.0.2\",\n  \"publishedAt\": 1781256061150\n}\n\nFile v1.0.2:references/ota-implementation.md\n\n# K10 HTTP OTA Implementation Guide\n\nScreen refresh rule: if the firmware displays OTA progress, WiFi state, IP addresses, voice state, or other status on the K10 screen, update only changed regions. Do not clear and redraw the whole screen in `loop()` unless full-screen refresh is measured above 30 fps; otherwise the display will visibly flicker.\n\n## Table of Contents\n\n1. [Why HTTP OTA Instead of ArduinoOTA](#why-http-ota)\n2. [Partition Table Requirements](#partition-table)\n3. [Firmware Code Changes](#firmware-code)\n4. [ESP-NOW Projects](#esp-now-projects)\n5. [Build and Upload Workflow](#build-upload)\n6. [OTA Update Workflow](#ota-update)\n7. [Reference: Complete Minimal Example](#minimal-example)\n\n---\n\n## Why HTTP OTA Instead of ArduinoOTA\n\n`ArduinoOTA` uses a UDP-based protocol:\n1. Host sends an authentication challenge to the device (port 3232)\n2. Device verifies password\n3. **Device opens a TCP connection back to the host** on a random port\n4. Host streams the firmware over this reverse connection\n\nStep 3 is the failure point on Windows because:\n- Windows Defender Firewall blocks inbound connections from the ESP32\n- No admin privileges available to add firewall rules\n- `arduino-cli` network upload cannot pass the password non-interactively\n\nHTTP OTA flips the direction:\n- Host opens a TCP connection **to** the device (outbound — always allowed)\n- Host POSTs the firmware as `multipart/form-data`\n- Device receives and writes to flash using the `Update` library\n\n---\n\n## Partition Table Requirements\n\nThe K10 BSP (`UNIHIKER:esp32` v0.0.3) ships with `large_spiffs_16MB.csv`:\n\n```csv\n# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\nfactory,  app,  factory, 0x10000, 0x500000,\nmodel,    data, spiffs,  0x510000,4563k,\nvoice_data,data, fat,    0x985000,2542k,\nfr,       data, ,        0xC01000,100K,\ncoredump, data, coredump,,        1K,\n```\n\n**Problem:** There is no `ota_0` / `ota_1` / `otadata` partition. `Update.begin()` fails immediately because it cannot find an inactive OTA slot to write to.\n\n**Solution for K10 AI projects:** Create `partitions.csv` in your sketch directory with OTA partitions that stop before the model region:\n\n```csv\n# Name,     Type, SubType, Offset,   Size,     Flags\nnvs,        data, nvs,     0x9000,   0x5000,\notadata,    data, ota,     0xe000,   0x2000,\napp0,       app,  ota_0,   0x10000,  0x280000,\napp1,       app,  ota_1,   0x290000, 0x280000,\nmodel,      data, spiffs,  0x510000, 4563K,\nvoice_data, data, fat,     0x985000, 2542K,\nfr,         data, ,        0xC01000, 100K,\ncoredump,   data, coredump,,         1K,\nspiffs,     data, spiffs,  0xC1B000, 0x3E5000,\n```\n\n- `app0` and `app1` are each 2.5 MB and end before `0x510000`\n- `otadata` is required for the bootloader to know which app partition to boot from\n- `model`, `voice_data`, and `fr` keep the factory offsets used by the K10 AI libraries\n- `spiffs` is moved after the model regions\n\n**Compile with the custom partition:**\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\"\n```\n\nThe first USB upload after this change will write the new partition table to flash. This is a one-time operation.\n\nIf the firmware image no longer fits in 2.5 MB, do not expand `app0` or `app1` over the model regions in an AI project. Reduce firmware size, drop OTA, or explicitly decide that the program will not use built-in AI model data.\n\n---\n\n## Optional: Speed Up Repeated Compiles\n\nArduino CLI has its own build cache. The reliable, current configuration keys are under `build_cache.*`:\n\n```bash\n# Linux/macOS\narduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\n```powershell\n# Windows PowerShell\narduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\nFor project-local repeat builds, keep the intermediate build folder stable and let Arduino CLI use all CPU cores:\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0\n```\n\nUse `--clean` only when you need a full rebuild; it deliberately bypasses cached build artifacts.\n\n`ccache` can help in some C/C++ toolchains, but it is not a documented Arduino CLI configuration path in current releases. Avoid treating these as standard Arduino CLI settings:\n\n```bash\narduino-cli config set compiler.cache.enable true\narduino-cli config set compiler.cache.path /path/to/ccache\n```\n\nThe current Arduino CLI command reference also does not list `compile --build-cache-path`, so prefer persistent `build_cache.path` configuration plus a stable `--build-path`.\n\n---\n\n## Firmware Code Changes\n\n### 1. Include Update Library\n\n```cpp\n#include <Update.h>\n```\n\n### 2. Add Upload Handler\n\nUse the **four-argument** `server.on()` overload to register both a final handler and an upload-progress handler:\n\n```cpp\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    scheduleRestart(\"OTA update done\");  // or ESP.restart()\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.printf(\"OTA: %s\\n\", upload.filename.c_str());\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  }\n  else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  }\n  else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n```\n\n### 3. Register the Route\n\n```cpp\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n```\n\n### 4. Restart Scheduling (Recommended)\n\nRestarting immediately inside the handler can cut off the HTTP response. Use a delayed restart:\n\n```cpp\nbool restartPending = false;\nuint32_t restartAtMs = 0;\n\nvoid scheduleRestart(const String &message) {\n  restartPending = true;\n  restartAtMs = millis() + 1200;  // 1.2s delay\n}\n\nvoid loop() {\n  // ... existing loop code ...\n  if (restartPending && millis() >= restartAtMs) {\n    ESP.restart();\n  }\n}\n```\n\n---\n\n## ESP-NOW Projects\n\nHTTP OTA can coexist with ESP-NOW, but not as a pure ESP-NOW transport. The OTA web endpoint needs an IP interface, so an ESP-NOW sketch must temporarily enable AP, STA, or AP+STA networking while accepting the firmware upload.\n\n### Recommended Pattern: OTA Maintenance Mode\n\nUse normal runtime for ESP-NOW. Enter OTA mode only when needed:\n\n- Button long press during boot or runtime\n- Serial command such as `ota`\n- Saved `Preferences` flag set by a previous command\n- Trusted ESP-NOW command from a controller node\n- Local web/admin command if the sketch already has a WebServer\n\nIn OTA mode:\n\n1. Stop or pause periodic ESP-NOW sends.\n2. Start `WIFI_AP` or `WIFI_AP_STA`.\n3. Start the WebServer and register `/ota`.\n4. Call `server.handleClient()` frequently.\n5. Mark `otaUploadActive = true` during upload writes.\n6. Restart after a successful update.\n\nMinimal pattern:\n\n```cpp\n#include <WiFi.h>\n#include <WebServer.h>\n#include <Update.h>\n#include <esp_now.h>\n\nWebServer server(80);\n\nbool otaMode = false;\nbool otaUploadActive = false;\nbool restartPending = false;\nuint32_t restartAtMs = 0;\n\nvoid scheduleRestart() {\n  restartPending = true;\n  restartAtMs = millis() + 1200;\n}\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    scheduleRestart();\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n\n  if (upload.status == UPLOAD_FILE_START) {\n    otaUploadActive = true;\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (!Update.end(true)) {\n      Update.printError(Serial);\n    }\n    otaUploadActive = false;\n  } else if (upload.status == UPLOAD_FILE_ABORTED) {\n    Update.abort();\n    otaUploadActive = false;\n  }\n}\n\nvoid enterOtaMode() {\n  otaMode = true;\n\n  // Prefer an AP fallback so OTA still works when router credentials are wrong.\n  WiFi.mode(WIFI_AP_STA);\n  WiFi.softAP(\"K10-OTA\", \"12345678\");\n\n  // Optional: also connect to infrastructure WiFi.\n  // WiFi.begin(savedSsid, savedPassword);\n\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n\n  Serial.print(\"OTA AP IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  // Example gate: hold a button at boot, read Preferences, or parse Serial.\n  bool requestedOtaMode = false;\n\n  if (requestedOtaMode) {\n    enterOtaMode();\n    return;\n  }\n\n  WiFi.mode(WIFI_STA);\n  // Set channel before esp_now_init() if the deployment uses a fixed ESP-NOW channel.\n  // esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);\n  esp_now_init();\n}\n\nvoid loop() {\n  if (otaMode) {\n    server.handleClient();\n    if (restartPending && millis() >= restartAtMs) {\n      ESP.restart();\n    }\n    return;\n  }\n\n  if (!otaUploadActive) {\n    // Normal ESP-NOW runtime here.\n  }\n}\n```\n\n### Channel Rules\n\nESP-NOW and WiFi share one 2.4 GHz radio:\n\n- If the device is only in `WIFI_STA` and does not connect to a router, set a fixed channel before `esp_now_init()`.\n- If STA connects to a router, the router decides the channel. ESP-NOW peers must use that same channel.\n- Peer channel `0` means \"use the current WiFi channel\" and is useful when the local device follows the AP/STA channel.\n- Avoid hidden channel changes while ESP-NOW peers are active; reconnecting STA may move the radio and break peers on the old channel.\n\nFor K10 OTA work, prefer this practical rule: in normal ESP-NOW mode use a known channel; in OTA maintenance mode pause ESP-NOW and allow AP/STA networking to own the radio.\n\n### Pure ESP-NOW OTA\n\nPure ESP-NOW OTA is possible but should be treated as a separate advanced feature, not the default for this skill. It requires:\n\n- Firmware chunking small enough for ESP-NOW payload limits\n- Sequence numbers and acknowledgements\n- Retry, resume, and timeout handling\n- Image size and checksum validation before boot switch\n- Writes through `Update` or ESP-IDF OTA APIs into the inactive OTA partition\n- A secure authorization model so arbitrary peers cannot flash the device\n\nUse pure ESP-NOW OTA only when the user explicitly needs updates without AP/STA IP networking. Otherwise, use HTTP OTA maintenance mode.\n\n---\n\n## Build and Upload Workflow\n\n### Initial Setup (USB Required)\n\n```bash\n# Compile with custom partition table\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  -j 0 \\\n  --build-property \"build.partitions=custom\"\n\n# Upload via USB (also flashes the new partition table)\narduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .\n```\n\nUse the serial port name for your operating system:\n\n| OS | Example port |\n|----|--------------|\n| Windows | `COM4` |\n| macOS | `/dev/cu.usbmodem1101` |\n| Linux | `/dev/ttyACM0` |\n\n### Subsequent Updates (WiFi OTA)\n\n```bash\n# Compile only\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . --build-path .arduino-build --output-dir build -j 0\n\n# Upload via HTTP with curl\ncurl -F \"file=@build/your_sketch.ino.bin\" http://192.168.9.42/ota\n\n# Or use the cross-platform Python uploader\npython scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42\n```\n\n---\n\n## OTA Update Workflow\n\n1. **Ensure the device is running an OTA-enabled sketch** (has `/ota` endpoint)\n2. **Get the device IP** from the web UI, serial output, or router\n3. **Compile** the new firmware\n4. **POST the `.bin` file** to `http://<ip>/ota`\n5. **Wait for `OK` response** (~5–10 seconds for a 1.1 MB firmware)\n6. **Device restarts automatically** after a short delay\n7. **Verify** by checking the web UI or JSON status endpoint\n\n---\n\n## Reference: Complete Minimal Example\n\n```cpp\n#include <WiFi.h>\n#include <WebServer.h>\n#include <Update.h>\n\nWebServer server(80);\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    delay(100);\n    ESP.restart();\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (!Update.end(true)) {\n      Update.printError(Serial);\n    }\n  }\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.softAP(\"K10-OTA-Test\", \"12345678\");\n\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n\n  Serial.print(\"IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nvoid loop() {\n  server.handleClient();\n}\n```\n\n---\n\n## Common Pitfalls\n\n| Pitfall | Why It Happens |\n|---------|---------------|\n| `Update.begin(size)` with exact size fails | ESP32 flash requires 4 KB alignment. Use `UPDATE_SIZE_UNKNOWN` instead. |\n| `Update.writeStream(server.client())` hangs | `writeStream()` waits for the client to close the connection, but the client waits for the HTTP response. Deadlock. Use chunked `client.read()` with a known `Content-Length` instead. |\n| `server.header(\"Content-Length\")` returns empty | Arduino WebServer stores `Content-Length` in `_clientContentLength`, not the headers map. Use `server.clientContentLength()` (public in ESP32 core). |\n| `FAIL` after full upload | `Update.end(true)` failed. Most common cause: firmware size exceeds OTA partition size. Ensure partition is large enough. |\n\nFile v1.0.2:skill-card.md\n\n## Description: <br>\nAdd 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. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[rockets-cn](https://clawhub.ai/user/rockets-cn) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and engineers use this skill to add scriptable HTTP firmware updates to Unihiker K10 Arduino projects without relying on USB or ArduinoOTA. It is especially relevant for AP/STA deployments and ESP-NOW projects that need an explicit OTA maintenance mode. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: Copyable examples expose firmware reflashing over plain HTTP with weak default Wi-Fi credentials and no access control. <br>\nMitigation: Use a unique per-device password or token, require an explicit short-lived maintenance mode such as a button press, and avoid exposing the OTA endpoint on untrusted networks. <br>\nRisk: Firmware uploads may be accepted without integrity validation. <br>\nMitigation: Validate firmware signatures or hashes before applying OTA updates. <br>\nRisk: ESP-NOW or time-critical control behavior can conflict with Wi-Fi networking and flash writes during OTA. <br>\nMitigation: Enter a dedicated OTA maintenance mode, align Wi-Fi and ESP-NOW channels when needed, and pause ESP-NOW sends and control loops while upload is active. <br>\nRisk: An incorrect partition table can overwrite K10 AI model regions or fail because no OTA partition is available. <br>\nMitigation: Use OTA app partitions that end before the documented model, voice_data, and fr regions, and perform the first partition-table change by USB. <br>\n\n\n## Reference(s): <br>\n- [K10 HTTP OTA Implementation Guide](references/ota-implementation.md) <br>\n- [ClawHub skill page](https://clawhub.ai/rockets-cn/skills/unihiker-k10-ota) <br>\n- [Publisher profile](https://clawhub.ai/user/rockets-cn) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [guidance, markdown, code, shell commands, configuration] <br>\n**Output Format:** [Markdown with inline C++, CSV, bash, PowerShell, and Python examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Produces firmware integration guidance, partition table configuration, OTA endpoint code, and upload commands for Unihiker K10 Arduino projects.] <br>\n\n## Skill Version(s): <br>\n1.0.2 (source: server release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.0.1: 6 files, 13879 bytes\n\nFiles: references/ota-implementation.md (13890b), scripts/ota_upload.ps1 (2097b), scripts/ota_upload.py (3022b), skill-card.md (2767b), SKILL.md (10114b), _meta.json (135b)\n\nFile v1.0.1:SKILL.md\n\n---\nname: unihiker-k10-ota\ndescription: 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.\n---\n\n# Unihiker K10 - HTTP OTA\n\n## Overview\n\nEnable wireless firmware updates for K10 Arduino projects via HTTP POST.\n\n**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.\n\n**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.\n\n**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.\n\n## When to Use\n\n- Your K10 is installed in a location difficult to reach with USB\n- You want to update firmware without opening the enclosure\n- You need a scriptable/automated deployment pipeline\n- ArduinoOTA network port upload fails with \"No response from device\"\n\n## Prerequisites\n\n- Existing K10 Arduino project with `WebServer` running\n- `arduino-cli` installed and K10 BSP (`UNIHIKER:esp32:k10`) available\n- Device and computer on the same network (or connected to K10's AP)\n\n## ESP-NOW Compatibility Rule\n\nESP-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.\n\nWhen adding OTA to an ESP-NOW program, use this policy:\n\n1. 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()`.\n2. 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`.\n3. For reliability, pause ESP-NOW sends and time-critical control loops while an OTA upload is active.\n4. 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.\n5. 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.\n\nSee `references/ota-implementation.md` for the ESP-NOW maintenance-mode code pattern.\n\n## Quick Start\n\n### Step 1: Add Custom Partition Table\n\nCreate `partitions.csv` in your sketch directory:\n\n```csv\n# K10 OTA partition table that preserves speech-recognition model regions.\n# Keep model/voice_data/fr offsets aligned with the DFRobot K10 factory table.\n# Name,     Type, SubType, Offset,   Size,     Flags\nnvs,        data, nvs,     0x9000,   0x5000,\notadata,    data, ota,     0xe000,   0x2000,\napp0,       app,  ota_0,   0x10000,  0x280000,\napp1,       app,  ota_1,   0x290000, 0x280000,\nmodel,      data, spiffs,  0x510000, 4563K,\nvoice_data, data, fat,     0x985000, 2542K,\nfr,         data, ,        0xC01000, 100K,\ncoredump,   data, coredump,,         1K,\nspiffs,     data, spiffs,  0xC1B000, 0x3E5000,\n```\n\nCompile with the custom partition:\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\"\n```\n\nOptional speed-up for repeated compiles:\n\n```bash\n# Use all CPU cores and keep build artifacts in stable project-local folders.\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0\n```\n\nArduino CLI already has a built-in `build_cache`. To use a longer-lived cache, configure the official `build_cache.*` keys rather than `compiler.cache.*`:\n\n```bash\narduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\nOn Windows PowerShell:\n\n```powershell\narduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\n### Step 2: Add OTA Endpoint to Firmware\n\nInclude the `Update` library and add a POST handler:\n\n```cpp\n#include <Update.h>\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    ESP.restart();  // or schedule a delayed restart\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n\n// In setup() or startNetwork():\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n```\n\nFor ESP-NOW sketches, do not leave OTA as an afterthought. Add an explicit OTA mode gate:\n\n```cpp\nbool otaMode = false;\nbool otaUploadActive = false;\n\nvoid enterOtaMode() {\n  otaMode = true;\n  WiFi.mode(WIFI_AP_STA);  // AP fallback plus optional STA\n  WiFi.softAP(\"K10-OTA\", \"12345678\");\n  // Optional: WiFi.begin(savedSsid, savedPassword);\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n}\n\nvoid loop() {\n  if (otaMode) {\n    server.handleClient();\n    return;  // keep ESP-NOW/control traffic paused during OTA maintenance\n  }\n\n  // normal ESP-NOW runtime\n}\n```\n\n### Step 3: First USB Upload (Required Once)\n\nThe first upload must be via USB to flash the new partition table:\n\n```bash\narduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .\n```\n\n### Step 4: Update Over WiFi\n\nAfter the first USB upload, use any of these methods:\n\n**curl:**\n```bash\ncurl -F \"file=@build/your_sketch.ino.bin\" http://192.168.9.42/ota\n```\n\n**Python script (works on Windows, macOS, and Linux):**\n```bash\npython scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42\n```\n\n**PowerShell 7+ (works on Windows, macOS, and Linux):**\n```powershell\npwsh ./scripts/ota_upload.ps1 -Bin build/your_sketch.ino.bin -Ip 192.168.9.42\n```\n\n## Important Notes\n\n- **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.\n- **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.\n- **Do not use `delay()` in `loop()` for long periods.** Use non-blocking `millis()` patterns so the WebServer can process the upload request.\n- **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.\n- **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.\n- **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.\n- **AI model regions:** Do not let OTA app partitions overlap `model` at `0x510000`, `voice_data` at `0x985000`, or `fr` at `0xC01000`. A generic large OTA layout can erase AI support data.\n- **Model recovery:** If AI functions reboot or model data is suspected damaged, use Mind+ `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.\n\n## Files\n\n```\nunihiker-k10-ota/\n├── SKILL.md                           # This file\n├── references/\n│   └── ota-implementation.md          # Detailed implementation guide\n└── scripts/\n    ├── ota_upload.py                  # Python OTA uploader\n    └── ota_upload.ps1                 # PowerShell OTA uploader\n```\n\n## Troubleshooting\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| `BEGIN_FAIL` | No OTA partitions in partition table | Add `partitions.csv` with `ota_0` + `ota_1` and reflash via USB |\n| `FAIL` after upload | `Update.write()` failed mid-stream | Check serial log; likely flash write error or insufficient space |\n| `NO_CONTENT` | `Content-Length` header missing | Ensure client sends valid `multipart/form-data` with file data |\n| Device does not restart | `ESP.restart()` called before response sent | Use `scheduleRestart()` with a small delay instead |\n| Network port not found | mDNS/ArduinoOTA not running | HTTP OTA does not need network port detection; use the device's IP directly |\n| ESP-NOW works until STA starts | STA changed the radio channel to the router channel | Put peers on the same channel or use peer channel `0` after STA connects |\n| OTA page unreachable in ESP-NOW sketch | Sketch never entered AP/STA maintenance mode | Add a button/serial/command path that calls `enterOtaMode()` and starts the WebServer |\n| ESP-NOW packets drop during OTA | Flashing and HTTP handling are competing with runtime traffic | Pause ESP-NOW sends/control loops while `otaUploadActive` or `otaMode` is true |\n\nFile v1.0.1:_meta.json\n\n{\n  \"ownerId\": \"kn7bn6y6610bpcvzaykr1h2m1d83e2rm\",\n  \"slug\": \"unihiker-k10-ota\",\n  \"version\": \"1.0.1\",\n  \"publishedAt\": 1781253740121\n}\n\nFile v1.0.1:references/ota-implementation.md\n\n# K10 HTTP OTA Implementation Guide\n\n## Table of Contents\n\n1. [Why HTTP OTA Instead of ArduinoOTA](#why-http-ota)\n2. [Partition Table Requirements](#partition-table)\n3. [Firmware Code Changes](#firmware-code)\n4. [ESP-NOW Projects](#esp-now-projects)\n5. [Build and Upload Workflow](#build-upload)\n6. [OTA Update Workflow](#ota-update)\n7. [Reference: Complete Minimal Example](#minimal-example)\n\n---\n\n## Why HTTP OTA Instead of ArduinoOTA\n\n`ArduinoOTA` uses a UDP-based protocol:\n1. Host sends an authentication challenge to the device (port 3232)\n2. Device verifies password\n3. **Device opens a TCP connection back to the host** on a random port\n4. Host streams the firmware over this reverse connection\n\nStep 3 is the failure point on Windows because:\n- Windows Defender Firewall blocks inbound connections from the ESP32\n- No admin privileges available to add firewall rules\n- `arduino-cli` network upload cannot pass the password non-interactively\n\nHTTP OTA flips the direction:\n- Host opens a TCP connection **to** the device (outbound — always allowed)\n- Host POSTs the firmware as `multipart/form-data`\n- Device receives and writes to flash using the `Update` library\n\n---\n\n## Partition Table Requirements\n\nThe K10 BSP (`UNIHIKER:esp32` v0.0.3) ships with `large_spiffs_16MB.csv`:\n\n```csv\n# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\nfactory,  app,  factory, 0x10000, 0x500000,\nmodel,    data, spiffs,  0x510000,4563k,\nvoice_data,data, fat,    0x985000,2542k,\nfr,       data, ,        0xC01000,100K,\ncoredump, data, coredump,,        1K,\n```\n\n**Problem:** There is no `ota_0` / `ota_1` / `otadata` partition. `Update.begin()` fails immediately because it cannot find an inactive OTA slot to write to.\n\n**Solution for K10 AI projects:** Create `partitions.csv` in your sketch directory with OTA partitions that stop before the model region:\n\n```csv\n# Name,     Type, SubType, Offset,   Size,     Flags\nnvs,        data, nvs,     0x9000,   0x5000,\notadata,    data, ota,     0xe000,   0x2000,\napp0,       app,  ota_0,   0x10000,  0x280000,\napp1,       app,  ota_1,   0x290000, 0x280000,\nmodel,      data, spiffs,  0x510000, 4563K,\nvoice_data, data, fat,     0x985000, 2542K,\nfr,         data, ,        0xC01000, 100K,\ncoredump,   data, coredump,,         1K,\nspiffs,     data, spiffs,  0xC1B000, 0x3E5000,\n```\n\n- `app0` and `app1` are each 2.5 MB and end before `0x510000`\n- `otadata` is required for the bootloader to know which app partition to boot from\n- `model`, `voice_data`, and `fr` keep the factory offsets used by the K10 AI libraries\n- `spiffs` is moved after the model regions\n\n**Compile with the custom partition:**\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\"\n```\n\nThe first USB upload after this change will write the new partition table to flash. This is a one-time operation.\n\nIf the firmware image no longer fits in 2.5 MB, do not expand `app0` or `app1` over the model regions in an AI project. Reduce firmware size, drop OTA, or explicitly decide that the program will not use built-in AI model data.\n\n---\n\n## Optional: Speed Up Repeated Compiles\n\nArduino CLI has its own build cache. The reliable, current configuration keys are under `build_cache.*`:\n\n```bash\n# Linux/macOS\narduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\n```powershell\n# Windows PowerShell\narduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\nFor project-local repeat builds, keep the intermediate build folder stable and let Arduino CLI use all CPU cores:\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0\n```\n\nUse `--clean` only when you need a full rebuild; it deliberately bypasses cached build artifacts.\n\n`ccache` can help in some C/C++ toolchains, but it is not a documented Arduino CLI configuration path in current releases. Avoid treating these as standard Arduino CLI settings:\n\n```bash\narduino-cli config set compiler.cache.enable true\narduino-cli config set compiler.cache.path /path/to/ccache\n```\n\nThe current Arduino CLI command reference also does not list `compile --build-cache-path`, so prefer persistent `build_cache.path` configuration plus a stable `--build-path`.\n\n---\n\n## Firmware Code Changes\n\n### 1. Include Update Library\n\n```cpp\n#include <Update.h>\n```\n\n### 2. Add Upload Handler\n\nUse the **four-argument** `server.on()` overload to register both a final handler and an upload-progress handler:\n\n```cpp\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    scheduleRestart(\"OTA update done\");  // or ESP.restart()\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.printf(\"OTA: %s\\n\", upload.filename.c_str());\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  }\n  else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  }\n  else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n```\n\n### 3. Register the Route\n\n```cpp\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n```\n\n### 4. Restart Scheduling (Recommended)\n\nRestarting immediately inside the handler can cut off the HTTP response. Use a delayed restart:\n\n```cpp\nbool restartPending = false;\nuint32_t restartAtMs = 0;\n\nvoid scheduleRestart(const String &message) {\n  restartPending = true;\n  restartAtMs = millis() + 1200;  // 1.2s delay\n}\n\nvoid loop() {\n  // ... existing loop code ...\n  if (restartPending && millis() >= restartAtMs) {\n    ESP.restart();\n  }\n}\n```\n\n---\n\n## ESP-NOW Projects\n\nHTTP OTA can coexist with ESP-NOW, but not as a pure ESP-NOW transport. The OTA web endpoint needs an IP interface, so an ESP-NOW sketch must temporarily enable AP, STA, or AP+STA networking while accepting the firmware upload.\n\n### Recommended Pattern: OTA Maintenance Mode\n\nUse normal runtime for ESP-NOW. Enter OTA mode only when needed:\n\n- Button long press during boot or runtime\n- Serial command such as `ota`\n- Saved `Preferences` flag set by a previous command\n- Trusted ESP-NOW command from a controller node\n- Local web/admin command if the sketch already has a WebServer\n\nIn OTA mode:\n\n1. Stop or pause periodic ESP-NOW sends.\n2. Start `WIFI_AP` or `WIFI_AP_STA`.\n3. Start the WebServer and register `/ota`.\n4. Call `server.handleClient()` frequently.\n5. Mark `otaUploadActive = true` during upload writes.\n6. Restart after a successful update.\n\nMinimal pattern:\n\n```cpp\n#include <WiFi.h>\n#include <WebServer.h>\n#include <Update.h>\n#include <esp_now.h>\n\nWebServer server(80);\n\nbool otaMode = false;\nbool otaUploadActive = false;\nbool restartPending = false;\nuint32_t restartAtMs = 0;\n\nvoid scheduleRestart() {\n  restartPending = true;\n  restartAtMs = millis() + 1200;\n}\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    scheduleRestart();\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n\n  if (upload.status == UPLOAD_FILE_START) {\n    otaUploadActive = true;\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (!Update.end(true)) {\n      Update.printError(Serial);\n    }\n    otaUploadActive = false;\n  } else if (upload.status == UPLOAD_FILE_ABORTED) {\n    Update.abort();\n    otaUploadActive = false;\n  }\n}\n\nvoid enterOtaMode() {\n  otaMode = true;\n\n  // Prefer an AP fallback so OTA still works when router credentials are wrong.\n  WiFi.mode(WIFI_AP_STA);\n  WiFi.softAP(\"K10-OTA\", \"12345678\");\n\n  // Optional: also connect to infrastructure WiFi.\n  // WiFi.begin(savedSsid, savedPassword);\n\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n\n  Serial.print(\"OTA AP IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  // Example gate: hold a button at boot, read Preferences, or parse Serial.\n  bool requestedOtaMode = false;\n\n  if (requestedOtaMode) {\n    enterOtaMode();\n    return;\n  }\n\n  WiFi.mode(WIFI_STA);\n  // Set channel before esp_now_init() if the deployment uses a fixed ESP-NOW channel.\n  // esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);\n  esp_now_init();\n}\n\nvoid loop() {\n  if (otaMode) {\n    server.handleClient();\n    if (restartPending && millis() >= restartAtMs) {\n      ESP.restart();\n    }\n    return;\n  }\n\n  if (!otaUploadActive) {\n    // Normal ESP-NOW runtime here.\n  }\n}\n```\n\n### Channel Rules\n\nESP-NOW and WiFi share one 2.4 GHz radio:\n\n- If the device is only in `WIFI_STA` and does not connect to a router, set a fixed channel before `esp_now_init()`.\n- If STA connects to a router, the router decides the channel. ESP-NOW peers must use that same channel.\n- Peer channel `0` means \"use the current WiFi channel\" and is useful when the local device follows the AP/STA channel.\n- Avoid hidden channel changes while ESP-NOW peers are active; reconnecting STA may move the radio and break peers on the old channel.\n\nFor K10 OTA work, prefer this practical rule: in normal ESP-NOW mode use a known channel; in OTA maintenance mode pause ESP-NOW and allow AP/STA networking to own the radio.\n\n### Pure ESP-NOW OTA\n\nPure ESP-NOW OTA is possible but should be treated as a separate advanced feature, not the default for this skill. It requires:\n\n- Firmware chunking small enough for ESP-NOW payload limits\n- Sequence numbers and acknowledgements\n- Retry, resume, and timeout handling\n- Image size and checksum validation before boot switch\n- Writes through `Update` or ESP-IDF OTA APIs into the inactive OTA partition\n- A secure authorization model so arbitrary peers cannot flash the device\n\nUse pure ESP-NOW OTA only when the user explicitly needs updates without AP/STA IP networking. Otherwise, use HTTP OTA maintenance mode.\n\n---\n\n## Build and Upload Workflow\n\n### Initial Setup (USB Required)\n\n```bash\n# Compile with custom partition table\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  -j 0 \\\n  --build-property \"build.partitions=custom\"\n\n# Upload via USB (also flashes the new partition table)\narduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .\n```\n\nUse the serial port name for your operating system:\n\n| OS | Example port |\n|----|--------------|\n| Windows | `COM4` |\n| macOS | `/dev/cu.usbmodem1101` |\n| Linux | `/dev/ttyACM0` |\n\n### Subsequent Updates (WiFi OTA)\n\n```bash\n# Compile only\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . --build-path .arduino-build --output-dir build -j 0\n\n# Upload via HTTP with curl\ncurl -F \"file=@build/your_sketch.ino.bin\" http://192.168.9.42/ota\n\n# Or use the cross-platform Python uploader\npython scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42\n```\n\n---\n\n## OTA Update Workflow\n\n1. **Ensure the device is running an OTA-enabled sketch** (has `/ota` endpoint)\n2. **Get the device IP** from the web UI, serial output, or router\n3. **Compile** the new firmware\n4. **POST the `.bin` file** to `http://<ip>/ota`\n5. **Wait for `OK` response** (~5–10 seconds for a 1.1 MB firmware)\n6. **Device restarts automatically** after a short delay\n7. **Verify** by checking the web UI or JSON status endpoint\n\n---\n\n## Reference: Complete Minimal Example\n\n```cpp\n#include <WiFi.h>\n#include <WebServer.h>\n#include <Update.h>\n\nWebServer server(80);\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    delay(100);\n    ESP.restart();\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (!Update.end(true)) {\n      Update.printError(Serial);\n    }\n  }\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.softAP(\"K10-OTA-Test\", \"12345678\");\n\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n\n  Serial.print(\"IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nvoid loop() {\n  server.handleClient();\n}\n```\n\n---\n\n## Common Pitfalls\n\n| Pitfall | Why It Happens |\n|---------|---------------|\n| `Update.begin(size)` with exact size fails | ESP32 flash requires 4 KB alignment. Use `UPDATE_SIZE_UNKNOWN` instead. |\n| `Update.writeStream(server.client())` hangs | `writeStream()` waits for the client to close the connection, but the client waits for the HTTP response. Deadlock. Use chunked `client.read()` with a known `Content-Length` instead. |\n| `server.header(\"Content-Length\")` returns empty | Arduino WebServer stores `Content-Length` in `_clientContentLength`, not the headers map. Use `server.clientContentLength()` (public in ESP32 core). |\n| `FAIL` after full upload | `Update.end(true)` failed. Most common cause: firmware size exceeds OTA partition size. Ensure partition is large enough. |\n\nFile v1.0.1:skill-card.md\n\n## Description: <br>\nAdd 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. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[rockets-cn](https://clawhub.ai/user/rockets-cn) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and embedded engineers use this skill to add HTTP firmware update paths to Unihiker K10 Arduino projects, including AP/STA projects and ESP-NOW projects that need a maintenance OTA mode. It helps plan the required partition table, firmware endpoint, upload workflow, and recovery considerations for model-preserving K10 deployments. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The example OTA setup can allow anyone who reaches the device to upload persistent firmware without authentication or integrity checks. <br>\nMitigation: Before using it beyond a private lab, add per-device authentication, unique AP credentials, network isolation, a deliberate short maintenance window, and signed or otherwise verified firmware images. <br>\nRisk: Exposing the /ota endpoint on an untrusted LAN or long-lived access point can create an embedded-device takeover path. <br>\nMitigation: Keep OTA access on trusted networks only, disable or gate OTA outside maintenance mode, and avoid leaving the access point available longer than needed. <br>\nRisk: Changing the K10 partition table can erase stored data or damage built-in AI model regions if the OTA partitions overlap factory model offsets. <br>\nMitigation: Use the model-preserving partition layout from the skill evidence, perform the first partition-table change by USB, and verify OTA app partitions end before the documented model regions. <br>\n\n\n## Reference(s): <br>\n- [K10 HTTP OTA Implementation Guide](references/ota-implementation.md) <br>\n- [ClawHub skill page](https://clawhub.ai/rockets-cn/unihiker-k10-ota) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [guidance, markdown, code, shell commands, configuration] <br>\n**Output Format:** [Markdown with inline code blocks and command examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Includes optional Python and PowerShell helper commands for uploading firmware to a device OTA endpoint.] <br>\n\n## Skill Version(s): <br>\n1.0.1 (source: server-resolved release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.0.0: 5 files, 12117 bytes\n\nFiles: references/ota-implementation.md (13384b), scripts/ota_upload.py (3022b), skill-card.md (2477b), SKILL.md (9076b), _meta.json (135b)\n\nFile v1.0.0:SKILL.md\n\n---\nname: unihiker-k10-ota\ndescription: 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.\n---\n\n# Unihiker K10 - HTTP OTA\n\n## Overview\n\nEnable wireless firmware updates for K10 Arduino projects via HTTP POST.\n\n**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.\n\n**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.\n\n## When to Use\n\n- Your K10 is installed in a location difficult to reach with USB\n- You want to update firmware without opening the enclosure\n- You need a scriptable/automated deployment pipeline\n- ArduinoOTA network port upload fails with \"No response from device\"\n\n## Prerequisites\n\n- Existing K10 Arduino project with `WebServer` running\n- `arduino-cli` installed and K10 BSP (`UNIHIKER:esp32:k10`) available\n- Device and computer on the same network (or connected to K10's AP)\n\n## ESP-NOW Compatibility Rule\n\nESP-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.\n\nWhen adding OTA to an ESP-NOW program, use this policy:\n\n1. 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()`.\n2. 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`.\n3. For reliability, pause ESP-NOW sends and time-critical control loops while an OTA upload is active.\n4. 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.\n5. 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.\n\nSee `references/ota-implementation.md` for the ESP-NOW maintenance-mode code pattern.\n\n## Quick Start\n\n### Step 1: Add Custom Partition Table\n\nCreate `partitions.csv` in your sketch directory:\n\n```csv\n# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x640000,\napp1,     app,  ota_1,   0x650000,0x640000,\nspiffs,   data, spiffs,  0xc90000,0x370000,\n```\n\nCompile with the custom partition:\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\"\n```\n\nOptional speed-up for repeated compiles:\n\n```bash\n# Use all CPU cores and keep build artifacts in stable project-local folders.\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0\n```\n\nArduino CLI already has a built-in `build_cache`. To use a longer-lived cache, configure the official `build_cache.*` keys rather than `compiler.cache.*`:\n\n```bash\narduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\nOn Windows PowerShell:\n\n```powershell\narduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\n### Step 2: Add OTA Endpoint to Firmware\n\nInclude the `Update` library and add a POST handler:\n\n```cpp\n#include <Update.h>\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    ESP.restart();  // or schedule a delayed restart\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n\n// In setup() or startNetwork():\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n```\n\nFor ESP-NOW sketches, do not leave OTA as an afterthought. Add an explicit OTA mode gate:\n\n```cpp\nbool otaMode = false;\nbool otaUploadActive = false;\n\nvoid enterOtaMode() {\n  otaMode = true;\n  WiFi.mode(WIFI_AP_STA);  // AP fallback plus optional STA\n  WiFi.softAP(\"K10-OTA\", \"12345678\");\n  // Optional: WiFi.begin(savedSsid, savedPassword);\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n}\n\nvoid loop() {\n  if (otaMode) {\n    server.handleClient();\n    return;  // keep ESP-NOW/control traffic paused during OTA maintenance\n  }\n\n  // normal ESP-NOW runtime\n}\n```\n\n### Step 3: First USB Upload (Required Once)\n\nThe first upload must be via USB to flash the new partition table:\n\n```bash\narduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .\n```\n\n### Step 4: Update Over WiFi\n\nAfter the first USB upload, use any of these methods:\n\n**curl:**\n```bash\ncurl -F \"file=@build/your_sketch.ino.bin\" http://192.168.9.42/ota\n```\n\n**Python script (works on Windows, macOS, and Linux):**\n```bash\npython scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42\n```\n\n**PowerShell 7+ (works on Windows, macOS, and Linux):**\n```powershell\npwsh ./scripts/ota_upload.ps1 -Bin build/your_sketch.ino.bin -Ip 192.168.9.42\n```\n\n## Important Notes\n\n- **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.\n- **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.\n- **Do not use `delay()` in `loop()` for long periods.** Use non-blocking `millis()` patterns so the WebServer can process the upload request.\n- **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.\n- **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.\n- **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.\n\n## Files\n\n```\nunihiker-k10-ota/\n├── SKILL.md                           # This file\n├── references/\n│   └── ota-implementation.md          # Detailed implementation guide\n└── scripts/\n    ├── ota_upload.py                  # Python OTA uploader\n    └── ota_upload.ps1                 # PowerShell OTA uploader\n```\n\n## Troubleshooting\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| `BEGIN_FAIL` | No OTA partitions in partition table | Add `partitions.csv` with `ota_0` + `ota_1` and reflash via USB |\n| `FAIL` after upload | `Update.write()` failed mid-stream | Check serial log; likely flash write error or insufficient space |\n| `NO_CONTENT` | `Content-Length` header missing | Ensure client sends valid `multipart/form-data` with file data |\n| Device does not restart | `ESP.restart()` called before response sent | Use `scheduleRestart()` with a small delay instead |\n| Network port not found | mDNS/ArduinoOTA not running | HTTP OTA does not need network port detection; use the device's IP directly |\n| ESP-NOW works until STA starts | STA changed the radio channel to the router channel | Put peers on the same channel or use peer channel `0` after STA connects |\n| OTA page unreachable in ESP-NOW sketch | Sketch never entered AP/STA maintenance mode | Add a button/serial/command path that calls `enterOtaMode()` and starts the WebServer |\n| ESP-NOW packets drop during OTA | Flashing and HTTP handling are competing with runtime traffic | Pause ESP-NOW sends/control loops while `otaUploadActive` or `otaMode` is true |\n\nFile v1.0.0:_meta.json\n\n{\n  \"ownerId\": \"kn7bn6y6610bpcvzaykr1h2m1d83e2rm\",\n  \"slug\": \"unihiker-k10-ota\",\n  \"version\": \"1.0.0\",\n  \"publishedAt\": 1781061896359\n}\n\nFile v1.0.0:references/ota-implementation.md\n\n# K10 HTTP OTA Implementation Guide\n\n## Table of Contents\n\n1. [Why HTTP OTA Instead of ArduinoOTA](#why-http-ota)\n2. [Partition Table Requirements](#partition-table)\n3. [Firmware Code Changes](#firmware-code)\n4. [ESP-NOW Projects](#esp-now-projects)\n5. [Build and Upload Workflow](#build-upload)\n6. [OTA Update Workflow](#ota-update)\n7. [Reference: Complete Minimal Example](#minimal-example)\n\n---\n\n## Why HTTP OTA Instead of ArduinoOTA\n\n`ArduinoOTA` uses a UDP-based protocol:\n1. Host sends an authentication challenge to the device (port 3232)\n2. Device verifies password\n3. **Device opens a TCP connection back to the host** on a random port\n4. Host streams the firmware over this reverse connection\n\nStep 3 is the failure point on Windows because:\n- Windows Defender Firewall blocks inbound connections from the ESP32\n- No admin privileges available to add firewall rules\n- `arduino-cli` network upload cannot pass the password non-interactively\n\nHTTP OTA flips the direction:\n- Host opens a TCP connection **to** the device (outbound — always allowed)\n- Host POSTs the firmware as `multipart/form-data`\n- Device receives and writes to flash using the `Update` library\n\n---\n\n## Partition Table Requirements\n\nThe K10 BSP (`UNIHIKER:esp32` v0.0.3) ships with `large_spiffs_16MB.csv`:\n\n```csv\n# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\nfactory,  app,  factory, 0x10000, 0x500000,\nmodel,    data, spiffs,  0x510000,4563k,\nvoice_data,data, fat,    0x985000,2542k,\nfr,       data, ,        0xC01000,100K,\ncoredump, data, coredump,,        1K,\n```\n\n**Problem:** There is no `ota_0` / `ota_1` / `otadata` partition. `Update.begin()` fails immediately because it cannot find an inactive OTA slot to write to.\n\n**Solution:** Create `partitions.csv` in your sketch directory with OTA partitions:\n\n```csv\n# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x640000,\napp1,     app,  ota_1,   0x650000,0x640000,\nspiffs,   data, spiffs,  0xc90000,0x370000,\n```\n\n- `app0` and `app1` are each ~6.5 MB — plenty of room for the ~1.1 MB K10 titrator firmware\n- `otadata` is required for the bootloader to know which app partition to boot from\n- `spiffs` is reduced compared to the factory table, but still 3.5 MB\n\n**Compile with the custom partition:**\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\"\n```\n\nThe first USB upload after this change will write the new partition table to flash. This is a one-time operation.\n\n---\n\n## Optional: Speed Up Repeated Compiles\n\nArduino CLI has its own build cache. The reliable, current configuration keys are under `build_cache.*`:\n\n```bash\n# Linux/macOS\narduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\n```powershell\n# Windows PowerShell\narduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0\n```\n\nFor project-local repeat builds, keep the intermediate build folder stable and let Arduino CLI use all CPU cores:\n\n```bash\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0\n```\n\nUse `--clean` only when you need a full rebuild; it deliberately bypasses cached build artifacts.\n\n`ccache` can help in some C/C++ toolchains, but it is not a documented Arduino CLI configuration path in current releases. Avoid treating these as standard Arduino CLI settings:\n\n```bash\narduino-cli config set compiler.cache.enable true\narduino-cli config set compiler.cache.path /path/to/ccache\n```\n\nThe current Arduino CLI command reference also does not list `compile --build-cache-path`, so prefer persistent `build_cache.path` configuration plus a stable `--build-path`.\n\n---\n\n## Firmware Code Changes\n\n### 1. Include Update Library\n\n```cpp\n#include <Update.h>\n```\n\n### 2. Add Upload Handler\n\nUse the **four-argument** `server.on()` overload to register both a final handler and an upload-progress handler:\n\n```cpp\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    scheduleRestart(\"OTA update done\");  // or ESP.restart()\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.printf(\"OTA: %s\\n\", upload.filename.c_str());\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  }\n  else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  }\n  else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n```\n\n### 3. Register the Route\n\n```cpp\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n```\n\n### 4. Restart Scheduling (Recommended)\n\nRestarting immediately inside the handler can cut off the HTTP response. Use a delayed restart:\n\n```cpp\nbool restartPending = false;\nuint32_t restartAtMs = 0;\n\nvoid scheduleRestart(const String &message) {\n  restartPending = true;\n  restartAtMs = millis() + 1200;  // 1.2s delay\n}\n\nvoid loop() {\n  // ... existing loop code ...\n  if (restartPending && millis() >= restartAtMs) {\n    ESP.restart();\n  }\n}\n```\n\n---\n\n## ESP-NOW Projects\n\nHTTP OTA can coexist with ESP-NOW, but not as a pure ESP-NOW transport. The OTA web endpoint needs an IP interface, so an ESP-NOW sketch must temporarily enable AP, STA, or AP+STA networking while accepting the firmware upload.\n\n### Recommended Pattern: OTA Maintenance Mode\n\nUse normal runtime for ESP-NOW. Enter OTA mode only when needed:\n\n- Button long press during boot or runtime\n- Serial command such as `ota`\n- Saved `Preferences` flag set by a previous command\n- Trusted ESP-NOW command from a controller node\n- Local web/admin command if the sketch already has a WebServer\n\nIn OTA mode:\n\n1. Stop or pause periodic ESP-NOW sends.\n2. Start `WIFI_AP` or `WIFI_AP_STA`.\n3. Start the WebServer and register `/ota`.\n4. Call `server.handleClient()` frequently.\n5. Mark `otaUploadActive = true` during upload writes.\n6. Restart after a successful update.\n\nMinimal pattern:\n\n```cpp\n#include <WiFi.h>\n#include <WebServer.h>\n#include <Update.h>\n#include <esp_now.h>\n\nWebServer server(80);\n\nbool otaMode = false;\nbool otaUploadActive = false;\nbool restartPending = false;\nuint32_t restartAtMs = 0;\n\nvoid scheduleRestart() {\n  restartPending = true;\n  restartAtMs = millis() + 1200;\n}\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    scheduleRestart();\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n\n  if (upload.status == UPLOAD_FILE_START) {\n    otaUploadActive = true;\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (!Update.end(true)) {\n      Update.printError(Serial);\n    }\n    otaUploadActive = false;\n  } else if (upload.status == UPLOAD_FILE_ABORTED) {\n    Update.abort();\n    otaUploadActive = false;\n  }\n}\n\nvoid enterOtaMode() {\n  otaMode = true;\n\n  // Prefer an AP fallback so OTA still works when router credentials are wrong.\n  WiFi.mode(WIFI_AP_STA);\n  WiFi.softAP(\"K10-OTA\", \"12345678\");\n\n  // Optional: also connect to infrastructure WiFi.\n  // WiFi.begin(savedSsid, savedPassword);\n\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n\n  Serial.print(\"OTA AP IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  // Example gate: hold a button at boot, read Preferences, or parse Serial.\n  bool requestedOtaMode = false;\n\n  if (requestedOtaMode) {\n    enterOtaMode();\n    return;\n  }\n\n  WiFi.mode(WIFI_STA);\n  // Set channel before esp_now_init() if the deployment uses a fixed ESP-NOW channel.\n  // esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);\n  esp_now_init();\n}\n\nvoid loop() {\n  if (otaMode) {\n    server.handleClient();\n    if (restartPending && millis() >= restartAtMs) {\n      ESP.restart();\n    }\n    return;\n  }\n\n  if (!otaUploadActive) {\n    // Normal ESP-NOW runtime here.\n  }\n}\n```\n\n### Channel Rules\n\nESP-NOW and WiFi share one 2.4 GHz radio:\n\n- If the device is only in `WIFI_STA` and does not connect to a router, set a fixed channel before `esp_now_init()`.\n- If STA connects to a router, the router decides the channel. ESP-NOW peers must use that same channel.\n- Peer channel `0` means \"use the current WiFi channel\" and is useful when the local device follows the AP/STA channel.\n- Avoid hidden channel changes while ESP-NOW peers are active; reconnecting STA may move the radio and break peers on the old channel.\n\nFor K10 OTA work, prefer this practical rule: in normal ESP-NOW mode use a known channel; in OTA maintenance mode pause ESP-NOW and allow AP/STA networking to own the radio.\n\n### Pure ESP-NOW OTA\n\nPure ESP-NOW OTA is possible but should be treated as a separate advanced feature, not the default for this skill. It requires:\n\n- Firmware chunking small enough for ESP-NOW payload limits\n- Sequence numbers and acknowledgements\n- Retry, resume, and timeout handling\n- Image size and checksum validation before boot switch\n- Writes through `Update` or ESP-IDF OTA APIs into the inactive OTA partition\n- A secure authorization model so arbitrary peers cannot flash the device\n\nUse pure ESP-NOW OTA only when the user explicitly needs updates without AP/STA IP networking. Otherwise, use HTTP OTA maintenance mode.\n\n---\n\n## Build and Upload Workflow\n\n### Initial Setup (USB Required)\n\n```bash\n# Compile with custom partition table\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  -j 0 \\\n  --build-property \"build.partitions=custom\"\n\n# Upload via USB (also flashes the new partition table)\narduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 .\n```\n\nUse the serial port name for your operating system:\n\n| OS | Example port |\n|----|--------------|\n| Windows | `COM4` |\n| macOS | `/dev/cu.usbmodem1101` |\n| Linux | `/dev/ttyACM0` |\n\n### Subsequent Updates (WiFi OTA)\n\n```bash\n# Compile only\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . --build-path .arduino-build --output-dir build -j 0\n\n# Upload via HTTP with curl\ncurl -F \"file=@build/your_sketch.ino.bin\" http://192.168.9.42/ota\n\n# Or use the cross-platform Python uploader\npython scripts/ota_upload.py build/your_sketch.ino.bin --ip 192.168.9.42\n```\n\n---\n\n## OTA Update Workflow\n\n1. **Ensure the device is running an OTA-enabled sketch** (has `/ota` endpoint)\n2. **Get the device IP** from the web UI, serial output, or router\n3. **Compile** the new firmware\n4. **POST the `.bin` file** to `http://<ip>/ota`\n5. **Wait for `OK` response** (~5–10 seconds for a 1.1 MB firmware)\n6. **Device restarts automatically** after a short delay\n7. **Verify** by checking the web UI or JSON status endpoint\n\n---\n\n## Reference: Complete Minimal Example\n\n```cpp\n#include <WiFi.h>\n#include <WebServer.h>\n#include <Update.h>\n\nWebServer server(80);\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    delay(100);\n    ESP.restart();\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (!Update.end(true)) {\n      Update.printError(Serial);\n    }\n  }\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.softAP(\"K10-OTA-Test\", \"12345678\");\n\n  server.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);\n  server.begin();\n\n  Serial.print(\"IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nvoid loop() {\n  server.handleClient();\n}\n```\n\n---\n\n## Common Pitfalls\n\n| Pitfall | Why It Happens |\n|---------|---------------|\n| `Update.begin(size)` with exact size fails | ESP32 flash requires 4 KB alignment. Use `UPDATE_SIZE_UNKNOWN` instead. |\n| `Update.writeStream(server.client())` hangs | `writeStream()` waits for the client to close the connection, but the client waits for the HTTP response. Deadlock. Use chunked `client.read()` with a known `Content-Length` instead. |\n| `server.header(\"Content-Length\")` returns empty | Arduino WebServer stores `Content-Length` in `_clientContentLength`, not the headers map. Use `server.clientContentLength()` (public in ESP32 core). |\n| `FAIL` after full upload | `Update.end(true)` failed. Most common cause: firmware size exceeds OTA partition size. Ensure partition is large enough. |\n\nFile v1.0.0:skill-card.md\n\n## Description: <br>\nAdds HTTP OTA firmware update guidance for Unihiker K10 Arduino projects, including AP/STA projects and ESP-NOW projects that need a maintenance OTA mode. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[rockets-cn](https://clawhub.ai/user/rockets-cn) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and engineers use this skill to add, build, and operate HTTP OTA update paths for Unihiker K10 Arduino firmware, especially when USB access is impractical or ArduinoOTA network upload fails. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: Copyable examples can expose firmware flashing over HTTP with weak or hardcoded credentials. <br>\nMitigation: Use explicit maintenance mode, unique strong per-device credentials, network isolation, and preferably signed firmware or another authenticity check before accepting uploads. <br>\nRisk: The `/ota` endpoint can allow unauthorized firmware replacement if exposed on an untrusted network. <br>\nMitigation: Do not expose `/ota` outside a trusted maintenance network, and disable or gate OTA behavior during normal runtime. <br>\nRisk: Changing the partition table can disrupt existing flash layout and stored device data. <br>\nMitigation: Plan the first USB flash as a maintenance operation and back up or recreate required device settings before switching to OTA partitions. <br>\n\n\n## Reference(s): <br>\n- [K10 HTTP OTA Implementation Guide](artifact/references/ota-implementation.md) <br>\n- [ClawHub skill page](https://clawhub.ai/rockets-cn/unihiker-k10-ota) <br>\n- [Publisher profile](https://clawhub.ai/user/rockets-cn) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance] <br>\n**Output Format:** [Markdown guidance with C++ snippets, shell commands, partition configuration, and a Python upload helper] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Guides local firmware changes and network upload workflow; does not call external APIs.] <br>\n\n## Skill Version(s): <br>\n1.0.0 (source: server release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>","readmeExcerpt":"Skill: Unihiker K10 Ota Owner: rockets-cn Summary: 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... Tags: arduino:1.0.0, esp32:1.0.0, k10:1.0.0, latest:1.0.4, ota:1.0.0, unihiker:1.0.0 Version history: v1.0.4 | 2026-07-22T07:43:03.837Z | user Recommend the DFRobot Gravity ADS1115 for safe K10 external analog","codeSnippets":[],"executableExamples":[{"language":"csv","snippet":"# K10 OTA partition table that preserves speech-recognition model regions.\n# Keep model/voice_data/fr offsets aligned with the DFRobot K10 factory table.\n# Name,     Type, SubType, Offset,   Size,     Flags\nnvs,        data, nvs,     0x9000,   0x5000,\notadata,    data, ota,     0xe000,   0x2000,\napp0,       app,  ota_0,   0x10000,  0x280000,\napp1,       app,  ota_1,   0x290000, 0x280000,\nmodel,      data, spiffs,  0x510000, 4563K,\nvoice_data, data, fat,     0x985000, 2542K,\nfr,         data, ,        0xC01000, 100K,\ncoredump,   data, coredump,,         1K,\nspiffs,     data, spiffs,  0xC1B000, 0x3E5000,"},{"language":"bash","snippet":"arduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\""},{"language":"bash","snippet":"# Use all CPU cores and keep build artifacts in stable project-local folders.\narduino-cli compile --fqbn UNIHIKER:esp32:k10 . \\\n  --build-path .arduino-build \\\n  --output-dir build \\\n  --build-property \"build.partitions=custom\" \\\n  -j 0"},{"language":"bash","snippet":"arduino-cli config set build_cache.path ~/.cache/arduino-build-cache\narduino-cli config set build_cache.compilations_before_purge 0"},{"language":"powershell","snippet":"arduino-cli config set build_cache.path \"$env:LOCALAPPDATA\\arduino\\build-cache\"\narduino-cli config set build_cache.compilations_before_purge 0"},{"language":"cpp","snippet":"#include <Update.h>\n\nvoid handleOta() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.send(200, \"text/plain\", Update.hasError() ? \"FAIL\" : \"OK\");\n  if (!Update.hasError()) {\n    ESP.restart();  // or schedule a delayed restart\n  }\n}\n\nvoid handleOtaUpload() {\n  HTTPUpload &upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) {\n      Serial.printf(\"OTA Success: %u bytes\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n  }\n}\n\n// In setup() or startNetwork():\nserver.on(\"/ota\", HTTP_POST, handleOta, handleOtaUpload);"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: unihiker-k10-ota\ndescription: 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.\n---\n\n# Unihiker K10 - HTTP OTA\n\n## Overview\n\nEnable wireless firmware updates for K10 Arduino projects via HTTP POST.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n## When to Use\n\n- Your K10 is installed in a location difficult to reach with USB\n- You want to update firmware without opening the enclosure\n- You need a scriptable/automated deployment pipeline\n- ArduinoOTA network port upload fails with \"No response from device\"\n\n## Prerequisites\n\n- Existing K10 Arduino project with `WebServer` running\n- `arduino-cli` installed and K10 BSP (`UNIHIKER:esp32:k10`) available\n- Device and computer on the same network (or connected to K10's AP)\n\n## ESP-NOW Compatibility Rule\n\nESP-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.\n\nWhen adding OTA to an ESP-NOW program, use this policy:\n\n1. 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()`.\n2. Keep ESP-NOW and WiFi on the same channel if they run together. If STA connects to a router, the router determines "},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn7bn6y6610bpcvzaykr1h2m1d83e2rm\",\n  \"slug\": \"unihiker-k10-ota\",\n  \"version\": \"1.0.4\",\n  \"publishedAt\": 1784706183837\n}"},{"path":"references/ota-implementation.md","content":"# K10 HTTP OTA Implementation Guide\n\nScreen refresh rule: if the firmware displays OTA progress, WiFi state, IP addresses, voice state, or other status on the K10 screen, update only changed regions. Do not clear and redraw the whole screen in `loop()` unless full-screen refresh is measured above 30 fps; otherwise the display will visibly flicker.\n\nTTS compatibility rule: K10 speech synthesis is available only in the Chinese firmware. Keeping the `voice_data` partition intact is necessary for compatible TTS builds but does not make `ASR::setAsrSpeed()` / `ASR::speak()` available in English/international firmware.\n\n## Table of Contents\n\n1. [Why HTTP OTA Instead of ArduinoOTA](#why-http-ota)\n2. [Partition Table Requirements](#partition-table)\n3. [Firmware Code Changes](#firmware-code)\n4. [ESP-NOW Projects](#esp-now-projects)\n5. [Build and Upload Workflow](#build-upload)\n6. [OTA Update Workflow](#ota-update)\n7. [Reference: Complete Minimal Example](#minimal-example)\n\n---\n\n## Why HTTP OTA Instead of ArduinoOTA\n\n`ArduinoOTA` uses a UDP-based protocol:\n1. Host sends an authentication challenge to the device (port 3232)\n2. Device verifies password\n3. **Device opens a TCP connection back to the host** on a random port\n4. Host streams the firmware over this reverse connection\n\nStep 3 is the failure point on Windows because:\n- Windows Defender Firewall blocks inbound connections from the ESP32\n- No admin privileges available to add firewall rules\n- `arduino-cli` network upload cannot pass the password non-interactively\n\nHTTP OTA flips the direction:\n- Host opens a TCP connection **to** the device (outbound — always allowed)\n- Host POSTs the firmware as `multipart/form-data`\n- Device receives and writes to flash using the `Update` library\n\n---\n\n## Partition Table Requirements\n\nThe K10 BSP (`UNIHIKER:esp32` v0.0.3) ships with `large_spiffs_16MB.csv`:\n\n```csv\n# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\nfactory,  app,  factory, 0x10000, 0x500000,\nmodel,    data, spiffs,  0x510000,4563k,\nvoice_data,data, fat,    0x985000,2542k,\nfr,       data, ,        0xC01000,100K,\ncoredump, data, coredump,,        1K,\n```\n\n**Problem:** There is no `ota_0` / `ota_1` / `otadata` partition. `Update.begin()` fails immediately because it cannot find an inactive OTA slot to write to.\n\n**Solution for K10 AI projects:** Create `partitions.csv` in your sketch directory with OTA partitions that stop before the model region:\n\n```csv\n# Name,     Type, SubType, Offset,   Size,     Flags\nnvs,        data, nvs,     0x9000,   0x5000,\notadata,    data, ota,     0xe000,   0x2000,\napp0,       app,  ota_0,   0x10000,  0x280000,\napp1,       app,  ota_1,   0x290000, 0x280000,\nmodel,      data, spiffs,  0x510000, 4563K,\nvoice_data, data, fat,     0x985000, 2542K,\nfr,         data, ,        0xC01000, 100K,\ncoredump,   data, coredump,,         1K,\nspiffs,     data, spiffs,  0xC1B000, 0x3E5000,\n```\n\n- `app0` and `app1` are each 2.5 MB and end before `0x5100"},{"path":"skill-card.md","content":"## Description:\n\nAdd 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.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[rockets-cn](https://clawhub.ai/user/rockets-cn)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and engineers use this skill to add and maintain HTTP OTA firmware updates for Unihiker K10 Arduino projects, including AP/STA setups and ESP-NOW projects that need a temporary OTA maintenance mode.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The OTA pattern can expose an unauthenticated HTTP firmware update endpoint to anyone with network access.\n\nMitigation: Add per-device authentication, keep OTA mode temporary and locally triggered, and use signed firmware or an equivalent integrity check before using it on a real network.\n\nRisk: Shared default access point credentials can make OTA maintenance mode easier to reach than intended.\n\nMitigation: Avoid shared default AP passwords and provision unique device credentials.\n\nRisk: Changing the K10 partition table can erase local data or damage built-in AI model regions if offsets are wrong.\n\nMitigation: Review the partition table before the first USB flash and keep OTA application partitions from overlapping the model, voice_data, and fr regions.\n\n## Reference(s):\n\n- [K10 HTTP OTA Implementation Guide](references/ota-implementation.md)\n\n## Skill Output:\n\n**Output Type(s):** [Markdown, Code, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown guidance with inline code, command examples, and helper upload scripts]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Includes Python and PowerShell helper upload scripts.]\n\n## Skill Version(s):\n\n1.0.4 (source: server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"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... Skill: Unihiker K10 Ota Owner: rockets-cn Summary: 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... Tags: arduino:1.0.0, esp32:1.0.0, k10:1.0.0, latest:1.0.4, ota:1.0.0, unihiker:1.0.0 Version history: v1.0.4 | 2026-07-22T07:43:03.837Z | user Recommend the DFRobot Gravity ADS1115 for safe K10 external analog","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1539,"uniquenessScore":45,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T04:14:19.560Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T04:14:19.560Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T07:43:47.692Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}