Add unit tests for Feed to FINDA state machine

+ improve infrastructure
pull/26/head
D.R.racer 2021-06-16 13:06:50 +02:00 committed by DRracer
parent 925201d77a
commit f0a042c1b6
15 changed files with 342 additions and 26 deletions

View File

@ -39,6 +39,9 @@ bool FeedToFinda::Step() {
mm::motion.PlanMove(-600, 0, 0, 4000, 0, 0); //@@TODO constants mm::motion.PlanMove(-600, 0, 0, 4000, 0, 0); //@@TODO constants
} else if (mm::motion.QueueEmpty()) { // all moves have been finished and FINDA didn't switch on } else if (mm::motion.QueueEmpty()) { // all moves have been finished and FINDA didn't switch on
state = Failed; state = Failed;
// @@TODO - shall we disengage the idler?
ml::leds.SetMode(mg::globals.ActiveSlot(), ml::Color::green, ml::off);
ml::leds.SetMode(mg::globals.ActiveSlot(), ml::Color::red, ml::blink0);
} }
return false; return false;
case UnloadBackToPTFE: case UnloadBackToPTFE:
@ -52,6 +55,7 @@ bool FeedToFinda::Step() {
state = OK; state = OK;
ml::leds.SetMode(mg::globals.ActiveSlot(), ml::Color::green, ml::on); ml::leds.SetMode(mg::globals.ActiveSlot(), ml::Color::green, ml::on);
} }
// @@TODO FINDA must be reported as OFF again as we are pulling the filament from it - is this correct?
return false; return false;
case OK: case OK:
case Failed: case Failed:

View File

@ -26,6 +26,7 @@ struct FeedToFinda {
/// @param feedPhaseLimited /// @param feedPhaseLimited
/// * true feed phase is limited, doesn't react on button press /// * true feed phase is limited, doesn't react on button press
/// * false feed phase is unlimited, can be interrupted by any button press after blanking time /// * false feed phase is unlimited, can be interrupted by any button press after blanking time
/// Beware: the function returns immediately without actually doing anything if the FINDA is "pressed", i.e. the filament is already at the FINDA
void Reset(bool feedPhaseLimited); void Reset(bool feedPhaseLimited);
/// @returns true if the state machine finished its job, false otherwise /// @returns true if the state machine finished its job, false otherwise

View File

@ -6,6 +6,14 @@ namespace motion {
Motion motion; Motion motion;
void Motion::InitAxis(Axis axis) {}
void Motion::DisableAxis(Axis axis) {}
bool Motion::StallGuard(Axis axis) { return false; }
void Motion::ClearStallGuardFlag(Axis axis) {}
void Motion::PlanMove(uint16_t pulley, uint16_t idler, uint16_t selector, uint16_t feedrate, uint16_t starting_speed, uint16_t ending_speed) {} void Motion::PlanMove(uint16_t pulley, uint16_t idler, uint16_t selector, uint16_t feedrate, uint16_t starting_speed, uint16_t ending_speed) {}
void Motion::Home(Axis axis, bool direction) {} void Motion::Home(Axis axis, bool direction) {}
@ -14,6 +22,10 @@ void Motion::SetMode(MotorMode mode) {}
void Motion::Step() {} void Motion::Step() {}
bool Motion::QueueEmpty() const { return false; }
void Motion::AbortPlannedMoves() {}
void ISR() {} void ISR() {}
//@@TODO check the directions //@@TODO check the directions

View File

@ -63,16 +63,16 @@ class Motion {
public: public:
/// Init axis driver - @@TODO this should be probably hidden somewhere deeper ... something should manage the axes and their state /// Init axis driver - @@TODO this should be probably hidden somewhere deeper ... something should manage the axes and their state
/// especially when the TMC may get randomly reset (deinited) /// especially when the TMC may get randomly reset (deinited)
void InitAxis(Axis axis) {} void InitAxis(Axis axis);
/// Disable axis motor /// Disable axis motor
void DisableAxis(Axis axis) {} void DisableAxis(Axis axis);
/// @returns true if a stall guard event occurred recently on the axis /// @returns true if a stall guard event occurred recently on the axis
bool StallGuard(Axis axis) { return false; } bool StallGuard(Axis axis);
/// clear stall guard flag reported on an axis /// clear stall guard flag reported on an axis
void ClearStallGuardFlag(Axis axis) {} void ClearStallGuardFlag(Axis axis);
/// Enqueue move of a specific motor/axis into planner buffer /// Enqueue move of a specific motor/axis into planner buffer
/// @param pulley, idler, selector - target coords /// @param pulley, idler, selector - target coords
@ -90,10 +90,10 @@ public:
void Step(); void Step();
/// @returns true if all planned moves have been finished /// @returns true if all planned moves have been finished
bool QueueEmpty() const { return false; } bool QueueEmpty() const;
/// stop whatever moves are being done /// stop whatever moves are being done
void AbortPlannedMoves() {} void AbortPlannedMoves();
/// probably higher-level operations knowing the semantic meaning of axes /// probably higher-level operations knowing the semantic meaning of axes

View File

@ -1,6 +1,6 @@
add_subdirectory(cut_filament) add_subdirectory(cut_filament)
# add_subdirectory(feed_to_finda) add_subdirectory(feed_to_finda)
# add_subdirectory(feed_to_bondtech) # add_subdirectory(feed_to_bondtech)

View File

@ -12,13 +12,13 @@ add_executable(
../../../../src/modules/globals.cpp ../../../../src/modules/globals.cpp
../../../../src/modules/idler.cpp ../../../../src/modules/idler.cpp
../../../../src/modules/leds.cpp ../../../../src/modules/leds.cpp
../../../../src/modules/motion.cpp
../../../../src/modules/permanent_storage.cpp ../../../../src/modules/permanent_storage.cpp
../../../../src/modules/selector.cpp ../../../../src/modules/selector.cpp
../../modules/stubs/stub_adc.cpp ../../modules/stubs/stub_adc.cpp
../../modules/stubs/stub_eeprom.cpp ../../modules/stubs/stub_eeprom.cpp
../../modules/stubs/stub_shr16.cpp ../../modules/stubs/stub_shr16.cpp
../stubs/main_loop_stub.cpp ../stubs/main_loop_stub.cpp
../stubs/stub_motion.cpp
test_cut_filament.cpp test_cut_filament.cpp
) )

View File

@ -6,19 +6,71 @@
#include "../../../../src/modules/globals.h" #include "../../../../src/modules/globals.h"
#include "../../../../src/modules/idler.h" #include "../../../../src/modules/idler.h"
#include "../../../../src/modules/leds.h" #include "../../../../src/modules/leds.h"
#include "../../../../src/modules/motion.h"
#include "../../../../src/modules/permanent_storage.h" #include "../../../../src/modules/permanent_storage.h"
#include "../../../../src/modules/selector.h" #include "../../../../src/modules/selector.h"
#include "../../../../src/logic/cut_filament.h" #include "../../../../src/logic/cut_filament.h"
#include "../../modules/stubs/stub_adc.h" #include "../../modules/stubs/stub_adc.h"
#include "../stubs/main_loop_stub.h" #include "../stubs/main_loop_stub.h"
#include "../stubs/stub_motion.h"
using Catch::Matchers::Equals; using Catch::Matchers::Equals;
TEST_CASE("cut_filament::basic1", "[cut_filament]") { template <typename COND>
bool WhileCondition(COND cond, uint32_t maxLoops = 5000) {
while (cond() && --maxLoops) {
main_loop();
}
return maxLoops > 0;
}
TEST_CASE("cut_filament::cut0", "[cut_filament]") {
using namespace logic; using namespace logic;
CutFilament cf; CutFilament cf;
currentCommand = &cf;
main_loop(); main_loop();
// let's assume we have the filament NOT loaded
modules::globals::globals.SetFilamentLoaded(false);
// restart the automaton
currentCommand = &cf;
cf.Reset(0);
// it should have instructed the selector and idler to move to slot 1
// check if the idler and selector have the right command
CHECK(modules::motion::axes[modules::motion::Idler].targetPos == 0); // @@TODO constants
CHECK(modules::motion::axes[modules::motion::Selector].targetPos == 0); // @@TODO constants
// now cycle at most some number of cycles (to be determined yet) and then verify, that the idler and selector reached their target positions
REQUIRE(WhileCondition([&]() { return cf.State() == ProgressCode::SelectingFilamentSlot; }, 5000));
// idler and selector reached their target positions and the CF automaton will start feeding to FINDA as the next step
REQUIRE(cf.State() == ProgressCode::FeedingToFinda);
REQUIRE(WhileCondition([&]() { return cf.State() == ProgressCode::FeedingToFinda; }, 5000));
// filament fed into FINDA, cutting...
REQUIRE(cf.State() == ProgressCode::PreparingBlade);
REQUIRE(WhileCondition([&]() { return cf.State() == ProgressCode::EngagingIdler; }, 5000));
// the idler should be at the active slot @@TODO
REQUIRE(cf.State() == ProgressCode::PushingFilament);
REQUIRE(WhileCondition([&]() { return cf.State() == ProgressCode::PushingFilament; }, 5000));
// filament pushed - performing cut
REQUIRE(cf.State() == ProgressCode::PerformingCut);
REQUIRE(WhileCondition([&]() { return cf.State() == ProgressCode::PerformingCut; }, 5000));
// returning selector
REQUIRE(cf.State() == ProgressCode::ReturningSelector);
REQUIRE(WhileCondition([&]() { return cf.State() == ProgressCode::ReturningSelector; }, 5000));
// the next states are still @@TODO
} }
// comments:
// The tricky part of the whole state machine are the edge cases - filament not loaded, stall guards etc.
// ... all the external influence we can get on the real HW
// But the good news is we can simulate them all in the unit test and thus ensure proper handling

View File

@ -0,0 +1,29 @@
# define the test executable
add_executable(
feed_to_finda_tests
../../../../src/logic/feed_to_finda.cpp
../../../../src/modules/buttons.cpp
../../../../src/modules/debouncer.cpp
../../../../src/modules/finda.cpp
../../../../src/modules/fsensor.cpp
../../../../src/modules/globals.cpp
../../../../src/modules/idler.cpp
../../../../src/modules/leds.cpp
../../../../src/modules/permanent_storage.cpp
../../../../src/modules/selector.cpp
../../modules/stubs/stub_adc.cpp
../../modules/stubs/stub_eeprom.cpp
../../modules/stubs/stub_shr16.cpp
../stubs/main_loop_stub.cpp
../stubs/stub_motion.cpp
test_feed_to_finda.cpp
)
# define required search paths
target_include_directories(
feed_to_finda_tests PUBLIC ${CMAKE_SOURCE_DIR}/src/modules ${CMAKE_SOURCE_DIR}/src/hal
${CMAKE_SOURCE_DIR}/src/logic
)
# tell build system about the test case
add_catch_test(feed_to_finda_tests)

View File

@ -0,0 +1,134 @@
#include "catch2/catch.hpp"
#include "../../../../src/modules/buttons.h"
#include "../../../../src/modules/finda.h"
#include "../../../../src/modules/fsensor.h"
#include "../../../../src/modules/globals.h"
#include "../../../../src/modules/idler.h"
#include "../../../../src/modules/leds.h"
#include "../../../../src/modules/motion.h"
#include "../../../../src/modules/permanent_storage.h"
#include "../../../../src/modules/selector.h"
#include "../../../../src/logic/feed_to_finda.h"
#include "../../modules/stubs/stub_adc.h"
#include "../stubs/main_loop_stub.h"
#include "../stubs/stub_motion.h"
using Catch::Matchers::Equals;
template <typename COND>
bool WhileCondition(logic::FeedToFinda &ff, COND cond, uint32_t maxLoops = 5000) {
while (cond() && --maxLoops) {
main_loop();
ff.Step();
}
return maxLoops > 0;
}
TEST_CASE("feed_to_finda::feed_phase_unlimited", "[feed_to_finda]") {
using namespace logic;
FeedToFinda ff;
main_loop();
// let's assume we have the filament NOT loaded and active slot 0
modules::globals::globals.SetFilamentLoaded(false);
modules::globals::globals.SetActiveSlot(0);
// restart the automaton
ff.Reset(false);
REQUIRE(ff.State() == FeedToFinda::EngagingIdler);
// it should have instructed the selector and idler to move to slot 1
// check if the idler and selector have the right command
CHECK(modules::motion::axes[modules::motion::Idler].targetPos == 0); // @@TODO constants
CHECK(modules::motion::axes[modules::motion::Selector].targetPos == 0); // @@TODO constants
// engaging idler
REQUIRE(WhileCondition(
ff,
[&]() { return !modules::idler::idler.Engaged(); },
5000));
// idler engaged, we'll start pushing filament
REQUIRE(ff.State() == FeedToFinda::PushingFilament);
// at least at the beginning the LED should shine green (it should be blinking, but this mode has been already verified in the LED's unit test)
REQUIRE(modules::leds::leds.LedOn(modules::globals::globals.ActiveSlot(), modules::leds::Color::green));
// now let the filament be pushed into the FINDA - do 500 steps without triggering the condition
// and then let the simulated ADC channel 1 create a FINDA switch
hal::adc::TADCData switchFindaOn({ 0, 600, 700, 800, 900 });
hal::adc::ReinitADC(1, std::move(switchFindaOn), 100);
REQUIRE(WhileCondition(
ff,
[&]() { return ff.State() == FeedToFinda::PushingFilament; },
1500));
// From now on the FINDA is reported as ON
// unloading back to PTFE
REQUIRE(ff.State() == FeedToFinda::UnloadBackToPTFE);
REQUIRE(WhileCondition(
ff,
[&]() { return ff.State() == FeedToFinda::UnloadBackToPTFE; },
5000));
// disengaging idler
REQUIRE(ff.State() == FeedToFinda::DisengagingIdler);
REQUIRE(WhileCondition(
ff,
[&]() { return modules::idler::idler.Engaged(); },
5000));
// state machine finished ok, the green LED should be on
REQUIRE(ff.State() == FeedToFinda::OK);
REQUIRE(modules::leds::leds.LedOn(modules::globals::globals.ActiveSlot(), modules::leds::Color::green));
REQUIRE(ff.Step() == true); // the automaton finished its work, any consecutive calls to Step must return true
}
TEST_CASE("feed_to_finda::FINDA_failed", "[feed_to_finda]") {
using namespace logic;
FeedToFinda ff;
main_loop();
// let's assume we have the filament NOT loaded and active slot 0
modules::globals::globals.SetFilamentLoaded(false);
modules::globals::globals.SetActiveSlot(0);
// restart the automaton - we want the limited version of the feed
ff.Reset(true);
REQUIRE(ff.State() == FeedToFinda::EngagingIdler);
// it should have instructed the selector and idler to move to slot 1
// check if the idler and selector have the right command
CHECK(modules::motion::axes[modules::motion::Idler].targetPos == 0); // @@TODO constants
CHECK(modules::motion::axes[modules::motion::Selector].targetPos == 0); // @@TODO constants
// engaging idler
REQUIRE(WhileCondition(
ff,
[&]() { return !modules::idler::idler.Engaged(); },
5000));
// idler engaged, we'll start pushing filament
REQUIRE(ff.State() == FeedToFinda::PushingFilament);
// at least at the beginning the LED should shine green (it should be blinking, but this mode has been already verified in the LED's unit test)
REQUIRE(modules::leds::leds.LedOn(modules::globals::globals.ActiveSlot(), modules::leds::Color::green));
// now let the filament be pushed into the FINDA - but we make sure the FINDA doesn't trigger at all
hal::adc::TADCData switchFindaOff({ 0 });
hal::adc::ReinitADC(1, std::move(switchFindaOff), 100);
REQUIRE(!WhileCondition(
ff, // boo, this formatting is UGLY!
[&]() { return ff.State() == FeedToFinda::PushingFilament; },
5000));
// the FINDA didn't trigger, we should be in the Failed state
REQUIRE(ff.State() == FeedToFinda::Failed);
REQUIRE(ff.Step() == true); // the automaton finished its work, any consecutive calls to Step must return true
}

View File

@ -13,10 +13,11 @@ logic::CommandBase *currentCommand = nullptr;
// just like in the real FW, step all the known automata // just like in the real FW, step all the known automata
void main_loop() { void main_loop() {
modules::buttons::buttons.Step(hal::adc::ReadADC(0)); modules::buttons::buttons.Step(hal::adc::ReadADC(0));
modules::leds::leds.Step(0); modules::leds::leds.Step(1);
modules::finda::finda.Step(0); modules::finda::finda.Step(1);
modules::fsensor::fsensor.Step(0); modules::fsensor::fsensor.Step(1);
modules::idler::idler.Step(); modules::idler::idler.Step();
modules::selector::selector.Step(); modules::selector::selector.Step();
if (currentCommand)
currentCommand->Step(); currentCommand->Step();
} }

View File

@ -0,0 +1,66 @@
#include "motion.h"
#include "stub_motion.h"
namespace modules {
namespace motion {
Motion motion;
AxisSim axes[3];
void Motion::InitAxis(Axis axis) {
axes[axis].enabled = true;
}
void Motion::DisableAxis(Axis axis) {
axes[axis].enabled = false;
}
bool Motion::StallGuard(Axis axis) {
return axes[axis].stallGuard;
}
void Motion::ClearStallGuardFlag(Axis axis) {
axes[axis].stallGuard = false;
}
void Motion::PlanMove(uint16_t pulley, uint16_t idler, uint16_t selector, uint16_t feedrate, uint16_t starting_speed, uint16_t ending_speed) {
axes[Pulley].targetPos = axes[Pulley].pos + pulley;
axes[Idler].targetPos = axes[Idler].pos + pulley;
axes[Selector].targetPos = axes[Selector].pos + pulley;
// speeds and feedrates are not simulated yet
}
void Motion::Home(Axis axis, bool direction) {
axes[Pulley].homed = true;
}
void Motion::SetMode(MotorMode mode) {
}
void Motion::Step() {
for (uint8_t i = 0; i < 3; ++i) {
if (axes[i].pos != axes[i].targetPos) {
int8_t dirInc = (axes[i].pos < axes[i].targetPos) ? 1 : -1;
axes[i].pos += dirInc;
}
}
}
bool Motion::QueueEmpty() const {
for (uint8_t i = 0; i < 3; ++i) {
if (axes[i].pos != axes[i].targetPos)
return false;
}
return true;
}
void Motion::AbortPlannedMoves() {
for (uint8_t i = 0; i < 3; ++i) {
axes[i].targetPos = axes[i].pos; // leave the axis where it was at the time of abort
}
}
/// probably higher-level operations knowing the semantic meaning of axes
} // namespace motion
} // namespace modules

View File

@ -0,0 +1,17 @@
#pragma once
namespace modules {
namespace motion {
struct AxisSim {
uint32_t pos;
uint32_t targetPos;
bool enabled;
bool homed;
bool stallGuard;
};
extern AxisSim axes[3];
} // namespace motion
} // namespace modules

View File

@ -34,7 +34,7 @@ bool Step_Basic_One_Button(hal::adc::TADCData &&d, uint8_t testedButtonIndex) {
// need to oversample the data as debouncing takes 100 cycles to accept a pressed button // need to oversample the data as debouncing takes 100 cycles to accept a pressed button
constexpr uint8_t oversampleFactor = 100; constexpr uint8_t oversampleFactor = 100;
hal::adc::ReinitADC(std::move(d), oversampleFactor); hal::adc::ReinitADC(0, std::move(d), oversampleFactor);
uint8_t otherButton1 = 1, otherButton2 = 2; uint8_t otherButton1 = 1, otherButton2 = 2;
switch (testedButtonIndex) { switch (testedButtonIndex) {
@ -76,7 +76,7 @@ TEST_CASE("buttons::Step-basic-button-one-after-other", "[buttons]") {
// need to oversample the data as debouncing takes 100 cycles to accept a pressed button // need to oversample the data as debouncing takes 100 cycles to accept a pressed button
constexpr uint8_t oversampleFactor = 100; constexpr uint8_t oversampleFactor = 100;
hal::adc::ReinitADC(std::move(d), oversampleFactor); hal::adc::ReinitADC(0, std::move(d), oversampleFactor);
CHECK(Step_Basic_One_Button_Test(b, oversampleFactor, 0, 1, 2)); CHECK(Step_Basic_One_Button_Test(b, oversampleFactor, 0, 1, 2));
CHECK(Step_Basic_One_Button_Test(b, oversampleFactor, 1, 0, 2)); CHECK(Step_Basic_One_Button_Test(b, oversampleFactor, 1, 0, 2));
@ -92,7 +92,7 @@ TEST_CASE("buttons::Step-debounce-one-button", "[buttons]") {
// need to oversample the data as debouncing takes 100 cycles to accept a pressed button // need to oversample the data as debouncing takes 100 cycles to accept a pressed button
constexpr uint8_t oversampleFactor = 25; constexpr uint8_t oversampleFactor = 25;
hal::adc::ReinitADC(std::move(d), oversampleFactor); hal::adc::ReinitADC(0, std::move(d), oversampleFactor);
Buttons b; Buttons b;

View File

@ -5,27 +5,27 @@
namespace hal { namespace hal {
namespace adc { namespace adc {
static TADCData values2Return; static TADCData values2Return[2];
static TADCData::const_iterator rdptr = values2Return.cbegin(); static TADCData::const_iterator rdptr[2] = { values2Return[0].cbegin(), values2Return[1].cbegin() };
static uint8_t oversampleFactor = 1; static uint8_t oversampleFactor = 1;
static uint8_t oversample = 1; ///< current count of oversampled values returned from the ADC - will get filled with oversampleFactor once it reaches zero static uint8_t oversample = 1; ///< current count of oversampled values returned from the ADC - will get filled with oversampleFactor once it reaches zero
void ReinitADC(TADCData &&d, uint8_t ovsmpl) { void ReinitADC(uint8_t channel, TADCData &&d, uint8_t ovsmpl) {
values2Return = std::move(d); values2Return[channel] = std::move(d);
oversampleFactor = ovsmpl; oversampleFactor = ovsmpl;
oversample = ovsmpl; oversample = ovsmpl;
rdptr = values2Return.cbegin(); rdptr[channel] = values2Return[channel].cbegin();
} }
/// ADC access routines /// ADC access routines
uint16_t ReadADC(uint8_t /*adc*/) { uint16_t ReadADC(uint8_t adc) {
if (!oversample) { if (!oversample) {
++rdptr; ++rdptr[adc];
oversample = oversampleFactor; oversample = oversampleFactor;
} else { } else {
--oversample; --oversample;
} }
return rdptr != values2Return.end() ? *rdptr : 1023; return rdptr[adc] != values2Return[adc].end() ? *rdptr[adc] : values2Return[adc].back();
} }
} // namespace adc } // namespace adc

View File

@ -8,7 +8,7 @@ namespace adc {
using TADCData = std::vector<uint16_t>; using TADCData = std::vector<uint16_t>;
void ReinitADC(TADCData &&d, uint8_t ovsmpl); void ReinitADC(uint8_t channel, TADCData &&d, uint8_t ovsmpl);
} // namespace adc } // namespace adc
} // namespace hal } // namespace hal