Neopixel LEDs and Servos for a Robo Ninja

Hello everyone,

This is my first time posting so please forgive me for any formatting mistakes. If this topic has already been addressed somewhere I have yet to find, please point me in the right direction.
I've been working on a project off and on for over a year now, constantly running into problems, scouring the webs for an answer, and then running into the next problem. So here I am screaming into the void, hoping the void whispers back.

I'm working on a cosplay Gray Fox helmet. It uses 4 servos, and many LEDs to open the mask and make the eyes and other bits blink/ light up when starting up.
Having no real experience coding outside of many youtube vids, I was able to find the code below for an iron man helmet which I was able to successfully modify and mock up to work on an Arduino Uno.

However, now I want to try to make it better by
1; using a smaller microcontroller. I'm looking at the Seeed Studio Xiao-ESP32-C6 because I believe that it is small enough to fit and has at least 6 PWM pins that I can use.
2; I want to use a NeoPixel Ring for the Mono eye and the single WS2812 LEDs for the rest of the helmet.

The code below has a 'blink on startup command' that runs on a button push which I'm having trouble getting to play nice with the Fast LED or Neopixel Libraries that I have tried to make work.

I appreciate any and all help I can get... I need it.

This is the code that I'm using.

//Gray Fox Helmet Code 2.0

#include <VarSpeedServo.h>        // servo reference library, for instructions see: https://github.com/netlabtoolkit/VarSpeedServo
#include <Bounce2.h>              
#include <ButtonEvents.h>	  // button effects, for instructions see: https://github.com/fasteddy516/ButtonEvents

const int servo1Pin = 6;          // set the pin for servo 1 (Right Cheek)
const int servo2Pin = 9;          // set the pin for servo 2 (Right Top Visor)
const int servo3Pin = 10;         // set the pin for servo 3 (Left Top Visor)
const int servo4Pin = 11;         // set the pin for servo 4 (Left Cheek)
const int buttonPin = 2;          // the pin that the pushbutton is attached to
const int monoEyePin = 3;         // set the pin for the mono eye (this will need to me modified for the led ring with multiple leds)
const int helmetLEDsPin = 5       // set the pin for the LEDs around the helmet

// Declare servo objects
;VarSpeedServo servo1;             // create servo object to control servo 1 (Right Cheek)
;VarSpeedServo servo2;             // create servo object to control servo 2 (Right Top Visor)
;VarSpeedServo servo3;             // create servo object to control servo 3 (Left Top Visor)
;VarSpeedServo servo4;             // create servo objest to control servo 4 (Left Cheek)

// Declare variables for servo speed control
const int servoCloseSpeed = 255;  // set the speed of the servo close function
const int servoOpenSpeed = 255;   // set the speed of the servo opening, recommend to set at max speed to aid in lift

// In Dual Servo Configuration the servos move in opposing directions, so the angles of the servos will be opposite to each other. 
// Normal Servo range is 0° ~ 180°, for initial setup the range has been adjusted to 20° ~ 160°, this allows for a 20° adjustment at both ends of the servo range.
// See Helmet tutorial for further information on servo setup.
const int servo1_OpenPos = 20;    // set the open position of servo 1 (Right Cheek)
const int servo2_OpenPos = 110;   // set the open position of servo 2 (Right Top Visor)
const int servo3_OpenPos = 20;    // set the open position of servo 3 (Left Top Visor)
const int servo4_OpenPos = 110;   // set the open position of servo 4 (Left Cheek)
const int servo1_ClosePos = 110;  // set the closed position of servo 1 (Right Cheek)
const int servo2_ClosePos = 20;   // set the closed position of servo 2 (Right Top Visor)
const int servo3_ClosePos = 110;  // set the closed position of servo 3 (Left Top Visor)
const int servo4_ClosePos = 20;   // set the closed position of servo 4 (Left Cheek)

// Declare variables for setup special effects (applies to LED eyes only for now)
#define SETUP_NONE 0              // No special effects, just turn on the LED eyes
#define SETUP_MOVIE_BLINK 1       // Blink LED eyes on setup, 
#define SETUP_FADE_ON 2           // Slowly brighten LED eyes until fully lit

// To use the specific feature below
// use double slashes "//" to comment, or uncomment (remove double slashes) in the code below

// Uncomment this line if you don't want any special effect during setup, comment this line to disable this effect
// const int setupFx = SETUP_NONE;

// Uncomment this line if you want the movie blink special effect during setup, comment this line to disable this effect
const int setupFx = SETUP_MOVIE_BLINK;

// Uncomment this line if you want the fade on special effect during setup, comment this line to disable this effect
// const int setupFx = SETUP_FADE_ON;

// Declare variables for LED eyes special effects (applies to LED eyes only for now)
#define EYES_NONE 0 			// No special effects, just turn on the LED eyes
#define EYES_MOVIE_BLINK 1 		// Blink LED eyes on setup, sequence based on Avengers Movie
#define EYES_FADE_ON 2 			// Slowly brighten LED eyes until fully lit

// To use the specific feature below
// use double slashes "//" to comment, or uncomment (remove double slashes) in the code below

// Uncomment this line if you don't want any special effect during setup, comment this line to disable this effect
// const int eyesFx = EYES_NONE;

// Uncomment this line if you want the movie blink special effect during setup, comment this line to disable this effect
const int eyesFx = EYES_MOVIE_BLINK;

// Uncomment this line if you want the fade on special effect during setup, comment this line to disable this effect
//const int eyesFx = EYES_FADE_ON;

// Declare variables for button control
boolean movieblinkOnClose = false; //Blink LEDs on close of faceplate, 

// Declare variable for AuxLED
//boolean auxLedEnabled = false; // Set to true if you want to enable the Aux LED
//boolean auxLedState = true; // Keeps track of the state of the LED on = true, off = false

// Declare variables for LED control
unsigned long fadeDelay = .1; //speed of the eye 'fade'
unsigned long callDelay = 10; //length to wait to start eye flicker after face plate comes down
unsigned long blinkSpeed = 60; //delay between init blink on/off
unsigned long currentPWM = 0; // keep track of where the current PWM level is at
boolean isOpen = true; // keep track of whether or not the faceplate is open

// Define object for primary button to handle 
// multiple button press features:
// 1. Single Tap
// 2. Double Tap
// 3. Long Press
ButtonEvents primaryButton = ButtonEvents();

// State of the faceplate 1 = open, 0 = closed
#define FACEPLATE_CLOSED 0
#define FACEPLATE_OPEN 1
int facePlateCurMode = FACEPLATE_OPEN; // Keep track if the faceplate is open or closed

// State of the LED eyes 1 = on, 2 = off
#define LED_EYES_OFF 0
#define LED_EYES_ON 1

// State of the LED eyes for dimming/brightening 1 = brighten, 2 = dim
#define LED_EYES_DIM_MODE 0
#define LED_EYES_BRIGHTEN_MODE 1

int ledEyesCurMode = LED_EYES_DIM_MODE; // Keep track if we're dimming or brightening
int ledEyesCurPwm = 0; // Tracking the level of the LED eyes for dim/brighten feature
const int ledEyesIncrement = 15; // Define the increments to brighten or dim the LED eyes


/**
 * Helper Method
 * Simulate a delay in processing without disabling the processor completely
 * 
 * @param[out] period - the amount of time in milliseconds to delay
 * 
 * See: https://randomnerdtutorials.com/why-you-shouldnt-always-use-the-arduino-delay-function/
*/
void simDelay(long period){
  long delayMillis = millis() + period;
  while (millis() <= delayMillis)
  {
    int x = 0; // dummy variable, does nothing
  }
}

/**
 * Simulate the eyes slowly blinking until fully lit
 */ 
void movieblink(){
  Serial.println(F("Start Movie Blink.."));

  // pause for effect...
  simDelay(300);

  int lowValue = 21;
  int delayInterval[] = { 210, 126, 84 };
  int delayVal = 0;

  // First blink on
  for (int i = 0; i <= lowValue; i++){
    setLedEyes(i);
    delayVal = delayInterval[0]/lowValue;
    simDelay(delayVal);
  }

  // Turn off
  setLedEyes(0);
  simDelay(delayInterval[0]);

  // Second blink on
  for (int i = 0; i <= lowValue; i++){
    setLedEyes(i);
    //setAuxLed();
    delayVal = delayInterval[1]/lowValue;
    simDelay(delayVal);
  }

  // Turn off
  setLedEyes(0);
  //setAuxLed();
  simDelay(delayInterval[1]);

  // Third blink on
  setLedEyes(lowValue);
  //setAuxLed();
  simDelay(delayInterval[2]);

  // Turn off
  setLedEyes(0);
  //setAuxLed();
  simDelay(delayInterval[2]);

  // All on
  setLedEyes(255);
}

/*
 * Simulate LED eyes slowly brightening until fully lit
 */
 void fadeEyesOn(){
  ledEyesCurMode = LED_EYES_BRIGHTEN_MODE;

  // loop until fully lit
  while (ledEyesCurPwm < 255){
    setLedEyes(ledEyesCurPwm);
  
    simDelay(200);
    ledEyesBrighten();
  }
  
 }

 /**
 * Method to open face plate
 */
 void facePlateOpen(){
  Serial.println(F("Servo Up!")); 

  // Re-attach the servos to their pins
  servo1.attach(servo1Pin);
  servo2.attach(servo2Pin);
  servo3.attach(servo3Pin);
  servo4.attach(servo4Pin);

  // Send data to the servos for movement
  
  servo1.write(servo1_OpenPos, servoOpenSpeed);
  servo4.write(servo4_OpenPos, servoOpenSpeed);
  //simDelay(1000); // wait doesn't wait long enough for servos to fully complete...   
  simDelay(500);
  servo2.write(servo2_OpenPos, servoOpenSpeed);
  servo3.write(servo3_OpenPos, servoOpenSpeed);
    
  simDelay(1000); // wait doesn't wait long enough for servos to fully complete...

  // Detach so motors don't "idle"
  servo1.detach();
  servo2.detach();
  servo3.detach();
  servo4.detach();
 
  facePlateCurMode = FACEPLATE_OPEN;
 }

 /**
  * Method to close face plate
  */
 void facePlateClose(){
  Serial.println(F("Servo Down"));  

  // Re-attach the servos to their pins
  servo1.attach(servo1Pin);
  servo2.attach(servo2Pin);
  servo3.attach(servo3Pin);
  servo4.attach(servo4Pin);

  // Send data to the servos for movement 

  servo2.write(servo2_ClosePos, servoCloseSpeed);
  servo3.write(servo3_ClosePos, servoCloseSpeed);
  simDelay(500);

  servo1.write(servo1_ClosePos, servoCloseSpeed);
  servo4.write(servo4_ClosePos, servoCloseSpeed);
  // Delay to allow Jaw to fully close before Faceplate closes

  simDelay(1000); // wait doesn't wait long enough for servos to fully complete...

  // Detach so motors don't "idle"
  servo1.detach();
  servo2.detach();
  servo3.detach();
  servo4.detach();

  facePlateCurMode = FACEPLATE_CLOSED;
 }

/**
 * Set the brightness of the LED eyes
 * 
 * @param[out] pwmValue - the PWM value (0-255) for the LED brightness
 */
void setLedEyes(int pwmValue){
  analogWrite(monoEyePin, pwmValue);
  analogWrite(helmetLEDsPin, pwmValue);
  ledEyesCurPwm = pwmValue;
}
 
/**
 * Method to turn on LED eyes
 */
void ledEyesOn(){
  Serial.println(F("Turning LED eyes on..."));
  
  setLedEyes(255);
  
  ledEyesCurMode = LED_EYES_DIM_MODE;
}

/**
 * Method to turn off LED eyes
 */
void ledEyesOff(){
  Serial.println(F("Turning LED eyes off..."));
  
  setLedEyes(0);

  ledEyesCurMode = LED_EYES_BRIGHTEN_MODE;
}

/**
 * Method to turn LED eyes on/off
 */
void ledEyesOnOff(){
  // LED eyes stay off when faceplate is open
  if(facePlateCurMode == FACEPLATE_CLOSED){
    if (ledEyesCurPwm > 0){
      ledEyesOff();
    } else {
      ledEyesOn();
    }
  }
}

void ledEyesDim(){
  Serial.println(F("Dimming LED eyes..."));

  ledEyesCurPwm = ledEyesCurPwm - ledEyesIncrement; // Decrease the brightness

  // Make sure we don't go over the limit
  if(ledEyesCurPwm <= 0){
    ledEyesCurPwm = 0;
  }
}

void ledEyesBrighten(){
  Serial.println(F("Brightening LED eyes..."));

  ledEyesCurPwm = ledEyesCurPwm + ledEyesIncrement; // Increase the brightness

  // Make sure we don't go over the limit
  if(ledEyesCurPwm >= 255){
    ledEyesCurPwm = 255;
  }
}

/**
 * Method to dim or brighten both LED eyes
 */
void ledEyesFade(){
  if(ledEyesCurPwm == 255){
    ledEyesCurMode = LED_EYES_DIM_MODE;
  } else if(ledEyesCurPwm == 0){
    ledEyesCurMode = LED_EYES_BRIGHTEN_MODE;
  }
  
  if(ledEyesCurMode == LED_EYES_BRIGHTEN_MODE){
    ledEyesBrighten();
  } else {
    ledEyesDim();
  }

  setLedEyes(ledEyesCurPwm);

  simDelay(200);
}

/**
 * Method to run sequence of sppecial effects when system first starts or sets up
 */
void startupFx(){
  //facePlateClose();

  facePlateClose();

  switch(setupFx){
    case SETUP_NONE:
      ledEyesOn();
      break;
    case SETUP_MOVIE_BLINK:
      movieblink();
      break;
    case SETUP_FADE_ON:
      fadeEyesOn();
      break;
  }

}

/**
 * Method to execute special effects when the faceplate opens
 */
void facePlateOpenFx(){
  // TODO: See if we need delays in between fx

  ledEyesOff();

  facePlateOpen();
}

/**
 * Method to execute special effects when the faceplate closes
 */
void facePlateCloseFx(){

  facePlateClose();

  switch(eyesFx){
    case EYES_NONE:
      ledEyesOn();
      break;
    case EYES_MOVIE_BLINK:
      movieblink();
      break;
    case EYES_FADE_ON:
      fadeEyesOn();
      break;
  }
}

/**
 * Handle faceplate special effects
 */
void facePlateFx(){
  if (facePlateCurMode == FACEPLATE_OPEN){
    facePlateCloseFx();
  } else {
    facePlateOpenFx();
  }
}

/**
 * Event handler for when the primary button is tapped once
 */
void handlePrimaryButtonSingleTap(){
  facePlateFx();
}

/**
 * Event handler for when the primary button is double tapped
 */
void handlePrimaryButtonDoubleTap(){
  ledEyesOnOff();
}

/**
 * Event handler for when the primary button is pressed and held
 */
void handlePrimaryButtonLongPress(){
  while(!primaryButton.update()){
    ledEyesFade(); // Dim or brighten the LED eyes
  }
}

/**
 * Initializes the primary button for multi-functions
 */
void initPrimaryButton(){
  // Attach the button to the pin on the board
  primaryButton.attach(buttonPin, INPUT_PULLUP);
  // Initialize button features...
  primaryButton.activeLow();
  primaryButton.debounceTime(15);
  primaryButton.doubleTapTime(250);
  primaryButton.holdTime(2000);
}

/**
 * Monitor for when the primary button is pushed
 */
void monitorPrimaryButton(){
  bool changed = primaryButton.update();

  // Was the button pushed?
  if (changed){
    int event = primaryButton.event(); // Get how the button was pushed

    switch(event){
      case(tap):
        Serial.println(F("Primary button single press..."));
        handlePrimaryButtonSingleTap();
        break;
      case (doubleTap):
        Serial.println(F("Primary button double press..."));
        handlePrimaryButtonDoubleTap();
        break;
      case (hold):
        Serial.println(F("Primary button long press..."));
        handlePrimaryButtonLongPress();
        break;
    }
  }
}

/**
 * Initialization method called by the Arduino library when the board boots up
 */
void setup() {
  // Set up serial port
  Serial.begin(115200);  
  
  simDelay(2000); // Give the serial service time to initialize

  Serial.print(F("Initializing Iron Man Servo version: "));
  //Serial.println(VERSION);

#ifdef SOUND
  init_player(); // initializes the sound player
#endif

  initPrimaryButton(); // initialize the primary button
  
  //pinMode(AuxLED, OUTPUT); // set output for AUX LED

  startupFx(); // Run the initial features
}

/**
 * Main program exeucution
 * This method will run perpetually on the board
 */
void loop() {
  monitorPrimaryButton(); // Since all features currently are tied to the one button...
}

Yes you will have. The problem is, as you have found you can't run these two devices because they interfere with each other. This is because the LEDs require to turn off the interrupts while they are loading data to the strips in order to generate the correct timing. The servo libiries require that interrupts always on. So that is a dilemma.

There are several ways round this but perhaps the simplest is to:-

Use the Dot Star LEDs as they have a protocol that does not require to turn off the interrupts.

This is because the transfer of data into the strip is itself interruptible. The down side is that you will require to use two signals, instead of one, to control this sort of strip.

Also read the links underneath this thread for other more complex solutions.

1 Like

Thanks for the reply Mike!

That was the first I had read about the interrupts. I'll see if I can find a DotStar LED that will work. This is the kind of LED ring I was trying to use because its already set for the Mono eye in the helmet, but I may have to just scrap that idea and go with what works.

Dot star LEDs are also known as APA102. You can get them individually so you could make them into a ring.

Other solutions I have used is to use to solve the problem involve using an ATtiny processor to drive normal WS2812 LEDs but transferring the data to this processor with a interruptible protocol, like the APA102 uses.

Also see this link:-

and for a separate independent servo drive see:-

1 Like

Let me list a few more.

The simplest is to use Makuna Neopixelbus which on ESP based boards has methods (the I2S/DMA method in particular which does not require interrupts to be turned off and runs fully in the background once the encoding is complete.

The library that you use for driving the Servos uses timers to constantly send a pulse to the servos, but if you use a method where you send the pulses only while you are not transmitting a led signal, your issues will also be resolved. There are different libraries available for servos that do that, and you can even send the pulses manually if required, it is not a complex protocol. The basic idea is to send a pulse of a length between 500us & 2500us (approx) and do this no faster than every 20ms. Those 20ms are fairly relaxed in timing and a pulse every 50ms or so is usually fine as well.

no need to buy more hardware in my opinion.

1 Like

Servo without a library... (could be improved using direct port access)

/*
  Servo PWM without library

  Most 180 degree servos use a pulse train with 5% to 10% duty cycle at 50Hz (50 pulses per second)
  1ms to 2 ms (1000us to 2000us) ON, 19ms to 18ms OFF (18000us to 19000us) over 20ms (20000us)
    _                     _                    x
  _|1|_______________19.0| |___________________x   0 degrees
    ___                   ___                  x
  _|1.5|_____________18.5|   |_________________x  90 degrees
    ____                  ____                 x
  _|2.0 |____________18.0|    |________________x 180 degrees

  - start timer
  - write PWM HIGH = begin EVENT 1
  - check timer for 1ms to 2ms (the angle you need, adjusted for your servo)
  - write PWM LOW = end EVENT 1
  - wait 19ms to 18ms (note this is the 20ms remainder of the 1ms to 2ms pulse)
  - any number of EVENTs can be started and ended at any time
*/

unsigned long totalTime = 20000; // PWM time a 20ms/50Hz pulse cycle
unsigned long oldTime, currentMicros, previousMicros = 0; // timers

byte potPin = A0; // potentiometer pin
byte servoPin = 9; // servo pin

void setup() {
  Serial.begin(115200); // for debug
  pinMode(servoPin, OUTPUT); // servo pin
  digitalWrite(servoPin, HIGH); // begin "ON" pulse
}

void loop() {
  bool timeONFlag, timeOFFFlag; // indicate xTime has finished
  int potentiometerValue; // potentiometer reading

  potentiometerValue = analogRead(potPin); // read the potentiometer

  // SIMULATOR: map 450us to 2550us. REAL SERVO: map 1000us to 2000us (1ms to 2ms) for average servo
  unsigned long timeON = map(potentiometerValue, 0, 1023, 440, 2550);
  unsigned long timeOFF = totalTime - timeON; // calculate timeOFF LOW signal

  // print only changing values
  if (oldTime != timeON) {
    Serial.print("Pulse width: ");
    Serial.println(timeON);
    oldTime = timeON;
  }

  currentMicros = micros(); // start a new pulse train timer

  // EVENT 1 - PWM OFF - occurs between 1ns (1000us) and 2ms (2000us) after start pulse to end HIGH pulse
  if (currentMicros - previousMicros >= timeON) { // bypass this for condition 1000us to 2000us
    if (timeONFlag == 0) { // test if timeON has previously been tested
      digitalWrite(servoPin, LOW); // set pulse OFF for timeOFF
      timeONFlag = 1; // timeON is finished, do not enter this condition until end of timeOFF
    }
  }

  // EVENT 2 - PWM ON - occurs every 20ms (20000us, 50Hz) to start a new pulse
  if (currentMicros - previousMicros >= timeOFF) { // bypass this for condition 18000us to 19000us
    timeONFlag = 0; // timeOFF is finished at 20ms. Reset timeONFlag
    digitalWrite(servoPin, HIGH); // set pulse ON to begin timeON
    previousMicros = currentMicros; // previousMicros is new reference time "zero"
  }
}
1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.