fix: keep shade simulation in step with what goes on air

- Hold a shade's simulated movement until its command is on air.
- Send stops at the target as priority jobs.
- If a stop can't be sent or is dropped, simulate the run to the limit.
- Roll back targets when a movement command is dropped.
- Only store a favorite position when SETMY was queued.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-11 12:21:25 +02:00
parent ad15ecacd4
commit e55d9e48f7
2 changed files with 193 additions and 76 deletions

View file

@ -61,6 +61,11 @@ static std::atomic<uint32_t> txGen{0};
static std::atomic<uint32_t> txPreemptWaiters{0}; static std::atomic<uint32_t> txPreemptWaiters{0};
// TX_SUSPEND_* reasons the radio must not transmit for (frequency scan, OTA). // TX_SUSPEND_* reasons the radio must not transmit for (frequency scan, OTA).
static std::atomic<uint32_t> txSuspendMask{0}; static std::atomic<uint32_t> txSuspendMask{0};
// Last sequence number handed to a queued job. Only touched under TxQueueLock.
static uint32_t txSeq = 0;
// How long a shade's simulated movement waits for its command to go on air
// before it starts anyway in case the event was lost.
#define TX_PENDING_TIMEOUT 30000
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;
@ -84,7 +89,7 @@ static void postTxEvent(tx_event_t type, const somfy_tx_job_t &job, uint32_t at)
evt.type = type; evt.type = type;
evt.cmd = job.frame.cmd; evt.cmd = job.frame.cmd;
evt.remoteAddress = job.frame.remoteAddress; evt.remoteAddress = job.frame.remoteAddress;
evt.queuedAt = job.queuedAt; evt.seq = job.seq;
evt.at = at; evt.at = at;
if(xQueueSend(txEventQueue, &evt, 0) != pdTRUE) ESP_LOGW(TAG, "Radio TX event queue full"); if(xQueueSend(txEventQueue, &evt, 0) != pdTRUE) ESP_LOGW(TAG, "Radio TX event queue full");
} }
@ -98,6 +103,21 @@ static void encodeTxJob(somfy_tx_job_t &job, somfy_frame_t &frame, uint8_t repea
job.frame = frame; job.frame = frame;
job.repeats = repeat; job.repeats = repeat;
} }
// Commands that can set a shade moving.
static bool startsMovement(somfy_commands cmd) {
switch(cmd) {
case somfy_commands::My:
case somfy_commands::Up:
case somfy_commands::Down:
case somfy_commands::StepUp:
case somfy_commands::StepDown:
case somfy_commands::Toggle:
case somfy_commands::Favorite:
return true;
default:
return false;
}
}
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);
@ -1124,6 +1144,17 @@ void SomfyShade::checkMovement() {
uint8_t currPos = floor(this->currentPos); uint8_t currPos = floor(this->currentPos);
uint8_t currTiltPos = floor(this->currentTiltPos); uint8_t currTiltPos = floor(this->currentTiltPos);
if(this->direction != 0) this->lastMovement = this->direction; if(this->direction != 0) this->lastMovement = this->direction;
if(this->txPendingSeq != 0) {
// The command that set us moving hasn't gone on air yet so the motor hasn't
// started. Hold the simulated position until it does.
if((uint32_t)curTime - this->txPendingSince < TX_PENDING_TIMEOUT) {
this->moveStart = this->tiltStart = curTime;
if(currDir != this->direction || currTiltDir != this->tiltDirection) this->emitState();
return;
}
ESP_LOGW(TAG, "[Shade %u] Timed out waiting for the radio, moving anyway", this->shadeId);
this->txPendingSeq = 0;
}
if (sunFlag) { if (sunFlag) {
if (isSunny && !isWindy) { // It is sunny and there is no wind so we should be extended if (isSunny && !isWindy) { // It is sunny and there is no wind so we should be extended
if (this->noWindDone if (this->noWindDone
@ -1211,14 +1242,15 @@ void SomfyShade::checkMovement() {
// If we need to stop the shade do this before we indicate that we are // If we need to stop the shade do this before we indicate that we are
// not moving otherwise the my function will kick in. // not moving otherwise the my function will kick in.
if(this->settingPos) { if(this->settingPos) {
// At the end limit the motor stops by itself. If the stop doesn't go
// out the shade is now running on to the limit so leave the tilt be.
if(this->target == 100.0 || this->sendStop(1, false)) {
if(!isAtTarget()) { if(!isAtTarget()) {
ESP_LOGD(TAG, "We are not at our tilt target: %.2f", this->tiltTarget); ESP_LOGD(TAG, "We are not at our tilt target: %.2f", this->tiltTarget);
if(this->target != 100.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
// We now need to move the tilt to the position we requested. // We now need to move the tilt to the position we requested.
this->moveToTiltTarget(this->tiltTarget); this->moveToTiltTarget(this->tiltTarget);
} }
else }
if(this->target != 100.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
} }
this->p_direction(0); this->p_direction(0);
this->tiltStart = curTime; this->tiltStart = curTime;
@ -1262,14 +1294,15 @@ void SomfyShade::checkMovement() {
// If we need to stop the shade do this before we indicate that we are // If we need to stop the shade do this before we indicate that we are
// not moving otherwise the my function will kick in. // not moving otherwise the my function will kick in.
if(this->settingPos) { if(this->settingPos) {
// At the end limit the motor stops by itself. If the stop doesn't go
// out the shade is now running on to the limit so leave the tilt be.
if(this->target == 0.0 || this->sendStop(-1, false)) {
if(!isAtTarget()) { if(!isAtTarget()) {
ESP_LOGD(TAG, "We are not at our tilt target: %.2f", this->tiltTarget); ESP_LOGD(TAG, "We are not at our tilt target: %.2f", this->tiltTarget);
if(this->target != 0.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
// We now need to move the tilt to the position we requested. // We now need to move the tilt to the position we requested.
this->moveToTiltTarget(this->tiltTarget); this->moveToTiltTarget(this->tiltTarget);
} }
else }
if(this->target != 0.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
} }
this->p_direction(0); this->p_direction(0);
this->tiltStart = curTime; this->tiltStart = curTime;
@ -1314,11 +1347,11 @@ void SomfyShade::checkMovement() {
if(this->tiltType == tilt_types::integrated) { if(this->tiltType == tilt_types::integrated) {
// If this is an integrated tilt mechanism the we will simply let it finish. If it is not then we will stop it. // If this is an integrated tilt mechanism the we will simply let it finish. If it is not then we will stop it.
//Serial.printf("Sending My -- tiltTarget: %.2f, tiltDirection: %d\n", this->tiltTarget, this->tiltDirection); //Serial.printf("Sending My -- tiltTarget: %.2f, tiltDirection: %d\n", this->tiltTarget, this->tiltDirection);
if(this->tiltTarget != 100.0f || this->currentPos != 100.0f) SomfyRemote::sendCommand(somfy_commands::My, this->repeats); if(this->tiltTarget != 100.0f || this->currentPos != 100.0f) this->sendStop(1, true);
} }
else { else {
// This is a tilt motor so let it complete if it is going to 100. // This is a tilt motor so let it complete if it is going to 100.
if(this->tiltTarget != 100.0f) SomfyRemote::sendCommand(somfy_commands::My, this->repeats); if(this->tiltTarget != 100.0f) this->sendStop(1, true);
} }
} }
this->p_tiltDirection(0); this->p_tiltDirection(0);
@ -1364,11 +1397,11 @@ void SomfyShade::checkMovement() {
if(this->tiltType == tilt_types::integrated) { if(this->tiltType == tilt_types::integrated) {
// If this is an integrated tilt mechanism the we will simply let it finish. If it is not then we will stop it. // If this is an integrated tilt mechanism the we will simply let it finish. If it is not then we will stop it.
//Serial.printf("Sending My -- tiltTarget: %.2f, tiltDirection: %d\n", this->tiltTarget, this->tiltDirection); //Serial.printf("Sending My -- tiltTarget: %.2f, tiltDirection: %d\n", this->tiltTarget, this->tiltDirection);
if(this->tiltTarget != 0.0 || this->currentPos != 0.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats); if(this->tiltTarget != 0.0 || this->currentPos != 0.0) this->sendStop(-1, true);
} }
else { else {
// This is a tilt motor so let it complete if it is going to 0. // This is a tilt motor so let it complete if it is going to 0.
if(this->tiltTarget != 0.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats); if(this->tiltTarget != 0.0) this->sendStop(-1, true);
} }
} }
this->p_tiltDirection(0); this->p_tiltDirection(0);
@ -1380,6 +1413,8 @@ void SomfyShade::checkMovement() {
if(this->settingMyPos && this->isAtTarget()) { if(this->settingMyPos && this->isAtTarget()) {
// Set this position before sending the command. If you don't the processFrame function // Set this position before sending the command. If you don't the processFrame function
// will send the shade back to its original My position. // will send the shade back to its original My position.
const float oldMyPos = this->myPos;
const float oldMyTiltPos = this->myTiltPos;
if(this->tiltType != tilt_types::none) { if(this->tiltType != tilt_types::none) {
if(this->myTiltPos == this->currentTiltPos && this->myPos == this->currentPos) this->myPos = this->myTiltPos = -1; if(this->myTiltPos == this->currentTiltPos && this->myPos == this->currentPos) this->myPos = this->myTiltPos = -1;
else { else {
@ -1393,9 +1428,13 @@ void SomfyShade::checkMovement() {
else this->p_myPos(this->currentPos); else this->p_myPos(this->currentPos);
} }
// Give the motor time to settle at the target before it is told to store it. // 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); 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->settingMyPos = false; this->settingMyPos = false;
this->commitMyPosition();
this->emitState(); this->emitState();
} }
else if(currDir != this->direction || currPos != floor(this->currentPos) || currTiltDir != this->tiltDirection || currTiltPos != floor(this->currentTiltPos)) { else if(currDir != this->direction || currPos != floor(this->currentPos) || currTiltDir != this->tiltDirection || currTiltPos != floor(this->currentTiltPos)) {
@ -2777,13 +2816,71 @@ void SomfyShade::setTiltMovement(int8_t dir) {
this->emitState(); this->emitState();
} }
} }
void SomfyShade::shiftMovementStart(uint32_t queuedAt, uint32_t startedAt) { void SomfyShade::markTxPending(uint32_t seq) {
// The movement was started when the command was queued but the motor only // Only a shade the command set moving has to wait for it.
// starts once the frame is on air. Move the start forward so the simulated if(this->isAtTarget()) return;
// position does not run ahead of the motor. this->txPendingSeq = seq;
const uint32_t wait = startedAt - queuedAt; this->txPendingSince = millis();
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::processTxEvent(const somfy_tx_event_t &evt) {
if(evt.seq == 0) return;
if(this->txPendingSeq == evt.seq) {
this->txPendingSeq = 0;
if(evt.type == tx_event_t::started) {
// The motor starts now so the simulated movement does too.
this->moveStart = this->tiltStart = evt.at;
}
else {
// The motor never got the command so it hasn't moved.
ESP_LOGW(TAG, "[Shade %u] Command dropped by the radio, staying at %.2f%%", this->shadeId, this->currentPos);
this->settingPos = this->settingTiltPos = this->settingMyPos = false;
this->p_target(this->currentPos);
this->p_tiltTarget(this->currentTiltPos);
this->emitState();
}
}
if(this->txStopSeq == evt.seq) {
this->txStopSeq = 0;
// Unless something has moved the shade since, the motor never got the stop
// and is still running.
if(evt.type == tx_event_t::dropped && this->direction == 0 && this->tiltDirection == 0) {
ESP_LOGW(TAG, "[Shade %u] Stop dropped by the radio, running on to the limit", this->shadeId);
this->runToLimit(this->txStopDir, this->txStopTilt, this->txStopAt);
this->emitState();
}
}
}
bool SomfyShade::sendStop(int8_t dir, bool tilt) {
const uint32_t now = millis();
// Stops jump the radio queue so the motor stops as close to the target as it can.
if(SomfyRemote::sendCommandAfter(somfy_commands::My, this->repeats, 0, 0, true)) {
this->txStopSeq = this->lastTxSeq;
this->txStopAt = now;
this->txStopDir = dir;
this->txStopTilt = tilt;
return true;
}
ESP_LOGW(TAG, "[Shade %u] Stop not sent, running on to the limit", this->shadeId);
this->runToLimit(dir, tilt, now);
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.
if(tilt) {
this->startTiltPos = this->currentTiltPos;
this->tiltStart = since;
this->settingTiltPos = false;
this->p_tiltTarget(dir > 0 ? 100.0f : 0.0f);
}
else {
this->startPos = this->currentPos;
this->moveStart = since;
this->settingPos = false;
this->p_target(dir > 0 ? 100.0f : 0.0f);
}
// The favorite can't be set where the shade won't stop.
this->settingMyPos = false;
} }
void SomfyShade::setMovement(int8_t dir) { void SomfyShade::setMovement(int8_t dir) {
int8_t currDir = this->direction; int8_t currDir = this->direction;
@ -2805,28 +2902,26 @@ void SomfyShade::setMyPosition(int8_t pos, int8_t tilt) {
if(this->tiltType == tilt_types::tiltonly) { if(this->tiltType == tilt_types::tiltonly) {
this->p_myPos(-1.0f); this->p_myPos(-1.0f);
if(tilt != floor(this->currentTiltPos)) { if(tilt != floor(this->currentTiltPos)) {
this->settingMyPos = true; // The favorite is set once we get there, if the move went out.
if(tilt == floor(this->myTiltPos)) if(tilt == floor(this->myTiltPos))
this->moveToMyPosition(); this->settingMyPos = this->moveToMyPosition();
else else
this->moveToTarget(100, tilt); this->settingMyPos = this->moveToTarget(100, tilt);
} }
else if(tilt == floor(this->myTiltPos)) { else if(tilt == floor(this->myTiltPos)) {
// Of so we need to clear the my position. These motors are finicky so send // Of so we need to clear the my position. These motors are finicky so send
// a my command to ensure we are actually at the my position then send the clear // a my command to ensure we are actually at the my position then send the clear
// command. There really is no other way to do this. // command. There really is no other way to do this.
if(this->currentTiltPos != this->myTiltPos) { if(this->currentTiltPos != this->myTiltPos) {
this->settingMyPos = true; this->settingMyPos = this->moveToMyPosition();
this->moveToMyPosition();
} }
else { else if(SomfyRemote::sendCommand(somfy_commands::My, this->repeats)) {
SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
this->settingPos = false; this->settingPos = false;
this->settingMyPos = true; this->settingMyPos = true;
} }
} }
else { else if(SomfyRemote::sendCommand(somfy_commands::My, SETMY_REPEATS)) {
SomfyRemote::sendCommand(somfy_commands::My, SETMY_REPEATS); // Only store a favorite the motor was told about.
this->p_myTiltPos(this->currentTiltPos); this->p_myTiltPos(this->currentTiltPos);
} }
this->commitMyPosition(); this->commitMyPosition();
@ -2835,28 +2930,26 @@ void SomfyShade::setMyPosition(int8_t pos, int8_t tilt) {
else if(this->tiltType != tilt_types::none) { else if(this->tiltType != tilt_types::none) {
if(tilt < 0) tilt = 0; if(tilt < 0) tilt = 0;
if(pos != floor(this->currentPos) || tilt != floor(this->currentTiltPos)) { if(pos != floor(this->currentPos) || tilt != floor(this->currentTiltPos)) {
this->settingMyPos = true; // The favorite is set once we get there, if the move went out.
if(pos == floor(this->myPos) && tilt == floor(this->myTiltPos)) if(pos == floor(this->myPos) && tilt == floor(this->myTiltPos))
this->moveToMyPosition(); this->settingMyPos = this->moveToMyPosition();
else else
this->moveToTarget(pos, tilt); this->settingMyPos = this->moveToTarget(pos, tilt);
} }
else if(pos == floor(this->myPos) && tilt == floor(this->myTiltPos)) { else if(pos == floor(this->myPos) && tilt == floor(this->myTiltPos)) {
// Of so we need to clear the my position. These motors are finicky so send // Of so we need to clear the my position. These motors are finicky so send
// a my command to ensure we are actually at the my position then send the clear // a my command to ensure we are actually at the my position then send the clear
// command. There really is no other way to do this. // command. There really is no other way to do this.
if(this->currentPos != this->myPos || this->currentTiltPos != this->myTiltPos) { if(this->currentPos != this->myPos || this->currentTiltPos != this->myTiltPos) {
this->settingMyPos = true; this->settingMyPos = this->moveToMyPosition();
this->moveToMyPosition();
} }
else { else if(SomfyRemote::sendCommand(somfy_commands::My, this->repeats)) {
SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
this->settingPos = false; this->settingPos = false;
this->settingMyPos = true; this->settingMyPos = true;
} }
} }
else { else if(SomfyRemote::sendCommand(somfy_commands::My, SETMY_REPEATS)) {
SomfyRemote::sendCommand(somfy_commands::My, SETMY_REPEATS); // Only store a favorite the motor was told about.
this->p_myPos(this->currentPos); this->p_myPos(this->currentPos);
this->p_myTiltPos(this->currentTiltPos); this->p_myTiltPos(this->currentTiltPos);
} }
@ -2865,28 +2958,26 @@ void SomfyShade::setMyPosition(int8_t pos, int8_t tilt) {
} }
else { else {
if(pos != floor(this->currentPos)) { if(pos != floor(this->currentPos)) {
this->settingMyPos = true; // The favorite is set once we get there, if the move went out.
if(pos == floor(this->myPos)) if(pos == floor(this->myPos))
this->moveToMyPosition(); this->settingMyPos = this->moveToMyPosition();
else else
this->moveToTarget(pos); this->settingMyPos = this->moveToTarget(pos);
} }
else if(pos == floor(this->myPos)) { else if(pos == floor(this->myPos)) {
// Of so we need to clear the my position. These motors are finicky so send // Of so we need to clear the my position. These motors are finicky so send
// a my command to ensure we are actually at the my position then send the clear // a my command to ensure we are actually at the my position then send the clear
// command. There really is no other way to do this. // command. There really is no other way to do this.
if(this->myPos != this->currentPos) { if(this->myPos != this->currentPos) {
this->settingMyPos = true; this->settingMyPos = this->moveToMyPosition();
this->moveToMyPosition();
} }
else { else if(SomfyRemote::sendCommand(somfy_commands::My, this->repeats)) {
SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
this->settingPos = false; this->settingPos = false;
this->settingMyPos = true; this->settingMyPos = true;
} }
} }
else { else if(SomfyRemote::sendCommand(somfy_commands::My, SETMY_REPEATS)) {
SomfyRemote::sendCommand(somfy_commands::My, SETMY_REPEATS); // Only store a favorite the motor was told about.
this->p_myPos(currentPos); this->p_myPos(currentPos);
this->p_myTiltPos(-1); this->p_myTiltPos(-1);
this->commitMyPosition(); this->commitMyPosition();
@ -3038,6 +3129,8 @@ bool SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz
} }
} }
} }
// The member shades only started moving above.
if(this->lastTxSeq != 0 && startsMovement(this->lastFrame.cmd)) somfy.markTxPending(this->getRemoteAddress(), this->lastTxSeq);
this->updateFlags(); this->updateFlags();
this->emitState(); this->emitState();
return true; return true;
@ -3496,7 +3589,7 @@ 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) { void SomfyShadeController::markTxPending(uint32_t remoteAddress, uint32_t seq) {
// Same matching as SomfyShade::processFrame for shade and linked remotes. // Same matching as SomfyShade::processFrame for shade and linked remotes.
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
SomfyShade *shade = &this->shades[i]; SomfyShade *shade = &this->shades[i];
@ -3504,7 +3597,7 @@ void SomfyShadeController::shiftMovementStart(uint32_t remoteAddress, uint32_t q
bool hasRemote = shade->getRemoteAddress() == remoteAddress; bool hasRemote = shade->getRemoteAddress() == remoteAddress;
for(uint8_t j = 0; !hasRemote && j < SOMFY_MAX_LINKED_REMOTES; j++) for(uint8_t j = 0; !hasRemote && j < SOMFY_MAX_LINKED_REMOTES; j++)
hasRemote = shade->linkedRemotes[j].getRemoteAddress() == remoteAddress; hasRemote = shade->linkedRemotes[j].getRemoteAddress() == remoteAddress;
if(hasRemote) shade->shiftMovementStart(queuedAt, startedAt); if(hasRemote) shade->markTxPending(seq);
} }
// Group commands move their member shades through processInternalCommand. // Group commands move their member shades through processInternalCommand.
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) { for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
@ -3513,10 +3606,14 @@ void SomfyShadeController::shiftMovementStart(uint32_t remoteAddress, uint32_t q
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) { for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) {
if(group->linkedShades[j] == 0) continue; if(group->linkedShades[j] == 0) continue;
SomfyShade *shade = this->getShadeById(group->linkedShades[j]); SomfyShade *shade = this->getShadeById(group->linkedShades[j]);
if(shade) shade->shiftMovementStart(queuedAt, startedAt); if(shade) shade->markTxPending(seq);
} }
} }
} }
void SomfyShadeController::processTxEvent(const somfy_tx_event_t &evt) {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
if(this->shades[i].getShadeId() != 255) this->shades[i].processTxEvent(evt);
}
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();
@ -3888,7 +3985,7 @@ bool SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repe
} }
bool SomfyRemote::sendCommand(somfy_commands cmd) { return this->sendCommand(cmd, this->repeats); } bool SomfyRemote::sendCommand(somfy_commands cmd) { return this->sendCommand(cmd, this->repeats); }
bool SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { return this->sendCommandAfter(cmd, repeat, stepSize, 0); } bool SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { return this->sendCommandAfter(cmd, repeat, stepSize, 0); }
bool SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs) { bool SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority) {
// Build the frame aside and only take the rolling code once the frame is // Build the frame aside and only take the rolling code once the frame is
// queued so a dropped command leaves the remote untouched. // queued so a dropped command leaves the remote untouched.
somfy_frame_t frame = this->lastFrame; somfy_frame_t frame = this->lastFrame;
@ -3907,6 +4004,7 @@ bool SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t s
// We have to set the processed to clear this if we are sending // We have to set the processed to clear this if we are sending
// another command. // another command.
frame.processed = false; frame.processed = false;
uint32_t seq = 0;
if(this->proto == radio_proto::GP_Relay) { if(this->proto == radio_proto::GP_Relay) {
ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d SETTING GPIO", ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d SETTING GPIO",
translateSomfyCommand(frame.cmd).c_str(), translateSomfyCommand(frame.cmd).c_str(),
@ -3923,11 +4021,15 @@ bool SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t s
translateSomfyCommand(frame.cmd).c_str(), translateSomfyCommand(frame.cmd).c_str(),
frame.remoteAddress, frame.rollingCode, repeat); frame.remoteAddress, frame.rollingCode, repeat);
// Only simulate the command if it is actually going on air. // Only simulate the command if it is actually going on air.
if(!somfy.sendFrame(frame, repeat, delayMs)) return false; if(!somfy.sendFrame(frame, repeat, delayMs, priority, &seq)) return false;
} }
this->commitRollingCode(frame.rollingCode); this->commitRollingCode(frame.rollingCode);
this->lastFrame = frame; this->lastFrame = frame;
this->lastTxSeq = seq;
somfy.processFrame(this->lastFrame, true); somfy.processFrame(this->lastFrame, true);
// Shades this set moving wait for the frame to go on air. Stops are sent
// ahead of everything else so they don't need to.
if(seq != 0 && !priority && startsMovement(frame.cmd)) somfy.markTxPending(frame.remoteAddress, seq);
return true; return true;
} }
bool SomfyRemote::isLastCommand(somfy_commands cmd) { bool SomfyRemote::isLastCommand(somfy_commands cmd) {
@ -3955,13 +4057,15 @@ bool SomfyRemote::repeatFrame(uint8_t repeat) {
//somfy.processFrame(this->lastFrame, true); //somfy.processFrame(this->lastFrame, true);
return true; return true;
} }
bool SomfyShadeController::sendFrame(somfy_frame_t &frame, uint8_t repeat, uint16_t delayMs, bool priority) { bool SomfyShadeController::sendFrame(somfy_frame_t &frame, uint8_t repeat, uint16_t delayMs, bool priority, uint32_t *seq) {
somfy_tx_job_t job; somfy_tx_job_t job;
encodeTxJob(job, frame, repeat); encodeTxJob(job, frame, repeat);
job.firstRepeat = 1; job.firstRepeat = 1;
job.priority = priority; job.priority = priority;
if(delayMs > 0) job.notBefore = millis() + delayMs; if(delayMs > 0) job.notBefore = millis() + delayMs;
return this->transceiver.queueTx(job); if(!this->transceiver.queueTx(job)) return false;
if(seq) *seq = job.seq;
return true;
} }
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++) {
@ -5019,13 +5123,10 @@ void Transceiver::loop() {
somfy_rx_t rx; somfy_rx_t rx;
somfy_tx_event_t evt; somfy_tx_event_t evt;
while(txEventQueue && xQueueReceive(txEventQueue, &evt, 0) == pdTRUE) { while(txEventQueue && xQueueReceive(txEventQueue, &evt, 0) == pdTRUE) {
if(evt.type == tx_event_t::started) { if(evt.type == tx_event_t::dropped)
// The shades were set moving when the job was queued so line their
// movement up with the motor.
somfy.shiftMovementStart(evt.remoteAddress, evt.queuedAt, evt.at);
}
else
ESP_LOGW(TAG, "Radio TX dropped %s for %d", translateSomfyCommand(evt.cmd).c_str(), evt.remoteAddress); ESP_LOGW(TAG, "Radio TX dropped %s for %d", translateSomfyCommand(evt.cmd).c_str(), evt.remoteAddress);
// Line the simulated shades up with what actually went on air.
somfy.processTxEvent(evt);
} }
if (noiseDetected && rxmode != 3 && this->config.noiseDetection) { if (noiseDetected && rxmode != 3 && this->config.noiseDetection) {
if (millis() - noiseStart > 100) { if (millis() - noiseStart > 100) {
@ -5090,7 +5191,8 @@ bool Transceiver::queueTx(somfy_tx_job_t &job) {
return false; return false;
} }
job.gen = txGen; job.gen = txGen;
job.queuedAt = millis(); if(++txSeq == 0) ++txSeq; // 0 means no job.
job.seq = txSeq;
// We are the only producer while holding the lock so the space check holds. // We are the only producer while holding the lock so the space check holds.
if(job.priority) xQueueSendToFront(txJobQueue, &job, 0); if(job.priority) xQueueSendToFront(txJobQueue, &job, 0);
else xQueueSendToBack(txJobQueue, &job, 0); else xQueueSendToBack(txJobQueue, &job, 0);

View file

@ -218,7 +218,7 @@ struct somfy_tx_job_t {
bool raw = false; // Repeater frame. Waits for the receiver to be idle. bool raw = false; // Repeater frame. Waits for the receiver to be idle.
bool priority = false; // Goes to the front of the queue and may use the reserved slots. bool priority = false; // Goes to the front of the queue and may use the reserved slots.
uint32_t gen = 0; // Flush generation when queued. Stale jobs are dropped. uint32_t gen = 0; // Flush generation when queued. Stale jobs are dropped.
uint32_t queuedAt = 0; // millis() when queued. uint32_t seq = 0; // Assigned when queued so the job's events can be matched up.
uint32_t notBefore = 0; // millis() before which the job must not go on air (0 = none). uint32_t notBefore = 0; // millis() before which the job must not go on air (0 = none).
}; };
enum class tx_event_t : uint8_t { enum class tx_event_t : uint8_t {
@ -230,7 +230,7 @@ struct somfy_tx_event_t {
tx_event_t type = tx_event_t::started; tx_event_t type = tx_event_t::started;
somfy_commands cmd = somfy_commands::My; somfy_commands cmd = somfy_commands::My;
uint32_t remoteAddress = 0; uint32_t remoteAddress = 0;
uint32_t queuedAt = 0; uint32_t seq = 0;
uint32_t at = 0; // millis() when the job started or was dropped. uint32_t at = 0; // millis() when the job started or was dropped.
}; };
@ -266,6 +266,7 @@ class SomfyRemote {
somfy_frame_t lastFrame; somfy_frame_t lastFrame;
bool flipCommands = false; bool flipCommands = false;
uint16_t lastRollingCode = 0; uint16_t lastRollingCode = 0;
uint32_t lastTxSeq = 0; // Radio job of the last command sent, 0 if it didn't use the radio.
uint8_t flags = 0; uint8_t flags = 0;
uint8_t bitLength = 0; uint8_t bitLength = 0;
uint8_t repeats = 1; uint8_t repeats = 1;
@ -286,7 +287,7 @@ class SomfyRemote {
// command. Nothing about the remote or shade state has changed in that case. // command. Nothing about the remote or shade state has changed in that case.
virtual bool sendCommand(somfy_commands cmd); virtual bool sendCommand(somfy_commands cmd);
virtual bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0); virtual bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
bool sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs); bool sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority = false);
bool sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat); bool sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat);
bool repeatFrame(uint8_t repeat); bool repeatFrame(uint8_t repeat);
virtual uint16_t p_lastRollingCode(uint16_t code); virtual uint16_t p_lastRollingCode(uint16_t code);
@ -318,6 +319,18 @@ class SomfyShade : public SomfyRemote {
bool settingPos = false; bool settingPos = false;
bool settingTiltPos = false; bool settingTiltPos = false;
uint32_t awaitMy = 0; uint32_t awaitMy = 0;
// Radio job that set the shade moving. The simulated position is held
// until it goes on air. 0 when nothing is waiting.
uint32_t txPendingSeq = 0;
uint32_t txPendingSince = 0;
// Stop sent when the simulated target was reached. If it never goes on
// air the motor runs on to its end limit. 0 once it is on air.
uint32_t txStopSeq = 0;
uint32_t txStopAt = 0;
int8_t txStopDir = 0;
bool txStopTilt = false;
bool sendStop(int8_t dir, bool tilt);
void runToLimit(int8_t dir, bool tilt, uint32_t since);
public: public:
uint8_t roomId = 0; uint8_t roomId = 0;
int8_t sortOrder = 0; int8_t sortOrder = 0;
@ -354,7 +367,8 @@ 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 markTxPending(uint32_t seq);
void processTxEvent(const somfy_tx_event_t &evt);
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);
@ -604,9 +618,10 @@ 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);
bool sendFrame(somfy_frame_t &frame, uint8_t repeats = 0, uint16_t delayMs = 0, bool priority = false); bool sendFrame(somfy_frame_t &frame, uint8_t repeats = 0, uint16_t delayMs = 0, bool priority = false, uint32_t *seq = nullptr);
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 markTxPending(uint32_t remoteAddress, uint32_t seq);
void processTxEvent(const somfy_tx_event_t &evt);
void emitState(uint8_t num = 255); void emitState(uint8_t num = 255);
void publish(); void publish();
void processWaitingFrame(); void processWaitingFrame();