I’ve got the Arduino code to the stage where it’s good enough for the version 1 machine. There’s a lot I’d like to change for the next version, though mostly on the hardware side, but here seems like a good place to take and share a snapshot.

This post is just is a lightly documented version of the code for the first version of my camera slider. This is much simplified from the old complex state machine. Here’s a diagram of the simpler state machine I’m building in this code:

All the code is here: https://github.com/andrewsleigh/fab-slider/tree/master/arduino-code/v1

Libraries

I’m using these Arduino libraries to handle motor control, non-blocking timers, and button debouncing respectively:

#include <AccelStepper.h>
#include <neotimer.h>
#include <Bounce2.h>

User configuration

The variables that you might want to change are listed at the top of the file so speeds can be tweaked

int videoMinSpeed = 500;
int videoMaxSpeed = 5000;
int timelapseMinSpeed = 50;
int timelapseMaxSpeed = 250;
int endStopPadding = 100; 

Debugging setup

As documented in Better debugging with the serial monitor (or GitHub link), this section sets up the definitions that allow you to switch serial debugging on and off. You’d want to turn it off in use because sending all that data to the serial monitor interrupts the motion of the stepper.

String errorMessage = ""; // this string needs to be initialised even if we're not running debugging

// comment these out to remove debugging
#define DEBUG // needed for all debugging
// #define DEBUG_TIMER // for messages that print every few seconds
// #define DEBUG_STATE_CHANGE // for messages that print only on a change in the machine state

#ifdef DEBUG


// set up string messages
#define DEBUG_PRINT(str)    \
  Serial.print(str); \
  Serial.print(" ------ ");      \
  Serial.print("In ");    \
  Serial.print(__PRETTY_FUNCTION__); \
  Serial.print(", line: ");      \
  Serial.println(__LINE__);     \

#else
#define DEBUG_PRINT(str) // just leaves an empty definition without serial printing
#endif

Creating objects and variables

After that we just setup all the objects and variables we’ll use later:

// Accelstepper object
AccelStepper stepper(AccelStepper::DRIVER, 2, 3); // format: interface, step pin, direction pin (for shield: step:D2 and dir:D3)
// Instantiate a Bounce object for the start button
Bounce debouncer = Bounce(); 


// define input/button pin numbers
const int startButtonPin = 10;
const int leftEndStopPin = 9;
const int rightEndStopPin = 8;
const int startFromLeftPin = 11;
const int isTimelapseSpeedPin = 12;

// define output pin numbers
const int motorOffPin = 13;
const int ledPin = 5;

// set up LED and debug timers
Neotimer warningBlinkTimer = Neotimer(); 
Neotimer readyBlinkTimer = Neotimer(); 
Neotimer statusUpdateTimer = Neotimer(); 

// define analogue input for speed knob
const int speedPotPin = A0;
int speedInput = 0; // a variable to stare it in
int traverseSpeed = 0; // variable to hold speed when calculated

// variables to determine kind of traverse
bool startFromLeft = true;
bool isTimelapseSpeed = false;

Setup state machine

As part of this, there is a section for the state machine itself:

// WARNING: values, order of items and total number of items must match the array below
enum possibleStates {
  INIT,
  TRAVERSING,
  ABORTED
};

// create an array of labels from the enum for debugging
// WARNING: this array must be updated if you change the enum
String stateLabels[3] = {
  "INIT",
  "TRAVERSING",
  "ABORTED"
};

// create a variable of this type
enum possibleStates currentState;

// keep track of whether the state has changed to limit status message updates
bool isNewState;

Setup()

In setup() we:

  • Set values for the motor
  • Set values for the timers
  • Open serial comms, if debugging is enabled
  • Set up the switches with internal pullup resistors (so they’re pulled low to activate)
  • Set up output pins for the motor enable function and the status LED
  • Use the debounce library to set up our one push-button
  • Finally, set the starting state, ready for the loop
void setup() {

  stepper.setMaxSpeed(4000); // See https://forum.arduino.cc/index.php?topic=487134.0
  stepper.setAcceleration(2000.0);

  // set up blink and debug message timers
  warningBlinkTimer.set(100);
  readyBlinkTimer.set(1000);
  statusUpdateTimer.set(2000);

#ifdef DEBUG
  Serial.begin(9600); // serial comms for debugging
#endif

  // initialize the button pins as inputs:
  pinMode(leftEndStopPin, INPUT_PULLUP);
  pinMode(rightEndStopPin, INPUT_PULLUP);
  pinMode(startFromLeftPin, INPUT_PULLUP);
  pinMode(isTimelapseSpeedPin, INPUT_PULLUP);

  // initialise output pins
  pinMode(motorOffPin, OUTPUT);
  pinMode(ledPin, OUTPUT);

  // Debounced start button
  debouncer.attach(startButtonPin,INPUT_PULLUP); // Attach the debouncer to a pin with INPUT_PULLUP mode
  debouncer.interval(25); // Use a debounce interval of 25 milliseconds

  // set startup state
  currentState = INIT;
  isNewState = true;
}

Loop()

Inside the main loop, we first check the state of the push-button, as this has some function in every state.

Then we switch through all of the possible machine states, printing any debug messages if required, calling the functions for that state, and checking to see if the push button has been pressed in case we need to switch to a different state.

After the state machine switch, we just check to see if continuous debug messages need to be printed.

void loop() {

  // Debounced start button
  debouncer.update(); // Update the Bounce instance


  switch (currentState) {
    case INIT:

      #ifdef DEBUG_STATE_CHANGE
        if (isNewState) {
          printDebugMessages();
          isNewState = false;
        }
      #endif

      setLEDStatus(0);       // LED status light 
      switchOffMotor();      // disable motor until we need it
      getControlInputs();    // get values for all the motion control variables

      if ( debouncer.fell() ) {  
        // first, check both end stops are connected and not currently closed
        if (digitalRead(rightEndStopPin) == HIGH || digitalRead(leftEndStopPin) == HIGH) {
          switchOffMotor; // just in case
          errorMessage = "ERROR: One of the end stops is either closed, or not connected";
          currentState = ABORTED;
          isNewState = true;
        } else { // otherwise, it's safe to start traverse
          currentState = TRAVERSING;
          isNewState = true;
        }
      }
      break;


    case TRAVERSING:
    
      #ifdef DEBUG_STATE_CHANGE
        if (isNewState) {
          printDebugMessages();
          isNewState = false;
        }
      #endif

      if ( debouncer.fell() ) {  
        currentState = INIT;
        isNewState = true;
      }

      switchOnMotor();
      traverse();
      setLEDStatus(1);    // LED status light 

      break;
      

    case ABORTED:

      #ifdef DEBUG_STATE_CHANGE
        if (isNewState) {
          printDebugMessages();
          isNewState = false;
        }
      #endif
      
      switchOffMotor();
      setLEDStatus(2);    // LED status light 

      if ( debouncer.fell() ) {  
        currentState = INIT;
        isNewState = true;
      }
  
      break;
      
  }


  // program-wide debug status messages
  // runs continuously on a timer
  #ifdef DEBUG_TIMER
    if(statusUpdateTimer.repeat()){
      printDebugMessages();
    }
  #endif

}

Other functions

These functions are called from different states within the main loop.

setLEDStatus switches based on the possible values of the enum we set up earlier called possibleStates. An easy mistake here is to write this function as: void setLEDStatus(enum possibleStates) {}. But of course the parameter type is possibleStates, not enum (even though possibleStates is a kind of enum we created earlier).

void setLEDStatus(possibleStates currentStateForLED) {
  switch (currentStateForLED) {
    case INIT:
      if(readyBlinkTimer.repeat()){
        digitalWrite(ledPin,!digitalRead(ledPin)); 
      }
      break;

    case TRAVERSING:
      digitalWrite(ledPin, HIGH);
      break;   

    case ABORTED:
      if(warningBlinkTimer.repeat()){
        digitalWrite(ledPin,!digitalRead(ledPin)); 
      }  
      break;
  } 
}

I only want to get the speed, direction and mode inputs when the slider is not moving; that way any unintentional changes don’t affect motion while you’re recording.

void getControlInputs() {

  // TIMELAPSE OR VIDEO MODE 
  if (digitalRead(isTimelapseSpeedPin) == LOW) { // timelapse mode selected
    isTimelapseSpeed = true;
  } else {
    isTimelapseSpeed = false;
  }

  // SET DIRECTION
  if (digitalRead(startFromLeftPin) == LOW) { // start from left selected
    startFromLeft = true;
  } else {
    startFromLeft = false;
  }
 

  // SPEED CONTROL 
  speedInput = analogRead(speedPotPin);

  // map the speed set on the knob to useful values
  if (isTimelapseSpeed) { // test to see if we're running in timelapse or video mode
    traverseSpeed = map(speedInput, 0, 1023, timelapseMinSpeed, timelapseMaxSpeed);
  } else {
    traverseSpeed = map(speedInput, 0, 1023, videoMinSpeed, videoMaxSpeed);
  }


  // test to see if we're running L-R or R-L
  if (!startFromLeft) { // if NOT startFromLeft
    traverseSpeed = -traverseSpeed; // Left is negative, so if we start from the left, speed must be positive!
  }
}

For the motion itself, I’m using the AccelStepper library. I documented some of my trials with that here:

void traverse() {
  stepper.setSpeed(traverseSpeed);
  stepper.runSpeed();

  if (digitalRead(leftEndStopPin) == HIGH) { // if it touches left end-stop
    DEBUG_PRINT("Hit left end stop");
    // move off the end stop
    stepper.runToNewPosition(stepper.currentPosition() + endStopPadding); // Blocks until stepper is in position
    errorMessage = "Finished traverse";
    currentState = INIT;
    isNewState = true;
  }

  if (digitalRead(rightEndStopPin) == HIGH) { // if it touches right end-stop
    DEBUG_PRINT("Hit left end stop");
    // move off the end stop
    stepper.runToNewPosition(stepper.currentPosition() - endStopPadding); // Blocks until stepper is in position
    errorMessage = "Finished traverse";
    currentState = INIT;
    isNewState = true;
  }
}

It’s pretty easy to switch off power to the motor by pulling the ENABLE pin high:

void switchOffMotor() {
  digitalWrite(motorOffPin, HIGH); // assumes ENABLE pin is connected to D13
}


void switchOnMotor() {
  digitalWrite(motorOffPin, LOW); 
}

And finally there’s a function handle printing the current status to the serial monitor:

void printDebugMessages() {
#ifdef DEBUG  
  Serial.println("\n--------------------   ----------");
  // machine state
  Serial.print("Current state:         ");
  Serial.println(stateLabels[currentState]);

  // latest error message
  if (errorMessage != "") {
    Serial.print("Last message:          ");
    Serial.println(errorMessage);
  }

  // control settings
  Serial.print("Start from:            ");
  if (startFromLeft == 1) {
    Serial.println("Left");  
  } else {
    Serial.println("Right"); 
  }
  Serial.print("Mode:                  ");
  if (isTimelapseSpeed == 1) {
    Serial.println("Time-lapse");  
  } else {
    Serial.println("Video"); 
  }
  Serial.print("Speed:                 ");
  Serial.println(traverseSpeed);
  Serial.println("--------------------   ----------\n");
#endif
}