Home

Awesome

Anjay-raspberry-client <img align="right" height="50px" src="https://avsystem.github.io/Anjay-doc/_images/avsystem_logo.png">

Overview

This the demonstration client for Linux based devices, specifically Raspberry Pi running on Raspbian Buster. It presents a feature called FSDM (File System Data Model) and implements a few LwM2M Objects directly on top of that.

Basic objects implemented are:

Additionally, we support Raspberry Pi Sense Hat extension board, and the following objects:

What is FSDM (File System Data Model)

FSDM is a plugin for our Linux client that provides an easy way for LwM2M Objects prototyping and development clasically done in C or C++. With the plugin however, there is no such limitation, and objects can be implemented pretty much in any language of choice. The only requirement is that the object structure follows certain schema, and executables behave in the way expected by the LwM2M Client that loads & manages them.

Currently, we have extensive support libraries and code-generators for Python and sh, to get you started even faster.

Installation instructions

Install svetovid_21.12-raspberry_armhf.deb on Raspberry Pi:

$ sudo dpkg -i svetovid_21.12-raspberry_armhf.deb

Note that this installs a svetovid.service systemd service, automatically enabled, and starts-up the Client immediately. You may want to disable this behavior in the following way:

$ sudo systemctl disable svetovid.service --now # disable and stop svetovid service

The basic installation package does not contain the FSDM plugin described above. To install it, please run:

$ sudo dpkg -i svetovid-plugin-fsdm_21.12-raspberry_armhf.deb \
avsystem_svetovid-21.12-raspberry-Linux-fsdmtool-runtime-python.deb

If you have the Raspberry Pi Sense Hat extension board, you may install a dedicated package to enable more objects:

$ sudo dpkg -i avsystem_svetovid-21.12-raspberry-Linux-sensehat.deb

Configuration

Svetovid keeps configuration in JSON files. For Raspberry Pi, the default location of these JSONs is /etc/svetovid/config. Default configuration directory may be overwritten by passing --conf-dir command line argument when starting a Svetovid binary.

WARNING: Only following JSON files are supposed to be modified manually:

Editing other JSON files in configuration directory may cause unexpected behavior of the client.

NOTE: The LwM2M client process may modify these files at runtime. To avoid your changes from being overwritten, make sure to stop the svetovid process before modifying the configuration (see Startup process and client operation section).

Global settings are stored in svd.json file

Note: This file can generally be left empty if you are fine with the defaults.

Example:

{
    "device": {
        "endpoint_name": "urn:dev:os:0023C7-EXAMPLE_DEVICE",
        "udp_listen_port": 1234
    },
    "logging": {
        "default_log_level": "debug",
        "log_level": {
            "svd": "info"
        }
    },
    "in_buffer_size_b": 1024,
    "out_buffer_size_b": 1024,
    "msg_cache_size_b": 65536
}

The following configuration options are recognized:

Server connection settings are stored in security.json and server.json

The default configuration is designed to let you easily connect to our Coiote IoT Device Management LwM2M Server platform. Please register at https://www.avsystem.com/try-anjay/ to get access.

In the security.json file you're gonna need to change the privkey_or_psk_hex with hexlified pre-shared-key of your choice. To convert raw string to hexlified string, you can use:

$ echo -n 'your-secret-key' | xxd -p

You can now restart or start (if not started already) the LwM2M Client:

# if you disabled svetovid.service in previous steps
$ svetovid
# or if you intend to use systemd to manage svetovid process
$ sudo systemctl restart svetovid.service

Complete reference for the security.json file options

Complete reference for the server.json file options

Using a Bootstrap Server

When using a Bootstrap Server, it may modify the contents of the Security and Server objects. These changes will NOT be written back to security.json or server.json files - instead, they will be persisted into /etc/svetovid/persistence/persistence.dat. Note that this is a binary file that is not intended for user modification.

The configuration in security.json and server.json will take preference if the persistence.dat file doesn't exist, or if either of the JSON files is newer than the last time Svetovid has been bootstrapped from them. In other words, if you modify or touch the JSON files, they shall take preference.

Developing custom objects

FSDM comes with a helper tool for generating stubs of all required scripts. Run svetovid-fsdmtool --help to see up-to-date help message with usage examples.

First, how does the FSDM actually work?

Structure

The FSDM plugin maps specific directory (/etc/svetovid/dm by default) and its structure to LwM2M Objects, Instances and Resources. The recognized structure is as follows:

NOTE: The svetovid-fsdmtool script, when generating object's structure, also adds human-readable symlinks under object_id/ directory to resources located under object_id/resources/.

Every LwM2M operation is mapped to execution of one or more scripts located under the object_id/. Examples:

Input and output

Resource scripts obtain necessary information either from parameters passed to them or from the standard input. For example, the LwM2M Write on a Resource containing payload "example", will execute the corresponding Resource script, passing the "example" string on its standard input.

Resource scripts return values to Svetovid via standard output when they're used to extract the value they represent. Apart from that, the scripts' exit codes are translated to CoAP error responses. In the Python implementations, any errors are communicated by exceptions, which are in turn translated to error codes by the runtime in a way transparent to the user.

LwM2M Execute arguments are passed as arguments to the script body. In Python implementations, execute arguments are passed as a parameter to execute() method.

Example

Say you want to implement the Time Object (/3333). It has a few basic read/write resources. You can start with generating the stub:

$ sudo svetovid-fsdmtool generate --object 3333 --output-dir /etc/svetovid/dm --generator python

This creates /etc/svetovid/dm/3333 directory containing (note the directory has the structure as described above):

├── Application_Type -> resources/5750
├── Current_Time -> resources/5506
├── Fractional_Time -> resources/5507
├── instances
└── resources
    ├── 5506
    ├── 5507
    └── 5750

Let's start with an "Application Type" resource implementation. The placeholders for read, write, and reset need to be filled in with some actual logic.

The first problem to think about is: how do we store the incoming Application Type written by the Server? Svetovid supports simple key-value store which is accessible from Python scripts via KvStore class. It provides a very simple interface:

We can use this to implement Application Type resource as follows:

#!/usr/bin/env python
# -*- encoding: utf-8 -*-

from fsdm import ResourceHandler, CoapError, DataType, KvStore

import sys # for sys.stdout.write() and sys.stdin.read()

class ResourceHandler_3333_5750(ResourceHandler):
    NAME = "Application Type"
    DESCRIPTION = '''\
The application type of the sensor or actuator as a string depending
 * on the use case.'''
    DATATYPE = DataType.STRING
    EXTERNAL_NOTIFY = False

    def read(self,
             instance_id,            # int
             resource_instance_id):  # int for multiple resources, None otherwise
        value = KvStore(namespace=3333).get('application_type')
        if value is None:
            # The value was not set, so it's not found.
            raise CoapError.NOT_FOUND

        # The value is present within the store, thus we can print it on stdout.
        # The important thing here is to remember to return string-typed resources
        # with sys.stdout.write(), as print() adds unnecessary newline character, so
        # if we used it instead, the value presented to the server would contain that
        # trailing newline character.
        sys.stdout.write(value)


    def write(self,
              instance_id,            # int
              resource_instance_id):  # int for multiple resources, None otherwise
        # All we need to do is to assign a value to the application_type key.
        KvStore(namespace=3333).set('application_type', sys.stdin.read())


    def reset(self,
              instance_id):  # int
        # We reset the resource to its original state by simply deleting the application_type
        # key
        KvStore(namespace=3333).delete('application_type')



if __name__ == '__main__':
    ResourceHandler_3333_5750().main()

Implementation of other resources is even simpler (assuming we make them read-only). For example, the Fractional Time resource can be implemented as follows:

#!/usr/bin/env python
# -*- encoding: utf-8 -*-

from fsdm import ResourceHandler, CoapError, DataType, KvStore


class ResourceHandler_3333_5506(ResourceHandler):
    NAME = "Current Time"
    DESCRIPTION = '''\
Unix Time. A signed integer representing the number of seconds since
 * Jan 1st, 1970 in the UTC time zone.'''
    DATATYPE = DataType.TIME
    EXTERNAL_NOTIFY = False

    def read(self,
             instance_id,            # int
             resource_instance_id):  # int for multiple resources, None otherwise
        # It's just that simple!
        import time
        print(int(time.time()))


    def write(self,
              instance_id,            # int
              resource_instance_id):  # int for multiple resources, None otherwise
        # NOTE: Implement this if you want to be able to change time on your system.
        raise CoapError.NOT_IMPLEMENTED

    def reset(self,
              instance_id):  # int
        # NOTE: reset resource to its original state. You can either set it to
        # a default value or delete the resource.
        pass



if __name__ == '__main__':
    ResourceHandler_3333_5506().main()

For more complex examples install avsystem_svetovid-21.12-raspberry-Linux-sensehat.deb package as described above, and have a look at other objects impementations in /etc/svetovid/dm.

NOTE: If you create FSDM scripts for an object ID that is already implemented in the core client, the FSDM implementation will take precedence. Please note that this might not be the case in case of other plugins (like the Sense Hat one), as the plugin loading order will decide - so you may prefer not to install the Sense Hat plugin if you intend to implement these objects yourself.

External notify mechanism

To activate "external notify" mechanism for an object instance or a resource, you need to explicitly enable that mode in the resource or instances scripts:

For Python scripts generated by fsdmtool, for readable entities, the class constant EXTERNAL_NOTIFY should be set to True (default value is False).

After enabling the functionality, it is possible to use special Unix domain socket to notify about value changes. The socket is created after Svetovid launch in Svetovid temporary directory (by default: /tmp/fsdm_local_socket).

You may send JSON containing information about changed state of instances and resources as follows:

{ "notify": ["/10", "/20", "/9/0/1", "/9/0/2"] }

In example above we want to inform that:

If any of these resources is indeed observed, Svetovid will then invoke the Read operation on the appropriate FSDM script to query the actual resource value.

To send the message through the socket, you can use standard tools like nc or socat.

nc

echo '{ "notify": ["/10", "/20", "/9/0/1", "/9/0/2"] }' | nc -NU /tmp/fsdm_local_socket

NOTE: Option -N is set because nc should shutdown the socket after EOF on the input.

socat

echo '{ "notify": ["/10", "/20", "/9/0/1", "/9/0/2"] }' | socat - UNIX-CONNECT:/tmp/fsdm_local_socket

Native socket API

You can use standard socket API of your preferred programming language. These are important things to remember about user-side socket:

Response

As a result of triggering a notify a response is sent through the socket. There are 3 kinds of result:

  1. {"result": "OK"}: There were no errors during triggering a notify.

  2. {"result": "warning", "details": [ ... ] }: In this case some of entries could not be processed and the reasons for each entry are indicated in details section. Entries omitted in details section were perfectly valid and there is no need to try notifying them again.

  3. {"result": "error", "details": "..." }: There was some serious problem with execution of user request (e.g. parsing error). In this case all entries should be considered as not processed.

Examples

"details" section for "OK" result is absent:

user@host $ echo '{ "notify": ["/1337"] }' | nc -NU /tmp/fsdm_local_socket
{
    "result": "OK"
}
user@host $

"details" for "warning" result is an array of failure reasons:

user@host $ echo '{ "notify": [":-)", "/1/2/3"] }' | nc -NU /tmp/fsdm_local_socket
{
    "result": "warning",
    "details": [
        {
            "path": ":-)",
            "reason": "not object or resource path"
        },
        {
            "path": "\/1",
            "reason": "non-FSDM object"
        }
    ]
}
user@host $

"details" for "error" result is a single diagnostic string:

user@host $ echo abcdefgh | nc -NU /tmp/fsdm_local_socket
{
    "result": "error",
    "details": "malformed input"
}
user@host $