Trying a "State Machine" for Servo Control

I also took a look at your code again. I think there's something wrong in your header detection.

I don't think you want the for loops in your header detection clauses. Right now, It looks for the '~' and if it doesn't see it, it breaks the for loop. Which means its falling through. Same thing for the '!'.

So, I think the flow you want is:
Wait for data to be present (while (!SerialBT.available());
Test if it's a '~' and restart the while loop if its not because its not the start of a data packet. So, if (SerialBT != '~') continue;
Otherwise, it was a ! so test for the '!' next the same way you did for the ~. If its not the ! its not the start of a packet so you need continue to restart the while loop.
If both tests pass, loop through the remaining data for your channelvalues. In the case of 1 servo, its only 1 iteration.

note my code might not be exactly correct, I did it in the browser, not an ide so don't just copy and paste.

Many thanks, Mike, for the help!

I have the same behavior with only one character, or 3, or none. And I adjust the sketch and Vixen accordingly.

As I had seen in other posts with the talking pumpkins, these hobbyists had no header. Adding the header maintained the channel order for me, otherwise they would shift around every time you restarted Vixen.

So I'll try an different header style tomorrow and report back.

Take care, Joe.
 
Hi Mike and Everyone,

So I did a 4 character header, but my same inefficient header coding:

Code:
#include <ESP32Servo.h>
#include "BluetoothSerial.h"


#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
#endif*

BluetoothSerial SerialBT;
Servo  myservo1;



void setup() {


  Serial.begin(115200);
  SerialBT.begin("ESP32BTServo");

 
  myservo1.attach(2);


  myservo1.write(95);
 
}


void loop() {
 while (!SerialBT.available());

 
  for ( ; ; )
  {
    if (SerialBT.read() == '!') {
      break;
    }
  }
 while (!SerialBT.available());
  for ( ; ; )
  {
    if (SerialBT.read() == '"') {
      break;
    }
  }

  while (!SerialBT.available());

 
  for ( ; ; )
  {
    if (SerialBT.read() == '#') {
      break;
    }
  }
 while (!SerialBT.available());

 
  for ( ; ; )
  {
    if (SerialBT.read() == '$') {
      break;
    }
  }


  byte channelValues;

  {
    while (!SerialBT.available());
    channelValues = SerialBT.read();
 

     
        if (channelValues <= 0 && channelValues >= 255) {  
          channelValues = 0;}
         
  }


 

  myservo1.write(map(channelValues, 0, 255, 82, 130)); //Moose 82-130 


}





Mike Krebs suggested the following in the Vixen general chat awhile back:

Here is an example "start code"

In the definition section
// Generic Serial controller config - must be present, must match controller setup
const int CONTROLLER_HEADER[3] = {33, 34, 35}; // This is character !"# (hard to replicate in sequencer)
const int SIZE_OF_HEADER = sizeof(CONTROLLER_HEADER) / sizeof(int); // no need to change

in loop add this before reading the serial data bytes
waitForControllerHeader(CONTROLLER_HEADER);

// this this will read the incoming bytes of data looking for the three character start bytes
void waitForControllerHeader(int header[])
{
for (int i = 0; i < SIZE_OF_HEADER; i++) {
// wait for serial available
while (!Serial.available()) {}
// Check the byte until it matches the CONTROLLER_HEADER byte
int inByte = Serial.read();
if (inByte != CONTROLLER_HEADER) {
i = -1; // wrong data set to "zero"
}
}
// found the header!
}

In Vixen, put !"# in the start characters in the controller setup.

Try that and report success or failure.


But I must be doing something very wrong when I tried it in my sketch.

So. what I posted above with the "!"#$" header worked rather well for several short runs (with a couple insignificant hiccups).

If I tighten up my inefficient code, it might be the ticket!

Thanks!

Take care, Joe
 
Hi Everyone,

I have had mixed results with help from Chat GPT for Arduino code suggestions.

Here was their rewrite of my code:

Code:
char waitForChar(char targetChar, unsigned long timeoutMillis) {
  unsigned long startTime = millis();

  while (!SerialBT.available() && millis() - startTime < timeoutMillis);

  while (SerialBT.available()) {
    char receivedChar = SerialBT.read();
    if (receivedChar == targetChar) {
      return receivedChar;
    }
  }

  return '\0';  // Return null character if targetChar is not found within the timeout
}

void loop() {
  char firstChar = waitForChar('!', 1000);  // Wait for '!' with a timeout of 1000 milliseconds
  char secondChar = waitForChar('"', 1000);  // Wait for '"' with a timeout of 1000 milliseconds
  char thirdChar = waitForChar('#', 1000);  // Wait for '#' with a timeout of 1000 milliseconds
  char fourthChar = waitForChar('$', 1000);  // Wait for '$' with a timeout of 1000 milliseconds

  // Process the received characters as needed
}

Should a try this or am I missing the boat?

Thanks, take care, Joe.
 
I hope you have this figured out by now but just in case i wanted to do a little write up of what I was talking about before with your header detection.

I wrote this up fairly quickly and didn't do a proof read so I hope it makes sense. This might not get rid of your stuttering issue but it will explain why the header guards weren't working with the original code I looked at.

Let me know if it helps.

Chris
 

Attachments

  • TheCaseOfStutteringMoose.pdf
    138.1 KB · Views: 13
Excellent write up Chris .

Thank you for taking the time to thoroughly explain this.
 
Last edited:
While I haven't read through every code sample in this thread, I've noticed a couple of things:
1) Most, if not all, code snippets have the 'while (!Serial.available());' construct in one or more places in the loop function. I don't think that this is a good way to code. The drawback is that this construct makes it difficult, if not impossible to do anything in the main loop while the processor is waiting to receive serial bytes. Another disadvantage is that the watchdog timer could trigger and reset the processor (if it's enabled, as is the standard case with ESP modules.
2) The state machine is implicit based on which 'while (!Serial.available());' instance is being executed, rather than explicit (which would make the code easier to understand and debug).

An example of Arduino code which avoids these two (IMO) drawbacks is in post #24 of this posting thread.
 
I totally agree Phil. i mentioned this a couple of times when I said the code was blocking but I figured it was easier in my write up to explain why the current code wasn't detecting the header correctly and I planned on following up on improving that if/when the code was updated using my suggestions. It doesn't appear to be happening though. I planned on using an example of adding a heartbeat led(if the esp module has an onboard led) to show the drawbacks of pausing the main loop as an example.
 
I totally agree Phil. i mentioned this a couple of times when I said the code was blocking but I figured it was easier in my write up to explain why the current code wasn't detecting the header correctly and I planned on following up on improving that if/when the code was updated using my suggestions. It doesn't appear to be happening though. I planned on using an example of adding a heartbeat led(if the esp module has an onboard led) to show the drawbacks of pausing the main loop as an example.

Hi Chris,

Please forgive me if I misunderstood, but are you waiting on me for to do something?

Thanks!

Take care, Joe.
 
Not necessarily waiting but in post #44 I showed why your header detection wasn't working and how to fix it. I'm just hoping you get it working. Then, I was going to chime in on improving it further.
 
Here is a newer version of the aforementioned code. The switch statement is removed from the main loop, and is replaced with a call to a function which performs the same actions as the removed code. Hopefully breaking the byte processing logic out of the main loop and placing it in a separate function will make it easier to follow.

Code:
#include <Servo.h>
#define numChannels 8

byte channelValues[numChannels];
Servo servo[numChannels];

enum {header_1st_byte_get, header_2nd_byte_get, body} packet_state;

int servo_pins[] = {10,3,4,5,6,9,7,8};

// *********************** Arduino setup function *****************************
void setup() {
  //start serial port
  Serial.begin(115200);
  
  //init servo objects

  for (uint8_t i=0;i<sizeof(servo_pins)/sizeof(int); i++)
    servo[i].attach(servo_pins[i]);

  packet_state = header_1st_byte_get;
}

int servo_update_index;		// count of received servo data bytes

void process_serial_input_char(int ch);

// *********************** Arduino loop function ******************************

void loop() {  
  if (Serial.available())
    process_serial_input_char(Serial.read());
    
  // Code for other functions (LED flash, sensor read, LCD output, etc).
} // end of 'loop' function

// ********************** Process one received character **********************
//
// Scan until a packet header (">>") has been detected, then copy the
//   appropriate number of characters into the servo data buffer, and then
//   write servo data buffer to the servo object and return to the starting
//   state waiting for the next packet header.
//
// ****************************************************************************
void process_serial_input_char(int ch ) {

  // in the initial un-synchronized state, check if the received character
  //   is the correct value for the first byte of the header.  If yes,
  //   progress to the state which looks for the second byte of the
  //   header.  If not, remain in the current state.

  if (header_1st_byte_get == packet_state)   {
    if ('>' == ch)
      packet_state=header_2nd_byte_get;
    return;
  }

  // first byte of header has been detected, now check if next character
  //   is the correct value for the second byte of the header.  If yes,
  //   progress to the state which handles servo data bytes.  If not,
  //   return to the first state.
  
  if (header_2nd_byte_get == packet_state) {
    if ('>' == ch) {
      servo_update_index=0;
      packet_state=body;
   } else
      packet_state=header_1st_byte_get;
   return;
   }

   // state for processing servo data after the packet header has been
   //   detected, returning to the un-synchronized state after the
   //   appropriate number of servo bytes have been read.
   
   if (body == packet_state) {
     if (servo_update_index < numChannels) {
       channelValues[servo_update_index++] = ch;
     } else {
       updateServoPositions();//this function updates the servos
       packet_state = header_1st_byte_get;
     }
     return;
  }
    
}

// Write the servo data buffer values to the servo object.

void updateServoPositions(){
  for(byte i=0; i<numChannels; i++){
    servo[i].write(map(channelValues[i], 0, 255, 0, 180));
  
    /*uncomment this to print test values to IDE serial monitor
    Serial.print("controller output ");
    Serial.print(i);
    Serial.print(" value = ");
    Serial.print(channelValues[i]);        
    Serial.print("/255 or ");
    Serial.print(map(channelValues[i], 0, 255, 0, 180));
    Serial.println("/180 degrees");
    */
  }
}
 
Not necessarily waiting but in post #44 I showed why your header detection wasn't working and how to fix it. I'm just hoping you get it working. Then, I was going to chime in on improving it further.

Hi Chris,

Thanks!

At the moment, I am confused.

One big question, taking it from the top.....Why do I need a header for only one channel? A sequence with only one line?

Then, because I am burning out on this, can some post uncommented code in full (that is supposed to cure my issue) for one channel, one pin, one servo? I will test that with a servo.

Otherwise, I can't devote any more time to trial and error attempts. I do appreciate all the kind help from everyone here!

Take care, Joe.
 
One big question, taking it from the top.....Why do I need a header for only one channel? A sequence with only one line?

I don't see any need for a header if there is only one channel. Chris and I've been focusing on the more general case of multiple channels.
 
Hi Chris,

Thanks!

At the moment, I am confused.

One big question, taking it from the top.....Why do I need a header for only one channel? A sequence with only one line?

Then, because I am burning out on this, can some post uncommented code in full (that is supposed to cure my issue) for one channel, one pin, one servo? I will test that with a servo.

Otherwise, I can't devote any more time to trial and error attempts. I do appreciate all the kind help from everyone here!

Take care, Joe.

It would appear that code is not the cure you require.
 
Hi Everyone,

So, I just tried this code on my Win 11 machine, upgraded to 32 gigs RAM, USB connection to the ESP32, servo with a separate power supply:

Code:
#include <ESP32Servo.h>
#define numChannels 1

byte channelValues[numChannels];
Servo servo[numChannels];

enum {header_1st_byte_get, header_2nd_byte_get, body} packet_state;

int servo_pins[] = {2};

void setup() {

   Serial.begin(115200);


   for (uint8_t i=0;i<sizeof(servo_pins)/sizeof(int); i++)
     servo[i].attach(servo_pins[i]);

   packet_state = header_1st_byte_get;
}
           
int servo_update_index;        // count of received servo data bytes

void process_serial_input_char(int ch);


void loop() {
   if (Serial.available())
     process_serial_input_char(Serial.read());

}


//  header (">>")

void process_serial_input_char(int ch ) {


   if (header_1st_byte_get == packet_state)   {
     if ('>' == ch)
       packet_state=header_2nd_byte_get;
     return;
   }



   if (header_2nd_byte_get == packet_state) {
     if ('>' == ch) {
       servo_update_index=0;
       packet_state=body;
    } else
       packet_state=header_1st_byte_get;
    return;
    }



    if (body == packet_state) {
      if (servo_update_index < numChannels) {
        channelValues[servo_update_index++] = ch;
      } else {
        updateServoPositions();//this function updates the servos
        packet_state = header_1st_byte_get;
      }
      return;
   }

}



void updateServoPositions(){
   for(byte i=0; i<numChannels; i++){
     servo[i].write(map(channelValues[i], 0, 255, 85, 120));

   }
}

Still the hiccups! So I am most appreciative for all the help, but clearly it's not the code.

So, I tested last night and with my code, Bluetooth with the serial monitor, and serial just stops to the ESP32 when the hiccup occurs.
So I think we can rule out the header, BT, and the code.
Something is causing the serial from Vixen to my COM port to stop, in my opinion. Used several ports, same behavior.
Same behavior with an actual UNO.

So I think I need to move on.

Thanks, Everyone, for all the help!!

Take care, Joe.
 
Last edited:
Back
Top