diff --git a/subsys/net/lib/lwm2m/CMakeLists.txt b/subsys/net/lib/lwm2m/CMakeLists.txt index 9383f39538b..54d7964f41f 100644 --- a/subsys/net/lib/lwm2m/CMakeLists.txt +++ b/subsys/net/lib/lwm2m/CMakeLists.txt @@ -55,6 +55,10 @@ zephyr_library_sources_ifdef(CONFIG_LWM2M_RW_JSON_SUPPORT zephyr_library_sources_ifdef(CONFIG_LWM2M_RW_SENML_JSON_SUPPORT lwm2m_rw_senml_json.c ) +# CBOR support +zephyr_library_sources_ifdef(CONFIG_LWM2M_RW_CBOR_SUPPORT + lwm2m_rw_cbor.c + ) # IPSO Objects zephyr_library_sources_ifdef(CONFIG_LWM2M_IPSO_TEMP_SENSOR diff --git a/subsys/net/lib/lwm2m/Kconfig b/subsys/net/lib/lwm2m/Kconfig index 241447fd190..b657a0a33fb 100644 --- a/subsys/net/lib/lwm2m/Kconfig +++ b/subsys/net/lib/lwm2m/Kconfig @@ -434,6 +434,13 @@ config LWM2M_COMPOSITE_PATH_LIST_SIZE help Define path list size for Composite Read and send operation. +config LWM2M_RW_CBOR_SUPPORT + bool "support for CBOR writer [EXPERIMENTAL]" + select ZCBOR + select EXPERIMENTAL + help + Include support for writing CBOR data + config LWM2M_DEVICE_PWRSRC_MAX int "Maximum # of device power source records" default 5 diff --git a/subsys/net/lib/lwm2m/lwm2m_engine.c b/subsys/net/lib/lwm2m/lwm2m_engine.c index a62533694f5..86ee59fff2d 100644 --- a/subsys/net/lib/lwm2m/lwm2m_engine.c +++ b/subsys/net/lib/lwm2m/lwm2m_engine.c @@ -51,6 +51,9 @@ LOG_MODULE_REGISTER(LOG_MODULE_NAME); #ifdef CONFIG_LWM2M_RW_JSON_SUPPORT #include "lwm2m_rw_json.h" #endif +#ifdef CONFIG_LWM2M_RW_CBOR_SUPPORT +#include "lwm2m_rw_cbor.h" +#endif #ifdef CONFIG_LWM2M_RD_CLIENT_SUPPORT #include "lwm2m_rd_client.h" #endif @@ -1603,6 +1606,12 @@ static int select_writer(struct lwm2m_output_context *out, uint16_t accept) break; #endif +#ifdef CONFIG_LWM2M_RW_CBOR_SUPPORT + case LWM2M_FORMAT_APP_CBOR: + out->writer = &cbor_writer; + break; +#endif + default: LOG_WRN("Unknown content type %u", accept); return -ENOMSG; @@ -1642,6 +1651,12 @@ static int select_reader(struct lwm2m_input_context *in, uint16_t format) break; #endif +#ifdef CONFIG_LWM2M_RW_CBOR_SUPPORT + case LWM2M_FORMAT_APP_CBOR: + in->reader = &cbor_reader; + break; +#endif + default: LOG_WRN("Unknown content type %u", format); return -ENOMSG; @@ -3706,6 +3721,11 @@ static int do_read_op(struct lwm2m_message *msg, uint16_t content_format) return do_read_op_senml_json(msg); #endif +#if defined(CONFIG_LWM2M_RW_CBOR_SUPPORT) + case LWM2M_FORMAT_APP_CBOR: + return do_read_op_cbor(msg); +#endif + default: LOG_ERR("Unsupported content-format: %u", content_format); return -ENOMSG; @@ -4252,6 +4272,11 @@ static int do_write_op(struct lwm2m_message *msg, return do_write_op_senml_json(msg); #endif +#ifdef CONFIG_LWM2M_RW_CBOR_SUPPORT + case LWM2M_FORMAT_APP_CBOR: + return do_write_op_cbor(msg); +#endif + default: LOG_ERR("Unsupported format: %u", format); return -ENOMSG; diff --git a/subsys/net/lib/lwm2m/lwm2m_engine.h b/subsys/net/lib/lwm2m/lwm2m_engine.h index eb35a2d61d8..cb31e97d244 100644 --- a/subsys/net/lib/lwm2m/lwm2m_engine.h +++ b/subsys/net/lib/lwm2m/lwm2m_engine.h @@ -27,6 +27,7 @@ #define LWM2M_FORMAT_APP_OCTET_STREAM 42 #define LWM2M_FORMAT_APP_EXI 47 #define LWM2M_FORMAT_APP_JSON 50 +#define LWM2M_FORMAT_APP_CBOR 60 #define LWM2M_FORMAT_APP_SEML_JSON 110 #define LWM2M_FORMAT_OMA_PLAIN_TEXT 1541 #define LWM2M_FORMAT_OMA_OLD_TLV 1542 diff --git a/subsys/net/lib/lwm2m/lwm2m_object.h b/subsys/net/lib/lwm2m/lwm2m_object.h index cc237cd7ce6..abdf14bc195 100644 --- a/subsys/net/lib/lwm2m/lwm2m_object.h +++ b/subsys/net/lib/lwm2m/lwm2m_object.h @@ -140,6 +140,13 @@ BUILD_ASSERT(CONFIG_LWM2M_COAP_BLOCK_SIZE <= CONFIG_LWM2M_COAP_MAX_MSG_SIZE, #define CPKT_BUF_W_PTR(cpkt) ((cpkt)->data + (cpkt)->offset) #define CPKT_BUF_W_SIZE(cpkt) ((cpkt)->max_len - (cpkt)->offset) +#define CPKT_BUF_W_REGION(cpkt) CPKT_BUF_W_PTR(cpkt), CPKT_BUF_W_SIZE(cpkt) + +/* Input context buffer util macros */ +#define ICTX_BUF_R_LEFT_SZ(i_ctx) ((i_ctx)->in_cpkt->max_len - (i_ctx)->offset) +#define ICTX_BUF_R_PTR(i_ctx) ((i_ctx)->in_cpkt->data + (i_ctx)->offset) +#define ICTX_BUF_R_REGION(i_ctx) ICTX_BUF_R_PTR(i_ctx), ICTX_BUF_R_LEFT_SZ(i_ctx) + struct lwm2m_engine_obj; struct lwm2m_message; diff --git a/subsys/net/lib/lwm2m/lwm2m_rw_cbor.c b/subsys/net/lib/lwm2m/lwm2m_rw_cbor.c new file mode 100644 index 00000000000..7cdc4028efe --- /dev/null +++ b/subsys/net/lib/lwm2m/lwm2m_rw_cbor.c @@ -0,0 +1,550 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define LOG_MODULE_NAME net_lwm2m_cbor +#define LOG_LEVEL CONFIG_LWM2M_LOG_LEVEL + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include "lwm2m_object.h" +#include "lwm2m_rw_cbor.h" +#include "lwm2m_engine.h" +#include "lwm2m_util.h" + +LOG_MODULE_REGISTER(LOG_MODULE_NAME); + +#define CPKT_CBOR_W_SZ(pos, cpkt) ((size_t)(pos) - (size_t)(cpkt)->data - (size_t)(cpkt)->offset) + +#define ICTX_CBOR_R_SZ(pos, ictx) ((size_t)pos - (size_t)(ictx)->in_cpkt->data - (ictx)->offset) + +static int put_time(struct lwm2m_output_context *out, struct lwm2m_obj_path *path, int64_t value) +{ + /* CBOR time output format is unspecified but SenML CBOR uses string format. + * Let's stick into the same format with plain CBOR + */ + struct tm dt; + char time_str[sizeof("1970-01-01T00:00:00-00:00")] = { 0 }; + int len; + int str_sz; + int tag_sz; + bool ret; + + if (gmtime_r((time_t *)&value, &dt) != &dt) { + LOG_ERR("unable to convert from secs since Epoch to a date/time construct"); + return -EINVAL; + } + + /* Time construct to a string. Time in UTC, offset to local time not known */ + len = snprintk(time_str, sizeof(time_str), + "%04d-%02d-%02dT%02d:%02d:%02d-00:00", + dt.tm_year+1900, + dt.tm_mon+1, + dt.tm_mday, + dt.tm_hour, + dt.tm_min, + dt.tm_sec); + + if (len < 0 || len > sizeof(time_str) - 1) { + LOG_ERR("unable to form a date/time string"); + return -EINVAL; + } + + ZCBOR_STATE_E(states, 0, CPKT_BUF_W_PTR(out->out_cpkt), CPKT_BUF_W_SIZE(out->out_cpkt), 1); + + /* Are tags required? V1.1 leaves this unspecified but some servers require tags */ + ret = zcbor_tag_encode(states, ZCBOR_TAG_TIME_TSTR); + + if (!ret) { + LOG_ERR("unable to encode date/time string tag"); + return -ENOMEM; + } + + tag_sz = CPKT_CBOR_W_SZ(states[0].payload, out->out_cpkt); + + out->out_cpkt->offset += tag_sz; + + ret = zcbor_tstr_put_term(states, time_str); + if (!ret) { + LOG_ERR("unable to encode date/time string"); + return -ENOMEM; + } + + str_sz = CPKT_CBOR_W_SZ(states[0].payload, out->out_cpkt); + + out->out_cpkt->offset += str_sz; + + return tag_sz + str_sz; +} + +static int put_s64(struct lwm2m_output_context *out, + struct lwm2m_obj_path *path, int64_t value) +{ + int payload_len; + bool ret; + + ZCBOR_STATE_E(states, 0, CPKT_BUF_W_PTR(out->out_cpkt), CPKT_BUF_W_SIZE(out->out_cpkt), 1); + + ret = zcbor_int64_encode(states, &value); + + if (ret) { + payload_len = CPKT_CBOR_W_SZ(states[0].payload, out->out_cpkt); + out->out_cpkt->offset += payload_len; + } else { + LOG_ERR("unable to encode a long long integer value"); + payload_len = -ENOMEM; + } + + return payload_len; +} + +static int put_s32(struct lwm2m_output_context *out, struct lwm2m_obj_path *path, int32_t value) +{ + int payload_len; + bool ret; + + ZCBOR_STATE_E(states, 0, CPKT_BUF_W_PTR(out->out_cpkt), CPKT_BUF_W_SIZE(out->out_cpkt), 1); + + ret = zcbor_int32_encode(states, &value); + + if (ret) { + payload_len = CPKT_CBOR_W_SZ(states[0].payload, out->out_cpkt); + out->out_cpkt->offset += payload_len; + } else { + LOG_ERR("unable to encode an integer value"); + payload_len = -ENOMEM; + } + + return payload_len; +} + +static int put_s16(struct lwm2m_output_context *out, struct lwm2m_obj_path *path, int16_t value) +{ + return put_s32(out, path, value); +} + +static int put_s8(struct lwm2m_output_context *out, struct lwm2m_obj_path *path, int8_t value) +{ + return put_s32(out, path, value); +} + +static int put_float(struct lwm2m_output_context *out, + struct lwm2m_obj_path *path, double *value) +{ + int payload_len; + bool ret; + + ZCBOR_STATE_E(states, 0, CPKT_BUF_W_PTR(out->out_cpkt), CPKT_BUF_W_SIZE(out->out_cpkt), 1); + + ret = zcbor_float64_encode(states, value); + + if (ret) { + payload_len = CPKT_CBOR_W_SZ(states[0].payload, out->out_cpkt); + out->out_cpkt->offset += payload_len; + } else { + payload_len = -ENOMEM; + } + + return payload_len; +} + +static int put_string(struct lwm2m_output_context *out, struct lwm2m_obj_path *path, + char *buf, size_t buflen) +{ + int payload_len; + bool ret; + + ZCBOR_STATE_E(states, 0, CPKT_BUF_W_PTR(out->out_cpkt), CPKT_BUF_W_SIZE(out->out_cpkt), 1); + + ret = zcbor_tstr_encode_ptr(states, buf, buflen); + + if (ret) { + payload_len = CPKT_CBOR_W_SZ(states[0].payload, out->out_cpkt); + out->out_cpkt->offset += payload_len; + } else { + payload_len = -ENOMEM; + } + + return payload_len; +} + +static int put_opaque(struct lwm2m_output_context *out, struct lwm2m_obj_path *path, + char *buf, size_t buflen) +{ + int payload_len; + bool ret; + + ZCBOR_STATE_E(states, 0, CPKT_BUF_W_PTR(out->out_cpkt), CPKT_BUF_W_SIZE(out->out_cpkt), 1); + + ret = zcbor_bstr_encode_ptr(states, buf, buflen); + + if (ret) { + payload_len = CPKT_CBOR_W_SZ(states[0].payload, out->out_cpkt); + out->out_cpkt->offset += payload_len; + } else { + payload_len = -ENOMEM; + } + + return payload_len; +} + +static int put_bool(struct lwm2m_output_context *out, struct lwm2m_obj_path *path, bool value) +{ + int payload_len; + bool ret; + + ZCBOR_STATE_E(states, 0, CPKT_BUF_W_PTR(out->out_cpkt), CPKT_BUF_W_SIZE(out->out_cpkt), 1); + + ret = zcbor_bool_encode(states, &value); + + if (ret) { + payload_len = CPKT_CBOR_W_SZ(states[0].payload, out->out_cpkt); + out->out_cpkt->offset += payload_len; + } else { + payload_len = -ENOMEM; + } + + return payload_len; +} + +static int put_objlnk(struct lwm2m_output_context *out, struct lwm2m_obj_path *path, + struct lwm2m_objlnk *value) +{ + char objlnk[sizeof("65535:65535")] = { 0 }; + + snprintk(objlnk, sizeof(objlnk), "%" PRIu16 ":%" PRIu16 "", value->obj_id, value->obj_inst); + + return put_string(out, path, objlnk, strlen(objlnk) + 1); +} + +static int get_s64(struct lwm2m_input_context *in, int64_t *value) +{ + ZCBOR_STATE_D(states, 0, ICTX_BUF_R_PTR(in), ICTX_BUF_R_LEFT_SZ(in), 1); + + if (!zcbor_int64_decode(states, value)) { + LOG_WRN("unable to decode a 64-bit integer value"); + return -EBADMSG; + } + + int len = ICTX_CBOR_R_SZ(states[0].payload, in); + + in->offset += len; + + return len; +} + +static int get_s32(struct lwm2m_input_context *in, int32_t *value) +{ + ZCBOR_STATE_D(states, 0, ICTX_BUF_R_PTR(in), ICTX_BUF_R_LEFT_SZ(in), 1); + + if (!zcbor_int32_decode(states, value)) { + LOG_WRN("unable to decode a 32-bit integer value, err: %d", + states->constant_state->error); + return -EBADMSG; + } + + int len = ICTX_CBOR_R_SZ(states[0].payload, in); + + in->offset += len; + + return len; +} + +static int get_float(struct lwm2m_input_context *in, double *value) +{ + ZCBOR_STATE_D(states, 0, ICTX_BUF_R_PTR(in), ICTX_BUF_R_LEFT_SZ(in), 1); + + if (!zcbor_float_decode(states, value)) { + LOG_ERR("unable to decode a floating-point value"); + return -EBADMSG; + } + + int len = ICTX_CBOR_R_SZ(states[0].payload, in); + + in->offset += len; + + return len; +} + +static int get_string(struct lwm2m_input_context *in, uint8_t *value, size_t buflen) +{ + struct zcbor_string hndl; + int len; + + ZCBOR_STATE_D(states, 0, ICTX_BUF_R_PTR(in), ICTX_BUF_R_LEFT_SZ(in), 1); + + if (!zcbor_tstr_decode(states, &hndl)) { + LOG_WRN("unable to decode a string"); + return -EBADMSG; + } + + len = MIN(buflen-1, hndl.len); + + memcpy(value, hndl.value, len); + + value[len] = '\0'; + + len = ICTX_CBOR_R_SZ(states[0].payload, in); + + in->offset += len; + + return len; +} + +/* Gets time decoded as a numeric string. + * + * return 0 on success, -EBADMSG if decoding fails or -EFAIL if value is invalid + */ +static int get_time_string(struct lwm2m_input_context *in, int64_t *value) +{ + char time_str[sizeof("4294967295")] = { 0 }; + struct zcbor_string hndl = { .value = time_str, .len = sizeof(time_str) - 1 }; + + ZCBOR_STATE_D(states, 0, ICTX_BUF_R_PTR(in), ICTX_BUF_R_LEFT_SZ(in), 1); + + if (!zcbor_tstr_decode(states, &hndl)) { + return -EBADMSG; + } + + /* TODO: decode date/time string */ + LOG_DBG("decoding a date/time string not supported"); + + return -ENOTSUP; +} + +/* Gets time decoded as a numerical. + * + * return 0 on success, -EBADMSG if decoding fails + */ +static int get_time_numerical(struct lwm2m_input_context *in, int64_t *value) +{ + ZCBOR_STATE_D(states, 0, ICTX_BUF_R_PTR(in), ICTX_BUF_R_LEFT_SZ(in), 1); + + if (!zcbor_int64_decode(states, value)) { + LOG_WRN("unable to decode seconds since Epoch"); + return -EBADMSG; + } + + return 0; +} + +static int get_time(struct lwm2m_input_context *in, int64_t *value) +{ + uint32_t tag; + int tag_sz = 0; + int data_len; + int ret; + bool success; + + ZCBOR_STATE_D(states, 0, ICTX_BUF_R_PTR(in), ICTX_BUF_R_LEFT_SZ(in), 1); + + success = zcbor_tag_decode(states, &tag); + + if (success) { + tag_sz = ICTX_CBOR_R_SZ(states[0].payload, in); + in->offset += tag_sz; + + switch (tag) { + case ZCBOR_TAG_TIME_NUM: + ret = get_time_numerical(in, value); + if (ret < 0) { + goto error; + } + break; + case ZCBOR_TAG_TIME_TSTR: + ret = get_time_string(in, value); + if (ret < 0) { + goto error; + } + break; + default: + LOG_WRN("expected tagged date/time, got tag %" PRIu32 "", tag); + return -EBADMSG; + } + } else { /* Assumption is that tags are optional */ + + /* Let's assume numeric string but in case that fails falls go back to numerical */ + ret = get_time_string(in, value); + if (ret == -EBADMSG) { + ret = get_time_numerical(in, value); + } + + if (ret < 0) { + goto error; + } + } + + data_len = ICTX_CBOR_R_SZ(states[0].payload, in); + in->offset += data_len; + + return tag_sz + data_len; + +error: + return ret; +} + +static int get_bool(struct lwm2m_input_context *in, bool *value) +{ + ZCBOR_STATE_D(states, 0, ICTX_BUF_R_PTR(in), ICTX_BUF_R_LEFT_SZ(in), 1); + + if (!zcbor_bool_decode(states, value)) { + LOG_WRN("unable to decode a boolean value"); + return -EBADMSG; + } + + int len = ICTX_CBOR_R_SZ(states[0].payload, in); + + in->offset += len; + + return len; +} + +static int get_opaque(struct lwm2m_input_context *in, uint8_t *value, size_t buflen, + struct lwm2m_opaque_context *opaque, bool *last_block) +{ + struct zcbor_string_fragment hndl = { 0 }; + int ret; + + ZCBOR_STATE_D(states, 1, ICTX_BUF_R_PTR(in), ICTX_BUF_R_LEFT_SZ(in), 1); + + /* Get the CBOR header only on first read. */ + if (opaque->remaining == 0) { + ret = zcbor_bstr_start_decode_fragment(states, &hndl); + + if (!ret) { + LOG_WRN("unable to decode opaque data header"); + return -EBADMSG; + } + + opaque->len = hndl.total_len; + opaque->remaining = hndl.total_len; + + int len = ICTX_CBOR_R_SZ(states[0].payload, in); + + in->offset += len; + } + + return lwm2m_engine_get_opaque_more(in, value, buflen, opaque, last_block); +} + +static int get_objlnk(struct lwm2m_input_context *in, struct lwm2m_objlnk *value) +{ + char objlnk[sizeof("65535:65535")] = { 0 }; + char *end; + char *idp = objlnk; + + value->obj_id = LWM2M_OBJLNK_MAX_ID; + value->obj_inst = LWM2M_OBJLNK_MAX_ID; + + int len = get_string(in, objlnk, sizeof(objlnk) - 1); + + + for (int idx = 0; idx < 2; idx++) { + errno = 0; + + unsigned long id = strtoul(idp, &end, 10); + + idp = end; + + if ((!id && errno == ERANGE) || id > LWM2M_OBJLNK_MAX_ID) { + LOG_WRN("decoded id %lu out of range[0..65535]", id); + return -EBADMSG; + } + + switch (idx) { + case 0: + value->obj_id = id; + continue; + case 1: + value->obj_inst = id; + continue; + } + } + + if (value->obj_inst != LWM2M_OBJLNK_MAX_ID && (value->obj_inst == LWM2M_OBJLNK_MAX_ID)) { + LOG_WRN("decoded obj inst id without obj id"); + return -EBADMSG; + } + + return len; +} + +const struct lwm2m_writer cbor_writer = { + .put_s8 = put_s8, + .put_s16 = put_s16, + .put_s32 = put_s32, + .put_s64 = put_s64, + .put_string = put_string, + .put_float = put_float, + .put_time = put_time, + .put_bool = put_bool, + .put_opaque = put_opaque, + .put_objlnk = put_objlnk, +}; + +const struct lwm2m_reader cbor_reader = { + .get_s32 = get_s32, + .get_s64 = get_s64, + .get_time = get_time, + .get_string = get_string, + .get_float = get_float, + .get_bool = get_bool, + .get_opaque = get_opaque, + .get_objlnk = get_objlnk, +}; + +int do_read_op_cbor(struct lwm2m_message *msg) +{ + /* Can only return single resource */ + if (msg->path.level < LWM2M_PATH_LEVEL_RESOURCE) { + return -EPERM; + } else if (msg->path.level > LWM2M_PATH_LEVEL_RESOURCE_INST) { + return -ENOENT; + } + + return lwm2m_perform_read_op(msg, LWM2M_FORMAT_APP_CBOR); +} + +int do_write_op_cbor(struct lwm2m_message *msg) +{ + struct lwm2m_engine_obj_inst *obj_inst = NULL; + struct lwm2m_engine_obj_field *obj_field; + struct lwm2m_engine_res *res = NULL; + struct lwm2m_engine_res_inst *res_inst = NULL; + int ret; + uint8_t created = 0U; + + ret = lwm2m_get_or_create_engine_obj(msg, &obj_inst, &created); + if (ret < 0) { + return ret; + } + + ret = lwm2m_engine_validate_write_access(msg, obj_inst, &obj_field); + if (ret < 0) { + return ret; + } + + ret = lwm2m_engine_get_create_res_inst(&msg->path, &res, &res_inst); + if (ret < 0) { + return -ENOENT; + } + + if (msg->path.level < LWM2M_PATH_LEVEL_RESOURCE) { + msg->path.level = LWM2M_PATH_LEVEL_RESOURCE; + } + + return lwm2m_write_handler(obj_inst, res, res_inst, obj_field, msg); +} diff --git a/subsys/net/lib/lwm2m/lwm2m_rw_cbor.h b/subsys/net/lib/lwm2m/lwm2m_rw_cbor.h new file mode 100644 index 00000000000..efd7142aed2 --- /dev/null +++ b/subsys/net/lib/lwm2m/lwm2m_rw_cbor.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef LWM2M_RW_CBOR_H_ +#define LWM2M_RW_CBOR_H_ + +#include "lwm2m_object.h" + +extern const struct lwm2m_writer cbor_writer; +extern const struct lwm2m_reader cbor_reader; + +int do_read_op_cbor(struct lwm2m_message *msg); +int do_write_op_cbor(struct lwm2m_message *msg); + +#endif /* LWM2M_RW_CBOR_H_ */