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.