We go through the effort of hiding eggs each year, but what if they could hide themselves? Better yet, what if they could try to run away from us to protect the little candies inside? Today, we're putting together a simple robotic system for allowing eggs to be on the lookout for an approaching human and run away if they see one.
I had this idea only a couple days before Easter, so it's a simple build that is focused around just being a fun and goofy project that also provided some amusement for my kiddos. It's a low cost and easy project for anyone looking for an easy one to learn some basics with.
Supplies
Romeo BLE6v 133RPM Motor (2x)Ultrasonic Sensor4 Slot AA Battery PackWheels (x2-4)Simple Robot Base (I used a scrap piece of wood)
The project takes only a few electronics. The Romeo BLE is handy because it acts as a motor driver and the arduino device, so it cuts down on the hardware list a bit and makes things slightly simpler. I had a couple of 6v 133RPM motors so I went ahead and used those for the robot's mobility. For the eyes, we're using an ultrasonic sensor. The power is just a 4x AA battery holder. Otherwise, it's all literal junk I had lying around, so the supply list from here feels super doable (and, very obviously, cheap!).
For wheels I used 2 lids of the same size. I used just a small thin board for the body, a couple rolly wheels I found for some added stability, and random wood pieces to get those wheels a bit closer to the ground relative to the lid-wheels. The motors sat on a bent piece of metal I had around.
Step 1: Wiring
Like I mentioned, the Romeo BLE acts as both the motor driver and the arduino device, so this section will be short. We just need to get our motors, ultrasonic sensor, and battery pack connected and we're good to go.
The motors get plugged into the designated left and right motor spots on the Romeo BLE.
The ultrasonic sensor connects like so:
Vcc → Romeo 5 V
Gnd → Romeo Gnd
Trig → D2
Echo → D3
And we just connect the battery pack to the Romeo BLE for power. This is enough power for both the motors as well without extra effort.
Step 2: Code
As always, the code is provided, but here's a brief overview of what it does. We check for if we see something between 1 foot and just under 6 feet and start the run away process if we see something. We look for something past 1 foot in case it stops near something. If it's hiding, it's better that it stays put instead of just scaring itself away over and over unless we do see someone approaching! We keep it under 6 feet to ensure the ceiling isn't detected during testing.
When we see someone within that range, the robot backs up, turns around, and then books it in the opposite direction. After a certain point in the running, it can do some slight turns for additional evasive maneuvering.
The robot runs through a few simple states: cruising around, dodging when it sees someone, sprinting away, and then stopping until it sees another person. The sprint time is random, just to keep things fun. And while I originally thought about making it find a hiding spot, it turns out just running like a maniac works better — so that's what it does now.
Step 3: Putting It Together
Last but not least is the egg. I'm going to include the models I put together for the egg. I wasn't able to use them, as they wouldn't have been done in time, but hey the models exist now so maybe someone out there will enjoy them. The 2 holes are for the ultrasonic sensors.
I ended up just adding a horde of plastic eggs to the top of the robot and calling it a win. Now, many eggs get the power to escape. Perfect.
My kids had a blast with it as the robot ran away from them on approach. It's a simple build, but I had fun with it so hopefully you enjoyed the journey as well. Have a good one.
enum State { CRUISE, EVADE, RUN, IDLE };
const char *stateName[] = { "CRUISE", "EVADE", "RUN", "IDLE" };
/* ------------- detection window ----------------------------------------- */
const int PERSON_NEAR_LOW = 30; // cm ≈ 1 ft
const int PERSON_NEAR_HIGH = 174; //183 cm ≈ 6 ft
/* ------------- pins (Romeo) --------------------------------------------- */
const int M1_EN = 5, M1_IN1 = 4; // left wheel
const int M2_EN = 6, M2_IN1 = 7; // right wheel
const int TRIG = 2, ECHO = 3;
/* ------------- behaviour parameters ------------------------------------- */
const int CRUISE_PWM = 180;
const int RUN_PWM = 255;
const unsigned long EVADE_BACK_MS = 800;
const unsigned long EVADE_SPIN_MS = 700;
const unsigned long MIN_RUN_MS = 2000UL; // 2 s
const unsigned long MAX_RUN_MS = 10000UL; // 10 s
const unsigned long JITTER_START_MS = 5000UL; // add wiggles after 5 s
const int JITTER_DELTA = 60; // ±PWM change for wiggle
const unsigned long LOG_INTERVAL = 200UL; // ms
const unsigned long GIVE_UP_TIME = 60000UL; // idle after 60 s inactivity
/* ------------- globals --------------------------------------------------- */
State state = CRUISE;
unsigned long stateStartMS = 0;
unsigned long lastDetectionMS = 0;
unsigned long lastLogMS = 0;
unsigned long runDurationMS = 0; // random sprint length
/* ------------- helpers --------------------------------------------------- */
void motor(int en, int in, int pwm, bool fwd) {
digitalWrite(in, fwd ? HIGH : LOW);
analogWrite(en, pwm);
}
void wheels(int lPWM, int rPWM) {
motor(M1_EN, M1_IN1, abs(lPWM), lPWM >= 0);
motor(M2_EN, M2_IN1, abs(rPWM), rPWM >= 0);
}
long pingCM() {
digitalWrite(TRIG, LOW); delayMicroseconds(2);
digitalWrite(TRIG, HIGH); delayMicroseconds(10);
digitalWrite(TRIG, LOW);
long us = pulseIn(ECHO, HIGH, 30000); // 30 ms = 5 m
if (us == 0) return -1;
return (us * 34L) / 2000; // cm
}
void logLine(long dist, int lPWM, int rPWM) {
unsigned long now = millis();
if (now - lastLogMS < LOG_INTERVAL && state != IDLE) return;
lastLogMS = now;
unsigned long inState = now - stateStartMS;
long toIdle = (state == IDLE) ? 0
: (long)GIVE_UP_TIME - (long)(now - lastDetectionMS);
long toRunEnd = (state == RUN)
? (long)runDurationMS - (long)inState : 0;
Serial.print(now); Serial.print(',');
Serial.print(stateName[state]); Serial.print(',');
Serial.print(dist); Serial.print(',');
Serial.print(lPWM); Serial.print(',');
Serial.print(rPWM); Serial.print(',');
Serial.print(inState/1000); Serial.print(',');
Serial.print(toIdle); Serial.print(',');
Serial.println(toRunEnd);
}
void changeState(State s) {
state = s;
stateStartMS = millis();
logLine(-1, 0, 0);
}
/* ------------- setup ----------------------------------------------------- */
void setup() {
pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT);
pinMode(M1_IN1, OUTPUT); pinMode(M2_IN1, OUTPUT);
pinMode(M1_EN, OUTPUT); pinMode(M2_EN, OUTPUT);
Serial.begin(115200);
Serial.println(
"ms,state,dist,leftPWM,rightPWM,secInState,msToIdle,msToRunEnd");
lastDetectionMS = millis();
stateStartMS = millis();
}
/* ------------- main loop ------------------------------------------------- */
void loop() {
unsigned long now = millis();
long d = pingCM(); // -1 = no reading
bool personSeen = (d >= PERSON_NEAR_LOW && d <= PERSON_NEAR_HIGH);
/* idle timeout for active states */
if (state != IDLE && (now - lastDetectionMS) > GIVE_UP_TIME) {
wheels(0, 0);
changeState(IDLE);
}
switch (state) {
/* ---------- CRUISE ---------- */
case CRUISE:
wheels(CRUISE_PWM, CRUISE_PWM);
logLine(d, CRUISE_PWM, CRUISE_PWM);
if (personSeen) {
lastDetectionMS = now;
changeState(EVADE);
}
break;
/* ---------- EVADE (reverse + spin) ---------- */
case EVADE: {
wheels(-RUN_PWM, -RUN_PWM); // back up
logLine(d, -RUN_PWM, -RUN_PWM);
delay(EVADE_BACK_MS);
bool left = random(0, 2); // spin random dir.
wheels(left ? -RUN_PWM : RUN_PWM,
left ? RUN_PWM : -RUN_PWM);
logLine(d,
left ? -RUN_PWM : RUN_PWM,
left ? RUN_PWM : -RUN_PWM);
delay(EVADE_SPIN_MS);
/* decide sprint length (2–10 s) */
runDurationMS = random(MIN_RUN_MS, MAX_RUN_MS + 1UL);
changeState(RUN);
}
break;
/* ---------- RUN (sprint away) ---------- */
case RUN: {
unsigned long elapsed = now - stateStartMS;
/* steering: straight first 5 s, then add wiggles */
int lPWM = RUN_PWM;
int rPWM = RUN_PWM;
if (elapsed > JITTER_START_MS) {
int delta = random(-JITTER_DELTA, JITTER_DELTA + 1);
lPWM = RUN_PWM + delta;
rPWM = RUN_PWM - delta;
lPWM = constrain(lPWM, 0, 255);
rPWM = constrain(rPWM, 0, 255);
}
wheels(lPWM, rPWM);
logLine(d, lPWM, rPWM);
if (elapsed >= runDurationMS) {
wheels(0, 0);
changeState(IDLE);
}
}
break;
/* ---------- IDLE ---------- */
case IDLE:
wheels(0, 0);
logLine(d, 0, 0);
if (personSeen) { // wake up
lastDetectionMS = now;
changeState(EVADE);
}
delay(250);
break;
}
}
