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 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-10 20:39:18 +02:00
parent 9a9afa5e86
commit 9d16d6cc6d
3 changed files with 204 additions and 71 deletions

View file

@ -4,6 +4,7 @@
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
#include <freertos/queue.h> #include <freertos/queue.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <atomic>
#include <esp_chip_info.h> #include <esp_chip_info.h>
#include "driver/gpio.h" #include "driver/gpio.h"
#include "esp_log.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_QUEUE_DELAY 100
#define TX_TASK_PRIORITY 5 #define TX_TASK_PRIORITY 5
#define TX_TASK_STACK 4096 #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 // 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. // radio task, the main loop and the async web server task.
static SemaphoreHandle_t radioMutex = nullptr; static SemaphoreHandle_t radioMutex = nullptr;
static QueueHandle_t txJobQueue = nullptr; static QueueHandle_t txJobQueue = nullptr;
static QueueHandle_t txStartQueue = nullptr;
static TaskHandle_t txTaskHandle = 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<uint32_t> txGen{0};
// Set while the radio must not transmit at all (frequency scan, OTA).
static volatile bool txSuspended = false;
struct RadioLock { struct RadioLock {
bool locked = true; // Before the mutex exists there is nothing to serialize against. bool locked = true; // Before the mutex exists there is nothing to serialize against.
bool owned = false; bool owned = false;
@ -54,6 +63,16 @@ struct RadioLock {
} }
~RadioLock() { if(owned) xSemaphoreGiveRecursive(radioMutex); } ~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 sort_asc(const void *cmp1, const void *cmp2) {
int a = *((uint8_t *)cmp1); int a = *((uint8_t *)cmp1);
@ -266,8 +285,9 @@ void somfy_frame_t::decodeFrame(somfy_rx_t *rx) {
this->bitLength = rx->bit_length; this->bitLength = rx->bit_length;
{ {
// Don't stall the main loop behind a transmission just to read the RSSI. // 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); RadioLock lock(0);
if(lock.locked) this->rssi = ELECHOUSE_cc1101.getRssi(); this->rssi = lock.locked ? ELECHOUSE_cc1101.getRssi() : 0;
} }
this->decodeFrame(rx->payload); this->decodeFrame(rx->payload);
} }
@ -1347,7 +1367,8 @@ void SomfyShade::checkMovement() {
if(this->myPos == this->currentPos) this->p_myPos(-1); if(this->myPos == this->currentPos) this->p_myPos(-1);
else this->p_myPos(this->currentPos); 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->settingMyPos = false;
this->commitMyPosition(); this->commitMyPosition();
this->emitState(); this->emitState();
@ -2731,6 +2752,14 @@ void SomfyShade::setTiltMovement(int8_t dir) {
this->emitState(); 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) { void SomfyShade::setMovement(int8_t dir) {
int8_t currDir = this->direction; int8_t currDir = this->direction;
int8_t currTiltDir = this->tiltDirection; 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) { this->sendCommand(cmd, this->repeats); }
void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) {
ESP_LOGD(TAG, "Send command start"); 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 // This sendCommand function will always be called externally. sendCommand at the remote level
// is expected to be called internally when the motor needs commanded. // is expected to be called internally when the motor needs commanded.
if(this->bitLength == 0) this->bitLength = somfy.transceiver.config.type; 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) { this->sendCommand(cmd, this->repeats); }
void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { 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); 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 // This sendCommand function will always be called externally. sendCommand at the remote level
// is expected to be called internally when the motor needs commanded. // is expected to be called internally when the motor needs commanded.
if(this->bitLength == 0) this->bitLength = somfy.transceiver.config.type; 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) { void SomfyShade::sendTiltCommand(somfy_commands cmd) {
if(!this->canTransmit()) return;
if(cmd == somfy_commands::Up) { if(cmd == somfy_commands::Up) {
SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : this->repeats); SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : this->repeats);
this->p_tiltTarget(0.0f); this->p_tiltTarget(0.0f);
@ -2999,6 +3032,7 @@ void SomfyShade::sendTiltCommand(somfy_commands cmd) {
} }
} }
void SomfyShade::moveToTiltTarget(float target) { void SomfyShade::moveToTiltTarget(float target) {
if(!this->canTransmit()) return;
somfy_commands cmd = somfy_commands::My; somfy_commands cmd = somfy_commands::My;
if(target < this->currentTiltPos) if(target < this->currentTiltPos)
cmd = somfy_commands::Up; cmd = somfy_commands::Up;
@ -3030,6 +3064,7 @@ void SomfyShade::moveToTarget(float pos, float tilt) {
this->emitState(); this->emitState();
return; return;
} }
if(!this->canTransmit()) return;
if(this->tiltType == tilt_types::tiltonly) { if(this->tiltType == tilt_types::tiltonly) {
this->p_target(100.0f); this->p_target(100.0f);
this->p_myPos(-1.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); 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() { void SomfyShadeController::processWaitingFrame() {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
if(this->shades[i].getShadeId() != 255) this->shades[i].processWaitingFrame(); if(this->shades[i].getShadeId() != 255) this->shades[i].processWaitingFrame();
@ -3783,6 +3839,7 @@ somfy_commands SomfyRemote::transformCommand(somfy_commands cmd) {
return cmd; return cmd;
} }
void SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat) { void SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat) {
if(!this->canTransmit()) return;
uint8_t flags = (this->flags >> 4) & 0x0F; uint8_t flags = (this->flags >> 4) & 0x0F;
if(isWindy > 0) flags |= 0x01; if(isWindy > 0) flags |= 0x01;
if(isSunny > 0) flags |= 0x02; 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", ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d REPEAT:%d",
translateSomfyCommand(this->lastFrame.cmd).c_str(), translateSomfyCommand(this->lastFrame.cmd).c_str(),
this->lastFrame.remoteAddress, this->lastFrame.rollingCode, repeat); this->lastFrame.remoteAddress, this->lastFrame.rollingCode, repeat);
somfy.sendFrame(this->lastFrame, repeat); if(somfy.sendFrame(this->lastFrame, repeat)) somfy.processFrame(this->lastFrame, true);
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) { 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.rollingCode = this->getNextRollingCode();
this->lastFrame.remoteAddress = this->getRemoteAddress(); this->lastFrame.remoteAddress = this->getRemoteAddress();
this->lastFrame.cmd = this->transformCommand(cmd); 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", ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d REPEAT:%d",
translateSomfyCommand(this->lastFrame.cmd).c_str(), translateSomfyCommand(this->lastFrame.cmd).c_str(),
this->lastFrame.remoteAddress, this->lastFrame.rollingCode, repeat); 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); somfy.processFrame(this->lastFrame, true);
} }
@ -3854,30 +3921,22 @@ void SomfyRemote::repeatFrame(uint8_t repeat) {
this->triggerGPIOs(this->lastFrame); this->triggerGPIOs(this->lastFrame);
return; return;
} }
if(repeat > MAX_TX_REPEATS) repeat = MAX_TX_REPEATS; repeat = txRepeats(repeat);
// The radio task encodes the first frame with the current repeat counter and // The first frame is encoded with the current repeat counter and the radio
// then counts up from there for each repeat frame. // task counts up from there for each repeat frame.
byte frm[10];
this->lastFrame.encodeFrame(frm);
somfy_tx_job_t job; somfy_tx_job_t job;
job.frame = this->lastFrame; encodeTxJob(job, this->lastFrame, repeat);
job.repeats = repeat;
job.firstRepeat = this->lastFrame.repeats + 2; job.firstRepeat = this->lastFrame.repeats + 2;
this->lastFrame.repeats += repeat + 1; this->lastFrame.repeats += repeat + 1;
somfy.transceiver.queueTx(job); somfy.transceiver.queueTx(job);
//somfy.processFrame(this->lastFrame, true); //somfy.processFrame(this->lastFrame, true);
} }
void SomfyShadeController::sendFrame(somfy_frame_t &frame, uint8_t repeat) { bool SomfyShadeController::sendFrame(somfy_frame_t &frame, uint8_t repeat, uint16_t delayMs) {
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);
somfy_tx_job_t job; somfy_tx_job_t job;
job.frame = frame; encodeTxJob(job, frame, txRepeats(repeat));
job.repeats = repeat;
job.firstRepeat = 1; 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) { bool SomfyShadeController::deleteShade(uint8_t shadeId) {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
@ -4360,6 +4419,9 @@ float markFreq = 433.0f;
int markRSSI = -100; int markRSSI = -100;
uint32_t lastScan = 0; uint32_t lastScan = 0;
void Transceiver::beginFrequencyScan() { 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; RadioLock lock;
if(this->config.enabled) { if(this->config.enabled) {
this->disableReceive(); this->disableReceive();
@ -4411,6 +4473,7 @@ void Transceiver::endFrequencyScan() {
if(interruptPin > 0) detachInterrupt(interruptPin); if(interruptPin > 0) detachInterrupt(interruptPin);
interruptPin = 0; interruptPin = 0;
this->config.apply(); this->config.apply();
this->resumeTx();
this->emitFrequencyScan(); this->emitFrequencyScan();
} }
} }
@ -4542,7 +4605,14 @@ bool Transceiver::toJSON(JsonObject& obj) {
bool Transceiver::fromJSON(JsonObject& obj) { bool Transceiver::fromJSON(JsonObject& obj) {
if (!obj["config"].isNull()) { if (!obj["config"].isNull()) {
JsonObject objConfig = obj["config"]; 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; return true;
} }
@ -4564,7 +4634,9 @@ bool Transceiver::save() {
return true; return true;
} }
bool Transceiver::end() { 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; RadioLock lock;
this->disableReceive(); this->disableReceive();
return true; return true;
@ -4913,6 +4985,11 @@ bool Transceiver::begin() {
} }
void Transceiver::loop() { void Transceiver::loop() {
somfy_rx_t rx; 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 (noiseDetected && rxmode != 3 && this->config.noiseDetection) {
if (millis() - noiseStart > 100) { if (millis() - noiseStart > 100) {
gpio_intr_enable((gpio_num_t)interruptPin); 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. // Repeater frames are handed to the radio task like any other transmission.
somfy_tx_job_t job; somfy_tx_job_t job;
job.raw = true; 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); this->queueTx(job);
tx_queue.delay_time = millis() + TX_QUEUE_DELAY; 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) { if(!txJobQueue) {
ESP_LOGE(TAG, "Radio task not started, dropping transmission"); ESP_LOGE(TAG, "Radio task not started, dropping transmission");
return false; 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) { if(xQueueSend(txJobQueue, &job, 0) != pdTRUE) {
ESP_LOGE(TAG, "Radio TX queue full, dropping transmission"); ESP_LOGE(TAG, "Radio TX queue full, dropping transmission");
return false; return false;
} }
return true; return true;
} }
bool Transceiver::canQueueTx() {
return txJobQueue && txTaskHandle && !txSuspended && uxQueueSpacesAvailable(txJobQueue) > 0;
}
void Transceiver::flushTx() { void Transceiver::flushTx() {
++txGen;
if(txJobQueue) xQueueReset(txJobQueue); 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) { void Transceiver::transmitJob(somfy_tx_job_t &job) {
if(job.raw) { somfy_tx_t &tx = job.tx;
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);
this->beginTransmit(); 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++) { 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 // For each 80-bit frame we need to adjust the byte encoding for the
// silence. // silence.
if(frame.bitLength == 80) frame.encode80BitFrame(&frm[0], (uint8_t)(job.firstRepeat + i)); if(tx.bit_length == 80) job.frame.encode80BitFrame(tx.payload, (uint8_t)(job.firstRepeat + i));
this->sendFrame(frm, frame.bitLength == 56 ? 7 : 6, frame.bitLength); this->sendFrame(tx.payload, tx.bit_length == 56 ? 7 : 6, tx.bit_length);
} }
this->endTransmit(); this->endTransmit();
} }
@ -4997,8 +5087,28 @@ void Transceiver::txTask(void *arg) {
somfy_tx_job_t job; somfy_tx_job_t job;
for(;;) { for(;;) {
if(xQueueReceive(txJobQueue, &job, portMAX_DELAY) != pdTRUE) continue; 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; 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); trans->transmitJob(job);
} }
// Give the motors a full frame beat between separate transmissions. // Give the motors a full frame beat between separate transmissions.
@ -5008,6 +5118,7 @@ void Transceiver::txTask(void *arg) {
void Transceiver::startTxTask() { void Transceiver::startTxTask() {
if(txTaskHandle) return; if(txTaskHandle) return;
if(!txJobQueue) txJobQueue = xQueueCreate(TX_JOB_QUEUE_SIZE, sizeof(somfy_tx_job_t)); 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) { if(!txJobQueue) {
ESP_LOGE(TAG, "Unable to create radio TX queue"); ESP_LOGE(TAG, "Unable to create radio TX queue");
return; return;

View file

@ -205,14 +205,25 @@ struct somfy_frame_t {
// radio time so this keeps a single job to roughly 6 seconds. // radio time so this keeps a single job to roughly 6 seconds.
#define MAX_TX_REPEATS 40 #define MAX_TX_REPEATS 40
#define TX_JOB_QUEUE_SIZE 32 #define TX_JOB_QUEUE_SIZE 32
// A transmission handed off to the radio task. The frame is a copy so the inline uint8_t txRepeats(uint8_t repeat) { return repeat > MAX_TX_REPEATS ? MAX_TX_REPEATS : repeat; }
// radio task never touches the remote's lastFrame. // 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 { struct somfy_tx_job_t {
somfy_tx_t tx; // Encoded first frame with its hw sync count and bit length.
somfy_frame_t frame; somfy_frame_t frame;
uint8_t repeats = 0; // Number of repeat frames after the first 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. 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). bool raw = false; // Repeater frame. Waits for the receiver to be idle.
somfy_tx_t rawTx; 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 { class SomfyRoom {
@ -264,8 +275,10 @@ class SomfyRemote {
void setSimMy(bool bSimMy); void setSimMy(bool bSimMy);
virtual void sendCommand(somfy_commands cmd); virtual void sendCommand(somfy_commands cmd);
virtual void sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0); 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 sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat);
void repeatFrame(uint8_t repeat); void repeatFrame(uint8_t repeat);
bool canTransmit();
virtual uint16_t p_lastRollingCode(uint16_t code); virtual uint16_t p_lastRollingCode(uint16_t code);
somfy_commands transformCommand(somfy_commands cmd); somfy_commands transformCommand(somfy_commands cmd);
virtual void triggerGPIOs(somfy_frame_t &frame); virtual void triggerGPIOs(somfy_frame_t &frame);
@ -331,6 +344,7 @@ class SomfyShade : public SomfyRemote {
void checkMovement(); void checkMovement();
void processFrame(somfy_frame_t &frame, bool internal = false); void processFrame(somfy_frame_t &frame, bool internal = false);
void processInternalCommand(somfy_commands cmd, uint8_t repeat = 1); void processInternalCommand(somfy_commands cmd, uint8_t repeat = 1);
void shiftMovementStart(uint32_t queuedAt, uint32_t startedAt);
void setTiltMovement(int8_t dir); void setTiltMovement(int8_t dir);
void setMovement(int8_t dir); void setMovement(int8_t dir);
void setTarget(float target); void setTarget(float target);
@ -521,8 +535,11 @@ class Transceiver {
void disableReceive(); void disableReceive();
somfy_frame_t& lastFrame(); somfy_frame_t& lastFrame();
void sendFrame(byte *frame, uint8_t sync, uint8_t bitLength = 56); 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 flushTx();
void suspendTx();
void resumeTx();
void beginTransmit(); void beginTransmit();
void endTransmit(); void endTransmit();
void emitFrame(somfy_frame_t *frame, somfy_rx_t *rx = nullptr); void emitFrame(somfy_frame_t *frame, somfy_rx_t *rx = nullptr);
@ -578,8 +595,9 @@ class SomfyShadeController {
SomfyGroup * getGroupById(uint8_t groupId); SomfyGroup * getGroupById(uint8_t groupId);
SomfyShade * findShadeByRemoteAddress(uint32_t address); SomfyShade * findShadeByRemoteAddress(uint32_t address);
SomfyGroup * findGroupByRemoteAddress(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 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 emitState(uint8_t num = 255);
void publish(); void publish();
void processWaitingFrame(); void processWaitingFrame();

View file

@ -47,20 +47,26 @@ static const char *TAG = "Web";
static QueueHandle_t webCmdQueue = nullptr; static QueueHandle_t webCmdQueue = nullptr;
// Radio work for these handlers runs on the main loop via the command queue. // 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 = {}; web_command_t cmd = {};
cmd.type = web_cmd_t::set_my_position; cmd.type = web_cmd_t::set_my_position;
cmd.shadeId = shade->getShadeId(); cmd.shadeId = shade->getShadeId();
cmd.position = pos; cmd.position = pos;
cmd.tiltPosition = tilt; cmd.tiltPosition = tilt;
webServer.queueCommand(cmd); return webServer.queueCommand(cmd);
} }
static void queueUnpairShade(SomfyShade *shade) { static bool queueUnpairShade(SomfyShade *shade) {
web_command_t cmd = {}; web_command_t cmd = {};
cmd.type = web_cmd_t::unpair_shade; cmd.type = web_cmd_t::unpair_shade;
cmd.shadeId = shade->getShadeId(); cmd.shadeId = shade->getShadeId();
if(!webServer.queueCommand(cmd)) return false;
// Reflect the change in the response; the main loop sets it again and saves. // 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); AsyncWebServer asyncServer(80);
@ -92,9 +98,7 @@ bool Web::queueCommand(const web_command_t &cmd) {
if(!webCmdQueue) return false; if(!webCmdQueue) return false;
// This runs on the async_tcp task which also services the sockets so it must // This runs on the async_tcp task which also services the sockets so it must
// never wait on the main loop or the radio. // never wait on the main loop or the radio.
web_command_t c = cmd; if(xQueueSend(webCmdQueue, &cmd, 0) != pdTRUE) {
if(c.repeat > MAX_TX_REPEATS) c.repeat = MAX_TX_REPEATS;
if(xQueueSend(webCmdQueue, &c, 0) != pdTRUE) {
ESP_LOGE(TAG, "Command queue full, dropping command"); ESP_LOGE(TAG, "Command queue full, dropping command");
return false; return false;
} }
@ -812,7 +816,7 @@ void Web::handleShadeCommand(AsyncWebServerRequest *request, JsonVariant &json)
cmd.command = command; cmd.command = command;
cmd.repeat = repeat; cmd.repeat = repeat;
cmd.stepSize = stepSize; cmd.stepSize = stepSize;
this->queueCommand(cmd); if(!this->queueCommand(cmd)) { sendQueueFull(request); return; }
AsyncJsonResp resp; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();
@ -853,7 +857,7 @@ void Web::handleGroupCommand(AsyncWebServerRequest *request, JsonVariant &json)
cmd.command = command; cmd.command = command;
cmd.repeat = repeat; cmd.repeat = repeat;
cmd.stepSize = stepSize; cmd.stepSize = stepSize;
this->queueCommand(cmd); if(!this->queueCommand(cmd)) { sendQueueFull(request); return; }
AsyncJsonResp resp; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();
@ -890,7 +894,7 @@ void Web::handleTiltCommand(AsyncWebServerRequest *request, JsonVariant &json) {
cmd.shadeId = shadeId; cmd.shadeId = shadeId;
cmd.target = target; cmd.target = target;
cmd.command = command; cmd.command = command;
this->queueCommand(cmd); if(!this->queueCommand(cmd)) { sendQueueFull(request); return; }
AsyncJsonResp resp; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();
@ -931,7 +935,7 @@ void Web::handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json)
cmd.command = command; cmd.command = command;
cmd.repeat = repeat; cmd.repeat = repeat;
cmd.stepSize = stepSize; cmd.stepSize = stepSize;
this->queueCommand(cmd); if(!this->queueCommand(cmd)) { sendQueueFull(request); return; }
AsyncJsonResp resp; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginArray(); resp.beginArray();
@ -949,7 +953,7 @@ void Web::handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json)
cmd.command = command; cmd.command = command;
cmd.repeat = repeat; cmd.repeat = repeat;
cmd.stepSize = stepSize; cmd.stepSize = stepSize;
this->queueCommand(cmd); if(!this->queueCommand(cmd)) { sendQueueFull(request); return; }
AsyncJsonResp resp; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();
@ -1043,7 +1047,7 @@ void Web::handleSetPositions(AsyncWebServerRequest *request, JsonVariant &json)
cmd.shadeId = shadeId; cmd.shadeId = shadeId;
cmd.position = pos; cmd.position = pos;
cmd.tiltPosition = tiltPos; cmd.tiltPosition = tiltPos;
this->queueCommand(cmd); if(!this->queueCommand(cmd)) { sendQueueFull(request); return; }
AsyncJsonResp resp; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();
@ -1086,7 +1090,7 @@ void Web::handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json) {
cmd.sunny = sunny; cmd.sunny = sunny;
cmd.windy = windy; cmd.windy = windy;
cmd.repeat = repeat; cmd.repeat = repeat;
this->queueCommand(cmd); if(!this->queueCommand(cmd)) { sendQueueFull(request); return; }
AsyncJsonResp resp; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();
@ -1105,7 +1109,7 @@ void Web::handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json) {
cmd.sunny = sunny; cmd.sunny = sunny;
cmd.windy = windy; cmd.windy = windy;
cmd.repeat = repeat; cmd.repeat = repeat;
this->queueCommand(cmd); if(!this->queueCommand(cmd)) { sendQueueFull(request); return; }
AsyncJsonResp resp; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();
@ -1626,7 +1630,7 @@ void Web::begin() {
if(tilt < 0) tilt = shade->myPos; if(tilt < 0) tilt = shade->myPos;
if(shade->tiltType == tilt_types::none) tilt = -1; if(shade->tiltType == tilt_types::none) tilt = -1;
if(pos >= 0 && pos <= 100) 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; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();
@ -1653,7 +1657,7 @@ void Web::begin() {
if(tilt < 0) tilt = shade->myPos; if(tilt < 0) tilt = shade->myPos;
if(shade->tiltType == tilt_types::none) tilt = -1; if(shade->tiltType == tilt_types::none) tilt = -1;
if(pos >= 0 && pos <= 100) 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; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();
@ -1778,7 +1782,7 @@ void Web::begin() {
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to unpair\"}")); request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to unpair\"}"));
} }
else { else {
queueUnpairShade(shade); if(!queueUnpairShade(shade)) { sendQueueFull(request); return; }
AsyncJsonResp resp; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();
@ -1797,7 +1801,7 @@ void Web::begin() {
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to unpair\"}")); request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to unpair\"}"));
} }
else { else {
queueUnpairShade(shade); if(!queueUnpairShade(shade)) { sendQueueFull(request); return; }
AsyncJsonResp resp; AsyncJsonResp resp;
resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginResponse(request, g_async_content, sizeof(g_async_content));
resp.beginObject(); resp.beginObject();