Troubleshooting¶
By symptom; each entry says why it happens and what fixes it, and links the
component page that owns the behaviour. Three places answer most questions
before any of this: the monitor (idf.py monitor), which narrates the
boot (Concepts); the device's
log ring at GET /api/v1/logs and the Logs page of the web UI, which
keep the boot log without a cable (below); and GET /api/v1/system/info,
whose reset_reason and last_reset say why the device last restarted.
Build¶
IDF_PATH is ESP-IDF vX.Y.Z; espOS builds with v6.0.0 up to (not including) v6.1.0- The IDF version policy (
cmake/espos_version.cmake). espOS is tested on the release named in.idf-version; any 6.0.x builds with one warning naming the tested one; another minor is refused, because it moves component APIs (esp_hosted, TWAI, the linux target) under the code. Fix: install a 6.0.x release and source itsexport.sh. To try anyway,-DESPOS_ALLOW_IDF_MISMATCH=1— and say so when reporting a problem. .idf-version pins vA but the espOS submodule pins vB- Your firmware carries its own
.idf-versionand it disagrees with the submodule's. Align them (espOS's pin is the one its CI tests) or delete the project's file to follow espOS. '…/components' has no espos_config — if espOS is a submodule, run git submodule update --init- The submodule directory is empty.
git submodule update --initin the firmware project. git describe here says v0.7.0-66-… which names an older tag- Not a fault, and the build is fine — the line says which espOS you have
(
baseis read from the submodule'sversion.txt, which is tracked and therefore always right).git submodule updatenever fetches new tags, so the submodule's idea of the newest release is as old as its last--init.git -C espos fetch --tagssilences it. Releasing. the espOS pin is at tag vX but its version.txt says Y- This one is a fault, and the two cannot be reconciled by fetching: a tag
reachable from
HEADis a tag the checkout already has, so the pinned commit genuinely carries aversion.txtthat was never bumped to match its tag. A device would report whichever the build preferred. Check the pinned commit. - The device serves a placeholder page instead of the web UI (
ui_storage: falsein/system/info; boot logui storage … has no index.html: serving the embedded page) - The LittleFS
storagepartition has no bundle. Either the project never calledespos_project_ui_partition()afterproject(), or a customDIRwas given and is missing (that is a CMake warning, not an error), or the image was written withidf.py app-flash, which skipsstorage.bin—idf.py flashwrites it. A missing default bundle (espos_httpd/ui-dist) is a hard configure error: restore it from git. Web UI. partitions.csvnot found after bumping espOS (or the OTA slots vanished)- The partition table moved to
components/espos_core/partitions/4mb.csvand is now selected by the prologue'sPARTITIONSargument. IDF applies defaults only to options asdkconfigdoes not already set, so a stalebuild/sdkconfigstill names the old path. Deletebuild/sdkconfig(or the build directory) once and reconfigure. The same applies after editing anysdkconfig.d/*file. Development. Failed to resolve component 'espos_ble' required by component 'main'(likewiseespos_n2k,espos_voice,espos_audio)- Consumer projects build with
MINIMAL_BUILD, and the optional espOS components are excluded outright unless named:espos_project_prologue(… COMPONENTS espos_ble). The core set needs no listing — naming one of those is itself an error (… is not an optional espOS component). Development. no secure_boot_signing_key.pem — generating a development RSA-3072 signing key- Expected on a fresh checkout; the key is git-ignored. It matters the day you ship: see OTA below.
- The build stops fitting (
size report,ota_0over 90 %) - The app slot is 1728 KB on the default 4 MB table. A Bluedroid firmware is
larger than that on a native-radio target — the BLE gateway measures
2.13 MB on an ESP32-C6 — so move to
components/espos_core/partitions/8mb.csv(Hardware). Two such slots plus storage do not fit a 4 MB chip at all, so a 4 MB board can carry that firmware only without OTA, in a singlefactoryslot.
WiFi¶
- No
espOS-xxxxnetwork appears - The portal comes up at once only when no network is configured or
wifi.sta_enabledis off. With networks configured it appears afterwifi.portal_after_s(90 s) of failed attempts, and never while connected or withwifi.portal_enabledoff. A phone that does not pop its sign-in sheet still reaches the page athttp://192.168.4.1. Awifi.portal_pskmakes the portal a protected network. WiFi → Portal. - It never connects;
reason: {code, text}inGET /api/v1/wifi/status 202,2,15— wrong password (also seen when a password is under 8 characters: that slot is skipped with a warning, WPA needs 8..63).201— network not in range or a hidden SSID.204— handshake timed out, weak signal.200— lost the beacon: out of range or the AP went away.1001— associated but no DHCP lease: the router. The full table is in WiFi → State machine. A password set on an open/WEP network is refused on purpose.<hostname>.localdoes not resolve- mDNS is off in the build (
CONFIG_ESPOS_NET_MDNS=n), or the client has no mDNS (older Windows), or you are on another subnet — mDNS does not cross routers. The monitor'sconnected to … as <ip>line has the address. - ESP32-P4: WiFi says
connected, nothing works, log full ofrpc_core: Timeout waiting for Resp - The SDIO link to the C6 co-processor wedged; the disconnect event cannot
cross the jammed link, so the state machine keeps saying connected. Two
configuration landmines cause it: a receive block-ack window of 16 (IDF
raises
CONFIG_WIFI_RMT_RX_BA_WINfrom 6 as soon as PSRAM is enabled), and the transport mempool in PSRAM combined with 128-byte L2 cache lines (the SDIO driver rejects the buffers withESP_ERR_INVALID_ARG). Both are pinned insdkconfig.d/espos.defaults.esp32p4; do not override them.SDIO mode mismatch … Abortingat boot is the third: streaming mode must stay on. Recovery is a restart — the heartbeat watchdog does it after three missed 20 s beats — neveresp_hosted_deinit()/init(), which asserts rather than failing. WiFi → Co-processor link watchdog.
Signal K¶
no server (waiting for discovery or manual host)- Discovery browses
_signalk-http._tcpover mDNS, which does not cross subnets or VLANs; the server also has to advertise (signalk-server does by default;avahi-browse -rt _signalk-http._tcpon a laptop shows what the device sees). On a network without mDNS setsk.server_host(andsk.server_port, default 80) on the device's Signal K page or withPUT /api/v1/config {"sk": {"server_host": "192.168.1.10", "server_port": 3000}}. Several servers on the boat and the wrong one chosen:sk.server_selfpins one by its self URN,sk.server_pinkeeps the current choice. Signal K → Discovery. access denied by the server — request again from the device's web UI when it is allowed- Someone denied the request, or the server has device access requests
disabled (the request is answered
403). Nothing retries by itself, on purpose. Fix the server side (Security → Access Requests, or its settings), then Request again on the device's Signal K page (POST /api/v1/sk/request). - Approved once, now
requestingagain; a new entry in Access Requests - The token was revoked — the device was deleted under Security → Devices,
or the server was reinstalled (new self URN, so the stored token no longer
applies). The device drops the token on the first
401and requests again; approve it. If the log saysalready requested(400), the server still holds a pending request for this client: approve or deny that one. Signal K → Token state machine. approved, streaming, but no values on the serverGET /api/v1/sk/status→ws:connected,sent,buffered,dropped. Publishing needs thereadwritepermission (sk.permissions); with read-only the server ignores deltas.sk.ws_enabledoff keeps buffering without a socket.droppedrising means the offline ring is too small for the outage (sk.buffer_msgs,sk.buffer_kb). Signal K → Delta stream.- A PUT answers
COMPLETEDwithstatusCode 405 "PUT not supported for …" - The server's honest answer: no plugin has registered a PUT handler for that path. Install or write one on the server side; the device did its part. Signal K → Inbound.
- The device restarts every ~5 minutes while the server is down
skLinkStalled: WiFi connected, the stream has worked once this boot, down forsk.stall_s(300 s). It exists for the P4's wedged-link case. If your server is routinely down longer, raisesk.stall_sor turn the watchdog off (Health).
OTA¶
image rejected: bad signature or corrupt- The image was signed with a key the device does not trust — most often
the development key, which every fresh checkout generates anew: a device
flashed from one clone rejects images built in another (or in CI). Keep
one key, outside the repository, and flash each device once over USB with
a build carrying it. Also rejected: an image whose
project_namediffers from the running app's, and a corrupt download. Verify a file withespsecure verify-signature --version 2 --keyfile <key>.pem build/<app>.bin. OTA → Signing key. - Swapped the key, rebuilt, the device still rejects the image
- ESP-IDF's signing step depends on the unsigned binary alone, so a new key
without a source change keeps the old signature. The prologue detects
the change and forces a re-link; a project with its own CMake that copied
the signing lines without that block deletes
build/.bin_timestampby hand. OTA → Swapping the key. rolled_back: trueinGET /api/v1/ota/status- The new image failed to confirm itself — it panicked before WiFi came up,
or never reached the network within
ota.confirm_tmo_s(600 s) — and the bootloader booted the previous slot;other_versionnames the failed build. A panic leaves a core dump (below). - A newer build is on the server but
availablestaysnull - The manifest entry must match
target,channel(ota.channel, defaultstable) andapp, and be strictly newer than the running version (1.0.0-beta.1 < 1.0.0; a-dirtybuild compares as a string).ota.last_errornames a manifest that failed to parse or exceeded 16 KiB. OTA → Manifest format.
Memory¶
lowMemorynotification- WARN below 40 KB total or 20 KB internal free; ALARM — fatal, restarts
after three consecutive 10 s ticks — below 12 KB internal or an 8 KB
largest free internal block. On a board with PSRAM the internal numbers
are the ones that matter: task stacks, the radio and DMA come from
internal RAM, and tens of megabytes of PSRAM hide its exhaustion. Watch
espos.<hostname>.internalFreeandlargestBlockon the server; move large buffers to PSRAM (heap_caps_malloc(…, MALLOC_CAP_SPIRAM)); keep the P4's hosted mempool in PSRAM as the defaults do. Each open TLS connection costs about 20 KB (CONFIG_ESPOS_SK_HTTP_MAX_CONCURRENT). Health → Built-in conditions.
Restarts and watchdogs¶
- The device restarts and you do not know why
GET /api/v1/system/info.last_resetnon-null: the health policy restarted it, andhealth_key/messagesay on which condition (lowMemory,taskStalled,skLinkStalled, or your own) with the heap low-water marks before it.reset_reason: task_wdt: a task registered withespos_health_watch_task()was silent for 30 s — there is a core dump.panic: core dump.brownout: the power supply. Loss of WiFi never restarts a device by design. Health → The reset record.- ESP32-P4 restarts about 60 s after the network stalls
- The co-processor heartbeat watchdog; the stall is the fault, the restart is the recovery (WiFi above).
- Turning the restarts off
espos_start_opts_t.health_watchdog = false, orCONFIG_ESPOS_CORE_HEALTH_WATCHDOG=nfor the whole firmware; conditions are still reported, strikes are never counted.
Logs¶
Without a cable, the ring holds the last 16 KiB of console lines including the boot:
H=http://espos-1a2b.local
curl -s "$H/api/v1/logs?limit=200" | python3 -c 'import json,sys; print(*json.load(sys.stdin)["lines"], sep="\n")'
curl -s -X PUT -H 'Content-Type: application/json' -d '{"level":"debug","tag":"espos_sk"}' "$H/api/v1/logs/level"
Poll on with ?after=<next - 1> from the previous reply; the level change
is runtime-only. The web UI's Logs page does the same with filter, follow
and download. REST API → Logs.
Core dumps¶
A panic writes a core dump to the coredump partition; the summary and the
image are served over REST, and the ELF of that exact build decodes it
(app_elf_sha256 in the summary identifies the build):
H=http://espos-1a2b.local
curl -s "$H/api/v1/system/coredump" # task, pc, app_elf_sha256, …
curl -s -o coredump.bin "$H/api/v1/system/coredump/raw"
espcoredump.py --chip esp32c6 info_corefile -c coredump.bin -t raw build/minimal.elf
curl -s -X DELETE "$H/api/v1/system/coredump"
espcoredump.py is $IDF_PATH/components/espcoredump/espcoredump.py, on
PATH in an exported IDF shell. The web UI's Status page shows the
summary and offers the download and the erase. REST API →
/system/coredump; the tutorial
Logs and core dumps walks through a
deliberate crash end to end.