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

归鹤龄
2023-12-01


最近做了一个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 bytes2 bytes2 bytes1 byte1 byteN 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库

介绍

从站记录的数据格式

timestamplonlatcog
1651924800108.603096720.57094167169.9
1651925400108.606122720.56535943170.7
1651926000108.609148720.5597772171.7
1651926600108.612174620.55419496143.8
1651927200108.615200620.54861273203.8
1651927800108.618226620.5430305207.8
1651928400108.621252620.53744826181.4
1651929000108.624278620.53186603180.6
1651929600108.627304620.52628379179.2
1651930200108.630330620.52070156172.9
1651930800108.633356620.51511933172.6
1651931400108.636382620.50953709173

主站

  • 根据示例改的,直接上代码
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,具体请自行查看源码!

有问题随时留言!

 类似资料: