4.3. Exercise: Pneumatic Feedback Control

Objective: apply principles of proportional feedback to create a visible interaction between a mechanism and an object, environment, or viewer.

Deliverable: short 10-20 second video posted to the blog of a simple actuated structure interacting using feedback control.

This exercise builds on the previous pneumatics exercise by adding an element of sensing.

4.3.1. Background

Most common definitions of robot include some element of sensing and responding to the world. In many actual robots, this sensing is purely internal in the form of control feedback which regulates movement to achieve a specified position goal, target velocity, or trajectory. This feedback uses proprioceptive sensors to measure joint positions, applies a control model to compute appropriate actuator commands for a known physical system, and then applies force via the actuators. If it works, the mechanism moves closer to the goal state and errors are reduced. If the disturbances are too great, the process can fail to achieve the target or become unstable.

The ideas behind feedback predate robotics: a classic example is the flyball governor, invented in 1788 to regulate steam engine speed. The ideas have been generalized broadly beyond mechanical systems in the study of cybernetics.

A controller ‘closes the loop’ by creating a cyclical energetic and informational pathway including both a process and a representation of that process in the controller. For this exercise, the process will involve a pneumatically-actuated mechanism and the representation will involve the digitized position measurements and computational processes.

In this view, cause and effect is not well-defined: if the mechanism encounters a disturbance, then the controller will respond with air flow to an actuator. Likewise, if the program chooses a new position target, the controller will respond with air flow. In equilibrium, both disturbances and goals affect the balance of the system, with the intent of converging in a stable way toward an objective. But the resulting behavior is the composite of both ‘intent’ and ‘reaction’.

One outcome is that even simple feedback-controlled devices can be easily interpreted as ‘alive’ or having ‘purpose’ in the right context, and this effect is what this exercise is intended to explore.

Relevant terms: proportional control, PID control, degree of freedom (DOF), closed-loop control, position target, goal, disturbance, error signal, stability, bandwidth, lag, set point.

4.3.2. Resources

We will use the same valve and Arduino resources as the previous Exercise: Pneumatic Valves and Actuators, with the addition of position-sensing potentiometers.

The ValveControl sketch includes a position-regulation capability which can execute a control loop locally on the Arduino, but we will first start by computing the control function externally in a Python script. This will have a lower update rate than the local loop but be easier to adjust when getting started. We should see the effect of a low update rate in the form of longer lag time and lower-bandwidth disturbance rejection, and possibly control instabilities.

The position sensor we will use is a Bourns 6630S0D-C28-A102 22mm precision 1K potentiometer, purchased for $14.96 each. These use a conductive plastic element for high mechanical resolution and long life. They are only single-turn, so there is a 20 degree arc over which the wiper does not contact the resistor and the readings will be ambiguous. Part of the installation process involves adjusting the shaft mounting so that all physical mechanism positions fall within the 340 degree sensing angle range.

4.3.3. Procedures

The idea is to proceed through three quick experiments to see the effects of the controller, then try creating a physical feedback process with meaningful behavior. The experiments are as follows:

  1. Manipulate the feedback sensor by hand and observe the actuator movements. This isn’t creating a closed loop, it is just using the sensor as a remote control. Once this works, try watching the actuator and getting it to move to the center and stay there. If you’re successful, then you have created a closed-loop system in which you are part of the sensing pathway.
  2. Attach the feedback sensor directly to an actuated joint, calibrate parameters as needed, and create a closed-loop movement. This is a form of proprioceptive feedback to create proportional position control.
  3. Attach the feedback sensor to sense the relation between the moving structure and an external environment. This can be seen as a form of tactile sensing. For some configurations and parameters, this might create a closed-loop control to regulate contact. If the sensor physically breaks contact, it may not longer provide useful data and then the control will to treat it as an intermittent sensor.

After these observations, the creative objective is to find a configuration and controller which produces a behavior which is surprisingly animate, even on a one-DOF system.

4.3.4. Python Scripting

The example control script builds on the pneumatics example, but now receives the sensor measurements output from the Arduino and uses them to calculate the valve flow commands.

feedback_exercise.position_regulation.feedback_cycle(port, target, gain=0.5, **kwargs)[source]

Run one cycle of proportional feedback control for a sensor and valve pair attached to an actuator. Returns immediately or after an optional delay. No return value. Extra keyword arguments are passed along to issue_command().

Parameters:
  • port – serial port stream handle
  • target – goal position, specified as an integer in arbitrary sensor units
  • gain – proportional gain scaling sensor units to actuator units
  • verbose – flag to enable printed debugging output
Returns:

None

feedback_exercise.position_regulation.issue_command(port, flow, extend=1, retract=2, verbose=False)[source]

Issue an open-loop set of flow commands for a valve pair attached to an actuator. Returns immediately. No return value.

Parameters:
  • port – serial port stream handle
  • flow – net flow from -100 full retraction rate to 100 full extension rate
  • extend – channel number for extension valve pair
  • retract – channel number for retract valve pair
  • verbose – flag to enable printed debugging output
Returns:

None

The full script is below. It can also be directly downloaded from the Python source tree on the course site.

  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
#!/usr/bin/env python
"""position_regulation.py

Apply proportional control using sensor feedback and pneumatic valve activations
using a serial port connection to an Arduino running ValveControl.

Copyright (c) 2015-2017, Garth Zeglin.  All rights reserved. Licensed under the
terms of the BSD 3-clause license.

"""
################################################################
# Import standard Python 2.7 modules.
from __future__ import print_function
import os, sys, time, argparse

# This module requires a pySerial installation.
#  Package details: https://pypi.python.org/pypi/pyserial,
#  Documentation: http://pythonhosted.org/pyserial/
import serial

################################################################
def issue_command(port, flow, extend=1, retract=2, verbose=False):
    """Issue an open-loop set of flow commands for a valve pair attached to an
    actuator.  Returns immediately.  No return value.

    :param port: serial port stream handle
    :param flow: net flow from -100 full retraction rate to 100 full extension rate
    :param extend: channel number for extension valve pair
    :param retract: channel number for retract valve pair
    :param verbose: flag to enable printed debugging output
    :return: None
    """

    # Make sure the flow value is integral and bounded.
    flow = int(max(min(flow, 100),-100))

    if flow > 0:
        ext_command  = "speed %d %d" % (extend, flow)
        ret_command = "speed %d %d" % (retract, -100)
    elif flow == 0:
        ext_command  = "speed %d %d" % (extend, 0)
        ret_command = "speed %d %d" % (retract, 0)
    else:
        ext_command  = "speed %d %d" % (extend, -100)
        ret_command = "speed %d %d" % (retract, -flow)

    if verbose:
        print ("issuing %s, %s" % (ext_command, ret_command))

    port.write("%s\n%s\n" % (ext_command, ret_command))

    return

################################################################
def feedback_cycle(port, target, gain=0.5, **kwargs):
    """Run one cycle of proportional feedback control for a sensor and valve pair
    attached to an actuator.  Returns immediately or after an optional delay.
    No return value.  Extra keyword arguments are passed along to issue_command().

    :param port: serial port stream handle
    :param target: goal position, specified as an integer in arbitrary sensor units
    :param gain: proportional gain scaling sensor units to actuator units
    :param verbose: flag to enable printed debugging output
    :return: None

    """

    # Wait for a line of sensor data to be received from the Arduino.
    raw_sensor_data = port.readline()

    # The data format is specified in ValveControl, and may need to be adjusted if the sketch changes.
    # This assumes it is a space-delimited set of six numbers:
    #   <raw-position-0> <calibrated-position-0> <pwm-output-0> <raw-position-1> <calibrated-position-1> <pwm-output-1>

    # Divide the line into a list of strings.
    value_strings = raw_sensor_data.split()

    # Parse just the first raw position as an integer.  This is simply the
    # unfiltered 10-bit value of analogRead() from the potentiometer.
    position = int(value_strings[0])

    # For simplicity, the target is assumed to be specified in the same units.
    # In practice, this value should be calibrated to reflect a real-world
    # measurement so that gains can be chosen independent of the sensor scale.
    position_error = target - position

    # A positive error is assumed to map to a positive flow, e.g., the target is more extended than the current position.
    # The scaling factor maps the arbitrary sensor unit to flow percentage unit.
    flow_command = int(gain*position_error)

    # Optionally print a report.
    if kwargs.get('verbose', False):
        print("position: %d, target: %d, error: %d,   " % (position, target, position_error), end='')

    # Send the computed command back to the valve controller.
    issue_command(port, flow_command, **kwargs)

    return

################################################################
# The following section is run when this is loaded as a script.
if __name__ == "__main__":

    # Initialize the command parser.
    parser = argparse.ArgumentParser(description = """Position regulation example using the ValveControl controller on an Arduino.""")
    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('-p1', default=500, type=int, help='First position target.')
    parser.add_argument('-p2', default=600, type=int, help='Second position target.')
    parser.add_argument('-p', '--port', default='/dev/tty.usbmodem1411', help='Specify the name of the Arduino serial port device (default is /dev/tty.usbmodem1411).')

    # Parse the command line, returning a Namespace.
    args = parser.parse_args()

    # Open the serial port, which should also reset the Arduino
    print("Connecting to Arduino.")
    port = serial.Serial(args.port, 115200, timeout=5 )
    if args.verbose:
        print("Opened serial port named", port.name)

    print("Sleeping briefly while Arduino boots...")
    time.sleep(2.0)

    # throw away any extraneous input
    print("Flushing Arduino input...")
    port.flushInput()

    # Begin the motion sequence.  This may be safely interrupted by the user pressing Control-C.
    kwargs = { 'verbose': args.verbose }
    try:
        print("Beginning movement sequence.")
        for i in range(100):
              feedback_cycle(port, target = args.p1, **kwargs)

        for i in range(100):
              feedback_cycle(port, target = args.p2, **kwargs)

        for i in range(100):
              feedback_cycle(port, target = args.p1, **kwargs)

        for i in range(100):
              feedback_cycle(port, target = args.p2, **kwargs)

        print("Movement complete.")

    except KeyboardInterrupt:
        print("User interrupted motion.")

    # Close the serial port connection.
    port.write("stop\n")
    port.close()