An SPSC ring buffer keeps the Companion mic from ever waiting.
Why CareOS uses a lock-free single-producer single-consumer ring between the mic task and the WebSocket pump — and what that means at the bedside.
On the M5Stack CoreS3 inside every Companion, two FreeRTOS tasks share an audio stream. One captures 16kHz mono 16-bit PCM off the microphone. The other drains those frames into a WebSocket bound for CareOS. They run at different priorities, pinned to different cores, and they must never touch the same byte at the same time.
The obvious answer is a mutex. The obvious answer is wrong. The moment the network pump hesitates — a roaming wifi handoff, a TLS renegotiation, a slow access point — it holds the lock, and the mic task blocks waiting to write the next 20ms frame. The resident's voice falls on the floor.
Single producer, single consumer
Instead we use a single-producer single-consumer ring buffer. The mic task owns the head index and only ever writes it. The pump task owns the tail index and only ever writes it. Each side reads the other's index with an acquire barrier and publishes its own with a release barrier. No mutex, no priority inversion, no contention. A general-purpose MPMC queue would need atomic CAS loops to coordinate multiple writers; with exactly one producer and one consumer, the hardware already gives us what we need.
Bounded backpressure beats blocking
The ring is fixed-size, which forces an honest decision when the network falls behind: drop the oldest frame and keep capturing, rather than block the mic. For audio this is the right call. A few hundred milliseconds of stale speech is not worth keeping; a microphone that stops listening is a Companion that has stopped caring.
At the bedside this is what a resident actually feels. They say hello and the device hears them, even when the facility wifi is having a bad afternoon. The mic task is never waiting on the network — it is always waiting on the next voice.