/*
* browser-demo/sketch.ino - Arduino-core port of spsc-ring-buffer-esp32,
* for the shareable Wokwi project. A DEMO, not the firmware.
*
* Wokwi's browser editor compiles Arduino, not ESP-IDF, so this port exists
* to give the README a click-to-run link. Two honest differences from the
* canonical firmware in main/main.c:
*
* 1. The producer here is HardwareSerial::onReceive(), which the Arduino
* core runs in ITS OWN TASK, not in the UART RX interrupt - the core's
* driver owns that interrupt and there is no polite way to take it.
* Producer and consumer are still two concurrent contexts, so the
* lock-free handoff is real; it is just task-to-task instead of
* ISR-to-task.
* 2. The buffer code is pasted inline (a sketch is one file). The
* canonical, tested, coverage-gated copy is main/ringbuf.h - if the two
* ever disagree, that one is right.
*
* Type into the serial monitor: stats | reset | dump
*/
#include <Arduino.h>
/* ---- main/ringbuf.h, inlined (see the repo for the annotated original) -- */
#include <stdint.h>
typedef struct {
uint8_t *buf;
uint32_t mask;
uint32_t head; /* producer-owned */
uint32_t tail; /* consumer-owned */
uint32_t overruns;
uint32_t high_water;
} ringbuf_t;
static bool ringbuf_init(ringbuf_t *rb, uint8_t *storage, uint32_t capacity)
{
if (capacity < 2u || (capacity & (capacity - 1u)) != 0u) return false;
rb->buf = storage; rb->mask = capacity - 1u;
rb->head = rb->tail = rb->overruns = rb->high_water = 0u;
return true;
}
static uint32_t ringbuf_count(const ringbuf_t *rb)
{
return __atomic_load_n(&rb->head, __ATOMIC_ACQUIRE)
- __atomic_load_n(&rb->tail, __ATOMIC_ACQUIRE);
}
static bool ringbuf_put(ringbuf_t *rb, uint8_t byte)
{
const uint32_t head = __atomic_load_n(&rb->head, __ATOMIC_RELAXED);
const uint32_t tail = __atomic_load_n(&rb->tail, __ATOMIC_ACQUIRE);
const uint32_t used = head - tail;
if (used > rb->mask) { rb->overruns++; return false; }
if (used + 1u > rb->high_water) rb->high_water = used + 1u;
rb->buf[head & rb->mask] = byte;
__atomic_store_n(&rb->head, head + 1u, __ATOMIC_RELEASE);
return true;
}
static bool ringbuf_get(ringbuf_t *rb, uint8_t *out)
{
const uint32_t tail = __atomic_load_n(&rb->tail, __ATOMIC_RELAXED);
const uint32_t head = __atomic_load_n(&rb->head, __ATOMIC_ACQUIRE);
if (head == tail) return false;
*out = rb->buf[tail & rb->mask];
__atomic_store_n(&rb->tail, tail + 1u, __ATOMIC_RELEASE);
return true;
}
static void ringbuf_reset(ringbuf_t *rb)
{
rb->tail = 0u; rb->overruns = 0u; rb->high_water = 0u;
__atomic_store_n(&rb->head, 0u, __ATOMIC_RELEASE);
}
/* ---- end of inlined buffer ---------------------------------------------- */
#define RX_RING_CAPACITY 1024u
static ringbuf_t rx_ring;
static uint8_t rx_storage[RX_RING_CAPACITY];
static uint32_t rx_total;
static uint8_t history[32];
static uint32_t history_next;
static char line_buf[128];
static size_t line_len;
/* Producer: runs in the Arduino core's UART event task. */
static void on_uart_rx(void)
{
while (Serial.available() > 0) {
(void)ringbuf_put(&rx_ring, (uint8_t)Serial.read());
}
}
static void cmd_stats(void)
{
Serial.printf("stats: rx=%lu dropped=%lu hw=%lu cap=%lu\n",
(unsigned long)rx_total,
(unsigned long)rx_ring.overruns,
(unsigned long)rx_ring.high_water,
(unsigned long)(rx_ring.mask + 1u));
}
static void cmd_reset(void)
{
/* The producer must be quiet while head is rewritten; onReceive(NULL)
* detaches the callback, the firmware equivalent masks the interrupt. */
Serial.onReceive(NULL);
ringbuf_reset(&rx_ring);
rx_total = 0; history_next = 0;
memset(history, 0, sizeof(history));
Serial.onReceive(on_uart_rx);
Serial.println("reset: counters and ring cleared");
}
static void cmd_dump(void)
{
const uint32_t n = sizeof(history);
const uint32_t count = (rx_total < n) ? rx_total : n;
const uint32_t start = (rx_total < n) ? 0 : history_next;
Serial.printf("dump: last %lu bytes consumed (oldest first):\n",
(unsigned long)count);
for (uint32_t i = 0; i < count; i++) {
Serial.printf(" %02x", history[(start + i) % n]);
if ((i + 1) % 16 == 0) Serial.println();
}
Serial.println();
}
static void dispatch(const char *line)
{
if (strcmp(line, "stats") == 0) cmd_stats();
else if (strcmp(line, "reset") == 0) cmd_reset();
else if (strcmp(line, "dump") == 0) cmd_dump();
else if (line[0] != '\0')
Serial.printf("unknown command '%s' - try stats | reset | dump\n", line);
Serial.print("> ");
}
void setup()
{
Serial.begin(115200);
ringbuf_init(&rx_ring, rx_storage, RX_RING_CAPACITY);
Serial.onReceive(on_uart_rx);
Serial.printf("up: spsc ring cap=%u (Arduino port - producer is the "
"core's UART task, not the RX ISR)\n",
(unsigned)RX_RING_CAPACITY);
Serial.println("type a command: stats | reset | dump");
Serial.print("> ");
}
void loop()
{
uint8_t byte;
while (ringbuf_get(&rx_ring, &byte)) {
rx_total++;
history[history_next] = byte;
history_next = (history_next + 1) % sizeof(history);
if (byte == '\r' || byte == '\n') {
Serial.println();
line_buf[line_len] = '\0';
dispatch(line_buf);
line_len = 0;
} else if (isprint(byte) && line_len < sizeof(line_buf) - 1) {
line_buf[line_len++] = (char)byte;
Serial.write(byte); /* echo */
}
}
delay(10);
}