ESP32上传ModBus设备数据到云平台

1. 前言
最基础的使用mqtt协议连接到云平台的程序基础框架已经搭建好了,接下来我们云平台得要有数据展示,不然只有一个壳,没啥用处。展示数据我们得要写移动app或者web前端吗,其实不用。阿里云物联网平台,有公版app或者web框架可以使用,零代码就可以做成展示应用了。接下来就是数据的来源了,这里我使用了两个ModBus设备,它们在工业或者生产环境经常使用,一条总线可以挂很多设备,线路可以很长,抗干扰能力强。单片机esp32使用modbus组件采集设备数据发送到队列,mqtt任务进行10秒间隔取队列数据并上报到云端。
2. 云平台
如果你问为什么不使用阿里云物联网的app作为展示端与控制端呢
那我只能告诉你,阿里云生活物联网飞燕平台,我是申请不到使用名额,现在申请已经要求你有公司的资质才能,那我只能使用通用的阿里云物联网平台了
阿里云生活物联网平台:偏消费类,含有设计公版app应用
阿里云物联网平台:通用类,含有设计web应用与手机web应用
2.1 阿里云IOT Studio 平台
1.创建项目
2.在项目中关联产品+管理设备(阿里云物联网平台中已创建的实例中的产品与产品中的设备)
3.在项目下新建应用(web应用或者移动应用,以web应用举例)
4.添加CNAME的域名解析到web应用的地址上 (使用moving123.cn域名),登录或者匿名访问:https://iot-ali.moving123.cn
PS:阿里云物联网服务器的域名解析是禁止使用非自家的域名服务器的,所以不能使用cloudflare代理了,即使不开启CDN,也是不行的。还好我的之前已经购买过阿里域名并完成备案,可以接入阿里云的服务了。
5.开通运营账号,用于管理后台和鉴定授权访问指定页面
6.添加CNAME解析到生成的账号后台地址上(使用moving123.cn域名),登录管理员页面:https://iot-ali-admin.moving123.cn
api接口的单播/组播/广播:
单播/组播:

广播: /broadcast/k1kthkYpz4S/${identifier} ;把 ${identifier} 替换为group01,设备端统一订阅/broadcast/k1kthkYpz4S/group01;云端发布group01的广播主题,所有订阅该组的设备都能接收到报文。payload要求是json的params对象

2.2 阿里云IOT Studio 设计应用

2.3 阿里云web应用访问


3. Modbus温湿度表
3.1 产品手册介绍








3.2 使用ModbusTools + usb-rs485调试工具
下载地址:github.com/serhmarch/ModbusTools

ps:扫描获得设备地址是1


3.3 使用ttl-rs485设备,使用分析仪抓取串口数据
写个简单串口程序并抓取串口数据:


modbus读取温度成功

3.4 浮点数数据格式拼装


3.5 浮点数数据解析
我是很笨搞了4种情况全部打印
分析:工业标准modbus 是大端模式,高字节在最前, modbus先发高字节0x41
esp32 是小端,低位在最前,而且工业设备是发送最小的一个数据单位u16,我们单片机收到两个u16
设备依次发送 41 → B7 → B1 → 32 ,单片机这里把第一个u16:0xb741存储到低地址,第二u16 :0x32b1继续存入
esp32 读出byte[0-3]为: 0xb74132b1;我们需要的是正确的大端 : 0x32b1b741;所以转换格式是CDAB ,后面我们使用esp-modbus组件的特征ID对象操作时候,定义为FLOAT_CDAB ,组件就会帮我们解析不用自己解析了
4. 电能表
4.1 产品手册介绍





4.2 寄存器地址
参考网站:正泰DDSU666 单相电能表 RS485-MODBUS RTU通讯说明
这个网站给出了电能表寄存器地址的定义,我在官网手册是找不到的,谢谢作者;

4.3 linux modbus调试工具
电能表太远了,没法比调试,最开始是使用树莓派linux环境调试,后面学聪明了,拉了10米的485通讯线到我房间
上瑞士军刀,linux调试modbus的利器---mbpoll
#安装命令行工具mbpoll
sudo apt update && sudo apt install mbpoll -y
ls /dev/ttyUSB*
目前已知参数是:手动按键操作查询面板得出 是9600波特率 ,8bit无校验停止1位,通讯地址是1 (这个通讯地址要手动改为2,不然与温表地址冲突)
sudo mbpoll
/dev/ttyUSB0 串口设备
-m rtu 模式:Modbus RTU
-b 9600 波特率:9600
-s 8 数据位:8位
-p none 校验位:无
-d 1 停止位:1位
-a 1 从站地址:1
-0 启用寄存器地址0开始
-r 1 起始地址:1
-c 10 读取数量:10个
-t 3 功能码: 1:读线圈,2:读离散输入,3:读保持,4:读输入
-1 执行一次
sudo mbpoll /dev/ttyUSB0 -m rtu -b 9600 -p none -a 1 -0 -r 0 -c 10 -t 3 -1
sudo mbpoll /dev/ttyUSB0 -m rtu -b 9600 -s 8 -p none -d 1 -a 1 -0 -t 3 -r 0 -c 15

我们知道波特率的值是3,设备地址是1,所以我们读取回来看看,结果是正确的,说明通讯数据跑通了
#波特率的寄存器地址是:12
#从寄存器地址0开始读15个
mbpoll /dev/ttyUSB0 -m rtu -b 9600 -p none -a 1 -0 -r 0 -c 15 -t 3 -1
#从寄存器地址0x2000(8192)开始读9*2=18个
mbpoll /dev/ttyUSB0 -m rtu -b 9600 -p none -a 1 -0 -r 8192 -c 18 -t 3 -1
#读累计电能消耗,4000H=>16384,读两个word
mbpoll /dev/ttyUSB0 -m rtu -b 9600 -p none -a 1 -0 -r 16384 -c 2 -t 3 -1


4.4 py程序调试
使用py脚本转换字节序:
这里是64位的树莓派系统debian,不是单片机的32小端,所以仅仅看看就可以了
import sys, struct
def convert(reg1, reg2):
print("===== 浮点数转换结果 =====")
print(f"输入: {reg1}, {reg2}")
print(f"ABCD: {struct.unpack('!f', struct.pack('>HH', reg1, reg2))[0]:.6f}")
print(f"BADC: {struct.unpack('!f', struct.pack('>HH', reg2, reg1))[0]:.6f}")
print(f"CDAB: {struct.unpack('!f', struct.pack('<HH', reg1, reg2))[0]:.6f}")
print(f"DCBA: {struct.unpack('!f', struct.pack('<HH', reg2, reg1))[0]:.6f}")
print("=========================")
if __name__ == "__main__":
if len(sys.argv)!=3:
print("用法: python float_conv.py 寄存器1 寄存器2")
print("示例: python float_conv.py 17258 13107")
sys.exit(1)
convert(int(sys.argv[1]), int(sys.argv[2]))

发现4个字节序是:ABCD, 但是它不适合我们32为小端单片机,仅仅看看就可以了
最后我们最关心的是,电能消耗,上图:

没错就是6451度电,那是一笔大消费呀
上图,看我树莓派,让我可以无线操控ssh终端读取数据,测试的活都是让你先上的:

最后我们得出并确认了这一套的解析方法是能读出有效数据了,上esp32
5. ESP32程序
GPIO矩阵,可以把串口1绑定到任意gpio上(串口0默认给系统烧录使用)
5.1 定义对象
定义数据对象:
enum {
// 电能表 CID
CID_ENERGY_HOLD_VOLTAGE_REG = 0, // 0x2000 电压寄存器 2word
CID_ENERGY_HOLD_CURRENT_REG, // 0x2002 电流寄存器 2word ENERGY_
CID_ENERGY_HOLD_POWER_REG, // 0x2004 功率寄存器 2word
CID_ENERGY_HOLD_ENERGY_REG, // 0x4000 能量寄存器 2word
// SHT30 CID
CID_SHT30_HOLD_TEMP_REG, // 0x0000 温度寄存器 1word ,需乘以0.1转换为实际温度值
CID_SHT30_HOLD_HUM_REG, // 0x0001 湿度寄存器 1word ,需乘以0.1转换为实际湿度值
CID_SHT30_HOLD_TEMP_F_REG, // 0x0002 温度寄存器 2word
CID_SHT30_HOLD_HUM_F_REG, // 0x0004 湿度寄存器 2word
CID_MAX // 数量
};
#define ENERGY_HOLDING_REG_BASE_ADDR_VOLTAGE 0x2000 // 电能表保持寄存器起始地址
#define ENERGY_HOLDING_REG_BASE_ADDR_ENERGY 0x4000 // 电能表保持寄存器起始地址
#define SHT30_HOLDING_REG_BASE_ADDR_TEMP_REG 0x0000 // SHT30保持寄存器起始地址
//定义保持寄存器结构体
#pragma pack(push, 1) //4字节对齐
typedef struct {
float voltage; // CID_VOLTAGE_REG = 0, // 0x2000 电压寄存器 2word
float current; // CID_CURRENT_REG, // 0x2002 电流寄存器 2word
float power; // CID_POWER_REG, // 0x2004 功率寄存器 2word
float energy; // CID_ENERGY_REG, // 0x4000 能量寄 2word
} energy_holding_reg_params_t;
#pragma pack(pop)
#pragma pack(push, 1)
typedef struct {
uint16_t temp_reg; // 0x0000 温度寄存器 1word
uint16_t hum_reg; // 0x0001 湿度寄存器 1word
float temp_f_reg; // 0x0002 温度寄存器 2word
float hum_f_reg; // 0x0004 湿度寄存器 2word
} sht30_holding_reg_params_t;
#pragma pack(pop)
定义特征CID对象:
// 参数描述符表
mb_parameter_descriptor_t device_parameters[] = {
// 电能表参数描述符
{
CID_ENERGY_HOLD_VOLTAGE_REG, STR("Voltage"), STR("V"), ENERGY_DEVICE_ADDR, MB_PARAM_HOLDING,
ENERGY_HOLD_REG_START(voltage) + ENERGY_HOLDING_REG_BASE_ADDR_VOLTAGE, ENERGY_HOLD_REG_SIZE(voltage),
ENERGY_HOLD_OFFSET(voltage), PARAM_TYPE_FLOAT_CDAB, 4,
ENERGY_OPTS(0, 1000, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
},
{
CID_ENERGY_HOLD_CURRENT_REG, STR("Current"), STR("A"), ENERGY_DEVICE_ADDR, MB_PARAM_HOLDING,
ENERGY_HOLD_REG_START(current) + ENERGY_HOLDING_REG_BASE_ADDR_VOLTAGE, ENERGY_HOLD_REG_SIZE(current),
ENERGY_HOLD_OFFSET(current), PARAM_TYPE_FLOAT_CDAB, 4,
ENERGY_OPTS(0, 1000, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
},
{
CID_ENERGY_HOLD_POWER_REG, STR("Power"), STR("W"), ENERGY_DEVICE_ADDR, MB_PARAM_HOLDING,
ENERGY_HOLD_REG_START(power) + ENERGY_HOLDING_REG_BASE_ADDR_VOLTAGE, ENERGY_HOLD_REG_SIZE(power),
ENERGY_HOLD_OFFSET(power), PARAM_TYPE_FLOAT_CDAB, 4,
ENERGY_OPTS(0, 1000, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
},
{
CID_ENERGY_HOLD_ENERGY_REG, STR("Energy"), STR("kWh"), ENERGY_DEVICE_ADDR, MB_PARAM_HOLDING,
ENERGY_HOLDING_REG_BASE_ADDR_ENERGY, ENERGY_HOLD_REG_SIZE(energy),
ENERGY_HOLD_OFFSET(energy), PARAM_TYPE_FLOAT_CDAB, 4,
ENERGY_OPTS(0, 1000, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
},
// SHT30 参数描述符
{
CID_SHT30_HOLD_TEMP_REG, STR("Temperature"), STR("°C"), SHT30_DEVCE_ADDR, MB_PARAM_HOLDING,
SHT30_HOLD_REG_START(temp_reg) + SHT30_HOLDING_REG_BASE_ADDR_TEMP_REG, SHT30_HOLD_REG_SIZE(temp_reg),
SHT30_HOLD_OFFSET(temp_reg), PARAM_TYPE_U16, 2,
SHT30_OPTS(-40, 125, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
},
{
CID_SHT30_HOLD_HUM_REG, STR("Humidity"), STR("%rH"), SHT30_DEVCE_ADDR, MB_PARAM_HOLDING,
SHT30_HOLD_REG_START(hum_reg) + SHT30_HOLDING_REG_BASE_ADDR_TEMP_REG, SHT30_HOLD_REG_SIZE(hum_reg),
SHT30_HOLD_OFFSET(hum_reg), PARAM_TYPE_U16, 2,
SHT30_OPTS(0, 100, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
},
{
CID_SHT30_HOLD_TEMP_F_REG, STR("Temperature_F"), STR("°F"), SHT30_DEVCE_ADDR, MB_PARAM_HOLDING,
SHT30_HOLD_REG_START(temp_f_reg) + SHT30_HOLDING_REG_BASE_ADDR_TEMP_REG, SHT30_HOLD_REG_SIZE(temp_f_reg),
SHT30_HOLD_OFFSET(temp_f_reg), PARAM_TYPE_FLOAT_CDAB, 4,
SHT30_OPTS(-40, 125, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
},
{
CID_SHT30_HOLD_HUM_F_REG, STR("Humidity_F"), STR("%rH"), SHT30_DEVCE_ADDR, MB_PARAM_HOLDING,
SHT30_HOLD_REG_START(hum_f_reg) + SHT30_HOLDING_REG_BASE_ADDR_TEMP_REG, SHT30_HOLD_REG_SIZE(hum_f_reg),
SHT30_HOLD_OFFSET(hum_f_reg), PARAM_TYPE_FLOAT_CDAB, 4,
SHT30_OPTS(0, 100, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
}
};
5.2 程序逻辑
1.创建描述符号,在menuconfig使能modbus组件的float数据类型扩展的编译选项
2.创建modbus 队列,把读到的数据发送到队列中,mqtt任务主动读取,读取后上报到云平台
#队列对象定义
typedef enum {
DEVICE_ENERGY,
DEVICE_SHT30
} mb_device_id_t;
typedef struct {
mb_device_id_t id; // 设备标识
bool is_online; // 当前是否在线
// 电能表参数
//float voltage;
float current;
float power;
float energy;
// 温湿度参数
//float temp;
//float hum;
float temp_f;
float hum_f;
} modbus_data_msg_t;
3.增加电能表读取成功信号 与 温湿度表读取成功的是否在线的属性
4.mqtt任务10s间隔在队列读取数据, cjson库拼装上报物模型属性报文,一次性发送多个属性
5.如果获取的队列对象中的是否在线的属性是0,就上报其他传感器属性为0,这样避免设备掉线了,云端还有传感器的数据显示非零值

上报的各种报文的payload可以参考文章:阿里云IoT物模型-属性,服务,事件通信的topic和payload详解——设备管理运维类_在物联网平台,不可以调用pub接口向物模型通信topic发送消息。-CSDN博客
5.3 commit的功能逻辑

5.4 modbus CID框架使用
CID设计框架是表驱动模式,不是事件回调,没有回调函数

mbc_master_get_cid_info 会使用ctx_master_handler实例句柄,在句柄里面找到你注册的CID表,匹配你传入的cid序号, 最后在cid表里面返回一个表偏移地址,这个地址就是你cid序号要找的那一份,然后修改你传入的指针的值(二级指针),让你的param_descriptor指针指向这个地址。

上面函数是自己实现的。你拿着param_descriptor指针,找出你要的CID的数据到时候是要打算存放在什么地址上,最后返回该地址。

mbc_master_get_parameter函数会发起总线访问时序,它会拿着你的cid序号,使用你实例化的句柄,和你要的数据存放的地址指针,和你已经注册到句柄的cid表,函数最后会返回获取到的数据类型type与数据 (存放在这个temp_data_ptr指针位置)。mbc_master_get_parameter函数有超时机制,与检验获取的数据是否匹配你的CID表的机制,有任何不符合的现象就返回错误代码。这要求你严格定义你的CID表的需求。
5.5 代码
本工程github代码仓库(版本标签:v1.1):
git clone -b v1.1 --single-branch https://github.com/haiou1220/esp32c3-mini-mqtt.git
#git checkout tags/v1.1
6. 最后
目前阶段完成了读取modbus传感器的数据并上报到云平台。
难点与关键点:
1.对modbus设备的浮点数的解析处理,会有不同的字节序的问题
2.数据对象的定义,要求增加是否在线的属性,在判断了掉线后发送最后一次数据对象到队列就可以了,设备不在线了就不要再发送数据到队列了
3.mqtt程序上了,mqtt任务当读取队列的时候,读取到设备A的数据时候,主动读取清空队列腾出新空间防止两个任务生产与消费频率不一致
4.理解抽象层CID的设计逻辑与API的使用,让你摆脱报文的手动组包与解析,,定义modbus抽象层特征CID时候,要求填写正确的浮点数解析的数据类型,之前我们手动测试发现的是float_cdba类型
5.多个设备连接到modbus总线的时候,设备的数据对象要求分别定义,但是特征CID要求是定义在一起,因为modbus组件框架只能允许一个modbus实例对象运行,我们可以通过条件宏让它们在语法上是可以单独的。
文章评论