Skip to content
This repository has been archived by the owner on Aug 25, 2020. It is now read-only.

Driver Development Guide

mlegenhausen edited this page Nov 27, 2011 · 13 revisions

Architectural Overview

The core idea of the wsn-device-drivers is to make the concurrent execution of Operations transparent for the driver developer, so he has not to care about threading. For this purpose drivers are divided in the following parts:

  • OperationRunnable: The smallest amount of work that can be implemented by the device developer to perform actions on the device.
  • Operation: A container for the OperationRunnable that manages the lifecycle and providing an execution context.
  • OperationExecutor: A executor that serialize the execution of all incoming Operations. The common used implementation is the ExecutorServiceOperationExecutor

How does these things fit together?

For better understanding, the workflow from the call of an operation on the Device interface till the operation execution on the physical device is described as follows:

  1. Method call on the Device instance.
  2. An instance of the OperationRunnable is created and all parameters are set which are needed by the runnable.
  3. An instance of the Operation is created and the the previous instantiated OperationRunnable is injected.
  4. The created Operation container is submitted to the OperationExecutor.
  5. The OperationExecutor add the operation to a queue and returns an OperationFuture for this Operation. This future allows to control the Operation.
  6. When no other Operation is currently running pop the next Operation from the Queue.

When the Operation is submitted to the OperationExecutor it can have the following states:

  • Waiting: The Operation was added to the queue and is now waiting for execution. This is the start state.
  • Running: The Operation is now executed in a thread. During the change to this state the onExecute method is called on the given OperationCallback.
  • Done: The Operation has finished successfully. During the change to this state the onSuccess method is called on the given OperationCallback.
  • Canceled: The Operation was canceled by the application. During the change to this state the onCancel method is called on the given OperationCallback.
  • Timed out: The Operation reached the timeout defined during the method call on Device. During the change to this state the onFailure method is called on the given OperationCallback with a TimeoutException as parameter.
  • Excepted: The Operation threw an Exception. During the change to this state the onFailure method is called on the given OperationCallback with the caught exception as parameter.

Note: The last four states are finish states.

During the idle time the OperationExecutor a idle operation is executed which copies all data coming from the device to an OutputStream which can be read by the InputStream object returned by the getInputStream method of the Device. This allows to read from the device in a save way cause only when no Operation is running the InputStream returns data.

When you should use wsn-device-drivers

Cause the wsn-device-drivers try to solve concurrency problems, you should only use the wsn-device-drivers for operations on the device that need a serialized execution. For other operations this framework would only add unnecessary complexity.

Creating a driver

The following describes how to create a simple "Hello World" driver. It is recommended to look in the already provided devices to get more informations on how to create a driver. Also note that the wsn-device-drivers make heavy use of Google Guice so you should be familiar with it.

Required Maven Dependencies

Add the following dependencies to you pom.xml

<dependency>
    <groupId>de.uniluebeck.itm</groupId>
    <artifactId>wsn-device-drivers-core</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

Implementing an OperationRunnable

An OperationRunnable defines the smallest amount of work that can be executed in the wsn-device-drivers. The interface is defined below:

public interface OperationRunnable<T> {
    T run(ProgressManager progressManager, OperationContext context) throws Exception;
}

As you can see the OperationRunnble is a generic class. The T defines the return value of your operation. When your operation returns nothing you can use the Java type Void.

When implementing the run method you get a ProgressManager and an OperationContext instance. With the ProgressManager you can publish the current progress of your operation to the caller. The OperationContext allows to get the current cancel state and to reuse already written operations. When throwing an Exception it is caught by the Operation and automatically handled for you.

Cause the wsn-device-drivers define a set of operations that can be executed on the device only the following OperationRunnables can be implemented:

  • ProgramOperation
  • ReadMacOperation
  • WriteMacOperation
  • ReadFlashOperation
  • WriteFlashOperation
  • EraseFlashOperation
  • ResetOperation
  • SendOperation

Because most of the OperationRunnables need parameters like an ROM image or addresses most of them have methods for setting these. To prevent rewriting this code there are the following abstract classes:

  • AbstractProgramOperation
  • AbstractReadFlashOperation
  • AbstractSendOperation
  • AbstractWriteFlashOperation
  • AbstractWriteMacAddressOperation

After choosing an appropriate OperationRunnable you can start implementing the interface. For our example we will implement the AbstractProgramOperation. This can look like the following:

public class HelloWorldProgramOperation extends AbstractProgramOperation {
    
    public Void run(ProgressManager progressManager, OperationContext context) throws Exception {
        System.out.println(new String(getImage()));
        return null;
    }
}

This Operation uses the getImage() method from AbstractProgramOperation to get the image as byte array and prints it on System.out.

Before we can run this Operation, we need to create a Guice Module. A Guice Module can look like below:

public class HelloWorldModule extends AbstractModule {
    public HelloWorldModule() {
        this(null);
    }

    public void configure() {
        bind(ProgramOperation.class).to(HelloWorldProgramOperation.class);
    }
}

In this module we bind the ProgramOperation to the HelloWorldProgramOperation. When you have more than this operation you can add all operations here.

Now we can initialize a Device and run the operation:

Device device = Guice.createInjector(new DeviceModule(), new HelloWorldModule());
device.program("Hello World".getBytes(), 1000, null);

This will output Hello World on System.out.

Using the ProgressManager

The ProgressManager allows you to publish the current process of the operation to the onProgress(float progress) method of the OperationCallback. Inside the run method you can use the ProgressManager as follows:

// Do some work
progressManager.worked(0.5f);
// Do again some work
progressManager.worked(0.25f);
// Do again some work
progressManager.done();

In this example we do the half work and indicate this by calling the worked method with the done amount of work. This will automatically call the onProgress method of the OperationCallback. In the next step we have done again some work and give again the amount of work to the worked method. With the done method we indicate that all work is done. This will call the onProgress method with 1.0f which is the maximum amount that can be worked in sum. Calling the done method is optional. This will be automatically called when run method finished. The done method is only needed when working with sub ProgressManager which is shown below:

// Do some work
progressManager.worked(0.5f);

// Create a sub progress manager that manages 0.25f of the overall progress.
ProgressManager subProgressManager = progressManager.createSub(0.25f);
// Do some work
subProgressManager.worked(0.5f);

// Create a sub sub progress manager that manages 0.75f of the sub progress manager overall progress.
ProgressManager subSubProgressManager = subProgressManager.createSub(0.5f);
// Do some work
subSubProgressManager.worked(0.75f);
// We finish our work. Calling done() is now not optional.
subSubProgressManager.done();

// We already reached with the subSubProgressManager a worked amount of 1.0f
subProgressManager.done();

// We have 0.25f work left for the progressManager
progressManager.worked(0.125f);

Creating sub ProgressManager is handy when you have methods where you want to allocate a unit of work to and you do not want to calculate the single worked values yourself. Sub ProgressManager become really useful when calling sub operations via the run method of the OperationContext. This is explained below.

Using the OperationContext

The OperationContext can be used for observing if the operation has to be canceled and for starting other operations.

For observating if an operation has to be finish you can use the following code:

if (context.isCanceled()) {
    // Exit the run method
}

This code should be used in long running operations.

When creating your operation it is sometimes handy to reuse already created operations. For example you want to program your device but want to save the mac address before flashing. See the code below.

public class MyProgramOperation extends AbstractProgramOperation {
    
    private final ReadMacAddressOperation readMacAddressOperation;

    @Inject
    public MyProgramOperation(ReadMacAddressOperation readMacAddressOperation) {
        this.readMacAddressOperation = readMacAddressOperation;
    }

    public Void run(ProgressManager progressManager, OperationContext context) throws Exception {
        MacAddress macAddress = context.run(readMacAddressOperation, progressManager.createSub(0.25f));
        // Program the device
        return null;
    }
}

In this example we use Guice to inject the already defined ReadMacAddressOperation. Note that you need in this case the following code in your Guice Module.

bind(ReadMacAddressOperation.class).to(MyReadMacAddressOperation.class);

The injected operation can now be used to execute it via the run method of the OperationContext. As you can see we use the createSub method of the ProgressManager to associate 0.25f of the work to the ReadMacAddressOperation. Note when this operation finished your overall operation progress is 0.25f.

When you need more than one instance of the same operation you can use a Provider from the Guice Framework. See the example below.

public class MyProgramOperation extends AbstractProgramOperation {
    
    private final Provider<ReadFlashOperation> readFlashOperationProvider;

    @Inject
    public MyProgramOperation(Provider<ReadFlashOperation> readFlashOperationProvider) {
        this.readFlashOperationProvider = readFlashOperationProvider;
    }

    public Void run(ProgressManager progressManager, OperationContext context) throws Exception {
        for(int i = 0; i < 10; ++i) {
            ReadFlashOperation readFlashOperation = readFlashOperationProvider.get();
            // Set the address of the readFlashOperation
            byte[] result = context.run(readFlashOperation, progressManager.createSub(0.1f));
            // Do something with the result
        }
        return null;
    }
}

In this example the Provider returns always a new instance when calling the get() method. This should be always done instead of reusing existing operations.

Creating a Connection

The drivers are intended to work with external devices. For abstracting the connection via e.g. the serial port or sockets the Connection interface can be used. Currently the wsn-device-drivers are optimized for the usage of serial port connections. Please review the SimpleSerialPortConnection to get an idea of how creating new types of connections.

Working with the SerialPortConnection

Entering the Programming Mode