Skip to content

Commit 2ceed3f

Browse files
authored
Merge pull request #1 from tannewt/lint
Lint
2 parents 62731b0 + 125e4db commit 2ceed3f

File tree

7 files changed

+323
-113
lines changed

7 files changed

+323
-113
lines changed

README.rst

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ Installing from PyPI
3535
.. note:: This library is not available on PyPI yet. Install documentation is included
3636
as a standard element. Stay tuned for PyPI availability!
3737

38-
.. todo:: Remove the above note if PyPI version is/will be available at time of release.
39-
If the library is not planned for PyPI, remove the entire 'Installing from PyPI' section.
40-
4138
On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
4239
PyPI <https://pypi.org/project/adafruit-circuitpython-ble_midi/>`_. To install for current user:
4340

@@ -63,7 +60,55 @@ To install in a virtual environment in your current project:
6360
Usage Example
6461
=============
6562

66-
.. todo:: Add a quick, simple example. It and other examples should live in the examples folder and be included in docs/examples.rst.
63+
.. code-block:: python
64+
65+
"""
66+
This example sends MIDI out. It sends NoteOn and then NoteOff with a random pitch bend.
67+
"""
68+
69+
import time
70+
import random
71+
import adafruit_ble
72+
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
73+
import adafruit_ble_midi
74+
import adafruit_midi
75+
from adafruit_midi.control_change import ControlChange
76+
from adafruit_midi.note_off import NoteOff
77+
from adafruit_midi.note_on import NoteOn
78+
from adafruit_midi.pitch_bend import PitchBend
79+
80+
# Use default HID descriptor
81+
midi_service = adafruit_ble_midi.MIDIService()
82+
advertisement = ProvideServicesAdvertisement(midi_service)
83+
# advertisement.appearance = 961
84+
85+
ble = adafruit_ble.BLERadio()
86+
if ble.connected:
87+
for c in ble.connections:
88+
c.disconnect()
89+
90+
midi = adafruit_midi.MIDI(midi_out=midi_service, out_channel=0)
91+
92+
print("advertising")
93+
ble.start_advertising(advertisement)
94+
95+
while True:
96+
print("Waiting for connection")
97+
while not ble.connected:
98+
pass
99+
print("Connected")
100+
while ble.connected:
101+
midi.send(NoteOn(44, 120)) # G sharp 2nd octave
102+
time.sleep(0.25)
103+
a_pitch_bend = PitchBend(random.randint(0, 16383))
104+
midi.send(a_pitch_bend)
105+
time.sleep(0.25)
106+
# note how a list of messages can be used
107+
midi.send([NoteOff("G#2", 120), ControlChange(3, 44)])
108+
time.sleep(0.5)
109+
print("Disconnected")
110+
print()
111+
ble.start_advertising(advertisement)
67112
68113
Contributing
69114
============

adafruit_ble_midi.py

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,87 +39,155 @@
3939
__version__ = "0.0.0-auto.0"
4040
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE_MIDI.git"
4141

42+
4243
class _MidiCharacteristic(ComplexCharacteristic):
4344
"""Endpoint for sending commands to a media player. The value read will list all available
4445
4546
commands."""
47+
4648
uuid = VendorUUID("7772E5DB-3868-4112-A1A9-F2669D106BF3")
4749

4850
def __init__(self):
49-
super().__init__(properties=Characteristic.WRITE_NO_RESPONSE | Characteristic.READ | Characteristic.NOTIFY,
50-
read_perm=Attribute.OPEN, write_perm=Attribute.OPEN,
51-
max_length=512,
52-
fixed_length=False)
51+
super().__init__(
52+
properties=Characteristic.WRITE_NO_RESPONSE
53+
| Characteristic.READ
54+
| Characteristic.NOTIFY,
55+
read_perm=Attribute.ENCRYPT_NO_MITM,
56+
write_perm=Attribute.ENCRYPT_NO_MITM,
57+
max_length=512,
58+
fixed_length=False,
59+
)
5360

5461
def bind(self, service):
5562
"""Binds the characteristic to the given Service."""
5663
bound_characteristic = super().bind(service)
57-
return _bleio.PacketBuffer(bound_characteristic,
58-
buffer_size=4)
64+
return _bleio.PacketBuffer(bound_characteristic, buffer_size=4)
65+
5966

6067
class MIDIService(Service):
68+
"""BLE MIDI service. It acts just like a USB MIDI PortIn and PortOut and can be used as a drop
69+
in replacement.
70+
71+
BLE MIDI's protocol includes timestamps for MIDI messages. This class automatically adds them
72+
to MIDI data written out and strips them from MIDI data read in."""
73+
6174
uuid = VendorUUID("03B80E5A-EDE8-4B33-A751-6CE34EC4C700")
6275
_raw = _MidiCharacteristic()
76+
# _raw gets shadowed for each MIDIService instance by a PacketBuffer. PyLint doesn't know this
77+
# so it complains about missing members.
78+
# pylint: disable=no-member
6379

6480
def __init__(self, **kwargs):
6581
super().__init__(**kwargs)
66-
self._in_buffer = bytearray(self._raw.packet_length)
82+
self._in_buffer = bytearray(self._raw.packet_size)
6783
self._out_buffer = None
6884
shared_buffer = memoryview(bytearray(4))
69-
self._buffers = [None, shared_buffer[:1], shared_buffer[:2], shared_buffer[:3], shared_buffer[:4]]
85+
self._buffers = [
86+
None,
87+
shared_buffer[:1],
88+
shared_buffer[:2],
89+
shared_buffer[:3],
90+
shared_buffer[:4],
91+
]
7092
self._header = bytearray(1)
7193
self._in_sysex = False
7294
self._message_target_length = None
7395
self._message_length = 0
7496
self._pending_realtime = None
97+
self._in_length = 0
98+
self._in_index = 1
99+
self._last_data = True
100+
101+
def readinto(self, buf, length):
102+
"""Reads up to ``length`` bytes into ``buf`` starting at index 0.
103+
104+
Returns the number of bytes written into ``buf``."""
105+
i = 0
106+
while i < length:
107+
if self._in_index < self._in_length:
108+
byte = self._in_buffer[self._in_index]
109+
if self._last_data and byte & 0x80 != 0:
110+
# Maybe manage timing here. Not done now because we're likely slower than we
111+
# need to be already.
112+
# low_ms = byte & 0x7f
113+
# print("low", low_ms)
114+
self._in_index += 1
115+
self._last_data = False
116+
continue
117+
self._in_index += 1
118+
self._last_data = True
119+
buf[i] = byte
120+
i += 1
121+
else:
122+
if len(self._in_buffer) < self._raw.packet_size:
123+
self._in_buffer = bytearray(self._raw.packet_size)
124+
self._in_length = self._raw.readinto(self._in_buffer)
125+
if self._in_length == 0:
126+
break
127+
# high_ms = self._in_buffer[0] & 0x3f
128+
# print("high", high_ms)
129+
self._in_index = 1
130+
self._last_data = True
131+
132+
return i
75133

76134
def read(self, length):
77-
self._raw.read(self._in_buffer)
78-
return None
135+
"""Reads up to ``length`` bytes and returns them."""
136+
result = bytearray(length)
137+
i = self.readinto(result, length)
138+
return result[:i]
79139

80140
def write(self, buf, length):
141+
"""Writes ``length`` bytes out."""
142+
# pylint: disable=too-many-branches
81143
timestamp_ms = time.monotonic_ns() // 1000000
82-
self._header[0] = (timestamp_ms >> 7 & 0x3f) | 0x80
144+
self._header[0] = (timestamp_ms >> 7 & 0x3F) | 0x80
83145
i = 0
84146
while i < length:
85147
data = buf[i]
86148
command = data & 0x80 != 0
87149
if self._in_sysex:
88-
if command: # End of sysex or real time
150+
if command: # End of sysex or real time
89151
b = self._buffers[2]
90-
b[0] = 0x80 | (timestamp_ms & 0x7f)
91-
b[1] = 0xf7
92-
self._raw.write(b, self._header)
93-
self._in_sysex = data == 0xf7
152+
b[0] = 0x80 | (timestamp_ms & 0x7F)
153+
b[1] = 0xF7
154+
self._raw.write(b, header=self._header)
155+
self._in_sysex = data == 0xF7
94156
else:
95157
b = self._buffers[1]
96158
b[0] = data
97-
self._raw.write(b, self._header)
159+
self._raw.write(b, header=self._header)
98160
elif command:
99-
self._in_sysex = data == 0xf0
161+
self._in_sysex = data == 0xF0
100162
b = self._buffers[2]
101-
b[0] = 0x80 | (timestamp_ms & 0x7f)
163+
b[0] = 0x80 | (timestamp_ms & 0x7F)
102164
b[1] = data
103-
if 0xf6 <= data <= 0xff or self._in_sysex: # Real time, command only or start sysex
165+
if (
166+
0xF6 <= data <= 0xFF or self._in_sysex
167+
): # Real time, command only or start sysex
104168
if self._message_target_length:
105169
self._pending_realtime = b
106170
else:
107171
self._raw.write(b, self._header)
108172
else:
109-
if 0x80 <= data <= 0xbf or 0xe0 <= data <= 0xef or data == 0xf2: # Two following bytes
173+
if (
174+
0x80 <= data <= 0xBF or 0xE0 <= data <= 0xEF or data == 0xF2
175+
): # Two following bytes
110176
self._message_target_length = 4
111177
else:
112178
self._message_target_length = 3
113179
b = self._buffers[self._message_target_length]
114-
# All of the buffers share memory so the timestamp and data have already been set.
180+
# All of the buffers share memory so the timestamp and data have already been
181+
# set.
115182
self._message_length = 2
116183
self._out_buffer = b
117184
else:
118185
self._out_buffer[self._message_length] = data
119186
self._message_length += 1
120187
if self._message_target_length == self._message_length:
121-
self._raw.write(self._out_buffer, self._header)
122-
if _pending_realtime:
123-
self._raw.write(self._pending_realtime, self._header)
188+
self._raw.write(self._out_buffer, header=self._header)
189+
if self._pending_realtime:
190+
self._raw.write(self._pending_realtime, header=self._header)
124191
self._pending_realtime = None
125192
self._message_target_length = None
193+
i += 1

0 commit comments

Comments
 (0)