3.5. Send method
The Send operation allows a LwM2M Client to transmit data to the LwM2M Server without receiving an explicit request from the Server.
To create a Send message, the client populates the anj_send_request_t
structure
with the desired resource records. These represent the data to be sent. Once the
structure is prepared, the client invokes the anj_send_new_request()
function
to register the message for transmission.
The payload can contain values from multiple resources, even if they belong to different objects, or represent time-series data for a given resource.
Note
Calling anj_send_new_request()
does not send the message
immediately. Anjay Lite queues the request and sends it only when:
A registration session with the LwM2M Server is active.
There is no other ongoing CoAP exchange in progress (e.g., a Server request, Update message, or Notification)
Note
The Send operation requires either SenML CBOR or LwM2M CBOR support to be enabled in Anjay Lite. Choose the format based on your application’s requirements.
SenML CBOR supports timestamps, which is useful for time-series data.
LwM2M CBOR cannot encode identical resource paths, but it typically produces more compact messages and is recommended when minimizing payload size is important.
3.5.1. Example
Note
Code related to this tutorial can be found under examples/tutorial/BC-Send in the Anjay Lite source directory and is based on examples/tutorial/BC-BasicObjectImplementation example.
First, set the queue size and enable Send operation (ANJ_WITH_LWM2M_SEND
)
in CMakeLists.txt:
cmake_minimum_required(VERSION 3.6.0)
project(anjay_lite_bc_send C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_EXTENSIONS OFF)
set(ANJ_WITH_LWM2M_SEND ON)
set(ANJ_LWM2M_SEND_QUEUE_SIZE 1)
This setting determines how many Send messages Anjay Lite can queue at the same time.
Next, we make a few modifications to the loop in which we call
anj_core_step()
:
anj_res_value_t value;
uint64_t next_read_time = anj_time_now() + 1000;
uint16_t send_id = 0;
anj_io_out_entry_t records[MAX_RECORDS];
fin_handler_data_t data = { 0 };
while (true) {
anj_core_step(&anj);
update_sensor_value(get_temperature_obj());
usleep(50 * 1000);
if (next_read_time < anj_time_now()) {
next_read_time = anj_time_now() + 1000;
if (data.record_idx < MAX_RECORDS) {
if (anj_dm_res_read(&anj,
&ANJ_MAKE_RESOURCE_PATH(3303, 0, 5700),
&value)) {
log(L_ERROR, "Failed to read resource");
} else {
records[data.record_idx].path =
ANJ_MAKE_RESOURCE_PATH(3303, 0, 5700);
records[data.record_idx].type = ANJ_DATA_TYPE_DOUBLE;
records[data.record_idx].value = value;
records[data.record_idx].timestamp =
(double) anj_time_real_now() / 1000;
data.record_idx++;
}
} else {
log(L_WARNING,
"Records array full, abort send operation ID: "
"%u",
send_id);
if (anj_send_abort(&anj, send_id)) {
log(L_ERROR,
"Failed to abort send operation");
} else {
data.record_idx = 0;
data.send_in_progress = false;
}
}
}
if (data.record_idx >= RECORDS_CNT_SEND_TRIGGER
&& !data.send_in_progress) {
data.records_cnt = data.record_idx;
data.records = records;
data.send_in_progress = true;
/* Record list full, request send */
anj_send_request_t send_req = {
.finished_handler = send_finished_handler,
.data = (void *) &data,
.content_format = ANJ_SEND_CONTENT_FORMAT_SENML_CBOR,
.records_cnt = data.records_cnt,
.records = records
};
if (anj_send_new_request(&anj, &send_req, &send_id)) {
log(L_ERROR, "Failed to request new send");
data.send_in_progress = false;
}
}
}
How it works
Here’s what each key variable does:
anj_res_value_t value
: holds the latest value read from the resource.
uint64_t next_read_time
: defines when the next resource read should happen. It’s updated every time we try to read the resource.
uint16_t send_id
: stores the current Send operation’s ID. You will need this value only if you want to abort the operation by callinganj_send_abort
.
anj_io_out_entry_t records[MAX_RECORDS]
: stores the list of values to be sent.
fin_handler_data_t data
tracks metadata that you want to process after the Send operation completes. The data structure looks like this:typedef struct fin_handler_data { size_t records_cnt; size_t record_idx; anj_io_out_entry_t *records; bool send_in_progress; } fin_handler_data_t;
3.5.1.1. Gathering the data for the Send message
Once per second, we attempt to call anj_dm_res_read
to read the /3303/0/5700
resource. If the read is successful, we create a new entry in the records array with:
the resource path
the data type
the current value
a timestamp
We use data.record_idx
to track the next free slot in the array and increase
it after each successful read.
records[data.record_idx].path =
ANJ_MAKE_RESOURCE_PATH(3303, 0, 5700);
records[data.record_idx].type = ANJ_DATA_TYPE_DOUBLE;
records[data.record_idx].value = value;
records[data.record_idx].timestamp =
(double) anj_time_real_now() / 1000;
data.record_idx++;
Note
If a timestamp is not required, you may omit setting this field in the record.
Note
The values we store in the records
array may be gathered directly from
the sensor object omiting the anj_dm_res_read
call.
3.5.1.2. Preparing the Send message
When data.record_idx
reaches or exceeds RECORDS_CNT_SEND_TRIGGER
, it
means we’ve gathered enough data to send.
Start by updating the data
structure:
data.records_cnt = data.record_idx;
data.records = records;
data.send_in_progress = true;
The send_in_progress
flag indicates that a Send message is currently in progress.
Note
If ANJ_LWM2M_SEND_QUEUE_SIZE
is set to 1
, only one Send request
can be active at a time. To support more simultaneous operations, increase
this setting.
The record_idx
and records
values are stored in the data
structure
so they can later be cleaned up once the send completes. The data
structure
is passed to the callback function that is invoked after the Send message
has been processed.
Now we create a Send request:
/* Record list full, request send */
anj_send_request_t send_req = {
.finished_handler = send_finished_handler,
.data = (void *) &data,
.content_format = ANJ_SEND_CONTENT_FORMAT_SENML_CBOR,
.records_cnt = data.records_cnt,
.records = records
};
We configure the following fields in the request structure:
finished_handler
: a callback function that will be called after the Send operation completes.
data
: a pointer to the user-defined structure passed to the callback.
content_format
: specifies the encoding formatv.
records_cnt
andrecords
: define the number of records and a pointer to the array containing them.
Note
The records
array passed in anj_send_request_t
is not copied
internally. Its contents must remain unchanged and valid until the Send operation
completes.
3.5.1.3. Schedule the send
Once the request is ready, pass it to anj_send_new_request()
. If the function
succeeds:
A new Send message is queued,
send_id
stores its ID, which you can use later to cancel the Send operation if needed,Anjay Lite will process the request during the subsequent
anj_core_step()
calls.
3.5.1.4. Send mesage completion
Once Anjay Lite finishes processing the Send request, it calls the handler function provided in the request to notify that the operation has completed:
static void
send_finished_handler(anj_t *anjay, uint16_t send_id, int result, void *data_) {
(void) anjay;
(void) send_id;
(void) result;
assert(data_);
fin_handler_data_t *data = (fin_handler_data_t *) data_;
/* move the records not yet processed to the begining of the array */
memmove(data->records,
data->records + data->records_cnt,
(MAX_RECORDS - data->records_cnt) * sizeof(anj_io_out_entry_t));
data->record_idx = data->record_idx - data->records_cnt;
data->send_in_progress = false;
}
The logic inside this function can be adjusted to suit your application needs.
What this handler does:
Clear the
send_in_progress
flag to indicate readiness for the next Send operation.Shifts any remaining unsent records to the front of the
records
array usingmemmove()
to free up space for new data.
That’s it! Your client is now ready to send data using the LwM2M Send method in Anjay Lite.