Add CoAP Resources to the Device Application#

Using the Send status / Retrieve UDP notifications technique, the devices are automatically sending messages to the Border Router (by default, another destination is possible). In a large Wi-SUN network, this may not be convenient, or the auto_send_sec value would be set to a very high value, such that these messages act as 'keep alive' messages, used to make sure the devices are still active and connected.

An approach more fitting for large networks consists of adding the capability for the nodes to reply with the status messages to CoAP 'get' messages. With this approach, the network management machine is responsible for regularly checking the node status, potentially infrequently. This is described in the next paragraphs.

If the devices are monitoring critical sensors, such as temperature values, they can be set to send notification messages if the monitored values pass above pre-determined thresholds, or enter critical zones. This is application-specific, out of scope of the current example application.

Add CoAP to the Project#

The demonstration code is in app_coap.c and app_coap.h. It uses the Wi-SUN CoAP service, available in SOFTWARE COMPONENTS > Wi-SUN > Wi-SUN Services > CoAP.

CoAP ComponentCoAP Component

Because the demonstration application uses a lot of CoAP resources, the Maximum capacity of the CoAP Resource Table is set to 30 in /wisun_soc_empty_stability/config/sl_wisun_coap_config.h, instead of the default of 10.

// <o SL_WISUN_COAP_RESOURCE_HND_MAX_RESOURCES> Maximum capacity of the CoAP Resource Table
// <i> Default: 10
#define SL_WISUN_COAP_RESOURCE_HND_MAX_RESOURCES        30U

The Resource handler service socket communication buffer size is left to its default value of 1024, declared as SL_WISUN_COAP_RESOURCE_HND_SOCK_BUFF_SIZE in sl_wisun_coap_config.h. This will limit the length of the JSON text string we can send.

// <o SL_WISUN_COAP_RESOURCE_HND_SOCK_BUFF_SIZE> Resource handler service socket communication buffer size
// <i> Default: 1024
#define SL_WISUN_COAP_RESOURCE_HND_SOCK_BUFF_SIZE       1024UL

Definition of CoAP Resources#

One of the most difficult parts of coding a CoAP application is setting correct names for resources, i.e. names that feel logical for most users, and present a consistent hierarchy for those resources.

In the demonstration application, Silicon Labs used:

  • 'info/' for parameters that will not change for a given device:

    • device_tag

    • chip

    • board

    • 'info/all' returns a json string with all of the above

  • 'status/' for parameters varying with time and not part of the statistics

    • running

    • parent (which can change if selecting a different parent)

    • neighbor (access to neighbor count if no payload, otherwise adding -e <n> returns info for neighbor n)

    • connected (not accumulated connection time)

    • 'status/all' returns a json string with all of the above

  • 'statistics/' for values accumulated by the application, or retrieved from the stack

    • 'statistic/app/'

      • join_states_sec

      • disconnected_total

      • connections

      • connected_total

      • availability

      • 'statistics/app/all' returns a json string with all of the above. Adding -e reset resets these statistics.

    • 'statistics/stack/'

  • 'settings/' for parameters of the application we want to change via CoAP

    • auto_send (returns the current auto_send_sec value if no payload, otherwise adding -e <s> sets auto_send_sec to s))

Declare a CoAP Callback Function for each CoAP Resource#

The application needs to know which function to execute when a CoAP get request is received. There is one function per resource. We will only document some of them. Adding more is straightforward once the first ones are clearly explained.

CoAP Callback Prototype#

Each CoAP callback function has the same return type and parameters:

  • return type sl_wisun_coap_packet_t *, a pointer to the response packet

  • parameter const sl_wisun_coap_packet_t *const req_packet, the CoAP request packet, defined in sl_wisun_coap.h as:

/**
 * \brief Main CoAP message struct
 */
typedef struct sn_coap_hdr_ {
  uint8_t                 token_len;            /**< 1-8 bytes. */

  sn_coap_status_e        coap_status;          /**< Used for telling to User special cases when parsing message */
  sn_coap_msg_code_e      msg_code;             /**< Empty: 0; Requests: 1-31; Responses: 64-191 */

  sn_coap_msg_type_e      msg_type;             /**< Confirmable, Non-Confirmable, Acknowledgment or Reset */
  sn_coap_content_format_e content_format;      /**< Set to COAP_CT_NONE if not used */

  uint16_t                msg_id;               /**< Message ID. Parser sets parsed message ID, builder sets message ID of built coap message */
  uint16_t                uri_path_len;         /**< 0-255 bytes. Repeatable. */
  uint16_t                payload_len;          /**< Must be set to zero if not used */

  uint8_t                *token_ptr;            /**< Must be set to NULL if not used */
  uint8_t                *uri_path_ptr;         /**< Must be set to NULL if not used. E.g: temp1/temp2 */
  uint8_t                *payload_ptr;          /**< Must be set to NULL if not used */

  /* Here are not so often used Options */
  sn_coap_options_list_s *options_list_ptr;     /**< Must be set to NULL if not used */
} sn_coap_hdr_s;
. . .
typedef sn_coap_hdr_s sl_wisun_coap_packet_t;

The above shows that there is a possible payload, corresponding to payload_ptr with a size of payload_len. The code can therefore check the payload to act differently. This payload corresponds to the -e <payload> the user can add to each CoAP request.

The settings resource is interesting to look at, since it can be used to retrieve the auto_send_sec value and also to change it.

CoAP Reply Function#

To more easily reply to a CoAP request with a text string as the result, the app_coap_reply() function is added to app_coap.c:

sl_wisun_coap_packet_t * app_coap_reply(char *response_string,
                  const sl_wisun_coap_packet_t *const req_packet) {

  sl_wisun_coap_packet_t* resp_packet = NULL;
  // Prepare CoAP response packet with default response string
  resp_packet = sl_wisun_coap_build_response(req_packet, COAP_MSG_CODE_RESPONSE_BAD_REQUEST);
  if (resp_packet == NULL) {
    return NULL;
  }

  resp_packet->msg_code       = COAP_MSG_CODE_RESPONSE_CONTENT;
  resp_packet->content_format = COAP_CT_TEXT_PLAIN;
  resp_packet->payload_ptr    = (uint8_t *)response_string;
  resp_packet->payload_len    = (uint16_t)sl_strnlen(response_string, COAP_MAX_RESPONSE_LEN);

  return resp_packet;
}

The CoAP component provides the sl_wisun_coap_build_response() function, which can be used to prepare most of the reply. Then the payload is set to the response_string input string.

CoAP Resource Handler#

The CoAP Resource Handler is the only part of the CoAP component you need to interact with. It will then perform the following tasks whenever a CoAP request is received:

  • Check that there is a known resource uri_path matching the request's uri_path_ptr.

    • If yes, call the resource's auto_response function.

    • If the auto_response function is coded to send a reply, the reply will be sent to the IPv6 address at the origin of the request.

  • If the request is ".well-known/core", return a JSON array with all resource set as discoverable

settings/auto_send Example#

The settings/auto_send resource is interesting to look at, since it can be used to retrieve the auto_send_sec value and also to change it.

settings/auto_send CoAP Callback Function#

sl_wisun_coap_packet_t * coap_callback_auto_send (
      const  sl_wisun_coap_packet_t *const req_packet)  {
  int sec = 0;
  int res;
  if (req_packet->payload_len) {
    res = sscanf((char *)req_packet->payload_ptr, "%d", &sec);
    if (res) { auto_send_sec = sec; }
  }
  snprintf(coap_response, COAP_MAX_RESPONSE_LEN, "auto_send_sec: %d", auto_send_sec);
return app_coap_reply(coap_response, req_packet); }

The one-before-last line of the function is formatting the text string corresponding to the reply's payload (req_packet->payload_ptr), then app_coap_reply() is called to send the reply to the sender. The reply is in the form auto_send_sec: 60.

Before this, the request payload has been checked, and if it contains a decimal value, this is used as the new auto_send_sec if it exists, so the reply will always contain the current value.

settings/auto_send CoAP Resource Registration#

To have the CoAP component handle this new resource, a part of the app_coap_resources_init() function provides a corresponding sl_wisun_coap_rhnd_resource_t to the CoAP Resource Handler:

uint8_t app_coap_resources_init() {
  sl_wisun_coap_rhnd_resource_t coap_resource = { 0 };
  uint8_t count = 0;

  // Add CoAP resources (one per item)
. . .
  coap_resource.data.uri_path = "/settings/auto_send";
  coap_resource.data.resource_type = "sec";
  coap_resource.data.interface = "settings";
  coap_resource.auto_response = coap_callback_auto_send;
  coap_resource.discoverable = true;
  assert(sl_wisun_coap_rhnd_resource_add(&coap_resource) == SL_STATUS_OK);
    count++;
. . .
  printf("  %d/%d CoAP resources added to CoAP Resource handler\n", count, SL_WISUN_COAP_RESOURCE_HND_MAX_RESOURCES);
  return count;
}

A check has been added at the end of app_coap_resources_init() on the number of CoAP resources registered with the CoAP Resource Handler. This allows you to check if SL_WISUN_COAP_RESOURCE_HND_MAX_RESOURCES is correctly set, and if it needs to be increased.

CoAP Resources Initialization by the Application#

The app_coap_resources_init() function is called once at startup from app-init.c/app_init(), adding all CoAP resources to the CoAP Resource Handler:

void app_init(void)
{
  app_coap_resources_init();

  /* Creating App main thread */
. . .

With the above, from the Border Router console, you can:

  • Discover the available CoAP resources on the device, using

coap-client -m get -N -B 3 coap://[<device_IPv6>]:5683/.well-known/core

  • Retrieve a CoAP resource using

coap-client -m get -N -B 3 coap://[<device_IPv6>]:5683/<resource>

Details about the possibilities are provided in the app_coap.c/print_coap_help() function once connected, because at this moment the device has obtained an IPv6 address:

void  print_coap_help (char* device_global_ipv6_string, char* border_router_ipv6_string) {
  printf("\n");
  printf("To start a CoAP server on the linux Border Router:\n");
  printf("  coap-server -A %s -p %d -d 10\n", border_router_ipv6_string, 5685);
  printf("CoAP discovery:\n");
  printf("  coap-client -m get -N -B 3 coap://[%s]:5683/.well-known/core\n", device_global_ipv6_string);
  printf("CoAP GET requests:\n");
  printf("  coap-client -m get -N -B 3 coap://[%s]:5683/<resource>, for the following resources:\n", device_global_ipv6_string);
  sl_wisun_coap_rhnd_print_resources();
  printf("  '/settings/auto_send'         returns the current notification duration in seconds\n");
  printf("  '/settings/auto_send' -e <d>' changes the notification duration to d seconds\n");
  printf("  '/status/neighbor'            returns the neighbor_count\n");
  printf("  '/status/neighbor -e <n>'     returns the neighbor information for neighbor at index n\n");
  printf("  '/statistics/stack/<group> -e reset' clears the Stack statistics for the selected group\n");
  printf("  '/statistics/app/all       -e reset' clears all statistics\n");
  printf("\n");
}

status/all Example#

The status/all resource is used to retrieve the running, connected, and parent information.

status/all CoAP Callback Function#

sl_wisun_coap_packet_t * coap_callback_all_statuses (
      const  sl_wisun_coap_packet_t *const req_packet)  {
  #define JSON_ALL_STATUSES_FORMAT_STR      \
    "{\n"                                 \
    "  \"running\": \"%s\",\n"            \
    "  \"connected\": \"%s\"\n"           \
    "  \"parent\": \"%s\",\n"             \
    "}\n"

  char running_str[40];
  char connected_str[40];

  snprintf(running_str  , 40, dhms(now_sec()) );
  snprintf(connected_str, 40, dhms(now_sec() - connection_time_sec) );

  snprintf(coap_response, COAP_MAX_RESPONSE_LEN, JSON_ALL_STATUSES_FORMAT_STR,
            running_str,
            connected_str,
            parent_tag
  );
  return app_coap_reply(coap_response, req_packet);
}

status/all CoAP Resource Registration#

To have the CoAP component handle this new resource, a part of the app_coap_resources_init() function provides a corresponding sl_wisun_coap_rhnd_resource_t to the CoAP Resource Handler:

uint8_t app_coap_resources_init() {
  sl_wisun_coap_rhnd_resource_t coap_resource = { 0 };
  uint8_t count = 0;

  // Add CoAP resources (one per item)
. . .
  coap_resource.data.uri_path = "/status/all";
  coap_resource.data.resource_type = "json";
  coap_resource.data.interface = "node";
  coap_resource.auto_response = coap_callback_all_statuses;
  coap_resource.discoverable = true;
  assert(sl_wisun_coap_rhnd_resource_add(&coap_resource) == SL_STATUS_OK);
  count++;
. . .
  printf("  %d/%d CoAP resources added to CoAP Resource handler\n", count, SL_WISUN_COAP_RESOURCE_HND_MAX_RESOURCES);
  return count;
}

As visible above, there are very few differences between the block of code used to register "/settings/auto_send" or "/status/all", so adding more is easy.

Before looking at adding CoAP to the Border Router, below is an example of the output you can get on the Border Router when monitoring multiple devices:

status/all in use#

./coap_all /status/all
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::62a4:23ff:fe37:a51c]:5683/status/all : { "running": " 8-05:37:43", "connected": " 0:01:49:55" "parent": "2527", }
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::62a4:23ff:fe37:a51d]:5683/status/all : { "running": " 0-23:00:55", "connected": " 0:22:59:53" "parent": "a901", }
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::62a4:23ff:fe37:a527]:5683/status/all : { "running": " 8-05:37:35", "connected": " 0:02:05:35" "parent": "333a", }
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::62a4:23ff:fe37:a8ff]:5683/status/all : { "running": " 8-05:37:38", "connected": " 0:21:05:04" "parent": "333a", }
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::62a4:23ff:fe37:a901]:5683/status/all : { "running": " 1-02:44:11", "connected": " 1:02:43:16" "parent": "333a", }
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::b635:22ff:fe98:2191]:5683/status/all : { "running": " 1-05:50:51", "connected": " 1:05:49:47" "parent": "333a", }
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::b635:22ff:fe98:2527]:5683/status/all : { "running": " 1-05:50:50", "connected": " 1:05:49:37" "parent": "a8ff", }
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::b635:22ff:fe98:2853]:5683/status/all : { "running": " 1-05:50:53", "connected": " 0:05:10:54" "parent": "333a", }
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::b6e3:f9ff:fec5:8486]:5683/status/all : { "running": " 1-05:43:48", "connected": " 0:00:47:53" "parent": "333a", }
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::b6e3:f9ff:fec5:8493]:5683/status/all : { "running": " 1-02:45:43", "connected": " 0:10:37:17" "parent": "2853", }
  coap-client -m get -N -B 3 coap://[fd00:6172:6d00::b6e3:f9ff:fec5:8503]:5683/status/all : { "running": " 1-02:43:29", "connected": " 0:07:51:09" "parent": "333a", }

NOTE: Remember that timestamping is in dd-hh:mm:ss format, meaning that in the example above " 8-05:37:43 means more than 8 days of connection time, corresponding to a group of 3 devices, while others have been started only a day ago, with about 3 hours between them. This shows how interesting it is to use this time stamping format.