zebra-report/include/modem.h
Russell Ballestrini 4fadfca5b1 phase 2: gap analysis — cat>> append + binary asset recovery
quality verification:
  - confirmed all 8 unapplied edits in phase 1 also failed in their
    original sessions (tool-result is_error=1, "String to replace
    not found"). Our reconstruction is faithful to live execution.

gaps closed:
  - include/modem.h: re-ran replay with cat>>heredoc handling in
    chronological position. BATTLE TOADS dual-channel stereo UART
    block (4495 bytes, 102 lines) now appended at correct point in
    timeline. Two previously-failing edits now apply against the
    post-append baseline. unapplied edits dropped 8 -> 6.
  - web/fonts/: chunkfive-regular-webfont.{woff,woff2} restored
    from live source /home/fox/git/www.unturf.com/css/chunkfive/.
    HTML+CSS @font-face references now resolve.

remaining unapplied edits (6) confirmed legitimate live-session
failures, not reconstruction artifacts.

internal references audit:
  - all #include directives resolve within recovered tree
  - all font url() references resolve to recovered web/fonts/
  - no other Bash file-creation ops target zebra-report

final tree: 22 files, ~160KB.
2026-05-27 13:54:19 -04:00

444 lines
18 KiB
C

#pragma once
/*
* modem.h -- inline UART encode/decode over PulseAudio volume
*
* Shared by tx.c, rx.c, chat.c.
* Depends on zebra.h (zebra_pulse_t, zebra_set_volume, zebra_get_volume,
* zebra_bit_to_vol, zebra_vol_to_bit, ZEBRA_VOL_THRESHOLD).
*/
#include <time.h>
#include <stdint.h>
#include "zebra.h"
/* callback invoked by zebra_rx_run for each decoded byte */
typedef void (*zebra_byte_cb)(uint8_t byte, void *userdata);
/* ------------------------------------------------------------------ *
* timing helper *
* ------------------------------------------------------------------ */
static inline void ts_add_ns(struct timespec *ts, long ns) {
ts->tv_nsec += ns;
if (ts->tv_nsec >= 1000000000L) {
ts->tv_sec += ts->tv_nsec / 1000000000L;
ts->tv_nsec %= 1000000000L;
}
}
/* ------------------------------------------------------------------ *
* TX: UART framing (start + 8 data LSB-first + stop) *
* ------------------------------------------------------------------ */
/* channels: from zebra_sink_t.channels — avoids PA channel-count query per symbol */
static inline void zebra_send_symbol(zebra_pulse_t *z, uint32_t sink,
uint8_t channels, int bit,
struct timespec *next, long period_ns) {
zebra_set_volume_noack(z, sink, channels, zebra_bit_to_vol(bit));
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, next, NULL);
ts_add_ns(next, period_ns);
}
static inline void zebra_send_byte(zebra_pulse_t *z, uint32_t sink,
uint8_t channels, uint8_t byte,
struct timespec *next, long period_ns) {
zebra_send_symbol(z, sink, channels, 0, next, period_ns); /* start */
for (int i = 0; i < 8; i++)
zebra_send_symbol(z, sink, channels, (byte >> i) & 1, next, period_ns);
zebra_send_symbol(z, sink, channels, 1, next, period_ns); /* stop */
}
/* ------------------------------------------------------------------ *
* RX: 2x oversampled UART decoder — runs forever, calls cb per byte *
* ------------------------------------------------------------------ */
static inline void zebra_rx_run(zebra_pulse_t *z, uint32_t sink, int baud,
zebra_byte_cb cb, void *userdata) {
const int oversample = 4;
long quarter_ns = 1000000000L / ((long)baud * oversample);
long half_ns = 2 * quarter_ns;
long full_ns = 4 * quarter_ns;
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
int prev = 1; /* assume MARK (idle) */
for (;;) {
ts_add_ns(&ts, quarter_ns);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL);
uint8_t vol;
if (zebra_get_volume(z, sink, &vol) < 0) {
/* sink gone — wait and retry */
struct timespec retry = {1, 0};
nanosleep(&retry, NULL);
prev = 1;
continue;
}
int cur = zebra_vol_to_bit(vol);
/* MARK→SPACE falling edge = start bit */
if (prev == 1 && cur == 0) {
/* advance to center of start bit and confirm */
ts_add_ns(&ts, half_ns);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL);
if (zebra_get_volume(z, sink, &vol) < 0) { prev = 1; continue; }
if (zebra_vol_to_bit(vol) != 0) { prev = 1; continue; }
/* sample 8 data bits */
uint8_t byte = 0;
int ok = 1;
for (int i = 0; i < 8; i++) {
ts_add_ns(&ts, full_ns);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL);
if (zebra_get_volume(z, sink, &vol) < 0) { ok = 0; break; }
int b = zebra_vol_to_bit(vol);
if (b < 0) b = (vol >= ZEBRA_VOL_THRESHOLD) ? 1 : 0;
byte |= (uint8_t)(b << i);
}
if (ok) cb(byte, userdata);
/* Resync sample clock to real time after each byte.
* Prevents accumulated edge-detection error from shifting
* data bit samples in subsequent bytes. */
clock_gettime(CLOCK_MONOTONIC, &ts);
prev = 1;
} else if (cur >= 0) {
prev = cur;
}
}
}
/* ------------------------------------------------------------------ *
* PURE HELPERS — no PA, fully testable *
* ------------------------------------------------------------------ */
/* Compute max safe baud from avg PA poll latency (nanoseconds).
* Formula: 1e9 / (avg_ns * 4x_oversample) * 0.8_safety = 2e8 / avg_ns */
static inline int zebra_baud_from_avg_ns(long avg_ns) {
if (avg_ns <= 0) return ZEBRA_BAUD_DEFAULT;
int baud = (int)(200000000L / avg_ns);
if (baud < ZEBRA_BAUD_MIN) baud = ZEBRA_BAUD_MIN;
if (baud > ZEBRA_BAUD_MAX) baud = ZEBRA_BAUD_MAX;
return baud;
}
/* Build a BAUD_OFFER handshake frame into buf[ZEBRA_HS_FRAME_LEN]. */
static inline void zebra_hs_build(uint8_t frame[ZEBRA_HS_FRAME_LEN], uint16_t baud) {
frame[0] = ZEBRA_HS_MAGIC_0;
frame[1] = ZEBRA_HS_MAGIC_1;
frame[2] = ZEBRA_HS_TYPE_OFFER;
frame[3] = (uint8_t)(baud & 0xFF);
frame[4] = (uint8_t)(baud >> 8);
frame[5] = frame[0] ^ frame[1] ^ frame[2] ^ frame[3] ^ frame[4];
}
/* Build a READY frame (type=0x02, baud=0) into buf[ZEBRA_HS_FRAME_LEN]. */
static inline void zebra_hs_build_ready(uint8_t frame[ZEBRA_HS_FRAME_LEN]) {
frame[0] = ZEBRA_HS_MAGIC_0;
frame[1] = ZEBRA_HS_MAGIC_1;
frame[2] = ZEBRA_HS_TYPE_READY;
frame[3] = 0;
frame[4] = 0;
frame[5] = frame[0] ^ frame[1] ^ frame[2] ^ frame[3] ^ frame[4];
}
/* Validate and parse a handshake frame.
* Returns 0 and sets *baud on success; -1 on bad magic, type, or checksum. */
static inline int zebra_hs_parse(const uint8_t frame[ZEBRA_HS_FRAME_LEN],
uint16_t *baud) {
if (frame[0] != ZEBRA_HS_MAGIC_0 || frame[1] != ZEBRA_HS_MAGIC_1) return -1;
if (frame[2] != ZEBRA_HS_TYPE_OFFER) return -1;
uint8_t ck = frame[0] ^ frame[1] ^ frame[2] ^ frame[3] ^ frame[4];
if (ck != frame[5]) return -1;
*baud = (uint16_t)(frame[3] | ((uint16_t)frame[4] << 8));
return 0;
}
/* ------------------------------------------------------------------ *
* BENCHMARK: measure PA poll latency, derive max safe baud *
* ------------------------------------------------------------------ */
/* Runs N zebra_get_volume calls and measures average round-trip time.
* Returns max baud receiver can sustain via zebra_baud_from_avg_ns. */
static inline int zebra_benchmark_baud(zebra_pulse_t *z, uint32_t sink) {
const int N = 100;
uint8_t vol;
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
for (int i = 0; i < N; i++)
zebra_get_volume(z, sink, &vol);
clock_gettime(CLOCK_MONOTONIC, &t1);
long elapsed_ns = (t1.tv_sec - t0.tv_sec) * 1000000000L
+ (t1.tv_nsec - t0.tv_nsec);
return zebra_baud_from_avg_ns(elapsed_ns / N);
}
/* ------------------------------------------------------------------ *
* HANDSHAKE TX: send negotiation frame at ZEBRA_BAUD_HANDSHAKE *
* ------------------------------------------------------------------ */
/* Frame layout (ZEBRA_HS_FRAME_LEN = 6 bytes):
* [0] 0x5A magic 'Z'
* [1] 0x42 magic 'B'
* [2] 0x01 type: BAUD_OFFER
* [3] baud low byte (uint16 little-endian)
* [4] baud high byte
* [5] XOR of bytes 0-4 (checksum) */
static inline int zebra_send_handshake(zebra_pulse_t *z, uint32_t sink,
uint8_t channels, uint16_t baud) {
uint8_t frame[ZEBRA_HS_FRAME_LEN];
zebra_hs_build(frame, baud);
long period_ns = 1000000000L / ZEBRA_BAUD_HANDSHAKE;
zebra_set_volume_fast(z, sink, channels, ZEBRA_VOL_MARK);
struct timespec next;
clock_gettime(CLOCK_MONOTONIC, &next);
ts_add_ns(&next, period_ns);
for (int i = 0; i < ZEBRA_HS_FRAME_LEN; i++)
zebra_send_byte(z, sink, channels, frame[i], &next, period_ns);
zebra_set_volume_fast(z, sink, channels, ZEBRA_VOL_MARK);
return 0;
}
/* ------------------------------------------------------------------ *
* HANDSHAKE RX: listen for any HS frame type with timeout *
* ------------------------------------------------------------------ */
/* General frame receiver: listens at ZEBRA_BAUD_HANDSHAKE for a frame
* whose type byte matches expected_type. Returns 0 and sets *out_baud
* (may be NULL for READY frames where baud=0) on success; -1 on timeout.
* Uses sliding-window magic-byte sync so partial frame receipt is OK. */
static inline int zebra_recv_hs_frame(zebra_pulse_t *z, uint32_t sink,
int timeout_ms, uint8_t expected_type,
uint16_t *out_baud) {
const int oversample = 4;
long quarter_ns = 1000000000L / ((long)ZEBRA_BAUD_HANDSHAKE * oversample);
long half_ns = 2 * quarter_ns;
long full_ns = 4 * quarter_ns;
struct timespec deadline, ts, now;
clock_gettime(CLOCK_MONOTONIC, &deadline);
ts_add_ns(&deadline, (long)timeout_ms * 1000000L);
clock_gettime(CLOCK_MONOTONIC, &ts);
int prev = 1;
uint8_t frame[ZEBRA_HS_FRAME_LEN];
int fpos = 0;
for (;;) {
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > deadline.tv_sec ||
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec))
return -1;
ts_add_ns(&ts, quarter_ns);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL);
uint8_t vol;
if (zebra_get_volume(z, sink, &vol) < 0) {
struct timespec r = {0, 10000000L}; /* 10ms retry */
nanosleep(&r, NULL);
prev = 1;
continue;
}
int cur = zebra_vol_to_bit(vol);
if (prev == 1 && cur == 0) {
/* start bit — confirm at center */
ts_add_ns(&ts, half_ns);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL);
if (zebra_get_volume(z, sink, &vol) < 0) { prev = 1; continue; }
if (zebra_vol_to_bit(vol) != 0) { prev = 1; continue; }
/* decode 8 data bits */
uint8_t byte = 0;
int ok = 1;
for (int i = 0; i < 8; i++) {
ts_add_ns(&ts, full_ns);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL);
if (zebra_get_volume(z, sink, &vol) < 0) { ok = 0; break; }
int b = zebra_vol_to_bit(vol);
if (b < 0) b = (vol >= ZEBRA_VOL_THRESHOLD) ? 1 : 0;
byte |= (uint8_t)(b << i);
}
if (!ok) { clock_gettime(CLOCK_MONOTONIC, &ts); prev = 1; continue; }
/* sliding frame sync on magic bytes */
if (fpos == 0) {
if (byte == ZEBRA_HS_MAGIC_0) frame[fpos++] = byte;
} else if (fpos == 1) {
if (byte == ZEBRA_HS_MAGIC_1) frame[fpos++] = byte;
else if (byte == ZEBRA_HS_MAGIC_0) { fpos = 1; frame[0] = byte; }
else fpos = 0;
} else {
frame[fpos++] = byte;
if (fpos == ZEBRA_HS_FRAME_LEN) {
/* validate: magic + expected_type + checksum */
uint8_t ck = frame[0]^frame[1]^frame[2]^frame[3]^frame[4];
if (frame[2] == expected_type && ck == frame[5]) {
if (out_baud)
*out_baud = (uint16_t)(frame[3]|((uint16_t)frame[4]<<8));
return 0;
}
fpos = 0;
}
}
clock_gettime(CLOCK_MONOTONIC, &ts); /* resync after each byte */
prev = 1;
} else if (cur >= 0) {
prev = cur;
}
}
}
/* Receive BAUD_OFFER frame. Wrapper around zebra_recv_hs_frame. */
static inline int zebra_recv_handshake(zebra_pulse_t *z, uint32_t sink,
int timeout_ms, uint16_t *out_baud) {
return zebra_recv_hs_frame(z, sink, timeout_ms, ZEBRA_HS_TYPE_OFFER, out_baud);
}
/* ------------------------------------------------------------------ *
* HANDSHAKE READY: 3-way handshake completion *
* *
* After sending BAUD_OFFER, RX sends 3x READY frames then enters its *
* receive loop immediately. TX waits for READY before sending data. *
* This eliminates the settle-timer race at high baud rates. *
* ------------------------------------------------------------------ */
/* Send READY frame 3x on sink so TX catches it even with scheduling jitter.
* RX calls this immediately after zebra_send_handshake, then enters rx loop. */
static inline int zebra_send_ready(zebra_pulse_t *z, uint32_t sink,
uint8_t channels) {
uint8_t frame[ZEBRA_HS_FRAME_LEN];
zebra_hs_build_ready(frame);
long period_ns = 1000000000L / ZEBRA_BAUD_HANDSHAKE;
for (int rep = 0; rep < 3; rep++) {
zebra_set_volume_fast(z, sink, channels, ZEBRA_VOL_MARK);
struct timespec next;
clock_gettime(CLOCK_MONOTONIC, &next);
ts_add_ns(&next, period_ns);
for (int i = 0; i < ZEBRA_HS_FRAME_LEN; i++)
zebra_send_byte(z, sink, channels, frame[i], &next, period_ns);
}
zebra_set_volume_fast(z, sink, channels, ZEBRA_VOL_MARK);
return 0;
}
/* Wait for READY frame from RX. Returns 0 on success, -1 on timeout.
* TX calls this after receiving BAUD_OFFER. Data send follows immediately. */
static inline int zebra_recv_ready(zebra_pulse_t *z, uint32_t sink,
int timeout_ms) {
return zebra_recv_hs_frame(z, sink, timeout_ms, ZEBRA_HS_TYPE_READY, NULL);
}
/* ------------------------------------------------------------------ *
* BATTLE TOADS: dual-channel stereo UART *
* *
* One stereo tab → L and R modulated independently. *
* Both channels use same UART framing and baud rate. *
* TX sends byte_L and byte_R simultaneously each frame. *
* RX decodes both channels from a single PA poll per half-symbol. *
* Net throughput: 2x single-channel at same baud rate. *
* ------------------------------------------------------------------ */
/* Send one dual-channel symbol: L=bit_l, R=bit_r */
static inline void bt_send_symbol(zebra_pulse_t *z, uint32_t sink,
int bit_l, int bit_r,
struct timespec *next, long period_ns) {
zebra_set_volume_lr_noack(z, sink,
zebra_bit_to_vol(bit_l),
zebra_bit_to_vol(bit_r));
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, next, NULL);
ts_add_ns(next, period_ns);
}
/* Send two bytes simultaneously: byte_l on L channel, byte_r on R channel.
* Frame: start(0,0) + 8 paired data bits + stop(1,1) = 10 symbols. */
static inline void bt_send_byte_lr(zebra_pulse_t *z, uint32_t sink,
uint8_t byte_l, uint8_t byte_r,
struct timespec *next, long period_ns) {
bt_send_symbol(z, sink, 0, 0, next, period_ns); /* start */
for (int i = 0; i < 8; i++)
bt_send_symbol(z, sink,
(byte_l >> i) & 1,
(byte_r >> i) & 1,
next, period_ns);
bt_send_symbol(z, sink, 1, 1, next, period_ns); /* stop */
}
/* Receive loop: decodes L and R channels from each PA poll.
* Calls cb twice per frame — first with L byte, then R byte.
* Output order: L0, R0, L1, R1, ... matching bt_send_byte_lr pairs. */
static inline void bt_rx_run(zebra_pulse_t *z, uint32_t sink, int baud,
zebra_byte_cb cb, void *userdata) {
const int oversample = 4;
long quarter_ns = 1000000000L / ((long)baud * oversample);
long half_ns = 2 * quarter_ns;
long full_ns = 4 * quarter_ns;
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
int prev_l = 1, prev_r = 1; /* both idle at MARK */
for (;;) {
ts_add_ns(&ts, quarter_ns);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL);
uint8_t vol_l, vol_r;
if (zebra_get_volume_lr(z, sink, &vol_l, &vol_r) < 0) {
struct timespec retry = {1, 0};
nanosleep(&retry, NULL);
prev_l = prev_r = 1;
continue;
}
int cur_l = zebra_vol_to_bit(vol_l);
int cur_r = zebra_vol_to_bit(vol_r);
/* start bit: detect falling edge on L (TX sends both low together) */
if (prev_l == 1 && cur_l == 0) {
/* confirm start at center */
ts_add_ns(&ts, half_ns);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL);
if (zebra_get_volume_lr(z, sink, &vol_l, &vol_r) < 0)
{ prev_l = prev_r = 1; continue; }
if (zebra_vol_to_bit(vol_l) != 0)
{ prev_l = prev_r = 1; continue; }
/* decode 8 paired data bits */
uint8_t byte_l = 0, byte_r = 0;
int ok = 1;
for (int i = 0; i < 8; i++) {
ts_add_ns(&ts, full_ns);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL);
if (zebra_get_volume_lr(z, sink, &vol_l, &vol_r) < 0)
{ ok = 0; break; }
int bl = zebra_vol_to_bit(vol_l);
int br = zebra_vol_to_bit(vol_r);
if (bl < 0) bl = (vol_l >= ZEBRA_VOL_THRESHOLD) ? 1 : 0;
if (br < 0) br = (vol_r >= ZEBRA_VOL_THRESHOLD) ? 1 : 0;
byte_l |= (uint8_t)(bl << i);
byte_r |= (uint8_t)(br << i);
}
if (ok) {
cb(byte_l, userdata);
cb(byte_r, userdata);
}
/* Resync sample clock after each frame — same fix as zebra_rx_run */
clock_gettime(CLOCK_MONOTONIC, &ts);
prev_l = prev_r = 1;
} else {
if (cur_l >= 0) prev_l = cur_l;
if (cur_r >= 0) prev_r = cur_r;
}
}
}