Stepper on Z axis


#1

Nice! I just used one of our teaching labs’ Arduino Leonardo boards wired up to a cheap stair stepper and now have a Z-value coming through on the generic USB game controller HID that increases from 0 to 127 the faster I step.

Documentation, with lots of pictures and the (probably horrid) Arduino code will be forthcomming once I get in an Arduino Micro and a 75 mm plastic pipe-cap to get all the guts neatly packaged back in the device frame.

This initial version just taps into the device’s own cycle-switch that triggers every left step, so has a resolution of up to 2 seconds! I am right now investigating which of several available methods of capturing data in a more fine-grained manner might be best.


#2

Refactored my Arduino code to a point where I am happy to make it public (note: I have absolutely no formal training as a programmer, be gentle! :grin:)

The exercise stepper I am using has a magnetic reed switch that activates each time the left foot pedal goes all the way down. The below code times the intervals between switch activations and converts it to a value from 0 to 127, which are sent to the computer via a Joystick HID Throttle analog channel.

Also a timing check to work out if the user has stopped moving and thence send out a throttle value of zero. The ‘timeout’ constant near the top of the code basically sets the minimum trigger speed of the switch and is used in both the cycle-speed-to-analog range conversion and the timeout routine.

The timeout resolution of this is about 2 seconds, meaning you can step as slowly as 1-second per footfall; however, if you stop moving it will take 2 seconds for that to register. Faster timeouts mean you can’t slow-step.

This should work with any device that periodically pulses a line to indicate speed of motion. Most exercise bikes, for example, have a magnetic reed switch on the wheel for the same effect, though obviously the timings will be different!

#include "Joystick.h"

// Create Joystick
Joystick_ Joystick;

const int timeout = 2048; // activity timeout to detect when device has stopped moving while switch active.

const int reedSwitch = 2;    // the pin that the reed switch is attached to
const int ledPin = 13;       // the pin that the LED is attached to
unsigned long thisEvent = 0;  // millisecond value for this switch event.
unsigned long lastEvent = 0;  // millisecond value for last switch even.
int output = 0; // the final value of the output to the JS.Z interfare
int switchState = LOW;         // current state of the button
int lastSwitchState = LOW;     // previous state of the button
int temp = 0; // temporary integer for loops

void setup() {

  Serial.begin(9600); // serial comms for testing and tuning
  
  Joystick.setThrottleRange(-127, 127); // set range on Joystick channel to be used.
  Joystick.begin();  // activate Joystick HID interface
  
  pinMode(reedSwitch, INPUT); // the reed switch input
  pinMode(ledPin, OUTPUT); // test LED

  lastEvent = millis() - timeout;  // set up initial value so timing comparisons work from first run
}

void loop() {

  thisEvent = millis(); // update current time
  
//detect high->low transition of switch
  lastSwitchState = switchState;   // save the current state as the last state, for next time through the loop
  switchState = digitalRead(reedSwitch);  // read the pushbutton input pin for current state

  if (switchState == LOW && lastSwitchState == HIGH) {  // switch state has changed from LOW to HIGH (rising edge)
    output = constrain( map(thisEvent - lastEvent, timeout,0, 0,127),0,127); // calculate the time since last event and turn it into a 0-127 value
         Serial.print(output);                  // print output value to console (monitoring for debug)
         Serial.print("\t");                    // print a separator
         for(temp = 0; temp < output/4; temp++) // loop to print a crude bar-graph of value (monitoring)
           Serial.print("#");                   // print a bar-graph element
         Serial.println("");                    // print a new-line after bar-graph
     Joystick.setThrottle(output); // send value to USB Joystick HID interface
     lastEvent = thisEvent; // grab the timing of the switch transition
 }

  if ( (thisEvent - lastEvent) > timeout) { // timeout indicates motion has stopped, so send a zero value
        Serial.println("0\tTIMEOUT");              // print an indicator that intra-event timeout occurred
    Joystick.setThrottle(0); // send value to USB Joystick HID interface
    delay(100); // slow timeouts down a bit!
  }

  delay(20);  // Delay loop a little bit to avoid switch bouncing
}

I just ordered a second magnetic reed switch from my electronics supplier which I will attach to the right foot pedal (the manufacture symmetry of this particular stepper means there is already an empty mount point on that side, even). This will allow me to double the time resolution, and hence halve the timeout without effecting minimum step-speed from what I have it as now.


#3

With two magnetic reed switches, and appropriate changes to code, I now have the no-longer-moving timeout down to 1 second. I wired the switches to separate inputs rather than in parallel as that gave me more leeway with overlapping triggering conditions if the stepper was set to shallow steps (manual operation via an adjustment screw on the device).

#include "Joystick.h"

// Create Joystick
Joystick_ Joystick;

const int timeout = 1024;       // activity timeout to detect when device has stopped moving

const int reedSwitch1 = 2;      // the pin that reed switch 1 is attached to
const int reedSwitch2 = 3;      // the pin that reed switch 2 is attached to
const int ledPin = 13;          // the pin that the LED is attached to
unsigned long thisEvent = 0;    // millisecond value for this switch event.
unsigned long lastEvent = 0;    // millisecond value for last switch even.
int lastOutput = 0;             // last output value (for averaging/smoothing)
int output = 0;                 // the final value of the output to the JS.Z interfare
int switchState1 = LOW;         // current state of reed switch 1
int lastSwitchState1 = LOW;     // previous state of reed switch 1
int switchState2 = LOW;         // current state of reed switch 2
int lastSwitchState2 = LOW;     // previous state of reed switch 2
int temp = 0; // temporary integer for loops

void setup() {

  Serial.begin(9600); // serial comms for testing and tuning
  
  Joystick.setThrottleRange(-127, 127); // set range on Joystick channel to be used.
  Joystick.begin();  // activate Joystick HID interface
  
  pinMode(reedSwitch1, INPUT); // setup reed switch 1 input
  pinMode(reedSwitch2, INPUT); // setup reed switch 2 input
  pinMode(ledPin, OUTPUT); // test LED

  lastEvent = millis() - timeout;  // set up initial value so timing comparisons work from first run
}

void loop() {

  thisEvent = millis(); // update current time
  
//detect high->low transition of switch 1
  lastSwitchState1 = switchState1;   // save the current state as the last state, for next time through the loop
  switchState1 = digitalRead(reedSwitch1);  // read the pushbutton input pin for current state

//detect high->low transition of switch 2
  lastSwitchState2 = switchState2;
  switchState2 = digitalRead(reedSwitch2);

  if ((switchState1 == HIGH && lastSwitchState1 == LOW)||(switchState2 == HIGH && lastSwitchState2 == LOW)) {  // either switch state has changed from HIGH to LOW (falling edge)
    output = constrain( map(thisEvent - lastEvent, timeout,0, -63,191),0,127); // calculate the time since last event and turn it into a 0-127 value
    output = (output + lastOutput) / 2;         // average the output value with the last one to smooth things out a bit
    lastOutput = output;                        // set the new lastOutput value for use next time through
         Serial.print(output);                  // print output value to console (monitoring for debug)
         Serial.print("\t");                    // print a separator
         for(temp = 0; temp < output/4; temp++) // loop to print a crude bar-graph of value (monitoring)
           Serial.print("#");                   // print a bar-graph element
         Serial.println("");                    // print a new-line after bar-graph
     Joystick.setThrottle(output); // send value to USB Joystick HID interface
     lastEvent = thisEvent; // grab the timing of the switch transition
 }

  if ( (thisEvent - lastEvent) > timeout) {     // timeout indicates motion has stopped, so send a zero value
        Serial.println("0\tTIMEOUT");           // print an indicator that intra-event timeout occurred
    Joystick.setThrottle(0);                    // send value to USB Joystick HID interface
    lastOutput = 0;                             // reset lastOutput also to zero
    delay(100);                                 // slow timeouts down a bit!
  }

  delay(20);  // Delay loop a little bit to avoid switch bouncing
}

Changes:

  • added trigger detection for the second input channel (Arduino digital input #3).
  • reduced the timeout to 1024ms (about 1 second).
  • fiddled the mapping of the timing readings to get better zero-full deflection on the output.
  • Installed on an Arduino Micro so it will fit easily inside the exercise equipment.
  • moved the joystick output to the “Throttle” channel for now, since I haven’t got around to looking into which channel is best for this on the HiFi end yet (Throttle or Z).

#4

Annnnnd… A bit of a write-up with pictures!! (not entirely happy with it yet, and will likely revise it over the next rest-of-my-life :wink:).


#5

As someone who tries to walk 10,000 steps per day I hate you for removing the " it’s raining" excuse
Vr excercise can and should be a huge part of vr
Cool project thanks for sharing it…


#6

It will get me by until I decide if I want to go all-in on a Virtuix Omni! :grinning:
(Costing about the same as a decent non-pro push bike, their pricing is very attractive, but I would want to get on one to try it in person before making a final decision).


#7

Nice to see a maker working in the virtual movement space. It is pretty cool to see an inexpensive stepper repurposed for VR walking :sunny:

The intriguing part of the Virtuix is how dead simple their approach is. Take a concave surface, make it very smooth. Put on slippers designed to make contact even lower friction. Their slipper seems to have something that the under-surface sensors use to determine movement. I wondered why they did not go with IMUs on ankle bracelets. They do not have to be dead accurate since their purpose is just to monitor gross movement. I think the real design smarts of the Virtuix Omni is ensuring a person does not fall down. When you see how fast people are running in place, making sure they do not launch themselves is paramount.


#8

Last demo video I saw of the Omni did involve ankle bracelets for tracking, which had me wondering why they weren’t using some sort of sensor in the bowl itself :smirk:


#9

The Virtuix has been evolving. I almost bought their early dev kits for a teen VR project but it was far too expensive for a 20 person experience. Back then they had just the slippers, no ankle bracelets. Good to see progress happening with that thing.

Still, I wonder when this all will shift to yoga and martial arts centers renting out their spaces for more physical VR. I guess when VR gear gets lighter and becomes portable we will see this transition.