See Also: intro, part 1, part 2, part 3 and part 4. (Github project.)
The story this far…
In 2023, Home Depot sold an animated Halloween prop called Lethal Lily Swamp Witch. This prop was unique in that it has a rather nicely animate head that could move its eyes, blink, and tilt the neck in various directions. This was done by having the prop controlled by servos. Lethal Lily is much more advanced that most props that just use simple motors and gears to repeat motions.
The animated head is controlled via a four-wire connector which provides power, ground, a serial data line, and some kind of line that pulses to control the servos. Somehow.
While I do not yet understand how the pulse line works, I have been able to capture the pulse timing from the Control Box with the goal of having the Arduino generate the same pulses directly to the head — hopefully animating it without using the Control Box.
Using my LethalLilyReader program from the previous part, I was able to capture the serial and pulse data and display it as numbers. This allowed me to copy the pulse timing data so I could put it in a new program that would pulse the blue wire going to the head on and off with appropriate timing delays between each change.
I will skip all the trial and error I went through to get this far, and just share the program.
One thing I should mention — since the Saleae was showing times like 145.6 ms and such, I wanted to capture the time as accurately as possible. I used micros() on the Arduino to capture microseconds. When I went to play this back, I saw that my delayMicroseconds() were not working. I found this note:
TCurrently, the largest value that will produce an accurate delay is 16383; larger values can produce an extremely short delay. This could change in future Arduino releases. For delays longer than a few thousand microseconds, you should use
delayMicroseconds() – Arduino Referencedelay()
instead.
Because of this, my choice was to use delay() and milliseconds (which might throw timing off, if those partial milliseconds matter — probably they don’t), or to use a combination of delay() for the large amount, then delayMicroseconds() for the remaining amount. Even then, this wouldn’t be 100% accurate since there is still the overhead of doing two library calls instead of just one.
ChatGPT provided me with this snippet:
void customDelayMicroseconds(unsigned long delayTimeMicros) {
unsigned long delayTimeMillis = delayTimeMicros / 1000;
unsigned long remainingMicros = delayTimeMicros % 1000;
delay(delayTimeMillis); // This will not delay if delayTimeMillis is 0
delayMicroseconds(remainingMicros); // This will handle the remaining microseconds
}
You will see this routine used in my program, below. Note that, since I am PRINTING OUT each pulse time (on, off, and time), it’s slowing it down anyway, so even with my “more accurate” delay, the timing will still be off from the Control Box. Hopefully this will not matter…
Here we go…
// LethalLilyWriter.ino
/*-----------------------------------------------------------------------------
Lethal Lily Swamp Witch animatronic Control Box writer.
By Allen C. Huffman (alsplace@pobox.com)
www.subethasoftware.com
This program writes the data to the Lethal Lily head over a
serial and digital input.
Hardware:
https://sviservice.com/collections/2023/products/sv2323794
Documentation:
CONFIGURATION:
1. Define the pins on the Arduino that will be used for TX in
. the Software Serial library (connected to the green wire), and the pulse
. sending pin (connected to the blue wire).
VERSION HISTORY:
2024-06-13 0.00 allenh - Created based on LethalLilyReader.ino
2024-06-14 0.01 allenh - Changed to PIN 13 so onboard LED will blink.
TODO:
* TODO...
TOFIX:
* TODO...
---------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------*/
// Configuration
/*--------------------------------------------------------------------------*/
#define BAUD_RATE 550
#define GREEN_WIRE_PIN 12 // Serial TX to HEAD
#define BLUE_WIRE_PIN 13 // Pulses OUT to HEAD
/*--------------------------------------------------------------------------*/
// Includes
/*--------------------------------------------------------------------------*/
#include <SoftwareSerial.h>
/*--------------------------------------------------------------------------*/
// RX pin connected to GREEN wire.
// TX pin not used.
/*--------------------------------------------------------------------------*/
SoftwareSerial mySerial(3, GREEN_WIRE_PIN); // RX, TX
/*--------------------------------------------------------------------------*/
// Globals
/*--------------------------------------------------------------------------*/
volatile unsigned int g_pulseCount = 0;
// Pattern one ON and OFF pulse time in micros.
const unsigned long g_pattern1[] = {
108336,
185892,
161408,
328592,
200140,
156160,
191696,
161860,
222340,
507668,
63712,
236864,
481116,
97364,
77224,
230036,
189148,
128228,
219716,
153316,
144596,
55636,
66692,
92192,
99984,
73600,
158388,
205228,
244916,
141988,
97352,
114224,
194472,
103252,
239248,
575908,
459728,
16744,
64608,
59992,
64688,
59916,
65168,
59624,
64788,
59820,
67140,
287636,
400560,
325912,
225148,
145256,
80220,
109232,
278032,
175556,
253068,
156324,
174984,
381868,
333784,
158776,
108328,
164388,
108324,
242136,
297388,
195164,
155604,
100400,
269976,
153988,
211304,
136536,
175524,
133576,
147248,
83664,
91740,
279872,
381224,
105904,
83312,
69700,
119660,
108528,
216900,
319000,
139236,
153120,
69424,
55812,
114036,
91828,
127924,
184272,
216976,
105800,
133452,
181064,
139080,
128108,
69424,
67880,
122192,
58644,
74852,
81140,
77672,
142156,
60888,
119856,
61256,
156028,
77752,
122588,
63792,
103344,
102604,
92016,
91632,
83948,
94256,
125492,
75016,
97204,
111228,
80860,
155744
};
//Prototypes
uint8_t convertValue (unsigned int value);
/*--------------------------------------------------------------------------*/
// Setup
/*--------------------------------------------------------------------------*/
void setup() {
// Arduino console port.
Serial.begin(9600);
while (!Serial);
// Control Box serial baud rate.
mySerial.begin(BAUD_RATE);
pinMode(BLUE_WIRE_PIN, OUTPUT);
for (int idx=0; idx<10; idx++) Serial.println();
Serial.println("LethalLillyWriter - "__DATE__" "__TIME__);
}
/*--------------------------------------------------------------------------*/
// Main loop
/*--------------------------------------------------------------------------*/
void loop()
{
Serial.println ("Sending pattern 1...");
mySerial.write (convertValue (8)); // End any sequence.
delay (1000);
mySerial.write (convertValue (1)); // Sequence 1
delay (745);
for (int idx=0; idx < sizeof(g_pattern1)/sizeof(g_pattern1[0]); idx++)
{
if (idx & 1)
{
digitalWrite(BLUE_WIRE_PIN, HIGH);
Serial.print ("On for ");
}
else
{
digitalWrite(BLUE_WIRE_PIN, LOW);
Serial.print ("Off for ");
}
Serial.println (g_pattern1[idx]);
// Large values do not work for delayMicroseconds(), so a custom
// function is used to delay using ms, then leftover time in us.
customDelayMicroseconds (g_pattern1[idx]);
}
Serial.println ("Done.");
while (1);
} // end of loop()
/*--------------------------------------------------------------------------*/
// Convert number (1-8) to Lethal Lilly byte format (0011 1100).
/*--------------------------------------------------------------------------*/
uint8_t convertValue (unsigned int value)
{
uint8_t returnByte = 0;
if ((value < 1) || (value > 8))
{
Serial.println ("convertValue must be 1-7 (pattern) or 8 (end).");
returnByte = 0;
}
else
{
returnByte = value | (~value << 4);
}
return returnByte;
}
// Created by ChatGPT.
void customDelayMicroseconds(unsigned long delayTimeMicros) {
unsigned long delayTimeMillis = delayTimeMicros / 1000;
unsigned long remainingMicros = delayTimeMicros % 1000;
delay(delayTimeMillis); // This will not delay if delayTimeMillis is 0
delayMicroseconds(remainingMicros); // This will handle the remaining microseconds
}
// End of LethalLilyWriter.ino
This program will send serial data out at 550 baud on Arduino pin 12 (which will hook to the green wire going in to the head), and pulse data on pin 13 (which will go to the blue wire in to the head). I chose pin 13 for the pulse line because it also has an LED connected to it on the UNO, and this will blink in time with the pulses being sent to the head.
The Arduino I/O pins are only 5V, while the Control Box pulses at 5.4V. I expect this may cause some differences in what the Arduino sends versus what the Control Box sends. Let’s find out!
Arduino, meet Lethal Lily’s head
I used the Control Box to provide power to the head via the red and black wires. I also connected the black ground wire to the Arduino, giving everything a common ground (bad things happen if you don’t do this, as I found out years ago when dealing with programmable LED strips using external power supplies).
My wiring looked like this:
Control Box Head Arduino UNO
----------- ----------- -----------
Green Green--------PIN 12
Red----------Red
Black--------Black--------GND
Blue Blue---------PIN 13 (build in LED)
My hope was that the Control Box could act as a power supply, providing 5.9V DC to the head, then the Arduino could act as the Control Box sending serial data on pin 12 to the head’s green wire, and pulsing data on pin 13 to the head’s blue wire.
My program sends the byte code for pattern 1, pauses (using the delay I could see from the Saleae Logic Analyer), then starts sending ON and OFF pulses using the time values I captured in the LethalLilyReader program. When the last pulse is sent, I do a short delay and then send out the “end” byte.
When I went to test it, it actually did something! The head does indeed animate, but the mouth did not move at all. Perhaps the low 5V power of the Arduino UNO’s I/O pin is not enough to match what the head expects. The Control Box sends pulses at 5.4V and perhaps at a much higher amperage than the Arduino is capable of.
This was an exciting first step.
Some questions…
- Why does the mouth not work? Is this due to the Arduino only able to pulse at 5V instead of 5.4V like the Control Box?
- What do the pulses actually mean? How can we control the servos in the head to create custom animations?
Some answers…
I tried writing a test program to send pulses at different rates, but nothing happened. It does look like the blue wire expects something beyond a digital I/O signal. I decided to take a chance and see what happens if I route power (from the Control Box red wire) in to the blue wire. I tapped a wire between the two, quickly, and saw … the mouth opened!
It looks like the blue wire has nothing to do with the servos at all! It just controls the mouth. The pulses I was capturing are only the mouth movements, to align with the audio being played by the Control Box! All the motion in the head (eyes, eyelids, neck) was just being done by the head itself when it receives the start code byte!
To test this, I made a simple program that would send the start byte code… and the head began animating. The pulses had nothing to do with the head animation (other than the mouth).
I guess I should have started there!
The bad news…
The bad news is that it looks like we cannot control the servos in the head, as-is. If we were hoping to make the animatronic look in specific directions or blink at specific times … that is not possible. While it may be possible to hack the head and get access to all the wires running to the servo, that was not I was hoping for. Also, since the blue wire needs a signal with more power than what the Arduino I/O pin can provide, there will be some extra hardware (like a relay) needed to control the mouth.
The good news…
The good news is — now we don’t have to learn how to control all those servos! The head will just “do its thing” and we can control the mouth to match custom audio. This is probably good enough for most things — an animated prop that delivers a custom spiel.
To test this, I took the power out from the Control Box (red/black wires) and ran them throuh an Arduino Relay Shield that then connected to the head’s blue/black wires. This would allow me to pulse the 5.9V coming out of the Control Box power supply to the head’s blue wire.
And it worked!
Here is the first simple test I did, where I played some audio and manually toggled the relay (that is the clicking sound you will hear) to make the mouth open/close — like a puppet :)
At this point, I believe I understand what I need to do to. By using a cheap triggered MP3 player (either an Arduino Shield, or some external board), and a relay, it should be quite easy to make Lily “speak” any audio I want her to.
My electronics (MP3 players, etc.) are in storage currently, so I am not sure how soon I can get to it, but I hope that this series will be…
To be continued…