概述
本文档介绍如何使用资源极低的 ESP32/8266 WiFi 模组进行前期开发联调与 BSP 无关的业务功能。方便开发人员快速上手。在基本业务功能端到端调试完毕之后再将标准的 C 语言程序移植到 cat1 或者 NBIoT 无线模组开发环境进行后续 BSP 的开发或者适配。
硬件选型
ESP 有多种型号可选,这里我们选用 ESP01(1m RAM)以及 M5Stack C Plus 进行开发,这是 ESP 系列当中配置最低的模组,在资源有限的环境中增加业务逻辑,是确保在不同配置模组之间移植的较好选择。
- ESP01
另外需要一个 CH340 下载器进行下载。
购买链接:
EPS01链接:https://item.jd.com/10023423910094.html
下载器链接:https://item.jd.com/10033537688506.html
- M5Stack C Plus
购买链接:https://ic-item.jd.com/10034666963148.html
开发环境搭建
注明:下面的开发环境均以 ESP01 为模板编写,M5Stack 的开发环境和 ESP01 略有不同,在每一段的注释中标记。
推荐开发和构建环境为 VSCode 和 PlatformIO 插件,在各种平台上均可以开发和利用 UART 烧录固件。
- 安装 VSCode,并且在插件栏目中搜索 PlatformIO。
- 装好之后可以在 VSCode 侧边栏里看到 PlatformIO 的图标以及首页。这时如果导入带有 platformio.ini 配置的工程文件夹,即可直接被 PlatformIO 捕获,并且可以直接编辑配置和进行构建。
- 如果是初次安装 PlatformIO 环境,则需要在上述面板的 Platforms 当中搜索 ESP8266 基础支持库,并且进行安装。
M5Stack:下载 ESP32 基础库,Libraries 当中在 example 工程的基础上添加 M5StickCPlus 即可。
4. 装好之后,导入你的 C/C++ 功能即可进行开发和构建了,下面三个蓝色框分别对应:构建、烧录、打印调试日志。
- 在工程的 platformio.ini 文件中可以对构建全局变量进行编辑,例如指定额外的源码目录、头文件目录、库等。详情可参考:https://docs.platformio.org/en/latest/projectconf/index.html
- ESP 是一个开放的,生态丰富的社区,有许多开发者为这款开源硬件贡献自己开发的稳定的功能库,例如 MQTTPubSub,WifiManager,AliotSDK 等,可以直接在 PlatformIO 的 Libraries 栏目中搜索并下载。下载好的库均是以源码形式和项目进行混合编译,库目录位于 ${USER}/.platformio/packages/framework-arduinoespressif8266/cores/esp8266/ 目录下面。
- 项目当中的内容分为两部分:模组逻辑部分和模组安全 SDK 集成。在开发体验过程中,我们可以同时扮演模组开发者以及安全SDK开发者。
模组安全 SDK 代码结构
模组安全 SDK 代码树结构的简单说明如下:
das2 +
+--board // 模组硬件相关的 BPS 侧差异化代码,通常是一些接口
+--example + // 可以用于测试和运行的示例,例如 ESP32 的示例就在其中
+--esp8266 // ESP8266 WiFi 模组适配样例
+--linux // GNULinux 通用适配样例
+--m5720 // M5720 cat1 模组适配样例
+--m5stack // 基于 ESP32 模组的 M5 CPLUS 1 设备适配样例(含设备 GUI 部分)
+--src // 主体代码
+--test // 单元测试代码
其中 src 目录下包括了所有模组安全检测和防御机能的通用能力,所有 example 均是可完整编译的工程,这些工程可以通过 symbolic link 的方式链接到 SDK 暴露出来的客户侧代码,方法如下:
Windows:
cd exmpale/esp8266
make_link.bat
M5Stack
cd example/m5stack
make_link.bat
Linux/Mac:
cd example/esp8266
./make_link.sh
M5Stack
cd example/m5stack
./make_link.sh
ESP 联网向导
在 ESP8266 example 当中,可以通过调整三元组定义,连接到阿里云物联网平台公共实例(内置一个免费三元组供体验)。方法如下:
- 在安装了 PlatformIO 插件的 VSCode 当中以文件夹方式打开 example/esp8266 目录,工程会自动导入和部署。
- 找到源码:example/esp8266/platform.ini,在其中可编辑 build.flag 的一个宏开关,来指定日常、预发和线上,其中指定:
- -DENV_RELEASE=1 表示线上环境
- 找到源码:src/alink.cpp,根据上面选择的不同的宏开关,填入在物联网平台上申请的 PK,DN,DS 然后进行固件编译。
- 对于需要切换 WiFi 热点的模组,需要先利用 ESP8266 Downloader 对 flash 进行擦除,然后再烧写固件,具体步骤见下面 “构建、烧录和调试”章节。
- EPS8266 上电之后,会首先切换为 STA 模式,并向周围发布一个名为 ESPXXXX 的 WiFi 热点。开发者准备一台自己的手机,寻找到这个热点之后进行连接。
- 连接上之后会弹出一个 WifiManager 的配置页面,点击配置,即可扫描获得周围的 AP,选择一个公开的 AP,例如 IoT 实验室 AP(亦或者自己的手机做 AP),输入密码进行配置。如果配置正确,ESP32 会自动重启进入 Client 模式,连接到指定的 AP,随后自动建立 MQTT 连接到阿里云 IoT 平台和安全中心,开始工作。
M5Stack:PlatformIO 的仓库里的 WiFiManager 版本由于年久失修,不能直接用了,需要从 github 下载手动集成最新的 WifiManager(example 里已经带了):https://github.com/tzapu/WiFiManager,
*此外 ESP32 的 LITTLEFS 和 ESP8266 的也略有不同,并且 PlatformIO 仓库里的 LITTLEFS 和 vfs api 有兼容性 bug,exist 函数的 create 布尔参数要自己改一下。
功能调试
模组侧 HOOK
在 ESP8266 的开源生态中,我们可以直接接触到 WiFi FWK 的所有源码,其中几个比较重要的部分,从协议栈接口向上依次为:
- Wifi-TCP 客户端 :它包含了一个用于实现对接层2协议栈的多态 C++ 实现和接口,对上接入 lwip 的 TCP 协议栈。在这个客户端上下文当中,可以自主添加代码,实现网络流量审计、保存、挂接(hook)网络访问事件。源码位置在:
ARDUINO_FRAMEWORK = ${USER}/.platformio/packages/framework-arduinoespressif8266
${ARDUINO_FRAMEWORK}/libraries/ESP8266WiFi/src/include/ClientContext.h
- TCP 协议链接 STUB 可挂接在 tcp_connect 函数入口,捕获 ip4_addr 以及 port 参数。
- 通过 Wifi-TCP Client 上行流量审计挂接在 write 函数入口,每次增加 dl 字节,保存。
- 通过 Wifi-TCP Client 下行流量审计挂接在 _consume 函数的 tcp_receved 之后,每次增加 size 字节,保存。
- Wifi-UDP 客户端:它包含了一个用于实现对接层2协议栈的多态 C++ 实现和接口,对上接入 lwip 的 UDP 协议栈。在这个客户端上下文当中,可以自主添加代码,实现网络流量审计、保存、挂接(hook)网络访问事件。源码位置在:
${ARDUINO_FRAMEWORK}/libraries/ESP8266WiFi/src/include/UDPContext.h
- UDP 协议链接 STUB 可挂接在 connect 函数入口,捕获 UDP 连接的 addr 和 port 信息。
- 通过 Wifi-UDP Client 上行流量审计挂接在 trySend 函数 udp_sendto 调用成功之后,每次增加 data_size 字节,保存。
- 通过 Wifi-UDP Client 下行流量审计挂接在 _consume 函数的末尾,每次增加 size 字节,保存。
- Wifi 模组作服务端,则会调用 WifiServer.cpp 当中的接口实现 listen 和 accept 等操作,暂时不做挂接,待将来有捕获网络连入事件时再进行入口捕获。
附件中为基于 ESP8266 模组已经配置好模组框架层挂接点的源代码,可以直接替换上述文件位置,随同模组固件一起编译,即可在模组层面向安全 SDK 提供相应的事件报告。
- 网络事件挂钩,这一组挂钩包括模组对外连接 IP:port 以及域名解析的挂钩模拟,以 TCP 为例:
- IP:port 类型访问挂钩在:
${ARDUINO_FRAMEWORK}/libraries/ESP8266WiFi/src/include/ClientContext.h
int connect(ip_addr_t* addr, uint16_t port) 的 tcp_connect 调用之前。挂钩函数原型:
void lsocClientReportIPAccessEvent(ip_addr_t*addr, uint16_tport, intipVer, constchar*protocol);
- host 域名访问挂钩在
${ARDUINO_FRAMEWORK}/libraries/ESP8266WiFi/src/WifiClient.cpp
int WiFiClient::connect(const char* host, uint16_t port) 开头的位置。挂钩函数原型:
void lsocClientReportHostAccessEvent(const char* hostname, uint16_t port);
这两个挂钩函数当中既需要调用 access stub 又要调用 control stub。
IPV4 UDP 协议和 IPV6 同理。
附件,修改过的 ESP8266 Wifi Client,其它埋点请自行修改。
M5Stack:
EPS32 的 ARDUINO_FRAMEWORK 是 ${USER}/.platformio\packages\framework-arduinoespressif32
TCP events:${ARDUINO_FRAMEWORK }/libraries/WiFi/src/WifiClient.cpp
- 域名连接事件:int WiFiClient::connect(const char *host, uint16_t port, int32_t timeout) 函数,域名解析完成之后 call 回调函数:lsocClientReportHostAccessEvent,带入域名和地址信息进行关联。
- IP 地址连接事件:int WiFiClient::connect(IPAddress ip, uint16_t port, int32_t timeout) 函数,直接 call lsocClientReportIPAccessEvent。
- 上行流量挂钩:size_tWiFiClient::write(constuint8_t*buf, size_tsize) 函数,发送成功之后增加 res 字节数。
- 下行流量挂钩:int WiFiClient::read(uint8_t *buf, size_t size) 函数,从 __rxBuffer 对象读取流成功之后增加 res 字节数。
(这里注意 ESP32 和 ESP8266 的数据结构略有差别)。
======================================================================
UDP events:${ARDUINO_FRAMEWORK }/libraries/WiFi/src/WifiUdp.cpp
- 向 IP 地址发送报文:uint8_t WiFiUDP::begin(IPAddress address, uint16_t port) 函数,在建立 UDP socket 之前 call 回调 lsocClientReportIPAccessEvent。
- 向 URL 发送报文:int WiFiUDP::beginPacket(const char *host, uint16_t port) 函数,在建立 UDP socket 之前 call 回调 lsocClientReportHostAccessEvent。
- 上行流量挂钩:int WiFiUDP::endPacket() 函数,在 sendto 成功之后增加 sent字节数。
- 下行流量挂钩:int WiFiUDP::parsePacket() 函数,在 rx_buffer write 成功之后增加 len 字节数。
======================================================================
附件,修改过的 ESP32 Wifi Client,其它埋点请自行修改。
模组主控流程
模组应用层的主控流程位于 src 目录下,其它 ESP 库和三方库可存放在工程目录的 lib 下。应用程序主入口在 main.cpp 的 setup() 函数当中。一些比较重要的模组主控和 SDK 集成的部分为:
- ${PROJECT}/lib/AliyunIoTSDK/src/AliyunIoTSDK.cpp,Alink SDK,用于连接 LP。
- ${PROJECT}/src/security.cpp,模组应用和 SDK 的集成点源码,当中实现了所有 hal 适配层的函数,包括填充固件信息、实现被动 stub(获取流量)接口等。
- ${PROJECT}/src/alink.cpp,模组应用和 AliyunIoTSDK 的集成点源码,实现阿里云上云,消息订阅和发布等基本操作,其中的 iot 对象被设置为了 SDK 当中的 session 对象,负责 SOC 消息收发和回调处理。
安全 SDK 在工程目录中为 ${DAS_ROOT}/src 下,对外部客户以静态库形式提供。包括所有的 SDK 主流程、步进驱动取证、服务、ATI、Stub 和 BSP 相关的接口等,在 ESP32 环境中可以和上述的模组协议栈、模组三方库共同通过源码进行编译。SDK 的作业驱动由模组主控的 security.cpp 当中通过调用 das_stepping 以及 MQTT 下行命令驱动实现,其中包含周期采样、消息上报等。详情参考模组安全 SDK 源代码。
构建、烧录和调试
- 点击 PlatformIO 的 build 按钮即可进行构建,如果需要引入额外的 include 路径,需要在 platformio.ini 当中进行 build_flag 配置:
ESP8266 如下:
build_flags =
-DLOG_DEBUG=1
-DLOG_INFO=1
-DLOG_ERROR=1
-DENV_DAILY=1
-DDAS_ESP8266=1
-DDAS_PLATFORM_ESP=1
-DDAS_DEBUG=1
-Wno-unused-function
-Wno-unused-variable
-I${common.workspace}/lib/lsoc_das2/
-I${common.workspace}/lib/lsoc_das2/include
-I${common.workspace}/lib/lsoc_das2/proto
-I${common.workspace}/lib/lsoc_das2/stubs/include
-I${common.workspace}/lib/lsoc_das2/stubs/include/at
-I${common.workspace}/lib/lsoc_das2/stubs/include/hal
-I${common.workspace}/lib/lsoc_das2/stubs/include/osa
-I${common.workspace}/lib/lsoc_das2/ati/inc
M5Stack 如下:
build_flags =
-DCORE_DEBUG_LEVEL=4
-DLOG_DEBUG=1
-DLOG_INFO=1
-DLOG_ERROR=1
-DENV_RELEASE=1
-DWITH_LCD=1
-DDAS_ESP32=1
-DDAS_PLATFORM_ESP=1
-DDAS_DEBUG=1
-Wno-unused-function
-Wno-unused-variable
-I${common.workspace}/lib/lsoc_das2/
-I${common.workspace}/lib/lsoc_das2/include
-I${common.workspace}/lib/lsoc_das2/proto
-I${common.workspace}/lib/lsoc_das2/stubs/include
-I${common.workspace}/lib/lsoc_das2/stubs/include/at
-I${common.workspace}/lib/lsoc_das2/stubs/include/hal
-I${common.workspace}/lib/lsoc_das2/stubs/include/osa
-I${common.workspace}/lib/lsoc_das2/ati/inc
-I${common.workspace}/lib/lsoc_das2/ati/inc/at/internal
- 构建好之后在 ${PROJECT}.pio/build/${ESP_PROFILE} 下会生成 firmware.bin 文件,将这个文件导入 ESP flash download tools 即可进行烧写。
- 烧写时请将 ESP01 芯片 pin2pin 插入下载器,下载器插入 PC 之后会枚举出 COM 口,选择正确的 COM 口以及波特率(115200)即可下载了。
- 下载完成后回到 PlatformIO,点击 monitor 按钮即可重新为更新过固件的 ESP01 模组上电并监控其日志信息,进行调试。
M5Stack 可以直接从 Platform IO 选择 Upload 或者 Upload&Monitor 进行烧录。
附 M5Stack 集成好之后并且访问白名单,不在白名单内的地址访问实施阻止演示:
关于从 Arduino 环境移植的说明
ESP模组系列 的整个网络通信的技术栈类似常见的 cat1 模组技术栈,都分为芯片厂商闭源区、模组厂商闭源区、模组厂商开放区、用户区等区域。下图对比了二者之间的关系以及各个开发者角色需要开发的部分的工作项。
移植注意事项
- 在 ESP32 01 上调试时,das_policy_t 结构体由于需要进行 4 字节对齐的序列化处理,因此只能以全局变量的形态存储在 RAM 的 .bss 段。由于 100 个 IP/domain 实在是太大,会导致 .bss 越界。因此暂时调整至 10 个 IP/domain。在其它模组系统中请按实际情况调整。
此外也可以考虑使用 heap 代替 .bss 存储方式。
- 在网络事件上报函数 das_report_network_event 当中,由于是在 HOOK 函数中同步调用的,因此需要将事件加入缓存队列,随后在 stepping 调度中在 das_module_nfi_service 的 info 函数中从队列中取出。由于开发环境的 ESP32 是 non-os 的无锁环境(但需要屏蔽中断),因此队列操作没有上锁。在向 Free-RTOS 或者 Linux 等抢占式系统移植时请注意加锁。
- 在模组安全 SDK 的 stepping 驱动当中,目前忽略了第一次对流量进行 sampling 的结果,因为考虑到 SDK 初始化和 MQTT 连接不一定是发生在模组上电时刻的,因此需要轮询 2 个 sampling 周期才会产生第一次有效采样,轮询 2 个 sysinfo 周期才会触发第一次上报,也许有可以优化的空间。
- 如果需要 mock HTTP 访问,请在 MQTT 使用的 WiFiClient 之外额外声明一个 WiFiClient 对象,避免底层 ClientContext 实例冲突而导致 TCP 断链。