Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
78900af
aioble: Add short name support to scan results.
FlantasticDan Feb 16, 2023
52fcb8e
cbor2: Add cbor2 library.
iabdalkader Nov 29, 2022
9ee0257
senml: Add SenML library.
iabdalkader Sep 27, 2022
1eb282a
tools/ci.sh: Support publishing package and index files to GitHub Pages.
projectgus Feb 20, 2023
40dfc5f
github/workflows: Attach built packages to GitHub workflow artifacts.
projectgus Feb 24, 2023
b9741f6
cbor2: Remove u-module prefix from imports.
iabdalkader Mar 2, 2023
295a9e3
senml: Remove u-module prefix from imports.
iabdalkader Mar 2, 2023
c860319
senml: Fix data record encoding to use binascii instead of base64.
iabdalkader Mar 3, 2023
ea21cb3
iperf3: Support devices without os.urandom().
dpgeorge Mar 21, 2023
f672353
unittest-discover: Print results when no tests are found/run.
pi-anl Mar 16, 2023
386ab99
python-ecosys: Add pypi= to metadata.
jimmo Apr 6, 2023
afc9d0a
micropython: Add missing metadata for packages.
jimmo Apr 6, 2023
9b5f4d7
tools/makepyproject.py: Add tool to generate PyPI package.
jimmo Mar 31, 2023
01db3da
senml: Allow publishing to PyPI as micropython-senml.
jimmo Mar 31, 2023
c113611
aioble: Fix descriptor flag handling.
jimmo Apr 11, 2023
a1b9aa9
aioespnow: Add library providing asyncio support for espnow module.
glenn20 Apr 7, 2023
7128d42
utarfile: Support creating/appending tar files.
dpwe May 14, 2023
1957f24
lora: Add lora modem drivers for SX127x and SX126x.
projectgus Jun 28, 2022
2fba6b8
lora: Workaround SX1262 bug with GetStatus.
projectgus Jan 16, 2023
da5ddfc
hashlib: Refactor, split, and optimise.
jimmo Jun 1, 2023
0a5b635
utarfile: Fix read/write handling of nulls in tar header.
dpgeorge Jul 21, 2023
fe3e0a2
cmd: Remove comments about using the string module.
jimmo Jul 21, 2023
66924d9
xmltok: Change StopIteration to EOFError due to PEP-479.
ThunderEX Feb 3, 2021
c48b17d
aiorepl/README.md: More info about globals.
jimmo Jul 21, 2023
c2b44ea
types: Add manifest file.
pi-anl Apr 19, 2023
0f768c9
bisect: Add manifest file.
pi-anl Apr 20, 2023
b95deb3
json: Add manifest file.
pi-anl Apr 20, 2023
028a369
keyword: Add manifest file.
pi-anl Apr 20, 2023
5329ef5
logging: Add full support for logging exception tracebacks.
pi-anl May 18, 2023
0e68c7d
copy: Declare dependency on types.
pi-anl Jun 13, 2023
ff84231
aiorepl: Replace f-string with str.format.
jimmo Jun 20, 2023
6103823
all: Remove __version__ from .py files.
jimmo Jun 22, 2023
e45a7f6
fnmatch: Fix compatibility with ure -> re.
pi-anl Jul 6, 2023
5004436
tarfile: Rename from utarfile.
jimmo Jul 21, 2023
8513bfb
requests: Rename urequests to requests.
jimmo Jul 21, 2023
87b4cda
aiorepl: Bump patch version.
jimmo Jul 21, 2023
97b7a30
xmltok: Bump patch version.
jimmo Jul 21, 2023
a19d2a3
copy: Bump patch version.
jimmo Jul 21, 2023
ebbb78e
logging: Bump minor version.
jimmo Jul 21, 2023
8fc9eda
all: Standardise x.y.z versioning for all packages.
jimmo Jul 21, 2023
752ce66
github/workflows: Build all example .py files as part of CI.
dpgeorge Jul 24, 2023
4da6e6f
all: Lint Python code with ruff.
cclauss May 2, 2023
36e74c1
zlib: Add zlib module.
jimmo Jun 26, 2023
2328592
tools/codeformat.py: Remove git state detection.
jimmo Jul 25, 2023
5cdfe71
top: Add pre-commit config.
jimmo Jul 25, 2023
efa0402
tools/codeformat.py: Fix ruff warnings.
jimmo Jul 25, 2023
ce3f282
github/workflows: Split ruff into its own action.
jimmo Jul 25, 2023
01ab7ba
iperf3: Add compatibility for servers pre version 3.2.
graeme-winter May 21, 2023
674e734
drivers/display/lcd160cr: Use isinstance() for type checking.
projectgus Aug 9, 2023
86050c3
bmm150: Remove broken reset function.
projectgus Aug 9, 2023
2d16f21
lsm6dsox: Add missing time import.
projectgus Aug 9, 2023
1f3002b
wm8960: Add missing self reference for sample table.
projectgus Aug 9, 2023
786c0ea
all: Add missing const imports
projectgus Aug 9, 2023
c6a72c7
cbor2: Improve decoder to pass Ruff F821 undefined-name.
projectgus Aug 9, 2023
991ac98
iperf3: Pre-declare some variables set in the loop.
projectgus Aug 9, 2023
b46306c
uaiohttpclient: Fix missing name in unreachable example code.
projectgus Aug 9, 2023
5b6fb2b
top: Enable Ruff linter to check undefined-name (F821).
projectgus Aug 9, 2023
1b557ee
lsm6dsox: Bump patch version.
dpgeorge Aug 23, 2023
dc765ad
wm8960: Bump patch version.
dpgeorge Aug 23, 2023
93bf707
lora: Remove the pin parameter from IRQ callback.
projectgus Aug 8, 2023
ed688cf
lora: Add STM32WL55 subghz LoRa modem class.
projectgus Nov 10, 2022
0bdecbc
lora: Note known issue with STM32WL5 HP antenna.
projectgus Aug 9, 2023
7fcc728
lora/sx126x: Fix busy timeout handling.
projectgus Aug 23, 2023
e6b89ea
all: Remove unnecessary start argument in range.
dpgeorge Aug 31, 2023
55d1d23
__future__: Add "annotations".
smurfix Sep 24, 2023
e5ba864
aioble/server.py: Add data arg for indicate.
jimmo Sep 14, 2023
46748d2
aioble/server.py: Allow BufferedCharacteristic to support all ops.
jimmo Sep 14, 2023
e025c84
requests: Fix detection of iterators in chunked data requests.
bwhitman May 30, 2023
0620d02
.github/workflows/ruff.yml: Pin to 0.1.0.
jimmo Oct 17, 2023
d8e163b
unix-ffi/re: Convert to PCRE2.
Ansuel Sep 28, 2023
ad0a259
tools/verifygitlog.py: Add git commit message checking.
jimmo Oct 26, 2023
cee0945
all: Replace "black" with "ruff format".
jimmo Oct 17, 2023
83f3991
lcd160cr: Remove support for options in manifest.
jimmo Nov 10, 2023
340243e
time: Add README to explain the purpose of the time extension library.
mattytrentini Sep 30, 2023
41aa257
base64: Implement custom maketrans and translate methods.
magixyu Nov 12, 2023
e051a12
aiorepl: Update import of asyncio.
pi-anl Oct 24, 2023
d41851c
aiorepl: Add support for paste mode (ctrl-e).
pi-anl Oct 24, 2023
10c9281
aiorepl: Add cursor left/right support.
pi-anl Oct 24, 2023
f672baa
aiorepl: Add support for raw mode (ctrl-a).
pi-anl Oct 24, 2023
ae8ea8d
os-path: Implement os.path.isfile().
scivision Sep 14, 2023
149226d
uaiohttpclient: Fix hard coded port 80.
bulletmark Nov 7, 2023
9d09cdd
uaiohttpclient: Make flake8 inspired improvements.
bulletmark Nov 7, 2023
05efdd0
uaiohttpclient: Update "yield from" to "await".
bulletmark Nov 7, 2023
9ceda53
uaiohttpclient: Update example client code.
bulletmark Nov 7, 2023
57ce3ba
aioble: Fix advertising variable name to use us not ms.
bhavesh-k Nov 20, 2023
7cdf708
aiohttp: Add new aiohttp package.
Carglglz Sep 5, 2023
803452a
umqtt.simple: Simplify check for user being unused.
felixdoerre Feb 1, 2024
35d41db
ssl: Restructure micropython SSL interface to a new tls module.
felixdoerre Feb 1, 2024
ddb1a27
hmac: Fix passing in a string for digestmod argument.
Pharkie Jan 20, 2024
56f514f
aiohttp: Fix binary data treatment.
Carglglz Jan 3, 2024
8058b29
tarfile-write: Fix permissions when adding to archive.
ubidefeo Feb 5, 2024
4cc6706
tools/ci.sh: Add unix-ffi library when testing unix-ffi subdirectory.
projectgus Feb 13, 2024
b712103
lora-sx126x: Fix invalid default configuration after reset.
projectgus Feb 13, 2024
ad6ab5a
lora-sync: Fix race with fast or failed send().
projectgus Feb 13, 2024
5462848
lora-sx127x: Implement missing syncword support.
projectgus Feb 13, 2024
35bb795
lora-sx126x: Fix syncword setting.
projectgus Feb 13, 2024
2242465
lora-sx126x: Clean up some struct formatting.
projectgus Feb 13, 2024
ffb07db
gzip: Fix recursion error in open() function.
dpgeorge Feb 29, 2024
23df50d
unix-ffi: Remove "unix_ffi" argument from require().
dpgeorge Mar 17, 2024
5c7e3fc
json: Move to unix-ffi.
jimmo Feb 22, 2024
8ee876d
cbor2: Deprecate decoder and encoder modules.
iabdalkader Mar 6, 2024
661efa4
senml: Use the updated cbor2 API.
iabdalkader Mar 6, 2024
744f8c4
usb: Add USB device support packages.
projectgus Mar 27, 2024
dfdedeb
The new lines 105 and 106 were necessary to ensure the proper handlin…
wzab Jun 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
usb: Add USB device support packages.
These packages build on top of machine.USBDevice() to provide high level
and flexible support for implementing USB devices in Python code.

Additional credits, as per included copyright notices:

- CDC support based on initial implementation by @hoihu with fixes by
  @linted.

- MIDI support based on initial implementation by @paulhamsh.

- HID keypad example based on work by @turmoni.

- Everyone who tested and provided feedback on early versions of these
  packages.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <[email protected]>
  • Loading branch information
projectgus committed Mar 27, 2024
commit 744f8c4ea5310d9c320beb46f75843b2e2df8fcb
85 changes: 85 additions & 0 deletions micropython/usb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Dynamic USB packages

These packages allow implementing USB functionality on a MicroPython device using pure Python code.

Currently only USB device is implemented, not USB host.

## USB Device support

### Support

USB Device support depends on the low-level [machine.USBDevice](https://docs.micropython.org/en/latest/library/machine.USBDevice.html) class. This class is new and not supported on all ports, so please check the documentation for your MicroPython version. It is possible to implement a USB device using only the low-level USBDevice class. These packages are higher level and easier to use.

For more information about how to install packages, or "freeze" them into a
firmware image, consult the [MicroPython documentation on "Package
management"](https://docs.micropython.org/en/latest/reference/packages.html).

### Examples

The [examples/device](examples/device) directory in this repo has a range of examples. After installing necessary packages, you can download an example and run it with `mpremote run EXAMPLE_FILENAME.py` ([mpremote docs](https://docs.micropython.org/en/latest/reference/mpremote.html#mpremote-command-run)).

#### Unexpected serial disconnects

If you normally connect to your MicroPython device over a USB serial port ("USB CDC"), then running a USB example will disconnect mpremote when the new USB device configuration activates and the serial port has to temporarily disconnect. It is likely that mpremote will print an error. The example should still start running, if necessary then you can reconnect with mpremote and type Ctrl-B to restore the MicroPython REPL and/or Ctrl-C to stop the running example.

If you use `mpremote run` again while a different runtime USB configuration is already active, then the USB serial port may disconnect immediately before the example runs. This is because mpremote has to soft-reset MicroPython, and when the existing USB device is reset then the entire USB port needs to reset. If this happens, run the same `mpremote run` command again.

We plan to add features to `mpremote` so that this limitation is less disruptive. Other tools that communicate with MicroPython over the serial port will encounter similar issues when runtime USB is in use.

### Initialising runtime USB

The overall pattern for enabling USB devices at runtime is:

1. Instantiate the Interface objects for your desired USB device.
2. Call `usb.device.get()` to get the singleton object for the high-level USB device.
3. Call `init(...)` to pass the desired interfaces as arguments, plus any custom
keyword arguments to configure the overall device.

An example, similar to [mouse_example.py](examples/device/mouse_example.py):

```py
m = usb.device.mouse.MouseInterface()
usb.device.get().init(m, builtin_driver=True)
```

Setting `builtin_driver=True` means that any built-in USB serial port will still
be available. Otherwise, you may permanently lose access to MicroPython until
the next time the device resets.

See [Unexpected serial disconnects](#Unexpected-serial-disconnects), above, for
an explanation of possible errors or disconnects when the runtime USB device
initialises.

Placing the call to `usb.device.get().init()` into the `boot.py` of the MicroPython file system allows the runtime USB device to initialise immediately on boot, before any built-in USB. However, note that calling this function on boot without `builtin_driver=True` will make the MicroPython USB serial interface permanently inaccessible until you "safe mode boot" (on supported boards) or completely erase the flash of your device.

### Package usb-device-keyboard

This package provides the `usb.device.keyboard` module. See [keyboard_example.py](examples/device/keyboard_example.py) for an example program.

### Package usb-device-mouse

This package provides the `usb.device.mouse` module. See [mouse_example.py](examples/device/mouse_example.py) for an example program.

### Package usb-device-hid

This package provides the `usb.device.hid` module. USB HID (Human Interface Device) class allows creating a wide variety of device types. The most common are mouse and keyboard, which have their own packages in micropython-lib. However, using the usb-device-hid package directly allows creation of any kind of HID device.

See [hid_custom_keypad_example.py](examples/device/hid_custom_keypad_example.py) for an example of a Keypad HID device with a custom HID descriptor.

### Package usb-device-cdc

This package provides the `usb.device.cdc` module. USB-CDC (Communications Device Class) is most commonly used for virtual serial port USB interfaces, and that is what is supported here.

The example [cdc_repl_example.py](examples/device/cdc_repl_example.py) demonstrates how to add a second USB serial interface and duplicate the MicroPython REPL between the two.

### Package usb-device-midi

This package provides the `usb.device.midi` module. This allows implementing MIDI devices in MicroPython.

The example [midi_example.py](examples/device/midi_example.py) demonstrates how to create a simple MIDI device to send MIDI data to the USB host.

### Package usb-device

This package contains the common implementation components for the other packages, and can be used to create new and different USB device types. All of the other packages depend on this package.

It provides the `usb.device.get()` function for accessing the Device singleton object, and the `usb.device.impl` module which contains the building blocks for implementing new USB interface drivers in Python. The best examples of how to use these classes is the source code of the other USB device packages.
41 changes: 41 additions & 0 deletions micropython/usb/examples/device/cdc_repl_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# MicroPython USB CDC REPL example
#
# Example demonstrating how to use os.dupterm() to provide the
# MicroPython REPL on a dynamic CDCInterface() serial port.
#
# Note that if you run this example on the built-in USB CDC port via 'mpremote
# run' then you'll have to reconnect after it re-enumerates, and it may be
# necessary afterward to type Ctrl-B to exit the Raw REPL mode and resume the
# interactive REPL back.
#
# This example uses the usb-device-cdc package for the CDCInterface class.
# This can be installed with:
#
# mpremote mip install usb-device-cdc
#
# MIT license; Copyright (c) 2023-2024 Angus Gratton
import os
import time
import usb.device
from usb.device.cdc import CDCInterface

cdc = CDCInterface()
cdc.init(timeout=0) # zero timeout makes this non-blocking, suitable for os.dupterm()

# pass builtin_driver=True so that we get the built-in USB-CDC alongside,
# if it's available.
usb.device.get().init(cdc, builtin_driver=True)

print("Waiting for USB host to configure the interface...")

# wait for host enumerate as a CDC device...
while not cdc.is_open():
time.sleep_ms(100)

# Note: This example doesn't wait for the host to access the new CDC port,
# which could be done by polling cdc.dtr, as this will block the REPL
# from resuming while this code is still executing.

print("CDC port enumerated, duplicating REPL...")

old_term = os.dupterm(cdc)
137 changes: 137 additions & 0 deletions micropython/usb/examples/device/hid_custom_keypad_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# MicroPython USB HID custom Keypad example
#
# This example demonstrates creating a custom HID device with its own
# HID descriptor, in this case for a USB number keypad.
#
# For higher level examples that require less code to use, see mouse_example.py
# and keyboard_example.py
#
# This example uses the usb-device-hid package for the HIDInterface class.
# This can be installed with:
#
# mpremote mip install usb-device-hid
#
# MIT license; Copyright (c) 2023 Dave Wickham, 2023-2024 Angus Gratton
from micropython import const
import time
import usb.device
from usb.device.hid import HIDInterface

_INTERFACE_PROTOCOL_KEYBOARD = const(0x01)


def keypad_example():
k = KeypadInterface()

usb.device.get().init(k, builtin_driver=True)

while not k.is_open():
time.sleep_ms(100)

while True:
time.sleep(2)
print("Press NumLock...")
k.send_key("<NumLock>")
time.sleep_ms(100)
k.send_key()
time.sleep(1)
# continue
print("Press ...")
for _ in range(3):
time.sleep(0.1)
k.send_key(".")
time.sleep(0.1)
k.send_key()
print("Starting again...")


class KeypadInterface(HIDInterface):
# Very basic synchronous USB keypad HID interface

def __init__(self):
super().__init__(
_KEYPAD_REPORT_DESC,
set_report_buf=bytearray(1),
protocol=_INTERFACE_PROTOCOL_KEYBOARD,
interface_str="MicroPython Keypad",
)
self.numlock = False

def on_set_report(self, report_data, _report_id, _report_type):
report = report_data[0]
b = bool(report & 1)
if b != self.numlock:
print("Numlock: ", b)
self.numlock = b

def send_key(self, key=None):
if key is None:
self.send_report(b"\x00")
else:
self.send_report(_key_to_id(key).to_bytes(1, "big"))


# See HID Usages and Descriptions 1.4, section 10 Keyboard/Keypad Page (0x07)
#
# This keypad example has a contiguous series of keys (KEYPAD_KEY_IDS) starting
# from the NumLock/Clear keypad key (0x53), but you can send any Key IDs from
# the table in the HID Usages specification.
_KEYPAD_KEY_OFFS = const(0x53)

_KEYPAD_KEY_IDS = [
"<NumLock>",
"/",
"*",
"-",
"+",
"<Enter>",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
".",
]


def _key_to_id(key):
# This is a little slower than making a dict for lookup, but uses
# less memory and O(n) can be fast enough when n is small.
return _KEYPAD_KEY_IDS.index(key) + _KEYPAD_KEY_OFFS


# HID Report descriptor for a numeric keypad
#
# fmt: off
_KEYPAD_REPORT_DESC = (
b'\x05\x01' # Usage Page (Generic Desktop)
b'\x09\x07' # Usage (Keypad)
b'\xA1\x01' # Collection (Application)
b'\x05\x07' # Usage Page (Keypad)
b'\x19\x00' # Usage Minimum (0)
b'\x29\xFF' # Usage Maximum (ff)
b'\x15\x00' # Logical Minimum (0)
b'\x25\xFF' # Logical Maximum (ff)
b'\x95\x01' # Report Count (1),
b'\x75\x08' # Report Size (8),
b'\x81\x00' # Input (Data, Array, Absolute)
b'\x05\x08' # Usage page (LEDs)
b'\x19\x01' # Usage Minimum (1)
b'\x29\x01' # Usage Maximum (1),
b'\x95\x01' # Report Count (1),
b'\x75\x01' # Report Size (1),
b'\x91\x02' # Output (Data, Variable, Absolute)
b'\x95\x01' # Report Count (1),
b'\x75\x07' # Report Size (7),
b'\x91\x01' # Output (Constant) - padding bits
b'\xC0' # End Collection
)
# fmt: on


keypad_example()
80 changes: 80 additions & 0 deletions micropython/usb/examples/device/keyboard_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# MicroPython USB Keyboard example
#
# This example uses the usb-device-keyboard package for the KeyboardInterface class.
# This can be installed with:
#
# mpremote mip install usb-device-keyboard
#
# To implement a keyboard with different USB HID characteristics, copy the
# usb-device-keyboard/usb/device/keyboard.py file into your own project and modify
# KeyboardInterface.
#
# MIT license; Copyright (c) 2024 Angus Gratton
import usb.device
from usb.device.keyboard import KeyboardInterface, KeyCode, LEDCode
from machine import Pin
import time

# Tuples mapping Pin inputs to the KeyCode each input generates
#
# (Big keyboards usually multiplex multiple keys per input with a scan matrix,
# but this is a simple example.)
KEYS = (
(Pin.cpu.GPIO10, KeyCode.CAPS_LOCK),
(Pin.cpu.GPIO11, KeyCode.LEFT_SHIFT),
(Pin.cpu.GPIO12, KeyCode.M),
(Pin.cpu.GPIO13, KeyCode.P),
# ... add more pin to KeyCode mappings here if needed
)

# Tuples mapping Pin outputs to the LEDCode that turns the output on
LEDS = (
(Pin.board.LED, LEDCode.CAPS_LOCK),
# ... add more pin to LEDCode mappings here if needed
)


class ExampleKeyboard(KeyboardInterface):
def on_led_update(self, led_mask):
# print(hex(led_mask))
for pin, code in LEDS:
# Set the pin high if 'code' bit is set in led_mask
pin(code & led_mask)


def keyboard_example():
# Initialise all the pins as active-low inputs with pullup resistors
for pin, _ in KEYS:
pin.init(Pin.IN, Pin.PULL_UP)

# Initialise all the LEDs as active-high outputs
for pin, _ in LEDS:
pin.init(Pin.OUT, value=0)

# Register the keyboard interface and re-enumerate
k = ExampleKeyboard()
usb.device.get().init(k, builtin_driver=True)

print("Entering keyboard loop...")

keys = [] # Keys held down, reuse the same list object
prev_keys = [None] # Previous keys, starts with a dummy value so first
# iteration will always send
while True:
if k.is_open():
keys.clear()
for pin, code in KEYS:
if not pin(): # active-low
keys.append(code)
if keys != prev_keys:
# print(keys)
k.send_keys(keys)
prev_keys.clear()
prev_keys.extend(keys)

# This simple example scans each input in an infinite loop, but a more
# complex implementation would probably use a timer or similar.
time.sleep_ms(1)


keyboard_example()
50 changes: 50 additions & 0 deletions micropython/usb/examples/device/midi_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# MicroPython USB MIDI example
#
# This example demonstrates creating a custom MIDI device.
#
# This example uses the usb-device-midi package for the MIDIInterface class.
# This can be installed with:
#
# mpremote mip install usb-device-midi
#
# MIT license; Copyright (c) 2023-2024 Angus Gratton
import usb.device
from usb.device.midi import MIDIInterface
import time


class MIDIExample(MIDIInterface):
def on_note_on(self, channel, pitch, vel):
print(f"RX Note On channel {channel} pitch {pitch} velocity {vel}")

def on_note_off(self, channel, pitch, vel):
print(f"RX Note Off channel {channel} pitch {pitch} velocity {vel}")

def on_control_change(self, channel, controller, value):
print(f"RX Control channel {channel} controller {controller} value {value}")


m = MIDIExample()
usb.device.get().init(m)

print("Waiting for USB host to configure the interface...")

while not m.is_open():
time.sleep_ms(100)

print("Starting MIDI loop...")

control_val = 0
channel = 0

while m.is_open():
time.sleep(1)
m.note_on(channel, 60)
time.sleep(0.5)
m.note_off(channel, 60)
time.sleep(1)
m.control_change(channel, 64, control_val)
control_val += 1
if control_val == 0x7F:
control_val = 0
time.sleep(1)
Loading