Custom Object Baseline
This is the baseline_client featuring an additional LwM2M Object with the following definition:
ID: 3200
Single instance
Resources:
ID | Type | Operations | Multiple | Description |
---|---|---|---|---|
5500 | Boolean | R | Single | A read-only boolean value |
5750 | String | R/W | Single | A writable string |
5503 | Integer | R/W | Single | A writable integer value |
In the LightweigthM2M protocol, the Objects and the Resources are only characterized by their ID and there is no metadata exchanges between the LwM2M Clients and the LwM2M Server. This means that the LwM2M Server must know the layout of the Object and the characteristics of their Resources. (Note that the LwM2M Server can perform a Discovery operation on the Object that will return the list of instantiated Resources in each Object Instance.)
When designing a custom Object for your application, you must communicate your Object definition to the LwM2M Server out-of-band. One way to achieve this is by registering your Object to the OMNA LwM2M Repository.
In this sample we reuse a registered Object: Digital Input (ID: 3200) to make sure it is known by the LwM2M Server we are testing with. In a real-world application needing this specific IPSO Object, you can use the
iowa_client_IPSO_add_sensor()
API.
The following API will be explained:
iowa_client_add_custom_object()
iowa_client_remove_custom_object()
Usage
The usage is the same as the Baseline Client sample.
When registered to the LwM2M Server, you will notice that the custom_object_baseline_client features the additional LwM2M Object described above:
The Resource names displayed by the LwM2M Server are the names registered for the Object with this specific ID at the OMNA LwM2M Repository. As explained, we are using this Object ID to make sure it is known by the LwM2M Server. Of course, the Resources do not reflect any real process.
From here, you can test the LwM2M operations on the Resources. You will notice that you can not write the "Digital Input State" Resource (URI: /3200/0/5500).
If you set an Observation on the Object or the Object Instance, when you write a new value for the "Digital Input Debounce" Resource (URI: /3200/0/5503) or the "Application Type" Resource (URI: /3200/0/5750), you will receive a notification.
After two minutes, custom_object_baseline_client unregisters from the LwM2M Server.
Breakdown
Client Pseudo Code
This is the pseudo code of Client main function:
main()
{
// Initialization
iowa_init();
// LwM2M Client configuration
iowa_client_configure(CLIENT_NAME);
// Custom Object addition
iowa_client_add_custom_object(OBJECT_ID, resourceDescription, dataCallback);
// LwM2M Server declaration
iowa_client_add_server(SERVER_SHORT_ID, SERVER_URI, SERVER_LIFETIME);
// "Main loop"
iowa_step(120s)
// Cleanup
iowa_client_remove_custom_object(OBJECT_ID);
iowa_client_IPSO_remove_sensor();
iowa_close();
}
and the pseudo code of the Object data callback:
dataCallback(operation, targetedResources)
{
for each targetedResources
if operation is READ
then targetedResources.value = resource value
if operation is WRITE
then resource value = targetedResources.value
}
Data Structures
This sample introduces two important data structures of IOWA.
iowa_lwm2m_resource_desc_t
This data structure is used to describe an LwM2M Object Resource.
typedef struct
{
uint16_t id;
iowa_lwm2m_data_type_t type;
uint8_t operations;
uint8_t flags;
} iowa_lwm2m_resource_desc_t;
id
is the numerical ID of the Resource. This ID must be unique among the Object Resource IDs.
type
is the data type of the Resource value.
operations
is the list of LwM2M operations allowed on the resources. The only allowed combinations are: "IOWA_DM_READ", "IOWA_DM_WRITE", "IOWA_DM_READ | IOWA_DM_WRITE", and "IOWA_DM_EXECUTE".
flags
will be explained in other samples.
iowa_lwm2m_data_t
This data structure is used to convey the Resource values between IOWA and the Object implementation.
typedef struct
{
uint16_t objectID;
uint16_t instanceID;
uint16_t resourceID;
uint16_t resInstanceID;
iowa_lwm2m_data_type_t type;
union
{
bool asBoolean;
int64_t asInteger;
double asFloat;
struct
{
size_t length;
uint8_t *buffer;
} asBuffer;
struct
{
size_t totalSize;
uint32_t details;
uint8_t *buffer;
} asBlock;
iowa_lwm2m_object_link_t asObjLink;
} value;
int32_t timestamp;
} iowa_lwm2m_data_t;
objectID
, instanceID
, and resourceID
contain the URI of the Resource. Some Resources may have multiple instances. In this case resInstanceID
is used, otherwise (like in this sample), it is set to IOWA_LWM2M_ID_ALL.
type
contains the data type of the Resource value.
value
contains the value of the Resource. It is an union depending on type
:
-
value.asBoolean
whentype
is IOWA_LWM2M_TYPE_BOOLEAN. -
value.asInteger
whentype
is IOWA_LWM2M_TYPE_INTEGER, IOWA_LWM2M_TYPE_TIME or IOWA_LWM2M_TYPE_UNSIGNED_INTEGER. -
value.asFloat
whentype
is IOWA_LWM2M_TYPE_FLOAT. -
value.asBuffer
whentype
is IOWA_LWM2M_TYPE_CORE_LINK, IOWA_LWM2M_TYPE_STRING, IOWA_LWM2M_TYPE_OPAQUE or IOWA_LWM2M_TYPE_UNDEFINED. -
value.asObjLink
whentype
is IOWA_LWM2M_TYPE_OBJECT_LINK. -
value.asBlock
is explained in another sample.
timestamp
is explained in another sample.
Main Function
Initialization
This step is the same as in the Baseline Client sample.
LwM2M Client Configuration
This step is the same as in the Baseline Client sample.
Custom Object Addition
Here we add a custom Object to the LwM2M Client.
sample_object_values_t objectValues;
iowa_lwm2m_resource_desc_t sample_object_resources[SAMPLE_RES_COUNT] =
{
{5500, IOWA_LWM2M_TYPE_BOOLEAN, IOWA_OPERATION_READ, IOWA_RESOURCE_FLAG_NONE},
{5750, IOWA_LWM2M_TYPE_STRING, IOWA_OPERATION_READ | IOWA_OPERATION_WRITE, IOWA_RESOURCE_FLAG_NONE},
{5503, IOWA_LWM2M_TYPE_INTEGER, IOWA_OPERATION_READ | IOWA_OPERATION_WRITE, IOWA_RESOURCE_FLAG_NONE}
}
objectValues.booleanValue = true;
objectValues.integerValue = 10;
objectValues.stringValue = NULL;
result = iowa_client_add_custom_object(iowaH,
SAMPLE_OBJECT_ID,
0, NULL,
SAMPLE_RES_COUNT, sample_object_resources,
sample_object_dataCallback,
NULL,
NULL,
&objectValues);
As always, the first argument is the IOWA context created in the Initialization step.
The second argument is the Object numerical ID. This ID must be unique.
The third argument is the number of Instances this Object has, the fourth argument is the list of the IDs of these Instances. This parameter is useful only for multiple-instance Objects. As we do not provide a list of Instances and the instance callback (the eighth parameter) is nil, IOWA consider this Object to be a single-instance one. See the sample custom_object_multiple_instances for more details.
The fifth and sixth arguments are the Object's Resources definition. This is an array of iowa_lwm2m_resource_desc_t
.
The seventh argument is the callback IOWA will call when an operation is to be performed on the Object's Resources. It is explained in more details below.
The eighth and ninth arguments are callbacks called by IOWA to manipulate the Object. There are explained in other samples.
The last argument is an user defined pointer passed as argument to the Object callbacks. In this example, it is a structure holding the Object values.
LwM2M Server Declaration
This step is the same as in the Baseline Client sample.
"Main Loop"
This step is the same as in the Baseline Client sample.
Cleanup
This step is the same as in the Baseline Client sample with one additional function call:
iowa_client_remove_custom_object(iowaH, SAMPLE_OBJECT_ID);
As always, the first argument is the IOWA context created in the Initialization step.
The second argument is the same ID of the Object as in the call to iowa_client_add_custom_object()
.
Object Data Callback
when a LwM2M Server perform an operation on our Object, IOWA takes care of all the necessary checks (Resource existence, operation rights, correct datatypes, etc...). The Data Callback deals only with the actual Resource values.
iowa_status_t sample_object_dataCallback(iowa_dm_operation_t operation,
iowa_lwm2m_data_t *dataP,
size_t numData,
void *userData,
iowa_context_t iowaH)
{
The parameters to the callback are the operation (IOWA_DM_READ, IOWA_DM_WRITE, IOWA_DM_EXECUTE, or IOWA_DM_FREE), an array of iowa_lwm2m_data_t
, the user data passed as parameter to iowa_client_add_custom_object()
, and the IOWA context.
The first thing we do is retrieving the actual values we stored in the user data:
sample_object_values_t *objectValuesP;
// Retrieve our Object values
objectValuesP = (sample_object_values_t *)userData;
Then we iterate over all the targeted Resources:
size_t i;
for (i = 0; i < numData; i++)
{
switch (dataP[i].resourceID)
{
We do not check dataP[i].objectID
and dataP[i].instanceID
as the callback is used only by our Object. These need to be considered only when the Object has several instances or if the callback is shared among several custom Objects.
case 5503:
if (operation == IOWA_DM_READ)
{
dataP[i].value.asInteger = objectValuesP->integerValue;
}
else if (operation == IOWA_DM_WRITE)
{
objectValuesP->integerValue = dataP[i].value.asInteger;
}
break;
For each Resource, if the operation is IOWA_DM_READ, we store the Resource value in the iowa_lwm2m_data_t
. If the operation is IOWA_DM_WRITE, we update the Resource value with the value from the iowa_lwm2m_data_t
.
The IOWA_DM_FREE Operation
To handle the IOWA_DM_READ case, the callback may allocate memory. For instance the Resource could be the content of a log file. When IOWA has no longer the use of a resource value, it calls the callback with the operation IOWA_DM_FREE.
This is illustrated in the sample by the Resource holding a string value (ID: 5750):
case 5750:
if (operation == IOWA_DM_READ)
{
if (objectValuesP->stringValue != NULL)
{
// For the sake of the example, we return a copy of our string value.
dataP[i].value.asBuffer.length = strlen(objectValuesP->stringValue);
dataP[i].value.asBuffer.buffer = strdup(objectValuesP->stringValue);
if (dataP[i].value.asBuffer.buffer == NULL)
{
return IOWA_COAP_500_INTERNAL_SERVER_ERROR;
}
}
else
{
dataP[i].value.asBuffer.length = 0;
dataP[i].value.asBuffer.buffer = NULL;
}
}
else if (operation == IOWA_DM_WRITE)
{
free(objectValuesP->stringValue);
objectValuesP->stringValue = (char *)malloc(dataP[i].value.asBuffer.length + 1);
if (objectValuesP->stringValue == NULL)
{
return IOWA_COAP_500_INTERNAL_SERVER_ERROR;
}
memcpy(objectValuesP->stringValue, dataP[i].value.asBuffer.buffer, dataP[i].value.asBuffer.length);
objectValuesP->stringValue[dataP[i].value.asBuffer.length] = 0;
}
else if (operation == IOWA_DM_FREE)
{
// IOWA has no longer use of the string value. We can free it.
free(dataP[i].value.asBuffer.buffer);
}
break;
When the operation is IOWA_DM_READ (and there is a value to return), we allocate a new string and duplicate the value. We pass the pointer to this new string in the iowa_lwm2m_data_t
.
Then when the callback is called with IOWA_DM_FREE, we can free the duplicate string as the iowa_lwm2m_data_t
still holds the pointer to it.
When handling the IOWA_DM_READ case, it is also possible for the callback to just return the pointer to our string value. IOWA will not modify it. In this case, there is nothing to do when the operation is IOWA_DM_FREE.
Next Steps
The sample illustrates only the baseline of the creation of a custom LwM2M Object. The other custom object samples dive into more useful concepts.