Robotics / Animatronics

Animatronics Head (Mini)

This animatronic head, is a smaller version of the original head I designed and made.

This animatronic took inspiration from MateoTechLab – Diy Robot and Dan Makes Things – Companion Robot

I wanted this Animatronics to be a “companion” or “wearable” robot, something that was easy to move, wasn’t too big.

Design

The majority of the items on this build were designed and made by my.  The overall design, PCB, Wiring were of my design.  The Arduino board and Neopixel boards were purchased parts.

3D Printed Parts – Parts were designed using Onshape  and using Creatlity Ender 3 V3 SE, the individual parts were printed.

Electronics were design in Kicad and manufactured by JLCPCB

Build

Throughout the build, there were updates and revisions made to the design.

These include, “braces” for the antenna, the original idea was to glue them on, which did work, but was brittle and did break. The original placement of the Arduino would have worked and the Arduino would have fit, however disconnected and connecting wires would have been incredibly difficult.

Below is the code that is running on the Arduino, this was made by AI, using prompts.  

The program does the following. 1) Runs through the Neopixels, showing off different patterns and colours and 2) turns the servo to different positions at random intervals.

The code runs the servo at a slow speed as to not damage itself, the Neopixels, don’t run all of the lights at the same time and when they do, they font run at full brightness.  This is to help run this project on batteries in the future.

➕➕
Arduino Code.cpp
#include <Adafruit_NeoPixel.h>
#include <Servo.h>

// --- Pin Definitions ---
#define JEWEL1_PIN     2
#define JEWEL2_PIN     4
#define STICK_PIN      3
#define SERVO_PIN      7

// --- Pixel Counts ---
#define JEWEL_NUMPIXELS  7
#define STICK_NUMPIXELS  8

// --- Brightness (25%) ---
#define BRIGHTNESS 25

// --- Timing ---
#define STEP_MS 60

// --- Servo Settings ---
#define SERVO_CENTER        90
#define SERVO_MIN            0
#define SERVO_MAX          180
#define SERVO_STEP_DELAY    20    // ms per degree (25% speed)
#define SERVO_MIN_MOVE      30
#define SERVO_SETTLE_MS    200    // Time to hold position before detaching

// --- Random interval between head moves (ms) ---
#define HEAD_INTERVAL_MIN  5000
#define HEAD_INTERVAL_MAX 12000

Adafruit_NeoPixel eye1(JEWEL_NUMPIXELS, JEWEL1_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel eye2(JEWEL_NUMPIXELS, JEWEL2_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel stick(STICK_NUMPIXELS, STICK_PIN, NEO_GRB + NEO_KHZ800);
Servo headServo;

// --- Colour palette ---
const uint32_t COLOURS[] = {
  0xFF0000, // Red
  0xFF4400, // Orange
  0x00FF00, // Green
  0x00FFFF, // Cyan
  0x0000FF, // Blue
  0xFF00FF, // Magenta
};
const uint8_t NUM_COLOURS = sizeof(COLOURS) / sizeof(COLOURS[0]);
uint8_t colourIndex = 0;

// --- Servo state ---
int           servoCurrentPos   = SERVO_CENTER;
unsigned long servoNextMoveTime = 0;

// ── Attach, move slowly, settle, then detach ─────────────
// Detaching kills the PWM signal so NeoPixel interrupt
// interference cannot cause jitter while servo is idle.
void servoMoveTo(int targetPos) {
  targetPos = constrain(targetPos, SERVO_MIN, SERVO_MAX);

  headServo.attach(SERVO_PIN);
  // Write current position first so the servo knows where
  // it is before we start stepping — prevents snap-to-target
  headServo.write(servoCurrentPos);
  delay(50);

  int step = (targetPos > servoCurrentPos) ? 1 : -1;
  while (servoCurrentPos != targetPos) {
    servoCurrentPos += step;
    headServo.write(servoCurrentPos);
    delay(SERVO_STEP_DELAY);
  }

  delay(SERVO_SETTLE_MS);   // Let servo settle fully at target
  headServo.detach();       // Stop PWM — eliminates idle jitter
}

// ── Pick a random target at least SERVO_MIN_MOVE away ────
int pickNewHeadTarget() {
  int target;
  do {
    target = random(SERVO_MIN, SERVO_MAX + 1);
  } while (abs(target - servoCurrentPos) < SERVO_MIN_MOVE);
  return target;
}

// ── Schedule the next random head movement ───────────────
void scheduleNextHeadMove() {
  servoNextMoveTime = millis() + random(HEAD_INTERVAL_MIN, HEAD_INTERVAL_MAX);
}

// ── Check if it's time to move the head ─────────────────
void updateServo() {
  if (millis() >= servoNextMoveTime) {
    servoMoveTo(pickNewHeadTarget());
    scheduleNextHeadMove();
  }
}

// ── Dim a colour to a fraction (0.0 - 1.0) ──────────────
uint32_t dimColour(Adafruit_NeoPixel &strip, uint32_t col, float frac) {
  if (frac <= 0) return 0;
  if (frac > 1)  frac = 1;
  uint8_t r = ((col >> 16) & 0xFF) * frac;
  uint8_t g = ((col >> 8)  & 0xFF) * frac;
  uint8_t b = ( col        & 0xFF) * frac;
  return strip.Color(r, g, b);
}

void clearAll() {
  eye1.clear();  eye1.show();
  eye2.clear();  eye2.show();
  stick.clear(); stick.show();
}

// ── Cylon sweep on the stick ─────────────────────────────
void stickCylon(uint8_t pos, int8_t dir, uint32_t col) {
  stick.clear();
  stick.setPixelColor(pos, dimColour(stick, col, 1.00));
  int8_t t1 = pos - dir;
  int8_t t2 = pos - dir * 2;
  if (t1 >= 0 && t1 < STICK_NUMPIXELS) stick.setPixelColor(t1, dimColour(stick, col, 0.30));
  if (t2 >= 0 && t2 < STICK_NUMPIXELS) stick.setPixelColor(t2, dimColour(stick, col, 0.10));
  stick.show();
}

// ── Circular sweep on a jewel ────────────────────────────
void jewelCylon(Adafruit_NeoPixel &eye, uint8_t ringPos, uint32_t col) {
  eye.clear();
  auto ringPix = [](uint8_t rp) -> uint8_t { return (rp % 6) + 1; };
  eye.setPixelColor(ringPix(ringPos),     dimColour(eye, col, 1.00));
  eye.setPixelColor(ringPix(ringPos + 5), dimColour(eye, col, 0.30));
  eye.setPixelColor(ringPix(ringPos + 4), dimColour(eye, col, 0.10));
  eye.show();
}

// ── Main LED effect loop ─────────────────────────────────
void cylonLoop(uint32_t col, uint16_t totalSteps) {
  uint8_t eye1Pos = 0;
  uint8_t eye2Pos = 0;
  uint8_t stickPos = 0;
  int8_t  stickDir = 1;

  for (uint16_t step = 0; step < totalSteps; step++) {
    jewelCylon(eye1, eye1Pos % 6,            col);
    jewelCylon(eye2, (6 - eye2Pos % 6) % 6, col);
    stickCylon(stickPos, stickDir, col);

    updateServo();

    delay(STEP_MS);

    eye1Pos++;
    eye2Pos++;

    stickPos += stickDir;
    if (stickPos >= STICK_NUMPIXELS - 1) { stickPos = STICK_NUMPIXELS - 1; stickDir = -1; }
    if (stickPos <= 0)                   { stickPos = 0;                   stickDir =  1; }
  }
}

// ── Colour crossfade transition ──────────────────────────
void crossfade(uint32_t fromCol, uint32_t toCol, uint8_t steps) {
  uint8_t fr = (fromCol >> 16) & 0xFF, fg = (fromCol >> 8) & 0xFF, fb = fromCol & 0xFF;
  uint8_t tr = (toCol   >> 16) & 0xFF, tg = (toCol   >> 8) & 0xFF, tb = toCol   & 0xFF;

  for (uint8_t s = 0; s <= steps; s++) {
    float f = (float)s / steps;
    uint8_t r = fr + (tr - fr) * f;
    uint8_t g = fg + (tg - fg) * f;
    uint8_t b = fb + (tb - fb) * f;
    uint32_t mid = eye1.Color(r, g, b);

    eye1.clear(); eye1.setPixelColor(0, dimColour(eye1, mid, 0.4)); eye1.show();
    eye2.clear(); eye2.setPixelColor(0, dimColour(eye2, mid, 0.4)); eye2.show();
    stick.clear();
    uint8_t midPix = STICK_NUMPIXELS / 2;
    stick.setPixelColor(midPix,     dimColour(stick, mid, 0.4));
    stick.setPixelColor(midPix - 1, dimColour(stick, mid, 0.2));
    stick.setPixelColor(midPix + 1, dimColour(stick, mid, 0.2));
    stick.show();

    updateServo();

    delay(20);
  }
}

// ── Setup & Loop ─────────────────────────────────────────
void setup() {
  eye1.begin();  eye1.setBrightness(BRIGHTNESS);  eye1.clear();  eye1.show();
  eye2.begin();  eye2.setBrightness(BRIGHTNESS);  eye2.clear();  eye2.show();
  stick.begin(); stick.setBrightness(BRIGHTNESS); stick.clear(); stick.show();

  // Servo startup: sweep slowly to centre, then detach
  headServo.attach(SERVO_PIN);
  headServo.write(SERVO_MIN);
  servoCurrentPos = SERVO_MIN;
  delay(500);
  servoMoveTo(SERVO_CENTER);
  // servoMoveTo() detaches at the end automatically

  randomSeed(analogRead(A0));
  scheduleNextHeadMove();

  delay(500);
}

void loop() {
  uint32_t col     = COLOURS[colourIndex];
  uint32_t nextCol = COLOURS[(colourIndex + 1) % NUM_COLOURS];

  cylonLoop(col, 80);
  crossfade(col, nextCol, 30);

  colourIndex = (colourIndex + 1) % NUM_COLOURS;
}

In Action

The majority of the items on this build were designed and made by my.  The overall design, PCB, Wiring were of my design.  The arduino board and neopixel boards were purcased parts.

3D Printed Parts – Parts were designd using Onshape  and using Creatlity Ender 3 V3 SE, were printed.

Conclusion

I am happy with this design, a few new skills and thoughts were learnt in the design and build process, including:

  • 3D printing
  • Even though it might fit, it might now be accessible. 
  • The importance of flux when soldering.

Going forward, this is a great starting point for more projects. I do have one update to the design, including buzzers to add sound to the Animatronic Head