3.3. Basic Object Implementation
3.3.1. Overview
This section describes how to implement a standard Object defined in the OMA LwM2M Registry. As an example, we will implement the Temperature Object with ID 3303 in version 1.1.
3.3.2. Supported Resources
The following resources from the Temperature Object are implemented:
ID |
Name |
Operations |
Mandatory |
Type |
Description |
---|---|---|---|---|---|
5700 |
Sensor Value |
R |
Mandatory |
Float |
Last or Current Measured Value from the Sensor. |
5701 |
Sensor Units |
R |
Optional |
String |
Measurement Units Definition. |
5601 |
Min Measured Value |
R |
Optional |
Float |
The minimum value measured by the sensor since power ON or reset. |
5602 |
Max Measured Value |
R |
Optional |
Float |
The maximum value measured by the sensor since power ON or reset. |
5603 |
Min Range Value |
R |
Optional |
Float |
The minimum value that can be measured by the sensor. |
5604 |
Max Range Value |
R |
Optional |
Float |
The maximum value that can be measured by the sensor. |
5605 |
Reset Min and Max Measured Values |
E |
Optional |
Exec |
Reset the Min and Max Measured Values to Current Value. |
5750 |
Application Type |
RW |
Optional |
String |
The application type of the sensor or actuator as a string depending on the use case. |
3.3.3. Implementing the Object
Note
Code related to this tutorial can be found under examples/tutorial/BC-BasicObjectImplementation in the Anjay Lite source directory and is based on examples/tutorial/BC-MandatoryObjects example.
3.3.3.1. Step 1: Define Resource Metadata
The first step in implementing the object is to define its resources.
We start by declaring the LwM2M resource IDs as constants and creating matching array indices for easy access within the resource definition array:
#define TEMPERATURE_OID 3303
#define TEMPERATURE_RESOURCES_COUNT 8
enum {
RID_MIN_MEASURED_VALUE = 5601,
RID_MAX_MEASURED_VALUE = 5602,
RID_MIN_RANGE_VALUE = 5603,
RID_MAX_RANGE_VALUE = 5604,
RID_RESET_MIN_MAX_MEASURED_VALUES = 5605,
RID_SENSOR_VALUE = 5700,
RID_SENSOR_UNIT = 5701,
RID_APPLICATION_TYPE = 5750,
};
enum {
RID_MIN_MEASURED_VALUE_IDX = 0,
RID_MAX_MEASURED_VALUE_IDX,
RID_MIN_RANGE_VALUE_IDX,
RID_MAX_RANGE_VALUE_IDX,
RID_RESET_MIN_MAX_MEASURED_VALUES_IDX,
RID_SENSOR_VALUE_IDX,
RID_SENSOR_UNIT_IDX,
RID_APPLICATION_TYPE_IDX,
_RID_LAST
};
ANJ_STATIC_ASSERT(_RID_LAST == TEMPERATURE_RESOURCES_COUNT,
temperature_resource_count_mismatch);
static const anj_dm_res_t RES[TEMPERATURE_RESOURCES_COUNT] = {
[RID_MIN_MEASURED_VALUE_IDX] = {
.rid = RID_MIN_MEASURED_VALUE,
.type = ANJ_DATA_TYPE_DOUBLE,
.operation = ANJ_DM_RES_R
},
[RID_MAX_MEASURED_VALUE_IDX] = {
.rid = RID_MAX_MEASURED_VALUE,
.type = ANJ_DATA_TYPE_DOUBLE,
.operation = ANJ_DM_RES_R
},
[RID_MIN_RANGE_VALUE_IDX] = {
.rid = RID_MIN_RANGE_VALUE,
.type = ANJ_DATA_TYPE_DOUBLE,
.operation = ANJ_DM_RES_R
},
[RID_MAX_RANGE_VALUE_IDX] = {
.rid = RID_MAX_RANGE_VALUE,
.type = ANJ_DATA_TYPE_DOUBLE,
.operation = ANJ_DM_RES_R
},
[RID_RESET_MIN_MAX_MEASURED_VALUES_IDX] = {
.rid = RID_RESET_MIN_MAX_MEASURED_VALUES,
.operation = ANJ_DM_RES_E
},
[RID_SENSOR_VALUE_IDX] = {
.rid = RID_SENSOR_VALUE,
.type = ANJ_DATA_TYPE_DOUBLE,
.operation = ANJ_DM_RES_R
},
[RID_SENSOR_UNIT_IDX] = {
.rid = RID_SENSOR_UNIT,
.type = ANJ_DATA_TYPE_STRING,
.operation = ANJ_DM_RES_R
},
[RID_APPLICATION_TYPE_IDX] = {
.rid = RID_APPLICATION_TYPE,
.type = ANJ_DATA_TYPE_STRING,
.operation = ANJ_DM_RES_RW
}
};
The RES
array contains the definitions of the resources implemented in this object.
Each entry defines one resource and includes the following fields:
Field |
Description |
---|---|
|
Numerical ID of the resource, as per LwM2M object definition (e.g., 5700) |
|
Data format of the resource. Not set for executable resources. |
|
Permitted LwM2M operations for the resource. |
Important
In Anjay Lite, the rid
values in the resource array must appear in strictly increasing order.
Failure to comply will result in initialization errors.
3.3.3.2. Step 2: Define Object State
The state of our Temperature Object is encapsulated in the temp_obj_ctx_t
structure.
This structure holds the current sensor measurement as well as the minimum and maximum
values observed during runtime. It represents the internal state of the temperature
sensor on the device.
Additionally, the application_type
field contains a user-configurable string
that describes the intended use case of the sensor. A cached version of this string
is stored in application_type_cached
as a backup value for transaction operations.
typedef struct {
double sensor_value;
double min_sensor_value;
double max_sensor_value;
char application_type[TEMP_OBJ_APPL_TYPE_MAX_SIZE];
char application_type_cached[TEMP_OBJ_APPL_TYPE_MAX_SIZE];
} temp_obj_ctx_t;
We will also add a function declaration that returns a pointer to a statically allocated structure representing the sensor state that we will need later.
static inline temp_obj_ctx_t *get_ctx(void);
3.3.3.3. Step 3: Simulate Sensor Values
Since this is just an example and we are not using a physical temperature sensor, we simulate sensor readings by generating pseudo-random values. To achieve this, we define helper functions that return new temperature values based on the previous reading:
#define MIN_TEMP_VALUE -10
#define MAX_TEMP_VALUE 40
// Simulates a temperature sensor readout based on the previous value
static double next_temperature(double current_temp, double volatility) {
double random_change =
((double) rand() / RAND_MAX) * 2.0 - 1.0; // Random value in [-1, 1]
return current_temp + volatility * random_change;
}
static double next_temperature_with_limit(double current_temp,
double volatility) {
double new_temp = next_temperature(current_temp, volatility);
if (new_temp < MIN_TEMP_VALUE) {
return MIN_TEMP_VALUE;
} else if (new_temp > MAX_TEMP_VALUE) {
return MAX_TEMP_VALUE;
}
return new_temp;
}
The next_temperature_with_limit()
function generates a realistic temperature
value that varies slightly from the previous one while staying within the defined
range of [-10, 40] degrees. The volatility
parameter controls the magnitude
of the random variation.
We then define a function that updates the internal state of the Temperature Object instance.
void update_sensor_value(const anj_dm_obj_t *obj) {
(void) obj;
temp_obj_ctx_t *ctx = get_ctx();
ctx->sensor_value = next_temperature_with_limit(ctx->sensor_value, 0.2);
if (ctx->sensor_value < ctx->min_sensor_value) {
ctx->min_sensor_value = ctx->sensor_value;
}
if (ctx->sensor_value > ctx->max_sensor_value) {
ctx->max_sensor_value = ctx->sensor_value;
}
}
This function simulates a new sensor measurement and updates the current,
minimum, and maximum observed values accordingly. We will periodically call
update_sensor_value()
to simulate ongoing temperature updates in our LwM2M
object.
3.3.3.4. Step 4: Implement Resource Handlers
Read handler
The following function handles the LwM2M Read operation for the Temperature Object.
It reads the value of a specific resource identified by its Resource ID (rid
) and
writes the result to the out_value
output structure:
static int res_read(anj_t *anj,
const anj_dm_obj_t *obj,
anj_iid_t iid,
anj_rid_t rid,
anj_riid_t riid,
anj_res_value_t *out_value) {
(void) anj;
(void) obj;
(void) iid;
(void) riid;
temp_obj_ctx_t *temp_obj_ctx = get_ctx();
switch (rid) {
case RID_SENSOR_VALUE:
out_value->double_value = temp_obj_ctx->sensor_value;
break;
case RID_MIN_MEASURED_VALUE:
out_value->double_value = temp_obj_ctx->min_sensor_value;
break;
case RID_MAX_MEASURED_VALUE:
out_value->double_value = temp_obj_ctx->max_sensor_value;
break;
case RID_MIN_RANGE_VALUE:
out_value->double_value = MIN_TEMP_VALUE;
break;
case RID_MAX_RANGE_VALUE:
out_value->double_value = MAX_TEMP_VALUE;
break;
case RID_SENSOR_UNIT:
out_value->bytes_or_string.data = TEMP_OBJ_SENSOR_UNITS_VAL;
break;
case RID_APPLICATION_TYPE:
out_value->bytes_or_string.data = temp_obj_ctx->application_type;
break;
default:
return ANJ_DM_ERR_NOT_FOUND;
}
return 0;
}
What this handler does:
Checks the
rid
to determine which resource is being accessed.Sets
out_value
based on the current value from the context.Returns
ANJ_DM_ERR_NOT_FOUND
if the resource is not supported or not readable.
Note
This implementation assumes a single-instance object and does not distinguish between
multiple Object Instances (iid
) or Resource Instances (riid
). These values
are ignored for simplicity.
Note
In the case of ANJ_DATA_TYPE_STRING
or ANJ_DATA_TYPE_BYTES
, the read function
operates on pointers, and the value is not copied. For ANJ_DATA_TYPE_BYTES
,
the function also needs to set the length of the data being returned.
The memory pointed to must remain unchanged for the duration of the data model operation.
Write handler
This function implements the LwM2M Write operation handler for the Temperature Object. It allows modifying the values of writable resources — specifically, the Application Type (Resource ID: 5750).
static int res_write(anj_t *anj,
const anj_dm_obj_t *obj,
anj_iid_t iid,
anj_rid_t rid,
anj_riid_t riid,
const anj_res_value_t *value) {
(void) anj;
(void) obj;
(void) iid;
(void) riid;
temp_obj_ctx_t *temp_obj_ctx = get_ctx();
switch (rid) {
case RID_APPLICATION_TYPE:
return anj_dm_write_string_chunked(value,
temp_obj_ctx->application_type,
TEMP_OBJ_APPL_TYPE_MAX_SIZE, NULL);
break;
default:
return ANJ_DM_ERR_NOT_FOUND;
}
return 0;
}
What this handler does:
Checks the resource ID (
rid
); onlyRID_APPLICATION_TYPE
is handled.Uses
anj_dm_write_string_chunked()
to write the received string value toapplication_type
.Returns
ANJ_DM_ERR_NOT_FOUND
if the resource is not supported or not writable.
Note
Anjay Lite uses chunked writing to support CoAP block-wise transfers.
The helper function anj_dm_write_string_chunked()
safely assembles the received chunks
into a single buffer with size checks. For data type other then ANJ_DATA_TYPE_BYTES
and
ANJ_DATA_TYPE_STRING
the payload always comes as a single write.
Note
For data type ANJ_DATA_TYPE_BYTES
there is an alternative function called
anj_dm_write_bytes_chunked()
to handle the block-wise transfers.
Execute handler
This function implements the LwM2M Execute operation handler for the Temperature Object. It allows triggering actions on specific executable resources — in this case, resetting recorded minimum and maximum measured values.
static int res_execute(anj_t *anj,
const anj_dm_obj_t *obj,
anj_iid_t iid,
anj_rid_t rid,
const char *execute_arg,
size_t execute_arg_len) {
(void) anj;
(void) obj;
(void) iid;
(void) execute_arg;
(void) execute_arg_len;
temp_obj_ctx_t *temp_obj_ctx = get_ctx();
switch (rid) {
case RID_RESET_MIN_MAX_MEASURED_VALUES: {
temp_obj_ctx->min_sensor_value = temp_obj_ctx->sensor_value;
temp_obj_ctx->max_sensor_value = temp_obj_ctx->sensor_value;
return 0;
}
default:
break;
}
return ANJ_DM_ERR_NOT_FOUND;
}
What this handler does:
If called on resource
5605
(Reset Min and Max Measured Values), it updates bothmin_sensor_value
andmax_sensor_value
to match the currentsensor_value
.If called on any other resource, it returns
ANJ_DM_ERR_NOT_FOUND
to indicate the operation is not supported.
Note
Although execute_arg
and execute_arg_len
are available for passing arguments to executable resources,
this implementation does not make use of them, and they are explicitly ignored.
3.3.3.5. Step 5: Define and Initialize Object
Now that all handlers and data structures are defined, we can finish an implementation of
the Temperature Object. We start by defining a constant anj_dm_handlers_t
structure that references
our previously implemented handlers:
static const anj_dm_handlers_t TEMP_OBJ_HANDLERS = {
.res_read = res_read,
.res_write = res_write,
.res_execute = res_execute,
.transaction_begin = transaction_begin,
.transaction_validate = transaction_validate,
.transaction_end = transaction_end,
};
Note
Only handlers for operation we want to support in a given object need to
be defined in anj_dm_handlers_t
structure.
Next, we define the object instance using anj_dm_obj_inst_t
. This structure
includes the instance ID (iid
), the number of resources, and a pointer to the
resource array we defined earlier:
static const anj_dm_obj_inst_t INST = {
.iid = 0,
.res_count = TEMPERATURE_RESOURCES_COUNT,
.resources = RES
};
Now, define the Object itself with the anj_dm_obj_t
structure and a function
that returns a pointer to it:
static const anj_dm_obj_t OBJ = {
.oid = TEMPERATURE_OID,
.version = "1.1",
.insts = &INST,
.handlers = &TEMP_OBJ_HANDLERS,
.max_inst_count = 1
};
const anj_dm_obj_t *get_temperature_obj(void) {
return &OBJ;
}
The fields in this structure contains metadata describing the object, such as its Object ID, version, associated instances, and handlers.
Here’s a quick summary of the fields:
oid
– Object ID.
version
– Object version.
insts
– Pointer to the object instance definition.
handlers
– Reference to the function table defined above.
max_inst_count
– Maximum number of object instances.
Finally, we set the initial state of the sensor within the temp_obj_ctx_t
context structure
and define the get_ctx()
function:
static temp_obj_ctx_t temperature_ctx = {
.application_type = "Sensor_1",
.sensor_value = 10.0,
.min_sensor_value = 10.0,
.max_sensor_value = 10.0
};
static inline temp_obj_ctx_t *get_ctx(void) {
return &temperature_ctx;
}
This defines the initial sensor reading and sets the minimum and maximum to the same value.
Note
Because this object is statically allocated, the initial values can be defined directly in the initializer. If the object were dynamically created its initial state would need to be set manually during the creation process.
3.3.3.6. Step 6: Add the Object to Anjay Lite
The final step is to register the Temperature Object with Anjay Lite and simulate
periodic sensor readings. To do this, we call update_sensor_value()
inside
the main loop of the application.
int main(int argc, char *argv[]) {
if (argc != 2) {
log(L_ERROR, "No endpoint name given");
return -1;
}
srand((unsigned int) time(
NULL)); // Use the current time as a seed for the random
// generator used by update_sensor_value()
anj_t anj;
anj_dm_device_obj_t device_obj;
anj_dm_server_obj_t server_obj;
anj_dm_security_obj_t security_obj;
anj_configuration_t config = {
.endpoint_name = argv[1]
};
if (anj_core_init(&anj, &config)) {
log(L_ERROR, "Failed to initialize Anjay Lite");
return -1;
}
if (install_device_obj(&anj, &device_obj)
|| install_security_obj(&anj, &security_obj)
|| install_server_obj(&anj, &server_obj)) {
return -1;
}
if (anj_dm_add_obj(&anj, get_temperature_obj())) {
log(L_ERROR, "install_temperature_object error");
return -1;
}
while (true) {
anj_core_step(&anj);
update_sensor_value(get_temperature_obj());
usleep(50 * 1000);
}
return 0;
}
3.3.4. Supporting transactional writes
Transactional writes protect object integrity when multiple writable resources are modified. Without transaction handling, a partial update could leave the object in an inconsistent state if an error occurs during the operation.
To avoid this, Anjay Lite supports transaction mechanisms that:
Save the current state before applying changes.
Validate the updated state.
Revert changes if validation fails or a Composite Write operation fails on any resource.
To support transactional operations, we must implement three handlers:
static int transaction_begin(anj_t *anj, const anj_dm_obj_t *obj) {
(void) anj;
(void) obj;
temp_obj_ctx_t *temp_obj_ctx = get_ctx();
memcpy(temp_obj_ctx->application_type_cached,
temp_obj_ctx->application_type, TEMP_OBJ_APPL_TYPE_MAX_SIZE);
return 0;
}
static int transaction_validate(anj_t *anj, const anj_dm_obj_t *obj) {
(void) anj;
(void) obj;
// Perform validation of the object
return 0;
}
static void transaction_end(anj_t *anj, const anj_dm_obj_t *obj, int result) {
(void) anj;
(void) obj;
temp_obj_ctx_t *temp_obj_ctx = get_ctx();
if (result) {
// Restore cached data
memcpy(temp_obj_ctx->application_type,
temp_obj_ctx->application_type_cached,
TEMP_OBJ_APPL_TYPE_MAX_SIZE);
}
}
These handlers are then registered in the object’s handler structure:
static const anj_dm_handlers_t TEMP_OBJ_HANDLERS = {
.res_read = res_read,
.res_write = res_write,
.res_execute = res_execute,
.transaction_begin = transaction_begin,
.transaction_validate = transaction_validate,
.transaction_end = transaction_end,
};
What these handlers do:
transaction_begin
: Called before any operation that may modify the object. In this case, it makes a backup of theapplication_type
string.
transaction_validate
: Called after all write operations have been performed. It allows checking whether the new state is valid before committing the transaction.
transaction_end
: Called at the end of the transaction. If theresult
indicates failure, the object state is restored using the previously cached data.
Note
Implementing the transaction_validate
handler is optional. Anjay Lite
will still call transaction_end
even if transaction_validate
is not implemented,
allowing the user to restore the object state in case of an error.
That’s it! Your client is now ready to use the new LwM2M Object. Other objects can be implemented in a similar way.