fix: commit favorite only once SETMY is fully on air

The favorite was persisted as soon as the long My press was queued.  A
flush or radio reconfiguration could drop or cut it short and the app
kept a favorite the motor never stored.

The radio task now reports finished/aborted for every job.  Setting a
favorite goes through sendSetMy, which commits it on finished and puts
the previous one back otherwise.  The new favorite is set before the
My is sent so the simulation no longer heads for the old one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-11 12:46:40 +02:00
parent 2a45dba37a
commit 352f36e478
2 changed files with 72 additions and 21 deletions

View file

@ -1449,12 +1449,7 @@ void SomfyShade::checkMovement() {
else this->p_myPos(this->currentPos);
}
// Give the motor time to settle at the target before it is told to store it.
if(SomfyRemote::sendCommandAfter(somfy_commands::My, SETMY_REPEATS, 0, 200)) this->commitMyPosition();
else {
// The motor never learned it so keep the favorite it has.
this->p_myPos(oldMyPos);
this->p_myTiltPos(oldMyTiltPos);
}
this->sendSetMy(200, oldMyPos, oldMyTiltPos);
this->settingMyPos = false;
this->emitState();
}
@ -2865,6 +2860,17 @@ void SomfyShade::processTxEvent(const somfy_tx_event_t &evt) {
this->emitState();
}
}
if(this->txMySeq == evt.seq && evt.type != tx_event_t::started) {
this->txMySeq = 0;
if(evt.type == tx_event_t::finished) this->commitMyPosition();
else {
// The motor only stores the favorite when it gets the whole long press.
ESP_LOGW(TAG, "[Shade %u] Favorite not sent in full, keeping the previous one", this->shadeId);
this->p_myPos(this->txMyOldPos);
this->p_myTiltPos(this->txMyOldTiltPos);
this->emitState();
}
}
if(this->txStopSeq == evt.seq) {
this->txStopSeq = 0;
// Unless something has moved the shade since, the motor never got the stop
@ -2890,6 +2896,22 @@ bool SomfyShade::sendStop(int8_t dir, bool tilt) {
this->runToLimit(dir, tilt, now);
return false;
}
bool SomfyShade::sendSetMy(uint16_t delayMs, float oldPos, float oldTiltPos) {
// The new favorite is already in myPos/myTiltPos. It is committed once the
// whole long press is on air, see processTxEvent.
if(SomfyRemote::sendCommandAfter(somfy_commands::My, SETMY_REPEATS, 0, delayMs)) {
this->txMySeq = this->lastTxSeq;
this->txMyOldPos = oldPos;
this->txMyOldTiltPos = oldTiltPos;
// Without the radio (GPIO remotes) there is nothing to wait for.
if(this->txMySeq == 0) this->commitMyPosition();
return true;
}
// The motor never learned it so keep the favorite it has.
this->p_myPos(oldPos);
this->p_myTiltPos(oldTiltPos);
return false;
}
void SomfyShade::runToLimit(int8_t dir, bool tilt, uint32_t since) {
// The motor didn't get a stop at the target so it keeps going until its end
// limit. Carry on simulating that from where and when it should have stopped.
@ -2946,9 +2968,14 @@ void SomfyShade::setMyPosition(int8_t pos, int8_t tilt) {
this->settingMyPos = true;
}
}
else if(SomfyRemote::sendCommand(somfy_commands::My, SETMY_REPEATS)) {
// Only store a favorite the motor was told about.
else {
// Set it before sending so processFrame doesn't head for the old one. It
// is only committed once the motor was told about it.
const float oldTiltPos = this->myTiltPos;
this->p_myTiltPos(this->currentTiltPos);
this->sendSetMy(0, this->myPos, oldTiltPos);
this->emitState();
return;
}
this->commitMyPosition();
this->emitState();
@ -2974,10 +3001,16 @@ void SomfyShade::setMyPosition(int8_t pos, int8_t tilt) {
this->settingMyPos = true;
}
}
else if(SomfyRemote::sendCommand(somfy_commands::My, SETMY_REPEATS)) {
// Only store a favorite the motor was told about.
else {
// Set it before sending so processFrame doesn't head for the old one.
// It is only committed once the motor was told about it.
const float oldPos = this->myPos;
const float oldTiltPos = this->myTiltPos;
this->p_myPos(this->currentPos);
this->p_myTiltPos(this->currentTiltPos);
this->sendSetMy(0, oldPos, oldTiltPos);
this->emitState();
return;
}
this->commitMyPosition();
this->emitState();
@ -3002,12 +3035,14 @@ void SomfyShade::setMyPosition(int8_t pos, int8_t tilt) {
this->settingMyPos = true;
}
}
else if(SomfyRemote::sendCommand(somfy_commands::My, SETMY_REPEATS)) {
// Only store a favorite the motor was told about.
this->p_myPos(currentPos);
else {
// Set it before sending so processFrame doesn't head for the old one. It
// is only committed once the motor was told about it.
const float oldPos = this->myPos;
const float oldTiltPos = this->myTiltPos;
this->p_myPos(this->currentPos);
this->p_myTiltPos(-1);
this->commitMyPosition();
this->emitState();
if(this->sendSetMy(0, oldPos, oldTiltPos)) this->emitState();
}
}
}
@ -5169,6 +5204,8 @@ void Transceiver::loop() {
while(txEventQueue && xQueueReceive(txEventQueue, &evt, 0) == pdTRUE) {
if(evt.type == tx_event_t::dropped)
ESP_LOGW(TAG, "Radio TX dropped %s for %d", translateSomfyCommand(evt.cmd).c_str(), evt.remoteAddress);
else if(evt.type == tx_event_t::aborted)
ESP_LOGW(TAG, "Radio TX cut short %s for %d", translateSomfyCommand(evt.cmd).c_str(), evt.remoteAddress);
// Line the simulated shades up with what actually went on air.
somfy.processTxEvent(evt);
}
@ -5271,20 +5308,25 @@ void Transceiver::suspendTx(uint8_t reason) {
void Transceiver::resumeTx(uint8_t reason) { txSuspendMask &= ~(uint32_t)reason; }
// 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) {
bool Transceiver::transmitJob(somfy_tx_job_t &job) {
somfy_tx_t &tx = job.tx;
bool complete = true;
this->beginTransmit();
this->sendFrame(tx.payload, tx.hwsync, tx.bit_length);
for(uint8_t i = 0; i < job.repeats; i++) {
// Stop early when flushed or when someone is waiting to reconfigure the
// radio so they get it within a frame.
if(job.gen != txGen || txPreemptWaiters > 0) break;
if(job.gen != txGen || txPreemptWaiters > 0) {
complete = false;
break;
}
// For each 80-bit frame we need to adjust the byte encoding for the
// silence.
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();
return complete;
}
void Transceiver::txTask(void *arg) {
Transceiver *trans = static_cast<Transceiver *>(arg);
@ -5314,7 +5356,8 @@ void Transceiver::txTask(void *arg) {
continue;
}
postTxEvent(tx_event_t::started, job, millis());
trans->transmitJob(job);
const bool complete = trans->transmitJob(job);
postTxEvent(complete ? tx_event_t::finished : tx_event_t::aborted, job, millis());
}
// Give the motors a full frame beat between separate transmissions.
vTaskDelay(pdMS_TO_TICKS(TX_QUEUE_DELAY));
@ -5323,8 +5366,8 @@ void Transceiver::txTask(void *arg) {
void Transceiver::startTxTask() {
if(txTaskHandle) return;
if(!txJobQueue) txJobQueue = xQueueCreate(TX_JOB_QUEUE_SIZE, sizeof(somfy_tx_job_t));
// Every queued job reports once, plus room for the jobs a flush drains.
if(!txEventQueue) txEventQueue = xQueueCreate(TX_JOB_QUEUE_SIZE * 2, sizeof(somfy_tx_event_t));
// Every job reports when it starts and ends, plus room for the jobs a flush drains.
if(!txEventQueue) txEventQueue = xQueueCreate(TX_JOB_QUEUE_SIZE * 3, sizeof(somfy_tx_event_t));
if(!txJobQueue) {
ESP_LOGE(TAG, "Unable to create radio TX queue");
return;

View file

@ -224,6 +224,8 @@ struct somfy_tx_job_t {
};
enum class tx_event_t : uint8_t {
started, // The job is going on air.
finished, // Every frame of the job went on air.
aborted, // The job was cut short by a flush or a radio reconfiguration.
dropped // The job was flushed and will never go on air.
};
// Reported by the radio task for every queued non-repeater job.
@ -331,7 +333,13 @@ class SomfyShade : public SomfyRemote {
uint32_t txStopAt = 0;
int8_t txStopDir = 0;
bool txStopTilt = false;
// Command telling the motor to store the favorite. The favorite is only
// committed once all of it is on air and restored if it never is.
uint32_t txMySeq = 0;
float txMyOldPos = -1.0f;
float txMyOldTiltPos = -1.0f;
bool sendStop(int8_t dir, bool tilt);
bool sendSetMy(uint16_t delayMs, float oldPos, float oldTiltPos);
void runToLimit(int8_t dir, bool tilt, uint32_t since);
public:
uint8_t roomId = 0;
@ -545,7 +553,7 @@ class Transceiver {
bool _received = false;
somfy_frame_t frame;
void startTxTask();
void transmitJob(somfy_tx_job_t &job);
bool transmitJob(somfy_tx_job_t &job);
public:
transceiver_config_t config;
bool printBuffer = false;