mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
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:
parent
9a9afa5e86
commit
9d16d6cc6d
3 changed files with 204 additions and 71 deletions
203
src/Somfy.cpp
203
src/Somfy.cpp
|
|
@ -4,6 +4,7 @@
|
|||
#include <esp_task_wdt.h>
|
||||
#include <freertos/queue.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <atomic>
|
||||
#include <esp_chip_info.h>
|
||||
#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<uint32_t> 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;
|
||||
|
|
|
|||
30
src/Somfy.h
30
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();
|
||||
|
|
|
|||
42
src/Web.cpp
42
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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue