Extract Unload filament into a separate file

+ cleanup the object structure
+ add no_command as the starting "operation" to simplify the rest of the command handling at runtime
pull/19/head
D.R.racer 2021-06-08 09:27:46 +02:00
parent a1b92a4775
commit 28d6744da4
15 changed files with 360 additions and 264 deletions

View File

@ -195,7 +195,10 @@ target_sources(
src/modules/finda.cpp src/modules/finda.cpp
src/modules/leds.cpp src/modules/leds.cpp
src/modules/motion.cpp src/modules/motion.cpp
src/logic/mm_control.cpp src/logic/command_base.cpp
src/logic/no_command.cpp
src/logic/unload_filament.cpp
src/logic/unload_to_finda.cpp
) )
set_property( set_property(

View File

@ -0,0 +1,5 @@
#include "command_base.h"
namespace logic {
} // namespace logic

49
src/logic/command_base.h Normal file
View File

@ -0,0 +1,49 @@
#pragma once
#include <stdint.h>
#include "error_codes.h"
#include "progress_codes.h"
/// Base class defining common API for high-level operations/commands/state machines
///
/// Which state machines are high-level? Those which are being initiated either by a command over the serial line or from a button
/// - they report their progress to the printer
/// - they can be composed of other sub automatons
namespace logic {
/// Tasks derived from this base class are the top-level operations inhibited by the printer.
/// These tasks report their progress and only one of these tasks is allowed to run at once.
class CommandBase {
public:
inline CommandBase()
: state(ProgressCode::OK)
, error(ErrorCode::OK) {}
// Normally, a base class should (must) have a virtual destructor to enable correct deallocation of superstructures.
// However, in our case we don't want ANY destruction of these objects and moreover - adding a destructor like this
// makes the linker complain about missing operator delete(), which is really not something we want/need in our case.
// Without the destructor, the linker is "happy" ;)
// virtual ~CommandBase() = default;
/// resets the automaton
virtual void Reset() = 0;
/// steps the state machine
/// @returns true if the automaton finished its work
virtual bool Step() = 0;
/// @returns progress of operation - each automaton consists of several internal states
/// which should be reported to the user via the printer's LCD
/// E.g. Tool change: first tries to unload filament, then selects another slot and then tries to load filament
virtual ProgressCode State() const { return state; }
/// @returns status of the operation - e.g. RUNNING, OK, or an error code if the operation failed
/// Please see @ErrorCode for more details
virtual ErrorCode Error() const { return error; }
protected:
ProgressCode state;
ErrorCode error;
};
} // namespace logic

15
src/logic/error_codes.h Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#include <stdint.h>
/// A complete set of error codes which may be a result of a high-level command/operation
/// This header file shall be included in the printer's firmware as well as a reference,
/// therefore the error codes have been extracted to one place
enum class ErrorCode : int_fast8_t {
RUNNING = 0, ///< the operation is still running
OK, ///< the operation finished OK
/// Unload Filament related error codes
UNLOAD_FINDA_DIDNT_TRIGGER = -1, ///< FINDA didn't trigger while unloading filament - either there is something blocking the metal ball or a cable is broken/disconnected
UNLOAD_ERROR2 = -2,
};

View File

@ -1,194 +0,0 @@
#include "mm_control.h"
#include "../modules/motion.h"
#include "../modules/leds.h"
#include "../modules/buttons.h"
#include "../modules/finda.h"
#include "../modules/permanent_storage.h"
namespace logic {
// "small" state machines will serve as building blocks for high-level commands/operations
// - engage/disengage idler
// - rotate pulley to some direction as long as the FINDA is on/off
// - rotate some axis to some fixed direction
// - load/unload to finda
//
// motion planning
// - we need some kind of planner buffer, especially because of accelerations
// because we may need to match the ramps between moves seamlessly - just like on a printer
/// A "small" automaton example - Try to unload filament to FINDA and if it fails try to recover several times.
/// \dot
/// digraph example {
/// node [shape=record, fontname=Helvetica, fontsize=10];
/// b [ label="class B" URL="\ref B"];
/// c [ label="class C" URL="\ref C"];
/// b -> c [ arrowhead="open", style="dashed" ];
///}
///\enddot
struct UnloadToFinda {
enum {
WaitingForFINDA,
OK,
Failed
};
uint8_t state;
uint8_t maxTries;
inline UnloadToFinda(uint8_t maxTries)
: maxTries(maxTries) { Reset(); }
/// Restart the automaton
inline void Reset() {
namespace mm = modules::motion;
namespace mf = modules::finda;
// check the inital state of FINDA and plan the moves
if (mf::finda.Pressed()) {
state = OK; // FINDA is already off, we assume the fillament is not there, i.e. already unloaded
} else {
// FINDA is sensing the filament, plan moves to unload it
int unloadSteps = /*BowdenLength::get() +*/ 1100; // @@TODO
const int second_point = unloadSteps - 1300;
// mm::motion.PlanMove(mm::Pulley, -1400, 6000); // @@TODO constants
// mm::motion.PlanMove(mm::Pulley, -1800 + 1400, 2500); // @@TODO constants 1800-1400 = 400
// mm::motion.PlanMove(mm::Pulley, -second_point + 1800, 550); // @@TODO constants
state = WaitingForFINDA;
}
}
/// @returns true if the state machine finished its job, false otherwise
bool Step() {
namespace mm = modules::motion;
namespace mf = modules::finda;
switch (state) {
case WaitingForFINDA:
if (modules::finda::finda.Pressed()) {
// detected end of filament
state = OK;
} else if (/*tmc2130_read_gstat() &&*/ mm::motion.QueueEmpty()) {
// we reached the end of move queue, but the FINDA didn't switch off
// two possible causes - grinded filament of malfunctioning FINDA
if (--maxTries) {
Reset(); // try again
} else {
state = Failed;
}
}
return false;
case OK:
case Failed:
default:
return true;
}
}
};
/// A high-level command state machine
/// Handles the complex logic of unloading filament
class UnloadFilament : public TaskBase {
enum State {
EngagingIdler,
UnloadingToFinda,
DisengagingIdler,
AvoidingGrind,
Finishing,
OK,
ERR1DisengagingIdler,
ERR1WaitingForUser
};
UnloadToFinda unl;
inline UnloadFilament()
: TaskBase()
, unl(3) { Reset(); }
/// Restart the automaton
void Reset() override {
namespace mm = modules::motion;
// unloads filament from extruder - filament is above Bondtech gears
mm::motion.InitAxis(mm::Pulley);
state = EngagingIdler;
mm::motion.Idler(mm::Engage);
}
/// @returns true if the state machine finished its job, false otherwise
bool Step() override {
namespace mm = modules::motion;
switch (state) {
case EngagingIdler: // state 1 engage idler
if (mm::motion.IdlerEngaged()) { // if idler is in parked position un-park it get in contact with filament
state = UnloadingToFinda;
unl.Reset();
}
return false;
case UnloadingToFinda: // state 2 rotate pulley as long as the FINDA is on
if (unl.Step()) {
if (unl.state == UnloadToFinda::Failed) {
// couldn't unload to FINDA, report error and wait for user to resolve it
state = ERR1DisengagingIdler;
// modules::leds::leds.SetMode(active_extruder, modules::leds::red, modules::leds::blink0);
} else {
state = DisengagingIdler;
}
// in all cases disengage the idler
mm::motion.Idler(mm::Disengage);
}
return false;
case DisengagingIdler:
if (mm::motion.IdlerDisengaged()) {
state = AvoidingGrind;
// mm::motion.PlanMove(mm::Pulley, -100, 10); // @@TODO constants
}
return false;
case AvoidingGrind: // state 3 move a little bit so it is not a grinded hole in filament
if (mm::motion.QueueEmpty()) {
state = Finishing;
mm::motion.Idler(mm::Disengage);
return true;
}
return false;
case Finishing:
if (mm::motion.QueueEmpty()) {
state = OK;
mm::motion.DisableAxis(mm::Pulley);
}
return false;
case ERR1DisengagingIdler: // couldn't unload to FINDA
if (mm::motion.IdlerDisengaged()) {
state = ERR1WaitingForUser;
}
return false;
case ERR1WaitingForUser: {
// waiting for user buttons and/or a command from the printer
bool help = modules::buttons::buttons.ButtonPressed(modules::buttons::Left) /*|| command_help()*/;
bool tryAgain = modules::buttons::buttons.ButtonPressed(modules::buttons::Middle) /*|| command_tryAgain()*/;
bool userResolved = modules::buttons::buttons.ButtonPressed(modules::buttons::Right) /*|| command_userResolved()*/;
if (help) {
// try to manually unload just a tiny bit - help the filament with the pulley
//@@TODO
} else if (tryAgain) {
// try again the whole sequence
Reset();
} else if (userResolved) {
// problem resolved - the user pulled the fillament by hand
// modules::leds::leds.SetMode(active_extruder, modules::leds::red, modules::leds::off);
// modules::leds::leds.SetMode(active_extruder, modules::leds::green, modules::leds::on);
// mm::motion.PlanMove(mm::Pulley, 450, 5000); // @@TODO constants
state = AvoidingGrind;
}
return false;
}
case OK:
// isFilamentLoaded = false; // filament unloaded
return true; // successfully finished
}
}
/// @returns progress of operation
virtual uint8_t Progress() const override {
return state; // for simplicity return state, will be more elaborate later in order to report the exact state of the MMU into the printer
}
};
} // namespace logic

View File

@ -1,42 +0,0 @@
#pragma once
#include <stdint.h>
/// @@TODO @3d-gussner
/// Extract the current state machines of high-level operations (load fillament, unload fillament etc.) here
/// Design some nice non-blocking API for these operations
///
/// Which automatons are high-level? Those which are being initiated either by a command over the serial line or from a button
/// - they report their progress to the printer
/// - they can be composed of other sub automatons
namespace logic {
/// Tasks derived from this base class are the top-level operations inhibited by the printer.
/// These tasks report their progress and only one of these tasks is allowed to run at once.
class TaskBase {
public:
inline TaskBase() = default;
virtual void Reset() = 0;
virtual bool Step() = 0;
/// probably individual states of the automaton
virtual uint8_t Progress() const = 0;
/// @@TODO cleanup status codes
/// @returns 0 if the operation is still running
/// 1 if the operation finished OK
/// >=2 if the operation failed - the value is the error code
virtual int8_t Status() const = 0;
protected:
uint8_t state;
};
class Logic {
public:
inline Logic() = default;
void UnloadFilament();
};
} // namespace logic

7
src/logic/no_command.cpp Normal file
View File

@ -0,0 +1,7 @@
#include "no_command.h"
namespace logic {
NoCommand noCommand;
} // namespace logic

23
src/logic/no_command.h Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <stdint.h>
#include "command_base.h"
#include "unload_to_finda.h"
namespace logic {
/// A dummy No-command operation just to make the init of the firmware consistent (and cleaner code during processing)
class NoCommand : public CommandBase {
public:
inline NoCommand()
: CommandBase() {}
/// Restart the automaton
void Reset() override {}
/// @returns true if the state machine finished its job, false otherwise
bool Step() override { return true; }
};
extern NoCommand noCommand;
} // namespace logic

View File

@ -0,0 +1,19 @@
#pragma once
#include <stdint.h>
/// A complete set of progress codes which may be reported while running a high-level command/operation
/// This header file shall be included in the printer's firmware as well as a reference,
/// therefore the progress codes have been extracted to one place
enum class ProgressCode : uint_fast8_t {
OK = 0, ///< finished ok
/// Unload Filament related progress codes
EngagingIdler,
UnloadingToFinda,
DisengagingIdler,
AvoidingGrind,
FinishingMoves,
ERR1DisengagingIdler,
ERR1WaitingForUser
};

View File

@ -0,0 +1,95 @@
#include "unload_filament.h"
#include "../modules/buttons.h"
#include "../modules/finda.h"
#include "../modules/leds.h"
#include "../modules/motion.h"
#include "../modules/permanent_storage.h"
namespace logic {
UnloadFilament unloadFilament;
void UnloadFilament::Reset() {
namespace mm = modules::motion;
// unloads filament from extruder - filament is above Bondtech gears
mm::motion.InitAxis(mm::Pulley);
state = ProgressCode::EngagingIdler;
error = ErrorCode::OK;
mm::motion.Idler(mm::Engage);
}
bool UnloadFilament::Step() {
namespace mm = modules::motion;
switch (state) {
case ProgressCode::EngagingIdler: // state 1 engage idler
if (mm::motion.IdlerEngaged()) { // if idler is in parked position un-park it get in contact with filament
state = ProgressCode::UnloadingToFinda;
unl.Reset();
}
return false;
case ProgressCode::UnloadingToFinda: // state 2 rotate pulley as long as the FINDA is on
if (unl.Step()) {
if (unl.state == UnloadToFinda::Failed) {
// couldn't unload to FINDA, report error and wait for user to resolve it
state = ProgressCode::ERR1DisengagingIdler;
// modules::leds::leds.SetMode(active_extruder, modules::leds::red, modules::leds::blink0);
} else {
state = ProgressCode::DisengagingIdler;
}
// in all cases disengage the idler
mm::motion.Idler(mm::Disengage);
}
return false;
case ProgressCode::DisengagingIdler:
if (mm::motion.IdlerDisengaged()) {
state = ProgressCode::AvoidingGrind;
// mm::motion.PlanMove(mm::Pulley, -100, 10); // @@TODO constants
}
return false;
case ProgressCode::AvoidingGrind: // state 3 move a little bit so it is not a grinded hole in filament
if (mm::motion.QueueEmpty()) {
state = ProgressCode::FinishingMoves;
mm::motion.Idler(mm::Disengage);
return true;
}
return false;
case ProgressCode::FinishingMoves:
if (mm::motion.QueueEmpty()) {
state = ProgressCode::OK;
mm::motion.DisableAxis(mm::Pulley);
}
return false;
case ProgressCode::ERR1DisengagingIdler: // couldn't unload to FINDA
error = ErrorCode::UNLOAD_FINDA_DIDNT_TRIGGER;
if (mm::motion.IdlerDisengaged()) {
state = ProgressCode::ERR1WaitingForUser;
}
return false;
case ProgressCode::ERR1WaitingForUser: {
// waiting for user buttons and/or a command from the printer
bool help = modules::buttons::buttons.ButtonPressed(modules::buttons::Left) /*|| command_help()*/;
bool tryAgain = modules::buttons::buttons.ButtonPressed(modules::buttons::Middle) /*|| command_tryAgain()*/;
bool userResolved = modules::buttons::buttons.ButtonPressed(modules::buttons::Right) /*|| command_userResolved()*/;
if (help) {
// try to manually unload just a tiny bit - help the filament with the pulley
//@@TODO
} else if (tryAgain) {
// try again the whole sequence
Reset();
} else if (userResolved) {
// problem resolved - the user pulled the fillament by hand
// modules::leds::leds.SetMode(active_extruder, modules::leds::red, modules::leds::off);
// modules::leds::leds.SetMode(active_extruder, modules::leds::green, modules::leds::on);
// mm::motion.PlanMove(mm::Pulley, 450, 5000); // @@TODO constants
state = ProgressCode::AvoidingGrind;
}
return false;
}
case ProgressCode::OK:
// isFilamentLoaded = false; // filament unloaded
return true; // successfully finished
}
return false;
}
} // namespace logic

View File

@ -0,0 +1,28 @@
#pragma once
#include <stdint.h>
#include "command_base.h"
#include "unload_to_finda.h"
namespace logic {
/// A high-level command state machine
/// Handles the complex logic of unloading filament
class UnloadFilament : public CommandBase {
public:
inline UnloadFilament()
: CommandBase()
, unl(3) { Reset(); }
/// Restart the automaton
void Reset() override;
/// @returns true if the state machine finished its job, false otherwise
bool Step() override;
private:
UnloadToFinda unl;
};
extern UnloadFilament unloadFilament;
} // namespace logic

View File

@ -0,0 +1,52 @@
#include "unload_to_finda.h"
#include "../modules/motion.h"
#include "../modules/leds.h"
#include "../modules/buttons.h"
#include "../modules/finda.h"
#include "../modules/permanent_storage.h"
namespace logic {
void UnloadToFinda::Reset() {
namespace mm = modules::motion;
namespace mf = modules::finda;
// check the inital state of FINDA and plan the moves
if (mf::finda.Pressed()) {
state = OK; // FINDA is already off, we assume the fillament is not there, i.e. already unloaded
} else {
// FINDA is sensing the filament, plan moves to unload it
int unloadSteps = /*BowdenLength::get() +*/ 1100; // @@TODO
const int second_point = unloadSteps - 1300;
// mm::motion.PlanMove(mm::Pulley, -1400, 6000); // @@TODO constants
// mm::motion.PlanMove(mm::Pulley, -1800 + 1400, 2500); // @@TODO constants 1800-1400 = 400
// mm::motion.PlanMove(mm::Pulley, -second_point + 1800, 550); // @@TODO constants
state = WaitingForFINDA;
}
}
bool UnloadToFinda::Step() {
namespace mm = modules::motion;
namespace mf = modules::finda;
switch (state) {
case WaitingForFINDA:
if (modules::finda::finda.Pressed()) {
// detected end of filament
state = OK;
} else if (/*tmc2130_read_gstat() &&*/ mm::motion.QueueEmpty()) {
// we reached the end of move queue, but the FINDA didn't switch off
// two possible causes - grinded filament of malfunctioning FINDA
if (--maxTries) {
Reset(); // try again
} else {
state = Failed;
}
}
return false;
case OK:
case Failed:
default:
return true;
}
}
} // namespace logic

View File

@ -0,0 +1,40 @@
#pragma once
#include <stdint.h>
/// Unload to FINDA "small" state machine
/// "small" state machines will serve as building blocks for high-level commands/operations
/// - engage/disengage idler
/// - rotate pulley to some direction as long as the FINDA is on/off
/// - rotate some axis to some fixed direction
/// - load/unload to finda
namespace logic {
/// A "small" automaton example - Try to unload filament to FINDA and if it fails try to recover several times.
/// \dot
/// digraph example {
/// node [shape=record, fontname=Helvetica, fontsize=10];
/// b [ label="class B" URL="\ref B"];
/// c [ label="class C" URL="\ref C"];
/// b -> c [ arrowhead="open", style="dashed" ];
///}
///\enddot
struct UnloadToFinda {
enum {
WaitingForFINDA,
OK,
Failed
};
uint8_t state;
uint8_t maxTries;
inline UnloadToFinda(uint8_t maxTries)
: maxTries(maxTries) { Reset(); }
/// Restart the automaton
void Reset();
/// @returns true if the state machine finished its job, false otherwise
bool Step();
};
} // namespace logic

View File

@ -13,14 +13,14 @@
#include "modules/leds.h" #include "modules/leds.h"
#include "modules/protocol.h" #include "modules/protocol.h"
#include "logic/mm_control.h" #include "logic/command_base.h"
#include "logic/no_command.h"
#include "logic/unload_filament.h"
static modules::protocol::Protocol protocol; static modules::protocol::Protocol protocol;
//static modules::buttons::Buttons buttons;
//static modules::leds::LEDs leds;
// @@TODO we need a dummy noCommand to init the pointer with ... makes the rest of the code much better and safer logic::CommandBase *currentCommand = &logic::noCommand;
logic::TaskBase *currentCommand = nullptr;
/// remember the request message that started the currently running command /// remember the request message that started the currently running command
modules::protocol::RequestMsg currentCommandRq(modules::protocol::RequestMsgCodes::unknown, 0); modules::protocol::RequestMsg currentCommandRq(modules::protocol::RequestMsgCodes::unknown, 0);
@ -121,7 +121,7 @@ void SendMessage(const modules::protocol::ResponseMsg &msg) {
void PlanCommand(const modules::protocol::RequestMsg &rq) { void PlanCommand(const modules::protocol::RequestMsg &rq) {
namespace mp = modules::protocol; namespace mp = modules::protocol;
if ((currentCommand == nullptr) || (currentCommand->Status() == 1)) { if (currentCommand->Error() == ErrorCode::OK) {
// we are allowed to start a new command // we are allowed to start a new command
switch (rq.code) { switch (rq.code) {
case mp::RequestMsgCodes::Cut: case mp::RequestMsgCodes::Cut:
@ -137,7 +137,7 @@ void PlanCommand(const modules::protocol::RequestMsg &rq) {
// currentCommand = &toolCommand; // currentCommand = &toolCommand;
break; break;
case mp::RequestMsgCodes::Unload: case mp::RequestMsgCodes::Unload:
// currentCommand = &unloadCommand; currentCommand = &logic::unloadFilament;
break; break;
default: default:
// currentCommand = &noCommand; // currentCommand = &noCommand;
@ -149,26 +149,22 @@ void PlanCommand(const modules::protocol::RequestMsg &rq) {
void ReportRunningCommand() { void ReportRunningCommand() {
namespace mp = modules::protocol; namespace mp = modules::protocol;
if (!currentCommand) {
// @@TODO what to report after startup?
} else {
mp::ResponseMsgParamCodes commandStatus; mp::ResponseMsgParamCodes commandStatus;
uint8_t value = 0; uint8_t value = 0;
switch (currentCommand->Status()) { switch (currentCommand->Error()) {
case 0: case ErrorCode::RUNNING:
commandStatus = mp::ResponseMsgParamCodes::Processing; commandStatus = mp::ResponseMsgParamCodes::Processing;
value = currentCommand->Progress(); value = (uint8_t)currentCommand->State();
break; break;
case 1: case ErrorCode::OK:
commandStatus = mp::ResponseMsgParamCodes::Finished; commandStatus = mp::ResponseMsgParamCodes::Finished;
break; break;
default: default:
commandStatus = mp::ResponseMsgParamCodes::Error; commandStatus = mp::ResponseMsgParamCodes::Error;
value = currentCommand->Status() - 2; // @@TODO cleanup value = (uint8_t)currentCommand->Error();
break; break;
} }
SendMessage(mp::ResponseMsg(currentCommandRq, commandStatus, value)); SendMessage(mp::ResponseMsg(currentCommandRq, commandStatus, value));
}
} }
void ProcessRequestMsg(const modules::protocol::RequestMsg &rq) { void ProcessRequestMsg(const modules::protocol::RequestMsg &rq) {

View File

@ -83,7 +83,7 @@ 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; bool QueueEmpty() const { return false; }
/// stop whatever moves are being done /// stop whatever moves are being done
void AbortPlannedMoves() {} void AbortPlannedMoves() {}