Firmware for turning an M5Stack StopWatch into a voice-coding BLE keyboard badge for macOS.
The badge does not act as a microphone. It behaves like a Bluetooth keyboard and sends shortcuts to the Mac. Speech recognition is handled by the Mac-side input method, such as WeChat Input.
See CHANGELOG.md for update history.
- BLE device name:
537VoiceCoding. - Yellow button A hold: after about 0.18 seconds, hold
Right Optionand release it when the button is released. This is designed for WeChat Input push-to-talk voice input configured as "hold Option to speak". - Yellow button A double click: enter hands-free voice mode by holding
Right Option; tap A again to stop dictation. In hands-free mode, a short press on blue button B also stops dictation, waits about 1.1 seconds for the text to appear, then sendsEnter. - Blue button B short press: send
Enter; no vibration. - Blue button B long press: if voice input is active, stop dictation and send
Cmd + Zto undo the newly inserted text. If voice input ended recently, long press also sendsCmd + Zwithin an 8-second window. Otherwise it sendsEsc. Cancel keeps the triple-vibration feedback. - Hold blue button B while booting: enter test mode. In test mode, yellow button A types
voice badge testto confirm that the BLE keyboard link is working. - Yellow button A + blue button B pressed together: manually toggle the sleep clock on or off, with one vibration as confirmation.
- Touch Usagi while Ready: play a random
waitingorwaiting2animation with its paired PCM sound, without sending any keyboard input or vibration. - Hold and swipe Usagi left while Ready: loop
running-leftuntil release, without sending keyboard input or vibration. - Hold and swipe Usagi right while Ready: loop
running-rightuntil release, without sending keyboard input or vibration.
The red power button is connected to the PMIC and is not readable by the firmware. It still handles the StopWatch's hardware power behavior: quick double press to power off, long press/reset behavior as defined by the device.
- Auto dim: after 30 seconds without button activity, the screen brightness drops from 120 to 45. Any button press or BLE reconnect restores brightness. Voice input prevents auto dimming.
- Battery status: shown only while charging or when the battery is 20% or lower. It replaces the Ready-state top dots and refreshes about every 5 seconds. Charging shows a yellow lightning icon.
- BLE battery reporting: the firmware reports the real battery level through the BLE Battery Service, so macOS can show the badge battery level in the Bluetooth menu.
- Sleep clock: after 5 minutes without activity, the display enters a low-brightness sleep clock. BLE stays connected and the buttons still work.
- Sleep clock display: animation is hidden; the screen shows Beijing time in
HH:MMformat and an English weekday such asTHU. The RTC is initialized at flash/build time using theAsia/Shanghaitimezone and then keeps time on-device. - Raise to wake: while sleeping, the IMU detects pickup/movement and wakes the display. Any button press also wakes the display.
The screen shows a centered Usagi animation and a compact top status indicator.
- Pairing: Usagi
jumping - Ready: Usagi
idle, with slow animated dots at the top. Charging or low battery replaces the dots with centered battery status. - Voice input: Usagi
running, with a white microphone and animated bars at the top. - Sent: Usagi
sent, with the sent status label. - Cancelled/undo: Usagi
failed, with the cancel status label. - Ready touch action: tap Usagi for
waitingorwaiting2, swipe left forrunning-left, or swipe right forrunning-right.
Generated animation assets are committed in src/usagi_animations.*. The helper script is tools/generate_usagi_assets.py.
The StopWatch has no Wi-Fi in this firmware. Weather is pushed from the Mac over BLE through a custom characteristic.
Related files:
tools/mac_weather_push.py: fetches weather on the Mac and writes a compact payload to the badge.tools/com.voicebadge.weather.plist: launchd example for running the weather push periodically.src/weather_icons.*: weather icons used by the sleep clock.
The helper uses the device name 537VoiceCoding and writes to characteristic 0xFFF1.
- Install and enable WeChat Input.
- Open WeChat Input settings.
- Set the voice input push-to-talk shortcut to
Option. - Open any text field, such as Notes, Codex, Claude Code, or a terminal prompt.
- Flash this firmware to the StopWatch.
- Open macOS System Settings -> Bluetooth.
- Find
537VoiceCoding. - Connect it.
- For the first test, boot while holding blue button B to enter test mode.
- Open Notes and press yellow button A.
- If
voice badge testappears, the BLE keyboard link is working.
- Open Codex, Claude Code, a terminal, or any other text input target.
- Make sure WeChat Input is active.
- Put the cursor in the text field.
- Hold yellow button A to speak; release it to finish dictation.
- You can also double click yellow button A for hands-free voice mode, then tap A again to finish.
- Short press blue button B to send. In hands-free voice mode, short pressing B stops dictation first, waits about 1.1 seconds, then sends.
- Long press blue button B to cancel/undo. During voice input, cancel stops dictation and then undoes the newly inserted text. If text has already appeared, long press B can still undo it within about 8 seconds.
Install PlatformIO, then run:
platformio runConnect the StopWatch over USB-C, then check the serial port:
platformio device listFlash the firmware:
platformio run -t upload --upload-port /dev/cu.usbmodem14201If your serial port is different, replace /dev/cu.usbmodem14201 with the port shown by platformio device list.
This project uses the PlatformIO configuration from the M5Stack StopWatch documentation:
platform = espressif32 @ 6.12.0board = esp32s3boxframework = arduinoM5UnifiedM5GFXM5PM1M5IOE1
It also uses ESP32 BLE Keyboard and NimBLE-Arduino.