I have plans to build a multi-function macro keyboard using the new Raspberry Pi Pico RP2040 microcontroller and CircuitPython. While I was waiting for it to arrive, I wanted to figure out how I was going to handle controlling the audio on my computer with a rotary encoder, so I built this simple project using CircuitPython and the QT Py M0 microcontroller. Unfortunately, rotaryio has not been implemented for the RP2040 yet in CircuitPython, so there is a bonus code snippet at the end using a potentiometer to accomplish the same thing for the Pi Pico.
Parts
- Adafruit QT Py – SAMD21 Dev Board with STEMMA QT or Seeeduino XIAO – Arduino Microcontroller – SAMD21 Cortex M0+
- Rotary Encoder + Extras
- Silicone Cover Stranded-Core Wire 26AWG
3D Printed SnapFit Enclosure
I modified the enclosure I made for my Zoom/Teams mute button and computer lock button projects.

Wiring Diagram

Code
The code.py file is also available on GitHub.
import board
import time
import usb_hid
from digitalio import DigitalInOut, Direction, Pull
from rotaryio import IncrementalEncoder
from adafruit_hid.consumer_control_code import ConsumerControlCode
from adafruit_hid.consumer_control import ConsumerControl
# Change the below code for different outcomes
# https://circuitpython.readthedocs.io/projects/hid/en/latest/
# Button Press will mute
BUTTON_CODE = ConsumerControlCode.MUTE
# Rotating the encoder clockwise will increase the volume
INCREMENT_CODE = ConsumerControlCode.VOLUME_INCREMENT
# Rotating the encoder clockwise will decrease teh volume
DECREMENT_CODE = ConsumerControlCode.VOLUME_DECREMENT
# initialize as hid device
consumer_control = ConsumerControl(usb_hid.devices)
# initialize encoder on pins D0 and D1 (QT Py M0)
encoder = IncrementalEncoder(board.D0, board.D1)
# initialize encoder click on pin D2 (QT Py M0)
button = DigitalInOut(board.D2)
button.direction = Direction.INPUT
button.pull = Pull.UP
button_in = False
last_position = None
while True:
if not button.value and not button_in:
print("button press")
button_in = True
consumer_control.send(BUTTON_CODE)
time.sleep(.2)
elif button.value and button_in:
button_in = False
elif button.value and not button_in:
position = encoder.position
if last_position is not None and position != last_position:
if position > last_position:
print("rotate clockwise")
consumer_control.send(INCREMENT_CODE)
elif position < last_position:
print("rotate counter-clockwise")
consumer_control.send(DECREMENT_CODE)
last_position = position
Bonus: Pi Pico 2040 Potentiometer

import board
import time
import usb_hid
from analogio import AnalogIn
from adafruit_hid.consumer_control_code import ConsumerControlCode
from adafruit_hid.consumer_control import ConsumerControl
READ_TIME = .001
def map_function(x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
# initialize hid device as consumer control
consumer_control = ConsumerControl(usb_hid.devices)
# initialize potentiometer (pot) wiper connected to GP26_A0
potentiometer = AnalogIn(board.GP26)
# intialize the read time
last_read = time.monotonic()
# decrease volume all the way down
# this allows the volume to be set by the current value of the pot
for i in range(32):
consumer_control.send(ConsumerControlCode.VOLUME_DECREMENT)
# initalize volume and last position
current_volume = 0
last_position = 0
while True:
if time.monotonic() - last_read > READ_TIME:
position = int(map_function(potentiometer.value , 200, 65520, 0, 32))
if abs(position - last_position) > 1:
last_position = position
if current_volume < position:
while current_volume < position:
# Raise volume.
print("Volume Up!")
consumer_control.send(ConsumerControlCode.VOLUME_INCREMENT)
current_volume+= 2
elif current_volume > position:
while current_volume > position:
# Lower volume.
print("Volume Down!")
consumer_control.send(ConsumerControlCode.VOLUME_DECREMENT)
current_volume -= 2
# update last_read to current time
last_read = time.monotonic()
# handle time.monotonic() overflow
if time.monotonic() < last_read:
last_read = time.monotonic()












