Skip to content

rock-core/drivers-orogen-iodrivers_base

Repository files navigation

Generic oroGen integration for drivers based on iodrivers_base

Usage

Your component must depend on the drivers/orogen/iodrivers_base package, and the driver component must subclass iodrivers_base::Task:

In the package's manifest.xml, add

<depend name="drivers/orogen/iodrivers_base" />

At the top of the oroGen file, add:

using_task_library "iodrivers_base"

Then, make your task a subclass of the iodrivers_base::Task task context:

task_context "Task", subclasses: "iodrivers_base::Task" do
end

There is two things left to do:

  • configureHook: create the device driver, open the device and call the setDriver() method. The device's "name" (i.e. device file, IP for network-based access, ...) is provided in the io_port property. Note that it is legal for this property to be empty (see the next section for explanations). To ensure exception-safety of the configureHook, one must use iodrivers_base's ConfigureGuard guard class (example below).
setDriver(driver)
  • process the incoming data in a "void processIO()" method that is going to be called by the base class from updateHook().

  • cleanupHook: the device's close() method is going to be called automatically by the base class. The pointer is not going to be deleted though, so you should take care of it if you want to recreate the object in cleanup/configure cycles.

For instance, assuming the class has a m_driver attribute of the Driver subclass

std::unique_ptr<Driver> m_driver;

One could have a setup looking like:

bool Task::configureHook()
{
    unique_ptr<Driver> driver(new Driver());
    // Un-configure the device driver if the configure fails.
    // You MUST call guard.commit() once the driver is fully
    // functional (usually before the configureHook's "return true;"
    iodrivers_base::ConfigureGuard guard(this);
    if (!_io_port.get().empty())
        driver->openURI(_io_port.get());
    setDriver(driver.get());

    // This is MANDATORY and MUST be called after the setDriver but before you do
    // anything with the driver
    if (!TaskBase::configureHook())
        return false;

    // If some device configuration was needed, it must be done after the
    // setDriver and call to configureHook on TaskBase (i.e., here)

    m_driver = move(driver);
    guard.commit();
    return true;
}
void Task::processIO()
{
    mDriver->processSamples();
    _samples.write(mDriver->getOrientationSample());
}
void Task::cleanupHook()
{
    // MUST BE done first. It detaches the driver from the task
    TaskBase::cleanupHook();
    // Not strictly necessary, the driver will be deleted on the next
    // successful configureHook anyways. YMMV.
    m_driver.reset();
}

In addition, you might want to start data acquisition in the startHook and stop it in the stopHook. Whether the acquisition start/stop should be in startHook/stopHook or configureHook/cleanupHook is governed by the following factor:

  • if starting/stopping acquisition is done a lot quicker than the whole device configuration, then do it in startHook/stopHook as you will not waste ressources doing acquisition while the data is not needed
  • if it is slow (some people would even say: not deterministically fast, but YMMV), do it in the configureHook/cleanupHook to ensure the responsiveness of the system when start/stop cycles are needed but reconfiguration is not needed.

By default, the standard runtime management of oroGen tasks entails that you will have a full stop/cleanup/configure/start cycle if reconfiguration is needed. You should therefore not care about dynamic reconfiguration in first implementations.

Runtime Errors

While the runtime error support in oroGen components is usually seldom used, it does have a very good use case in device drivers. The semantic of the state is that of a "degraded state".

In the case of devices, it can be used when the primary function of the device is not available - for instance for reason of safety, or the presence of an alarm, but we need the driver to run to know whether the cause of the malfunction disappears. Examples:

  • a device using subsea transducers (e.g. sonar, USBL) is not in water, but we need the device's status to know whether it is in the water.
  • a motor gets into an overtemperature failure mode, but we need the motor feedback to know it gets out of this mode.

iodrivers_base originally did not have any support for this kind of states. Therefore, for backward compatibility reasons, the support needs to be enabled in C++ by calling setRuntimeErrorIOProcessing(true) (e.g. in the constructor)

Once this is done, the common expected driver-related error handling will be enabled (output of I/O status, handling of FD errors and of io_wait_timeout). IO processing has its own hook called errorProcessIO. But in this case, a default implementation exists, which calls processIO.

Details about the iodrivers_base::Task interface

This interface provides two means of communication between the device and the driver.

  • direct I/O access. This is done by setting io_port to a non-empty string. Acceptable values for io_port if the driver uses openURI (which it should do) are listed in the property's documentation.
  • port-based access. In this mode, the data is flowing through the io_raw_in and io_raw_out ports. The transfer of data between the ports and the Driver object is made by the iodrivers_base::Task class.

Other properties control the behavior of the system in both modes (read timeout) and write statistics about the I/O. Some properties are specific to one mode, in which case this is documented in the property documentation directly.

Unit testing components built on top of iodrivers_base::Task

The package will by default install helpers that help unit testing components built on top of iodrivers_base::Task using the Syskit-based unit testing support.

Note that, following the Rock philosophy, most of the device driver's testing should of course be done at the library level. These unit tests should be handling runtime logic that is specific to the component integration.

To use these helpers, you need to require the helpers and include them in the toplevel describe block:

# frozen_string_literal: true

using_task_library "iodrivers_base"
require "iodrivers_base/orogen_test_helpers"
using_task_library 'mydriver_project'

describe OroGen.mydriver_project.Task do
    include IODriversBase::OroGenTestHelpers

    run_live
end

The general workflow is to:

  1. deploy the driver-under-test and save it as a @task instance variable
  2. call setup_iodrivers_base_with_ports. It returns a task whose in port allows to read from the driver and out to write to it
  3. call unit tests using this task

For instance:

before do
    @task = syskit_deploy(
        OroGen.mydriver_project.Task.deployed_as("task_under_test")
    )
    @raw_io = setup_iodrivers_base_with_ports @task
    syskit_configure_and_start(@task)
end

it "interprets a status message from the device" do
    status =
        expect_execution { syskit_write @raw_io.out_port, some_message }
        .to { have_one_new_sample @task.status_samples_port }

    # Do some checks on `status`
end

Other helpful methods are raw_packet_to_s and raw_packet_from_s which convert a String to or from a Types.iodrivers_base.RawPacket. See the helpers source code for more information

About

oroGen integration of Rock's drivers-iodrivers_base package

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 9