1.可用代码
只处理了Modbus RTU 保持寄存器
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# 获取当前脚本文件所在目录的父目录,并构建相对路径
import os
import sys
current_dir = os.path.dirname(os.path.abspath(__file__))
project_path = os.path.join(current_dir, '.')
common_path = os.path.join(project_path, 'common')
sub_path = os.path.join(project_path, 'sub')
sys.path.append(project_path)
sys.path.append(current_dir)
sys.path.append(sub_path)
sys.path.append(common_path)
import json
import serial
import threading
import struct
from pymodbus.server.sync import StartSerialServer
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.transaction import ModbusRtuFramer
from serial import Serial
from pymodbus.datastore import ModbusSequentialDataBlock
# 定义要维护的寄存器的初始值[0~255]
# 定义要维护的寄存器的初始值
hr_block = ModbusSequentialDataBlock(0, [0] * 0x10000) # Holding Register
block = ModbusSlaveContext(hr=hr_block)
store = ModbusServerContext(slaves=block, single=True)
import ctypes
import threading
import ctypes.util
# 定义调用的 C 库函数
libc = ctypes.CDLL(ctypes.util.find_library('c'))
pthread_setschedparam = libc.pthread_setschedparam
pthread_setschedparam.argtypes = [ctypes.c_ulong, ctypes.c_ulong, ctypes.POINTER(ctypes.c_void_p)]
def set_thread_priority(thread_id, priority):
param = ctypes.c_void_p()
param.value = priority
pthread_setschedparam(thread_id, 0, ctypes.byref(param))
import ctypes
import threading
def terminate_thread(thread):
if not thread.is_alive():
return
# 终止线程的函数定义
def _async_raise(tid, exctype):
"""raises the exception, performs cleanup if needed"""
tid = ctypes.c_long(tid)
if ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype)):
return
raise ValueError("invalid thread id")
thread_id = thread.ident
_async_raise(thread_id, SystemExit)
class GpModbus:
def __init__(self):
self.modbusIf = None
self.serial_thread = None
self.modbusIf = block
self.modbusAddress = None,
self.serialPortDevPath = None,
self.serial_baud = None
def startModbusSlave(self, modbusAddress, serialPortDevPath, serial_baud):
if isinstance(modbusAddress, tuple):
modbusAddress = modbusAddress[0]
if isinstance(serialPortDevPath, tuple):
serialPortDevPath = serialPortDevPath[0]
print(f'start modbus slave: {modbusAddress}, {serialPortDevPath}, {serial_baud}');
self.modbusAddress = modbusAddress,
self.serialPortDevPath = serialPortDevPath,
self.serial_baud = serial_baud
# 创建线程并启动
self.serial_thread = threading.Thread(target=self.startModbusSlave_ThenBlock, args=[modbusAddress, serialPortDevPath, serial_baud])
self.serial_thread.start()
# 获取线程 ID
#thread_id = self.serial_thread.ident
# 设置线程的优先级
#set_thread_priority(thread_id, 90) # 优先级值可以根据具体需求调整
#address is u16 serial, serialPortDevPath="" means using 虚拟串口
#禁止使用,应当使用startModbusSlave
def startModbusSlave_ThenBlock(self, modbusAddress, serialPortDevPath, serial_baud):
# 配置虚拟串口
try:
serialPort = None
#print(f"start modbus slave {serialPortDevPath} - {serial_baud}")
if(serialPortDevPath is not None):
# 启动 Modbus RTU 从机
server = StartSerialServer(context=store, identity=None, framer=ModbusRtuFramer,
port=serialPortDevPath,
baudrate=serial_baud,
bytesize=8, # ASCII 通常使用 7 位数据位
parity='N', # ASCII 通常使用偶校验
stopbits=1,
timeout=0.5,
broadcast_enable=True) #可以向用户广播
server.start()
except Exception as e:
print('gpModbus_startVirtualModbusSlave()', e)
print(f"self.modbusIf = {block}")
try:
while True:
pass
except KeyboardInterrupt:
print("程序已停止")
# 模拟寄存器读 0,123
def write_register(self, register_addr, register_value):
self.write_hold_register(register_addr, register_value)
# 模拟寄存器读 0,123
def write_registers(self, register_addr, register_values):
self.write_hold_register(register_addr, register_value)
def gpU16ToReversed(self, value):
if isinstance(value, list):
ret = [self.gpU16ToReversed(elem) for elem in value]
else:
ret = struct.unpack('>H', struct.pack('<H', value))[0]
return ret
# 模拟保持寄存器写[resiter_value可以为数组]
def write_hold_register(self, register_addr, register_value):
try:
#regisgter_addr_reversed = self.gpU16ToReversed(register_addr)
#register_value_reversed = self.gpU16ToReversed(register_value)
if isinstance(register_value, list):
print(f'addr:{register_addr}, {register_value}, cnt={len(register_value)}')
hr_block.setValues(register_addr+1, register_value) # 获取 Holding Registers 的值
else:
print(f'addr:{register_addr}, {register_value}, cnt=1')
hr_block.setValues(register_addr+1, [register_value]) # 获取 Holding Registers 的值
except Exception as e:
print(f'write register error {e}');
def read_hold_registers(self, register_addr, nCnt):
try:
print(f'read:{self.modbusIf} {register_addr},cnt={nCnt}')
if(self.modbusIf is None):
return [];
return hr_block.getValues(register_addr+1, nCnt) # 获取 Holding Registers 的值
except Exception as e:
print(f'read_register register error{e}');
return []
2.说明
- 创建外围线程的目的只是因为使用的Pymodbus的串口服务是个阻塞函数。
- 在修改寄存器值时需要修改的对象是最原始的hr_block。它有两个关联参数:
- 启动pymodbus是需要添加:broadcast_enable = true,使得对原始缓冲区中寄存器的修改会传递给所有的用户。
- 创建ServerStore是,使用:Single=True,表示所有的客户端共享同一个缓冲区。
- pyModbus在2=>3的过程中语法大变,上面的版本基于pymodbus==2.5.3
- 上面包含了大小端转换,但是最终没有用上,pymodbus模块自行处理了这个问题。在串口协议传递时,它按摩托罗拉的大端模式传递,但是在Python平台直接读写寄存器时,仍然是默认的小端模式——总之,就是大小端的转换对用户是透明的。
- 上面包含了线程的优先级和终止线程的处理,但未用到。
2.1 pip包下载语法:
python3 -m pip install pymodbus==2.5.3 --timeout 200 -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
3.Modbus调试用工具UART Assist
串口调试助手是在5.0.1之后加入对modbus的支持的。点击最右侧的黄色窄带,即可弹出快捷工具箱。下载位置:UartAssist串口调试助手-软件工具-野人家园
,很意外地发现NetAssist也是这个野人家园出的。。。。
Last updated: Jul13,2024 (I think this is the final version)