Help: Controlling a stepper motor with an Arduino and RF remote - programming issues

Hello!

I am working on a science project, where I need to turn a motor periodically so as to expose different Petri dishes at specified times. Ideally, the motor turns on its own but sometimes (in case something interesting happens), I want to be able to press a button on my remote and the motor turns automatically, skipping the wait.

For this, I am using an Arduino Leonardo, a Polulu Tic T825 motor driver (which is based on a TI DRV8825 driver), a standard stepper motor (reference 17HS3430) and an RF 433 reciever. The T825 recieves a 12V input, which it sends to the motor, as well as to the Arduino on its 5V output pin. The two communicate using I2C on the SCL and SCA pins. The RF reciever is on the Leonardo's 3rd pin (default RF pin). I've tested both the RF receiver and the motor driver using example sketches and they work fine, but putting it all together is an issue...

My code is this:

#include <RCSwitch.h>
#include <Wire.h>
#include <Tic.h>

TicI2C tic;
RCSwitch mySwitch = RCSwitch();
const int rf_delay = 500;
const unsigned long debounceDelay = 500;
const unsigned long waitBeforeShutdown = 5000;

// Values gotten from testing the remote using an example code from the RCSwitch library, works fine.
const unsigned long BUTTON_START = 5338371;
const unsigned long BUTTON_SKIP = 5330236;
const unsigned long BUTTON_STOP = 5338380;

unsigned long lastPressTime = 0;

// Change the command timeout on the tic which seems to be causing issues.
void setTicCommandTimeout(uint16_t timeout_ms) {
  Wire.beginTransmission(14); // 14 is the I2C address of the Tic
  Wire.write(0x8C); // Command byte for setting command timeout
  Wire.write((byte)(timeout_ms >> 0)); // Lower byte of timeout_ms
  Wire.write((byte)(timeout_ms >> 8)); // Upper byte of timeout_ms
  Wire.endTransmission();
}

void setup() {
  Wire.begin();
  delay(20);
  tic.setProduct(TicProduct::T825);
  tic.setMaxAccel(100000);
  tic.energize();
  tic.exitSafeStart();
  tic.resetCommandTimeout();
  Serial.begin(9600);
  mySwitch.enableReceive(0);
  // Set the command timeout to 2000 ms (2 seconds)
  setTicCommandTimeout(2000);
}

// Provided functino from tic 
void resetCommandTimeout() {
  tic.resetCommandTimeout();
  tic.energize();
}

// This function is used whilst the motor is in motion and shouldn't be interupted, so no need to check for a button press
int delayduringmovement(unsigned long ms, unsigned long start) {
  while ((millis() - start) < ms) {
    tic.resetCommandTimeout();
    delay(10);
  }
  Serial.print("First delay complete");
  return 1;
}

// This function is used whilst the motor is in waiting and does need to check for a button press
int delayWhileResettingCommandTimeoutOrButtonPress(unsigned long ms, unsigned long start) {
  while ((millis() - start) < ms) {
    // I've printed the different waiting times to see where the program stops waiting 
    Serial.println(millis()-start);
    tic.resetCommandTimeout();
    delay(10);

    if (mySwitch.available()) {
      unsigned long receivedValue = mySwitch.getReceivedValue();
      if (receivedValue == BUTTON_SKIP) {
        Serial.println("Button pressed! Skipping delay.");
        mySwitch.resetAvailable();
        return 1;
      }
      mySwitch.resetAvailable(); // Ensure any received value is cleared
      continue;
    }
  }
  Serial.println("Exiting delay function without button press");
  return 1;
}

// Main function which controls motor movement and waiting
void movement(unsigned long time_durations[], size_t size) {
  for (size_t i = 0; i < size; i++) {
    time_durations[i] *= 1000; // Convert to milliseconds
  }
  mySwitch.resetAvailable();
  unsigned long time_diff[size - 1];

  // Create a vector of time differences -> this will be how long I actually need to wait between movements
  for (size_t i = 1; i < size; ++i) {
    time_diff[i - 1] = time_durations[i] - time_durations[i - 1];
  }

  Serial.print("Movement function start");
  for (size_t i = 0; i < size - 1; ++i) {
    tic.resetCommandTimeout();
    tic.exitSafeStart();
    // Begin movement
    tic.setTargetVelocity(2000000);
    unsigned long start = millis();
    // Keep turning for 2 seconds.
    int returned = delayduringmovement(2000, start);

    tic.haltAndHold();  // Stop the motor

    // Now wait for the specified amount of time / if the skip button is pressed
    unsigned long start1 = millis();
    int returned1=delayWhileResettingCommandTimeoutOrButtonPress(time_diff[i], start1);
    Serial.print("Completed iteration ");
    Serial.println(i + 1);
  }
}

// Function to add button debouncing delay to prevent signal spam
bool isButtonPressed(unsigned long buttonValue) {
  if (mySwitch.getReceivedValue() == buttonValue) {
    unsigned long currentTime = millis();
    if (currentTime - lastPressTime > debounceDelay) {
      lastPressTime = currentTime;
      mySwitch.resetAvailable();
      return true;
    }
  }
  return false;
}

// Turn off the Arduino loop
void waitForShutdown() {
  unsigned long start = millis();
  Serial.println("Waiting before shutting down the program.");
  
  while (millis() - start < waitBeforeShutdown) {
    if (mySwitch.available()) {
      unsigned long value = mySwitch.getReceivedValue();
      if (value == BUTTON_START || value == BUTTON_SKIP) {
        Serial.println("Button pressed, resuming program.");
        mySwitch.resetAvailable();
        return;
      }
      delay(debounceDelay);
    }
  }
  Serial.println("Shutting down the program.");
}

void loop() {
  // Specify when you want the motor to turn ( At t=5s, t=10s, ...)
  unsigned long time_durations[] = {5, 10, 15, 20};
  size_t size = sizeof(time_durations) / sizeof(time_durations[0]);

  if (mySwitch.available()) {
    if (isButtonPressed(BUTTON_START)) {
      Serial.println("Turning on the program");
      delay(rf_delay);
      // Send the time durations to the movement function to actually start motor movement
      movement(time_durations, size);

    } else if (isButtonPressed(BUTTON_STOP)) {
      Serial.println("Turning off the program.");
      delay(rf_delay);
      waitForShutdown();
    }
  } else Serial.print("Waiting ");
}

The loop works fine and waits until the button press. However, the Arduino seems to be having issues looping through the different waiting periods. Printing the time shows it sort of stops randomly in the delayWhileResettingCommandTimeoutOrButtonPress() function, sometimes going through several iterations then crashing, sometimes reaching about 400 (out of 5000) on the first iteration and just dying out.

I've tried monitoring the Polulu using usb and their software whilst the program was running, and it seems to show the motor becomes denergized and stops, so maybe the driver is causing the whole thing to crash? It should become energized though, as the resetcommandtimeout() function does this. Fiddling around with command timeout durations doesn't change much either. I also thought maybe the Leonardo was running out dynamic memory, but reducing Serial prints does nothing, and the IDE tells me I'm only using around 30% of dynamic or static memory.

If you have any ideas or pointers as to what could be going wrong I'd be infinitely grateful. Thank you!

Is your goal to turn a motor at a regular interval and only pause the rotation on a button press? Does the rotation then continue from time-zero when the button is released, OR, continue with the countdown from when the button was pressed, OR, catch up the whole rotation for the time lost during the button press?

Make the "rotate with button pause" work before adding the RF433 or I2C comm devices.

This sounds like an overflow (in the positive or negative direction)... something builds up and then causes a crash.

You never use "returned1"... is it used in a library call? Why do you preserve the return value? Inside the "verylongfunctionname()" you also have two "return 1" calls... so even if "returned1" is used, how would one "return 1" be recognized from the other "return 1" or the "int" return?

I think "return1" can be "void"

very had for me to follow your code with so many while loops, presumably expected to run for limited amounts of time

why not monitor for events that elicit some action and periodically or aperiodically depending on events update motor operation in order to make the code more understandable.

To answer your questions:
1.

The goal is for Petri dishes to be exposed at set time intervals (e.g. 0-10 minutes, 10-20...), the motor rotates to expose them at different times (so here at the 10 minute mark). The button press is meant to do something like this:

Wanted movement times = [ 10 mins, 20 mins, 30 mins, 40 mins, 50 mins]
After button press at 23 mins = [ 10 mins, 20 mins, 23 mins, 33 mins, 43 mins]

I already made the I2C work and implemented the button press using something like:

const int buttonPin = 2;
if (digitalRead(buttonPin) == LOW) // Button is pressed when the pin reads LOW
    {
      Serial.println("Button pressed! Skipping delay.");
      break; // Return true to indicate button was pressed
    }

The overall code work fine at that point, but being able to do things from a distance (and not press a button on a breadboard) was an imperative, so I turned to radio. It's the RF433 that is causing the issues, I don't know if the while loop (which @gcjr is right in saying they are confusing, but I think they work better than a timer? Might be wrong though)) is causing the signal to be reset.

Returning something from the verylongfunction() was my way of trying to force an output after a button press, as using just return; or break; didn't seem to be doing anything. That's an easy fix, but doesn't change my overflow issue. Is there any way of easily monitoring that? I haven't seen any obvious posts on the forum or on StackOverflow.

I see the symptoms you describe as similar to "overflow"... but do not be focused on the word. I also see timeout or bad connections of transmit/receive as possible issues.

I think you should follow @gcjr suggestion to monitor events. Make sure your code enters and exits functions cleanly, with valid data.

Verify the use of "start"... used as local and passed to other functions remains intact and not initialized (or is initialized rather than remaining intact).

I've done this and it now works. Thank you to the both of you for your help! How does this forum work for closing discussions? Do I post my new code for future users? Or just accept this as a solution and close it ?

Click the "Solution" box in the Post most relevant to the solution.