-
Notifications
You must be signed in to change notification settings - Fork 4
Driver Development Guide
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 theOperationRunnablethat manages the lifecycle and providing an execution context. -
OperationExecutor: A executor that serialize the execution of all incomingOperations. The common used implementation is theExecutorServiceOperationExecutor
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:
- Method call on the
Deviceinstance. - An instance of the
OperationRunnableis created and all parameters are set which are needed by the runnable. - An instance of the
Operationis created and the the previous instantiatedOperationRunnableis injected. - The created
Operationcontainer is submitted to theOperationExecutor. - The
OperationExecutoradd the operation to a queue and returns anOperationFuturefor thisOperation. This future allows to control theOperation. - When no other
Operationis currently running pop the nextOperationfrom the Queue.
When the Operation is submitted to the OperationExecutor it can have the following states:
- Waiting: The
Operationwas added to the queue and is now waiting for execution. This is the start state. - Running: The
Operationis now executed in a thread. During the change to this state theonExecutemethod is called on the givenOperationCallback. - Done: The
Operationhas finished successfully. During the change to this state theonSuccessmethod is called on the givenOperationCallback. - Canceled: The
Operationwas canceled by the application. During the change to this state theonCancelmethod is called on the givenOperationCallback. - Timed out: The
Operationreached the timeout defined during the method call onDevice. During the change to this state theonFailuremethod is called on the givenOperationCallbackwith aTimeoutExceptionas parameter. - Excepted: The
Operationthrew anException. During the change to this state theonFailuremethod is called on the givenOperationCallbackwith 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.
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.
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.
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>
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:
ProgramOperationReadMacOperationWriteMacOperationReadFlashOperationWriteFlashOperationEraseFlashOperationResetOperationSendOperation
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:
AbstractProgramOperationAbstractReadFlashOperationAbstractSendOperationAbstractWriteFlashOperationAbstractWriteMacAddressOperation
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.
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.
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.
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.