Setup
Set up the Feather Sense for use with CircuitPython by following this guide.
To use with CircuitPython, you need to first install a few libraries, into the lib folder on your CIRCUITPY drive. Then you need to update code.py with the example script.
Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, open the directory PowerGlove_BLE_MIDI/ and then click on the directory that matches the version of CircuitPython you're using and copy the contents of that directory to your CIRCUITPY drive.
CIRCUITPY
Here's how the code will work:
- On start, it will advertise itself as a Bluetooth Low Energy device named "Power Glove MIDI" with the BLE_MIDI service available, and wait for a connection.
- When a connection is made, it will read the Feather's accelerometer and the four flex sensors as analog inputs.
- These sensor readings are converted to values of 0-127
- The six sensor values are sent via BLE MIDI as continuous controller (CC) numbers 7, 70, 71, 75, and 77 over MIDI channel 1.
- This repeats continuously until the connection is broken or the Power Glove is powered off.
Code
Copy the code from the code-block below and paste it into the Mu editor and save it to your Feather as code.py (or copy code.py from the zip file and place on the CIRCUITPY drive).
# SPDX-FileCopyrightText: 2021 John Park for Adafruit Industries.
#
# SPDX-License-Identifier: MIT
"""
Power Glove BLE MIDI with Feather Sense nRF52840
Sends MIDI CC values based on finger flex sensors and accelerometer
"""
import time
import board
import adafruit_ble
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
import adafruit_ble_midi
import adafruit_midi
from adafruit_midi.control_change import ControlChange
# from adafruit_midi.note_on import NoteOn
# from adafruit_midi.pitch_bend import PitchBend
import adafruit_lsm6ds.lsm6ds33 # accelerometer
import simpleio
from analogio import AnalogIn
i2c = board.I2C() # uses board.SCL and board.SDA
# i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller
sense_accel = adafruit_lsm6ds.lsm6ds33.LSM6DS33(i2c)
analog_in_thumb = AnalogIn(board.A3)
analog_in_index = AnalogIn(board.A2)
analog_in_middle = AnalogIn(board.A1)
analog_in_ring = AnalogIn(board.A0)
# Pick your MIDI CC numbers here
cc_x_num = 7 # volume
cc_y_num = 70 # unassigned
cc_thumb_num = 71 # unassigned
cc_index_num = 75 # unassigned
cc_middle_num = 76 # unassigned
cc_ring_num = 77 # unassigned
midi_channel = 1 # pick your midi out channel here
# Use default HID descriptor
midi_service = adafruit_ble_midi.MIDIService()
advertisement = ProvideServicesAdvertisement(midi_service)
ble = adafruit_ble.BLERadio()
if ble.connected:
for c in ble.connections:
c.disconnect()
midi = adafruit_midi.MIDI(midi_out=midi_service, out_channel=midi_channel - 1)
print("advertising")
ble.name="Power Glove MIDI"
ble.start_advertising(advertisement)
# reads an analog pin and returns value remapped to out range, e.g., 0-127
def get_flex_cc(sensor, low_in, high_in, min_out, max_out):
flex_raw = sensor.value
flex_cc = simpleio.map_range(flex_raw, low_in, high_in, min_out, max_out)
flex_cc = int(flex_cc)
return flex_cc
debug = False # set debug mode True to test raw values, set False to run BLE MIDI
while True:
if debug:
accel_data = sense_accel.acceleration # get accelerometer reading
accel_x = accel_data[0]
accel_y = accel_data[1]
accel_z = accel_data[2]
print(
"x:{} y:{} z:{} thumb:{} index:{} middle:{} ring:{}".format(
accel_x,
accel_y,
accel_x,
analog_in_thumb.value,
analog_in_index.value,
analog_in_middle.value,
analog_in_ring.value,
)
)
time.sleep(0.2)
else:
print("Waiting for connection")
while not ble.connected:
pass
print("Connected")
while ble.connected:
# Feather Sense accelerometer readings to CC
accel_data = sense_accel.acceleration # get accelerometer reading
accel_x = accel_data[0]
accel_y = accel_data[1]
# accel_z = accel_data[2]
# Remap analog readings to cc range
cc_x = int(simpleio.map_range(accel_x, 0, 9, 127, 0))
cc_y = int(simpleio.map_range(accel_y, 1, -9, 0, 127))
cc_thumb = get_flex_cc(analog_in_thumb, 49000, 35000, 127, 0)
cc_index = get_flex_cc(analog_in_index, 50000, 35000, 0, 127)
cc_middle = get_flex_cc(analog_in_middle, 55000, 40000, 0, 127)
cc_ring = get_flex_cc(analog_in_ring, 55000, 42000, 0, 127)
'''
print(
"CC_X:{} CC_Y:{} CC_Thumb:{} CC_Index:{} CC_Middle:{} CC_Ring:{}".format(
cc_x, cc_y, cc_thumb, cc_index, cc_middle, cc_ring
)
)'''
# send all the midi messages in a list
midi.send(
[
ControlChange(cc_x_num, cc_x),
ControlChange(cc_y_num, cc_y),
ControlChange(cc_thumb_num, cc_thumb),
ControlChange(cc_index_num, cc_index),
ControlChange(cc_middle_num, cc_middle),
ControlChange(cc_ring_num, cc_ring),
]
)
# If you want to send NoteOn or Pitch Bend, here are examples:
# midi.send(NoteOn(44, 120)) # G sharp 2nd octave
# a_pitch_bend = PitchBend(random.randint(0, 16383))
# midi.send(a_pitch_bend)
print("Disconnected")
print()
ble.start_advertising(advertisement)
Libraries
First we'll import the necessary libraries. These include time, board, adafruit_lsm6ds, simpleio, and analogio to deal with pauses, board hardware access, LSM6DS accelerometer reading, IO, and analog reads.
We also import adafruit_ble, adafruit_ble_midi, and adafruit_midi to enable Bluetooth LE control, sending BLE MIDI messages, and general MIDI commands.
import time import board import adafruit_ble from adafruit_ble.advertising.standard import ProvideServicesAdvertisement import adafruit_ble_midi import adafruit_midi from adafruit_midi.control_change import ControlChange # from adafruit_midi.note_on import NoteOn # from adafruit_midi.pitch_bend import PitchBend import adafruit_lsm6ds # accelerometer import simpleio from analogio import AnalogIn
i2c = board.I2C() sense_accel = adafruit_lsm6ds.LSM6DS33(i2c)
Analog Read Pins
The finger flex sensors will be read on four analog input pins -- we'll set them up with nice names.
analog_in_thumb = AnalogIn(board.A3) analog_in_index = AnalogIn(board.A2) analog_in_middle = AnalogIn(board.A1) analog_in_ring = AnalogIn(board.A0)
MIDI CCs and Channel
We'll create variables for each of the six sensors we'll be reading to assign them to MIDI CC numbers.
Some CC numbers have standardized assignments on most synthesizers and synth software, while others are wide open to your own assignments. Here's a good list to consult. I chose:
- For Power Glove tilt I chose CC 7, which is typically used for overall volume.
- For Power Glove roll, CC 70, which is a vague "sound variation" control, but you should be able to assign as you like.
- Thumb flex is CC 71, typically filter resonance.
- Index flex is CC 75, generic, so you can assign freely.
- Middle flex is CC 76, generic.
- Ring flex is CC 77, generic.
We'll also set the MIDI output to channel 1. You can pick any channel from 1-16 depending on your needs.
cc_x_num = 7 # volume cc_y_num = 70 # unassigned cc_thumb_num = 71 # unassigned cc_index_num = 75 # unassigned cc_middle_num = 76 # unassigned cc_ring_num = 77 # unassigned midi_channel = 1 # pick your midi out channel here
MIDI BLE Setup
Now we will set up the BLE MIDI service, and set the BLE radio to advertise the device, with the name "Power Glove MIDI".
midi_service = adafruit_ble_midi.MIDIService()
advertisement = ProvideServicesAdvertisement(midi_service)
ble = adafruit_ble.BLERadio()
if ble.connected:
for c in ble.connections:
c.disconnect()
midi = adafruit_midi.MIDI(midi_out=midi_service, out_channel=midi_channel - 1)
print("advertising")
ble.name="Power Glove MIDI"
ble.start_advertising(advertisement)
Get Flex CC Function
To enable reading of the flex sensors and converting their values from a 16-bit analog read to a 7-bit MIDI CC value, we'll create a function called get_flex_cc(). This function takes five arguments: sensor name to read, low input value, high input value, minimum output value, and maximum output value.
When it is called it will return a CC value somewhere between 0-127.
def get_flex_cc(sensor, low_in, high_in, min_out, max_out):
flex_raw = sensor.value
flex_cc = simpleio.map_range(flex_raw, low_in, high_in, min_out, max_out)
flex_cc = int(flex_cc)
return flex_cc
Debug
In order to facilitate a type of manual sensor calibration you can turn the debug variable from False to True.
In the main loop of the program, when debug is True the raw accelerometer and flex sensor data will print to the serial port (REPL in Mu). This way you can check the value ranges and adjust the ranges in the main code to match.
debug = False # set debug mode True to test raw values, set False to run BLE MIDI
while True:
if debug:
accel_data = sense_accel.acceleration # get accelerometer reading
accel_x = accel_data[0]
accel_y = accel_data[1]
accel_z = accel_data[2]
print(
"x:{} y:{} z:{} thumb:{} index:{} middle:{} ring:{}".format(
accel_x,
accel_y,
accel_x,
analog_in_thumb.value,
analog_in_index.value,
analog_in_middle.value,
analog_in_ring.value,
)
)
time.sleep(0.2)
Wait for Connection
When we're out of debug mode, the first thing that happens in the main loop is that the BLE connection will be tested. It will remain in this state until a connection is made.
print("Waiting for connection")
while not ble.connected:
pass
print("Connected")
Connected
Once the Feather has connected to a BLE MIDI device, we proceed with checking all of the sensors.
First, the accelerometer values are read, and then the x (tilt) and y (roll) values are remapped as integers between 0-127 and cast to a pair of variables named cc_x and cc_y.
You can see here that the cc_x is set to send a value of 127 when the accelerometer x-axis reads 0, meaning the volume will be full when the Power Glove is parallel to the ground. When you tilt the Power Glove up toward the sky, the accelerometer x-axis will read around 9, and the CC value will be set to 0, so the volume will be turn down to nothing.
accel_data = sense_accel.acceleration # get accelerometer reading
accel_x = accel_data[0]
accel_y = accel_data[1]
# accel_z = accel_data[2]
# Remap analog readings to cc range
cc_x = int(simpleio.map_range(accel_x, 0, 9, 127, 0))
cc_y = int(simpleio.map_range(accel_y, 1, -9, 0, 127))
If you wanted to reverse the tilt/volume behavior you could specify the remapping as: cc_x = int(simpleio.map_range(accel_x, 0, 9, 0, 127))
Flex
Then, the four variables for the flex sensors are cast with the remapped values returned by the get_flex_cc() function.
Note the values for the upper and lower ranges were determined first by running the program in debug mode and writing down those values.
Again, you can swap the "knob direction" of any of these by swapping the min-max CC values called from 0-127 or 127-0.
cc_thumb = get_flex_cc(analog_in_thumb, 49000, 35000, 127, 0)
cc_index = get_flex_cc(analog_in_index, 50000, 35000, 0, 127)
cc_middle = get_flex_cc(analog_in_middle, 55000, 40000, 0, 127)
cc_ring = get_flex_cc(analog_in_ring, 55000, 42000, 0, 127)
Send MIDI Message
With all of the CC values read, we'll now send a series of 8-bit MIDI messages. You can call the midi.send() command individually for each MIDI message, or call the command as a list. (They'll all still be sent as individual MIDI messages.)
midi.send(
[
ControlChange(cc_x_num, cc_x),
ControlChange(cc_y_num, cc_y),
ControlChange(cc_thumb_num, cc_thumb),
ControlChange(cc_index_num, cc_index),
ControlChange(cc_middle_num, cc_middle),
ControlChange(cc_ring_num, cc_ring),
]
)
These will be transmitted in near real-time to the MIDI device, but not instantaneously -- BLE MIDI latency runs between 7.5 ms-22 ms typically. This isn't really noticeable with most CC parameters or playing notes with soft attacks, but could be bothersome with percussive, sharp attack NoteOn messages.
Optional Message Types
If you uncomment the necessary library imports, you can try out the NoteOn and PitchBend message types.
These lines of commented code show the essentials.
# midi.send(NoteOn(44, 120)) # G sharp 2nd octave # a_pitch_bend = PitchBend(random.randint(0, 16383)) # midi.send(a_pitch_bend)
Disconnected
If the connection is broken, the ble.start_advertising(advertisement) will begin again.
print("Disconnected")
print()
ble.start_advertising(advertisement)
Page last edited January 21, 2025
Text editor powered by tinymce.