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