MIDI-to-MQTT Bridge (console)

This command-line Python script transmits MQTT messages based on events from a MIDI input device such as the Akai MPD218 Drum Pad Controller. This can be used as a platform for remote control of one or multiple embedded devices.

The MQTT and MIDI configuration must be customized by editing variables with the script before this can be used.

The script is provided all in one file and can be be directly downloaded from midi_to_mqtt.py. The following sections have both documentation and the full code.

Installation Requirements

The code requires a working installation of Python 3 with pySerial, paho-mqtt, and python-rtmidi. For suggestions on setting up your system please see Python 3 Installation.

User Guide

The procedure for use generally follows this sequence:

  1. Install one of the remote connection examples on your CircuitPython board. These can serve as starting points for your own sketches.
  2. Open the midi_to_mqtt.py program in a Python editor.
  3. Locate the top section named “Configuration” and follow the prompts to customize the settings.
  4. Customize the MIDI event processing to produce output compatible with your sketch.
  5. Run the script using Python 3, typically as follows: python3 midi_to_mqtt.py
  6. To stop the script, type Control-C.

If testing locally, it is convenient also to run the MQTT Monitor (PyQt5) in order to simulate the collaborating system.

Full Code

#!/usr/bin/env python3

midi_to_mqtt.py : sample code in Python to translate MIDI events into MQTT messages

No copyright, 2021, Garth Zeglin.  This file is explicitly placed in the public domain.

# Configuration

# The following variables must be customized to set up the network and MIDI
# port settings.

# IDeATe MQTT server name.
mqtt_hostname = "mqtt.ideate.cmu.edu"

# IDeATe MQTT server port, specific to each course.
# Please see https://mqtt.ideate.cmu.edu for details.
mqtt_portnum  = 8884   # 16-223

# Username and password, provided by instructor for each course.
mqtt_username = ''
mqtt_password = ''

# MQTT publication topic.  This is usually the students Andrew ID.
mqtt_topic = 'orchestra'
# mqtt_topic = ''

# MQTT receive subscription.  This is usually the partner Andrew ID.
mqtt_subscription = 'unspecified'

# MIDI port name on macOS for our preferred Akai drum pad.
midi_portname = 'MPD218 Port A'

# Standard Python modules.
import sys, time, signal, platform

if mqtt_username == '' or mqtt_password == '' or mqtt_topic == '' or midi_portname == '': 
This script must be customized before it can be used.  Please edit the file with
a Python or text editor and set the variables appropriately in the Configuration
section at the top of the file.

# Import the MQTT client library.
# documentation: https://www.eclipse.org/paho/clients/python/docs/
import paho.mqtt.client as mqtt

# For documentation on python-rtmidi: https://pypi.org/project/python-rtmidi/
import rtmidi

class MidiMqtt(object):
    """Class to manage a MIDI input connection and process MIDI events into MQTT messages."""
    def __init__(self, portname, client):

        # save the client handle
        self.client = client

        print(f"Opening MIDI input to look for {portname}.")
        # Initialize the MIDI input system and read the currently available ports.
        self.midi_in = rtmidi.MidiIn()
        for idx, name in enumerate(self.midi_in.get_ports()):
            if portname in name:
                print("Found preferred MIDI input device %d: %s" % (idx, name))
                print("Ignoring unselected MIDI device: ", name)

        if not self.midi_in.is_port_open():
            if platform.system() == 'Windows':
                print("Virtual MIDI inputs are not currently supported on Windows, see python-rtmidi documentation.")
                print("Creating virtual MIDI input.")

    def decode_mpd218_key(self, key):
        """Interpret a MPD218 pad event key value as a row, column, and bank position.
        Row 0 is the front/bottom row (Pads 1-4), row 3 is the back/top row (Pads 13-16).
        Column 0 is the left, column 3 is the right.
        Bank 0 is the A bank, bank 1 is the B bank, bank 2 is the C bank.

        :param key: an integer MIDI note value
        :return: (row, column, bank)
        # Decode the key into coordinates on the 4x4 pad grid.
        bank = (key - 36) // 16
        pos = (key - 36) % 16
        row = pos // 4
        col = pos % 4
        return row, col, bank

    def decode_mpd218_cc(self, cc):
        """Interpret a MPD218 knob control change event as a knob index and bank position.
        The MPD218 uses a non-contiguous set of channel indices so this normalizes the result.
        The knob index ranges from 1 to 6 matching the knob labels.
        Bank 0 is the A bank, bank 1 is the B bank, bank 2 is the C bank.

        :param cc: an integer MIDI control channel identifier
        :return: (knob, bank)
        if cc < 16:
            knob = {3:1, 9:2, 12:3, 13:4, 14:5, 15:6}.get(cc)
            bank = 0
            knob = 1 + ((cc - 16) % 6)
            bank = 1 + ((cc - 16) // 6)
        return knob, bank

    def decode_message(self, message):
        """Decode a MIDI message expressed as a list of integers and perform callbacks
        for recognized message types.

        :param message: list of integers containing a single MIDI message
        if len(message) > 0:
            status = message[0] & 0xf0
            channel = (message[0] & 0x0f) + 1

            if len(message) == 2:
                if status == 0xd0: # == 0xdx, channel pressure, any channel
                    return self.channel_pressure(channel, message[1])

            elif len(message) == 3:
                if status == 0x90: # == 0x9x, note on, any channel
                    return self.note_on(channel, message[1], message[2])

                elif status == 0x80: # == 0x8x, note off, any channel
                    return self.note_off(channel, message[1], message[2])

                elif status == 0xb0: # == 0xbx, control change, any channel
                    return self.control_change(channel, message[1], message[2])

                elif status == 0xa0: # == 0xax, polyphonic key pressure, any channel
                    return self.polyphonic_key_pressure(channel, message[1], message[2])
    def midi_received(self, data, unused):
        msg, delta_time = data

    def note_off(self, channel, key, velocity):
        """Function to receive messages starting with 0x80 through 0x8F.

        :param channel: integer from 1 to 16
        :param key: integer from 0 to 127
        :param velocity: integer from 0 to 127

    def note_on(self, channel, key, velocity):
        """Function to receive messages starting with 0x90 through 0x9F.

        :param channel: integer from 1 to 16
        :param key: integer from 0 to 127
        :param velocity: integer from 0 to 127
        topic = mqtt_topic + '/noteOn'
        payload = "%d %d" % (key, velocity)
        self.client.publish(topic, payload)

        # apply some logic to route control inputs to other topics
        row, column, bank = self.decode_mpd218_key(key)
        topic = mqtt_topic + '/instrument%d' % (column+1)
        payload = "%d %d" % (row, velocity)
        self.client.publish(topic, payload)
    def polyphonic_key_pressure(self, channel, key, value):
        """Function to receive messages starting with 0xA0 through 0xAF.

        :param channel: integer from 1 to 16
        :param key: integer from 0 to 127
        :param value: integer from 0 to 127

    def control_change(self, channel, control, value):
        """Function to receive messages starting with 0xB0 through 0xBF.

        :param channel: integer from 1 to 16
        :param control: integer from 0 to 127; some have special meanings
        :param value: integer from 0 to 127
        topic = mqtt_topic + '/controlChange'
        payload = "%d %d" % (control, value)
        self.client.publish(topic, payload)

        # apply some logic to route control inputs to other topics
        knob, bank = self.decode_mpd218_cc(control)
        topic = mqtt_topic + '/project%d' % (knob)
        payload = "%d" % (value)
        self.client.publish(topic, payload)

    def channel_pressure(self, channel, value):
        """Function to receive messages starting with 0xD0 through 0xDF.

        :param channel: integer from 1 to 16
        :param value: integer from 0 to 127

# Global script variables.

midi_port = None
client = None
# Attach a handler to the keyboard interrupt (control-C).
def _sigint_handler(signal, frame):
    print("Keyboard interrupt caught, closing down...")
    if client is not None:
signal.signal(signal.SIGINT, _sigint_handler)        

# MQTT networking functions.

# The callback for when the broker responds to our connection request.
def on_connect(client, userdata, flags, rc):
    print(f"MQTT connected with flags: {flags}, result code: {rc}")

    # Subscribing in on_connect() means that if we lose the connection and
    # reconnect then subscriptions will be renewed.  The hash mark is a
    # multi-level wildcard, so this will subscribe to all subtopics of 16223

# The callback for when a message has been received on a topic to which this
# client is subscribed.  The message variable is a MQTTMessage that describes
# all of the message parameters.

# Some useful MQTTMessage fields: topic, payload, qos, retain, mid, properties.
#   The payload is a binary string (bytes).
#   qos is an integer quality of service indicator (0,1, or 2)
#   mid is an integer message ID.
def on_message(client, userdata, msg):
    print(f"message received: topic: {msg.topic} payload: {msg.payload}")

# Launch the MQTT network client
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.username_pw_set(mqtt_username, mqtt_password)

# Start a background thread to connect to the MQTT network.
client.connect_async(mqtt_hostname, mqtt_portnum)

# Connect to the MIDI port.  MIDI events will be received on a separate thread.
midi_port = MidiMqtt(midi_portname, client)

# Nothing to do on the main thread.

Source: MIDI-to-MQTT Bridge (console)

About The Author

Scroll to Top
Read previous post:

Designed To Support Unattended Operation, IP65+, And Passive Cooling Have you ever tried designing a truly embedded x86-based system? We...