Skip to content

Firmware diagnostics

Task timing investigation

ESPurr diagnostics → Report task CPU usage and stack margins to serial (CONFIG_ESPURR_TASK_STATS) is an optional debug build feature, off by default and unavailable in recovery. It enables native FreeRTOS task runtime statistics and reports every five seconds: task ID/name, core affinity (-1 means unpinned), priority, interval CPU percentage, and minimum remaining stack in bytes. Percentages use one core as 100%, giving 200% capacity on dual-core builds. New tasks begin interval accounting at the next snapshot; deleted tasks disappear from the report. Interrupt time is charged to whichever task was interrupted, so these figures do not attribute ISR cost or show individual preemptions.

Use the ESP TIMER runtime-statistics clock under Component config → FreeRTOS; the report rejects CPU-clock counters because they use different units. The default ESP timer runs at 1 MHz. Select the uint64_t counter type for long captures; uint32_t rolls over after roughly 71 minutes. Existing private configurations remain authoritative. No settings, flash writes or phone protocol changes are involved.

The render owner calls the bounded 48-task snapshot only after all LED transfers drain, then prints outside the runtime state mutex. The LEDs retain their last latched frame throughout. Enumeration scans stack margins under the kernel lock; the report logs both snapshot duration and total reporting pause. This deliberately perturbs cadence and is excluded from the consecutive-overrun guard. It is useful for CPU saturation, task placement and stack investigation; use an execution trace or captured waveform for sub-frame interrupt/refill deadlines. It is not continuous unperturbed timing evidence. Reports are serial-only and excluded from the app's short application log ring.

Application diagnostics

Both phone apps expose Settings → Firmware diagnostics. Active warnings also appear on Control; the local browser shows warnings and explicit log refresh/save. Warnings describe current subsystem state, independently of historical log lines. Older firmware may omit both facilities; absence is unavailable, not a healthy report. Disconnected/background observations are labelled as the last update. There is no notification subscription, flash log history or automatic upload. Watch decodes optional warnings through the shared package but keeps its compact remote UI; detailed diagnostics and file export belong on the phone.

Recent application logs

CONFIG_ESPURR_LOG_BUFFER defaults on with IDF Log V1/text output. It can be disabled in menuconfig without disabling active warnings. A 4096-byte RAM ring starts at app_main, survives radio handovers and is lost on reboot. It does not collect ROM, bootloader or earlier system startup output. Serial logging continues normally.

The IDF esp_log_set_vprintf callback formats at most 255 bytes per record on its caller's stack, strips ANSI/control characters, retains the first line and adds a newline. Long lines are truncated; the oldest retained byte can be mid-line. Only application tags espurr, runtime, imu, settings, power, sensing and ota, plus warning/error records from ble and web, are retained. Third-party, security and informational BLE/AP logs are excluded. Keep new messages in these tags free of credentials, device identifiers and raw audio. This is a deliberately limited diagnostic window, not a complete serial dump or a general redaction engine.

The hook uses bounded stack storage and a short ring-copy critical section; it never allocates, writes flash, calls another logger or waits for a consumer. No render/sample-rate logging is added. Enabled logging adds 4 KiB plus ring metadata to static RAM and two 256-byte formatting buffers to the stack of a log caller. Runtime heap/stack/timing acceptance remains a hardware check.

A random nonzero epoch identifies the current ring. INFO freezes [start,end); READ serves at most 192 bytes within that window. An overwritten requested range or changed epoch is refused. Offset overflow resets the ring and advances its epoch. Clients discard incomplete transfers and require explicit Refresh; they do not join pages from separate windows. No automatic retry loop competes with logs. Successful text is bounded ASCII with newline, at most 4096 bytes, displayed as text and exported through the native file picker/browser download.

Active warnings property v1

firmware.alerts is an additive optional read-only property, type 21, 16 bytes. All multibyte fields are little-endian. It does not change protocol/catalogue/NVS versions, effect identities, settings or command completion semantics.

Offset Size Meaning
0 1 Version 1
1 1 Log reads available, Boolean
2 2 Reserved zero
4 4 Active warning mask
8 4 Ring epoch; nonzero iff log reads available
12 4 Model load/validation error; nonzero iff warning bit 7 is set
Bit Meaning
0 Settings save failed
1 V2 lighting restricted by power policy
2 Motion stale/failed
3 Either ambient-light source stale/failed
4 Either ear sensor stale/failed
5 Requested air audio stale/failed or spectrum processing degraded
6 Requested contact audio stale/failed/waiting for sensor
7 Voice model load/validation failed; recognition disabled by its future owner
8 Runtime unhealthy after its initial 150-frame health interval

Unknown warning bits produce a generic additional-warning message. Compile-time omissions and ordinary startup/off states are not sensor failures. Model errors are ephemeral, explicitly published through diagnostics_model_error; the future model owner must disable recognition on failure and clear the error only after a successful valid load. That reporting hook does not implement a loader or disable an inference engine by itself. Production currently has neither.

Readers reuse their existing runtime snapshot and copy independent expiring sensor publications. This adds no sensor I/O or long render mutex to GATT callbacks. Flags can change independently of settings revision; normal health polls refresh them. Historical errors in logs do not latch warning flags.

Authenticated Bluetooth log operation

Opcode 0x81 on the existing espurr/catalogue endpoint is separate from catalogue requests and audio opcode 0x80. Bonded LE Secure Connections authentication and encryption apply. The server validates the live control generation before and after its bounded read, returning no bytes after revocation. No extra characteristic or credential path is added. Clients serialize complete secured exchanges with polling/actions, discard results after disconnect/session changes and retain the authenticated session for a well-formed application refusal.

Request is exactly 24 bytes:

Offset Size Meaning
0 1 Version 1
1 1 Opcode 0x81
2 2 Reserved zero
4 4 Live control generation, nonzero
8 4 Epoch; zero selects INFO
12 4 READ offset
16 4 Frozen READ end from INFO
20 2 READ count 1–192
22 2 Reserved zero

INFO requires epoch/offset/end/count all zero. Reply header is 28 bytes, followed by the stated number of bytes (maximum total 220):

Offset Size Meaning
0 1 Version 1
1 1 Opcode 0x81
2 2 ESP error; zero on success
4 4 Control generation
8 4 Current ring epoch
12 4 Current oldest retained offset
16 4 INFO end or echoed frozen READ end
20 4 INFO start or echoed READ offset
24 2 Payload count; zero for INFO/errors
26 1 Logs available, Boolean
27 1 Reserved zero

Successful READ returns min(count,end-offset) bytes. A stale/overwritten request returns ESP_ERR_INVALID_STATE; disabled logging returns ESP_ERR_NOT_SUPPORTED. Malformed requests or revoked sessions fail the transport operation without log payload. Maximum request/reply sizes fit the existing bounded encrypted endpoint. Shared C/Swift/Kotlin fixtures live in tests/fixtures/firmware-diagnostics-v1.json.

Local browser

/api/status adds a diagnostics object with alerts, logs_available and model_error. GET /api/logs returns { "text": "..." } for a complete bounded window, or an error. Reads use the same local Host/Origin allow-list as writes; Content-Type is only required for writes. This uses the existing local AP access boundary, not BLE authentication. Responses use Cache-Control: no-store. Overwritten reads return HTTP 409 and require Refresh. The handler copies short chunks outside render; JSON allocation/encoding happens outside the ring lock.