428 lines
15 KiB
C++
428 lines
15 KiB
C++
#pragma once
|
|
|
|
#include <Arduino.h>
|
|
#include <atomic>
|
|
#include <limits.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
namespace mesh {
|
|
|
|
// A Stream facade for transports which provide a genuinely nonblocking write
|
|
// primitive separate from Stream::write(). Adafruit_USBD_CDC::write() loops
|
|
// and yields until it has queued the complete request; TinyUSB's lower-level
|
|
// tud_cdc_n_write() instead returns after one FIFO attempt. Keeping the
|
|
// single-attempt policy here makes the primary Companion CDC and the dedicated
|
|
// logging CDC share the same reentrancy guard without changing other targets.
|
|
class SingleAttemptNonBlockingStream : public Stream {
|
|
public:
|
|
using TryWrite = size_t (*)(void* context, const uint8_t* data,
|
|
size_t size);
|
|
using CanAccess = bool (*)(void* context);
|
|
using ExclusiveOperation = void (*)(void* context);
|
|
|
|
SingleAttemptNonBlockingStream(Stream& delegate, TryWrite try_write,
|
|
void* context = nullptr,
|
|
CanAccess can_access = nullptr)
|
|
: _delegate(delegate), _try_write(try_write), _context(context),
|
|
_can_access(can_access) {}
|
|
|
|
int available() override {
|
|
if (!canAccess()) return 0;
|
|
const int count = _delegate.available();
|
|
return canAccess() ? count : 0;
|
|
}
|
|
int read() override {
|
|
if (!canAccess()) return -1;
|
|
const int value = _delegate.read();
|
|
// If the host changed while read() was in flight, consume/drop that byte
|
|
// instead of handing it to the next session's protocol parser.
|
|
return canAccess() ? value : -1;
|
|
}
|
|
int peek() override {
|
|
if (!canAccess()) return -1;
|
|
const int value = _delegate.peek();
|
|
return canAccess() ? value : -1;
|
|
}
|
|
// The delegate's flush may wait forever while a CDC host keeps DTR asserted
|
|
// but stops reading. A single-attempt facade must keep every operation
|
|
// single-attempt, including flush.
|
|
void flush() override {}
|
|
int availableForWrite() override {
|
|
return canAccess() ? _delegate.availableForWrite() : 0;
|
|
}
|
|
|
|
size_t write(uint8_t value) override { return write(&value, 1); }
|
|
|
|
size_t write(const uint8_t* data, size_t size) override {
|
|
if (data == nullptr || size == 0 || _try_write == nullptr) return 0;
|
|
if (_writer_busy.test_and_set(std::memory_order_acquire)) return 0;
|
|
|
|
if (!canAccess()) {
|
|
_writer_busy.clear(std::memory_order_release);
|
|
return 0;
|
|
}
|
|
|
|
size_t written = _try_write(_context, data, size);
|
|
if (written > size) written = size;
|
|
|
|
_writer_busy.clear(std::memory_order_release);
|
|
return written;
|
|
}
|
|
|
|
// Check capacity and perform the sole write under the same producer gate.
|
|
// A separate outer preflight is insufficient: another writer over this
|
|
// shared CDC can otherwise consume capacity between check and write.
|
|
size_t writeWholeRecord(const uint8_t* data, size_t size) {
|
|
if (data == nullptr || size == 0 || _try_write == nullptr) return 0;
|
|
if (_writer_busy.test_and_set(std::memory_order_acquire)) return 0;
|
|
|
|
if (!canAccess()) {
|
|
_writer_busy.clear(std::memory_order_release);
|
|
return 0;
|
|
}
|
|
|
|
size_t written = 0;
|
|
const int available = _delegate.availableForWrite();
|
|
if (available >= 0 && static_cast<size_t>(available) >= size) {
|
|
written = _try_write(_context, data, size);
|
|
if (written > size) written = size;
|
|
}
|
|
|
|
_writer_busy.clear(std::memory_order_release);
|
|
return written;
|
|
}
|
|
|
|
// Run a short transport/session operation only when no application writer
|
|
// is in flight. The caller retries later on false; this never waits.
|
|
bool tryRunExclusive(ExclusiveOperation operation, void* context = nullptr) {
|
|
if (operation == nullptr
|
|
|| _writer_busy.test_and_set(std::memory_order_acquire)) {
|
|
return false;
|
|
}
|
|
operation(context);
|
|
_writer_busy.clear(std::memory_order_release);
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
bool canAccess() const {
|
|
return _can_access == nullptr || _can_access(_context);
|
|
}
|
|
|
|
Stream& _delegate;
|
|
TryWrite _try_write;
|
|
void* _context;
|
|
CanAccess _can_access;
|
|
std::atomic_flag _writer_busy = ATOMIC_FLAG_INIT;
|
|
};
|
|
|
|
// A whole-record view over a shared SingleAttemptNonBlockingStream. Unlike a
|
|
// generic wrapper, its capacity preflight uses the delegate's own writer gate,
|
|
// so Binary, terminal, logging, and mOTA producers cannot interleave between
|
|
// the capacity sample and the one low-level write.
|
|
template <size_t MAX_WRITE_SIZE = 256>
|
|
class AtomicWholeRecordNonBlockingStream : public Stream {
|
|
public:
|
|
explicit AtomicWholeRecordNonBlockingStream(
|
|
SingleAttemptNonBlockingStream& delegate) : _delegate(delegate) {}
|
|
|
|
int available() override { return _delegate.available(); }
|
|
int read() override { return _delegate.read(); }
|
|
int peek() override { return _delegate.peek(); }
|
|
void flush() override {}
|
|
int availableForWrite() override { return _delegate.availableForWrite(); }
|
|
|
|
size_t write(uint8_t value) override { return write(&value, 1); }
|
|
|
|
size_t write(const uint8_t* data, size_t size) override {
|
|
if (data == nullptr || size == 0 || size > MAX_WRITE_SIZE) return 0;
|
|
constexpr size_t PRINTF_SCRATCH_SIZE = 256;
|
|
if (size == PRINTF_SCRATCH_SIZE && data[size - 1] == '\0') return 0;
|
|
return _delegate.writeWholeRecord(data, size);
|
|
}
|
|
|
|
private:
|
|
SingleAttemptNonBlockingStream& _delegate;
|
|
};
|
|
|
|
// A bounded byte queue for functional text written through a single-attempt
|
|
// transport. Producer calls are admitted whole and never wait; service() keeps
|
|
// a byte offset across short delegate writes so an ordinary draining host sees
|
|
// the exact ordered stream. Once the queue fills, later records are dropped as
|
|
// a whole rather than blocking the application loop.
|
|
//
|
|
// Adafruit nRF52 Print::printf() formats into a 256-byte scratch buffer but can
|
|
// pass vsnprintf()'s larger required length to write(). The NUL at byte 255 is
|
|
// the only safe signal that such a record was truncated before this facade.
|
|
template <size_t CAPACITY = 4096, size_t PRINTF_SCRATCH_SIZE = 256>
|
|
class BufferedNonBlockingWriteStream : public Stream {
|
|
public:
|
|
static_assert(CAPACITY > 1, "byte queue needs at least two bytes");
|
|
static_assert(PRINTF_SCRATCH_SIZE > 1,
|
|
"printf scratch size needs at least two bytes");
|
|
|
|
explicit BufferedNonBlockingWriteStream(Stream& delegate)
|
|
: _delegate(delegate) {}
|
|
|
|
int available() override { return _delegate.available(); }
|
|
int read() override { return _delegate.read(); }
|
|
int peek() override { return _delegate.peek(); }
|
|
|
|
// Drain only currently available capacity. Never forward a potentially
|
|
// blocking delegate flush from the application loop.
|
|
void flush() override { service(); }
|
|
|
|
int availableForWrite() override {
|
|
if (_busy.test_and_set(std::memory_order_acquire)) return 0;
|
|
const size_t free = CAPACITY - _count;
|
|
_busy.clear(std::memory_order_release);
|
|
return free > static_cast<size_t>(INT_MAX)
|
|
? INT_MAX
|
|
: static_cast<int>(free);
|
|
}
|
|
|
|
size_t write(uint8_t value) override { return write(&value, 1); }
|
|
|
|
size_t write(const uint8_t* data, size_t size) override {
|
|
if (data == nullptr || size == 0 || size > CAPACITY) return 0;
|
|
if (size >= PRINTF_SCRATCH_SIZE
|
|
&& data[PRINTF_SCRATCH_SIZE - 1] == '\0') {
|
|
return 0;
|
|
}
|
|
if (_busy.test_and_set(std::memory_order_acquire)) return 0;
|
|
|
|
drainUnlocked();
|
|
if (size > CAPACITY - _count) {
|
|
_busy.clear(std::memory_order_release);
|
|
return 0;
|
|
}
|
|
|
|
const size_t first = size < CAPACITY - _head
|
|
? size
|
|
: CAPACITY - _head;
|
|
memcpy(&_buffer[_head], data, first);
|
|
if (first < size) memcpy(_buffer, data + first, size - first);
|
|
_head = (_head + size) % CAPACITY;
|
|
_count += size;
|
|
|
|
drainUnlocked();
|
|
_busy.clear(std::memory_order_release);
|
|
return size;
|
|
}
|
|
|
|
// Called from the application loop. Retains every unwritten suffix for the
|
|
// next call and stops immediately when the delegate reports no progress.
|
|
size_t service() {
|
|
if (_busy.test_and_set(std::memory_order_acquire)) return 0;
|
|
const size_t written = drainUnlocked();
|
|
_busy.clear(std::memory_order_release);
|
|
return written;
|
|
}
|
|
|
|
bool tryDiscardPending() {
|
|
if (_busy.test_and_set(std::memory_order_acquire)) return false;
|
|
_head = 0;
|
|
_tail = 0;
|
|
_count = 0;
|
|
_busy.clear(std::memory_order_release);
|
|
return true;
|
|
}
|
|
|
|
void discardPending() { (void)tryDiscardPending(); }
|
|
|
|
size_t queuedByteCount() {
|
|
if (_busy.test_and_set(std::memory_order_acquire)) return CAPACITY;
|
|
const size_t count = _count;
|
|
_busy.clear(std::memory_order_release);
|
|
return count;
|
|
}
|
|
|
|
private:
|
|
size_t drainUnlocked() {
|
|
size_t total = 0;
|
|
while (_count > 0) {
|
|
const int available = _delegate.availableForWrite();
|
|
if (available <= 0) break;
|
|
|
|
size_t attempt = _count < CAPACITY - _tail
|
|
? _count
|
|
: CAPACITY - _tail;
|
|
if (attempt > static_cast<size_t>(available)) {
|
|
attempt = static_cast<size_t>(available);
|
|
}
|
|
size_t written = _delegate.write(&_buffer[_tail], attempt);
|
|
if (written > attempt) written = attempt;
|
|
if (written == 0) break;
|
|
|
|
_tail = (_tail + written) % CAPACITY;
|
|
_count -= written;
|
|
total += written;
|
|
if (written < attempt) break;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
Stream& _delegate;
|
|
uint8_t _buffer[CAPACITY]{};
|
|
size_t _head = 0;
|
|
size_t _tail = 0;
|
|
size_t _count = 0;
|
|
std::atomic_flag _busy = ATOMIC_FLAG_INIT;
|
|
};
|
|
|
|
// A bounded, lossy record queue for transports whose low-level write must run
|
|
// in one owner task. Producers only copy complete records into this queue; they
|
|
// never touch the delegate. The owner calls drainOne() from its own task. This
|
|
// is particularly important for nRF52 TinyUSB: starting a CDC transfer from an
|
|
// application task can race control transfers handled by TinyUSB's high-
|
|
// priority task, while an unread host can leave that race armed indefinitely.
|
|
template <size_t SLOT_COUNT = 8, size_t MAX_WRITE_SIZE = 256>
|
|
class TaskOwnedWriteStream : public Stream {
|
|
public:
|
|
static_assert(SLOT_COUNT >= 2, "record queue needs at least two slots");
|
|
|
|
explicit TaskOwnedWriteStream(Stream& delegate) : _delegate(delegate) {}
|
|
|
|
int available() override { return _delegate.available(); }
|
|
int read() override { return _delegate.read(); }
|
|
int peek() override { return _delegate.peek(); }
|
|
|
|
// Flushing is owned by the transport task. A producer must never re-enter
|
|
// the delegate through this facade.
|
|
void flush() override {}
|
|
|
|
int availableForWrite() override {
|
|
const size_t head = _head.load(std::memory_order_relaxed);
|
|
const size_t next = increment(head);
|
|
return next == _tail.load(std::memory_order_acquire)
|
|
? 0
|
|
: static_cast<int>(MAX_WRITE_SIZE);
|
|
}
|
|
|
|
size_t write(uint8_t value) override { return write(&value, 1); }
|
|
|
|
size_t write(const uint8_t* data, size_t size) override {
|
|
if (data == nullptr || size == 0 || size > MAX_WRITE_SIZE) return 0;
|
|
if (_producer_busy.test_and_set(std::memory_order_acquire)) return 0;
|
|
|
|
const size_t head = _head.load(std::memory_order_relaxed);
|
|
const size_t next = increment(head);
|
|
if (next == _tail.load(std::memory_order_acquire)) {
|
|
_producer_busy.clear(std::memory_order_release);
|
|
return 0;
|
|
}
|
|
|
|
Record& record = _records[head];
|
|
memcpy(record.data, data, size);
|
|
record.size = size;
|
|
_head.store(next, std::memory_order_release);
|
|
_producer_busy.clear(std::memory_order_release);
|
|
return size;
|
|
}
|
|
|
|
// Called only by the delegate's owner task. A short delegate write is
|
|
// discarded rather than retried, because replaying a prefix would duplicate
|
|
// bytes and diagnostics are intentionally best effort.
|
|
size_t drainOne() {
|
|
const size_t tail = _tail.load(std::memory_order_relaxed);
|
|
if (tail == _head.load(std::memory_order_acquire)) return 0;
|
|
|
|
const Record& record = _records[tail];
|
|
const int available = _delegate.availableForWrite();
|
|
if (available < 0 || static_cast<size_t>(available) < record.size) return 0;
|
|
|
|
size_t written = _delegate.write(record.data, record.size);
|
|
if (written > record.size) written = record.size;
|
|
if (written > 0) {
|
|
_tail.store(increment(tail), std::memory_order_release);
|
|
}
|
|
return written;
|
|
}
|
|
|
|
// Called by the owner task, or after its transport callback has been disabled
|
|
// so no consumer can race this cursor. Producers may publish concurrently;
|
|
// loading head before storing tail preserves that newly published record.
|
|
void discardPending() {
|
|
_tail.store(_head.load(std::memory_order_acquire),
|
|
std::memory_order_release);
|
|
}
|
|
|
|
size_t queuedRecordCount() const {
|
|
const size_t head = _head.load(std::memory_order_acquire);
|
|
const size_t tail = _tail.load(std::memory_order_acquire);
|
|
return head >= tail ? head - tail : SLOT_COUNT - tail + head;
|
|
}
|
|
|
|
private:
|
|
struct Record {
|
|
size_t size = 0;
|
|
uint8_t data[MAX_WRITE_SIZE]{};
|
|
};
|
|
|
|
static constexpr size_t increment(size_t value) {
|
|
return value + 1 == SLOT_COUNT ? 0 : value + 1;
|
|
}
|
|
|
|
Stream& _delegate;
|
|
Record _records[SLOT_COUNT];
|
|
std::atomic<size_t> _head{0};
|
|
std::atomic<size_t> _tail{0};
|
|
std::atomic_flag _producer_busy = ATOMIC_FLAG_INIT;
|
|
};
|
|
|
|
// A Stream facade for a transport whose underlying write() may wait for FIFO
|
|
// space. It never retries or forwards a write larger than one nRF52 TinyUSB TX
|
|
// FIFO. Print::printf() can report a length larger than its 256-byte scratch
|
|
// buffer; rejecting such a call here also prevents that stale length from
|
|
// making the delegate read beyond the scratch buffer.
|
|
template <size_t MAX_WRITE_SIZE = 256>
|
|
class WholeRecordNonBlockingStream : public Stream {
|
|
public:
|
|
explicit WholeRecordNonBlockingStream(Stream& delegate)
|
|
: _delegate(delegate) {}
|
|
|
|
int available() override { return _delegate.available(); }
|
|
int read() override { return _delegate.read(); }
|
|
int peek() override { return _delegate.peek(); }
|
|
void flush() override { _delegate.flush(); }
|
|
int availableForWrite() override { return _delegate.availableForWrite(); }
|
|
|
|
size_t write(uint8_t value) override {
|
|
return write(&value, 1);
|
|
}
|
|
|
|
size_t write(const uint8_t* data, size_t size) override {
|
|
if (data == nullptr || size == 0 || size > MAX_WRITE_SIZE) return 0;
|
|
|
|
// Adafruit nRF52 Print::printf() formats into char[256], then forwards
|
|
// vsnprintf()'s required length without clamping it to that buffer. An
|
|
// exactly 256-character result therefore arrives here as 255 characters
|
|
// plus the formatter's trailing NUL. Drop that truncated record as a
|
|
// whole, just as we do for reported lengths greater than the buffer.
|
|
constexpr size_t PRINTF_SCRATCH_SIZE = 256;
|
|
if (size == PRINTF_SCRATCH_SIZE && data[size - 1] == '\0') return 0;
|
|
|
|
// Never wait for another application writer. All diagnostic access to the
|
|
// delegate is routed through this facade; the USB task can only drain the
|
|
// TX FIFO, so free capacity cannot shrink while this gate is held.
|
|
if (_writer_busy.test_and_set(std::memory_order_acquire)) return 0;
|
|
|
|
size_t written = 0;
|
|
const int available = _delegate.availableForWrite();
|
|
if (available >= 0 && static_cast<size_t>(available) >= size) {
|
|
written = _delegate.write(data, size);
|
|
}
|
|
|
|
_writer_busy.clear(std::memory_order_release);
|
|
return written;
|
|
}
|
|
|
|
private:
|
|
Stream& _delegate;
|
|
std::atomic_flag _writer_busy = ATOMIC_FLAG_INIT;
|
|
};
|
|
|
|
} // namespace mesh
|