Code the Power Glove

Setup

Set up the Feather Sense for use with CircuitPython by following this guide and this library setup page.

You'll also need to add the adafruit_midi and the adafruit_ble_midi libraries from the library bundle.

Your Feather's CIRCUITPY drive should look like this.

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).

"""
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  # accelerometer
import simpleio
from analogio import AnalogIn

i2c = board.I2C()
sense_accel = adafruit_lsm6ds.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)

Code Explainer

Here's how the code works, and some areas that are ready for customization.

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.

Download: file
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
You can uncomment the NoteOn and PitchBend imports if you choose to send those kinds of MIDI messages.

Sense Setup

The Feather Sense's accelerometer is on the I2C bus, so we'll set that up next.

Download: file
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.

Download: file
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.

Download: file
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".

Download: file
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.

Download: file
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.

Download: file
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.

Download: file
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.

Download: file
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.

Download: file
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.)

Download: file
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.

Download: file
# 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.

Download: file
print("Disconnected")
        print()
        ble.start_advertising(advertisement)
This guide was first published on Apr 19, 2020. It was last updated on Apr 19, 2020.
This page (Code the Power Glove) was last updated on Jul 08, 2020.