mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +01:00
test: cover the radio queue rules, shade TX state, rolling codes and web command hold
Move the pieces of the radio TX path that don't need FreeRTOS or NVS out of Somfy.cpp and Web.cpp so they build in the native test env: the TX job and event types and job encoding (TxBuffer), the queue rules (TxQueuePolicy.h), a shade's pending radio jobs (ShadeTxState), the next rolling code (RollingCode.h) and the web command queue types (WebCommand.h). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6a3769406f
commit
1962778912
15 changed files with 912 additions and 186 deletions
16
src/RollingCode.h
Normal file
16
src/RollingCode.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#ifndef ROLLING_CODE_H
|
||||
#define ROLLING_CODE_H
|
||||
#include <stdint.h>
|
||||
|
||||
// Working out a remote's next rolling code (src/Somfy.cpp). Kept free of NVS
|
||||
// so it can be unit tested on the PC (pio test -e native).
|
||||
|
||||
// The code the next frame uses. It is only read here and consumed once the
|
||||
// frame using it has been queued. synced says last matches what is stored for
|
||||
// this address, so the store is only read through loadStored until a code has
|
||||
// been written.
|
||||
template<typename LoadStored>
|
||||
inline uint16_t nextRollingCode(bool synced, uint16_t last, LoadStored loadStored) {
|
||||
return (uint16_t)((synced ? last : loadStored()) + 1);
|
||||
}
|
||||
#endif
|
||||
|
|
@ -29,6 +29,7 @@ int8_t displayPosition(float pos, bool flip) {
|
|||
if(pos < 0) return -1;
|
||||
return static_cast<int8_t>(flip && pos >= 0.00f ? floor(100.0f - pos) : floor(pos));
|
||||
}
|
||||
float limitTarget(int8_t dir) { return dir > 0 ? 100.0f : 0.0f; }
|
||||
void TxHold::mark(uint32_t jobSeq, bool moving, uint32_t now) {
|
||||
if(jobSeq == 0 || this->seq != 0 || moving) return;
|
||||
this->seq = jobSeq;
|
||||
|
|
@ -39,3 +40,58 @@ bool TxHold::release(uint32_t jobSeq) {
|
|||
this->seq = 0;
|
||||
return true;
|
||||
}
|
||||
TxHoldCheck TxHold::check(bool moving, uint32_t now, uint32_t timeoutMs) {
|
||||
if(!this->held()) return TxHoldCheck::none;
|
||||
if(!moving) {
|
||||
// The command didn't set the shade moving so there is nothing to hold.
|
||||
this->clear();
|
||||
return TxHoldCheck::none;
|
||||
}
|
||||
if(!this->timedOut(now, timeoutMs)) return TxHoldCheck::holding;
|
||||
this->clear();
|
||||
return TxHoldCheck::expired;
|
||||
}
|
||||
void ShadeTxState::stopSent(uint32_t seq, uint32_t now, int8_t dir, bool tilt) {
|
||||
this->stopSeq = seq;
|
||||
this->stopAt = now;
|
||||
this->stopDir = dir;
|
||||
this->stopTilt = tilt;
|
||||
}
|
||||
void ShadeTxState::setMySent(uint32_t seq, float oldPos, float oldTiltPos) {
|
||||
this->mySeq = seq;
|
||||
this->myOldPos = oldPos;
|
||||
this->myOldTiltPos = oldTiltPos;
|
||||
}
|
||||
ShadeTxActions ShadeTxState::onEvent(const somfy_tx_event_t &evt, bool moving) {
|
||||
ShadeTxActions act;
|
||||
if(evt.seq == 0) return act;
|
||||
if(this->hold.release(evt.seq)) {
|
||||
if(evt.type == tx_event_t::started) {
|
||||
act.startMovement = true;
|
||||
act.startAt = evt.at;
|
||||
}
|
||||
else act.cancelMovement = true;
|
||||
}
|
||||
if(this->mySeq == evt.seq && evt.type != tx_event_t::started) {
|
||||
this->mySeq = 0;
|
||||
// The motor only stores the favorite when it gets the whole long press.
|
||||
if(evt.type == tx_event_t::finished) act.commitMy = true;
|
||||
else {
|
||||
act.restoreMy = true;
|
||||
act.myOldPos = this->myOldPos;
|
||||
act.myOldTiltPos = this->myOldTiltPos;
|
||||
}
|
||||
}
|
||||
if(this->stopSeq == evt.seq) {
|
||||
this->stopSeq = 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 && !moving) {
|
||||
act.runToLimit = true;
|
||||
act.limitDir = this->stopDir;
|
||||
act.limitTilt = this->stopTilt;
|
||||
act.limitSince = this->stopAt;
|
||||
}
|
||||
}
|
||||
return act;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#ifndef SHADE_MOTION_H
|
||||
#define SHADE_MOTION_H
|
||||
#include <Arduino.h>
|
||||
#include "TxBuffer.h"
|
||||
|
||||
// Shade position maths. Kept free of the shade state, radio and network so it
|
||||
// can be unit tested on the PC (pio test -e native).
|
||||
|
|
@ -12,7 +13,15 @@ float travelPosition(float startPos, int8_t dir, uint32_t elapsedMs, int32_t tra
|
|||
// A position as reported to clients: -1 when unset, otherwise the whole
|
||||
// percentage, counted from the other end when flipped.
|
||||
int8_t displayPosition(float pos, bool flip);
|
||||
// Where a shade (or its tilt) moving in dir ends up when nothing stops it.
|
||||
float limitTarget(int8_t dir);
|
||||
|
||||
// What a held shade does on this pass of checkMovement().
|
||||
enum class TxHoldCheck {
|
||||
none, // Nothing is held.
|
||||
holding, // Still waiting for the job to go on air.
|
||||
expired // Waited too long, so the hold was dropped and the shade moves anyway.
|
||||
};
|
||||
// The radio job that set an idle shade moving. Its simulated movement is held
|
||||
// until the job goes on air, since that is when the motor starts.
|
||||
struct TxHold {
|
||||
|
|
@ -30,5 +39,51 @@ struct TxHold {
|
|||
bool held() const { return this->seq != 0; }
|
||||
bool timedOut(uint32_t now, uint32_t timeoutMs) const { return now - this->since >= timeoutMs; }
|
||||
void clear() { this->seq = 0; }
|
||||
// Drops a hold on a shade the command didn't set moving, or one that has
|
||||
// waited timeoutMs for the radio.
|
||||
TxHoldCheck check(bool moving, uint32_t now, uint32_t timeoutMs);
|
||||
};
|
||||
// What a shade has to do about a radio event. More than one can apply to the
|
||||
// same event.
|
||||
struct ShadeTxActions {
|
||||
// The held job went on air, so the motor starts at startAt.
|
||||
bool startMovement = false;
|
||||
uint32_t startAt = 0;
|
||||
// The held job never went on air, so the motor hasn't moved.
|
||||
bool cancelMovement = false;
|
||||
// The favorite went on air in full and is committed, or it didn't and the
|
||||
// previous one is put back.
|
||||
bool commitMy = false;
|
||||
bool restoreMy = false;
|
||||
float myOldPos = -1.0f;
|
||||
float myOldTiltPos = -1.0f;
|
||||
// The stop never went on air, so the motor runs on to its end limit from
|
||||
// where and when it should have stopped.
|
||||
bool runToLimit = false;
|
||||
int8_t limitDir = 0;
|
||||
bool limitTilt = false;
|
||||
uint32_t limitSince = 0;
|
||||
bool any() const { return startMovement || cancelMovement || commitMy || restoreMy || runToLimit; }
|
||||
};
|
||||
// The radio jobs a shade waits on to find out what its motor really did.
|
||||
struct ShadeTxState {
|
||||
// Radio job that set the shade moving. The simulated position is held
|
||||
// until it goes on air.
|
||||
TxHold hold;
|
||||
// Stop sent when the simulated target was reached. If it never goes on
|
||||
// air the motor runs on to its end limit. 0 once an event for it arrived.
|
||||
uint32_t stopSeq = 0;
|
||||
uint32_t stopAt = 0;
|
||||
int8_t stopDir = 0;
|
||||
bool stopTilt = 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 mySeq = 0;
|
||||
float myOldPos = -1.0f;
|
||||
float myOldTiltPos = -1.0f;
|
||||
void stopSent(uint32_t seq, uint32_t now, int8_t dir, bool tilt);
|
||||
void setMySent(uint32_t seq, float oldPos, float oldTiltPos);
|
||||
// What to do about evt. moving is whether the shade is moving now.
|
||||
ShadeTxActions onEvent(const somfy_tx_event_t &evt, bool moving);
|
||||
};
|
||||
#endif
|
||||
|
|
|
|||
158
src/Somfy.cpp
158
src/Somfy.cpp
|
|
@ -12,6 +12,8 @@
|
|||
#include "ConfigSettings.h"
|
||||
#include "Somfy.h"
|
||||
#include "ShadeMotion.h"
|
||||
#include "TxQueuePolicy.h"
|
||||
#include "RollingCode.h"
|
||||
#include "Sockets.h"
|
||||
#include "MQTT.h"
|
||||
#include "ConfigFile.h"
|
||||
|
|
@ -39,10 +41,6 @@ uint8_t rxmode = 0; // Indicates whether the radio is in receive mode. Just to
|
|||
|
||||
#define SETMY_REPEATS 35
|
||||
#define TILT_REPEATS 15
|
||||
// The radio task bit-bangs a whole job on loop()'s core at a higher priority
|
||||
// so loop() only runs between jobs. A frame takes about 140ms so this keeps a
|
||||
// job around 7s, well inside the 15s task watchdog loop() is subscribed to.
|
||||
#define TX_MAX_REPEATS 50
|
||||
#define TX_QUEUE_DELAY 100
|
||||
#define TX_TASK_PRIORITY 5
|
||||
#define TX_TASK_STACK 4096
|
||||
|
|
@ -75,7 +73,7 @@ static std::atomic<uint32_t> txBackTaken{0};
|
|||
// The radio task and a flush both move txBackTaken. Never let it go backwards.
|
||||
static void advanceBackTaken(uint32_t seq) {
|
||||
uint32_t cur = txBackTaken;
|
||||
while((int32_t)(seq - cur) > 0 && !txBackTaken.compare_exchange_weak(cur, seq)) {}
|
||||
while(txSeqAfter(seq, cur) && !txBackTaken.compare_exchange_weak(cur, seq)) {}
|
||||
}
|
||||
// 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.
|
||||
|
|
@ -107,20 +105,6 @@ static void postTxEvent(tx_event_t type, const somfy_tx_job_t &job, uint32_t at)
|
|||
evt.at = at;
|
||||
if(xQueueSend(txEventQueue, &evt, 0) != pdTRUE) ESP_LOGW(TAG, "Radio TX event queue full");
|
||||
}
|
||||
// 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;
|
||||
if(repeat > TX_MAX_REPEATS) {
|
||||
ESP_LOGW(TAG, "Limiting %u repeats to %u", repeat, TX_MAX_REPEATS);
|
||||
repeat = TX_MAX_REPEATS;
|
||||
}
|
||||
job.repeats = repeat;
|
||||
}
|
||||
// Commands that can set a shade moving.
|
||||
static bool startsMovement(somfy_commands cmd) {
|
||||
switch(cmd) {
|
||||
|
|
@ -762,20 +746,18 @@ void SomfyShade::checkMovement() {
|
|||
uint8_t currPos = floor(this->currentPos);
|
||||
uint8_t currTiltPos = floor(this->currentTiltPos);
|
||||
if(this->direction != 0) this->lastMovement = this->direction;
|
||||
if(this->txHold.held() && this->direction == 0 && this->tiltDirection == 0) {
|
||||
// The command didn't set the shade moving so there is nothing to hold.
|
||||
this->txHold.clear();
|
||||
}
|
||||
if(this->txHold.held()) {
|
||||
// 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(!this->txHold.timedOut((uint32_t)curTime, TX_PENDING_TIMEOUT)) {
|
||||
switch(this->tx.hold.check(this->direction != 0 || this->tiltDirection != 0, (uint32_t)curTime, TX_PENDING_TIMEOUT)) {
|
||||
case TxHoldCheck::holding:
|
||||
// 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.
|
||||
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->txHold.clear();
|
||||
case TxHoldCheck::expired:
|
||||
ESP_LOGW(TAG, "[Shade %u] Timed out waiting for the radio, moving anyway", this->shadeId);
|
||||
break;
|
||||
case TxHoldCheck::none:
|
||||
break;
|
||||
}
|
||||
if (sunFlag) {
|
||||
if (isSunny && !isWindy) { // It is sunny and there is no wind so we should be extended
|
||||
|
|
@ -2339,54 +2321,40 @@ void SomfyShade::markTxPending(uint32_t seq) {
|
|||
// whether the shade was moving. A reversal is simulated as soon as it is
|
||||
// queued even though the motor turns once it is on air. See TxHold::mark
|
||||
// for which of several queued commands is held.
|
||||
this->txHold.mark(seq, this->direction != 0 || this->tiltDirection != 0, millis());
|
||||
this->tx.hold.mark(seq, this->direction != 0 || this->tiltDirection != 0, millis());
|
||||
}
|
||||
void SomfyShade::processTxEvent(const somfy_tx_event_t &evt) {
|
||||
if(evt.seq == 0) return;
|
||||
if(this->txHold.release(evt.seq)) {
|
||||
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();
|
||||
}
|
||||
const ShadeTxActions act = this->tx.onEvent(evt, this->direction != 0 || this->tiltDirection != 0);
|
||||
if(act.startMovement) {
|
||||
// The motor starts now so the simulated movement does too.
|
||||
this->moveStart = this->tiltStart = act.startAt;
|
||||
}
|
||||
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(act.cancelMovement) {
|
||||
// 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();
|
||||
}
|
||||
if(act.commitMy) this->commitMyPosition();
|
||||
if(act.restoreMy) {
|
||||
ESP_LOGW(TAG, "[Shade %u] Favorite not sent in full, keeping the previous one", this->shadeId);
|
||||
this->p_myPos(act.myOldPos);
|
||||
this->p_myTiltPos(act.myOldTiltPos);
|
||||
this->emitState();
|
||||
}
|
||||
if(act.runToLimit) {
|
||||
ESP_LOGW(TAG, "[Shade %u] Stop dropped by the radio, running on to the limit", this->shadeId);
|
||||
this->runToLimit(act.limitDir, act.limitTilt, act.limitSince);
|
||||
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;
|
||||
this->tx.stopSent(this->lastTxSeq, now, dir, tilt);
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "[Shade %u] Stop not sent, running on to the limit", this->shadeId);
|
||||
|
|
@ -2397,11 +2365,9 @@ 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;
|
||||
this->tx.setMySent(this->lastTxSeq, oldPos, oldTiltPos);
|
||||
// Without the radio (GPIO remotes) there is nothing to wait for.
|
||||
if(this->txMySeq == 0) this->commitMyPosition();
|
||||
if(this->lastTxSeq == 0) this->commitMyPosition();
|
||||
return true;
|
||||
}
|
||||
// The motor never learned it so keep the favorite it has.
|
||||
|
|
@ -2416,13 +2382,13 @@ void SomfyShade::runToLimit(int8_t dir, bool tilt, uint32_t since) {
|
|||
this->startTiltPos = this->currentTiltPos;
|
||||
this->tiltStart = since;
|
||||
this->settingTiltPos = false;
|
||||
this->p_tiltTarget(dir > 0 ? 100.0f : 0.0f);
|
||||
this->p_tiltTarget(limitTarget(dir));
|
||||
}
|
||||
else {
|
||||
this->startPos = this->currentPos;
|
||||
this->moveStart = since;
|
||||
this->settingPos = false;
|
||||
this->p_target(dir > 0 ? 100.0f : 0.0f);
|
||||
this->p_target(limitTarget(dir));
|
||||
}
|
||||
// The favorite can't be set where the shade won't stop.
|
||||
this->settingMyPos = false;
|
||||
|
|
@ -3551,10 +3517,7 @@ bool SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t s
|
|||
ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d REPEAT:%d",
|
||||
translateSomfyCommand(frame.cmd).c_str(),
|
||||
frame.remoteAddress, frame.rollingCode, repeat);
|
||||
// A stop jumps the queue unless an earlier frame of this remote is still
|
||||
// waiting. Overtaking it would put the rolling codes out of order and the
|
||||
// motor would reject the older one.
|
||||
const bool front = priority && !somfy.transceiver.isQueued(this->lastBackTxSeq);
|
||||
const bool front = stopGoesToFront(priority, somfy.transceiver.isQueued(this->lastBackTxSeq));
|
||||
// Only simulate the command if it is actually going on air.
|
||||
if(!somfy.sendFrame(frame, repeat, delayMs, priority, front, &seq)) return false;
|
||||
if(!front) this->lastBackTxSeq = seq;
|
||||
|
|
@ -3582,15 +3545,11 @@ bool SomfyRemote::repeatFrame(uint8_t repeat) {
|
|||
this->triggerGPIOs(this->lastFrame);
|
||||
return true;
|
||||
}
|
||||
// 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;
|
||||
encodeTxJob(job, this->lastFrame, repeat);
|
||||
job.firstRepeat = this->lastFrame.repeats + 2;
|
||||
// Only advance the counter for frames that will go on air.
|
||||
encodeRepeatJob(job, this->lastFrame, repeat);
|
||||
if(!somfy.transceiver.queueTx(job)) return false;
|
||||
this->lastBackTxSeq = job.seq;
|
||||
this->lastFrame.repeats += job.repeats + 1;
|
||||
noteRepeatQueued(this->lastFrame, job);
|
||||
//somfy.processFrame(this->lastFrame, true);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -3669,17 +3628,17 @@ bool SomfyShadeController::deleteGroup(uint8_t groupId) {
|
|||
}
|
||||
|
||||
bool SomfyShadeController::loadShadesFile(const char *filename) { return ShadeConfigFile::load(this, filename); }
|
||||
// The next rolling code is only read here. It is consumed by commitRollingCode
|
||||
// once the frame using it has been queued. NVS is only read until a code for
|
||||
// this address has been written, after that lastRollingCode holds the same.
|
||||
// The next rolling code is consumed by commitRollingCode once the frame using
|
||||
// it has been queued. After that lastRollingCode holds what NVS does.
|
||||
uint16_t SomfyRemote::peekNextRollingCode() {
|
||||
if(this->m_rollingCodeSynced) return (uint16_t)(this->lastRollingCode + 1);
|
||||
if(!pref.begin("ShadeCodes")) {
|
||||
ESP_LOGE(TAG, "peekNextRollingCode() pref.begin(ShadeCodes) FAILED");
|
||||
}
|
||||
uint16_t code = pref.getUShort(this->m_remotePrefId, 0);
|
||||
pref.end();
|
||||
return (uint16_t)(code + 1);
|
||||
return nextRollingCode(this->m_rollingCodeSynced, this->lastRollingCode, [this]() {
|
||||
if(!pref.begin("ShadeCodes")) {
|
||||
ESP_LOGE(TAG, "peekNextRollingCode() pref.begin(ShadeCodes) FAILED");
|
||||
}
|
||||
uint16_t code = pref.getUShort(this->m_remotePrefId, 0);
|
||||
pref.end();
|
||||
return code;
|
||||
});
|
||||
}
|
||||
void SomfyRemote::commitRollingCode(uint16_t code) {
|
||||
if(!pref.begin("ShadeCodes")) {
|
||||
|
|
@ -4717,13 +4676,12 @@ bool Transceiver::queueTx(somfy_tx_job_t &job) {
|
|||
ESP_LOGW(TAG, "Radio TX suspended, dropping transmission");
|
||||
return false;
|
||||
}
|
||||
// Normal jobs leave the reserved slots free for stops.
|
||||
if(uxQueueSpacesAvailable(txJobQueue) <= (job.priority ? 0 : TX_RESERVED_SLOTS)) {
|
||||
if(!txAdmits(uxQueueSpacesAvailable(txJobQueue), job.priority)) {
|
||||
ESP_LOGE(TAG, "Radio TX queue full, dropping transmission");
|
||||
return false;
|
||||
}
|
||||
job.gen = txGen;
|
||||
if(++txSeq == 0) ++txSeq; // 0 means no job.
|
||||
txSeq = nextTxSeq(txSeq);
|
||||
job.seq = txSeq;
|
||||
// We are the only producer while holding the lock so the space check holds.
|
||||
if(job.front) {
|
||||
|
|
@ -4736,13 +4694,13 @@ bool Transceiver::queueTx(somfy_tx_job_t &job) {
|
|||
}
|
||||
bool Transceiver::isQueued(uint32_t seq) {
|
||||
// Only meaningful for jobs queued at the back.
|
||||
return seq != 0 && (int32_t)(seq - txBackTaken) > 0;
|
||||
return txSeqQueued(seq, txBackTaken);
|
||||
}
|
||||
bool Transceiver::txSuspended() { return txSuspendMask != 0; }
|
||||
bool Transceiver::txHasRoom() {
|
||||
// Without a radio task queueTx() fails straight away so there is no point waiting.
|
||||
if(!txJobQueue || !txTaskHandle) return true;
|
||||
return uxQueueSpacesAvailable(txJobQueue) > TX_RESERVED_SLOTS;
|
||||
return txAdmits(uxQueueSpacesAvailable(txJobQueue), false);
|
||||
}
|
||||
void Transceiver::flushTx() {
|
||||
TxQueueLock lock;
|
||||
|
|
@ -4810,7 +4768,7 @@ void Transceiver::runJob(somfy_tx_job_t &job) {
|
|||
void Transceiver::waitUntilDue(const somfy_tx_job_t &job) {
|
||||
for(;;) {
|
||||
if(txSuspendMask || job.gen != txGen) return;
|
||||
const int32_t wait = (int32_t)(job.notBefore - millis());
|
||||
const int32_t wait = txDueInMs(job.notBefore, millis());
|
||||
if(wait <= 0) return;
|
||||
// A stop only goes to the front when its remote has nothing waiting, and
|
||||
// this job still counts as waiting, so any front job is free to go first.
|
||||
|
|
|
|||
48
src/Somfy.h
48
src/Somfy.h
|
|
@ -115,41 +115,9 @@ struct somfy_relay_t {
|
|||
uint8_t sync = 0;
|
||||
byte frame[10] = {0};
|
||||
};
|
||||
#define TX_JOB_QUEUE_SIZE 32
|
||||
// Slots only priority jobs (stops) may use so a stop always fits in the queue.
|
||||
#define TX_RESERVED_SLOTS 4
|
||||
// Reasons the radio must not transmit. TX resumes once every reason is cleared.
|
||||
#define TX_SUSPEND_SCAN 0x01
|
||||
#define TX_SUSPEND_OTA 0x02
|
||||
// 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; // Repeater frame. Waits for the receiver to be idle.
|
||||
bool priority = false; // May use the reserved slots.
|
||||
bool front = false; // Goes to the front of the queue. Only for priority jobs.
|
||||
uint32_t gen = 0; // Flush generation when queued. Stale jobs are dropped.
|
||||
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).
|
||||
};
|
||||
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.
|
||||
struct somfy_tx_event_t {
|
||||
tx_event_t type = tx_event_t::started;
|
||||
somfy_commands cmd = somfy_commands::My;
|
||||
uint32_t remoteAddress = 0;
|
||||
uint32_t seq = 0;
|
||||
uint32_t at = 0; // millis() when the job started or was dropped.
|
||||
};
|
||||
|
||||
class SomfyRoom {
|
||||
public:
|
||||
|
|
@ -240,20 +208,8 @@ class SomfyShade : public SomfyRemote {
|
|||
bool settingPos = false;
|
||||
bool settingTiltPos = false;
|
||||
uint32_t awaitMy = 0;
|
||||
// Radio job that set the shade moving. The simulated position is held
|
||||
// until it goes on air.
|
||||
TxHold txHold;
|
||||
// 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;
|
||||
// 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;
|
||||
// Radio jobs whose outcome decides what the motor really did.
|
||||
ShadeTxState tx;
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
#include "TxBuffer.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
// Logged as part of the radio code it came from.
|
||||
static const char *TAG = "Somfy";
|
||||
|
||||
int8_t somfy_tx_queue_t::oldest() const {
|
||||
// The newest index is at position 0 so the oldest is the last one in use.
|
||||
|
|
@ -53,3 +57,22 @@ void somfy_tx_queue_t::push(uint8_t hwsync, uint8_t *payload, uint8_t bit_length
|
|||
// When popping from the queue we always pull from the end
|
||||
this->index[0] = first;
|
||||
}
|
||||
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;
|
||||
if(repeat > TX_MAX_REPEATS) {
|
||||
ESP_LOGW(TAG, "Limiting %u repeats to %u", repeat, TX_MAX_REPEATS);
|
||||
repeat = TX_MAX_REPEATS;
|
||||
}
|
||||
job.repeats = repeat;
|
||||
}
|
||||
void encodeRepeatJob(somfy_tx_job_t &job, somfy_frame_t &frame, uint8_t repeat) {
|
||||
encodeTxJob(job, frame, repeat);
|
||||
job.firstRepeat = frame.repeats + 2;
|
||||
}
|
||||
void noteRepeatQueued(somfy_frame_t &frame, const somfy_tx_job_t &job) {
|
||||
// Only advance the counter for frames that will go on air.
|
||||
frame.repeats += job.repeats + 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
#ifndef TX_BUFFER_H
|
||||
#define TX_BUFFER_H
|
||||
#include <Arduino.h>
|
||||
#include "SomfyFrame.h"
|
||||
|
||||
// Repeater frames waiting to be handed to the radio task. Kept free of the
|
||||
// radio and timers so it can be unit tested on the PC (pio test -e native).
|
||||
// Repeater frames waiting to be handed to the radio task, and the jobs and
|
||||
// events passed to and from it. Kept free of the radio and timers so it can be
|
||||
// unit tested on the PC (pio test -e native).
|
||||
|
||||
#define MAX_TX_BUFFER 5
|
||||
#define TX_JOB_QUEUE_SIZE 32
|
||||
// Slots only priority jobs (stops) may use so a stop always fits in the queue.
|
||||
#define TX_RESERVED_SLOTS 4
|
||||
// The radio task bit-bangs a whole job on loop()'s core at a higher priority
|
||||
// so loop() only runs between jobs. A frame takes about 140ms so this keeps a
|
||||
// job around 7s, well inside the 15s task watchdog loop() is subscribed to.
|
||||
#define TX_MAX_REPEATS 50
|
||||
|
||||
struct somfy_tx_t {
|
||||
void clear() {
|
||||
|
|
@ -40,4 +49,43 @@ struct somfy_tx_queue_t {
|
|||
// Position in index of the oldest frame, -1 when empty.
|
||||
int8_t oldest() const;
|
||||
};
|
||||
// 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; // Repeater frame. Waits for the receiver to be idle.
|
||||
bool priority = false; // May use the reserved slots.
|
||||
bool front = false; // Goes to the front of the queue. Only for priority jobs.
|
||||
uint32_t gen = 0; // Flush generation when queued. Stale jobs are dropped.
|
||||
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).
|
||||
};
|
||||
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.
|
||||
struct somfy_tx_event_t {
|
||||
tx_event_t type = tx_event_t::started;
|
||||
somfy_commands cmd = somfy_commands::My;
|
||||
uint32_t remoteAddress = 0;
|
||||
uint32_t seq = 0;
|
||||
uint32_t at = 0; // millis() when the job started or was dropped.
|
||||
};
|
||||
// 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. More
|
||||
// than TX_MAX_REPEATS repeats are cut down to it.
|
||||
void encodeTxJob(somfy_tx_job_t &job, somfy_frame_t &frame, uint8_t repeat);
|
||||
// Encodes a repeat of the frame last sent. The first frame carries the
|
||||
// frame's current repeat counter and the radio task counts up from there.
|
||||
void encodeRepeatJob(somfy_tx_job_t &job, somfy_frame_t &frame, uint8_t repeat);
|
||||
// Advances the frame's repeat counter past a repeat job the radio took.
|
||||
void noteRepeatQueued(somfy_frame_t &frame, const somfy_tx_job_t &job);
|
||||
#endif
|
||||
|
|
|
|||
31
src/TxQueuePolicy.h
Normal file
31
src/TxQueuePolicy.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#ifndef TX_QUEUE_POLICY_H
|
||||
#define TX_QUEUE_POLICY_H
|
||||
#include <stdint.h>
|
||||
#include "TxBuffer.h"
|
||||
|
||||
// The rules the radio TX queue follows (src/Somfy.cpp). Kept free of FreeRTOS
|
||||
// so they can be unit tested on the PC (pio test -e native).
|
||||
|
||||
// Whether a job fits with spaces slots free. Normal jobs leave the reserved
|
||||
// slots free for stops.
|
||||
inline bool txAdmits(uint32_t spaces, bool priority) {
|
||||
return spaces > (priority ? 0u : (uint32_t)TX_RESERVED_SLOTS);
|
||||
}
|
||||
// The sequence number after seq. 0 means no job so it is skipped on wrap.
|
||||
inline uint32_t nextTxSeq(uint32_t seq) {
|
||||
if(++seq == 0) ++seq;
|
||||
return seq;
|
||||
}
|
||||
// Whether job seq was queued after job cur, allowing for wrap.
|
||||
inline bool txSeqAfter(uint32_t seq, uint32_t cur) { return (int32_t)(seq - cur) > 0; }
|
||||
// Whether a job queued at the back is still waiting, given taken is the last
|
||||
// back job the radio task took or a flush dropped. Those go out in order.
|
||||
inline bool txSeqQueued(uint32_t seq, uint32_t taken) { return seq != 0 && txSeqAfter(seq, taken); }
|
||||
// A stop jumps the queue unless an earlier frame of its remote is still
|
||||
// waiting. Overtaking it would put the rolling codes out of order and the
|
||||
// motor would reject the older one.
|
||||
inline bool stopGoesToFront(bool priority, bool lastBackQueued) { return priority && !lastBackQueued; }
|
||||
// Milliseconds until a delayed job is due, 0 or less once it is. Works
|
||||
// across a millis() wrap.
|
||||
inline int32_t txDueInMs(uint32_t notBefore, uint32_t now) { return (int32_t)(notBefore - now); }
|
||||
#endif
|
||||
|
|
@ -134,17 +134,12 @@ bool Web::queueCommand(const web_command_t &cmd) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
// Only setting positions never goes on air.
|
||||
static bool usesRadio(web_cmd_t type) { return type != web_cmd_t::set_positions; }
|
||||
void Web::processQueue() {
|
||||
if(!webCmdQueue) return;
|
||||
web_command_t cmd;
|
||||
// The client was already told the command was accepted so leave it queued
|
||||
// until the radio has room rather than have the radio drop it. While TX is
|
||||
// suspended nothing gets through anyway so don't hold anything back then.
|
||||
// Commands run in order, so only the one at the head can be held back.
|
||||
while(xQueuePeek(webCmdQueue, &cmd, 0) == pdTRUE) {
|
||||
if(usesRadio(cmd.type) && !somfy.transceiver.txSuspended() && !somfy.transceiver.txHasRoom()) break;
|
||||
if(holdWebCommand(cmd.type, somfy.transceiver.txSuspended(), somfy.transceiver.txHasRoom())) break;
|
||||
// loop() is the only consumer so this is the command we peeked.
|
||||
xQueueReceive(webCmdQueue, &cmd, 0);
|
||||
bool sent = true;
|
||||
|
|
|
|||
33
src/Web.h
33
src/Web.h
|
|
@ -3,41 +3,10 @@
|
|||
#include <freertos/queue.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include "Somfy.h"
|
||||
#include "WebCommand.h"
|
||||
#ifndef webserver_h
|
||||
#define webserver_h
|
||||
|
||||
// Commands are queued by the async web server and drained by the main loop.
|
||||
// Big enough for Home Assistant firing a command at every shade at once.
|
||||
#define WEB_CMD_QUEUE_SIZE 32
|
||||
|
||||
enum class web_cmd_t : uint8_t {
|
||||
shade_command, // moveToTarget or sendCommand
|
||||
group_command, // group sendCommand
|
||||
tilt_command, // moveToTiltTarget or sendTiltCommand
|
||||
shade_repeat, // shade sendCommand/repeatFrame
|
||||
group_repeat, // group sendCommand/repeatFrame
|
||||
set_positions, // set shade position directly
|
||||
shade_sensor, // shade sensor command
|
||||
group_sensor, // group sensor command
|
||||
set_my_position, // shade setMyPosition (position/tiltPosition)
|
||||
unpair_shade, // shade unpair (Prog) and clear paired flag
|
||||
};
|
||||
|
||||
struct web_command_t {
|
||||
web_cmd_t type;
|
||||
uint8_t shadeId;
|
||||
uint8_t groupId;
|
||||
uint8_t target; // 0-100 or 255 (none)
|
||||
somfy_commands command;
|
||||
int8_t repeat;
|
||||
uint8_t stepSize;
|
||||
int8_t position; // for setPositions
|
||||
int8_t tiltPosition; // for setPositions/tilt
|
||||
int8_t sunny; // for sensor
|
||||
int8_t windy; // for sensor
|
||||
bool wasPaired; // for unpair, restored if the Prog can't be sent
|
||||
};
|
||||
|
||||
class Web {
|
||||
public:
|
||||
bool uploadSuccess = false;
|
||||
|
|
|
|||
51
src/WebCommand.h
Normal file
51
src/WebCommand.h
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#ifndef WEB_COMMAND_H
|
||||
#define WEB_COMMAND_H
|
||||
#include <stdint.h>
|
||||
#include "SomfyFrame.h"
|
||||
|
||||
// Commands the web servers hand to the main loop (src/Web.cpp). Kept free of
|
||||
// the web server and FreeRTOS so it can be unit tested on the PC
|
||||
// (pio test -e native).
|
||||
|
||||
// Commands are queued by the async web server and drained by the main loop.
|
||||
// Big enough for Home Assistant firing a command at every shade at once.
|
||||
#define WEB_CMD_QUEUE_SIZE 32
|
||||
|
||||
enum class web_cmd_t : uint8_t {
|
||||
shade_command, // moveToTarget or sendCommand
|
||||
group_command, // group sendCommand
|
||||
tilt_command, // moveToTiltTarget or sendTiltCommand
|
||||
shade_repeat, // shade sendCommand/repeatFrame
|
||||
group_repeat, // group sendCommand/repeatFrame
|
||||
set_positions, // set shade position directly
|
||||
shade_sensor, // shade sensor command
|
||||
group_sensor, // group sensor command
|
||||
set_my_position, // shade setMyPosition (position/tiltPosition)
|
||||
unpair_shade, // shade unpair (Prog) and clear paired flag
|
||||
};
|
||||
|
||||
struct web_command_t {
|
||||
web_cmd_t type;
|
||||
uint8_t shadeId;
|
||||
uint8_t groupId;
|
||||
uint8_t target; // 0-100 or 255 (none)
|
||||
somfy_commands command;
|
||||
int8_t repeat;
|
||||
uint8_t stepSize;
|
||||
int8_t position; // for setPositions
|
||||
int8_t tiltPosition; // for setPositions/tilt
|
||||
int8_t sunny; // for sensor
|
||||
int8_t windy; // for sensor
|
||||
bool wasPaired; // for unpair, restored if the Prog can't be sent
|
||||
};
|
||||
|
||||
// Only setting positions never goes on air.
|
||||
inline bool webCommandUsesRadio(web_cmd_t type) { return type != web_cmd_t::set_positions; }
|
||||
// Whether the command at the head of the queue waits for the radio. The
|
||||
// client was already told it was accepted so it stays queued until the radio
|
||||
// has room rather than have the radio drop it. While TX is suspended nothing
|
||||
// gets through anyway so nothing is held back then.
|
||||
inline bool holdWebCommand(web_cmd_t type, bool txSuspended, bool txHasRoom) {
|
||||
return webCommandUsesRadio(type) && !txSuspended && !txHasRoom;
|
||||
}
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue