跳到主要内容

Mesh 2.4G 多点(10个点以内)通信

前言

基于 ShineBlink C1 (Core)实现 Mesh 通信功能。注意:只能用C1不能用C2,因为C2 不支持Mesh。

C1Mini

一、实现功能

本章节实现了多个Core设备(10个以内)进行通信,其中一个设备是server、其他设备都是client。每当client设备按下电路板上的BTN1按键时,会向server上传数据。当server收到client的数据并验证通过后也会切换电路板上的LED1的亮灭状态,同时client也会收到server下发的命令,client验证命令通过后也会切换电路板上的LED1的亮灭状态。

另外server和client的电路板上的LED2都用来指示连接状态,只有当两边的LED2都同时亮时,才说明二者可以开始正常通信。

注意:由于Core内部自带2.4G无线功能,所以无需外部器件或模块即可实现本章节的无线通讯功能。但如果用了2.4G功能,Core的Ble蓝牙功能和USB功能就无法使用了,这一点开发者需要注意。

二、通信原理概述

  • Core在底层采用了基于 Thread无线网络的COAP协议,它本质上是一个多点通信的mesh网络通信协议

    通过了解Thread无线网络的机制应该知道,在Thread中的节点分为三类:Leader,Router,EndDevice。但请不要将它们和COAP协议中的server,client概念混淆。因为在Thread Coap网络中的server和client节点可能是Leader,Router,EndDevice三者角色中的任意一种,而且还不是固定的,会随着网络的动态变化而变化,而这也体现出了Thread网络的强大之处,即网络中如果某一个路由节点出现了问题,网络其他节点会动态调整自己的角色来自愈网络。

  • Thread网络拓扑图:

    thread topology

    上图是Thread网络的常规物理拓扑图,但我们在开发过程中并不用关注它是如何形成和维护的,这里Thread协议栈已经在底层帮我们做好一切了(包括如何形成网络、组建网络、网络自愈以及数据的路由和转发功能)。

    所以我们只需要知道如下概念即可:

    • 上图中蓝色正方形所表示的Border Router负责将Thread网络和其他的网络(WiFi,NBIOT,Lora等)建立连接。Border Router只能是Server节点,并且网络中只能有一个节点是Server,所以上图中的两个蓝色正方形在我们的实际开发中只能使用其中一个。

    • Client和Server可能是上图中的EndDevice、Leader、Router三种角色中的任意一点,至于是哪种角色完全由Thread协议栈自己来维护,开发者不需要关心。

  • Server和某个Client间的通信示意图

    server_client

  • 通信限制

    • 一个网络中只能有一个server,但可以有上百个client(本例中只支持10个client,为何是10个请看下文中会有解释)。

      所以server可以作为整个网络的网关,来连接到外部其他的网络比如wifi,NBIOT,LORA等。

    • PanID和频率Channel决定了mesh网络的唯一性。

      基于此,我们可以设计PanID和频率Channel不同的Mesh网络来共存在同一个物理空间。

    • client的名字必须为8个字符,client上传给server的数据长度必须为8个字节,server下发给client的命令长度也必须为8个字节。(一般8个字节的空间对于传感器数据上传或控制类命令下发来讲已经足够了。)

    • server下发给client的命令不会马上被client收到,而是等到client下次上传数据给server时,server才会在应答过程中附带上命令数据下发给client。所以如果想让client尽早收到命令,client可以增加向server上传数据的频率。

      这么做的原因有三点:(1) 如果client节点是低功耗休眠的传感器设备,大多数时候可能都不会在线,所以server也不可能实时下发数据 (2) 如果client节点是动态入网离网的,server也无法和这种节点建立稳定的连接 (3) Core的内部资源有限,如果为每个client节点都维护一个连接会耗费大量的Ram。

    • Client端代码中的ClientName变量必须是固定的8个字符且范围在“CONTROL0"~"CONTROL9”这10个中。至于为什么只能是这10个名字,原因如下:

      server端下发的command并不保证一定会被client端收到,比如下发一条command给"client01"还没被"client01"收到就被新下发给"client02"的command给覆盖了。所以为了解决这个问题,Core专门为名称是**"CONTROL0" ~ "CONTROL9"**的10个client分配了专有command缓存,所以mesh网络内最多可以有10个client可以确保接收命令不被其他client覆盖。(至于为何只分配了10个是因为芯片的Ram资源有限)

      注意1:由于网络负荷或其他原因,Core并不保证每次下发给client的command能够100%被client收到,这一点"CONTROL(0 ~ 9)"也不例外,所以如果希望server下发的command 100%到达client,不仅client的名字需为“CONTROL(0 ~ 9)”,而且需要开发者在应用层面上做保障,比如读取下一次client上传的状态以确认上一次的command是否达到。 注意2:ClientName是Client在网络中被Server所识别的唯一身份ID。

三、Client端完整代码

最多十个Client,每个Clientname名称必须是"CONTROL0"~"CONTROL9"中的一个。

--PanID和Channel决定了mesh网络的唯一性
PanID = 0x1234 --16位整型
Channel = 11 --选择范围(11~26)
MyRole = "Client" --一个mesh网络里可以有多个client角色
LIB_MeshConfig(MyRole,PanID,Channel) --启动并加入mesh网络
--client自身的名字"CONTROL0"和server端所提及到的client名字要一致
ClientName = "CONTROL0" --ClientName是Client在网络中被Server所识别的唯一身份ID
LIB_GpioOutputConfig("D8","STANDARD") --LED1
LIB_GpioOutputConfig("D9","STANDARD") --LED2
LIB_GpioWrite("D8",1) --灭
LIB_GpioWrite("D9",1) --灭
--设置按键1(占用D0口,低电平有效)
LIB_ButtonConfig("BTN1","D10","L")
net_state = 0
--开始大循环
while(GC(1) == true)
do
--网络状态led指示
net_state = LIB_MeshClientNetStateQuery()
if net_state == 2 then --client已加入mesh网且找到了server
LIB_GpioWrite("D9",0) --LED2亮
else
LIB_GpioWrite("D9",1) --LED2灭
end
--如果BTN1按键短按且client已经加入mesh网并找到了server,就向Server上传数据
key = LIB_ButtonQuery("BTN1")
if key == 1 and net_state == 2 then
data = {0xA0,0x00,0x00,0x00,0x00,0x00,0x00,0x00}
LIB_MeshClientSendData(ClientName,data)
end
--查询是否收到来自server下发的8字节命令,并解析
recv_flag, cmd = LIB_MeshClientRecvCommand()
if recv_flag == 1 and #cmd == 8 then
if cmd[1] == 0x10 then --这里只解析server下发的命令的第一个字节
LIB_GpioToggle("D8") --LED1亮或灭切换
end
end
end
如果感兴趣,上面代码中出现的LIB开头的库函数可以在 API文档 中通过Ctrl+F查询。

四、Server端完整代码

--PanID和Channel决定了mesh网络的唯一性
PanID = 0x1234 --16位整型
Channel = 11 --选择范围(11~26)
MyRole = "Server" --一个mesh网络里只能有一个Server
LIB_MeshConfig(MyRole,PanID,Channel) --启动并加入mesh网络
LIB_GpioOutputConfig("D8","STANDARD") --LED1
LIB_GpioOutputConfig("D9","STANDARD") --LED2
LIB_GpioWrite("D8",1) --灭
LIB_GpioWrite("D9",1) --灭
net_state = 0
--开始大循环
while(GC(1) == true)
do
--网络状态led指示
net_state = LIB_MeshServerNetStateQuery()
if net_state == 1 then --server已加入mesh网
LIB_GpioWrite("D9",0) --LED2亮
else --server未加入mesh网
LIB_GpioWrite("D9",1) --LED2灭
end
--接收clients上传的8字节数据并进行解析(这里只解析名字、长度和data的第1个字节)
recv_flag, client_name, data = LIB_MeshServerRecvData()
if recv_flag == 1 and #data == 8 then
if client_name == "CONTROL0" and data[1] == 0xA0 then
LIB_GpioToggle("D8") --LED1亮或灭切换
elseif client_name == "CONTROL1" and data[1] == 0xA1 then
LIB_GpioToggle("D8") --LED1亮或灭切换
elseif client_name == "CONTROL2" and data[1] == 0xA2 then
LIB_GpioToggle("D8") --LED1亮或灭切换
--[[
elseif 这里可以定义更多的client节点数据解析
]]
else
print("Invalide Clitent!\r\n")
end
end
--这里看似一直在向clients下发命令,但实际上只是缓存了命令
--命令只有在对应的client上传消息时才会发送到client
if true then --这里的true可以替换成其他条件,取决于你的应用
--向名字为"CONTROL0"的节点下发命令,cmd必须是8个字节
cmd01 = {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00}
LIB_MeshServerSendCommand("CONTROL0",cmd01)
--向名字为"CONTROL1"的节点下发命令,cmd必须是8个字节
cmd02 = {0x11,0x00,0x00,0x00,0x00,0x00,0x00,0x00}
LIB_MeshServerSendCommand("CONTROL1",cmd02)
--向名字为"CONTROL2"的节点下发命令,cmd必须是8个字节
cmd03 = {0x12,0x00,0x00,0x00,0x00,0x00,0x00,0x00}
LIB_MeshServerSendCommand("CONTROL2",cmd03)
--[[ 这里可以向更多的client节点下发命令
cmdXX = {0xXX,0x00,0x00,0x00,0x00,0x00,0x00,0x00}
LIB_MeshServerSendCommand("CONTROLn",cmdXX)
]]
end
end
如果感兴趣,上面代码中出现的LIB开头的库函数可以在 API文档< 中通过Ctrl+F查询。

五、利用Monitor工具查看Thread网络实时拓扑图

如果你的手头上有多余的Core,可以将其中一个转换成Monitor工具,结合运行PC端的应用程序可以更加直观的实时查看Thread Mesh网络结构,制作方法请参考《Mesh 网络监控调试器 DIY》文章,实际效果如下图:

tnodes