APIs with an Example
We use an example to teach how to use Libertas Cluster Library APIs.
The On/Off Light Example
nrf_switch is an example firmware of an on/off light. It is a fully functional firmware using the Hornet mesh network.
An example of other protocols, such as Thread/Matter, will be released later.
The Source Code
The full source code can be found here.
The Device
The device is an nRF52840 dongle, emulating an on/off light device.
Local and Remote Endpoints
This example is a minimum working example. The dongle only emulates an on/off light. The on/off light is an endpoint.
Because the demo is designed to transparently support Libertas Thing-App, the Thing-App management is assigned another endpoint.
The above two endpoints are inherent to the device. They are called “local endpoints.”
The developer knows the information about initial local endpoints. The developer shall write code to initialize the local endpoints.
Once the device joins a mesh network, the management tool may bind a local endpoint with multiple endpoints of remote devices. Those endpoints by binding are called “remote endpoints.”
Note remote endpoints are unknown to developers at the development time. Also, endpoints may change dynamically during runtime. The central controller (Libertas Hub) may add or drop remote points according to users’ requests.
The Libertas framework passes a pointer to a remote endpoint to developer-implemented callback functions. Since remote endpoints are managed dynamically, the firmware code usually shall not keep a copy of the pointer to a remote endpoint out of callback contexts.
Exchanging data between a local endpoint and associated remote endpoints and synchronizing consensus states among bound endpoints are part of the features of this Libertas Cluster Library.
Two data structures are defined:
libertas_ep_data_local
libertas_ep_data_remote
Initialization
The developer is required to implement this function.
void libertas_impl_init_device();
In this example, the initialization performs the following:
- Initialize the hardware, including the LED (the light) and the button.
- The device is a USB dongle. If the user holds the button while powering up and continues holding up for 10 seconds, the device shall perform a “factory reset.” The initial detection is in this function.
- Initialize the local endpoints.
- Report the initial on/off state attribute (always off initially) to bound peers
- Load persistent configuration from flash memory using Flash DB API
Initialize Local Endpoints
The developer is responsible for initializing the local endpoints.
This device has two endpoints:
1. The on/off light endpoint, `EP_LOAD`
2. The Thing-App endpoint, `EP_APP`
libertas_update_local_ep
Updating Attributes
In the initialization code, the firmware must report the initial on/off state attribute (always off initially) to bound peers.
Macro LIBERTAS_UPDATE_ATTR(ep_data,cluster_id,attr_id,action,exclude)
LIBERTAS_UPDATE_ATTR
is used to update an attribute. In this example:
const libertas_ep_data_local* ep_load_data = libertas_get_ep_data_local(EP_LOAD);
LIBERTAS_UPDATE_ATTR(
ep_load_data,
LIBERTAS_CLUSTER_GEN_ON_OFF,
LIBERTAS_ATTR_ON_OFF,
LIBERTAS_ATTR_ACTION_REPORT,
NULL);
The first argument is a pointer to a local endpoint. The second argument is a cluster ID.
- The third argument is an attribute ID.
- The fourth argument is an action
- This device is a load device, so it sends a report (
LIBERTAS_ATTR_ACTION_REPORT
) to all bound peers - If this device is not a load, e.g., a light switch, we should use
LIBERTAS_ATTR_ACTION_QUERY
to query the bound peer
- This device is a load device, so it sends a report (
- The fifth argument is a remote endpoint to exclude. We will discuss it later.
Note the macro only “marks” the attribute for reporting. It does not send the report right away. The report will be sent out as soon as possible.
- Multiple calls of
LIBERTAS_UPDATE_ATTR
can be made. The attributes may be queued as a batch and sent out within a single report message - Every time an attribute is changed,
LIBERTAS_UPDATE_ATTR
shall be called to make the attribute toLIBERTAS_ATTR_ACTION_WRITE
orLIBERTAS_ATTR_ACTION_REPORT
- Attributes are states instead of history. Only the last modified value counts. If an attribute is modified too fast, some changes will automatically be “skipped,” and only the last modified will be sent out
- Note that this call doesn’t care about the attribute value. When the actual attribute message is composed, the values will be asked again through a call to
libertas_impl_get_local_attr_sets.
The developer must implement that function to supply the values.
Macro LIBERTAS_UPDATE_ATTR_SET(ep_data,cluster_id,attr_set,action,exclude)
LIBERTAS_UPDATE_ATTR_SET
is used to update every attribute in an attribute set.
Both macros are designed to be intelli-sense friendly. The cluster name, attribute name, and attribute set name are all predefined symbols. The generated code is efficient with O(1) complexity.
Sending a Command
Whenever a user presses the button, the on/off of the light will be changed. The device shall send this change command to all listening (outgoing) bound peers.
Note that v
is a boolean value of on/off. The command ID depends on the value of v, either LIBERTAS_CMD_ON
or LIBERTAS_CMD_OFF.
The command doesn’t have a body, so the body is NULL, and the body length is 0.
The last one is a remote endpoint to exclude; in this case, it is NULL.
const libertas_ep_data_local* ep_data = libertas_get_ep_data_local(EP_LOAD);
libertas_update_cmd_req(
ep_data,
LIBERTAS_CLUSTER_GEN_ON_OFF,
(v) ? LIBERTAS_CMD_ON : LIBERTAS_CMD_OFF,
NULL,
0,
NULL);
Managing Attributes
Attribute Commands
There are four commands.
- ZCL_CMD_READ - Query attributes
- ZCL_CMD_READ_RSP - Result of query request from the peer
- ZCL_CMD_WRITE - Write attributes
- ZCL_CMD_REPORT - Report attributes
The difference between “write” and “report” is:
- Write is an action, report is a status
- Write may represent an immediate state change
- Report can be sent out routinely; the value could be aged instead of immediate
Only ZCL_CMD_READ
is read; the other three commands are updates.
Memory Management
An attribute set is a struct
of attribute values. An attribute_sets is a struct of multiple attribute set pointers. Before the function libertas_impl_get_local_attr_sets
is called, the pointers (of attribute set) of the attr_sets
have been initialized, and the pointers of memory have been properly allocated from a temporary memory block. So the code can fill in the values.
The firmware developers shall manage the storage of attributes on their own. Nevertheless, the attribute data model is decided by Libertas with pre-generated C code. Read Attribute Set for more details.
Request Attribute Data On-demand
Whenever the library code needs the attribute data, it will call a callback function. The developer shall implement the callback and provide the data on demand.
In the example below, the implementation code “fills in” the values of the entire attribute sets according to the cluster-ID. The void* attr_sets
shall be cast to the correct attribute sets type based on the value of cluster-ID.
void libertas_impl_get_local_attr_sets(const libertas_ep_data_remote* remote_ep_data, uint16_t cluster, uint8_t cmd, uint8_t is_req, void* attr_sets) {
if (LIBERTAS_CLUSTER_GEN_ON_OFF == cluster) {
libertas_cluster_attr_sets_gen_on_off *onoff_sets = (libertas_cluster_attr_sets_gen_on_off *)attr_sets;
onoff_sets->on_off->on_off = load_on_off;
onoff_sets->on_off->on_time = 0xffff;
if (TIMED_ON == timed_state) {
uint32_t on_ticks = ((uint32_t)load_on_time) * SYS_CLOCK_SECOND / 10;
uint32_t ticks_elapsed = sys_clock_ticks() - timed_start_ticks;
uint32_t ticks_left = (on_ticks > ticks_elapsed) ? (on_ticks - ticks_elapsed) : 0;
onoff_sets->on_off->on_time = (ticks_left * 10) / SYS_CLOCK_SECOND;
}
onoff_sets->on_off->off_wait_time = 0;
if (TIMED_NA == timed_state) {
onoff_sets->on_off->off_wait_time = load_off_wait_time;
if (TIMED_OFF == timed_state) {
uint32_t off_ticks = ((uint32_t)load_on_time) * SYS_CLOCK_SECOND / 10;
uint32_t ticks_elapsed = sys_clock_ticks() - timed_start_ticks;
uint32_t ticks_left = (off_ticks > ticks_elapsed) ? (off_ticks - ticks_elapsed) : 0;
onoff_sets->on_off->off_wait_time = (ticks_left * 10) / SYS_CLOCK_SECOND;
}
}
} else if (LIBERTAS_CLUSTER_GEN_BASIC == cluster) {
libertas_cluster_attr_sets_gen_basic *basic_sets = (libertas_cluster_attr_sets_gen_basic *)attr_sets;
basic_sets->gen_basic_persistent->device_enabled = load_enabled;
}
// ... ...
Attribute Callback
If the peer requests a list of attributes to read after supplying the data in libertas_impl_get_local_attr_sets,
the framework automatically generates the response with the supplied data.
If the peer sends a write, report, or read response after supplying the data in libertas_impl_get_local_attr_sets
call, the framework will apply the new data on the old data set and then trigger the callback with the resulting data sets.
void libertas_impl_on_input(
const libertas_ep_data_remote* remote_ep_data,
uint8_t possible_dup,
uint16_t cluster,
uint8_t cmd,
const void* attr_sets,
uint8_t* status);
Note that the request could be a duplicate; a duplicate may be treated differently. However, an implementation must supply a resulting status.
Status is usually ZCL_STATUS_SUCCESS
or an error code.
Temporary Buffer
A temporary buffer that holds the data of the entire attribute_sets of a cluster is passed to two callback functions in the form of void* attr_sets.
libertas_impl_get_local_attr_sets
libertas_impl_on_input
The developer must first cast the void* attr_sets
pointer into a more meaningful pointer according to the cluster. For example, if the cluster is LIBERTAS_CLUSTER_GEN_ON_OFF,
the type of the attr_sets pointer is libertas_cluster_attr_sets_gen_on_off *.
if (LIBERTAS_CLUSTER_GEN_ON_OFF == cluster) {
libertas_cluster_attr_sets_gen_on_off *onoff = (libertas_cluster_attr_sets_gen_on_off *)attr_sets;
// ... ...
The framework manages this buffer memory, and the buffer is only valid within the context of the callback function.
Fill in Values to attribute_sets
There are two ways to fill in values to the attribute_sets.
- Assign value to every field of every attribute_set in this attribute_sets. Note attribute_sets is a collection of pointers to more than one attribute_set.
- Keep a copy of an attribute_set instance somewhere and assign the pointer to the corresponding pointer in the structure.
An example of the first method is in the example source code:
onoff_sets->on_off->on_off = load_on_off;
onoff_sets->on_off->on_time = 0xffff;
// ... ...
An example of the method two is demonstrated below. There is an instance of libertas_attr_set_on_off
named on_off_data.
Instead of setting the contents of onoff_sets->on_off, we can directly assign the onoff_sets->on_off pointer.
static libertas_attr_set_on_off on_off_data;
// ... ...
onoff_sets->on_off = &on_off_data;
Libertas design gives the developer complete flexibility in how to manage the data. We don’t care how the data is calculated or stored as long as the data is supplied on demand.
Double Buffering
For write operations, including “ZCL_CMD_READ_RSP,” “ZCL_CMD_WRITE,” or “ZCL_CMD_REPORT,” the temporary buffer attr_sets
serves as a double buffer to protect outside data.
Even if the developer supplies a pointer to their own managed structure in libertas_impl_get_local_attr_sets
(as method two in Fill in Values to attribute_sets), the data won’t be overwritten. Instead, our framework will detect these “alien pointers” and automatically make a copy of the data into a safe temporary memory.
In other words, in the above example of Fill in Values to attribute_sets, the pointer to on_off_data
is supplied. After the framework processes a “write” operation, the content of on_off_data
will not be affected by this “write” operation.
static libertas_attr_set_on_off on_off_data;
// ... ...
onoff_sets->on_off = &on_off_data;
Internally, the framework first copies the on_off_data
to a safe temporary memory, reassigns the value of onoff_sets->on_off
with the new safe memory, and then processes the “write” operation. As a result, the onoff_sets->on_off
will point to a structure with a mixture of the old on_off_data
data and some newly written attributes.
Note for “write” operations: filling in any data to attr_sets
is not even mandatory. However, after the “write” operation, the attr_sets
will only contain the new attributes in the command message. The rest are all garbage. Developers may find it difficult to process a data structure like that.
Attributes Access List
An attribute command may only access a subset of attributes within a cluster. It is crucial to know which attribute (by ID) is accessed.
There are two macros to detect whether an attribute or an attribute set is accessed.
LIBERTAS_ATTR_ACCESSED(cluster_id,attr_set)
LIBERTAS_ATTR_SET_ACCESSED(cluster_id,attr_set)
Those are macros because we can make them intelli-sense friendly so the code editor can prompt the correct names.
Note that those two macros are only valid within attribute-related callback functions. Calling the macros outside of the context will cause assertion failure.
Incoming Command
A callback is called when an incoming command is received from a remote endpoint.
zcl_handling_inst libertas_impl_on_cmd(
const libertas_ep_data_remote* remote_ep_data,
uint8_t possible_dup,
uint16_t cluster,
const zcl_header_t* header,
const uint8_t* payload,
const uint8_t* payload_end,
uint8_t *status);
- The command could be a duplicate indicated by
possible_dup
- The cluster ID, ZCL header, and payload are also passed
- The function must return a status code and a handling instruction (zcl_handling_inst)
typedef enum {
ZCL_FRAME_NOT_HANDLED, // Frame is not handled, shall be passed on to next handler!
ZCL_FRAME_HANDLED, // Frame is handled
ZCL_FRAME_DELAYED_RSP, // The implementation will send a response later. The framework shall mark a flag to expect such a response.
ZCL_FRAME_DEFAULT_RSP, // Framework shall send default rsp with returned status
} zcl_handling_inst;
Response and Acknowledgement
All application messages shall be responded to or acknowledged unless it is a broadcast or multicast.
The framework will process the incoming messages. A proper callback function is called if a message is a response or an acknowledgment.
The developer is responsible for implementing those callback functions.
Suppose the sequence number of an incoming response doesn’t match the last outgoing request sequence number. In that case, the framework will assume it is duplicated and discard the message as if nothing happened.
void libertas_impl_on_attr_error(const libertas_ep_data_remote* remote_ep_data, uint8_t status);
void libertas_impl_on_update_rsp(
const libertas_ep_data_remote* remote_ep_data,
uint16_t cluster,
uint8_t status,
const zcl_write_attribute_status_t* attr_status,
const zcl_write_attribute_status_t* end_attr_status);
void libertas_impl_on_cmd_status_rsp(const libertas_ep_data_remote* remote_ep_data, uint16_t cluster, const uint8_t* header, uint16_t payload_len, uint8_t status);
Note each callback is passed with a status
code. For libertas_impl_on_update_rsp
and libertas_impl_on_cmd_status_rsp,
the status
could be ZCL_STATUS_SUCCESS.
If there is an error code, the status doesn’t have to be limited to application error codes. It could be an error in other layers, such as NWK_STATUS_ROUTE_DISCOVERY_FAILED
(node is unreachable).
For libertas_impl_on_update_rsp,
each attribute sent out may have a separate error code stored between attr_status
and end_attr_status.
There is no error in most cases, so the value is NULL.
Make sure NULL
is checked.
State Consensus Among Bound Endpoints
In the binding example, when the light endpoint receives a command such as “LIBERTAS_CMD_ON” or “LIBERTAS_CMD_OFF,” the light shall be turned on or off accordingly.
In the meantime, the light shall notify the state change to other bound endpoints. The state change will change the LED status on the switch panel.
Excluding a Remote Endpoint
Our code performs the state consensus by re-sending the command to other peers, except for the message originator, because the original command has already been acknowledged, and the originator already knows the state change. We use the “exclude” argument in libertas_update_cmd_req
or LIBERTAS_UPDATE_ATTR.
libertas_update_cmd_req(
remote_ep_data->header.local_ep_data,
cluster,
header->cmd,
payload,
payload_end - payload,
remote_ep_data);
LIBERTAS_UPDATE_ATTR
and LIBERTAS_UPDATE_ATTR_SET
also take a exclude argument.
Special Treatment for Attributes Write
When a local endpoint receives an “attributes write” command, it may want to propagate the exact writes to other bound endpoints (except for the originator). We have a helper function for that.
Using the function eliminates the need to check which attributes are included in the write with LIBERTAS_ATTR_ACCESSED.
void libertas_forward_attributes();
Error Handling
In most cases, if a device receives a permanent application-level error, there is not much it can do. One side must be picked out to be at fault, and new firmware must fix the defect!
For non-permanent errors, the effort can be retried. The retry policy is up to the developer based on the nature of the application. Here are some general rules.
- If an action such as a command or “write attribute” fails, the retry shall not be the same action; it should be “report attributes” instead.
- The application framework maintains a timer to schedule a retry. Generally, the more retries, the longer the timer should wait, though there can be an upper limit.
Wait-Retry Helper Functions
By default, after the developer-implemented error handler callback returns, the state changes (command or attribute) for the remote endpoint will be automatically discarded unless a retry is scheduled within the error handler callback code.
libertas_remote_ep_wait
must be called within the error handler context, wait_type
must be EP_WAIT_NET_ERR.
void libertas_remote_ep_wait(libertas_ep_data_remote* remote_ep_data, libertas_ep_wait_t wait_type, int32_t wait_ticks);
If the retry is a “write attributes,” changing the command to “report attributes” is recommended. There is a helper function for this:
void libertas_remote_ep_pending_attributes_change_cmd(libertas_ep_data_remote* remote_ep_data, uint8_t new_cmd);
The libertas_remote_ep_pending_attributes_change_cmd
function shall be called within the context of the error handler callback functions before or after libertas_remote_ep_wait.
Event Polling
The framework maintains the event loop. The developer is responsible for implementing a “poll” function that will be called in every event loop.
void libertas_impl_poll();
Note that hornet_os_main_thread_set_next_wakeup
is used to schedule the next wakeup ticks. The event loop will wake up when the expiration ticks is reached, or wake up early on hardware and network events. So, we won’t miss anything.
void hornet_os_main_thread_set_next_wakeup(uint32_t ticks);
Other Details
Persistent Attributes
This firmware supports the standard “General” cluster’s “Device Enabled” attribute. We store the attribute set (libertas_attr_set_gen_basic_persistent) into flash memory as a Flash DB record.
The stored record will be loaded during startup. A new device without record in Flash DB will get a default value.
If the value of “Device Enabled” attribute is “false”, the user can not use the button on dongle to control the light on the dongle.
A user can change the value of “Device Enabled” using Libertas smartphone tool. Read “Managing Devices” for more details.
On/Off with A Timer
The firmware supports “ON_WITH_TIMED_OFF” standard command. The command has two fields:
- On Time Field - The On Time field is 16 bits in length and specifies the length of time (in 1/10ths second) that the lamp is to remain “on”, i.e., with its OnOff attribute equal to 0x01, before automatically turning “off”.
- Off Wait Time - The Off Wait Time field is 16 bits in length and specifies the length of time (in 1/10ths second) that the lamp shall remain “off”, i.e., with its OnOff attribute equal to 0x00, and guarded to prevent an on command turning the light back “on”.
Read the source code to learn how we implemented the feature, with one state variable (timed_state
) and one 32 bit time variable (timed_expire_ticks
).