Once you've finished setting up your QT Py RP2040 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: Copyright (c) 2026 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
'''QT Py RP2040 MIDI Breath Controller with BMP585'''
import time
import board
import simpleio
from adafruit_bmp5xx import BMP5XX
from adafruit_seesaw import digitalio, neopixel, rotaryio, seesaw
import usb_midi
import adafruit_midi
from adafruit_midi.control_change import ControlChange
from adafruit_midi.channel_pressure import ChannelPressure
SEALEVELPRESSURE_HPA = 1013.25
change_sense = 0.1 # change sensitivity
low_press = 995 # lowest pressure reading range
high_press = 1032 # highest pressure reading range
i2c = board.STEMMA_I2C()
bmp = BMP5XX.over_i2c(i2c)
bmp.sea_level_pressure = SEALEVELPRESSURE_HPA
seesaw = seesaw.Seesaw(i2c, addr=0x36)
seesaw.pin_mode(24, seesaw.INPUT_PULLUP)
button = digitalio.DigitalIO(seesaw, 24)
encoder = rotaryio.IncrementalEncoder(seesaw)
pixel = neopixel.NeoPixel(seesaw, 6, 1)
pixel.brightness = 0.2
midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
# CC message channels
# last index is place holder for Channel Pressure message type
messages = [1, 2, 7, 64, 0]
# neopixel colors to associate with CC messages
colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255),
(255, 255, 0), (255, 0, 255)]
pressure = 0
mod_val1 = 0
mod_val2 = 0
effect_index = 0
last_position = 0
button_state = False
pixel.fill(colors[effect_index])
while True:
position = -encoder.position
if position != last_position:
# encoder changes midi CC message type
if position > last_position:
effect_index = (effect_index + 1) % len(messages)
else:
effect_index = (effect_index - 1) % len(messages)
pixel.fill(colors[effect_index])
last_position = position
if not button.value and not button_state:
pixel.fill((255, 255, 255))
# button press sends MIDI panic
panic = ControlChange(123, 0)
midi.send(panic)
# and turns sustain off
sus_off = ControlChange(64, 0)
midi.send(sus_off)
button_state = True
if button.value and button_state:
# reset neopixel to CC msg color
pixel.fill(colors[effect_index])
button_state = False
if bmp.data_ready:
# get pressure reading
pressure = bmp.pressure
# if the pressure has changed enough
# (adjust change_sense value at top to inc or dec)
if abs(pressure - mod_val2) > change_sense:
# map pressure reading to CC range
mod_val1 = round(simpleio.map_range(pressure, low_press, high_press, 0, 127))
# updates previous value to hold current value
mod_val2 = pressure
# MIDI data has to be sent as an integer
modulation = int(mod_val1)
# possible midi messages determined by effect_index value:
# 1: modulation
# 2: breath controller
# 7: volume
# 64: sustain
# ChannelPressure(modulation)
if effect_index < 4:
# prep CC message with CC number and value as mapped pressure reading
modWheel = ControlChange(messages[effect_index], modulation)
else:
# prep Channel Pressure message with value as mapped pressure reading
modWheel = ChannelPressure(modulation)
# CC message is sent
midi.send(modWheel)
# print(modWheel)
# delay to settle MIDI data
time.sleep(0.001)
Upload the Code and Libraries to the QT Py RP2040
After downloading the Project Bundle, plug your QT Py RP2040 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 QT Py RP2040's CIRCUITPY drive.
- lib folder
- code.py
Your QT Py RP2040 CIRCUITPY drive should look like this after copying the lib folder and code.py file:
How the CircuitPython Code Works
At the top of the code are some user parameters that you can edit:
-
SEALEVELPRESSURE_HPA- the default sea level pressure -
change_sense- the difference between pressure readings that needs to be detected before it sends a new MIDI message -
low_press- the low range of the pressure readings -
high_press- the high range of the pressure readings
You can update the low_press and high_press values depending on the pressure readings that you get from your use of the pressure sensor.
SEALEVELPRESSURE_HPA = 1013.25 change_sense = 0.1 # change sensitivity low_press = 995 # lowest pressure reading range high_press = 1032 # highest pressure reading range
I2C Devices
Next, the I2C devices are instantiated. First, the BMP585 sensor and then the seesaw rotary encoder, switch and onboard NeoPixel.
i2c = board.STEMMA_I2C() bmp = BMP5XX.over_i2c(i2c) bmp.sea_level_pressure = SEALEVELPRESSURE_HPA seesaw = seesaw.Seesaw(i2c, addr=0x36) seesaw.pin_mode(24, seesaw.INPUT_PULLUP) button = digitalio.DigitalIO(seesaw, 24) encoder = rotaryio.IncrementalEncoder(seesaw) pixel = neopixel.NeoPixel(seesaw, 6, 1) pixel.brightness = 0.2
MIDI and Lists
A MIDI object is created to use USB MIDI out. Two lists are used:
-
messages- this list has the ControlChange (CC) message numbers -
colors- this list has the associated colors for the NeoPixel on the rotary encoder. These colors change along with the CC message.
midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
# CC message channels
# last index is place holder for Channel Pressure message type
messages = [1, 2, 7, 64, 0]
# neopixel colors to associate with CC messages
colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255),
(255, 255, 0), (255, 0, 255)]
States and Variables
A few states and variables are used throughout the loop. pressure holds the pressure reading from the BMP585. mod_val1 and mod_val2 hold the modulation values that are sent as a part of the MIDI CC message. effect_index tracks the MIDI CC message from the messages list. last_position tracks the previous position for the rotary encoder. button_state is the state of the button for debouncing.
Finally, right before the loop, the NeoPixel on the rotary encoder is lit up with the color that corresponds with the default MIDI CC message.
pressure = 0 mod_val1 = 0 mod_val2 = 0 effect_index = 0 last_position = 0 button_state = False pixel.fill(colors[effect_index])
The Loop
In the loop, the rotary encoder position is tracked. Turning the encoder changes the effect_index value. This value changes the NeoPixel color and what MIDI CC message type is sent.
while True:
position = -encoder.position
if position != last_position:
# encoder changes midi CC message type
if position > last_position:
effect_index = (effect_index + 1) % len(messages)
else:
effect_index = (effect_index - 1) % len(messages)
pixel.fill(colors[effect_index])
last_position = position
Panic Button
If you press the button the rotary encoder, then the MIDI panic command is sent. That command turns off all notes. It also sends a sustain command that turns sustain off. When the button is pressed, the NeoPixel turns white.
if not button.value and not button_state:
pixel.fill((255, 255, 255))
# button press sends MIDI panic
panic = ControlChange(123, 0)
midi.send(panic)
# and turns sustain off
sus_off = ControlChange(64, 0)
midi.send(sus_off)
button_state = True
if button.value and button_state:
# reset neopixel to CC msg color
pixel.fill(colors[effect_index])
button_state = False
Under Pressure
When a pressure reading is ready from the BMP585, it's checked against the previous pressure reading. If the difference between the two values is greater than the change_sense value, the reading is mapped to a MIDI CC value between 0 and 127.
The mapped value is sent as part of the MIDI CC message. The message type is determined by the value of effect_index, which is used as the index value in the messages list. The following CC message types are in the list:
- Modulation (1)
- Breath Controller (2)
- Volume (7)
- Sustain (64)
- ChannelPressure
You can add additionally message types to the messages list to use. In testing, these felt like the most effective to pair with breath control.
if bmp.data_ready:
# get pressure reading
pressure = bmp.pressure
# if the pressure has changed enough
# (adjust change_sense value at top to inc or dec)
if abs(pressure - mod_val2) > change_sense:
# map pressure reading to CC range
mod_val1 = round(simpleio.map_range(pressure, low_press, high_press, 0, 127))
# updates previous value to hold current value
mod_val2 = pressure
# MIDI data has to be sent as an integer
modulation = int(mod_val1)
# possible midi messages determined by effect_index value:
# 1: modulation
# 2: breath controller
# 7: volume
# 64: sustain
# ChannelPressure(modulation)
if effect_index < 4:
# prep CC message with CC number and value as mapped pressure reading
modWheel = ControlChange(messages[effect_index], modulation)
else:
# prep Channel Pressure message with value as mapped pressure reading
modWheel = ChannelPressure(modulation)
# CC message is sent
midi.send(modWheel)
# print(modWheel)
# delay to settle MIDI data
time.sleep(0.001)
Page last edited March 02, 2026
Text editor powered by tinymce.