fix: keep a repeater frame until the radio queue takes it

The frame was popped before queueTx(), so whenever the radio queue was busy
it was lost.  Peek it instead, only remove it once it is queued (or TX is
suspended), and don't try while the queue has no room.  The repeater buffer
moves to TxBuffer.h/.cpp so it can be unit tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-12 18:31:39 +02:00
parent e1c76bc2af
commit 7e9687c023
6 changed files with 227 additions and 80 deletions

View file

@ -3805,52 +3805,6 @@ static int16_t bitMin = SYMBOL * TOLERANCE_MIN;
static somfy_rx_t somfy_rx;
static somfy_rx_queue_t rx_queue;
static somfy_tx_queue_t tx_queue;
bool somfy_tx_queue_t::pop(somfy_tx_t *tx) {
// Read the oldest index.
for(int8_t i = MAX_TX_BUFFER - 1; i >= 0; i--) {
if(this->index[i] < MAX_TX_BUFFER) {
uint8_t ndx = this->index[i];
memcpy(tx, &this->items[ndx], sizeof(somfy_tx_t));
this->items[ndx].clear();
if(this->length > 0) this->length--;
this->index[i] = 255;
return true;
}
}
return false;
}
void somfy_tx_queue_t::push(somfy_rx_t *rx) { this->push(rx->cpt_synchro_hw, rx->payload, rx->bit_length); }
void somfy_tx_queue_t::push(uint8_t hwsync, uint8_t *payload, uint8_t bit_length) {
if(this->length >= MAX_TX_BUFFER) {
// We have overflowed the buffer simply empty the last item
// in this instance we will simply throw it away.
uint8_t ndx = this->index[MAX_TX_BUFFER - 1];
if(ndx < MAX_TX_BUFFER) this->items[ndx].clear();
this->index[MAX_TX_BUFFER - 1] = 255;
this->length--;
}
uint8_t first = 0;
// Place this record in the first empty slot. There will
// be one since we cleared a space above should there
// be an overflow.
for(uint8_t i = 0; i < MAX_TX_BUFFER; i++) {
if(this->items[i].bit_length == 0) {
first = i;
this->items[i].bit_length = bit_length;
this->items[i].hwsync = hwsync;
memcpy(&this->items[i].payload, payload, sizeof(this->items[i].payload));
break;
}
}
// Move the index so that it is the at position 0. The oldest item will fall off.
for(uint8_t i = MAX_TX_BUFFER - 1; i > 0; i--) {
this->index[i] = this->index[i - 1];
}
this->length++;
// When popping from the queue we always pull from the end
this->index[0] = first;
this->delay_time = millis() + TX_QUEUE_DELAY; // We do not want to process this frame until a full frame beat has passed.
}
void somfy_rx_queue_t::init() {
ESP_LOGD(TAG, "Initializing RX Queue");
for (uint8_t i = 0; i < MAX_RX_BUFFER; i++)
@ -4715,7 +4669,9 @@ void Transceiver::loop() {
else if (this->receive(&rx)) {
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
if(somfy.repeaters[i] == frame.remoteAddress) {
tx_queue.push(&rx);
tx_queue.push(rx.cpt_synchro_hw, rx.payload, rx.bit_length);
// We do not want to process this frame until a full frame beat has passed.
tx_queue.delay_time = millis() + TX_QUEUE_DELAY;
ESP_LOGD(TAG, "Queued repeater frame...");
break;
}
@ -4725,11 +4681,14 @@ void Transceiver::loop() {
else {
somfy.processWaitingFrame();
// Check to see if there is anything in the buffer
if(tx_queue.length > 0 && millis() > tx_queue.delay_time && somfy_rx.cpt_synchro_hw == 0) {
// Repeater frames are handed to the radio task like any other transmission.
// Repeater frames are handed to the radio task like any other transmission.
// One stays here until the radio has room for it rather than being lost
// while the radio is busy. While TX is suspended nothing goes out anyway.
if(tx_queue.length > 0 && millis() > tx_queue.delay_time && somfy_rx.cpt_synchro_hw == 0 &&
(this->txSuspended() || this->txHasRoom())) {
somfy_tx_job_t job;
job.raw = true;
tx_queue.pop(&job.tx);
tx_queue.peek(&job.tx);
{
somfy_tx_t &tx = job.tx;
char frame_buf[128];
@ -4740,7 +4699,10 @@ void Transceiver::loop() {
snprintf(frame_buf + fpos, sizeof(frame_buf) - fpos, "]");
ESP_LOGD(TAG, "%s", frame_buf);
}
this->queueTx(job);
if(this->queueTx(job) || this->txSuspended()) {
somfy_tx_t sent;
tx_queue.pop(&sent);
}
tx_queue.delay_time = millis() + TX_QUEUE_DELAY;
}
}

View file

@ -22,6 +22,7 @@
#include "SomfyFrame.h"
#include "TxBuffer.h"
enum class group_types : byte {
channel = 0x00
};
@ -54,7 +55,6 @@ enum class tilt_types : byte {
#define MAX_TIMINGS 300
#define MAX_RX_BUFFER 3
#define MAX_TX_BUFFER 5
typedef enum {
waiting_synchro = 0,
@ -95,33 +95,6 @@ struct somfy_rx_queue_t {
void push(somfy_rx_t *rx);
bool pop(somfy_rx_t *rx);
};
struct somfy_tx_t {
void clear() {
this->hwsync = 0;
this->bit_length = 0;
memset(this->payload, 0x00, sizeof(this->payload));
}
uint8_t hwsync = 0;
uint8_t bit_length = 0;
uint8_t payload[10] = {};
};
struct somfy_tx_queue_t {
somfy_tx_queue_t() { this->clear(); }
void clear() {
for (uint8_t i = 0; i < MAX_TX_BUFFER; i++) {
this->index[i] = 255;
this->items[i].clear();
}
this->length = 0;
}
unsigned long delay_time = 0;
uint8_t length = 0;
uint8_t index[MAX_TX_BUFFER] = {255};
somfy_tx_t items[MAX_TX_BUFFER];
bool pop(somfy_tx_t *tx);
void push(somfy_rx_t *rx); // Used for repeats
void push(uint8_t hwsync, byte *payload, uint8_t bit_length);
};
enum class somfy_flags_t : byte {
SunFlag = 0x01,

55
src/TxBuffer.cpp Normal file
View file

@ -0,0 +1,55 @@
#include "TxBuffer.h"
int8_t somfy_tx_queue_t::oldest() const {
// The newest index is at position 0 so the oldest is the last one in use.
for(int8_t i = MAX_TX_BUFFER - 1; i >= 0; i--) {
if(this->index[i] < MAX_TX_BUFFER) return i;
}
return -1;
}
bool somfy_tx_queue_t::peek(somfy_tx_t *tx) const {
const int8_t i = this->oldest();
if(i < 0) return false;
memcpy(tx, &this->items[this->index[i]], sizeof(somfy_tx_t));
return true;
}
bool somfy_tx_queue_t::pop(somfy_tx_t *tx) {
const int8_t i = this->oldest();
if(i < 0) return false;
uint8_t ndx = this->index[i];
memcpy(tx, &this->items[ndx], sizeof(somfy_tx_t));
this->items[ndx].clear();
if(this->length > 0) this->length--;
this->index[i] = 255;
return true;
}
void somfy_tx_queue_t::push(uint8_t hwsync, uint8_t *payload, uint8_t bit_length) {
if(this->length >= MAX_TX_BUFFER) {
// We have overflowed the buffer simply empty the last item
// in this instance we will simply throw it away.
uint8_t ndx = this->index[MAX_TX_BUFFER - 1];
if(ndx < MAX_TX_BUFFER) this->items[ndx].clear();
this->index[MAX_TX_BUFFER - 1] = 255;
this->length--;
}
uint8_t first = 0;
// Place this record in the first empty slot. There will
// be one since we cleared a space above should there
// be an overflow.
for(uint8_t i = 0; i < MAX_TX_BUFFER; i++) {
if(this->items[i].bit_length == 0) {
first = i;
this->items[i].bit_length = bit_length;
this->items[i].hwsync = hwsync;
memcpy(&this->items[i].payload, payload, sizeof(this->items[i].payload));
break;
}
}
// Move the index so that it is the at position 0. The oldest item will fall off.
for(uint8_t i = MAX_TX_BUFFER - 1; i > 0; i--) {
this->index[i] = this->index[i - 1];
}
this->length++;
// When popping from the queue we always pull from the end
this->index[0] = first;
}

43
src/TxBuffer.h Normal file
View file

@ -0,0 +1,43 @@
#ifndef TX_BUFFER_H
#define TX_BUFFER_H
#include <Arduino.h>
// Repeater frames waiting to be handed to the radio task. Kept free of the
// radio and timers so it can be unit tested on the PC (pio test -e native).
#define MAX_TX_BUFFER 5
struct somfy_tx_t {
void clear() {
this->hwsync = 0;
this->bit_length = 0;
memset(this->payload, 0x00, sizeof(this->payload));
}
uint8_t hwsync = 0;
uint8_t bit_length = 0;
uint8_t payload[10] = {};
};
struct somfy_tx_queue_t {
somfy_tx_queue_t() { this->clear(); }
void clear() {
for (uint8_t i = 0; i < MAX_TX_BUFFER; i++) {
this->index[i] = 255;
this->items[i].clear();
}
this->length = 0;
}
unsigned long delay_time = 0; // Set by the caller, not before this millis().
uint8_t length = 0;
uint8_t index[MAX_TX_BUFFER] = {255};
somfy_tx_t items[MAX_TX_BUFFER];
// The oldest frame without removing it, so it can stay queued until the radio
// takes it.
bool peek(somfy_tx_t *tx) const;
bool pop(somfy_tx_t *tx);
// When full the oldest frame is thrown away.
void push(uint8_t hwsync, byte *payload, uint8_t bit_length);
private:
// Position in index of the oldest frame, -1 when empty.
int8_t oldest() const;
};
#endif