A practical component example

The usage of a framework is often best explained by showing a real world example. One typical component that consists of usual parts like a device driver, a component's internal behavior and a service level interface is for example a Laser Range Finder (LRF). The usage of a LRF component instead of the device library directly leads to several advantages. First the driver is encapsulated inside a component once for all other components. Hence the driver can be placed at any location in the system and can be even replaced by another device, without modifications to other components. Second the usage of communication ports lead to a well defined and understood interface with clear semantic and a clear data representation coming from communication objects. This allows choosing at runtime between different LRF components depending on the overall system behavior (improving fault tolerance and the freedom of choosing between devices mounted on different locations on the robot ...)

Component design

Figure 1: Laser Range Finder component design.

Figure 1 gives an overview on the internals of a typical LRF component. We assume a LRF component for a SICK laser range finder. Hence the component consists of a DummyLRFDevice class, which is in this example not a real driver but a dummy class simulating a real SICK driver. This class could be implemented as a SmartTask if it needs an own thread of execution. Next the component consists of a LaserTask that implements the component's behavior. We assume that the component is running with an infinite loop, in which a current laser scan is read from the LRF device and is then published through the communication ports. To publish the current (newest) laser scan, it can be stored internally in a communication object, namely CommMobileLaserScan. To prevent race conditions from concurrent access to this communication object a SmartMutex can be used. The communication object and the mutex are encapsulated in the LocalLaserScan class. Finally a LaserQueryHandler handles the incoming query requests. The component behavior is summed up in the following listing:

  1. Read the current laser scan using the DummyLRFDevice class
  2. Get current position from BaseStatePushNewestClient port
  3. Request the mutex in LocalLaserScan
  4. Store current laser scan as communication object inside of LocalLaserScan and stamp it with the current base position
  5. Release the mutex in LocalLaserScan
  6. Trigger the passive LaserPushNewestServer to publish current laser scan object

The LaserQueryServer does not have to be triggered by the LaserTask because a QueryServer is triggered from the outside of a component, from connected QueryClients. When a QueryServer gets a query-request, it delegates this request to the registered QueryServerHandler. In this example LaserQueryHandler is the handler of the LaserQueryServer communication port.

Figure 2: LRF component's internal classes

Figure 2 zoom further into the component's internals. The behavior task LaserTask takes control over the laser device driver and the server ports. This is feasible in this example because the LaserTask is the only task that uses both the driver and the communication ports. Otherwise the ports or the driver would have to be shared among different tasks. The initialization of the device driver, the client port, the server ports and the query handler is implemented in the open_lrf(std::string device-name) method of the LaserTask. Additionally this method starts the behavior task. The behavior loop is implemented in the svc() method. Next the method close_lrf() stops the behavior task and the device driver.

The class DummyLRFDevice defines the get_next_laser_scan(...) getter method. This method is typically a blocking call which implements the communication with the real device to get the newest laser-scan data.

Blocking calls are an important issue in a component's design. Every blocking SMARTSOFT/ACE kernel-operation can be unblocked instantly by calling blocking(false) method of the component. This allows a user to end all task activities immediately inside a component. This fails if a task uses libraries with blocking calls that are not under SMARTSOFT/ACE kernel control. There are two possible solutions to handle this issue. First some device libraries provide some kind of shutdown functions that end up all driver activities. That function can be called inside the signal handler of a component as described in section component-hull of the article how to build a component. If the library does not provide such a function, the task is forced to abort by the SMARTSOFT/ACE kernel. It should be ensured, that memory clean up does not lead to segmentation faults. An example for this function is shown inside the close_lrf() method.

Next some of the crucial methods will be presented on source code basis to show a possible implementation to the issues mentioned above.

Implementation issues

The first listing shows the implementation for the methods open_lrf(...) and close_lrf() of LaserTask. These methods are called out of the component as shown below. The open function initializes the lrf device driver and then starts the device driver's and its own thread of execution. Second, the close function sets the boolean variable lrf_device_running to false, which will stop the LaserTask's thread loop. Additionally the device driver is stopped. In this case it is assumed, that stopping the lrf device driver will break up all blocking calls on this driver.

int LaserTask::open_lrf(std::string device_name)
{
	// open laser-range-finder device
	if(dummyLRFDevice.open_device(device_name) == 0)
	{
		// indicate running device
		lrf_device_running = true;

		// start lrf-thread and own-main-thread
		if(dummyLRFDevice.open() == 0 && this->open() == 0)
		{
			std::cout << "LaserTask: sucessfully opened!" << std::endl;
			return 0;
		}
	}

	std::cout << "LaserTask: open() FAILED!" << std::endl;

	return -1;
}

int LaserTask::close_lrf()
{
	// indicate stopped lrf-device
	lrf_device_running = false;

	std::cout << "LaserTask: close lrf-device" << std::endl;

	return dummyLRFDevice.close_device();
}

As described above LaserTask implements the main behavior of the component, which includes reading data from lrf device, copying this data to local laser scan communication object and publishing the data through the push newest server port. The implementation for this is shown in the next listing. Additionally the base position can be used to stamp the current laser scan with it. With that other components can reconstruct, where the laser scan was made.

int LaserTask::svc()
{
	// local variables
	int basepos_x, basepos_y;

	// try to connect to PioneerBase->StateService
	if(use_base)
	{
		if(connect_to_base_service("PioneerBase", "StateService") != 0)
			return -1;
	}

	while(lrf_device_running)
	{
		// 1) get the newest laser scan (blocking wait)
		LaserScan current_scan;
		if(dummyLRFDevice.get_next_laser_scan(current_scan) != 0)
		{
			std::cerr << "LaserTask: error getting laser scan" << std::endl;
			lrf_device_running = false;
			continue;
		}

		// 2) get current base position out of base state (if configured)
		if(use_base)
		{
			CommBaseState base_state_obj;

			// get current base state (blocking wait)
			base_state.getUpdateWait(base_state_obj);

			// safe state position in local variables
			base_state_obj.get_base_position(basepos_x, basepos_y);
		}

		// 3) create a communication object out of laser data
		{
			// get local mutex using SMART_GUARD_RETURN
			// if mutex request fails due to an error, return -1 is performed
			SMART_GUARD_RETURN(LocalLaserScan::mutex, -1);

			if(use_base) 
				LocalLaserScan::comm_obj.set_base_position(basepos_x, basepos_y);

			LocalLaserScan::comm_obj.set_scan_parameter(0,360);
			for(unsigned i=0; i < current_scan.getNumberOfScanPoints(); ++i)
			{
				LocalLaserScan::comm_obj.set_scanpoint_polar
					(
						current_scan.laser_scan[i].index, 
						current_scan.laser_scan[i].distance
					);
			}
		} // mutex release is performed automatically here because of SMART_GUARD

		// 4) publish the comm-obj to push-newest service
		if( laser_pn.put(LocalLaserScan::comm_obj) != CHS::SMART_OK) {
			std::cerr << "LaserTask: PushNewestService error publishing current scan!" 
				<< std::endl;
		}
	} // end while(lrf_device_running)

	return 0;
}

The implementation of the query handler for LaserQueryServer is quite simple. As mentioned above the handler is called each time a query request is received on the query server port. The handler simply gets the mutex for local laser scan, answers to this query request with a copy of the local laser scan and releases the mutex. A SMART_GUARD simplifies mutex handling.

void LaserQueryHandler::handleQuery(CHS::QueryServer& server, 
	const CHS::QueryId id, const CommVoid& request) throw()
{
	// get the mutex (release is done automatically at the end of this function)
	SMART_GUARD(LocalLaserScan::mutex);

	// answer the query with current laser scan
	server.answer(id, LocalLaserScan::comm_obj);
}

Finally the implementation of the SMARTSOFT/ACE component is done similar to the version described in component-hull section of the article how to build a component. The difference this time is the initialization of the LaserTask, which is done inside the constructor. For that the constructor of LaserTask is called, passing the reference to the SmartComponent and a boolean variable as constructor parameters. Further the lrf device task starts its execution thread with the laser_task.open_lrf() method. If opening of lrf task fails an exception is thrown and the component initialization aborts. A running component can be aborted regularly with a SIGINT signal, which is signalled after pressing STRG+C buttons. As shown in the listing below the shutdown sequence consists of three steps. First, all blocking calls caused by the SMARTSOFT/ACE kernel are aborted with the method call this->blocking(false). After that the lrf task is stopped, which also stops the lrf device driver. After doing so the component can be aborted safely with the exit(0) call.

// smart soft
#include 

// main user-task
#include "LaserTask.h"

/**
 * Class LaserRangeFinder represents the component hull around user-task
 */
class LaserRangeFinder : public CHS::SmartComponent
{
private:
	/// LaserTask doing the main work inside of LRF component
	LaserTask laser_task;

public:
	/// Default constructor
	LaserRangeFinder(std::string name, int argc, char* argv[])
		// delegate component initialization to parent class
	:	CHS::SmartComponent(name, argc, argv)		
		// initialize the main user-task (pass SmartComponent reference as parameter 
		// and enable/disable base_state usage)
	,	laser_task(this, false)		
	{
		// open device drivers and start execution of all user-task(s)
		if(laser_task.open_lrf() != 0)
		{
			// throw a std::exception on faulty device- or task-startup
			throw std::logic_error("LaserTask could not be opened");
		}
	}

	/// default destructor
	virtual ~LaserRangeFinder()
	{
		// close device drivers
		laser_task.close_lrf();
	}

	/// default signal handler
	int handle_signal(int signum, siginfo_t *, ucontext_t *)
	{
	    std::cerr << "\nComponent Signal " << signum << " received" << std::endl;
	    if (signum == SIGINT) {
	        // STRG+C signal received

	        // 1) disable blocking waits on communication ports of this component
	        this->blocking(false);

	        //-----------------------------------------
	        // 2) shutdown the LaserRangeFinder-Device
	        laser_task.close_lrf();
	        //-----------------------------------------

	        // 3) abort immediately
	        exit(0);
	    }

		return 0;
	}
};

Of course this example is not the best implementation possible for a laser range finder, but shows some typical traps to avoid. The focus lies more on the readability of the code and less on the memory efficiency. By replacing the DummyLRFDevice by any other device library, this component design can be used for several other hardware devices.