Defining motions in motion.json with excessive speed or intensity (short duration_ms, large angle changes, high speed values for turnX(), etc.) can damage the servo motor or its gears/mounting. Tune motion parameters carefully and test at low intensity first. The author takes no responsibility for any servo damage, injury, or other loss resulting from custom motion data.
Please do not run this on a Takao-version Stack-chan, especially one using SG90-series servos.
The Repair Kit for Stackchan includes a replacement 7-pin cable. We recommend picking one up in case it breaks.
Firmware for an M5Stack CoreS3 based Stack-chan style robot.
This project does the following:
- Plays
/dance/dance1.mp3from the SD card - Shows an
M5Stack-Avatarface on the display - Moves the mouth based on audio level
- Runs scheduled servo motions defined in JSON files
- Switches motions at absolute times from the schedule
- Shows battery level / charging status as an icon on the avatar
- Board:
M5Stack CoreS3 - Framework:
ArduinoviaPlatformIO - Motion servo driver:
SCServo - Display avatar:
M5Stack-Avatar - Audio playback:
ESP8266Audio
Files in data/ are copied to the SD card.
data/dance/dance1.mp3Audio file played by the firmware.data/dance/dance1.jsonMotion schedule file fordance1. Additional songs can be added asdata/dance/dance2.json/dance2.mp3, etc.data/dance/motion.jsonMotion library file, shared across all songs indata/dance/.data/dance/dance_list.yamlSong list file. Up to 10mp3/dancefilename pairs can be specified. Used by Button A (play list in order) and Button B (play list shuffled).data/yaml/SC_BasicConfig.yamlServo initial-setup file forstackchan-arduino. Defines servo pin numbers, offsets, center angle, range of motion, and servo type.
On the device, these files are read from the SD card root as:
/dance/dance1.mp3/dance/dance1.json/dance/motion.json/dance/dance_list.yaml/yaml/SC_BasicConfig.yaml
This file defines when each motion starts for one song.
Example:
{
"motions": [
{ "motion": "intro", "start_ms": 2000 },
{ "motion": "shakehead", "start_ms": 15000 },
{ "motion": "v", "start_ms": 40000 }
]
}Fields:
motion: Motion name defined inmotion.jsonstart_ms: Absolute start time from playback start
Important behavior:
start_mshas priority- When a new motion starts, the previous motion is stopped
- Each motion repeats automatically until the next motion's
start_ms - The last scheduled motion runs once
This file defines named motion patterns.
Example:
{
"Motion": {
"intro": [
{ "x": 0, "y": 45, "duration_ms": 500 },
{ "y": 30, "speed": 300, "is_cw": true, "millis_for_move": 700 },
{ "x": 20, "y": 45, "duration_ms": 500 },
{ "x": 0, "y": 45, "duration_ms": 500 }
]
}
}Fields for each step:
x: X-axis position from -100% to 100%. -100% maps tolower_limit, 0% to the midpoint, and 100% toupper_limity: Y-axis position from 0% to 100%. 0% maps tolower_limitand 100% toupper_limit. For rotation steps, if specified, Y starts moving overmillis_for_moveandturnX()starts immediately without waiting for the move to completeduration_ms: Duration of this stepspeed: Optional. Ifspeed,is_cw, andmillis_for_moveare all present, executestackchanSERVO::turnX()is_cw: Optional clockwise flag forturnX()millis_for_move: Optional rotation duration forturnX(). Ifduration_msis omitted, this value is used as the step duration
Notes:
t_msis no longer used- Step timing is calculated by accumulating
duration_ms - Rotation is ignored unless all of
speed,is_cw, andmillis_for_moveare specified - For rotation steps,
xis not used - Out-of-range
xandyvalues are clamped to their respective minimum or maximum
Defines the song list. Up to 10 mp3 / dance (motion schedule) filename pairs can be specified. Filenames are relative to the data/dance/ folder.
Example:
dance_list:
- mp3: "dance1.mp3"
dance: "dance1.json"
- mp3: "dance2.mp3"
dance: "dance2.json"This file is loaded at startup and used for button-triggered playback:
- Button A: Plays every song in
dance_list.yaml, in list order - Button A (long-press, 10 seconds): Powers off this unit. If any peers are configured (see ESP-NOW Multi-Device Sync), it also broadcasts a power-off command so every configured peer shuts down too.
- Button B: Plays every song in
dance_list.yaml, in shuffled (random) order - Button C: Always plays the first song in
dance_list.yaml - Button C (long-press, 2 seconds): Enters random motion mode — plays every song in
dance_list.yamlin order, but instead of following that song'sdanceschedule file, picks a random motion frommotion.jsonevery 2 seconds and repeats it until the next pick. When a song ends, the currently running motion is cut short and the head returns to front over 1 second before the next song starts.
For each song, the shared motion.json motion library is reused, but that song's dance (motion schedule) file is (re)loaded before playback (except in random motion mode, which never loads a dance schedule file). Button presses are ignored while playback is in progress; the firmware blocks until playback finishes (the whole list for Button A/B/long-press-C, a single song for a short Button C press). If dance_list.yaml is missing or empty, it falls back to playing dance1.mp3 alone (with dance1.json for normal playback, or random motions for random motion mode). Each button press plays a short confirmation tone.
Multiple Stack-chan units can play the same song and start dancing at the same time, using ESP-NOW (no Wi-Fi access point required).
- Whichever unit's button is pressed becomes the leader for that song: it broadcasts the mp3/dance filenames to its configured peers.
- Every peer that receives the command loads the same-named files from its own SD card into PSRAM, then replies with a "ready" message.
- The leader waits for all configured peers to become ready (or a 5-second timeout), then broadcasts a "start" signal so every unit begins playback and dancing at the same moment.
- If a peer doesn't respond in time, the leader proceeds with whichever peers are ready.
- This also applies to the random motion mode (Button C long-press): the leader still broadcasts which song to play, and additionally broadcasts the name of each randomly-picked motion every 2 seconds, so peers execute the exact same motion at the exact same time instead of picking their own.
- Copy
data/yaml/SC_SecConfig.yaml.sampletodata/yaml/SC_SecConfig.yaml. - Flash each unit once and check the Serial log for a line like
Own MAC: AA:BB:CC:DD:EE:FFto find its MAC address. - List the MAC addresses of all units (including each unit itself) under
espnow.peersinSC_SecConfig.yaml. Each unit automatically excludes its own address, so you can copy the exact same file onto every unit's SD card. - Copy
SC_SecConfig.yamlto/yaml/SC_SecConfig.yamlon every unit's SD card, alongside the samedata/dance/files (same filenames on every unit).
SC_SecConfig.yaml identifies your own hardware, so it's excluded via .gitignore and never committed — only the .sample template is tracked.
If SC_SecConfig.yaml is missing or has no peers, the firmware runs standalone exactly as before.
- No arbitration if two units are used as leader at the same time (e.g. two buttons pressed simultaneously).
- A unit already playing a song ignores incoming play commands from other leaders.
- Synchronization accuracy depends on Wi-Fi channel conditions; this is a best-effort sync, not a hard real-time guarantee.
Build for CoreS3:
~/.platformio/penv/bin/pio run -e m5stack-cores3Upload firmware:
~/.platformio/penv/bin/pio run -e m5stack-cores3 -t uploadThe JSON and MP3 files in data/ must be copied to the SD card root.
Copy these files to the SD card:
dance/dance1.mp3dance/dance1.jsondance/dance_list.yamldance/motion.jsonyaml/SC_BasicConfig.yamlyaml/SC_SecConfig.yaml(optional, only needed for ESP-NOW multi-device sync)
Recommended update order:
- Update files in
data/ - Copy the changed files to the SD card root
- Upload firmware if code changed
If only JSON or MP3 files changed, updating the SD card is enough.
During playback, the firmware prints the motion name only when a motion starts.
Example:
intro
shakehead
v
- This project reads data from the SD card, not from
SPIFFS - If you rename or modify files in
data/, copy them to the SD card again - If a scheduled motion name does not exist in
motion.json, startup fails - Before playback, the whole MP3 file is loaded into PSRAM, then played from there. This avoids audio glitches caused by SD card read latency. If the PSRAM allocation fails (e.g. not enough free PSRAM), that song is skipped