{"id":16991442,"url":"https://github.com/theelims/strokeengine","last_synced_at":"2025-03-23T17:31:12.276Z","repository":{"id":39797579,"uuid":"395463600","full_name":"theelims/StrokeEngine","owner":"theelims","description":"A library to create a variety of stroking motions with a stepper or servo motor on an ESP32.","archived":false,"fork":false,"pushed_at":"2023-10-17T21:18:37.000Z","size":1344,"stargazers_count":37,"open_issues_count":2,"forks_count":13,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-03-18T22:24:47.654Z","etag":null,"topics":["esp32"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/theelims.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-08-12T23:01:02.000Z","updated_at":"2025-03-17T13:06:24.000Z","dependencies_parsed_at":"2023-02-10T12:15:44.040Z","dependency_job_id":"3a8beb54-8b14-4f5c-8222-47d007d1611c","html_url":"https://github.com/theelims/StrokeEngine","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theelims%2FStrokeEngine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theelims%2FStrokeEngine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theelims%2FStrokeEngine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theelims%2FStrokeEngine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/theelims","download_url":"https://codeload.github.com/theelims/StrokeEngine/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245140922,"owners_count":20567473,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["esp32"],"created_at":"2024-10-14T03:25:55.059Z","updated_at":"2025-03-23T17:31:11.413Z","avatar_url":"https://github.com/theelims.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# StrokeEngine\nA 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](https://github.com/theelims/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](https://github.com/KinkyMakers/OSSM-hardware)\n\nEvery DIY fucking machine with a linear position drive powered by a stepper or servo motor can be used with this library. \n\n## Concepts\nStrokeEngine 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. \n\nUnder the hood it uses the fabulous [FastAccelStepper](https://github.com/gin66/FastAccelStepper) library to interface stepper or servo motors with commonly found STEP / DIR interfaces.\n\nUnderstanding the underlying concepts will help you to get up and running with StrokeEngine faster. \n\n### Coordinate System\nThe 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. \n\n![Coordinate System](./doc/coordinates.svg)\n* The system is 1-dimensional and the positive move direction is towards the front a.k.a. towards the body.\n* The __physicalTravel__ is the real physical travel the machine has from one hard endstop to the other. \n* From `physicalTravel` a safety distance called __keepoutBoundary__ is subtracted on each side giving the real working distance **_travel**: \n  ```\n  _travel = physicalTravel - (2 * keepoutBoundary)\n  ``` \n  This gives a safety margin to avoid crashes into a hard endstop.\n* 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 at `physicalTravel` as well.\n* Zero __MIN = 0__ is `keepoutBoundary` away from the home position.\n* Pattern make use of __Depth__ and __Stroke__. These values are dynamic parameter and may be adjusted during runtime:\n  * __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.\n  * __Stroke__ is the longest working distance a stroking motion will have.\n\nThink of __Stroke__ as the amplitude and __Depth__ a linear offset that is added.\n\n### Pattern\nOne 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](./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.\n\n### Graceful Behavior\nOne 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. \n\nAlso 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 \u003e 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. \n\n### Mid-Stroke Parameter Update\nIt 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.\n\n### State Machine\nAn 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:\n```mermaid\nstateDiagram-v2\n    [*] --\u003e UNDEFINED: begin()\n    UNDEFINED --\u003e READY     : thisIsHome()\u003cbr\u003eenableAndHome()\n    READY --\u003e PATTERN       : startPattern()\n    READY --\u003e UNDEFINED     : disable()\n    READY --\u003e READY         : moveToMin()\u003cbr\u003emoveToMax()\n    READY --\u003e SETUPDEPTH    : setupDepth()\n    SETUPDEPTH --\u003e UNDEFINED: disable()\n    PATTERN --\u003e READY       : stopMotion()\u003cbr\u003emoveToMin()\u003cbr\u003emoveToMax()\n    PATTERN --\u003e SETUPDEPTH  : setupDepth()\n    PATTERN --\u003e UNDEFINED   : disable()\n    SETUPDEPTH --\u003e PATTERN  : startPattern()\n    SETUPDEPTH --\u003e READY    : stopMotion()\u003cbr\u003emoveToMin()\u003cbr\u003emoveToMax()\n```\n* __UNDEFINED:__ The initial state prior to homing. Stepper / Servo are disabled and the position is undefined.\n* __READY:__ Homing defines the position inside the internal coordinate system. Machine is now ready to be used and accepts motion commands.\n* __PATTERN:__ The cyclic motion has started and the pattern generator is commanding a sequence of trapezoidal motions until stopped.\n* __SETUPDEPTH:__ The servo always follows the depth position. This can be used to setup the optimal stroke depth. \n\n## Usage\nStrokeEngine 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.\n### Initialize\nFirst 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.\n```cpp\n#include \u003cStrokeEngine.h\u003e\n\n// Pin Definitions\n#define SERVO_PULSE       4\n#define SERVO_DIR         16\n#define SERVO_ENABLE      17\n#define SERVO_ENDSTOP     25        // Optional: Only needed if you have a homing switch\n\n// Calculation Aid:\n#define STEP_PER_REV      2000      // How many steps per revolution of the motor (S1 off, S2 on, S3 on, S4 off)\n#define PULLEY_TEETH      20        // How many teeth has the pulley\n#define BELT_PITCH        2         // What is the timing belt pitch in mm\n#define MAX_RPM           3000.0    // Maximum RPM of motor\n#define STEP_PER_MM       STEP_PER_REV / (PULLEY_TEETH * BELT_PITCH)\n#define MAX_SPEED         (MAX_RPM / 60.0) * PULLEY_TEETH * BELT_PITCH\n\nstatic motorProperties servoMotor {\n  .maxSpeed = MAX_SPEED,              // Maximum speed the system can go in mm/s\n  .maxAcceleration = 10000,           // Maximum linear acceleration in mm/s²\n  .stepsPerMillimeter = STEP_PER_MM,  // Steps per millimeter \n  .invertDirection = true,            // One of many ways to change the direction,  \n                                      // should things move the wrong way\n  .enableActiveLow = true,            // Polarity of the enable signal      \n  .stepPin = SERVO_PULSE,             // Pin of the STEP signal\n  .directionPin = SERVO_DIR,          // Pin of the DIR signal\n  .enablePin = SERVO_ENABLE           // Pin of the enable signal\n};\n\nstatic machineGeometry strokingMachine = {\n  .physicalTravel = 160.0,            // Real physical travel from one hard endstop to the other\n  .keepoutBoundary = 5.0              // Safe distance the motion is constrained to avoiding crashes\n};\n\n// Configure Homing Procedure\nstatic endstopProperties endstop = {\n  .homeToBack = true,                 // Endstop sits at the rear of the machine\n  .activeLow = true,                  // switch is wired active low\n  .endstopPin = SERVO_ENDSTOP,        // Pin number\n  .pinMode = INPUT                    // pinmode INPUT with external pull-up resistor\n};\n\nStrokeEngine Stroker;\n```\nInside `void setup()` call the following functions to initialize the StrokeEngine:\n```cpp\nvoid setup() \n{\n  // Setup Stroke Engine\n  Stroker.begin(\u0026strokingMachine, \u0026servoMotor);\n  Stroker.enableAndHome(\u0026endstop);    // pointer to the homing config struct\n  \n  // other initialization code\n  \n  // wait for homing to complete\n  while (Stroker.getState() != READY) {\n    delay(100);\n  }\n}\n```\n\n#### Alternate Manual Homing Procedure __[Dangerous]__\nSome machines may not have a homing switch mounted. For these you may use a manual homing procedure instead of `Stroker.enableAndHome(\u0026endstop);`. Manually move back until the physical endstop and then call:\n```cpp\nStroker.thisIsHome();\n```\nThis enables the driver and sets the current position as `-keepoutBoundary`. It then slowly moves to 0. \n\n__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!__\n\n#### Retrieve Available Patterns as JSON-String\nThis 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.\n```cpp\nString getPatternJSON() {\n    String JSON = \"[{\\\"\";\n    for (size_t i = 0; i \u003c Stroker.getNumberOfPattern(); i++) {\n        JSON += String(Stroker.getPatternName(i));\n        JSON += \"\\\": \";\n        JSON += String(i, DEC);\n        if (i \u003c Stroker.getNumberOfPattern() - 1) {\n            JSON += \"},{\\\"\";\n        } else {\n            JSON += \"}]\";\n        }\n    }\n    Serial.println(JSON);\n    return JSON;\n}\n```\n\n### Running\n#### Start \u0026 Stop the Stroking Action\nUse `Stroker.startPattern();` and `Stroker.stopMotion();` to start and stop the motion. Stop is immediate and with the highest possible acceleration.\n\n#### Move to the Minimum or Maximum Position\nYou 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.\n\n#### Setup Optimal Depth Interactively\nIn 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. \n\n##### Fancy Mode\nTo 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.\n\n#### Change Parameters\nParameters 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:\n```cpp\nStroker.setSpeed(float speed, bool applyNow);          // Speed in Cycles (in \u0026 out) per minute, constrained from 0.5 to 6000\nStroker.setDepth(float depth, bool applyNow);          // Depth in mm, constrained to [0, _travel]\nStroker.setStroke(float stroke, bool applyNow);        // Stroke length in mm, constrained to [0, _travel]\nStroker.setSensation(float sensation, bool applyNow);  // Sensation (arbitrary value a pattern may use to alter its behavior), \n                                                       // constrained to [-100, 100] with 0 being neutral.\nStroker.setPattern(int index, bool applyNow);          // Pattern, index must be \u003c Stroker.getNumberOfPattern()\n```\nNormally 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`. \n\n#### Readout Parameters\nEach 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.\n\n```cpp\nfloat Stroker.getSpeed();          // Speed in Cycles (in \u0026 out) per minute, constrained from 0.5 to 6000\nfloat Stroker.getDepth();          // Depth in mm, constrained to [0, _travel]\nfloat Stroker.getStroke();         // Stroke length in mm, constrained to [0, _travel]\nfloat Stroker.getSensation();      // Sensation (arbitrary value a pattern may use to alter its behavior), \n                                   // constrained to [-100, 100] with 0 being neutral.\nint Stroker.getPattern();          // Pattern, index is [o, Stroker.getNumberOfPattern()[\n```\n\n### Advanced Functions\nConsult [StrokeEngine.h](./src/StrokeEngine.h) for further functions and a more detailed documentation of each function. Some functions are overloaded and may provide additional useful functionalities.\n#### Telemetry\nIt 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)`. \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftheelims%2Fstrokeengine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftheelims%2Fstrokeengine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftheelims%2Fstrokeengine/lists"}