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
|
||||
|
|
|
|||
122
src/Somfy.cpp
122
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;
|
||||
}
|
||||
case TxHoldCheck::expired:
|
||||
ESP_LOGW(TAG, "[Shade %u] Timed out waiting for the radio, moving anyway", this->shadeId);
|
||||
this->txHold.clear();
|
||||
break;
|
||||
case TxHoldCheck::none:
|
||||
break;
|
||||
}
|
||||
if (sunFlag) {
|
||||
if (isSunny && !isWindy) { // It is sunny and there is no wind so we should be extended
|
||||
|
|
@ -2339,16 +2321,15 @@ 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) {
|
||||
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 = evt.at;
|
||||
this->moveStart = this->tiltStart = act.startAt;
|
||||
}
|
||||
else {
|
||||
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;
|
||||
|
|
@ -2356,37 +2337,24 @@ void SomfyShade::processTxEvent(const somfy_tx_event_t &evt) {
|
|||
this->p_tiltTarget(this->currentTiltPos);
|
||||
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.
|
||||
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(this->txMyOldPos);
|
||||
this->p_myTiltPos(this->txMyOldTiltPos);
|
||||
this->p_myPos(act.myOldPos);
|
||||
this->p_myTiltPos(act.myOldTiltPos);
|
||||
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) {
|
||||
if(act.runToLimit) {
|
||||
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->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);
|
||||
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 (uint16_t)(code + 1);
|
||||
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
|
||||
59
test/test_rollingcode/test_rollingcode.cpp
Normal file
59
test/test_rollingcode/test_rollingcode.cpp
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#include <unity.h>
|
||||
#include "RollingCode.h"
|
||||
|
||||
// Stands in for NVS and counts how often it is read.
|
||||
struct FakeStore {
|
||||
uint16_t code = 0;
|
||||
int reads = 0;
|
||||
uint16_t operator()() { this->reads++; return this->code; }
|
||||
};
|
||||
// Passes the store by reference so the test sees the read count.
|
||||
static uint16_t next(bool synced, uint16_t last, FakeStore &store) {
|
||||
return nextRollingCode(synced, last, [&store]() { return store(); });
|
||||
}
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static void test_unsynced_reads_the_store() {
|
||||
FakeStore store;
|
||||
store.code = 41;
|
||||
TEST_ASSERT_EQUAL_UINT16(42, next(false, 7, store));
|
||||
TEST_ASSERT_EQUAL_INT(1, store.reads);
|
||||
}
|
||||
static void test_synced_never_reads_the_store() {
|
||||
FakeStore store;
|
||||
store.code = 41;
|
||||
TEST_ASSERT_EQUAL_UINT16(8, next(true, 7, store));
|
||||
TEST_ASSERT_EQUAL_INT(0, store.reads);
|
||||
}
|
||||
static void test_reading_does_not_use_up_a_code() {
|
||||
// A command the radio refuses must leave the remote on the same code.
|
||||
FakeStore store;
|
||||
store.code = 41;
|
||||
TEST_ASSERT_EQUAL_UINT16(42, next(false, 0, store));
|
||||
TEST_ASSERT_EQUAL_UINT16(42, next(false, 0, store));
|
||||
TEST_ASSERT_EQUAL_UINT16(8, next(true, 7, store));
|
||||
TEST_ASSERT_EQUAL_UINT16(8, next(true, 7, store));
|
||||
}
|
||||
static void test_wraps_to_zero_after_0xFFFF() {
|
||||
// Current behaviour, kept on purpose for now: the code after 0xFFFF is 0.
|
||||
// Our own decoder rejects a zero rolling code (see
|
||||
// test_rejects_zero_rolling_code_except_sensor in test_frame), so a remote
|
||||
// that gets this far sends one frame other receivers may ignore.
|
||||
FakeStore store;
|
||||
store.code = 0xFFFF;
|
||||
TEST_ASSERT_EQUAL_UINT16(0, next(false, 0, store));
|
||||
TEST_ASSERT_EQUAL_UINT16(0, next(true, 0xFFFF, store));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_unsynced_reads_the_store);
|
||||
RUN_TEST(test_synced_never_reads_the_store);
|
||||
RUN_TEST(test_reading_does_not_use_up_a_code);
|
||||
RUN_TEST(test_wraps_to_zero_after_0xFFFF);
|
||||
return UNITY_END();
|
||||
}
|
||||
245
test/test_shadetx/test_shadetx.cpp
Normal file
245
test/test_shadetx/test_shadetx.cpp
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
#include <unity.h>
|
||||
#include "ShadeMotion.h"
|
||||
|
||||
#define ASSERT_POS(expected, actual) TEST_ASSERT_FLOAT_WITHIN(0.01f, (expected), (actual))
|
||||
|
||||
static const int8_t DOWN = 1;
|
||||
static const int8_t UP = -1;
|
||||
static const bool IDLE = false;
|
||||
static const bool MOVING = true;
|
||||
|
||||
static somfy_tx_event_t evt(tx_event_t type, uint32_t seq, uint32_t at = 0) {
|
||||
somfy_tx_event_t e;
|
||||
e.type = type;
|
||||
e.seq = seq;
|
||||
e.at = at;
|
||||
return e;
|
||||
}
|
||||
// A shade whose idle motor was set moving by radio job seq.
|
||||
static ShadeTxState heldBy(uint32_t seq) {
|
||||
ShadeTxState tx;
|
||||
tx.hold.mark(seq, false, 0);
|
||||
return tx;
|
||||
}
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
/*********************************************************************
|
||||
* The job that set the shade moving
|
||||
********************************************************************/
|
||||
static void test_held_job_started_starts_the_movement() {
|
||||
ShadeTxState tx = heldBy(7);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::started, 7, 1234), MOVING);
|
||||
TEST_ASSERT_TRUE(act.startMovement);
|
||||
TEST_ASSERT_EQUAL_UINT32(1234, act.startAt);
|
||||
TEST_ASSERT_FALSE(act.cancelMovement);
|
||||
TEST_ASSERT_FALSE(tx.hold.held());
|
||||
}
|
||||
static void test_held_job_finished_after_start_does_nothing() {
|
||||
ShadeTxState tx = heldBy(7);
|
||||
tx.onEvent(evt(tx_event_t::started, 7, 1234), MOVING);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::finished, 7, 2000), MOVING);
|
||||
TEST_ASSERT_FALSE(act.any());
|
||||
}
|
||||
static void test_held_job_dropped_cancels_the_movement() {
|
||||
ShadeTxState tx = heldBy(7);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 7), MOVING);
|
||||
TEST_ASSERT_TRUE(act.cancelMovement);
|
||||
TEST_ASSERT_FALSE(act.startMovement);
|
||||
TEST_ASSERT_FALSE(tx.hold.held());
|
||||
}
|
||||
static void test_held_job_aborted_before_start_cancels_the_movement() {
|
||||
ShadeTxState tx = heldBy(7);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::aborted, 7), MOVING);
|
||||
TEST_ASSERT_TRUE(act.cancelMovement);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* The favorite (long press My)
|
||||
********************************************************************/
|
||||
static void test_favorite_started_waits_for_the_end() {
|
||||
ShadeTxState tx;
|
||||
tx.setMySent(9, 40.0f, 10.0f);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::started, 9), IDLE);
|
||||
TEST_ASSERT_FALSE(act.any());
|
||||
TEST_ASSERT_EQUAL_UINT32(9, tx.mySeq);
|
||||
}
|
||||
static void test_favorite_finished_is_committed() {
|
||||
ShadeTxState tx;
|
||||
tx.setMySent(9, 40.0f, 10.0f);
|
||||
tx.onEvent(evt(tx_event_t::started, 9), IDLE);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::finished, 9), IDLE);
|
||||
TEST_ASSERT_TRUE(act.commitMy);
|
||||
TEST_ASSERT_FALSE(act.restoreMy);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, tx.mySeq);
|
||||
}
|
||||
static void test_favorite_aborted_restores_the_previous_one() {
|
||||
// The motor only stores the favorite when it gets the whole long press.
|
||||
ShadeTxState tx;
|
||||
tx.setMySent(9, 40.0f, 10.0f);
|
||||
tx.onEvent(evt(tx_event_t::started, 9), IDLE);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::aborted, 9), IDLE);
|
||||
TEST_ASSERT_TRUE(act.restoreMy);
|
||||
TEST_ASSERT_FALSE(act.commitMy);
|
||||
ASSERT_POS(40.0f, act.myOldPos);
|
||||
ASSERT_POS(10.0f, act.myOldTiltPos);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, tx.mySeq);
|
||||
}
|
||||
static void test_favorite_dropped_restores_the_previous_one() {
|
||||
ShadeTxState tx;
|
||||
tx.setMySent(9, -1.0f, -1.0f);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 9), IDLE);
|
||||
TEST_ASSERT_TRUE(act.restoreMy);
|
||||
ASSERT_POS(-1.0f, act.myOldPos);
|
||||
ASSERT_POS(-1.0f, act.myOldTiltPos);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* The stop sent at the target
|
||||
********************************************************************/
|
||||
static void test_stop_dropped_runs_on_to_the_limit() {
|
||||
ShadeTxState tx;
|
||||
tx.stopSent(12, 5000, DOWN, false);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 12, 5300), IDLE);
|
||||
TEST_ASSERT_TRUE(act.runToLimit);
|
||||
TEST_ASSERT_EQUAL_INT8(DOWN, act.limitDir);
|
||||
TEST_ASSERT_FALSE(act.limitTilt);
|
||||
// From when the motor should have stopped, not when the drop was reported.
|
||||
TEST_ASSERT_EQUAL_UINT32(5000, act.limitSince);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, tx.stopSeq);
|
||||
}
|
||||
static void test_tilt_stop_dropped_runs_the_tilt_on() {
|
||||
ShadeTxState tx;
|
||||
tx.stopSent(12, 5000, UP, true);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 12), IDLE);
|
||||
TEST_ASSERT_TRUE(act.runToLimit);
|
||||
TEST_ASSERT_EQUAL_INT8(UP, act.limitDir);
|
||||
TEST_ASSERT_TRUE(act.limitTilt);
|
||||
}
|
||||
static void test_stop_dropped_after_moving_again_does_nothing() {
|
||||
// Something set the shade moving since, so that command decides where the
|
||||
// motor goes.
|
||||
ShadeTxState tx;
|
||||
tx.stopSent(12, 5000, DOWN, false);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 12), MOVING);
|
||||
TEST_ASSERT_FALSE(act.any());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, tx.stopSeq);
|
||||
}
|
||||
static void test_stop_on_air_does_nothing() {
|
||||
ShadeTxState tx;
|
||||
tx.stopSent(12, 5000, DOWN, false);
|
||||
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::started, 12), IDLE).any());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, tx.stopSeq);
|
||||
// Nothing is waiting on it any more.
|
||||
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::dropped, 12), IDLE).any());
|
||||
}
|
||||
static void test_stop_aborted_does_not_run_on() {
|
||||
// Part of the stop went on air, which is enough for the motor.
|
||||
ShadeTxState tx;
|
||||
tx.stopSent(12, 5000, DOWN, false);
|
||||
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::aborted, 12), IDLE).any());
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Edge cases
|
||||
********************************************************************/
|
||||
static void test_one_event_for_the_held_job_and_the_favorite() {
|
||||
// The delayed My that stores the favorite also sets an idle shade moving,
|
||||
// so the same job is held and awaited as the favorite.
|
||||
ShadeTxState tx = heldBy(9);
|
||||
tx.setMySent(9, 40.0f, 10.0f);
|
||||
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 9), MOVING);
|
||||
TEST_ASSERT_TRUE(act.cancelMovement);
|
||||
TEST_ASSERT_TRUE(act.restoreMy);
|
||||
TEST_ASSERT_FALSE(tx.hold.held());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, tx.mySeq);
|
||||
}
|
||||
static void test_seq_zero_is_ignored() {
|
||||
ShadeTxState tx;
|
||||
tx.setMySent(0, 40.0f, 10.0f);
|
||||
tx.stopSent(0, 5000, DOWN, false);
|
||||
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::dropped, 0), IDLE).any());
|
||||
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::finished, 0), IDLE).any());
|
||||
}
|
||||
static void test_other_jobs_are_ignored() {
|
||||
ShadeTxState tx = heldBy(7);
|
||||
tx.setMySent(9, 40.0f, 10.0f);
|
||||
tx.stopSent(12, 5000, DOWN, false);
|
||||
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::dropped, 8), IDLE).any());
|
||||
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::finished, 13), IDLE).any());
|
||||
TEST_ASSERT_EQUAL_UINT32(7, tx.hold.seq);
|
||||
TEST_ASSERT_EQUAL_UINT32(9, tx.mySeq);
|
||||
TEST_ASSERT_EQUAL_UINT32(12, tx.stopSeq);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* limitTarget
|
||||
********************************************************************/
|
||||
static void test_limitTarget() {
|
||||
ASSERT_POS(100.0f, limitTarget(DOWN));
|
||||
ASSERT_POS(0.0f, limitTarget(UP));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* TxHold::check
|
||||
********************************************************************/
|
||||
static void test_check_nothing_held() {
|
||||
TxHold hold;
|
||||
TEST_ASSERT_TRUE(hold.check(true, 1000, 30000) == TxHoldCheck::none);
|
||||
}
|
||||
static void test_check_idle_shade_drops_the_hold() {
|
||||
// The command didn't set the shade moving so there is nothing to hold.
|
||||
TxHold hold;
|
||||
hold.mark(5, false, 1000);
|
||||
TEST_ASSERT_TRUE(hold.check(false, 1000, 30000) == TxHoldCheck::none);
|
||||
TEST_ASSERT_FALSE(hold.held());
|
||||
}
|
||||
static void test_check_holds_a_moving_shade() {
|
||||
TxHold hold;
|
||||
hold.mark(5, false, 1000);
|
||||
TEST_ASSERT_TRUE(hold.check(true, 30999, 30000) == TxHoldCheck::holding);
|
||||
TEST_ASSERT_TRUE(hold.held());
|
||||
}
|
||||
static void test_check_expires() {
|
||||
TxHold hold;
|
||||
hold.mark(5, false, 1000);
|
||||
TEST_ASSERT_TRUE(hold.check(true, 31000, 30000) == TxHoldCheck::expired);
|
||||
TEST_ASSERT_FALSE(hold.held());
|
||||
TEST_ASSERT_TRUE(hold.check(true, 31001, 30000) == TxHoldCheck::none);
|
||||
}
|
||||
static void test_check_across_millis_wrap() {
|
||||
TxHold hold;
|
||||
hold.mark(5, false, 0xFFFFF000u);
|
||||
TEST_ASSERT_TRUE(hold.check(true, 0x00006000u, 30000) == TxHoldCheck::holding);
|
||||
TEST_ASSERT_TRUE(hold.check(true, 0x00007000u, 30000) == TxHoldCheck::expired);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_held_job_started_starts_the_movement);
|
||||
RUN_TEST(test_held_job_finished_after_start_does_nothing);
|
||||
RUN_TEST(test_held_job_dropped_cancels_the_movement);
|
||||
RUN_TEST(test_held_job_aborted_before_start_cancels_the_movement);
|
||||
RUN_TEST(test_favorite_started_waits_for_the_end);
|
||||
RUN_TEST(test_favorite_finished_is_committed);
|
||||
RUN_TEST(test_favorite_aborted_restores_the_previous_one);
|
||||
RUN_TEST(test_favorite_dropped_restores_the_previous_one);
|
||||
RUN_TEST(test_stop_dropped_runs_on_to_the_limit);
|
||||
RUN_TEST(test_tilt_stop_dropped_runs_the_tilt_on);
|
||||
RUN_TEST(test_stop_dropped_after_moving_again_does_nothing);
|
||||
RUN_TEST(test_stop_on_air_does_nothing);
|
||||
RUN_TEST(test_stop_aborted_does_not_run_on);
|
||||
RUN_TEST(test_one_event_for_the_held_job_and_the_favorite);
|
||||
RUN_TEST(test_seq_zero_is_ignored);
|
||||
RUN_TEST(test_other_jobs_are_ignored);
|
||||
RUN_TEST(test_limitTarget);
|
||||
RUN_TEST(test_check_nothing_held);
|
||||
RUN_TEST(test_check_idle_shade_drops_the_hold);
|
||||
RUN_TEST(test_check_holds_a_moving_shade);
|
||||
RUN_TEST(test_check_expires);
|
||||
RUN_TEST(test_check_across_millis_wrap);
|
||||
return UNITY_END();
|
||||
}
|
||||
216
test/test_txqueue/test_txqueue.cpp
Normal file
216
test/test_txqueue/test_txqueue.cpp
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
#include <unity.h>
|
||||
#include "TxQueuePolicy.h"
|
||||
#include "TxBuffer.h"
|
||||
|
||||
static const uint32_t ADDR = 0x123456;
|
||||
|
||||
// A frame as SomfyRemote::sendCommandAfter builds it.
|
||||
static somfy_frame_t makeFrame(somfy_commands cmd, uint8_t bitLength = 56) {
|
||||
somfy_frame_t f;
|
||||
f.cmd = cmd;
|
||||
f.remoteAddress = ADDR;
|
||||
f.rollingCode = 0x0102;
|
||||
f.encKey = 0xA2;
|
||||
f.bitLength = bitLength;
|
||||
f.valid = true;
|
||||
return f;
|
||||
}
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
/*********************************************************************
|
||||
* txAdmits
|
||||
********************************************************************/
|
||||
static void test_normal_job_leaves_the_reserved_slots() {
|
||||
TEST_ASSERT_FALSE(txAdmits(0, false));
|
||||
TEST_ASSERT_FALSE(txAdmits(TX_RESERVED_SLOTS, false));
|
||||
TEST_ASSERT_TRUE(txAdmits(TX_RESERVED_SLOTS + 1, false));
|
||||
TEST_ASSERT_TRUE(txAdmits(TX_JOB_QUEUE_SIZE, false));
|
||||
}
|
||||
static void test_priority_job_uses_the_reserved_slots() {
|
||||
TEST_ASSERT_FALSE(txAdmits(0, true));
|
||||
TEST_ASSERT_TRUE(txAdmits(1, true));
|
||||
TEST_ASSERT_TRUE(txAdmits(TX_RESERVED_SLOTS, true));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Sequence numbers
|
||||
********************************************************************/
|
||||
static void test_nextTxSeq_counts_up() {
|
||||
TEST_ASSERT_EQUAL_UINT32(1, nextTxSeq(0));
|
||||
TEST_ASSERT_EQUAL_UINT32(2, nextTxSeq(1));
|
||||
}
|
||||
static void test_nextTxSeq_skips_zero_on_wrap() {
|
||||
TEST_ASSERT_EQUAL_UINT32(1, nextTxSeq(0xFFFFFFFFu));
|
||||
}
|
||||
static void test_txSeqQueued_zero_is_never_queued() {
|
||||
TEST_ASSERT_FALSE(txSeqQueued(0, 0));
|
||||
TEST_ASSERT_FALSE(txSeqQueued(0, 10));
|
||||
TEST_ASSERT_FALSE(txSeqQueued(0, 0xFFFFFFF0u));
|
||||
}
|
||||
static void test_txSeqQueued_after_taken() {
|
||||
TEST_ASSERT_FALSE(txSeqQueued(9, 10));
|
||||
TEST_ASSERT_FALSE(txSeqQueued(10, 10));
|
||||
TEST_ASSERT_TRUE(txSeqQueued(11, 10));
|
||||
}
|
||||
static void test_txSeqQueued_across_wrap() {
|
||||
// Job 5 was numbered after the counter wrapped, so it is still waiting.
|
||||
TEST_ASSERT_TRUE(txSeqQueued(5, 0xFFFFFFF0u));
|
||||
TEST_ASSERT_TRUE(txSeqQueued(0xFFFFFFFFu, 0xFFFFFFF0u));
|
||||
TEST_ASSERT_FALSE(txSeqQueued(0xFFFFFFF0u, 5));
|
||||
}
|
||||
static void test_txSeqAfter_never_moves_back() {
|
||||
// A flush marks every job up to 20 as taken. The radio task then reports
|
||||
// job 15, which it took off the queue before the flush. That must not move
|
||||
// the mark back or jobs 16-20 would look queued again.
|
||||
uint32_t taken = 20;
|
||||
if(txSeqAfter(15, taken)) taken = 15;
|
||||
TEST_ASSERT_EQUAL_UINT32(20, taken);
|
||||
if(txSeqAfter(21, taken)) taken = 21;
|
||||
TEST_ASSERT_EQUAL_UINT32(21, taken);
|
||||
TEST_ASSERT_FALSE(txSeqAfter(21, 21));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* stopGoesToFront
|
||||
********************************************************************/
|
||||
static void test_stop_goes_to_front_when_nothing_waits() {
|
||||
TEST_ASSERT_TRUE(stopGoesToFront(true, false));
|
||||
}
|
||||
static void test_stop_queues_behind_its_remote() {
|
||||
// Overtaking the remote's waiting frame would send the rolling codes out of
|
||||
// order.
|
||||
TEST_ASSERT_FALSE(stopGoesToFront(true, true));
|
||||
}
|
||||
static void test_normal_job_never_goes_to_front() {
|
||||
TEST_ASSERT_FALSE(stopGoesToFront(false, false));
|
||||
TEST_ASSERT_FALSE(stopGoesToFront(false, true));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* txDueInMs
|
||||
********************************************************************/
|
||||
static void test_txDueInMs() {
|
||||
TEST_ASSERT_EQUAL_INT32(500, txDueInMs(1500, 1000));
|
||||
TEST_ASSERT_EQUAL_INT32(0, txDueInMs(1000, 1000));
|
||||
TEST_ASSERT_EQUAL_INT32(-200, txDueInMs(1000, 1200));
|
||||
}
|
||||
static void test_txDueInMs_across_millis_wrap() {
|
||||
// Queued 0x100ms before the wrap with a 0x200ms delay.
|
||||
const uint32_t notBefore = 0xFFFFFF00u + 0x200u;
|
||||
TEST_ASSERT_EQUAL_INT32(0x200, txDueInMs(notBefore, 0xFFFFFF00u));
|
||||
TEST_ASSERT_EQUAL_INT32(0x100, txDueInMs(notBefore, 0));
|
||||
TEST_ASSERT_EQUAL_INT32(-0x10, txDueInMs(notBefore, 0x110u));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* encodeTxJob
|
||||
********************************************************************/
|
||||
static void test_encode_56bit_job() {
|
||||
somfy_frame_t f = makeFrame(somfy_commands::Up);
|
||||
somfy_tx_job_t job;
|
||||
encodeTxJob(job, f, 3);
|
||||
byte expected[10] = {};
|
||||
somfy_frame_t copy = makeFrame(somfy_commands::Up);
|
||||
copy.encodeFrame(expected);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, job.tx.hwsync);
|
||||
TEST_ASSERT_EQUAL_UINT8(56, job.tx.bit_length);
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, job.tx.payload, sizeof(expected));
|
||||
TEST_ASSERT_EQUAL_UINT8(3, job.repeats);
|
||||
}
|
||||
static void test_encode_80bit_job() {
|
||||
somfy_frame_t f = makeFrame(somfy_commands::Down, 80);
|
||||
somfy_tx_job_t job;
|
||||
encodeTxJob(job, f, 1);
|
||||
byte expected[10] = {};
|
||||
somfy_frame_t copy = makeFrame(somfy_commands::Down, 80);
|
||||
copy.encodeFrame(expected);
|
||||
TEST_ASSERT_EQUAL_UINT8(12, job.tx.hwsync);
|
||||
TEST_ASSERT_EQUAL_UINT8(80, job.tx.bit_length);
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, job.tx.payload, sizeof(expected));
|
||||
}
|
||||
static void test_encode_job_copies_the_frame() {
|
||||
somfy_frame_t f = makeFrame(somfy_commands::My);
|
||||
somfy_tx_job_t job;
|
||||
encodeTxJob(job, f, 1);
|
||||
TEST_ASSERT_EQUAL_UINT32(ADDR, job.frame.remoteAddress);
|
||||
TEST_ASSERT_EQUAL_UINT16(0x0102, job.frame.rollingCode);
|
||||
// The radio task uses its own copy, so later changes to the remote's frame
|
||||
// don't reach it.
|
||||
f.rollingCode = 0x0200;
|
||||
TEST_ASSERT_EQUAL_UINT16(0x0102, job.frame.rollingCode);
|
||||
}
|
||||
static void test_encode_job_limits_repeats() {
|
||||
somfy_frame_t f = makeFrame(somfy_commands::My);
|
||||
somfy_tx_job_t job;
|
||||
encodeTxJob(job, f, TX_MAX_REPEATS);
|
||||
TEST_ASSERT_EQUAL_UINT8(TX_MAX_REPEATS, job.repeats);
|
||||
encodeTxJob(job, f, TX_MAX_REPEATS + 1);
|
||||
TEST_ASSERT_EQUAL_UINT8(TX_MAX_REPEATS, job.repeats);
|
||||
encodeTxJob(job, f, 255);
|
||||
TEST_ASSERT_EQUAL_UINT8(TX_MAX_REPEATS, job.repeats);
|
||||
encodeTxJob(job, f, 0);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, job.repeats);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Repeat jobs
|
||||
********************************************************************/
|
||||
static void test_repeat_job_counts_on_from_the_frame() {
|
||||
somfy_frame_t f = makeFrame(somfy_commands::Up, 80);
|
||||
f.repeats = 4;
|
||||
somfy_tx_job_t job;
|
||||
encodeRepeatJob(job, f, 2);
|
||||
TEST_ASSERT_EQUAL_UINT8(6, job.firstRepeat);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, job.repeats);
|
||||
// Nothing moves until the radio takes the job.
|
||||
TEST_ASSERT_EQUAL_UINT8(4, f.repeats);
|
||||
}
|
||||
static void test_repeat_queued_advances_the_counter() {
|
||||
somfy_frame_t f = makeFrame(somfy_commands::Up, 80);
|
||||
f.repeats = 4;
|
||||
somfy_tx_job_t job;
|
||||
encodeRepeatJob(job, f, 2);
|
||||
noteRepeatQueued(f, job);
|
||||
// The first frame plus two repeats went out.
|
||||
TEST_ASSERT_EQUAL_UINT8(7, f.repeats);
|
||||
// The next repeat job picks up where this one stops.
|
||||
somfy_tx_job_t next;
|
||||
encodeRepeatJob(next, f, 1);
|
||||
TEST_ASSERT_EQUAL_UINT8(9, next.firstRepeat);
|
||||
}
|
||||
static void test_repeat_queued_uses_the_limited_repeats() {
|
||||
somfy_frame_t f = makeFrame(somfy_commands::Up);
|
||||
somfy_tx_job_t job;
|
||||
encodeRepeatJob(job, f, 200);
|
||||
noteRepeatQueued(f, job);
|
||||
TEST_ASSERT_EQUAL_UINT8(TX_MAX_REPEATS + 1, f.repeats);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_normal_job_leaves_the_reserved_slots);
|
||||
RUN_TEST(test_priority_job_uses_the_reserved_slots);
|
||||
RUN_TEST(test_nextTxSeq_counts_up);
|
||||
RUN_TEST(test_nextTxSeq_skips_zero_on_wrap);
|
||||
RUN_TEST(test_txSeqQueued_zero_is_never_queued);
|
||||
RUN_TEST(test_txSeqQueued_after_taken);
|
||||
RUN_TEST(test_txSeqQueued_across_wrap);
|
||||
RUN_TEST(test_txSeqAfter_never_moves_back);
|
||||
RUN_TEST(test_stop_goes_to_front_when_nothing_waits);
|
||||
RUN_TEST(test_stop_queues_behind_its_remote);
|
||||
RUN_TEST(test_normal_job_never_goes_to_front);
|
||||
RUN_TEST(test_txDueInMs);
|
||||
RUN_TEST(test_txDueInMs_across_millis_wrap);
|
||||
RUN_TEST(test_encode_56bit_job);
|
||||
RUN_TEST(test_encode_80bit_job);
|
||||
RUN_TEST(test_encode_job_copies_the_frame);
|
||||
RUN_TEST(test_encode_job_limits_repeats);
|
||||
RUN_TEST(test_repeat_job_counts_on_from_the_frame);
|
||||
RUN_TEST(test_repeat_queued_advances_the_counter);
|
||||
RUN_TEST(test_repeat_queued_uses_the_limited_repeats);
|
||||
return UNITY_END();
|
||||
}
|
||||
48
test/test_webcmd/test_webcmd.cpp
Normal file
48
test/test_webcmd/test_webcmd.cpp
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#include <unity.h>
|
||||
#include "WebCommand.h"
|
||||
|
||||
static const web_cmd_t RADIO_COMMANDS[] = {
|
||||
web_cmd_t::shade_command, web_cmd_t::group_command, web_cmd_t::tilt_command,
|
||||
web_cmd_t::shade_repeat, web_cmd_t::group_repeat, web_cmd_t::shade_sensor,
|
||||
web_cmd_t::group_sensor, web_cmd_t::set_my_position, web_cmd_t::unpair_shade
|
||||
};
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static void test_only_set_positions_stays_off_the_radio() {
|
||||
int offRadio = 0;
|
||||
for(uint8_t t = 0; t <= static_cast<uint8_t>(web_cmd_t::unpair_shade); t++) {
|
||||
if(!webCommandUsesRadio(static_cast<web_cmd_t>(t))) offRadio++;
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(1, offRadio);
|
||||
TEST_ASSERT_FALSE(webCommandUsesRadio(web_cmd_t::set_positions));
|
||||
}
|
||||
static void test_set_positions_is_never_held() {
|
||||
TEST_ASSERT_FALSE(holdWebCommand(web_cmd_t::set_positions, false, false));
|
||||
TEST_ASSERT_FALSE(holdWebCommand(web_cmd_t::set_positions, true, false));
|
||||
}
|
||||
static void test_radio_command_waits_for_room() {
|
||||
for(web_cmd_t t : RADIO_COMMANDS) {
|
||||
TEST_ASSERT_TRUE(holdWebCommand(t, false, false));
|
||||
TEST_ASSERT_FALSE(holdWebCommand(t, false, true));
|
||||
}
|
||||
}
|
||||
static void test_nothing_is_held_while_suspended() {
|
||||
// Nothing goes on air during a frequency scan or OTA anyway.
|
||||
for(web_cmd_t t : RADIO_COMMANDS) {
|
||||
TEST_ASSERT_FALSE(holdWebCommand(t, true, false));
|
||||
TEST_ASSERT_FALSE(holdWebCommand(t, true, true));
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_only_set_positions_stays_off_the_radio);
|
||||
RUN_TEST(test_set_positions_is_never_held);
|
||||
RUN_TEST(test_radio_command_waits_for_room);
|
||||
RUN_TEST(test_nothing_is_held_while_suspended);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue