Stepper Motor + Export + Sync
This guide builds one complete example with two Step / Dir stepper motors. It uses the existing stepper auto-sync callbacks together with custom exported animation playback logic.
In this example:
- Stepper A moves toward its home switch and becomes homed as soon as the switch is active.
- Stepper B moves toward its home switch, then backs away from it by a fixed number of steps before becoming homed.
- After both steppers are homed, exported animation index 0 starts and loops.
This is an example to adapt to your own hardware, not a universal stepper setup. First make sure each stepper can auto-sync correctly while connected to the Bottango desktop app. The Stepper Motor Sync guide explains the base callbacks, and Exported Playback Logic explains the playback methods used here.
This is not a “one size fits all” set of code you can copy and paste without adapting. Take the below as examples and lessons you will need to adapt based on your specific requirements. For instance, this example will home both steppers simultaneously. If you wanted to wait for one stepper to finish before starting the next, that change would be in your hands to build upon this example.
When You Export Animations
Section titled “When You Export Animations”The code in this guide starts animation 0 itself. Disable all built-in exported playback triggers before exporting:
- Do not select an animation to play on start.
- Do not select an idle animation.
- Do not configure an animation to play from a pin input.
You will provide your own animation triggering logic. If an exported animation has one of those triggers enabled, it can start before the steppers finish homing. An unhomed stepper will not drive in that case, but the animation will still try to play.
A Tour of the Callback File
Section titled “A Tour of the Callback File”The rest of this guide walks from top to bottom through the parts of the completed callbacks file that make this example work.
The Example Fields
Section titled “The Example Fields”The example begins at the top of BottangoArduinoCallbacks.cpp, inside namespace Callbacks.
The identifiers for Step / Dir steppers are their Step Pin values written as strings. In this example, stepper A uses Step Pin 6 and stepper B uses Step Pin 7. Each stepper has its own:
- Identifier
- Auto Sync Direction
- Home switch pin
- Home switch active level
- Post-home step amount (optional, only used in example Stepper B)
- State tracking bool (registered and homed)
The example also tracks animation playback state for its own triggering logic.
#include "BottangoArduinoCallbacks.h"#include "src/AbstractEffector.h"#include "src/Outgoing.h"#include "src/BottangoCore.h"
namespace Callbacks{ // !! FIELDS !! // // The below fields are examples. You will need to adjust their values, add / remove fields, // and potentially change how they are used depending on the requirements of your exact setup // // Step / Dir stepper identifiers are their STEP pins, expressed as strings. // These values must match the Step Pin values assigned in Bottango as a string. const char STEPPER_A_IDENTIFIER[] = "6"; const char STEPPER_B_IDENTIFIER[] = "7";
// Auto sync direction: 1 is clockwise and -1 is counter-clockwise. // These directions must move each stepper toward its own home switch. const int STEPPER_A_AUTO_SYNC_DIRECTION = -1; const int STEPPER_B_AUTO_SYNC_DIRECTION = 1;
// Each stepper in this example has its own active-low home switch. Set the pins and // active level below to match the hardware wiring. const byte STEPPER_A_HOME_PIN = 10; const byte STEPPER_B_HOME_PIN = 11;#define STEPPER_A_ACTIVE_HOME LOW#define STEPPER_B_ACTIVE_HOME LOW
// Stepper B demonstrates the optional post-home move. After it reaches // its switch, it moves this many steps in the opposite direction before // Bottango considers it homed. Stepper A homes immediately at its switch. const int STEPPER_B_POST_HOME_STEPS = 150;
// These fields track the state of each stepper. // We track registered and homed status for both. bool stepperAIsRegistered = false; bool stepperAIsHomed = false; bool stepperBIsRegistered = false; bool stepperBIsHomed = false;
// Simple animation playback state tracking. We start // animation 0 after everything is homed. bool animation0HasStarted = false;
// Simple convenience method to check an identifier against an effector. bool isExpectedStepper(AbstractEffector *effector, const char *identifier) { char effectorIdentifier[9]; effector->getIdentifier(effectorIdentifier, 9); return strcmp(effectorIdentifier, identifier) == 0; }
// rest of file continues...The fields give the rest of the file names for the two steppers, their switches, and the small amount of state this example tracks. Change all of these values for your hardware. In particular, verify that each auto-sync direction moves toward its own home switch. The active home values can be LOW or HIGH, depending on your switch wiring.
The flags are intentionally simple. They let this example wait for two registered, homed steppers and make sure it starts animation 0 only once. You could build your own state machine for more abstract tracking. Use the concepts here as a guide rather than an exact recipe to follow.
Controller Lifecycle Callbacks
Section titled “Controller Lifecycle Callbacks”The first callback used by this example is onThisControllerStarted. It configures each home switch input once, then clears the tracking fields before the exported setup registers the steppers.
void onThisControllerStarted(){ // This example expects the same active-low limit-switch wiring shown // in the stepper sync documentation. We set up each sensor pin here. pinMode(STEPPER_A_HOME_PIN, INPUT); pinMode(STEPPER_B_HOME_PIN, INPUT);
// A controller start begins a fresh registration and homing sequence // so we clear all state tracking bools. stepperAIsRegistered = false; stepperBIsRegistered = false; stepperAIsHomed = false; stepperBIsHomed = false; animation0HasStarted = false;}onThisControllerStopped is the matching cleanup point. It clears the same fields so a later controller start repeats the registration and homing sequence.
void onThisControllerStopped(){ // Clear the local tracking flags stepperAIsRegistered = false; stepperBIsRegistered = false; stepperAIsHomed = false; stepperBIsHomed = false; animation0HasStarted = false;}onEarlyLoop: The Playback Gate
Section titled “onEarlyLoop: The Playback Gate”onEarlyLoop appears next in the callbacks file. At this point in the source, the code only describes the condition that must be true before exported playback begins: both expected steppers must have registered, both must be homed, and animation 0 must not already have started.
void onEarlyLoop(){
// We put our animation triggering logic here.#if defined(USE_CODE_COMMAND_STREAM) || defined(USE_SD_CARD_COMMAND_STREAM) // Do not auto-sync or start an exported animation while connected to // the desktop application. This example starts playback only in offline mode // after all steppers are done homing. if (!BottangoCore::isOffline()) { return; }
// Wait for both expected steppers to register and finish their own // homing paths before beginning animation 0. The flag prevents this // command from being sent again every loop. if (stepperAIsRegistered && stepperBIsRegistered && stepperAIsHomed && stepperBIsHomed && !animation0HasStarted && BottangoCore::commandStreamProvider != nullptr) { // Start animation 0 with true for looping BottangoCore::commandStreamProvider->startCommandStream(0, true);
// Track that we started animation 0. animation0HasStarted = true;
// If you need different animation triggering and playback logic, // replace this code with your own. }#endif}onEffectorRegistered: The Auto-Sync Start
Section titled “onEffectorRegistered: The Auto-Sync Start”onEffectorRegistered runs when an effector becomes live. That includes startup before playing exported animations, as well as normal desktop-app control.
This example starts automatic homing only in offline mode. It identifies the newly registered effector, records that the expected stepper has registered, then starts auto sync in the configured direction. The passed-in effector is the one that just registered, so this code uses it directly.
void onEffectorRegistered(AbstractEffector *effector){ // Registration also happens when the desktop app controls the driver. // Only start automatic homing for exported/offline playback. if (!BottangoCore::isOffline()) { return; }
// Was the effector that was registered stepper A? if (isExpectedStepper(effector, STEPPER_A_IDENTIFIER)) { // Track that stepper A is registered but not homed. stepperAIsRegistered = true; stepperAIsHomed = false;
// Start auto sync on stepper A in its auto sync direction. effector->setAutoSync(STEPPER_A_AUTO_SYNC_DIRECTION); } // Was the effector that was registered stepper B? else if (isExpectedStepper(effector, STEPPER_B_IDENTIFIER)) { // Track that stepper B is registered but not homed. stepperBIsRegistered = true; stepperBIsHomed = false;
// Start auto sync on stepper B in its auto sync direction. effector->setAutoSync(STEPPER_B_AUTO_SYNC_DIRECTION); }}onEffectorDeregistered: Reset a Removed Stepper
Section titled “onEffectorDeregistered: Reset a Removed Stepper”The next participating method clears this example’s local knowledge when an expected stepper deregisters.
void onEffectorDeregistered(AbstractEffector *effector){ // If an expected stepper is removed, it can no longer be treated as // ready for exported playback. if (isExpectedStepper(effector, STEPPER_A_IDENTIFIER)) { stepperAIsRegistered = false; stepperAIsHomed = false; } else if (isExpectedStepper(effector, STEPPER_B_IDENTIFIER)) { stepperBIsRegistered = false; stepperBIsHomed = false; }}isEffectorAutoHomeComplete: The Home-Switch Logic
Section titled “isEffectorAutoHomeComplete: The Home-Switch Logic”While a stepper is auto-syncing, Bottango repeatedly calls isEffectorAutoHomeComplete. Return true when the relevant home switch is active.
The direction checks are optional, but useful in this example. If the configured direction is wrong, the stepper may be moving away from its switch instead of towards it. The code logs an error and stops the firmware instead of continuing to move.
Stepper A becomes homed as soon as its switch is active. Stepper B instead requests a post-home move in the opposite direction. The distinction between those two branches is the central idea in this example: do not set stepper B’s homed flag until that secondary move is complete.
bool isEffectorAutoHomeComplete(AbstractEffector *effector, int &postAutoSyncMove, int autoSyncDirection){ // The driver calls this while an auto-syncing stepper is moving. Return // true when the matching switch is reached.
// check if the stepper we're checking is Stepper A if (isExpectedStepper(effector, STEPPER_A_IDENTIFIER)) { // A direction mismatch means the stepper may be travelling // away from its switch, so stop safely. if (autoSyncDirection != STEPPER_A_AUTO_SYNC_DIRECTION) { Outgoing::printOutputStringFlash(F("ERROR: Stepper A sync wrong direction")); Outgoing::printLine(); BottangoCore::stop(true); return false; }
// check if the sensor for stepper A is active if (digitalRead(STEPPER_A_HOME_PIN) == STEPPER_A_ACTIVE_HOME) { // Stepper A has no post-home move, so it is fully homed as // soon as returning true completes this auto-sync operation. // Track it for our own state tracking, and tell Bottango it is homed. stepperAIsHomed = true; return true; } } // check if the stepper we're checking is Stepper B else if (isExpectedStepper(effector, STEPPER_B_IDENTIFIER)) { // A direction mismatch means the stepper may be travelling // away from its switch, so stop safely. if (autoSyncDirection != STEPPER_B_AUTO_SYNC_DIRECTION) { Outgoing::printOutputStringFlash(F("ERROR: Stepper B sync wrong direction")); Outgoing::printLine(); BottangoCore::stop(true); return false; }
// check if the sensor for stepper B is active if (digitalRead(STEPPER_B_HOME_PIN) == STEPPER_B_ACTIVE_HOME) { // Stepper B demonstrates post-auto-home secondary sync. It // reaches the switch, then backs away in the opposite direction by a specific number of steps. // Its homed flag is set in the completion callback below. postAutoSyncMove = STEPPER_B_POST_HOME_STEPS * autoSyncDirection * -1; return true; } }
return false;}This callback also works if you manually start auto sync from the desktop app. Only the automatic start in onEffectorRegistered and the exported-animation start in onEarlyLoop are limited to offline mode.
Stepper B’s Secondary Completion Callback
Section titled “Stepper B’s Secondary Completion Callback”Stepper B reached its switch in the previous callback, but it is not ready until it finishes moving back from the switch. onEffectorPostAutoHomeSecondarySyncComplete runs at that point.
void onEffectorPostAutoHomeSecondarySyncComplete(AbstractEffector *effector){ // This callback only applies to an auto sync with a nonzero // postAutoSyncMove. Stepper B is now fully homed after backing away // from its home switch. if (isExpectedStepper(effector, STEPPER_B_IDENTIFIER)) { stepperBIsHomed = true; }}Stepper A does not use this callback because it has no post-home move.
onEffectorHomeReset: Home loss callback
Section titled “onEffectorHomeReset: Home loss callback”onEffectorHomeReset is the final participating callback in the file. It clears a local homed flag if Bottango resets that stepper’s home state.
void onEffectorHomeReset(AbstractEffector *effector){ // If for any reason Bottango flags a stepper as losing its home state, // we will track that here too. if (isExpectedStepper(effector, STEPPER_A_IDENTIFIER)) { stepperAIsHomed = false; } else if (isExpectedStepper(effector, STEPPER_B_IDENTIFIER)) { stepperBIsHomed = false; }}Complete Callback File
Section titled “Complete Callback File”The complete version we walked through above is given below, with the default callback methods that this example does not use left in place. You can copy and paste it into your callbacks file, then explore and edit it to learn from and modify the example for your specific needs.
#include "BottangoArduinoCallbacks.h"#include "src/AbstractEffector.h"#include "src/Outgoing.h"#include "src/BottangoCore.h"
namespace Callbacks{ // !! BIG IDEA !! // // This is a complete example for an exported animation setup with two // Step / Dir steppers. It intentionally uses a few plainly named fields so // it is easy to adapt. // // The code does the following: // 1. When each expected stepper registers in offline mode, we start its auto sync in the right direction. // 2. Stepper A finishes as soon as its home switch is reached. // 3. Stepper B reaches its switch, then backs away before it is fully homed. // 4. Once both flags say the steppers are homed, start exported animation index 0 looping. // // This builds heavily on the example for auto sync given in the Bottango docs: // https://docs.bottango.com/bottango-firmware/callbacks-file/stepper-motor-sync/ // Make sure you have gotten auto sync working as needed with a connection to a computer // first before attempting to make it work in offline mode. // // Exported animation playback logic documentation can be found here: // https://docs.bottango.com/bottango-firmware/callbacks-file/exported-playback-logic/ // // !!!!! IMPORTANT NOTE !!!!!! Because we are providing our own animation playback triggering logic, // ensure that all exported animations have no built-in trigger when exported // and that no animation is selected to "play on start." We own the playback logic ourselves // in these callbacks, rather than relying on the built-in convenience options you can select when exporting. // // !! FIELDS !! // // The below fields are examples. You will need to adjust their values, add / remove fields, // and potentially change how they are used depending on the requirements of your exact setup // // Step / Dir stepper identifiers are their STEP pins, expressed as strings. // These values must match the Step Pin values assigned in Bottango as a string. const char STEPPER_A_IDENTIFIER[] = "6"; const char STEPPER_B_IDENTIFIER[] = "7";
// Auto sync direction: 1 is clockwise and -1 is counter-clockwise. // These directions must move each stepper toward its own home switch. const int STEPPER_A_AUTO_SYNC_DIRECTION = -1; const int STEPPER_B_AUTO_SYNC_DIRECTION = 1;
// Each stepper in this example has its own active-low home switch. Set the pins and // active level below to match the hardware wiring. const byte STEPPER_A_HOME_PIN = 10; const byte STEPPER_B_HOME_PIN = 11;#define STEPPER_A_ACTIVE_HOME LOW#define STEPPER_B_ACTIVE_HOME LOW
// Stepper B demonstrates the optional post-home move. After it reaches // its switch, it moves this many steps in the opposite direction before // Bottango considers it homed. Stepper A homes immediately at its switch. const int STEPPER_B_POST_HOME_STEPS = 150;
// These fields track the state of each stepper. // We track registered and homed status for both. bool stepperAIsRegistered = false; bool stepperAIsHomed = false; bool stepperBIsRegistered = false; bool stepperBIsHomed = false;
// Simple animation playback state tracking. We start // animation 0 after everything is homed. bool animation0HasStarted = false;
// Simple convenience method to check an identifier against an effector. bool isExpectedStepper(AbstractEffector *effector, const char *identifier) { char effectorIdentifier[9]; effector->getIdentifier(effectorIdentifier, 9); return strcmp(effectorIdentifier, identifier) == 0; }
// !!!!!!!!!!!!!!! // // !! CONTROLLER LIFECYCLE CALLBACKS !! // // !!!!!!!!!!!!!!! //
// called AFTER a successful handshake with the Bottango application, signifying that this controller has started. // use for general case startup process // Effector registration will happen after this callback, in their own callback. // If you have effector registration specific needs, you should use onEffectorRegistered void onThisControllerStarted() { // This example expects the same active-low limit-switch wiring shown // in the stepper sync documentation. We set up each sensor pin here. pinMode(STEPPER_A_HOME_PIN, INPUT); pinMode(STEPPER_B_HOME_PIN, INPUT);
// A controller start begins a fresh registration and homing sequence // so we clear all state tracking bools. stepperAIsRegistered = false; stepperBIsRegistered = false; stepperAIsHomed = false; stepperBIsHomed = false; animation0HasStarted = false; }
// called after the controller recieves a stop command. The controller will stop all movement, deregister all effectors // After which this call back is triggered. void onThisControllerStopped() { // Clear the local tracking flags stepperAIsRegistered = false; stepperBIsRegistered = false; stepperAIsHomed = false; stepperBIsHomed = false; animation0HasStarted = false; }
// called each loop cycle. If you have timing based code you'd like to utilize outside of the Bottango animation // This callback occurs BEFORE all effectors process their movement, at the end of the loop. void onEarlyLoop() {
// We put our animation triggering logic here.#if defined(USE_CODE_COMMAND_STREAM) || defined(USE_SD_CARD_COMMAND_STREAM) // Do not auto-sync or start an exported animation while connected to // the desktop application. This example starts playback only in offline mode // after all steppers are done homing. if (!BottangoCore::isOffline()) { return; }
// Wait for both expected steppers to register and finish their own // homing paths before beginning animation 0. The flag prevents this // command from being sent again every loop. if (stepperAIsRegistered && stepperBIsRegistered && stepperAIsHomed && stepperBIsHomed && !animation0HasStarted && BottangoCore::commandStreamProvider != nullptr) { // Start animation 0 with true for looping BottangoCore::commandStreamProvider->startCommandStream(0, true);
// Track that we started animation 0. animation0HasStarted = true;
// If you need different animation triggering and playback logic, // replace this code with your own. }#endif }
// called each loop cycle. // This callback occurs AFTER all effectors process their movement, at the end of the loop. void onLateLoop() {
// EX: Request stop on driver, and disconnect all active connections // Outgoing::outgoing_requestShutdown();
// EX: Pause Playing in App // Outgoing::outgoing_requestStopPlay();
// EX: Start Playing in App (in current animation and time) // Outgoing::outgoing_requestStartPlay();
// EX: Start Playing in App (with animation index, and start time in milliseconds) // Outgoing::outgoing_requestStartPlay(1,1000); }
// !!!!!!!!!!!!!!! // // !! EFFECTOR CALLBACKS !! // // !!!!!!!!!!!!!!! //
// All effectors have an identifier. It is an 8 char or less string. Check Bottango to see the identifier for a given effector in app. // for most effectors, it is the first pin in their set of pins // i2c effectors have the i2c address before the first pin // you query for an effector with a c string char array, instanitated at 9 characters (8 for the identifier, and a null terminating char)
// The below are called by built in effectors at various stages in their lifecycle
// called by an effector when registered, after registration is complete void onEffectorRegistered(AbstractEffector *effector) { // Registration also happens when the desktop app controls the driver. // Only start automatic homing for exported/offline playback. if (!BottangoCore::isOffline()) { return; }
// Was the effector that was registered stepper A? if (isExpectedStepper(effector, STEPPER_A_IDENTIFIER)) { // Track that stepper A is registered but not homed. stepperAIsRegistered = true; stepperAIsHomed = false;
// Start auto sync on stepper A in its auto sync direction. effector->setAutoSync(STEPPER_A_AUTO_SYNC_DIRECTION); } // Was the effector that was registered stepper B? else if (isExpectedStepper(effector, STEPPER_B_IDENTIFIER)) { // Track that stepper B is registered but not homed. stepperBIsRegistered = true; stepperBIsHomed = false;
// Start auto sync on stepper B in its auto sync direction. effector->setAutoSync(STEPPER_B_AUTO_SYNC_DIRECTION); } }
// called by an effector when deregistered, before deregistration is complete void onEffectorDeregistered(AbstractEffector *effector) { // If an expected stepper is removed, it can no longer be treated as // ready for exported playback. if (isExpectedStepper(effector, STEPPER_A_IDENTIFIER)) { stepperAIsRegistered = false; stepperAIsHomed = false; } else if (isExpectedStepper(effector, STEPPER_B_IDENTIFIER)) { stepperBIsRegistered = false; stepperBIsHomed = false; } }
// called by effectors each loop with its current signal (example: servo PWM or stepper steps from home ) // didChange is true if different from last update called void effectorSignalOnLoop(AbstractEffector *effector, int signal, bool didChange) { // example, set built in led for effector with identifier "1" based on if signal is greater than 1500
// char effectorIdentifier[9]; // effector->getIdentifier(effectorIdentifier, 9);
// if (strcmp(effectorIdentifier, "1") == 0) // { // pinMode(LED_BUILTIN, OUTPUT); // if (signal > 1500) // { // digitalWrite(LED_BUILTIN, HIGH); // } // else // { // digitalWrite(LED_BUILTIN, LOW); // } // }
// another example, drive a custom motor, which you have coded to have a setSignal function // if (strcmp(effectorIdentifier, "myMotor") == 0) // { // myMotor->setSignal(signal); // } }
// !!!!!!!!!!!!!!!!!!! // // !! CUSTOM EVENTS !! // // !!!!!!!!!!!!!!!!!!! // // The below are called by custom events so you can provide your own behaviours
// called by a curved custom event any time the movement value is changed during a curved movement. (Movement is a normalized float between 0.0 - 1.0) void onCurvedCustomEventMovementChanged(AbstractEffector *effector, float newMovement) { // example, fade an led based on the new movement value // char effectorIdentifier[9]; // effector->getIdentifier(effectorIdentifier, 9);
// if (strcmp(effectorIdentifier, "myLight") == 0) // { // pinMode(5, OUTPUT); // int brightness = 255 * newMovement; // analogWrite(5, brightness); // } }
// called by a on off custom event any time the on off value is changed. void onOnOffCustomEventOnOffChanged(AbstractEffector *effector, bool on) { // example, turn on built in led based on the on off value // char effectorIdentifier[9]; // effector->getIdentifier(effectorIdentifier, 9);
// if (strcmp(effectorIdentifier, "myLight") == 0) // { // pinMode(LED_BUILTIN, OUTPUT); // digitalWrite(LED_BUILTIN, on ? HIGH : LOW); // } }
// called by a trigger custom event any time the on event is triggered. void onTriggerCustomEventTriggered(AbstractEffector *effector) { // example, set led to a random brightness each trigger // char effectorIdentifier[9]; // effector->getIdentifier(effectorIdentifier, 9);
// if (strcmp(effectorIdentifier, "myLight") == 0) // { // pinMode(5, OUTPUT); // int brightness = random(0, 256); // analogWrite(5, brightness); // } }
void onColorCustomEventColorChanged(AbstractEffector *effector, byte newRed, byte newGreen, byte newBlue) { // example, set rgb LED on pins 3, 5, and 6 to given red, green, and blue colors (represented as a byte between 0 and 255) // char effectorIdentifier[9]; // effector->getIdentifier(effectorIdentifier, 9);
// if (strcmp(effectorIdentifier, "myRGB") == 0) // { // pinMode(3, OUTPUT); // pinMode(5, OUTPUT); // pinMode(6, OUTPUT);
// analogWrite(3, newRed); // analogWrite(5, newGreen); // analogWrite(6, newBlue); // }
// code free support for addressable LED's (neopixel, etc. coming soon) // in the meanwhile, get support in the Bottango discord channel for "how to" info }
bool isEffectorAutoHomeComplete(AbstractEffector *effector, int &postAutoSyncMove, int autoSyncDirection) { // The driver calls this while an auto-syncing stepper is moving. Return // true when the matching switch is reached.
// check if the stepper we're checking is Stepper A if (isExpectedStepper(effector, STEPPER_A_IDENTIFIER)) { // A direction mismatch means the stepper may be travelling // away from its switch, so stop safely. if (autoSyncDirection != STEPPER_A_AUTO_SYNC_DIRECTION) { Outgoing::printOutputStringFlash(F("ERROR: Stepper A sync wrong direction")); Outgoing::printLine(); BottangoCore::stop(true); return false; }
// check if the sensor for stepper A is active if (digitalRead(STEPPER_A_HOME_PIN) == STEPPER_A_ACTIVE_HOME) { // Stepper A has no post-home move, so it is fully homed as // soon as returning true completes this auto-sync operation. // Track it for our own state tracking, and tell Bottango it is homed. stepperAIsHomed = true; return true; } } // check if the stepper we're checking is Stepper B else if (isExpectedStepper(effector, STEPPER_B_IDENTIFIER)) { // A direction mismatch means the stepper may be travelling // away from its switch, so stop safely. if (autoSyncDirection != STEPPER_B_AUTO_SYNC_DIRECTION) { Outgoing::printOutputStringFlash(F("ERROR: Stepper B sync wrong direction")); Outgoing::printLine(); BottangoCore::stop(true); return false; }
// check if the sensor for stepper B is active if (digitalRead(STEPPER_B_HOME_PIN) == STEPPER_B_ACTIVE_HOME) { // Stepper B demonstrates post-auto-home secondary sync. It // reaches the switch, then backs away in the opposite direction by a specific number of steps. // Its homed flag is set in the completion callback below. postAutoSyncMove = STEPPER_B_POST_HOME_STEPS * autoSyncDirection * -1; return true; } }
return false; }
void onEffectorPostAutoHomeSecondarySyncComplete(AbstractEffector *effector) { // This callback only applies to an auto sync with a nonzero // postAutoSyncMove. Stepper B is now fully homed after backing away // from its home switch. if (isExpectedStepper(effector, STEPPER_B_IDENTIFIER)) { stepperBIsHomed = true; } }
void onEffectorHomeReset(AbstractEffector *effector) { // If for any reason Bottango flags a stepper as losing its home state, // we will track that here too. if (isExpectedStepper(effector, STEPPER_A_IDENTIFIER)) { stepperAIsHomed = false; } else if (isExpectedStepper(effector, STEPPER_B_IDENTIFIER)) { stepperBIsHomed = false; } }} // namespace CallbacksMarking a Stepper as Homed Without Auto Sync
Section titled “Marking a Stepper as Homed Without Auto Sync”Auto sync is not the only way to establish a stepper’s home state. Advanced callback implementations can issue an optional fixed manual sync move with setSync(), then call setHome() on an AbstractEffector once their own logic determines that move is complete.
This is useful when your hardware has a repeatable mechanical reference without a limit switch, or when homing depends on logic outside Bottango’s normal auto-sync callback.
The important distinction is that setSync() only requests the relative move. It does not mark the stepper as homed when that move finishes. setHome() performs that separate state change.
If you use this approach, your callback code must maintain its own state to know when the requested movement has completed—for example, by tracking the expected number of step changes for that specific effector in effectorSignalOnLoop.
Once your own completion condition is met, call setHome() on the effector. If your exported playback logic also uses local stepperIsHomed flags like this guide’s example, update the matching local flag at the same time. setHome() changes Bottango’s internal homed state, but it does not invoke the auto-sync completion callbacks.