Finite State Machine Controller#

Overview#

The library ships with three example implementations of the same finite state machine walking controller.

A diagram of the finite state machine

The first implementation is all in Python, and it uses the state_machine modules from the controls subpackage of this library. If you plan to write your controllers exclusively in Python, this example would be a good place to start.

The library also provides support for using compiled C and C++ library functions via the compiled_controller modules. You can see a very basic example usage of this module on the tutorials page, which may be helpful to walk through before starting with this example (Compiled Controllers). The source code for the compiled controllers (C++ and MATLAB implementations) is available in this repository. Please refer to the documentation in that repository for information on how to compile both the C++ and MATLAB source code.

Python Implementation#

Setup and Configuraiton#

First, we’ll perform some standard imports.

import numpy as np

import opensourceleg.tools.units as units
from opensourceleg.control.state_machine import Event, State, StateMachine
from opensourceleg.osl import OpenSourceLeg

offline_mode = False  # Set to true for debugging without hardware

Next, we’ll define all of the tunable FSM parameters. These include the impedance parameters for each state as well as the transitions between states.

# ------------- TUNABLE FSM PARAMETERS ---------------- #
BODY_WEIGHT = 60 * 9.8

# STATE 1: EARLY STANCE
KNEE_K_ESTANCE = 99.372
KNEE_B_ESTANCE = 3.180
KNEE_THETA_ESTANCE = 5
ANKLE_K_ESTANCE = 19.874
ANKLE_B_ESTANCE = 0
ANKLE_THETA_ESTANCE = -2
LOAD_LSTANCE: float = -1.0 * BODY_WEIGHT * 0.25
ANKLE_THETA_ESTANCE_TO_LSTANCE = 6.0

# STATE 2: LATE STANCE
KNEE_K_LSTANCE = 99.372
KNEE_B_LSTANCE = 1.272
KNEE_THETA_LSTANCE = 8
ANKLE_K_LSTANCE = 79.498
ANKLE_B_LSTANCE = 0.063
ANKLE_THETA_LSTANCE = -20
LOAD_ESWING: float = -1.0 * BODY_WEIGHT * 0.15

# STATE 3: EARLY SWING
KNEE_K_ESWING = 39.749
KNEE_B_ESWING = 0.063
KNEE_THETA_ESWING = 60
ANKLE_K_ESWING = 7.949
ANKLE_B_ESWING = 0.0
ANKLE_THETA_ESWING = 25
KNEE_THETA_ESWING_TO_LSWING = 50
KNEE_DTHETA_ESWING_TO_LSWING = 3

# STATE 4: LATE SWING
KNEE_K_LSWING = 15.899
KNEE_B_LSWING = 3.816
KNEE_THETA_LSWING = 5
ANKLE_K_LSWING = 7.949
ANKLE_B_LSWING = 0.0
ANKLE_THETA_LSWING = 15
LOAD_ESTANCE: float = -1.0 * BODY_WEIGHT * 0.4
KNEE_THETA_LSWING_TO_ESTANCE = 30
# ---------------------------------------------------- #

Note

These parameters were roughly tuned for a moderately paced walking gait. You may want to tune them to better suit your intended use case.

Next, we enter our main function for this script, run_FSM_controller(). We first instantiate an OSL object, add the joints, and add a loadcell.

def run_FSM_controller():
    """
    This is the main function for this script.
    It creates an OSL object and builds a state machine with 4 states.
    It runs a main loop that updates the state machine based on the
    hardware information and sends updated commands to the motors.
    """
    osl = OpenSourceLeg(frequency=200)
    osl.add_joint(name="knee", gear_ratio=41.4999, offline_mode=offline_mode)
    osl.add_joint(name="ankle", gear_ratio=41.4999, offline_mode=offline_mode)
    LOADCELL_MATRIX = np.array(
        [
            (-38.72600, -1817.74700, 9.84900, 43.37400, -44.54000, 1824.67000),
            (-8.61600, 1041.14900, 18.86100, -2098.82200, 31.79400, 1058.6230),
            (-1047.16800, 8.63900, -1047.28200, -20.70000, -1073.08800, -8.92300),
            (20.57600, -0.04000, -0.24600, 0.55400, -21.40800, -0.47600),
            (-12.13400, -1.10800, 24.36100, 0.02300, -12.14100, 0.79200),
            (-0.65100, -28.28700, 0.02200, -25.23000, 0.47300, -27.3070),
        ]
    )
    osl.add_loadcell(
        dephy_mode=False,
        offline_mode=offline_mode,
        loadcell_matrix=LOADCELL_MATRIX,
    )

Note

If instantiating the OSL object is unfamiliar, check out the Adding an Actuator and Adding a Loadcell tutorial pages.

Then, we create a StateMachine instance. We’ve written a helper function to do this just to keep the code tidy, and we’ll get to that function later on.

    fsm = build_4_state_FSM(osl)

Next, we configure the OSL log:

    osl.log.add_attributes(container=osl, attributes=["timestamp"])
    osl.log.add_attributes(
        container=osl.knee,
        attributes=[
            "output_position",
            "motor_current",
            "joint_torque",
            "motor_voltage",
            "accelx",
        ],
    )
    osl.log.add_attributes(
        container=osl.ankle,
        attributes=[
            "output_position",
            "motor_current",
            "joint_torque",
            "motor_voltage",
            "accelx",
        ],
    )
    osl.log.add_attributes(container=osl.loadcell, attributes=["fz"])
    osl.log.add_attributes(container=fsm.current_state, attributes=["name"])

Main Loop#

Now that everything is set up, we will home the OSL and enter the main loop. During each iteration of the main loop, we call the update method for both the OSL and the FSM. We then write the current impedance parameters for each joint to the hardware. A print statement is also included for debugging.

    with osl:
        osl.home()
        fsm.start()

        for t in osl.clock:
            osl.update()
            fsm.update()

            if osl.knee.mode != osl.knee.control_modes.impedance:
                osl.knee.set_mode(mode=osl.knee.control_modes.impedance)
                osl.knee.set_impedance_gains()
            osl.knee.set_joint_impedance(
                K=units.convert_to_default(
                    fsm.current_state.knee_stiffness,
                    units.stiffness.N_m_per_rad,
                ),
                B=units.convert_to_default(
                    fsm.current_state.knee_damping,
                    units.damping.N_m_per_rad_per_s,
                ),
            )
            osl.knee.set_output_position(
                position=units.convert_to_default(
                    fsm.current_state.knee_theta, units.position.deg
                ),
            )

            if osl.ankle.mode != osl.ankle.control_modes.impedance:
                osl.ankle.set_mode(osl.ankle.control_modes.impedance)
                osl.ankle.set_impedance_gains()
            osl.ankle.set_joint_impedance(
                K=units.convert_to_default(
                    fsm.current_state.ankle_stiffness,
                    units.stiffness.N_m_per_rad,
                ),
                B=units.convert_to_default(
                    fsm.current_state.ankle_damping,
                    units.damping.N_m_per_rad_per_s,
                ),
            )
            osl.ankle.set_output_position(
                position=units.convert_to_default(
                    fsm.current_state.ankle_theta, units.position.deg
                ),
            )
            print(
                "Current time in state {}: {:.2f} seconds, Knee Eq {:.2f}, Ankle Eq {:.2f}, Fz {:.2f}".format(
                    fsm.current_state.name,
                    fsm.current_state.current_time_in_state,
                    fsm.current_state.knee_theta,
                    fsm.current_state.ankle_theta,
                    osl.loadcell.fz,
                ),
                end="\r",
            )

Note

The OSL library provides sensor values in default units. If your library expects other units, you need to convert the values prior to assigning them. You can use the units module in the tools subpackage to do this. For example you can convert from radians (the default) to degrees using ankle_angle_in_deg = units.convert_from_default(osl.ankle.output_position, units.position.deg).

Building the State Machine#

The function build_4_state_FSM() uses the StateMachine functionality of the opensourceleg.control module to make a state machine with 4 states. First, we create the states using the State class.

def build_4_state_FSM(osl: OpenSourceLeg) -> StateMachine:
    """This method builds a state machine with 4 states.
    The states are early stance, late stance, early swing, and late swing.
    It uses the impedance parameters and transition criteria above.

    Inputs:
        OSL instance
    Returns:
        FSM object"""

    early_stance = State(name="e_stance")
    late_stance = State(name="l_stance")
    early_swing = State(name="e_swing")
    late_swing = State(name="l_swing")

Then, we assign impedance values for each state.

    early_stance.set_knee_impedance_paramters(
        theta=KNEE_THETA_ESTANCE, k=KNEE_K_ESTANCE, b=KNEE_B_ESTANCE
    )
    early_stance.make_knee_active()
    early_stance.set_ankle_impedance_paramters(
        theta=ANKLE_THETA_ESTANCE, k=ANKLE_K_ESTANCE, b=ANKLE_B_ESTANCE
    )
    early_stance.make_ankle_active()

    late_stance.set_knee_impedance_paramters(
        theta=KNEE_THETA_LSTANCE, k=KNEE_K_LSTANCE, b=KNEE_B_LSTANCE
    )
    late_stance.make_knee_active()
    late_stance.set_ankle_impedance_paramters(
        theta=ANKLE_THETA_LSTANCE, k=ANKLE_K_LSTANCE, b=ANKLE_B_LSTANCE
    )
    late_stance.make_ankle_active()

    early_swing.set_knee_impedance_paramters(
        theta=KNEE_THETA_ESWING, k=KNEE_K_ESWING, b=KNEE_B_ESWING
    )
    early_swing.make_knee_active()
    early_swing.set_ankle_impedance_paramters(
        theta=ANKLE_THETA_ESWING, k=ANKLE_K_ESWING, b=ANKLE_B_ESWING
    )
    early_swing.make_ankle_active()

    late_swing.set_knee_impedance_paramters(
        theta=KNEE_THETA_LSWING, k=KNEE_K_LSWING, b=KNEE_B_LSWING
    )
    late_swing.make_knee_active()
    late_swing.set_ankle_impedance_paramters(
        theta=ANKLE_THETA_LSWING, k=ANKLE_K_LSWING, b=ANKLE_B_LSWING
    )
    late_swing.make_ankle_active()

After the states have been defined, we define transition functions. These functions take the osl instance as arguments and return a boolean when transition criteria are met. For example, we first define the transition from early stance to late stance based on the loadcell z force and the ankle angle as:

    def estance_to_lstance(osl: OpenSourceLeg) -> bool:
        """
        Transition from early stance to late stance when the loadcell
        reads a force greater than a threshold.
        """
        assert osl.loadcell is not None
        return bool(
            osl.loadcell.fz < LOAD_LSTANCE
            and osl.ankle.output_position > ANKLE_THETA_ESTANCE_TO_LSTANCE
        )

The remaining transition functions are defined similarly.

    def estance_to_lstance(osl: OpenSourceLeg) -> bool:
        """
        Transition from early stance to late stance when the loadcell
        reads a force greater than a threshold.
        """
        assert osl.loadcell is not None
        return bool(
            osl.loadcell.fz < LOAD_LSTANCE
            and osl.ankle.output_position > ANKLE_THETA_ESTANCE_TO_LSTANCE
        )

    def lstance_to_eswing(osl: OpenSourceLeg) -> bool:
        """
        Transition from late stance to early swing when the loadcell
        reads a force less than a threshold.
        """
        assert osl.loadcell is not None
        return bool(osl.loadcell.fz > LOAD_ESWING)

    def eswing_to_lswing(osl: OpenSourceLeg) -> bool:
        """
        Transition from early swing to late swing when the knee angle
        is greater than a threshold and the knee velocity is less than
        a threshold.
        """
        assert osl.knee is not None
        return bool(
            osl.knee.output_position > KNEE_THETA_ESWING_TO_LSWING
            and osl.knee.output_velocity < KNEE_DTHETA_ESWING_TO_LSWING
        )

    def lswing_to_estance(osl: OpenSourceLeg) -> bool:
        """
        Transition from late swing to early stance when the loadcell
        reads a force greater than a threshold or the knee angle is
        less than a threshold.
        """
        assert osl.knee is not None and osl.loadcell is not None
        return bool(
            osl.loadcell.fz < LOAD_ESTANCE
            or osl.knee.output_position < KNEE_THETA_LSWING_TO_ESTANCE
        )

Next, we define events corresponding to the state transtions using the Event class.

    foot_flat = Event(name="foot_flat")
    heel_off = Event(name="heel_off")
    toe_off = Event(name="toe_off")
    pre_heel_strike = Event(name="pre_heel_strike")
    heel_strike = Event(name="heel_strike")

Finally, we make an instance of the StateMachine class and add the states, events, and transitions that we’ve created. The add_transition() method takes arguments of a source state, a destination state, an event, and the callback function defining when that transition occurs. After that, the FSM is fully built and can be returned.

    fsm = StateMachine(osl=osl, spoof=offline_mode)
    fsm.add_state(state=early_stance, initial_state=True)
    fsm.add_state(state=late_stance)
    fsm.add_state(state=early_swing)
    fsm.add_state(state=late_swing)

    fsm.add_event(event=foot_flat)
    fsm.add_event(event=heel_off)
    fsm.add_event(event=toe_off)
    fsm.add_event(event=pre_heel_strike)
    fsm.add_event(event=heel_strike)

    fsm.add_transition(
        source=early_stance,
        destination=late_stance,
        event=foot_flat,
        callback=estance_to_lstance,
    )
    fsm.add_transition(
        source=late_stance,
        destination=early_swing,
        event=heel_off,
        callback=lstance_to_eswing,
    )
    fsm.add_transition(
        source=early_swing,
        destination=late_swing,
        event=toe_off,
        callback=eswing_to_lswing,
    )
    fsm.add_transition(
        source=late_swing,
        destination=early_stance,
        event=heel_strike,
        callback=lswing_to_estance,
    )
    return fsm

Finally, we call our main function:

if __name__ == "__main__":
    run_FSM_controller()

Full Code for This Example#

  1"""
  2Example FSM walking controller for the OSL.
  3This implementation is all in python.
  4
  5Senthur Raj Ayyappan, Kevin Best
  6Neurobionics Lab
  7Robotics Department
  8University of Michigan
  9November 14, 2023
 10"""
 11import numpy as np
 12
 13import opensourceleg.tools.units as units
 14from opensourceleg.control.state_machine import Event, State, StateMachine
 15from opensourceleg.osl import OpenSourceLeg
 16
 17offline_mode = False  # Set to true for debugging without hardware
 18
 19# ------------- TUNABLE FSM PARAMETERS ---------------- #
 20BODY_WEIGHT = 60 * 9.8
 21
 22# STATE 1: EARLY STANCE
 23KNEE_K_ESTANCE = 99.372
 24KNEE_B_ESTANCE = 3.180
 25KNEE_THETA_ESTANCE = 5
 26ANKLE_K_ESTANCE = 19.874
 27ANKLE_B_ESTANCE = 0
 28ANKLE_THETA_ESTANCE = -2
 29LOAD_LSTANCE: float = -1.0 * BODY_WEIGHT * 0.25
 30ANKLE_THETA_ESTANCE_TO_LSTANCE = 6.0
 31
 32# STATE 2: LATE STANCE
 33KNEE_K_LSTANCE = 99.372
 34KNEE_B_LSTANCE = 1.272
 35KNEE_THETA_LSTANCE = 8
 36ANKLE_K_LSTANCE = 79.498
 37ANKLE_B_LSTANCE = 0.063
 38ANKLE_THETA_LSTANCE = -20
 39LOAD_ESWING: float = -1.0 * BODY_WEIGHT * 0.15
 40
 41# STATE 3: EARLY SWING
 42KNEE_K_ESWING = 39.749
 43KNEE_B_ESWING = 0.063
 44KNEE_THETA_ESWING = 60
 45ANKLE_K_ESWING = 7.949
 46ANKLE_B_ESWING = 0.0
 47ANKLE_THETA_ESWING = 25
 48KNEE_THETA_ESWING_TO_LSWING = 50
 49KNEE_DTHETA_ESWING_TO_LSWING = 3
 50
 51# STATE 4: LATE SWING
 52KNEE_K_LSWING = 15.899
 53KNEE_B_LSWING = 3.816
 54KNEE_THETA_LSWING = 5
 55ANKLE_K_LSWING = 7.949
 56ANKLE_B_LSWING = 0.0
 57ANKLE_THETA_LSWING = 15
 58LOAD_ESTANCE: float = -1.0 * BODY_WEIGHT * 0.4
 59KNEE_THETA_LSWING_TO_ESTANCE = 30
 60# ---------------------------------------------------- #
 61
 62
 63def run_FSM_controller():
 64    """
 65    This is the main function for this script.
 66    It creates an OSL object and builds a state machine with 4 states.
 67    It runs a main loop that updates the state machine based on the
 68    hardware information and sends updated commands to the motors.
 69    """
 70    osl = OpenSourceLeg(frequency=200)
 71    osl.add_joint(name="knee", gear_ratio=41.4999, offline_mode=offline_mode)
 72    osl.add_joint(name="ankle", gear_ratio=41.4999, offline_mode=offline_mode)
 73    LOADCELL_MATRIX = np.array(
 74        [
 75            (-38.72600, -1817.74700, 9.84900, 43.37400, -44.54000, 1824.67000),
 76            (-8.61600, 1041.14900, 18.86100, -2098.82200, 31.79400, 1058.6230),
 77            (-1047.16800, 8.63900, -1047.28200, -20.70000, -1073.08800, -8.92300),
 78            (20.57600, -0.04000, -0.24600, 0.55400, -21.40800, -0.47600),
 79            (-12.13400, -1.10800, 24.36100, 0.02300, -12.14100, 0.79200),
 80            (-0.65100, -28.28700, 0.02200, -25.23000, 0.47300, -27.3070),
 81        ]
 82    )
 83    osl.add_loadcell(
 84        dephy_mode=False,
 85        offline_mode=offline_mode,
 86        loadcell_matrix=LOADCELL_MATRIX,
 87    )
 88
 89    fsm = build_4_state_FSM(osl)
 90
 91    osl.log.add_attributes(container=osl, attributes=["timestamp"])
 92    osl.log.add_attributes(
 93        container=osl.knee,
 94        attributes=[
 95            "output_position",
 96            "motor_current",
 97            "joint_torque",
 98            "motor_voltage",
 99            "accelx",
100        ],
101    )
102    osl.log.add_attributes(
103        container=osl.ankle,
104        attributes=[
105            "output_position",
106            "motor_current",
107            "joint_torque",
108            "motor_voltage",
109            "accelx",
110        ],
111    )
112    osl.log.add_attributes(container=osl.loadcell, attributes=["fz"])
113    osl.log.add_attributes(container=fsm.current_state, attributes=["name"])
114
115    with osl:
116        osl.home()
117        fsm.start()
118
119        for t in osl.clock:
120            osl.update()
121            fsm.update()
122
123            if osl.knee.mode != osl.knee.control_modes.impedance:
124                osl.knee.set_mode(mode=osl.knee.control_modes.impedance)
125                osl.knee.set_impedance_gains()
126            osl.knee.set_joint_impedance(
127                K=units.convert_to_default(
128                    fsm.current_state.knee_stiffness,
129                    units.stiffness.N_m_per_rad,
130                ),
131                B=units.convert_to_default(
132                    fsm.current_state.knee_damping,
133                    units.damping.N_m_per_rad_per_s,
134                ),
135            )
136            osl.knee.set_output_position(
137                position=units.convert_to_default(
138                    fsm.current_state.knee_theta, units.position.deg
139                ),
140            )
141
142            if osl.ankle.mode != osl.ankle.control_modes.impedance:
143                osl.ankle.set_mode(osl.ankle.control_modes.impedance)
144                osl.ankle.set_impedance_gains()
145            osl.ankle.set_joint_impedance(
146                K=units.convert_to_default(
147                    fsm.current_state.ankle_stiffness,
148                    units.stiffness.N_m_per_rad,
149                ),
150                B=units.convert_to_default(
151                    fsm.current_state.ankle_damping,
152                    units.damping.N_m_per_rad_per_s,
153                ),
154            )
155            osl.ankle.set_output_position(
156                position=units.convert_to_default(
157                    fsm.current_state.ankle_theta, units.position.deg
158                ),
159            )
160            print(
161                "Current time in state {}: {:.2f} seconds, Knee Eq {:.2f}, Ankle Eq {:.2f}, Fz {:.2f}".format(
162                    fsm.current_state.name,
163                    fsm.current_state.current_time_in_state,
164                    fsm.current_state.knee_theta,
165                    fsm.current_state.ankle_theta,
166                    osl.loadcell.fz,
167                ),
168                end="\r",
169            )
170
171        fsm.stop()
172        print("")
173
174
175def build_4_state_FSM(osl: OpenSourceLeg) -> StateMachine:
176    """This method builds a state machine with 4 states.
177    The states are early stance, late stance, early swing, and late swing.
178    It uses the impedance parameters and transition criteria above.
179
180    Inputs:
181        OSL instance
182    Returns:
183        FSM object"""
184
185    early_stance = State(name="e_stance")
186    late_stance = State(name="l_stance")
187    early_swing = State(name="e_swing")
188    late_swing = State(name="l_swing")
189
190    early_stance.set_knee_impedance_paramters(
191        theta=KNEE_THETA_ESTANCE, k=KNEE_K_ESTANCE, b=KNEE_B_ESTANCE
192    )
193    early_stance.make_knee_active()
194    early_stance.set_ankle_impedance_paramters(
195        theta=ANKLE_THETA_ESTANCE, k=ANKLE_K_ESTANCE, b=ANKLE_B_ESTANCE
196    )
197    early_stance.make_ankle_active()
198
199    late_stance.set_knee_impedance_paramters(
200        theta=KNEE_THETA_LSTANCE, k=KNEE_K_LSTANCE, b=KNEE_B_LSTANCE
201    )
202    late_stance.make_knee_active()
203    late_stance.set_ankle_impedance_paramters(
204        theta=ANKLE_THETA_LSTANCE, k=ANKLE_K_LSTANCE, b=ANKLE_B_LSTANCE
205    )
206    late_stance.make_ankle_active()
207
208    early_swing.set_knee_impedance_paramters(
209        theta=KNEE_THETA_ESWING, k=KNEE_K_ESWING, b=KNEE_B_ESWING
210    )
211    early_swing.make_knee_active()
212    early_swing.set_ankle_impedance_paramters(
213        theta=ANKLE_THETA_ESWING, k=ANKLE_K_ESWING, b=ANKLE_B_ESWING
214    )
215    early_swing.make_ankle_active()
216
217    late_swing.set_knee_impedance_paramters(
218        theta=KNEE_THETA_LSWING, k=KNEE_K_LSWING, b=KNEE_B_LSWING
219    )
220    late_swing.make_knee_active()
221    late_swing.set_ankle_impedance_paramters(
222        theta=ANKLE_THETA_LSWING, k=ANKLE_K_LSWING, b=ANKLE_B_LSWING
223    )
224    late_swing.make_ankle_active()
225
226    def estance_to_lstance(osl: OpenSourceLeg) -> bool:
227        """
228        Transition from early stance to late stance when the loadcell
229        reads a force greater than a threshold.
230        """
231        assert osl.loadcell is not None
232        return bool(
233            osl.loadcell.fz < LOAD_LSTANCE
234            and osl.ankle.output_position > ANKLE_THETA_ESTANCE_TO_LSTANCE
235        )
236
237    def lstance_to_eswing(osl: OpenSourceLeg) -> bool:
238        """
239        Transition from late stance to early swing when the loadcell
240        reads a force less than a threshold.
241        """
242        assert osl.loadcell is not None
243        return bool(osl.loadcell.fz > LOAD_ESWING)
244
245    def eswing_to_lswing(osl: OpenSourceLeg) -> bool:
246        """
247        Transition from early swing to late swing when the knee angle
248        is greater than a threshold and the knee velocity is less than
249        a threshold.
250        """
251        assert osl.knee is not None
252        return bool(
253            osl.knee.output_position > KNEE_THETA_ESWING_TO_LSWING
254            and osl.knee.output_velocity < KNEE_DTHETA_ESWING_TO_LSWING
255        )
256
257    def lswing_to_estance(osl: OpenSourceLeg) -> bool:
258        """
259        Transition from late swing to early stance when the loadcell
260        reads a force greater than a threshold or the knee angle is
261        less than a threshold.
262        """
263        assert osl.knee is not None and osl.loadcell is not None
264        return bool(
265            osl.loadcell.fz < LOAD_ESTANCE
266            or osl.knee.output_position < KNEE_THETA_LSWING_TO_ESTANCE
267        )
268
269    foot_flat = Event(name="foot_flat")
270    heel_off = Event(name="heel_off")
271    toe_off = Event(name="toe_off")
272    pre_heel_strike = Event(name="pre_heel_strike")
273    heel_strike = Event(name="heel_strike")
274
275    fsm = StateMachine(osl=osl, spoof=offline_mode)
276    fsm.add_state(state=early_stance, initial_state=True)
277    fsm.add_state(state=late_stance)
278    fsm.add_state(state=early_swing)
279    fsm.add_state(state=late_swing)
280
281    fsm.add_event(event=foot_flat)
282    fsm.add_event(event=heel_off)
283    fsm.add_event(event=toe_off)
284    fsm.add_event(event=pre_heel_strike)
285    fsm.add_event(event=heel_strike)
286
287    fsm.add_transition(
288        source=early_stance,
289        destination=late_stance,
290        event=foot_flat,
291        callback=estance_to_lstance,
292    )
293    fsm.add_transition(
294        source=late_stance,
295        destination=early_swing,
296        event=heel_off,
297        callback=lstance_to_eswing,
298    )
299    fsm.add_transition(
300        source=early_swing,
301        destination=late_swing,
302        event=toe_off,
303        callback=eswing_to_lswing,
304    )
305    fsm.add_transition(
306        source=late_swing,
307        destination=early_stance,
308        event=heel_strike,
309        callback=lswing_to_estance,
310    )
311    return fsm
312
313
314if __name__ == "__main__":
315    run_FSM_controller()

C++ and MATLAB Implementation#

To get started, make sure you have compiled either the C++ or the MATLAB source code and have a FSMController.so library. If not, please see the source repository for compilation instructions. To run this example as is, copy the library you generated to the examples directory. Alternatively, you can modify the search path for the library when loading the controller (see below).

Load Compiled Library#

First, we’ll perform standard imports, handle some paths, and setup our OSL object.

 1import inspect
 2import os
 3
 4import numpy as np
 5
 6from opensourceleg.control.compiled_controller import CompiledController
 7from opensourceleg.osl import OpenSourceLeg
 8from opensourceleg.tools import units
 9
10osl = OpenSourceLeg(frequency=200)
11use_offline_mode = False
12osl.add_joint("knee", gear_ratio=9 * 83 / 18, offline_mode=use_offline_mode)
13osl.add_joint("ankle", gear_ratio=9 * 83 / 18, offline_mode=use_offline_mode)
14LOADCELL_MATRIX = np.array(
15    [
16        (-38.72600, -1817.74700, 9.84900, 43.37400, -44.54000, 1824.67000),
17        (-8.61600, 1041.14900, 18.86100, -2098.82200, 31.79400, 1058.6230),
18        (-1047.16800, 8.63900, -1047.28200, -20.70000, -1073.08800, -8.92300),
19        (20.57600, -0.04000, -0.24600, 0.55400, -21.40800, -0.47600),
20        (-12.13400, -1.10800, 24.36100, 0.02300, -12.14100, 0.79200),
21        (-0.65100, -28.28700, 0.02200, -25.23000, 0.47300, -27.3070),
22    ]
23)
24osl.add_loadcell(
25    joint=osl.knee, offline_mode=use_offline_mode, loadcell_matrix=LOADCELL_MATRIX
26)

Note

If instantiating the OSL object is unfamiliar, check out the Adding an Actuator and Adding a Loadcell tutorial pages.

Next, we’ll instantiate a CompiledController wrapper object. This takes arguments of the name of the library (without extension), the path at which it is located (which in this case is the current directory), and the names of the main, initializaiton, and cleanup functions.

1currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))  # type: ignore
2controller = CompiledController(
3    library_name="FSMController",
4    library_path=currentdir,
5    main_function_name="FSMController",
6    initialization_function_name="FSMController_initialize",
7    cleanup_function_name="FSMController_terminate",
8)

Define Custom Datatypes#

Next, we define the data structures used in the controller. These must exactly match (size and order) what was used to create the library. First, we define a type for a single set of impedance parameters called impedance_param_type, where stiffness, damping, and eq_angle are each doubles. The CompiledController object provides building block types within its types attribute.

1controller.define_type(
2    "impedance_param_type",
3    [
4        ("stiffness", controller.types.c_double),
5        ("damping", controller.types.c_double),
6        ("eq_angle", controller.types.c_double),
7    ],
8)

We then define the type joint_impedance_set which contains a set of impedance parameters for each of the four states. Because we have already defined the impedance_param_type in the previous lines, we can now use it to define additional types.

1controller.define_type(
2    "joint_impedance_set",
3    [
4        ("early_stance", controller.types.impedance_param_type),
5        ("late_stance", controller.types.impedance_param_type),
6        ("early_swing", controller.types.impedance_param_type),
7        ("late_swing", controller.types.impedance_param_type),
8    ],
9)

We then similarly define the transition_parameters type as well as the overall UserParameters type.

 1controller.define_type(
 2    "transition_parameters",
 3    [
 4        ("min_time_in_state", controller.types.c_double),
 5        ("loadLStance", controller.types.c_double),
 6        ("ankleThetaEstanceToLstance", controller.types.c_double),
 7        ("kneeThetaESwingToLSwing", controller.types.c_double),
 8        ("kneeDthetaESwingToLSwing", controller.types.c_double),
 9        ("loadESwing", controller.types.c_double),
10        ("loadEStance", controller.types.c_double),
11        ("kneeThetaLSwingToEStance", controller.types.c_double),
12    ],
13)
14controller.define_type(
15    "UserParameters",
16    [
17        ("body_weight", controller.types.c_double),
18        ("knee_impedance", controller.types.joint_impedance_set),
19        ("ankle_impedance", controller.types.joint_impedance_set),
20        ("transition_parameters", controller.types.transition_parameters),
21    ],
22)

We also define a sensors type. We’re using the default sensors, which the CompiledController class provides in list form already setup correctly.

1controller.define_type("sensors", controller.DEFAULT_SENSOR_LIST)

The final type definitions are for the input and output types.

 1controller.define_inputs(
 2    [
 3        ("parameters", controller.types.UserParameters),
 4        ("sensors", controller.types.sensors),
 5        ("time", controller.types.c_double),
 6    ]
 7)
 8controller.define_outputs(
 9    [
10        ("current_state", controller.types.c_int),
11        ("time_in_current_state", controller.types.c_double),
12        ("knee_impedance", controller.types.impedance_param_type),
13        ("ankle_impedance", controller.types.impedance_param_type),
14    ]
15)

Configure Impedance and Transition Parameters#

The next section of code configures the impedance and transition paramters based on a pre-defined tuning. Feel free to play with these values to get the behavior you want.

 1controller.inputs.parameters.knee_impedance.early_stance.stiffness = 99.372  # type: ignore
 2controller.inputs.parameters.knee_impedance.early_stance.damping = 3.180  # type: ignore
 3controller.inputs.parameters.knee_impedance.early_stance.eq_angle = 5  # type: ignore
 4controller.inputs.parameters.knee_impedance.late_stance.stiffness = 99.372  # type: ignore
 5controller.inputs.parameters.knee_impedance.late_stance.damping = 1.272  # type: ignore
 6controller.inputs.parameters.knee_impedance.late_stance.eq_angle = 8  # type: ignore
 7controller.inputs.parameters.knee_impedance.early_swing.stiffness = 39.746  # type: ignore
 8controller.inputs.parameters.knee_impedance.early_swing.damping = 0.063  # type: ignore
 9controller.inputs.parameters.knee_impedance.early_swing.eq_angle = 60  # type: ignore
10controller.inputs.parameters.knee_impedance.late_swing.stiffness = 15.899  # type: ignore
11controller.inputs.parameters.knee_impedance.late_swing.damping = 3.186  # type: ignore
12controller.inputs.parameters.knee_impedance.late_swing.eq_angle = 5  # type: ignore
13controller.inputs.parameters.ankle_impedance.early_stance.stiffness = 19.874  # type: ignore
14controller.inputs.parameters.ankle_impedance.early_stance.damping = 0  # type: ignore
15controller.inputs.parameters.ankle_impedance.early_stance.eq_angle = -2  # type: ignore
16controller.inputs.parameters.ankle_impedance.late_stance.stiffness = 79.498  # type: ignore
17controller.inputs.parameters.ankle_impedance.late_stance.damping = 0.063  # type: ignore
18controller.inputs.parameters.ankle_impedance.late_stance.eq_angle = -20  # type: ignore
19controller.inputs.parameters.ankle_impedance.early_swing.stiffness = 7.949  # type: ignore
20controller.inputs.parameters.ankle_impedance.early_swing.damping = 0  # type: ignore
21controller.inputs.parameters.ankle_impedance.early_swing.eq_angle = 25  # type: ignore
22controller.inputs.parameters.ankle_impedance.late_swing.stiffness = 7.949  # type: ignore
23controller.inputs.parameters.ankle_impedance.late_swing.damping = 0.0  # type: ignore
24controller.inputs.parameters.ankle_impedance.late_swing.eq_angle = 15  # type: ignore
25
26# Configure state machine
27body_weight = 82  # kg
28controller.inputs.parameters.body_weight = body_weight  # type: ignore
29controller.inputs.parameters.transition_parameters.min_time_in_state = 0.20  # type: ignore
30controller.inputs.parameters.transition_parameters.loadLStance = -body_weight * 0.25  # type: ignore
31controller.inputs.parameters.transition_parameters.ankleThetaEstanceToLstance = 6.0  # type: ignore
32controller.inputs.parameters.transition_parameters.loadESwing = -body_weight * 0.15  # type: ignore
33controller.inputs.parameters.transition_parameters.kneeThetaESwingToLSwing = 50  # type: ignore
34controller.inputs.parameters.transition_parameters.kneeDthetaESwingToLSwing = 3  # type: ignore
35controller.inputs.parameters.transition_parameters.loadEStance = -body_weight * 0.4  # type: ignore
36controller.inputs.parameters.transition_parameters.kneeThetaLSwingToEStance = 30  # type: ignore

Main Loop#

Now that the controller is configured, we can home the osl, calibrate the loadcell, set both joints to impedance mode, and begin running. Note that within the main loop after calling osl.update(), we assign the relevant sensor values from the osl object to the controller inputs. We also write to any other inputs that change every loop, such as the current time t. Once all inputs are assigned, we call the run() method, which calls our compiled library with the configured inputs. The run method returns the outputs object, which is populated by the compiled library function.

 1with osl:
 2    osl.home()
 3    osl.update()
 4    osl.knee.set_mode(osl.knee.control_modes.impedance)
 5    osl.ankle.set_mode(osl.ankle.control_modes.impedance)
 6
 7    # Main Loop
 8    for t in osl.clock:
 9        osl.update()
10
11        controller.inputs.sensors.knee_angle = (  # type: ignore
12            units.convert_from_default(osl.knee.output_position, units.position.deg)
13        )
14        controller.inputs.sensors.ankle_angle = units.convert_from_default(osl.ankle.output_position, units.position.deg)  # type: ignore
15        controller.inputs.sensors.knee_velocity = units.convert_from_default(osl.knee.output_velocity, units.velocity.deg_per_s)  # type: ignore
16        controller.inputs.sensors.ankle_velocity = units.convert_from_default(osl.ankle.output_velocity, units.velocity.deg_per_s)  # type: ignore
17        controller.inputs.sensors.Fz = osl.loadcell.fz  # type: ignore
18
19        # Update any control inputs that change every loop
20        controller.inputs.time = t  # type: ignore
21
22        # Call the controller
23        outputs = controller.run()
24
25        # Test print to ensure external library call works
26        print(
27            "Current time in state {}: {:.2f} seconds, Knee Eq {:.2f}, Ankle Eq {:.2f}, Fz {:.2f}".format(
28                outputs.current_state,
29                outputs.time_in_current_state,
30                outputs.knee_impedance.eq_angle,
31                outputs.ankle_impedance.eq_angle,
32                osl.loadcell.fz,
33            ),
34            end="\r",
35        )

Note

The OSL library provides sensor values in default units. If your library expects other units, you need to convert the values prior to assigning them. You can use the units module in the tools subpackage to do this. For example you can convert from radians (the default) to degrees using ankle_angle_in_deg = units.convert_from_default(osl.ankle.output_position, units.position.deg).

Finally, we write from the outputs structure to the hardware. Be careful with units at this step as well, making sure your values are either in default units, or that you call appropriate conversion functions. As our library was in degrees, we convert to radians.

 1        osl.knee.set_joint_impedance(
 2            K=units.convert_to_default(
 3                outputs.knee_impedance.stiffness, units.stiffness.N_m_per_rad
 4            ),
 5            B=units.convert_to_default(
 6                outputs.knee_impedance.damping, units.damping.N_m_per_rad_per_s
 7            ),
 8        )
 9        osl.knee.set_output_position(
10            position=units.convert_to_default(
11                outputs.knee_impedance.eq_angle, units.position.deg
12            )
13        )
14        osl.ankle.set_joint_impedance(
15            K=units.convert_to_default(
16                outputs.ankle_impedance.stiffness, units.stiffness.N_m_per_rad
17            ),
18            B=units.convert_to_default(
19                outputs.ankle_impedance.damping, units.damping.N_m_per_rad_per_s
20            ),
21        )
22        osl.ankle.set_output_position(
23            position=units.convert_to_default(
24                outputs.ankle_impedance.eq_angle, units.position.deg
25            )
26        )

Full Code for This Example#

  1"""
  2Example walking controller for the OSL.
  3
  4Senthur Raj Ayyappan, Kevin Best
  5Neurobionics Lab
  6Robotics Department
  7University of Michigan
  8October 9, 2023
  9"""
 10
 11import inspect
 12import os
 13
 14import numpy as np
 15
 16from opensourceleg.control.compiled_controller import CompiledController
 17from opensourceleg.osl import OpenSourceLeg
 18from opensourceleg.tools import units
 19
 20osl = OpenSourceLeg(frequency=200)
 21use_offline_mode = False
 22osl.add_joint("knee", gear_ratio=9 * 83 / 18, offline_mode=use_offline_mode)
 23osl.add_joint("ankle", gear_ratio=9 * 83 / 18, offline_mode=use_offline_mode)
 24LOADCELL_MATRIX = np.array(
 25    [
 26        (-38.72600, -1817.74700, 9.84900, 43.37400, -44.54000, 1824.67000),
 27        (-8.61600, 1041.14900, 18.86100, -2098.82200, 31.79400, 1058.6230),
 28        (-1047.16800, 8.63900, -1047.28200, -20.70000, -1073.08800, -8.92300),
 29        (20.57600, -0.04000, -0.24600, 0.55400, -21.40800, -0.47600),
 30        (-12.13400, -1.10800, 24.36100, 0.02300, -12.14100, 0.79200),
 31        (-0.65100, -28.28700, 0.02200, -25.23000, 0.47300, -27.3070),
 32    ]
 33)
 34osl.add_loadcell(
 35    joint=osl.knee, offline_mode=use_offline_mode, loadcell_matrix=LOADCELL_MATRIX
 36)
 37
 38currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))  # type: ignore
 39controller = CompiledController(
 40    library_name="FSMController",
 41    library_path=currentdir,
 42    main_function_name="FSMController",
 43    initialization_function_name="FSMController_initialize",
 44    cleanup_function_name="FSMController_terminate",
 45)
 46
 47controller.define_type(
 48    "impedance_param_type",
 49    [
 50        ("stiffness", controller.types.c_double),
 51        ("damping", controller.types.c_double),
 52        ("eq_angle", controller.types.c_double),
 53    ],
 54)
 55controller.define_type(
 56    "joint_impedance_set",
 57    [
 58        ("early_stance", controller.types.impedance_param_type),
 59        ("late_stance", controller.types.impedance_param_type),
 60        ("early_swing", controller.types.impedance_param_type),
 61        ("late_swing", controller.types.impedance_param_type),
 62    ],
 63)
 64controller.define_type(
 65    "transition_parameters",
 66    [
 67        ("min_time_in_state", controller.types.c_double),
 68        ("loadLStance", controller.types.c_double),
 69        ("ankleThetaEstanceToLstance", controller.types.c_double),
 70        ("kneeThetaESwingToLSwing", controller.types.c_double),
 71        ("kneeDthetaESwingToLSwing", controller.types.c_double),
 72        ("loadESwing", controller.types.c_double),
 73        ("loadEStance", controller.types.c_double),
 74        ("kneeThetaLSwingToEStance", controller.types.c_double),
 75    ],
 76)
 77controller.define_type(
 78    "UserParameters",
 79    [
 80        ("body_weight", controller.types.c_double),
 81        ("knee_impedance", controller.types.joint_impedance_set),
 82        ("ankle_impedance", controller.types.joint_impedance_set),
 83        ("transition_parameters", controller.types.transition_parameters),
 84    ],
 85)
 86controller.define_type("sensors", controller.DEFAULT_SENSOR_LIST)
 87
 88controller.define_inputs(
 89    [
 90        ("parameters", controller.types.UserParameters),
 91        ("sensors", controller.types.sensors),
 92        ("time", controller.types.c_double),
 93    ]
 94)
 95controller.define_outputs(
 96    [
 97        ("current_state", controller.types.c_int),
 98        ("time_in_current_state", controller.types.c_double),
 99        ("knee_impedance", controller.types.impedance_param_type),
100        ("ankle_impedance", controller.types.impedance_param_type),
101    ]
102)
103
104# Populate Controller inputs as needed
105controller.inputs.parameters.knee_impedance.early_stance.stiffness = 99.372  # type: ignore
106controller.inputs.parameters.knee_impedance.early_stance.damping = 3.180  # type: ignore
107controller.inputs.parameters.knee_impedance.early_stance.eq_angle = 5  # type: ignore
108controller.inputs.parameters.knee_impedance.late_stance.stiffness = 99.372  # type: ignore
109controller.inputs.parameters.knee_impedance.late_stance.damping = 1.272  # type: ignore
110controller.inputs.parameters.knee_impedance.late_stance.eq_angle = 8  # type: ignore
111controller.inputs.parameters.knee_impedance.early_swing.stiffness = 39.746  # type: ignore
112controller.inputs.parameters.knee_impedance.early_swing.damping = 0.063  # type: ignore
113controller.inputs.parameters.knee_impedance.early_swing.eq_angle = 60  # type: ignore
114controller.inputs.parameters.knee_impedance.late_swing.stiffness = 15.899  # type: ignore
115controller.inputs.parameters.knee_impedance.late_swing.damping = 3.186  # type: ignore
116controller.inputs.parameters.knee_impedance.late_swing.eq_angle = 5  # type: ignore
117controller.inputs.parameters.ankle_impedance.early_stance.stiffness = 19.874  # type: ignore
118controller.inputs.parameters.ankle_impedance.early_stance.damping = 0  # type: ignore
119controller.inputs.parameters.ankle_impedance.early_stance.eq_angle = -2  # type: ignore
120controller.inputs.parameters.ankle_impedance.late_stance.stiffness = 79.498  # type: ignore
121controller.inputs.parameters.ankle_impedance.late_stance.damping = 0.063  # type: ignore
122controller.inputs.parameters.ankle_impedance.late_stance.eq_angle = -20  # type: ignore
123controller.inputs.parameters.ankle_impedance.early_swing.stiffness = 7.949  # type: ignore
124controller.inputs.parameters.ankle_impedance.early_swing.damping = 0  # type: ignore
125controller.inputs.parameters.ankle_impedance.early_swing.eq_angle = 25  # type: ignore
126controller.inputs.parameters.ankle_impedance.late_swing.stiffness = 7.949  # type: ignore
127controller.inputs.parameters.ankle_impedance.late_swing.damping = 0.0  # type: ignore
128controller.inputs.parameters.ankle_impedance.late_swing.eq_angle = 15  # type: ignore
129
130# Configure state machine
131body_weight = 82  # kg
132controller.inputs.parameters.body_weight = body_weight  # type: ignore
133controller.inputs.parameters.transition_parameters.min_time_in_state = 0.20  # type: ignore
134controller.inputs.parameters.transition_parameters.loadLStance = -body_weight * 0.25  # type: ignore
135controller.inputs.parameters.transition_parameters.ankleThetaEstanceToLstance = 6.0  # type: ignore
136controller.inputs.parameters.transition_parameters.loadESwing = -body_weight * 0.15  # type: ignore
137controller.inputs.parameters.transition_parameters.kneeThetaESwingToLSwing = 50  # type: ignore
138controller.inputs.parameters.transition_parameters.kneeDthetaESwingToLSwing = 3  # type: ignore
139controller.inputs.parameters.transition_parameters.loadEStance = -body_weight * 0.4  # type: ignore
140controller.inputs.parameters.transition_parameters.kneeThetaLSwingToEStance = 30  # type: ignore
141
142with osl:
143    osl.home()
144    osl.update()
145    osl.knee.set_mode(osl.knee.control_modes.impedance)
146    osl.ankle.set_mode(osl.ankle.control_modes.impedance)
147
148    # Main Loop
149    for t in osl.clock:
150        osl.update()
151
152        controller.inputs.sensors.knee_angle = (  # type: ignore
153            units.convert_from_default(osl.knee.output_position, units.position.deg)
154        )
155        controller.inputs.sensors.ankle_angle = units.convert_from_default(osl.ankle.output_position, units.position.deg)  # type: ignore
156        controller.inputs.sensors.knee_velocity = units.convert_from_default(osl.knee.output_velocity, units.velocity.deg_per_s)  # type: ignore
157        controller.inputs.sensors.ankle_velocity = units.convert_from_default(osl.ankle.output_velocity, units.velocity.deg_per_s)  # type: ignore
158        controller.inputs.sensors.Fz = osl.loadcell.fz  # type: ignore
159
160        # Update any control inputs that change every loop
161        controller.inputs.time = t  # type: ignore
162
163        # Call the controller
164        outputs = controller.run()
165
166        # Test print to ensure external library call works
167        print(
168            "Current time in state {}: {:.2f} seconds, Knee Eq {:.2f}, Ankle Eq {:.2f}, Fz {:.2f}".format(
169                outputs.current_state,
170                outputs.time_in_current_state,
171                outputs.knee_impedance.eq_angle,
172                outputs.ankle_impedance.eq_angle,
173                osl.loadcell.fz,
174            ),
175            end="\r",
176        )
177
178        # Write to the hardware
179        osl.knee.set_joint_impedance(
180            K=units.convert_to_default(
181                outputs.knee_impedance.stiffness, units.stiffness.N_m_per_rad
182            ),
183            B=units.convert_to_default(
184                outputs.knee_impedance.damping, units.damping.N_m_per_rad_per_s
185            ),
186        )
187        osl.knee.set_output_position(
188            position=units.convert_to_default(
189                outputs.knee_impedance.eq_angle, units.position.deg
190            )
191        )
192        osl.ankle.set_joint_impedance(
193            K=units.convert_to_default(
194                outputs.ankle_impedance.stiffness, units.stiffness.N_m_per_rad
195            ),
196            B=units.convert_to_default(
197                outputs.ankle_impedance.damping, units.damping.N_m_per_rad_per_s
198            ),
199        )
200        osl.ankle.set_output_position(
201            position=units.convert_to_default(
202                outputs.ankle_impedance.eq_angle, units.position.deg
203            )
204        )
205
206    print("\n")