文章目录
概述
本实验基于NB-IoT实现智慧农业案例,实现实时数据采集,实现命令下发和响应,实现端云互通。实验目的包括:
- 掌握NB-IoT通信方式的配置。
- 掌握智慧农业案例的开发过程。
- 平台侧模型定义和编解码插件开发。
- 端侧嵌入式程序开发与程序调试。(设备接入、数据上报、命令接收等)
- LwM2M/CoAP协议与NB-IoT的AT指令存在怎样的关联呢?
@NOTE
转载请标明出处,https://blog.csdn.net/quguanxin/category_12929470.html
@HISTORY
从AT指令实验的那个时候开始,我就有一个疑问?物联网端侧程序是要通过AT指令与通信模组交互吗,目标板卡上的应用程序难道要给模组应用核内的程序发送串口指令?
@HISTORY
我曾试图在哪里找到关于智慧农业的直接可复制源码,省的再敲键盘,浪费时间,可惜没找到。
在LiteOS_Lab_HCIP\targets\STM32L431_BearPi\Demos\oc_agriculture_template、
或在新建的 LiteOS Studio工程时生成的demo中(.demos\agriculture),
都只是存在部分代码的片段,也不与实验手册一致,参考价值不大。算了还是自己敲吧,有了Trae AI等插件的加持,也不费事。
扩展板 E53_IA1
E53_IA1是一款遵循E53标准接口规范的智慧农业扩展板,专为物联网开发设计,适配多种开发板,用于快速实现农业环境监测与控制的原型开发。“E”代表扩展(Expansion),“53”表示板卡尺寸为5×3 cm,具备统一物理接口,支持跨平台适配。IA 应用场景Intelligent Agriculture(智慧农业)的简称,标识该扩展板专用于农业监测场景。子类编号 1 代表智慧农业类别中的基础农业监测功能,如,温湿度、光照监测与控制。
小熊派社区提供了 E53_IA1扩展板驱动,主要宏定义和接口声明如下,
#ifndef __E53_IA1_H__
#define __E53_IA1_H__
#include "stm32l4xx_hal.h"
/* 控制设备IO口定义 ------------------------------------------------------------*/
#define IA1_Motor_Pin GPIO_PIN_8
#define IA1_Motor_GPIO_Port GPIOB
#define IA1_Motor_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define IA1_Light_Pin GPIO_PIN_0
#define IA1_Light_GPIO_Port GPIOA
#define IA1_Light_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
/* E53_IA1传感器数据类型定义 ------------------------------------------------------------*/
typedef struct {
float Lux; //光照强度
float Humidity; //湿度
float Temperature; //温度
} E53_IA1_Data_TypeDef;
extern E53_IA1_Data_TypeDef E53_IA1_Data;
/* 寄存器宏定义 --------------------------------------------------------------------*/
#define I2C_OWN_ADDRESS 0x0A
//
#define SHT30_Addr 0x44
//
#define BH1750_Addr 0x46
#define BH1750_ON 0x01
#define BH1750_CON 0x10
#define BH1750_ONE 0x20
#define BH1750_RSET 0x07
void Init_E53_IA1(void);
void E53_IA1_Read_Data(void);
#endif
智慧农业平台测开发
参见 <HCIP IoT Developer V2.5 实验手册> 1 物联网平台开发实验,实验任务配置步骤,搭建智慧农业平台侧。
功能定义/模型开发
部分内容请参考 #<IoT/HCIP实验-1/物联网开发平台实验Part1(快速入门,MQTT.fx对接IoTDA># 其中的编解码插件开发相关章节。本章只做简述。
创建产品–协议类型 LwM2M/CoAP–数据格式–二进制码流,
创建产品成功后,点击产品名称或产品详情进入,在基本信息卡页面点击自定义模型进入,
参见前文服务和属性列表,5分钟就可以完成产品模型定义/功能定义。结果如下,
编解码插件开发-消息
部分内容请参考 #<IoT/HCIP实验-1/物联网开发平台实验Part2(HCIP-IoT实验手册版)># 其中的编解码插件开发相关章节。
从产品详情界面,进入插件开发,使用图形化开发方式,按照上文消息和字段说明逐个定义。
如上消息创建工作即完成。这里再回顾一下mid响应标识的概念。通过消息建立过程和现象可知,在IoTDA下命令和命令的响应是两种消息,它们的消息标识messageid通常分别为n和n+1。在华为云 IoTDA 的命令下发机制中,mid(Message Identifier)字段是消息的唯一追踪标识符,其核心作用是实现命令请求与响应的精准匹配,并解决异步通信场景下的消息关联难题。mid字段的核心作用,可从如下3点来理解,命令与响应的逐一对应、防止重复处理与消息去重、状态追踪与调试。mid是0-65536之间的一个动态变动的值,
// 平台下发命令(带mid)
{
"messageId": "SET_TEMPERATURE",
"mid": "20231108001",
"params": {"temp": 25}
}
// 设备响应(返回相同mid)
{
"messageId": "SET_TEMPERATURE_RESPONSE",
"mid": "20231108001",
"result": 0 // 0表示成功
}
编解码插件开发-关联
如上,通过拖拽方式,管理消息字段和产品模型中的属性、命令字段、响应字段。
编解码插件开发-部署
一定不要忘了部署编解码插件。之后最好使用虚拟设备先验证你的模型和编解码插件。
注册实际设备
依次操作左侧选项卡中的-所有设备-注册设备,进入单设备注册界面,录入相关信息。设备标识码使用 IMEI 国际移动设备标识码,可以在通信模组封装上看到,也可以使用 AT+CGSN=1 进行查询。好久不去练习AT指令了,我在这里又搞了一次乌龙。我将串口拨码开关拨到AT PC侧,在串口终端中输入AT,点击发送却没有任何反应。呢?尝试了10几分钟,才恍然大悟,是波特率的问题。AT指令收发操作时,使用的串口波特率是9600,而端侧MCU程序开发中配置的串口波特率都是115200。
还要继续参考 #<IoT/基于NB28-A/BC28-CNV通信模组使用AT指令连接华为云IoTDA平台(HCIP-IoT实验2)># 和 #<IoT/HCIP实验-2/基于NB-IoT模组的AT指令实验(小熊派IoT+NB28-A模组)># 等文章,完成设备创建信息的填写,尤其是密钥信息的录入。
智慧农业端侧编码
代码模版参考LiteOS_Lab_HCIP示例工程下的targets\STM32L431_BearPi\Demos\oc_agriculture_template 下的模版文件。
工程配置
关于Kconfig相关配置,可以直接修改 LiteOS_Lab_HCIP\targets\STM32L431_BearPi 根目录下的 .config 文件,可以可以使用示例工程目录下的 defaults.sdkconfig 配置文件覆盖它,只要修改配置为,
CONFIG_Demo_Agriculture=y
CONFIG_USER_DEMO="oc_agriculture_template"
KConfig体系下的.mk文件(该后缀名可能是Makefile的缩写)是Linux内核及基于KConfig的构建系统中实现源代码编译规则与配置联动的核心组件。它们通过解析.config文件中的符号(如CONFIG_FOO),动态控制代码的编译方式、模块生成及目录递归构建流程。在.config内配置 CONFIG_USER_DEMO 宏的取值后,与智慧农业相关的驱动层和应用层源码就会被动态的包含到最终的Makefile文件中。
同样还需要修改的是与 .config 同一目录下的 iot_config.h 文件,理论上在 genconfig 工具下,可以将 .config 转换为 iot_config.h。这里不再尝试,只是按照手册简单的修改头文件下的 CONFIG_USER_DEMO 宏定义与 .config 保持一致即可。@note 关注实验内容,不关注IDE。
数据结构定义
端侧程序中使用的C语言数据结构,其与平台编解码插件开发中的消息定义要完全匹配。
//消息结构体
#pragma pack(1)
//上行 //数据上报
typedef struct {
int8u messageId;
int8u Temperature;
int8u Humidity;
int16u Luminance; //注意字段顺序和字节数
} tag_app_Agricultural;
//上行 //光照开关控制-响应数据
typedef struct {
int8u messageId;
int8u mid;
int8u errcode;
int8u Light_State;
} tag_app_Response_Agricultural_Control_Light;
//上行 //电机开关控制-响应数据
typedef struct {
int8u messageId;
int8u mid;
int8u errcode;
int8u Motor_State;
} tag_app_Response_Agricultural_Control_Motor;
//下行 //光照开关控制
typedef struct {
int8u messageId;
int16u mid;
string Light[3];
} tag_app_Agricultural_Control_Light;
//下行 //电机开关控制
typedef struct {
int8u messageId;
int16u mid;
string Motor[3];
} tag_app_Agricultural_Control_Motor;
#pragma pack()
数据收集任务
//数据收集任务
static int app_data_collect_task(void *usr_data) {
//扩展板初始化
Init_E53_IA1();
//实时读取和显示传感器或控制器数据
while (1) {
//更新传感器值/驱动中导出的全局变量
E53_IA1_Read_Data();
//在串口中输出
#if 0
printf("** RTT Value:Lux(%2.2f), Humidity(%2.2f), Temperature(%2.2f) \r\n", E53_IA1_Data.Lux, E53_IA1_Data.Humidity, E53_IA1_Data.Temperature);
#endif
//显示在屏幕上
LCD_ShowString(10, 140, 200, 16, 16, "Temperature:");
LCD_ShowNum(140, 140, (int)E53_IA1_Data.Temperature, 5, 16);
LCD_ShowString(10, 170, 200, 16, 16, "Humidity:");
LCD_ShowNum(140, 170, (int)E53_IA1_Data.Humidity, 5, 16);
LCD_ShowString(10, 200, 200, 16, 16, "Lux:");
LCD_ShowNum(140, 200, (int)E53_IA1_Data.Lux, 5, 16);
//刷新频率
osal_task_sleep(2*1000);
}
return 0;
}
每次任务循环都要执行 E53_IA1_Read_Data() 操作,该函数并没有设置输入输出参数,这是因为E53_IA1驱动程序的接口头文件中直接导出了 E53_IA1_Data 这个全局变量,我对这种大写字母开头的变量定义,真是极度的不适应的。E53_IA1_Read_Data 函数被调用时,其内部会 读取扩展板各传感器的值并直接赋值给 E53_IA1_Data 变量的各个字段。
数据上报任务
数据上报任务中,干了3件事情。注册平台指令的接收函数app_msg_deal、设备接入过程 oc_lwm2m_config、采集数据的上报。
//数据上报任务
static int app_data_report_task(void *usr_data) {
int ret = -1;
//oc_ 代表OpenCPU开发模式 /Part2会继续讨论
//允许开发者直接在通信模组(如NB-IoT、4G模组)的微控制器(MCU)上运行应用程序逻辑
oc_config_param_t oc_config;
tag_app_Agricultural Agricultural;
(void) memset(&oc_config,0,sizeof(oc_config));
//设置引导模式
oc_config.boot_mode = en_oc_boot_strap_mode_factory;
oc_config.rcv_func = app_msg_deal;
oc_config.usr_data = NULL;
oc_config.app_server.address = cn_app_server;
oc_config.app_server.port = cn_app_port;
oc_config.app_server.ep_id = cn_endpoint_id;
#if 1 //CoAPS/DTLS加密
oc_config.app_server.psk = (char *)cn_app_psk;
oc_config.app_server.psk_len = cn_app_psklen;
oc_config.app_server.psk_id = cn_endpoint_id;
#endif
ret = oc_lwm2m_config(&oc_config);
if (0 != ret) {
printf("oc lwm2m_config failure code %d", ret);
return ret;
}
else {
//在我的测试环境下/等待成功的时间在20s左右
printf("oc lwm2m_config successful..");
}
while(1) //--TODO ,you could add your own code here
{
Agricultural.messageId = cn_app_msg_Agricultural;
//多字节数据要转大端模式
Agricultural.Luminance = htons((int16_t)E53_IA1_Data.Lux);
Agricultural.Humidity = (int8_t)E53_IA1_Data.Humidity;
Agricultural.Temperature = (int8_t)E53_IA1_Data.Temperature;
//执行数据上报操作
oc_lwm2m_report( (char *)&Agricultural, sizeof(Agricultural), 1000);
//
osal_task_sleep(4*1000);
}
return ret;
}
数据上报任务本身并不复杂,就是将E53_IA1_Data数据转换为平台消息格式并调用lwM2M接口上报。设备接入/LwM2M初始化过程、app_msg_deal 平台命令接收过程,会在后续的两个小节中分别讲述。
设备接入过程
在一些资料中提及 oc_lwm2m_config(&oc_config) 函数是阻塞的,但通过这里的示例代码来看,它显然不是,至少不完全是。也有的地方提到,该函数可以工作在阻塞或非阻塞两种模式下,但并不是通过某种配置来选择。在华为LiteOS的物联网开发框架中,该函数的阻塞/非阻塞行为取决于底层对接模式的选择,而非通过参数直接配置。
//F:\0hwi\LiteOS_Lab_HCIP\iot_link\oc\oc_lwm2m\oc_lwm2m.mk
ifeq ($(CONFIG_OCLWM2MTINY_ENABLE), y)
include $(iot_link_root)/oc/oc_lwm2m/atiny_lwm2m/atiny_lwm2m.mk
else ifeq ($(CONFIG_BOUDICA150_ENABLE),y)
include $(iot_link_root)/oc/oc_lwm2m/boudica150_oc/boudica150_oc.mk
endif
而事实上,以上两种说法都是片面的。在NB通信下,上述初始化过程最终调用的函数如下,
static bool_t boudica150_boot(const char *plmn, const char *apn, const char *bands,const char *server,const char *port)
{
//(void) memset(&s_boudica150_oc_cb,0,sizeof(s_boudica150_oc_cb));
at_oobregister("qlwevind",cn_urc_qlwevtind,strlen(cn_urc_qlwevtind),urc_qlwevtind,NULL);
at_oobregister("boudica150rcv",cn_boudica150_rcvindex,strlen(cn_boudica150_rcvindex),boudica150_rcvdeal,NULL);
while(1) {
s_boudica150_oc_cb.lwm2m_observe = false;
boudica150_reboot();
...
boudica150_set_nnmi(1);
//
if(false == boudica150_check_netattach(16)) {
continue;
}
//函数内部,检查不通过时将执行osal_task_sleep(1000)操作
if(false == boudica150_check_observe(16)) {
continue; //we should do the reboot for the nB
}
break;
} //while
...
}
通过实际程序调试,可以看到,当oc连接参数不正确时,boudica150_check_observe 函数内部会执行osal_task_sleep(1000)操作,挂起线程,并对多执行16次,进而使得 boudica150_boot 函数和其上层的 oc_lwm2m_config 函数无法返回。直到检查通过。该函数是 LiteOS 中为 Boudica150 NB-IoT 芯片设计的 LwM2M 协议栈辅助函数,主要用于验证 LwM2M 服务器是否成功注册了对设备资源的观察请求。
正确设置接入参数
参见 #<IoT/基于NB28-A/BC28-CNV通信模组使用AT指令连接华为云IoTDA平台(HCIP-IoT实验2)># 中的描述,进行IP和秘钥等设置。
//连接平台基本变量/所有以下配置务必要与平台测配置一致哈
#define cn_endpoint_id "6850b867d582f20018321e88_860xxxxxxxxxx03"
#define cn_app_server "124.70.30.197"
#define cn_app_port "5684"
#define cn_app_pskid "860xxxxxxxxxx03"
#define cn_app_psk "xxxxxxxxxxxxxxxxxxxx"
#define cn_app_psklen 20
从IoTDA实例详情中获取 CoAPS 服务端地址,并通过ping指令转换为IP地址,
参见上文连接,保证CDP配置正确、DTLS开启、PSK配置位数大于16位16进制数字。这些所有的配置最好也通过PC模式下的AT指令进行测试,直至 AT+NMGS=5,00193C0064 可以正常发送数据,之后再回到代码测试中来。
命令响应任务
在模板中已经存在了接收平台命令的回调函数的实现,且使用了行缓冲区和信号量。
//if your command is very fast,please use a queue here--TODO
#define cn_app_rcv_buf_len 128
static int s_rcv_buffer[cn_app_rcv_buf_len];
static int s_rcv_datalen;
static osal_semp_t s_rcv_sync;
//理论上可以在此函数内直接处理平台下发的指令/但通常不建议这个干
static int app_msg_deal(void *usr_data, en_oc_lwm2m_msg_t type, void *data, int len) {
unsigned char *msg;
msg = data;
int ret = -1;
if(len <= cn_app_rcv_buf_len) {
//0xAAAA是平台对端侧数据上报的反馈/如果开启了的话
if (msg[0] == 0xaa && msg[1] == 0xaa) {
printf("OC respond message received! \n\r");
return ret;
}
//向行缓冲区存放指令数据
memcpy(s_rcv_buffer,msg,len);
s_rcv_datalen = len;
//信号量+1
(void) osal_semp_post(s_rcv_sync);
//调试打印
printf("recv msgID[%d] and semp_post....\r\n", msg[0]);
//返回处理正常
ret = 0;
}
return ret;
}
//下发命令处理任务(从队列中取数据) //app_msg_deal负责向队列中写数据
static int app_cmd_deal_task(void *usr_data) {
int ret = -1;
//用于响应平台操作
tag_app_Response_Agricultural_Control_Light Response_Control_Light;
tag_app_Response_Agricultural_Control_Motor Response_Control_Motor;
//用于转换收到到指令
tag_app_Agricultural_Control_Light *pt_Light_cmd;
tag_app_Agricultural_Control_Motor *pt_Motor_cmd;
//消息标识
int8u msgid = 0;
while (1)
{
//函数返回bool值
if (osal_semp_pend(s_rcv_sync, cn_osal_timeout_forever)) {
//_rcv_buffer是int数组/以下获取其低字节/也即其第1字段的值/注意是位与运算不是逻辑与运算
msgid = s_rcv_buffer[0] & 0x000000FF;
//调试打印
printf("get msgID[%d] and semp_pend..\r\n", msgid);
//指令消息ID
switch (msgid) {
case cn_app_msg_Agricultural_Control_Light:
pt_Light_cmd = (tag_app_Agricultural_Control_Light *)&s_rcv_buffer;
//注意要将来自网络的大端数据转换为本地小端模式
printf("Agricultural_Control_Light: msgid:%d mid:%d Light:%s \n\r", pt_Light_cmd->messageId, ntohs(pt_Light_cmd->mid), pt_Light_cmd->Light);
//指令为打开操作
if (pt_Light_cmd->Light[0] == 'O' && pt_Light_cmd->Light[1] == 'N')
{
HAL_GPIO_WritePin(IA1_Light_GPIO_Port, IA1_Light_Pin, GPIO_PIN_SET);
//设置应答数据
Response_Control_Light.messageId = cn_app_rspnd_Agricultural_Control_Light;
Response_Control_Light.mid = pt_Light_cmd->mid;
Response_Control_Light.errcode = 0;
Response_Control_Light.Light_State = 1;
//执行数据上报操作
oc_lwm2m_report( (char *)&Response_Control_Light, sizeof(Response_Control_Light), 1000);
}
if (pt_Light_cmd->Light[0] == 'O' && pt_Light_cmd->Light[1] == 'F' && pt_Light_cmd->Light[2] == 'F')
{
HAL_GPIO_WritePin(IA1_Light_GPIO_Port, IA1_Light_Pin, GPIO_PIN_RESET);
//设置应答数据
Response_Control_Light.messageId = cn_app_rspnd_Agricultural_Control_Light;
Response_Control_Light.mid = pt_Light_cmd->mid;
Response_Control_Light.errcode = 0;
Response_Control_Light.Light_State = 0;
//执行数据上报操作
oc_lwm2m_report( (char *)&Response_Control_Light, sizeof(Response_Control_Light), 1000);
}
break;
case cn_app_msg_Agricultural_Control_Motor:
pt_Motor_cmd = (tag_app_Agricultural_Control_Motor *)&s_rcv_buffer;
//注意要将来自网络的大端数据转换为本地小端模式
printf("Agricultural_Control_Motor: msgid:%d mid:%d Motor:%s \n\r", pt_Motor_cmd->messageId, ntohs(pt_Motor_cmd->mid), pt_Motor_cmd->Motor);
//指令为打开操作
if (pt_Motor_cmd->Motor[0] == 'O' && pt_Motor_cmd->Motor[1] == 'N')
{
HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_SET);
//设置应答数据
Response_Control_Motor.messageId = cn_app_rspnd_Agricultural_Control_Motor;
Response_Control_Motor.mid = pt_Motor_cmd->mid;
Response_Control_Motor.errcode = 0;
Response_Control_Motor.Motor_State = 1;
//执行数据上报操作
oc_lwm2m_report( (char *)&Response_Control_Motor, sizeof(Response_Control_Motor), 1000);
}
if (pt_Motor_cmd->Motor[0] == 'O' && pt_Motor_cmd->Motor[1] == 'F' && pt_Motor_cmd->Motor[2] == 'F')
{
HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_RESET);
//设置应答数据
Response_Control_Motor.messageId = cn_app_rspnd_Agricultural_Control_Motor;
Response_Control_Motor.mid = pt_Motor_cmd->mid;
Response_Control_Motor.errcode = 0;
Response_Control_Motor.Motor_State = 0;
//执行数据上报操作
oc_lwm2m_report( (char *)&Response_Control_Motor, sizeof(Response_Control_Motor), 1000);
}
break;
default:
printf("Err command recved!");
break;
} // switch
} //if semp pend
else {
printf("osal_semp pend failurre!");
}
} //while
return ret;
}
在 oc_lwm2m_config 函数的输入参数中,我们传递了 app_msg_deal 函数指针进去。
平台下发的指令到达端侧LwM2M底层后,通过回调app_msg_deal,将指令消息传递到用户应用层。在我们的上述代码中,这些数据被存储到 s_rcv_buffer 缓冲区,在信号量的计数控制下,我们在命令处理任务中从 s_rcv_buffer 取出数据,判定不同消息ID后分别处理。
程序调试
首先要通过串口打印或LCD屏幕显示,确保程序基础运行是正常的。然后确定设备连接华为云平台成功,
确认平台侧收到了设备上报的实时数据,
数据下发操作和响应,
扩展板灯光控制效果,
其他
实验进行到现在这个阶段,就已经可以断定,在NB-IoT通信模组的情况下,主板程序在应用层调用LwM2M的接口进行初始化、数据上报和命令接收过程,在某个层次上确有AT指令参与工作。至于LwM2M和NB模组如何协同,我们下一篇再详谈,或单独开篇。HCIP实验-5/基于NB-IoT的智慧农业实验,完整工程源码,从CSDN资源下载。本文只是从面上实践了一个例程,还有很长的路要走。
下一篇,
#<IoT/HCIP实验-5/基于NB-IoT / WIFI的智慧农业实验(探寻LwM2M/CoAP与AT指令、Wifi、NB-IoT之间的关联)>#