一.模型的简介
在蓝牙Mesh解决方案中,模型用于定义节点的功能。每个模型表示一组状态和行为,并定义用于与模型状态交互的消息。模型的一个例子是配置模型,它是所有mesh设备中必须的一个模型。此模型表示节点的配置(以各种状态的形式),并提供消息来设置或查询配置参数(行为)。本指南介绍了如何创建新模型的基础知识,您可以实现自己的特定的模型,该模型将使您的设备能够提供自定义状态和行为,而不是来自于已经定义标准模型
二.模型的创建
要创建一个模型有以下几步:
1、定义处理函数:通过access_opcode_handler_t数组,为传入消息定义处理函数表
2、分配并将模型绑定到元素:使用access_model_add() API来分配、初始化模型,并将模型绑定到给定索引处的元素。这个模型实例由输出参数p_model_handle的句柄值标识。调用访问层API函数时使用此句柄。所有模型都必须绑定到一个元素。元素表示设备中的可寻址单元,如灯具中的一个灯泡。因此,启动配置过程为每个元素分配一个单独的单播地址
注意,一个模型可以被扩展成一个或多个模型。这些模型实例可以绑定到不同的元素,从而使完整的模型跨越多个元素,这些模型称为扩展模型
三.发布和订阅
发布:
模型发送消息是通过发布完成的,每个模型都有一个发布地址。消息的发布可以是周期性的,也可以是一次性的,发布的消息可以发送到单播、组播或虚拟地址。相关发布状态的配置通常由Provisioner通过配置模型来控制。发布对于传感器节点定期报告数据读取非常有用。可以使用access_model_publish() API函数发布消息,该函数将根据模型设置(间隔、目标)发布消息
客户端模型也可以向服务器模型发布消息。然而在许多情况下,应用程序希望控制从客户端模型发布的消息的目的地,而不是依赖于外部Provisioner(在许多情况下,客户端的应用程序还是Provisioner)。为此提供access_model_publish_address_set()函数
订阅:
订阅允许模型侦听来自特定地址的传入消息。这可以用来监听,例如,从传感器节点发布的周期性消息。要模型订阅地址,首先需要使用access_model_subscription_list_alloc() API函数分配订阅列表
注意,在使用客户端模型时,不需要订阅要发送消息的地址
四.一个简单的点灯模型
本描述的示例模型很简单,可以作为创建自定义mesh模型的一个很好的入门示例。mesh应用程序使用客户端-服务器结构来,其中客户端和服务器模型使用发布/订阅机制来彼此通信。因此模型将使用两部分来实现:服务器模型(维护OnOff状态)和客户端模型(用于在服务器上操作OnOff状态)。当此服务器模型从客户端模型接收到GET或(可靠的)SET消息时,它将OnOff状态的当前值作为响应发送。这将使客户端保持关于服务器状态的最新信息。下表显示了该模型支持的操作码
在空中发送的操作码是针对特定于的模型的三个字节。完整的操作码是特定的操作码和公司标识符的组合,我们的模型使用以下标识符:
此表中使用的公司标识符是Nordic分配的蓝牙公司ID。在实际应用程序中,应该使用自己公司分配的ID
服务器模型:
当OnOff服务器接收SET和GET消息时,它调用应用程序提供的回调函数,并通过回调函数参数共享/请求数据。为此,我们需要定义一个模型结构体,其中包含了指向回调函数的指针。将此结构体传递给所有消息处理程序。下面的代码显示了服务器模型所需的结构体(simple_on_off_server_t)
typedef struct __simple_on_off_server simple_on_off_server_t;
typedef bool (*simple_on_off_get_cb_t)(const simple_on_off_server_t * p_self);
typedef bool (*simple_on_off_set_cb_t)(const simple_on_off_server_t * p_self, bool on_off);
struct __simple_on_off_server
{
access_model_handle_t model_handle;
simple_on_off_get_cb_t get_cb;
simple_on_off_set_cb_t set_cb;
};
接下来,我们需要定义操作码并创建必要的操作码处理函数来处理服务器模型的传入消息。所有模型的所有操作码处理函数都应使用下面定义的相同函数原型
typedef void (*access_opcode_handler_cb_t)(access_model_handle_t handle,
const access_message_rx_t * p_message,
void * p_args);
服务器中需要三个操作码处理函数来处理SIMPLE_ON_OFF_OPCODE_GET、SIMPLE_ON_OFF_OPCODE_SET和SIMPLE_ON_OFF_OPCODE_SET_UNRELIABLE 消息。这些操作码处理函数中的每一个都将结构体simple_on_off_server_t调用相应的用户回调函数,通过p_args参数传递来区分。此外,正如Mesh Profile规范v1.0定义的那样,每个接收元素通过响应该消息发送一条确认的消息,响应通常是一个状态消息,而且状态消息通常包含由SET消息设置的状态的值。因此,模型使用set_cb()回调函数从用户应用程序获取当前OnOff状态值,并使用reply_status()函数发送该值。如果发布地址由provisioner设置,则服务器模型使用publish_state()函数响应响应接收到的消息
static void handle_set_cb(access_model_handle_t handle, const access_message_rx_t * p_message, void * p_args)
{
simple_on_off_server_t * p_server = p_args;
NRF_MESH_ASSERT(p_server->set_cb != NULL);
bool value = (((simple_on_off_msg_set_t*) p_message->p_data)->on_off) > 0;
value = p_server->set_cb(p_server, value);
reply_status(p_server, p_message, value);
publish_state(p_server, value);
}
static void handle_get_cb(access_model_handle_t handle, const access_message_rx_t * p_message, void * p_args)
{
simple_on_off_server_t * p_server = p_args;
NRF_MESH_ASSERT(p_server->get_cb != NULL);
reply_status(p_server, p_message, p_server->get_cb(p_server));
}
static void handle_set_unreliable_cb(access_model_handle_t handle, const access_message_rx_t * p_message, void * p_args)
{
simple_on_off_server_t * p_server = p_args;
NRF_MESH_ASSERT(p_server->set_cb != NULL);
bool value = (((simple_on_off_msg_set_unreliable_t*) p_message->p_data)->on_off) > 0;
value = p_server->set_cb(p_server, value);
publish_state(p_server, value);
}
使用access_model_reply() API将SIMPLE_ON_OFF_OPCODE_STATUS消息中的当前状态值作为响应发送给客户端。
static void reply_status(const simple_on_off_server_t * p_server,
const access_message_rx_t * p_message,
bool present_on_off)
{
simple_on_off_msg_status_t status;
status.present_on_off = present_on_off ? 1 : 0;
access_message_tx_t reply;
reply.opcode.opcode = SIMPLE_ON_OFF_OPCODE_STATUS;
reply.opcode.company_id = ACCESS_COMPANY_ID_NORDIC;
reply.p_buffer = (const uint8_t *) &status;
reply.length = sizeof(status);
reply.force_segmented = false;
reply.transmic_size = NRF_MESH_TRANSMIC_SIZE_DEFAULT;
(void) access_model_reply(p_server->model_handle, p_message, &reply);
}
publish_state()函数与reply_status()函数非常相似,只是它使用access_model_publish() API来发布响应消息。如果provisioner没有配置客户端模型的发布地址,access_model_publish()将发布不了消息。要将给定的操作码和公司ID链接到相应的处理函数,需要指定一个操作码处理函数查找表。当向访问层注册模型时,这个查找表作为输入参数给出。表中的每个条目都是access_opcode_handler_t类型,由操作码、供应商ID和一个处理函数指针组成。对于服务器模型被定义为:
static const access_opcode_handler_t m_opcode_handlers[] =
{
{ACCESS_OPCODE_VENDOR(SIMPLE_ON_OFF_OPCODE_SET, ACCESS_COMPANY_ID_NORDIC), handle_set_cb},
{ACCESS_OPCODE_VENDOR(SIMPLE_ON_OFF_OPCODE_GET, ACCESS_COMPANY_ID_NORDIC), handle_get_cb},
{ACCESS_OPCODE_VENDOR(SIMPLE_ON_OFF_OPCODE_SET_UNRELIABLE, ACCESS_COMPANY_ID_NORDIC), handle_set_unreliable_cb}
};
现在我们已经将模型初始化所需的一切,初始化函数必须分配模型并将其添加到访问层中:
uint32_t simple_on_off_server_init(simple_on_off_server_t * p_server, uint16_t element_index)
{
if (p_server == NULL ||
p_server->get_cb == NULL ||
p_server->set_cb == NULL)
{
return NRF_ERROR_NULL;
}
access_model_add_params_t init_params;
init_params.element_index = element_index;
init_params.model_id.model_id = SIMPLE_ON_OFF_SERVER_MODEL_ID;
init_params.model_id.company_id = ACCESS_COMPANY_ID_NORDIC;
init_params.p_opcode_handlers = &m_opcode_handlers[0];
init_params.opcode_count = sizeof(m_opcode_handlers) / sizeof(m_opcode_handlers[0]);
init_params.p_args = p_server;
init_params.publish_timeout_cb = NULL;
return access_model_add(&init_params, &p_server->model_handle);
}
现在您已经有了一个简单OnOff服务器模型的基本框架,可以对其进行扩展或调整,以生成更复杂的服务器模型
客户端模型:
客户端模型用于与相应的服务器模型进行交互。它发送SET和GET消息,并处理传入的状态回复,它使用指定的发布地址作为发送消息的目的地。就像在服务器实现中一样,客户机需要一个结构体来保存关于回调及其模型句柄的信息。此外,我们使用布尔变量来跟踪事务当前是否处于活动状态,并防止同时运行多个事务。在mesh网络中,消息可能是无序传递的,也可能根本不传递。因此,客户端每次只应该执行一个事务和它对应的服务器。客户端模型通过回调函数向用户应用程序提供服务器状态。如果服务器没有在给定的时间范围内回复,它将使用错误代码SIMPLE_ON_OFF_STATUS_ERROR_NO_REPLY通知用户应用程序,下面的代码显示了该模型所需的状态码(simple_on_off_status_t)和模型结构体(simple_on_off_client_t):
typedef enum
{
SIMPLE_ON_OFF_STATUS_ON,
SIMPLE_ON_OFF_STATUS_OFF,
SIMPLE_ON_OFF_STATUS_ERROR_NO_REPLY
} simple_on_off_status_t;
typedef struct __simple_on_off_client simple_on_off_client_t;
typedef void (*simple_on_off_status_cb_t)(const simple_on_off_client_t * p_self, simple_on_off_status_t status, uint16_t src);
struct __simple_on_off_client
{
access_model_handle_t model_handle;
simple_on_off_status_cb_t status_cb;
struct
{
bool reliable_transfer_active;
simple_on_off_msg_set_t data;
} state;
};
客户端模型可以发送两种消息:可靠(需要确认)消息和不可靠(不需要确认)消息。客户端模型使用access_model_reliable_publish() API来发送可靠的消息,access_model_publish() API用于发送不可靠的消息。access_model_reliable_publish() API要通过重复调用保证消息的传递,直到从目标节点接收到响应或超时为止。当事件完成(或超时)时,将调用一个回调函数来通知客户端模型。如果没有收到来自服务器模型的响应,则通过调用用户的状态回调函数将相应的错误通知给用户应用程序。下面的代码显示了客户端模型的reliable_status_cb()回调函数和send_reliable_message()函数:
static void reliable_status_cb(access_model_handle_t model_handle,
void * p_args,
access_reliable_status_t status)
{
simple_on_off_client_t * p_client = p_args;
NRF_MESH_ASSERT(p_client->status_cb != NULL);
p_client->state.reliable_transfer_active = false;
switch (status)
{
case ACCESS_RELIABLE_TRANSFER_SUCCESS:
/* Ignore */
break;
case ACCESS_RELIABLE_TRANSFER_TIMEOUT:
p_client->status_cb(p_client, SIMPLE_ON_OFF_STATUS_ERROR_NO_REPLY, NRF_MESH_ADDR_UNASSIGNED);
break;
case ACCESS_RELIABLE_TRANSFER_CANCELLED:
p_client->status_cb(p_client, SIMPLE_ON_OFF_STATUS_CANCELLED, NRF_MESH_ADDR_UNASSIGNED);
break;
default:
/* Should not be possible. */
NRF_MESH_ASSERT(false);
break;
}
}
static uint32_t send_reliable_message(const simple_on_off_client_t * p_client,
simple_on_off_opcode_t opcode,
const uint8_t * p_data,
uint16_t length)
{
access_reliable_t reliable;
reliable.model_handle = p_client->model_handle;
reliable.message.p_buffer = p_data;
reliable.message.length = length;
reliable.message.opcode.opcode = opcode;
reliable.message.opcode.company_id = ACCESS_COMPANY_ID_NORDIC;
reliable.message.force_segmented = false;
reliable.message.transmic_size = NRF_MESH_TRANSMIC_SIZE_DEFAULT;
reliable.reply_opcode.opcode = SIMPLE_ON_OFF_OPCODE_STATUS;
reliable.reply_opcode.company_id = ACCESS_COMPANY_ID_NORDIC;
reliable.timeout = ACCESS_RELIABLE_TIMEOUT_MIN;
reliable.status_cb = reliable_status_cb;
return access_model_reliable_publish(&reliable);
}
以下函数用来发送GET和SET消息:
uint32_t simple_on_off_client_set(simple_on_off_client_t * p_client, bool on_off)
{
if (p_client == NULL || p_client->status_cb == NULL)
{
return NRF_ERROR_NULL;
}
else if (p_client->state.reliable_transfer_active)
{
return NRF_ERROR_INVALID_STATE;
}
p_client->state.data.on_off = on_off ? 1 : 0;
p_client->state.data.tid = m_tid++;
uint32_t status = send_reliable_message(p_client,
SIMPLE_ON_OFF_OPCODE_SET,
(const uint8_t *)&p_client->state.data,
sizeof(simple_on_off_msg_set_t));
if (status == NRF_SUCCESS)
{
p_client->state.reliable_transfer_active = true;
}
return status;
}
uint32_t simple_on_off_client_set_unreliable(simple_on_off_client_t * p_client, bool on_off, uint8_t repeats)
{
simple_on_off_msg_set_unreliable_t set_unreliable;
set_unreliable.on_off = on_off ? 1 : 0;
set_unreliable.tid = m_tid++;
access_message_tx_t message;
message.opcode.opcode = SIMPLE_ON_OFF_OPCODE_SET_UNRELIABLE;
message.opcode.company_id = ACCESS_COMPANY_ID_NORDIC;
message.p_buffer = (const uint8_t*) &set_unreliable;
message.length = sizeof(set_unreliable);
message.force_segmented = false;
message.transmic_size = NRF_MESH_TRANSMIC_SIZE_DEFAULT;
uint32_t status = NRF_SUCCESS;
for (uint8_t i = 0; i < repeats; ++i)
{
status = access_model_publish(p_client->model_handle, &message);
if (status != NRF_SUCCESS)
{
break;
}
}
return status;
}
uint32_t simple_on_off_client_get(simple_on_off_client_t * p_client)
{
if (p_client == NULL || p_client->status_cb == NULL)
{
return NRF_ERROR_NULL;
}
else if (p_client->state.reliable_transfer_active)
{
return NRF_ERROR_INVALID_STATE;
}
uint32_t status = send_reliable_message(p_client,
SIMPLE_ON_OFF_OPCODE_GET,
NULL,
0);
if (status == NRF_SUCCESS)
{
p_client->state.reliable_transfer_active = true;
}
return status;
}
为了处理响应消息,我们需要为SIMPLE_ON_OFF_OPCODE_STATUS操作码添加一个处理函数。所有传入消息,即使是对从节点发送的消息的响应,都需要处理一个操作码处理函数。这个代码显示了opcode处理函数实现,并为客户端模型定义了opcode处理函数查找表:
static void handle_status_cb(access_model_handle_t handle, const access_message_rx_t * p_message, void * p_args)
{
simple_on_off_client_t * p_client = p_args;
NRF_MESH_ASSERT(p_client->status_cb != NULL);
if (!is_valid_source(p_client, p_message))
{
return;
}
simple_on_off_msg_status_t * p_status =
(simple_on_off_msg_status_t *) p_message->p_data;
simple_on_off_status_t on_off_status = (p_status->present_on_off ?
SIMPLE_ON_OFF_STATUS_ON : SIMPLE_ON_OFF_STATUS_OFF);
p_client->status_cb(p_client, on_off_status, p_message->meta_data.src.value);
}
static const access_opcode_handler_t m_opcode_handlers[] =
{
{{SIMPLE_ON_OFF_OPCODE_STATUS, ACCESS_COMPANY_ID_NORDIC}, handle_status_cb}
};
客户端模型初始化的方法与服务器模型完全相同:
uint32_t simple_on_off_client_init(simple_on_off_client_t * p_client, uint16_t element_index)
{
if (p_client == NULL ||
p_client->status_cb == NULL)
{
return NRF_ERROR_NULL;
}
access_model_add_params_t init_params;
init_params.model_id.model_id = SIMPLE_ON_OFF_CLIENT_MODEL_ID;
init_params.model_id.company_id = ACCESS_COMPANY_ID_NORDIC;
init_params.element_index = element_index;
init_params.p_opcode_handlers = &m_opcode_handlers[0];
init_params.opcode_count = sizeof(m_opcode_handlers) / sizeof(m_opcode_handlers[0]);
init_params.p_args = p_client;
init_params.publish_timeout_cb = NULL;
return access_model_add(&init_params, &p_client->model_handle);
}
客户端模型现在已经完成,您应该能够使用它通过与服务器节点通信来打开或关闭灯了