Once your CPB is set up with CircuitPython, you'll also need to add some libraries. Follow this page for info on how to download and add libraries to your CPB.

From the library bundle you downloaded in that guide page, transfer the following libraries onto the CPB board's /lib directory:

  • adafruit_ble
  • adafruit_hid
  • neopixel

Text Editor

Adafruit recommends using the Mu editor for using your CircuitPython code with the Circuit Playground Bluefruit boards. You can get more info in this guide.

Alternatively, you can use any text editor that saves files.

Code.py

Copy the code below and paste it into Mu. Then, save it to your CPB as code.py.

# SPDX-FileCopyrightText: 2020 John Park for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
A CircuitPython 'multimedia' dial demo
Uses a Circuit Playground Bluefruit + Rotary Encoder -> BLE out
Knob controls volume, push encoder for mute, CPB button A for Play/Pause
Once paired, bonding will auto re-connect devices
"""

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

import adafruit_ble
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.standard.hid import HIDService
from adafruit_ble.services.standard.device_info import DeviceInfoService


ble = adafruit_ble.BLERadio()
ble.name = "Bluefruit-Volume-Control"
# Using default HID Descriptor.
hid = HIDService()
device_info = DeviceInfoService(software_revision=adafruit_ble.__version__,
                                manufacturer="Adafruit Industries")
advertisement = ProvideServicesAdvertisement(hid)
cc = ConsumerControl(hid.devices)

FILL_COLOR = (0, 32, 32)
UNMUTED_COLOR = (0, 128, 128)
MUTED_COLOR = (128, 0, 0)
DISCONNECTED_COLOR = (40, 40, 0)

# NeoPixel LED ring
# Ring code will auto-adjust if not 16 so change to any value!
ring = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=0.05, auto_write = False)
ring.fill(DISCONNECTED_COLOR)
ring.show()
dot_location = 0  # what dot is currently lit

# CPB button for Play/Pause
button_A = digitalio.DigitalInOut(board.BUTTON_A)
button_A.switch_to_input(pull=digitalio.Pull.DOWN)

button_a_pressed = False  # for debounce state

# Encoder button is a digital input with pullup on A1
# so button.value == False means pressed.
button = digitalio.DigitalInOut(board.A1)
button.pull = digitalio.Pull.UP

encoder = rotaryio.IncrementalEncoder(board.A2, board.A3)

last_pos = encoder.position
muted = False
command = None
# Disconnect if already connected, so that we pair properly.
if ble.connected:
    for connection in ble.connections:
        connection.disconnect()


def draw():
    if not muted:
        ring.fill(FILL_COLOR)
        ring[dot_location] = UNMUTED_COLOR
    else:
        ring.fill(MUTED_COLOR)
    ring.show()


advertising = False
connection_made = False
print("let's go!")
while True:
    if not ble.connected:
        ring.fill(DISCONNECTED_COLOR)
        ring.show()
        connection_made = False
        if not advertising:
            ble.start_advertising(advertisement)
            advertising = True
        continue
    else:
        if connection_made:
            pass
        else:
            ring.fill(FILL_COLOR)
            ring.show()
            connection_made = True

    advertising = False

    pos = encoder.position
    delta = pos - last_pos
    last_pos = pos
    direction = 0

    if delta > 0:
        command = ConsumerControlCode.VOLUME_INCREMENT
        direction = -1
    elif delta < 0:
        command = ConsumerControlCode.VOLUME_DECREMENT
        direction = 1

    if direction:
        muted = False
        for _ in range(abs(delta)):
            cc.send(command)
            # spin neopixel LED around in the correct direction!
            dot_location = (dot_location + direction) % len(ring)
            draw()

    if not button.value:
        if not muted:
            print("Muting")
            cc.send(ConsumerControlCode.MUTE)
            muted = True
        else:
            print("Unmuting")
            cc.send(ConsumerControlCode.MUTE)
            muted = False
        draw()
        while not button.value:  # debounce
            time.sleep(0.1)

    if button_A.value and not button_a_pressed:  # button is pushed
        cc.send(ConsumerControlCode.PLAY_PAUSE)
        print("Play/Pause")
        button_a_pressed = True  # state for debouncing
        time.sleep(0.05)

    if not button_A.value and button_a_pressed:
        button_a_pressed = False
        time.sleep(0.05)

Code Explainer

Here are the main things our code does:

  • Loading libraries for time, digitalio, rotaryio, neopixel, hid consumer control (media buttons), and adafruit BLE
  • Advertising that it is a BLE device that can be connected to
  • setting up the button A on the CPB and rotary encoder for reading
  • lighting up the NeoPixels yellow until there's a connection made
  • lighting the NeoPixels cyan, with one brighter NeoPixel to represent relative volume position
  • lighting the NeoPixels red when the push encoder is pressed to mute the volume

Pairing and Bonding

One of the more advanced features used in this project is BLE bonding.

When the Central (your mobile device or computer) connects with the Peripheral (the CPB), you will be asked on the mobile device or computer if you want to Pair with the CPB. Once you agree to pair, a bonding process takes place.

During bonding, encrypted keys are exchanged between the two devices and saved away for use the next time the devices attempt to connect. Since they are bonded, the two will connect automatically without asking for a pairing confirmation. This is really convenient, because it means you can walk one of the devices out of range, thus dropping the connection, and when you return, the two devices will re-connect as if nothing ever happened!

Now, let's build the volume knob!

Beyond the Code

Want to try some variations on the code? A user pointed out:

Maybe this should use the Circuit Playground to simplify accessing the button: https://learn.adafruit.com/circuitpython-made-easy-on-circuit-playground-express/circuit-playground-express-library

Maybe this could use the Debouncing library: https://learn.adafruit.com/debouncer-library-python-circuitpython-buttons-sensors/overview

This guide was first published on Jan 21, 2020. It was last updated on Jan 21, 2020.

This page (Code the BLE Volume Knob) was last updated on Mar 28, 2023.

Text editor powered by tinymce.