How to Create a DIY Star Wars Lo-La59 Shoulder Buddy with ESP32 Home Assistant Integration
- Logan Morris
- May 28, 2024
- 5 min read
Updated: Jun 2, 2024
In this article, we will look at how to create a Home Assistant ESP32 powered Lola-59 Shoulder Buddy.
Requirements
Home Assistant up and running.
ESPHome is setup and running.
Seeed Studio XIAO ESP32S3
Parts Required
Preparing the Lo-La59 Droid
Congratulations! You've purchased the Lo-La59 droid and are preparing to mod it for miles of fun and automation. A Dremel of any kind is heavily recommended as you will basically be removing all of the inside of the toy to make room for the SG90 servos, DFPlayer, Step-Up Converter/LiPo Battery Charger and the ESP32S3 Board.
Remove the entire area underneath the wings as that is where the servos will go. You can watch the video here so you can get an idea of where they go.
Once you've done that level out the inside of the droid with the Dremel so that you can get a small piece of flat plastic in there so the servos can be placed on top of it.
The droid comes with a small PCB board inside of it. Remove it as you will not need it anymore. You can place the ESP32S3 in its place if you want or you can mount it somewhere else inside the droid.
The wings have notches in it that allowed it to snap in place before. You are going to smooth those out with the Dremel so that wings can move freely up and down.
Because of how the wing moves you cant attach the servo arm directly to it to move. Attach a tight coiled spring to the servo arm and the wing. The tension will cause the wing to move up and down
The USB-C power input and power switch are placed underneath the droid, but feel free to put them where you feel like they should go.
Where the battery slot was, I cut out all of that and placed a slim piece of plastic right over the slot and mounted the step up converter and DFPlayer there.
The 8 Ohm speaker can either go where it originally was or you can mount it in the wing like I did.
You may find it difficult to put everything together depending on where you place things so I used JST connectors to connect things even for power and ground.
Note: Its going to be scary cutting into the toy, But don't be shy, anything don't on the inside wont be seen from the top and hot glue/gorilla glue will be your best friend.
Schematics for the Wiring

Coding the ESP32
Below is the code for the droid. You can find a video of the breakdown of the code here.
substitutions:
name: esphome-web-812b34
friendly_name: LOLA_S-SEED
esphome:
name: ${name}
friendly_name: ${friendly_name}
name_add_mac_suffix: false
platformio_options:
board_build.flash_mode: dio
project:
name: esphome.web
version: '1.0'
on_boot:
- priority: -800
then:
- delay: 5s
- light.turn_on:
id: lola_light
- dfplayer.set_volume: 20
esp32:
board: esp32-s3-devkitc-1
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
services:
- service: dfplayer_next
then:
- dfplayer.play_next:
- service: dfplayer_previous
then:
- dfplayer.play_previous:
- service: dfplayer_play
variables:
file: int
then:
- dfplayer.play: !lambda 'return file;'
- service: dfplayer_play_loop
variables:
file: int
loop_: bool
then:
- dfplayer.play:
file: !lambda 'return file;'
loop: !lambda 'return loop_;'
- service: dfplayer_play_folder
variables:
folder: int
file: int
then:
- dfplayer.play_folder:
folder: !lambda 'return folder;'
file: !lambda 'return file;'
- service: dfplayer_play_loop_folder
variables:
folder: int
then:
- dfplayer.play_folder:
folder: !lambda 'return folder;'
loop: true
- service: dfplayer_set_device_tf
then:
- dfplayer.set_device: TF_CARD
- service: dfplayer_set_device_usb
then:
- dfplayer.set_device: USB
- service: dfplayer_set_volume
variables:
volume: int
then:
- dfplayer.set_volume: !lambda 'return volume;'
- service: dfplayer_set_eq
variables:
preset: int
then:
- dfplayer.set_eq: !lambda 'return static_cast<dfplayer::EqPreset>(preset);'
- service: dfplayer_sleep
then:
- dfplayer.sleep
- service: dfplayer_reset
then:
- dfplayer.reset
- service: dfplayer_start
then:
- dfplayer.start
- service: dfplayer_pause
then:
- dfplayer.pause
- service: dfplayer_stop
then:
- dfplayer.stop
- service: dfplayer_random
then:
- dfplayer.random
- service: dfplayer_volume_up
then:
- dfplayer.volume_up
- service: dfplayer_volume_down
then:
- dfplayer.volume_down
uart:
tx_pin: GPIO02
rx_pin: GPIO01
baud_rate: 9600
dfplayer:
on_finished_playback:
then:
logger.log: 'Playback finished event'
#########################################################################################
#Global Integers
globals:
- id: randomDelay
type: int
restore_value: no
initial_value: '0'
###############################################################################################
#Light
light:
- platform: binary
name: "Lola Bottom Lights"
output: bottom_blue
id: lola_light
#Outputs
output:
- platform: ledc
id: bottom_blue
pin: GPIO05
- platform: ledc
id: pwm_output
pin: GPIO04
frequency: 50 Hz
- platform: ledc
id: pwm_output2
pin: GPIO09
frequency: 50 Hz
############################################################################################
#Servo Code
servo:
- id: right_wing
output: pwm_output
- id: left_wing
output: pwm_output2
number:
- platform: template
id: servo_value
name: Right Wing
min_value: -100
initial_value: 0
max_value: 100
step: 1
optimistic: true
set_action:
then:
- servo.write:
id: right_wing
level: !lambda 'return x / 100.0;'
- platform: template
id: servo_value1
name: Left Wing
min_value: -100
initial_value: 0
max_value: 100
step: 1
optimistic: true
set_action:
then:
- servo.write:
id: left_wing
level: !lambda 'return x / 100.0;'
##############################################################################################
#Switch (Button)
binary_sensor:
- platform: gpio
pin:
number: 6
mode:
input: true
pullup: true
name: Lola Button"
id: lola_button
filters:
- invert:
on_click:
- min_length: 50ms
max_length: 350ms
then:
- script.execute: wings_animation
- min_length: 3s
max_length: 5s
then:
- script.stop: wings_animation
- delay: 3s
- script.execute: wings_closed
##############################################################################################
#Scripts#
script:
- id: wing_shuttering
then:
- dfplayer.play: 5
- delay: .004s
- lambda: id(right_wing).write(-.55); #.48%
- delay: .004s
- lambda: id(left_wing).write(.52);
- delay: .01s
- lambda: id(right_wing).write(-.26); #.48%
- delay: .004s
- lambda: id(left_wing).write(.39);
- delay: .01s
- delay: .004s
- lambda: id(right_wing).write(-.55); #.48%
- delay: .004s
- lambda: id(left_wing).write(.52);
- delay: .01s
- lambda: id(right_wing).write(-.26); #.48%
- delay: .004s
- lambda: id(left_wing).write(.39);
- delay: .01s
- delay: .004s
- lambda: id(right_wing).write(-.55); #.48%
- delay: .004s
- lambda: id(left_wing).write(.52);
- delay: .01s
- lambda: id(right_wing).write(-.26); #.48%
- delay: .004s
- lambda: id(left_wing).write(.39);
- delay: .01s
- delay: .004s
- lambda: id(right_wing).write(-.55); #.48%
- delay: .004s
- lambda: id(left_wing).write(.52);
- delay: .01s
- lambda: id(right_wing).write(-.26); #.48%
- delay: .004s
- lambda: id(left_wing).write(.39);
- delay: .01s
- delay: .004s
- lambda: id(right_wing).write(-.55); #.48%
- delay: .004s
- lambda: id(left_wing).write(.52);
- delay: .01s
- lambda: id(right_wing).write(-.26); #.48%
- delay: .004s
- lambda: id(left_wing).write(.39);
- delay: .01s
- delay: .004s
- lambda: id(right_wing).write(-.55); #.48%
- delay: .004s
- lambda: id(left_wing).write(.52);
- delay: .01s
- lambda: id(right_wing).write(-.26); #.48%
- delay: .004s
- lambda: id(left_wing).write(.39);
- delay: .01s
- dfplayer.play: 17
- id: wings_open
then:
- lambda: id(right_wing).write(.27);
- lambda: id(left_wing).write(.13);
- id: wings_closed
then:
- lambda: id(right_wing).write(-1.0);
- lambda: id(left_wing).write(1.0);
- id: wings_animation
mode: queued
then:
- light.turn_on: lola_light
- lambda: id(right_wing).write(-1.0); #MIN-R
- lambda: id(left_wing).write(.13); #MAX-L
- delay: 2s
- lambda: id(left_wing).write(1.0); #MIN-L
- lambda: id(right_wing).write(.27); #MAX-R
- delay: 2s
- lambda: id(right_wing).write(-1.0); #MIN-R
- lambda: id(left_wing).write(.13); #MAX-L
- delay: 2s
- script.execute: wing_shuttering
- lambda: id(left_wing).write(1.0); #MIN-L
- lambda: id(right_wing).write(.27); #MAX-R
- delay: 2s
- lambda: id(right_wing).write(-1.0); #MIN-R
- lambda: id(left_wing).write(.13); #MAX-L
- delay: 2s
- lambda: id(left_wing).write(1.0); #MIN-L
- lambda: id(right_wing).write(.27); #MAX-R
- delay: 3s
- dfplayer.play: 18
- delay: 3s
- script.execute: wing_shuttering
- delay: 3s
- script.execute: wings_open
- delay: .5s
- script.execute: wings_closed
- delay: .5s
- script.execute: wings_open
- delay: .5s
- script.execute: wings_closed
- delay: .5s
- dfplayer.play: 5
- script.execute: wing_shuttering
- delay: .5s
- script.execute: wings_animation
# Allow Over-The-Air updates
ota:
# Allow provisioning Wi-Fi via serial
improv_serial:
wifi:
# Set up a wifi access point
ap:
ssid: "Logan ESP"
password: ""
# In combination with the `ap` this allows the user
# to provision wifi credentials to the device via WiFi AP.
captive_portal:
dashboard_import:
package_import_url: github://esphome/firmware/esphome-web/esp32s3.yaml@v2
import_full_config: true
# Sets up Bluetooth LE (Only on ESP32) to allow the user
# to provision wifi credentials to the device.
esp32_improv:
authorizer: none
# To have a "next url" for improv serial
web_server:
Congratulations you are done! Rey thanks you!
Comentarios