Adapters, Blink, & Ifaces Reference Project Part 2

Integration with Java

To integrate the adapter in Java there are a couple things that your KOS application will have to do.

  • Establish Iface connections and handle them according the Iface contract.
  • Make the adapter binary available to system by mounting the kab on disk.
  • Starting the adapter binary and passing in program arguments as required

Establishing Iface connections

For a KOS application to interact with an Iface, it needs to define a representation of the Iface in Java, and a way to interact with it and dynamically create an instance when a blink connection with that Iface is established. To define the Iface KOS has the BinaryMsgIface class, which contains the methods that sends and receives messages sent over the blink connection.

Creating the Iface

Implementing an Iface on the Java side is simple, BinaryMsgIface provides methods to interact with the API’s defined by the Iface, as well as handle incoming messages. Below is the minimum required to extend the BinaryMsgIface class. All that is required is to implement the constructor. Here constants that define the Iface’s API’s have also been included.

public class ExampleIface extends BinaryMsgIface {
    private static final String IFACE_NAME = "kondra.exampleIface";
    // Java Api numbers
    private static final int API_JAVA_SEND              = 0; // Java sends a message
    private static final int API_JAVA_SEND_AND_RECEIVE  = 1; // Java sends a message and receives a response
    private static final int API_JAVA_SEND_STRUCT       = 2; // Java sends structured data
    // Native Api numbers
    private static final int API_NATIVE_SEND            = 0; // native sends an event to Java
    public ExampleIface(BinaryMsgSession session, IfaceClient<ExampleIface> IfaceClient ) {
        super(IFACE_NAME, session, IfaceClient, null);
    }

The super constructor of BinaryMsgIface takes four arguments.

  • The name of the Iface, used to establish the Iface connection within the BLINK connection
  • BinaryMsgSession which is an abstraction of the BLINK network connection.
  • IfaceClient is a wrapper around the BinaryMsgIface that provides many helper functions to safely call methods on the BinaryMsgIface and handle null references and exceptions.
  • BinaryMsgIfaceListener is listener for connect and disconnect events of the Iface.

The BinaryMsgIface class provides the necessary methods to transmit and receive BLINK messages. To send a message over BLINK, an instance of BinaryMsg is created, this object manages the payload buffer for a blink message, it has methods to write and read data from the buffer. When the BinaryMsg is ready it can be sent via the send() method or if expecting a response with the sendAndRecv(). Below are two methods, one is an example of an Iface method that sends a BLINK message with a boolean encoded in the BLINK message. The other shows how a BLINK response can be received and data retrieved from it.

public void resetModule(boolean isHardReset) throws IOException {
      BinaryMsg msg = msg(API_JAVA_SEND);
      msg.writeBoolean(isHardReset);
      send(msg);
 }
public double[] getCurrentCoordinates() throws IOException {
    double[] coords = new double[3];
    BinaryMsg request = msg(API_JAVA_SEND_AND_RECEIVE);
    BinaryMsg response = sendAndRecv(request);
    coords[0] = response.readDouble();
    coords[1] = response.readDouble();
    coords[2] = response.readDouble();
    return coords;
}

Receiving BLINK messages

To handle BLINK messages coming from the adapter a listener needs to be registered for the specific API number that one is listening for. This is done by calling the addRequestHandler method and registering a callback, like the example below.

// Register listeners for events from adapter
this.addRequestHandler(API_NATIVE_SEND, res -> {
    int severity = res.readInt();
    log.info("Recieved event from adapter with severity {}", severity);
});

Creating instances of BinaryMsgIface

There are a number of ways to create an instance of a BinaryMsgIface given a blink connection. Fundamentally an instance of BinaryMsgIface is expected to be created by a class that implements the BinaryMsgIfaceFactory interface. For KOS to get access to an instance of the Iface factory it needs to be added to the context. When KOS receives a blink connection request it will attempt to create and connect the Iface when it receives a connection request, with the IfaceFactories available in the context.

Typically Ifaces are not standalone and are used within a service or as an interface to a board. KOS has IfaceAware interfaces, that implement BinaryMsgIfaceFactory for these patterns. For services there is the IfaceAwareNodeService and IfaceAwareService. The IfaceAwareNodeService can be used for interacting with an adapter on one or more nodes in a KOS device, it provides additional methods for caching node specific data. The IfaceAwareService does not have the additional caching methods and is not recommended to be used in multi-node configurations.

Following the KOS hardware abstraction model a Board class will contain functionality related to the physical board it represents. As mentioned before, KOS defines an iface for boards, the Board class includes the necessary information to link a board to the adapter that implments its iface. KOS will raise the BoardNotLinked trouble until that boards Iface connects. For cases where the board is not communicating to hardware via an adapter, a Board should implement the SelfLinkingBoard interface and call the selfLink() and selfUnlink() methods at appropriate times.

To facilitate other Iface connections a board can implement the IfaceAwareBoard interface, which provides the onLinkSession() method which is called when the board Iface connection is established. This method is the place to create instances of the other Iface that a board may use when interacting with an adapter. Below is an example implementation of a board, showing how the Iface is linked and used with a IfaceClient.

public class ExampleBoard extends Board implements IfaceAwareBoard {
    private static final String BOARD_TYPE = "kondraExample";
    private IfaceClient<ExampleIface> IfaceClient;
    public ExampleBoard(Assembly assembly) {
        super(assembly, BOARD_TYPE);
        IfaceClient = new IfaceClient<>();
    }
    public double[] getCurrentCoordinates() {
        return IfaceClient.from(iface -> iface.getCurrentCoordinates());
    }
    public void resetModule(boolean isHardReset) {
        IfaceClient.withCatch(iface -> iface.resetModule(isHardReset));
    }
    @Override
    public String getType() {
      // Must match board type specified on the adapter side
        return BOARD_TYPE;
    }
    @Override
    public String getInstanceId() {
      // In this case returning null since there is only one instance
      // of this board in this example.
      // If there are multiple instances of a board in a device they
      // are differentiated by instanceId
        return null;
    }
    @Override
    public void onLinkSession(BinaryMsgSession session) {
        new ExampleIface(session, IfaceClient);
    }
}

Why IfaceClient?

The IfaceClient abstraction was created to remove boilerplate code around null checks and handling of exceptions. Thus IfaceClient has methods for handling the different scenarios, there are two basic methods from() and with(), and variations with additional arguments for handling exceptions and in case the Iface is null. The from() method is used to receive a value from the Iface and with() is for safely using the Iface, without triggering a null pointer exception.

Starting an adapter

At this point we have created an adapter that uses our custom Iface, and have integrated that Iface into our KOS application. Following Native Development for KOS we have packaged our adapter binary into a KAB. The last missing piece is to run the adapter binary on our KOS device. We have to mount the binary into the filesystem and then start it.

Mounting files on disk

To preserve immutability KOS runs in a read only file system. To make the contents of a KAB available to the rest of the system KOS provides FuseService to mount and unmount KABs to the local filesystem. This is how we can take the KAB with our adapter and add it to the local filesystem.

@Autowired
    private FuseService fuseService;
    @Override
    public void load() throws Exception {
        // Get the adapter kab from the same section as this app.
        KabFile adapterKab = getKabByType("kos.adapter");
        if (adapterKab != null) {
            FuseMount mount = fuseService.mount(adapterKab);
            log.info("Adapter has been mounted at {}", mount.getRootDir());
        } else {
            log.error("No adapter found");
        }
    }

Starting the adapter

Adapters can be started explicitly using SpawnService or by using a SerialAdapterFactory. Both methods require the use of an Adapter object. This object specifies the different configurations for running the program, such as specifying program arguments, the working directory, setting the group the process should run as, see more in the Javadocs. Below is a minimal implementation.

public class ExampleAdapter extends Adapter {
    private static final String ADAPTER_NAME = "exampleAdapter";
    public ExampleAdapter(File baseDir) {
        super(ADAPTER_NAME);
        setBasePath(baseDir);
    }
}

Once an adapter is started KOS will ensure it continuously runs, so if the program exists or is killed, KOS will restart it. To start an adapter using the SpawnService the addProcess() method is called with an Adapter instance as an argument, like so.

spawnService.addProcess(adapter);

Alternatively the adapter can be started by a SerialAdapterFactory. This class provides a method to match serial devices, allowing the app to start the adapter when the hardware is attached to the KOS device. The method provides an instance of a SerialDevice object which is used to identify the device. Like in the example below it provides the vendor Id and product Id of the device, but provides additional methods for interacting with serial device directly permitting the application to probe the serial device to confirm it’s identity, for situations where the VID/PID may not be robust enough.

public class ArduinoSerialAdapterFactory extends SerialAdapterFactory {
    private static final int VENDOR_ID = 0x10c4;
    private static final int PRODUCT_ID = 0xea60;
    private static final String ADAPTER_NAME = "ArduinoAdapter";
    // base dir of the adapter binary
    private File baseDir;
    public ArduinoSerialAdapterFactory(File baseDir) {
        this.baseDir = baseDir;
    }
    @Override
    public Adapter matchDevice(SerialDevice device) {
        if ((device.getVendorId() == VENDOR_ID) && (device.getProductId() == PRODUCT_ID)) {
            // build the adapter
            Adapter adapter = new Adapter(ADAPTER_NAME);
            adapter.setBasePath(baseDir);
            adapter.addArg("-v");
            adapter.addArg("-d");
            adapter.addArg(device.getDevname());
            return adapter;
        }
        return null;
    }
}

As alluded to in the code above Arduinos are a common USB device used for development, so common in fact that KOS has added first class support when it comes to interacting with them. An example of how they can be integrated into KOS can be found in the Thermostat Tutorial in the last section Hardware Integration.