diff --git a/README.rst b/README.rst
index 1142360..338b164 100644
--- a/README.rst
+++ b/README.rst
@@ -35,9 +35,6 @@ Installing from PyPI
.. note:: This library is not available on PyPI yet. Install documentation is included
as a standard element. Stay tuned for PyPI availability!
-.. todo:: Remove the above note if PyPI version is/will be available at time of release.
- If the library is not planned for PyPI, remove the entire 'Installing from PyPI' section.
-
On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
PyPI `_. To install for current user:
@@ -63,7 +60,55 @@ To install in a virtual environment in your current project:
Usage Example
=============
-.. todo:: Add a quick, simple example. It and other examples should live in the examples folder and be included in docs/examples.rst.
+.. code-block:: python
+
+ """
+ This example sends MIDI out. It sends NoteOn and then NoteOff with a random pitch bend.
+ """
+
+ import time
+ import random
+ 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_off import NoteOff
+ from adafruit_midi.note_on import NoteOn
+ from adafruit_midi.pitch_bend import PitchBend
+
+ # Use default HID descriptor
+ midi_service = adafruit_ble_midi.MIDIService()
+ advertisement = ProvideServicesAdvertisement(midi_service)
+ # advertisement.appearance = 961
+
+ ble = adafruit_ble.BLERadio()
+ if ble.connected:
+ for c in ble.connections:
+ c.disconnect()
+
+ midi = adafruit_midi.MIDI(midi_out=midi_service, out_channel=0)
+
+ print("advertising")
+ ble.start_advertising(advertisement)
+
+ while True:
+ print("Waiting for connection")
+ while not ble.connected:
+ pass
+ print("Connected")
+ while ble.connected:
+ midi.send(NoteOn(44, 120)) # G sharp 2nd octave
+ time.sleep(0.25)
+ a_pitch_bend = PitchBend(random.randint(0, 16383))
+ midi.send(a_pitch_bend)
+ time.sleep(0.25)
+ # note how a list of messages can be used
+ midi.send([NoteOff("G#2", 120), ControlChange(3, 44)])
+ time.sleep(0.5)
+ print("Disconnected")
+ print()
+ ble.start_advertising(advertisement)
Contributing
============
diff --git a/adafruit_ble_midi.py b/adafruit_ble_midi.py
index 0f1d18c..d348352 100644
--- a/adafruit_ble_midi.py
+++ b/adafruit_ble_midi.py
@@ -39,87 +39,155 @@ from adafruit_ble.services import Service
__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE_MIDI.git"
+
class _MidiCharacteristic(ComplexCharacteristic):
"""Endpoint for sending commands to a media player. The value read will list all available
commands."""
+
uuid = VendorUUID("7772E5DB-3868-4112-A1A9-F2669D106BF3")
def __init__(self):
- super().__init__(properties=Characteristic.WRITE_NO_RESPONSE | Characteristic.READ | Characteristic.NOTIFY,
- read_perm=Attribute.OPEN, write_perm=Attribute.OPEN,
- max_length=512,
- fixed_length=False)
+ super().__init__(
+ properties=Characteristic.WRITE_NO_RESPONSE
+ | Characteristic.READ
+ | Characteristic.NOTIFY,
+ read_perm=Attribute.ENCRYPT_NO_MITM,
+ write_perm=Attribute.ENCRYPT_NO_MITM,
+ max_length=512,
+ fixed_length=False,
+ )
def bind(self, service):
"""Binds the characteristic to the given Service."""
bound_characteristic = super().bind(service)
- return _bleio.PacketBuffer(bound_characteristic,
- buffer_size=4)
+ return _bleio.PacketBuffer(bound_characteristic, buffer_size=4)
+
class MIDIService(Service):
+ """BLE MIDI service. It acts just like a USB MIDI PortIn and PortOut and can be used as a drop
+ in replacement.
+
+ BLE MIDI's protocol includes timestamps for MIDI messages. This class automatically adds them
+ to MIDI data written out and strips them from MIDI data read in."""
+
uuid = VendorUUID("03B80E5A-EDE8-4B33-A751-6CE34EC4C700")
_raw = _MidiCharacteristic()
+ # _raw gets shadowed for each MIDIService instance by a PacketBuffer. PyLint doesn't know this
+ # so it complains about missing members.
+ # pylint: disable=no-member
def __init__(self, **kwargs):
super().__init__(**kwargs)
- self._in_buffer = bytearray(self._raw.packet_length)
+ self._in_buffer = bytearray(self._raw.packet_size)
self._out_buffer = None
shared_buffer = memoryview(bytearray(4))
- self._buffers = [None, shared_buffer[:1], shared_buffer[:2], shared_buffer[:3], shared_buffer[:4]]
+ self._buffers = [
+ None,
+ shared_buffer[:1],
+ shared_buffer[:2],
+ shared_buffer[:3],
+ shared_buffer[:4],
+ ]
self._header = bytearray(1)
self._in_sysex = False
self._message_target_length = None
self._message_length = 0
self._pending_realtime = None
+ self._in_length = 0
+ self._in_index = 1
+ self._last_data = True
+
+ def readinto(self, buf, length):
+ """Reads up to ``length`` bytes into ``buf`` starting at index 0.
+
+ Returns the number of bytes written into ``buf``."""
+ i = 0
+ while i < length:
+ if self._in_index < self._in_length:
+ byte = self._in_buffer[self._in_index]
+ if self._last_data and byte & 0x80 != 0:
+ # Maybe manage timing here. Not done now because we're likely slower than we
+ # need to be already.
+ # low_ms = byte & 0x7f
+ # print("low", low_ms)
+ self._in_index += 1
+ self._last_data = False
+ continue
+ self._in_index += 1
+ self._last_data = True
+ buf[i] = byte
+ i += 1
+ else:
+ if len(self._in_buffer) < self._raw.packet_size:
+ self._in_buffer = bytearray(self._raw.packet_size)
+ self._in_length = self._raw.readinto(self._in_buffer)
+ if self._in_length == 0:
+ break
+ # high_ms = self._in_buffer[0] & 0x3f
+ # print("high", high_ms)
+ self._in_index = 1
+ self._last_data = True
+
+ return i
def read(self, length):
- self._raw.read(self._in_buffer)
- return None
+ """Reads up to ``length`` bytes and returns them."""
+ result = bytearray(length)
+ i = self.readinto(result, length)
+ return result[:i]
def write(self, buf, length):
+ """Writes ``length`` bytes out."""
+ # pylint: disable=too-many-branches
timestamp_ms = time.monotonic_ns() // 1000000
- self._header[0] = (timestamp_ms >> 7 & 0x3f) | 0x80
+ self._header[0] = (timestamp_ms >> 7 & 0x3F) | 0x80
i = 0
while i < length:
data = buf[i]
command = data & 0x80 != 0
if self._in_sysex:
- if command: # End of sysex or real time
+ if command: # End of sysex or real time
b = self._buffers[2]
- b[0] = 0x80 | (timestamp_ms & 0x7f)
- b[1] = 0xf7
- self._raw.write(b, self._header)
- self._in_sysex = data == 0xf7
+ b[0] = 0x80 | (timestamp_ms & 0x7F)
+ b[1] = 0xF7
+ self._raw.write(b, header=self._header)
+ self._in_sysex = data == 0xF7
else:
b = self._buffers[1]
b[0] = data
- self._raw.write(b, self._header)
+ self._raw.write(b, header=self._header)
elif command:
- self._in_sysex = data == 0xf0
+ self._in_sysex = data == 0xF0
b = self._buffers[2]
- b[0] = 0x80 | (timestamp_ms & 0x7f)
+ b[0] = 0x80 | (timestamp_ms & 0x7F)
b[1] = data
- if 0xf6 <= data <= 0xff or self._in_sysex: # Real time, command only or start sysex
+ if (
+ 0xF6 <= data <= 0xFF or self._in_sysex
+ ): # Real time, command only or start sysex
if self._message_target_length:
self._pending_realtime = b
else:
self._raw.write(b, self._header)
else:
- if 0x80 <= data <= 0xbf or 0xe0 <= data <= 0xef or data == 0xf2: # Two following bytes
+ if (
+ 0x80 <= data <= 0xBF or 0xE0 <= data <= 0xEF or data == 0xF2
+ ): # Two following bytes
self._message_target_length = 4
else:
self._message_target_length = 3
b = self._buffers[self._message_target_length]
- # All of the buffers share memory so the timestamp and data have already been set.
+ # All of the buffers share memory so the timestamp and data have already been
+ # set.
self._message_length = 2
self._out_buffer = b
else:
self._out_buffer[self._message_length] = data
self._message_length += 1
if self._message_target_length == self._message_length:
- self._raw.write(self._out_buffer, self._header)
- if _pending_realtime:
- self._raw.write(self._pending_realtime, self._header)
+ self._raw.write(self._out_buffer, header=self._header)
+ if self._pending_realtime:
+ self._raw.write(self._pending_realtime, header=self._header)
self._pending_realtime = None
self._message_target_length = None
+ i += 1
diff --git a/docs/conf.py b/docs/conf.py
index feee64a..2753e7b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -2,7 +2,8 @@
import os
import sys
-sys.path.insert(0, os.path.abspath('..'))
+
+sys.path.insert(0, os.path.abspath(".."))
# -- General configuration ------------------------------------------------
@@ -10,10 +11,10 @@ sys.path.insert(0, os.path.abspath('..'))
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
- 'sphinx.ext.autodoc',
- 'sphinx.ext.intersphinx',
- 'sphinx.ext.napoleon',
- 'sphinx.ext.todo',
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.napoleon",
+ "sphinx.ext.todo",
]
# TODO: Please Read!
@@ -23,29 +24,32 @@ extensions = [
# autodoc_mock_imports = ["digitalio", "busio"]
-intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None),'CircuitPython': ('https://circuitpython.readthedocs.io/en/latest/', None)}
+intersphinx_mapping = {
+ "python": ("https://docs.python.org/3.4", None),
+ "CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None),
+}
# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
-source_suffix = '.rst'
+source_suffix = ".rst"
# The master toctree document.
-master_doc = 'index'
+master_doc = "index"
# General information about the project.
-project = u'Adafruit BLE_MIDI Library'
-copyright = u'2020 Scott Shawcroft'
-author = u'Scott Shawcroft'
+project = "Adafruit BLE_MIDI Library"
+copyright = "2020 Scott Shawcroft"
+author = "Scott Shawcroft"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
-version = u'1.0'
+version = "1.0"
# The full version, including alpha/beta/rc tags.
-release = u'1.0'
+release = "1.0"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -57,7 +61,7 @@ language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.env', 'CODE_OF_CONDUCT.md']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".env", "CODE_OF_CONDUCT.md"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
@@ -69,7 +73,7 @@ default_role = "any"
add_function_parentheses = True
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
@@ -84,59 +88,62 @@ napoleon_numpy_docstring = False
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+on_rtd = os.environ.get("READTHEDOCS", None) == "True"
if not on_rtd: # only import and set the theme if we're building docs locally
try:
import sphinx_rtd_theme
- html_theme = 'sphinx_rtd_theme'
- html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), '.']
+
+ html_theme = "sphinx_rtd_theme"
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."]
except:
- html_theme = 'default'
- html_theme_path = ['.']
+ html_theme = "default"
+ html_theme_path = ["."]
else:
- html_theme_path = ['.']
+ html_theme_path = ["."]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
-html_favicon = '_static/favicon.ico'
+html_favicon = "_static/favicon.ico"
# Output file base name for HTML help builder.
-htmlhelp_basename = 'AdafruitBle_midiLibrarydoc'
+htmlhelp_basename = "AdafruitBle_midiLibrarydoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
- # The paper size ('letterpaper' or 'a4paper').
- #
- # 'papersize': 'letterpaper',
-
- # The font size ('10pt', '11pt' or '12pt').
- #
- # 'pointsize': '10pt',
-
- # Additional stuff for the LaTeX preamble.
- #
- # 'preamble': '',
-
- # Latex figure (float) alignment
- #
- # 'figure_align': 'htbp',
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
- (master_doc, 'AdafruitBLE_MIDILibrary.tex', u'AdafruitBLE_MIDI Library Documentation',
- author, 'manual'),
+ (
+ master_doc,
+ "AdafruitBLE_MIDILibrary.tex",
+ "AdafruitBLE_MIDI Library Documentation",
+ author,
+ "manual",
+ ),
]
# -- Options for manual page output ---------------------------------------
@@ -144,8 +151,13 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
- (master_doc, 'AdafruitBLE_MIDIlibrary', u'Adafruit BLE_MIDI Library Documentation',
- [author], 1)
+ (
+ master_doc,
+ "AdafruitBLE_MIDIlibrary",
+ "Adafruit BLE_MIDI Library Documentation",
+ [author],
+ 1,
+ )
]
# -- Options for Texinfo output -------------------------------------------
@@ -154,7 +166,13 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- (master_doc, 'AdafruitBLE_MIDILibrary', u'Adafruit BLE_MIDI Library Documentation',
- author, 'AdafruitBLE_MIDILibrary', 'One line description of project.',
- 'Miscellaneous'),
+ (
+ master_doc,
+ "AdafruitBLE_MIDILibrary",
+ "Adafruit BLE_MIDI Library Documentation",
+ author,
+ "AdafruitBLE_MIDILibrary",
+ "One line description of project.",
+ "Miscellaneous",
+ ),
]
diff --git a/docs/index.rst b/docs/index.rst
index 0e01bc8..e2639cc 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -23,14 +23,11 @@ Table of Contents
.. toctree::
:caption: Tutorials
-.. todo:: Add any Learn guide links here. If there are none, then simply delete this todo and leave
- the toctree above for use later.
-
.. toctree::
:caption: Related Products
-.. todo:: Add any product links here. If there are none, then simply delete this todo and leave
- the toctree above for use later.
+ Circuit Playground Bluefruit
+ Adafruit CLUE nRF52840 Express
.. toctree::
:caption: Other Links
diff --git a/examples/ble_midi_simplein.py b/examples/ble_midi_simplein.py
new file mode 100644
index 0000000..038e1a8
--- /dev/null
+++ b/examples/ble_midi_simplein.py
@@ -0,0 +1,46 @@
+"""
+This example acts as a keyboard to peer devices.
+"""
+
+import time
+import adafruit_ble
+from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
+import adafruit_ble_midi
+import adafruit_midi
+
+# These import auto-register the message type with the MIDI machinery.
+# pylint: disable=unused-import
+from adafruit_midi.control_change import ControlChange
+from adafruit_midi.midi_message import MIDIUnknownEvent
+from adafruit_midi.note_off import NoteOff
+from adafruit_midi.note_on import NoteOn
+from adafruit_midi.pitch_bend import PitchBend
+
+# 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, midi_in=midi_service, out_channel=0)
+
+print("advertising")
+ble.start_advertising(advertisement)
+
+while True:
+ print("Waiting for connection")
+ while not ble.connected:
+ pass
+ print("Connected")
+ while ble.connected:
+ midi_in = midi.receive()
+ while midi_in:
+ if not isinstance(midi_in, MIDIUnknownEvent):
+ print(time.monotonic(), midi_in)
+ midi_in = midi.receive()
+ print("Disconnected")
+ print()
+ ble.start_advertising(advertisement)
diff --git a/examples/ble_midi_simpletest.py b/examples/ble_midi_simpletest.py
index e69de29..7f77bcd 100644
--- a/examples/ble_midi_simpletest.py
+++ b/examples/ble_midi_simpletest.py
@@ -0,0 +1,47 @@
+"""
+This example sends MIDI out. It sends NoteOn and then NoteOff with a random pitch bend.
+"""
+
+import time
+import random
+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_off import NoteOff
+from adafruit_midi.note_on import NoteOn
+from adafruit_midi.pitch_bend import PitchBend
+
+# Use default HID descriptor
+midi_service = adafruit_ble_midi.MIDIService()
+advertisement = ProvideServicesAdvertisement(midi_service)
+# advertisement.appearance = 961
+
+ble = adafruit_ble.BLERadio()
+if ble.connected:
+ for c in ble.connections:
+ c.disconnect()
+
+midi = adafruit_midi.MIDI(midi_out=midi_service, out_channel=0)
+
+print("advertising")
+ble.start_advertising(advertisement)
+
+while True:
+ print("Waiting for connection")
+ while not ble.connected:
+ pass
+ print("Connected")
+ while ble.connected:
+ midi.send(NoteOn(44, 120)) # G sharp 2nd octave
+ time.sleep(0.25)
+ a_pitch_bend = PitchBend(random.randint(0, 16383))
+ midi.send(a_pitch_bend)
+ time.sleep(0.25)
+ # note how a list of messages can be used
+ midi.send([NoteOff("G#2", 120), ControlChange(3, 44)])
+ time.sleep(0.5)
+ print("Disconnected")
+ print()
+ ble.start_advertising(advertisement)
diff --git a/setup.py b/setup.py
index d7a1d9b..746aee2 100644
--- a/setup.py
+++ b/setup.py
@@ -6,6 +6,7 @@ https://github.com/pypa/sampleproject
"""
from setuptools import setup, find_packages
+
# To use a consistent encoding
from codecs import open
from os import path
@@ -13,52 +14,40 @@ from os import path
here = path.abspath(path.dirname(__file__))
# Get the long description from the README file
-with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
+with open(path.join(here, "README.rst"), encoding="utf-8") as f:
long_description = f.read()
setup(
- name='adafruit-circuitpython-ble-midi',
-
+ name="adafruit-circuitpython-ble-midi",
use_scm_version=True,
- setup_requires=['setuptools_scm'],
-
- description='BLE MIDI service for CircuitPython',
+ setup_requires=["setuptools_scm"],
+ description="BLE MIDI service for CircuitPython",
long_description=long_description,
- long_description_content_type='text/x-rst',
-
+ long_description_content_type="text/x-rst",
# The project's main homepage.
- url='https://github.com/adafruit/Adafruit_CircuitPython_BLE_MIDI',
-
+ url="https://github.com/adafruit/Adafruit_CircuitPython_BLE_MIDI",
# Author details
- author='Adafruit Industries',
- author_email='circuitpython@adafruit.com',
-
- install_requires=[
- 'Adafruit-Blinka',
- 'adafruit-circuitpython-ble'
- ],
-
+ author="Adafruit Industries",
+ author_email="circuitpython@adafruit.com",
+ install_requires=["Adafruit-Blinka", "adafruit-circuitpython-ble"],
# Choose your license
- license='MIT',
-
+ license="MIT",
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
- 'Development Status :: 3 - Alpha',
- 'Intended Audience :: Developers',
- 'Topic :: Software Development :: Libraries',
- 'Topic :: System :: Hardware',
- 'License :: OSI Approved :: MIT License',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.4',
- 'Programming Language :: Python :: 3.5',
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Topic :: Software Development :: Libraries",
+ "Topic :: System :: Hardware",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
],
-
# What does your project relate to?
- keywords='adafruit blinka circuitpython micropython ble_midi ble midi',
-
+ keywords="adafruit blinka circuitpython micropython ble_midi ble midi",
# You can just specify the packages manually here if your project is
# simple. Or you can use find_packages().
# TODO: IF LIBRARY FILES ARE A PACKAGE FOLDER,
# CHANGE `py_modules=['...']` TO `packages=['...']`
- py_modules=['adafruit_ble_midi'],
+ py_modules=["adafruit_ble_midi"],
)