Tutorial 1: Take Control with JoyApp

Live control, keyboard interaction, sequential behavior, and non-instantaneous behavior can be a real pain to code. ckbot.JoyApp was designed to ease these pains.

JoyApp uses “cooperative multithreading.” This means that you can run many things in parallel (just like in regular multithreading) but instead of having to worry about how to prevent threads from corrupting each others’ data, you can be confident that your co-threads will play nicely with each other.

The cost is that each co-thread must yield often enough to allow all other co-threads to work–thus you must never ever use Python’s built in time.sleep() method! Nothing else can run while your code sleeps, and everything grinds to a halt.

Remember: CONTROL IS ASTOUNDINGLY EASY IN JOYAPP–IF YOU LET IT BE EASY!

Please don’t make it hard on yourself.

Meet demo-buggy.py

demo-buggy.py is just a step beyond a “Hello, World!” You’ve built a little robot with two wheels; demo-buggy.py let’s you control it with a pair of joysticks (e.g., a PlayStation controller).

When you understand demo-buggy.py, you will understand how JoyApp can make your life much easier.

demo-buggy.py Walkthrough

Let’s walk through the code line-by-line. The first two lines of demo-buggy.py are practically boilerplate, and will be nearly identical in most projects:

from joy.decl import *
from joy import Plan, JoyApp, StickFilter

Line one imports everything from the JoyApp library (mostly constants, plus the logging function progress()). Line two imports some specific classes: Plan, JoyApp, and StickFilter.

Plan is a superclass of all “plans” (the JoyApp equivalent of threads). We need to import it in order to sub-class (i.e., make our own) plans. The same goes for JoyApp. These are basically boxes we need to organize our code: the Plan is a box (or set of boxes) filled with the stuff that does our bidding (“behaviors”). The JoyApp is a box to hold our Plan box(es).

JoyApps execute Plans; Plans have two main pieces: An event filter (the onEvent() method) and a behaviour (the behavior() method). Plans receive events from your hardware. If the event filter deems an event worthy of note (i.e., if onEvent() returns a Python true), then the behavior gets to run, and will do so at the next available opportunity (i.e., the next open timeslice).

StickFilter, on the other hand, we import because we need its functionality, to connect to the joysticks. StickFilter is where some magic happens.

The problem: Joysticks run on their own time and are analog (as are the human hands diddling them). Neither of these things is fun to code around. StickFilter makes this ugly situation sweet and easy: It sits in the background checking for joystick events, then processes them into tidy numbers you can read at any time.

There are two very useful ways to process a joystick’s analog signal: With a low-pass filter (which smooths the signal and adds a bit of lag–useful for direct position control) or with an integrator (which will give the joystick a throttle-like behavior: Push the joystick up a little to go slow, or all the way to go turbo). There’s a bit more to StickFilter, if you’re curious: It’s a utility class–a subclass of Plan–with its own demo: demo-stickFilter.py.

Our JoyApp is going to need something to execute, so we start by making our Plan:

class Buggy( Plan ):
  def __init__(self,*arg,**kw):
    Plan.__init__(self,*arg,**kw)

The first line creates the “Buggy” class as a subclass of Plan. The second calls the superclass constructor in the Python fashion (i.e., it’s boilerplate).

Another key feature of PyCKBot–which makes your life much easier, as a roboticist–is that it gives you lots of feedback. PyCKbot has many ways to tell you what is happening inside the software in real-time. One example:

    
  def onStart( self ):
    progress("Buggy started")
  def onStop( self ):
    progress("Stopping")

These two methods are called–surprise!–when the Plan is started and stopped: The Buggy Plan will print “Buggy started” when it starts, and “Stopping” when it stops.

A note on progress(): Why are we using this instead of something more familiar, like print? print buffers, and can lead to your feedback coming to you out of sync with the things it is trying to tell you are happening; that is less than helpful. The function progress() forces the message to be printed immediately, be time stamped with JoyApp’s global timer.

When the Plan is stopped, we’d like the actual robot to stop. Let’s make that happen:

    r = self.app.robot.at
    r.lwheel.set_speed(0)
    r.rwheel.set_speed(0)

Short version: Your robot has a right wheel and a left wheel; we’re making sure they are set to zero speed.

Long version: Each Plan has an attribute called app that references the JoyApp running this specific Plan. Whenever a JoyApp has a robot associated with it the Cluster controlling that robot is stored in the robot attribute (i.e., ‘self.app.robot’). Every Cluster has a convenience object called at that lets you access the individual modules in that cluster using human-readable names (like lwheel, which is the left wheel). Note that we’re loading the convenience object into r. This isn’t just for easy reading: Storing the object in r reduces both processing load and the likelihood of a typo resulting in you spending 20 hours trying to pin down a hardware problem that doesn’t exist.

Next we’ll define the behavior() method, describe the sequential behavior we want from our robot:

  def behavior( self ):
    oncePer = self.app.onceEvery(0.5)

As noted earlier, every Plan has an event filter and a behaviour. This is our behaviour, behaviour–it’s a method that executes the thread (the sequential actions of the Plan).

Now, pay attention to the second line–onceEvery() is very handy! It will save you time and preserve your sanity. If you want something to happen every X period–for example writing a debug printout every half-second, so you know the current status of the two wheels–use the onceEvery() method. It takes one parameter: a time interval of your choosing (in seconds). onceEvery() then creates a function that will return true once in every time interval of that length.

Please don’t re-invent this wheel! Don’t try and use Python’s time module! Just use the tools we give you. We are trying to help you.

Now we get to the main loop of the behaviour, which runs “forever”:

    while True:   
      yield

As you’ll recall, JoyApp uses “cooperative multithreading.” By preventing unexpected switching between threads, cooperative multithreading exterminates all of the usual hobgoblins of multi-threaded programming–but this also means you must explicitly release the processor. That is what happens here: every iteration of the loop the code yields the processor back to JoyApp. JoyApp then determines what to do–check for events, run other Plans, etc.

      sf = self.app.sf

      lspeed = sf.getValue('joy0axis1')
      rspeed = sf.getValue('joy0axis4')

This chunk of code reads the joysticks: sf is the stickfilter attribute of this Plan’s owner JoyApp. lspeed and rspeed are local variables, the front-back values of the left and right joysticks (these values are fractional numbers ranging between -1 and 1–push the right stick all the way forward and rspeed = 1, pull the left stick 90% back, and lspeed = -.9).

      r = self.app.robot.at
      r.lwheel.set_speed(lspeed*200)
      r.rwheel.set_speed(rspeed*200)

The Plan takes whatever values it found on the joysticks, multiplies these by 200–because set_speed() is in RPM–and sends that value to the motor module. Robot goes zoom!

Finally, we check to see if our onceEvery() condition has been met. If so, the Plan prints the terminal message, reporting the status of the wheels:

      if oncePer():
        progress("Buggy: left wheel %6f right wheel %6f"
          % (lspeed,rspeed))

Our Buggy Plan is complete. Now we need a JoyApp to run it.

First we create the BuggyApp sub-class of the JoyApp Class:

class BuggyApp( JoyApp ):
  def __init__(self,robot=dict(count=2),*arg,**kw):
    JoyApp.__init__(self,robot=robot,*arg,**kw)

The first line creates the BuggyApp sub-class. The second line could be translated as “By default our robot requires two modules to be available” (which is what “count=2” means). The third line is more “superclass constructor” boilerplate; it initializes the application’s app.robot attribute, which we need to control the robot.

(Incidentally, that .robot attribute is a logical.Cluster–remember those from the beginning of Tutorial 0? It’s is set up for you automatically whenever you pass a robot= to the JoyApp constructor. )

Note: You may be wondering where these names “lwheel” and “rwheel” came from. Heck, if you’ve gone through Tutorial 0, then the fact that we haven’t used names= yet should be driving you nuts. We’ll address this at the end of this document, in the .

  def onStart(self):
    sf = StickFilter(self,dt=0.05)
    sf.setLowpass( 'joy0axis1',10)
    sf.setLowpass( 'joy0axis4',10)
    sf.start()
    self.sf = sf
    self.ma = Buggy(self) 

This onStart() runs once the robot is successfully initialized. It then calls the StickFilter class. The first paramater StickFilter() must receive is the current JoyApp (i.e., self). dt is the time step for the filter. In this case, we used .05, which means “20 timers per second.”

The next two lines tell the StickFilter that it wants to care about a couple of joysticks (joy0axis1 and joy0axis4) and process those signals using the low-pass filter (hence, setLowpass()) with a cutoff time of 10 samples and a time constant of 0.5 seconds (hence, dt=0.05).

The last three lines:

  1. Start this StickFilter Plan. We want it running all the time.
  2. Store this Plan instance in the BuggyApp .sf attribute
  3. Create a Buggy plan instance and store it in .ma. (Note that this Plan is ready to be started, but isn’t yet running.)

At the top of this tutorial we acknowledged that sequential and user-event driven behaviors are a real pain to coordinate in code. We already covered how JoyApp handles the sequential parts with behavior() methods in the Plan. Here’s how JoyApp makes events easy to handle:

  def onEvent(self,evt):

For code hygiene (and an easy life), we want to be sure that the top-level event handler immediately hands off all events to the appropriate Plan. onEvent(self,evt) is the main event planning method in JoyApp. It is the top level method, which watches for all events (including timer events and user input), and decides what happens.

HANDY FEATURE ALERT!: By default, onEvent() does two very handy things:

  1. It looks for esc and Q keystrokes and quits JoyApp when it sees either
  2. It prints all events it sees to the standard output in a format you can cut-&-paste directly into your code. Want your robot to perform a Death Blossom when you press D and click the right mouse button? Do not waste time googling ASCII codes! Just mash the buttons down while watching the terminal, and paste what pops up directly into your code.

StickFilter is running, the event handler is ready to pass events to the right Plan. It’s time to capture those control events:

    if evt.type == JOYAXISMOTION:
      self.sf.push(evt)
    elif evt.type==JOYBUTTONDOWN and evt.joy==0 and evt.button==0:
      self.ma.stop()
    elif evt.type==JOYBUTTONUP and evt.joy==0 and evt.button==0:
      self.ma.start()

Translation of the first two lines: “Is a joystick moving? If so, then push that event to StickFilter().” (Note that, as per our initial commitment to code hygiene and an easy life, we’re immediately handing this off to the StickFilter Plan.) The remainder of this chunk constitutes a “dead-man switch”: If you are holding down the 0 button the robot runs; if you let go (or drop the controller), the robot stops. (remember: Safety first!)

The next chunk of code makes sure we understand what’s happening inside our robot at all times:

    elif evt.type==CKBOTPOSITION:
      return
    JoyApp.onEvent(self,evt)

Motors can report position change as events. Since you don’t want your printout full of these, the first line of the above chunk of code filters these out. All other events (keypresses, joystick or mouse events, timer events, etc.) get printed.

Finally, we’re morally obligated to prevent our robots from running amok and killing all humans. This code does that:

  def onStop(self):
    self.ma.onStop()

onStop() stops the program from running and the robot from moving. (FYI: Regardless of what your code says, whenever JoyApp ends it calls robot.off, which will try to aggressively switch off all motors in the robot–again, Safety First!!!)

Finally, we have some Python housekeeping/boilerplate:

if __name__=="__main__":
  app = BuggyApp()
  app.run()  

The first line is a Python idiom: when this code runs as a top level script, __name__ implicitly equals __main__. As such, this statement is true and we create a BuggyApp object and run it. If, on the other hand, thise code has been imported as a Python module, __name__ will the module name. In that case, the classes will have been declared, but no code would run, because __name__ does not equal __main__.


Don’t re-invent the lwheel!

If we never used the names= parameter (see Tutorial 0), then where did the lwheel and rwheel module names come from?

JoyApp has a mechanism for storing configuration files. This is a good idea for several reasons:

  1. Code fixes should be independent from hardware fixes
  2. Identical robots should be able to run identical code

If Jack and Jill are running the same robot software, they should be able pull the same code updates without needing to tediously patch their code to account for having different numeric module IDs. Similarly, if Jack writes some test code, it should be easy to ensure that his module names match the ones that Jill uses–even after a module is replaced because of a spilled Starbucks.

The JoyApp configuration file is stored in the pyckbot/cfg directory (sibling to the pyckbot/py directory in your PYTHONPATH). In that directory you will find various YAML files, one of which is JoyApp.yml, which contains a nodeNames field. Putting:

nodeNames: {
  0x14 : lwheel,
  0x41 : rwheel
}

in this file will mean that for all JoyApps on this computer module 0x14 will be named “lwheel” and module 0x41 will be named “rwheel”.

Jill can have her unique JoyApp.yml file, Jack can have his, and they can thus run the same code, even though her motors are 0x14 and 0x41, while his are 0x13 and 0x77.


Improve this page