diff --git a/src/logic/feed_to_finda.cpp b/src/logic/feed_to_finda.cpp index d7022be..165c294 100644 --- a/src/logic/feed_to_finda.cpp +++ b/src/logic/feed_to_finda.cpp @@ -39,6 +39,9 @@ bool FeedToFinda::Step() { 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 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; case UnloadBackToPTFE: @@ -52,6 +55,7 @@ bool FeedToFinda::Step() { state = OK; 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; case OK: case Failed: diff --git a/src/logic/feed_to_finda.h b/src/logic/feed_to_finda.h index 6c01cfd..4d20736 100644 --- a/src/logic/feed_to_finda.h +++ b/src/logic/feed_to_finda.h @@ -26,6 +26,7 @@ struct FeedToFinda { /// @param feedPhaseLimited /// * 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 + /// 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); /// @returns true if the state machine finished its job, false otherwise diff --git a/src/modules/motion.cpp b/src/modules/motion.cpp index 0e43c82..787eca8 100644 --- a/src/modules/motion.cpp +++ b/src/modules/motion.cpp @@ -6,6 +6,14 @@ namespace 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::Home(Axis axis, bool direction) {} @@ -14,6 +22,10 @@ void Motion::SetMode(MotorMode mode) {} void Motion::Step() {} +bool Motion::QueueEmpty() const { return false; } + +void Motion::AbortPlannedMoves() {} + void ISR() {} //@@TODO check the directions diff --git a/src/modules/motion.h b/src/modules/motion.h index cdd4a35..3da0418 100644 --- a/src/modules/motion.h +++ b/src/modules/motion.h @@ -63,16 +63,16 @@ class Motion { public: /// 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) - void InitAxis(Axis axis) {} + void InitAxis(Axis axis); /// Disable axis motor - void DisableAxis(Axis axis) {} + void DisableAxis(Axis 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 - void ClearStallGuardFlag(Axis axis) {} + void ClearStallGuardFlag(Axis axis); /// Enqueue move of a specific motor/axis into planner buffer /// @param pulley, idler, selector - target coords @@ -90,10 +90,10 @@ public: void Step(); /// @returns true if all planned moves have been finished - bool QueueEmpty() const { return false; } + bool QueueEmpty() const; /// stop whatever moves are being done - void AbortPlannedMoves() {} + void AbortPlannedMoves(); /// probably higher-level operations knowing the semantic meaning of axes diff --git a/tests/unit/logic/CMakeLists.txt b/tests/unit/logic/CMakeLists.txt index 96b1158..d08d796 100644 --- a/tests/unit/logic/CMakeLists.txt +++ b/tests/unit/logic/CMakeLists.txt @@ -1,6 +1,6 @@ add_subdirectory(cut_filament) -# add_subdirectory(feed_to_finda) +add_subdirectory(feed_to_finda) # add_subdirectory(feed_to_bondtech) diff --git a/tests/unit/logic/cut_filament/CMakeLists.txt b/tests/unit/logic/cut_filament/CMakeLists.txt index f12fb31..14d4b2f 100644 --- a/tests/unit/logic/cut_filament/CMakeLists.txt +++ b/tests/unit/logic/cut_filament/CMakeLists.txt @@ -12,13 +12,13 @@ add_executable( ../../../../src/modules/globals.cpp ../../../../src/modules/idler.cpp ../../../../src/modules/leds.cpp - ../../../../src/modules/motion.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_cut_filament.cpp ) diff --git a/tests/unit/logic/cut_filament/test_cut_filament.cpp b/tests/unit/logic/cut_filament/test_cut_filament.cpp index 3b6f101..b9c369b 100644 --- a/tests/unit/logic/cut_filament/test_cut_filament.cpp +++ b/tests/unit/logic/cut_filament/test_cut_filament.cpp @@ -6,19 +6,71 @@ #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/cut_filament.h" #include "../../modules/stubs/stub_adc.h" + #include "../stubs/main_loop_stub.h" +#include "../stubs/stub_motion.h" using Catch::Matchers::Equals; -TEST_CASE("cut_filament::basic1", "[cut_filament]") { +template +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; CutFilament cf; - currentCommand = &cf; 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 diff --git a/tests/unit/logic/feed_to_finda/CMakeLists.txt b/tests/unit/logic/feed_to_finda/CMakeLists.txt new file mode 100644 index 0000000..163c2ff --- /dev/null +++ b/tests/unit/logic/feed_to_finda/CMakeLists.txt @@ -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) diff --git a/tests/unit/logic/feed_to_finda/test_feed_to_finda.cpp b/tests/unit/logic/feed_to_finda/test_feed_to_finda.cpp new file mode 100644 index 0000000..2325b10 --- /dev/null +++ b/tests/unit/logic/feed_to_finda/test_feed_to_finda.cpp @@ -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 +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 +} diff --git a/tests/unit/logic/stubs/main_loop_stub.cpp b/tests/unit/logic/stubs/main_loop_stub.cpp index 14b2b78..d6d00b6 100644 --- a/tests/unit/logic/stubs/main_loop_stub.cpp +++ b/tests/unit/logic/stubs/main_loop_stub.cpp @@ -13,10 +13,11 @@ logic::CommandBase *currentCommand = nullptr; // just like in the real FW, step all the known automata void main_loop() { modules::buttons::buttons.Step(hal::adc::ReadADC(0)); - modules::leds::leds.Step(0); - modules::finda::finda.Step(0); - modules::fsensor::fsensor.Step(0); + modules::leds::leds.Step(1); + modules::finda::finda.Step(1); + modules::fsensor::fsensor.Step(1); modules::idler::idler.Step(); modules::selector::selector.Step(); - currentCommand->Step(); + if (currentCommand) + currentCommand->Step(); } diff --git a/tests/unit/logic/stubs/stub_motion.cpp b/tests/unit/logic/stubs/stub_motion.cpp new file mode 100644 index 0000000..7592082 --- /dev/null +++ b/tests/unit/logic/stubs/stub_motion.cpp @@ -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 diff --git a/tests/unit/logic/stubs/stub_motion.h b/tests/unit/logic/stubs/stub_motion.h new file mode 100644 index 0000000..6c94389 --- /dev/null +++ b/tests/unit/logic/stubs/stub_motion.h @@ -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 diff --git a/tests/unit/modules/buttons/test_buttons.cpp b/tests/unit/modules/buttons/test_buttons.cpp index b01e8d7..b2daa00 100644 --- a/tests/unit/modules/buttons/test_buttons.cpp +++ b/tests/unit/modules/buttons/test_buttons.cpp @@ -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 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; 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 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, 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 constexpr uint8_t oversampleFactor = 25; - hal::adc::ReinitADC(std::move(d), oversampleFactor); + hal::adc::ReinitADC(0, std::move(d), oversampleFactor); Buttons b; diff --git a/tests/unit/modules/stubs/stub_adc.cpp b/tests/unit/modules/stubs/stub_adc.cpp index 5cf7593..6002137 100644 --- a/tests/unit/modules/stubs/stub_adc.cpp +++ b/tests/unit/modules/stubs/stub_adc.cpp @@ -5,27 +5,27 @@ namespace hal { namespace adc { -static TADCData values2Return; -static TADCData::const_iterator rdptr = values2Return.cbegin(); +static TADCData values2Return[2]; +static TADCData::const_iterator rdptr[2] = { values2Return[0].cbegin(), values2Return[1].cbegin() }; 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 -void ReinitADC(TADCData &&d, uint8_t ovsmpl) { - values2Return = std::move(d); +void ReinitADC(uint8_t channel, TADCData &&d, uint8_t ovsmpl) { + values2Return[channel] = std::move(d); oversampleFactor = ovsmpl; oversample = ovsmpl; - rdptr = values2Return.cbegin(); + rdptr[channel] = values2Return[channel].cbegin(); } /// ADC access routines -uint16_t ReadADC(uint8_t /*adc*/) { +uint16_t ReadADC(uint8_t adc) { if (!oversample) { - ++rdptr; + ++rdptr[adc]; oversample = oversampleFactor; } else { --oversample; } - return rdptr != values2Return.end() ? *rdptr : 1023; + return rdptr[adc] != values2Return[adc].end() ? *rdptr[adc] : values2Return[adc].back(); } } // namespace adc diff --git a/tests/unit/modules/stubs/stub_adc.h b/tests/unit/modules/stubs/stub_adc.h index 4f76f70..2e2f360 100644 --- a/tests/unit/modules/stubs/stub_adc.h +++ b/tests/unit/modules/stubs/stub_adc.h @@ -8,7 +8,7 @@ namespace adc { using TADCData = std::vector; -void ReinitADC(TADCData &&d, uint8_t ovsmpl); +void ReinitADC(uint8_t channel, TADCData &&d, uint8_t ovsmpl); } // namespace adc } // namespace hal