python 基于modbus_tk库实现modbusTCP 主站和从站[非常详细]

python 基于modbus_tk库实现modbusTCP 主站和从站

  • modbus 协议
    • modbus 通信过程
    • modbus 存储区
  • Modbus-TCP 协议
    • Modbus-TCP 报文帧结构
  • mosbus_tk库
    • 介绍
    • 从站记录的数据格式
    • 主站
    • 从站
    • hook函数

最近做了一个modbus tcp 传输浮点数的项目,参考了一些CSDN大佬的文章,这里做一个 整合和记录

modbus 协议

modbus 通信过程

  • 摘自详解Modbus通信协议—清晰易懂
  • 一主多从的通信协议:Modbus 通信中只有主机可以发送请求。其他从设备接收主机发送的数据来进行响应——处理信息和使用 Modbus 将其数据发送给主站。从机不会主动发送消息给主站。
  • Modbus 不能同步进行通信,主机在同一时间内只能向一个从机发送请求,总线上每次只有一个数据进行传输,即主机发送,从机应答,主机不发送,总线上就没有数据通信。
  • Modbus没有忙机制判断,比方说主机给从机发送命令, 从机没有收到或者正在处理其他东西,这时候就不能响应主机,因为 modbus 的总线只是传输数据,没有其他仲裁机制,所以需要通过软件的方式来判断是否正常接收。

Modbus 数据传输的方式,可以简单地理解成打电话。并且是单向通信的打电话

主机发送数据,首先需要从机的电话号码(区分每个从机,每个地址必须唯一),告诉从机打电话要干什么事情,然后是需要发送的内容,最后再问问从机,我说的话你都听清楚了没有呀,没有听错吧?

然后从机这里,得到了主机打过来的电话,从机回复主机需要的内容,主机得到从机数据,这样就是一个主机到从机的通信过程

modbus 存储区

  • 忘记从哪找的了hhh

  • 从机存储数据 -> 存储区

    • 文件操作 -> 只读(-r)和读写(-wr)
    • 数据类型 -> 布尔量 和 16 位寄存器
      • 布尔量比如 IO 口的电平高低,灯的开关状态等。
      • 16 位寄存器比如 传感器的温度数据,存储的密码等。
  • Modbus 协议规定了 4 个存储区,分别是 0 1 3 4 区 其中 1 区和 4 区是可读可写,1 区和 3 区是只读

    区号 名称 读写 地址范围
    0 区 输出线圈 可读可写布尔量 00001-09999
    1 区 输入线圈 只读布尔量 10001-19999
    3 区 输入寄存器 只读寄存器 30001-39999
    4 区 保持寄存器 可读可写寄存器 40001-49999
  • Modbus 给每个区都划分了地址范围,主机向从机获取数据时,只需要告诉从机数据的起始地址,还有获取多少字节的数据,从机就可以发送数据给主机

  • 每一个从机,都有实际的物理存储,跟 modbus 的存储区相对应,主机读写从机的存储区,实际上就是对从机设备对应的实际存储空间进行读写

Modbus-TCP 协议

Modbus-TCP 报文帧结构

  • 摘自ModbusTCP协议报文详细分析

  • 报文帧结构

    > > > MBAP 报文头(7 bytes) > 协议数据单元(PDU)
    事务处理标识符 协议标识符 长度 单元标识符 功能码 数据
    2 bytes 2 bytes 2 bytes 1 byte 1 byte N bytes
  • MBAP 报文头

    长度 说明 客户机 服务器
    事务处理标识符 2 字节 Modbus 请求/响应事务处理的标识 客户机启动 复制响应
    协议标识符 2 字节 0=Modbus 协议 客户机启动 复制响应
    长度 2 字节 长度之后的字节总数 客户机启动 服务器启动
    单元标识符 1 字节 串行链路或其它总线的从站识别 客户端启动 复制响应

    事务处理标识符 and 协议标识符 正常使用设置为 0 即可,长度为 4 个字节 -> 0x00000000

  • 功能码

    功能码 功能说明
    01H 读取输出线圈
    02H 读取输入线圈
    03H 读取保持寄存器
    04H 读取输入寄存器
    05H 写入单线圈
    06H 写入单寄存器
    0FH 写入多线圈
    10H 写入多寄存器
  • 每次可读数据最长长度(260 字节 - 9 字节 = 251 字节)

    • Modbus TCP 报文帧最长为 260 字节 -> MBAP 7 字节 + PDU 253 字节
    • 返回报文的 PDU 中: 功能码 1 字节 + 字节计数 1 字节 + 数据 251 字节
    • 一个寄存器 2 字节
    • 当数据为 32 位浮点数,一个数据占用两个寄存器 -> 一个数据 4 字节
    • 每次最多可读取:数据 251 字节 -> 125 个寄存器 -> 62 个 32 位浮点数(读取 124 个寄存器)
    • 从站返回报文格式详解
    > > 返回报文编码格式详解
    字节位 结构 编码格式
    前 6 个字节 事务/协议和数据长度 ‘>HHH’
    第 7 个字节 单元标识 ‘>B’
    第 8 个字节 功能码 ‘>B’
    第 9 个字节 数据长度 ‘>B’
    剩余字节 数据 data_format(default or 自定义)

mosbus_tk库

介绍

  • 安装: pip install modbus_tk
  • github主页
  • 主站示例代码
  • 从站示例代码

从站记录的数据格式

timestamp lon lat cog
1651924800 108.6030967 20.57094167 169.9
1651925400 108.6061227 20.56535943 170.7
1651926000 108.6091487 20.5597772 171.7
1651926600 108.6121746 20.55419496 143.8
1651927200 108.6152006 20.54861273 203.8
1651927800 108.6182266 20.5430305 207.8
1651928400 108.6212526 20.53744826 181.4
1651929000 108.6242786 20.53186603 180.6
1651929600 108.6273046 20.52628379 179.2
1651930200 108.6303306 20.52070156 172.9
1651930800 108.6333566 20.51511933 172.6
1651931400 108.6363826 20.50953709 173

主站

  • 根据示例改的,直接上代码
import modbus_tk
import modbus_tk.defines as cst
from modbus_tk import modbus_tcp, hooks
import numpy as np
import pandas as pd

master = modbus_tcp.TcpMaster()
master.set_timeout(5.0)
print("connected")

# 连接从站读取数据,一次最多读取125个寄存器,由于2个寄存器为一个数据,故 size 设置为124
data = []  # 存放读取的数据
data += master.execute(  # 向从站发报文读取[0,123]区间的寄存器数据
    1,  # 从站标识符
    cst.READ_HOLDING_REGISTERS,  # 功能码
    0,  # 起始寄存器地址
    124,  # 读取的寄存器数量
    data_format='62f',  # 数据解码格式
)
data += master.execute( # 向从站发送报文读取[124,247]区间的寄存器数据
    1, cst.READ_HOLDING_REGISTERS, 124, 124, data_format='62f'
)

# 将数据保存到csv文件中
if len(data) % 4 != 0:  # 如果数据长度不是4的倍数,则用 0 补齐
    for i in range(0, 4 - len(data) % 4):
        data.append(0)
data = np.reshape(data, (-1, 4))  # 将数据重新转为4列的二维数组
# print(data, data.shape) # 打印数据
df = pd.DataFrame(
    data, columns=['timestamp', 'lon', 'lat', 'cog']
)  # 将数据转为DataFrame,设置列名
df.to_csv('data_recv.csv', index=False)  # 将数据保存到csv文件中
  • 常见问题:
    • Modbus Error: Exception code = 3: 看看是不是接收的数据超出最大长度了

    • struct.error: unpack requires a buffer of xx bytes: 如果在master.execute()时设置了data_format,注意data_format必须与接收到的数据长度匹配!

      • 例如传输的数据为32位float,每个数据为4个字节,收到24字节的数据,那么收到了6个数据,那么data_format必须为'6f'

从站

  • 从站负责接收主站的请求并返回数据,modbut_tk库已经集成好了这些功能,所有从站的代码非常简单。
  • 当我们给从站存入数据时,一个寄存器只有2个字节,那么怎么存入4个字节的数据呢?
    1. 当量缩放:量程固定时不传输具体数据,只传输百分比0-100%
    2. 用两个寄存器存一个数据:把32位浮点数分为两部分(代码中就是这种方法)
pi_bytes = [int(a_byte) for a_byte in struct.pack("f", num)]
pi_register1 = pi_bytes[0] * 256 + pi_bytes[1]
pi_register2 = pi_bytes[2] * 256 + pi_bytes[3]
registers_list.append(pi_register1)
registers_list.append(pi_register2)
  • 上完整代码!
import sys

import modbus_tk
import modbus_tk.defines as cst
from modbus_tk import modbus_tcp, hooks
import struct
import pandas as pd
'''
可使用的函数:
创建从站: server.add_slave(slave_id)
    slave_id(int):从站id
为从站添加存储区: slave.add_block(block_name, block_type, starting_address, size)
    block_name(str):block名
    block_type(int):block类型,COILS = 1,DISCRETE_INPUTS = 2,HOLDING_REGISTERS = 3,ANALOG_INPUTS = 4
    starting_address(int):起始地址
    size(int):block大小
设置block值:slave.set_values(block_name, address, values)
    block_name(str):block名
    address(int):开始修改的地址
    values(a list or a tuple or a number):要修改的一个(a number)或多个(a list or a tuple)值
获取block值:slave.get_values(block_name, address, size)
    block_name(str):block名
    address(int):开始获取的地址
    size(int):要获取的值的数量
'''
# 创建从站总服务器
server = modbus_tcp.TcpServer(address='127.0.0.1')  # address必须设置,port默认为502
print("running...")
print("enter 'quit' for closing the server")
server.start()

# 创建从站
slave_1 = server.add_slave(1)  # slave_id = 1
# 为从站添加存储区
slave_1.add_block(
    '0', cst.HOLDING_REGISTERS, 0, 1200
)  # block_name = '0', block_type = cst.HOLDING_REGISTERS, starting_address = 0, size = 1200

# 将数据存入寄存器
data = pd.read_csv('modbus_tcp.csv').values  # 读取数据data
num_array = data.flatten()  # 将data压为一维数组

registers_list = []  # 要存入寄存器的数据

# 将数据转化为32位float格式,每个数据4个字节 -> 占2个寄存器
for num in num_array:
    pi_bytes = [int(a_byte) for a_byte in struct.pack("f", num)]
    pi_register1 = pi_bytes[0] * 256 + pi_bytes[1]
    pi_register2 = pi_bytes[2] * 256 + pi_bytes[3]
    registers_list.append(pi_register1)
    registers_list.append(pi_register2)
slave_1.set_values(
    '0', 0, registers_list
)  # 将数据存入寄存器, block_name = '0', address = 0, values = registers_list

while True:
    cmd = sys.stdin.readline()  # input
    args = cmd.split(' ')  # 按空格分割输入

    if cmd.find('quit') == 0:  # 指令 quit -> 退出服务器
        print('bye-bye')
        break
  • 可以看到,从站的代码非常简单!从站只需要一直开着(while True),当主站发送请求时,从站就会自动处理请求

hook函数

  • 主站从站都可以设置多个hook函数,实现自定义的功能
def on_before_connect(args):
    '''
    钩子函数,连接前输出主站信息

    Args:
        args (tuple): (self(主站对象),)
    '''
    master = args[0]
    print("host: {0},port: {1}".format(master._host, master._port))

hooks.install_hook("modbus_tcp.TcpMaster.before_connect", on_before_connect)

def on_after_recv(args):
    '''
    钩子函数,在收到报文后输出报文总长度

    Args:
        args (tuple): (self(主站对象), response(从站返回的数据))
    '''
    response = args[1]
    print("{0} bytes received".format(len(response)))

hooks.install_hook("modbus_tcp.TcpMaster.after_recv", on_after_recv)
  • 设置hook函数分三步
    • 定义函数
      • 参数args是一个元组,是主站/从站在执行完某个操作,如发送数据,后使用hook函数时传入的参数,元祖中包含的参数不固定,可以自己查看源码
    • 载入函数: hooks.install_hook('触发hook函数的操作名',函数名)
    • 愉快的使用!
  • modbus_tk.hooks中定义了哪些操作可以触发hook,具体请自行查看源码!

有问题随时留言!

你可能感兴趣的:(modbustcp,python,开发语言,网络协议)