A library to create a variety of stroking motions with a stepper or servo motor on an ESP32. A usage example can be found in my other related project: FuckIO. It will work with all kinds of stepper / servo operated fucking and stroking machines. An other popular example is the Kinky Makers OSSM-Project
Every DIY fucking machine with a linear position drive powered by a stepper or servo motor can be used with this library.
StrokeEngine takes full advantage of the freedom a servo / stepper driven stroking or fucking machine can provide over fixed cam-driven designs. To this date there are only few commercial offerings using this advantage. And often so the implementation is rather boring, not utilizing the full possibilities of such a linear position drive.
Under the hood it uses the fabulous FastAccelStepper library to interface stepper or servo motors with commonly found STEP / DIR interfaces.
Understanding the underlying concepts will help you to get up and running with StrokeEngine faster.
The machine spans it's own internal coordinate system. It takes the real world (metric) units and converts them into the internal coordinate system just counting the encoder / stepper steps of the motor and vice versa. This offers the advantage, that this is independent of a specific implementation and works with all machine sizes and regardless of the motor chosen.
- The system is 1-dimensional and the positive move direction is towards the front a.k.a. towards the body.
- The physicalTravel is the real physical travel the machine has from one hard endstop to the other.
- From
physicalTravel
a safety distance called keepoutBoundary is subtracted on each side giving the real working distance _travel:This gives a safety margin to avoid crashes into a hard endstop._travel = physicalTravel - (2 * keepoutBoundary)
- The Home-position is expected to be at
-keepoutBoundary
. Albeit not recommended for safety reasons, it is possible to mount the home switch in the front atphysicalTravel
as well. - Zero MIN = 0 is
keepoutBoundary
away from the home position. - Pattern make use of Depth and Stroke. These values are dynamic parameter and may be adjusted during runtime:
- Depth is the furthest point the machine can extract at any given time. This is useful to find a sweet spot in positioning the body relative to the machine.
- Stroke is the longest working distance a stroking motion will have.
Think of Stroke as the amplitude and Depth a linear offset that is added.
One of the biggest benefits of a linear position drive over a cam-driven motion is its versatility. StrokeEngine uses a pattern generator to provide a wide variety of sensations where parameters like speed, stroke and depth are adjusted dynamically on a motion by motion basis. It uses a trapezoidal motion profile with a defined acceleration and deceleration distance. In between it moves with a constant speed. Pattern take depth, stroke, speed and an arbitrary sensation parameter. In Pattern.md you can find a detailed description of each available pattern. Also some information how to write your own patterns and contribute them to this project.
One design goal was to have a unobtrusive failure handling when invalid parameters are given. Either from the user with values that lay outside the physics of the machine, or from a pattern commanding an impossible speed, position or acceleration. All set-functions make use of a constrain()
-function to limit the input to the physical capabilities of the given machine. Values outside the bounds are simply cropped.
Also on the pattern side constrain()
is used to ensure no impossible motion commands leading to crashes or step losses are executed. This manifests in a distortion of the motion. Strokes may be shortened when position targets outside of the machine bounds are requested (e.g. stroke > depth
). Acceleration and speed are limited leading to distorted ramps. The motion is executed over the full distance, but may take slightly longer then expected to reach the target position.
It is possible to update any parameter like depth, stroke, speed and pattern mid-stroke. This gives a very responsive and fluid user experience. Safeguards are in place to ensure the move stays inside the bounds of the machine at any time.
An internal finite state machine handles the different states of the machine. See the below graph with all functions relating to the state machine and how to cause transitions:
stateDiagram-v2
[*] --> UNDEFINED: begin()
UNDEFINED --> READY : thisIsHome()<br>enableAndHome()
READY --> PATTERN : startPattern()
READY --> UNDEFINED : disable()
READY --> READY : moveToMin()<br>moveToMax()
READY --> SETUPDEPTH : setupDepth()
SETUPDEPTH --> UNDEFINED: disable()
PATTERN --> READY : stopMotion()<br>moveToMin()<br>moveToMax()
PATTERN --> SETUPDEPTH : setupDepth()
PATTERN --> UNDEFINED : disable()
SETUPDEPTH --> PATTERN : startPattern()
SETUPDEPTH --> READY : stopMotion()<br>moveToMin()<br>moveToMax()
- UNDEFINED: The initial state prior to homing. Stepper / Servo are disabled and the position is undefined.
- READY: Homing defines the position inside the internal coordinate system. Machine is now ready to be used and accepts motion commands.
- PATTERN: The cyclic motion has started and the pattern generator is commanding a sequence of trapezoidal motions until stopped.
- SETUPDEPTH: The servo always follows the depth position. This can be used to setup the optimal stroke depth.
StrokeEngine aims to have a simple and straight forward, yet powerful API. The following describes the minimum case to get up and running. All input parameters need to be specified in real world (metric) units.
First all parameters of the machine and the servo need to be set. Including the pins for interacting with the driver and an (optionally) homing switch.
#include <StrokeEngine.h>
// Pin Definitions
#define SERVO_PULSE 4
#define SERVO_DIR 16
#define SERVO_ENABLE 17
#define SERVO_ENDSTOP 25 // Optional: Only needed if you have a homing switch
// Calculation Aid:
#define STEP_PER_REV 2000 // How many steps per revolution of the motor (S1 off, S2 on, S3 on, S4 off)
#define PULLEY_TEETH 20 // How many teeth has the pulley
#define BELT_PITCH 2 // What is the timing belt pitch in mm
#define MAX_RPM 3000.0 // Maximum RPM of motor
#define STEP_PER_MM STEP_PER_REV / (PULLEY_TEETH * BELT_PITCH)
#define MAX_SPEED (MAX_RPM / 60.0) * PULLEY_TEETH * BELT_PITCH
static motorProperties servoMotor {
.maxSpeed = MAX_SPEED, // Maximum speed the system can go in mm/s
.maxAcceleration = 10000, // Maximum linear acceleration in mm/s²
.stepsPerMillimeter = STEP_PER_MM, // Steps per millimeter
.invertDirection = true, // One of many ways to change the direction,
// should things move the wrong way
.enableActiveLow = true, // Polarity of the enable signal
.stepPin = SERVO_PULSE, // Pin of the STEP signal
.directionPin = SERVO_DIR, // Pin of the DIR signal
.enablePin = SERVO_ENABLE // Pin of the enable signal
};
static machineGeometry strokingMachine = {
.physicalTravel = 160.0, // Real physical travel from one hard endstop to the other
.keepoutBoundary = 5.0 // Safe distance the motion is constrained to avoiding crashes
};
// Configure Homing Procedure
static endstopProperties endstop = {
.homeToBack = true, // Endstop sits at the rear of the machine
.activeLow = true, // switch is wired active low
.endstopPin = SERVO_ENDSTOP, // Pin number
.pinMode = INPUT // pinmode INPUT with external pull-up resistor
};
StrokeEngine Stroker;
Inside void setup()
call the following functions to initialize the StrokeEngine:
void setup()
{
// Setup Stroke Engine
Stroker.begin(&strokingMachine, &servoMotor);
Stroker.enableAndHome(&endstop); // pointer to the homing config struct
// other initialization code
// wait for homing to complete
while (Stroker.getState() != READY) {
delay(100);
}
}
Some machines may not have a homing switch mounted. For these you may use a manual homing procedure instead of Stroker.enableAndHome(&endstop);
. Manually move back until the physical endstop and then call:
Stroker.thisIsHome();
This enables the driver and sets the current position as -keepoutBoundary
. It then slowly moves to 0.
Be sure to know what you do. If this function is called while not at the physical endstop the internal coordinate system is off resulting in a certain crash! This could damage your machine!
This is an example snippet showing how Stroker.getNumberOfPattern()
and Stroker.getPatternName(i)
may be used to iterate through the available patterns and composing a JSON-String.
String getPatternJSON() {
String JSON = "[{\"";
for (size_t i = 0; i < Stroker.getNumberOfPattern(); i++) {
JSON += String(Stroker.getPatternName(i));
JSON += "\": ";
JSON += String(i, DEC);
if (i < Stroker.getNumberOfPattern() - 1) {
JSON += "},{\"";
} else {
JSON += "}]";
}
}
Serial.println(JSON);
return JSON;
}
Use Stroker.startPattern();
and Stroker.stopMotion();
to start and stop the motion. Stop is immediate and with the highest possible acceleration.
You can move to either end of the machine for setting up reaches. Call Stroker.moveToMin();
to move all they way back towards home. With Stroker.moveToMax();
it moves all the way out. Takes the speed in mm/s as an argument: e.g. Stroker.moveToMax(10.0);
Speed defaults to 10 mm/s. Can be called from states SERVO_RUNNING
and SERVO_READY
and stops any current motion. Returns false
if called in a wrong state.
In a special setup mode it will always follow the Depth position. By evoking Stroker.setupDepth();
it will start to follow the depth position whenever Stroker.setDepth(float);
is updated. Takes the speed in mm/s as an argument: e.g. Stroker.setupDepth(10.0);
Speed defaults to 10 mm/s. With float Stroker.getDepth()
one may obtain the current set depth to calculate incremental updates for Stroker.setDepth(float)
. Can be called from states SERVO_RUNNING
and SERVO_READY
and stops any current motion. Returns false
if called in a wrong state.
To setup the optimal depth and reach of the machine Stroker.setupDepth(10.0, true)
evokes a special fancy adjustment mode. This allows not only to interactively adjust depth
, but also stroke
by using the sensation slider. sensation
gets mapped into the interval [depth-stroke, depth]
: sensation = 100
adjusts depth
-position, whereas sensation = -100
adjusts the stroke
-position. sensation = 0
yields the midpoint of the stroke.
Parameters can be updated in any state and are stored internally. On Stroker.startMotion();
they will be used to initialize the pattern. Each one may be called individually. The argument given to the function is constrained to the physical limits of the machine:
Stroker.setSpeed(float speed, bool applyNow); // Speed in Cycles (in & out) per minute, constrained from 0.5 to 6000
Stroker.setDepth(float depth, bool applyNow); // Depth in mm, constrained to [0, _travel]
Stroker.setStroke(float stroke, bool applyNow); // Stroke length in mm, constrained to [0, _travel]
Stroker.setSensation(float sensation, bool applyNow); // Sensation (arbitrary value a pattern may use to alter its behavior),
// constrained to [-100, 100] with 0 being neutral.
Stroker.setPattern(int index, bool applyNow); // Pattern, index must be < Stroker.getNumberOfPattern()
Normally a parameter change is only executed after the current stroke has finished. However, sometimes it is desired to have the changes take effect immediately, even mid-stroke. In that case set the argument bool applyNow
to true
.
Each set-function has a corresponding get-function to read out what parameters are currently set. As each set-function constrains it's input one can read back the truncated value that is actually used by the StrokeEngine. This is useful for implementing UI's.
float Stroker.getSpeed(); // Speed in Cycles (in & out) per minute, constrained from 0.5 to 6000
float Stroker.getDepth(); // Depth in mm, constrained to [0, _travel]
float Stroker.getStroke(); // Stroke length in mm, constrained to [0, _travel]
float Stroker.getSensation(); // Sensation (arbitrary value a pattern may use to alter its behavior),
// constrained to [-100, 100] with 0 being neutral.
int Stroker.getPattern(); // Pattern, index is [o, Stroker.getNumberOfPattern()[
Consult StrokeEngine.h for further functions and a more detailed documentation of each function. Some functions are overloaded and may provide additional useful functionalities.
It is possible to receive telemetry information's about each trapezoidal move a pattern generates. You may register a callback function y calling Stroker.registerTelemetryCallback(callbackTelemetry)
with the following signature void callbackTelemetry(float position, float speed, bool clipping)
.