CircuitPython

For this guide you must be running CircuitPython 3.0 Beta 1 or newer! Download the latest release from the CircuitPython releases page. Follow these steps to install or update CircuitPython.

You can use any CircuitPython Express board. Note that at this time, Gemma M0, Trinket M0 and other non-Express boards do not support rotaryio

To access the REPL/serial console, we assume you are using the Mu editor which is both a great Python editor and it has Adafruit support baked in to access the REPL via USB. You can follow this guide to get Mu set up.

CircuitPython 3.0 comes with rotaryio built in. rotaryio makes it easy to use rotary encoders. Let's take a look at a simple example.

import rotaryio
import board

encoder = rotaryio.IncrementalEncoder(board.D10, board.D9)
last_position = None
while True:
    position = encoder.position
    if last_position is None or position != last_position:
        print(position)
    last_position = position

Open the serial console (REPL) and try turning your rotary encoder. Depending on the direction you turn it, you'll see the number go up or down.

Let's look at the code.

Library Imports

First we import the libraries we need rotaryio and board. These are both built into CircuitPython and do not require any external library files.

Create Encoder

Next we create the encoder to use in our code. We're using an incremental encoder, so we're going to set it up as such. It requires you to provide the pins that you've wired your rotary encoder to. We used D10 and D9. So, we assign encoder = rotaryio.IncrementalEncoder(board.D10, board.D9).

We assign last_position = None so we can use this variable later to help identify when the rotary encoder is moving.

The Loop!

Inside our loop we begin by assigning position = encoder.position. This allows us to use position to identify the current position of the rotary encoder.

Then we have an if block. It starts with if last_position is None or position != last_position:. != means not equal to. So, this says, if the variable last_position is equal to None or position is not equal to last_position, then do whatever is indented under this line. In this case, we have print(position) intended under it which prints the current position to the serial console.

Last we assign last_position = position. This is how we begin to track the current position of the rotary encoder.

Let's break it down. The first part of our if statement is true when last_position is equal to None. Remember, we assigned last_position = None before our loop, so when the loop runs for the first time, this is true. So the first time the loop runs, it will print the position of the rotary encoder to the serial console, and the first position is always 0.

The second part of our if statement is true when position is not equal to last_position. Each time we run the loop, we assign last_position = position. If no changes to position have occurred, the two are the same and the second part of the if statement is false. So if you do not move the rotary encoder at all, the code will not print anything to the serial console. But, as soon as you do move it, position is no longer the same value, and position is no longer equal to last_position (since they're set to be equal). So, each time you move it, the second part of the if statement is true, and it prints the position to the serial console.

You can use this code to utilize the rotary encoder as an input. Let's take a look at an example.

Rotary Encoder Volume Knob and Play/Pause Button

This code allows the rotary encoder to control the volume on your computer, as well as play and pause music playing. It uses HID to emulate the media control keys on a keyboard. Be aware that once you save this code to the board, the knob will control volume and play/pause. Any other media controls will continue to work the same.

Copy the following code to your code.py file on your CIRCUITPY drive.

import rotaryio
import board
import digitalio
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode

button = digitalio.DigitalInOut(board.D12)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP

encoder = rotaryio.IncrementalEncoder(board.D10, board.D9)

cc = ConsumerControl()

button_state = None
last_position = encoder.position

while True:
    current_position = encoder.position
    position_change = current_position - last_position
    if position_change > 0:
        for _ in range(position_change):
            cc.send(ConsumerControlCode.VOLUME_INCREMENT)
        print(current_position)
    elif position_change < 0:
        for _ in range(-position_change):
            cc.send(ConsumerControlCode.VOLUME_DECREMENT)
        print(current_position)
    last_position = current_position
    if not button.value and button_state is None:
        button_state = "pressed"
    if button.value and button_state == "pressed":
        print("Button pressed.")
        cc.send(ConsumerControlCode.PLAY_PAUSE)
        button_state = None

Now, while playing music or a video, try rotating the knob slowly to the right. The volume increases! Try turning it to the left. The volume decreases. Try pressing down on the knob to pause if it's playing and play music if it's paused!

Let's take a look at the code.

First we import all of the libraries we need for this program: rotaryio, board, digitalio, ConsumerControl from adafruit_hid.consumer_control, and ConsumerControlCode from adafruit_hid.consumer_control_code. The adafruit_hid library has many features, and since we're only using two, we specifically import the parts we need so we're not importing a bunch of features we don't need.

We setup the push button switch. We assign button = digitalio.DigitalInOut(board.D12). We wired it up to D12, so that's the pin we provide. Then we set direction to INPUT and pull UP.

Next we create the rotary encoder object. We are using an incremental encoder, with the A pin wired to D10 and the B pin wired to D9. So, we assign encoder = rotaryio.IncrementalEncoder(board.D10, board.D9).

Then we instantiate the HID object by assigning cc = ConsumerControl(). Now we can use cc to call the HID functions.

With basic code, when the button switch is pressed, it will spam the state change. That is great if  you're trying to track the state or print "Button pressed!". However, in our case, we're going to be using it to send a play/pause command. The play/pause command works by sending a single command to the player, which depending on the current state of the player, it will play or pause. If the player is currently playing, it will pause it. If it is currently paused, it will play. Therefore, if we were to use basic button code, it would spam playing and pausing every time you pressed the button. This won't work for our purposes. So, we're going to create a simple state machine to see each button press as a single press, and allow us to use it for playing and pausing. To begin, we set button_state = None for use later with our state machine.

To change the volume, we're going to track the change in rotational position. If you rotate it three steps clockwise, it will increment the volume by three. If you rotate it three steps counterclockwise, it will decrement the volume by three. To track a change in position, you need to keep track of the last position and the current position. So, we assign last_position = encoder.position to create the initial position to track. This means wherever the rotary encoder is when the code first runs will be considered "0". So, if you don't like where the initial position is, you can move your rotary encoder to that position and reload the code.

Inside our loop, we find the current position by assigning current_position = encoder.position. Then, we find the change in position by subtracting last_position from current_position.

Next we have two code blocks that are quite similar. if and elif statements say, if the statement is valid, run the code indented underneath. for _ in range(position_change): takes the change in position we just calculated, and increments the volume by that amount with cc.send(ConsumerControlCode.VOLUME_INCREMENT). So, if the position_change is greater than 0, the volume will increase by position_change steps.

The second block of code is essentially the same, however, in this case, if position_change < 0, we take the negative position_change and decrement the volume by that amount.

Now that we've tracked the change and applied it to the volume, we set last_position = current_position so we can begin tracking again from that point.

Last, we have our simple state machine. We start by setting the the initial state. Remember, we assigned button_state = None before our loop. The first line checks to see if the button is pressed AND the button_state is None, and if both of these things are valid, sets button_state = "pressed". Both of these things are valid on the first click because we've now changed button_state. If you press and hold the button, nothing will appear to happen because the only thing occurring on the button press is the change in button_state.

Next we check to see if the button is released AND button_state is equal to "pressed". If both of these are valid, it prints "Button pressed." to the serial console, sends the play/pause command with cc.send(ConsumerControlCode.PLAY_PAUSE), and then sets button_state = None.

Now we're back to the previous state and able to begin again - if you were to press the button now, the button is pressed and the button_state is None which is what's required for the first part of the state machine!

This guide was first published on Jun 18, 2018. It was last updated on Jun 18, 2018. This page (CircuitPython) was last updated on Oct 13, 2019.