ESPSomfy-RTS/test/test_shadebehaviour/test_shadebehaviour.cpp
cjkas 7220f38fcd fix: wait out a remote's long press across the millis() wrap
The 500 ms wait that tells a short press from a long one compared
now > await, so a press just before the wrap acted at once. It now
compares the wrap-safe difference. Reconcile Q21 in
SHADE_BEHAVIOUR_PLAN.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:41:48 +02:00

667 lines
23 KiB
C++

#include <unity.h>
#include <vector>
#include "ShadeBehaviour.h"
// Scenario tests pinning what ShadeBehaviour does today, moved unchanged from
// SomfyShade. Where the copies of a rule disagree the test says "today:" so
// a later reconcile shows up as a test change (SHADE_BEHAVIOUR_PLAN.md).
#define ASSERT_POS(expected, actual) TEST_ASSERT_FLOAT_WITHIN(0.5f, (expected), (actual))
#define ASSERT_CMD(expected, actual) TEST_ASSERT_EQUAL_HEX8(static_cast<uint8_t>(expected), static_cast<uint8_t>(actual))
static const uint32_t ADDRESS = 0x123456;
static const uint32_t REMOTE = 0x654321;
// One command the shade sent through its MotorLink.
struct Sent {
somfy_commands cmd; // As it went on air, after flipCommands.
uint8_t repeat;
uint8_t stepSize;
uint16_t delayMs;
bool priority;
uint32_t ticket;
};
// Builds the frame the way SomfyRemote::sendCommandAfter does and hands out
// tickets 1, 2, 3, ... gpio sends without a ticket, refuseNext makes the
// radio refuse the next command.
struct FakeLink : MotorLink {
somfy_frame_t &lastFrame;
const RemoteSettings &remote;
std::vector<Sent> sent;
uint16_t rollingCode = 100;
uint32_t nextTicket = 1;
bool refuseNext = false;
bool gpio = false;
FakeLink(somfy_frame_t &lastFrame, const RemoteSettings &remote) : lastFrame(lastFrame), remote(remote) {}
send_result_t send(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority) override {
send_result_t result;
if(this->refuseNext) {
this->refuseNext = false;
return result;
}
somfy_frame_t frame = this->lastFrame;
frame.rollingCode = this->rollingCode + 1;
frame.remoteAddress = ADDRESS;
frame.cmd = this->remote.flipCommands ? flipCommand(cmd) : cmd;
frame.repeats = repeat;
frame.bitLength = this->remote.bitLength;
frame.stepSize = stepSize;
frame.valid = true;
frame.encKey = 0xA0 | static_cast<uint8_t>(frame.rollingCode & 0x000F);
frame.processed = false;
if(!this->gpio) {
somfy_tx_job_t job;
encodeTxJob(job, frame, repeat);
result.ticket = this->nextTicket++;
}
result.sent = true;
this->rollingCode = frame.rollingCode;
this->lastFrame = frame;
this->sent.push_back({frame.cmd, repeat, stepSize, delayMs, priority, result.ticket});
return result;
}
};
struct Command {
somfy_commands cmd;
std::string source;
};
struct FakeEvents : ShadeEvents {
uint16_t changed = 0;
std::vector<Command> commands;
int saves = 0;
int sensorFlags = 0;
void onChanged(uint16_t bits) override { this->changed |= bits; }
void onCommand(somfy_commands cmd, const char *source, uint32_t) override { this->commands.push_back({cmd, source}); }
void onSaveWanted() override { this->saves++; }
void onSensorFlags() override { this->sensorFlags++; }
};
// A shade with its settings, link and events, and a clock.
struct Rig {
ShadeSettings settings;
RemoteSettings remote;
somfy_frame_t lastFrame;
FakeLink link{lastFrame, remote};
FakeEvents events;
uint8_t id = 1;
char name[21] = "Test";
ShadeBehaviour b{settings, remote, lastFrame, link, events, id, name};
uint32_t now = 1000;
uint16_t remoteCode = 500;
Rig() {
this->remote.bitLength = 56;
this->lastFrame.remoteAddress = ADDRESS;
}
const ShadeBehaviour::State &st() const { return this->b.state(); }
// loop() passes for ms, 10 ms apart.
void run(uint32_t ms) {
for(uint32_t t = 0; t < ms; t += 10) {
this->now += 10;
this->b.tick(this->now);
}
}
void event(tx_event_t type, uint32_t ticket) {
somfy_tx_event_t evt;
evt.type = type;
evt.ticket = ticket;
evt.at = this->now;
this->b.onTxEvent(evt);
}
// The radio starts the last job sent.
void onAir() { this->event(tx_event_t::started, this->lastSent().ticket); }
const Sent &lastSent() const { return this->link.sent.back(); }
// A remote press: the first frame, then repeats more of the same frame.
void press(somfy_commands cmd, uint8_t repeats = 0, uint32_t address = ADDRESS, uint8_t stepSize = 0) {
somfy_frame_t frame;
frame.valid = true;
frame.cmd = cmd;
frame.stepSize = stepSize;
frame.remoteAddress = address;
frame.rollingCode = ++this->remoteCode;
this->b.onFrame(frame, false, this->now);
for(uint8_t i = 0; i < repeats; i++) {
this->now += 30;
this->b.onFrame(frame, false, this->now);
}
}
// Receive passes with no frame, as loop() calls onWaitingFrame.
void wait(uint32_t ms) {
for(uint32_t t = 0; t < ms; t += 10) {
this->now += 10;
this->b.onWaitingFrame(this->now);
this->b.tick(this->now);
}
}
void sensorFrame(bool sunny, bool windy) {
somfy_frame_t frame;
frame.valid = true;
frame.cmd = somfy_commands::Sensor;
frame.remoteAddress = ADDRESS;
// The sensor's bits ride in the rolling code, shifted down by four.
frame.rollingCode = (sunny ? 0x02 : 0) | (windy ? 0x01 : 0);
this->b.onFrame(frame, false, this->now);
}
};
void setUp() {}
void tearDown() {}
/*********************************************************************
* Commands from the web, MQTT and Home Assistant
********************************************************************/
static void test_down_waits_for_the_radio_then_travels() {
Rig r;
TEST_ASSERT_TRUE(r.b.sendCommand(somfy_commands::Down, 1, 0, r.now));
ASSERT_CMD(somfy_commands::Down, r.lastSent().cmd);
ASSERT_POS(100, r.st().target);
r.run(2000);
// The motor hasn't started until the command is on air.
ASSERT_POS(0, r.st().currentPos);
TEST_ASSERT_EQUAL_INT8(1, r.st().direction);
r.onAir();
r.run(5000);
ASSERT_POS(50, r.st().currentPos);
r.run(5100);
ASSERT_POS(100, r.st().currentPos);
TEST_ASSERT_EQUAL_INT8(0, r.st().direction);
TEST_ASSERT_TRUE(r.events.saves > 0);
TEST_ASSERT_TRUE(r.events.changed & SHADE_CHANGED_POSITION);
// A run to the end limit sends no stop.
TEST_ASSERT_EQUAL(1, r.link.sent.size());
}
static void test_refused_command_moves_nothing() {
Rig r;
r.link.refuseNext = true;
TEST_ASSERT_FALSE(r.b.sendCommand(somfy_commands::Down, 1, 0, r.now));
ASSERT_POS(0, r.st().target);
r.run(1000);
TEST_ASSERT_EQUAL_INT8(0, r.st().direction);
TEST_ASSERT_EQUAL(0, r.events.commands.size());
}
static void test_hold_times_out_without_a_radio_event() {
Rig r;
r.b.sendCommand(somfy_commands::Down, 1, 0, r.now);
r.run(29000);
ASSERT_POS(0, r.st().currentPos);
r.run(2000);
// After 30 s it moves anyway, timed from the end of the hold.
TEST_ASSERT_TRUE(r.st().currentPos > 0);
}
static void test_gpio_command_moves_at_once() {
Rig r;
r.link.gpio = true;
r.b.sendCommand(somfy_commands::Down, 1, 0, r.now);
r.run(5000);
ASSERT_POS(50, r.st().currentPos);
}
static void test_same_direction_again_stops_tracking() {
Rig r;
r.b.sendCommand(somfy_commands::Down, 1, 0, r.now);
r.onAir();
r.run(3000);
TEST_ASSERT_TRUE(r.b.sendCommand(somfy_commands::Down, 1, 0, r.now));
// The motor stops by itself when it gets the same direction again.
ASSERT_POS(r.st().currentPos, r.st().target);
r.run(1000);
ASSERT_POS(30, r.st().currentPos);
TEST_ASSERT_EQUAL_INT8(0, r.st().direction);
}
static void test_stop_from_the_web_works_through_the_echo() {
Rig r;
r.b.sendCommand(somfy_commands::Down, 1, 0, r.now);
r.onAir();
r.run(4000);
// sendCommand only sends Stop; hearing its own frame is what stops the shade.
r.b.sendCommand(somfy_commands::Stop, 1, 0, r.now);
ASSERT_CMD(somfy_commands::Stop, r.lastSent().cmd);
ASSERT_POS(40, r.st().target);
ASSERT_CMD(somfy_commands::Stop, r.events.commands.back().cmd);
TEST_ASSERT_EQUAL_STRING("internal", r.events.commands.back().source.c_str());
}
static void test_move_to_target_sends_a_stop_there() {
Rig r;
TEST_ASSERT_TRUE(r.b.moveToTarget(40, -1, r.now));
ASSERT_CMD(somfy_commands::Down, r.lastSent().cmd);
r.onAir();
r.run(4100);
ASSERT_POS(40, r.st().currentPos);
TEST_ASSERT_EQUAL(2, r.link.sent.size());
ASSERT_CMD(somfy_commands::My, r.lastSent().cmd);
TEST_ASSERT_TRUE(r.lastSent().priority);
r.event(tx_event_t::finished, r.lastSent().ticket);
r.run(1000);
ASSERT_POS(40, r.st().currentPos);
}
static void test_dropped_stop_runs_on_to_the_limit() {
Rig r;
r.b.moveToTarget(40, -1, r.now);
r.onAir();
r.run(4100);
r.event(tx_event_t::dropped, r.lastSent().ticket);
ASSERT_POS(100, r.st().target);
r.run(7000);
ASSERT_POS(100, r.st().currentPos);
}
static void test_refused_stop_runs_on_to_the_limit() {
Rig r;
r.b.moveToTarget(40, -1, r.now);
r.onAir();
r.run(3900);
r.link.refuseNext = true;
r.run(200);
TEST_ASSERT_EQUAL(1, r.link.sent.size());
ASSERT_POS(100, r.st().target);
}
static void test_a_step_after_move_to_target_sends_no_stop() {
// The stop at the target ends the positioning, so a later step stops by
// itself. A stop sent then would be a My to a still motor: its favorite.
Rig r;
r.b.moveToTarget(40, -1, r.now);
r.onAir();
r.run(4100);
r.event(tx_event_t::finished, r.lastSent().ticket);
const size_t before = r.link.sent.size();
r.b.sendCommand(somfy_commands::StepDown, 1, 1, r.now);
r.onAir();
r.run(2000);
TEST_ASSERT_EQUAL(before + 1, r.link.sent.size());
ASSERT_CMD(somfy_commands::StepDown, r.lastSent().cmd);
TEST_ASSERT_EQUAL_INT8(0, r.st().direction);
}
static void test_a_group_step_after_move_to_target_sends_no_stop() {
Rig r;
r.b.moveToTarget(40, -1, r.now);
r.onAir();
r.run(4100);
const size_t before = r.link.sent.size();
r.b.onGroupCommand(somfy_commands::StepDown, 1, r.now);
r.run(1000);
ASSERT_POS(41, r.st().currentPos);
TEST_ASSERT_EQUAL(before, r.link.sent.size());
}
static void test_toggle_on_a_56_bit_shade_sends_my() {
Rig r;
r.b.sendCommand(somfy_commands::Toggle, 1, 0, r.now);
ASSERT_CMD(somfy_commands::My, r.lastSent().cmd);
r.remote.bitLength = 80;
r.b.sendCommand(somfy_commands::Toggle, 1, 0, r.now);
ASSERT_CMD(somfy_commands::Toggle, r.lastSent().cmd);
}
static void test_flipped_shade_sends_the_other_way() {
Rig r;
r.remote.flipCommands = true;
r.b.sendCommand(somfy_commands::Up, 1, 0, r.now);
// On air it is Down; the shade itself still heads for the top.
ASSERT_CMD(somfy_commands::Down, r.lastSent().cmd);
ASSERT_POS(0, r.st().target);
}
/*********************************************************************
* Frames from remotes
********************************************************************/
static void test_remote_down_and_my_stop() {
Rig r;
r.press(somfy_commands::Down);
ASSERT_POS(100, r.st().target);
ASSERT_CMD(somfy_commands::Down, r.events.commands.back().cmd);
TEST_ASSERT_EQUAL_STRING("remote", r.events.commands.back().source.c_str());
// A remote's frame is already on air, so nothing is held.
r.run(3000);
ASSERT_POS(30, r.st().currentPos);
r.press(somfy_commands::My);
ASSERT_POS(30, r.st().target);
r.run(100);
TEST_ASSERT_EQUAL_INT8(0, r.st().direction);
TEST_ASSERT_EQUAL(0, r.link.sent.size());
}
static void test_remote_my_when_idle_goes_to_the_favorite_after_half_a_second() {
Rig r;
r.b.restore(0, 0, 60, -1);
r.press(somfy_commands::My);
ASSERT_POS(0, r.st().target);
r.wait(400);
ASSERT_POS(0, r.st().target);
r.wait(200);
ASSERT_POS(60, r.st().target);
}
static void test_remote_my_waits_half_a_second_across_the_millis_wrap() {
Rig r;
r.b.restore(0, 0, 60, -1);
r.now = 0xFFFFFFFF - 200;
r.press(somfy_commands::My);
// The deadline is past the wrap; it still waits for it.
r.wait(300);
ASSERT_POS(0, r.st().target);
r.wait(300);
ASSERT_POS(60, r.st().target);
}
static void test_remote_my_held_sets_the_favorite() {
Rig r;
r.b.restore(30, 0, 60, -1);
r.press(somfy_commands::My, 35);
r.wait(600);
ASSERT_POS(30, r.st().myPos);
ASSERT_POS(30, r.st().target);
TEST_ASSERT_TRUE(r.events.saves > 0);
}
static void test_remote_held_up_on_a_tilt_motor_tilts() {
Rig r;
r.settings.shadeType = shade_types::blind;
r.settings.tiltType = tilt_types::tiltmotor;
r.b.restore(50, 50, -1, -1);
r.press(somfy_commands::Up);
r.wait(600);
// A short press moves the lift.
ASSERT_POS(0, r.st().target);
ASSERT_POS(50, r.st().tiltTarget);
Rig h;
h.settings.shadeType = shade_types::blind;
h.settings.tiltType = tilt_types::tiltmotor;
h.b.restore(50, 50, -1, -1);
h.press(somfy_commands::Up, 15);
h.wait(600);
// A long one moves the tilt.
ASSERT_POS(50, h.st().target);
ASSERT_POS(0, h.st().tiltTarget);
}
static void test_remote_step_uses_the_frames_step_size() {
Rig r;
r.b.restore(50, 0, -1, -1);
// A frame without a step size is one step.
r.press(somfy_commands::StepDown);
ASSERT_POS(51, r.st().target);
// What this shade last sent doesn't matter.
r.lastFrame.stepSize = 7;
r.press(somfy_commands::StepDown, 0, ADDRESS, 3);
// Steps count from where the shade is, still 50.
ASSERT_POS(53, r.st().target);
}
static void test_web_step_uses_the_step_size_sent() {
Rig r;
r.b.restore(50, 0, -1, -1);
r.b.sendCommand(somfy_commands::StepDown, 1, 3, r.now);
TEST_ASSERT_EQUAL_UINT8(3, r.lastSent().stepSize);
ASSERT_POS(53, r.st().target);
}
static void test_wind_blocks_a_remote_down() {
Rig r;
r.settings.sunSensor = true;
r.sensorFrame(false, true);
TEST_ASSERT_EQUAL(1, r.events.sensorFlags);
TEST_ASSERT_TRUE(r.events.changed & SHADE_CHANGED_WINDY);
r.press(somfy_commands::Down);
ASSERT_POS(0, r.st().target);
}
static void test_wind_retracts_the_shade() {
Rig r;
r.b.restore(80, 0, -1, -1);
r.sensorFrame(false, true);
r.run(2100);
ASSERT_POS(0, r.st().target);
}
static void test_sun_flag_frame_turns_it_on_and_saves() {
Rig r;
r.press(somfy_commands::SunFlag);
TEST_ASSERT_TRUE(r.st().stateFlags & static_cast<uint8_t>(somfy_flags_t::SunFlag));
TEST_ASSERT_EQUAL(1, r.events.saves);
TEST_ASSERT_EQUAL(1, r.events.sensorFlags);
r.press(somfy_commands::Flag);
TEST_ASSERT_FALSE(r.st().stateFlags & static_cast<uint8_t>(somfy_flags_t::SunFlag));
}
/*********************************************************************
* Integrated tilt
********************************************************************/
static void integrated(Rig &r) {
r.settings.shadeType = shade_types::blind;
r.settings.tiltType = tilt_types::integrated;
r.settings.tiltTime = 2000;
}
static void test_integrated_tilt_turns_the_slats_first() {
Rig r;
integrated(r);
TEST_ASSERT_TRUE(r.b.moveToTarget(40, 100, r.now));
r.onAir();
r.run(1500);
// Still turning the slats; the lift waits.
ASSERT_POS(75, r.st().currentTiltPos);
ASSERT_POS(0, r.st().currentPos);
r.run(2500);
ASSERT_POS(100, r.st().currentTiltPos);
ASSERT_POS(20, r.st().currentPos);
r.run(2100);
ASSERT_POS(40, r.st().currentPos);
ASSERT_CMD(somfy_commands::My, r.lastSent().cmd);
// The radio drops the stop and the shade runs on to 100%.
r.event(tx_event_t::dropped, r.lastSent().ticket);
r.run(6100);
ASSERT_POS(100, r.st().currentPos);
}
static void test_today_integrated_tilt_turns_back_after_a_lift_target() {
// today: moveToTarget without a tilt leaves the tilt target where it was,
// so after the stop the shade turns its slats back with an Up.
Rig r;
integrated(r);
r.b.moveToTarget(40, -1, r.now);
r.onAir();
r.run(6100);
TEST_ASSERT_EQUAL(3, r.link.sent.size());
ASSERT_CMD(somfy_commands::My, r.link.sent[1].cmd);
ASSERT_CMD(somfy_commands::Up, r.link.sent[2].cmd);
ASSERT_POS(0, r.st().tiltTarget);
r.onAir();
r.run(2100);
ASSERT_POS(0, r.st().currentTiltPos);
ASSERT_POS(40, r.st().currentPos);
}
/*********************************************************************
* Tilt-only shades: the copies disagree
********************************************************************/
static Rig *tiltOnly(Rig &r) {
r.settings.shadeType = shade_types::blind;
r.settings.tiltType = tilt_types::tiltonly;
r.b.restore(100, 50, -1, -1);
return &r;
}
static void test_today_up_on_tilt_only_from_the_web() {
Rig r;
tiltOnly(r);
r.b.sendCommand(somfy_commands::Up, 1, 0, r.now);
ASSERT_POS(100, r.st().target);
ASSERT_POS(100, r.st().currentPos);
ASSERT_POS(0, r.st().tiltTarget);
}
static void test_today_up_on_tilt_only_from_a_remote() {
// today: a remote only sets the tilt target, where the web and groups also
// put the lift at 100.
Rig r;
tiltOnly(r);
r.b.restore(60, 50, -1, -1);
r.press(somfy_commands::Up);
ASSERT_POS(60, r.st().target);
ASSERT_POS(0, r.st().tiltTarget);
}
static void test_today_up_on_tilt_only_from_a_group() {
Rig r;
tiltOnly(r);
r.b.restore(60, 50, -1, -1);
r.b.onGroupCommand(somfy_commands::Up, 1, r.now);
ASSERT_POS(100, r.st().target);
ASSERT_POS(100, r.st().currentPos);
ASSERT_POS(0, r.st().tiltTarget);
}
static void test_today_my_while_tilt_only_moves() {
// today: a group's My puts the lift target at 100, a remote's leaves it.
Rig g;
tiltOnly(g);
g.b.restore(60, 50, -1, -1);
g.b.onGroupCommand(somfy_commands::Down, 1, g.now);
g.run(1000);
g.b.onGroupCommand(somfy_commands::My, 1, g.now);
ASSERT_POS(100, g.st().target);
Rig r;
tiltOnly(r);
r.b.restore(60, 50, -1, -1);
r.press(somfy_commands::Down);
r.run(1000);
r.press(somfy_commands::My);
ASSERT_POS(60, r.st().target);
}
/*********************************************************************
* Groups
********************************************************************/
static void test_group_down_and_step() {
Rig r;
r.b.onGroupCommand(somfy_commands::Down, 1, r.now);
ASSERT_POS(100, r.st().target);
// A group command sends nothing itself.
TEST_ASSERT_EQUAL(0, r.link.sent.size());
Rig s;
s.b.restore(50, 0, -1, -1);
s.b.onGroupCommand(somfy_commands::StepDown, 1, s.now);
ASSERT_POS(51, s.st().target);
}
static void test_group_sun_flag_needs_a_sensor() {
Rig r;
r.b.onGroupCommand(somfy_commands::SunFlag, 1, r.now);
TEST_ASSERT_FALSE(r.st().stateFlags & static_cast<uint8_t>(somfy_flags_t::SunFlag));
r.settings.sunSensor = true;
r.b.onGroupCommand(somfy_commands::SunFlag, 1, r.now);
TEST_ASSERT_TRUE(r.st().stateFlags & static_cast<uint8_t>(somfy_flags_t::SunFlag));
TEST_ASSERT_EQUAL(1, r.events.saves);
// As for a frame, every group works its flags out again.
TEST_ASSERT_EQUAL(1, r.events.sensorFlags);
r.b.onGroupCommand(somfy_commands::Flag, 1, r.now);
TEST_ASSERT_FALSE(r.st().stateFlags & static_cast<uint8_t>(somfy_flags_t::SunFlag));
TEST_ASSERT_EQUAL(2, r.events.sensorFlags);
}
/*********************************************************************
* The favorite
********************************************************************/
static void test_set_favorite_here_is_committed_when_all_on_air() {
Rig r;
r.b.restore(30, 0, -1, -1);
TEST_ASSERT_TRUE(r.b.setMyPosition(30, -1, r.now));
ASSERT_CMD(somfy_commands::My, r.lastSent().cmd);
TEST_ASSERT_EQUAL_UINT8(35, r.lastSent().repeat);
ASSERT_POS(30, r.st().myPos);
const int saves = r.events.saves;
r.event(tx_event_t::finished, r.lastSent().ticket);
TEST_ASSERT_EQUAL(saves + 1, r.events.saves);
}
static void test_set_favorite_dropped_keeps_the_old_one() {
Rig r;
r.b.restore(30, 0, 70, -1);
r.b.setMyPosition(30, -1, r.now);
r.event(tx_event_t::dropped, r.lastSent().ticket);
ASSERT_POS(70, r.st().myPos);
}
static void test_set_favorite_elsewhere_moves_there_first() {
Rig r;
TEST_ASSERT_TRUE(r.b.setMyPosition(40, -1, r.now));
ASSERT_CMD(somfy_commands::Down, r.lastSent().cmd);
r.onAir();
r.run(4100);
// The stop, then the long My 200 ms later that stores it.
ASSERT_POS(40, r.st().myPos);
ASSERT_CMD(somfy_commands::My, r.lastSent().cmd);
TEST_ASSERT_EQUAL_UINT8(35, r.lastSent().repeat);
TEST_ASSERT_EQUAL_UINT16(200, r.lastSent().delayMs);
}
static void test_sim_my_favorite_moves_to_it() {
Rig r;
r.settings.simMy = true;
r.b.restore(0, 0, 60, -1);
TEST_ASSERT_TRUE(r.b.moveToMyPosition(r.now));
ASSERT_CMD(somfy_commands::Down, r.lastSent().cmd);
ASSERT_POS(60, r.st().target);
}
/*********************************************************************
* Dry contacts and one-button devices
********************************************************************/
static void test_garage_my_toggles_open_and_shut() {
Rig r;
r.settings.shadeType = shade_types::garage1;
r.press(somfy_commands::My);
ASSERT_POS(100, r.st().target);
r.run(10100);
r.press(somfy_commands::My);
ASSERT_POS(0, r.st().target);
}
/*********************************************************************
* Time, setup and the outside writers
********************************************************************/
static void test_travel_across_the_millis_wrap() {
Rig r;
r.now = 0xFFFFFFFF - 2000;
r.press(somfy_commands::Down);
r.run(5000);
ASSERT_POS(50, r.st().currentPos);
}
static void test_restore_set_position_and_clear_tilt() {
Rig r;
r.b.restore(42.6f, 10.2f, 30, 5);
ASSERT_POS(42.6f, r.st().currentPos);
// The targets are the whole positions.
TEST_ASSERT_EQUAL_FLOAT(42.0f, r.st().target);
TEST_ASSERT_EQUAL_FLOAT(10.0f, r.st().tiltTarget);
TEST_ASSERT_EQUAL_UINT16(0, r.events.changed);
r.b.setPosition(70, -1);
ASSERT_POS(70, r.st().currentPos);
ASSERT_POS(70, r.st().target);
ASSERT_POS(10.2f, r.st().currentTiltPos);
TEST_ASSERT_TRUE(r.events.changed & SHADE_CHANGED_POSITION);
r.b.clearTilt();
TEST_ASSERT_EQUAL_FLOAT(0.0f, r.st().currentTiltPos);
TEST_ASSERT_EQUAL_FLOAT(-1.0f, r.st().myTiltPos);
}
int main(int argc, char **argv) {
UNITY_BEGIN();
RUN_TEST(test_down_waits_for_the_radio_then_travels);
RUN_TEST(test_refused_command_moves_nothing);
RUN_TEST(test_hold_times_out_without_a_radio_event);
RUN_TEST(test_gpio_command_moves_at_once);
RUN_TEST(test_same_direction_again_stops_tracking);
RUN_TEST(test_stop_from_the_web_works_through_the_echo);
RUN_TEST(test_move_to_target_sends_a_stop_there);
RUN_TEST(test_dropped_stop_runs_on_to_the_limit);
RUN_TEST(test_refused_stop_runs_on_to_the_limit);
RUN_TEST(test_a_step_after_move_to_target_sends_no_stop);
RUN_TEST(test_a_group_step_after_move_to_target_sends_no_stop);
RUN_TEST(test_toggle_on_a_56_bit_shade_sends_my);
RUN_TEST(test_flipped_shade_sends_the_other_way);
RUN_TEST(test_remote_down_and_my_stop);
RUN_TEST(test_remote_my_when_idle_goes_to_the_favorite_after_half_a_second);
RUN_TEST(test_remote_my_waits_half_a_second_across_the_millis_wrap);
RUN_TEST(test_remote_my_held_sets_the_favorite);
RUN_TEST(test_remote_held_up_on_a_tilt_motor_tilts);
RUN_TEST(test_remote_step_uses_the_frames_step_size);
RUN_TEST(test_web_step_uses_the_step_size_sent);
RUN_TEST(test_wind_blocks_a_remote_down);
RUN_TEST(test_wind_retracts_the_shade);
RUN_TEST(test_sun_flag_frame_turns_it_on_and_saves);
RUN_TEST(test_integrated_tilt_turns_the_slats_first);
RUN_TEST(test_today_integrated_tilt_turns_back_after_a_lift_target);
RUN_TEST(test_today_up_on_tilt_only_from_the_web);
RUN_TEST(test_today_up_on_tilt_only_from_a_remote);
RUN_TEST(test_today_up_on_tilt_only_from_a_group);
RUN_TEST(test_today_my_while_tilt_only_moves);
RUN_TEST(test_group_down_and_step);
RUN_TEST(test_group_sun_flag_needs_a_sensor);
RUN_TEST(test_set_favorite_here_is_committed_when_all_on_air);
RUN_TEST(test_set_favorite_dropped_keeps_the_old_one);
RUN_TEST(test_set_favorite_elsewhere_moves_there_first);
RUN_TEST(test_sim_my_favorite_moves_to_it);
RUN_TEST(test_garage_my_toggles_open_and_shut);
RUN_TEST(test_travel_across_the_millis_wrap);
RUN_TEST(test_restore_set_position_and_clear_tilt);
return UNITY_END();
}