To make things more concrete a reference project has been provided here. We will go over the parts that are pertinent to the development and integration of adapters. The whole project can be found here. This project demonstrates the most common reason for creating an adapter, which is communicating with hardware. Consequently this project has an assembly and a board to abstract the hardware in Java. This project will make use of two Ifaces, the board Iface, and a custom Iface that has been implemented.
Custom Iface
We’ll first start with the defining the Iface and then it can be implemented on both the Java side and the adapter side. For this Iface we’ll define 4 APIs to show off the variety of ways that the Iface can be used.
API Definition
| API Name | API Number | Description | Example |
|---|---|---|---|
| Java Send | 0 | Java will send a BLINK message with data to the adapter but not receive any data back from the adapter. | Streaming configurations for LEDs, which LEDs are on showing what color at what brightness. There’s no error states with LED’s they’re either on or off, so Java doesn’t need a response just to send the requested state. |
| Java Send and receive | 1 | Java sends a BLINK message and expects data to be returned. | Requesting a payment terminal to charge a specific amount. Java would want to receive the status of the request and handle failure or success appropriately. |
| Java send struct | 2 | Java sends a BLINK message with structured data that can be converted to a C struct. For this API Java will also expect data to be returned. | Configuring the state of the board. Java would send many pieces of data that the adapter can use for configuration and the adapter can return the status of the configuration. |
| Adapter send | 0 | The adapter sends an BLINK message to Java. | A door switch detects the door opened and would send that message to java. |
These are the different cases of BLINK messages Java can send a message with or without data, it can wait for a response and retrieve data from the response. Or the adapter can send an BLINK message to Java with or without data.
Notice that the API number 0 has been reused. When Java sends a BLINK message with API number 0 only the adapter will receive that message, so Java can safely listen for messages from the adapter sent over API 0 without conflict.
The name for this Iface is arbitrary, but it must match up on both the adapter and Java side to establish a connection. For this example it will be kondra.exampleIface.
Iface Implementation in an Adapter
Starting with the native C side, a minimal adapter that creates a BLINK client and processes BLINK connections can be created like so. From here we can build out the full implementation.
#include <kos/kos.h>
#include <stdlib.h>
// Blink protocol name
#define BLINK_NAME "kondra.exampleIface"
// Verisign number
#define VERSION 1
static blinkClient *client; // blink client
int main(int argc, char *argv[]) {
// create a client to connect to a SockEndpoint running
// in a JVM on the local machine... override via properties
client = blinkCreate(BLINK_NAME, VERSION, NULL, NULL, 0, 0);
// process connections from Java indefinitely
blinkDispatch(client);
}This program makes use of the KOS C library, that can be found on a KOS device by selecting the developer tools options. The developer tools option is only necessary when using a KOS device for active development, and should be unchecked otherwise. Since KOS is an operating system it is expected that any native programs that are developed to run on KOS will be built on KOS, as it guarantees compatibility of the binaries. For more information on developing native programs for KOS check out this article Native Development for KOS.
From the KOS library we’ll make use of many different functions the first being blinkCreate() , which creates a blink client struct used for communicating to Java.
/*
* name - name of the core iface
* rev - version of the iface
* server - URL to blink server
* prop - properties to override defaults
* inBufSize - size of buffer for incoming messages
* outBufSize - size of buffer for outgoing messages
*/
blinkClient* blinkCreate(char *name, int rev, char *server,
struct properties *prop, uint32_t inBufSize, uint32_t outBufSize)
Looking at minimal implementation we see that we pass the name of the Iface we defined as the first argument to the function declaring it the core Iface. Each BLINK connection has a core Iface, or the core reason for this BLINK connection. The second argument, the version number, can be used for backwards compatibility. The third argument in the function is the server where you can specify the URL of the blink server you want to connect to, useful in multinode situations. For most cases you just want to connect to the JVM running on the same machine so passing NULL is fine for the third argument. The fourth argument is a properties struct where you can specify properties to override defaults. The last two arguments are the size of the input buffer and output buffer, which can be overridden as needed, but default to 8192.
The second function used from the KOS library is the blinkDispatch() method which takes in a blinkClient struct, this function will continuously attempt to connect to Java as opposed to just ending the program whenever Java disconnects.
Registering call backs for BLINK APIs
To register callbacks for BLINK API’s a blinkIface struct is required. The reference to the core iface’s struct can be obtained from the blinkClient like so.
blinkIface *exampleIface = client->coreIface;When additional Ifaces are needed, they should be obtained via the blinkRegisterInterface() method.
/* client - the blink client
* IfaceName - the unique interface name
* maxApiNum - max api number that will be used or zero for unknown
* userData - user data for the interface
*/
blinkIface* blinkRegisterInterface(blinkClient *client,
char *IfaceName, uint8_t maxApiNum, void *userData)Now that we have our Iface struct we can register callbacks for the different API’s that we’ve defined using the blinkRegisterApi() function which takes in as arguments a blinkIface struct, the API number were registering against, the blinkApiHandler function to be called when the BLINK message is received, and then a void pointer to pass any additional data that may be needed for the the handler. We can update our adapter like so.
// Java Api numbers
#define API_JAVA_SEND 0 // Java sends a command decode input
#define API_JAVA_SEND_AND_RECEIVE 1 // Java sends a command and receives a response
#define API_JAVA_SEND_STRUCT 2 // cast received data to a struct
int main(int argc, char *argv[]) {
//...
// register the handler to API number and pass the user data
blinkRegisterApi(exampleIface, API_JAVA_SEND, processJavaSendAPI, NULL);
blinkRegisterApi(exampleIface, API_JAVA_SEND_AND_RECEIVE, processJavaSendAndRecvAPI, NULL);
blinkRegisterApi(exampleIface, API_JAVA_SEND_STRUCT, processJavaSendStructAPI, NULL);
//...Now let’s look into implementing the callback functions. These functions take in a single argument, a blinkApiArg struct. This struct is used to decode data from the BLINK message and encode data for the response message if the API was defined that way. Both the KOS Java sdk and the KOS C library have helper functions to encode and decode BLINK messages. To make this concrete let’s look at an implementation of the "Java Send" API we’ve defined for our Iface.
static int processJavaSendAPI(blinkApiArg *arg) {
int isFullReset = decodeBool(&arg->dec);
kosLog("Received message from Java performing full reset: %s ",
isFullReset ? "true" : "false");
return 0;
}A couple things
We’re using the decodeBool() function on a pointer to a decoder struct. This is decoding the first byte in the BLINK message data buffer. There are many different helper functions available to decode all of the standard data types.
Another function from the KOS c library is kosLog() this will by default output logs to stdout which can be useful for debugging. But to have the logs show up in the standard KOS location the kosLogToSyslog() function needs to be called. This way KOS will process the logs and they’ll be accessible through the “Log Viewer” tool in KOS Studio or at /mnt/datafs/logs/live/syslog* on the running device.
The value that the function returns is 0. This value is the size of the data in the response message. But since, for this API, Java isn’t expecting a response we can safely return 0.
Now let’s look into encoding data into the response message, with an implementation of the second API
static int processJavaSendAndRecvAPI(blinkApiArg *arg) {
double xCoord = drand48() * 1000;
encodeDouble(&arg->enc, xCoord);
double yCoord = drand48() * 1000;
encodeDouble(&arg->enc, yCoord);
double zCoord = drand48() * 1000;
encodeDouble(&arg->enc, zCoord);
kosLog("Were currently at (x,y,z) (%f,%f,%f)", xCoord, yCoord, zCoord);
return encoderSize(&arg->enc);
}Here we see the use of one of the encode helper functions, in this case encoding 3 doubles to the data buffer that will be sent in the response message that the Java side can consume. The other thing to note is that the return value of the function is returned from a call to another helper function encoderSize() built for this very use case where multiple pieces of data are placed into the response buffer.
This encoding and decoding works well for a few data points but BLINK really shines in the ability to cast the data received into a C struct greatly enhancing the developer experience. An example of this can be seen below where we define a new struct and cast the BLINK data buffer pointer into a pointer of our new type. When dealing with structs beware of any padding that the compiler may add, as this may cause issues.
typedef struct NavigationModuleDestination {
// coordinates of destination
double xCoordinate;
double yCoordinate;
double zCoordinate;
// name of destination
char name[100];
} NavigationModuleDestination;
static int processJavaSendStructAPI(blinkApiArg *arg) {
NavigationModuleDestination *destinationPtr =
(NavigationModuleDestination *)arg->inBuf;
char *name = destinationPtr->name;
double x = destinationPtr->xCoordinate;
double y = destinationPtr->yCoordinate;
double z = destinationPtr->zCoordinate;
int isConfigurationSucessful = rand() % 2;
if (isConfigurationSucessful) {
kosLog("We're going to %s which is located at (x,y,z) (%f,%f,%f)", name, x,
y, z);
} else {
kosLog("Failed to configure new destination %s at (x,y,z) (%f,%f,%f)", name, x,
y, z);
}
encodeBool(&arg->enc, isConfigurationSucessful);
return encoderSize(&arg->enc);
}Sending events from the adapter
We’ve explored the features around receiving and dealing with a BLINK message that was sent from Java to native code, but what if the adapter is interfacing with hardware and needs to surface an event to Java? This can be done with the blinkSendMsg() function. Here’s an example that sends an event every 20 seconds.
The blinkSendMsg() function takes 4 arguments, the Iface which the message is for, the API for that Iface, data, and the size of the data that is being sent with the message.
Implementing the Board Iface
One of the core tenets of KOS is the logical abstraction of hardware, which is done through boards and assemblies on the Java side. Since an adapter is often the connection to a piece of hardware, the status of a boards connectivity is the status of the BLINK connection. So if the BLINK connection is no longer connected the board will become unlinked on the Java side, and can be dealt with accordingly at that level of abstraction.
The convention in KOS is that adapters are written to run indefinitely, the Java side determines when to start the adapter and when to kill the adapter. When an adapter is started KOS will restart it if the process is ever terminated. So when it comes to a boards connection status it is linked to whether the adapter is running.
Since USB is often used to connect hardware to the KOS node, KOS has standard methods to start adapters when a USB is inserted, and KOS will kill the adapter when that USB is removed greatly reducing the amount of code required to manage hardware connections.
To link an adapter to the board object in Java the BLINK connection must include the board Iface. The board Iface has a simple api that retrieves data about the board. This makes adding the board Iface to a blink connection simple, get all of the data about the board and put it into an boardIfaceData struct and call blinkAddBoardIface() which takes in the blinkClient and a pointer to the boardIfaceData struct. Now the adapter is linked to a board, below is an example of this process.
#include <kos/board.h>
#include <kos/kos.h>
#include <stdlib.h>
#include <string.h>
#define BOARD_TYPE "kondraExample"
int main(int argc, char *argv[]) {
//....
struct boardIfaceData boardData; // board data for linking to Java
// zero the memory in boardData
memset(&boardData, 0, sizeof(boardData));
// initialize the board data
boardData.type = BOARD_TYPE;
// additional fields used for matching and metadata but unnecessary for this example
// boardData.identity = "kondraIdentity";
// boardData.instanceId = "1";
// boardData.mfgSerialNum = "123984f8as7vs897f";
// register the board Iface
blinkAddBoardIface(client, &boardData);
//...
}With this we have completed our adapter, we still need to compile it and put it into a KAB. This tutorial Native Development For KOS describes how you can do so, along with information on how to setup build and publishing automaton for your native artifacts.
In part 2 we’ll look at how to integrate this adapter with our custom iface into a KOS app in java.
