Nabto Edge Integration

This guide describes how to integrate the Nabto Edge Embedded SDK on a new platform, ie one that is not supported per default. If using one of the default supported platforms, no integration work is necessary to do. The set of officially supported platforms is currently slightly limited (only Linux based embedded systems are supported, ESP32 is on its way but not yet officially released).

Also see the step-by-step walk-through of a full integration with the help of a test scaffold to test each step.

Overall Architecture

Nabto Edge needs to know about the underlying platform it is running on. The way to “inform” Nabto Edge about this platform is to implement a list of functions and supply Nabto Edge with these functions. The functions are defined in .h files, some implementations are provided as function pointers setup at runtime, some are linked into the target as outlined in the following.

The integration consist of three types:

  1. Lifecycle functions src/api/nabto_device_platform.h, these functions are only used to bootstrap and tear down the integration interfaces.
  2. Functions linked into the target that the Nabto Edge platform uses at runtime (defined in /api/nabto_device_threads.h and /api/nabto_device_logging.h - the two leftmost orange boxes in the figure).
  3. Functions and optional user data setup via structs and initialized and torn down by the lifecycle functions (the large right box to the right on the figure). They are used by the platform at runtime to interact with the underlying operating system (and/or hardware). These functions/interfaces can be found in the src/platform/interfaces folder.

For Nabto Edge to run on a specific target, it needs to know about:

  1. DNS - how the system resolves hostnames to ip addresses (both ipv4:A and ipv6:AAAA addresses)
  2. Timestamp - interface for Nabto Edge to know about the current time for scheduling events
  3. Event Queue - put events on a queue for serialized (under mutex) execution, which minimizes/optimizes callstacks.
  4. TCP - specify TCP operations on the specific target
  5. UDP - specify UDP operations on the specific target
  6. Local ip - specify how to find the local-ip address(es) of the device (ie. which IP does the target have on the local network)
  7. mDNS - specify/setup mDNS interface for local discovery

Components Needed for a Custom Platform

First of all, the Nabto Edge Embedded SDK implementation files need to be included in the development tool/IDE of the new platform.

The specific needed list of files can be seen in nabto_files.cmake which also could be used for IDEs capable of using cmake.

Once this is done, the Nabto Edge system needs to be supplied with knowledge of the platform/hardware it is running on. 3 major files need to be examined for this:

api/nabto_device_platform.h

This file contains 3 functions: an init, a deinit and a stop function. That is, functions needed for bootstrap and teardown of the system. These functions are called when a device is created, destroyed and stopped. The init function should call the appropriate setter functions to setup the integration modules for the platform.

api/nabto_device_integration.h

The purpose of these functions is to be called from the nabto_device_platform_init function to setup the module struct of src/platform/interfaces/*.h (described later) via appropriate setter functions and to provide the overall functionality which is required to run on a specific platform.

The sequence illustrated in the following figure helps understanding the relation between the initialization of a device and the platform integration:

The call sequence and initialization of the integration modules will start when the main program initializes a new NabtoDevice, something like:

NabtoDevice* device = nabto_device_new();

This will at some point call the initialization of the integration interface (nabto_devcie_platform_init) which has the responsibillity to setup the different integration modules (tcp, udp, mdns, timestamp etc.) via calling the appropriate nabto_device_interation_set_<modulename>_impl().

When setting up the integration modules, the integrator will probably need to allocate different types of resources. These resources will need to be deallocated later on when/if the Nabto platform is stopped.

This can be accomplished by setting a pointer to the user specified data via the nabto_device_integration_set_platform_data and nabto_device_integration_get_platform_data functions which are reachable inside both the nabto_device_platform_init, nabto_device_platform_init and nabto_devcie_platform_stop function. This way a pointer to the data can be created and stored in init and deallocated in deinit and stop.

If the integration is sure that only one instance of the nabto device is started on a specific device (via nabto_device_new()), this user specified data could reside in a static single allocated location (and there will be no need for either the nabto_device_integration_set_platform_data or nabto_device_integration_get_platform_data). For the general case multiple devices could run inside the same environment and memory, so the functions are supplied.

The following figure illustrates the full life cycle and interaction between the application that creates a device context that triggers the Nabto Edge core to initialize the platform integration module:

api/nabto_device_threads.h

The api nabto/nabto_device.h is a thread safe API, which also exposes functionality which can block the system. The system currently also need to have a thread implementation. The thread abstraction defines threads, mutexes and condition variables.

See the header file for more information or take a look at the existing implementations in the src/modules/threads folder.

Currently, for Nabto Edge to run, the system needs a threads implementation, condition variables and mutexes. It is planned (the platform is made ready for) that in future versions the platform can be run onto a single-thread platform. This is the reason why integrations dependent on system calls are asynchronous / nonblocking as explained later.

Threads

The integration needs to supply the following function linked onto the platform:

struct nabto_device_thread* nabto_device_threads_create_thread(void);
void nabto_device_threads_free_thread(struct nabto_device_thread* thread);
void nabto_device_threads_join(struct nabto_device_thread* thread);
np_error_code nabto_device_threads_run(struct nabto_device_thread* thread,
                                       void *(*run_routine) (void *), void* data);

The above functions are assumed to be well-known by the integrator - if not the case, it would be a good idea to explore pthreads interface on Linux.

  • nabto_device_threads_create_thread : Shall allocate the needed resources for a new thread
  • nabto_device_threads_free_thread : Shall deallocate the resources allocated in the create function
  • nabto_device_threads_join : Shall make the current caller thread join the to the function given thread
  • nabto_device_threads_run : Shall start the given thread on the given function with the given data

Condition variables

The Nabto platform is dependent on condition variables for synchronization between threads. The functions needed at link time is:

struct nabto_device_condition* nabto_device_threads_create_condition(void);
void nabto_device_threads_free_cond(struct nabto_device_condition* cond);
void nabto_device_threads_cond_signal(struct nabto_device_condition* cond);
void nabto_device_threads_cond_wait(struct nabto_device_condition* cond,
                                    struct nabto_device_mutex* mut);
void nabto_device_threads_cond_timed_wait(struct nabto_device_condition* cond,
                                          struct nabto_device_mutex* mut,
                                          uint32_t ms);

The implementation should follow the pthread semantics of the similar functions.

Mutexes

The Nabto platform needs a mutex abstraction to synchronize access to shared memory and variables. The functions provided by the integration and needed at link time are:

struct nabto_device_mutex* nabto_device_threads_create_mutex(void);
void nabto_device_threads_free_mutex(struct nabto_device_mutex* mutex);
void nabto_device_threads_mutex_lock(struct nabto_device_mutex* mutex);
void nabto_device_threads_mutex_unlock(struct nabto_device_mutex* mutex);

Just like the condition abstraction, the functions should follow the same semantics as the pthreads mutex abstraction.

Example integration

In the directory platform_integration_example a full example integration can be viewed. This example works on UNIX systems so modules which works on such a system have been choosen.

Also, 3 different integration modules of different complexities are walked through in this guide, start with the simple integration example.