mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
refactor: let one RadioScheduler own the radio job queue
The order radio jobs go out in was spread across callers, file statics and the radio task: each remote tracked its last back job so a stop could decide whether it may jump the queue, and the task kept a separate count of the back jobs it had taken. RadioScheduler now owns the waiting jobs, their tickets, the stop-goes-first rule, delayed jobs, flush and suspend, and the outcome of every job. It is plain C++, with the time passed in and the caller locking, so all of it is covered by native tests. Transceiver keeps the FreeRTOS task, the locks and the CC1101. sendCommandAfter returns the job's ticket instead of leaving it in lastTxSeq. Two stops for the same remote now go out in the order they were sent; queued to the front they used to come out reversed, putting the rolling codes out of order. Adds GLOSSARY.md for the radio terms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1962778912
commit
4135f21ee7
11 changed files with 862 additions and 410 deletions
32
GLOSSARY.md
Normal file
32
GLOSSARY.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Glossary
|
||||
|
||||
Domain terms used in the code, commits and design discussions.
|
||||
|
||||
## Radio
|
||||
|
||||
**Radio job**
|
||||
One transmission handed to the radio task: an encoded first frame plus a number of repeat frames. A job is either *our own* (a command or sensor frame from one of our remotes, with outcome events) or a *repeater frame* (a frame received from someone else's remote and sent again). A repeater frame reports no events, and since it carries none of our rolling codes it never holds back a stop.
|
||||
|
||||
**RadioScheduler**
|
||||
The module that owns the radio jobs waiting for, or on, the air. It decides their order, when a delayed job is due, which jobs a flush or suspend drops, and the outcome of every job. Producers hand it jobs and get a ticket back. The radio task asks it for the next job and reports back how the job went. It knows nothing about FreeRTOS, the CC1101 or shades.
|
||||
|
||||
**Ticket**
|
||||
What submitting a radio job returns: the job's sequence number, 0 when the radio refused it. Every event about the job carries it. A command that went out over GPIO, not the radio, has ticket 0.
|
||||
|
||||
**Back job / front job**
|
||||
A job normally joins the back of the queue. A *priority* job (a stop) goes to the front, unless an earlier job from the same remote is still waiting. Overtaking that job would put the remote's rolling codes out of order, and the motor would reject the older frame.
|
||||
|
||||
**Delayed job**
|
||||
A job that must not go on air before a given time (for example the long press that stores a favorite). It holds up the jobs behind it, but front jobs may overtake it while it waits.
|
||||
|
||||
**Flush**
|
||||
Drops every waiting job (each gets a *dropped* event) and stops the job on air after its current frame (it gets *aborted*).
|
||||
|
||||
**Suspend**
|
||||
Stops the radio taking jobs for a reason (frequency scan, OTA). Suspending flushes; while suspended, new jobs are refused.
|
||||
|
||||
**Job outcome**
|
||||
Every job we submit gets exactly one *started* event, unless it is dropped before going on air. It then ends with exactly one of *finished* (every frame went on air), *aborted* (cut short) or *dropped* (never went on air).
|
||||
|
||||
**Rolling code**
|
||||
The per-remote counter in every Somfy frame. A motor ignores any frame whose code is not newer than the last one it accepted, so a remote's frames must go on air in the order their codes were assigned.
|
||||
|
|
@ -104,7 +104,7 @@ lib_deps =
|
|||
platform = native
|
||||
test_framework = unity
|
||||
test_build_src = yes
|
||||
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<TxBuffer.cpp>
|
||||
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp>
|
||||
build_flags =
|
||||
-std=gnu++17
|
||||
-Isrc
|
||||
|
|
|
|||
130
src/RadioScheduler.cpp
Normal file
130
src/RadioScheduler.cpp
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#include "RadioScheduler.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
// Logged as part of the radio code it came from.
|
||||
static const char *TAG = "Somfy";
|
||||
|
||||
#define TX_EVENT_QUEUE_SIZE (TX_JOB_QUEUE_SIZE * 3)
|
||||
|
||||
// Whether a job fits with spaces slots free. Normal jobs leave the reserved
|
||||
// slots free for stops.
|
||||
static bool admits(uint32_t spaces, bool priority) {
|
||||
return spaces > (priority ? 0u : (uint32_t)TX_RESERVED_SLOTS);
|
||||
}
|
||||
uint32_t RadioScheduler::submit(somfy_tx_job_t &job, bool priority, uint16_t delayMs, uint32_t now) {
|
||||
job.seq = 0;
|
||||
if(this->suspendMask) {
|
||||
ESP_LOGW(TAG, "Radio TX suspended, dropping transmission");
|
||||
return 0;
|
||||
}
|
||||
if(!admits(TX_JOB_QUEUE_SIZE - this->count, priority)) {
|
||||
ESP_LOGE(TAG, "Radio TX queue full, dropping transmission");
|
||||
return 0;
|
||||
}
|
||||
// 0 means no job so it is skipped on wrap.
|
||||
if(++this->lastSeq == 0) ++this->lastSeq;
|
||||
job.seq = this->lastSeq;
|
||||
job.priority = priority;
|
||||
job.front = priority && !job.raw && !this->waitingFor(job.frame.remoteAddress);
|
||||
job.notBefore = 0;
|
||||
if(delayMs > 0) {
|
||||
job.notBefore = now + delayMs;
|
||||
if(job.notBefore == 0) job.notBefore = 1; // 0 means not delayed.
|
||||
}
|
||||
// Front jobs go out in the order they came, ahead of the back.
|
||||
uint8_t at = this->count;
|
||||
if(job.front) {
|
||||
at = 0;
|
||||
while(at < this->count && this->jobs[at].front) at++;
|
||||
}
|
||||
for(uint8_t i = this->count; i > at; i--) this->jobs[i] = this->jobs[i - 1];
|
||||
this->jobs[at] = job;
|
||||
this->count++;
|
||||
return job.seq;
|
||||
}
|
||||
bool RadioScheduler::hasRoom(bool priority) const {
|
||||
return !this->suspendMask && admits(TX_JOB_QUEUE_SIZE - this->count, priority);
|
||||
}
|
||||
bool RadioScheduler::waitingFor(uint32_t remoteAddress) const {
|
||||
for(uint8_t i = 0; i < this->count; i++) {
|
||||
if(!this->jobs[i].raw && this->jobs[i].frame.remoteAddress == remoteAddress) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void RadioScheduler::remove(uint8_t i) {
|
||||
for(; i + 1 < this->count; i++) this->jobs[i] = this->jobs[i + 1];
|
||||
this->count--;
|
||||
}
|
||||
bool RadioScheduler::next(uint32_t now, somfy_tx_job_t &job, uint32_t &waitMs) {
|
||||
waitMs = RADIO_WAIT_FOREVER;
|
||||
if(this->count == 0) return false;
|
||||
// Jobs go out in order, so a delayed job holds up the ones behind it. A
|
||||
// front job queued meanwhile is ahead of it.
|
||||
const somfy_tx_job_t &head = this->jobs[0];
|
||||
if(head.notBefore != 0) {
|
||||
// Works across a millis() wrap.
|
||||
const int32_t due = (int32_t)(head.notBefore - now);
|
||||
if(due > 0) {
|
||||
waitMs = (uint32_t)due;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
job = head;
|
||||
this->remove(0);
|
||||
this->taken = job.seq;
|
||||
this->takenFlushed = false;
|
||||
this->takenJob = job;
|
||||
return true;
|
||||
}
|
||||
bool RadioScheduler::start(uint32_t ticket, uint32_t now) {
|
||||
if(ticket == 0 || ticket != this->taken) return false;
|
||||
if(this->takenFlushed) {
|
||||
// Flushed after the radio task took it.
|
||||
this->post(tx_event_t::dropped, this->takenJob, now);
|
||||
this->taken = 0;
|
||||
return false;
|
||||
}
|
||||
this->post(tx_event_t::started, this->takenJob, now);
|
||||
return true;
|
||||
}
|
||||
bool RadioScheduler::keepSending(uint32_t ticket) const {
|
||||
return ticket != 0 && ticket == this->taken && !this->takenFlushed;
|
||||
}
|
||||
void RadioScheduler::ended(uint32_t ticket, bool complete, uint32_t now) {
|
||||
if(ticket == 0 || ticket != this->taken) return;
|
||||
this->post(complete ? tx_event_t::finished : tx_event_t::aborted, this->takenJob, now);
|
||||
this->taken = 0;
|
||||
}
|
||||
void RadioScheduler::flush(uint32_t now) {
|
||||
// Every discarded job is reported as dropped.
|
||||
for(uint8_t i = 0; i < this->count; i++) this->post(tx_event_t::dropped, this->jobs[i], now);
|
||||
this->count = 0;
|
||||
if(this->taken) this->takenFlushed = true;
|
||||
}
|
||||
void RadioScheduler::suspend(uint8_t reason, uint32_t now) {
|
||||
this->suspendMask |= reason;
|
||||
this->flush(now);
|
||||
}
|
||||
void RadioScheduler::resume(uint8_t reason) { this->suspendMask &= ~reason; }
|
||||
void RadioScheduler::post(tx_event_t type, const somfy_tx_job_t &job, uint32_t at) {
|
||||
// Repeater frames don't belong to any of our remotes.
|
||||
if(job.raw) return;
|
||||
if(this->eventCount >= TX_EVENT_QUEUE_SIZE) {
|
||||
ESP_LOGW(TAG, "Radio TX event queue full");
|
||||
return;
|
||||
}
|
||||
somfy_tx_event_t &evt = this->events[(this->eventHead + this->eventCount) % TX_EVENT_QUEUE_SIZE];
|
||||
evt.type = type;
|
||||
evt.cmd = job.frame.cmd;
|
||||
evt.remoteAddress = job.frame.remoteAddress;
|
||||
evt.seq = job.seq;
|
||||
evt.at = at;
|
||||
this->eventCount++;
|
||||
}
|
||||
bool RadioScheduler::popEvent(somfy_tx_event_t &evt) {
|
||||
if(this->eventCount == 0) return false;
|
||||
evt = this->events[this->eventHead];
|
||||
this->eventHead = (uint8_t)((this->eventHead + 1) % TX_EVENT_QUEUE_SIZE);
|
||||
this->eventCount--;
|
||||
return true;
|
||||
}
|
||||
70
src/RadioScheduler.h
Normal file
70
src/RadioScheduler.h
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#ifndef RADIO_SCHEDULER_H
|
||||
#define RADIO_SCHEDULER_H
|
||||
#include <stdint.h>
|
||||
#include "TxBuffer.h"
|
||||
|
||||
// Reasons the radio must not transmit. TX resumes once every reason is cleared.
|
||||
#define TX_SUSPEND_SCAN 0x01
|
||||
#define TX_SUSPEND_OTA 0x02
|
||||
// No job is waiting so the radio task sleeps until it is woken.
|
||||
#define RADIO_WAIT_FOREVER 0xFFFFFFFFu
|
||||
|
||||
// The radio jobs waiting for, or on, the air (see GLOSSARY.md). It decides
|
||||
// their order, when a delayed job is due, what a flush or suspend drops, and
|
||||
// reports the outcome of every job. Kept free of FreeRTOS and the radio so it
|
||||
// can be unit tested on the PC (pio test -e native).
|
||||
//
|
||||
// Not thread safe. The caller serializes every call and passes the time in.
|
||||
class RadioScheduler {
|
||||
public:
|
||||
// Queues a job. Its ticket comes back and is also set in job.seq, 0 when
|
||||
// the queue is full or suspended. A priority job (a stop) may use the
|
||||
// reserved slots and goes ahead of every job queued at the back, unless a
|
||||
// job of its remote is already waiting: overtaking that would put the
|
||||
// rolling codes out of order. A delayed job doesn't go on air before
|
||||
// delayMs from now and holds up the jobs queued behind it.
|
||||
uint32_t submit(somfy_tx_job_t &job, bool priority, uint16_t delayMs, uint32_t now);
|
||||
// Whether a job would be taken right now.
|
||||
bool hasRoom(bool priority) const;
|
||||
// For the radio task: takes the job to send next. False when there is
|
||||
// none due, waitMs then says how long until there is (RADIO_WAIT_FOREVER
|
||||
// when nothing waits). A job taken goes on air through start() and
|
||||
// ended().
|
||||
bool next(uint32_t now, somfy_tx_job_t &job, uint32_t &waitMs);
|
||||
// The job taken is about to go on air. False when it was flushed since,
|
||||
// it is then dropped and must not be sent.
|
||||
bool start(uint32_t ticket, uint32_t now);
|
||||
// Whether the job on air should send its next frame. A flush cuts it
|
||||
// short.
|
||||
bool keepSending(uint32_t ticket) const;
|
||||
// The job on air stopped, after every frame when complete.
|
||||
void ended(uint32_t ticket, bool complete, uint32_t now);
|
||||
// Drops every waiting job and cuts short the one on air.
|
||||
void flush(uint32_t now);
|
||||
// Refuses new jobs until every reason (TX_SUSPEND_*) is resumed. Flushes.
|
||||
void suspend(uint8_t reason, uint32_t now);
|
||||
void resume(uint8_t reason);
|
||||
bool suspended() const { return this->suspendMask != 0; }
|
||||
// Takes the oldest job outcome not yet handed out. Every job submitted
|
||||
// that isn't a repeater frame is started and then finished or aborted, or
|
||||
// dropped.
|
||||
bool popEvent(somfy_tx_event_t &evt);
|
||||
private:
|
||||
somfy_tx_job_t jobs[TX_JOB_QUEUE_SIZE];
|
||||
uint8_t count = 0;
|
||||
uint32_t lastSeq = 0;
|
||||
uint8_t suspendMask = 0;
|
||||
// The job taken by next() and not yet ended (0 for none), kept for its
|
||||
// events.
|
||||
uint32_t taken = 0;
|
||||
somfy_tx_job_t takenJob;
|
||||
bool takenFlushed = false;
|
||||
somfy_tx_event_t events[TX_JOB_QUEUE_SIZE * 3];
|
||||
uint8_t eventHead = 0;
|
||||
uint8_t eventCount = 0;
|
||||
// Whether a job of the remote is still waiting.
|
||||
bool waitingFor(uint32_t remoteAddress) const;
|
||||
void remove(uint8_t i);
|
||||
void post(tx_event_t type, const somfy_tx_job_t &job, uint32_t at);
|
||||
};
|
||||
#endif
|
||||
223
src/Somfy.cpp
223
src/Somfy.cpp
|
|
@ -12,7 +12,6 @@
|
|||
#include "ConfigSettings.h"
|
||||
#include "Somfy.h"
|
||||
#include "ShadeMotion.h"
|
||||
#include "TxQueuePolicy.h"
|
||||
#include "RollingCode.h"
|
||||
#include "Sockets.h"
|
||||
#include "MQTT.h"
|
||||
|
|
@ -49,32 +48,13 @@ uint8_t rxmode = 0; // Indicates whether the radio is in receive mode. Just to
|
|||
// Serializes all access to the CC1101 (SPI) and the TX/RX pins between the
|
||||
// radio task, the main loop and the async web server task.
|
||||
static SemaphoreHandle_t radioMutex = nullptr;
|
||||
// Makes queueing a job and flushing the queue atomic with respect to each other
|
||||
// so a job queueTx() reported as queued can't be silently lost to a flush.
|
||||
// Serializes every call into the RadioScheduler. The radio task, the main loop
|
||||
// and the async web server task all use it.
|
||||
static SemaphoreHandle_t txQueueMutex = nullptr;
|
||||
static QueueHandle_t txJobQueue = nullptr;
|
||||
static QueueHandle_t txEventQueue = nullptr;
|
||||
static TaskHandle_t txTaskHandle = nullptr;
|
||||
// Bumped by flushTx(). A job from an older generation that the radio task
|
||||
// already took off the queue is dropped, and one in flight stops after its
|
||||
// current frame.
|
||||
static std::atomic<uint32_t> txGen{0};
|
||||
// Number of tasks waiting for the radio to reconfigure it. A job in flight
|
||||
// stops after its current frame so they never wait for a whole transmission.
|
||||
static std::atomic<uint32_t> txPreemptWaiters{0};
|
||||
// TX_SUSPEND_* reasons the radio must not transmit for (frequency scan, OTA).
|
||||
static std::atomic<uint32_t> txSuspendMask{0};
|
||||
// Last sequence number handed to a queued job. Only touched under TxQueueLock.
|
||||
static uint32_t txSeq = 0;
|
||||
// Sequence number of the last job queued at the back that the radio task took
|
||||
// (a delayed one only once it is due) or a flush dropped. Those jobs go out
|
||||
// in order so any later one is still waiting in the queue.
|
||||
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(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.
|
||||
#define TX_PENDING_TIMEOUT 30000
|
||||
|
|
@ -94,17 +74,6 @@ struct TxQueueLock {
|
|||
TxQueueLock() { if(txQueueMutex) xSemaphoreTake(txQueueMutex, portMAX_DELAY); }
|
||||
~TxQueueLock() { if(txQueueMutex) xSemaphoreGive(txQueueMutex); }
|
||||
};
|
||||
static void postTxEvent(tx_event_t type, const somfy_tx_job_t &job, uint32_t at) {
|
||||
// Repeater frames don't belong to any of our remotes.
|
||||
if(job.raw || !txEventQueue) return;
|
||||
somfy_tx_event_t evt;
|
||||
evt.type = type;
|
||||
evt.cmd = job.frame.cmd;
|
||||
evt.remoteAddress = job.frame.remoteAddress;
|
||||
evt.seq = job.seq;
|
||||
evt.at = at;
|
||||
if(xQueueSend(txEventQueue, &evt, 0) != pdTRUE) ESP_LOGW(TAG, "Radio TX event queue full");
|
||||
}
|
||||
// Commands that can set a shade moving.
|
||||
static bool startsMovement(somfy_commands cmd) {
|
||||
switch(cmd) {
|
||||
|
|
@ -2353,8 +2322,8 @@ void SomfyShade::processTxEvent(const somfy_tx_event_t &evt) {
|
|||
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->tx.stopSent(this->lastTxSeq, now, dir, tilt);
|
||||
if(const send_result_t sent = SomfyRemote::sendCommandAfter(somfy_commands::My, this->repeats, 0, 0, true)) {
|
||||
this->tx.stopSent(sent.ticket, now, dir, tilt);
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "[Shade %u] Stop not sent, running on to the limit", this->shadeId);
|
||||
|
|
@ -2364,10 +2333,10 @@ bool SomfyShade::sendStop(int8_t dir, bool tilt) {
|
|||
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->tx.setMySent(this->lastTxSeq, oldPos, oldTiltPos);
|
||||
if(const send_result_t sent = SomfyRemote::sendCommandAfter(somfy_commands::My, SETMY_REPEATS, 0, delayMs)) {
|
||||
this->tx.setMySent(sent.ticket, oldPos, oldTiltPos);
|
||||
// Without the radio (GPIO remotes) there is nothing to wait for.
|
||||
if(this->lastTxSeq == 0) this->commitMyPosition();
|
||||
if(sent.ticket == 0) this->commitMyPosition();
|
||||
return true;
|
||||
}
|
||||
// The motor never learned it so keep the favorite it has.
|
||||
|
|
@ -3481,8 +3450,8 @@ bool SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repe
|
|||
return true;
|
||||
}
|
||||
bool SomfyRemote::sendCommand(somfy_commands cmd) { return this->sendCommand(cmd, this->repeats); }
|
||||
bool SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { return this->sendCommandAfter(cmd, repeat, stepSize, 0); }
|
||||
bool SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority) {
|
||||
bool SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { return this->sendCommandAfter(cmd, repeat, stepSize, 0).sent; }
|
||||
send_result_t SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority) {
|
||||
// Build the frame aside and only take the rolling code once the frame is
|
||||
// queued so a dropped command leaves the remote untouched.
|
||||
somfy_frame_t frame = this->lastFrame;
|
||||
|
|
@ -3501,7 +3470,7 @@ bool SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t s
|
|||
// We have to set the processed to clear this if we are sending
|
||||
// another command.
|
||||
frame.processed = false;
|
||||
uint32_t seq = 0;
|
||||
send_result_t result;
|
||||
if(this->proto == radio_proto::GP_Relay) {
|
||||
ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d SETTING GPIO",
|
||||
translateSomfyCommand(frame.cmd).c_str(),
|
||||
|
|
@ -3517,19 +3486,18 @@ 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);
|
||||
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;
|
||||
result.ticket = somfy.sendFrame(frame, repeat, delayMs, priority);
|
||||
if(result.ticket == 0) return result;
|
||||
}
|
||||
result.sent = true;
|
||||
this->commitRollingCode(frame.rollingCode);
|
||||
this->lastFrame = frame;
|
||||
this->lastTxSeq = seq;
|
||||
somfy.processFrame(this->lastFrame, true);
|
||||
// Idle shades (and group members) this sets moving wait for the frame to go
|
||||
// on air. Stops are sent ahead of everything else so they don't need to.
|
||||
if(seq != 0 && !priority && startsMovement(frame.cmd)) somfy.markTxPending(frame.remoteAddress, seq);
|
||||
return true;
|
||||
if(result.ticket != 0 && !priority && startsMovement(frame.cmd)) somfy.markTxPending(frame.remoteAddress, result.ticket);
|
||||
return result;
|
||||
}
|
||||
bool SomfyRemote::isLastCommand(somfy_commands cmd) {
|
||||
if(this->lastFrame.cmd != cmd || this->lastFrame.rollingCode != this->lastRollingCode) {
|
||||
|
|
@ -3547,22 +3515,16 @@ bool SomfyRemote::repeatFrame(uint8_t repeat) {
|
|||
}
|
||||
somfy_tx_job_t job;
|
||||
encodeRepeatJob(job, this->lastFrame, repeat);
|
||||
if(!somfy.transceiver.queueTx(job)) return false;
|
||||
this->lastBackTxSeq = job.seq;
|
||||
if(!somfy.transceiver.submitTx(job)) return false;
|
||||
noteRepeatQueued(this->lastFrame, job);
|
||||
//somfy.processFrame(this->lastFrame, true);
|
||||
return true;
|
||||
}
|
||||
bool SomfyShadeController::sendFrame(somfy_frame_t &frame, uint8_t repeat, uint16_t delayMs, bool priority, bool front, uint32_t *seq) {
|
||||
uint32_t SomfyShadeController::sendFrame(somfy_frame_t &frame, uint8_t repeat, uint16_t delayMs, bool priority) {
|
||||
somfy_tx_job_t job;
|
||||
encodeTxJob(job, frame, repeat);
|
||||
job.firstRepeat = 1;
|
||||
job.priority = priority;
|
||||
job.front = priority && front;
|
||||
if(delayMs > 0) job.notBefore = millis() + delayMs;
|
||||
if(!this->transceiver.queueTx(job)) return false;
|
||||
if(seq) *seq = job.seq;
|
||||
return true;
|
||||
return this->transceiver.submitTx(job, priority, delayMs);
|
||||
}
|
||||
bool SomfyShadeController::deleteShade(uint8_t shadeId) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
|
|
@ -4598,7 +4560,11 @@ bool Transceiver::begin() {
|
|||
void Transceiver::loop() {
|
||||
somfy_rx_t rx;
|
||||
somfy_tx_event_t evt;
|
||||
while(txEventQueue && xQueueReceive(txEventQueue, &evt, 0) == pdTRUE) {
|
||||
for(;;) {
|
||||
{
|
||||
TxQueueLock lock;
|
||||
if(!this->sched.popEvent(evt)) break;
|
||||
}
|
||||
if(evt.type == tx_event_t::dropped)
|
||||
ESP_LOGW(TAG, "Radio TX dropped %s for %d", translateSomfyCommand(evt.cmd).c_str(), evt.remoteAddress);
|
||||
else if(evt.type == tx_event_t::aborted)
|
||||
|
|
@ -4658,7 +4624,7 @@ void Transceiver::loop() {
|
|||
snprintf(frame_buf + fpos, sizeof(frame_buf) - fpos, "]");
|
||||
ESP_LOGD(TAG, "%s", frame_buf);
|
||||
}
|
||||
if(this->queueTx(job) || this->txSuspended()) {
|
||||
if(this->submitTx(job) || this->txSuspended()) {
|
||||
somfy_tx_t sent;
|
||||
tx_queue.pop(&sent);
|
||||
}
|
||||
|
|
@ -4666,63 +4632,50 @@ void Transceiver::loop() {
|
|||
}
|
||||
}
|
||||
}
|
||||
bool Transceiver::queueTx(somfy_tx_job_t &job) {
|
||||
if(!txJobQueue || !txTaskHandle) {
|
||||
uint32_t Transceiver::submitTx(somfy_tx_job_t &job, bool priority, uint16_t delayMs) {
|
||||
if(!txTaskHandle) {
|
||||
ESP_LOGE(TAG, "Radio task not started, dropping transmission");
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
uint32_t ticket;
|
||||
{
|
||||
TxQueueLock lock;
|
||||
ticket = this->sched.submit(job, priority, delayMs, millis());
|
||||
}
|
||||
// The radio task may be idle, or waiting on a delayed job this one is ahead of.
|
||||
if(ticket) xTaskNotifyGive(txTaskHandle);
|
||||
return ticket;
|
||||
}
|
||||
bool Transceiver::txSuspended() {
|
||||
TxQueueLock lock;
|
||||
if(txSuspendMask) {
|
||||
ESP_LOGW(TAG, "Radio TX suspended, dropping transmission");
|
||||
return false;
|
||||
}
|
||||
if(!txAdmits(uxQueueSpacesAvailable(txJobQueue), job.priority)) {
|
||||
ESP_LOGE(TAG, "Radio TX queue full, dropping transmission");
|
||||
return false;
|
||||
}
|
||||
job.gen = txGen;
|
||||
txSeq = nextTxSeq(txSeq);
|
||||
job.seq = txSeq;
|
||||
// We are the only producer while holding the lock so the space check holds.
|
||||
if(job.front) {
|
||||
xQueueSendToFront(txJobQueue, &job, 0);
|
||||
// It may overtake a delayed job the radio task is waiting on.
|
||||
xTaskNotifyGive(txTaskHandle);
|
||||
}
|
||||
else xQueueSendToBack(txJobQueue, &job, 0);
|
||||
return true;
|
||||
return this->sched.suspended();
|
||||
}
|
||||
bool Transceiver::isQueued(uint32_t seq) {
|
||||
// Only meaningful for jobs queued at the back.
|
||||
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 txAdmits(uxQueueSpacesAvailable(txJobQueue), false);
|
||||
// Without a radio task submitTx() fails straight away so there is no point waiting.
|
||||
if(!txTaskHandle) return true;
|
||||
TxQueueLock lock;
|
||||
return this->sched.hasRoom(false);
|
||||
}
|
||||
void Transceiver::flushTx() {
|
||||
TxQueueLock lock;
|
||||
++txGen;
|
||||
advanceBackTaken(txSeq);
|
||||
if(!txJobQueue) return;
|
||||
// Drain rather than reset so every discarded job is reported as dropped.
|
||||
somfy_tx_job_t job;
|
||||
const uint32_t now = millis();
|
||||
while(xQueueReceive(txJobQueue, &job, 0) == pdTRUE) postTxEvent(tx_event_t::dropped, job, now);
|
||||
// Drop a delayed job the radio task is waiting on now rather than when it is due.
|
||||
{
|
||||
TxQueueLock lock;
|
||||
this->sched.flush(millis());
|
||||
}
|
||||
// Stop waiting on a delayed job that is gone.
|
||||
if(txTaskHandle) xTaskNotifyGive(txTaskHandle);
|
||||
}
|
||||
void Transceiver::suspendTx(uint8_t reason) {
|
||||
{
|
||||
// Set under the queue lock so nothing is queued between this and the flush.
|
||||
// Suspends and flushes in one go so nothing is queued in between.
|
||||
TxQueueLock lock;
|
||||
txSuspendMask |= reason;
|
||||
this->sched.suspend(reason, millis());
|
||||
}
|
||||
this->flushTx();
|
||||
if(txTaskHandle) xTaskNotifyGive(txTaskHandle);
|
||||
}
|
||||
void Transceiver::resumeTx(uint8_t reason) {
|
||||
TxQueueLock lock;
|
||||
this->sched.resume(reason);
|
||||
}
|
||||
void Transceiver::resumeTx(uint8_t reason) { txSuspendMask &= ~(uint32_t)reason; }
|
||||
// cpt_synchro_hw is updated by the RX interrupt and read here from the radio task.
|
||||
static bool rxInProgress() { return *(volatile uint8_t *)&somfy_rx.cpt_synchro_hw != 0; }
|
||||
bool Transceiver::transmitJob(somfy_tx_job_t &job) {
|
||||
|
|
@ -4731,9 +4684,14 @@ bool Transceiver::transmitJob(somfy_tx_job_t &job) {
|
|||
this->beginTransmit();
|
||||
this->sendFrame(tx.payload, tx.hwsync, tx.bit_length);
|
||||
for(uint8_t i = 0; i < job.repeats; i++) {
|
||||
bool keep;
|
||||
{
|
||||
TxQueueLock lock;
|
||||
keep = this->sched.keepSending(job.seq);
|
||||
}
|
||||
// Stop early when flushed or when someone is waiting to reconfigure the
|
||||
// radio so they get it within a frame.
|
||||
if(job.gen != txGen || txPreemptWaiters > 0) {
|
||||
if(!keep || txPreemptWaiters > 0) {
|
||||
complete = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -4753,66 +4711,37 @@ void Transceiver::runJob(somfy_tx_job_t &job) {
|
|||
}
|
||||
{
|
||||
RadioLock lock;
|
||||
// The queue may have been flushed while we waited.
|
||||
if(txSuspendMask || job.gen != txGen) {
|
||||
postTxEvent(tx_event_t::dropped, job, millis());
|
||||
return;
|
||||
bool start;
|
||||
{
|
||||
// The queue may have been flushed while we waited.
|
||||
TxQueueLock qlock;
|
||||
start = this->sched.start(job.seq, millis());
|
||||
}
|
||||
postTxEvent(tx_event_t::started, job, millis());
|
||||
if(!start) return;
|
||||
const bool complete = this->transmitJob(job);
|
||||
postTxEvent(complete ? tx_event_t::finished : tx_event_t::aborted, job, millis());
|
||||
TxQueueLock qlock;
|
||||
this->sched.ended(job.seq, complete, millis());
|
||||
}
|
||||
// Give the motors a full frame beat between separate transmissions.
|
||||
vTaskDelay(pdMS_TO_TICKS(TX_QUEUE_DELAY));
|
||||
}
|
||||
void Transceiver::waitUntilDue(const somfy_tx_job_t &job) {
|
||||
for(;;) {
|
||||
if(txSuspendMask || job.gen != txGen) return;
|
||||
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.
|
||||
somfy_tx_job_t next;
|
||||
bool haveNext = false;
|
||||
{
|
||||
TxQueueLock lock;
|
||||
if(xQueuePeek(txJobQueue, &next, 0) == pdTRUE && next.front)
|
||||
haveNext = xQueueReceive(txJobQueue, &next, 0) == pdTRUE;
|
||||
}
|
||||
if(haveNext) {
|
||||
this->runJob(next);
|
||||
continue;
|
||||
}
|
||||
// queueTx() and flushTx() wake us for a new front job or a flush.
|
||||
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(wait));
|
||||
}
|
||||
}
|
||||
void Transceiver::txTask(void *arg) {
|
||||
Transceiver *trans = static_cast<Transceiver *>(arg);
|
||||
somfy_tx_job_t job;
|
||||
for(;;) {
|
||||
if(xQueueReceive(txJobQueue, &job, portMAX_DELAY) != pdTRUE) continue;
|
||||
if(job.notBefore != 0) trans->waitUntilDue(job);
|
||||
// Only now is a delayed job off the queue as far as isQueued() is
|
||||
// concerned, so a stop for its remote queues behind it while it waits.
|
||||
if(!job.front) advanceBackTaken(job.seq);
|
||||
// Flushed after we took it off the queue.
|
||||
if(txSuspendMask || job.gen != txGen) {
|
||||
postTxEvent(tx_event_t::dropped, job, millis());
|
||||
continue;
|
||||
uint32_t waitMs;
|
||||
bool haveJob;
|
||||
{
|
||||
TxQueueLock lock;
|
||||
haveJob = trans->sched.next(millis(), job, waitMs);
|
||||
}
|
||||
trans->runJob(job);
|
||||
if(haveJob) trans->runJob(job);
|
||||
// submitTx() and flushTx() wake us for a new job or a flush.
|
||||
else ulTaskNotifyTake(pdTRUE, waitMs == RADIO_WAIT_FOREVER ? portMAX_DELAY : pdMS_TO_TICKS(waitMs) + 1);
|
||||
}
|
||||
}
|
||||
void Transceiver::startTxTask() {
|
||||
if(txTaskHandle) return;
|
||||
if(!txJobQueue) txJobQueue = xQueueCreate(TX_JOB_QUEUE_SIZE, sizeof(somfy_tx_job_t));
|
||||
// Every job reports when it starts and ends, plus room for the jobs a flush drains.
|
||||
if(!txEventQueue) txEventQueue = xQueueCreate(TX_JOB_QUEUE_SIZE * 3, sizeof(somfy_tx_event_t));
|
||||
if(!txJobQueue) {
|
||||
ESP_LOGE(TAG, "Unable to create radio TX queue");
|
||||
return;
|
||||
}
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
xTaskCreate(Transceiver::txTask, "radio_tx", TX_TASK_STACK, this, TX_TASK_PRIORITY, &txTaskHandle);
|
||||
#else
|
||||
|
|
|
|||
28
src/Somfy.h
28
src/Somfy.h
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
#include "SomfyFrame.h"
|
||||
#include "TxBuffer.h"
|
||||
#include "RadioScheduler.h"
|
||||
#include "ShadeMotion.h"
|
||||
enum class group_types : byte {
|
||||
channel = 0x00
|
||||
|
|
@ -115,9 +116,6 @@ struct somfy_relay_t {
|
|||
uint8_t sync = 0;
|
||||
byte frame[10] = {0};
|
||||
};
|
||||
// Reasons the radio must not transmit. TX resumes once every reason is cleared.
|
||||
#define TX_SUSPEND_SCAN 0x01
|
||||
#define TX_SUSPEND_OTA 0x02
|
||||
|
||||
class SomfyRoom {
|
||||
public:
|
||||
|
|
@ -133,6 +131,13 @@ class SomfyRoom {
|
|||
void unpublish();
|
||||
};
|
||||
|
||||
// What sending a command did. ticket is the radio job carrying it, 0 when the
|
||||
// command went out over GPIO instead.
|
||||
struct send_result_t {
|
||||
bool sent = false;
|
||||
uint32_t ticket = 0;
|
||||
explicit operator bool() const { return this->sent; }
|
||||
};
|
||||
class SomfyRemote {
|
||||
// These sizes for the data have been
|
||||
// confirmed. The address is actually 24bits
|
||||
|
|
@ -154,8 +159,6 @@ class SomfyRemote {
|
|||
somfy_frame_t lastFrame;
|
||||
bool flipCommands = false;
|
||||
uint16_t lastRollingCode = 0;
|
||||
uint32_t lastTxSeq = 0; // Radio job of the last command sent, 0 if it didn't use the radio.
|
||||
uint32_t lastBackTxSeq = 0; // Last radio job queued at the back of the queue.
|
||||
uint8_t flags = 0;
|
||||
uint8_t bitLength = 0;
|
||||
uint8_t repeats = 1;
|
||||
|
|
@ -176,7 +179,7 @@ class SomfyRemote {
|
|||
// command. Nothing about the remote or shade state has changed in that case.
|
||||
virtual bool sendCommand(somfy_commands cmd);
|
||||
virtual bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
|
||||
bool sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority = false);
|
||||
send_result_t sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority = false);
|
||||
bool sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat);
|
||||
bool repeatFrame(uint8_t repeat);
|
||||
virtual uint16_t p_lastRollingCode(uint16_t code);
|
||||
|
|
@ -426,13 +429,12 @@ class Transceiver {
|
|||
static void txTask(void *arg);
|
||||
bool _received = false;
|
||||
somfy_frame_t frame;
|
||||
// Only touched under the TX queue lock.
|
||||
RadioScheduler sched;
|
||||
void startTxTask();
|
||||
bool transmitJob(somfy_tx_job_t &job);
|
||||
// Sends a job taken off the queue and leaves a frame beat after it.
|
||||
void runJob(somfy_tx_job_t &job);
|
||||
// Holds a delayed job until it is due, sending stops queued at the front
|
||||
// in the meantime.
|
||||
void waitUntilDue(const somfy_tx_job_t &job);
|
||||
public:
|
||||
transceiver_config_t config;
|
||||
bool printBuffer = false;
|
||||
|
|
@ -449,11 +451,12 @@ class Transceiver {
|
|||
void disableReceive();
|
||||
somfy_frame_t& lastFrame();
|
||||
void sendFrame(byte *frame, uint8_t sync, uint8_t bitLength = 56);
|
||||
bool queueTx(somfy_tx_job_t &job);
|
||||
// Hands a job to the radio task, see RadioScheduler::submit(). Returns its
|
||||
// ticket, 0 when the radio can't take it. Safe from any task.
|
||||
uint32_t submitTx(somfy_tx_job_t &job, bool priority = false, uint16_t delayMs = 0);
|
||||
void flushTx();
|
||||
void suspendTx(uint8_t reason);
|
||||
void resumeTx(uint8_t reason);
|
||||
bool isQueued(uint32_t seq);
|
||||
bool txSuspended();
|
||||
bool txHasRoom();
|
||||
void beginTransmit();
|
||||
|
|
@ -511,7 +514,8 @@ class SomfyShadeController {
|
|||
SomfyGroup * getGroupById(uint8_t groupId);
|
||||
SomfyShade * findShadeByRemoteAddress(uint32_t address);
|
||||
SomfyGroup * findGroupByRemoteAddress(uint32_t address);
|
||||
bool sendFrame(somfy_frame_t &frame, uint8_t repeats = 0, uint16_t delayMs = 0, bool priority = false, bool front = false, uint32_t *seq = nullptr);
|
||||
// Returns the radio job's ticket, 0 when the radio can't take it.
|
||||
uint32_t sendFrame(somfy_frame_t &frame, uint8_t repeats = 0, uint16_t delayMs = 0, bool priority = false);
|
||||
void processFrame(somfy_frame_t &frame, bool internal = false);
|
||||
void markTxPending(uint32_t remoteAddress, uint32_t seq);
|
||||
void processTxEvent(const somfy_tx_event_t &evt);
|
||||
|
|
|
|||
|
|
@ -58,10 +58,10 @@ struct somfy_tx_job_t {
|
|||
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.
|
||||
// Set by RadioScheduler::submit().
|
||||
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.
|
||||
bool front = false; // Queued ahead of the back. Only for priority jobs.
|
||||
uint32_t seq = 0; // Ticket the job's events are matched up by.
|
||||
uint32_t notBefore = 0; // millis() before which the job must not go on air (0 = none).
|
||||
};
|
||||
enum class tx_event_t : uint8_t {
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
#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
|
||||
417
test/test_radioscheduler/test_radioscheduler.cpp
Normal file
417
test/test_radioscheduler/test_radioscheduler.cpp
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
#include <unity.h>
|
||||
#include "RadioScheduler.h"
|
||||
|
||||
static const uint32_t REMOTE_A = 0x111111;
|
||||
static const uint32_t REMOTE_B = 0x222222;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static somfy_tx_job_t makeJob(uint32_t remoteAddress, somfy_commands cmd = somfy_commands::Up) {
|
||||
somfy_tx_job_t job;
|
||||
job.frame.remoteAddress = remoteAddress;
|
||||
job.frame.cmd = cmd;
|
||||
job.repeats = 2;
|
||||
return job;
|
||||
}
|
||||
static uint32_t submit(RadioScheduler &s, uint32_t remoteAddress, uint32_t now = 0, bool priority = false, uint16_t delayMs = 0) {
|
||||
somfy_tx_job_t job = makeJob(remoteAddress, priority ? somfy_commands::My : somfy_commands::Up);
|
||||
return s.submit(job, priority, delayMs, now);
|
||||
}
|
||||
// Takes the next job and sends all of it, as the radio task does. Returns its
|
||||
// ticket, 0 when none is due.
|
||||
static uint32_t runNext(RadioScheduler &s, uint32_t now = 0) {
|
||||
somfy_tx_job_t job;
|
||||
uint32_t waitMs;
|
||||
if(!s.next(now, job, waitMs)) return 0;
|
||||
if(!s.start(job.seq, now)) return job.seq;
|
||||
s.ended(job.seq, true, now);
|
||||
return job.seq;
|
||||
}
|
||||
static void assertEvent(RadioScheduler &s, tx_event_t type, uint32_t ticket) {
|
||||
somfy_tx_event_t evt;
|
||||
TEST_ASSERT_TRUE_MESSAGE(s.popEvent(evt), "expected another event");
|
||||
TEST_ASSERT_EQUAL_UINT8((uint8_t)type, (uint8_t)evt.type);
|
||||
TEST_ASSERT_EQUAL_UINT32(ticket, evt.seq);
|
||||
}
|
||||
static void assertNoEvents(RadioScheduler &s) {
|
||||
somfy_tx_event_t evt;
|
||||
TEST_ASSERT_FALSE(s.popEvent(evt));
|
||||
}
|
||||
static void discardEvents(RadioScheduler &s) {
|
||||
somfy_tx_event_t evt;
|
||||
while(s.popEvent(evt)) {}
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Order
|
||||
********************************************************************/
|
||||
static void test_jobs_go_out_in_the_order_they_came() {
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A);
|
||||
const uint32_t b = submit(s, REMOTE_B);
|
||||
const uint32_t c = submit(s, REMOTE_A);
|
||||
TEST_ASSERT_NOT_EQUAL(0, a);
|
||||
TEST_ASSERT_EQUAL_UINT32(a, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(b, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(c, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, runNext(s));
|
||||
}
|
||||
static void test_tickets_are_distinct_and_set_in_the_job() {
|
||||
RadioScheduler s;
|
||||
somfy_tx_job_t job = makeJob(REMOTE_A);
|
||||
const uint32_t a = s.submit(job, false, 0, 0);
|
||||
TEST_ASSERT_EQUAL_UINT32(a, job.seq);
|
||||
TEST_ASSERT_NOT_EQUAL(a, submit(s, REMOTE_A));
|
||||
}
|
||||
static void test_nothing_waiting_sleeps_until_woken() {
|
||||
RadioScheduler s;
|
||||
somfy_tx_job_t job;
|
||||
uint32_t waitMs = 0;
|
||||
TEST_ASSERT_FALSE(s.next(0, job, waitMs));
|
||||
TEST_ASSERT_EQUAL_UINT32(RADIO_WAIT_FOREVER, waitMs);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Stops (priority jobs)
|
||||
********************************************************************/
|
||||
static void test_stop_goes_ahead_of_other_remotes() {
|
||||
RadioScheduler s;
|
||||
const uint32_t b1 = submit(s, REMOTE_B);
|
||||
const uint32_t b2 = submit(s, REMOTE_B);
|
||||
const uint32_t stop = submit(s, REMOTE_A, 0, true);
|
||||
TEST_ASSERT_EQUAL_UINT32(stop, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(b1, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(b2, runNext(s));
|
||||
}
|
||||
static void test_stop_queues_behind_its_own_remote() {
|
||||
// Overtaking the remote's waiting frame would send the rolling codes out of
|
||||
// order and the motor would reject the older one.
|
||||
RadioScheduler s;
|
||||
const uint32_t up = submit(s, REMOTE_A);
|
||||
const uint32_t b = submit(s, REMOTE_B);
|
||||
const uint32_t stop = submit(s, REMOTE_A, 0, true);
|
||||
TEST_ASSERT_EQUAL_UINT32(up, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(b, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(stop, runNext(s));
|
||||
}
|
||||
static void test_stop_goes_ahead_once_its_remote_is_on_air() {
|
||||
RadioScheduler s;
|
||||
const uint32_t up = submit(s, REMOTE_A);
|
||||
const uint32_t b = submit(s, REMOTE_B);
|
||||
somfy_tx_job_t job;
|
||||
uint32_t waitMs;
|
||||
TEST_ASSERT_TRUE(s.next(0, job, waitMs));
|
||||
TEST_ASSERT_EQUAL_UINT32(up, job.seq);
|
||||
// The remote's frame is already going out so the stop can't overtake it.
|
||||
const uint32_t stop = submit(s, REMOTE_A, 0, true);
|
||||
s.start(up, 0);
|
||||
s.ended(up, true, 0);
|
||||
TEST_ASSERT_EQUAL_UINT32(stop, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(b, runNext(s));
|
||||
}
|
||||
static void test_stops_go_out_in_the_order_they_came() {
|
||||
RadioScheduler s;
|
||||
const uint32_t back = submit(s, REMOTE_B);
|
||||
const uint32_t stopA = submit(s, REMOTE_A, 0, true);
|
||||
const uint32_t stopC = submit(s, 0x333333, 0, true);
|
||||
TEST_ASSERT_EQUAL_UINT32(stopA, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(stopC, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(back, runNext(s));
|
||||
}
|
||||
static void test_second_stop_of_a_remote_stays_behind_the_first() {
|
||||
RadioScheduler s;
|
||||
const uint32_t back = submit(s, REMOTE_B);
|
||||
const uint32_t first = submit(s, REMOTE_A, 0, true);
|
||||
const uint32_t second = submit(s, REMOTE_A, 0, true);
|
||||
TEST_ASSERT_EQUAL_UINT32(first, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(back, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(second, runNext(s));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Room
|
||||
********************************************************************/
|
||||
static void test_normal_jobs_leave_the_reserved_slots() {
|
||||
RadioScheduler s;
|
||||
for(uint8_t i = 0; i < TX_JOB_QUEUE_SIZE - TX_RESERVED_SLOTS; i++)
|
||||
TEST_ASSERT_NOT_EQUAL(0, submit(s, REMOTE_B));
|
||||
TEST_ASSERT_FALSE(s.hasRoom(false));
|
||||
TEST_ASSERT_TRUE(s.hasRoom(true));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, submit(s, REMOTE_B));
|
||||
}
|
||||
static void test_stops_use_the_reserved_slots() {
|
||||
RadioScheduler s;
|
||||
for(uint8_t i = 0; i < TX_JOB_QUEUE_SIZE - TX_RESERVED_SLOTS; i++) submit(s, REMOTE_B);
|
||||
for(uint8_t i = 0; i < TX_RESERVED_SLOTS; i++)
|
||||
TEST_ASSERT_NOT_EQUAL(0, submit(s, REMOTE_A, 0, true));
|
||||
TEST_ASSERT_FALSE(s.hasRoom(true));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, submit(s, REMOTE_A, 0, true));
|
||||
}
|
||||
static void test_a_refused_job_has_no_ticket() {
|
||||
RadioScheduler s;
|
||||
for(uint8_t i = 0; i < TX_JOB_QUEUE_SIZE - TX_RESERVED_SLOTS; i++) submit(s, REMOTE_B);
|
||||
somfy_tx_job_t job = makeJob(REMOTE_A);
|
||||
job.seq = 99;
|
||||
TEST_ASSERT_EQUAL_UINT32(0, s.submit(job, false, 0, 0));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, job.seq);
|
||||
}
|
||||
static void test_taking_a_job_frees_its_slot() {
|
||||
RadioScheduler s;
|
||||
for(uint8_t i = 0; i < TX_JOB_QUEUE_SIZE - TX_RESERVED_SLOTS; i++) submit(s, REMOTE_B);
|
||||
runNext(s);
|
||||
TEST_ASSERT_TRUE(s.hasRoom(false));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Delayed jobs
|
||||
********************************************************************/
|
||||
static void test_delayed_job_waits_until_due() {
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A, 1000, false, 500);
|
||||
somfy_tx_job_t job;
|
||||
uint32_t waitMs;
|
||||
TEST_ASSERT_FALSE(s.next(1000, job, waitMs));
|
||||
TEST_ASSERT_EQUAL_UINT32(500, waitMs);
|
||||
TEST_ASSERT_FALSE(s.next(1400, job, waitMs));
|
||||
TEST_ASSERT_EQUAL_UINT32(100, waitMs);
|
||||
TEST_ASSERT_TRUE(s.next(1500, job, waitMs));
|
||||
TEST_ASSERT_EQUAL_UINT32(a, job.seq);
|
||||
}
|
||||
static void test_delayed_job_holds_up_the_jobs_behind_it() {
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A, 0, false, 500);
|
||||
const uint32_t b = submit(s, REMOTE_B);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, runNext(s, 100));
|
||||
TEST_ASSERT_EQUAL_UINT32(a, runNext(s, 500));
|
||||
TEST_ASSERT_EQUAL_UINT32(b, runNext(s, 500));
|
||||
}
|
||||
static void test_stop_overtakes_a_delayed_job_of_another_remote() {
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A, 0, false, 500);
|
||||
const uint32_t stop = submit(s, REMOTE_B, 100, true);
|
||||
TEST_ASSERT_EQUAL_UINT32(stop, runNext(s, 100));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, runNext(s, 200));
|
||||
TEST_ASSERT_EQUAL_UINT32(a, runNext(s, 500));
|
||||
}
|
||||
static void test_stop_waits_behind_its_remotes_delayed_job() {
|
||||
// The favorite's long press holds a rolling code, so the stop can't go first.
|
||||
RadioScheduler s;
|
||||
const uint32_t setMy = submit(s, REMOTE_A, 0, false, 500);
|
||||
const uint32_t stop = submit(s, REMOTE_A, 100, true);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, runNext(s, 100));
|
||||
TEST_ASSERT_EQUAL_UINT32(setMy, runNext(s, 500));
|
||||
TEST_ASSERT_EQUAL_UINT32(stop, runNext(s, 500));
|
||||
}
|
||||
static void test_delay_across_millis_wrap() {
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A, 0xFFFFFF00u, false, 0x200);
|
||||
somfy_tx_job_t job;
|
||||
uint32_t waitMs;
|
||||
TEST_ASSERT_FALSE(s.next(0xFFFFFF00u, job, waitMs));
|
||||
TEST_ASSERT_EQUAL_UINT32(0x200, waitMs);
|
||||
TEST_ASSERT_FALSE(s.next(0x50, job, waitMs));
|
||||
TEST_ASSERT_EQUAL_UINT32(0xB0, waitMs);
|
||||
TEST_ASSERT_TRUE(s.next(0x100, job, waitMs));
|
||||
TEST_ASSERT_EQUAL_UINT32(a, job.seq);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Outcomes
|
||||
********************************************************************/
|
||||
static void test_job_reports_started_then_finished() {
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A);
|
||||
somfy_tx_job_t job;
|
||||
uint32_t waitMs;
|
||||
s.next(10, job, waitMs);
|
||||
TEST_ASSERT_TRUE(s.start(a, 20));
|
||||
TEST_ASSERT_TRUE(s.keepSending(a));
|
||||
s.ended(a, true, 300);
|
||||
somfy_tx_event_t evt;
|
||||
TEST_ASSERT_TRUE(s.popEvent(evt));
|
||||
TEST_ASSERT_EQUAL_UINT8((uint8_t)tx_event_t::started, (uint8_t)evt.type);
|
||||
TEST_ASSERT_EQUAL_UINT32(a, evt.seq);
|
||||
TEST_ASSERT_EQUAL_UINT32(20, evt.at);
|
||||
TEST_ASSERT_EQUAL_UINT32(REMOTE_A, evt.remoteAddress);
|
||||
TEST_ASSERT_EQUAL_UINT8((uint8_t)somfy_commands::Up, (uint8_t)evt.cmd);
|
||||
TEST_ASSERT_TRUE(s.popEvent(evt));
|
||||
TEST_ASSERT_EQUAL_UINT8((uint8_t)tx_event_t::finished, (uint8_t)evt.type);
|
||||
TEST_ASSERT_EQUAL_UINT32(300, evt.at);
|
||||
assertNoEvents(s);
|
||||
}
|
||||
static void test_cut_short_job_reports_aborted() {
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A);
|
||||
somfy_tx_job_t job;
|
||||
uint32_t waitMs;
|
||||
s.next(0, job, waitMs);
|
||||
s.start(a, 0);
|
||||
s.ended(a, false, 0);
|
||||
assertEvent(s, tx_event_t::started, a);
|
||||
assertEvent(s, tx_event_t::aborted, a);
|
||||
assertNoEvents(s);
|
||||
}
|
||||
static void test_flush_drops_every_waiting_job_once() {
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A);
|
||||
const uint32_t stop = submit(s, REMOTE_B, 0, true);
|
||||
const uint32_t delayed = submit(s, REMOTE_A, 0, false, 5000);
|
||||
s.flush(0);
|
||||
assertEvent(s, tx_event_t::dropped, stop);
|
||||
assertEvent(s, tx_event_t::dropped, a);
|
||||
assertEvent(s, tx_event_t::dropped, delayed);
|
||||
assertNoEvents(s);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, runNext(s, 5000));
|
||||
}
|
||||
static void test_flush_cuts_short_the_job_on_air() {
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A);
|
||||
somfy_tx_job_t job;
|
||||
uint32_t waitMs;
|
||||
s.next(0, job, waitMs);
|
||||
s.start(a, 0);
|
||||
s.flush(0);
|
||||
TEST_ASSERT_FALSE(s.keepSending(a));
|
||||
s.ended(a, false, 0);
|
||||
assertEvent(s, tx_event_t::started, a);
|
||||
assertEvent(s, tx_event_t::aborted, a);
|
||||
assertNoEvents(s);
|
||||
}
|
||||
static void test_job_flushed_before_it_starts_is_dropped() {
|
||||
// The radio task took it off the queue, then a flush came before it got the
|
||||
// radio.
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A);
|
||||
somfy_tx_job_t job;
|
||||
uint32_t waitMs;
|
||||
s.next(0, job, waitMs);
|
||||
s.flush(0);
|
||||
TEST_ASSERT_FALSE(s.start(a, 0));
|
||||
assertEvent(s, tx_event_t::dropped, a);
|
||||
assertNoEvents(s);
|
||||
}
|
||||
static void test_every_job_gets_one_outcome_across_a_flush() {
|
||||
RadioScheduler s;
|
||||
uint32_t tickets[6];
|
||||
for(uint8_t i = 0; i < 3; i++) tickets[i] = submit(s, REMOTE_A + i);
|
||||
somfy_tx_job_t job;
|
||||
uint32_t waitMs;
|
||||
s.next(0, job, waitMs);
|
||||
s.start(job.seq, 0);
|
||||
s.flush(0);
|
||||
// Jobs queued after the flush are unaffected by it.
|
||||
for(uint8_t i = 3; i < 6; i++) tickets[i] = submit(s, REMOTE_B + i);
|
||||
s.ended(job.seq, s.keepSending(job.seq), 0);
|
||||
while(runNext(s)) {}
|
||||
uint8_t outcomes[6] = {};
|
||||
somfy_tx_event_t evt;
|
||||
while(s.popEvent(evt)) {
|
||||
if(evt.type == tx_event_t::started) continue;
|
||||
for(uint8_t i = 0; i < 6; i++)
|
||||
if(tickets[i] == evt.seq) outcomes[i]++;
|
||||
}
|
||||
for(uint8_t i = 0; i < 6; i++) TEST_ASSERT_EQUAL_UINT8(1, outcomes[i]);
|
||||
}
|
||||
static void test_unknown_ticket_is_ignored() {
|
||||
RadioScheduler s;
|
||||
TEST_ASSERT_FALSE(s.keepSending(0));
|
||||
TEST_ASSERT_FALSE(s.keepSending(42));
|
||||
TEST_ASSERT_FALSE(s.start(42, 0));
|
||||
s.ended(42, true, 0);
|
||||
assertNoEvents(s);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Suspend
|
||||
********************************************************************/
|
||||
static void test_suspend_flushes_and_refuses_jobs() {
|
||||
RadioScheduler s;
|
||||
const uint32_t a = submit(s, REMOTE_A);
|
||||
s.suspend(TX_SUSPEND_SCAN, 0);
|
||||
TEST_ASSERT_TRUE(s.suspended());
|
||||
assertEvent(s, tx_event_t::dropped, a);
|
||||
TEST_ASSERT_FALSE(s.hasRoom(true));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, submit(s, REMOTE_A));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, submit(s, REMOTE_A, 0, true));
|
||||
assertNoEvents(s);
|
||||
}
|
||||
static void test_resume_needs_every_reason_cleared() {
|
||||
RadioScheduler s;
|
||||
s.suspend(TX_SUSPEND_SCAN, 0);
|
||||
s.suspend(TX_SUSPEND_OTA, 0);
|
||||
s.resume(TX_SUSPEND_SCAN);
|
||||
TEST_ASSERT_TRUE(s.suspended());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, submit(s, REMOTE_A));
|
||||
s.resume(TX_SUSPEND_OTA);
|
||||
TEST_ASSERT_FALSE(s.suspended());
|
||||
TEST_ASSERT_NOT_EQUAL(0, submit(s, REMOTE_A));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Repeater frames
|
||||
********************************************************************/
|
||||
static void test_repeater_frames_report_nothing() {
|
||||
RadioScheduler s;
|
||||
somfy_tx_job_t job = makeJob(REMOTE_A);
|
||||
job.raw = true;
|
||||
const uint32_t r = s.submit(job, false, 0, 0);
|
||||
TEST_ASSERT_NOT_EQUAL(0, r);
|
||||
TEST_ASSERT_EQUAL_UINT32(r, runNext(s));
|
||||
submit(s, REMOTE_B);
|
||||
job = makeJob(REMOTE_A);
|
||||
job.raw = true;
|
||||
s.submit(job, false, 0, 0);
|
||||
s.flush(0);
|
||||
somfy_tx_event_t evt;
|
||||
TEST_ASSERT_TRUE(s.popEvent(evt));
|
||||
TEST_ASSERT_EQUAL_UINT32(REMOTE_B, evt.remoteAddress);
|
||||
assertNoEvents(s);
|
||||
}
|
||||
static void test_repeater_frame_doesnt_hold_back_a_stop() {
|
||||
// It isn't one of our remotes so it has no rolling code of ours to keep in
|
||||
// order.
|
||||
RadioScheduler s;
|
||||
somfy_tx_job_t job = makeJob(REMOTE_A);
|
||||
job.raw = true;
|
||||
const uint32_t r = s.submit(job, false, 0, 0);
|
||||
const uint32_t stop = submit(s, REMOTE_A, 0, true);
|
||||
TEST_ASSERT_EQUAL_UINT32(stop, runNext(s));
|
||||
TEST_ASSERT_EQUAL_UINT32(r, runNext(s));
|
||||
discardEvents(s);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_jobs_go_out_in_the_order_they_came);
|
||||
RUN_TEST(test_tickets_are_distinct_and_set_in_the_job);
|
||||
RUN_TEST(test_nothing_waiting_sleeps_until_woken);
|
||||
RUN_TEST(test_stop_goes_ahead_of_other_remotes);
|
||||
RUN_TEST(test_stop_queues_behind_its_own_remote);
|
||||
RUN_TEST(test_stop_goes_ahead_once_its_remote_is_on_air);
|
||||
RUN_TEST(test_stops_go_out_in_the_order_they_came);
|
||||
RUN_TEST(test_second_stop_of_a_remote_stays_behind_the_first);
|
||||
RUN_TEST(test_normal_jobs_leave_the_reserved_slots);
|
||||
RUN_TEST(test_stops_use_the_reserved_slots);
|
||||
RUN_TEST(test_a_refused_job_has_no_ticket);
|
||||
RUN_TEST(test_taking_a_job_frees_its_slot);
|
||||
RUN_TEST(test_delayed_job_waits_until_due);
|
||||
RUN_TEST(test_delayed_job_holds_up_the_jobs_behind_it);
|
||||
RUN_TEST(test_stop_overtakes_a_delayed_job_of_another_remote);
|
||||
RUN_TEST(test_stop_waits_behind_its_remotes_delayed_job);
|
||||
RUN_TEST(test_delay_across_millis_wrap);
|
||||
RUN_TEST(test_job_reports_started_then_finished);
|
||||
RUN_TEST(test_cut_short_job_reports_aborted);
|
||||
RUN_TEST(test_flush_drops_every_waiting_job_once);
|
||||
RUN_TEST(test_flush_cuts_short_the_job_on_air);
|
||||
RUN_TEST(test_job_flushed_before_it_starts_is_dropped);
|
||||
RUN_TEST(test_every_job_gets_one_outcome_across_a_flush);
|
||||
RUN_TEST(test_unknown_ticket_is_ignored);
|
||||
RUN_TEST(test_suspend_flushes_and_refuses_jobs);
|
||||
RUN_TEST(test_resume_needs_every_reason_cleared);
|
||||
RUN_TEST(test_repeater_frames_report_nothing);
|
||||
RUN_TEST(test_repeater_frame_doesnt_hold_back_a_stop);
|
||||
return UNITY_END();
|
||||
}
|
||||
117
test/test_txjob/test_txjob.cpp
Normal file
117
test/test_txjob/test_txjob.cpp
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
#include <unity.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() {}
|
||||
|
||||
/*********************************************************************
|
||||
* 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_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();
|
||||
}
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
#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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue