# Text + Voice demo baseline 0.5.1

Prepared on branch `codex/demo-text-voice-baseline-0.5.1`, whose parent is the
physically proven firmware source `6bffe571b7c1b61cfdb168b0f7a03737bad394f1`.
This work did not restart, deploy, or flash production.

## Production observation

The active Gateway remained PID 355703 in
`/home/lighthouse/esp32-dev-buffered-0.5.2` at commit `a0f020c`, with zero systemd
restarts. The service was healthy while the device connection was not: there were
23 `offline` events from 19:28:53 through 20:22:47. The final three completed
online intervals were approximately 39, 39, and 40 seconds. This is connection
failure, not a Gateway process restart.

No current Gateway log identifies `voice_start`, chunk receipt, or `voice_end`.
The existing record therefore cannot place the reported Long-A failure at STT.
This branch adds content-free `[VOICE_RX]` and `[VOICE_STT]` lifecycle logs only;
it never logs audio, base64, transcript, token, credential, or Agent input.

## Commit audit

The following conclusions come from file content and blob hashes, not commit
titles.

| Question | 6bffe57 -> 3b46835 -> c7039ab -> a0f020c |
|---|---|
| Voice capture state machine | No direct change |
| Voice WSS upload / JSON framing | No change |
| Voice PCM chunk | No change: 3,528 bytes / 40 ms |
| Device WSS send lock and timeout | No change: shared SDK lock, 2,000 ms caller timeout |
| Gateway Voice/STT implementation | Identical `voice.py` blob at all four commits |
| Wi-Fi manager / PS behavior | Identical `wifi_manager.cpp`; no new PS override |
| TCP defaults | No buffered-branch change |
| Shared audio backend | Yes: buffered ownership/result state and full-buffer queue descriptor |
| New task | Yes: lazy-created, permanent `speech_download`, 6,144-byte stack, priority 4 |
| New socket | Yes: independent `esp_http_client` HTTPS/TLS connection alongside WSS |
| New allocation | Yes: full PCM in PSRAM plus HTTP/TLS/lwIP internal allocations |

`6bffe57..3b46835` adds documentation and opt-in speech downlink diagnostics.
The firmware diagnostic option defaults off. The Gateway send wrapper reduces to
the same `send_lock` plus three-second `send_json` timeout when tracing is off.
Those changes were not part of the known physical pass, so newer `main` is not a
stronger baseline merely because it is newer.

## Buffered downloader audit

Once `esp_http_client_init()` succeeds, the synchronous download function reaches
`esp_http_client_cleanup()` after open failure, header failure, non-200, encoded or
wrong-length body, cancellation observed between reads, timeout/EAGAIN, short body,
read failure, complete body, and hash success/failure. Cleanup happens before local
playback. PCM is freed on every non-transfer path. No direct missing-cleanup branch
was found.

Cancellation is not immediate while blocked inside one HTTP operation. A read can
hold the worker until its four-second operation timeout; only then can `alive()` be
observed and cleanup run. Cleanup itself has no independent deadline or completion
telemetry. These are bounded-by-design assumptions, not proof that ESP-TLS/lwIP
released every resource on the physical failure.

The task is created on the first buffered request and then remains forever blocked
on its one-entry queue. A permanent worker is a normal design, but it retains its
stack, TCB, and queue for the rest of the boot. During a request it also retains the
PSRAM PCM, HTTP buffers, TLS context, socket, and lwIP buffers. Its priority (4) is
below cloud/input/audio tasks (5), and blocking HTTP reads should yield, so pure CPU
task starvation is not the leading explanation. SHA was never reached in the known
failure.

The PCM body is explicitly PSRAM-only, so its roughly 350 KB size is not itself an
internal-heap allocation. The 6 KB task stack, TLS records/context, HTTP buffers,
socket state, TCP PCBs, and pbufs do consume scarce internal or network memory. The
diagnostic candidate logs internal free/minimum and largest DMA block, but no serial
values from the failing device were available here. Memory pressure is plausible,
not measured.

There is normally one WSS socket plus one temporary HTTPS socket, below the configured
ten socket descriptors. Explicit client cleanup argues against a simple descriptor
leak. Repeated active closes can still leave transient TCP PCBs/TIME_WAIT state (the
build has a 60-second TCP MSL), and a stuck cleanup or library-level retention has not
been excluded. No socket/PCB counters were captured, so progressive exhaustion is
possible rather than proven.

## 18.4 kbps interpretation

Supported: the endpoint returned HTTP 200; the device received 70,728 bytes; the
Lighthouse public fixture passed; later Gateway ASGI response sends completed in
about 0.4 ms. The endpoint and public route were usable, and the observed bottleneck
was downstream of the Gateway application's response handoff.

Likely: the ESP32/public-last-mile receive path was the limiting segment, with the
device network stack or hotspot/radio path more likely than TTS or Gateway body
generation. The later repeated WSS disconnects make a device/link resource problem
more likely than a speech-server throughput problem.

Possible: ESP HTTP/TLS read behavior, lwIP/internal-memory pressure, cellular hotspot
loss/latency, or the Cloudflare-to-hotspot route. Task scheduling is possible but less
likely because the downloader blocks and runs below the WSS/input tasks.

Unknown: the exact split among radio, route, TLS, lwIP, and heap. No parameter tuning
is justified by the present evidence.

## Voice failure classification

The direct Voice code is unchanged. Long A adds repeated approximately 4.8 KB WSS
JSON uploads every 40 ms to a connection already demonstrating idle disconnects.
The leading diagnosis is therefore a shared connectivity/resource regression exposed
by Voice traffic, not an STT regression. Because old production did not log Voice RX:

- no `[VOICE_RX] start`: device/WSS/connectivity boundary;
- start/chunks followed by offline: Voice uplink/current-connection health;
- complete end followed by failure: only then investigate STT/Agent.

There is currently no evidence for the third case.

## Baseline decision

Use `6bffe57` firmware behavior, not `3b46835`, for the demonstration baseline.
It is the artifact source that physically passed Voice capture/upload, STT,
confirmation/send, Agent activity, Approval, final Chinese rendering, and terminal
vibration. Keep Speech Mode `OFF`; retain speech code but do not invoke Read Screen,
AUTO_REPLY, or AUTO_SUMMARY. Do not add a capability-suppression build option unless
OFF mode proves insufficient; changing the proven firmware for that purpose would
reduce baseline confidence.

## Prepared firmware artifact

The baseline firmware was rebuilt from this worktree with firmware version `0.5.1`:

- merged image: `dist/m5stack-stopwatch/merged-flash.bin`
- merged size: 2,123,712 bytes
- merged SHA-256: `d0b59686cb4aabebebb8fecce9ef4e59e4f2b321b2245f3678bb319bd70e849f`
- application image: `dist/m5stack-stopwatch/firmware.bin`
- application size: 2,058,176 bytes
- application SHA-256: `673cc7112a9a842a8e0292bd6ab0977f278181f0e5ca28d8ce489ca2a7ef2993`

The merged size matches the historical physical-pass image, but its SHA-256 does
not match historical SHA-256
`673ba20d8d010a562e441c7f136ec48afcb0853f45d684e40c489465c0009cbf`.
The current tracked firmware and board source trees are byte-identical to
`6bffe57`; the rebuild used the current local provisioning defaults and current
build environment. The historical binary/config inputs are not available here for
a byte-level attribution, so this is a newly built candidate from the proven source,
not the same historical artifact.

## Authorized-later rollback plan

Do not merely remove the systemd drop-in: `/home/lighthouse/esp32-dev` is currently
checked out on `codex/speech-40ms-gateway-ab-0.5.1`, not `main`.

During an explicitly authorized maintenance window:

1. Verify the root checkout is clean, switch `/home/lighthouse/esp32-dev` to `main`
   at the reviewed commit, and re-run `./scripts/test-server.sh`. For the strictest
   historical match, use exact `6bffe57` instead of assuming newer main is physically
   equivalent.
2. Remove or move aside
   `~/.config/systemd/user/device-gateway.service.d/buffered-experiment.conf`, run
   `systemctl --user daemon-reload`, then restart `device-gateway.service` once.
3. Verify `WorkingDirectory=/home/lighthouse/esp32-dev`, expected PID/start time,
   `NRestarts=0`, `/health`, one online device, Speech Mode `OFF`, and no new offline
   event before physical testing. If Voice boundary evidence is still needed, deploy
   this branch's server-only lifecycle-log commit through a separately reviewed path;
   it does not require changing firmware.

## Physical acceptance

1. Idle five minutes: no offline.
2. Agent text: RUNNING, activity, DONE, and final text.
3. Long A, say `哈喽哈喽`: Voice start/chunks/end, STT transcript, A SEND, Pi input.
4. Repeat Voice three times with zero unexpected offline.

Only all four passing establishes `DEMO BASELINE PHYSICAL PASS`. Speech output is
frozen and is not part of this acceptance.
