Throbol Cookbook

If you're used to programming robots with imperative languages where you can update state variables in-place, it may take a while to get used to the purely functional style, where new states are calculated as functions of previous states and sensor inputs.

This cookbook gives recipies for solving many common problems with updating state.

Simple State machines

You can build state machines by calculating the new state as a function of the previous state, like:

light = turnon ? 1 : turnoff ? 0 : light[-dt]


foo = foo[-dt] + dt * bar // foo is the integral of bar

The value of every cell at time < 0 is 0 (or the equivalent of 0 for the type: empty string, empty set, etc.). So this integrator starts from 0.

foo = initial ? 123 : foo[-dt] + dt * bar // add an initial condition other than zero.
foo = integrate(bar) + 123 // equivalent to above.

PID loops

A simple PID feedback loop to make arm.pos follow target.pos and target.vel

arm.torque = 
  p.gain * (target.pos - arm.pos) + 
  d.gain * (target.vel - vel(arm.pos)) + 
  i.gain * integrate(target.pos - arm.pos)

The vel(x) function returns essentially (x - x[-dt])/dt

PID loops suffer from a phenomenon called windup, where if for some reason it isn't able to achieve the desired position, the integral of position error gets larger and larger. So it's best to clamp the value of the integrator within some limits. The integrate(x, reset, lo, hi) function is useful here:

arm.torque = 
  p.gain * (target.pos - arm.pos) + 
  d.gain * (vel(target.pos) - vel(arm.pos)) + 
  i.gain * integrate(target.pos - arm.pos, 0, -0.5~1, 0.5~1)


When the position sensor is noisy, it may help to add low-pass filtering on the output of a feedback loop. Thus a complete joint controller looks like:

arm.torque = clamp(lpfilter1(0.02~+0.1,
  10~50 * (target.pos - arm.pos[-dt]) + 
  5~20 * (vel(target.pos) - vel(arm.pos[-dt])) + 
  1~10 * integrate(target.pos - arm.pos[-dt], 0, -0.5~1, 0.5~1)),
  -1, 1)

Behavior trees

Throbol includes behavior trees to make it easier. A behavior cell can be in one of 3 states: running, failure, success. In running mode, it contains a set of symbols representing what is running. Consider a state machine for testing a walking robot.

harness.state = seq(
    !robot.fallen || run('recover.robot),
    robot.ready || run('prepare.robot),
    robot.onground || run('lower.robot),
    robot.stable || run('stabilize.robot),
    robot.fallen || run('stand.robot),

The seq operator runs through each argument until it finds one that is running or success.

Other cells can change their behavior based on what's running. For instance, we set the harness actuator to -0.5 meters in some run modes, 0 otherwise:

act.harness.desiredPos = isrunning(harness.state, 
    'lower.robot |
    'stabilize.robot |
    'stand.robot) ? 
  -0.5 : 0