A radial controller is a HID device that transmits information about something that turns, such as a knob. Microsoft supports certain kinds of radial controllers on Windows, and sells the Microsoft Surface Dial, which is a radial controller with haptic feedback. A radial controller reports relative rotation changes as you turn its dial, and has a momentary switch you activate by pressing down.

The example below is a simple implementation of a radial controller, without haptic feedback, that works on Windows 10 or later. If you use it on Windows, the interactions will be similar to those described by Microsoft for the Surface Dial.

macOS and Linux don't support radial controllers right now, so try this project on Windows.

Software and Hardware Components

There are three pieces of software you use to implement this radial controller in CircuitPython. For hardware, you'll use a rotary encoder with a built-in switch.

  1. The adafruit_radial_controller library (documentation), which is in the Adafruit library bundle.
  2. A boot.py file, which creates the device before USB starts.
  3. A code.py file, which monitors a rotary encoder and passes state changes via the library. The code.py file also needs the adafruit_hid and adafruit_debouncer libraries.

This example is written for an Adafruit Rotary Trinkey, but could be adapted for any board with a rotary encoder connected. You will need to change the pin names for other boards.

Note: The current radial controller report descriptor used in this example works, but is not perfect. We are still experimenting with some refinements.

This screnshot shows what you'll see on your CIRCUITPY drive when you have everything you need, except that you also need the boot.py file, which is missing from this picture.


The Rotary Trinkey will be easier to use if you mount it in something that can sit on your desk. For a finished project, you can make a 3D-printed enclosure reminiscent of the Media Dial or the Microsoft Surface dial, with a base and a large top knob. But for a quick trial you can just mount the Trinkey in a food container or similar, as shown in the photos.

Installing the Project Code

Use the Download Project Bundle button in either code block below to download the project code. Unzip the download, and copy boot.py, code.py and the lib folder to your board.


The boot.py file in this project creates the radial controller device, and sets it up to be available when code.py starts.

# SPDX-FileCopyrightText: Copyright (c) 2021 Dan Halbert for Adafruit Industries
# SPDX-License-Identifier: Unlicense

import usb_hid

import adafruit_radial_controller.device


radial_controller_device = adafruit_radial_controller.device.device(REPORT_ID)


The code.py program in this project first creates an adafruit_radial_controller.RadialController. Then it loops forever, continuously checks the state of the switch on the rotary encoder, and whether the encoder position has changed. If so, it sends a report with the changed state information. The encoder change is relative to the previous position.

A single click of the encoder in either direction is a rotaryio.IncrementalEncoder.position change of just 1 or -1. But that value is not sent directly. Experimentation showed that such a small value is sometimes ignored by Windows unless many occur in quick succession. So this multiplies the actual position change by DEGREE_TENTHS_MULTIPLIER, which is 100 in the code. You can experiment with other multiplier values if you'd like. Try 20 or 50, for example.

The rotation value sent is an integer, in units of tenths of a degree. So for instance, a sending a 1 represents a 0.1 degree rotation. This is not actually the rotation angle of the physical encoder we're using, but it is what the Microsoft driver expects.

# SPDX-FileCopyrightText: Copyright (c) 2021 Dan Halbert for Adafruit Industries
# SPDX-License-Identifier: Unlicense

import board
import digitalio
import rotaryio
import usb_hid

from adafruit_debouncer import Debouncer
import adafruit_radial_controller

switch = digitalio.DigitalInOut(board.SWITCH)
switch.pull = digitalio.Pull.DOWN
debounced_switch = Debouncer(switch)

encoder = rotaryio.IncrementalEncoder(board.ROTA, board.ROTB)

radial_controller = adafruit_radial_controller.RadialController(usb_hid.devices)

last_position = 0

while True:
    if debounced_switch.rose:
    if debounced_switch.fell:

    position = encoder.position
    delta = position - last_position
    if delta != 0:
        radial_controller.rotate(delta * DEGREE_TENTHS_MULTIPLIER)
        last_position = position

This guide was first published on Oct 01, 2021. It was last updated on Feb 29, 2024.

This page (Radial Controller) was last updated on Feb 27, 2024.

Text editor powered by tinymce.