fix: refuse a radio job when its outcomes might not fit in the event ring

A full event ring threw an outcome away, breaking the rule that every job
we submit ends with exactly one of finished, aborted or dropped.  A lost
outcome left a favorite uncommitted and a held shade waiting out its 30s
timeout.  submit() and hasRoom() now keep room for two outcomes of every
job not yet ended, so an event can't be lost; if loop() stops collecting
them, new jobs are refused instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-13 15:05:06 +02:00
parent b768c5f987
commit f0e8b1d896
3 changed files with 68 additions and 3 deletions

View file

@ -21,6 +21,10 @@ uint32_t RadioScheduler::submit(somfy_tx_job_t &job, bool priority, uint16_t del
ESP_LOGE(TAG, "Radio TX queue full, dropping transmission");
return 0;
}
if(!this->outcomesFit()) {
ESP_LOGE(TAG, "Radio TX outcomes not collected, dropping transmission");
return 0;
}
// 0 means no job so it is skipped on wrap.
if(++this->lastSeq == 0) ++this->lastSeq;
job.seq = this->lastSeq;
@ -40,7 +44,13 @@ uint32_t RadioScheduler::submit(somfy_tx_job_t &job, bool priority, uint16_t del
return job.seq;
}
bool RadioScheduler::hasRoom(bool priority) const {
return !this->suspendMask && admits(TX_JOB_QUEUE_SIZE - this->count, priority);
return !this->suspendMask && admits(TX_JOB_QUEUE_SIZE - this->count, priority) && this->outcomesFit();
}
bool RadioScheduler::outcomesFit() const {
// Each job not yet ended reports at most two outcomes: started, then how it
// ended. Counting repeater frames too keeps this simple and errs safe.
const uint32_t unended = this->count + (this->taken ? 1u : 0u) + 1u;
return this->eventCount + 2u * unended <= TX_EVENT_QUEUE_SIZE;
}
bool RadioScheduler::waitingFor(uint32_t remoteAddress) const {
for(uint8_t i = 0; i < this->count; i++) {
@ -112,7 +122,8 @@ void RadioScheduler::post(tx_event_t type, const somfy_tx_job_t &job, uint32_t a
// Repeater frames don't belong to any of our remotes.
if(job.raw) return;
if(this->eventCount >= TX_EVENT_QUEUE_SIZE) {
ESP_LOGW(TAG, "Radio TX event queue full");
// submit() leaves room for every outcome so this can't happen short of a bug.
ESP_LOGE(TAG, "Radio TX event queue full, outcome lost");
return;
}
somfy_tx_event_t &evt = this->events[(this->eventHead + this->eventCount) % TX_EVENT_QUEUE_SIZE];

View file

@ -18,7 +18,8 @@
class RadioScheduler {
public:
// Queues a job. Its ticket comes back and is also set in job.seq, 0 when
// the queue is full or suspended. A priority job (a stop) may use the
// the queue is full or suspended, or when outcomes aren't collected with
// popEvent() fast enough to leave room for its own. A priority job (a stop) may use the
// reserved slots and goes ahead of every job queued at the back, unless a
// job of its remote is already waiting: overtaking that would put the
// rolling codes out of order. A delayed job doesn't go on air until
@ -66,6 +67,9 @@ class RadioScheduler {
somfy_tx_event_t events[TX_JOB_QUEUE_SIZE * 3];
uint8_t eventHead = 0;
uint8_t eventCount = 0;
// Whether the outcomes of every job not yet ended, and of one more, still
// fit in events. A job is refused otherwise so no outcome is ever lost.
bool outcomesFit() const;
// Whether a job of the remote is still waiting.
bool waitingFor(uint32_t remoteAddress) const;
void remove(uint8_t i);