The control system first reads a user-supplied configuration file, then initializes components according to the specified settings. When initilization is successfully completed, the system enters the main event loop. In this loop, events are generated or captured, and dispatched to the handlers associated with the current behavior of the robot. When the main processing loop is finished, the system releases all components and exits.
The configuration information loaded at startup is stored in memory in a form that all components understand. Each component knows how to initialize and release itself as well as how to operate while the vehicle is in action. The propulsion system, DVL, DAC, altimeter, acoustics, cameras, pressure sensor, and dropper are each associated with a component object. With the exceptions of the DAC, pressure sensor, and dropper, each component begins its own thread of execution during initialization. The threads that handle the DVL, altimeter, acoustics and cameras are each responsible for continuously reading from those input sources and either updating global shared state (in the case of the DVL and altimeter) or generating events to be handled according to the current behavior (in the case of the acoustics and cameras). Thus, the system is highly concurrent, and a processing or I/O delay in one component typically will not affect the operations of other components. All concurrent access to shared data is protected with synchronization primitives implemented with the POSIX threading API.
Each component logs any pertinent information during runtime to its own log. All errors are written to a shared log, as well as to standard error. All log entries contain the exact execution and entry timestamps for debugging purposes. The configuration state of the vehicle (position, velocity, altitude, altitudinal velocity, heading, and angular velocity) are logged at each main processing loop pass, making replay of the mission possible from the logs. Any events, such as behavior changes and decisions, are also appended to a separate log. The logs are protected by synchronization primitives, so that multiple threads may safely write to the same log.
The vehicle's PC104 stack runs the Ubuntu Server 7.04 distribution of Linux with the 2.6.20 kernel. The main board's onboard ethernet controller is fully supported by kernel drivers . The two USB web cameras are supported by the USB Video Class (UVC) driver. The system interfaces with the WinSystems PCM-MIO data acquisition card via the manufacturer's driver. The doppler velocity logger and altimeter connect to the system's two serial ports, and communication with the propulsion, dropper, and acoustics hardware is performed via the DAC.
The control system itself is written primarily in Java. Libraries against which the software is linked, via the Java Native Interface (JNI), include the PCM-MIO and UVC drivers.
Software: Interfacing with Sensors
As described in the design overview, the sensor component objects each handle their own I/O.
- DVL: The DVL component's initialization routine reads settings such as the DVL's offset in the robot, angular offset from forward, heading bias, and input device name from the user configuration. It opens the input device (which may be a regular file or a serial device) and, if the device is a serial port, establishes communication and sends the "start pinging" command. It then begins its own thread of execution, which repeatedly reads a binary sample from the input source, processes the data according to the configuration settings, and updates the robot's shared configuration state. The DVL component uses the Depth component to read the Z coordinate of position and velocity instead of relying on the data coming from the DVL itself. If the configuration specifies an output file, all binary data is also written to this file. This allows runs to be recorded to a file, which can later be specified as the input source to allow the robot to exactly replay movements. The system receives approximately 8 samples a second from the DVL.
- Cameras: Each of the two cameras has its own component object. The component first checks the configuration for any custom picture settings (size, brightness, saturation, etc.). Once the specified camera device is opened and initialized throug the Video4Linux interface, the component starts a separate thread of execution. Each of the camera threads sleeps by default. When a behavior requests that the camera be started, the thread wakes up and begins reading frames from the device. When each frame is read, the camera component passes it to a listener object for processing. Because the listener object is called from the camera's thread, vision processing does not interfere with more frequent but less computationally intensive processing. When a behavior is done with camera data, the camera thread goes back to sleep.
- Acoustics: The acoustics component thread polls a serial port for a packet containing pairwise time receipt differences between the hydrophones.
Software: Propulsion and Navigation
The propulsion component takes request for navigation and translates them to thruster movements. It first reads the umpteen navigational control parameters from the configuration settings, then begins its own thread of execution to handle requests. Requests are made from behaviors running in other threads, so all access is synchronized. Requests are almost always in the form of a target waypoint, consisting of a position and heading. The propulsion thread continuously performs the following calculations: It checks the current mode of propulsion. If there is no request, it idles. If there is a waypoint request, it calculates the distance from the current position and heading to the target. It then finds the instantaneous linear and angular that brings the vehicle's configuration closer to the target. These accelerations are converted to force and torque based on the mass and moment of inertia of the vehicle, as well as its current linear and angular velocity. Next, the propulsion component converts the force and torque to desired thrust forces from each of the four thrusters. This is a linear mapping, because the force and torque equations are linear and there are four inputs (three one-dimensional forces and one torque) and four unknowns (the thrust delivered by each of the four thrusters). The delivered thrusts are scaled according to input parameters that determine how depth, position, and heading progress are weighted against each other. The thrusts are scaled and clamped to within the range possible given the thrusters, and the resulting forces are converted to control voltages using a cubic model of the thrust-to-control-voltage relationship derived for a single thruster. These control voltages are sent to the thrusters through the DAC analog outputs. The propulsion system can deliver arbitrary direction forces and torques independently of each other -- the robot can move in any direction while facing any direction.
The conversion algorithm described above changes according to input parameters. For instance,the short_distance parameter determines the distance between current state and target waypoint inside which the vehicle's acceleration will be proportional to the distance. Thus with short_distance equal to one meter, the vehicle will not attempt to slow down until it is within one meter of its waypoint. Furthermore, the vehicle will not attempt to assume its target heading until it is within short_distance of the target, instead turning towards its target for most efficient travel up to that proximity. Variables such as kp_depth and kp_heading change how the propulsion system weights differences in depth and heading (between current state and target state) over differences in position. A high kp_depth results in navigation that sacrifices linear velocity for correct direction.
The control system is event based, and the behavior of the vehicle is determined by reactions to events. The system groups sets of event handlers into objects called behaviors. A behavior is simply an object with arbitrary internal state that handles events generated by the system. The following events are generated:
enter
- The enter event is generated when a behavior object is first activated
by the control system. The behavior typically handles the enter event
by performing any necessary one-time initialization. This event includes
information about the state of the vehicle and the current time.
think
- The think event is generated periodically (typically ten times
a second) by the system. This event can be used to trigger any periodic,
continuous, or time-sensitive aspects of a behavior. This event include
information about the state of the vehicle and the current time.
leave
- The leave event occurs when a behavior has finished its operation
and is being deactivated. Including the same information as the enter
and think events, this allows the behavior to free any resources it
has acquired.
frame
- The frame event is generated by an active camera component upon
receiving an image frame from the camera device. The image and a handle
to the camera is passed to the behavior's event handler. The frame handler
is used to initiate vision processing on the captured frame.
ping
- The ping event is generated by the acoustics component and notifies
the active behavior that a ping has been detected. The event includes
information about where the acoustics hardware believes the pinger is.
Consider, as an example, a Waypoint behavior. This might handle events as follows:
enter
- The Waypoint behavior would handle this event by requesting the
target waypoint through the propulsion component interface.
think
- Here the behavior would check whether the robot is close enough
to its target. If so, the behavior would notify the system that it is
done (triggering the leave event). The behavior might also check whether
a specified time limit has expired and leave due to timeout if that
is the case.
Behaviors can be implemented in compiled C++ or in interpreted code via the scripting engine.
The control system includes a scripting engine, written from scratch, for a language similar to C++ or Java. The language is loosely typed and supports primitive types (real numbers, strings, 3d vectors, and lists of arbitrary objects) as well as user-defined compound object types with methods. Scripts are parsed and virtual machine code is generated from them at runtime, so changes to scripts require no recompiles of the C++ code. The scripting engine supports both interpreted and native types and functions, so any expensive processing can be implemented in C++ and called from scripts.
The real usefulness of scripts lies in the ability of the user to declare and implement behaviors in the scripts themselves. This means that arbitrary behaviors can be expressed and tested without a single recompile of any code. For example, consider the hypothetical Waypoint behavior discussed here. Here is a possible implementation of this behavior in script:
# This is a comment line.
# Declare the behavior type.
behavior Waypoint {
# Members
real startTime;
real targetDirection = 0;
vector targetPosition;
real timeLimit;
vector target;
# Constructors
method Test() {}
method Test(vector t, real time) { targetPosition = t; timeLimit = time; }
method Test(vector t, real time, real d) { target = t; timeLimit = time;
targetDirection =d; }
# Handle the enter event by setting our waypoint. Once the waypoint is set,
# the propulsion component will continuously move us closer to it.
# Also turn on the forward-facing camera and capture color frames.
method onEnter(robot, t) {
startTime = t;
setWaypoint(targetPosition, targetDirection);
startFrontCamera(1);
}
# Handle the think event
method onThink(robot, t) {
# How far away are we?
diff = targetPosition - robot.position;
dirDiff = targetDirection - robot.heading;
# Output some debug info.
print("Current pos: ",robot.position, "\tDistance to target: ",
diff.abs(),"\n");
# Are we close enough to be done?
if (diff.abs() < 0.2 && abs(dirDiff) < PI/36 ) {
print("Attained waypoint: ", targetPosition, "\n");
leave();
} else if (now()-startTime > timeLimit) { # Timed out
print("Timed out.\n");
leave();
}
}
# Save each frame to a file
method onFrame(robot, t, camera, image) {
print("onFrame: t = ", t, "\n");
saveImage(image, "frame_at_time_"+t+".jpg");
}
# This is how we identify this behavior object.
method toString() {
return "Waypoint["+targetPosition+","+targetDirection+"]"; }
}
# This is executed after initialization
main()
{
# Add a new waypoint to the queue of behaviors.
queueBehavior(new Waypoint([0,2,-1], 30, PI));
}
The scripting engine is also used to parse and evaluate configuration settings, allowing for arbitrary expressions in the configuration file. Here's an example configuration file:
devices {
dmm32 = "on";
depth = "on";
dvl = "on";
altimeter = "on";
propulsion = "on";
frontcam = "on";
downcam = "off";
}
frontcam {
device = "/dev/video0";
brightness = 0.5;
contrast = 0.5;
hue = 0;
}
downcam {
device = "/dev/video1";
brightness = frontcam.brightness;
contrast = 0.5;
hue = 0;
}
depth {
a = 0.4;
b = 0.5/(0.5-a);
channel = 13;
use_altimeter = "no";
bottom = 4.58;
}
propulsion {
check_supply_period = 1000000;
supply_voltage = 33;
channels = { 2, 3, 0, 1 };
inversions = { 0, 0, 0, 1 };
no_dac = 0;
max_forward_thrust = 10*NEWTONS_PER_POUND;
max_reverse_thrust = -4*NEWTONS_PER_POUND;
long_distance = 1.5;
short_distance = 0.5;
big_turn = 3.149;
kp_heading = 1;
kp_depth = 4;
}
dvl {
zero_heading = -3*PI/4;
angle_to_forward = 3*PI/4;
offset_from_center = [0,0.25,0];
start_pos = [0,0,0];
device = "/dev/ttyS0";
}
altimeter {
device = "/dev/ttyS1";
window = 1;
}
robot {
no_log = 0;
}
Vision processing algorithms operate in real time on the system's single processor. Detecting the bins on the floor of the pool works as follows. First, the image's gradient field is calculated (both horizontal and vertical). Then, edge pixels are selected by thresholding on the magnitude of the gradient, around the mean gradient level plus one standard deviation. Then, a fast queue-based flood fill algorithm computes non-edge connected regions. Regions that are too small are discarded. Each surviving region's star-shaped boundary is determined by scanning its edge pixels rotationally from the center of mass of the region. The resulting boundary is simplified by merging nearly-collinear consecutive segments. The longest segments of the resulting coarsened boundary are selected, and their intersections are found. These intersections are used as the vertices of a candidate shape. The shape's angles are checked, and if they fall within specified bounds, the object is considered a bin.