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