From 9d16d6cc6dedcc7aa3dfda35874d565cb1b9dd5d Mon Sep 17 00:00:00 2001 From: cjkas Date: Sat, 10 Oct 2026 20:39:18 +0200 Subject: [PATCH] fix: address radio TX task review findings - Abort in-flight transmissions on flush so config save, frequency scan and OTA never wait on the radio lock for a whole job - Suspend TX during frequency scan and OTA - Refuse commands the radio queue cannot take before consuming a rolling code or moving simulated shade state; shift movement start when a job goes on air late - Apply radio config changes under the radio lock - Re-check receiver idle before repeater transmissions - Return 503 from web handlers when the command queue is full - Restore 200 ms settle delay before the set-My command - Report RSSI 0 instead of a stale value when the radio is busy - Encode each frame once and centralise the repeat clamp Co-Authored-By: Claude Sonnet 5.5 --- src/Somfy.cpp | 203 ++++++++++++++++++++++++++++++++++++++------------ src/Somfy.h | 30 ++++++-- src/Web.cpp | 42 ++++++----- 3 files changed, 204 insertions(+), 71 deletions(-) diff --git a/src/Somfy.cpp b/src/Somfy.cpp index 935441b..fe9cd06 100644 --- a/src/Somfy.cpp +++ b/src/Somfy.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "driver/gpio.h" #include "esp_log.h" @@ -40,12 +41,20 @@ uint8_t rxmode = 0; // Indicates whether the radio is in receive mode. Just to #define TX_QUEUE_DELAY 100 #define TX_TASK_PRIORITY 5 #define TX_TASK_STACK 4096 +#define TX_RX_IDLE_WAIT 300 // Max ms a repeater frame waits for an incoming frame to finish. // Serializes all access to the CC1101 (SPI) and the TX/RX pins between the // radio task, the main loop and the async web server task. static SemaphoreHandle_t radioMutex = nullptr; static QueueHandle_t txJobQueue = nullptr; +static QueueHandle_t txStartQueue = nullptr; static TaskHandle_t txTaskHandle = nullptr; +// Bumped by flushTx(). Queued jobs from an older generation are dropped and a +// job in flight stops after its current frame so nobody waits on the radio lock +// for a whole transmission. +static std::atomic txGen{0}; +// Set while the radio must not transmit at all (frequency scan, OTA). +static volatile bool txSuspended = false; struct RadioLock { bool locked = true; // Before the mutex exists there is nothing to serialize against. bool owned = false; @@ -54,6 +63,16 @@ struct RadioLock { } ~RadioLock() { if(owned) xSemaphoreGiveRecursive(radioMutex); } }; +// Encodes the first frame of a transmission into the job. Encoding here keeps +// its side effects on the frame (valid flag, default step size) visible to the +// caller and the radio task only re-encodes the 80-bit repeat frames. +static void encodeTxJob(somfy_tx_job_t &job, somfy_frame_t &frame, uint8_t repeat) { + frame.encodeFrame(job.tx.payload); + job.tx.hwsync = frame.bitLength == 56 ? 2 : 12; + job.tx.bit_length = frame.bitLength; + job.frame = frame; + job.repeats = repeat; +} int sort_asc(const void *cmp1, const void *cmp2) { int a = *((uint8_t *)cmp1); @@ -266,8 +285,9 @@ void somfy_frame_t::decodeFrame(somfy_rx_t *rx) { this->bitLength = rx->bit_length; { // Don't stall the main loop behind a transmission just to read the RSSI. + // The frame object is reused so clear the RSSI rather than reporting a stale one. RadioLock lock(0); - if(lock.locked) this->rssi = ELECHOUSE_cc1101.getRssi(); + this->rssi = lock.locked ? ELECHOUSE_cc1101.getRssi() : 0; } this->decodeFrame(rx->payload); } @@ -1347,7 +1367,8 @@ void SomfyShade::checkMovement() { if(this->myPos == this->currentPos) this->p_myPos(-1); else this->p_myPos(this->currentPos); } - SomfyRemote::sendCommand(somfy_commands::My, SETMY_REPEATS); + // Give the motor time to settle at the target before it is told to store it. + SomfyRemote::sendCommandAfter(somfy_commands::My, SETMY_REPEATS, 0, 200); this->settingMyPos = false; this->commitMyPosition(); this->emitState(); @@ -2731,6 +2752,14 @@ void SomfyShade::setTiltMovement(int8_t dir) { this->emitState(); } } +void SomfyShade::shiftMovementStart(uint32_t queuedAt, uint32_t startedAt) { + // The movement was started when the command was queued but the motor only + // starts once the frame is on air. Move the start forward so the simulated + // position does not run ahead of the motor. + const uint32_t wait = startedAt - queuedAt; + if(this->direction != 0 && (uint32_t)((uint32_t)this->moveStart - queuedAt) < wait) this->moveStart = startedAt; + if(this->tiltDirection != 0 && (uint32_t)((uint32_t)this->tiltStart - queuedAt) < wait) this->tiltStart = startedAt; +} void SomfyShade::setMovement(int8_t dir) { int8_t currDir = this->direction; int8_t currTiltDir = this->tiltDirection; @@ -2868,6 +2897,8 @@ void SomfyShade::moveToMyPosition() { void SomfyShade::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); } void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { ESP_LOGD(TAG, "Send command start"); + // Don't move the simulated state for a command the radio can't take. + if(!this->canTransmit()) return; // This sendCommand function will always be called externally. sendCommand at the remote level // is expected to be called internally when the motor needs commanded. if(this->bitLength == 0) this->bitLength = somfy.transceiver.config.type; @@ -2951,6 +2982,7 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz void SomfyGroup::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); } void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { ESP_LOGI(TAG, "[Group %u] sendCommand cmd=%s repeat=%u", this->getGroupId(), translateSomfyCommand(cmd).c_str(), repeat); + if(!this->canTransmit()) return; // This sendCommand function will always be called externally. sendCommand at the remote level // is expected to be called internally when the motor needs commanded. if(this->bitLength == 0) this->bitLength = somfy.transceiver.config.type; @@ -2985,6 +3017,7 @@ void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz } void SomfyShade::sendTiltCommand(somfy_commands cmd) { + if(!this->canTransmit()) return; if(cmd == somfy_commands::Up) { SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : this->repeats); this->p_tiltTarget(0.0f); @@ -2999,6 +3032,7 @@ void SomfyShade::sendTiltCommand(somfy_commands cmd) { } } void SomfyShade::moveToTiltTarget(float target) { + if(!this->canTransmit()) return; somfy_commands cmd = somfy_commands::My; if(target < this->currentTiltPos) cmd = somfy_commands::Up; @@ -3030,6 +3064,7 @@ void SomfyShade::moveToTarget(float pos, float tilt) { this->emitState(); return; } + if(!this->canTransmit()) return; if(this->tiltType == tilt_types::tiltonly) { this->p_target(100.0f); this->p_myPos(-1.0f); @@ -3438,6 +3473,27 @@ void SomfyShadeController::processFrame(somfy_frame_t &frame, bool internal) { if(this->shades[i].getShadeId() != 255) this->shades[i].processFrame(frame, internal); } } +void SomfyShadeController::shiftMovementStart(uint32_t remoteAddress, uint32_t queuedAt, uint32_t startedAt) { + // Same matching as SomfyShade::processFrame for shade and linked remotes. + for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { + SomfyShade *shade = &this->shades[i]; + if(shade->getShadeId() == 255) continue; + bool hasRemote = shade->getRemoteAddress() == remoteAddress; + for(uint8_t j = 0; !hasRemote && j < SOMFY_MAX_LINKED_REMOTES; j++) + hasRemote = shade->linkedRemotes[j].getRemoteAddress() == remoteAddress; + if(hasRemote) shade->shiftMovementStart(queuedAt, startedAt); + } + // Group commands move their member shades through processInternalCommand. + for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) { + SomfyGroup *group = &this->groups[i]; + if(group->getGroupId() == 255 || group->getRemoteAddress() != remoteAddress) continue; + for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) { + if(group->linkedShades[j] == 0) continue; + SomfyShade *shade = this->getShadeById(group->linkedShades[j]); + if(shade) shade->shiftMovementStart(queuedAt, startedAt); + } + } +} void SomfyShadeController::processWaitingFrame() { for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) if(this->shades[i].getShadeId() != 255) this->shades[i].processWaitingFrame(); @@ -3783,6 +3839,7 @@ somfy_commands SomfyRemote::transformCommand(somfy_commands cmd) { return cmd; } void SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat) { + if(!this->canTransmit()) return; uint8_t flags = (this->flags >> 4) & 0x0F; if(isWindy > 0) flags |= 0x01; if(isSunny > 0) flags |= 0x02; @@ -3800,11 +3857,20 @@ void SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repe ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d REPEAT:%d", translateSomfyCommand(this->lastFrame.cmd).c_str(), this->lastFrame.remoteAddress, this->lastFrame.rollingCode, repeat); - somfy.sendFrame(this->lastFrame, repeat); - somfy.processFrame(this->lastFrame, true); + if(somfy.sendFrame(this->lastFrame, repeat)) somfy.processFrame(this->lastFrame, true); +} +bool SomfyRemote::canTransmit() { + // GPIO remotes don't use the radio. + if(this->proto == radio_proto::GP_Relay || this->proto == radio_proto::GP_Remote) return true; + if(somfy.transceiver.canQueueTx()) return true; + ESP_LOGE(TAG, "Radio TX unavailable, dropping command for %d", this->getRemoteAddress()); + return false; } void SomfyRemote::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); } -void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { +void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { this->sendCommandAfter(cmd, repeat, stepSize, 0); } +void SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs) { + // Check before taking a rolling code so a dropped command doesn't consume one. + if(!this->canTransmit()) return; this->lastFrame.rollingCode = this->getNextRollingCode(); this->lastFrame.remoteAddress = this->getRemoteAddress(); this->lastFrame.cmd = this->transformCommand(cmd); @@ -3836,7 +3902,8 @@ void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSi ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d REPEAT:%d", translateSomfyCommand(this->lastFrame.cmd).c_str(), this->lastFrame.remoteAddress, this->lastFrame.rollingCode, repeat); - somfy.sendFrame(this->lastFrame, repeat); + // Only simulate the command if it is actually going on air. + if(!somfy.sendFrame(this->lastFrame, repeat, delayMs)) return; } somfy.processFrame(this->lastFrame, true); } @@ -3854,30 +3921,22 @@ void SomfyRemote::repeatFrame(uint8_t repeat) { this->triggerGPIOs(this->lastFrame); return; } - if(repeat > MAX_TX_REPEATS) repeat = MAX_TX_REPEATS; - // The radio task encodes the first frame with the current repeat counter and - // then counts up from there for each repeat frame. - byte frm[10]; - this->lastFrame.encodeFrame(frm); + repeat = txRepeats(repeat); + // The first frame is encoded with the current repeat counter and the radio + // task counts up from there for each repeat frame. somfy_tx_job_t job; - job.frame = this->lastFrame; - job.repeats = repeat; + encodeTxJob(job, this->lastFrame, repeat); job.firstRepeat = this->lastFrame.repeats + 2; this->lastFrame.repeats += repeat + 1; somfy.transceiver.queueTx(job); //somfy.processFrame(this->lastFrame, true); } -void SomfyShadeController::sendFrame(somfy_frame_t &frame, uint8_t repeat) { - if(repeat > MAX_TX_REPEATS) repeat = MAX_TX_REPEATS; - // Encode once here so any side effects of the encoding on the frame - // (valid flag, default step size) are visible to the caller immediately. - byte frm[10]; - frame.encodeFrame(frm); +bool SomfyShadeController::sendFrame(somfy_frame_t &frame, uint8_t repeat, uint16_t delayMs) { somfy_tx_job_t job; - job.frame = frame; - job.repeats = repeat; + encodeTxJob(job, frame, txRepeats(repeat)); job.firstRepeat = 1; - this->transceiver.queueTx(job); + if(delayMs > 0) job.notBefore = millis() + delayMs; + return this->transceiver.queueTx(job); } bool SomfyShadeController::deleteShade(uint8_t shadeId) { for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { @@ -4360,6 +4419,9 @@ float markFreq = 433.0f; int markRSSI = -100; uint32_t lastScan = 0; void Transceiver::beginFrequencyScan() { + // The scan retunes the radio so nothing may transmit until it ends. This + // also aborts a transmission in flight so the lock below is never held long. + if(this->config.enabled) this->suspendTx(); RadioLock lock; if(this->config.enabled) { this->disableReceive(); @@ -4411,6 +4473,7 @@ void Transceiver::endFrequencyScan() { if(interruptPin > 0) detachInterrupt(interruptPin); interruptPin = 0; this->config.apply(); + this->resumeTx(); this->emitFrequencyScan(); } } @@ -4542,7 +4605,14 @@ bool Transceiver::toJSON(JsonObject& obj) { bool Transceiver::fromJSON(JsonObject& obj) { if (!obj["config"].isNull()) { JsonObject objConfig = obj["config"]; - this->config.fromJSON(objConfig); + // Parse into a copy so the radio task never sees a half updated config, + // then swap it in once any transmission in flight has stopped. + transceiver_config_t cfg = this->config; + cfg.fromJSON(objConfig); + this->flushTx(); + RadioLock lock; + if(this->config.enabled && this->config.TXPin != cfg.TXPin) pinMode(this->config.TXPin, INPUT); + this->config = cfg; } return true; } @@ -4564,7 +4634,9 @@ bool Transceiver::save() { return true; } bool Transceiver::end() { - this->flushTx(); + // Nothing may transmit (and re-enable receive) after this. It is not + // resumed since the only caller is OTA which always reboots. + this->suspendTx(); RadioLock lock; this->disableReceive(); return true; @@ -4913,6 +4985,11 @@ bool Transceiver::begin() { } void Transceiver::loop() { somfy_rx_t rx; + // Jobs that went on air late. The shades were set moving when the job was + // queued so line their movement up with the motor. + somfy_tx_start_t started; + while(txStartQueue && xQueueReceive(txStartQueue, &started, 0) == pdTRUE) + somfy.shiftMovementStart(started.remoteAddress, started.queuedAt, started.startedAt); if (noiseDetected && rxmode != 3 && this->config.noiseDetection) { if (millis() - noiseStart > 100) { gpio_intr_enable((gpio_num_t)interruptPin); @@ -4944,51 +5021,64 @@ void Transceiver::loop() { // Repeater frames are handed to the radio task like any other transmission. somfy_tx_job_t job; job.raw = true; - tx_queue.pop(&job.rawTx); + tx_queue.pop(&job.tx); + { + somfy_tx_t &tx = job.tx; + char frame_buf[128]; + int fpos = snprintf(frame_buf, sizeof(frame_buf), "Sending frame %d - %d-BIT [", tx.hwsync, tx.bit_length); + for(uint8_t j = 0; j < 10; j++) { + fpos += snprintf(frame_buf + fpos, sizeof(frame_buf) - fpos, "%d%s", tx.payload[j], j < 9 ? ", " : ""); + } + snprintf(frame_buf + fpos, sizeof(frame_buf) - fpos, "]"); + ESP_LOGD(TAG, "%s", frame_buf); + } this->queueTx(job); tx_queue.delay_time = millis() + TX_QUEUE_DELAY; } } } -bool Transceiver::queueTx(const somfy_tx_job_t &job) { +bool Transceiver::queueTx(somfy_tx_job_t &job) { if(!txJobQueue) { ESP_LOGE(TAG, "Radio task not started, dropping transmission"); return false; } + if(txSuspended) { + ESP_LOGW(TAG, "Radio TX suspended, dropping transmission"); + return false; + } + job.gen = txGen; + job.queuedAt = millis(); if(xQueueSend(txJobQueue, &job, 0) != pdTRUE) { ESP_LOGE(TAG, "Radio TX queue full, dropping transmission"); return false; } return true; } +bool Transceiver::canQueueTx() { + return txJobQueue && txTaskHandle && !txSuspended && uxQueueSpacesAvailable(txJobQueue) > 0; +} void Transceiver::flushTx() { + ++txGen; if(txJobQueue) xQueueReset(txJobQueue); } +void Transceiver::suspendTx() { + txSuspended = true; + this->flushTx(); +} +void Transceiver::resumeTx() { txSuspended = false; } +// cpt_synchro_hw is updated by the RX interrupt and read here from the radio task. +static bool rxInProgress() { return *(volatile uint8_t *)&somfy_rx.cpt_synchro_hw != 0; } void Transceiver::transmitJob(somfy_tx_job_t &job) { - if(job.raw) { - somfy_tx_t &tx = job.rawTx; - char frame_buf[128]; - int fpos = snprintf(frame_buf, sizeof(frame_buf), "Sending frame %d - %d-BIT [", tx.hwsync, tx.bit_length); - for(uint8_t j = 0; j < 10; j++) { - fpos += snprintf(frame_buf + fpos, sizeof(frame_buf) - fpos, "%d%s", tx.payload[j], j < 9 ? ", " : ""); - } - snprintf(frame_buf + fpos, sizeof(frame_buf) - fpos, "]"); - ESP_LOGD(TAG, "%s", frame_buf); - this->beginTransmit(); - this->sendFrame(tx.payload, tx.hwsync, tx.bit_length); - this->endTransmit(); - return; - } - somfy_frame_t &frame = job.frame; - byte frm[10]; - frame.encodeFrame(frm); + somfy_tx_t &tx = job.tx; this->beginTransmit(); - this->sendFrame(frm, frame.bitLength == 56 ? 2 : 12, frame.bitLength); + this->sendFrame(tx.payload, tx.hwsync, tx.bit_length); for(uint8_t i = 0; i < job.repeats; i++) { + // Stop early when flushed so whoever flushed gets the radio within a frame. + if(job.gen != txGen) break; // For each 80-bit frame we need to adjust the byte encoding for the // silence. - if(frame.bitLength == 80) frame.encode80BitFrame(&frm[0], (uint8_t)(job.firstRepeat + i)); - this->sendFrame(frm, frame.bitLength == 56 ? 7 : 6, frame.bitLength); + if(tx.bit_length == 80) job.frame.encode80BitFrame(tx.payload, (uint8_t)(job.firstRepeat + i)); + this->sendFrame(tx.payload, tx.bit_length == 56 ? 7 : 6, tx.bit_length); } this->endTransmit(); } @@ -4997,8 +5087,28 @@ void Transceiver::txTask(void *arg) { somfy_tx_job_t job; for(;;) { if(xQueueReceive(txJobQueue, &job, portMAX_DELAY) != pdTRUE) continue; + if(txSuspended || job.gen != txGen) continue; + if(job.notBefore != 0) { + int32_t wait = (int32_t)(job.notBefore - millis()); + if(wait > 0) vTaskDelay(pdMS_TO_TICKS(wait)); + } + if(job.raw) { + // Don't step on a remote that is transmitting right now. + for(uint32_t waited = 0; rxInProgress() && waited < TX_RX_IDLE_WAIT; waited += 5) + vTaskDelay(pdMS_TO_TICKS(5)); + } { RadioLock lock; + // The queue may have been flushed while we waited. + if(txSuspended || job.gen != txGen) continue; + const uint32_t startedAt = millis(); + if(!job.raw && txStartQueue && startedAt - job.queuedAt > TX_QUEUE_DELAY * 2) { + somfy_tx_start_t started; + started.remoteAddress = job.frame.remoteAddress; + started.queuedAt = job.queuedAt; + started.startedAt = startedAt; + xQueueSend(txStartQueue, &started, 0); + } trans->transmitJob(job); } // Give the motors a full frame beat between separate transmissions. @@ -5008,6 +5118,7 @@ void Transceiver::txTask(void *arg) { void Transceiver::startTxTask() { if(txTaskHandle) return; if(!txJobQueue) txJobQueue = xQueueCreate(TX_JOB_QUEUE_SIZE, sizeof(somfy_tx_job_t)); + if(!txStartQueue) txStartQueue = xQueueCreate(TX_JOB_QUEUE_SIZE, sizeof(somfy_tx_start_t)); if(!txJobQueue) { ESP_LOGE(TAG, "Unable to create radio TX queue"); return; diff --git a/src/Somfy.h b/src/Somfy.h index 07753c1..0b7a435 100644 --- a/src/Somfy.h +++ b/src/Somfy.h @@ -205,14 +205,25 @@ struct somfy_frame_t { // radio time so this keeps a single job to roughly 6 seconds. #define MAX_TX_REPEATS 40 #define TX_JOB_QUEUE_SIZE 32 -// A transmission handed off to the radio task. The frame is a copy so the -// radio task never touches the remote's lastFrame. +inline uint8_t txRepeats(uint8_t repeat) { return repeat > MAX_TX_REPEATS ? MAX_TX_REPEATS : repeat; } +// 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. struct somfy_tx_job_t { + somfy_tx_t tx; // Encoded first frame with its hw sync count and bit length. somfy_frame_t frame; 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; // When true rawTx holds a pre-encoded frame (repeater). - somfy_tx_t rawTx; + bool raw = false; // Repeater frame. Waits for the receiver to be idle. + 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 { + uint32_t remoteAddress = 0; + uint32_t queuedAt = 0; + uint32_t startedAt = 0; }; class SomfyRoom { @@ -264,8 +275,10 @@ class SomfyRemote { void setSimMy(bool bSimMy); virtual void sendCommand(somfy_commands cmd); virtual void sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0); + void sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs); void sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat); void repeatFrame(uint8_t repeat); + bool canTransmit(); virtual uint16_t p_lastRollingCode(uint16_t code); somfy_commands transformCommand(somfy_commands cmd); virtual void triggerGPIOs(somfy_frame_t &frame); @@ -331,6 +344,7 @@ class SomfyShade : public SomfyRemote { void checkMovement(); void processFrame(somfy_frame_t &frame, bool internal = false); void processInternalCommand(somfy_commands cmd, uint8_t repeat = 1); + void shiftMovementStart(uint32_t queuedAt, uint32_t startedAt); void setTiltMovement(int8_t dir); void setMovement(int8_t dir); void setTarget(float target); @@ -521,8 +535,11 @@ class Transceiver { void disableReceive(); somfy_frame_t& lastFrame(); void sendFrame(byte *frame, uint8_t sync, uint8_t bitLength = 56); - bool queueTx(const somfy_tx_job_t &job); + bool queueTx(somfy_tx_job_t &job); + bool canQueueTx(); void flushTx(); + void suspendTx(); + void resumeTx(); void beginTransmit(); void endTransmit(); void emitFrame(somfy_frame_t *frame, somfy_rx_t *rx = nullptr); @@ -578,8 +595,9 @@ class SomfyShadeController { SomfyGroup * getGroupById(uint8_t groupId); SomfyShade * findShadeByRemoteAddress(uint32_t address); SomfyGroup * findGroupByRemoteAddress(uint32_t address); - void sendFrame(somfy_frame_t &frame, uint8_t repeats = 0); + bool sendFrame(somfy_frame_t &frame, uint8_t repeats = 0, uint16_t delayMs = 0); 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); void publish(); void processWaitingFrame(); diff --git a/src/Web.cpp b/src/Web.cpp index 699b4a5..c780449 100644 --- a/src/Web.cpp +++ b/src/Web.cpp @@ -47,20 +47,26 @@ static const char *TAG = "Web"; static QueueHandle_t webCmdQueue = nullptr; // Radio work for these handlers runs on the main loop via the command queue. -static void queueSetMyPosition(SomfyShade *shade, int8_t pos, int8_t tilt) { +static bool queueSetMyPosition(SomfyShade *shade, int8_t pos, int8_t tilt) { web_command_t cmd = {}; cmd.type = web_cmd_t::set_my_position; cmd.shadeId = shade->getShadeId(); cmd.position = pos; cmd.tiltPosition = tilt; - webServer.queueCommand(cmd); + return webServer.queueCommand(cmd); } -static void queueUnpairShade(SomfyShade *shade) { +static bool queueUnpairShade(SomfyShade *shade) { web_command_t cmd = {}; cmd.type = web_cmd_t::unpair_shade; cmd.shadeId = shade->getShadeId(); + if(!webServer.queueCommand(cmd)) return false; // Reflect the change in the response; the main loop sets it again and saves. - if(webServer.queueCommand(cmd)) shade->paired = false; + shade->paired = false; + return true; +} +// Sent when a command can't be handed to the main loop. +static void sendQueueFull(AsyncWebServerRequest *request) { + request->send(503, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Command queue full\"}")); } AsyncWebServer asyncServer(80); @@ -92,9 +98,7 @@ bool Web::queueCommand(const web_command_t &cmd) { if(!webCmdQueue) return false; // This runs on the async_tcp task which also services the sockets so it must // never wait on the main loop or the radio. - web_command_t c = cmd; - if(c.repeat > MAX_TX_REPEATS) c.repeat = MAX_TX_REPEATS; - if(xQueueSend(webCmdQueue, &c, 0) != pdTRUE) { + if(xQueueSend(webCmdQueue, &cmd, 0) != pdTRUE) { ESP_LOGE(TAG, "Command queue full, dropping command"); return false; } @@ -812,7 +816,7 @@ void Web::handleShadeCommand(AsyncWebServerRequest *request, JsonVariant &json) cmd.command = command; cmd.repeat = repeat; cmd.stepSize = stepSize; - this->queueCommand(cmd); + if(!this->queueCommand(cmd)) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -853,7 +857,7 @@ void Web::handleGroupCommand(AsyncWebServerRequest *request, JsonVariant &json) cmd.command = command; cmd.repeat = repeat; cmd.stepSize = stepSize; - this->queueCommand(cmd); + if(!this->queueCommand(cmd)) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -890,7 +894,7 @@ void Web::handleTiltCommand(AsyncWebServerRequest *request, JsonVariant &json) { cmd.shadeId = shadeId; cmd.target = target; cmd.command = command; - this->queueCommand(cmd); + if(!this->queueCommand(cmd)) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -931,7 +935,7 @@ void Web::handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json) cmd.command = command; cmd.repeat = repeat; cmd.stepSize = stepSize; - this->queueCommand(cmd); + if(!this->queueCommand(cmd)) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginArray(); @@ -949,7 +953,7 @@ void Web::handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json) cmd.command = command; cmd.repeat = repeat; cmd.stepSize = stepSize; - this->queueCommand(cmd); + if(!this->queueCommand(cmd)) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -1043,7 +1047,7 @@ void Web::handleSetPositions(AsyncWebServerRequest *request, JsonVariant &json) cmd.shadeId = shadeId; cmd.position = pos; cmd.tiltPosition = tiltPos; - this->queueCommand(cmd); + if(!this->queueCommand(cmd)) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -1086,7 +1090,7 @@ void Web::handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json) { cmd.sunny = sunny; cmd.windy = windy; cmd.repeat = repeat; - this->queueCommand(cmd); + if(!this->queueCommand(cmd)) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -1105,7 +1109,7 @@ void Web::handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json) { cmd.sunny = sunny; cmd.windy = windy; cmd.repeat = repeat; - this->queueCommand(cmd); + if(!this->queueCommand(cmd)) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -1626,7 +1630,7 @@ void Web::begin() { if(tilt < 0) tilt = shade->myPos; if(shade->tiltType == tilt_types::none) tilt = -1; if(pos >= 0 && pos <= 100) - queueSetMyPosition(shade, shade->transformPosition(pos), shade->transformPosition(tilt)); + if(!queueSetMyPosition(shade, shade->transformPosition(pos), shade->transformPosition(tilt))) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -1653,7 +1657,7 @@ void Web::begin() { if(tilt < 0) tilt = shade->myPos; if(shade->tiltType == tilt_types::none) tilt = -1; if(pos >= 0 && pos <= 100) - queueSetMyPosition(shade, shade->transformPosition(pos), shade->transformPosition(tilt)); + if(!queueSetMyPosition(shade, shade->transformPosition(pos), shade->transformPosition(tilt))) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -1778,7 +1782,7 @@ void Web::begin() { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to unpair\"}")); } else { - queueUnpairShade(shade); + if(!queueUnpairShade(shade)) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -1797,7 +1801,7 @@ void Web::begin() { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to unpair\"}")); } else { - queueUnpairShade(shade); + if(!queueUnpairShade(shade)) { sendQueueFull(request); return; } AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject();