you, in this case -- Original Poster
Here's a modification of your simulation with a state machine to step things along:
// https://wokwi.com/projects/405569918525772801
// modified from:
// https://wokwi.com/projects/405480976869836801
// https://forum.arduino.cc/t/advice-on-finite-state-machine-code-to-move-two-motors-with-two-buttons-and-3-endstops/1284857
// Defined pins - just to show the code, real pins will differ
# define rightButtonPin 2
# define leftButtonPin 3
# define rightEndstopPin 4
# define leftEndstopPin 5
# define middleEndstopPin 6
# define leftCar 0
# define rightCar 1
# define left (-1)
# define right 1
bool fork; // if the mechanical simulation is asked to do the impossible this goes true.
# include <ezButton.h>
ezButton button[5] = {2, leftButtonPin, 4, 5, 6};
enum buttonNames {RIGHT = 0, LEFT, R_STOP, M_STOP, L_STOP};
char * buttonTags[] = {"Right", "Left", "Right Limit", "Middle Limit", "Left Limit"};
bool limitLeft ;
bool limitMiddle;
bool limitRight;
void setup() {
// Serial print for debugging
Serial.begin(115200);
// Initialize pins - for now all.
pinMode(rightButtonPin, INPUT_PULLUP);
pinMode(leftButtonPin, INPUT_PULLUP);
pinMode(rightEndstopPin, INPUT_PULLUP);
pinMode(leftEndstopPin, INPUT_PULLUP);
pinMode(middleEndstopPin, INPUT_PULLUP);
// for now, they all ezButtons
for (int ii = 0; ii < 5; ii++)
button[ii].setDebounceTime(20);
setupMech();
}
// simple loop steps a cabinet if it can
void loop() {
// if the high level logic doesn't break things, this should never happen
if (fork) {
Serial.println("logic error");
for (; ;);
}
// INPUT the user pushbuttons and the limit switches
button[RIGHT].loop();
button[LEFT].loop();
limitLeft = digitalRead(6) == HIGH;
limitMiddle = digitalRead(5) == HIGH;
limitRight = digitalRead(4) == HIGH;
if (0) {
// button presses for now:
if (button[LEFT].isPressed()) {
if (!limitLeft) move(leftCar, left);
else if (!limitMiddle) move(rightCar, left);
}
if (button[RIGHT].isPressed()) {
if (!limitRight) move(rightCar, right);
else if (!limitMiddle) move(leftCar, right);
}
}
process();
updateMech();
renderMech();
return;
}
void run(int theCar, int theStep) {
static uint32_t last;
uint32_t now = millis();
if (now - last > 377) {
last = now;
move(theCar, theStep);
}
}
void process(void) {
enum STATES {IDLE, LEFT_LEFTWARDS, RIGHT_LEFTWARDS, RIGHT_RIGHTWARDS, LEFT_RIGHTWARDS};
static STATES state = IDLE;
switch (state) {
case IDLE: {
if (button[RIGHT].isPressed()) {
if (!limitRight) state = RIGHT_RIGHTWARDS; // move(rightCar, right);
else if (!limitMiddle) state = LEFT_RIGHTWARDS; // move(leftCar, right);
} else {
if (button[LEFT].isPressed()) {
if (!limitLeft) state = LEFT_LEFTWARDS;//move(leftCar, left);
else if (!limitMiddle) state = RIGHT_LEFTWARDS; //move(rightCar, left);
}
}
}
break;
case RIGHT_RIGHTWARDS: {
if (button[RIGHT].isPressed() || button[LEFT].isPressed()) {
state = IDLE;
}
else {
if (limitRight) state = LEFT_RIGHTWARDS;
else run(rightCar, right);
}
}
break;
case LEFT_RIGHTWARDS: {
if (button[RIGHT].isPressed() || button[LEFT].isPressed()) {
state = IDLE;
}
else {
if (limitMiddle) state = IDLE; // move(rightCar, right);
else run(leftCar, right);
}
}
break;
case LEFT_LEFTWARDS: {
if (button[RIGHT].isPressed() || button[LEFT].isPressed()) {
state = IDLE;
}
else {
if (limitLeft) state = RIGHT_LEFTWARDS;
else run(leftCar, left);
}
}
break;
case RIGHT_LEFTWARDS: {
if (button[RIGHT].isPressed() || button[LEFT].isPressed()) {
state = IDLE;
}
else {
if (limitMiddle) state = IDLE;
else run(rightCar, left);
}
}
break;
}
}
// mostly the simulation of the mechanism
// draw the "cars", set the limit signals
# define N_REAL 30
# define PIN_NEOPIXEL 45
# include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel track(N_REAL, PIN_NEOPIXEL, NEO_RGB + NEO_KHZ800);
int carL, carR;
const byte lLimitPin = 51;
const byte mLimitPin = 49;
const byte rLimitPin = 47;
bool isLeft, isMiddle, isRight;
void setupMech()
{
pinMode(lLimitPin, OUTPUT);
pinMode(mLimitPin, OUTPUT);
pinMode(rLimitPin, OUTPUT);
track.begin();
track.setPixelColor(1, 0xffffff);
track.show(); delay(777);
carL = 0; carR = 20;
renderCars();
track.show();
delay(777);
}
void renderCars() {
for (int ii = 0; ii < 10; ii++) {
int p = carL + ii;
if (p >= 0 && p < 30) track.setPixelColor(p, track.getPixelColor(p) | 0xff0000);
p = carR + ii;
if (p >= 0 && p < 30) track.setPixelColor(p, track.getPixelColor(p) | 0x0000ff);
}
}
// move the L/R wardrobe L or R
// 0 is left car, 1 is right. -1 is to the left, 1 is to the right
// should never try to do something impossible
// fork is a global denoting that a logic error occurred.
void move(int theCar, int theStep)
{
Serial.print(theCar == leftCar ? "left cabinet " : "right cabinet ");
Serial.println(theStep == -1 ? "left" : "right");
if (theCar == rightCar) { // right hand car
carR += theStep; if (carR > 20) fork = true;
}
else { // left hand car
carL += theStep; if (carL < 0) fork = true;
}
// various other forks here or before the move - limit inhibits.
}
void updateMech() {}
void renderMech()
{
track.clear();
renderCars();
track.show(); // doh!
//... will update sensors here
isLeft = carL <= 0;
isMiddle = carR == carL + 10;
isRight = carR >= 20;
// runProxies
digitalWrite(lLimitPin, isLeft ? HIGH : LOW);
digitalWrite(mLimitPin, isMiddle ? HIGH : LOW);
digitalWrite(rLimitPin, isRight ? HIGH : LOW);
}
/*
/*
// Defineed states from FSM
enum State {
IDLE,
RIGHTWARDS,
LEFTWARDS,
MOVE_R_RIGHTWARDS,
MOVE_L_RIGHTWARDS,
MOVE_L_LEFTWARDS,
MOVE_R_LEFTWARDS,
FINISHED_MOVEMENT,
NO_STATE
};
char *stateTags[] = {
"IDLE",
"RIGHTWARDS",
"LEFTWARDS",
"MOVE_R_RIGHTWARDS",
"MOVE_L_RIGHTWARDS",
"MOVE_L_LEFTWARDS",
"MOVE_R_LEFTWARDS",
"FINISHED_MOVEMENT",
"NO_STATE"
};
State currentState = IDLE; // Initial state for FSM
// State previousState = IDLE; // Previous state to track changes
// Function to check button and end stop states
bool isButtonPressed(int pin) {
return digitalRead(pin) == LOW;
}
bool isEndstopTriggered(int pin) {
return digitalRead(pin) == LOW;
}
*/
//// junk from development and testing
/*
if (0) {
Serial.print(limitLeft ? "LEFT_LIMIT!" : " ");
Serial.print(limitMiddle ? "MID_LIMIT!" : " ");
Serial.print(limitRight ? "RIGHT_LIMIT!" : " ");
Serial.println("");
delay(100);
}
*/
// test mech buttons and cars
void updateMech0()
{
for (int ii = 0; ii < 5; ii++)
button[ii].loop();
// curiously pin 3 is an output for some reason.
// Serial.println((char *) (button[1].getState() ? "hello" : "goodbye")); delay(100);
// three tests with the main buttons
/*
if (button[0].isPressed()) { Serial.print("r+ "); carR++; Serial.println(carR); }
if (button[1].isPressed()) { Serial.println("r- "); carR--; Serial.println(carR); }
if (button[3].isPressed()) { Serial.println("l+"); carL++;}
if (button[4].isPressed()) { Serial.println("l-"); carL--;}
*/
/*
if (button[0].isPressed()) carR++;
if (button[1].isPressed()) carR--;
if (button[3].isPressed()) carL++;
if (button[4].isPressed()) carL--;
*/
static unsigned long lastStep;
unsigned long now = millis();
if (now - lastStep < 333) return;
lastStep = now;
if (!button[0].getState()) carR++;
if (!button[1].getState()) carR--;
if (!button[3].getState()) carL++;
if (!button[4].getState()) carL--;
}
/*
// #include <CANSAME5x.h> //... we gonna fake this
unsigned char synPacket[8]; //... the connection to the synthetic packet for now
const byte xLED = 9; // heart beat
# include <ezButton.h>
# define JAWS_PIN 12
ezButton jawsControl(JAWS_PIN);
// Pin configurations
#define LEFT_PIN 10
#define RIGHT_PIN 11
# define NUM_LEDS 15 //... gonna make 5 segments of 2, later 5 segments of N
# define N_REAL 115
# define SEGMENT 3 //... length of segments
# define PIN_NEOPIXEL 8
#define CAN_BRAKE_MESSAGE_ID 0x46
#define ONBOARD_NEOPIXEL PIN_NEOPIXEL
//
// Global variables for lights and CAN communication
*/
/*
void loop0() {
updateMech();
renderMech();
return;
{
static State printedState = NO_STATE;
if (printedState != currentState) {
Serial.print("state : ");
Serial.println(stateTags[currentState]);
}
printedState = currentState;
}
switch (currentState) {
case IDLE : // Check for button press, if both buttons or no buttons are pressed, we stay in IDLE
if (isButtonPressed(rightButtonPin) && !isButtonPressed(leftButtonPin)) {
currentState = RIGHTWARDS; // Right button pressed, proceed to Rightwards movement
Serial.println("RIGHTWARDS");
} else if (isButtonPressed(leftButtonPin) && !isButtonPressed(rightButtonPin)) {
currentState = LEFTWARDS; // Left button pressed, proceed to Leftwards movement
Serial.println("LEFTWARDS");
}
break;
case RIGHTWARDS : // Determine, if corresponding endstop is triggered
if (!isEndstopTriggered(rightEndstopPin)) {
currentState = MOVE_R_RIGHTWARDS; // It is not = move right cabinet
Serial.println("MOVE_R_RIGHTWARDS");
} else {
currentState = MOVE_L_RIGHTWARDS; // It is = cabinet is parked = move left cabinet
Serial.println("MOVE_L_RIGHTWARDS");
}
break;
case LEFTWARDS : // Determine, if corresponding endstop is triggered
if (!isEndstopTriggered(leftEndstopPin)) {
currentState = MOVE_L_LEFTWARDS; // It is not = move left cabinet
Serial.println("MOVE_L_LEFTWARDS");
} else {
currentState = MOVE_R_LEFTWARDS; // It is = cabinet is parked = move right cabinet
Serial.println("MOVE_R_LEFTWARDS");
}
break;
case MOVE_R_RIGHTWARDS :
// Code to move rightwards
if (isEndstopTriggered(rightEndstopPin)) {
currentState = RIGHTWARDS;
Serial.println("RIGHTWARDS");
}
break;
case MOVE_L_RIGHTWARDS :
// Code to move rightwards
if (isEndstopTriggered(rightEndstopPin)) {
currentState = MOVE_L_RIGHTWARDS;
Serial.println("MOVE_L_RIGHTWARDS");
} else if (isEndstopTriggered(middleEndstopPin)) {
currentState = FINISHED_MOVEMENT;
Serial.println("FINISHED_MOVEMENT");
}
break;
case MOVE_L_LEFTWARDS :
// Code to move leftwards
if (isEndstopTriggered(leftEndstopPin)) {
currentState = LEFTWARDS;
Serial.println("LEFTWARDS");
}
break;
case MOVE_R_LEFTWARDS :
// Code to move leftwards
if (isEndstopTriggered(leftEndstopPin)) {
currentState = MOVE_L_LEFTWARDS;
Serial.println("MOVE_L_LEFTWARDS");
} else if (isEndstopTriggered(middleEndstopPin)) {
currentState = FINISHED_MOVEMENT;
Serial.println("FINISHED_MOVEMENT");
}
break;
case FINISHED_MOVEMENT :
currentState = IDLE;
Serial.println("IDLE");
break;
}
// Check for button release to return to IDLE
if (!isButtonPressed(rightButtonPin) && !isButtonPressed(leftButtonPin)) {
currentState = IDLE;
// Serial.println("IDLE");
}
}
*/
With your clear code, I added this function to process a FSM:
void process(void) {
enum STATES {IDLE, LEFT_LEFTWARDS, RIGHT_LEFTWARDS, RIGHT_RIGHTWARDS, LEFT_RIGHTWARDS};
static STATES state = IDLE;
switch (state) {
case IDLE: {
if (button[RIGHT].isPressed()) {
if (!limitRight) state = RIGHT_RIGHTWARDS; // move(rightCar, right);
else if (!limitMiddle) state = LEFT_RIGHTWARDS; // move(leftCar, right);
} else {
if (button[LEFT].isPressed()) {
if (!limitLeft) state = LEFT_LEFTWARDS;//move(leftCar, left);
else if (!limitMiddle) state = RIGHT_LEFTWARDS; //move(rightCar, left);
}
}
}
break;
case RIGHT_RIGHTWARDS: {
if (button[RIGHT].isPressed() || button[LEFT].isPressed()) {
state = IDLE;
}
else {
if (limitRight) state = LEFT_RIGHTWARDS;
else run(rightCar, right);
}
}
break;
case LEFT_RIGHTWARDS: {
if (button[RIGHT].isPressed() || button[LEFT].isPressed()) {
state = IDLE;
}
else {
if (limitMiddle) state = IDLE; // move(rightCar, right);
else run(leftCar, right);
}
}
break;
case LEFT_LEFTWARDS: {
if (button[RIGHT].isPressed() || button[LEFT].isPressed()) {
state = IDLE;
}
else {
if (limitLeft) state = RIGHT_LEFTWARDS;
else run(leftCar, left);
}
}
break;
case RIGHT_LEFTWARDS: {
if (button[RIGHT].isPressed() || button[LEFT].isPressed()) {
state = IDLE;
}
else {
if (limitMiddle) state = IDLE;
else run(rightCar, left);
}
}
break;
}
}
and this function to move periodically:
void run(int theCar, int theStep) {
static uint32_t last;
uint32_t now = millis();
if (now - last > 377) {
last = now;
move(theCar, theStep);
}
}
and had to move the limit switch flags to global.
It is still assumes the system is inertialess, but I think one could handle that a couple ways: four LEFT_LEFTWARDS_DECEL-like intermediate states and a mod to run() to handle modifying speed, or maybe just a mod to run() and some extra checking of speed without extra states.
Nice. I dropped it into the simulation and away they go! It does operate on transitions on the user buttons, not their state, but that's probably not a difficult adjustment.
It also runs on after having, say, moved the left car to the left. The right car will begin moving to the left.
I was going further down the press-and-hold for motion, but stop and require a new press and hold to get the other cabinet moving.
I was also planning to do the run() thing a bit differently - faster throttle period of, say 50 milliseconds and an accumulation of position using a floating point variable.
This I thought might ever be useful for more detailed physical modeling.
It wouldn't be too hard to "know", at least roughly, where the two cars are at, which might inform acceleration and deceleration, still with banging to a stop but with luck slowing down in time enough to be quiet and safe. The only reason to do would be if moving slow enough at all times is just excruciatingly slow.
I was also hoping to put the OP's latest logic into a loop() for this. It was close to OK, and I did notice some redundancy in all the testing, which I think is reflected in @DaveX's similar but simpler FSM.
In case it is not obvious, the only reason this pair of processes - control and simulation of that which is being controlled - can function together is that both are non-blocking light-weight tasks that together leave 98 percent of the processor time yet unused.
What to do with all that time I now have before she who must not be kept waiting drops me a text she's rolling? ![]()
a7
Yes, since the OP has steppers and limit switches, you could measure the distances and locations after the first switch crash to 1-step resolution. Maybe limit motion to slow speed until you've located the switches. E.g, Measure leftWardrobePosition from the left switch, rightWardrobePositionfrom the right switch, and after a activating those switches, set them as zero on their axes, and record how far a homed left wardrobe can step until it hits a homed right wardrobe. Then with those knowables known (leftMax, rightMax?), you could plan high-speed motion with acceleration to snug up to the targets with something like AccelStepper's stepperL.moveTo(0) and stepperL.moveTo(leftMax - rightPosition).
run() could easily increment or decrement a leftCarPosition and rightCarPosition and then add some of the functionality of AccelStepper's run(). You could do the accumulation in high resolution integer steps, and then do the ratio to the lower resolution LED/move() simulated hardware with something Bresenham-like. Each LED could mean a centimeter.
I wrote my own process() function for @DaveX's version.
With a few adjustments to the code, it can handle press-and-hold state controlled or press-to-go toggle mode. It also allows choosing to stop at the end of a first cabinet's move and requiring m'lady to press again to move the second cabinet.
I took out some dead-ish code and removed magic numbers from the mechanical simulation and made a few other inconsequential tweaks.
Try it here:
UA/Dave_X/alto777 Wardrobe Controller Simulation
This is the process() function:
enum STATES {IDLE, L2L, R2L, R2R, L2R, NONE};
char *stateTags[] = {
"Idle.",
"LEFT_LEFTWARDS",
"RIGHT_LEFTWARDS",
"RIGHT_RIGHTWARDS",
"LEFT_RIGHTWARDS",
};
// if true stop between cars. Put it on a DIP switch configuration?
const bool fullStop = true;
void process(void)
{
static STATES state = IDLE;
{
static STATES lastPrinted = NONE;
if (lastPrinted != state) {
Serial.print("x ");
Serial.println(stateTags[state]);
lastPrinted = state;
}
}
bool leftPress = button[LEFT].isPressed();
bool rightPress = button[RIGHT].isPressed();
bool xLeft = button[LEFT].getState() == LOW;
bool xRight = button[RIGHT].getState() == LOW;
// this line makes constant button pressing necessary. DIP switch?
// choose fullStop thoughfully also
if (!xLeft && !xRight) state = IDLE;
// this stops the car when you press the button
if (state != IDLE)
if (leftPress || rightPress) {
state = IDLE;
leftPress = false;
rightPress = false;
}
switch (state) {
case IDLE :
if (rightPress) {
if (!limitRight) state = R2R; // move(rightCar, right);
else if (!limitMiddle) state = L2R; // move(leftCar, right);
}
if (leftPress) {
if (!limitLeft) state = L2L; // move(leftCar, left);
else if (!limitMiddle) state = R2L; // move(rightCar, left);
}
break;
case R2R :
if (limitRight) state = fullStop ? IDLE : L2R;
else run(rightCar, right);
break;
case L2R :
if (limitMiddle) state = IDLE;
else run(leftCar, right);
break;
case L2L :
if (limitLeft) state = fullStop ? IDLE : R2L;
else run(leftCar, left);
break;
case R2L :
if (limitMiddle) state = IDLE;
else run(rightCar, left);
break;
}
}
The next changes would make use of all the symmetry between and among the cases, and (but) the result while very compact would be more obscrurer, harder to read, understand and modify.
At a certain point, it just makes sense to program in a simple way. The top level code should be about like process(), any of the versions that could have served there: just straight ahead literal steps for moving amongst the obvious states and running some motors.
Haha, clear enough evidently. I would return what I took as a compliment, but TBH your code was so clear I didn't even need to read it. Although I did; the run() function is a nice reach across from the controller to the controlled. First do no harm.
a7

Something just occurred to me safety-wise. In the simulation an additional fixed storage area is visible when the right side car is in center position (ie. capable of being moved to the right). If somebody is accessing the fixed storage bad things could happen if the car were to begin moving to the rightmost position. Maybe consider some kind of presence sensor(s) / lockout to prevent movement if the area is occupied.
Haha :D don't worry, this is just 3D model, which doesnt represent the real gaps (thanks god :D) IRL the gap is not there :)
But Im still thinking about some presence sensor for the insides, if there will be child inside and other child will play with the buttons.... Im not sure to be honest.
Also, it is starting to get a little over my head, because there are still some small problems hardware wise, especially with the pulleys and cables, that consumes a lot of time instead of focusing on the program itself.
It seems, that my silly little project got bunch of you really excited which I love
![]()
I also love the wokwi simulation, but I still struggle, to convert provided codes for Wokwi to work with steppers instead of the Led display ![]()
The code is kinda complex (at least from my perspective as an architect
)
Could you give me just a small hint, where could I start with adapting the code? I mean, which parts are meant to control the display specifially. I hope that that part should be then exchanged for the stepper control right?
In my code, it would be in this part:
or deeper in @alto777's move():
Those are where you'd insert code or a function call to makes a low-level stepper take a step. Since there are far more stepper steps than LEDs, you would want to decrease the interval between updates and decrease the printed output.
If you are using the higher-level accelStepper, you'd just update the targets within the case statements as you are doing the transitioning and let the main loop() do the stepperL.run() and stepperR.run() to check the targets and do steps as needed to reach the targets. For example, something like this completely untested snippet:
case RIGHT_RIGHTWARDS: {
if (button[RIGHT].isPressed() || button[LEFT].isPressed()) {
stepperR.stop();
state = IDLE;
}
else {
if (limitRight) {
state = LEFT_RIGHTWARDS;
stepperR.stop();
stepperL.moveTo(largeNumberOfSteps);
}
else {
; //run(rightCar, right);
}
}
}
break;
...
}
}
void loop(void){
...
stepperR.run();
stepperL.run();
} // end of loop()
Thats great, Im really thankful for that, I think Ive learned a lot.
I will try to implement and test it tomorrow, I hope I can figure it out
![]()
Thnaks once again, I really want to finish this so I can then show the final result here.
One thing no sketch does yet is to actually stop any motor that ran something into a limit switch.
Currently, the logic just stops calling run().
Any transition to IDLE or being in the IDLE state should call a stop() function.
run() could also watch the limit switches and get off the juice a bit faster. In some systems, the limit switches would not need to be scanned and reacted to, rather they would cut power at a lower level for reliability.
a7
the code in post #58 does.
there's more inputs that states in this case which is why that code is structured around monitoring all the inputs and determining what to do based on some flags, rather having states and tests for all possibly inputs in each state
it's a "stim"-machine
rather than a state-machine
I added some stepper motors and reworked the neopixel bar graph. There's some things I still haven't figured out about AccelStepper library.
The steppers are completely driven by knowledge of how many steps to the limits. This could be obtained by a calibration dance. The LED graph is just a depiction of the current steps count of the two motors.
Try it here, looks like I wrote over an earlier simulation, oops:
UA Wardrobe Controller Steppers
The code.
// https://wokwi.com/projects/405620646744181761
// https://forum.arduino.cc/t/advice-on-finite-state-machine-code-to-move-two-motors-with-two-buttons-and-3-endstops/1284857
// user input
# define rightButtonPin 2
# define leftButtonPin 3
// limit switches
# define leftEndstopPin 6
# define middleEndstopPin 5
# define rightEndstopPin 4
// mech simulation
# define leftCar 0
# define rightCar 1
# define endStop 0
# define middleStop 1
# define left (-1)
# define right 1
bool fork; // if the mechanical simulation is asked to do the impossible this goes true.
//// now the higher level code
# include <ezButton.h>
ezButton button[2] = {rightButtonPin, leftButtonPin};
enum buttonNames {RIGHT = 0, LEFT};
char * buttonTags[] = {"Right", "Left"};
bool limitLeft;
bool limitMiddle;
bool limitRight;
void setup()
{
// Serial print for debugging
Serial.begin(115200);
setupSteppers();
pinMode(leftEndstopPin, INPUT_PULLUP);
pinMode(middleEndstopPin, INPUT_PULLUP);
pinMode(rightEndstopPin, INPUT_PULLUP);
// just two ezButtons
for (int ii = 0; ii < 2; ii++)
button[ii].setDebounceTime(20);
setupMech();
}
// limit switches normal/wiring
# define ASSERTED HIGH
// simple loop steps a cabinet if it can
void loop()
{
// just the steppers ma'am
if (0) {
testSteppersLoop();
return;
}
// if the high level logic doesn't break things, this should never happen
if (fork) {
Serial.println("logic error");
for (; ;);
}
// INPUT the user pushbuttons and the limit switches
button[RIGHT].loop();
button[LEFT].loop();
limitLeft = digitalRead(leftEndstopPin) == ASSERTED;
limitMiddle = digitalRead(middleEndstopPin) == ASSERTED;
limitRight = digitalRead(rightEndstopPin) == ASSERTED;
process();
stepperLoop(); // run both steppers
updateMech();
renderMech();
return;
}
// Idle LEFT_LEFTWARDS RIGHT_LEFTWARDS RIGHT_RIGHTWARDS LEFT_RIGHTWARDS
enum STATES {IDLE, L2L, R2L, R2R, L2R, NONE};
char *stateTags[] = {
"Idle.",
"L2L LEFT_LEFTWARDS",
"R2L RIGHT_LEFTWARDS",
"RLR RIGHT_RIGHTWARDS",
"L2R LEFT_RIGHTWARDS",
};
// if true stop between cars:
const bool fullStop = true;
// first process to call on steppers
void process(void)
{
static STATES state = IDLE;
{
static STATES lastPrinted = NONE;
if (lastPrinted != state) {
Serial.print("x ");
Serial.println(stateTags[state]);
lastPrinted = state;
}
}
bool leftPress = button[LEFT].isPressed();
bool rightPress = button[RIGHT].isPressed();
bool xLeft = button[LEFT].getState() == LOW;
bool xRight = button[RIGHT].getState() == LOW;
// this line makes constant button pressing necessary
// choose fullStop thoughfully also
// if (!xLeft && !xRight) state = IDLE;
if (state != IDLE)
if (leftPress || rightPress) {
state = IDLE;
leftPress = false;
rightPress = false;
stopCars();
}
switch (state) {
case IDLE :
if (rightPress) {
if (!limitRight) state = R2R; // move rightCar right
else if (!limitMiddle) state = L2R; // move leftCar right
}
if (leftPress) {
if (!limitLeft) state = L2L; // move leftCar left
else if (!limitMiddle) state = R2L; // move rightCar left
}
break;
case R2R :
if (limitRight) state = fullStop ? IDLE : L2R;
else run(rightCar, endStop);
break;
case L2R :
if (limitMiddle) state = IDLE;
else run(leftCar, middleStop);
break;
case L2L :
if (limitLeft) state = fullStop ? IDLE : R2L;
else run(leftCar, endStop);
break;
case R2L :
if (limitMiddle) state = IDLE;
else run(rightCar, middleStop);
break;
}
// if (state == IDLE) stopCars();
}
// mostly the simulation of the mechanism
// draw the "cars", set the limit signals
// mechanical simulation now runs steppers
# include <AccelStepper.h>
# define stepPinLeft 25
# define directionPinLeft 27
# define stepPinRight 29
# define directionPinRight 31
AccelStepper leftStepper(AccelStepper::DRIVER, stepPinLeft, directionPinLeft);
AccelStepper rightStepper(AccelStepper::DRIVER, stepPinRight, directionPinRight);
const byte PositionPot = A0;
const byte AccelerationPot = A1;
//// mostly two steppers
void setupSteppers()
{
leftStepper.setPinsInverted(false, false, true);
leftStepper.enableOutputs();
leftStepper.setMaxSpeed(1000);
leftStepper.setSpeed(200);
leftStepper.setAcceleration(150); // whatever. from the original non-motor-controller version
rightStepper.setPinsInverted(false, false, true);
rightStepper.enableOutputs();
rightStepper.setMaxSpeed(1000);
rightStepper.setSpeed(200);
rightStepper.setAcceleration(150);
}
bool isRunning; // has been moveTo, may not be moving
void stepperLoop()
{
rightStepper.run();
leftStepper.run();
}
// 0 left car / 1 right car; 0 to the end stop / 1 to the middle stop
void run(int theCar, int theStop)
{
if (0) {
static long printed = -1;
long leftP = leftStepper.currentPosition();
if (printed != leftP) {
Serial.println(leftP);
printed = leftP;
}
}
// if (isRunning) return;
if (!theCar) { // the left car will move
if (!theStop) leftStepper.moveTo(0);
else leftStepper.moveTo(1300);
}
else { // the right car
if (!theStop) rightStepper.moveTo(0);
else rightStepper.moveTo(1300);
}
isRunning = true;
}
// did this break all hell loose? yes.
void stopCars()
{
// if (!isRunning) return;
isRunning = false;
// ??
leftStepper.moveTo(leftStepper.currentPosition());
rightStepper.moveTo(rightStepper.currentPosition());
// or ??
// leftStepper.stop();
// rightStepper.stop();
}
// neopixel linear graph
# define WIDTH 10 // Sry^3 might break easily
# define N_REAL (33)
# define PIN_NEOPIXEL 45
# include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel track(N_REAL, PIN_NEOPIXEL, NEO_RGB + NEO_KHZ800);
int carL, carR;
const byte lLimitPin = 51;
const byte mLimitPin = 49;
const byte rLimitPin = 47;
void setupMech()
{
pinMode(lLimitPin, OUTPUT);
pinMode(mLimitPin, OUTPUT);
pinMode(rLimitPin, OUTPUT);
track.begin();
track.setPixelColor(1, 0xffffff);
track.show(); delay(777);
carL = 0; carR = N_REAL - WIDTH;
renderCars();
track.show();
delay(777);
}
void renderCars() {
int carL = leftStepper.currentPosition() / 100;
int carR = N_REAL - rightStepper.currentPosition() / 100 - WIDTH;
for (int ii = 0; ii < WIDTH; ii++) {
int p = carL + ii;
if (p >= 0 && p < (N_REAL)) track.setPixelColor(p, track.getPixelColor(p) | 0xff0000);
p = carR + ii;
if (p >= 0 && p < (N_REAL)) track.setPixelColor(p, track.getPixelColor(p) | 0x0000ff);
}
}
void updateMech() {}
// new render mechanism process for steppers
void renderMech()
{
track.clear();
renderCars();
track.show(); // doh!
int leftP = leftStepper.currentPosition();
int rightP = rightStepper.currentPosition();
// width 10 of 33 @ 100/ ! yikes
bool isLeft = leftP < 5;
bool isMiddle = ((3299 - rightP) - leftP) < 2005;
bool isRight = rightP < 5;
// runProxies
digitalWrite(lLimitPin, isLeft ? HIGH : LOW);
digitalWrite(mLimitPin, isMiddle ? HIGH : LOW);
digitalWrite(rLimitPin, isRight ? HIGH : LOW);
}
// testing and idea factory
void testSteppersLoop()
{
static bool running = true;
int analog_in = analogRead(PositionPot);
if (running) {
leftStepper.setAcceleration(analogRead(AccelerationPot));
leftStepper.moveTo(analog_in);
leftStepper.run();
rightStepper.setAcceleration(analogRead(AccelerationPot));
rightStepper.moveTo(1023 - analog_in);
rightStepper.run();
}
if (digitalRead(rightButtonPin) == LOW) {
running = false;
rightStepper.moveTo(rightStepper.currentPosition());
}
rightStepper.run();
leftStepper.run();
}
Gotta jet, but it is fun to play with at this point.
a7
And with a little help from some friends, here's a version that has left and right buttons for each cabinet.
It raises some interesting ideas for finer control of the motion. For example, not implemented yet, one could have both cabinets move in some circumstances for some pushbuttons.
Here you can interrupt the motion of one cabinet and run the other cabinet within its ability. Or run the first cabinet either diredtion.
In any case, having the full simulation does allow easy testing of UI ideas.
Try it here:
The motion and limit switches are still faked; all motion relies on known dimensions and perfect stepping.
a7
Hi guys, after some time Im back on track with some updates I made.
I got some great points and advices from @alto777 so shoutout to him please.
I went back few steps and started over with testing the endstops, each motor independently without endstops, than I added endstop to the routine and finally I made the whole program with FSM routine.
Here is the latest working code.
#include <AccelStepper.h>
// Buttons
#define BTN_R 5 // Buttons - Leftwise
#define BTN_L 4 // Buttons - Rightwise
// Endstops
#define END_L A0 // Endstop - left
#define END_M A1 // Endstop - middle
#define END_R A2 // Endstop - right
#define SSR A3 // SSR to power powersupply for drivers
// Stepper motor pins for the left stepper
#define STEP_L 7 // Define step pin (pulse pin) for the left stepper motor
#define DIR_L 8 // Define direction pin for the left stepper motor
#define ENA_L 9 // Define enable pin for the left stepper motor
// Stepper motor pins for the right stepper
#define STEP_R 10 // Define step pin (pulse pin) for the right stepper motor
#define DIR_R 11 // Define direction pin for the right stepper motor
#define ENA_R 12 // Define enable pin for the right stepper motor
#define ALM 2 // alarm pin
AccelStepper stepperL(AccelStepper::DRIVER, STEP_L, DIR_L);
AccelStepper stepperR(AccelStepper::DRIVER, STEP_R, DIR_R);
// Variables - button states and endstop states
bool btnR_state = false; // actual button state
bool btnL_state = false; // actual button state
bool END_L_STATE = false; // actual endstop state
bool END_M_STATE = false; // actual endstop state
bool END_R_STATE = false; // actual endstop state
enum State { IDLE, MOVING_R2R, MOVING_R2L, MOVING_L2L, MOVING_L2R };
State currentState = IDLE;
void setup() {
Serial.begin(115200);
// Initialize the buttons
pinMode(BTN_R, INPUT_PULLUP);
pinMode(BTN_L, INPUT_PULLUP);
// Initialize the endstops
pinMode(END_L, INPUT_PULLUP);
pinMode(END_M, INPUT_PULLUP);
pinMode(END_R, INPUT_PULLUP);
// Initialize the left stepper motor
stepperL.setMaxSpeed(350); // Set maximum speed
stepperL.setAcceleration(125); // Set acceleration
stepperL.setEnablePin(ENA_L);
stepperL.setPinsInverted(false, false, true);
stepperL.disableOutputs();
// Initialize the right stepper motor
stepperR.setMaxSpeed(350);
stepperR.setAcceleration(125);
stepperR.setEnablePin(ENA_R);
stepperR.setPinsInverted(false, false, true);
stepperR.disableOutputs();
//SSR
pinMode(SSR, OUTPUT);
digitalWrite(SSR, HIGH);
void updateBtnStates() {
btnR_state = (digitalRead(BTN_R) == LOW);
btnL_state = (digitalRead(BTN_L) == LOW);
}
void updateEndStates() {
END_L_STATE = (digitalRead(END_L) == LOW);
END_M_STATE = (digitalRead(END_M) == LOW);
END_R_STATE = (digitalRead(END_R) == LOW);
}
void resetPositions() {
// Reset positions to zero when no button is pressed
stepperL.setCurrentPosition(0);
stepperR.setCurrentPosition(0);
}
void loop() {
updateBtnStates();
updateEndStates();
switch (currentState) {
case IDLE:
Serial.println("Stav: IDLE");
if (btnR_state && !END_R_STATE) {
currentState = MOVING_R2R; //Right motor rightwards
stepperR.enableOutputs();
}
else if (btnL_state && !END_L_STATE) {
currentState = MOVING_L2L; // left motor letfwards
stepperL.enableOutputs();
}
else if (btnL_state && END_L_STATE && !END_M_STATE) {
currentState = MOVING_R2L; // left motor finished, right motor leftwards
stepperR.enableOutputs();
}
else if (btnR_state && END_R_STATE && !END_M_STATE) {
currentState = MOVING_L2R; // right motor finished, left motor rightwards
stepperL.enableOutputs();
}
break;
case MOVING_R2R:
Serial.println("Stav: MOVING_R2R");
if (!END_R_STATE) {
stepperR.moveTo(stepperR.currentPosition() + 10000);
stepperR.run();
}
if (END_R_STATE || !btnR_state) {
stepperR.stop();
stepperR.disableOutputs();
if (btnR_state && END_R_STATE && !END_M_STATE) {
currentState = MOVING_L2R; // right motor finished, left motor rightwards
stepperL.enableOutputs();
} else {
currentState = IDLE;
}
}
break;
case MOVING_R2L:
Serial.println("Stav: MOVING_R2L");
if (!END_M_STATE) {
stepperR.moveTo(stepperR.currentPosition() - 10000);
stepperR.run();
}
if (END_M_STATE || !btnL_state) {
stepperR.stop();
stepperR.disableOutputs();
currentState = IDLE;
}
break;
case MOVING_L2L:
Serial.println("Stav: MOVING_L2L");
if (!END_L_STATE) {
stepperL.moveTo(stepperL.currentPosition() - 10000);
stepperL.run();
}
if (END_L_STATE || !btnL_state) {
stepperL.stop();
stepperL.disableOutputs();
if (btnL_state && END_L_STATE && !END_M_STATE) {
currentState = MOVING_R2L; // left motor finished, right motor leftwards
stepperR.enableOutputs();
} else {
currentState = IDLE;
}
}
break;
case MOVING_L2R:
Serial.println("Stav: MOVING_L2R");
if (!END_M_STATE) {
stepperL.moveTo(stepperL.currentPosition() + 10000);
stepperL.run();
}
if (END_M_STATE || !btnR_state) {
stepperL.stop();
stepperL.disableOutputs();
currentState = IDLE;
}
break;
}
if (currentState == IDLE) {
stepperL.disableOutputs();
stepperR.disableOutputs();
resetPositions();
}
}
It finally does what I want it to do, so Im really happy with all the inputs from everybody in this thread.
I also made a video with the working thing. As Im saying in the video, the cable work looks kinda messy, but I will clean it afterwards (and it is already separated with the heatsrhink tubes and I keep away the cables from cables going to the motors to reduce interference.
My next step is to add some sleep mode for the PSU, so it doesn't consume electricity when it is not moving anything and also it will reduce temperature, because it still puts out some heat. The other thing will be alarm checking and restart for the PSU, so if the stepper drivers goes into alarm trigger mode, the SSR will restart the PSU.
Thanks again and if anybody sees some points in the code, that could be improved you are more then welcome to share ideas.
Thank you.
Matej
UPDATE
I updated the code with added functionality for checking the alarm on stepper drivers and I also added sleep mode for powersupply, so after 2 minutes it will turn off the PSU for stepper to save some energy and also to make it cooler, because it puts out some heat even when it is not moving the motors.
#include <AccelStepper.h>
// Buttons
#define BTN_L 4 // Leftwise
#define BTN_R 5 // Rightwise
// Endstops
#define END_L A0 // left
#define END_M A1 // middle
#define END_R A2 // right
#define SSR A3 // SSR to power powersupply for drivers
// Stepper motor pins for the left stepper
#define STEP_L 7 // pulse/step pin left
#define DIR_L 8 // direction pin
#define ENA_L 9 // enable pin
// Stepper motor pins for the right stepper
#define STEP_R 10 // pulse/step pin right
#define DIR_R 11 // direction pin
#define ENA_R 12 // enable pin
#define ALM 2 // alarm pin (from both drivers)
// AccelStepper objects
AccelStepper stepperL(AccelStepper::DRIVER, STEP_L, DIR_L);
AccelStepper stepperR(AccelStepper::DRIVER, STEP_R, DIR_R);
// Variables - button states and endstop states
// actual button states
bool btnR_state = false;
bool btnL_state = false;
// actual endstop state
bool END_L_STATE = false;
bool END_M_STATE = false;
bool END_R_STATE = false;
// Variable for checking alarm
volatile bool alarmTriggered = false;
// Sleep mode timer
unsigned long lastStateChangeTime = 0;
const unsigned long idleTimeout = 2 * 60 * 1000; // 2 minutes (in ms)
// Timer for restarting SSR
unsigned long alarmTriggerTime = 0;
const unsigned long alarmRestartDelay = 10000; // 10 seconds (in ms)
// FSM states
enum State { IDLE, MOVING_R2R, MOVING_R2L, MOVING_L2L, MOVING_L2R, SLEEP };
State currentState = IDLE;
void setup() {
Serial.begin(115200);
// Initialize the buttons
pinMode(BTN_R, INPUT_PULLUP);
pinMode(BTN_L, INPUT_PULLUP);
// Initialize the endstops
pinMode(END_L, INPUT_PULLUP);
pinMode(END_M, INPUT_PULLUP);
pinMode(END_R, INPUT_PULLUP);
// Initialize the left stepper motor
stepperL.setMaxSpeed(450); // Set maximum speed
stepperL.setAcceleration(100); // Set acceleration
stepperL.setEnablePin(ENA_L);
stepperL.setPinsInverted(false, false, true);
stepperL.disableOutputs();
// Initialize the right stepper motor
stepperR.setMaxSpeed(450);
stepperR.setAcceleration(100);
stepperR.setEnablePin(ENA_R);
stepperR.setPinsInverted(false, false, true);
stepperR.disableOutputs();
//SSR
pinMode(SSR, OUTPUT);
digitalWrite(SSR, HIGH);
pinMode(ALM, INPUT);
// attachInterrupt(digitalPinToInterrupt(ALM), handleAlarm, RISING); // ISR routine when rising occurs
}
// Own functions ready to be called from loop
void updateBtnStates() {
btnR_state = (digitalRead(BTN_R) == LOW);
btnL_state = (digitalRead(BTN_L) == LOW);
}
void updateEndStates() {
END_L_STATE = (digitalRead(END_L) == LOW);
END_M_STATE = (digitalRead(END_M) == LOW);
END_R_STATE = (digitalRead(END_R) == LOW);
}
void resetPositions() {
// Reset positions to zero when no button is pressed/endstop is hit
stepperL.setCurrentPosition(0);
stepperR.setCurrentPosition(0);
}
void handleSSRRestart() {
Serial.println("Alarm detected! Restarting SSR...");
digitalWrite(SSR, LOW); // SSR turn off
delay(10000); // 10s delay before restart
digitalWrite(SSR, HIGH); // SSR turn on
Serial.println("SSR reactivated!");
}
void enterSleepMode() {
Serial.println("Entering sleep mode...");
digitalWrite(SSR, LOW); // Turn off SSR for sleep mode
currentState = SLEEP;
}
void exitSleepMode() {
Serial.println("Exiting sleep mode...");
digitalWrite(SSR, HIGH); // Turn on SSR after exiting sleep mode
currentState = IDLE;
}
void handleIdleState() {
Serial.println("Stav: IDLE");
if (btnR_state && !END_R_STATE) {
currentState = MOVING_R2R; //Right motor rightwards
stepperR.enableOutputs();
}
else if (btnL_state && !END_L_STATE) {
currentState = MOVING_L2L; // left motor leftwards
stepperL.enableOutputs();
}
else if (btnL_state && END_L_STATE && !END_M_STATE) {
currentState = MOVING_R2L; // left motor finished, right motor leftwards
stepperR.enableOutputs();
}
else if (btnR_state && END_R_STATE && !END_M_STATE) {
currentState = MOVING_L2R; // right motor finished, left motor rightwards
stepperL.enableOutputs();
}
}
void handleMovingR2RState() {
Serial.println("Stav: MOVING_R2R");
if (!END_R_STATE) {
stepperR.moveTo(stepperR.currentPosition() + 10000);
stepperR.run();
}
if (END_R_STATE || !btnR_state) {
stepperR.stop();
stepperR.disableOutputs();
currentState = IDLE;
}
}
void handleMovingR2LState() {
Serial.println("Stav: MOVING_R2L");
if (!END_M_STATE) {
stepperR.moveTo(stepperR.currentPosition() - 10000);
stepperR.run();
}
if (END_M_STATE || !btnL_state) {
stepperR.stop();
stepperR.disableOutputs();
currentState = IDLE;
}
}
void handleMovingL2LState() {
Serial.println("Stav: MOVING_L2L");
if (!END_L_STATE) {
stepperL.moveTo(stepperL.currentPosition() - 10000);
stepperL.run();
}
if (END_L_STATE || !btnL_state) {
stepperL.stop();
stepperL.disableOutputs();
currentState = IDLE;
}
}
void handleMovingL2RState() {
Serial.println("Stav: MOVING_L2R");
if (!END_M_STATE) {
stepperL.moveTo(stepperL.currentPosition() + 10000);
stepperL.run();
}
if (END_M_STATE || !btnR_state) {
stepperL.stop();
stepperL.disableOutputs();
currentState = IDLE;
}
}
void loop() {
updateBtnStates();
updateEndStates();
// Check alarm in loop
static bool alarmDetected = false;
static unsigned long alarmStartTime = 0;
if (digitalRead(ALM) == HIGH) {
if (!alarmDetected) {
alarmDetected = true;
alarmStartTime = millis();
} else if (millis() - alarmStartTime >= 500) {
// Check if the alarm is on for at least 500ms (software debounce, otherwise it would go into loop with restarting the SSR)
alarmTriggered = true;
}
} else {
alarmDetected = false;
}
if (alarmTriggered) {
handleSSRRestart();
alarmTriggered = false;
}
if (currentState == SLEEP) {
if (btnR_state || btnL_state) {
exitSleepMode();
}
return;
}
if (btnR_state || btnL_state) {
lastStateChangeTime = millis();
}
if (millis() - lastStateChangeTime >= idleTimeout) {
enterSleepMode();
}
switch (currentState) {
case IDLE: handleIdleState(); break;
case MOVING_R2R: handleMovingR2RState(); break;
case MOVING_R2L: handleMovingR2LState(); break;
case MOVING_L2L: handleMovingL2LState(); break;
case MOVING_L2R: handleMovingL2RState(); break;
}
if (currentState == IDLE) {
stepperL.disableOutputs();
stepperR.disableOutputs();
resetPositions();
}
}