Embedded Edge Agent API

This document contains information on the Embedded Edge Agent API, including how to integrate the EEA into your native code.

Definitions

  • i8: 8-bit signed integer.
  • i16: 16-bit signed integer.
  • i32: 32-bit signed integer.
  • i64: 64-bit signed integer.
  • u8: 8-bit unsigned integer.
  • u16: 16-bit unsigned integer.
  • u32: 32-bit unsigned integer.
  • u64: 64-bit unsigned integer.
  • f32: 32-bit floating-point number.
  • f64: 64-bit floating-point number.
  • WASM: Short for WebAssembly.
  • EEA: Short for Embedded Edge Agent.
  • bundle: The compiled WASM that is deployed to devices. Contains both the EEA and all deployed workflows.
  • native code: The user-provided code instantiating and managing the WebAssembly module.
  • imported function: Function defined in native code and invoked the EEA.
  • exported function: Function defined in the EEA and invoked by the native code.

EEA Versioning

Everything outlined in this document represents the interface (or API) between the EEA and your native code. Any time “version” is mentioned in the context of EEA, it is referencing the version of the interface defined in this document.

Losant follows semantic versioning, and the major version (1.x.x → 2.x.x) will only change if the interface in this document changes in a way that becomes incompatible with existing native code.

For example, changing the signature of any imported or exported function will break compatibility with native code and result in a major version change (1.x.x → 2.x.x). However, adding a new exported function does not break compatibility since your native code was never invoking that function. This would result in a minor version change (1.0.0 → 1.1.0).

Since the EEA’s and your workflow’s implementations are entirely compiled into the WASM bundle, Losant has the ability to periodically update the bundle’s inner implementation without impacting the interface. This allows us to release bug fixes, performance optimizations, and entirely new workflow nodes without requiring a version change, and therefore no changes to your native code.

Because Losant periodically updates a bundle’s implementation, you may notice bundle sizes (in bytes) change. This only matters if you are allocating memory to hold bundles, because the size of the bundle may change even if no changes were made to your workflows. Each time a bundle is deployed to a device, Losant will compile the WASM bundle using the most up-to-date implementation. Losant maintains backwards compatibility, so even if an implementation has changed, the behavior will remain the same.

System Time and Timer Triggers

The current system time, in milliseconds, is passed to the EEA on each invocation of eea_loop. The system time is either since Epoch or since boot (whichever is available on your system). This timestamp is used by the EEA to determine when Timer Triggers should invoke their corresponding workflows.

Depending on the specifics of your system, there may be jumps in time due to NTP updates (Linux) or integer rollovers (embedded). Jumps in time impact Timer Triggers in the following ways:

  1. Jumps forward: If the time jumps forward, timers will use the new time as-is and assume that much time has actually passed. This could cause a timer to trigger back-to-back if the time jumps forward a significant amount (more than the configured duration of a timer).
  2. Jumps backward: If the time jumps backwards, meaning that the timestamp passed to eea_loop is less than the timestamp passed previously, timers will ignore this single iteration in their time calculation. This will result in all timers being delayed for the amount of time between invocations of eea_loop. For example, if eea_loop is called every 100 milliseconds and the time jumps backwards, the loop iteration after the jump will be ignored, which will result in timers being delayed by approximately 100 milliseconds.

Message Buffers

The EEA contains integrated memory management on top of WASM’s linear memory buffer. Because the memory is managed by the EEA, it is not possible for the native code to directly allocate memory as a way to pass data to the EEA. There is no way for the native code to know what memory addresses are available.

To solve this, the EEA pre-allocates buffers and provides pointers that can be populated by the native code as a way to send data to the EEA.

On initialization (eea_init), the EEA invokes the imported function, eea_set_message_buffers, and passes it two pointers to pre-allocated buffers (topic and payload). The native code must maintain references to these pointers for the lifetime of the WASM bundle. These pointers will change each time a new WASM bundle is initialized, even if it is the same bundle being re-initialized after something like a power cycle.

The messages buffers have the following default sizes:

  • Topic Buffer: 256 bytes
  • Payload Buffer: 4,096 bytes

The size of these buffers can be changed by invoking eea_config_set_message_buffer_lengths at any time. Whenever this function is invoked, the EEA will invoke eea_set_message_buffers with pointers to the newly allocated buffers.

These buffers are used to both forward MQTT messages to the EEA and when invoking the Direct Trigger using the eea_direct_trigger function. The required size, therefore, depends on your specific topic and payload sizes. The largest possible payload size that can be received over Losant’s MQTT broker is 256KB. The largest possible topic size is 230 bytes.

To use these buffers, the native code must populate the topic and payload buffers with UTF-8 encoded strings and then invoke eea_message_received.

eea_message_received takes two arguments:

  • The length of the topic string, in bytes.
  • The length of the payload string, in bytes.

The two length parameters represent how much data was actually written to the pre-allocated buffers. The native code must ensure that the amount of data written is less than the available capacity of each buffer.

Invoking eea_message_received can result in your workflows executing one of the following triggers, if your workflow(s) have a corresponding trigger configured:

  1. Device: Command
  2. MQTT
  3. Virtual Button

If you send a message to the EEA for a trigger that is not present on any deployed workflow, the message will be ignored. For example, if you send a message with the topic losant/<device-id>/command (which represents a Device Command) and your workflows do not have any Device: Command Triggers, the message will be ignored by the EEA and no action will be taken.

Error Codes

The following table lists the error codes that can be returned by the EEA’s exported functions. Error codes 2 through 15 will never be used by the EEA and are good options for custom error codes returned by imported and registered functions.

It’s important to use non-conflicting error codes because in some cases an exported function may return an error code from an imported function. For example, eea_init will invoke eea_storage_read. If you return an error code from eea_storage_read, the eea_init function will fail and will return your custom error as its error code.

Code Name Description
0 Success This code is returned for a successful execution of an operation.
1 General Error Return this code to signify a general error.
16 Init Already Called eea_init was called by native code after it was already called.
17 No Init Called eea_init was not invoked prior to invoking eea_loop, eea_shutdown, eea_direct_trigger, or eea_message_received.
18 Trigger Not Found eea_message_received was called, but the workflow contained no Device Command Triggers; Virtual Button Triggers matching the provided ID; or MQTT Triggers configured to fire on the provided topic. This error can also occur when invoking eea_direct_trigger with a key not matching any Direct Triggers deployed to the device.
19 Trigger Queue Full A workflow run failed to queue due to the queue size exceeding its limit. The size of the queue can be adjusted by calling eea_config_set_queue_size.
20 Invalid Trigger ID A trigger ID was not provided (or an invalid ID was provided) for a Virtual Button Trigger.
21 Storage Full A Storage: Set Value Node failed to add a value to storage as there was no more available space. The available storage space can be adjusted by calling eea_config_set_storage_size.
22 Loop Break Code (Internal use only) Signifies that a Loop Node encountered a Break Node and should break.
23 Invalid Topic An MQTT Node attempted to publish a message to an invalid topic.
24 Invalid Payload A trigger was invoked with an invalid payload; usually this means eea_direct_trigger was called with a payload that is not valid JSON.
25 Invalid Path A node failed because it attempted to read or set a value at an invalid payload path.
26 Invalid Type A node failed because an argument passed to it was not of the correct data type.
27 Invalid JSON A node failed because invalid JSON was provided as an argument.
28 Invalid FFT Input A Fast Fourier Transform Node failed because the input array contained 0 items or more than the maximum number of items (2,048).
29 Invalid Loop Iteration A Loop Node failed because it attempted to run more than the maximum number of iterations (2,048).
30 Invalid Delay Input A Delay Node failed because an invalid length of time was provided (less than 0 seconds or more than 60 seconds).
31 Invalid RMS Input A Root Mean Square Node failed because the input array contained 0 items or more than the maximum number of items (2,048).
32 Invalid Array Index An Array Node failed because an operation that required an index received an index that was out of range for the provided array (less than 0 or greater than the index of the array’s last item).
33 Array Sort Error An Array Node failed because it attempted to execute a “Sort” operation on an array containing values of incongruous types (such as a mix of strings and numbers).
34 Array Sum Error An Array Node failed because it attempted to execute a “Sum” operation on an array containing non-numeric values.
35 Invalid Store Value Input A Storage: Set Value Node failed because an operation was attempted on a value of the wrong type (such as incrementing a stored value by the amount of a string), or if a storage key resolved to a blank string.
36 Invalid Get Value Input A Storage: Get Value Node failed because the storage key resolved to a blank string.
37 Invalid Device ID eea_get_device_id failed because it returned an invalid device ID.
38 Invalid Trace Config eea_config_set_trace_level failed because it attempted to set a trace level higher than was allowed by the compiler configuration.
39 Invalid Debug Config eea_config_set_debug_enabled failed because it attempted to enable Debug Node messages when compiler configuration does not allow doing so.
40 Invalid Storage Config eea_config_set_storage_size failed because 0 was passed in as the requested storage size.

Reading the Bundle Identifier and Interface Version

The WASM bundle exposes information about itself that can be accessed by your native code.

Bundle Identifier

The bundle identifier is a string that contains the unique ID for the bundle deployed to this device. It is required when reporting back to the platform via the Hello Message. The bundle ID can be accessed in the following ways:

  • Read from the bundleIdentifier WASM custom section.
  • Decoded as a string using the following two WASM globals:

    • BUNDLE_IDENTIFIER_LENGTH: Pointer to a u8 that contains the length of the bundle ID string, in bytes.
    • BUNDLE_IDENTIFIER: Pointer to the bundle ID string.

Interface Version

The interface version represents the version of the EEA API (i.e. this document). If the signatures of any function in this document change, the version of the interface will also change. Your native code can inspect this version to ensure it contains the correct implementation to support the bundle you’ve received.

The interface version can be read from the interfaceVersion WASM custom section.

Exported Functions

These function are exported by the EEA WASM module.

eea_config_set_debug_enabled

u8 eea_config_set_debug_enabled(enabled: bool)

Changes the debug configuration for the EEA. This controls whether the EEA will invoke eea_send_message whenever a Debug Node is invoked. You may want to disable this to reduce the amount of bandwidth consumed by your device. This configuration option is constrained by the disableDebugMessage compiler option sent in the Hello Message. For example, if your device sent disableDebugMessage: true, attempting to enable debug messages using this function will have no effect and will return an error code.

Parameters

  • enabled: Whether debug messages will be sent by the EEA. Defaults to true.

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_config_set_message_buffer_lengths

u8 eea_config_set_message_buffer_lengths(topic_buffer_length: u16, payload_buffer_length: u32)

Sets the length, in bytes, of the message buffers, which are used to send data from native code to the EEA.

Parameters

  • topic_buffer_length: The size, in bytes, to pre-allocate for the message buffer’s topic. Defaults to 256 bytes.
  • payload_buffer_length: The size, in bytes, to pre-allocate for the message buffer’s payload. Defaults to 4096 bytes.

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_config_set_queue_size

u8 eea_config_set_queue_size(size: u32)

Limits the total amount of memory the workflow queue can consume. Each call to eea_loop will execute, at most, one workflow. So for example, if your native code calls eea_direct_trigger or eea_message_received several times between calls to eea_loop, each of those triggers and their corresponding workflows will be queued for eventual execution in subsequent calls to eea_loop.

If the queue is full, any attempts to trigger additional workflows will fail with Error Code 19 (triggerQueueFull).

Parameters

  • size: The maximum amount of memory, in bytes, that the workflow queue can consume. Defaults to 1024000 bytes (1024 * 1000).

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_config_set_storage_interval

u8 eea_config_set_storage_interval(interval: u32)

Sets the interval, in milliseconds, that the EEA will invoke eea_storage_save to persist all workflow storage values. Setting this value to 0 will disable persisting workflow storage and storage values will only exist in memory.

Parameters

  • interval: The interval, in milliseconds, that the EEA will persist workflow storage. Defaults to 0 (disabled).

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_config_set_storage_size

u8 eea_config_set_storage_size(size: u32)

Sets the maximum size, in bytes, that workflow storage is allowed to consume. This also controls the size of the buffer sent to eea_storage_read when the EEA needs to read persisted storage values. For embedded devices with constrained memory, you will likely be required to lower this value.

If a Storage: Set Value Node is used and the value cannot be stored because the result would cause storage to exceed this limit, the workflow will error.

Parameters

  • size: The maximum size, in bytes, that workflow storage will consume. Defaults to 1024000 (1024 * 1000).

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_config_set_trace_level

u8 eea_config_set_trace_level(level: u8)

Filters the trace logs by a specific level. The maximum level is constrained by the traceLevel compiler option sent in the Hello Message. For example, if your device sent traceLevel: 1 in the compiler options, attempting to set the trace level to 2 using this function will have no effect and will return an error code.

Parameters

  • level: The trace log level. Defaults to 0 (disabled). Options:

    • 0: Disabled
    • 1: Errors only
    • 2: All/verbose

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_direct_trigger

u8 eea_direct_trigger(id_length: u16, payload_length: u32)

Invokes one or more Direct Triggers. The trigger ID and payload must be populated using the buffers provided by eea_set_message_buffers before invoking this function. See Message Buffers for details. The payload must be a valid JSON string. Workflows can contain any number of Direct Triggers. The user-specified ID can be the same for multiple triggers. The trigger’s ID is defined using the workflow editor. This function invokes every Direct Trigger that matches the ID provided.

Parameters

  • id_length: The length, in bytes, of the UTF-8 encoded trigger ID string.
  • payload_length: The length, in bytes, of the UTF-8 encoded payload JSON string.

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_init

u8 eea_init()

Initializes the EEA. Must be the first thing called after loading the EEA WASM bundle. This function will invoke several imported functions as part of the initialization process:

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_loop

u8 eea_loop(ticks_milliseconds: u64)

The main worker loop for the EEA. This must be called continually by the native code with minimal delay between invocations (< 100ms). This function requires the current system time, in milliseconds, either since Epoch or since boot (whichever is available on your system).

A call to eea_loop will execute, at most, a single workflow in each iteration. Workflows are executed in the order they are queued. For example, if your native code queued a workflow by invoking eea_direct_trigger, and there are no other workflows queued, the next call to eea_loop will execute the workflow. That same call to eea_loop may result in a timer being triggered, but since a workflow has already run on this iteration, any workflows triggered from that timer will be queued for the next call to eea_loop.

Parameters

  • ticks_milliseconds: The milliseconds of time since Epoch or since device boot.

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_message_received

u8 eea_message_received(topic_length: u16, payload_length: u32)

Forwards MQTT data to the EEA. The topic and payload must be populated using the buffers provided by eea_set_message_buffers before invoking this function. See Message Buffers for details.

Parameters

  • topic_length: The length, in bytes, of the UTF-8 encoded topic string.
  • payload_length: The length, in bytes, of the UTF-8 encoded payload string.

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_set_connection_status

u8 eea_set_connection_status(connected: bool)

Called by your native code whenever the MQTT connection status changes. This information is primarily used to set the isConnectedToLosant field that is automatically added to every workflow payload.

Parameters

  • connected: Whether the device is connected to the MQTT broker (true = connected, false = not connected).

Returns

  • Error code. 0 for success. See Error Codes for more details.

eea_shutdown

u8 eea_shutdown()

Called by your native code prior to destroying an EEA WASM bundle. This is primarily used to invoke eea_storage_save to ensure any pending workflow storage values are persisted. The most common reason to invoke eea_shutdown is when a device receives a new WASM bundle. The native code must destroy the prior bundle before loading the new bundle.

Returns

  • Error code. 0 for success. See Error Codes for more details.

Imported Functions

These functions are defined in native code and imported into the EEA WebAssembly module.

eea_get_device_id

u8 eea_get_device_id(out_id: u8*, buffer_length: u8, out_id_length: u8*)

Invoked by the EEA to obtain the current Losant device ID. This is invoked as part of the initialization process when eea_init is called.

Parameters

  • out_id: Pointer to the pre-allocated buffer on which to place the UTF-8 encoded device ID string.
  • buffer_length: The length, in bytes, of the out_id pre-allocated buffer.
  • out_id_length: Pointer to the pre-allocated u8 to write the number of bytes written to out_id.

Returns

  • Error code. 0 for success. Return any other value to represent a custom error code.

eea_get_time

u8 eea_get_time(out_ticks_milliseconds: u64*)

Invoked by the EEA to retrieve the current system time since Epoch. If your system does not support time since Epoch, you must provide 0.

Parameters

  • out_ticks_milliseconds: The milliseconds of time since Epoch. If time since Epoch is not available, set this value to 0.

Returns

  • Error code. 0 for success. Return any other value to represent a custom error code.

eea_send_message

u8 eea_send_message(topic: u8*, topic_length: u16, payload: u8*, payload_length: u32, qos: u8)

Invoked by the EEA whenever a message needs to be sent to the Losant platform. This is primarily done whenever an MQTT, Device State, or Debug Node is executed. Every message includes a topic, payload, and quality of service (QoS) value. If your application is connected to Losant’s MQTT broker, these messages can be directly published, as-is, using your MQTT client.

Parameters

  • topic: Pointer to the UTF-8 encoded topic string.
  • topic_length: The length, in bytes, of the topic string.
  • payload: Pointer to the UTF-8 encoded payload string.
  • payload_length: The length, in bytes, of the payload string.
  • qos: The MQTT QoS value. This will be 0 for messages generated by the Debug Node and 1 for messages generated by the MQTT and Device State Nodes.

Returns

  • Error code. 0 for success. Return any other value to represent a custom error code.

eea_set_message_buffers

u8 eea_set_message_buffers(out_topic: u8*, out_topic_length: u16, out_message: u8*, out_message_length: u32)

Invoked by the EEA to provide pre-allocated buffers that are used when passing data from native code to the EEA by invoking eea_message_received and eea_direct_trigger. See Message Buffers for details.

Parameters

  • out_topic: Pre-allocated buffer to hold a message topic UTF-8 string.
  • out_topic_length: Length, in bytes, of the topic buffer.
  • out_message: Pre-allocated buffer to hold a message payload UTF-8 string.
  • out_message_length: Length, in bytes, of the message buffer.

Returns

  • Error code. 0 for success. Return any other value to represent a custom error code.

eea_sleep

u8 eea_sleep(milliseconds: u32)

Invoked by the EEA to sleep the executing thread. This is invoked whenever a Delay Node is executed.

Parameters

  • milliseconds: The amount of time to sleep, in milliseconds.

Returns

  • Error code. 0 for success. Return any other value to represent a custom error code.

eea_storage_read

u8 eea_storage_read(out_values: u8*, buffer_length: u32, out_values_length: u32*)

Invoked by the EEA to read persisted workflow storage values. This is called by eea_init whenever a new WASM bundle is initialized. If you have no persisted workflow values, your native code can return 0 without populating anything in the provided buffers. This is the companion function to eea_storage_save and the data you provide to the EEA should be identical to the data sent to your native code.

Parameters

  • out_values: Pointer to the pre-allocated buffer to place the UTF-8 encoded string of storage values.
  • buffer_length: The length, in bytes, of the pre-allocated out_values buffer. The value is determined by the value passed to eea_config_storage_set_size. Defaults to 1024000 bytes (1024 * 1000).
  • out_values_length: Pre-allocated u32 pointer to place the amount of data, in bytes, that has been written to the out_values buffer.

Returns

  • Error code. 0 for success. Return any other value to represent a custom error code.

eea_storage_save

u8 eea_storage_save(values: u8*, values_length: u32)

Invoked by the EEA to persist all workflow storage values. This function will be invoked on an interval defined by eea_config_set_storage_interval and whenever eea_shutdown is called (if the configured interval is not 0). The values are provided as a JSON string and it’s up to your native code to determine how to save the data (e.g. file or flash storage).

Parameters

  • values: Pointer to UTF-8 encoded string.
  • values_length: The length, in bytes, of the values string.

Returns

  • Error code. 0 for success. Return any other value to represent a custom error code.

eea_trace

u8 eea_trace(message: u8*, message_length: u32, level: u8)

Invoked by the EEA to log tracing information. This is useful for early development or to debug unexpected behavior. The most common practice is to write these messages to a console or standard output. Tracing can be dynamically enabled or disabled by invoking eea_config_set_trace_level.

Parameters

  • message: Pointer to UTF-8 encoded string.
  • message_length: The length of the string, in bytes.
  • level: The level of the current trace message.

Returns

  • Error code. 0 for success. Return any other value to represent a custom error code.

Registered Function API

The EEA can invoke custom functions that are defined in native code. This is primarily used to communicate with GPIO, sensors, or other peripherals. Registered functions are treated as WebAssembly imports.

To avoid naming conflicts with built-in functions, all registered functions are prefixed with eea_fn_. For example, if you configure a Registered Function Node to invoke a function named read_accelerometer, the compiled WebAssembly module will import a function named eea_fn_read_accelerometer.

Inputs

Registered functions can optionally be passed input values from the current workflow payload. Input values are limited to the following types:

  • string
  • i8, i16, i32, i64
  • u8, u16, u32, u64
  • f32, f64
  • bool
  • array of any numerical type or bool

string and array inputs are represented by two arguments when passed to registered functions:

  • Pointer to the string or array.
  • Length of the buffer (u32):

    • For strings, the length is in bytes.
    • For arrays, the length is the number of elements in the array.

For example, if a Registered Function Node contains four inputs of the following types:

  • i8
  • string
  • f64
  • array of i32 values

The following is an example C-code function signature for the registered function:

int my_registered_function(
  int8_t i8Val,
  char *stringVal,
  uint32_t stringBufferLen,
  double f64Val,
  int32_t *i32Vals,
  uint32_t i32ValsLength)

Outputs

Registered functions can optionally provide output values that will be added to the current workflow payload. All outputs are provided to the registered function as pre-allocated pointers that must be populated by the native code. Output values are limited to the following types:

  • string
  • json
  • bool
  • i8, i16, i32, i64
  • u8, u16, u32, u64
  • f32, f64
  • array of any numerical type or bool

string, array, and json outputs are represented by three arguments when passed to registered functions:

  • Pre-allocated pointer to string or array to populate.
  • The length of the pre-allocated buffer (u32):

    • For strings, the length is the number of bytes in the pre-allocated buffer.
    • For arrays, the length is the total number of elements the pre-allocated array can hold.
  • Pre-allocated u32 pointer that must be populated with the amount of data written to the buffer:

    • For strings, the length is in bytes.
    • For arrays, the length is in number of elements.

For example, if a registered function node contains four outputs of the following types:

  1. i8
  2. string
  3. f64
  4. array of i32 values

The following is an example C-code function signature for the registered function:

int my_registered_function(
  int8_t *outi8Val,
  char *outStringVal,
  uint32_t stringBufferLen,
  uint32_t *outStringBytesWritten,
  double *outf64Val,
  int32_t *outi32Vals,
  uint32_t i32ValsLength,
  uint32_t *outi32ValsWritten)

Return Values

Registered functions are required to return a u8 to represent success or error. On success, the function must return 0. If the function failed, you can return any custom u8 to represent the error. Returned error codes are added to the workflow payload.

Was this page helpful?


Still looking for help? You can also search the Losant Forums or submit your question there.