Two Streams, One Link: Lossless Fair Delivery Over BLE

Two Streams, One Link: Lossless Fair Delivery Over BLE

A pet tracker has to be honest about two very different things at once. Step counts come from a continuous, high-frequency motion stream that never stops, while audio arrives in big, bursty clips only when something happens. Both are health data, both must reach the cloud intact — and they have to share a single, skinny wireless link that regularly drops as the dog wanders out of range.

The tempting shortcut is to prioritize one and let the other suffer, or to drop data when buffers fill. We refused both. Our tracker's design goal is blunt: equal priority for audio and step count, and no silent data loss under any condition. This post is about how we actually deliver on that promise over one Bluetooth Low Energy (BLE) link.

The Problem: Fairness and Loss Are in Tension

Put two mismatched streams on one narrow pipe and the failure modes write themselves. A greedy audio clip — hundreds of kilobytes — can hog the link for seconds and starve the steady trickle of motion data. Flip the priority and a chatty sensor stream can keep a clip waiting forever.

Then there's loss. The link isn't reliable: it stalls under load, and it disappears entirely when the pet leaves range. Any design that "drops on full" or "sends and forgets" will quietly lose exactly the data a vet later wishes it had. Worse, the loss is invisible — the device looks online while a chewed-off afternoon simply never arrives.

So the real requirements are three, and they fight each other: never drop (persist everything until it's confirmed delivered), never starve (both streams make steady progress), and never block the real-time capture tasks while satisfying the first two. The architecture below is how those three coexist.

The Approach: Priority Policy, Flash Queues, One Fair Uploader

Our design separates what order to send from how to guarantee arrival. The ordering is a priority policy: control commands first, then motion data, then audio metadata, then the bulky audio payload. But priority alone starves the bottom of the list, so the policy is enforced by a weighted, round-robin uploader — each class gets a fair share of link time in rotation, with bounded latency, so audio payload keeps flowing even while motion data streams continuously. Priority decides who goes first; the weighted rotation guarantees everyone goes.

Underneath that, every stream is backed by a flash-persisted queue. Data is written to durable storage the moment it's produced and only deleted after the hub acknowledges it. That single rule — store first, delete after ACK — is what turns an unreliable link into a lossless one. A BLE dropout stops delivery; it never causes loss, because the bytes are still on flash waiting to be re-sent.

Finally, a single uploader owns the link. One consumer, self-pacing against the radio's available capacity, means there's never a fight between senders — just one fair scheduler draining durable queues at whatever rate the air can sustain.

The Process: Four Guarantees, in Real Code

1. Audio never drops a chunk — it waits instead. An audio clip is verified by a whole-file checksum on the hub, so a single missing chunk invalidates the entire recording. Rather than drop on a full buffer, the clip writer blocks for space (with a timeout so it can retry later), because losing one block means losing the whole clip:

364	    /* Like write_raw_block_safe(), but for audio clip transfer: a clip is
365	     * verified by a whole-file CRC on the hub, so a single dropped chunk
366	     * invalidates the entire 5-second clip. Rather than drop on a full
367	     * ring, block for space (the caller is the low-priority audio_xfer
368	     * task — it can wait; the I2S DMA producer is on a different path and
369	     * is unaffected). If space never frees (L2CAP fully stalled), time out
370	     * so the caller can abort and retry the clip later. */
371	    if (!s_enabled) return -1;
372	    if (len == 0 || len > MAX_FRAME_BYTES) {
373	        ESP_LOGE(TAG, "write_clip: invalid len %zu", len);
374	        return -1;
375	    }
376	    if (xSemaphoreTake(s_writer_mtx, pdMS_TO_TICKS(200)) != pdTRUE) return -1;
377	    size_t sent = xMessageBufferSend(s_ring, payload, len, pdMS_TO_TICKS(500));
378	    xSemaphoreGive(s_writer_mtx);
379	    if (sent != len) return -1;     /* ring stayed full for 500 ms — drain stalled */

2. The queue tail advances only on confirmed delivery. For the motion stream, the uploader treats a send as complete only when it succeeds. On any transient back-pressure it keeps the bytes and returns — the very next cycle re-sends the same data, so nothing is lost to a momentary stall:

675	            if (rc == 0) {
676	                sent++;
677	                circ_file.tail += next_len;
678	                continue;
679	            }
680	            if (rc == BLE_HS_ENOMEM || rc == BLE_HS_EBUSY ||
681	                rc == BLE_HS_EAGAIN || rc == BLE_HS_EMSGSIZE) {
682	                /* Stash for retry; don't advance tail — the SDU is intact
683	                 * and the next drain cycle re-tries the same bytes. */
684	                memcpy(pending, scratch, got);
685	                pending_len = got;
686	                return -1;
687	            }

The tail is the durable read cursor. Advancing it only after rc == 0 is the "delete after ACK" rule in one line: unconfirmed data is never forgotten.

3. Audio clips are acknowledged with selective retransmit. A clip is sent, then the hub either acknowledges it (delivered and checksum-verified) or reports exactly which byte ranges are missing — and we resend only those. The transfer converges even on a lossy link, and the clip is deleted from flash only once the hub confirms it:

858	    for (int attempt = 1; attempt <= AF_MAX_ATTEMPTS; attempt++) {
859	        if (!ble_l2cap_is_open()) { close(fd); return; }
860	        int sent = full ? af_send_whole_clip(fd, &hdr, payload, crc)
861	                        : af_send_selective(fd, &hdr, payload, ack.miss, ack.n_miss);
862	        if (sent != 0) { close(fd); return; }      /* stalled/closed: keep .adp, retry later */
863	
864	        int r = af_wait_ack(hdr.cid, &ack);
865	        if (r == 1) {                              /* ok=true: delivered + CRC-verified */
866	            close(fd);
867	            remove(path);
868	            ESP_LOGI(TAG, "clip cid=%llu delivered+acked (%u B, %u blk) in %d round(s)",
869	                     (unsigned long long)hdr.cid, (unsigned)payload, (unsigned)hdr.n_blocks, attempt);
870	            return;
871	        }

Notice line 862 and line 867: a stall or a dropped link keeps the clip file on flash for a later retry, and the file is removed only after a verified acknowledgement. If a clip can't get through after many attempts, it's dead-lettered — kept on flash, out of the way of newer clips, and revived on the next reconnect. Nothing is ever thrown away to make room.

4. The uploader self-paces and auto-resumes. Because one consumer owns the link, it can throttle itself to the radio's capacity instead of overrunning it. When the radio has no room (or no peer), it simply holds the current frame and returns; producers keep writing into durable queues without ever blocking:

696	        rc = ble_notify_payload(scratch, got);
697	        if (rc == -8) {
698	            memcpy(pending, scratch, got);
699	            pending_len = got;
700	            return -1;
701	        }

This is what keeps the capture tasks real-time and the streams fair: no producer stalls waiting on the link, and the single uploader ships at exactly the rate the air allows. When the pet comes back into range, the queues are still there and delivery resumes from where it left off.

The Results

The combined behavior is a link that degrades gracefully instead of losing data. A dropout pauses delivery; a stall triggers a retry; a partially-received clip converges through selective retransmit — and in every case the bytes sit safely on flash until the hub confirms them. Both streams keep making progress because the single uploader shares link time fairly rather than letting either monopolize it.

Just as important, the guarantees are cheap and local. There's no distributed transaction, no second radio, no heavyweight protocol — just durable queues, one self-pacing consumer, and the discipline of advancing a cursor or deleting a file only after an acknowledgement. The capture tasks never block, so step counting and audio recording stay real-time while the delivery layer quietly does the hard part.

Why It Matters at Hoomanely

Hoomanely is reinventing healthcare for pets — replacing reactive, imprecise care with continuous, clinical-grade monitoring that catches problems early. Our devices form a Physical Intelligence ecosystem: sensors fused at the edge, feeding the Biosense AI Engine that turns raw signals into personalized, preventive insights.

Those insights are only as good as the data beneath them, and health data has no "good enough." A missing afternoon of activity can hide a developing limp; a dropped audio clip can lose the one cough that mattered. Treating both motion and sound as equal-priority, and engineering the link so neither is ever silently lost, is what lets the Biosense engine reason over a complete record rather than a lucky sample of one.

Our guiding principle is that every signal matters and every detail counts. Fair, lossless delivery over an unreliable link is that principle rendered in firmware — the unglamorous plumbing that makes every downstream promise trustworthy.

Key Takeaways

  • Separate ordering from delivery. A priority policy decides who goes first; a weighted round-robin uploader guarantees the lowest-priority stream still makes steady progress.
  • Store first, delete after ACK. Persisting to flash and only removing data after a verified acknowledgement turns an unreliable link into a lossless one.
  • Advance the cursor only on success. Whether it's a queue tail or a clip file, unconfirmed data must be retried, never forgotten.
  • Converge with selective retransmit. Resending only the missing chunks of a checksummed clip reaches 100% delivery even on a lossy link.
  • One self-pacing uploader keeps it fair and real-time. A single consumer throttled to the radio's capacity means producers never block and neither stream starves.

Author's Note

This delivery architecture is the backbone of the collar-worn tracker in Hoomanely's Physical Intelligence ecosystem, carrying both the step stream and acoustic clips to the hub over one BLE link. Capture is the easy part; getting every byte to the cloud, fairly and without loss, across dropouts and stalls, is where the real engineering lives. It's the quiet guarantee that lets us tell a pet parent — truthfully — that nothing about their pet's day went unrecorded.

Read more