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:
- Security (/0),
- Server (/1),
- Access Control (/2),
- Device (/3),
- Firmware Update (/5).
Additionally, we support Raspberry Pi Sense Hat extension board, and the following objects:
- Temperature (/3303),
- Accelerometer (/3313),
- Magnetometer (/3314),
- Barometer (/3315),
- Gyrometer (/3334),
- Addressable Text Display (/3341).
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:
security.json
server.json
svd.json
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:
-
device.endpoint_name
- if set, the LwM2M Endpoint Client Name will be literally set to the configured value. Otherwise, it will be set tourn:dev:os:B827EB-<SERIAL_NUMBER>
, with<SERIAL_NUMBER>
replaced by the actual serial number of the Pi - the value that can be read in/proc/cpuinfo
and/sys/firmware/devicetree/base/serial-number
. -
device.udp_listen_port
- force binding to a specific UDP port. If set to a non-zero value, all UDP sockets created by the LwM2M client will be bound to configured port. Otherwise, random ephemeral ports will be used. -
device.server_initiated_bootstrap
- enables / disables LwM2M Server Initiated bootstrap support. If set to true, connection to the Bootstrap Server will be closed immediately after making a successful connection to any regular LwM2M Server and only opened again if (re)connection to a regular server is rejected. Default value is:0
. -
logging.default_log_level
- log level applied to messages in case no more specific log level exists.Acceptable values:
- "trace" (log all messages)
- "debug"
- "info"
- "warning"
- "error"
- "quiet" (do not log anything)
Default value: "info"
-
logging.log_level.MODULE_NAME
- log level applied to messages originating fromMODULE_NAME
only. Can be used to selectively control logging level. -
in_buffer_size_b
- size (in bytes) of the buffer used for storing incoming LwM2M messages. The client will not be able to handle packets bigger than this size.Default value: 4096
-
out_buffer_size_b
- size (in bytes) of the buffer used for storing outgoing LwM2M messages. In cases where the message sent would exceed this size, the client will attempt a BLOCK-wise CoAP transfer instead.Default value: 4096
-
msg_cache_size_b
- size (in bytes) of the buffer used for storing outgoing LwM2M messages. When the client receives a duplicate request while an already-prepared response is in the cache, it is used instead of generating a new one. Cached messages are removed after their validity expires. If total size of cached messages exceeds configured value, oldest entries are evicted to make room for fresh ones.Setting this value to 0 disables message caching. In such case, the client will handle all received retransmitted requests as if they were new ones, which may result in performing non-idempotent operations multiple times.
Default value: 65536
-
retry_after_s
- enables / disables reconnection policy which after specified period of time (in seconds) after all server connections failed performs a reconnection attempt. Value of 0 disables reconnection attempts, and causes the client to shutdown if it is unable to establish any connection.Default value: 30
-
dirs.persistence
- path to the persistence directory. That path MUST NOT get cleared on FW update.Default value:
SVETOVID_PERSISTENCE_DIR
set during compile time. -
dirs.volatile_persistence
- path to a volatile persistence directory. That path MUST be cleared on FW update, but persist across reboots.Default value:
/etc/svetovid/persistence
-
dirs.plugins
- path to svd plugin installation directory.Default value:
/usr/lib/svetovid
-
dirs.temp
- path to a directory used for temporary file storage.Default value:
/tmp
-
dirs.firmware_download_dir
- path where PULL FW downloads will be kept. It MAY be cleared on FW update, but SHOULD persist across reboots in order to support firmware download resumption.Default value:
/tmp
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
-
server_uri
- LwM2M Server URI ("coap://" or "coaps://" URI, depending on thesecurity_mode
value), -
is_bootstrap
- Bootstrap Server (boolean) -
security_mode
- Security Mode (one of: "psk", "nosec", "cert") -
pubkey_or_identity_hex
- Public Key or Identity (hex string). NOTE: this must be a hex string, even if the value is in fact a printable text. For example, if the PSK identity is supposed to be "identity", this value should be set to "6964656e74697479". -
server_pubkey_hex
- Server Public Key (hex string; see NOTE above) -
privkey_or_psk_hex
- Secret Key (hex string; see NOTE above) -
ssid
- Short Server ID (1-65534) -
holdoff_s
- Client Hold Off Time (seconds) -
bs_timeout_s
- Bootstrap-Server Account Timeout (seconds)
Complete reference for the server.json
file options
-
ssid
- Short Server ID (1-65534, must matchssid
of some Security Object Instance) -
lifetime
- Lifetime (seconds) -
default_min_period
- Default Minimum Period (seconds) -
default_max_period
- Default Maximum Period (seconds) -
binding
- Binding (one of: "U", "UQ") -
notification_storing
- Notification Storing When Disabled or Offline (boolean) -
disable_timeout
- Disable Timeout (seconds)
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:
/etc/svetovid/dm/
object_id/
(e.g.3333
) - directory representing an LwM2M Object with given ID.resources/
- directory containing scripts used to access individual Resources. Names of individual Resources MUST exactly correspond to their Resource IDs. (autogenerated bysvetovid-fsdmtool
),instances
- an optional executable script for managing instances (autogenerated bysvetovid-fsdmtool
),transaction
- an optional executable script used to handle transactional processing of object resources (this is a complicated topic, not yet covered in this demo).
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:
- LwM2M Read on some
/Object ID/Instance ID/Resource ID
will be transformed into:- getting the list of instances from
Object ID/instances
script to verify if the targeted instance exists, - calling the
Object ID/Resource ID
script to read the value of the resource for that instance (the Instance ID is passed toResource ID
script as parameter).
- getting the list of instances from
- LwM2M Read on some
/Object ID/Instance ID
will be transformed into:- getting the list of instances from
Object ID/instances
script to enumerate instance to be read, - calling resource scripts (as above), but for every present instance.
- getting the list of instances from
- LwM2M Observe is transformed into:
- if the "external notify" mechanism is disabled for a given LwM2M Object
(default): periodical LwM2M Reads to see if resource values changed. You
can control the frequency of reads/notifications by
pmin
andpmax
LwM2M Attributes. - it is the user responsibility to notify Svetovid about the following
changes in FSDM:
- list of valid Instance IDs for an object has changed for other reason than receiving the Create or Delete operation calls from Svetovid itself
- readable resource has changed its value for other reason than receiving the Write, Reset or Clear operation calls from Svetovid itself
- if the "external notify" mechanism is disabled for a given LwM2M Object
(default): periodical LwM2M Reads to see if resource values changed. You
can control the frequency of reads/notifications by
- LwM2M Delete is transformed into:
- calls to
instances
script to delete instances.
- calls to
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:
KvStore(namespace)
- constructor that takesnamespace
parameter as an argument. Thisnamespace
should be set to e.g. an Object ID in which theKvStore
is used. The idea is that when many objects are implemented and utilize the store, there's a risk of key-collision between them. Thenamespace
parameter is supposed to uniquely distinguish between different objects.get(key, default=None)
- for getting the givenkey
, or returning thedefault
value if it is not present in the storeset(key, value)
- for creating/replacing thevalue
assigned to a givenkey
,delete(key)
- for deleting thekey
and the value associated with it.
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:
- instances lists of objects 10 and 20 have changed
- values of resources
/9/0/1
and/9/0/2
have changed
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:
- socket domain should be
AF_UNIX
/AF_LOCAL
- socket type should be
SOCK_STREAM
- after sending whole message the socket should be shut down for further
transmissions (
SHUT_WR
flag)
Response
As a result of triggering a notify a response is sent through the socket. There are 3 kinds of result:
-
{"result": "OK"}
: There were no errors during triggering a notify. -
{"result": "warning", "details": [ ... ] }
: In this case some of entries could not be processed and the reasons for each entry are indicated indetails
section. Entries omitted indetails
section were perfectly valid and there is no need to try notifying them again. -
{"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 $