Skip to main content
StrokeEngine is a library for creating a variety of stroking motions with stepper or servo motors on an ESP32. You can use it with any DIY machine featuring a linear position drive powered by a stepper or servo motor.

Core concepts

StrokeEngine takes full advantage of servo and stepper driven machines over fixed cam-driven designs. Under the hood, it uses the FastAccelStepper library to interface with motors using standard STEP/DIR signals.
Understanding these concepts helps you get up and running with StrokeEngine faster.

Coordinate system

The machine uses an internal coordinate system that converts real-world metric units into encoder/stepper steps. This abstraction works with all machine sizes regardless of the motor you choose.
Coordinate system diagram showing the relationship between physicalTravel, keepoutBoundary, depth, and stroke parameters

StrokeEngine coordinate system showing physical travel, keepout boundaries, and stroke parameters

Key coordinate concepts:
TermDescription
physicalTravelThe real physical travel from one hard endstop to the other
keepoutBoundarySafety distance subtracted from each side to prevent crashes
_travelWorking distance: physicalTravel - (2 * keepoutBoundary)
HomePosition at -keepoutBoundary (typically at the rear)
MIN = 0Zero position, keepoutBoundary away from home
DepthThe furthest point the machine extends (adjustable at runtime)
StrokeThe working distance of a stroking motion (adjustable at runtime)
Think of Stroke as the amplitude and Depth as a linear offset added to it. The positive move direction is towards the front (towards the body).

Patterns

StrokeEngine uses a pattern generator to provide a wide variety of sensations. Patterns dynamically adjust parameters like speed, stroke, and depth on a motion-by-motion basis using trapezoidal motion profiles. Each pattern accepts four parameters:
  • depth — Maximum extension point
  • stroke — Motion amplitude
  • speed — Cycles per minute
  • sensation — Arbitrary modifier for pattern behavior (-100 to 100)
See the Pattern documentation for detailed descriptions of available patterns and instructions for creating your own.

Graceful error handling

StrokeEngine handles invalid parameters gracefully without interrupting operation:
  • All setter functions use constrain() to limit inputs to the machine’s physical capabilities
  • Values outside bounds are automatically cropped
  • Pattern commands that exceed machine limits result in shortened strokes or adjusted ramps
  • Motion completes over the full distance but may take slightly longer than expected

Mid-stroke parameter updates

You can update depth, stroke, speed, and pattern parameters mid-stroke for a responsive, fluid user experience. Built-in safeguards ensure the machine stays within bounds at all times.

State machine

An internal finite state machine manages the machine states:
StateDescription
UNDEFINEDInitial state before homing. Motor is disabled and position is unknown.
READYHoming complete. Machine accepts motion commands.
PATTERNPattern generator is running cyclic motions.
SETUPDEPTHMotor follows the depth position for interactive adjustment.

Usage

StrokeEngine provides a simple yet powerful API. Specify all input parameters in real-world metric units.

Initialize the library

1

Define pin configurations

Set up the pins for your motor driver and optional homing switch:
main.cpp
#include <StrokeEngine.h>

// Pin Definitions
#define SERVO_PULSE       4
#define SERVO_DIR         16
#define SERVO_ENABLE      17
#define SERVO_ENDSTOP     25        // Optional: Only needed with a homing switch
2

Configure motor properties

Calculate steps per millimeter based on your hardware:
main.cpp
// Calculation Aid:
#define STEP_PER_REV      2000      // Steps per revolution (check driver DIP switches)
#define PULLEY_TEETH      20        // Teeth on the drive pulley
#define BELT_PITCH        2         // Timing belt pitch in mm
#define MAX_RPM           3000.0    // Maximum motor RPM
#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 in mm/s
  .maxAcceleration = 10000,           // Maximum acceleration in mm/s²
  .stepsPerMillimeter = STEP_PER_MM,  // Steps per millimeter 
  .invertDirection = true,            // Flip direction if motor moves wrong way
  .enableActiveLow = true,            // Enable signal polarity      
  .stepPin = SERVO_PULSE,             // STEP signal pin
  .directionPin = SERVO_DIR,          // DIR signal pin
  .enablePin = SERVO_ENABLE           // Enable signal pin
};
3

Define machine geometry

Specify the physical dimensions of your machine:
main.cpp
static machineGeometry strokingMachine = {
  .physicalTravel = 160.0,            // Total travel between hard endstops (mm)
  .keepoutBoundary = 5.0              // Safety margin on each side (mm)
};
4

Configure homing

Set up the endstop switch properties:
main.cpp
static endstopProperties endstop = {
  .homeToBack = true,                 // Endstop at rear of machine
  .activeLow = true,                  // Switch wired active low
  .endstopPin = SERVO_ENDSTOP,        // Endstop pin number
  .pinMode = INPUT                    // Use INPUT with external pull-up
};

StrokeEngine Stroker;
5

Initialize in setup()

Call the initialization functions and wait for homing to complete:
main.cpp
void setup() {
  // Initialize StrokeEngine
  Stroker.begin(&strokingMachine, &servoMotor);
  Stroker.enableAndHome(&endstop);
  
  // Your other initialization code here
  
  // Wait for homing to complete
  while (Stroker.getState() != READY) {
    delay(100);
  }
}
When getState() returns READY, the machine is homed and ready for motion commands.

Manual homing (no endstop switch)

Manual homing is dangerous. Calling thisIsHome() when not at the physical endstop causes an incorrect coordinate system, resulting in a crash that could damage your machine.
If your machine lacks a homing switch, you can use manual homing:
  1. Physically move the machine to the rear endstop
  2. Call Stroker.thisIsHome()
Stroker.thisIsHome();
This enables the driver, sets the current position as -keepoutBoundary, and slowly moves to position 0.

Retrieve available patterns

Use getNumberOfPattern() and getPatternName() to enumerate available patterns:
patterns.cpp
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;
}

Motion control

Start and stop motion

FunctionDescription
Stroker.startPattern()Start the pattern-based stroking motion
Stroker.stopMotion()Stop immediately with maximum deceleration

Position commands

Move to either end of the machine for setup purposes:
Stroker.moveToMin();        // Move to rear (home) position
Stroker.moveToMax();        // Move to front (maximum) position
Stroker.moveToMax(10.0);    // Move at 10 mm/s (default speed)
These functions can be called from PATTERN or READY states and stop any current motion. They return false if called in an invalid state.

Interactive depth setup

Enter depth setup mode where the motor follows the depth position in real-time:
Stroker.setupDepth();           // Default 10 mm/s
Stroker.setupDepth(10.0);       // Specify speed
Update the depth position with Stroker.setDepth(float) and read the current value with Stroker.getDepth().
Fancy Mode: Call Stroker.setupDepth(10.0, true) to adjust both depth and stroke interactively. The sensation slider maps to the interval [depth-stroke, depth]:
  • sensation = 100 — Adjusts depth position
  • sensation = -100 — Adjusts stroke position
  • sensation = 0 — Positions at stroke midpoint

Parameter functions

Update parameters at any time. Values are automatically constrained to machine limits:
// Setter functions
Stroker.setSpeed(float speed, bool applyNow);      // 0.5–6000 cycles/min
Stroker.setDepth(float depth, bool applyNow);      // 0 to _travel (mm)
Stroker.setStroke(float stroke, bool applyNow);    // 0 to _travel (mm)
Stroker.setSensation(float sensation, bool applyNow); // -100 to 100
Stroker.setPattern(int index, bool applyNow);      // 0 to getNumberOfPattern()-1
Set applyNow to true to apply changes immediately mid-stroke. Otherwise, changes take effect after the current stroke completes.
Read back the constrained values actually used by StrokeEngine:
// Getter functions
float speed = Stroker.getSpeed();         // Cycles per minute
float depth = Stroker.getDepth();         // Depth in mm
float stroke = Stroker.getStroke();       // Stroke length in mm
float sensation = Stroker.getSensation(); // -100 to 100
int pattern = Stroker.getPattern();       // Pattern index

Advanced features

Telemetry callback

Register a callback to receive telemetry data for each trapezoidal move:
void callbackTelemetry(float position, float speed, bool clipping) {
  // Handle telemetry data
}

Stroker.registerTelemetryCallback(callbackTelemetry);
Consult StrokeEngine.h in the source repository for the complete API reference, including overloaded functions and additional features.