fix: make radio TX queue flush-safe and report job outcomes

- Serialize queueTx and flushTx so a queued job can't be lost to a flush.
- Settings changes preempt only the job in flight; the queue survives.
- Flush drains the queue and reports each job as dropped.
- Track suspend reasons (scan, OTA) separately.
- Report started/dropped for every job on a single event queue.
- Add priority jobs that go to the front and may use reserved slots.
- Remove the MAX_TX_REPEATS clamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-11 12:08:48 +02:00
parent 9d16d6cc6d
commit a426efe53f
2 changed files with 117 additions and 62 deletions

View file

@ -201,11 +201,12 @@ struct somfy_frame_t {
bool isSynonym(somfy_frame_t &f);
void copy(somfy_frame_t &f);
};
// Upper bound on repeats for a single transmission. Each frame is ~140ms of
// radio time so this keeps a single job to roughly 6 seconds.
#define MAX_TX_REPEATS 40
#define TX_JOB_QUEUE_SIZE 32
inline uint8_t txRepeats(uint8_t repeat) { return repeat > MAX_TX_REPEATS ? MAX_TX_REPEATS : repeat; }
// Slots only priority jobs (stops) may use so a stop always fits in the queue.
#define TX_RESERVED_SLOTS 4
// Reasons the radio must not transmit. TX resumes once every reason is cleared.
#define TX_SUSPEND_SCAN 0x01
#define TX_SUSPEND_OTA 0x02
// A transmission handed off to the radio task. The first frame is encoded by
// the producer into tx. The frame is a copy so the radio task never touches the
// remote's lastFrame and is only used to re-encode 80-bit repeat frames.
@ -215,15 +216,22 @@ struct somfy_tx_job_t {
uint8_t repeats = 0; // Number of repeat frames after the first frame.
uint8_t firstRepeat = 1; // 80-bit repeat counter used for the first repeat frame.
bool raw = false; // Repeater frame. Waits for the receiver to be idle.
bool priority = false; // Goes to the front of the queue and may use the reserved slots.
uint32_t gen = 0; // Flush generation when queued. Stale jobs are dropped.
uint32_t queuedAt = 0; // millis() when queued.
uint32_t notBefore = 0; // millis() before which the job must not go on air (0 = none).
};
// Reported by the radio task when a queued job finally goes on air.
struct somfy_tx_start_t {
enum class tx_event_t : uint8_t {
started, // The job is going on air.
dropped // The job was flushed and will never go on air.
};
// Reported by the radio task for every queued non-repeater job.
struct somfy_tx_event_t {
tx_event_t type = tx_event_t::started;
somfy_commands cmd = somfy_commands::My;
uint32_t remoteAddress = 0;
uint32_t queuedAt = 0;
uint32_t startedAt = 0;
uint32_t at = 0; // millis() when the job started or was dropped.
};
class SomfyRoom {
@ -536,10 +544,10 @@ class Transceiver {
somfy_frame_t& lastFrame();
void sendFrame(byte *frame, uint8_t sync, uint8_t bitLength = 56);
bool queueTx(somfy_tx_job_t &job);
bool canQueueTx();
bool canQueueTx(bool priority = false);
void flushTx();
void suspendTx();
void resumeTx();
void suspendTx(uint8_t reason);
void resumeTx(uint8_t reason);
void beginTransmit();
void endTransmit();
void emitFrame(somfy_frame_t *frame, somfy_rx_t *rx = nullptr);
@ -595,7 +603,7 @@ class SomfyShadeController {
SomfyGroup * getGroupById(uint8_t groupId);
SomfyShade * findShadeByRemoteAddress(uint32_t address);
SomfyGroup * findGroupByRemoteAddress(uint32_t address);
bool sendFrame(somfy_frame_t &frame, uint8_t repeats = 0, uint16_t delayMs = 0);
bool sendFrame(somfy_frame_t &frame, uint8_t repeats = 0, uint16_t delayMs = 0, bool priority = false);
void processFrame(somfy_frame_t &frame, bool internal = false);
void shiftMovementStart(uint32_t remoteAddress, uint32_t queuedAt, uint32_t startedAt);
void emitState(uint8_t num = 255);