Data: 07 11 2008
Duration of activity: 3 hours
Group members participating: all group members
1. The Goal
The goal of this lab sessions is to monitor different behaviors of the robot, when these behaviors are handled by different threads.
2. The Plan
- Try a program which suggests different behaviors for the robot
- Try different levels of thread enabledness in SoundCar.java
- Discuss the two aspects in Class Behavior's behavior
- Make changes in SoundCar.java with the idea from lesson 7
3. The Results
- As is was mentioned, the idea of this lab session is actually to monitor different behaviors of one robot, when every behavior pattern is implemented by a different thread. To actually see what is going with the robot's behavior, different kinds of sensors come in handy. Read more about the construction in 3.1. The construction of the robot.
- The first thing to do is to try the code that was given for lesson 8. The idea is in the SoundCar.java program and the remaining classes needed to run that program. See 3.2. Different behaviors of the robot.
- The most important aspect of this lab session's work is to actually see how the robot acts, when it is using threads. Some threads perform more significant behavior than others, nevertheless all threads together produces interesting results. The robot even seems to be exhibit ``smart'' behavior. Read more about this in 3.3. Different thread activenesses.
- As we are analyzing interesting behavior aspects, the ideas in the source code might not always be so straightforward. This brings some discussions. Read about them in 3.4. Discussions.
- In the initial code we have three threads of behavior: Random driving, avoiding obstacles, and playing beeping sound. The last part of the lab session is to add one more behavior in a new thread---going towards the light. This is based on ideas from Tom Dean's notes. Read more about this implementation in 3.5. New thread for light following.
3.1. The construction of the robot
This lab work basically relies on playing with the code more than figuring out how NXT works. This was done in many previous lab sessions. Now the construction of the robot is not significant. For this particular lab session we have a robot which has two motors and is mounted with two thick, not-so-tall wheels. The robot has to be able to go forward, backward, turn right, and turn left. Also, the robot is mounted with three sensors: An ultrasonic sensor, and two light sensors.
And the robot now looks like this from the side:
And the robot looks like this from the front:
3.2. Different behaviors of the robot
When inspecting the robot's behavior, it seemed to be driving around without meaningful purposes: Sometimes driving forward, sometimes to the left or to the right. This was done in different speeds and with significant pauses. Also, whenever an obstacle was spotted close enough, the robot turned the motors full speed backwards for a moment of time. Lastly, the robot was beeping an annoying sound with a constant time interval.
One more thing to note was the LCD output. When driving randomly, "f" was written whenever it was driving forward, to the left or to the right. "s" was written whenever the car was in the stopping mode. Nothing was written whenever the car was not effected by the random driving. Now, when the car was handled by the obstacle avoidance thread, besides "f" and "s" there were "b" for the cases when the car was driving full speed backwards. What is more, the distance value was printed as well. The last thing, there was "s" printed with respect to playing sound, and this was done right before the beeping sound could be heard.
3.3. Different thread activenesses
In this model a layer can suppress every layer beneath it. It is important to notice that higher-layers do not get more CPU-time then lower-layers; they simple suppress whatever the underlaying layer might want to do (while this might give the high-level more CPU-time because the underlaying layers have nothing to do). So if a higher layer always is active, the lower layer will never get into play. An observation is that the lowest level probably is something the robot can do when there is nothing else (nothing smarter) to do. For instance, random-walking.
When only the lowest level is activated, the robot only drives randomly around, bumping into whatever might get in its way.
It gets much more interesting when the second layer is activated. The robot at this point suppresses the urge to drive randomly around every time it is about to hit something. This way of reacting is much like our humans' nerve-system. We can let our hands run freely across a surface, but as soon as we feel something burning hot or sharp we retract our arms to get away from the source of pain. This is the exact same behavior our robot started to show as soon as the avoid-layer was activated.
- Daemon threads.
The term ``daemon'' was first used at MIT, in some of the earliest work on making a multiuser operating system, and something UNIX would later inherit. A daemon process is a process which, immediately after its creation is disowned by its parent, and made a child of the init-proces (pid 0). This way, the initial parent-process can terminate without also terminating the daemon-process. As a effect a daemon-process is often used to do maintenance work.
In Java, these semantics are somewhat inverted: A daemon thread is one that the VM is not kept running for the sake of (and thereby, its existence becomes relatively volatile). Rather, if a thread is a daemon thread, it is not waited for. See also the javadoc on Thread.setDaemon().
In our code, the classes extending the Behavior-class need to be running until the robot stops, and by making them daemon, the threads will be terminated when the main-thread terminates.
- Boolean suppression value.
Every thread created from a class extending Behavior has a boolean field "suppressed". If this boolean is true, the thread will not send out motor commands. More precisely: Every method in those threads who uses the motors, have a
if ( ! suppressed )statement as the first line of code. Therefore, if another thread has set the boolean value to
true, the method will not do anything.
This method has pros and cons:
A good result, is that a ``lesser'' behavior quite effectively gets suppressed by a higher behavior. The drawback is that one need to implement the
if ( ! suppressed )for every piece of code that someone or something would want to suppress. Another drawback is that we need to know at compile-time how many behaviors we want to suppress. As of now, we tell every layer beneath us that we want to suppress them, and afterwards unsuppress. It is therefore part of the programmer's job to be sure that he/she has remembered to suppress and later unsuppress all the layers, or else the entire algorithm falls apart.
A positive thing is that this method is somewhat easy to extend (not as in Java ``extends''). We can later on add a layer that will do something more important than playing a sound---and therefore suppress everything we've designed so far. But we can not put layers in between of already existing ones, easily.
This is a rather limiting factor for this programming style, because often one would put a ``panicking'' layer at the top. E.g.: When close to hitting a wall, the most important thing is to avoid. But if one uses this programming style as a way to add more fine-grained behavior, we would have to revisit the Avoid-layer for each layer being added to make it suppress that one, also.
All in all, designing a robot as a stack of layers, wherein a higher layer can suppress everything beneath it, is a very good way to structure the different elements of the robot, but the current design does not scale as good as one would like, and pretty much demands one to have a complete overview of the robots design beforehand. This partly is why we refactored the code slightly. See more in 4. Refactoring the code, suppressionwise
3.5. New thread for light following
To program a thread that implements the behavior of seeking light, we went for the Braitenberg approach: A simple 2b-typed robot, where sensors linearly feed the opposite motors positively.
The interesting thing to note about this approach is that when programming with multiple ``concurrent'' (or interleaved, at least), behaviors, it naturally becomes hard to distinguish the different behaviors, and when they're in play.
Two things aim to mend this: The generous use of explicit delays and the inhibition of other behaviors, so that at any one time, only one behavior is visible.
The use of explicit delays seem like wasting time: Any goal that the robot is supposed to reach will only be reached more slowly, when using explicit delays, as opposed to when leaving them out. However, this may not be entirely true, since when developing the robot, the imperative objective is to gain knowledge of the behavior---why it is doing as it does and what needs to be changed to get closer to the desired behavior.
This motivates the use of explicit delays. Otherwise, the robot's behavior becomes incomprehensible, simply because it happens too fast. Obviously, the ultimate goal is to make the robot work as fast or precise as possible, but up until then, the development can refine its explicit delays.
4. Refactoring the code, suppressionwise
The code that was made available contained an admittedly silly implementation of suppression: The various classes had constructors with a parameter each for the by-this-suppression Behaviors. Also, the given Behavior-subclasses that were made available defined fields for each of those, and manually called
Obviously, suppression is shared behavior amongst the various subclasses of Behavior, so in this scenario, it was attractive to refactor the code so that Behavior defined suppressSupressees() and unsuppressSupressees(), which, given constructor initialization of the ``suppressees'', will suppress all of the
This however proved cumbersome, and as a word of warning, below is the obstacles that hit the idea:
- Refactoring six places at one time is never easy. It is prone to errors. And in fact, a Behavior-subclass stood completely untouched right until it generated errors.
- Programming with Java 1.5 generics doesn't work. Lejos doesn't support that. So static type safety guarantees must be enforced by the programmer.
- There is no easily-accessible variadic method-implementation. That is, it's cumbersome to get started doing, and it may not even be supported. Confer with the point on generics.
- Java 1.5 for loops over Iterable types doesn't work. There is no extended for loop available. (Albeit, this is a corollary to generics not working).
However, the advantage is that the resulting code is a tad nicer, from a separation-of-duties point of view. The interfacer has gotten a tougher job, though.
A snapshot of the code is available here. The interesting parts are repeated below:
The Behavior class was extended with:
public Behavior(String name, int LCDrow, ArrayList s)
suppressees = s;
public void suppressSupressees()
for(int i = 0; i < suppressees.size(); i++)
public void unsuppressSupressees()
for(int i = 0; i < suppressees.size(); i++)
The run()-methods of the various Behavior-subclasses were altered to call suppressSupressees() and unsuppressSupressees().
And lastly, the main() method of SoundCar was changed to call the constructors appropriately:
ArrayList afsupressees = new ArrayList(),
rdsupressees = new ArrayList(),
pssupressees = new ArrayList(),
slsupressees = new ArrayList();
sl = new SeekLight ("Light",4,slsupressees);
rd = new RandomDrive("Drive",1,rdsupressees);
af = new AvoidFront ("Avoid",2,afsupressees);
ps = new PlaySounds ("Play ",3,pssupressees);
We've succesfully played around with behavior-governed robots, and gained experience with layered architectures in robot control. Among those experiences are:
Robots designed with a layered architecture make for easy understanding of what's happening. One's mindset can attribute different steps to different modules in the code, and this is helped by using explicit delays to slow down the program's actions.
When one contemplates a layered architecture, one might want to go all-in from the start, and make a smart dependency/suppression-system, early on. We, for one, have quickly outgrown the simple constructor-parameter scheme presented to us.
6. Further work
The programs we wrote were not fully-developed, algorithm-wise, due to time constraints. The SeekLight behavior ought to get some thought in order to more consistently drive towards a light source.
Our robot design might also gain something from being redesigned. The robot is almost tipping over, when it's avoiding obstacles, and the light sensors are aimed almost in parallel.
Most importantly, some generic way of handling a DAG of Behavior dependencies ought to be researched and implemented.