Overview
Build a Media Controller
Turn an old Apple Watch into a mini iPod! Use BLE on a Feather ESP32 S3 with an ANO rotary encoder wheel to control the watch interface. The encoder wheel is reminiscent of the original clicking scroll wheel interface on the first iPods
This project features the ANO rotary encoder wheel. Itās got four directional buttons and a center button for scrolling and clicking. Itās essentially a wireless USB controller that pairs with an Apple Watch over BLE. Itās inspired by The Tiny Pod project we saw online, to make an open source and hackable version!
ESP32-S3 BLE with CircuitPython
Powered by the Adafruit Feather ESP32-S3, itās got Wifi, BLE and on board battery charging, perfect for making portable IOT and BLE projects.
The ANO Rotary encoder breakout connects over STEMMA QT, making it easy to plug and play with the Adafruit Feather.
With Appleās watchOS, you can enable the Voiceover accessibility feature to allow pairing keyboard devices over BLE. This allows you to scroll through the elements on screen and select items using the directional buttons and center click wheel.
Ā
Page last edited December 02, 2024
Text editor powered by tinymce.
Circuit Diagram
Circuit Diagram
The diagram below provides a general visual reference for wiring of the components once you get to the Assembly page. This diagram was created using the software package Fritzing.
Adafruit Library for Fritzing
Adafruit uses the Adafruit's Fritzing parts library to create circuit diagrams for projects. You can download the library or just grab individual parts. Get the library and parts from GitHub - Adafruit Fritzing Parts.
Page last edited December 02, 2024
Text editor powered by tinymce.
Watch Setup
Enable Voice Over
Go to Settings and then Accessibility. Now go to the VoiceOver sub menu.
Wait to enable Voice Over until the keyboard is paired via Bluetooth.
https://support.apple.com/guide/watch/use-a-bluetooth-keyboard-apd006574536/11.0/watchos/11.0
Set the modifier key to "caps lock". The Up button on the ANO encoder is setup to start up the keyboard with caps lock key.
Setup 3 button press with the Crown button to turn VoiceOver On/Off: https://support.apple.com/guide/watch/the-accessibility-shortcut-apda74993b58/watchos
Navigation buttons
The scroll wheel scrolls either up or down, left and right arrows do the same, center button is used for double tap to select and down button reads out the characters.
Pair Keyboard
Inside the VoiceOver settings, tap on the Keyboard sub menu. Under Device, tap on the CIRCUITPY device to pair.
Disconnect Keyboard
If you need to disconnect/forget the Feather from BLE, you can hold down the center select button on the encoder while power cycle the Feather.Ā
If you're connected to the repl you'll see "Last BLE bonding deleted, restarting.." and then the board reboots.Ā
Page last edited December 02, 2024
Text editor powered by tinymce.
Code
Once you've finished setting up your Feather ESP32-S3 with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import microcontroller
import board
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
import adafruit_ble
from adafruit_ble.advertising import Advertisement
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.standard.hid import HIDService
from adafruit_seesaw import seesaw, rotaryio, digitalio
# ios modifier
mod = Keycode.CAPS_LOCK
# keycodes
KBD_CODES = [
[Keycode.SPACE], # center
[mod], # up
[Keycode.LEFT_ARROW], # left
[Keycode.DOWN_ARROW], # down
[Keycode.RIGHT_ARROW], # right
]
i2c = board.STEMMA_I2C()
seesaw = seesaw.Seesaw(i2c, addr=0x49)
seesaw_product = (seesaw.get_version() >> 16) & 0xFFFF
print(f"Found product {seesaw_product}")
if seesaw_product != 5740:
print("Wrong firmware loaded? Expected 5740")
buttons = []
button_names = ["Select", "Up", "Left", "Down", "Right"]
button_states = []
for s in range(1, 6):
seesaw.pin_mode(s, seesaw.INPUT_PULLUP)
pin = digitalio.DigitalIO(seesaw, s)
pin_state = False
buttons.append(pin)
button_states.append(pin_state)
encoder = rotaryio.IncrementalEncoder(seesaw)
last_position = 0
if not buttons[0].value and button_states[0] is False:
button_states[0] = True
try:
import _bleio
time.sleep(2)
_bleio.adapter.erase_bonding()
time.sleep(2)
print("Last BLE bonding deleted, restarting..")
time.sleep(2)
microcontroller.reset()
except Exception: # pylint: disable=broad-except
pass
# BLE HID setup
hid = HIDService()
# keyboard & mouse HID setup
kbd = Keyboard(hid.devices)
advertisement = ProvideServicesAdvertisement(hid)
advertisement.appearance = 961
scan_response = Advertisement()
scan_response.complete_name = "CircuitPython HID"
ble = adafruit_ble.BLERadio()
if not ble.connected:
print("advertising")
ble.start_advertising(advertisement, scan_response)
else:
print("already connected")
print(ble.connections)
time.sleep(2)
while True:
# check for BLE connection
while not ble.connected:
pass
# while BLE connected
while ble.connected:
position = encoder.position
if position != last_position:
# if the encoder is turned to the right
if position > last_position:
kbd.send(Keycode.RIGHT_ARROW)
# if the encoder is turned to the left
if position < last_position:
kbd.send(Keycode.LEFT_ARROW)
# reset encoder position
last_position = position
print(f"Position: {position}")
for b in range(5):
if not buttons[b].value and button_states[b] is False:
button_states[b] = True
if b != 0:
kbd.press(*KBD_CODES[b])
print(*KBD_CODES[b])
else:
kbd.press(mod)
kbd.press(*KBD_CODES[b])
print(f"{button_names[b]} button pressed")
if buttons[b].value and button_states[b] is True:
button_states[b] = False
kbd.release_all()
print(f"{button_names[b]} button released")
# if BLE disconnects, begin advertising again
ble.start_advertising(advertisement)
Upload the Code and Libraries to the Feather ESP32-S3
After downloading the Project Bundle, plug your Feather ESP32-S3 into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Feather ESP32-S3's CIRCUITPY drive.
- lib folder
- code.py
Your Feather ESP32-S3 CIRCUITPY drive should look like this after copying the lib folder and the code.py file.
How the CircuitPython Code Works
At the top of the code are user modifiable keycodes. The defaults match the documented keycodes in the Apple Watch documentation. The code uses the caps lock key as the modifier key.
# ios modifier
mod = Keycode.CAPS_LOCK
# keycodes
KBD_CODES = [
[Keycode.SPACE], # center
[mod], # up
[Keycode.LEFT_ARROW], # left
[Keycode.DOWN_ARROW], # down
[Keycode.RIGHT_ARROW], # right
]
i2c = board.STEMMA_I2C()
seesaw = seesaw.Seesaw(i2c, addr=0x49)
seesaw_product = (seesaw.get_version() >> 16) & 0xFFFF
print(f"Found product {seesaw_product}")
if seesaw_product != 5740:
print("Wrong firmware loaded? Expected 5740")
buttons = []
button_names = ["Select", "Up", "Left", "Down", "Right"]
button_states = []
for s in range(1, 6):
seesaw.pin_mode(s, seesaw.INPUT_PULLUP)
pin = digitalio.DigitalIO(seesaw, s)
pin_state = False
buttons.append(pin)
button_states.append(pin_state)
encoder = rotaryio.IncrementalEncoder(seesaw)
last_position = 0
Erase Bonding
If you find you need to forget BLE pairing on the Feather, you can hold down the center button the encoder and reboot the board. If the board detects that that button is being held down before the loop, it will delete all BLE bonding and reset.
if not buttons[0].value and button_states[0] is False:
button_states[0] = True
try:
import _bleio
time.sleep(2)
_bleio.adapter.erase_bonding()
time.sleep(2)
print("Last BLE bonding deleted, restarting..")
time.sleep(2)
microcontroller.reset()
except Exception: # pylint: disable=broad-except
pass
# BLE HID setup
hid = HIDService()
# keyboard & mouse HID setup
kbd = Keyboard(hid.devices)
advertisement = ProvideServicesAdvertisement(hid)
advertisement.appearance = 961
scan_response = Advertisement()
scan_response.complete_name = "CircuitPython HID"
ble = adafruit_ble.BLERadio()
if not ble.connected:
print("advertising")
ble.start_advertising(advertisement, scan_response)
else:
print("already connected")
print(ble.connections)
time.sleep(2)
The Loop
In the loop, the encoder sends the right arrow command when turned to the right and sends the left arrow command when turned to the left. The up, down, left, right and center buttons send the keycodes listed in the KBD_CODES array.
while True:
# check for BLE connection
while not ble.connected:
pass
# while BLE connected
while ble.connected:
position = encoder.position
if position != last_position:
# if the encoder is turned to the right
if position > last_position:
kbd.send(Keycode.RIGHT_ARROW)
# if the encoder is turned to the left
if position < last_position:
kbd.send(Keycode.LEFT_ARROW)
# reset encoder position
last_position = position
print(f"Position: {position}")
for b in range(5):
if not buttons[b].value and button_states[b] is False:
button_states[b] = True
if b != 0:
kbd.press(*KBD_CODES[b])
print(*KBD_CODES[b])
else:
kbd.press(mod)
kbd.press(*KBD_CODES[b])
print(f"{button_names[b]} button pressed")
if buttons[b].value and button_states[b] is True:
button_states[b] = False
kbd.release_all()
print(f"{button_names[b]} button released")
# if BLE disconnects, begin advertising again
ble.start_advertising(advertisement)
Page last edited December 02, 2024
Text editor powered by tinymce.
3D Printing
3D Printed Parts
STL files for 3D printing will need to be oriented for print using either FDM or SLS machines.
Parts were built with Translucent Clear PLA filament.
Original design source files may be downloaded using the links below.
Slice with Settings for PLA materialĀ
The parts were sliced using CURA using the slice settings below.
- PLA filament 200c extruder
- 0.2 layer height
- 10% gyroid infill
- 60mm/s print speed
- 60c heated bed
Page last edited December 02, 2024
Text editor powered by tinymce.
Assembly
Solder Slide Switch
Measure wires for the slide switch to reach the cutouts on the case, about 25mm long.
ANO Rotary Breakout and Encoder Wheel
Install the rotary wheel to the ANO STEMMA QT breakout by orienting pins in the correct the holes.
Solder all of the pins on the ANO rotary encoder wheel to the ANO STEMMA QT breakout.
Connect a 50mm long STEMMA cable to the right STEMMA port as shown.
Mount Lipo Battery
Thread the battery wire through the cutout on the case. Align the battery lid part to the snaps on each side of the case.
Connect battery to Feather
Plug the battery into the Feather before mounting the board to the case.Ā
Align the USB C port on the Feather to the cutout on the case.
Use M2.5x5mm screws on the screws holes near the USB C port.
Finish mounting the Feather board with M2x5mm screws on the smaller screw holes.
Press Fit the Slide Switch
Angle the slide switch and press fit into the cutout on the side of the case.
Connect ANO rotary
Plug the STEMMA cable into the port on the Feather.
Align the ANO board with the Adafruit logo pointing up and the through hole pins pointing down towards the USB C port as shown.
The ANO rotary board mounts to the standoffs with M2.5x5mm screws.
Attach Watch
Align the Apple Watch crown and button to the cutouts on the case.
The screen lid press fits onto the snaps on the side of the case.
Attach ANO lid
Align the cutout on the case lid to the ANO and press fit on the snaps on each Ā side of the case.
Page last edited December 02, 2024
Text editor powered by tinymce.