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:
cjkas 2026-10-12 20:26:33 +02:00
parent 6a3769406f
commit 1962778912
15 changed files with 912 additions and 186 deletions

View file

@ -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