suitcase scripting examples¶
The following samples can be browsed at suitcase_script/
suitcase_midi_demo.py¶
The following script opens both an Arduino serial port connection and a MIDI input port, then translates drum pad ‘note on’ events into motion commands. This is intended to be run from the command line and needs to configured for the specific ports on your machine using command line arguments. To see the current options:
python3 suitcase_midi_demo.py --help
A typical invocation under macOS with all options specified might look like this:
python3 suitcase_midi_demo.py --port /dev/cu.usbmodem1444301 --midi "MPD218 Port A" --verbose --debug
A typical invocation under Window with all options specified might look like this:
python.exe -u suitcase_midi_demo.py --port "COM5" --midi "MPD218 0" --verbose --debug
The exact serial name for –port and MIDI device name for –midi depend on your system. One way to identify the Arduino is to launch the Arduino IDE. The MIDI devices can be enumerated from Python using the list_MIDI_ports.py utility.
The full code follows, but may also be downloaded from suitcase_midi_demo.py.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | #!/usr/bin/env python3
"""\
suitcase_midi_demo.py : sample code in Python to translate MIDI events into
motion commands for an Arduino running StepperWinch
No copyright, 2021, Garth Zeglin. This file is explicitly placed in the public domain.
"""
#================================================================
# Standard Python modules.
import argparse
import time
import platform
# This requires a pySerial installation.
# Package details: https://pypi.python.org/pypi/pyserial,
# Documentation: http://pythonhosted.org/pyserial/
import serial
# For documentation on python-rtmidi: https://pypi.org/project/python-rtmidi/
import rtmidi
#================================================================
class StepperWinchClient(object):
"""Class to manage a connection to a serial-connected Arduino running the
StepperWinch script.
:param port: the name of the serial port device
:param verbose: flag to increase console output
:param debug: flag to print raw inputs on console
:param kwargs: collect any unused keyword arguments
"""
def __init__(self, port=None, verbose=False, debug=False, **kwargs ):
# initialize the client state
self.arduino_time = 0
self.position = [0, 0, 0]
self.target = [0, 0, 0]
self.verbose = verbose
self.debug = debug
self.awake = False
# open the serial port, which should also reset the Arduino
if self.verbose:
print(f"Opening Arduino on {port}")
self.port = serial.Serial( port, 115200, timeout=5 )
if self.verbose:
print("Opened serial port named", self.port.name)
print("Sleeping briefly while Arduino boots...")
# wait briefly for the Arduino to finish booting
time.sleep(2) # units are seconds
# throw away any extraneous input
self.port.flushInput()
return
def close(self):
"""Shut down the serial connection to the Arduino, after which this object may
no longer be used."""
self.port.close()
self.port = None
return
def _wait_for_input(self):
line = self.port.readline().rstrip()
if line:
elements = line.split()
if self.debug:
print("Received: '%s'" % line)
if elements[0] == b'txyza':
self.arduino_time = int(elements[1])
self.position = [int(s) for s in elements[2:]]
elif elements[0] == b'awake':
self.awake = True
elif elements[0] == b'dbg':
print("Received debugging message:", line)
else:
print("Unknown status message: ", line)
return
def _send_command(self, string):
if self.verbose:
print("Sending: ", string)
self.port.write(string.encode() + b'\n')
return
def motor_enable( self, value=True):
"""Issue a command to enable or disable the stepper motor drivers."""
self._send_command( "enable 1" if value is True else "enable 0" )
return
def set_freq_damping(self, freq, damping):
"""Issue a command to set the second-order model gains."""
self._send_command("g xyza %f %f" % (freq, damping))
return
def wait_for_wakeup(self):
"""Issue a status query and wait until an 'awake' status has been received."""
while self.awake is False:
self._send_command("ping")
self._wait_for_input()
def relative_move(self, flags, positions):
"""Issue a non-blocking relative move command for a subset of channels defined
by a set of flag characters."""
values = " ".join(["%d" % (i) for i in positions])
self._send_command("d %s %s" % (flags, values))
def move_to(self, position):
"""Issue a command to move to a [x, y, z, a] absolute position (specified in
microsteps) and wait until completion.
:param position: a list or tuple with at least four elements
"""
self._send_command("a xyza %d %d %d %d" % tuple(position))
self.target = position
# wait for all reported positions to be close to the request
moving = True
while moving:
self._wait_for_input()
if self.verbose:
print ("Position:", self.position)
moving = any([abs(pos - target) > 5 for pos, target in zip(self.position, self.target)])
return
################################################################
class MidiDriver(object):
"""Class to manage a MIDI input connection and translate events to motion commands."""
def __init__(self, client, args):
# save the client handle
self.client = client
if args.verbose:
print(f"Opening MIDI input to look for {args.midi}.")
# 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 args.midi in name:
print("Found preferred MIDI input device %d: %s" % (idx, name))
self.midi_in.open_port(idx)
self.midi_in.set_callback(self.midi_received)
break
else:
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.")
else:
print("Creating virtual MIDI input.")
self.midi_in.open_virtual_port(args.midi)
def midi_received(self, data, unused):
msg, delta_time = data
if len(msg) > 2:
if msg[0] == 153: # note on, channel 9
key = (msg[1] - 36) % 16
row = key // 4
col = key % 4
velocity = msg[2]
print("Pad (%d, %d): %d" % (row, col, velocity))
delta = velocity if row > 1 else -velocity
if row == 0 or row == 3:
delta = delta * 2
flag = "xyza"[col]
# send a single-channel relative move command
self.client.relative_move(flag, [delta])
#================================================================
cmd_desc = "Translate MIDI events to motion commands for the StepperWinch firmware on an Arduino."
# The following section is run when this is loaded as a script.
if __name__ == "__main__":
# Initialize the command parser.
parser = argparse.ArgumentParser(description = cmd_desc)
parser.add_argument('-v', '--verbose', action='store_true', help='Enable more detailed output.' )
parser.add_argument('--debug', action='store_true', help='Enable debugging output.' )
parser.add_argument('--midi', type=str, default = "MPD218 Port A",
help = "Keyword identifying the MIDI input device (default: %(default)s).")
parser.add_argument('-p', '--port', default='/dev/tty.usbmodem14101',
help='Specify the name of the Arduino serial port device (default is %(default)s).')
# Parse the command line, returning a Namespace.
args = parser.parse_args()
# Convert the Namespace to a set of keyword arguments.
kwargs = vars(args)
# Connect to the Arduino. Note that it will ignore extraneous values, e.g.,
# from additional command argument inputs unneeded by this object.
client = StepperWinchClient(**kwargs)
# Connect to the MIDI port.
driver = MidiDriver(client, args)
# Enter the event loop. MIDI events will arrive at the callback and trigger motion commands.
try:
print("Waiting for wakeup.")
client.wait_for_wakeup()
client.set_freq_damping(1.0, 0.5)
while True:
client._wait_for_input()
except KeyboardInterrupt:
print("User interrupted motion.")
# Issue a command to turn off the drivers, then shut down the connection.
client.motor_enable(False)
client.close()
|
suitcase_motion_demo.py¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | #!/usr/bin/env python3
"""\
suitcase_motion_demo.py : sample code in Python to communicate with an Arduino running StepperWinch
No copyright, 2021, Garth Zeglin. This file is explicitly placed in the public domain.
"""
#================================================================
import argparse
import time
# This requires a pySerial installation.
# Package details: https://pypi.python.org/pypi/pyserial,
# Documentation: http://pythonhosted.org/pyserial/
import serial
#================================================================
class StepperWinchClient(object):
"""Class to manage a connection to a serial-connected Arduino running the StepperWinch script.
:param port: the name of the serial port device
:param verbose: flag to increase console output
:param debug: flag to print raw inputs on sconsole
:param kwargs: collect any unused keyword arguments
"""
def __init__(self, port=None, verbose=False, debug=False, **kwargs ):
# initialize the client state
self.arduino_time = 0
self.position = [0, 0, 0]
self.target = [0, 0, 0]
self.verbose = verbose
self.debug = debug
self.awake = False
# open the serial port, which should also reset the Arduino
self.port = serial.Serial( port, 115200, timeout=5 )
if self.verbose:
print("Opened serial port named", self.port.name)
print("Sleeping briefly while Arduino boots...")
# wait briefly for the Arduino to finish booting
time.sleep(2) # units are seconds
# throw away any extraneous input
self.port.flushInput()
return
def close(self):
"""Shut down the serial connection to the Arduino, after which this object may no longer be used."""
self.port.close()
self.port = None
return
def _wait_for_input(self):
line = self.port.readline().rstrip()
if line:
elements = line.split()
if self.debug:
print("Received: '%s'" % line)
if elements[0] == b'txyza':
self.arduino_time = int(elements[1])
self.position = [int(s) for s in elements[2:]]
elif elements[0] == b'awake':
self.awake = True
elif elements[0] == b'dbg':
print("Received debugging message:", line)
else:
print("Unknown status message: ", line)
return
def _send_command(self, string):
if self.verbose:
print("Sending: ", string)
self.port.write(string.encode() + b'\n')
return
def motor_enable( self, value=True):
"""Issue a command to enable or disable the stepper motor drivers."""
self._send_command( "enable 1" if value is True else "enable 0" )
return
def set_freq_damping(self, freq, damping):
"""Issue a command to set the second-order model gains."""
self._send_command("g xyza %f %f" % (freq, damping))
return
def wait_for_wakeup(self):
"""Issue a status query and wait until an 'awake' status has been received."""
while self.awake is False:
self._send_command("ping")
self._wait_for_input()
def move_to(self, position):
"""Issue a command to move to a [x, y, z, a] absolute position (specified in microsteps) and wait until completion.
:param position: a list or tuple with at least four elements
"""
self._send_command("a xyza %d %d %d %d" % tuple(position))
self.target = position
# wait for all reported positions to be close to the request
moving = True
while moving:
self._wait_for_input()
if self.verbose:
print ("Position:", self.position)
moving = any([abs(pos - target) > 5 for pos, target in zip(self.position, self.target)])
return
#================================================================
cmd_desc = "Simple test client to send data to the StepperWinch firmware on an Arduino."
# The following section is run when this is loaded as a script.
if __name__ == "__main__":
# Initialize the command parser.
parser = argparse.ArgumentParser(description = cmd_desc)
parser.add_argument( '-v', '--verbose', action='store_true', help='Enable more detailed output.' )
parser.add_argument( '--debug', action='store_true', help='Enable debugging output.' )
parser.add_argument( '--extra', action='store_true', help='Unused.')
parser.add_argument( '-p', '--port', default='/dev/tty.usbmodem14101',
help='Specify the name of the Arduino serial port device (default is %(default)s.)')
# Parse the command line, returning a Namespace.
args = parser.parse_args()
# Convert the Namespace to a set of keyword arguments and initialize the
# client object. Note that it will ignore extraneous values, e.g., from
# additional command argument inputs unneeded by this object.
client = StepperWinchClient(**vars(args))
# Begin the motion sequence. This may be safely interrupted by the user pressing Control-C.
try:
print("Waiting for wakeup.")
client.wait_for_wakeup()
client.set_freq_damping(1.0, 0.5)
while True:
print("Beginning movement sequence.")
client.motor_enable()
client.move_to([100, 200, 300, 400])
client.move_to([0, 0, 0, 0])
client.move_to([4000, 1000, 2000, 3000])
client.move_to([0, 0, 0, 0])
client.move_to([400, 300, 200, 100])
client.move_to([0, 0, 0, 0])
client.move_to([3000, 4000, 1000, 2000])
client.move_to([0, 0, 0, 0])
except KeyboardInterrupt:
print("User interrupted motion.")
# Issue a command to turn off the drivers, then shut down the connection.
client.motor_enable(False)
client.close()
|