目录
一、前言
其实在2019年8月份就写了一篇《基于rt-thread使用nrf24l01实现多点通信》,详细记录了怎么修改nrf24l01软件包实现多点通讯,来采集多个18B20节点的温度数据的功能。使用文件系统进行数据存储、使用OneNet软件包与OneNet云端交互也是在那个时候就完成的。
那为什么当时没写呢?没别的原因 ,就是 赖 赖 赖!!!!!拖 拖 拖 !!!!
那为什么现在又要写了呢?是因为当我现在再来看之前做的这个项目的时候,都不敢相信是自己做的,对于使用的各个软件包功能、实时过程中的一些细节等问题当时是记得非常清楚理得非常顺的,但时间一久到现在完全忘了,如同过眼云烟,再去理的时候相当痛苦。再加上最近想在单片机上实现一个web功能,所以准备重拾当时的这个项目,借此文档重新梳理一遍。备忘!!!
二、硬件环境
STM32F103ZET6:512KFALSH、64KSRAM。正点原子精英开发板
SD Card、ESP8266模块、NRF24L01模块
三、功能描述
1、利用nrf24l01无线组网实现多点通讯,采集多个18B20节点温度数据
2、使用文件系统,存储采集的温度数据
3、使用esp8266-wifi模块将接收节点的数据传输至OneNet云
4、OneNet云的简单应用开发,实现远程监控
四、组件与软件包列表
SAL组件 |
该组件完成对不同网络协议栈或网络实现接口的抽象并对上层提供一组标准的 BSD Socket API。这样开发者只需要关心和使用网络应用层提供的网络接口,而无需关心底层具体网络协议栈类型和实现,方便开发者完成协议栈的适配和网络相关的开发。主要功能如下: ● 抽象、统一多种网络协议栈接口; ● 提供 Socket 层面的 TLS 加密传输特性; ● 支持标准 BSD Socket API; ● 统一的 FD 管理,便于使用 read/write poll/select 来操作网络功能; |
netdev 组件主要作用是解决设备多网卡连接时网络连接问题,用于统一管理各个网卡信息与网络连接状态,并且提供统一的网卡调试命令接口。 其主要功能特点如下所示: ● 抽象网卡概念,每个网络连接设备可注册唯一网卡。 ● 提供多种网络连接信息查询,方便用户实时获取当前网卡网络状态; ● 建立网卡列表和默认网卡,可用于网络连接的切换; ● 提供多种网卡操作接口(设置 IP、DNS 服务器地址,设置网卡状态等); ● 统一管理网卡调试命令(ping、ifconfig、netstat、dns 等命令); |
|
AT 组件1.3.0 |
AT 组件是基于 RT-Thread 系统的 AT Server 和 AT Client 的实现,组件完成 AT 命令的发送、命令格式及参数判断、命令的响应、响应数据的接收、响应数据的解析、URC 数据处理等整个 AT 命令数据交互流程。而AT Socket是作为 AT Client 功能的延伸,使用 AT 命令收发作为基础,实现标准的 BSD Socket API,完成数据的收发功能,使用户通过 AT 命令完成设备连网和数据通讯。 |
at_device-V2.0.0 |
该软件包是不同模块针对AT组件的移植文件,来实现的AT Socket功能,进而实现的AT设备Socket功能。V1.X.0版本与V2.X.X版本,实现差异比较大,主要用于适配 AT 组件和系统的改动。 |
该软件包用于解析和打包JSON数据,用于MQTT通讯时数据的解析和打包 |
|
该软件包是在 Eclipse paho-mqtt 源码包的基础上设计的一套 MQTT 客户端程序,源码包只是提供一套MQTT在发布数据和订阅主题时对MQTT协议的解析和封包,用户需要自己实现TCP连接建立与断开、TCP数据发送与接收功能。正是基于这些需求,pahomqtt软件包不仅基于socket实现了网络连接与断开、网络数据包的发送接收等功能,还实现了只需简单配置MQTT连接参数就可轻松连云、主题发布与订阅等功能 |
|
该软件包基于pahomqtt软件包实现了OneNet 平台MQTT连接、数据的发送、数据的接收、设备的注册和控制等功能 |
|
1、SAL 组件
简介 https://www.rt-thread.org/document/site/programming-manual/sal/sal/
2、netdev 组件
简介 https://www.rt-thread.org/document/site/programming-manual/netdev/netdev/
Socket 套接字描述符的创建建立在 netdev 网卡基础上,所以每个创建的 Socket 对应唯一的网卡。协议簇、网卡和 socket 之间关系如下图所示:
3、AT 组件
简介 https://www.rt-thread.org/document/site/programming-manual/at/at/
通过 AT 组件,设备可以作为 AT Client 使用串口连接其他设备发送并接收解析数据,可以作为 AT Server 让其他设备甚至电脑端连接完成发送数据的响应,也可以在本地 shell 启动 CLI 模式使设备同时支持 AT Server 和 AT Client 功能,该模式多用于设备开发调试。
4、at device软件包
简介 https://github.com/RT-Thread-packages/at_device
示例说明 https://www.rt-thread.org/document/site/application-note/components/at/an0014-at-client/
依赖
- RT_Thread 4.0.2+
- RT_Thread AT 组件 1.3.0+
- RT_Thread SAL 组件
- RT-Thread netdev 组件
版本号说明
AT device 软件包目前已经发布多个版本,各个版本之间选项配置方式和其对应的系统版本有所不同,下面主要列出当前可使用的软件包版本信息:
V1.2.0:适用于 RT-Thread 版本小于 V3.1.3,AT 组件版本等于 V1.0.0;
V1.3.0:适用于 RT-Thread 版本小于 V3.1.3,AT 组件版本等于 V1.1.0;
V1.4.0:适用于 RT-Thread 版本小于 V3.1.3或等于 V4.0.0, AT 组件版本等于 V1.2.0;
V1.5.0:适用于 RT-Thread 版本小于 V3.1.3 或等于 V4.0.0, AT 组件版本等于 V1.2.0;
V1.6.0:适用于 RT-Thread 版本等于 V3.1.3 或等于 V4.0.1, AT 组件版本等于 V1.2.0;
V2.0.0/V2.0.1:适用于 RT-Thread 版本大于 V4.0.1 或者大于 3.1.3, AT 组件版本等于 V1.3.0;
laster:只适用于 RT-Thread 版本大于V4.0.1 或者大于 3.1.3, AT 组件版本等于 V1.3.0;
注意事项
1、AT device 软件包适配的模块暂时不支持作为 TCP Server 完成服务器相关操作(如 accept 等);
2、AT device 软件包默认设备类型为未选择,使用时需要指定使用设备型号;
3、laster 版本支持多个选中多个 AT 设备接入实现 AT Socket 功能,V1.X.X 版本只支持单个 AT 设备接入。
4、AT device 软件包目前多个版本主要用于适配 AT 组件和系统的改动,推荐使用最新版本 RT-Thread 系统,并在 menuconfig 选项中选择 latest 版本
at device与AT 组件(AT Client & AT Socket)、SAL组件、netdev组件的关系
对于AT设备,应用层在调用SAL 提供的BSP网络接口的时候,其实调用的就是 AT Socket(AT设备标准的 BSD Socket API),而AT Socket是借助于netdev组件来实现的,并不是在 at device软件包中封装了at_device(AT设备的句柄)中的3个ops功能就直接提供给了AT Socket;AT Device是先基于AT Client实现了at_device中3个ops功能 , 再将at_device 注册到 at_device_list / at_device.c链表中,最后通过netdev组件抽象网卡的功能将at_device中的netdev注册到网卡列表 netdev_list 中; 通常应用程序在使用AT设备的BSP Socket(AT Socket)的时候,会先申请一个socket,这个时候会从默认网卡netdev_default 或在 网卡列表netdev_list 中根据协议族去找到对应的网卡,再根据网卡信息从at_device_list 链表中获取到该网卡对应的at_device,接下来的AT Socket操作就是利用at_device中的3个ops来实现的。
拿ec20设备来具体分析下实现过程:
第一步:在ec20的class文件at_device_ec20.c和at_socket_ec20.c中,基于at client分别实现了ec20_device_ops、ec20_netdev_ops 、ec20_socket_ops 结构中的功能。
const struct at_device_ops ec20_device_ops =
{
ec20_init,
ec20_deinit,
ec20_control,
};
const struct netdev_ops ec20_netdev_ops =
{
ec20_netdev_set_up,
ec20_netdev_set_down,
RT_NULL,
ec20_netdev_set_dns_server,
RT_NULL,
#ifdef NETDEV_USING_PING
ec20_netdev_ping,
#endif
#ifdef NETDEV_USING_NETSTAT
ec20_netdev_netstat,
#endif
};
static const struct at_socket_ops ec20_socket_ops =
{
ec20_socket_connect,
ec20_socket_close,
ec20_socket_send,
ec20_domain_resolve,
ec20_socket_set_event_cb,
};
第二步:在at_device_ec20.c中,系统自动初始化 INIT_DEVICE_EXPORT(ec20_device_class_register) 将 at_device_class(ec20_device_ops +ec20_socket_ops )自动注册到了at_device_class_list / at_device.c 链表中
struct at_device_class
{
uint16_t class_id; /* AT device class ID */
const struct at_device_ops *device_ops; /* AT device operaiotns */
#ifdef AT_USING_SOCKET
uint32_t socket_num; /* The maximum number of sockets support */
const struct at_socket_ops *socket_ops; /* AT device socket operations */
#endif
rt_slist_t list; /* AT device class list */
};
第三步:在应用程序中调用 at_device_register / at_device.c ,调用过程如下:
1、在应用程序中定义at_device_ec20 的实例 e0
struct at_device_ec20
{
char *device_name;
char *client_name;
int reset_pin;//2019、9、10 add by denghengli
int power_pin;
int power_status_pin;
size_t recv_line_num;
struct at_device device;
void *socket_data;
void *user_data;
};
2、调用 at_device_register / at_device.c 完成了 e0 中 at_device 中 at_socket 内存申请、将 at_device_class (c20_socket_ops + ec20_device_ops) 从at_device_class_list中读出赋值 at_device中的class、将 at_device 注册到 at_device_list / at_device.c链表中
struct at_device
{
char name[RT_NAME_MAX]; /* AT device name */
rt_bool_t is_init; /* AT device initialization completed */
struct at_device_class *class; /* AT device class object */
struct at_client *client; /* AT Client object for AT device */
struct netdev *netdev; /* Network interface device for AT device */
#ifdef AT_USING_SOCKET
rt_event_t socket_event; /* AT device socket event */
struct at_socket *sockets; /* AT device sockets list */
#endif
rt_slist_t list; /* AT device list */
char (*init_succ_ind)(void);
void *user_data; /* User-specific data */
};
3、调用ec20_init的函数指针,完成 e0 中 at_device 中 at_client 的初始化、netdev 网卡的注册、ec20入网使能
- at_client的初始化,创建了client_parser线程,作用为ec20与串口之间的数据交互,是ops功能实现的前提
struct at_client
{
rt_device_t device;
at_status_t status;
char end_sign;
/* the current received one line data buffer */
char *recv_line_buf;
/* The length of the currently received one line data */
rt_size_t recv_line_len;
/* The maximum supported receive data length */
rt_size_t recv_bufsz;
rt_sem_t rx_notice;
rt_mutex_t lock;
at_response_t resp;
rt_sem_t resp_notice;
at_resp_status_t resp_status;
struct at_urc_table *urc_table;
rt_size_t urc_table_size;
rt_thread_t parser;
};
- netdev网卡注册,netdev_ops = ec20_netdev_ops 和 sal_user_data 的赋值
/* network interface device object */
struct netdev
{
rt_slist_t list;
char name[RT_NAME_MAX]; /* network interface device name */
ip_addr_t ip_addr; /* IP address */
ip_addr_t netmask; /* subnet mask */
ip_addr_t gw; /* gateway */
#if NETDEV_IPV6
ip_addr_t ip6_addr[NETDEV_IPV6_NUM_ADDRESSES]; /* array of IPv6 addresses */
#endif /* NETDEV_IPV6 */
ip_addr_t dns_servers[NETDEV_DNS_SERVERS_NUM]; /* DNS server */
uint8_t hwaddr_len; /* hardware address length */
uint8_t hwaddr[NETDEV_HWADDR_MAX_LEN]; /* hardware address */
uint16_t flags; /* network interface device status flag */
uint16_t mtu; /* maximum transfer unit (in bytes) */
const struct netdev_ops *ops; /* network interface device operations */
netdev_callback_fn status_callback; /* network interface device flags change callback */
netdev_callback_fn addr_callback; /* network interface device address information change callback */
#ifdef RT_USING_SAL
void *sal_user_data; /* user-specific data for SAL */
#endif /* RT_USING_SAL */
void *user_data; /* user-specific data */
};
- ec20入网使能,创建了ec20_init_thread_entry线程设备联网,联网成功后退出。在联网成功后接着创建了ec20_check_link_status_entry线程用为监测网络状态并更新网卡状态,可以在这里做一些网络连接异常后硬件复位、重新初始化模块,通过监测网络状态来实现掉卡重连的功能,但是一般这里监测的网络状态不是实时的,因为一般蜂窝模块设备 SIM 卡去掉之后不会实时同步模块内部状态,只有等待模块内部信息改变了,netdev 网卡信息才会同步,所以最好还是通过心跳机制来监测链路的异常情况。
5、pahomqtt软件包
简介 https://github.com/RT-Thread-packages/paho-mqtt
工作原理 https://github.com/RT-Thread-packages/paho-mqtt/blob/master/docs/principle.md
使用指南 https://github.com/RT-Thread-packages/paho-mqtt/blob/master/docs/user-guide.md
示例说明 https://github.com/RT-Thread-packages/paho-mqtt/blob/master/docs/samples.md
6、onenet软件包
简介 https://github.com/RT-Thread-packages/onenet
工作原理 https://github.com/RT-Thread-packages/onenet/blob/master/docs/principle.md
使用指南 https://github.com/RT-Thread-packages/onenet/blob/master/docs/user-guide.md
示例说明 https://github.com/RT-Thread-packages/onenet/blob/master/docs/samples.md
注意事项
1、设置命令响应回调函数之前必须要先调用onenet_mqtt_init函数,在初始化函数里会将回调函数指向RT_NULL。
2、命令响应回调函数里存放响应内容的 buffer 必须是 malloc 出来的,在发送完响应内容后,程序会将这个 buffer 释放掉。
static void onenet_cmd_rsp(uint8_t *recv_data, size_t recv_size, uint8_t **resp_data, size_t *resp_size) { char resp_buf[] = { "cmd is received!\n" }; LOG_D("recv data is %.*s\n", recv_size, recv_data); /* user have to malloc memory for response data */ *resp_data = (uint8_t *) rt_malloc(strlen(resp_buf)); strncpy((char *)*resp_data, resp_buf, strlen(resp_buf)); *resp_size = strlen(resp_buf); }
3、在向主题发布数据的时候,并不能在onenet_mqtt_init 初始化成功之后就开始肆无忌惮的发布数据了,需要判断MQTT真正连接成功执行mqtt_online_callback上线回调函数之后才能开始发布,一旦MQTT连接断执行mqtt_offline_callback下线回调函数之后则停止发布数据,否则就会发布失败出现各种问题 如下图,有可能MQTT就一直连接不成功。具体原因看看onenet_mqtt_init 初始化中到底干了啥
- 获取onenet连接时需要认证的参数。如 产品ID、产品秘钥、设备ID 等。
- 配置MQTT连接参数如三元组、遗嘱信息、订阅主题信息等信息,注册连接上线下线成功回调函数
创建paho_mqtt_thread线程,这个线程才是真正的去连接MQTT服务器、向主题发布数据、订阅主题并接受主题下发数据。在开始连接的时候会执行 mqtt_connect_callback 、MQTTQ服务器连接成功的时候会执行mqtt_online_callback 、MQTT断开连接的时候会执行mqtt_offline_callback
五、应用实现
1、nrf24l01温度数据采集
static void nrf24l01_thread_entry(void* parameter)
{
struct nrf_msg nrf24l01_msg;
struct temp_data temp_data;
struct nrf24_cfg nrf24l01_cfg;
int rlen;
char rbuf[32 + 1], tmp_buf[50];
char tbuf[32] = "first\r\n";
char res=0,cnt = 0;
uint8_t chnum=0;
nrf24l01_dev = (rt_spi_wire_device_t)rt_device_find(SPI_WIRE_DEV_NAME);
if (nrf24l01_dev == RT_NULL)
{
LOG_E("Can't find device:%s\n", SPI_WIRE_DEV_NAME);
return;
}
nrf24l01_default_param(&nrf24l01_cfg);
nrf24l01_cfg.selchx = 0X3F;
nrf24l01_cfg.use_irq = 1;
nrf24l01_cfg.role = ROLE_PRX;
nrf24l01_dev->nrf24l01.ops->nrf_init(nrf24l01_cfg, NRF24L01_CE_PIN, NRF24L01_IRQ_PIN);
while (1)
{
rt_memset(rbuf, 0, sizeof(rbuf));
rlen = nrf24l01_dev->nrf24l01.ops->prx_cycle((uint8_t*)rbuf, (uint8_t*)tbuf, rt_strlen((char *)tbuf), &chnum);
if (rlen > 0) // received data (also indicating that the previous frame of data was sent complete)
{
/* 打印接收到的数据 */
rt_memset(tmp_buf, 0, sizeof(tmp_buf));
rt_sprintf(tmp_buf, "nrf24l01 pipe(%d) data:%s\n", (char)chnum, rbuf);
LOG_D((char *)tmp_buf);
/* 准备应答的数据 */
rt_sprintf((char *)tbuf, "i-am-PRX:%dth\n", cnt++);
/* 通过sscnaf解析收到的数据 */
res = sscanf((char *)rbuf, "%d,+%f", &temp_data.timestamp, &temp_data.temperature);
if(res != 2)
{
/* 通过sscnaf解析收到的数据 */
if(sscanf((char *)rbuf, "%d,-%f", &temp_data.timestamp, &temp_data.temperature) != 2)
{
continue;
}
temp_data.temperature = -temp_data.temperature;
}
rt_memset(tmp_buf, 0, sizeof(tmp_buf));
sprintf(tmp_buf, "%d,%f\n", temp_data.timestamp, temp_data.temperature);
/* 将数据存放到ringbuffer里 */
if (chnum == 0) rt_ringbuffer_put(nrf_p0_ringbuf, (rt_uint8_t *)tmp_buf, strlen(tmp_buf));
else if (chnum == 1) rt_ringbuffer_put(nrf_p1_ringbuf, (rt_uint8_t *)tmp_buf, strlen(tmp_buf));
else if (chnum == 2) rt_ringbuffer_put(nrf_p2_ringbuf, (rt_uint8_t *)tmp_buf, strlen(tmp_buf));
else if (chnum == 3) rt_ringbuffer_put(nrf_p3_ringbuf, (rt_uint8_t *)tmp_buf, strlen(tmp_buf));
else if (chnum == 4) rt_ringbuffer_put(nrf_p4_ringbuf, (rt_uint8_t *)tmp_buf, strlen(tmp_buf));
else if (chnum == 5) rt_ringbuffer_put(nrf_p5_ringbuf, (rt_uint8_t *)tmp_buf, strlen(tmp_buf));
/* 收到数据,并将数据存放到ringbuffer里后,才发送事件, 用于通知DFS线程存储数据 */
rt_event_send(nrf_event, WRITE_EVENT);
/* 判断onenet是否上报完成,不会等待 */
if (rt_sem_take(onenet_upok_sem, 0) == RT_EOK)
{
nrf24l01_msg.chnum = chnum;
nrf24l01_msg.data.timestamp = temp_data.timestamp;
nrf24l01_msg.data.temperature = temp_data.temperature;
rt_mq_send(nrf_msg_mq, &nrf24l01_msg, sizeof(struct nrf_msg));
}
}
else // no data
{
// LOG_E("nrf not received\n");
}
rt_thread_mdelay(100);
}
}
2、onenet数据上报
static void onenet_trans_thread_entry(void* parameter)
{
struct nrf_msg nrf24l01_msg;
char tmp_buf[100], stream_name[20] = "";
/* 永久等待方式接收信号量,若收不到,该线程会一直挂起 */
rt_sem_take(mqttinit_sem, RT_WAITING_FOREVER);
/* 后面用不到这个信号量了,把它删除了,回收资源 */
rt_sem_delete(mqttinit_sem);
rt_sem_release(onenet_upok_sem);
while (1)
{
if (rt_mq_recv(nrf_msg_mq, &nrf24l01_msg, sizeof(struct nrf_msg), RT_WAITING_FOREVER) == RT_EOK)
{
rt_sprintf(stream_name, "temperature_p%d", nrf24l01_msg.chnum);
/* 上传发送节点1的数据到OneNet服务器,数据流名字是temperature_p0 */
if (onenet_mqtt_upload_digit(stream_name, nrf24l01_msg.data.temperature) != RT_EOK)
{
/* 打印接收到的数据 */
rt_memset(tmp_buf, 0, sizeof(tmp_buf));
sprintf(tmp_buf, "upload %s has an error, try again\n", stream_name);
LOG_D((char*)tmp_buf);
}
else
{
rt_memset(tmp_buf, 0, sizeof(tmp_buf));
sprintf(tmp_buf, "upload %s OK >>> temp:%f\n", stream_name, nrf24l01_msg.data.temperature);
LOG_D((char*)tmp_buf);
}
rt_thread_delay(rt_tick_from_millisecond(1000));
/* onenet上报成功之后,释放信号量告知nrf线程可以往消息队列发送消息了 */
rt_sem_release(onenet_upok_sem);
}
}
}
static void onenet_init_thread_entry(void* parameter)
{
char onenet_mqtt_init_failed_times = 0;
/* mqtt初始化 */
while (1)
{
if (!onenet_mqtt_init())
{
/* 注册平台下行命令响应回调函数 */
onenet_set_cmd_rsp_cb(onenet_cmd_rsp);
/* mqtt初始化成功之后,释放信号量告知onenet_upload_data_thread线程可以上传数据了 */
rt_sem_release(mqttinit_sem);
return;
}
rt_thread_mdelay(100);
LOG_E("onenet mqtt init failed %d times", onenet_mqtt_init_failed_times++);
}
}
六、结果展示
1、平台设备数据流展示
2、平台应用展示
来源:CSDN
作者:hurryddd
链接:https://blog.csdn.net/m0_37845735/article/details/104494759