embeNET Node User Guide

Table of contents Table of contents

This is the embeNET Node User Guide. You can read through the following topics:

Networking basics

The following sections give brief information about the embeNET networking. This is just an overview. Most topics are covered in depth in the following chapters.

Overview of the network

The embeNET network is a wireless mesh network capable of providing communication between hundreds of nodes. It uses a special TSCH (Time Slotted Channel Hopping) scheme for accessing the radio channels, which greatly reduces idle listening and packet collisions even in dense networks. The network works with IEEE 802.15.4 compatible radio transceivers. The network is synchronized meaning that all nodes that joined the same network have a common sense of time, which is very precise and allows the nodes to schedule radio transmission and reception in exact moments.

Border router and nodes

The embeNET network is started by a special device called border router. Other nodes need to join the network started by the border router in order to be able to communicate. The border router may accept or reject a node based on the specifically configured joining rules.

The network joining process

In order to join a network the node must know the pre-configured network key (K1). This key is shared across all devices within a network and typically can be the same for all devices used in an organization. It is a utility measure and network security is not based on this key. The second key is a pre-shared key (PSK) which is associated with the particular node. It is advised but not mandatory for each node to have a unique pre-shared key.

The joining of node to the network is a multi-step process. Firstly the node synchronizes to the network, meaning that it can follow the scheduled slots in which the communication with other nodes takes place. It selects a node (called pledge) through which it wishes to join. Next it sends credentials including own UID and PSK to the border router, which accepts the node or denies access. Once the node is accepted it can communicate with any other node in the network.

IPv6 and multicast

The embeNET network uses the IPv6 protocol for addressing the nodes. It supports multicast addressing mode so that devices can be grouped and the data can be sent to all devices within a group simultaneously. Each device in the network is identified by a 64-bit unique identifier (UID) which is an EUI64 address. In most cases this number comes from the hardware platform the stack works on and should not be altered by the application code. In addition each network started by the border router is identified by a 64-bit network prefix. These two values - the network prefix and the UID are used to form a unicast IPv6 address of the node. A multicast address is being formed by the network prefix and group identifier.

UPD and sockets

The embeNET network uses UDP protocol for transporting data between the nodes. The UDP datagrams are handled by sockets which are registered in the stack.

Events and time

The network is synchronized meaning that all nodes that joined the same network have a common sense of time, which is very precise. Thanks to this, the embeNET Node library allows to schedule application-defined events that can be triggered nearly simultaneously in multiple networked nodes. The embeNET Node library allows also to schedule events in local node time, which is not synchronized, but in turn work even if the node is not joined to any network.

Stack architecture

General information

The embeNET Node library implements a stack of wireless communication protocols that allow the device that runs the stack to communicate in a wireless mesh network. From the user perspective it is a library that provides several interfaces, that the user application (and other middleware - such as network services) can use. However, due to the fact that the library is also portable across multiple hardware platforms, it also relies on some required interfaces. Those need to be implemented in either the 'port' or 'bsp'. Here, we will only focus on the provided interfaces - see embeNET Node Porting Guide for the description of the required interfaces and how to implement them on a new or custom hardware.

The embeNET Node library utilizes event-driven style of programming. Most actions taken by the node last for a relatively long time before results are observed. Thus many function calls take callbacks as arguments.

Interfaces

The embeNET Node library provides the following interfaces:

Node interface

The embeNET Node API provides a set of functions that allow the stack to initialize and run in the node. It also allows the node to join a specific network that is typically started by a special device called border router. The node can also join several groups which are then addressable through multicast addressing. This interface also allows to schedule user defined tasks that are run in an event-driven fashion in the node.

UDP interface

The embeNET UDP C API provides a set of functions that allow to register network sockets, through which all user communication is carried out. The protocol of choice here is UDP. See embeNET UDP C API for the description of functions and Using UDP sockets for the information on how to use them.

Stack handling

Basic stack handling

The embeNET Node follows a simple init-proc-deinit rule. Before using the stack you need to initialize the library through a call to EMBENET_NODE_Init. Once the stack is initialized it is expected that the EMBENET_NODE_Proc is called periodically - usually within the main program loop. When (and if) the application is done with the stack it may call EMBENET_NODE_Deinit to deinitialize it.

The following example illustrates the idea:

#include "embenet_node.h" // embeNET Node API
// Nodes main function
int main(void)
{
// Initialize embeNET Node
while (1) {
// Process the embeNET Node stack
}
// Deinitialize the stack (if needed)
return 0;
}
embeNET Node API
EMBENET_Result EMBENET_NODE_Init(const EMBENET_NODE_EventHandlers *eventHandlers)
Initializes the embeNET networking stack for node.
void EMBENET_NODE_Deinit(void)
Deinitializes the embeNET networking stack.
void EMBENET_NODE_Proc(void)
Runs the networking process of the embeNET stack for node.

Once the stack is initialized the following elements of the API are available:

  • network management (see Network handling)
  • group management (see Group handling)
  • task management (see Task handling)
  • utility functions, however some of them may not return valid results until the node joins the network (consult their description)

Networking callbacks

When networking begins, the stack generates important events that can and should be handled by the application code. These events are handled through callback handlers that are passed the the EMBENET_NODE_Init function. This function accepts a structure that gathers all the handlers which are later called by the stack on specific events. See Network handling for the description of the events and the callbacks.

Exception handling

There is just one more thing to consider. The embeNET Node stack uses EXPECT error handling utility for handling input validation and unrecoverable errors. Whenever the stack detects an really faulty condition it calls the EXPECT_OnAbortHandler which must be defined in the user code. The program must not continue operation after calling this function. A typical behavior of such function is to:

  • go to a safe state
  • log the error
  • halt or reset the application

Below is the empty implementation of such function that you can copy, paste and extend.

void EXPECT_OnAbortHandler(char const* why, char const* file, int line) {
// TODO
while (1) { ; }
}
EXPECT_INTERNAL_NORETURN void EXPECT_OnAbortHandler(char const *why, char const *file, int line)

Network handling

Once the embeNET Node is initialized through a call to EMBENET_NODE_Init it is possible to start networking. The following sections will guide you through the topics concerning networking.

Network configuration

In order to join a network the node should provide three things:

  • UID - This is the unique node identifier, also called EUI64. This should be unique across the whole device inventory as it identifies the actual piece of hardware the stack runs on. Typically this address should be provided by the hardware and the stack user should not alter it. The application code may get this UID through a call to EMBENET_NODE_GetUID. In rare cases the application may want to change the UID. This can be done through a call to EMBENET_NODE_SetUID, however it is strongly advised not to do so.
  • network key (K1) - This is a common 128-bit key shared across all the nodes within a single network. This key is not used for security and must be the same in all the nodes wishing to communicate, including the border router.
  • pre-shared-key (PSK) - This is a device-specific 128-bit key. Depending on the join rule strategy applied in the border router this key can be used to authorize the node in the border router. It is recommended that each node in the network has a different PSK key, as it allows the border router to apply more selective joining process of nodes. The PSK key can be treated as a device signature and should be kept secret.

Join rules in border router

At this point it is worth to mention how network joining rules work in the border router. The border router may apply one of these rules:

  • every node may join the network - this is useful for development but not for real deployments, due to security concerns
  • only nodes with matching UIDs may join - this rule only considers UIDs of the nodes and provides very limited security, as the UIDs are visible to the outside world
  • only nodes with matching PSK may join - this rule only considers pre-shared-keys so that only nodes with matching PSKs can join, which gives reasonable security as PSK should be kept secret
  • only nodes with matching pairs of UID+PSK can join - this is the most secure node authorization strategy, but requires most preparation in the border router.

As a result, employing different PSKs in nodes gives most options in deploying various authorization strategies in the border router.

Joining the network

In order to join the network the application should call EMBENET_NODE_Join after the stack is initialized. The following example illustrates the idea:

#include "embenet_node.h" // embeNET Node API
#include <stdio.h>
#include <string.h>
// Network key (K1):
uint8_t k1[16] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee};
// Pre-shared key (PSK):
uint8_t psk[16] = {0x1b, 0xac, 0xe4, 0x43, 0x41, 0xfd, 0x31, 0x70, 0xab, 0xef, 0xa1, 0x72, 0x99, 0xe1, 0xf3};
// Event hander called when node joins network.
static void onJoined(EMBENET_PANID panId, const EMBENET_NODE_QuickJoinCredentials* quickJoinCredentials) {
(void)quickJoinCredentials;
(void)panId;
puts("This node has joined the network");
}
// Nodes main function
int main(void) {
// Prepare event handlers structure and clear all handlers
EMBENET_NODE_EventHandlers eventHandlers = {NULL};
// Set event handler that will be called once the node joins the network
eventHandlers.onJoined = onJoined;
// Initialize embeNET Node - pass event handlers
EMBENET_NODE_Init(&eventHandlers);
// Prepare configuration structure
memcpy(config.k1.val, k1, 16);
memcpy(config.psk.val, psk, 16);
// Start the joining process - pass the configuration
while (1) {
// Process the embeNET Node stack
}
// Deinitialize the stack (if needed)
return 0;
}
uint16_t EMBENET_PANID
IEEE802.15.4e PAN ID.
Definition: embenet_defs.h:55
EMBENET_Result EMBENET_NODE_Join(const EMBENET_NODE_Config *config)
Starts the network joining process as a node.
uint8_t val[16]
Stored value.
Definition: embenet_defs.h:94
Definition: embenet_node.h:85
EMBENET_PSK psk
Pre-shared key.
Definition: embenet_node.h:87
EMBENET_K1 k1
Common network key. This key must be the same for all nodes and border router joining the same networ...
Definition: embenet_node.h:86
Structure holding embeNET Node stack event handlers.
Definition: embenet_node_event_handlers.h:82
EMBENET_NODE_OnJoined onJoined
Event hander that is called when the node joins a given network.
Definition: embenet_node_event_handlers.h:84
Structure describing the data necessary to perform a quick network rejoin.
Definition: embenet_defs.h:105
uint8_t val[16]
Stored value.
Definition: embenet_defs.h:100

In the above example, there is just one callback function hooked up as an event handler - EMBENET_NODE_EventHandlers.onJoin. This function will be called when (and only if) the node joins the network with the given configuration options.

Once the EMBENET_NODE_Join is called the node enables radio reception listening to the neighboring networks. It then tries to connect to the networks that match the K1 network key. During the join process the node sends authentication data to the border router, which accepts or rejects the node. This process may take a significant amount of time, depending on the network structure and load.

If there is more than one network visible to the node, the node will sequentially try to join each one of them. For each attempt to join a network the stack will call the EMBENET_NODE_EventHandlers::onJoinAttempt event handler to indicate that a particular network is considered.

The joining process stops only when the node successfully joins to one of the available networks. The join process has no timeout. If the application needs to have such timeout then it should call EMBENET_NODE_Leave after the timeout was reached (see Task handling on how such a task can be scheduled).

When the node joins the network, the onJoinCallback function will be called from the context of the EMBENET_NODE_Proc function. This callback will receive the PAN ID identifier that identifies the radio network that the node has joined. In addition the application will receive a set of credentials (EMBENET_NODE_QuickJoinCredentials) that can be used to quickly re-join the same network. See Quick join for more details.

Once the node has joined the network it is possible to:

Quick join

In some cases joining the network can take a significant amount of time. To speed up this process a quick join technique is introduced. It is particularly useful in cases when the node goes through to reset (either intentional or unintentional) and it would be beneficial to quickly rejoin the network the node was joined to when that happened.

The quick join mechanism relies on the fact that during regular network join the node obtains a set of credentials that are negotiated with the border router. Knowing these credentials allows the node to participate in the network. These credentials (EMBENET_NODE_QuickJoinCredentials) are provided in the EMBENET_NODE_EventHandlers::onJoined event callback. The application that wishes to re-use them for quick join should store them in secure, non-volatile memory. Once this is done the next time the node joins the network it is possible to call EMBENET_NODE_QuickJoin (instead of EMBENET_NODE_Join), providing the recalled credentials.

Once the EMBENET_NODE_QuickJoin is called the node tries to join the network matching the credentials. In such a network is found, the process takes up to 1 minute. And if the join succeeded, the EMBENET_NODE_EventHandlers::onJoined event handler is called, just like during regular join.

However if the quick join fails to join, it is assumed that the provided credentials became already obsolete. This might happen for example if the whole network is restarted. In such case the EMBENET_NODE_EventHandlers::onQuickJoinCredentialsObsolete event handler is called, indicating that the stored credentials should probably be forgotten (deleted from the application memory). This however does not stop the join process. The node falls back to the regular join process without any additional user intervention and may receive EMBENET_NODE_EventHandlers::onJoined callback if the matching network becomes available.

The following code is an exemplary application of the quick join mechanism.

#include "embenet_node.h" // embeNET Node API
#include <stdio.h>
#include <string.h>
// Network key (K1):
uint8_t k1[16] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee};
// Pre-shared key (PSK):
uint8_t psk[16] = {0x1b, 0xac, 0xe4, 0x43, 0x41, 0xfd, 0x31, 0x70, 0xab, 0xef, 0xa1, 0x72, 0x99, 0xe1, 0xf3};
static void saveQuickJoinCredentials(const EMBENET_NODE_QuickJoinCredentials* quickJoinCredentials) {
(void)quickJoinCredentials;
// TODO: store quick join credentials to non-volatile memory
}
static bool loadQuickJoinCredentials(const EMBENET_NODE_QuickJoinCredentials* quickJoinCredentials) {
(void)quickJoinCredentials;
// TODO: try to read quick join credentials from non-volatile memory
return true;
}
static void clearQuickJoinCredentials(void) {
// TODO: clear the quick join credentials in the non-volatile memory
}
// Event hander called when node joins network.
static void onJoined(EMBENET_PANID panId, const EMBENET_NODE_QuickJoinCredentials* quickJoinCredentials) {
(void)panId;
saveQuickJoinCredentials(quickJoinCredentials);
puts("This node has joined the network");
}
static void onQuickJoinCredentialsObsolete(void) {
clearQuickJoinCredentials();
}
// Joins the network either by regular or quick join
static void join(void) {
EMBENET_NODE_QuickJoinCredentials quickJoinCredentials;
// Check if we have credentials
if (loadQuickJoinCredentials(&quickJoinCredentials)) {
puts("Quick joining");
// Start quick join process - pass the credentials
EMBENET_NODE_QuickJoin(&quickJoinCredentials);
} else {
puts("Regular joining");
// Prepare configuration structure
memcpy(config.k1.val, k1, 16);
memcpy(config.psk.val, psk, 16);
// Start the joining process - pass the configuration
}
}
// Nodes main function
int main(void) {
// Prepare event handlers structure
EMBENET_NODE_EventHandlers eventHandlers = {NULL};
// Set callback that will be called once the node joins the network
eventHandlers.onJoined = onJoined;
// Set callback that will be called when quick join credentials become obsolete
eventHandlers.onQuickJoinCredentialsObsolete = onQuickJoinCredentialsObsolete;
// Initialize embeNET Node - pass event handlers
EMBENET_NODE_Init(&eventHandlers);
join();
while (1) {
// Process the embeNET Node stack
}
// Deinitialize the stack (if needed)
return 0;
}
EMBENET_Result EMBENET_NODE_QuickJoin(const EMBENET_NODE_QuickJoinCredentials *quickJoinCredentials)
Starts the network joining process as a node using a previously stored EMBENET_NODE_QuickJoinCredenti...
EMBENET_NODE_OnQuickJoinCredentialsObsolete onQuickJoinCredentialsObsolete
Event hander that is called when the quick join credentials become obsolete.
Definition: embenet_node_event_handlers.h:90

In the above code, after stack initialization (EMBENET_NODE_Init) the 'join' function is called. This function checks if there are any quick join credentials stored. If so it starts the quick join mechanism and calling the EMBENET_NODE_QuickJoin function. If not a regular join (EMBENET_NODE_Join) is performed. In any case if the join is successful the EMBENET_NODE_EventHandlers::onJoined event handler is called and the new credentials are stored. However if the quick join is performed and it fails, the EMBENET_NODE_EventHandlers::onQuickJoinCredentialsObsolete is called where the stored credentials should be cleared.

Note, that in the above code the 'saveQuickJoinCredentials', 'loadQuickJoinCredentials' and 'clearQuickJoinCredentials' need to be implemented for the given hardware platform.

Leaving the network

If the application wishes to leave the network it should call EMBENET_NODE_Leave. As a result all network activity of the node will be stopped. In addition, if the node was joined to the network then all the tasks that were scheduled in network time get canceled. This call can also be used to stop the on-going joining process.

Leaving the network can also be caused by external conditions. If the node permanently looses wireless connection to other nodes it will in turn leave the network automatically.

In any case, if the node was joined and then left the network (either by a call to EMBENET_NODE_Leave or due to external circumstances causes it to leave the network then the EMBENET_NODE_EventHandlers::onLeft event handler will be called.

Event handlers

The EMBENET_NODE_EventHandlers structure gathers all the event handlers. During stack initialization this structure is passed to the EMBENET_NODE_Init function. If a particular event handler is not used it should be set to NULL.
The following table lists all the event handlers with their description:

Event handler Description
EMBENET_NODE_EventHandlers::onJoined Called when the node joins a network (see Joining the network)
EMBENET_NODE_EventHandlers::onLeft Called when the node leaves a network (see Leaving the network)
EMBENET_NODE_EventHandlers::onJoinAttempt Called when the node attempts to join a network (see Joining the network)
EMBENET_NODE_EventHandlers::onQuickJoinCredentialsObsolete Called when the credentials used for quick join become obsolete (see Quick join)
EMBENET_NODE_EventHandlers::onDataOnUnregisteredPort Called when an UDP datagram is received but no socket can handle it

Group handling

Nodes can be organized in groups. The idea is to enable multicast addressing of such a group so that UDP packets can be sent to all the members of the group simultaneously. It is the node that decides to which groups it belongs to. The border router only observes the grouping behavior of nodes and can list all the group members, but it cannot directly make a given node join or leave particular group.

Each group is identified by a EMBENET_GroupId group identifier, which is freely chosen by the nodes.

In order to join a given group the nodes application should use EMBENET_NODE_JoinGroup after the stack was initialized. Node can join multiple groups by subsequently calling the EMBENET_NODE_JoinGroup function.

The node may also leave particular group using EMBENET_NODE_LeaveGroup function.

At any time after stack initialization the nodes application can get the number of groups it belongs to using EMBENET_NODE_GetGroupCount. Next, it is possible to get group identifiers the node belongs to by calling EMBENET_NODE_GetGroupByIndex with index starting from 0 to the value returned by EMBENET_NODE_GetGroupCount - 1.

The example below illustrates this API usage:

#include "embenet_node.h" // embeNET Node API
#include <stdio.h>
// Nodes main function
int main(void) {
// Initialize embeNET Node
// Join groups 13 and 27
if (false == EMBENET_NODE_JoinGroup(13)) {
puts("Failed to join group 13");
}
if (false == EMBENET_NODE_JoinGroup(27)) {
puts("Failed to join group 27");
}
// Get number of groups the node belongs to
size_t groupCount = EMBENET_NODE_GetGroupCount(); // this should return 2
// Go through all indexes
for (size_t groupIndex = 0; groupIndex < groupCount; groupIndex++) {
// Get groupId
printf("I'm in group: %d ", (int)groupId);
}
while (1) {
// Process the embeNET Node stack
}
// Deinitialize the stack (if needed)
return 0;
}
uint16_t EMBENET_GroupId
Multicast group ID.
Definition: embenet_defs.h:52
EMBENET_GroupId EMBENET_NODE_GetGroupByIndex(size_t index)
Gets the groups the node belongs to by their index.
bool EMBENET_NODE_JoinGroup(EMBENET_GroupId groupId)
Makes the node join the given multicast group.
size_t EMBENET_NODE_GetGroupCount(void)
Gets the number of groups the node belongs to.

Task handling

The embeNET Node library provides additional scheduling mechanism that allows to run application code callbacks from the context of the EMBENET_NODE_Proc at a given time. This is useful for implementing periodic behavior such as gathering data from sensors or sending it to the network.

Local time and network time

The embeNET Node library uses two notions of time - local time and network time.

Local time

Local time is the time taken from a clock that starts to run once the embeNET Node stack is initialized by the EMBENET_NODE_Init function. This clock ticks in milliseconds from 0 until the EMBENET_NODE_Deinit is called. This time can be accessed through a call to EMBENET_NODE_GetLocalTime and it has 64-bit resolution.

Network time

In embeNET, every node connected to the network is synchronized to the common network time. The origin of this network clock is the border router. Network time is accessible to the node'a application after the node synchronizes to a network. It can be read through a call to EMBENET_NODE_GetNetworkTime. The network time accessible to the application in measured in milliseconds with 64-bit resolution. Due to security concerns, the network time does not start from zero, but from a random value established at the border router. It is important to node that this time 'flows' only for nodes that are synchronized to the network. If a node desynchronizes - for example leaves the network - then the* network time in that node stops. If the node synchronizes again to the same network then the network time may already have a larger value. Moreover, if the node synchronizes to different network or the network is restarted, the network time may have a completely different (even earlier) value.

Scheduling tasks

The embeNET Node library uses an abstraction of task. Once the stack is initialized by the EMBENET_NODE_Init function, a task can be created using EMBENET_NODE_TaskCreate. The function accepts two arguments:

  • the actual function to call as task implementation,
  • a user-defined context that will be passed to the task implementation function once it gets called.

The created task is identified by the EMBENET_TaskId value - a task identifier. The stack can only handle a limited number of tasks, so if EMBENET_NODE_TaskCreate returns EMBENET_TASKID_INVALID then no more task can be created.

Once the task is created it can be scheduled to run at a given time - local time or network time - using EMBENET_NODE_TaskSchedule function. When the actual time comes, the task will be run from the EMBENET_NODE_Proc function.

The following example illustrates the task API:

#include "embenet_node.h" // embeNET Node API
#include <stdio.h>
void myTaskFunction(EMBENET_TaskId taskId, EMBENET_NODE_TimeSource timeSource, uint64_t t, void* context) {
printf("Task was called, context is: %s", (char*)context);
// reschedule the task after another 5 seconds
EMBENET_NODE_TaskSchedule(taskId, timeSource, t + 5000);
}
static char* myContext = (char*)"MyContext";
// Nodes main function
int main(void) {
// Initialize embeNET Node
// Create a task
EMBENET_TaskId taskId = EMBENET_NODE_TaskCreate(myTaskFunction, myContext);
// Schedule the task to run 10 seconds after initialization
while (1) {
// Process the embeNET Node stack
}
// Destroy the task (if needed)
// Deinitialize the stack (if needed)
return 0;
}
void EMBENET_NODE_TaskDestroy(EMBENET_TaskId taskId)
Destroys a task.
EMBENET_Result EMBENET_NODE_TaskSchedule(EMBENET_TaskId taskId, EMBENET_NODE_TimeSource timeSource, uint64_t t)
Schedules task in time, reschedules if task was already scheduled.
EMBENET_NODE_TimeSource
Definition: embenet_node.h:97
EMBENET_TaskId EMBENET_NODE_TaskCreate(EMBENET_NODE_TaskFunction taskFunction, void *userContext)
Registers an application-level task.
size_t EMBENET_TaskId
Identifier of an application-level task running within the stack.
Definition: embenet_node.h:91
@ EMBENET_NODE_TIME_SOURCE_LOCAL
Local node time.
Definition: embenet_node.h:99

It is worth to mention, that it is relatively easy to reschedule the task that was run inside the task callback function, as shown in the above example.

It is possible to cancel the scheduled task using EMBENET_NODE_TaskCancel. Such tasks can be scheduled to run later.

When the task is not needed, it can be destroyed by a call to EMBENET_NODE_TaskDestroy, however the taskId may be reused by the stack when another task is created.

Tasks scheduled in network time

The tasks that are scheduled in the same network time are invoked synchronously across all the nodes that scheduled them. This opens the opportunity to implement precise synchronous behavior in many networked nodes. However the drawback is that if the node desynchronizes from the network, the events scheduled in network time get canceled automatically and may need to be rescheduled once the node synchronizes again.

UDP and sockets

For the description of the UDP and sockets refer to Using UDP sockets.

Random numbers

In many cases, internally the embeNET Node library uses the underlying hardware to generate random numbers. For convenience, the random number generator is also exposed to the API in to form of the EMBENET_NODE_GetRandomValue.