ESPSomfy-RTS/src/ShadeMotion.h
cjkas bea2f78cb6 refactor: share the shade step maths in ShadeMotion.cpp
processFrame worked out a StepUp/StepDown target with the same formula in
eight places, each with its own divide-by-zero guard.  4dd4a98 fixed one
that guarded on downTime but divided by upTime.  stepPosition() takes the
travel time once, for both the guard and the maths, so they can't drift
apart again, and it is now covered by native tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:09:15 +02:00

94 lines
4.5 KiB
C

#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).
// Where a shade (or its tilt) is after travelling elapsedMs from startPos
// (0-100). dir > 0 moves toward 100 and dir < 0 toward 0. travelMs is the
// time a full travel takes in that direction, 0 meaning it gets there at once.
float travelPosition(float startPos, int8_t dir, uint32_t elapsedMs, int32_t travelMs);
// 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);
// Where a step command moves a shade (or its tilt) from pos, in out. dir > 0
// steps toward 100 and dir < 0 toward 0. A step is stepSize * frameStepSize
// ms of the travelMs a full travel takes in that direction. False when
// travelMs or stepSize is 0, so the step is ignored.
bool stepPosition(float pos, int8_t dir, uint32_t travelMs, uint16_t stepSize, uint8_t frameStepSize, float &out);
// 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 {
uint32_t seq = 0; // 0 when nothing is held.
uint32_t since = 0; // millis() when the hold started.
// Holds for job seq unless the shade was already moving (its motor carries on
// with the earlier command) or a job is already held. The first job held is
// the one that starts the motor. A command sent while processing another
// registers before it: for a simMy shade My/Favorite queues its Up/Down from
// moveToMyPosition() first, and that Up/Down is what starts a motor with no
// favorite of its own, so the outer My/Favorite is ignored on purpose.
void mark(uint32_t jobSeq, bool moving, uint32_t now);
// Ends the hold when jobSeq is the held job. True when it was.
bool release(uint32_t jobSeq);
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