mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 21:11:12 +00:00
Initial commit
This commit is contained in:
0
unilabos/device_comms/__init__.py
Normal file
0
unilabos/device_comms/__init__.py
Normal file
0
unilabos/device_comms/modbus_plc/__init__.py
Normal file
0
unilabos/device_comms/modbus_plc/__init__.py
Normal file
537
unilabos/device_comms/modbus_plc/client.py
Normal file
537
unilabos/device_comms/modbus_plc/client.py
Normal file
@@ -0,0 +1,537 @@
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from typing import Any, Union, List, Dict, Callable, Optional, Tuple
|
||||
from pydantic import BaseModel
|
||||
|
||||
from pymodbus.client import ModbusSerialClient, ModbusTcpClient
|
||||
from pymodbus.framer import FramerType
|
||||
from typing import TypedDict
|
||||
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import DeviceType, HoldRegister, Coil, InputRegister, DiscreteInputs, DataType, WorderOrder
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Base as ModbusNodeBase
|
||||
from unilabos.device_comms.universal_driver import UniversalDriver
|
||||
from unilabos.utils.log import logger
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class ModbusNode(BaseModel):
|
||||
name: str
|
||||
device_type: DeviceType
|
||||
address: int
|
||||
data_type: DataType = DataType.INT16
|
||||
slave: int = 1
|
||||
|
||||
|
||||
class PLCWorkflow(BaseModel):
|
||||
name: str
|
||||
actions: List[
|
||||
Union[
|
||||
"PLCWorkflow",
|
||||
Callable[
|
||||
[Callable[[str], ModbusNodeBase]],
|
||||
None
|
||||
]]
|
||||
]
|
||||
|
||||
class Action(BaseModel):
|
||||
name: str
|
||||
rw: bool # read是0 write是1
|
||||
|
||||
class WorkflowAction(BaseModel):
|
||||
init: Optional[Callable[[Callable[[str], ModbusNodeBase]], bool]] = None
|
||||
start: Optional[Callable[[Callable[[str], ModbusNodeBase]], bool]] = None
|
||||
stop: Optional[Callable[[Callable[[str], ModbusNodeBase]], bool]] = None
|
||||
cleanup: Optional[Callable[[Callable[[str], ModbusNodeBase]], None]] = None
|
||||
|
||||
|
||||
class ModbusWorkflow(BaseModel):
|
||||
name: str
|
||||
actions: List[Union["ModbusWorkflow", WorkflowAction]]
|
||||
|
||||
|
||||
""" 前后端Json解析用 """
|
||||
class AddressFunctionJson(TypedDict):
|
||||
func_name: str
|
||||
node_name: str
|
||||
mode: str
|
||||
value: Any
|
||||
|
||||
class InitFunctionJson(AddressFunctionJson):
|
||||
pass
|
||||
|
||||
class StartFunctionJson(AddressFunctionJson):
|
||||
pass
|
||||
|
||||
class StopFunctionJson(AddressFunctionJson):
|
||||
pass
|
||||
|
||||
class CleanupFunctionJson(AddressFunctionJson):
|
||||
pass
|
||||
|
||||
class ActionJson(TypedDict):
|
||||
address_function_to_create: list[AddressFunctionJson]
|
||||
create_init_function: Optional[InitFunctionJson]
|
||||
create_start_function: Optional[StartFunctionJson]
|
||||
create_stop_function: Optional[StopFunctionJson]
|
||||
create_cleanup_function: Optional[CleanupFunctionJson]
|
||||
|
||||
class WorkflowCreateJson(TypedDict):
|
||||
name: str
|
||||
action: list[Union[ActionJson, 'WorkflowCreateJson'] | str]
|
||||
|
||||
class ExecuteProcedureJson(TypedDict):
|
||||
register_node_list_from_csv_path: Optional[dict[str, Any]]
|
||||
create_flow: list[WorkflowCreateJson]
|
||||
execute_flow: list[str]
|
||||
|
||||
|
||||
|
||||
class BaseClient(UniversalDriver):
|
||||
client: Optional[Union[ModbusSerialClient, ModbusTcpClient]] = None
|
||||
_node_registry: Dict[str, ModbusNodeBase] = {}
|
||||
DEFAULT_ADDRESS_PATH = ""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# self.register_node_list_from_csv_path()
|
||||
|
||||
def _set_client(self, client: Optional[Union[ModbusSerialClient, ModbusTcpClient]]) -> None:
|
||||
if client is None:
|
||||
raise ValueError('client is not valid')
|
||||
# if not isinstance(client, TCPClient ) or not isinstance(client, RTUClient):
|
||||
# raise ValueError('client is not valid')
|
||||
self.client = client
|
||||
|
||||
def _connect(self) -> None:
|
||||
logger.info('try to connect client...')
|
||||
if self.client:
|
||||
if self.client.connect():
|
||||
logger.info('client connected!')
|
||||
else:
|
||||
logger.error('client connect failed')
|
||||
else:
|
||||
raise ValueError('client is not initialized')
|
||||
|
||||
@classmethod
|
||||
def load_csv(cls, file_path: str):
|
||||
df = pd.read_csv(file_path)
|
||||
df = df.drop_duplicates(subset='Name', keep='first') # FIXME: 重复的数据应该报错
|
||||
data_dict = df.set_index('Name').to_dict(orient='index')
|
||||
nodes = []
|
||||
for k, v in data_dict.items():
|
||||
deviceType = v.get('DeviceType', None)
|
||||
addr = v.get('Address', 0)
|
||||
dataType = v.get('DataType', 'BOOL')
|
||||
if not deviceType or not addr:
|
||||
continue
|
||||
|
||||
if deviceType == DeviceType.COIL.value:
|
||||
byteAddr = int(addr / 10)
|
||||
bitAddr = addr % 10
|
||||
addr = byteAddr * 8 + bitAddr
|
||||
|
||||
if dataType == 'BOOL':
|
||||
# noinspection PyTypeChecker
|
||||
dataType = 'INT16'
|
||||
# noinspection PyTypeChecker
|
||||
if pd.isna(dataType):
|
||||
print(v, "没有注册成功!")
|
||||
continue
|
||||
dataType: DataType = DataType[dataType]
|
||||
nodes.append(ModbusNode(name=k, device_type=DeviceType(deviceType), address=addr, data_type=dataType))
|
||||
return nodes
|
||||
|
||||
def use_node(self, name: str) -> ModbusNodeBase:
|
||||
if name not in self._node_registry:
|
||||
raise ValueError(f'node {name} is not registered')
|
||||
|
||||
return self._node_registry[name]
|
||||
|
||||
def get_node_registry(self) -> Dict[str, ModbusNodeBase]:
|
||||
return self._node_registry
|
||||
|
||||
def register_node_list_from_csv_path(self, path: str = None) -> "BaseClient":
|
||||
if path is None:
|
||||
path = self.DEFAULT_ADDRESS_PATH
|
||||
nodes = self.load_csv(path)
|
||||
return self.register_node_list(nodes)
|
||||
|
||||
def register_node_list(self, node_list: List[ModbusNode]) -> "BaseClient":
|
||||
if not self.client:
|
||||
raise ValueError('client is not connected')
|
||||
|
||||
if not node_list or len(node_list) == 0:
|
||||
logger.warning('node list is empty')
|
||||
return self
|
||||
|
||||
logger.info(f'start to register {len(node_list)} nodes...')
|
||||
for node in node_list:
|
||||
if node is None:
|
||||
continue
|
||||
if node.name in self._node_registry:
|
||||
logger.info(f'node {node.name} already exists')
|
||||
exist = self._node_registry[node.name]
|
||||
if exist.type != node.device_type:
|
||||
raise ValueError(f'node {node.name} type {node.device_type} is diplicated with {exist.type}')
|
||||
if exist.address != node.address:
|
||||
raise ValueError(f'node {node.name} address is duplicated with {exist.address}')
|
||||
continue
|
||||
if not isinstance(node.device_type, DeviceType):
|
||||
raise ValueError(f'node {node.name} type is not valid')
|
||||
|
||||
if node.device_type == DeviceType.HOLD_REGISTER:
|
||||
self._node_registry[node.name] = HoldRegister(self.client, node.name, node.address, node.data_type)
|
||||
elif node.device_type == DeviceType.COIL:
|
||||
self._node_registry[node.name] = Coil(self.client, node.name, node.address, node.data_type)
|
||||
elif node.device_type == DeviceType.INPUT_REGISTER:
|
||||
self._node_registry[node.name] = InputRegister(self.client, node.name, node.address, node.data_type)
|
||||
elif node.device_type == DeviceType.DISCRETE_INPUTS:
|
||||
self._node_registry[node.name] = DiscreteInputs(self.client, node.name, node.address, node.data_type)
|
||||
else:
|
||||
raise ValueError(f'node {node.name} type {node.device_type} is not valid')
|
||||
|
||||
logger.info('register nodes done.')
|
||||
return self
|
||||
|
||||
def run_plc_workflow(self, workflow: PLCWorkflow) -> None:
|
||||
if not self.client:
|
||||
raise ValueError('client is not connected')
|
||||
|
||||
logger.info(f'start to run workflow {workflow.name}...')
|
||||
|
||||
for action in workflow.actions:
|
||||
if isinstance(action, PLCWorkflow):
|
||||
self.run_plc_workflow(action)
|
||||
elif isinstance(action, Callable):
|
||||
action(self.use_node)
|
||||
else:
|
||||
raise ValueError(f'invalid action {action}')
|
||||
|
||||
def call_lifecycle_fn(
|
||||
self,
|
||||
workflow: ModbusWorkflow,
|
||||
fn: Optional[Callable[[Callable], bool]],
|
||||
) -> bool:
|
||||
if not fn:
|
||||
raise ValueError('fn is not valid in call_lifecycle_fn')
|
||||
try:
|
||||
return fn(self.use_node)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logger.error(f'execute {workflow.name} lifecycle failed, err: {e}')
|
||||
return False
|
||||
|
||||
def run_modbus_workflow(self, workflow: ModbusWorkflow) -> bool:
|
||||
if not self.client:
|
||||
raise ValueError('client is not connected')
|
||||
|
||||
logger.info(f'start to run workflow {workflow.name}...')
|
||||
|
||||
for action in workflow.actions:
|
||||
if isinstance(action, ModbusWorkflow):
|
||||
if self.run_modbus_workflow(action):
|
||||
logger.info(f"{action.name} workflow done.")
|
||||
continue
|
||||
else:
|
||||
logger.error(f"{action.name} workflow failed")
|
||||
return False
|
||||
elif isinstance(action, WorkflowAction):
|
||||
init = action.init
|
||||
start = action.start
|
||||
stop = action.stop
|
||||
cleanup = action.cleanup
|
||||
if not init and not start and not stop:
|
||||
raise ValueError(f'invalid action {action}')
|
||||
|
||||
is_err = False
|
||||
try:
|
||||
if init and not self.call_lifecycle_fn(workflow, init):
|
||||
raise ValueError(f"{workflow.name} init action failed")
|
||||
if not self.call_lifecycle_fn(workflow, start):
|
||||
raise ValueError(f"{workflow.name} start action failed")
|
||||
if not self.call_lifecycle_fn(workflow, stop):
|
||||
raise ValueError(f"{workflow.name} stop action failed")
|
||||
logger.info(f"{workflow.name} action done.")
|
||||
except Exception as e:
|
||||
is_err = True
|
||||
traceback.print_exc()
|
||||
logger.error(f"{workflow.name} action failed, err: {e}")
|
||||
finally:
|
||||
logger.info(f"{workflow.name} try to run cleanup")
|
||||
if cleanup:
|
||||
self.call_lifecycle_fn(workflow, cleanup)
|
||||
else:
|
||||
logger.info(f"{workflow.name} cleanup is not defined")
|
||||
if is_err:
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
raise ValueError(f'invalid action type {type(action)}')
|
||||
|
||||
return True
|
||||
|
||||
function_name: dict[str, Callable[[Callable[[str], ModbusNodeBase]], bool]] = {}
|
||||
|
||||
@classmethod
|
||||
def pack_func(cls, func, value="UNDEFINED"):
|
||||
def execute_pack_func(use_node: Callable[[str], ModbusNodeBase]):
|
||||
if value == "UNDEFINED":
|
||||
func()
|
||||
else:
|
||||
func(use_node, value)
|
||||
return execute_pack_func
|
||||
|
||||
def create_address_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, data_type: Optional[DataType] = None, word_order: WorderOrder = None, slave: Optional[int] = None) -> Callable[[Callable[[str], ModbusNodeBase]], bool]:
|
||||
def execute_address_function(use_node: Callable[[str], ModbusNodeBase]) -> Union[bool, Tuple[Union[int, float, str, list[bool], list[int], list[float]], bool]]:
|
||||
param = {"value": value}
|
||||
if data_type is not None:
|
||||
param["data_type"] = data_type
|
||||
if word_order is not None:
|
||||
param["word_order"] = word_order
|
||||
if slave is not None:
|
||||
param["slave"] = slave
|
||||
target_node = use_node(node_name)
|
||||
print("执行", node_name, type(target_node).__name__, target_node.address, mode, value)
|
||||
if mode == 'read':
|
||||
return use_node(node_name).read(**param)
|
||||
elif mode == 'write':
|
||||
return not use_node(node_name).write(**param)
|
||||
return False
|
||||
if func_name is None:
|
||||
func_name = node_name + '_' + mode + '_' + str(value)
|
||||
print("创建 address function", mode, func_name)
|
||||
self.function_name[func_name] = execute_address_function
|
||||
return execute_address_function
|
||||
|
||||
def create_init_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, data_type: Optional[DataType] = None, word_order: WorderOrder = None, slave: Optional[int] = None):
|
||||
return self.create_address_function(func_name, node_name, mode, value, data_type, word_order, slave)
|
||||
|
||||
def create_stop_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, data_type: Optional[DataType] = None, word_order: WorderOrder = None, slave: Optional[int] = None):
|
||||
return self.create_address_function(func_name, node_name, mode, value, data_type, word_order, slave)
|
||||
|
||||
def create_cleanup_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, data_type: Optional[DataType] = None, word_order: WorderOrder = None, slave: Optional[int] = None):
|
||||
return self.create_address_function(func_name, node_name, mode, value, data_type, word_order, slave)
|
||||
|
||||
def create_start_function(self, func_name: str, write_functions: list[str], condition_functions: list[str], stop_condition_expression: str):
|
||||
def execute_start_function(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
for write_function in write_functions:
|
||||
self.function_name[write_function](use_node)
|
||||
while True:
|
||||
next_loop = False
|
||||
condition_source = {}
|
||||
for condition_function in condition_functions:
|
||||
read_res, read_err = self.function_name[condition_function](use_node)
|
||||
if read_err:
|
||||
next_loop = True
|
||||
break
|
||||
condition_source[condition_function] = read_res
|
||||
if not next_loop:
|
||||
if stop_condition_expression:
|
||||
condition_source["__RESULT"] = None
|
||||
exec(f"__RESULT = {stop_condition_expression}", {}, condition_source) # todo: safety check
|
||||
res = condition_source["__RESULT"]
|
||||
print("取得计算结果;", res)
|
||||
if res:
|
||||
break
|
||||
else:
|
||||
time.sleep(0.3)
|
||||
return True
|
||||
return execute_start_function
|
||||
|
||||
def create_action_from_json(self, data: ActionJson):
|
||||
for i in data["address_function_to_create"]:
|
||||
self.create_address_function(**i)
|
||||
init = None
|
||||
start = None
|
||||
stop = None
|
||||
cleanup = None
|
||||
if data["create_init_function"]:
|
||||
print("创建 init function")
|
||||
init = self.create_init_function(**data["create_init_function"])
|
||||
if data["create_start_function"]:
|
||||
print("创建 start function")
|
||||
start = self.create_start_function(**data["create_start_function"])
|
||||
if data["create_stop_function"]:
|
||||
print("创建 stop function")
|
||||
stop = self.create_stop_function(**data["create_stop_function"])
|
||||
if data["create_cleanup_function"]:
|
||||
print("创建 cleanup function")
|
||||
cleanup = self.create_cleanup_function(**data["create_cleanup_function"])
|
||||
return WorkflowAction(init=init, start=start, stop=stop, cleanup=cleanup)
|
||||
|
||||
workflow_name = {}
|
||||
|
||||
def create_workflow_from_json(self, data: list[WorkflowCreateJson]):
|
||||
for ind, flow in enumerate(data):
|
||||
print("正在创建 workflow", ind, flow["name"])
|
||||
actions = []
|
||||
for i in flow["action"]:
|
||||
if isinstance(i, str):
|
||||
print("沿用 已有workflow 作为action", i)
|
||||
action = self.workflow_name[i]
|
||||
else:
|
||||
print("创建 action")
|
||||
action = self.create_action_from_json(i)
|
||||
actions.append(action)
|
||||
flow_instance = ModbusWorkflow(name=flow["name"], actions=actions)
|
||||
print("创建完成 workflow", flow["name"])
|
||||
self.workflow_name[flow["name"]] = flow_instance
|
||||
|
||||
def execute_workflow_from_json(self, data: list[str]):
|
||||
for i in data:
|
||||
print("正在执行 workflow", i)
|
||||
self.run_modbus_workflow(self.workflow_name[i])
|
||||
|
||||
def execute_procedure_from_json(self, data: ExecuteProcedureJson):
|
||||
if data["register_node_list_from_csv_path"]:
|
||||
print("注册节点 csv", data["register_node_list_from_csv_path"])
|
||||
self.register_node_list_from_csv_path(**data["register_node_list_from_csv_path"])
|
||||
print("创建工作流")
|
||||
self.create_workflow_from_json(data["create_flow"])
|
||||
print("执行工作流")
|
||||
self.execute_workflow_from_json(data["execute_flow"])
|
||||
|
||||
|
||||
class TCPClient(BaseClient):
|
||||
def __init__(self, addr: str, port: int):
|
||||
super().__init__()
|
||||
self._set_client(ModbusTcpClient(host=addr, port=port))
|
||||
# self._connect()
|
||||
|
||||
|
||||
class RTUClient(BaseClient):
|
||||
def __init__(self, port: str, baudrate: int, timeout: int):
|
||||
super().__init__()
|
||||
self._set_client(ModbusSerialClient(framer=FramerType.RTU, port=port, baudrate=baudrate, timeout=timeout))
|
||||
self._connect()
|
||||
|
||||
if __name__ == '__main__':
|
||||
""" 代码写法① """
|
||||
def idel_init(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
# 修改速度
|
||||
use_node('M01_idlepos_velocity_rw').write(20.0)
|
||||
# 修改位置
|
||||
# use_node('M01_idlepos_position_rw').write(35.22)
|
||||
return True
|
||||
|
||||
def idel_position(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
use_node('M01_idlepos_coil_w').write(True)
|
||||
while True:
|
||||
pos_idel, idel_err = use_node('M01_idlepos_coil_r').read(1)
|
||||
pos_stop, stop_err = use_node('M01_manual_stop_coil_r').read(1)
|
||||
time.sleep(0.5)
|
||||
if not idel_err and not stop_err and pos_idel[0] and pos_stop[0]:
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
def idel_stop(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
use_node('M01_idlepos_coil_w').write(False)
|
||||
return True
|
||||
|
||||
move_idel = ModbusWorkflow(name="测试待机位置", actions=[WorkflowAction(
|
||||
init=idel_init,
|
||||
start=idel_position,
|
||||
stop=idel_stop,
|
||||
)])
|
||||
|
||||
def pipetter_init(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
# 修改速度
|
||||
# use_node('M01_idlepos_velocity_rw').write(10.0)
|
||||
# 修改位置
|
||||
# use_node('M01_idlepos_position_rw').write(35.22)
|
||||
return True
|
||||
|
||||
def pipetter_position(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
use_node('M01_pipette0_coil_w').write(True)
|
||||
while True:
|
||||
pos_idel, isError = use_node('M01_pipette0_coil_r').read(1)
|
||||
pos_stop, isError = use_node('M01_manual_stop_coil_r').read(1)
|
||||
time.sleep(0.5)
|
||||
if pos_idel[0] and pos_stop[0]:
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
def pipetter_stop(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
use_node('M01_pipette0_coil_w').write(False)
|
||||
return True
|
||||
|
||||
move_pipetter = ModbusWorkflow(name="测试待机位置", actions=[WorkflowAction(
|
||||
init=None,
|
||||
start=pipetter_position,
|
||||
stop=pipetter_stop,
|
||||
)])
|
||||
|
||||
workflow_test_2 = ModbusWorkflow(name="测试水平移动并停止", actions=[
|
||||
move_idel,
|
||||
move_pipetter,
|
||||
])
|
||||
|
||||
# .run_modbus_workflow(move_2_left_workflow)
|
||||
|
||||
""" 代码写法② """
|
||||
# if False:
|
||||
# modbus_tcp_client_test2 = TCPClient('192.168.3.2', 502)
|
||||
# modbus_tcp_client_test2.register_node_list_from_csv_path('M01.csv')
|
||||
# init = modbus_tcp_client_test2.create_init_function('idel_init', 'M01_idlepos_velocity_rw', 'write', 20.0)
|
||||
#
|
||||
# modbus_tcp_client_test2.create_address_function('pos_tip', 'M01_idlepos_coil_w', 'write', True)
|
||||
# modbus_tcp_client_test2.create_address_function('pos_tip_read', 'M01_idlepos_coil_r', 'read', 1)
|
||||
# modbus_tcp_client_test2.create_address_function('manual_stop', 'M01_manual_stop_coil_r', 'read', 1)
|
||||
# start = modbus_tcp_client_test2.create_start_function(
|
||||
# 'idel_position',
|
||||
# write_functions=[
|
||||
# 'pos_tip'
|
||||
# ],
|
||||
# condition_functions=[
|
||||
# 'pos_tip_read',
|
||||
# 'manual_stop'
|
||||
# ],
|
||||
# stop_condition_expression='pos_tip_read[0] and manual_stop[0]'
|
||||
# )
|
||||
# stop = modbus_tcp_client_test2.create_stop_function('idel_stop', 'M01_idlepos_coil_w', 'write', False)
|
||||
#
|
||||
# move_idel = ModbusWorkflow(name="归位", actions=[WorkflowAction(
|
||||
# init=init,
|
||||
# start=start,
|
||||
# stop=stop,
|
||||
# )])
|
||||
#
|
||||
# modbus_tcp_client_test2.create_address_function('pipetter_position', 'M01_pipette0_coil_w', 'write', True)
|
||||
# modbus_tcp_client_test2.create_address_function('pipetter_position_read', 'M01_pipette0_coil_r', 'read', 1)
|
||||
# modbus_tcp_client_test2.create_address_function('pipetter_stop_read', 'M01_manual_stop_coil_r', 'read', 1)
|
||||
# pipetter_position = modbus_tcp_client_test2.create_start_function(
|
||||
# 'pipetter_start',
|
||||
# write_functions=[
|
||||
# 'pipetter_position'
|
||||
# ],
|
||||
# condition_functions=[
|
||||
# 'pipetter_position_read',
|
||||
# 'pipetter_stop_read'
|
||||
# ],
|
||||
# stop_condition_expression='pipetter_position[0] and pipetter_stop_read[0]'
|
||||
# )
|
||||
# pipetter_stop = modbus_tcp_client_test2.create_stop_function('pipetter_stop', 'M01_pipette0_coil_w', 'write', False)
|
||||
#
|
||||
# move_pipetter = ModbusWorkflow(name="测试待机位置", actions=[WorkflowAction(
|
||||
# init=None,
|
||||
# start=pipetter_position,
|
||||
# stop=pipetter_stop,
|
||||
# )])
|
||||
#
|
||||
# workflow_test_2 = ModbusWorkflow(name="测试水平移动并停止", actions=[
|
||||
# move_idel,
|
||||
# move_pipetter,
|
||||
# ])
|
||||
#
|
||||
# workflow_test_2.run_modbus_workflow()
|
||||
|
||||
""" 代码写法③ """
|
||||
with open('example_json.json', 'r', encoding='utf-8') as f:
|
||||
example_json = json.load(f)
|
||||
modbus_tcp_client_test2 = TCPClient('127.0.0.1', 5021)
|
||||
modbus_tcp_client_test2.execute_procedure_from_json(example_json)
|
||||
# .run_modbus_workflow(move_2_left_workflow)
|
||||
# init_client(FramerType.SOCKET, "", '192.168.3.2', 502)
|
||||
104
unilabos/device_comms/modbus_plc/example_json.json
Normal file
104
unilabos/device_comms/modbus_plc/example_json.json
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"register_node_list_from_csv_path": {
|
||||
"path": "M01.csv"
|
||||
},
|
||||
"create_flow": [
|
||||
{
|
||||
"name": "归位",
|
||||
"action": [
|
||||
{
|
||||
"address_function_to_create": [
|
||||
{
|
||||
"func_name": "pos_tip",
|
||||
"node_name": "M01_idlepos_coil_w",
|
||||
"mode": "write",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"func_name": "pos_tip_read",
|
||||
"node_name": "M01_idlepos_coil_r",
|
||||
"mode": "read",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"func_name": "manual_stop",
|
||||
"node_name": "M01_manual_stop_coil_r",
|
||||
"mode": "read",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"create_init_function": {
|
||||
"func_name": "idel_init",
|
||||
"node_name": "M01_idlepos_velocity_rw",
|
||||
"mode": "write",
|
||||
"value": 20.0
|
||||
},
|
||||
"create_start_function": {
|
||||
"func_name": "idel_position",
|
||||
"write_functions": ["pos_tip"],
|
||||
"condition_functions": ["pos_tip_read", "manual_stop"],
|
||||
"stop_condition_expression": "pos_tip_read[0] and manual_stop[0]"
|
||||
},
|
||||
"create_stop_function": {
|
||||
"func_name": "idel_stop",
|
||||
"node_name": "M01_idlepos_coil_w",
|
||||
"mode": "write",
|
||||
"value": false
|
||||
},
|
||||
"create_cleanup_function": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "测试待机位置",
|
||||
"action": [
|
||||
{
|
||||
"address_function_to_create": [
|
||||
{
|
||||
"func_name": "pipetter_position",
|
||||
"node_name": "M01_pipette0_coil_w",
|
||||
"mode": "write",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"func_name": "pipetter_position_read",
|
||||
"node_name": "M01_pipette0_coil_r",
|
||||
"mode": "read",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"func_name": "pipetter_stop_read",
|
||||
"node_name": "M01_manual_stop_coil_r",
|
||||
"mode": "read",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"create_init_function": null,
|
||||
"create_start_function": {
|
||||
"func_name": "pipetter_start",
|
||||
"write_functions": ["pipetter_position"],
|
||||
"condition_functions": ["pipetter_position_read", "pipetter_stop_read"],
|
||||
"stop_condition_expression": "pipetter_position_read[0] and pipetter_stop_read[0]"
|
||||
},
|
||||
"create_stop_function": {
|
||||
"func_name": "pipetter_stop",
|
||||
"node_name": "M01_pipette0_coil_w",
|
||||
"mode": "write",
|
||||
"value": false
|
||||
},
|
||||
"create_cleanup_function": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "归位并测试待机位置",
|
||||
"action": [
|
||||
"归位",
|
||||
"测试待机位置"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execute_flow": [
|
||||
"归位并测试待机位置"
|
||||
]
|
||||
}
|
||||
0
unilabos/device_comms/modbus_plc/node/__init__.py
Normal file
0
unilabos/device_comms/modbus_plc/node/__init__.py
Normal file
161
unilabos/device_comms/modbus_plc/node/modbus.py
Normal file
161
unilabos/device_comms/modbus_plc/node/modbus.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# coding=utf-8
|
||||
from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from pymodbus.client import ModbusBaseSyncClient
|
||||
from pymodbus.client.mixin import ModbusClientMixin
|
||||
from typing import Tuple, Union, Optional
|
||||
|
||||
DataType = ModbusClientMixin.DATATYPE
|
||||
|
||||
class WorderOrder(Enum):
|
||||
BIG = "big"
|
||||
LITTLE = "little"
|
||||
|
||||
class DeviceType(Enum):
|
||||
COIL = 'coil'
|
||||
DISCRETE_INPUTS = 'discrete_inputs'
|
||||
HOLD_REGISTER = 'hold_register'
|
||||
INPUT_REGISTER = 'input_register'
|
||||
|
||||
|
||||
class Base(ABC):
|
||||
def __init__(self, client: ModbusBaseSyncClient, name: str, address: int, typ: DeviceType, data_type: DataType):
|
||||
self._address: int = address
|
||||
self._client = client
|
||||
self._name = name
|
||||
self._type = typ
|
||||
self._data_type = data_type
|
||||
|
||||
@abstractmethod
|
||||
def read(self, value, data_type: Optional[DataType] = None, word_order: WorderOrder = WorderOrder.BIG, slave = 1,) -> Tuple[Union[int, float, str, list[bool], list[int], list[float]], bool]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def write(self, value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool:
|
||||
pass
|
||||
|
||||
@property
|
||||
def type(self) -> DeviceType:
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def address(self) -> int:
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
|
||||
class Coil(Base):
|
||||
def __init__(self, client,name, address: int, data_type: DataType):
|
||||
super().__init__(client, name, address, DeviceType.COIL, data_type)
|
||||
|
||||
def read(self, value, data_type: Optional[DataType] = None, word_order: WorderOrder = WorderOrder.BIG, slave = 1,) -> Tuple[Union[int, float, str, list[bool], list[int], list[float]], bool]:
|
||||
resp = self._client.read_coils(
|
||||
address = self.address,
|
||||
count = value,
|
||||
slave = slave)
|
||||
|
||||
return resp.bits, resp.isError()
|
||||
|
||||
def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool:
|
||||
if isinstance(value, list):
|
||||
for v in value:
|
||||
if not isinstance(v, bool):
|
||||
raise ValueError(f'value invalidate: {value}')
|
||||
|
||||
return self._client.write_coils(
|
||||
address = self.address,
|
||||
values = [bool(v) for v in value],
|
||||
slave = slave).isError()
|
||||
else:
|
||||
return self._client.write_coil(
|
||||
address = self.address,
|
||||
value = bool(value),
|
||||
slave = slave).isError()
|
||||
|
||||
|
||||
class DiscreteInputs(Base):
|
||||
def __init__(self, client,name, address: int, data_type: DataType):
|
||||
super().__init__(client, name, address, DeviceType.COIL, data_type)
|
||||
|
||||
def read(self, value, data_type: Optional[DataType] = None, word_order: WorderOrder = WorderOrder.BIG, slave = 1,) -> Tuple[Union[int, float, str, list[bool], list[int], list[float]], bool]:
|
||||
if not data_type and not self._data_type:
|
||||
raise ValueError('data type is required')
|
||||
if not data_type:
|
||||
data_type = self._data_type
|
||||
resp = self._client.read_discrete_inputs(
|
||||
self.address,
|
||||
count = value,
|
||||
slave = slave)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
return self._client.convert_from_registers(resp.registers, data_type, word_order=word_order.value), resp.isError()
|
||||
|
||||
def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool:
|
||||
raise ValueError('discrete inputs only support read')
|
||||
|
||||
class HoldRegister(Base):
|
||||
def __init__(self, client,name, address: int, data_type: DataType):
|
||||
super().__init__(client, name, address, DeviceType.COIL, data_type)
|
||||
|
||||
def read(self, value, data_type: Optional[DataType] = None, word_order: WorderOrder = WorderOrder.BIG, slave = 1,) -> Tuple[Union[int, float, str, list[bool], list[int], list[float]], bool]:
|
||||
if not data_type and not self._data_type:
|
||||
raise ValueError('data type is required')
|
||||
|
||||
if not data_type:
|
||||
data_type = self._data_type
|
||||
|
||||
resp = self._client.read_holding_registers(
|
||||
address = self.address,
|
||||
count = value,
|
||||
slave = slave)
|
||||
# noinspection PyTypeChecker
|
||||
return self._client.convert_from_registers(resp.registers, data_type, word_order=word_order.value), resp.isError()
|
||||
|
||||
|
||||
def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool:
|
||||
if not data_type and not self._data_type:
|
||||
raise ValueError('data type is required')
|
||||
|
||||
if not data_type:
|
||||
data_type = self._data_type
|
||||
|
||||
if isinstance(value , bool):
|
||||
if value:
|
||||
return self._client.write_register(self.address, 1, slave= slave).isError()
|
||||
else:
|
||||
return self._client.write_register(self.address, 0, slave= slave).isError()
|
||||
elif isinstance(value, int):
|
||||
return self._client.write_register(self.address, value, slave= slave).isError()
|
||||
else:
|
||||
# noinspection PyTypeChecker
|
||||
encoder_resp = self._client.convert_to_registers(value, data_type=data_type, word_order=word_order.value)
|
||||
return self._client.write_registers(self.address, encoder_resp, slave=slave).isError()
|
||||
|
||||
|
||||
|
||||
class InputRegister(Base):
|
||||
def __init__(self, client,name, address: int, data_type: DataType):
|
||||
super().__init__(client, name, address, DeviceType.COIL, data_type)
|
||||
|
||||
|
||||
def read(self, value, data_type: Optional[DataType] = None, word_order: WorderOrder = WorderOrder.BIG, slave = 1) -> Tuple[Union[int, float, str, list[bool], list[int], list[float]], bool]:
|
||||
if not data_type and not self._data_type:
|
||||
raise ValueError('data type is required')
|
||||
|
||||
if not data_type:
|
||||
data_type = self._data_type
|
||||
|
||||
resp = self._client.read_holding_registers(
|
||||
address = self.address,
|
||||
count = value,
|
||||
slave = slave)
|
||||
# noinspection PyTypeChecker
|
||||
return self._client.convert_from_registers(resp.registers, data_type, word_order=word_order.value), resp.isError()
|
||||
|
||||
def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool:
|
||||
raise ValueError('input register only support read')
|
||||
|
||||
37
unilabos/device_comms/modbus_plc/server.py
Normal file
37
unilabos/device_comms/modbus_plc/server.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import modbus_tk.defines as cst
|
||||
import modbus_tk.modbus_tcp as modbus_tcp
|
||||
|
||||
# 创建一个 Modbus TCP 服务器
|
||||
server = modbus_tcp.TcpServer(
|
||||
address="127.0.0.1", port=5021, timeout_in_sec=1
|
||||
)
|
||||
|
||||
# 添加一个从设备 (slave)
|
||||
slave_id = 1
|
||||
slave = server.add_slave(slave_id)
|
||||
|
||||
# 为从设备分配地址空间,例如保持寄存器 (holding registers)
|
||||
# 假设地址范围为 7000 到 7100,对应客户端M01_idlepos_velocity_rw
|
||||
slave.add_block('hr', cst.HOLDING_REGISTERS, 7000, 100)
|
||||
slave.add_block('coil_block', cst.COILS, 56000, 1000)
|
||||
|
||||
|
||||
# 初始化地址 56488 和 56432 的值为 True
|
||||
slave.set_values('coil_block', 56488, [True]) # Coil 56488 设置为 True
|
||||
slave.set_values('coil_block', 56432, [True]) # Coil 56432 设置为 True
|
||||
|
||||
slave.set_values('coil_block', 56496, [True]) # Coil 56488 设置为 True
|
||||
slave.set_values('coil_block', 56432, [True]) # Coil 56432 设置为 True
|
||||
|
||||
|
||||
# slave.add_block('hr', cst.COILS, 7000, 100)
|
||||
server.start()
|
||||
print("Modbus TCP server running on localhost:5021")
|
||||
|
||||
# 保持服务器运行,直到按下 Ctrl+C
|
||||
try:
|
||||
while True:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
server.stop()
|
||||
print("Server stopped.")
|
||||
0
unilabos/device_comms/modbus_plc/test/__init__.py
Normal file
0
unilabos/device_comms/modbus_plc/test/__init__.py
Normal file
107
unilabos/device_comms/modbus_plc/test/client.py
Normal file
107
unilabos/device_comms/modbus_plc/test/client.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import time
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Coil, HoldRegister
|
||||
from pymodbus.payload import BinaryPayloadDecoder
|
||||
from pymodbus.constants import Endian
|
||||
|
||||
# client = ModbusTcpClient('localhost', port=5020)
|
||||
client = ModbusTcpClient('192.168.3.2', port=502)
|
||||
|
||||
client.connect()
|
||||
|
||||
|
||||
coil1 = Coil(client=client, name='coil_test1', data_type=bool, address=4502*8)
|
||||
coil1.write(True)
|
||||
time.sleep(3)
|
||||
coil1.write(False)
|
||||
|
||||
|
||||
|
||||
coil1 = Coil(client=client, name='coil_test1', data_type=bool, address=4503*8)
|
||||
coil1.write(True)
|
||||
time.sleep(3)
|
||||
coil1.write(False)
|
||||
|
||||
|
||||
exit(0)
|
||||
|
||||
|
||||
|
||||
register1 = HoldRegister(client=client, name="test-1", address=7040)
|
||||
|
||||
|
||||
coil1 = Coil(client=client, name='coil_test1', address=7002*8)
|
||||
|
||||
|
||||
coil1.write(True)
|
||||
|
||||
while True:
|
||||
# result = client.read_holding_registers(address=7040, count=2, slave=1) # unit=1 是从站地址
|
||||
result = register1.read(2, slave=1)
|
||||
if result.isError():
|
||||
print("读取失败")
|
||||
else:
|
||||
print("读取成功:", result.registers)
|
||||
decoder = BinaryPayloadDecoder.fromRegisters(
|
||||
result.registers, byteorder=Endian.BIG, wordorder=Endian.LITTLE
|
||||
)
|
||||
real_value = decoder.decode_32bit_float()
|
||||
print("这里的值是: ", real_value)
|
||||
if real_value > 42:
|
||||
coil1.write(False)
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
# # 创建 Modbus TCP 客户端,连接到本地模拟的服务器
|
||||
# client = ModbusClient('localhost', port=5020)
|
||||
|
||||
# # 连接到服务器
|
||||
|
||||
# # 读取保持寄存器(地址 0,读取 10 个寄存器)
|
||||
# # address: int,
|
||||
# # *,
|
||||
# # count: int = 1,
|
||||
# # slave: int = 1,
|
||||
# response = client.read_holding_registers(
|
||||
# address=0, count=10, slave=1
|
||||
# )
|
||||
|
||||
# response = coil1.read(2, slave=1)
|
||||
#
|
||||
# if response.isError():
|
||||
# print(f"Error reading registers: {response}")
|
||||
# else:
|
||||
# print(f"Read holding registers: {response.bits}")
|
||||
#
|
||||
# coil1.write(1, slave=1)
|
||||
# print("Wrote value 1234 to holding register 0")
|
||||
#
|
||||
# response = coil1.read(2, slave=1)
|
||||
# if response.isError():
|
||||
# print(f"Error reading registers: {response}")
|
||||
# else:
|
||||
# print(f"Read holding registers after write: {response.bits}")
|
||||
#
|
||||
#
|
||||
# if response.isError():
|
||||
# print(f"Error reading registers: {response}")
|
||||
# else:
|
||||
# print(f"Read holding registers after write: {response.bits}")
|
||||
|
||||
client.close()
|
||||
|
||||
# # 写入保持寄存器(地址 0,值为 1234)
|
||||
# client.write_register(0, 1234, slave=1)
|
||||
# print("Wrote value 1234 to holding register 0")
|
||||
|
||||
# # 再次读取寄存器,确认写入成功
|
||||
# response = client.read_holding_registers(address=0, count=10, slave=1)
|
||||
# if response.isError():
|
||||
# print(f"Error reading registers: {response}")
|
||||
# else:
|
||||
# print(f"Read holding registers after write: {response.registers}")
|
||||
|
||||
# # 关闭连接
|
||||
# client.close()
|
||||
|
||||
45
unilabos/device_comms/modbus_plc/test/node_test.py
Normal file
45
unilabos/device_comms/modbus_plc/test/node_test.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# coding=utf-8
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Coil
|
||||
import time
|
||||
|
||||
|
||||
client = ModbusTcpClient('192.168.3.2', port=502)
|
||||
client.connect()
|
||||
|
||||
coil1 = Coil(client=client, name='0', address=7012*8)
|
||||
|
||||
coil2 = Coil(client=client, name='0', address=7062*8)
|
||||
coil3 = Coil(client=client, name='0', address=7054*8)
|
||||
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
resp, isError = coil2.read(1)
|
||||
resp1, isError = coil3.read(1)
|
||||
print(resp[0], resp1[0])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# hr = HoldRegister(client, '1', 100)
|
||||
# resp = hr.write([666.3, 777.4], data_type=DATATYPE.FLOAT32, word_order=WORDORDER.BIG)
|
||||
# print('write ===== hr1', resp)
|
||||
# time.sleep(1)
|
||||
# h_resp = hr.read(4, data_type=DATATYPE.FLOAT32, word_order=WORDORDER.BIG)
|
||||
# print('=======hr1', h_resp)
|
||||
#
|
||||
#
|
||||
# resp = hr.write([666, 777], data_type=DATATYPE.INT32, word_order=WORDORDER.BIG)
|
||||
# print('write ===== hr1', resp)
|
||||
# time.sleep(1)
|
||||
# h_resp = hr.read(4, data_type=DATATYPE.INT32, word_order=WORDORDER.BIG)
|
||||
# print('=======hr1', h_resp)
|
||||
#
|
||||
#
|
||||
# resp = hr.write('hello world!', data_type=DATATYPE.STRING, word_order=WORDORDER.BIG)
|
||||
# print('write ===== hr1', resp)
|
||||
# time.sleep(1)
|
||||
# h_resp = hr.read(12, data_type=DATATYPE.STRING, word_order=WORDORDER.BIG)
|
||||
# print('=======hr1', h_resp)
|
||||
42
unilabos/device_comms/modbus_plc/test/server.py
Normal file
42
unilabos/device_comms/modbus_plc/test/server.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import modbus_tk.modbus_tcp as modbus_tcp
|
||||
import modbus_tk.defines as cst
|
||||
from modbus_tk.modbus import Slave
|
||||
|
||||
# 创建一个 Modbus TCP 服务器
|
||||
server = modbus_tcp.TcpServer(
|
||||
address="localhost", port=5020, timeout_in_sec=1
|
||||
)
|
||||
|
||||
# 设置服务器的地址和端口
|
||||
# server.set_address("localhost", 5020) # 监听在本地端口5020
|
||||
|
||||
# 添加一个从设备,Slave ID 设为 1
|
||||
slave: Slave = server.add_slave(1)
|
||||
|
||||
|
||||
# 向从设备添加一个保持寄存器块,假设从地址0开始,10个寄存器
|
||||
# def add_block(self, block_name, block_type, starting_address, size)
|
||||
# slave.add_block('0', cst.HOLDING_REGISTERS, 0, 10)
|
||||
|
||||
# 添加一个线圈
|
||||
# 0 名字, 从 16 字节内存位置开始,分配连续两个字节的内存大小,注意地址只能是 8 的整数倍
|
||||
slave.add_block('0', cst.COILS, 2*8, 2)
|
||||
|
||||
# 1 名字,100 起始地址, 8 是从 100 的位置分配 8 个字节内存, 两个线圈的量
|
||||
slave.add_block('1', cst.HOLDING_REGISTERS, 100, 8)
|
||||
slave.add_block('2', cst.HOLDING_REGISTERS, 200, 16)
|
||||
|
||||
# slave.add_block('2', cst.DISCRETE_INPUTS , 200, 2)
|
||||
# slave.add_block('3', cst.ANALOG_INPUTS , 300, 2)
|
||||
# 启动服务器
|
||||
server.start()
|
||||
|
||||
print("Modbus TCP server running on localhost:5020")
|
||||
|
||||
# 保持服务器运行,直到按下 Ctrl+C
|
||||
try:
|
||||
while True:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
server.stop()
|
||||
print("Server stopped.")
|
||||
168
unilabos/device_comms/modbus_plc/test/test_workflow.py
Normal file
168
unilabos/device_comms/modbus_plc/test/test_workflow.py
Normal file
@@ -0,0 +1,168 @@
|
||||
import time
|
||||
from typing import Callable
|
||||
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusWorkflow, WorkflowAction, load_csv
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Base as ModbusNodeBase
|
||||
|
||||
############ 第一种写法 ##############
|
||||
|
||||
|
||||
# modbus_tcp_client_test1 = TCPClient('192.168.3.2', 502)
|
||||
#
|
||||
#
|
||||
# node_list = [
|
||||
# ModbusNode(name="left_move_coli", device_type=DeviceType.COIL, address=7003 * 8),
|
||||
# ModbusNode(name="right_move_coli", device_type=DeviceType.COIL, address=7002 * 8),
|
||||
# ModbusNode(name="position_register", device_type=DeviceType.HOLD_REGISTER, address=7040),
|
||||
# ]
|
||||
#
|
||||
# def judge_position(node: ModbusNodeBase):
|
||||
# idx = 0
|
||||
# while idx <= 5:
|
||||
# result, is_err = node.read(2)
|
||||
# if is_err:
|
||||
# print("读取失败")
|
||||
# else:
|
||||
# print("读取成功:", result)
|
||||
# idx+=1
|
||||
# time.sleep(1)
|
||||
#
|
||||
# workflow_move_2_right = PLCWorkflow(name="测试水平向右移动", actions=[
|
||||
# lambda use_node: use_node('left_move_coli').write(False),
|
||||
# lambda use_node: use_node('right_move_coli').write(True),
|
||||
# lambda use_node: judge_position(use_node('position_register')),
|
||||
# lambda use_node: use_node('right_move_coli').write(False),
|
||||
# ])
|
||||
#
|
||||
#
|
||||
# workflow_move_2_left = PLCWorkflow(name="测试水平向左移动", actions=[
|
||||
# lambda use_node: use_node('right_move_coli').write(False),
|
||||
# lambda use_node: use_node('left_move_coli').write(True),
|
||||
# lambda use_node: judge_position(use_node('position_register')),
|
||||
# lambda use_node: use_node('left_move_coli').write(False),
|
||||
# ])
|
||||
#
|
||||
#
|
||||
# workflow_test_1 = PLCWorkflow(name="测试水平移动并停止", actions=[
|
||||
# workflow_move_2_right,
|
||||
# workflow_move_2_left,
|
||||
# ])
|
||||
#
|
||||
# modbus_tcp_client_test1 \
|
||||
# .register_node_list(node_list) \
|
||||
# .run_plc_workflow(workflow_test_1)
|
||||
#
|
||||
|
||||
|
||||
############ 第二种写法 ##############
|
||||
|
||||
modbus_tcp_client_test2 = TCPClient('192.168.3.2', 502)
|
||||
|
||||
# def judge_position(node: ModbusNodeBase):
|
||||
# idx = 0
|
||||
# while idx <= 5:
|
||||
# result, is_err = node.read(2)
|
||||
# if is_err:
|
||||
# print("读取失败")
|
||||
# else:
|
||||
# print("读取成功:", result)
|
||||
# idx+=1
|
||||
# time.sleep(1)
|
||||
|
||||
|
||||
|
||||
# def move_2_right_init(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
# use_node('left_move_coli').write(False)
|
||||
# use_node('right_move_coli').write(True)
|
||||
# return True
|
||||
|
||||
# def move_2_right_start(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
# judge_position(use_node('position_register'))
|
||||
# return True
|
||||
|
||||
# def move_2_right_stop(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
# use_node('right_move_coli').write(False)
|
||||
# return True
|
||||
|
||||
# move_2_right_workflow = ModbusWorkflow(name="测试水平向右移动", actions=[WorkflowAction(
|
||||
# init=move_2_right_init,
|
||||
# start=move_2_right_start,
|
||||
# stop=move_2_right_stop,
|
||||
# )])
|
||||
|
||||
# move_2_right_workflow = ModbusWorkflow(name="测试水平向右移动", actions=[WorkflowAction(
|
||||
# init=move_2_right_init,
|
||||
# start= None,
|
||||
# stop= None,
|
||||
# cleanup=None,
|
||||
# )])
|
||||
|
||||
|
||||
def idel_init(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
# 修改速度
|
||||
use_node('M01_idlepos_velocity_rw').write(20.0)
|
||||
# 修改位置
|
||||
# use_node('M01_idlepos_position_rw').write(35.22)
|
||||
return True
|
||||
|
||||
def idel_position(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
use_node('M01_idlepos_coil_w').write(True)
|
||||
while True:
|
||||
pos_idel, idel_err = use_node('M01_idlepos_coil_r').read(1)
|
||||
pos_stop, stop_err = use_node('M01_manual_stop_coil_r').read(1)
|
||||
time.sleep(0.5)
|
||||
if not idel_err and not stop_err and pos_idel[0] and pos_stop[0]:
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
def idel_stop(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
use_node('M01_idlepos_coil_w').write(False)
|
||||
return True
|
||||
|
||||
move_idel= ModbusWorkflow(name="测试待机位置", actions=[WorkflowAction(
|
||||
init=idel_init,
|
||||
start=idel_position,
|
||||
stop=idel_stop,
|
||||
)])
|
||||
|
||||
def pipetter_init(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
# 修改速度
|
||||
# use_node('M01_idlepos_velocity_rw').write(10.0)
|
||||
# 修改位置
|
||||
# use_node('M01_idlepos_position_rw').write(35.22)
|
||||
return True
|
||||
|
||||
def pipetter_position(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
use_node('M01_pipette0_coil_w').write(True)
|
||||
while True:
|
||||
pos_idel, isError = use_node('M01_pipette0_coil_r').read(1)
|
||||
pos_stop, isError = use_node('M01_manual_stop_coil_r').read(1)
|
||||
time.sleep(0.5)
|
||||
if pos_idel[0] and pos_stop[0]:
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
def pipetter_stop(use_node: Callable[[str], ModbusNodeBase]) -> bool:
|
||||
use_node('M01_pipette0_coil_w').write(False)
|
||||
return True
|
||||
|
||||
move_pipetter= ModbusWorkflow(name="测试待机位置", actions=[WorkflowAction(
|
||||
init=None,
|
||||
start=pipetter_position,
|
||||
stop=pipetter_stop,
|
||||
)])
|
||||
|
||||
|
||||
|
||||
workflow_test_2 = ModbusWorkflow(name="测试水平移动并停止", actions=[
|
||||
move_idel,
|
||||
move_pipetter,
|
||||
])
|
||||
|
||||
nodes = load_csv('/Users/dingshinn/Desktop/lbg/uni-lab/M01.csv')
|
||||
|
||||
modbus_tcp_client_test2 \
|
||||
.register_node_list(nodes) \
|
||||
.run_modbus_workflow(workflow_test_2)
|
||||
# .run_modbus_workflow(move_2_left_workflow)
|
||||
66
unilabos/device_comms/rpc.py
Normal file
66
unilabos/device_comms/rpc.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import json
|
||||
import requests
|
||||
from rclpy.logging import get_logger
|
||||
|
||||
|
||||
class BaseRequest:
|
||||
def __init__(self):
|
||||
self._logger = get_logger(__name__)
|
||||
|
||||
def get_logger(self):
|
||||
return self._logger
|
||||
|
||||
def get(self, url, params, headers={"Content-Type": "application/json"}):
|
||||
try:
|
||||
response = requests.get(url, params=params, headers=headers, timeout=30)
|
||||
self.get_logger().debug(
|
||||
f"Request >>> : {params} {response.status_code} {response.text}"
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
|
||||
except Exception as e:
|
||||
self.get_logger().error(f"Request ERROR: {e}")
|
||||
return
|
||||
|
||||
def post(self, url, params={}, files=None, headers={"Content-Type": "application/json"}):
|
||||
try:
|
||||
response = requests.post(
|
||||
url, data=json.dumps(params) if params else None, headers=headers, timeout=120, files=files
|
||||
)
|
||||
self.get_logger().debug(
|
||||
f"Request >>> : {response.request.body} {response.status_code} {response.text}"
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception("Request ERROR:", response.text)
|
||||
|
||||
except Exception as e:
|
||||
self.get_logger().error(f"Request ERROR: {e}")
|
||||
return
|
||||
|
||||
def form_post(self, url, params):
|
||||
try:
|
||||
response = requests.post(
|
||||
url=url,
|
||||
data=params,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=3,
|
||||
)
|
||||
self.get_logger().debug(
|
||||
f"Request >>> : {response.request.body} {response.status_code} {response.text}"
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception("Request ERROR:", response.text)
|
||||
|
||||
except Exception as e:
|
||||
self.get_logger().error(f"Request ERROR: {e}")
|
||||
return
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
228
unilabos/device_comms/universal_driver.py
Normal file
228
unilabos/device_comms/universal_driver.py
Normal file
@@ -0,0 +1,228 @@
|
||||
|
||||
from abc import abstractmethod
|
||||
from functools import wraps
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
from socket import socket
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
import serial
|
||||
|
||||
class SingleRunningExecutor(object):
|
||||
"""
|
||||
异步执行单个任务,不允许重复执行,通过class的函数获得唯一任务名的实例
|
||||
需要对
|
||||
"""
|
||||
__instance = {}
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, func, post_func=None, name=None, *var, **kwargs):
|
||||
print(f"!!!get_instance: {name} {kwargs}")
|
||||
if name is None:
|
||||
name = func.__name__
|
||||
if name not in cls.__instance:
|
||||
cls.__instance[name] = cls(func, post_func, *var, **kwargs)
|
||||
return cls.__instance[name]
|
||||
|
||||
start_time: float = None
|
||||
end_time: float = None
|
||||
is_running: bool = None
|
||||
is_error: bool = None
|
||||
is_success: bool = None
|
||||
|
||||
@property
|
||||
def is_ended(self):
|
||||
return not self.is_running and (self.is_error or self.is_success)
|
||||
|
||||
@property
|
||||
def is_started(self):
|
||||
return self.is_running or self.is_error or self.is_success
|
||||
|
||||
def reset(self):
|
||||
self.start_time = None
|
||||
self.end_time = None
|
||||
self.is_running = False
|
||||
self.is_error = False
|
||||
self.is_success = False
|
||||
self._final_var = {}
|
||||
self._thread = threading.Thread(target=self._execute)
|
||||
|
||||
def __init__(self, func, post_func=None, *var, **kwargs):
|
||||
self._func = func
|
||||
self._post_func = post_func
|
||||
self._sig = inspect.signature(self._func)
|
||||
self._var = var
|
||||
self._kwargs = kwargs
|
||||
self.reset()
|
||||
|
||||
def _execute(self):
|
||||
res = None
|
||||
try:
|
||||
for ind, i in enumerate(self._var):
|
||||
self._final_var[list(self._sig.parameters.keys())[ind]] = i
|
||||
for k, v in self._kwargs.items():
|
||||
if k in self._sig.parameters.keys():
|
||||
self._final_var[k] = v
|
||||
self.is_running = True
|
||||
print(f"!!!_final_var: {self._final_var}")
|
||||
res = self._func(**self._final_var)
|
||||
except Exception as e:
|
||||
self.is_running = False
|
||||
self.is_error = True
|
||||
traceback.print_exc()
|
||||
if callable(self._post_func):
|
||||
self._post_func(res, self._final_var)
|
||||
|
||||
def start(self, **kwargs):
|
||||
if len(kwargs) > 0:
|
||||
self._kwargs = kwargs
|
||||
self.start_time = time.time()
|
||||
self._thread.start()
|
||||
|
||||
def join(self):
|
||||
if self.is_running:
|
||||
self._thread.join()
|
||||
self.end_time = time.time()
|
||||
|
||||
|
||||
def command(func):
|
||||
"""
|
||||
Decorator for command_set execution. Checks if the method is called in the same thread as the class instance,
|
||||
if so enqueues the command_set and waits for a reply in the reply queue. Else it concludes it must be the command
|
||||
handler thread and actually executes the method. This way methods in the child classes need to be written
|
||||
just once and decorated accordingly.
|
||||
:return: decorated method
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
device_instance = args[0]
|
||||
if threading.get_ident() == device_instance.current_thread:
|
||||
command_set = [func, args, kwargs]
|
||||
device_instance.command_queue.put(command_set)
|
||||
while True:
|
||||
if not device_instance.reply_queue.empty():
|
||||
return device_instance.reply_queue.get()
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
class UniversalDriver(object):
|
||||
def _init_logger(self):
|
||||
self.logger = logging.getLogger(f"{self.__class__.__name__}_logger")
|
||||
|
||||
def __init__(self):
|
||||
self._init_logger()
|
||||
|
||||
def execute_command_from_outer(self, command: str):
|
||||
try:
|
||||
command = json.loads(command.replace("'", '"').replace("False", "false").replace("True", "true")) # 要求不能出现'作为字符串
|
||||
except Exception as e:
|
||||
print(f"Json解析失败: {e}")
|
||||
return False
|
||||
for k, v in command.items():
|
||||
print(f"执行{k}方法,参数为{v}")
|
||||
try:
|
||||
getattr(self, k)(**v)
|
||||
except Exception as e:
|
||||
print(f"执行{k}方法失败: {e}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class TransportDriver(UniversalDriver):
|
||||
COMMAND_QUEUE_ENABLE = True
|
||||
command_handler_thread: Optional[threading.Thread] = None
|
||||
__connection: Optional[serial.Serial | socket] = None
|
||||
|
||||
|
||||
def _init_command_queue(self):
|
||||
self.command_queue = queue.Queue()
|
||||
self.reply_queue = queue.Queue()
|
||||
|
||||
def __command_handler_daemon(self):
|
||||
while True:
|
||||
try:
|
||||
if not self.command_queue.empty():
|
||||
command_item = self.command_queue.get()
|
||||
method = command_item[0]
|
||||
arguments = command_item[1]
|
||||
keywordarguments = command_item[2]
|
||||
reply = method(*arguments, **keywordarguments)
|
||||
self.reply_queue.put(reply)
|
||||
else:
|
||||
self.keepalive()
|
||||
except ValueError as e:
|
||||
# workaround if something goes wrong with the serial connection
|
||||
# future me will certainly not hate past me for this...
|
||||
self.logger.critical(e)
|
||||
self.__connection.flush()
|
||||
# thread-safe purging of both queues
|
||||
while not self.command_queue.empty():
|
||||
self.command_queue.get()
|
||||
while not self.reply_queue.empty():
|
||||
self.reply_queue.get()
|
||||
|
||||
def launch_command_handler(self):
|
||||
if self.COMMAND_QUEUE_ENABLE:
|
||||
self.command_handler_thread = threading.Thread(target=self.__command_handler_daemon, name="{0}_command_handler".format(self.device_name), daemon=True)
|
||||
self.command_handler_thread.start()
|
||||
|
||||
@abstractmethod
|
||||
def open_connection(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close_connection(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def keepalive(self):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
if self.COMMAND_QUEUE_ENABLE:
|
||||
self.launch_command_handler()
|
||||
|
||||
|
||||
class DriverChecker(object):
|
||||
def __init__(self, driver, interval: int | float):
|
||||
self.driver = driver
|
||||
self.interval = interval
|
||||
self._thread = threading.Thread(target=self._monitor)
|
||||
self._thread.daemon = True
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
def _monitor(self):
|
||||
while not self._stop_event.is_set():
|
||||
try:
|
||||
# print(self.__class__.__name__, "Started!")
|
||||
self.check()
|
||||
except Exception as e:
|
||||
print(f"Error in {self.__class__.__name__}: {str(e)}")
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
time.sleep(self.interval)
|
||||
|
||||
@abstractmethod
|
||||
def check(self):
|
||||
"""子类必须实现此方法"""
|
||||
raise NotImplementedError
|
||||
|
||||
def start_monitoring(self):
|
||||
self._thread.start()
|
||||
|
||||
def stop_monitoring(self):
|
||||
self._stop_event.set()
|
||||
self._thread.join()
|
||||
|
||||
Reference in New Issue
Block a user