/* * Copyright (c) 2020 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include #include #include #include #include #include #include #include "ots_internal.h" #include "ots_dir_list_internal.h" #include "ots_obj_manager_internal.h" #include LOG_MODULE_DECLARE(bt_ots, CONFIG_BT_OTS_LOG_LEVEL); #define OACP_PROC_TYPE_SIZE 1 /** * OTS_v10.pdf Table 3.10: Format of OACP Response V * OACP Response Value contains * 1 octet Procedure code * 1 octet Request op code * 1 octet Result Code * 4 octet CRC checksum (if present) * Execute operation is not supported **/ #define OACP_RES_MAX_SIZE (3 + sizeof(uint32_t)) #if defined(CONFIG_BT_OTS_OACP_WRITE_SUPPORT) static ssize_t oacp_write_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, struct bt_conn *conn, struct net_buf *buf); static void oacp_l2cap_closed(struct bt_gatt_ots_l2cap *l2cap_ctx, struct bt_conn *conn) { struct bt_ots *ots; ots = CONTAINER_OF(l2cap_ctx, struct bt_ots, l2cap); if (!ots->cur_obj) { return; } ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; l2cap_ctx->rx_done = NULL; l2cap_ctx->tx_done = NULL; } #endif #if defined(CONFIG_BT_OTS_OACP_CREATE_SUPPORT) static enum bt_gatt_ots_oacp_res_code oacp_create_proc_validate( struct bt_conn *conn, struct bt_ots *ots, struct bt_gatt_ots_oacp_proc *proc) { char str[BT_UUID_STR_LEN]; int err; struct bt_gatt_ots_object *obj; const struct bt_ots_obj_add_param param = { .size = proc->create_params.size, .type = proc->create_params.type, }; bt_uuid_to_str(¶m.type.uuid, str, BT_UUID_STR_LEN); LOG_DBG("Validating Create procedure with size: 0x%08X and " "type: %s", param.size, str); if (!BT_OTS_OACP_GET_FEAT_CREATE(ots->features.oacp)) { LOG_DBG("Create Procedure is not supported."); return BT_GATT_OTS_OACP_RES_OPCODE_NOT_SUP; } err = bt_ots_obj_add_internal(ots, conn, ¶m, &obj); if (err) { goto exit; } /* Verify Initialization Metadata */ if (strlen(obj->metadata.name) > 0) { LOG_ERR("Object name shall be a zero length string after object creation."); (void)bt_ots_obj_delete(ots, obj->id); err = -ECANCELED; goto exit; } if (obj->metadata.size.cur > 0) { LOG_ERR("Object current size must be 0."); (void)bt_ots_obj_delete(ots, obj->id); err = -ECANCELED; goto exit; } if (!BT_OTS_OBJ_GET_PROP_WRITE(obj->metadata.props)) { LOG_ERR("Created object must have write property."); (void)bt_ots_obj_delete(ots, obj->id); err = -ECANCELED; goto exit; } ots->cur_obj = obj; ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; LOG_DBG("Create procedure is complete"); exit: switch (err) { case 0: return BT_GATT_OTS_OACP_RES_SUCCESS; case -ENOTSUP: return BT_GATT_OTS_OACP_RES_UNSUP_TYPE; case -ENOMEM: return BT_GATT_OTS_OACP_RES_INSUFF_RES; case -EINVAL: return BT_GATT_OTS_OACP_RES_INV_PARAM; case -ECANCELED: default: return BT_GATT_OTS_OACP_RES_OPER_FAILED; } } #endif #if defined(CONFIG_BT_OTS_OACP_DELETE_SUPPORT) static enum bt_gatt_ots_oacp_res_code oacp_delete_proc_validate( struct bt_conn *conn, struct bt_ots *ots, struct bt_gatt_ots_oacp_proc *proc) { int err; if (!BT_OTS_OACP_GET_FEAT_DELETE(ots->features.oacp)) { LOG_DBG("Delete Procedure is not supported."); return BT_GATT_OTS_OACP_RES_OPCODE_NOT_SUP; } if (!ots->cur_obj) { LOG_DBG("No object is selected."); return BT_GATT_OTS_OACP_RES_INV_OBJ; } if (!BT_OTS_OBJ_GET_PROP_DELETE(ots->cur_obj->metadata.props)) { LOG_DBG("Object properties do not permit deletion."); return BT_GATT_OTS_OACP_RES_NOT_PERMITTED; } err = bt_ots_obj_delete(ots, ots->cur_obj->id); if (err) { LOG_ERR("Deleting object during Delete procedure failed: %d", err); goto exit; } LOG_DBG("Delete procedure is complete"); exit: switch (err) { case 0: return BT_GATT_OTS_OACP_RES_SUCCESS; case -EBUSY: return BT_GATT_OTS_OACP_RES_OBJ_LOCKED; default: return BT_GATT_OTS_OACP_RES_OPER_FAILED; } } #endif #if defined(CONFIG_BT_OTS_OACP_CHECKSUM_SUPPORT) static enum bt_gatt_ots_oacp_res_code oacp_checksum_proc_validate( struct bt_conn *conn, struct bt_ots *ots, struct bt_gatt_ots_oacp_proc *proc, struct net_buf_simple *resp_param) { struct bt_gatt_ots_oacp_cs_calc_params *params = &proc->cs_calc_params; void *obj_data; int err; uint32_t checksum; LOG_DBG("Validating Checksum procedure with offset: 0x%08X and " "length: 0x%08X", params->offset, params->len); if (!ots->cur_obj) { return BT_GATT_OTS_OACP_RES_INV_OBJ; } if (params->offset > ots->cur_obj->metadata.size.cur) { return BT_GATT_OTS_OACP_RES_INV_PARAM; } if ((params->offset + (uint64_t) params->len) > ots->cur_obj->metadata.size.alloc) { return BT_GATT_OTS_OACP_RES_INV_PARAM; } if (ots->cur_obj->state.type != BT_GATT_OTS_OBJECT_IDLE_STATE) { return BT_GATT_OTS_OACP_RES_OBJ_LOCKED; } if (ots->cb->obj_cal_checksum) { err = ots->cb->obj_cal_checksum(ots, conn, ots->cur_obj->id, params->offset, params->len, &obj_data); if (err != 0) { return BT_GATT_OTS_OACP_RES_OPER_FAILED; } checksum = bt_ots_client_calc_checksum((const uint8_t *)obj_data, params->len); net_buf_simple_add_le32(resp_param, checksum); LOG_DBG("Calculate from offset %u len %u checksum 0x%08x\n", params->offset, params->len, checksum); return BT_GATT_OTS_OACP_RES_SUCCESS; } else { return BT_GATT_OTS_OACP_RES_OPER_FAILED; } } #endif static enum bt_gatt_ots_oacp_res_code oacp_read_proc_validate( struct bt_conn *conn, struct bt_ots *ots, const struct bt_gatt_ots_oacp_proc *proc) { const struct bt_gatt_ots_oacp_read_params *params = &proc->read_params; LOG_DBG("Validating Read procedure with offset: 0x%08X and " "length: 0x%08X", params->offset, params->len); if (!ots->cur_obj) { return BT_GATT_OTS_OACP_RES_INV_OBJ; } if (!BT_OTS_OBJ_GET_PROP_READ(ots->cur_obj->metadata.props)) { return BT_GATT_OTS_OACP_RES_NOT_PERMITTED; } if (!bt_gatt_ots_l2cap_is_open(&ots->l2cap, conn)) { return BT_GATT_OTS_OACP_RES_CHAN_UNAVAIL; } if ((params->offset + (uint64_t) params->len) > ots->cur_obj->metadata.size.cur) { return BT_GATT_OTS_OACP_RES_INV_PARAM; } if (ots->cur_obj->state.type != BT_GATT_OTS_OBJECT_IDLE_STATE) { return BT_GATT_OTS_OACP_RES_OBJ_LOCKED; } ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_READ_OP_STATE; ots->cur_obj->state.read_op.sent_len = 0; memcpy(&ots->cur_obj->state.read_op.oacp_params, &proc->read_params, sizeof(ots->cur_obj->state.read_op.oacp_params)); LOG_DBG("Read procedure is accepted"); return BT_GATT_OTS_OACP_RES_SUCCESS; } #if defined(CONFIG_BT_OTS_OACP_WRITE_SUPPORT) static enum bt_gatt_ots_oacp_res_code oacp_write_proc_validate( struct bt_conn *conn, struct bt_ots *ots, struct bt_gatt_ots_oacp_proc *proc) { struct bt_gatt_ots_oacp_write_params *params = &proc->write_params; LOG_DBG("Validating Write procedure with offset: 0x%08X and " "length: 0x%08X", params->offset, params->len); if (!ots->cur_obj) { return BT_GATT_OTS_OACP_RES_INV_OBJ; } if (!BT_OTS_OBJ_GET_PROP_WRITE(ots->cur_obj->metadata.props)) { return BT_GATT_OTS_OACP_RES_NOT_PERMITTED; } /* patching is attempted */ if (params->offset < ots->cur_obj->metadata.size.cur) { if (!BT_OTS_OACP_GET_FEAT_PATCH(ots->features.oacp)) { return BT_GATT_OTS_OACP_RES_NOT_PERMITTED; } if (!BT_OTS_OBJ_GET_PROP_PATCH(ots->cur_obj->metadata.props)) { return BT_GATT_OTS_OACP_RES_NOT_PERMITTED; } } /* truncation is not supported */ if (BT_GATT_OTS_OACP_PROC_WRITE_MODE_GET_TRUNC(params->mode)) { return BT_GATT_OTS_OACP_RES_NOT_PERMITTED; } if (!bt_gatt_ots_l2cap_is_open(&ots->l2cap, conn)) { return BT_GATT_OTS_OACP_RES_CHAN_UNAVAIL; } if (BT_GATT_OTS_OACP_PROC_WRITE_MODE_GET_RFU(params->mode)) { return BT_GATT_OTS_OACP_RES_INV_PARAM; } if (params->offset > ots->cur_obj->metadata.size.cur) { return BT_GATT_OTS_OACP_RES_INV_PARAM; } /* append is not supported */ if ((params->offset + (uint64_t) params->len) > ots->cur_obj->metadata.size.alloc) { return BT_GATT_OTS_OACP_RES_INV_PARAM; } if (ots->cur_obj->state.type != BT_GATT_OTS_OBJECT_IDLE_STATE) { return BT_GATT_OTS_OACP_RES_OBJ_LOCKED; } ots->l2cap.rx_done = oacp_write_proc_cb; ots->l2cap.closed = oacp_l2cap_closed; ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_WRITE_OP_STATE; ots->cur_obj->state.write_op.recv_len = 0; memcpy(&ots->cur_obj->state.write_op.oacp_params, params, sizeof(ots->cur_obj->state.write_op.oacp_params)); LOG_DBG("Write procedure is accepted"); return BT_GATT_OTS_OACP_RES_SUCCESS; } #endif static enum bt_gatt_ots_oacp_res_code oacp_proc_validate( struct bt_conn *conn, struct bt_ots *ots, struct bt_gatt_ots_oacp_proc *proc, struct net_buf_simple *resp_param) { switch (proc->type) { case BT_GATT_OTS_OACP_PROC_READ: return oacp_read_proc_validate(conn, ots, proc); #if defined(CONFIG_BT_OTS_OACP_WRITE_SUPPORT) case BT_GATT_OTS_OACP_PROC_WRITE: return oacp_write_proc_validate(conn, ots, proc); #endif #if defined(CONFIG_BT_OTS_OACP_CREATE_SUPPORT) case BT_GATT_OTS_OACP_PROC_CREATE: return oacp_create_proc_validate(conn, ots, proc); #endif #if defined(CONFIG_BT_OTS_OACP_DELETE_SUPPORT) case BT_GATT_OTS_OACP_PROC_DELETE: return oacp_delete_proc_validate(conn, ots, proc); #endif #if defined(CONFIG_BT_OTS_OACP_CHECKSUM_SUPPORT) case BT_GATT_OTS_OACP_PROC_CHECKSUM_CALC: return oacp_checksum_proc_validate(conn, ots, proc, resp_param); #endif case BT_GATT_OTS_OACP_PROC_EXECUTE: case BT_GATT_OTS_OACP_PROC_ABORT: default: return BT_GATT_OTS_OACP_RES_OPCODE_NOT_SUP; } }; static int oacp_command_decode(const uint8_t *buf, uint16_t len, struct bt_gatt_ots_oacp_proc *proc) { struct net_buf_simple net_buf; if (len < OACP_PROC_TYPE_SIZE) { return -ENODATA; } net_buf_simple_init_with_data(&net_buf, (void *) buf, len); proc->type = net_buf_simple_pull_u8(&net_buf); switch (proc->type) { #if defined(CONFIG_BT_OTS_OACP_CREATE_SUPPORT) case BT_GATT_OTS_OACP_PROC_CREATE: if (net_buf.len < BT_GATT_OTS_OACP_CREATE_GENERIC_PARAMS_SIZE) { return -EBADMSG; } proc->create_params.size = net_buf_simple_pull_le32(&net_buf); if (!bt_uuid_create(&proc->create_params.type.uuid, net_buf.data, net_buf.len)) { return -EBADMSG; } net_buf_simple_pull_mem(&net_buf, net_buf.len); /* Only 16-bit and 128-bit UUIDs are supported */ switch (proc->create_params.type.uuid.type) { case BT_UUID_TYPE_16: case BT_UUID_TYPE_128: return 0; default: break; } return -EBADMSG; #endif #if defined(CONFIG_BT_OTS_OACP_DELETE_SUPPORT) case BT_GATT_OTS_OACP_PROC_DELETE: if (net_buf.len != 0) { return -EBADMSG; } return 0; #endif case BT_GATT_OTS_OACP_PROC_CHECKSUM_CALC: if (net_buf.len != BT_GATT_OTS_OACP_CS_CALC_PARAMS_SIZE) { return -EBADMSG; } proc->cs_calc_params.offset = net_buf_simple_pull_le32(&net_buf); proc->cs_calc_params.len = net_buf_simple_pull_le32(&net_buf); return 0; case BT_GATT_OTS_OACP_PROC_EXECUTE: if (net_buf.len != 0) { return -EBADMSG; } return 0; case BT_GATT_OTS_OACP_PROC_READ: if (net_buf.len != BT_GATT_OTS_OACP_READ_PARAMS_SIZE) { return -EBADMSG; } proc->read_params.offset = net_buf_simple_pull_le32(&net_buf); proc->read_params.len = net_buf_simple_pull_le32(&net_buf); return 0; #if defined(CONFIG_BT_OTS_OACP_WRITE_SUPPORT) case BT_GATT_OTS_OACP_PROC_WRITE: if (net_buf.len != BT_GATT_OTS_OACP_WRITE_PARAMS_SIZE) { return -EBADMSG; } proc->write_params.offset = net_buf_simple_pull_le32(&net_buf); proc->write_params.len = net_buf_simple_pull_le32(&net_buf); proc->write_params.mode = net_buf_simple_pull_u8(&net_buf); return 0; #endif case BT_GATT_OTS_OACP_PROC_ABORT: default: break; } return -ENOTSUP; } static void oacp_read_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, struct bt_conn *conn) { int err; void *obj_chunk; off_t offset; ssize_t len; struct bt_ots *ots; struct bt_gatt_ots_object_read_op *read_op; ots = CONTAINER_OF(l2cap_ctx, struct bt_ots, l2cap); read_op = &ots->cur_obj->state.read_op; offset = read_op->oacp_params.offset + read_op->sent_len; if (read_op->sent_len >= read_op->oacp_params.len) { LOG_DBG("OACP Read Op over L2CAP is completed"); if (read_op->sent_len > read_op->oacp_params.len) { LOG_WRN("More bytes sent that the client requested"); } ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->cur_obj->id == OTS_OBJ_ID_DIR_LIST) { return; } ots->cb->obj_read(ots, conn, ots->cur_obj->id, NULL, 0, offset); return; } len = read_op->oacp_params.len - read_op->sent_len; if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->cur_obj->id == OTS_OBJ_ID_DIR_LIST) { len = bt_ots_dir_list_content_get(ots->dir_list, ots->obj_manager, &obj_chunk, len, offset); } else { len = ots->cb->obj_read(ots, conn, ots->cur_obj->id, &obj_chunk, len, offset); } if (len < 0) { LOG_ERR("OCAP Read Op failed with error: %zd", len); bt_gatt_ots_l2cap_disconnect(&ots->l2cap); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; return; } ots->l2cap.tx_done = oacp_read_proc_cb; err = bt_gatt_ots_l2cap_send(&ots->l2cap, obj_chunk, len); if (err) { LOG_ERR("L2CAP CoC error: %d while trying to execute OACP " "Read procedure", err); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; } else { read_op->sent_len += len; } } static void oacp_read_proc_execute(struct bt_ots *ots, struct bt_conn *conn) { struct bt_gatt_ots_oacp_read_params *params = &ots->cur_obj->state.read_op.oacp_params; if (!ots->cur_obj) { LOG_ERR("Invalid Current Object on OACP Read procedure"); return; } LOG_DBG("Executing Read procedure with offset: 0x%08X and " "length: 0x%08X", params->offset, params->len); if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->cur_obj->id == OTS_OBJ_ID_DIR_LIST) { oacp_read_proc_cb(&ots->l2cap, conn); } else if (ots->cb->obj_read) { oacp_read_proc_cb(&ots->l2cap, conn); } else { ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; LOG_ERR("OTS Read operation failed: " "there is no OTS Read callback"); } } #if defined(CONFIG_BT_OTS_OACP_WRITE_SUPPORT) static ssize_t oacp_write_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, struct bt_conn *conn, struct net_buf *buf) { struct bt_gatt_ots_object_write_op *write_op; struct bt_ots *ots; off_t offset; size_t rem; size_t len; ssize_t rc; ots = CONTAINER_OF(l2cap_ctx, struct bt_ots, l2cap); if (!ots->cur_obj) { LOG_ERR("Invalid Current Object on OACP Write procedure"); return -ENODEV; } if (!ots->cb->obj_write) { LOG_ERR("OTS Write operation failed: " "there is no OTS Write callback"); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; return -ENODEV; } write_op = &ots->cur_obj->state.write_op; offset = write_op->oacp_params.offset + write_op->recv_len; len = buf->len; if (write_op->recv_len + len > write_op->oacp_params.len) { LOG_WRN("More bytes received than the client indicated"); len = write_op->oacp_params.len - write_op->recv_len; } rem = write_op->oacp_params.len - (write_op->recv_len + len); rc = ots->cb->obj_write(ots, conn, ots->cur_obj->id, buf->data, len, offset, rem); if (rc < 0) { len = 0; /* * Returning an EINPROGRESS return code results in the write buffer not being * released by the l2cap layer. This is an unsupported use case at the moment. */ if (rc == -EINPROGRESS) { LOG_ERR("Unsupported error code %zd returned by object write callback", rc); } LOG_ERR("OTS Write operation failed with error: %zd", rc); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; } else { /* Return -EIO as an error if all of data was not written */ if (rc != len) { len = rc; rc = -EIO; } } write_op->recv_len += len; if (write_op->recv_len == write_op->oacp_params.len) { LOG_DBG("OACP Write Op over L2CAP is completed"); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; } if (offset + len > ots->cur_obj->metadata.size.cur) { ots->cur_obj->metadata.size.cur = offset + len; } return rc; } #endif static void oacp_ind_cb(struct bt_conn *conn, struct bt_gatt_indicate_params *params, uint8_t err) { struct bt_ots *ots = (struct bt_ots *) params->attr->user_data; LOG_DBG("Received OACP Indication ACK with status: 0x%04X", err); if (!ots->cur_obj) { LOG_DBG("There is no object associated with this ACK"); return; } switch (ots->cur_obj->state.type) { case BT_GATT_OTS_OBJECT_READ_OP_STATE: oacp_read_proc_execute(ots, conn); break; case BT_GATT_OTS_OBJECT_WRITE_OP_STATE: /* procedure execution is driven by L2CAP socket receive */ break; case BT_GATT_OTS_OBJECT_IDLE_STATE: /* procedure is not in progress and was already completed */ break; default: LOG_ERR("Unsupported OTS state: %d", ots->cur_obj->state.type); break; } } static int oacp_ind_send(const struct bt_gatt_attr *oacp_attr, struct bt_gatt_ots_oacp_proc oacp_proc, enum bt_gatt_ots_oacp_res_code oacp_status, struct net_buf_simple *resp_param) { uint8_t oacp_res[OACP_RES_MAX_SIZE]; uint16_t oacp_res_len = 0; struct bt_ots *ots = (struct bt_ots *) oacp_attr->user_data; /* Encode OACP Response */ oacp_res[oacp_res_len++] = BT_GATT_OTS_OACP_PROC_RESP; oacp_res[oacp_res_len++] = oacp_proc.type; oacp_res[oacp_res_len++] = oacp_status; if (oacp_proc.type == BT_GATT_OTS_OACP_PROC_CHECKSUM_CALC) { sys_put_le32(net_buf_simple_pull_le32(resp_param), (oacp_res + oacp_res_len)); oacp_res_len += sizeof(uint32_t); } /* Prepare indication parameters */ memset(&ots->oacp_ind.params, 0, sizeof(ots->oacp_ind.params)); memcpy(&ots->oacp_ind.attr, oacp_attr, sizeof(ots->oacp_ind.attr)); ots->oacp_ind.params.attr = &ots->oacp_ind.attr; ots->oacp_ind.params.func = oacp_ind_cb; ots->oacp_ind.params.data = oacp_res; ots->oacp_ind.params.len = oacp_res_len; LOG_DBG("Sending OACP indication"); return bt_gatt_indicate(NULL, &ots->oacp_ind.params); } ssize_t bt_gatt_ots_oacp_write(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, uint16_t offset, uint8_t flags) { enum bt_gatt_ots_oacp_res_code oacp_status; int decode_status; struct bt_gatt_ots_oacp_proc oacp_proc = {0}; struct bt_ots *ots = (struct bt_ots *) attr->user_data; NET_BUF_SIMPLE_DEFINE(resp_param, sizeof(uint32_t)); LOG_DBG("Object Action Control Point GATT Write Operation"); if (!ots->oacp_ind.is_enabled) { LOG_WRN("OACP indications not enabled"); return BT_GATT_ERR(BT_ATT_ERR_CCC_IMPROPER_CONF); } if (offset != 0) { LOG_ERR("Invalid offset of OACP Write Request"); return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); } decode_status = oacp_command_decode(buf, len, &oacp_proc); switch (decode_status) { case 0: oacp_status = oacp_proc_validate(conn, ots, &oacp_proc, &resp_param); if (oacp_status != BT_GATT_OTS_OACP_RES_SUCCESS) { LOG_WRN("OACP Write error status: 0x%02X", oacp_status); } break; case -ENOTSUP: oacp_status = BT_GATT_OTS_OACP_RES_OPCODE_NOT_SUP; LOG_WRN("OACP unsupported procedure type: 0x%02X", oacp_proc.type); break; case -EBADMSG: LOG_ERR("Invalid length of OACP Write Request for 0x%02X " "Op Code", oacp_proc.type); return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); case -ENODATA: LOG_ERR("Invalid length of OACP Write Request"); return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); default: LOG_ERR("Invalid return code from oacp_command_decode: %d", decode_status); return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); } oacp_ind_send(attr, oacp_proc, oacp_status, &resp_param); return len; } void bt_gatt_ots_oacp_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) { struct bt_gatt_ots_indicate *oacp_ind = CONTAINER_OF((struct _bt_gatt_ccc *) attr->user_data, struct bt_gatt_ots_indicate, ccc); LOG_DBG("Object Action Control Point CCCD value: 0x%04X", value); oacp_ind->is_enabled = false; if (value == BT_GATT_CCC_INDICATE) { oacp_ind->is_enabled = true; } }