Architecture of the agent¶
Blobs¶
The EVP Application SDK
in include/evp/sdk_blob.h
defines several blob operation types:
EVP_BLOB_TYPE_AZURE_BLOB
, where the non-standardx-ms-blob-type: BlockBlob
HTTP header is appended.EVP_BLOB_TYPE_EVP
EVP_BLOB_TYPE_EVP_EXT
EVP_BLOB_TYPE_HTTP
EVP_BLOB_TYPE_HTTP_EXT
For example,
for standard HTTP operations EVP_BLOB_TYPE_HTTP_EXT
is used.
Internally,
the agent defines several implementations that are loosely coupled
against the blob operation types described above.
src/libevp-agent/blob_type_azure_blob.c
src/libevp-agent/blob_type_evp.c
src/libevp-agent/blob_type_http.c
Since all of the operation types described above rely on HTTP,
in turn they all rely on src/libevp-agent/blob_http.c
,
which will then interact with the Web Client HTTP library.
Modules¶
The agent follows the same terminology used in WebAssembly, where modules refer to executables that can be instantiated into module instances. As an OCI/Docker analogy, modules mean to the agent what images mean to OCI/Docker.
Nevertheless, the agent is not limited to Docker or WebAssembly. It is designed to support any backend, so that the higher-level implementation of the agent remains agnostic.
The officially supported module implementations
and their respective Kconfig
variables are:
WebAssembly:
EVP_MODULE_IMPL_WASM
Python:
EVP_MODULE_IMPL_PYTHON
The following module implementations are considered experimental:
Docker:
EVP_MODULE_IMPL_DOCKER
Shared libraries:
EVP_MODULE_IMPL_DLFCN
Executables:
EVP_MODULE_IMPL_SPAWN
Every module implementation is implemented by its
src/libevp-agent/module_impl_<name>.c
,
where <name>
refers to the implementation name
for example, wasm
or dlfcn
.
An implementation is implemented using a common interface,
namely module_impl_ops
.
See the definition for module_impl_ops
in src/libevp-agent/module_impl_ops.h
for further reference.
Module instances¶
The agent follows the same terminology used in WebAssembly, where module instances refer to modules instantiated by the backend. As an OCI/Docker analogy, module instances mean to the agent what containers mean to OCI/Docker.
The supported module instance implementations
is that of the supported module implementations.
Every module instance implementation is implemented by its
src/libevp-agent/module_instance_impl_<name>.c
,
where <name>
refers to the implementation name
for example, wasm
or dlfcn
.
An implementation is implemented using a common interface,
namely module_instance_impl_ops
.
See the definition for module_instance_impl_ops
in src/libevp-agent/module_instance_impl_ops.h
for further reference.
Config¶
The agent supports definining key/value pairs
to configure EVP module instances.
Module instances can subscribe to configurations
via EVP_setConfigurationCallback()
.
Module instance configuration is implemented
by src/libevp-agent/instance_config.c
,
and is processed according to the configured version of the EVP protocol.
As part of the hub
interface,
src/libevp-agent/instance_config.h
exports the following implementations:
hub_evp1_parse_instance_config()
hub_evp1_notify_config()
hub_evp2_parse_instance_config()
hub_evp2_notify_config()
Apart from module instance configurations,
src/libevp-agent/instance_config.c
also handles system app configurations.
Notifications¶
Since the agent is usually provided as a library,
library users might want to receive events on specific internal events,
such as connection/disconnection events from the hub.
The agent defines a list of internal events
in src/libevp-agent/agent_event.c
,
to which users might want to subscribe,
via the evp_agent_notification_subscribe()
function.
The user-defined callback can also receive
an optional pointer to user-defined data (NULL
in the example below).
The library will not attempt to dereference this pointer.
Every notification type might include additional data related to the event,
and is passed as a read-only pointer to the user-defined callback.
Warning
The user is responsible for casting the read-only pointer passed to the user-defined callback to the appropriate data type. Otherwise, the behaviour is undefined.
For example, the network/error
notification passes a read-only string
meant to help users to debug the networking issue:
int my_callback(const void *args, void *user_data)
{
const char *error = args;
fprintf(stderr, "%s: network error: %s\n", __func__, error);
return 0;
}
int foo(struct evp_agent_ctxt *ctxt)
{
if (evp_agent_notification_subscribe(ctxt, "network/error", my_callback, NULL)) {
fprintf(stderr, "%s: notification_register failed\n",
__func__);
evp_agent_free(ctxt);
return -1;
}
}
Events published by the agent¶
As documented in Notification,
every notification can include additional data passed as a const void *
,
requiring the user to cast the pointer into its appropriate data type.
The data types for each event type are described below.
Note
src/libevp-agent/agent_event.c
is considered the source of truth.
The events listed below are documented as a best effort.
agent/status
: aconst char *
with the connection status (connected
ordisconnected
).blob/result
:const struct evp_agent_notification_blob_result *
.deployment/reconcileStatus
:const struct reconcileStatusNotify *
.mqtt/sync/err
: aconst char *
with an error string coming from the MQTT library.network/error
: aconst char *
with additional information about the error. Its value is only meant for debugging purposes, and therefore stability is not guaranteed.start
: alwaysNULL
.wasm/stopped
:const struct evp_agent_notification_wasm_stopped *
.
Platform¶
Being a platform-agnostic library,
the agent defines a platform abstraction layer.
The interface is provided by src/libevp-agent/platform.h
,
and all of the operations default to portable implementations where possible.
The list of platform-specific functions has grown organically, based on internal requirements and historical reasons.
Persist¶
The agent can fetch a local copy of the most recently applied deployment on startup. This is useful under some circumstances: for example, if the agent starts without connectivity against the MQTT broker,
This is achieved by storing a pair of JSON databases,
namely current.json
and desired.json
,
which express the last applied deployment and the desired deployment,
respectively.
This feature is activated via the EVP_TWINS_PERSISTENCE
Kconfig
variable.
SDK interface¶
Module instances interact with the agent via the EVP Application SDK. The transport layer used between module instances and the agent depends on the module instance implementation:
- Communication is made inside the same process (
EVP_SDK_LOCAL
). WebAssembly (
wasm
).Shared libraries (
dlfcn
).
- Communication is made inside the same process (
- Communication is made from a separate process via a Unix socket (
EVP_SDK_SOCKET
). Docker containers (
docker
).Executables (
spawn
).Python (
python
).
- Communication is made from a separate process via a Unix socket (
src/libevp-agent/sdk_local.c
provides the implementation
for module instance implementations communicating
with the agent within the same process.
However,
module instances running on a sandbox, such as WebAssembly, might require
data extraction from the runtime.
For example, src/libevp-agent/sdk_local_wasm.c
defines
the WebAssembly-specific implementation.
Deployment¶
The agent attempts to reconcile
the desired deploymentManifest
whenever possible.
The logic to achieve this is defined by src/libevp-agent/reconcile.c
.
On every iteration of the main loop,
apply_deployment
is unconditionally called
to attempt the reconciliation.
This involves:
Loading any modules not loaded yet. This might involve downloading the module from an external service.
Instantiating those modules that are already loaded.
Stopping module instances no longer defined by the
deploymentManifest
.Garbage-collecting modules no longer defined by the
deploymentManifest
.
Note
src/libevp-agent/deployment.c
is in fact only related to deployment
resume/stop.
Request¶
Report¶
The agent publishes a periodical report to the MQTT broker
with information about its state
and that of its modules, module instances, and system apps,
as well as some other system information.
The periodicity of this report is determined
by the EVP_REPORT_STATUS_INTERVAL_MIN_SEC
and EVP_REPORT_STATUS_INTERVAL_MAX_SEC
Kconfig
variable.
The periodical report is not sent
if its contents were not changed from the last report.
The logic for the periodical report
is implemented by src/libevp-agent/report.c
.
Telemetry¶
Multi-storage token provider (mSTP) cache¶
The agent can store the tokens used to access cloud storage providers into local storage, so that they can be accessed while the device is disconnected from the Hub.
mSTP cache can only be available if storage provider supports multi-file with the same token.
The Hub provides the knowledge if
storage supports multi-file in
storagetoken-response
if
responseType
field is multifile
.
Note
Currently, multi-file is only supported with Azure.
This cache has been designed as a single-file JSON database with the following format:
[
{
"instanceName": <string>
"remoteName": <string>
"storageName": <string>,
"storagetoken-response": {
"responseType": "multifile",
...
}
}
]
Only one entry with the same instanceName
, remoteName
and storageName
can exist within the database.
The cache can be manipulated via the following functions:
int blob_type_evp_load(const struct evp_agent_context *agent, const struct blob_work *wk, struct storagetoken_response *resp);
int blob_type_evp_store(const struct blob_work *wk, const JSON_Value *v);
where:
blob_type_evp_load
, as suggested, loads an entry from the cache.agent
is thestruct
holding agent-specific information, and it is required to determine how to parse thestoragetoken-response
JSON object based on the EVP hub version.wk
defines theinstanceName
,remoteName
andstorageName
that must be looked up on the database so as to retrieve its matchingstoragetoken-response
.resp
is the object that shall be filled once a matching entry is found.blob_type_evp_store
writes an entry, as defined bywk
andv
, into the cache. Existing entries with matchinginstanceName
,remoteName
andstorageName
shall be replaced with the new entry.
Hub¶
Apart from the configured EVP version, which defines the onwire protocol, the agent can connect to different IoT platforms, which in turn encapsulate the on-write protocol defined by the EVP version.
However,
only Thingsboard is supported,
which is implemented by src/libevp-agent/hub/tb/tb.c
and src/libevp-agent/hub/hub_tb.c
.
Streams¶
EVP streams are meant as a communication mechanism between module instances. Their design is closely inspired by the POSIX sockets interface, but streams are opinionated towards asynchronous communication.
Read Experimental features for further reference.
Transport¶
The agent interacts with several network services during its execution,
with a variety of protocols.
The transport
component is therefore meant as a thin abstraction layer
over the MQTT client library
and implements higher-level operations
such as reconnections, sending messages, or subscribing to a topic.
PAL¶
Being a platform-agnostic library,
MQTT-C splits platform-specific details
into src/libevp-agent/MQTT-C/src/mqtt_pal.c
.
TLS¶
The agent relies on the Mbed-TLS library for cryptographic operations.
MQTT¶
The agent relies on the MQTT-C library to handle MQTT connections.
Web Client¶
For HTTP operations,
the agent relies on the
WebClient library,
and defines src/libevp-agent/webclient_mbedtls.c
to implement the
TLS-specific details required by webclient
.
On the other hand,
since users can disconnect the agent from the network using the embedded API
(i.e., via evp_agent_disconnect()
),
src/libevp-agent/connections.c
is defined
as a thin wrapper over webclient
that stops ongoing HTTP operations if required.