Initial commit

This commit is contained in:
Junhan Chang
2025-04-17 15:19:47 +08:00
parent a47a3f5c3a
commit c78ac482d8
262 changed files with 39871 additions and 0 deletions

0
unilabos/__init__.py Normal file
View File

0
unilabos/app/__init__.py Normal file
View File

35
unilabos/app/backend.py Normal file
View File

@@ -0,0 +1,35 @@
import threading
from unilabos.utils import logger
# 根据选择的 backend 启动相应的功能
def start_backend(
backend: str,
devices_config: dict = {},
resources_config: dict = {},
graph=None,
controllers_config: dict = {},
bridges=[],
without_host: bool = False,
**kwargs
):
if backend == "ros":
# 假设 ros_main, simple_main, automancer_main 是不同 backend 的启动函数
from unilabos.ros.main_slave_run import main, slave # 如果选择 'ros' 作为 backend
elif backend == 'simple':
# 这里假设 simple_backend 和 automancer_backend 是你定义的其他两个后端
# from simple_backend import main as simple_main
pass
elif backend == 'automancer':
# from automancer_backend import main as automancer_main
pass
else:
raise ValueError(f"Unsupported backend: {backend}")
backend_thread = threading.Thread(
target=main if not without_host else slave,
args=(devices_config, resources_config, graph, controllers_config, bridges)
)
backend_thread.start()
logger.info(f"Backend {backend} started.")

34
unilabos/app/controler.py Normal file
View File

@@ -0,0 +1,34 @@
import json
import uuid
from unilabos.app.model import JobAddReq, JobData
from unilabos.ros.nodes.presets.host_node import HostNode
def get_resources() -> tuple:
if HostNode.get_instance() is None:
return False, "Host node not initialized"
return True, HostNode.get_instance().resources_config
def devices() -> tuple:
if HostNode.get_instance() is None:
return False, "Host node not initialized"
return True, HostNode.get_instance().devices_config
def job_info(id: str):
get_goal_status = HostNode.get_instance().get_goal_status(id)
return JobData(jobId=id, status=get_goal_status)
def job_add(req: JobAddReq) -> JobData:
if req.job_id is None:
req.job_id = str(uuid.uuid4())
action_name = req.data["action"]
action_kwargs = req.data["action_kwargs"]
req.data['action'] = action_name
if action_name == "execute_command_from_outer":
action_kwargs = {"command": json.dumps(action_kwargs)}
print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id)
return JobData(jobId=req.job_id)

155
unilabos/app/main.py Normal file
View File

@@ -0,0 +1,155 @@
import argparse
import os
import signal
import sys
import json
import yaml
from copy import deepcopy
# 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__))
ilabos_dir = os.path.dirname(os.path.dirname(current_dir))
if ilabos_dir not in sys.path:
sys.path.append(ilabos_dir)
from unilabos.config.config import load_config, BasicConfig
from unilabos.utils.banner_print import print_status, print_unilab_banner
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
parser.add_argument("-g", "--graph", help="Physical setup graph.")
parser.add_argument("-d", "--devices", help="Devices config file.")
parser.add_argument("-r", "--resources", help="Resources config file.")
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file.")
parser.add_argument(
"--registry_path",
type=str,
default=None,
action="append",
help="Path to the registry",
)
parser.add_argument(
"--backend",
choices=["ros", "simple", "automancer"],
default="ros",
help="Choose the backend to run with: 'ros', 'simple', or 'automancer'.",
)
parser.add_argument(
"--app_bridges",
nargs="+",
default=["mqtt", "fastapi"],
help="Bridges to connect to. Now support 'mqtt' and 'fastapi'.",
)
parser.add_argument(
"--without_host",
action="store_true",
help="Run the backend as slave (without host).",
)
parser.add_argument(
"--slave_no_host",
action="store_true",
help="Slave模式下跳过等待host服务",
)
parser.add_argument(
"--config",
type=str,
default=None,
help="配置文件路径,支持.py格式的Python配置文件",
)
return parser.parse_args()
def main():
"""主函数"""
# 解析命令行参数
args = parse_args()
args_dict = vars(args)
# 加载配置文件 - 这里保持最先加载配置的逻辑
if args_dict.get("config"):
config_path = args_dict["config"]
if not os.path.exists(config_path):
print_status(f"配置文件 {config_path} 不存在", "error")
elif not config_path.endswith(".py"):
print_status(f"配置文件 {config_path} 不是Python文件必须以.py结尾", "error")
else:
load_config(config_path)
# 设置BasicConfig参数
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
from unilabos.resources.graphio import (
read_node_link_json,
read_graphml,
dict_from_graph,
dict_to_nested_dict,
initialize_resources,
)
from unilabos.app.mq import mqtt_client
from unilabos.registry.registry import build_registry
from unilabos.app.backend import start_backend
from unilabos.web import http_client
from unilabos.web import start_server
# 显示启动横幅
print_unilab_banner(args_dict)
# 注册表
build_registry(args_dict["registry_path"])
if args_dict["graph"] is not None:
import unilabos.resources.graphio as graph_res
graph_res.physical_setup_graph = (
read_node_link_json(args_dict["graph"])
if args_dict["graph"].endswith(".json")
else read_graphml(args_dict["graph"])
)
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
# args_dict["resources_config"] = dict_to_tree(devices_and_resources, devices_only=False)
args_dict["graph"] = graph_res.physical_setup_graph
else:
if args_dict["devices"] is None or args_dict["resources"] is None:
print_status("Either graph or devices and resources must be provided.", "error")
sys.exit(1)
args_dict["devices_config"] = json.load(open(args_dict["devices"], encoding="utf-8"))
args_dict["resources_config"] = initialize_resources(
list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
)
print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info")
for i in args_dict["resources_config"]:
print_status(f"DeviceId: {i['id']}, Class: {i['class']}", "info")
if args_dict["controllers"] is not None:
args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8"))
else:
args_dict["controllers_config"] = None
args_dict["bridges"] = []
if "mqtt" in args_dict["app_bridges"]:
args_dict["bridges"].append(mqtt_client)
if "fastapi" in args_dict["app_bridges"]:
args_dict["bridges"].append(http_client)
if "mqtt" in args_dict["app_bridges"]:
def _exit(signum, frame):
mqtt_client.stop()
sys.exit(0)
signal.signal(signal.SIGINT, _exit)
signal.signal(signal.SIGTERM, _exit)
mqtt_client.start()
start_backend(**args_dict)
start_server()
if __name__ == "__main__":
main()

137
unilabos/app/model.py Normal file
View File

@@ -0,0 +1,137 @@
from pydantic import BaseModel, Field
class RespCode:
Success = 0
ErrorHostNotInit = 2001 # Host node not initialized
ErrorInvalidReq = 2002 # Invalid request data
class DeviceAction(BaseModel):
x: str
y: str
action: str
class Device(BaseModel):
id: str
name: str
action: DeviceAction
class DeviceList(BaseModel):
items: list[Device] = []
page: int
pageSize: int
class DevicesResponse(BaseModel):
code: int
data: DeviceList
class DeviceInfoResponse(BaseModel):
code: int
data: Device
class PageResp(BaseModel):
item: list = []
page: int = 1
pageSize: int = 10
class Resp(BaseModel):
code: int = RespCode.Success
data: dict = {}
message: str = "success"
class JobAddReq(BaseModel):
device_id: str = Field(examples=["Gripper"], description="device id")
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}])
job_id: str = Field(examples=["sfsfsfeq"], description="goal uuid")
node_id: str = Field(examples=["sfsfsfeq"], description="node uuid")
class JobStepFinishReq(BaseModel):
token: str = Field(examples=["030944"], description="token")
request_time: str = Field(
examples=["2024-12-12 12:12:12.xxx"], description="requestTime"
)
data: dict = Field(
examples=[
{
"orderCode": "任务号。字符串",
"orderName": "任务名称。字符串",
"stepName": "步骤名称。字符串",
"stepId": "步骤Id。GUID",
"sampleId": "通量Id。GUID",
"startTime": "开始时间。时间格式",
"endTime": "完成时间。时间格式",
}
]
)
class JobPreintakeFinishReq(BaseModel):
token: str = Field(examples=["030944"], description="token")
request_time: str = Field(
examples=["2024-12-12 12:12:12.xxx"], description="requestTime"
)
data: dict = Field(
examples=[
{
"orderCode": "任务号。字符串",
"orderName": "任务名称。字符串",
"sampleId": "通量Id。GUID",
"startTime": "开始时间。时间格式",
"endTime": "完成时间。时间格式",
"Status": "通量状态,0待生产、2进样、10开始、完成20、异常停止-2、人工停止或取消-3",
}
]
)
class JobFinishReq(BaseModel):
token: str = Field(examples=["030944"], description="token")
request_time: str = Field(
examples=["2024-12-12 12:12:12.xxx"], description="requestTime"
)
data: dict = Field(
examples=[
{
"orderCode": "任务号。字符串",
"orderName": "任务名称。字符串",
"startTime": "开始时间。时间格式",
"endTime": "完成时间。时间格式",
"status": "通量状态,完成30、异常停止-11、人工停止或取消-12",
"usedMaterials": [
{
"materialId": "物料Id。GUID",
"locationId": "库位Id。GUID",
"typeMode": "物料类型。 样品1、试剂2、耗材0",
"usedQuantity": "使用的数量。 数字",
}
],
}
]
)
class JobData(BaseModel):
jobId: str = Field(examples=["sfsfsfeq"], description="goal uuid")
status: int = Field(
examples=[0, 1],
default=0,
description="0:UNKNOWN, 1:ACCEPTED, 2:EXECUTING, 3:CANCELING, 4:SUCCEEDED, 5:CANCELED, 6:ABORTED",
)
class JobStatusResp(Resp):
data: JobData
class JobAddResp(Resp):
data: JobData

177
unilabos/app/mq.py Normal file
View File

@@ -0,0 +1,177 @@
import json
import time
import uuid
import paho.mqtt.client as mqtt
import ssl, base64, hmac
from hashlib import sha1
import tempfile
import os
from unilabos.config.config import MQConfig
from unilabos.app.controler import devices, job_add
from unilabos.app.model import JobAddReq, JobAddResp
from unilabos.utils import logger
from unilabos.utils.type_check import TypeEncoder
class MQTTClient:
mqtt_disable = True
def __init__(self):
self.mqtt_disable = not MQConfig.lab_id
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
self._setup_callbacks()
def _setup_callbacks(self):
self.client.on_log = self._on_log
self.client.on_connect = self._on_connect
self.client.on_message = self._on_message
self.client.on_disconnect = self._on_disconnect
def _on_log(self, client, userdata, level, buf):
logger.info(f"[MQTT] log: {buf}")
def _on_connect(self, client, userdata, flags, rc, properties=None):
logger.info("[MQTT] Connected with result code " + str(rc))
client.subscribe(f"labs/{MQConfig.lab_id}/job/start/", 0)
isok, data = devices()
if not isok:
logger.error("[MQTT] on_connect ErrorHostNotInit")
return
def _on_message(self, client, userdata, msg):
logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
try:
payload_str = msg.payload.decode("utf-8")
payload_json = json.loads(payload_str)
logger.debug(f"Topic: {msg.topic}")
logger.debug("Payload:", json.dumps(payload_json, indent=2, ensure_ascii=False))
if msg.topic == f"labs/{MQConfig.lab_id}/job/start/":
logger.debug("job_add", type(payload_json), payload_json)
job_req = JobAddReq.model_validate(payload_json)
data = job_add(job_req)
return JobAddResp(data=data)
except json.JSONDecodeError as e:
logger.error(f"[MQTT] JSON 解析错误: {e}")
logger.error(f"[MQTT] Raw message: {msg.payload}")
except Exception as e:
logger.error(f"[MQTT] 处理消息时出错: {e}")
def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None):
if rc != 0:
logger.error(f"[MQTT] Unexpected disconnection {rc}")
def _setup_ssl_context(self):
temp_files = []
try:
with tempfile.NamedTemporaryFile(mode="w", delete=False) as ca_temp:
ca_temp.write(MQConfig.ca_content)
temp_files.append(ca_temp.name)
with tempfile.NamedTemporaryFile(mode="w", delete=False) as cert_temp:
cert_temp.write(MQConfig.cert_content)
temp_files.append(cert_temp.name)
with tempfile.NamedTemporaryFile(mode="w", delete=False) as key_temp:
key_temp.write(MQConfig.key_content)
temp_files.append(key_temp.name)
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
context.load_verify_locations(cafile=temp_files[0])
context.load_cert_chain(certfile=temp_files[1], keyfile=temp_files[2])
self.client.tls_set_context(context)
finally:
for temp_file in temp_files:
try:
os.unlink(temp_file)
except:
pass
def start(self):
if self.mqtt_disable:
logger.warning("MQTT is disabled, skipping connection.")
return
userName = f"Signature|{MQConfig.access_key}|{MQConfig.instance_id}"
password = base64.b64encode(
hmac.new(MQConfig.secret_key.encode(), self.client_id.encode(), sha1).digest()
).decode()
self.client.username_pw_set(userName, password)
self._setup_ssl_context()
# 创建连接线程
def connect_thread_func():
try:
self.client.connect(MQConfig.broker_url, MQConfig.port, 60)
self.client.loop_start()
# 添加连接超时检测
max_attempts = 5
attempt = 0
while not self.client.is_connected() and attempt < max_attempts:
logger.info(
f"[MQTT] 正在连接到 {MQConfig.broker_url}:{MQConfig.port},尝试 {attempt+1}/{max_attempts}"
)
time.sleep(3)
attempt += 1
if self.client.is_connected():
logger.info(f"[MQTT] 已成功连接到 {MQConfig.broker_url}:{MQConfig.port}")
else:
logger.error(f"[MQTT] 连接超时,可能是账号密码错误或网络问题")
self.client.loop_stop()
except Exception as e:
logger.error(f"[MQTT] 连接失败: {str(e)}")
connect_thread_func()
# connect_thread = threading.Thread(target=connect_thread_func)
# connect_thread.daemon = True
# connect_thread.start()
def stop(self):
if self.mqtt_disable:
return
self.client.disconnect()
self.client.loop_stop()
def publish_device_status(self, device_status: dict, device_id, property_name):
# status = device_status.get(device_id, {})
if self.mqtt_disable:
return
status = {"data": device_status.get(device_id, {}), "device_id": device_id}
address = f"labs/{MQConfig.lab_id}/devices"
self.client.publish(address, json.dumps(status), qos=2)
logger.critical(f"Device status published: address: {address}, {status}")
def publish_job_status(self, feedback_data: dict, job_id: str, status: str):
if self.mqtt_disable:
return
jobdata = {"job_id": job_id, "data": feedback_data, "status": status}
self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2)
def publish_registry(self, device_id: str, device_info: dict):
if self.mqtt_disable:
return
address = f"labs/{MQConfig.lab_id}/registry/"
registry_data = json.dumps({device_id: device_info}, ensure_ascii = False, cls = TypeEncoder)
self.client.publish(address, registry_data, qos=2)
logger.debug(f"Registry data published: address: {address}, {registry_data}")
def publish_actions(self, action_id: str, action_info: dict):
if self.mqtt_disable:
return
address = f"labs/{MQConfig.lab_id}/actions/"
action_type_name = action_info["title"]
action_info["title"] = action_id
action_data = json.dumps({action_type_name: action_info}, ensure_ascii=False)
self.client.publish(address, action_data, qos=2)
logger.debug(f"Action data published: address: {address}, {action_data}")
mqtt_client = MQTTClient()
if __name__ == "__main__":
mqtt_client.start()

231
unilabos/app/oss_upload.py Normal file
View File

@@ -0,0 +1,231 @@
import argparse
import os
import time
from typing import Dict, Optional, Tuple
import requests
from unilabos.config.config import OSSUploadConfig
def _init_upload(file_path: str, oss_path: str, filename: Optional[str] = None,
process_key: str = "file-upload", device_id: str = "default",
expires_hours: int = 1) -> Tuple[bool, Dict]:
"""
初始化上传过程
Args:
file_path: 本地文件路径
oss_path: OSS目标路径
filename: 文件名如果为None则使用file_path的文件名
process_key: 处理键
device_id: 设备ID
expires_hours: 链接过期小时数
Returns:
(成功标志, 响应数据)
"""
if filename is None:
filename = os.path.basename(file_path)
# 构造初始化请求
url = f"{OSSUploadConfig.api_host}{OSSUploadConfig.init_endpoint}"
headers = {
"Authorization": OSSUploadConfig.authorization,
"Content-Type": "application/json"
}
payload = {
"device_id": device_id,
"process_key": process_key,
"filename": filename,
"path": oss_path,
"expires_hours": expires_hours
}
try:
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 201:
result = response.json()
if result.get("code") == "10000":
return True, result.get("data", {})
print(f"初始化上传失败: {response.status_code}, {response.text}")
return False, {}
except Exception as e:
print(f"初始化上传异常: {str(e)}")
return False, {}
def _put_upload(file_path: str, upload_url: str) -> bool:
"""
执行PUT上传
Args:
file_path: 本地文件路径
upload_url: 上传URL
Returns:
是否成功
"""
try:
with open(file_path, "rb") as f:
response = requests.put(upload_url, data=f)
if response.status_code == 200:
return True
print(f"PUT上传失败: {response.status_code}, {response.text}")
return False
except Exception as e:
print(f"PUT上传异常: {str(e)}")
return False
def _complete_upload(uuid: str) -> bool:
"""
完成上传过程
Args:
uuid: 上传的UUID
Returns:
是否成功
"""
url = f"{OSSUploadConfig.api_host}{OSSUploadConfig.complete_endpoint}"
headers = {
"Authorization": OSSUploadConfig.authorization,
"Content-Type": "application/json"
}
payload = {
"uuid": uuid
}
try:
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
if result.get("code") == "10000":
return True
print(f"完成上传失败: {response.status_code}, {response.text}")
return False
except Exception as e:
print(f"完成上传异常: {str(e)}")
return False
def oss_upload(file_path: str, oss_path: str, filename: Optional[str] = None,
process_key: str = "file-upload", device_id: str = "default") -> bool:
"""
文件上传主函数,包含重试机制
Args:
file_path: 本地文件路径
oss_path: OSS目标路径
filename: 文件名如果为None则使用file_path的文件名
process_key: 处理键
device_id: 设备ID
Returns:
是否成功上传
"""
max_retries = OSSUploadConfig.max_retries
retry_count = 0
while retry_count < max_retries:
try:
# 步骤1初始化上传
init_success, init_data = _init_upload(
file_path=file_path,
oss_path=oss_path,
filename=filename,
process_key=process_key,
device_id=device_id
)
if not init_success:
print(f"初始化上传失败,重试 {retry_count + 1}/{max_retries}")
retry_count += 1
time.sleep(1) # 等待1秒后重试
continue
# 获取UUID和上传URL
uuid = init_data.get("uuid")
upload_url = init_data.get("upload_url")
if not uuid or not upload_url:
print(f"初始化上传返回数据不完整,重试 {retry_count + 1}/{max_retries}")
retry_count += 1
time.sleep(1)
continue
# 步骤2PUT上传文件
put_success = _put_upload(file_path, upload_url)
if not put_success:
print(f"PUT上传失败重试 {retry_count + 1}/{max_retries}")
retry_count += 1
time.sleep(1)
continue
# 步骤3完成上传
complete_success = _complete_upload(uuid)
if not complete_success:
print(f"完成上传失败,重试 {retry_count + 1}/{max_retries}")
retry_count += 1
time.sleep(1)
continue
# 所有步骤都成功
print(f"文件 {file_path} 上传成功")
return True
except Exception as e:
print(f"上传过程异常: {str(e)},重试 {retry_count + 1}/{max_retries}")
retry_count += 1
time.sleep(1)
print(f"文件 {file_path} 上传失败,已达到最大重试次数 {max_retries}")
return False
if __name__ == "__main__":
# python -m unilabos.app.oss_upload -f /path/to/your/file.txt
# 命令行参数解析
parser = argparse.ArgumentParser(description='文件上传测试工具')
parser.add_argument('--file', '-f', type=str, required=True, help='要上传的本地文件路径')
parser.add_argument('--path', '-p', type=str, default='/HPLC1/Any', help='OSS目标路径')
parser.add_argument('--device', '-d', type=str, default='test-device', help='设备ID')
parser.add_argument('--process', '-k', type=str, default='HPLC-txt-result', help='处理键')
args = parser.parse_args()
# 检查文件是否存在
if not os.path.exists(args.file):
print(f"错误:文件 {args.file} 不存在")
exit(1)
print("=" * 50)
print(f"开始上传文件: {args.file}")
print(f"目标路径: {args.path}")
print(f"设备ID: {args.device}")
print(f"处理键: {args.process}")
print("=" * 50)
# 执行上传
success = oss_upload(
file_path=args.file,
oss_path=args.path,
filename=None, # 使用默认文件名
process_key=args.process,
device_id=args.device
)
# 输出结果
if success:
print("\n√ 文件上传成功!")
exit(0)
else:
print("\n× 文件上传失败!")
exit(1)

View File

@@ -0,0 +1,19 @@
from unilabos.messages import *
from .pump_protocol import generate_pump_protocol, generate_pump_protocol_with_rinsing
from .clean_protocol import generate_clean_protocol
from .separate_protocol import generate_separate_protocol
from .evaporate_protocol import generate_evaporate_protocol
from .evacuateandrefill_protocol import generate_evacuateandrefill_protocol
from .agv_transfer_protocol import generate_agv_transfer_protocol
# Define a dictionary of protocol generators.
action_protocol_generators = {
PumpTransferProtocol: generate_pump_protocol_with_rinsing,
CleanProtocol: generate_clean_protocol,
SeparateProtocol: generate_separate_protocol,
EvaporateProtocol: generate_evaporate_protocol,
EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol,
AGVTransferProtocol: generate_agv_transfer_protocol,
}
# End Protocols

View File

@@ -0,0 +1,53 @@
import networkx as nx
def generate_agv_transfer_protocol(
G: nx.Graph,
from_repo: dict,
from_repo_position: str,
to_repo: dict = {},
to_repo_position: str = ""
):
from_repo_ = list(from_repo.values())[0]
to_repo_ = list(to_repo.values())[0]
resource_to_move = from_repo_["children"].pop(from_repo_position)
resource_to_move["parent"] = to_repo_["id"]
to_repo_["children"][to_repo_position] = resource_to_move
from_repo_id = from_repo_["id"]
to_repo_id = to_repo_["id"]
wf_list = {
("AiChemEcoHiWo", "zhixing_agv"): {"nav_command" : '{"target" : "LM14"}',
"arm_command": '{"task_name" : "camera/250111_biaozhi.urp"}'},
("AiChemEcoHiWo", "AGV"): {"nav_command" : '{"target" : "LM14"}',
"arm_command": '{"task_name" : "camera/250111_biaozhi.urp"}'},
("zhixing_agv", "Revvity"): {"nav_command" : '{"target" : "LM13"}',
"arm_command": '{"task_name" : "camera/250111_put_board.urp"}'},
("AGV", "Revvity"): {"nav_command" : '{"target" : "LM13"}',
"arm_command": '{"task_name" : "camera/250111_put_board.urp"}'},
("Revvity", "HPLC"): {"nav_command": '{"target" : "LM13"}',
"arm_command": '{"task_name" : "camera/250111_hplc.urp"}'},
("HPLC", "Revvity"): {"nav_command": '{"target" : "LM13"}',
"arm_command": '{"task_name" : "camera/250111_lfp.urp"}'},
}
return [
{
"device_id": "zhixing_agv",
"action_name": "send_nav_task",
"action_kwargs": {
"command": wf_list[(from_repo_id, to_repo_id)]["nav_command"]
}
},
{
"device_id": "zhixing_ur_arm",
"action_name": "move_pos_task",
"action_kwargs": {
"command": wf_list[(from_repo_id, to_repo_id)]["arm_command"]
}
}
]

View File

@@ -0,0 +1,62 @@
import numpy as np
import networkx as nx
def generate_clean_protocol(
G: nx.DiGraph,
vessel: str, # Vessel to clean.
solvent: str, # Solvent to clean vessel with.
volume: float = 25000.0, # Optional. Volume of solvent to clean vessel with.
temp: float = 25, # Optional. Temperature to heat vessel to while cleaning.
repeats: int = 1, # Optional. Number of cleaning cycles to perform.
) -> list[dict]:
"""
Generate a protocol to clean a vessel with a solvent.
:param G: Directed graph. Nodes are containers and pumps, edges are fluidic connections.
:param vessel: Vessel to clean.
:param solvent: Solvent to clean vessel with.
:param volume: Volume of solvent to clean vessel with.
:param temp: Temperature to heat vessel to while cleaning.
:param repeats: Number of cleaning cycles to perform.
:return: List of actions to clean vessel.
"""
# 生成泵操作的动作序列
pump_action_sequence = []
from_vessel = f"flask_{solvent}"
waste_vessel = f"waste_workup"
transfer_flowrate = flowrate = 2500.0
# 生成泵操作的动作序列
for i in range(repeats):
# 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
pump_action_sequence.extend([
{
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": from_vessel,
"to_vessel": vessel,
"volume": volume,
"time": volume / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
}
])
pump_action_sequence.extend([
{
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": vessel,
"to_vessel": waste_vessel,
"volume": volume,
"time": volume / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
}
])
return pump_action_sequence

View File

@@ -0,0 +1,143 @@
import numpy as np
import networkx as nx
def generate_evacuateandrefill_protocol(
G: nx.DiGraph,
vessel: str,
gas: str,
repeats: int = 1
) -> list[dict]:
"""
生成泵操作的动作序列。
:param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
:param from_vessel: 容器A
:param to_vessel: 容器B
:param volume: 转移的体积
:param flowrate: 最终注入容器B时的流速
:param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同)
:return: 泵操作的动作序列
"""
# 生成电磁阀、真空泵、气源操作的动作序列
vacuum_action_sequence = []
nodes = G.nodes(data=True)
# 找到和 vessel 相连的电磁阀和真空泵、气源
vacuum_backbone = {"vessel": vessel}
for neighbor in G.neighbors(vessel):
if nodes[neighbor]["class"].startswith("solenoid_valve"):
for neighbor2 in G.neighbors(neighbor):
if neighbor2 == vessel:
continue
if nodes[neighbor2]["class"].startswith("vacuum_pump"):
vacuum_backbone.update({"vacuum_valve": neighbor, "pump": neighbor2})
break
elif nodes[neighbor2]["class"].startswith("gas_source"):
vacuum_backbone.update({"gas_valve": neighbor, "gas": neighbor2})
break
# 判断是否设备齐全
if len(vacuum_backbone) < 5:
print(f"\n\n\n{vacuum_backbone}\n\n\n")
raise ValueError("Not all devices are connected to the vessel.")
# 生成操作的动作序列
for i in range(repeats):
# 打开真空泵阀门、关闭气源阀门
vacuum_action_sequence.append([
{
"device_id": vacuum_backbone["vacuum_valve"],
"action_name": "set_valve_position",
"action_kwargs": {
"command": "OPEN"
}
},
{
"device_id": vacuum_backbone["gas_valve"],
"action_name": "set_valve_position",
"action_kwargs": {
"command": "CLOSED"
}
}
])
# 打开真空泵、关闭气源
vacuum_action_sequence.append([
{
"device_id": vacuum_backbone["pump"],
"action_name": "set_status",
"action_kwargs": {
"command": "ON"
}
},
{
"device_id": vacuum_backbone["gas"],
"action_name": "set_status",
"action_kwargs": {
"command": "OFF"
}
}
])
vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}})
# 关闭真空泵阀门、打开气源阀门
vacuum_action_sequence.append([
{
"device_id": vacuum_backbone["vacuum_valve"],
"action_name": "set_valve_position",
"action_kwargs": {
"command": "CLOSED"
}
},
{
"device_id": vacuum_backbone["gas_valve"],
"action_name": "set_valve_position",
"action_kwargs": {
"command": "OPEN"
}
}
])
# 关闭真空泵、打开气源
vacuum_action_sequence.append([
{
"device_id": vacuum_backbone["pump"],
"action_name": "set_status",
"action_kwargs": {
"command": "OFF"
}
},
{
"device_id": vacuum_backbone["gas"],
"action_name": "set_status",
"action_kwargs": {
"command": "ON"
}
}
])
vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}})
# 关闭气源
vacuum_action_sequence.append(
{
"device_id": vacuum_backbone["gas"],
"action_name": "set_status",
"action_kwargs": {
"command": "OFF"
}
}
)
# 关闭阀门
vacuum_action_sequence.append(
{
"device_id": vacuum_backbone["gas_valve"],
"action_name": "set_valve_position",
"action_kwargs": {
"command": "CLOSED"
}
}
)
return vacuum_action_sequence

View File

@@ -0,0 +1,81 @@
import numpy as np
import networkx as nx
def generate_evaporate_protocol(
G: nx.DiGraph,
vessel: str,
pressure: float,
temp: float,
time: float,
stir_speed: float
) -> list[dict]:
"""
Generate a protocol to evaporate a solution from a vessel.
:param G: Directed graph. Nodes are containers and pumps, edges are fluidic connections.
:param vessel: Vessel to clean.
:param solvent: Solvent to clean vessel with.
:param volume: Volume of solvent to clean vessel with.
:param temp: Temperature to heat vessel to while cleaning.
:param repeats: Number of cleaning cycles to perform.
:return: List of actions to clean vessel.
"""
# 生成泵操作的动作序列
pump_action_sequence = []
reactor_volume = 500000.0
transfer_flowrate = flowrate = 2500.0
# 开启冷凝器
pump_action_sequence.append({
"device_id": "rotavap_chiller",
"action_name": "set_temperature",
"action_kwargs": {
"command": "-40"
}
})
# TODO: 通过温度反馈改为 HeatChillToTemp而非等待固定时间
pump_action_sequence.append({
"action_name": "wait",
"action_kwargs": {
"time": 1800
}
})
# 开启旋蒸真空泵、旋转在液体转移后运行time时间
pump_action_sequence.append({
"device_id": "rotavap_controller",
"action_name": "set_pump_time",
"action_kwargs": {
"command": str(time + reactor_volume / flowrate * 3)
}
})
pump_action_sequence.append({
"device_id": "rotavap_controller",
"action_name": "set_pump_time",
"action_kwargs": {
"command": str(time + reactor_volume / flowrate * 3)
}
})
# 液体转入旋转蒸发器
pump_action_sequence.append({
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": vessel,
"to_vessel": "rotavap",
"volume": reactor_volume,
"time": reactor_volume / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
})
pump_action_sequence.append({
"action_name": "wait",
"action_kwargs": {
"time": time
}
})
return pump_action_sequence

View File

@@ -0,0 +1,213 @@
import numpy as np
import networkx as nx
def generate_pump_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float,
flowrate: float = 500.0,
transfer_flowrate: float = 0,
) -> list[dict]:
"""
生成泵操作的动作序列。
:param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
:param from_vessel: 容器A
:param to_vessel: 容器B
:param volume: 转移的体积
:param flowrate: 最终注入容器B时的流速
:param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同)
:return: 泵操作的动作序列
"""
# 生成泵操作的动作序列
pump_action_sequence = []
nodes = G.nodes(data=True)
# 从from_vessel到to_vessel的最短路径
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
print(shortest_path)
pump_backbone = shortest_path
if not from_vessel.startswith("pump"):
pump_backbone = pump_backbone[1:]
if not to_vessel.startswith("pump"):
pump_backbone = pump_backbone[:-1]
if transfer_flowrate == 0:
transfer_flowrate = flowrate
min_transfer_volume = min([nodes[pump]["max_volume"] for pump in pump_backbone])
repeats = int(np.ceil(volume / min_transfer_volume))
if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")):
raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.")
volume_left = volume
# 生成泵操作的动作序列
for i in range(repeats):
# 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
if not from_vessel.startswith("pump"):
pump_action_sequence.extend([
{
"device_id": pump_backbone[0],
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]]
}
},
{
"device_id": pump_backbone[0],
"action_name": "set_position",
"action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)),
"max_velocity": transfer_flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]):
# 相邻两泵同时切换阀门至连通位置
pump_action_sequence.append([
{
"device_id": pumpA,
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(pumpA, pumpB)["port"][pumpA]
}
},
{
"device_id": pumpB,
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(pumpB, pumpA)["port"][pumpB],
}
}
])
# 相邻两泵液体转移泵A排出液体泵B吸入液体
pump_action_sequence.append([
{
"device_id": pumpA,
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": transfer_flowrate
}
},
{
"device_id": pumpB,
"action_name": "set_position",
"action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)),
"max_velocity": transfer_flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
if not to_vessel.startswith("pump"):
# 单泵依次执行阀指令、活塞指令将最后一台泵液体缓慢加入容器B
pump_action_sequence.extend([
{
"device_id": pump_backbone[-1],
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]]
}
},
{
"device_id": pump_backbone[-1],
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
volume_left -= min_transfer_volume
return pump_action_sequence
# Pump protocol compilation
def generate_pump_protocol_with_rinsing(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float,
amount: str = "",
time: float = 0,
viscous: bool = False,
rinsing_solvent: str = "air",
rinsing_volume: float = 5000.0,
rinsing_repeats: int = 2,
solid: bool = False,
flowrate: float = 2500.0,
transfer_flowrate: float = 500.0,
) -> list[dict]:
"""
Generates a pump protocol for transferring a specified volume between vessels, including rinsing steps with a chosen solvent. This function constructs a sequence of pump actions based on the provided parameters and the shortest path in a directed graph.
Args:
G (nx.DiGraph): The directed graph representing the vessels and connections. 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
from_vessel (str): The name of the vessel to transfer from.
to_vessel (str): The name of the vessel to transfer to.
volume (float): The volume to transfer.
amount (str, optional): Additional amount specification (default is "").
time (float, optional): Time over which to perform the transfer (default is 0).
viscous (bool, optional): Indicates if the fluid is viscous (default is False).
rinsing_solvent (str, optional): The solvent to use for rinsing (default is "air").
rinsing_volume (float, optional): The volume of rinsing solvent to use (default is 5000.0).
rinsing_repeats (int, optional): The number of times to repeat rinsing (default is 2).
solid (bool, optional): Indicates if the transfer involves a solid (default is False).
flowrate (float, optional): The flow rate for the transfer (default is 2500.0). 最终注入容器B时的流速
transfer_flowrate (float, optional): The flow rate for the transfer action (default is 500.0). 泵骨架中转移流速(若不指定,默认与注入流速相同)
Returns:
list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列.
Raises:
AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats.
Examples:
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 100.0, rinsing_solvent="water")
"""
air_vessel = "flask_air"
waste_vessel = f"waste_workup"
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
pump_backbone = shortest_path[1: -1]
nodes = G.nodes(data=True)
min_transfer_volume = float(min([nodes[pump]["max_volume"] for pump in pump_backbone]))
if time != 0:
flowrate = transfer_flowrate = volume / time
pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
if rinsing_solvent != "air":
if "," in rinsing_solvent:
rinsing_solvents = rinsing_solvent.split(",")
assert len(rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats."
else:
rinsing_solvents = [rinsing_solvent] * rinsing_repeats
for rinsing_solvent in rinsing_solvents:
solvent_vessel = f"flask_{rinsing_solvent}"
# 清洗泵
pump_action_sequence.extend(
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) +
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) +
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate)
)
# 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
if rinsing_solvent == rinsing_solvents[0]:
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
return pump_action_sequence
# End Protocols

View File

@@ -0,0 +1,230 @@
import numpy as np
import networkx as nx
def generate_separate_protocol(
G: nx.DiGraph,
purpose: str, # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'.
product_phase: str, # 'top' or 'bottom'. Phase that product will be in.
from_vessel: str, #Contents of from_vessel are transferred to separation_vessel and separation is performed.
separation_vessel: str, # Vessel in which separation of phases will be carried out.
to_vessel: str, # Vessel to send product phase to.
waste_phase_to_vessel: str, # Optional. Vessel to send waste phase to.
solvent: str, # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases.
solvent_volume: float = 50000, # Optional. Volume of solvent to add.
through: str = "", # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'.
repeats: int = 1, # Optional. Number of separations to perform.
stir_time: float = 30, # Optional. Time stir for after adding solvent, before separation of phases.
stir_speed: float = 300, # Optional. Speed to stir at after adding solvent, before separation of phases.
settling_time: float = 300 # Optional. Time
) -> list[dict]:
"""
Generate a protocol to clean a vessel with a solvent.
:param G: Directed graph. Nodes are containers and pumps, edges are fluidic connections.
:param vessel: Vessel to clean.
:param solvent: Solvent to clean vessel with.
:param volume: Volume of solvent to clean vessel with.
:param temp: Temperature to heat vessel to while cleaning.
:param repeats: Number of cleaning cycles to perform.
:return: List of actions to clean vessel.
"""
# 生成泵操作的动作序列
pump_action_sequence = []
reactor_volume = 500000.0
waste_vessel = waste_phase_to_vessel
# TODO通过物料管理系统找到溶剂的容器
if "," in solvent:
solvents = solvent.split(",")
assert len(solvents) == repeats, "Number of solvents must match number of repeats."
else:
solvents = [solvent] * repeats
# TODO: 通过设备连接图找到分离容器的控制器、底部出口
separator_controller = f"{separation_vessel}_controller"
separation_vessel_bottom = f"flask_{separation_vessel}"
transfer_flowrate = flowrate = 2500.0
if from_vessel != separation_vessel:
pump_action_sequence.append(
{
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": from_vessel,
"to_vessel": separation_vessel,
"volume": reactor_volume,
"time": reactor_volume / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
}
)
# for i in range(2):
# pump_action_sequence.append(
# {
# "device_id": "",
# "action_name": "CleanProtocol",
# "action_kwargs": {
# "vessel": from_vessel,
# "solvent": "H2O", # Solvent to clean vessel with.
# "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with.
# "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning.
# "repeats": 1
# }
# }
# )
# pump_action_sequence.append(
# {
# "device_id": "",
# "action_name": "CleanProtocol",
# "action_kwargs": {
# "vessel": from_vessel,
# "solvent": "CH2Cl2", # Solvent to clean vessel with.
# "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with.
# "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning.
# "repeats": 1
# }
# }
# )
# 生成泵操作的动作序列
for i in range(repeats):
# 找到当次萃取所用溶剂
solvent_thistime = solvents[i]
solvent_vessel = f"flask_{solvent_thistime}"
pump_action_sequence.append(
{
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": solvent_vessel,
"to_vessel": separation_vessel,
"volume": solvent_volume,
"time": solvent_volume / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
}
)
pump_action_sequence.extend([
# 搅拌、静置
{
"device_id": separator_controller,
"action_name": "stir",
"action_kwargs": {
"stir_time": stir_time,
"stir_speed": stir_speed,
"settling_time": settling_time
}
},
# 分液(判断电导突跃)
{
"device_id": separator_controller,
"action_name": "valve_open",
"action_kwargs": {
"command": "delta > 0.05"
}
}
])
if product_phase == "bottom":
# 产物转移到目标瓶
pump_action_sequence.append(
{
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": separation_vessel_bottom,
"to_vessel": to_vessel,
"volume": 250000.0,
"time": 250000.0 / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
}
)
# 放出上面那一相60秒后关阀门
pump_action_sequence.append(
{
"device_id": separator_controller,
"action_name": "valve_open",
"action_kwargs": {
"command": "time > 60"
}
}
)
# 弃去上面那一相进废液
pump_action_sequence.append(
{
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": separation_vessel_bottom,
"to_vessel": waste_vessel,
"volume": 250000.0,
"time": 250000.0 / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
}
)
elif product_phase == "top":
# 弃去下面那一相进废液
pump_action_sequence.append(
{
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": separation_vessel_bottom,
"to_vessel": waste_vessel,
"volume": 250000.0,
"time": 250000.0 / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
}
)
# 放出上面那一相
pump_action_sequence.append(
{
"device_id": separator_controller,
"action_name": "valve_open",
"action_kwargs": {
"command": "time > 60"
}
}
)
# 产物转移到目标瓶
pump_action_sequence.append(
{
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": separation_vessel_bottom,
"to_vessel": to_vessel,
"volume": 250000.0,
"time": 250000.0 / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
}
)
elif product_phase == "organic":
pass
# 如果不是最后一次,从中转瓶转移回分液漏斗
if i < repeats - 1:
pump_action_sequence.append(
{
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": to_vessel,
"to_vessel": separation_vessel,
"volume": 250000.0,
"time": 250000.0 / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
}
)
return pump_action_sequence

View File

123
unilabos/config/config.py Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python
# coding=utf-8
# 定义配置变量和加载函数
import traceback
import os
import importlib.util
from unilabos.utils import logger
class BasicConfig:
ENV = "pro" # 'test'
config_path = ""
is_host_mode = True # 从registry.py移动过来
slave_no_host = False # 是否跳过rclient.wait_for_service()
# MQTT配置
class MQConfig:
lab_id = ""
instance_id = ""
access_key = ""
secret_key = ""
group_id = ""
broker_url = ""
port = 1883
ca_content = ""
cert_content = ""
key_content = ""
# 指定
ca_file = ""
cert_file = ""
key_file = ""
# OSS上传配置
class OSSUploadConfig:
api_host = ""
authorization = ""
init_endpoint = ""
complete_endpoint = ""
max_retries = 3
# HTTP配置
class HTTPConfig:
remote_addr = "http://127.0.0.1:48197/api/v1"
# ROS配置
class ROSConfig:
modules = [
"std_msgs.msg",
"geometry_msgs.msg",
"control_msgs.msg",
"control_msgs.action",
"nav2_msgs.action",
"unilabos_msgs.msg",
"unilabos_msgs.action",
]
def _update_config_from_module(module):
for name, obj in globals().items():
if isinstance(obj, type) and name.endswith("Config"):
if hasattr(module, name) and isinstance(getattr(module, name), type):
for attr in dir(getattr(module, name)):
if not attr.startswith("_"):
setattr(obj, attr, getattr(getattr(module, name), attr))
# 更新OSS认证
if len(OSSUploadConfig.authorization) == 0:
OSSUploadConfig.authorization = f"lab {MQConfig.lab_id}"
# 对 ca_file cert_file key_file 进行初始化
if len(MQConfig.ca_content) == 0:
# 需要先判断是否为相对路径
if MQConfig.ca_file.startswith("."):
MQConfig.ca_file = os.path.join(BasicConfig.config_path, MQConfig.ca_file)
with open(MQConfig.ca_file, "r", encoding="utf-8") as f:
MQConfig.ca_content = f.read()
if len(MQConfig.cert_content) == 0:
# 需要先判断是否为相对路径
if MQConfig.cert_file.startswith("."):
MQConfig.cert_file = os.path.join(BasicConfig.config_path, MQConfig.cert_file)
with open(MQConfig.cert_file, "r", encoding="utf-8") as f:
MQConfig.cert_content = f.read()
if len(MQConfig.key_content) == 0:
# 需要先判断是否为相对路径
if MQConfig.key_file.startswith("."):
MQConfig.key_file = os.path.join(BasicConfig.config_path, MQConfig.key_file)
with open(MQConfig.key_file, "r", encoding="utf-8") as f:
MQConfig.key_content = f.read()
def load_config(config_path=None):
# 如果提供了配置文件路径,从该文件导入配置
if config_path:
BasicConfig.config_path = os.path.abspath(os.path.dirname(config_path))
if not os.path.exists(config_path):
logger.error(f"配置文件 {config_path} 不存在")
return
try:
module_name = "lab_" + os.path.basename(config_path).replace(".py", "")
spec = importlib.util.spec_from_file_location(module_name, config_path)
if spec is None:
logger.error(f"配置文件 {config_path} 错误")
return
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
_update_config_from_module(module)
logger.info(f"配置文件 {config_path} 加载成功")
except Exception as e:
logger.error(f"加载配置文件 {config_path} 失败: {e}")
traceback.print_exc()
exit(1)
else:
try:
import unilabos.config.local_config as local_config # type: ignore
_update_config_from_module(local_config)
logger.info("已加载默认配置 unilabos.config.local_config")
except ImportError:
pass

View File

@@ -0,0 +1 @@
from .eis_model import EISModelBasedController

View File

@@ -0,0 +1,5 @@
import numpy as np
def EISModelBasedController(eis: np.array) -> float:
return 0.0

View File

View 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)

View 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": [
"归位并测试待机位置"
]
}

View 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')

View 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.")

View 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()

View 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)

View 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.")

View 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)

View 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

View 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()

View File

View File

@@ -0,0 +1,333 @@
import tkinter as tk
from tkinter import ttk # 使用 ttk 替换 tk 控件
from tkinter import messagebox
from tkinter.font import Font
from threading import Thread
from ttkthemes import ThemedTk
import time
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import clr # pythonnet library
import sys
import threading
import datetime
jifenshijian = 10 #积分时间
shuaxinshijian = 0.01 #刷新时间
zihaodaxiao = 16 #字号大小
ymax = 70000
ymin = -2000
# 加载DLL
dll_path = "C:\\auto\\UV_spec\\idea-sdk 3.0.9\\idea-sdk.UPI\\IdeaOptics.dll"
clr.AddReference(dll_path)
from IdeaOptics import Wrapper
# 初始化Wrapper对象和光谱仪
wrapper = Wrapper()
number_of_spectrometers = wrapper.OpenAllSpectrometers()
if number_of_spectrometers > 0:
spectrometer_index = 0 # 假设使用第一个光谱仪
integration_time = jifenshijian # 设置积分时间
wrapper.setIntegrationTime(spectrometer_index, integration_time)
class App:
def __init__(self, root):
self.root = root
self.root.title("光谱测试")
self.is_continuous = False
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
self.stop_event = threading.Event() # 使用Event代替布尔标志
self.continuous_thread = None # 在这里初始化
self.background_spectrum = None
self.correct_background = False
self.test_count = 0
self.background_count = 0
self.source_spectrum = None # 初始化光源强度变量
self.transmission_mode = False # 初始化透射模式标志
self.data_ready = False
# 使用 grid 布局
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
self.root.rowconfigure(1, weight=1)
self.root.rowconfigure(2, weight=1)
self.root.rowconfigure(3, weight=1)
self.current_ylim = [-100, 1000] # 初始化y轴范围
# 创建一个 Style 对象
style = ttk.Style()
# 定义一个新的样式
style.configure('Custom.TButton', font=('Helvetica', zihaodaxiao, 'bold'), foreground='white')
# 创建滑动条和按钮的容器 Frame
control_frame = ttk.Frame(self.root)
control_frame.grid(row=0, column=0, sticky="ew")
# 创建一个滑动条来选择平滑次数
self.boxcar_width_slider = tk.Scale(control_frame, from_=0, to=10, orient=tk.HORIZONTAL, length=300, label="平滑次数", font=("Helvetica", zihaodaxiao, 'bold'))
self.boxcar_width_slider.grid(row=0, column=0, padx=10, pady=10)
# 创建一个滑动条来选择平均次数
self.scans_to_average_slider = tk.Scale(control_frame, from_=1, to=10, orient=tk.HORIZONTAL, length=300, label="平均次数", font=("Helvetica", zihaodaxiao, 'bold'))
self.scans_to_average_slider.grid(row=0, column=1, padx=10, pady=10)
# 调整 Scale 控件的外观
self.boxcar_width_slider.config(bg='grey', fg='white')
self.scans_to_average_slider.config(bg='grey', fg='white')
# 字体设置
entry_font = ('Helvetica', zihaodaxiao, 'bold')
# 添加输入框的容器 Frame
entry_frame = ttk.Frame(self.root)
entry_frame.grid(row=1, column=0, sticky="ew")
# 创建并放置"积分时间(ms)"输入框
ttk.Label(entry_frame, text="积分时间(ms):", font=entry_font).grid(row=0, column=0, padx=10, pady=10)
self.integration_time_entry = ttk.Entry(entry_frame, font=entry_font)
self.integration_time_entry.grid(row=0, column=1, padx=10, pady=10)
self.integration_time_entry.insert(0, "10") # 设置默认值
# 创建并放置"刷新间隔(s)"输入框
ttk.Label(entry_frame, text="刷新间隔(s):", font=entry_font).grid(row=0, column=2, padx=10, pady=10)
self.refresh_interval_entry = ttk.Entry(entry_frame, font=entry_font)
self.refresh_interval_entry.grid(row=0, column=3, padx=10, pady=10)
self.refresh_interval_entry.insert(0, "0.01") # 设置默认值
# 创建按钮的容器 Frame
button_frame = ttk.Frame(self.root)
button_frame.grid(row=2, column=0, sticky="ew")
# 创建并放置按钮
ttk.Button(button_frame, text="测试一下", style='Custom.TButton', command=self.single_test).grid(row=0, column=0, padx=10, pady=10)
ttk.Button(button_frame, text="连续测试", style='Custom.TButton', command=self.start_continuous_test).grid(row=0, column=1, padx=10, pady=10)
ttk.Button(button_frame, text="停止测试", style='Custom.TButton', command=self.stop_continuous_test).grid(row=0, column=2, padx=10, pady=10)
# 创建背景相关按钮的容器 Frame
background_frame = ttk.Frame(self.root)
background_frame.grid(row=3, column=0, sticky="ew")
# 创建并放置“采集背景”按钮
self.collect_background_button = ttk.Button(background_frame, text="采集背景", style='Custom.TButton', command=self.collect_background)
self.collect_background_button.grid(row=0, column=0, padx=10, pady=10)
# 创建并放置“背景校正”按钮
self.correct_background_button = ttk.Button(background_frame, text="背景校正", style='Custom.TButton', command=self.toggle_background_correction)
self.correct_background_button.grid(row=0, column=1, padx=10, pady=10)
# 创建“光源采集”按钮
ttk.Button(background_frame, text="光源采集", style='Custom.TButton', command=self.collect_source).grid(row=0, column=2, padx=10, pady=10)
# 创建“透射模式”按钮
self.transmission_button = ttk.Button(background_frame, text="透射模式", style='Custom.TButton', command=self.toggle_transmission_mode)
self.transmission_button.grid(row=0, column=3, padx=10, pady=10)
# 创建 matplotlib 画布
plt.style.use('ggplot') # 使用预定义的样式,如 'ggplot'
self.figure, self.ax = plt.subplots(figsize=(10, 8))
self.canvas = FigureCanvasTkAgg(self.figure, self.root)
self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.grid(row=3, column=0, sticky="ew")
# 使用 grid 布局来放置 matplotlib 画布
self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.grid(row=4, column=0, sticky="ew")
# 创建文件名并打开文件
start_time = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
self.data_file = open(f"C:\\auto\\UV_spec\\data\\{start_time}.txt", "w")
def get_spectrum_data(self):
# 获取波长和光谱值
pixels = wrapper.getNumberOfPixels(spectrometer_index)
spectrum = wrapper.getSpectrum(spectrometer_index)
wavelengths = wrapper.getWavelengths(spectrometer_index)
# 转换.NET数组到Python列表
spectrum_list = [spectrum[i] for i in range(pixels)]
wavelengths_list = [wavelengths[i] for i in range(pixels)]
self.data_ready = True
return wavelengths_list, spectrum_list
def collect_source(self):
# 采集光源强度数据
wavelengths, self.source_spectrum = self.get_spectrum_data()
conditions = f"jifenshijian = {jifenshijian} shuaxinshijian = {shuaxinshijian} zihaodaxiao = {zihaodaxiao}"
self.write_data_to_file("source", 1, conditions, self.source_spectrum)
self.update_plot(wavelengths, self.source_spectrum)
def toggle_transmission_mode(self):
# 切换透射模式
self.transmission_mode = not self.transmission_mode
self.transmission_button.config(text="正在透射" if self.transmission_mode else "透射模式")
def calculate_transmission(self, spectrum):
# 计算透射率
transmission = []
for s, b, src in zip(spectrum, self.background_spectrum, self.source_spectrum):
denominator = max(src - b, 0.1)
trans_value = (s - b) / denominator * 100
trans_value = max(0, min(trans_value, 100))
transmission.append(trans_value)
return transmission
def update_plot(self, wavelengths, spectrum, plot_type='spectrum'):
if not self.data_ready:
return
self.ax.clear()
if plot_type == 'transmission':
# 透射率模式的绘图设置
self.ax.plot(wavelengths, spectrum, label='Transmission (%)')
self.ax.set_ylim(-10, 110) # 设置y轴范围为0%到100%
self.ax.set_ylabel('Transmission (%)', fontname='Arial', fontsize=zihaodaxiao)
else:
# 普通光谱模式的绘图设置
self.ax.plot(wavelengths, spectrum)
self.ax.set_ylim(self.current_ylim) # 使用当前y轴范围
# 计算新的最大值和最小值
new_min, new_max = min(spectrum), max(spectrum)
# 检查新的最大值或最小值是否超过当前y轴范围
while new_min < self.current_ylim[0] or new_max > self.current_ylim[1]:
# 扩大y轴范围
self.current_ylim = [self.current_ylim[0] * 2, self.current_ylim[1] * 2]
# 确保新的y轴范围不超过最大限制
if self.current_ylim[0] < ymin:
self.current_ylim[0] = ymin
if self.current_ylim[1] > ymax:
self.current_ylim[1] = ymax
break
self.ax.set_ylabel('Intensity', fontname='Arial', fontsize=zihaodaxiao)
self.ax.set_xlabel('Wavelength (nm)', fontname='Arial', fontsize=zihaodaxiao)
self.ax.set_title('Spectrum', fontname='Arial', fontsize=zihaodaxiao)
self.canvas.draw()
self.data_ready = False
def draw_plot(self):
self.canvas.draw()
def write_data_to_file(self, test_type, test_number, conditions, spectrum):
data_str = " ".join(map(str, spectrum))
self.data_file.write(f"{test_type}{test_number}\n{conditions}\n{data_str}\n\n")
self.data_file.flush()
def collect_background(self):
# 设置平滑次数
boxcar_width = self.boxcar_width_slider.get()
wrapper.setBoxcarWidth(spectrometer_index, boxcar_width)
# 设置平均次数
scans_to_average = self.scans_to_average_slider.get()
wrapper.setScansToAverage(spectrometer_index, scans_to_average)
# 采集背景数据
wavelengths, self.background_spectrum = self.get_spectrum_data()
conditions = f"jifenshijian = {jifenshijian} shuaxinshijian = {shuaxinshijian} zihaodaxiao = {zihaodaxiao} pinghuacishu = {self.boxcar_width_slider.get()} pingjuncishu = {self.scans_to_average_slider.get()}"
self.background_count += 1
self.write_data_to_file("background", self.background_count, conditions, self.background_spectrum)
self.update_plot(wavelengths, self.background_spectrum)
def toggle_background_correction(self):
self.correct_background = not self.correct_background
self.correct_background_button.config(text="正在校正" if self.correct_background else "背景校正")
def apply_background_correction(self, spectrum):
if self.background_spectrum is not None and self.correct_background:
return [s - b for s, b in zip(spectrum, self.background_spectrum)]
return spectrum
def single_test(self):
# 获取输入框的值
jifenshijian = float(self.integration_time_entry.get())
shuaxinshijian = float(self.refresh_interval_entry.get())
# 设置平滑次数
boxcar_width = self.boxcar_width_slider.get()
wrapper.setBoxcarWidth(spectrometer_index, boxcar_width)
# 设置平均次数
scans_to_average = self.scans_to_average_slider.get()
wrapper.setScansToAverage(spectrometer_index, scans_to_average)
conditions = f"jifenshijian = {jifenshijian} shuaxinshijian = {shuaxinshijian} zihaodaxiao = {zihaodaxiao} pinghuacishu = {self.boxcar_width_slider.get()} pingjuncishu = {self.scans_to_average_slider.get()}"
self.test_count += 1
wavelengths, spectrum = self.get_spectrum_data()
# 在透射模式下计算透射率,否则应用背景校正
if self.transmission_mode and self.background_spectrum is not None and self.source_spectrum is not None:
transmission = self.calculate_transmission(spectrum)
self.update_plot(wavelengths, transmission, plot_type='transmission')
else:
corrected_spectrum = self.apply_background_correction(spectrum)
self.update_plot(wavelengths, corrected_spectrum, plot_type='spectrum')
def continuous_test(self):
while not self.stop_event.is_set():
# 获取输入框的值
jifenshijian = float(self.integration_time_entry.get())
shuaxinshijian = float(self.refresh_interval_entry.get())
# 设置平滑次数和平均次数
boxcar_width = self.boxcar_width_slider.get()
wrapper.setBoxcarWidth(spectrometer_index, boxcar_width)
scans_to_average = self.scans_to_average_slider.get()
wrapper.setScansToAverage(spectrometer_index, scans_to_average)
conditions = f"jifenshijian = {jifenshijian} shuaxinshijian = {shuaxinshijian} zihaodaxiao = {zihaodaxiao} pinghuacishu = {self.boxcar_width_slider.get()} pingjuncishu = {self.scans_to_average_slider.get()}"
self.test_count += 1
wavelengths, spectrum = self.get_spectrum_data()
self.write_data_to_file("test", self.test_count, conditions, spectrum)
# 根据当前模式计算并更新图表
if self.transmission_mode and self.background_spectrum is not None and self.source_spectrum is not None:
transmission = self.calculate_transmission(spectrum)
self.update_plot(wavelengths, transmission, plot_type='transmission')
else:
corrected_spectrum = self.apply_background_correction(spectrum)
self.update_plot(wavelengths, corrected_spectrum)
time.sleep(shuaxinshijian)
def start_continuous_test(self):
self.stop_event.clear() # 重置事件
self.continuous_thread = Thread(target=self.continuous_test)
self.continuous_thread.start()
def stop_continuous_test(self):
self.stop_event.set() # 设置事件通知线程停止
self.continuous_thread = None
def on_closing(self):
if self.data_file:
self.data_file.close()
if messagebox.askyesno("退出", "实验g了"):
self.stop_continuous_test()
self.root.destroy()
sys.exit()
if __name__ == "__main__":
# 使用 ThemedTk 而不是普通的 Tk
root = ThemedTk(theme="equilux") # 使用 'arc' 主题
# 由于我们已经使用了 ttkthemes 来设置主题,下面这些行可以省略
# style = ttk.Style()
# style.theme_use('arc')
app = App(root)
root.mainloop()

View File

View File

View File

@@ -0,0 +1,101 @@
import socket
import json
import time
from pydantic import BaseModel
class AgvNavigator:
def __init__(self, host):
self.control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.receive_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.control_socket.connect((host, 19206))
self.receive_socket.connect((host, 19204))
self.rec_cmd_code = {
"pose" : "03EC",
"status" : "03FC",
"nav" : "0BEB"
}
self.status_list = ['NONE', 'WAITING', 'RUNNING', 'SUSPENDED', 'COMPLETED', 'FAILED', 'CANCELED']
self._pose = []
self._status = 'NONE'
self.success = False
@property
def pose(self) -> list:
data = self.send('pose')
try:
self._pose = [data['x'], data['y'], data['angle']]
except:
print(data)
return self._pose
@property
def status(self) -> str:
data = self.send('status')
self._status = self.status_list[data['task_status']]
return self._status
def send(self, cmd, ex_data = '', obj = 'receive_socket'):
data = bytearray.fromhex(f"5A 01 00 01 00 00 00 00 {self.rec_cmd_code[cmd]} 00 00 00 00 00 00")
if ex_data:
data_ = ex_data
data[7] = len(data_)
data= data + bytearray(data_,"utf-8")
cmd_obj = getattr(self, obj)
cmd_obj.sendall(data)
response_data = b""
while True:
part = cmd_obj.recv(4096) # 每次接收 4096 字节
response_data += part
if len(part) < 4096: # 当接收到的数据少于缓冲区大小时,表示接收完毕
break
response_str = response_data.hex()
json_start = response_str.find('7b') # 找到JSON的开始位置
if json_start == -1:
raise "Error: No JSON data found in response."
json_data = bytes.fromhex(response_str[json_start:])
# 尝试解析 JSON 数据
try:
parsed_json = json.loads(json_data.decode('utf-8'))
return parsed_json
except json.JSONDecodeError as e:
raise f"JSON Decode Error: {e}"
def send_nav_task(self, command:str):
self.success = False
# source,target = cmd.replace(' ','').split("->")
target = json.loads(command)['target']
json_data = {}
# json_data["source_id"] = source
json_data["id"] = target
# json_data["use_down_pgv"] = True
result = self.send('nav', ex_data=json.dumps(json_data), obj="control_socket")
try:
if result['ret_code'] == 0:
# print(result)
while True:
if self.status == 'COMPLETED':
break
time.sleep(1)
self.success = True
except:
self.success = False
def __del__(self):
self.control_socket.close()
self.receive_socket.close()
if __name__ == "__main__":
agv = AgvNavigator("192.168.1.42")
# print(agv.pose)
agv.send_nav_task('LM14')

View File

@@ -0,0 +1,54 @@
{
"pretreatment": [
[
"moveL",
[0.443, -0.0435, 0.40, 1.209, 1.209, 1.209]
],
[
"moveL",
[0.543, -0.0435, 0.50, 0, 1.57, 0]
],
[
"moveL",
[0.443, -0.0435, 0.60, 0, 1.57, 0]
],
[
"wait",
[2]
],
[
"gripper",
[255,255,255]
],
[
"wait",
[2]
],
[
"gripper",
[0,0,0]
],
[
"set",
[0.3,0.2]
],
[
"moveL",
[0.643, -0.0435, 0.40, 1.209, 1.209, 1.209]
]
],
"pose": [
[
"moveL",
[0.243, -0.0435, 0.30, 0.0, 3.12, 0.0]
],
[
"moveL",
[0.443, -0.0435, 0.40, 0.0, 3.12, 0.0]
],
[
"moveL",
[0.243, -0.0435, 0.50, 0.0, 3.12, 0.0]
]
]
}

View File

@@ -0,0 +1,298 @@
"""Module to control Robotiq's grippers - tested with HAND-E"""
import socket
import threading
import time
from enum import Enum
from typing import Union, Tuple, OrderedDict
class RobotiqGripper:
"""
Communicates with the gripper directly, via socket with string commands, leveraging string names for variables.
"""
# WRITE VARIABLES (CAN ALSO READ)
ACT = 'ACT' # act : activate (1 while activated, can be reset to clear fault status)
GTO = 'GTO' # gto : go to (will perform go to with the actions set in pos, for, spe)
ATR = 'ATR' # atr : auto-release (emergency slow move)
ADR = 'ADR' # adr : auto-release direction (open(1) or close(0) during auto-release)
FOR = 'FOR' # for : force (0-255)
SPE = 'SPE' # spe : speed (0-255)
POS = 'POS' # pos : position (0-255), 0 = open
# READ VARIABLES
STA = 'STA' # status (0 = is reset, 1 = activating, 3 = active)
PRE = 'PRE' # position request (echo of last commanded position)
OBJ = 'OBJ' # object detection (0 = moving, 1 = outer grip, 2 = inner grip, 3 = no object at rest)
FLT = 'FLT' # fault (0=ok, see manual for errors if not zero)
ENCODING = 'UTF-8' # ASCII and UTF-8 both seem to work
class GripperStatus(Enum):
"""Gripper status reported by the gripper. The integer values have to match what the gripper sends."""
RESET = 0
ACTIVATING = 1
# UNUSED = 2 # This value is currently not used by the gripper firmware
ACTIVE = 3
class ObjectStatus(Enum):
"""Object status reported by the gripper. The integer values have to match what the gripper sends."""
MOVING = 0
STOPPED_OUTER_OBJECT = 1
STOPPED_INNER_OBJECT = 2
AT_DEST = 3
def __init__(self ,host):
"""Constructor."""
self.socket = None
self.command_lock = threading.Lock()
self._min_position = 0
self._max_position = 255
self._min_speed = 0
self._max_speed = 255
self._min_force = 0
self._max_force = 255
self.connect(host)
# self.activate()
def connect(self, hostname: str, port: int = 63352, socket_timeout: float = 2.0) -> None:
"""Connects to a gripper at the given address.
:param hostname: Hostname or ip.
:param port: Port.
:param socket_timeout: Timeout for blocking socket operations.
"""
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((hostname, port))
self.socket.settimeout(socket_timeout)
def disconnect(self) -> None:
"""Closes the connection with the gripper."""
self.socket.close()
def _set_vars(self, var_dict: OrderedDict[str, Union[int, float]]):
"""Sends the appropriate command via socket to set the value of n variables, and waits for its 'ack' response.
:param var_dict: Dictionary of variables to set (variable_name, value).
:return: True on successful reception of ack, false if no ack was received, indicating the set may not
have been effective.
"""
# construct unique command
cmd = "SET"
for variable, value in var_dict.items():
cmd += f" {variable} {str(value)}"
cmd += '\n' # new line is required for the command to finish
# atomic commands send/rcv
with self.command_lock:
self.socket.sendall(cmd.encode(self.ENCODING))
data = self.socket.recv(1024)
return self._is_ack(data)
def _set_var(self, variable: str, value: Union[int, float]):
"""Sends the appropriate command via socket to set the value of a variable, and waits for its 'ack' response.
:param variable: Variable to set.
:param value: Value to set for the variable.
:return: True on successful reception of ack, false if no ack was received, indicating the set may not
have been effective.
"""
return self._set_vars(OrderedDict([(variable, value)]))
def _get_var(self, variable: str):
"""Sends the appropriate command to retrieve the value of a variable from the gripper, blocking until the
response is received or the socket times out.
:param variable: Name of the variable to retrieve.
:return: Value of the variable as integer.
"""
# atomic commands send/rcv
with self.command_lock:
cmd = f"GET {variable}\n"
self.socket.sendall(cmd.encode(self.ENCODING))
data = self.socket.recv(1024)
# expect data of the form 'VAR x', where VAR is an echo of the variable name, and X the value
# note some special variables (like FLT) may send 2 bytes, instead of an integer. We assume integer here
var_name, value_str = data.decode(self.ENCODING).split()
if var_name != variable:
raise ValueError(f"Unexpected response {data} ({data.decode(self.ENCODING)}): does not match '{variable}'")
value = int(value_str)
return value
@staticmethod
def _is_ack(data: str):
return data == b'ack'
def _reset(self):
"""
Reset the gripper.
The following code is executed in the corresponding script function
def rq_reset(gripper_socket="1"):
rq_set_var("ACT", 0, gripper_socket)
rq_set_var("ATR", 0, gripper_socket)
while(not rq_get_var("ACT", 1, gripper_socket) == 0 or not rq_get_var("STA", 1, gripper_socket) == 0):
rq_set_var("ACT", 0, gripper_socket)
rq_set_var("ATR", 0, gripper_socket)
sync()
end
sleep(0.5)
end
"""
self._set_var(self.ACT, 0)
self._set_var(self.ATR, 0)
while (not self._get_var(self.ACT) == 0 or not self._get_var(self.STA) == 0):
self._set_var(self.ACT, 0)
self._set_var(self.ATR, 0)
time.sleep(0.5)
def activate(self, auto_calibrate: bool = True):
"""Resets the activation flag in the gripper, and sets it back to one, clearing previous fault flags.
:param auto_calibrate: Whether to calibrate the minimum and maximum positions based on actual motion.
The following code is executed in the corresponding script function
def rq_activate(gripper_socket="1"):
if (not rq_is_gripper_activated(gripper_socket)):
rq_reset(gripper_socket)
while(not rq_get_var("ACT", 1, gripper_socket) == 0 or not rq_get_var("STA", 1, gripper_socket) == 0):
rq_reset(gripper_socket)
sync()
end
rq_set_var("ACT",1, gripper_socket)
end
end
def rq_activate_and_wait(gripper_socket="1"):
if (not rq_is_gripper_activated(gripper_socket)):
rq_activate(gripper_socket)
sleep(1.0)
while(not rq_get_var("ACT", 1, gripper_socket) == 1 or not rq_get_var("STA", 1, gripper_socket) == 3):
sleep(0.1)
end
sleep(0.5)
end
end
"""
if not self.is_active():
self._reset()
while (not self._get_var(self.ACT) == 0 or not self._get_var(self.STA) == 0):
time.sleep(0.01)
self._set_var(self.ACT, 1)
time.sleep(1.0)
while (not self._get_var(self.ACT) == 1 or not self._get_var(self.STA) == 3):
time.sleep(0.01)
# auto-calibrate position range if desired
if auto_calibrate:
self.auto_calibrate()
def is_active(self):
"""Returns whether the gripper is active."""
status = self._get_var(self.STA)
return RobotiqGripper.GripperStatus(status) == RobotiqGripper.GripperStatus.ACTIVE
def get_min_position(self) -> int:
"""Returns the minimum position the gripper can reach (open position)."""
return self._min_position
def get_max_position(self) -> int:
"""Returns the maximum position the gripper can reach (closed position)."""
return self._max_position
def get_open_position(self) -> int:
"""Returns what is considered the open position for gripper (minimum position value)."""
return self.get_min_position()
def get_closed_position(self) -> int:
"""Returns what is considered the closed position for gripper (maximum position value)."""
return self.get_max_position()
def is_open(self):
"""Returns whether the current position is considered as being fully open."""
return self.get_current_position() <= self.get_open_position()
def is_closed(self):
"""Returns whether the current position is considered as being fully closed."""
return self.get_current_position() >= self.get_closed_position()
def get_current_position(self) -> int:
"""Returns the current position as returned by the physical hardware."""
return self._get_var(self.POS)
def auto_calibrate(self, log: bool = True) -> None:
"""Attempts to calibrate the open and closed positions, by slowly closing and opening the gripper.
:param log: Whether to print the results to log.
"""
# first try to open in case we are holding an object
(position, status) = self.move_and_wait_for_pos(self.get_open_position(), 64, 1)
if RobotiqGripper.ObjectStatus(status) != RobotiqGripper.ObjectStatus.AT_DEST:
raise RuntimeError(f"Calibration failed opening to start: {str(status)}")
# try to close as far as possible, and record the number
(position, status) = self.move_and_wait_for_pos(self.get_closed_position(), 64, 1)
if RobotiqGripper.ObjectStatus(status) != RobotiqGripper.ObjectStatus.AT_DEST:
raise RuntimeError(f"Calibration failed because of an object: {str(status)}")
assert position <= self._max_position
self._max_position = position
# try to open as far as possible, and record the number
(position, status) = self.move_and_wait_for_pos(self.get_open_position(), 64, 1)
if RobotiqGripper.ObjectStatus(status) != RobotiqGripper.ObjectStatus.AT_DEST:
raise RuntimeError(f"Calibration failed because of an object: {str(status)}")
assert position >= self._min_position
self._min_position = position
if log:
print(f"Gripper auto-calibrated to [{self.get_min_position()}, {self.get_max_position()}]")
def move(self, position: int, speed: int, force: int) -> Tuple[bool, int]:
"""Sends commands to start moving towards the given position, with the specified speed and force.
:param position: Position to move to [min_position, max_position]
:param speed: Speed to move at [min_speed, max_speed]
:param force: Force to use [min_force, max_force]
:return: A tuple with a bool indicating whether the action it was successfully sent, and an integer with
the actual position that was requested, after being adjusted to the min/max calibrated range.
"""
def clip_val(min_val, val, max_val):
return max(min_val, min(val, max_val))
clip_pos = clip_val(self._min_position, position, self._max_position)
clip_spe = clip_val(self._min_speed, speed, self._max_speed)
clip_for = clip_val(self._min_force, force, self._max_force)
# moves to the given position with the given speed and force
var_dict = OrderedDict([(self.POS, clip_pos), (self.SPE, clip_spe), (self.FOR, clip_for), (self.GTO, 1)])
return self._set_vars(var_dict), clip_pos
def move_and_wait_for_pos(self, position: int, speed: int, force: int) -> Tuple[int, ObjectStatus]: # noqa
"""Sends commands to start moving towards the given position, with the specified speed and force, and
then waits for the move to complete.
:param position: Position to move to [min_position, max_position]
:param speed: Speed to move at [min_speed, max_speed]
:param force: Force to use [min_force, max_force]
:return: A tuple with an integer representing the last position returned by the gripper after it notified
that the move had completed, a status indicating how the move ended (see ObjectStatus enum for details). Note
that it is possible that the position was not reached, if an object was detected during motion.
"""
set_ok, cmd_pos = self.move(position, speed, force)
if not set_ok:
raise RuntimeError("Failed to set variables for move.")
# wait until the gripper acknowledges that it will try to go to the requested position
while self._get_var(self.PRE) != cmd_pos:
time.sleep(0.001)
# wait until not moving
cur_obj = self._get_var(self.OBJ)
while RobotiqGripper.ObjectStatus(cur_obj) == RobotiqGripper.ObjectStatus.MOVING:
cur_obj = self._get_var(self.OBJ)
# report the actual position and the object status
final_pos = self._get_var(self.POS)
final_obj = cur_obj
return final_pos, RobotiqGripper.ObjectStatus(final_obj)
if __name__ == '__main__':
gripper = RobotiqGripper('192.168.1.178')
gripper.move(255,0,0)
print(gripper.move(255,0,0))

View File

@@ -0,0 +1,166 @@
import rtde_control
import dashboard_client
import time
import json
from unilabos.devices.agv.robotiq_gripper import RobotiqGripper
import rtde_receive
from std_msgs.msg import Float64MultiArray
from pydantic import BaseModel
class UrArmTask():
def __init__(self, host, retry=30):
self.init_flag = False
self.dash_c = None
n = 0
while self.dash_c is None:
try:
self.dash_c = dashboard_client.DashboardClient(host)
if not self.dash_c.isConnected():
self.dash_c.connect()
self.dash_c.loadURP('camera/250111_put_board.urp')
self.arm_init()
self.dash_c.running()
except Exception as e:
print(e)
self.dash_c = None
time.sleep(1)
n += 1
if n > retry:
raise Exception('Can not connect to the robot dashboard server!')
self.vel = 0.1
self.acc = 0.1
self.rtde_c = None
self.rtde_r = None
self.gripper = None
self._pose = [0.0,0.0,0.0,0.0,0.0,0.0]
self._gripper_pose = None
self._status = 'IDLE'
self._gripper_status = 'AT_DEST'
self.gripper_s_list = ['MOVING','STOPPED_OUTER_OBJECT','STOPPED_INNER_OBJECT','AT_DEST']
self.dash_c.loadURP('camera/250111_put_board.urp')
self.arm_init()
self.success = True
self.init_flag = True
n = 0
while self.gripper is None:
try:
self.gripper = RobotiqGripper(host)
self.gripper.activate()
# self._gripper_status = self.gripper_s_list[self.gripper._get_var('OBJ')]
except:
self.gripper = None
time.sleep(1)
n += 1
if n > retry:
raise Exception('Can not connect to the robot gripper server!')
n = 0
while self.rtde_r is None:
try:
self.rtde_r = rtde_receive.RTDEReceiveInterface(host)
if not self.rtde_r.isConnected():
self.rtde_r.reconnect()
self._pose = self.rtde_r.getActualTCPPose()
except Exception as e:
print(e)
self.rtde_r = None
time.sleep(1)
n += 1
if n > retry:
raise Exception('Can not connect to the arm info server!')
self.pose_data = {}
self.pose_file = 'C:\\auto\\unilabos\\unilabos\\devices\\agv\\pose.json'
self.reload_pose()
self.dash_c.stop()
def arm_init(self):
self.dash_c.powerOn()
self.dash_c.brakeRelease()
self.dash_c.unlockProtectiveStop()
running = self.dash_c.running()
while running:
running = self.dash_c.running()
time.sleep(1)
# def __del__(self):
# self.dash_c.disconnect()
# self.rtde_c.disconnect()
# self.rtde_r.disconnect()
# self.gripper.disconnect()
def load_pose_file(self,file):
self.pose_file = file
self.reload_pose()
def reload_pose(self):
self.pose_data = json.load(open(self.pose_file))
def load_pose_data(self,data):
self.pose_data = json.loads(data)
@property
def arm_pose(self) -> list:
try:
if not self.rtde_r.isConnected():
self.rtde_r.reconnect()
print('_'*30,'Reconnect to the arm info server!')
self._pose = self.rtde_r.getActualTCPPose()
# print(self._pose)
except Exception as e:
self._pose = self._pose
print('-'*20,'zhixing_arm\n',e)
return self._pose
@property
def gripper_pose(self) -> float:
if self.init_flag:
try:
self._gripper_status = self.gripper_s_list[self.gripper._get_var('OBJ')]
self._gripper_pose = self.gripper.get_current_position()
except Exception as e:
self._gripper_status = self._gripper_status
self._gripper_pose = self._gripper_pose
print('-'*20,'zhixing_gripper\n',e)
return self._gripper_pose
@property
def arm_status(self) -> str:
return self._status
@property
def gripper_status(self) -> str:
if self.init_flag:
return self._gripper_status
def move_pos_task(self,command):
self.success = False
task_name = json.loads(command)['task_name']
self.dash_c.loadURP(task_name)
self.dash_c.play()
time.sleep(0.5)
self._status = 'RUNNING'
while self._status == 'RUNNING':
running = self.dash_c.running()
if not running:
self._status = 'IDLE'
time.sleep(1)
self.success = True
if __name__ == "__main__":
arm = UrArmTask("192.168.1.178")
# arm.move_pos_task('t2_y4_transfer3.urp')
# print(arm.arm_pose())

View File

View File

@@ -0,0 +1,265 @@
import os
import asyncio
from asyncio import Event, Future, Lock, Task
from enum import Enum
from dataclasses import dataclass
import re
import time
from typing import Any, Union, Optional, overload
import serial.tools.list_ports
from serial import Serial
from serial.serialutil import SerialException
from unilabos.messages import Point3D
class GrblCNCConnectionError(Exception):
pass
@dataclass(frozen=True, kw_only=True)
class GrblCNCInfo:
port: str
address: str = "1"
limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, 0, 60)
def create(self):
return GrblCNCAsync(self.port, self.address, self.limits)
class GrblCNCAsync:
_status: str = "Offline"
_position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
def __init__(self, port: str, address: str = "1", limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, 0, 60)):
self.port = port
self.address = address
self.limits = limits
try:
self._serial = Serial(
baudrate=115200,
port=port
)
except (OSError, SerialException) as e:
raise GrblCNCConnectionError from e
self._busy = False
self._closing = False
self._pose_number = self.pose_number_remaining = -1
self._error_event = Event()
self._query_future = Future[Any]()
self._query_lock = Lock()
self._read_task: Optional[Task[None]] = None
self._read_extra_line = False
self._run_future: Optional[Future[Any]] = None
self._run_lock = Lock()
def _read_all(self):
data = self._serial.read_until(b"\n")
data_decoded = data.decode()
while not "ok" in data_decoded and not "Grbl" in data_decoded:
data += self._serial.read_until(b"\n")
data_decoded = data.decode()
return data
async def _read_loop(self):
try:
while True:
self._receive((await asyncio.to_thread(lambda: self._read_all())))
except SerialException as e:
raise GrblCNCConnectionError from e
finally:
if not self._closing:
self._error_event.set()
if self._query_future and not self._query_future.done():
self._query_future.set_exception(GrblCNCConnectionError())
if self._run_future and not self._run_future.done():
self._run_future.set_exception(GrblCNCConnectionError())
@overload
async def _query(self, command: str, dtype: type[bool]) -> bool:
pass
@overload
async def _query(self, command: str, dtype: type[int]) -> int:
pass
@overload
async def _query(self, command: str, dtype = None) -> str:
pass
async def _query(self, command: str, dtype: Optional[type] = None):
async with self._query_lock:
if self._closing or self._error_event.is_set():
raise GrblCNCConnectionError
self._query_future = Future[Any]()
self._read_extra_line = command.startswith("?")
run = ''
full_command = f"{command}{run}\n"
full_command_data = bytearray(full_command, 'ascii')
try:
# await asyncio.to_thread(lambda: self._serial.write(full_command_data))
self._serial.write(full_command_data)
return self._parse(await asyncio.wait_for(asyncio.shield(self._query_future), timeout=5.0), dtype=dtype)
except (SerialException, asyncio.TimeoutError) as e:
self._error_event.set()
raise GrblCNCConnectionError from e
finally:
self._query_future = None
def _parse(self, data: bytes, dtype: Optional[type] = None):
response = data.decode()
if dtype == bool:
return response == "1"
elif dtype == int:
return int(response)
else:
return response
def _receive(self, data: bytes):
ascii_string = "".join(chr(byte) for byte in data)
was_busy = self._busy
self._busy = "Idle" not in ascii_string
# if self._read_extra_line and ascii_string.startswith("ok"):
# self._read_extra_line = False
# return
if self._run_future and was_busy and not self._busy:
self._run_future.set_result(data)
if self._query_future:
self._query_future.set_result(data)
else:
raise Exception("Dropping data")
async def _run(self, command: str):
async with self._run_lock:
self._run_future = Future[Any]()
# self._busy = True
try:
await self._query(command)
while True:
await asyncio.sleep(0.2) # Wait for 0.5 seconds before polling again
status = await self.get_status()
if "Idle" in status:
break
await asyncio.shield(self._run_future)
finally:
self._run_future = None
async def initialize(self):
time.sleep(0.5)
await self._run("G0X0Y0Z0")
status = await self.get_status()
return status
# Operations
# Status Queries
@property
def status(self) -> str:
return self._status
async def get_status(self):
__pos_pattern__ = re.compile('.Pos:(\-?\d+\.\d+),(\-?\d+\.\d+),(\-?\d+\.\d+)')
__status_pattern__ = re.compile('<([a-zA-Z]+),')
response = await self._query("?")
pat = re.search(__pos_pattern__, response)
if pat is not None:
pos = pat.group().split(":")[1].split(",")
self._status = re.search(__status_pattern__, response).group(1).lstrip("<").rstrip(",")
self._position = Point3D(x=float(pos[0]), y=float(pos[1]), z=float(pos[2]))
return self.status
# Position Setpoint and Queries
@property
def position(self) -> Point3D:
# 由于此时一定调用过 get_status所以 position 一定是被更新过的
return self._position
def get_position(self):
return self.position
async def set_position(self, position: Point3D):
"""
Move to absolute position (unit: mm)
Args:
x, y, z: float
Returns:
None
"""
x = max(self.limits[0], min(self.limits[1], position.x))
y = max(self.limits[2], min(self.limits[3], position.y))
z = max(self.limits[4], min(self.limits[5], position.z))
return await self._run(f"G0X{x:.3f}Y{y:.3f}Z{z:.3f}")
async def move_through_points(self, points: list[Point3D]):
for i, point in enumerate(points):
self._pose_number = i
self.pose_number_remaining = len(points) - i
await self.set_position(point)
await asyncio.sleep(0.5)
self._step_number = -1
async def stop_operation(self):
return await self._run("T")
# Queries
async def wait_error(self):
await self._error_event.wait()
async def __aenter__(self):
await self.open()
return self
async def __aexit__(self, exc_type, exc, tb):
await self.close()
async def open(self):
if self._read_task:
raise GrblCNCConnectionError
self._read_task = asyncio.create_task(self._read_loop())
try:
await self.get_status()
except Exception:
await self.close()
raise
async def close(self):
if self._closing or not self._read_task:
raise GrblCNCConnectionError
self._closing = True
self._read_task.cancel()
try:
await self._read_task
except asyncio.CancelledError:
pass
finally:
del self._read_task
self._serial.close()
@staticmethod
def list():
for item in serial.tools.list_ports.comports():
yield GrblCNCInfo(port=item.device)

View File

@@ -0,0 +1,205 @@
import os
import asyncio
from threading import Event, Lock
from enum import Enum
from dataclasses import dataclass
import re
import time
from typing import Any, Union, Optional, overload
import serial.tools.list_ports
from serial import Serial
from serial.serialutil import SerialException
from unilabos.messages import Point3D
class GrblCNCConnectionError(Exception):
pass
@dataclass(frozen=True, kw_only=True)
class GrblCNCInfo:
port: str
address: str = "1"
limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, -80, 0)
def create(self):
return GrblCNC(self.port, self.address, self.limits)
class GrblCNC:
_status: str = "Offline"
_position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
_spindle_speed: float = 0.0
def __init__(self, port: str, address: str = "1", limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, -80, 0)):
self.port = port
self.address = address
self.limits = limits
try:
self._serial = Serial(
baudrate=115200,
port=port
)
except (OSError, SerialException) as e:
raise GrblCNCConnectionError from e
self._busy = False
self._closing = False
self._pose_number = self.pose_number_remaining = -1
self._query_lock = Lock()
self._run_lock = Lock()
self._error_event = Event()
def _read_all(self):
data = self._serial.read_until(b"\n")
data_decoded = data.decode()
while not "ok" in data_decoded and not "Grbl" in data_decoded:
data += self._serial.read_until(b"\n")
data_decoded = data.decode()
return data
@overload
def _query(self, command: str, dtype: type[bool]) -> bool:
pass
@overload
def _query(self, command: str, dtype: type[int]) -> int:
pass
@overload
def _query(self, command: str, dtype = None) -> str:
pass
def _query(self, command: str, dtype: Optional[type] = None):
with self._query_lock:
if self._closing or self._error_event.is_set():
raise GrblCNCConnectionError
self._read_extra_line = command.startswith("?")
run = ''
full_command = f"{command}{run}\n"
full_command_data = bytearray(full_command, 'ascii')
try:
# await asyncio.to_thread(lambda: self._serial.write(full_command_data))
self._serial.write(full_command_data)
time.sleep(0.1)
return self._receive(self._read_all())
except (SerialException, asyncio.TimeoutError) as e:
self._error_event.set()
raise GrblCNCConnectionError from e
def _receive(self, data: bytes):
ascii_string = "".join(chr(byte) for byte in data)
was_busy = self._busy
self._busy = "Idle" not in ascii_string
return ascii_string
def _run(self, command: str):
with self._run_lock:
try:
self._query(command)
while True:
time.sleep(0.2) # Wait for 0.5 seconds before polling again
status = self.get_status()
if "Idle" in status:
break
except:
self._error_event.set()
def initialize(self):
time.sleep(0.5)
self._run("G0X0Y0Z0")
status = self.get_status()
return status
# Operations
# Status Queries
@property
def status(self) -> str:
return self._status
def get_status(self):
__pos_pattern__ = re.compile('.Pos:(\-?\d+\.\d+),(\-?\d+\.\d+),(\-?\d+\.\d+)')
__status_pattern__ = re.compile('<([a-zA-Z]+),')
response = self._query("?")
pat = re.search(__pos_pattern__, response)
if pat is not None:
pos = pat.group().split(":")[1].split(",")
self._status = re.search(__status_pattern__, response).group(1).lstrip("<").rstrip(",")
self._position = Point3D(x=float(pos[0]), y=float(pos[1]), z=float(pos[2]))
return self.status
# Position Setpoint and Queries
@property
def position(self) -> Point3D:
# 由于此时一定调用过 get_status所以 position 一定是被更新过的
return self._position
def get_position(self):
return self.position
def set_position(self, position: Point3D):
"""
Move to absolute position (unit: mm)
Args:
x, y, z: float
Returns:
None
"""
x = max(self.limits[0], min(self.limits[1], position.x))
y = max(self.limits[2], min(self.limits[3], position.y))
z = max(self.limits[4], min(self.limits[5], position.z))
return self._run(f"G0X{x:.3f}Y{y:.3f}Z{z:.3f}")
def move_through_points(self, positions: list[Point3D]):
for i, point in enumerate(positions):
self._pose_number = i
self.pose_number_remaining = len(positions) - i
self.set_position(point)
time.sleep(0.5)
self._pose_number = -1
@property
def spindle_speed(self) -> float:
return self._spindle_speed
# def get_spindle_speed(self):
# self._spindle_speed = float(self._query("M3?"))
# return self.spindle_speed
def set_spindle_speed(self, spindle_speed: float, max_velocity: float = 500):
if spindle_speed < 0:
spindle_speed = 0
self._run("M5")
else:
spindle_speed = min(max_velocity, spindle_speed)
self._run(f"M3 S{spindle_speed}")
self._spindle_speed = spindle_speed
def stop_operation(self):
return self._run("T")
# Queries
async def wait_error(self):
await self._error_event.wait()
@staticmethod
def list():
for item in serial.tools.list_ports.comports():
yield GrblCNCInfo(port=item.device)

View File

@@ -0,0 +1,42 @@
import time
import asyncio
from pydantic import BaseModel
class Point3D(BaseModel):
x: float
y: float
z: float
def d(a: Point3D, b: Point3D) -> float:
return ((a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2) ** 0.5
class MockCNCAsync:
def __init__(self):
self._position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
self._status = "Idle"
@property
def position(self) -> Point3D:
return self._position
async def get_position(self):
return self.position
@property
def status(self) -> str:
return self._status
async def set_position(self, position: Point3D, velocity: float = 10.0):
self._status = "Running"
current_pos = self.position
move_time = d(position, current_pos) / velocity
for i in range(20):
self._position.x = current_pos.x + (position.x - current_pos.x) / 20 * (i+1)
self._position.y = current_pos.y + (position.y - current_pos.y) / 20 * (i+1)
self._position.z = current_pos.z + (position.z - current_pos.z) / 20 * (i+1)
await asyncio.sleep(move_time / 20)
self._status = "Idle"

View File

View File

@@ -0,0 +1,46 @@
import time
class MockGripper:
def __init__(self):
self._position: float = 0.0
self._velocity: float = 2.0
self._torque: float = 0.0
self._status = "Idle"
@property
def position(self) -> float:
return self._position
@property
def velocity(self) -> float:
return self._velocity
@property
def torque(self) -> float:
return self._torque
@property
def status(self) -> str:
return self._status
def push_to(self, position: float, torque: float, velocity: float = 0.0):
self._status = "Running"
current_pos = self.position
if velocity == 0.0:
velocity = self.velocity
move_time = abs(position - current_pos) / velocity
for i in range(20):
self._position = current_pos + (position - current_pos) / 20 * (i+1)
self._torque = torque / (20 - i)
self._velocity = velocity
time.sleep(move_time / 20)
self._torque = torque
self._status = "Idle"
def edit_id(self, wf_name: str = "gripper_run", params: str = "{}", resource: dict = {"Gripper1": {}}):
v = list(resource.values())[0]
v["sample_id"] = "EDITED"
time.sleep(10)
return resource

View File

@@ -0,0 +1,408 @@
import time
from asyncio import Event
from enum import Enum, auto
from dataclasses import dataclass
from typing import Dict, Tuple
from pymodbus.client import ModbusSerialClient as ModbusClient
from pymodbus.client import ModbusTcpClient as ModbusTcpClient
class CommandType(Enum):
COMMAND_NONE = 0
COMMAND_GO_HOME = 1
COMMAND_DELAY = 2
COMMAND_MOVE_ABSOLUTE = 3
COMMAND_PUSH = 4
COMMAND_MOVE_RELATIVE = 5
COMMAND_PRECISE_PUSH = 6
class ParamType(Enum):
BOOLEAN = 0
INT32 = 1
FLOAT = 2
ENUM = 3
class ParamEdit(Enum):
NORMAL = 0
READONLY = 1
@dataclass
class Param:
type: ParamType
editability: ParamEdit
address: int
# 用于存储参数的字典类型
ParamsDict = Dict[str, Param]
# Constants and other required elements can be defined as needed
IO_GAP_TIME = 0.01
EXECUTE_COMMAND_INDEX = 15 # Example index
COMMAND_REACH_SIGNAL = "reach_15"
# Define other constants or configurations as needed
def REVERSE(x):
return ((x << 16) & 0xFFFF0000) | ((x >> 16) & 0x0000FFFF)
def int32_to_uint16_list(int32_list):
uint16_list = []
for num in int32_list:
lower_half = num & 0xFFFF
upper_half = (num >> 16) & 0xFFFF
uint16_list.extend([upper_half, lower_half])
return uint16_list
def uint16_list_to_int32_list(uint16_list):
if len(uint16_list) % 2 != 0:
raise ValueError("Input list must have even number of uint16 elements.")
int32_list = []
for i in range(0, len(uint16_list), 2):
# Combine two uint16 values into one int32 value
high = uint16_list[i + 1]
low = uint16_list[i]
# Assuming the uint16_list is in big-endian order
int32_value = (high << 16) | low
int32_list.append(int(int32_value))
return int32_list
class RMAxis:
modbus_device = {}
def __init__(self, port, is_modbus_rtu, baudrate: int = 115200, address: str = "", slave_id: int = 1):
self.device = port
self.is_modbus_rtu = is_modbus_rtu
if is_modbus_rtu:
self.client = ModbusClient(port=port, baudrate=baudrate, parity='N', stopbits=1, bytesize=8, timeout=3)
else:
self.client = ModbusTcpClient(address, port)
if not self.client.connect():
raise Exception(f"Modbus Connection failed")
self.slave_id = slave_id
self._error_event = Event()
self.device_params = {} # Assuming some initialization for parameters
self.command_edited = {}
self.init_parameters(self.device_params)
def init_parameters(self, params):
params["current_command_position"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 4902)
params["current_command_velocity"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 4904)
params["current_command_acceleration"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 4906)
params["current_command_deacceleration"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 4908)
params["current_position"] = Param(ParamType.FLOAT, ParamEdit.READONLY, 0)
params["current_velocity"] = Param(ParamType.FLOAT, ParamEdit.READONLY, 2)
params["control_torque"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 4)
params["error"] = Param(ParamType.INT32, ParamEdit.READONLY, 6)
params["current_force_sensor"] = Param(ParamType.FLOAT, ParamEdit.READONLY, 18)
params["io_in_go_home"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1401)
params["io_in_error_reset"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1402)
params["io_in_start"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1403)
params["io_in_servo"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1404)
params["io_in_stop"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1405)
params["io_in_force_reset"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1424)
params["io_in_save_parameters"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1440)
params["io_in_load_parameters"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1441)
params["io_in_save_positions"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1442)
params["io_in_load_positions"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1443)
params["io_out_gone_home"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1501)
params["command_address"] = Param(ParamType.INT32, ParamEdit.NORMAL, 5000)
params["selected_command_index"] = Param(ParamType.INT32, ParamEdit.NORMAL, 4001)
params["io_out_reach_15"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1521)
params["io_out_moving"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1505)
params["limit_pos"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 74)
params["limit_neg"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 72)
params["hardware_direct_output"] = Param(ParamType.INT32, ParamEdit.NORMAL, 158)
params["hardware_direct_input"] = Param(ParamType.INT32, ParamEdit.NORMAL, 160)
def get_version(self):
version_major = self.client.read_input_registers(8, 1, unit=self.slave_id).registers[0]
version_minor = self.client.read_input_registers(10, 1, unit=self.slave_id).registers[0]
version_build = self.client.read_input_registers(12, 1, unit=self.slave_id).registers[0]
version_type = self.client.read_input_registers(14, 1, unit=self.slave_id).registers[0]
return (version_major, version_minor, version_build, version_type)
def set_input_signal(self, signal, level):
param_name = f"io_in_{signal}"
self.set_parameter(param_name, level)
def get_output_signal(self, signal):
param_name = f"io_out_{signal}"
return self.get_device_parameter(param_name)
def config_motion(self, velocity, acceleration, deacceleration):
self.set_parameter("current_command_velocity", velocity)
self.set_parameter("current_command_acceleration", acceleration)
self.set_parameter("current_command_deacceleration", deacceleration)
def move_to(self, position):
self.set_parameter("current_command_position", position)
def go_home(self):
self.set_input_signal("go_home", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("go_home", True)
def move_absolute(self, position, velocity, acceleration, deacceleration, band):
command = {
'type': CommandType.COMMAND_MOVE_ABSOLUTE.value,
'position': position,
'velocity': velocity,
'acceleration': acceleration,
'deacceleration': deacceleration,
'band': band,
'push_force': 0,
'push_distance': 0,
'delay': 0,
'next_command_index': -1
}
self.execute_command(command)
def move_relative(self, position, velocity, acceleration, deacceleration, band):
command = {
'type': CommandType.COMMAND_MOVE_RELATIVE.value,
'position': position,
'velocity': velocity,
'acceleration': acceleration,
'deacceleration': deacceleration,
'band': band,
'push_force': 0,
'push_distance': 0,
'delay': 0,
'next_command_index': -1
}
self.execute_command(command)
def push(self, force, distance, velocity):
command = {
'type': CommandType.COMMAND_PUSH.value,
'position': 0,
'velocity': velocity,
'acceleration': 0,
'deacceleration': 0,
'band': 0,
'push_force': force,
'push_distance': distance,
'delay': 0,
'next_command_index': -1
}
self.execute_command(command)
def precise_push(self, force, distance, velocity, force_band, force_check_time):
command = {
'type': CommandType.COMMAND_PRECISE_PUSH.value,
'position': 0,
'velocity': velocity,
'acceleration': 0,
'deacceleration': 0,
'band': force_band,
'push_force': force,
'push_distance': distance,
'delay': force_check_time,
'next_command_index': -1
}
self.execute_command(command)
def is_moving(self):
return self.get_output_signal("moving")
def is_reached(self):
return self.get_output_signal(COMMAND_REACH_SIGNAL)
def is_push_empty(self):
return not self.is_moving()
def set_command(self, index, command):
print("Setting command", command)
self.command_edited[index] = True
command_buffer = [
command['type'],
int(command['position'] * 1000),
int(command['velocity'] * 1000),
int(command['acceleration'] * 1000),
int(command['deacceleration'] * 1000),
int(command['band'] * 1000),
int(command['push_force'] * 1000),
int(command['push_distance'] * 1000),
int(command['delay']),
int(command['next_command_index'])
]
buffer = int32_to_uint16_list(command_buffer)
response = self.client.write_registers(self.device_params["command_address"].address + index * 20, buffer, self.slave_id)
def get_command(self, index):
response = self.client.read_holding_registers(self.device_params["command_address"].address + index * 20, 20, self.slave_id)
print(response)
buffer = response.registers
command_buffer = uint16_list_to_int32_list(buffer)
command = {
'type': command_buffer[0],
'position': command_buffer[1] / 1000.0,
'velocity': command_buffer[2] / 1000.0,
'acceleration': command_buffer[3] / 1000.0,
'deacceleration': command_buffer[4] / 1000.0,
'band': command_buffer[5] / 1000.0,
'push_force': command_buffer[6] / 1000.0,
'push_distance': command_buffer[7] / 1000.0,
'delay': command_buffer[8],
'next_command_index': command_buffer[9]
}
return command
def execute_command(self, command):
self.set_command(EXECUTE_COMMAND_INDEX, command)
self.save_commands()
self.trig_command(EXECUTE_COMMAND_INDEX)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
def trig_command(self, index):
print("Triggering command", index)
self.set_parameter("selected_command_index", index)
self.set_input_signal("start", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("start", True)
def load_commands(self):
self.set_input_signal("load_positions", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("load_positions", True)
def save_commands(self):
for index, edited in self.command_edited.items():
if edited:
self.set_parameter("selected_command_index", index)
self.set_input_signal("save_positions", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("save_positions", True)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.command_edited[index] = False
@property
def position(self) -> float:
return self.get_device_parameter("current_position")
def get_position(self) -> float:
return self.get_device_parameter("current_position")
@property
def velocity(self) -> float:
return self.get_device_parameter("current_velocity")
@property
def torque(self) -> float:
return self.get_device_parameter("control_torque")
@property
def force_sensor(self) -> float:
return self.get_device_parameter("current_force_sensor")
def error_code(self):
return self.get_device_parameter("error")
def get_device_parameter(self, name):
# Assuming self.device_params is a dictionary with address and type
param = self.device_params.get(name)
if not param:
self._error_event.set()
raise Exception(f"parameter {name} does not exist")
address = param.address
if param.editability == ParamEdit.READONLY:
if param.type == ParamType.BOOLEAN:
return self.client.read_input_discretes(address, 1).bits[0]
elif param.type == ParamType.ENUM:
return self.client.read_input_registers(address, 1).registers[0]
elif param.type == ParamType.INT32:
return self.client.read_input_registers(address, 2).registers[0] # Handle as needed
elif param.type == ParamType.FLOAT:
return self.client.read_input_registers(address, 2).registers[0] # Handle as needed
else:
self._error_event.set()
raise Exception(f"parameter {name} has unknown data type {param.type}")
else:
if param.type == ParamType.BOOLEAN:
return self.client.read_holding_registers(address, 1).registers[0]
elif param.type == ParamType.ENUM:
return self.client.read_holding_registers(address, 1).registers[0]
elif param.type == ParamType.INT32:
return self.client.read_holding_registers(address, 2).registers[0] # Handle as needed
elif param.type == ParamType.FLOAT:
return self.client.read_holding_registers(address, 2).registers[0] # Handle as needed
else:
self._error_event.set()
raise Exception(f"parameter {name} has unknown data type {param['type']}")
def set_parameter(self, name, value):
param = self.device_params.get(name)
if not param:
self._error_event.set()
raise Exception(f"parameter {name} does not exist")
address = param.address
if param.editability == ParamEdit.READONLY:
raise Exception(f"parameter {name} is read only")
else:
if param.type == ParamType.BOOLEAN:
self.client.write_coil(address, bool(value))
elif param.type == ParamType.ENUM:
self.client.write_register(address, value)
elif param.type == ParamType.INT32:
self.client.write_register(address, int(value))
elif param.type == ParamType.FLOAT:
self.client.write_register(address, float(value))
else:
self._error_event.set()
raise Exception(f"parameter {name} has unknown data type {param['type']}")
def load_parameters(self):
self.set_input_signal("load_parameters", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("load_parameters", True)
def save_parameters(self):
self.set_input_signal("save_parameters", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("save_parameters", True)
def reset_error(self):
self.set_input_signal("error_reset", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("error_reset", True)
def set_servo_on_off(self, on_off):
self.set_input_signal("servo", on_off)
def stop(self):
self.set_input_signal("stop", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("stop", True)
def soft_reset(self):
self.client.write_register(186, 0x22205682)
async def wait_error(self):
await self._error_event.wait()
def close(self):
self.client.close()
del self.client
if __name__ == "__main__":
# gripper = RMAxis.create_rmaxis_modbus_rtu("COM7", 115200, 1)
gripper = RMAxis.create_rmaxis_modbus_rtu('/dev/tty.usbserial-B002YGXY', 115200, 0)
gripper.go_home()
# gripper.move_to(20)
# print("Moving abs...")
# gripper.move_absolute(20, 5, 100, 100, 0.1)
# print(gripper.get_command(EXECUTE_COMMAND_INDEX))
# gripper.go_home()
# print("Pushing...")
# gripper.push(0.7, 10, 20)

View File

@@ -0,0 +1,197 @@
import json
import serial
import time as systime
class HeaterStirrer_DaLong:
def __init__(self, port: str = 'COM6', temp_warning = 50.0, baudrate: int = 9600):
try:
self.serial = serial.Serial(port, baudrate, timeout=2)
except serial.SerialException as e:
print("串口错误", f"无法打开串口{port}: {e}")
self._status = "Idle"
self._stir_speed = 0.0
self._temp = 0.0
self._temp_warning = temp_warning
self.set_temp_warning(temp_warning)
self._temp_target = 20.0
self.success = False
@property
def status(self) -> str:
return self._status
def get_status(self) -> str:
self._status = "Idle" if self.stir_speed == 0 else "Running"
@property
def stir_speed(self) -> float:
return self._stir_speed
def set_stir_speed(self, speed: float):
try:
# 转换速度为整数
speed_int = int(speed)
# 确保速度在允许的范围内
if speed_int < 0 or speed_int > 65535:
raise ValueError("速度必须在0到65535之间")
except ValueError as e:
print("输入错误", str(e))
return
# 计算高位和低位
speed_high = speed_int >> 8
speed_low = speed_int & 0xFF
# 构建搅拌控制指令
command = bytearray([0xfe, 0xB1, speed_high, speed_low, 0x00])
# 计算校验和
command.append(sum(command[1:]) % 256)
# 发送指令
self.serial.write(command)
# 检查响应
response = self.serial.read(6)
if len(response) == 6 and response[0] == 0xfd and response[1] == 0xB1 and response[2] == 0x00:
print("成功", "搅拌速度更新成功")
self._stir_speed = speed
else:
print("失败", "搅拌速度更新失败")
def heatchill(
self,
vessel: str,
temp: float,
time: float = 3600,
stir: bool = True,
stir_speed: float = 300,
purpose: str = "reaction"
):
self.set_temp_target(temp)
if stir:
self.set_stir_speed(stir_speed)
self.status = "Stirring"
systime.sleep(time)
self.set_stir_speed(0)
self.status = "Idle"
@property
def temp(self) -> float:
self._temp = self.get_temp()
return self._temp
def get_temp(self):
# 构建读取温度的指令
command = bytearray([0xfe, 0xA2, 0x00, 0x00, 0x00])
command.append(sum(command[1:]) % 256)
# 发送指令
self.serial.write(command)
# 读取响应
systime.sleep(0.1)
num_bytes = self.serial.in_waiting
response = self.serial.read(num_bytes)
try:
high_value = response[8]
low_value = response[9]
raw_temp = (high_value << 8) + low_value
if raw_temp & 0x8000: # 如果低位寄存器最高位为1表示负值
raw_temp -= 0x10000 # 转换为正确的负数表示
temp = raw_temp / 10
return temp
except:
return None
@property
def temp_warning(self) -> float:
return self._temp_warning
def set_temp_warning(self, temp):
self.success = False
# temp = round(float(warning_temp), 1)
if self.set_temp_inner(float(temp), "warning"):
self._temp_warning = round(float(temp), 1)
self.success = True
@property
def temp_target(self) -> float:
return self._temp_target
def set_temp_target(self, temp):
self.success = False
# temp = round(float(target_temp), 1)
if self.set_temp_inner(float(temp), "target"):
self._temp_target = round(float(temp), 1)
self.success = True
def set_temp_inner(self, temp: float, type: str = "warning"):
try:
# 转换为整数
temp_int = int(temp*10)
except ValueError as e:
print("输入错误", str(e))
return
# 计算高位和低位
temp_high = temp_int >> 8
temp_low = temp_int & 0xFF
# 构建控制指令
if type == "warning":
command = bytearray([0xfe, 0xB4, temp_high, temp_low, 0x00])
elif type == "target":
command = bytearray([0xfe, 0xB2, temp_high, temp_low, 0x00])
else:
return False
# 计算校验和
command.append(sum(command[1:]) % 256)
print(command)
# 发送指令
self.serial.write(command)
# 检查响应
systime.sleep(0.1)
response = self.serial.read(6)
print(response)
if len(response) == 6 and response[0] == 0xfd and response[1] == 0xB4 and response[2] == 0x00:
print("成功", "安全温度设置成功")
return True
else:
print("失败", "安全温度设置失败")
return False
def close(self):
self.serial.close()
if __name__ == "__main__":
import tkinter as tk
from tkinter import messagebox
heaterstirrer = HeaterStirrer_DaLong()
# heaterstirrer.set_mix_speed(0)
heaterstirrer.get_temp()
# heaterstirrer.set_warning(17)
print(heaterstirrer.temp)
print(heaterstirrer.temp_warning)
# 创建主窗口
# root = tk.Tk()
# root.title("搅拌速度控制")
# # 创建速度变量
# speed_var = tk.StringVar()
# # 创建输入框
# speed_entry = tk.Entry(root, textvariable=speed_var)
# speed_entry.pack(pady=10)
# # 创建按钮
# set_speed_button = tk.Button(root, text="确定", command=heaterstirrer.set_mix_speed)
# # set_speed_button = tk.Button(root, text="确定", command=heaterstirrer.read_temp)
# set_speed_button.pack(pady=5)
# # 运行主事件循环
# root.mainloop()
# 关闭串口
heaterstirrer.serial.close()

View File

@@ -0,0 +1,470 @@
import traceback
from datetime import datetime
import os
import re
from typing import TypedDict
import pyautogui
from pywinauto import Application
from pywinauto.application import WindowSpecification
from pywinauto.controls.uiawrapper import UIAWrapper
from pywinauto.uia_element_info import UIAElementInfo
from unilabos.app.oss_upload import oss_upload
from unilabos.device_comms import universal_driver as ud
from unilabos.device_comms.universal_driver import UniversalDriver
class DeviceStatusInfo(TypedDict):
name: str
name_obj: UIAWrapper
status: str
status_obj: UIAWrapper
open_btn: UIAWrapper
close_btn: UIAWrapper
sub_item: UIAWrapper
class DeviceStatus(TypedDict):
VWD: DeviceStatusInfo
JinYangQi: DeviceStatusInfo
Beng: DeviceStatusInfo
ShouJiQi: DeviceStatusInfo
class HPLCDriver(UniversalDriver):
# 设备状态
_device_status: DeviceStatus = None
_is_running: bool = False
_success: bool = False
_finished: int = None
_total_sample_number: int = None
_status_text: str = ""
# 外部传入
_wf_name: str = ""
# 暂时用不到用来支持action name
gantt: str = ""
status: str = ""
@property
def status_text(self) -> str:
return self._status_text
@property
def device_status(self) -> str:
return f", ".join([f"{k}:{v.get('status')}" for k, v in self._device_status.items()])
@property
def could_run(self) -> bool:
return self.driver_init_ok and all([v.get('status') == "空闲" for v in self._device_status.values()])
@property
def driver_init_ok(self) -> bool:
for k, v in self._device_status.items():
if v.get("open_btn") is None:
return False
if v.get("close_btn") is None:
return False
return len(self._device_status) == 4
@property
def is_running(self) -> bool:
return self._is_running
@property
def success(self) -> bool:
return self._success
@property
def finish_status(self) -> str:
return f"{self._finished}/{self._total_sample_number}"
def try_open_sub_device(self, device_name: str = None):
if not self.driver_init_ok:
self._success = False
print(f"仪器还没有初始化完成,无法查询设备:{device_name}")
return
if device_name is None:
for k, v in self._device_status.items():
self.try_open_sub_device(k)
return
target_device_status = self._device_status[device_name]
if target_device_status["status"] == "未就绪":
print(f"尝试打开{device_name}设备")
target_device_status["open_btn"].click()
else:
print(f"{device_name}设备状态不支持打开:{target_device_status['status']}")
def try_close_sub_device(self, device_name: str = None):
if not self.driver_init_ok:
self._success = False
print(f"仪器还没有初始化完成,无法查询设备:{device_name}")
return
if device_name is None:
for k, v in self._device_status.items():
self.try_close_sub_device(k)
return
target_device_status = self._device_status[device_name]
if target_device_status["status"] == "空闲":
print(f"尝试关闭{device_name}设备")
target_device_status["close_btn"].click()
else:
print(f"{device_name}设备状态不支持关闭:{target_device_status['status']}")
def _get_resource_sample_id(self, wf_name, idx):
try:
root = list(self.resource_info[wf_name].values())[0]
# print(root)
plates = root["children"]
plate_01 = list(plates.values())[0]
pots = list(plate_01["children"].values())
return pots[idx]['sample_id']
except Exception as ex:
traceback.print_exc()
def start_sequence(self, wf_name: str, params: str = None, resource: dict = None):
print("!!!!!! 任务启动")
self.resource_info[wf_name] = resource
# 后续workflow_name将同步一下
if self.is_running:
print("设备正在运行,无法启动序列")
self._success = False
return False
if not self.driver_init_ok:
print(f"仪器还没有初始化完成,无法启动序列")
self._success = False
return False
if not self.could_run:
print(f"仪器不处于空闲状态,无法运行")
self._success = False
return False
# 参考:
# with UIPath(u"PREP-LC (联机): 方法和运行控制 ||Window"):
# with UIPath(u"panelNavTabChem||Pane->||Pane->panelControlChemStation||Pane->||Tab->仪器控制||Pane->||Pane->panelChemStation||Pane->PREP-LC (联机): 方法和运行控制 ||Pane->ViewMGR||Pane->MRC view||Pane->||Pane->||Pane->||Pane->||Custom->||Custom"):
# click(u"||Button#[0,1]")
app = Application(backend='uia').connect(title=u"PREP-LC (联机): 方法和运行控制 ")
window = app['PREP-LC (联机): 方法和运行控制']
window.allow_magic_lookup = False
panel_nav_tab = window.child_window(title="panelNavTabChem", auto_id="panelNavTabChem", control_type="Pane")
first_pane = panel_nav_tab.child_window(auto_id="uctlNavTabChem1", control_type="Pane")
panel_control_station = first_pane.child_window(title="panelControlChemStation", auto_id="panelControlChemStation", control_type="Pane")
instrument_control_tab: WindowSpecification = panel_control_station.\
child_window(auto_id="tabControlChem", control_type="Tab").\
child_window(title="仪器控制", auto_id="tabPage1", control_type="Pane").\
child_window(auto_id="uctrlChemStation", control_type="Pane").\
child_window(title="panelChemStation", auto_id="panelChemStation", control_type="Pane").\
child_window(title="PREP-LC (联机): 方法和运行控制 ", control_type="Pane").\
child_window(title="ViewMGR", control_type="Pane").\
child_window(title="MRC view", control_type="Pane").\
child_window(auto_id="mainMrcControlHost", control_type="Pane").\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Custom", found_index=0).\
child_window(control_type="Custom", found_index=0)
instrument_control_tab.dump_tree(3)
btn: UIAWrapper = instrument_control_tab.child_window(auto_id="methodButtonStartSequence", control_type="Button").wrapper_object()
self.start_time = datetime.now()
btn.click()
self._wf_name = wf_name
self._success = True
return True
def check_status(self):
app = Application(backend='uia').connect(title=u"PREP-LC (联机): 方法和运行控制 ")
window = app['PREP-LC (联机): 方法和运行控制']
ui_window = window.child_window(title="靠顶部", control_type="Group").\
child_window(title="状态", control_type="ToolBar").\
child_window(title="项目", control_type="Button", found_index=0)
# 检测pixel的颜色
element_info: UIAElementInfo = ui_window.element_info
rectangle = element_info.rectangle
point_x = int(rectangle.left + rectangle.width() * 0.15)
point_y = int(rectangle.top + rectangle.height() * 0.15)
r, g, b = pyautogui.pixel(point_x, point_y)
if 270 > r > 250 and 200 > g > 180 and b < 10: # 是黄色
self._is_running = False
self._status_text = "Not Ready"
elif r > 110 and g > 190 and 50 < b < 60:
self._is_running = False
self._status_text = "Ready"
elif 75 > r > 65 and 135 > g > 120 and 240 > b > 230:
self._is_running = True
self._status_text = "Running"
else:
print(point_x, point_y, "未知的状态", r, g, b)
def extract_data_from_txt(self, file_path):
# 打开文件
print(file_path)
with open(file_path, mode='r', encoding='utf-16') as f:
lines = f.readlines()
# 定义一个标志变量来判断是否已经找到“馏分列表”
started = False
data = []
for line in lines:
# 查找“馏分列表”,并开始提取后续行
if line.startswith("-----|-----|-----"):
started = True
continue # 跳过当前行
if started:
# 遇到"==="表示结束读取
if '=' * 80 in line:
break
# 使用正则表达式提取馏分、孔、位置和原因
res = re.split(r'\s+', line.strip())
if res:
fraction, hole, position, reason = res[0], res[1], res[2], res[-1]
data.append({
'馏分': fraction,
'': hole,
'位置': position,
'原因': reason.strip()
})
return data
def get_data_file(self, mat_index: str = None, after_time: datetime = None) -> tuple[str, str]:
"""
获取数据文件
after_time: 由于HPLC是启动后生成一个带时间的目录所以会选取after_time后的文件
"""
if mat_index is None:
print(f"mat_index不能为空")
return None
if after_time is None:
after_time = self.start_time
files = [i for i in os.listdir(self.data_file_path) if i.startswith(self.using_method)]
time_to_files: list[tuple[datetime, str]] = [(datetime.strptime(i.split(" ", 1)[1], "%Y-%m-%d %H-%M-%S"), i) for i in files]
time_to_files.sort(key=lambda x: x[0])
choose_folder = None
for i in time_to_files:
if i[0] > after_time:
print(i[0], after_time)
print(f"选取时间{datetime.strftime(after_time, '%Y-%m-%d %H-%M-%S')}之后的文件夹{i[1]}")
choose_folder = i[1]
break
if choose_folder is None:
print(f"没有找到{self.using_method} {datetime.strftime(after_time, '%Y-%m-%d %H-%M-%S')}之后的文件夹")
return None
current_data_path = os.path.join(self.data_file_path, choose_folder)
# 需要匹配 数字数字数字-.* 001-P2-E1-DQJ-4-70.D
target_row = [i for i in os.listdir(current_data_path) if re.match(r"\d{3}-.*", i)]
index2filepath = {int(k.split("-")[0]): os.path.join(current_data_path, k) for k in target_row}
print(f"查找文件{mat_index}")
if int(mat_index) not in index2filepath:
print(f"没有找到{mat_index}的文件 已找到:{index2filepath}")
return None
mat_final_path = index2filepath[int(mat_index)]
pdf = os.path.join(mat_final_path, "Report.PDF")
txt = os.path.join(mat_final_path, "Report.TXT")
fractions = self.extract_data_from_txt(txt)
print(fractions)
return pdf, txt
def __init__(self, driver_debug=False):
super().__init__()
self.data_file_path = r"D:\ChemStation\1\Data"
self.using_method = f"1106-dqj-4-64"
self.start_time = datetime.now()
self._device_status = dict()
self.resource_info: dict[str, dict] = dict()
# 启动所有监控器
self.checkers = [
InstrumentChecker(self, 1),
RunningChecker(self, 1),
RunningResultChecker(self, 1),
]
if not driver_debug:
for checker in self.checkers:
checker.start_monitoring()
class DriverChecker(ud.DriverChecker):
driver: HPLCDriver
class InstrumentChecker(DriverChecker):
_instrument_control_tab = None
_instrument_control_tab_wrapper = None
def get_instrument_status(self):
if self._instrument_control_tab is not None:
return self._instrument_control_tab
# 连接到目标窗口
app = Application(backend='uia').connect(title=u"PREP-LC (联机): 方法和运行控制 ")
window = app['PREP-LC (联机): 方法和运行控制']
window.allow_magic_lookup = False
panel_nav_tab = window.child_window(title="panelNavTabChem", auto_id="panelNavTabChem", control_type="Pane")
first_pane = panel_nav_tab.child_window(auto_id="uctlNavTabChem1", control_type="Pane")
panel_control_station = first_pane.child_window(title="panelControlChemStation", auto_id="panelControlChemStation", control_type="Pane")
instrument_control_tab: WindowSpecification = panel_control_station.\
child_window(auto_id="tabControlChem", control_type="Tab").\
child_window(title="仪器控制", auto_id="tabPage1", control_type="Pane").\
child_window(auto_id="uctrlChemStation", control_type="Pane").\
child_window(title="panelChemStation", auto_id="panelChemStation", control_type="Pane").\
child_window(title="PREP-LC (联机): 方法和运行控制 ", control_type="Pane").\
child_window(title="ViewMGR", control_type="Pane").\
child_window(title="MRC view", control_type="Pane").\
child_window(auto_id="mainMrcControlHost", control_type="Pane").\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Custom", found_index=0).\
child_window(best_match="Custom6").\
child_window(auto_id="ListBox_DashboardPanel", control_type="List")
if self._instrument_control_tab is None:
self._instrument_control_tab = instrument_control_tab
self._instrument_control_tab_wrapper = instrument_control_tab.wrapper_object()
return self._instrument_control_tab
def check(self):
self.get_instrument_status()
if self._instrument_control_tab_wrapper is None or self._instrument_control_tab is None:
return
item: UIAWrapper
index = 0
keys = list(self.driver._device_status.keys())
for item in self._instrument_control_tab_wrapper.children():
info: UIAElementInfo = item.element_info
if info.control_type == "ListItem" and item.window_text() == "Agilent.RapidControl.StatusDashboard.PluginViewModel":
sub_item: WindowSpecification = self._instrument_control_tab.\
child_window(title="Agilent.RapidControl.StatusDashboard.PluginViewModel", control_type="ListItem", found_index=index).\
child_window(control_type="Custom", found_index=0)
if index < len(keys):
deviceStatusInfo = self.driver._device_status[keys[index]]
name = deviceStatusInfo["name"]
deviceStatusInfo["status"] = deviceStatusInfo["status_obj"].window_text()
print(name, index, deviceStatusInfo["status"], "刷新")
if deviceStatusInfo["open_btn"] is not None and deviceStatusInfo["close_btn"] is not None:
index += 1
continue
else:
name_obj = sub_item.child_window(control_type="Text", found_index=0).wrapper_object()
name = name_obj.window_text()
self.driver._device_status[name] = dict()
self.driver._device_status[name]["name_obj"] = name_obj
self.driver._device_status[name]["name"] = name
print(name, index)
status = sub_item.child_window(control_type="Custom", found_index=0).\
child_window(auto_id="TextBlock_StateLabel", control_type="Text")
status_obj: UIAWrapper = status.wrapper_object()
self.driver._device_status[name]["status_obj"] = status_obj
self.driver._device_status[name]["status"] = status_obj.window_text()
print(status.window_text())
sub_item = sub_item.wrapper_object()
found_index = 0
open_btn = None
close_btn = None
for btn in sub_item.children():
if btn.element_info.control_type == "Button":
found_index += 1
if found_index == 5:
open_btn = btn
elif found_index == 6:
close_btn = btn
self.driver._device_status[name]["open_btn"] = open_btn
self.driver._device_status[name]["close_btn"] = close_btn
index += 1
class RunningChecker(DriverChecker):
def check(self):
self.driver.check_status()
class RunningResultChecker(DriverChecker):
_finished: UIAWrapper = None
_total_sample_number: UIAWrapper = None
def check(self):
if self._finished is None or self._total_sample_number is None:
app = Application(backend='uia').connect(title=u"PREP-LC (联机): 方法和运行控制 ")
window = app['PREP-LC (联机): 方法和运行控制']
window.allow_magic_lookup = False
panel_nav_tab = window.child_window(title="panelNavTabChem", auto_id="panelNavTabChem", control_type="Pane")
first_pane = panel_nav_tab.child_window(auto_id="uctlNavTabChem1", control_type="Pane")
panel_control_station = first_pane.child_window(title="panelControlChemStation", auto_id="panelControlChemStation", control_type="Pane")
instrument_control_tab: WindowSpecification = panel_control_station.\
child_window(auto_id="tabControlChem", control_type="Tab").\
child_window(title="仪器控制", auto_id="tabPage1", control_type="Pane").\
child_window(auto_id="uctrlChemStation", control_type="Pane").\
child_window(title="panelChemStation", auto_id="panelChemStation", control_type="Pane").\
child_window(title="PREP-LC (联机): 方法和运行控制 ", control_type="Pane").\
child_window(title="ViewMGR", control_type="Pane").\
child_window(title="MRC view", control_type="Pane").\
child_window(auto_id="mainMrcControlHost", control_type="Pane").\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Custom", found_index=0).\
child_window(auto_id="mainControlExpanderSampleInformation", control_type="Group").\
child_window(auto_id="controlsSampleInfo", control_type="Custom")
self._finished = instrument_control_tab.child_window(best_match="Static15").wrapper_object()
self._total_sample_number = instrument_control_tab.child_window(best_match="Static16").wrapper_object()
try:
temp = int(self._finished.window_text())
if self.driver._finished is None or temp > self.driver._finished:
if self.driver._finished is None:
self.driver._finished = 0
for i in range(self.driver._finished, temp):
sample_id = self.driver._get_resource_sample_id(self.driver._wf_name, i) # 从0开始计数
pdf, txt = self.driver.get_data_file(i + 1)
device_id = self.driver.device_id if hasattr(self.driver, "device_id") else "default"
oss_upload(pdf, f"hplc/{sample_id}/{os.path.basename(pdf)}", process_key="example", device_id=device_id)
oss_upload(txt, f"hplc/{sample_id}/{os.path.basename(txt)}", process_key="HPLC-txt-result", device_id=device_id)
# self.driver.extract_data_from_txt()
except Exception as ex:
self.driver._finished = 0
print("转换数字出错", ex)
try:
self.driver._total_sample_number = int(self._total_sample_number.window_text())
except Exception as ex:
self.driver._total_sample_number = 0
print("转换数字出错", ex)
# 示例用法
if __name__ == "__main__":
# obj = HPLCDriver.__new__(HPLCDriver)
# obj.start_sequence()
# obj = HPLCDriver.__new__(HPLCDriver)
# obj.data_file_path = r"D:\ChemStation\1\Data"
# obj.using_method = r"1106-dqj-4-64"
# obj.get_data_file("001", after_time=datetime(2024, 11, 6, 19, 3, 6))
obj = HPLCDriver.__new__(HPLCDriver)
obj.data_file_path = r"D:\ChemStation\1\Data"
obj.using_method = r"1106-dqj-4-64"
obj._wf_name = "test"
obj.resource_info = {
"test": {
"1": {
"children": {
"11": {
"children": {
"111": {
"sample_id": "sample-1"
},
"112": {
"sample_id": "sample-2"
}
}
}
}
}
}
}
sample_id = obj._get_resource_sample_id("test", 0)
pdf, txt = obj.get_data_file("1", after_time=datetime(2024, 11, 6, 19, 3, 6))
oss_upload(pdf, f"hplc/{sample_id}/{os.path.basename(pdf)}", process_key="example")
oss_upload(txt, f"hplc/{sample_id}/{os.path.basename(txt)}", process_key="HPLC-txt-result")
# driver = HPLCDriver()
# for i in range(10000):
# print({k: v for k, v in driver._device_status.items() if isinstance(v, str)})
# print(driver.device_status)
# print(driver.could_run)
# print(driver.driver_init_ok)
# print(driver.is_running)
# print(driver.finish_status)
# print(driver.status_text)
# time.sleep(5)

View File

View File

@@ -0,0 +1,90 @@
import time
import sys
import io
# sys.path.insert(0, r'C:\kui\winprep_cli\winprep_c_Uni-lab\x64\Debug')
import winprep_c
from queue import Queue
class Revvity:
_success: bool = False
_status: str = "Idle"
_status_queue: Queue = Queue()
def __init__(self):
self._status = "Idle"
self._success = False
self._status_queue = Queue()
@property
def success(self) -> bool:
# print("success")
return self._success
@property
def status(self) -> str:
if not self._status_queue.empty():
self._status = self._status_queue.get()
return self._status
def _run_script(self, file_path: str):
output = io.StringIO()
sys.stdout = output # 重定向标准输出
try:
# 执行 winprep_c.test_mtp_script 并打印结果
winprep_c.test_mtp_script(file_path)
except Exception as e:
# 捕获执行过程中的异常
print(e)
self._status_queue.put(f"Error: {str(e)}")
finally:
sys.stdout = sys.__stdout__ # 恢复标准输出
# 获取捕获的输出并逐行更新状态
for line in output.getvalue().splitlines():
print(line)
self._status_queue.put(line)
self._status=line
def run(self, file_path: str, params:str, resource: dict = {"AichemecoHiwo": {"id": "AichemecoHiwo"}}):
# 设置状态为 Running
self._status = "Running"
winprep_c.test_mtp_script(file_path)
# 在一个新的线程中运行 MTP 脚本,避免阻塞主线程
# thread = threading.Thread(target=self._run_script, args=(file_path,))
# thread.start()
# self._run_script(file_path)
#
# # 在主线程中持续访问状态
# while thread.is_alive() or self._success == False:
# current_status = self.status() # 获取当前的状态
# print(f"Current Status: {current_status}")
# time.sleep(0.5)
# output = io.StringIO()
# sys.stdout = output # 重定向标准输出
# try:
# # 执行 winprep_c.test_mtp_script 并打印结果
# winprep_c.test_mtp_script(file_path)
# finally:
# sys.stdout = sys.__stdout__ # 恢复标准输出
# # 获取捕获的输出并逐行更新状态
# for line in output.getvalue().splitlines():
# self._status_queue.put(line)
# self._success = True
# 修改物料信息
workstation = list(resource.values())[0]
input_plate_wells = list(workstation["children"]["test-GL96-2A02"]["children"].values())
output_plate_wells = list(workstation["children"]["HPLC_Plate"]["children"].values())
for j in range(8):
output_plate_wells[j]["data"]["liquid"] += input_plate_wells[j]["data"]["liquid"]
output_plate_wells[j]["sample_id"] = input_plate_wells[j]["sample_id"]
self._status = "Idle"
self._success = True

View File

@@ -0,0 +1,4 @@
class Config(object):
DEBUG_MODE = True
OPEN_DEVICE = True
DEVICE_ADDRESS = 0x01

View File

@@ -0,0 +1,21 @@
class FakeSerial:
def __init__(self):
self.data = b''
def write(self, data):
print("发送数据: ", end="")
for i in data:
print(f"{i:02x}", end=" ")
print() # 换行
# 这里可模拟把假数据写到某个内部缓存
# self.data = ...
def setRTS(self, b):
pass
def read(self, n):
# 这里可返回预设的响应,例如 b'\x01\x03\x02\x00\x19\x79\x8E'
return b'\x01\x03\x02\x00\x19\x79\x8E'
def close(self):
pass

View File

@@ -0,0 +1,153 @@
import time
from serial import Serial
from threading import Thread
from unilabos.device_comms.universal_driver import UniversalDriver
class EleGripper(UniversalDriver):
@property
def status(self) -> str:
return f"spin_pos: {self.rot_data[0]}, grasp_pos: {self.gri_data[0]}, spin_v: {self.rot_data[1]}, grasp_v: {self.gri_data[1]}, spin_F: {self.rot_data[2]}, grasp_F: {self.gri_data[2]}"
def __init__(self,port,baudrate=115200, pos_error=-11, id = 9):
self.ser = Serial(port,baudrate)
self.pos_error = pos_error
self.success = False
# [pos, speed, force]
self.gri_data = [0,0,0]
self.rot_data = [0,0,0]
self.id = id
self.init_gripper()
self.wait_for_gripper_init()
t = Thread(target=self.data_loop)
t.start()
self.gripper_move(0,255,255)
self.rotate_move_abs(0,255,255)
def modbus_crc(self, data: bytes) -> bytes:
crc = 0xFFFF
for pos in data:
crc ^= pos
for _ in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc.to_bytes(2, byteorder='little')
def send_cmd(self, id, fun, address, data:str):
data_len = len(bytes.fromhex(data))
data_ = f"{id:02X} {fun} {address} {data_len//2:04X} {data_len:02X} {data}"
data_bytes = bytes.fromhex(data_)
data_with_checksum = data_bytes + self.modbus_crc(data_bytes)
self.ser.write(data_with_checksum)
time.sleep(0.01)
self.ser.read(8)
def read_address(self, id , address, data_len):
data = f"{id:02X} 03 {address} {data_len:04X}"
data_bytes = bytes.fromhex(data)
data_with_checksum = data_bytes + self.modbus_crc(data_bytes)
self.ser.write(data_with_checksum)
time.sleep(0.01)
res_len = 5+data_len*2
res = self.ser.read(res_len)
return res
def init_gripper(self):
self.send_cmd(self.id,'10','03 E8','00 01')
self.send_cmd(self.id,'10','03 E9','00 01')
def gripper_move(self, pos, speed, force):
self.send_cmd(self.id,'10', '03 EA', f"{speed:02x} {pos:02x} {force:02x} 01")
def rotate_move_abs(self, pos, speed, force):
pos += self.pos_error
if pos < 0:
pos = (1 << 16) + pos
self.send_cmd(self.id,'10', '03 EC', f"{(pos):04x} {force:02x} {speed:02x} 0000 00 01")
# def rotate_move_rel(self, pos, speed, force):
# if pos < 0:
# pos = (1 << 16) + pos
# print(f'{pos:04x}')
# self.send_cmd(self.id,'10', '03 EC', f"0000 {force:02x} {speed:02x} {pos:04x} 00 02")
def wait_for_gripper_init(self):
res = self.read_address(self.id, "07 D0", 1)
res_r = self.read_address(self.id, "07 D1", 1)
while res[4] == 0x08 or res_r[4] == 0x08:
res = self.read_address(self.id, "07 D0", 1)
res_r = self.read_address(self.id, "07 D1", 1)
time.sleep(0.1)
def wait_for_gripper(self):
while self.gri_data[1] != 0:
time.sleep(0.1)
def wait_for_rotate(self):
while self.rot_data[1] != 0:
time.sleep(0.1)
def data_reader(self):
res_g = self.read_address(self.id, "07 D2", 2)
res_r = self.read_address(self.id, "07 D4", 4)
int32_value = (res_r[3] << 8) | res_r[4]
if int32_value > 0x7FFF:
int32_value -= 0x10000
self.gri_data = (int(res_g[4]), int(res_g[3]), int(res_g[5]))
self.rot_data = (int32_value, int(res_r[5]), int(res_r[6]))
def data_loop(self):
while True:
self.data_reader()
time.sleep(0.1)
def node_gripper_move(self, cmd:str):
self.success = False
try:
cmd_list = [int(x) for x in cmd.replace(' ','').split(',')]
self.gripper_move(*cmd_list)
self.wait_for_gripper()
except Exception as e:
raise e
self.success = True
def node_rotate_move(self, cmd:str):
self.success = False
try:
cmd_list = [int(x) for x in cmd.replace(' ','').split(',')]
self.rotate_move_abs(*cmd_list)
self.wait_for_rotate()
except Exception as e:
raise e
self.success = True
def move_and_rotate(self, spin_pos, grasp_pos, spin_v, grasp_v, spin_F, grasp_F):
self.gripper_move(grasp_pos, grasp_v, grasp_F)
self.wait_for_gripper()
self.rotate_move_abs(spin_pos, spin_v, spin_F)
self.wait_for_rotate()
if __name__ == "__main__":
gripper = EleGripper("COM12")
gripper.init_gripper()
gripper.wait_for_gripper_init()
gripper.gripper_move(210,127,255)
gripper.wait_for_gripper()
gripper.rotate_move_abs(135,10,255)
gripper.data_reader()
print(gripper.rot_data)

View File

@@ -0,0 +1,87 @@
import struct
import time
from typing import Union
from .Consts import Config
def calculate_modbus_crc16(data: bytes) -> tuple[int, int]:
"""
计算 Modbus RTU 的 CRC16 校验码,返回 (low_byte, high_byte)。
data 可以是 bytes 或者 bytearray
"""
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if (crc & 0x0001):
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
# 低字节在前、高字节在后
low_byte = crc & 0xFF
high_byte = (crc >> 8) & 0xFF
return low_byte, high_byte
def create_command(slave_id, func_code, address, data):
"""
生成完整的 Modbus 通信指令:
- 第1字节: 从站地址
- 第2字节: 功能码
- 第3~4字节: 寄存器地址 (大端)
- 第5~6字节: 数据(或读寄存器个数) (大端)
- 第7~8字节: CRC校验, 先低后高
"""
# 按照大端格式打包B(1字节), B(1字节), H(2字节), H(2字节)
# 例如: 0x03, 0x03, 0x0191, 0x0001
# 生成的命令将是: 03 03 01 91 00 01 (不含 CRC)
command = struct.pack(">B B H H", slave_id, func_code, address, data)
# 计算CRC并将低字节、后高字节拼到末尾
low_byte, high_byte = calculate_modbus_crc16(command)
return command + bytes([low_byte, high_byte])
def send_command(ser, command) -> Union[bytes, str]:
"""通过串口发送指令并打印响应"""
# Modbus RTU 半双工,发送前拉高 RTS
ser.setRTS(True)
time.sleep(0.02)
ser.write(command) # 发送指令
if Config.OPEN_DEVICE:
# 如果是实际串口就打印16进制的发送内容
print(f"发送的数据: ", end="")
for ind, c in enumerate(command.hex().upper()):
if ind % 2 == 0 and ind != 0:
print(" ", end="")
print(c, end="")
# 发送完成后,切换到接收模式
ser.setRTS(False)
# 读取响应,具体长度要看从站返回,有时多字节
response = ser.read(8) # 假设响应是8字节
print(f"接收到的数据: ", end="")
for ind, c in enumerate(response.hex().upper()):
if ind % 2 == 0 and ind != 0:
print(" ", end="")
print(c, end="")
print()
return response
def get_result_byte_int(data: bytes, byte_start: int = 6, byte_end: int = 10) -> int:
return int(data.hex()[byte_start:byte_end], 16)
def get_result_byte_str(data: bytes, byte_start: int = 6, byte_end: int = 10) -> str:
return data.hex()[byte_start:byte_end]
def run_commands(ser, duration=0.1, *commands):
for cmd in commands:
if isinstance(cmd, list):
for c in cmd:
send_command(ser, c)
time.sleep(duration)
else:
send_command(ser, cmd)
time.sleep(duration)

View File

View File

@@ -0,0 +1,37 @@
from enum import Enum
class PR_PATH(Enum):
# TYPE (Bit0-3)
TYPE_NO_ACTION = 0x0000 # 无动作
TYPE_POSITIONING = 0x0001 # 位置定位
TYPE_VELOCITY = 0x0002 # 速度运行
TYPE_HOME = 0x0003 # 回零
# INS (Bit4) - 默认都是插断模式
INS_INTERRUPT = 0x0010 # 插断
# OVLP (Bit5) - 默认都是不重叠
OVLP_NO_OVERLAP = 0x0000 # 不重叠
# POSITION MODE (Bit6)
POS_ABSOLUTE = 0x0000 # 绝对位置
POS_RELATIVE = 0x0040 # 相对位置
# MOTOR MODE (Bit7)
MOTOR_ABSOLUTE = 0x0000 # 绝对电机
MOTOR_RELATIVE = 0x0080 # 相对电机
# 常用组合(默认都是插断且不重叠)
# 位置定位相关
ABS_POS = TYPE_POSITIONING | INS_INTERRUPT | OVLP_NO_OVERLAP | POS_ABSOLUTE # 绝对定位
REL_POS = TYPE_POSITIONING | INS_INTERRUPT | OVLP_NO_OVERLAP | POS_RELATIVE # 相对定位
# 速度运行相关
VELOCITY = TYPE_VELOCITY | INS_INTERRUPT | OVLP_NO_OVERLAP # 速度模式
# 回零相关
HOME = TYPE_HOME | INS_INTERRUPT | OVLP_NO_OVERLAP # 回零模式
# 电机模式组合
ABS_POS_REL_MOTOR = ABS_POS | MOTOR_RELATIVE # 绝对定位+相对电机
REL_POS_REL_MOTOR = REL_POS | MOTOR_RELATIVE # 相对定位+相对电机

View File

@@ -0,0 +1,92 @@
from ..base_types.PrPath import PR_PATH
from ..Utils import create_command, get_result_byte_int, get_result_byte_str, send_command
def create_position_commands(slave_id: int, which: int, how: PR_PATH,
position: float, velocity: int = 300,
acc_time: int = 50, dec_time: int = 50,
special: int = 0) -> list[list[bytes]]:
"""
创建位置相关的Modbus命令列表
Args:
slave_id: 从站地址
which: PR路径号(0-7)
how: PR_PATH枚举定义运动方式
position: 目标位置(Pulse)
velocity: 运行速度(rpm)
acc_time: 加速时间(ms/Krpm)默认50
dec_time: 减速时间(ms/Krpm)默认50
special: 特殊参数默认0
Returns:
包含所有设置命令的列表
"""
if not 0 <= which <= 7:
raise ValueError("which必须在0到7之间")
base_addr = 0x6200 + which * 8
# 位置值保持原样Pulse
position = int(position)
print(f"路径方式 {' '.join(bin(how.value)[2:])} 位置 {position} 速度 {velocity}")
position_high = (position >> 16) & 0xFFFF # 获取高16位
position_low = position & 0xFFFF # 获取低16位
# 速度值rpm转换为0x0000格式
velocity_value = 0x0000 + velocity
# 加减速时间ms/Krpm转换为0x0000格式
acc_time_value = 0x0000 + int(acc_time)
dec_time_value = 0x0000 + int(dec_time)
# 特殊参数转换为0x0000格式
special_value = 0x0000 + special
return [
create_command(slave_id, 0x06, base_addr + 0, how.value), # 路径方式
create_command(slave_id, 0x06, base_addr + 1, position_high), # 位置高16位
create_command(slave_id, 0x06, base_addr + 2, position_low), # 位置低16位
create_command(slave_id, 0x06, base_addr + 3, velocity_value), # 运行速度
create_command(slave_id, 0x06, base_addr + 4, acc_time_value), # 加速时间
create_command(slave_id, 0x06, base_addr + 5, dec_time_value), # 减速时间
create_command(slave_id, 0x06, base_addr + 6, special_value), # 特殊参数
]
def create_position_run_command(slave_id: int, which: int) -> list[list[bytes]]:
print(f"运行路径 PR{which}")
return [create_command(slave_id, 0x06, 0x6002, 0x0010 + which)]
def run_set_position_zero(ser, DEVICE_ADDRESS) -> list[list[bytes]]:
print(f"手动回零")
send_command(ser, create_command(DEVICE_ADDRESS, 0x06, 0x6002, 0x0021))
def run_stop(ser, DEVICE_ADDRESS) -> list[list[bytes]]:
print(f"急停")
send_command(ser, create_command(DEVICE_ADDRESS, 0x06, 0x6002, 0x0040))
def run_set_forward_run(ser, DEVICE_ADDRESS) -> list[list[bytes]]:
print(f"设定正方向运动")
send_command(ser, create_command(DEVICE_ADDRESS, 0x06, 0x0007, 0x0000))
def run_set_backward_run(ser, DEVICE_ADDRESS) -> list[list[bytes]]:
print(f"设定反方向运动")
send_command(ser, create_command(DEVICE_ADDRESS, 0x06, 0x0007, 0x0001))
def run_get_command_position(ser, DEVICE_ADDRESS, print_pos=True) -> int:
retH = send_command(ser, create_command(DEVICE_ADDRESS, 0x03, 0x602A, 0x0001)) # 命令位置H
retL = send_command(ser, create_command(DEVICE_ADDRESS, 0x03, 0x602B, 0x0001)) # 命令位置L
value = get_result_byte_str(retH) + get_result_byte_str(retL)
value = int(value, 16)
if print_pos:
print(f"命令位置: {value}")
return value
def run_get_motor_position(ser, DEVICE_ADDRESS, print_pos=True) -> int:
retH = send_command(ser, create_command(DEVICE_ADDRESS, 0x03, 0x602C, 0x0001)) # 电机位置H
retL = send_command(ser, create_command(DEVICE_ADDRESS, 0x03, 0x602D, 0x0001)) # 电机位置L
value = get_result_byte_str(retH) + get_result_byte_str(retL)
value = int(value, 16)
if print_pos:
print(f"电机位置: {value}")
return value

View File

@@ -0,0 +1,44 @@
import time
from ..Utils import create_command, send_command
from .PositionControl import run_get_motor_position
def run_get_status(ser, DEVICE_ADDRESS, print_status=True) -> list[list[bytes]]:
# Bit0 故障
# Bit1 使能
# Bit2 运行
# Bit4 指令完成
# Bit5 路径完成
# Bit6 回零完成
ret = send_command(ser, create_command(DEVICE_ADDRESS, 0x03, 0x1003, 0x0001))
status = bin(int(ret.hex()[6:10], 16))[2:]
# 用0左位补齐
status = status.zfill(8)
status_info_list = []
if status[-1] == "1":
status_info_list.append("故障")
if status[-2] == "1":
status_info_list.append("使能")
if status[-3] == "1":
status_info_list.append("运行")
if status[-5] == "1":
status_info_list.append("指令完成")
if status[-6] == "1":
status_info_list.append("路径完成")
if status[-7] == "1":
status_info_list.append("回零完成")
if print_status:
print(f"获取状态: {' '.join(status_info_list)}")
return status_info_list
def run_until_status(ser, DEVICE_ADDRESS, status_info: str, max_time=10):
start_time = time.time()
while True:
ret = run_get_status(ser, DEVICE_ADDRESS)
if status_info in ret:
break
if time.time() - start_time > max_time:
print(f"状态未达到 {status_info} 超时")
return False
time.sleep(0.05)
return True

View File

@@ -0,0 +1,12 @@
from serial import Serial
from ..Consts import Config
from ..Utils import create_command, send_command
def create_elimate_warning_command(DEVICE_ADDRESS):
return create_command(DEVICE_ADDRESS, 0x06, 0x0145, 0x0087)
def run_elimate_warning(ser: Serial, DEVICE_ADDRESS):
send_command(ser, create_elimate_warning_command(DEVICE_ADDRESS))
print("清除警报")

View File

@@ -0,0 +1,126 @@
import os
import sys
from abc import abstractmethod
from typing import Optional
import serial
from .Consts import Config
from .FakeSerial import FakeSerial
from .Utils import run_commands
from .base_types.PrPath import PR_PATH
from .commands.PositionControl import run_get_command_position, run_set_forward_run, create_position_commands, \
create_position_run_command
from .commands.Warning import run_elimate_warning
try:
from unilabos.utils.pywinauto_util import connect_application, get_process_pid_by_name, get_ui_path_with_window_specification, print_wrapper_identifiers
from unilabos.device_comms.universal_driver import UniversalDriver, SingleRunningExecutor
from unilabos.device_comms import universal_driver as ud
except Exception as e:
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))
from unilabos.utils.pywinauto_util import connect_application, get_process_pid_by_name, get_ui_path_with_window_specification, print_wrapper_identifiers
from unilabos.device_comms.universal_driver import UniversalDriver, SingleRunningExecutor
from unilabos.devices.template_driver import universal_driver as ud
print(f"使用文件DEBUG运行: {e}")
class iCL42Driver(UniversalDriver):
_ser: Optional[serial.Serial | FakeSerial] = None
_motor_position: Optional[int] = None
_DEVICE_COM: Optional[str] = None
_DEVICE_ADDRESS: Optional[int] = None
# 运行状态
_is_executing_run: bool = False
_success: bool = False
@property
def motor_position(self) -> int:
return self._motor_position
@property
def is_executing_run(self) -> bool:
return self._is_executing_run
@property
def success(self) -> bool:
return self._success
def init_device(self):
# 配置串口参数
if Config.OPEN_DEVICE:
self._ser = serial.Serial(
port=self._DEVICE_COM, # 串口号
baudrate=38400, # 波特率
bytesize=serial.EIGHTBITS, # 数据位
parity=serial.PARITY_NONE, # 校验位 N-无校验
stopbits=serial.STOPBITS_TWO, # 停止位
timeout=1 # 超时时间
)
else:
self._ser = FakeSerial()
def run_motor(self, mode: str, position: float, velocity: int):
if self._ser is None:
print("Device is not initialized")
self._success = False
return False
def post_func(res, _):
self._success = res
if not res:
self._is_executing_run = False
ins: SingleRunningExecutor = SingleRunningExecutor.get_instance(
self.execute_run_motor, post_func
)
# if not ins.is_ended and ins.is_started:
# print("Function is running")
# self._success = False
# return False
# elif not ins.is_started:
# print("Function started")
# ins.start() # 开始执行
# else:
# print("Function reset and started")
ins.reset()
ins.start(mode=mode, position=position, velocity=velocity)
def execute_run_motor(self, mode: str, position: float, velocity: int):
position = int(position * 1000)
PR = 0
run_elimate_warning(self._ser, self._DEVICE_ADDRESS)
run_set_forward_run(self._ser, self._DEVICE_ADDRESS)
run_commands(
self._ser, 0.1,
create_position_commands(self._DEVICE_ADDRESS, PR, PR_PATH[mode], position, velocity), # 41.8cm 21.8cm
create_position_run_command(self._DEVICE_ADDRESS, PR),
)
# if run_until_status(self._ser, self._DEVICE_ADDRESS, "路径完成"):
# pass
def __init__(self, device_com: str = "COM9", device_address: int = 0x01):
self._DEVICE_COM = device_com
self._DEVICE_ADDRESS = device_address
self.init_device()
# 启动所有监控器
self.checkers = [
# PositionChecker(self, 1),
]
for checker in self.checkers:
checker.start_monitoring()
@abstractmethod
class DriverChecker(ud.DriverChecker):
driver: iCL42Driver
class PositionChecker(DriverChecker):
def check(self):
# noinspection PyProtectedMember
if self.driver._ser is not None:
# noinspection PyProtectedMember
self.driver._motor_position = run_get_command_position(self.driver._ser, self.driver._DEVICE_ADDRESS)
# 示例用法
if __name__ == "__main__":
driver = iCL42Driver("COM3")
driver._is_executing_run = True

View File

@@ -0,0 +1,79 @@
# 使用pyserial进行485的协议通信端口指定为COM4
import serial
from serial.rs485 import RS485Settings
import traceback
from Consts import Config
from FakeSerial import FakeSerial
from base_types.PrPath import PR_PATH
from Utils import run_commands
from commands.PositionControl import create_position_commands, create_position_run_command, run_get_command_position, run_get_motor_position, run_set_forward_run
from commands.Status import run_get_status, run_until_status
from commands.Warning import run_elimate_warning
from Grasp import EleGripper
DEVICE_ADDRESS = Config.DEVICE_ADDRESS
# 配置串口参数
if Config.OPEN_DEVICE:
ser = serial.Serial(
port='COM11', # 串口号
baudrate=38400, # 波特率
bytesize=serial.EIGHTBITS, # 数据位
parity=serial.PARITY_NONE, # 校验位 N-无校验
stopbits=serial.STOPBITS_TWO, # 停止位
timeout=1 # 超时时间
)
else:
ser = FakeSerial()
# RS485模式支持如果需要
try:
ser.rs485_mode = RS485Settings(
rts_level_for_tx=True,
rts_level_for_rx=False,
# delay_before_tx=0.01,
# delay_before_rx=0.01
)
except AttributeError:
traceback.print_exc()
print("RS485模式需要支持的硬件和pyserial版本")
# run_set_position_zero(ser, DEVICE_ADDRESS)
# api.get_running_state(ser, DEVICE_ADDRESS)
gripper = EleGripper("COM12")
gripper.init_gripper()
gripper.wait_for_gripper_init()
PR = 0
run_get_status(ser, DEVICE_ADDRESS)
run_elimate_warning(ser, DEVICE_ADDRESS)
run_set_forward_run(ser, DEVICE_ADDRESS)
run_commands(
ser, 0.1,
create_position_commands(DEVICE_ADDRESS, PR, PR_PATH.ABS_POS, 20 * 1000, 300), # 41.8cm 21.8cm
create_position_run_command(DEVICE_ADDRESS, PR),
)
if run_until_status(ser, DEVICE_ADDRESS, "路径完成"):
pass
gripper.gripper_move(210,127,255)
gripper.wait_for_gripper()
gripper.rotate_move_abs(135,10,255)
gripper.data_reader()
print(gripper.rot_data)
run_commands(
ser, 0.1,
create_position_commands(DEVICE_ADDRESS, PR, PR_PATH.ABS_POS, 30 * 1000, 300), # 41.8cm 21.8cm
create_position_run_command(DEVICE_ADDRESS, PR),
)
gripper.gripper_move(210,127,255)
gripper.wait_for_gripper()
gripper.rotate_move_abs(135,10,255)
gripper.data_reader()
# run_get_command_position(ser, DEVICE_ADDRESS)
# run_get_motor_position(ser, DEVICE_ADDRESS)
# ser.close()

View File

@@ -0,0 +1,295 @@
import time
import traceback
from typing import Optional
import requests
from pywinauto.application import WindowSpecification
from pywinauto.controls.uiawrapper import UIAWrapper
from pywinauto_recorder import UIApplication
from pywinauto_recorder.player import UIPath, click, focus_on_application, exists, find, FailedSearch
from unilabos.device_comms import universal_driver as ud
from unilabos.device_comms.universal_driver import UniversalDriver, SingleRunningExecutor
from unilabos.utils.pywinauto_util import connect_application, get_process_pid_by_name, \
get_ui_path_with_window_specification
class NivoDriver(UniversalDriver):
# 初始指定
_device_ip: str = None
# 软件状态检测
_software_enabled: bool = False
_software_pid: int = None
_http_service_available: bool = False
# 初始化状态
_is_initialized: bool = False
# 任务是否成功
_success: bool = False
# 运行状态
_is_executing_run: bool = False
_executing_ui_path: Optional[UIPath] = None
_executing_index: Optional[int] = None
_executing_status: Optional[str] = None
_total_tasks: Optional[list[str]] = None
_guide_app: Optional[UIApplication] = None
@property
def executing_status(self) -> str:
if self._total_tasks is None:
return f"无任务"
if self._executing_index is None:
return f"等待任务开始,总计{len(self._total_tasks)}".encode('utf-8').decode('utf-8')
else:
return f"正在执行第{self._executing_index + 1}/{len(self._total_tasks)}个任务,当前状态:{self._executing_status}".encode('utf-8').decode('utf-8')
@property
def device_ip(self) -> str:
return self._device_ip
@property
def success(self) -> bool:
return self._success
@property
def status(self) -> str:
return f"Software: {self._software_enabled}, HTTP: {self._http_service_available} Initialized: {self._is_initialized} Executing: {self._is_executing_run}"
def set_device_addr(self, device_ip_str):
self._device_ip = device_ip_str
print(f"Set device IP to: {self.device_ip}")
def run_instrument(self):
if not self._is_initialized:
print("Instrument is not initialized")
self._success = False
return False
def post_func(res, _):
self._success = res
if not res:
self._is_executing_run = False
ins: SingleRunningExecutor = SingleRunningExecutor.get_instance(self.execute_run_instrument, post_func)
if not ins.is_ended and ins.is_started:
print("Function is running")
self._success = False
return False
elif not ins.is_started:
print("Function started")
ins.start() # 开始执行
else:
print("Function reset and started")
ins.reset()
ins.start()
def execute_run_instrument(self):
process_found, process_pid = get_process_pid_by_name("Guide.exe", min_memory_mb=20)
if not process_found:
uiapp = connect_application(process=self._software_pid)
focus_on_application(uiapp)
ui_window: WindowSpecification = uiapp.app.top_window()
button: WindowSpecification = ui_window.child_window(title="xtpBarTop", class_name="XTPDockBar").child_window(
title="Standard", class_name="XTPToolBar").child_window(title="Protocol", control_type="Button")
click(button.wrapper_object())
for _ in range(5):
time.sleep(1)
process_found, process_pid = get_process_pid_by_name("Guide.exe", min_memory_mb=20)
if process_found:
break
if not process_found:
print("Guide.exe not found")
self._success = False
return False
uiapp = connect_application(process=process_pid)
self._guide_app = uiapp
focus_on_application(uiapp)
wrapper_object = uiapp.app.top_window().wrapper_object()
ui_path = get_ui_path_with_window_specification(wrapper_object)
self._executing_ui_path = ui_path
with ui_path:
try:
click(u"||Custom->||RadioButton-> Run||Text-> Run||Text")
except FailedSearch as e:
print(f"未找到Run按钮可能已经在执行了")
with UIPath(u"WAA.Guide.Guide.RunControlViewModel||Custom->||Custom"):
click(u"Start||Button")
with UIPath(u"WAA.Guide.Guide.RunControlViewModel||Custom->||Custom->||Group->WAA.Guide.Guide.StartupControlViewModel||Custom->||Custom->Ok||Button"):
while self._executing_index is None or self._executing_index == 0:
if exists(None, timeout=2):
click(u"Ok||Text")
print("Run Init Success!")
self._is_executing_run = True
return True
else:
print("Wait for Ok button")
def check_execute_run_status(self):
if not self._is_executing_run:
return False
if self._executing_ui_path is None:
return False
total_tasks = []
executing_index = 0
executing_status = ""
procedure_name_found = False
with self._executing_ui_path:
with UIPath(u"WAA.Guide.Guide.RunControlViewModel||Custom->||Custom"):
with UIPath("Progress||Group->||DataGrid"):
wrappered_object: UIAWrapper = find(timeout=0.5) # BUG: 在查找的时候会触发全局锁建议还是使用Process来检测
for custom_wrapper in wrappered_object.children():
if len(custom_wrapper.children()) == 1:
each_custom_wrapper = custom_wrapper.children()[0]
if len(each_custom_wrapper.children()) == 2:
if not procedure_name_found:
procedure_name_found = True
continue
task_wrapper = each_custom_wrapper.children()[0]
total_tasks.append(task_wrapper.window_text())
status_wrapper = each_custom_wrapper.children()[1]
status = status_wrapper.window_text()
if len(status) > 0:
executing_index = len(total_tasks) - 1
executing_status = status
try:
if self._guide_app is not None:
wrapper_object = self._guide_app.app.top_window().wrapper_object()
ui_path = get_ui_path_with_window_specification(wrapper_object)
with ui_path:
with UIPath("OK||Button"):
btn = find(timeout=1)
if btn is not None:
btn.set_focus()
click(btn, timeout=1)
self._is_executing_run = False
print("运行完成!")
except:
pass
self._executing_index = executing_index
self._executing_status = executing_status
self._total_tasks = total_tasks
return True
def initialize_instrument(self, force=False):
if not self._software_enabled:
print("Software is not opened")
self._success = False
return
if not self._http_service_available:
print("HTTP Server Not Available")
self._success = False
return
if self._is_initialized and not force:
print("Already Initialized")
self._success = True
return True
ins: SingleRunningExecutor = SingleRunningExecutor.get_instance(self.execute_initialize, lambda res, _: setattr(self, '_success', res))
if not ins.is_ended and ins.is_started:
print("Initialize is running")
self._success = False
return False
elif not ins.is_started:
print("Initialize started")
ins.start()
else: # 可能外面is_initialized被设置为False又进来重新初始化了
print("Initialize reset and started")
ins.reset()
ins.start()
return True
def execute_initialize(self, process=None) -> bool:
if process is None:
process = self._software_pid
try:
uiapp = connect_application(process=process)
ui_window: WindowSpecification = uiapp.app.top_window()
button = ui_window.child_window(title="xtpBarTop", class_name="XTPDockBar").child_window(
title="Standard", class_name="XTPToolBar").child_window(title="Initialize Instrument", control_type="Button")
focus_on_application(uiapp)
click(button.wrapper_object())
with get_ui_path_with_window_specification(ui_window):
with UIPath("Regex: (Initializing|Resetting|Perking).*||Window"):
# 检测窗口是否存在
for i in range(3):
try:
initializing_windows = exists(None, timeout=2)
break
except:
pass
print("window has recovered", initializing_windows)
time.sleep(5) # another wait
self._is_initialized = True
return True
except Exception as e:
print("An error occurred during initialization:")
traceback.print_exc()
return False
def __init__(self):
self._device_ip = "192.168.0.2"
# 启动所有监控器
self.checkers = [
ProcessChecker(self, 1),
HttpServiceChecker(self, 3),
RunStatusChecker(self, 1),
OkButtonChecker(self, 2) # 添加新的Checker
]
for checker in self.checkers:
checker.start_monitoring()
class DriverChecker(ud.DriverChecker):
driver: NivoDriver
class ProcessChecker(DriverChecker):
def check(self):
process_found, process_pid = get_process_pid_by_name("JANUS.exe", min_memory_mb=20)
self.driver._software_pid = process_pid
self.driver._software_enabled = process_found
if not process_found:
self.driver._is_initialized = False
class HttpServiceChecker(DriverChecker):
def check(self):
http_service_available = False
if self.driver.device_ip:
try:
response = requests.get(f"http://{self.driver.device_ip}", timeout=5)
http_service_available = response.status_code == 200
except requests.RequestException:
pass
self.driver._http_service_available = http_service_available
class RunStatusChecker(DriverChecker):
def check(self):
process_found, process_pid = get_process_pid_by_name("Guide.exe", min_memory_mb=20)
if not process_found:
self.driver._is_executing_run = False
return
self.driver.check_execute_run_status()
class OkButtonChecker(DriverChecker):
def check(self):
if not self.driver._is_executing_run or self.driver._guide_app is None:
return
# uiapp = connect_application(process=11276)
# self.driver._guide_app = uiapp
try:
ui_window: UIAWrapper = self.driver._guide_app.app.top_window()
btn: WindowSpecification = ui_window.child_window(title="OK", auto_id="2", control_type="Button")
if btn.exists(2):
click(btn.wrapper_object())
self.driver._is_executing_run = False
print("运行完成!")
except Exception as e:
# traceback.print_exc()
pass
# 示例用法
if __name__ == "__main__":
driver = NivoDriver()
driver.set_device_addr("192.168.0.2") # 设置设备 IP 地址
driver._is_executing_run = True

View File

@@ -0,0 +1,166 @@
import psutil
import pywinauto
from pywinauto_recorder import UIApplication
from pywinauto_recorder.player import UIPath, click, focus_on_application, exists, find, get_wrapper_path
from pywinauto.controls.uiawrapper import UIAWrapper
from pywinauto.application import WindowSpecification
from pywinauto import findbestmatch
import sys
import codecs
import os
import locale
import six
def connect_application(backend="uia", **kwargs):
app = pywinauto.Application(backend=backend)
app.connect(**kwargs)
top_window = app.top_window().wrapper_object()
native_window_handle = top_window.handle
return UIApplication(app, native_window_handle)
def get_ui_path_with_window_specification(obj):
return UIPath(get_wrapper_path(obj))
def get_process_pid_by_name(process_name: str, min_memory_mb: float = 0) -> tuple[bool, int]:
"""
通过进程名称和最小内存要求获取进程PID
Args:
process_name: 进程名称
min_memory_mb: 最小内存要求(MB)默认为0表示不检查内存
Returns:
tuple[bool, int]: (是否找到进程, 进程PID)
"""
process_found = False
process_pid = None
min_memory_bytes = min_memory_mb * 1024 * 1024 # 转换为字节
try:
for proc in psutil.process_iter(['name', 'pid', 'memory_info']):
try:
# 获取进程信息
proc_info = proc.info
if proc_info['name'] == process_name:
# 如果设置了内存限制,则检查内存
if min_memory_mb > 0:
memory_info = proc_info.get('memory_info')
if memory_info and memory_info.rss > min_memory_bytes:
process_found = True
process_pid = proc_info['pid']
break
else:
# 不检查内存,直接返回找到的进程
process_found = True
process_pid = proc_info['pid']
break
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
except Exception as e:
print(f"获取进程信息时发生错误: {str(e)}")
return process_found, process_pid
def print_wrapper_identifiers(wrapper_object, depth=None, filename=None):
"""
打印控件及其子控件的标识信息
Args:
wrapper_object: UIAWrapper对象
depth: 打印的最大深度,None表示打印全部
filename: 输出文件名,None表示打印到控制台
"""
if depth is None:
depth = sys.maxsize
# 创建所有控件的列表(当前控件及其所有子代)
all_ctrls = [wrapper_object, ] + wrapper_object.descendants()
# 创建所有可见文本控件的列表
txt_ctrls = [ctrl for ctrl in all_ctrls if ctrl.can_be_label and ctrl.is_visible() and ctrl.window_text()]
# 构建唯一的控件名称字典
name_ctrl_id_map = findbestmatch.UniqueDict()
for index, ctrl in enumerate(all_ctrls):
ctrl_names = findbestmatch.get_control_names(ctrl, all_ctrls, txt_ctrls)
for name in ctrl_names:
name_ctrl_id_map[name] = index
# 反转映射关系(控件索引到名称列表)
ctrl_id_name_map = {}
for name, index in name_ctrl_id_map.items():
ctrl_id_name_map.setdefault(index, []).append(name)
def print_identifiers(ctrls, current_depth=1, log_func=print):
"""递归打印控件及其子代的标识信息"""
if len(ctrls) == 0 or current_depth > depth:
return
indent = (current_depth - 1) * u" | "
for ctrl in ctrls:
try:
ctrl_id = all_ctrls.index(ctrl)
except ValueError:
continue
ctrl_text = ctrl.window_text()
if ctrl_text:
# 将多行文本转换为单行
ctrl_text = ctrl_text.replace('\n', r'\n').replace('\r', r'\r')
output = indent + u'\n'
output += indent + u"{class_name} - '{text}' {rect}\n"\
"".format(class_name=ctrl.friendly_class_name(),
text=ctrl_text,
rect=ctrl.rectangle())
output += indent + u'{}'.format(ctrl_id_name_map[ctrl_id])
title = ctrl_text
class_name = ctrl.class_name()
auto_id = None
control_type = None
if hasattr(ctrl.element_info, 'automation_id'):
auto_id = ctrl.element_info.automation_id
if hasattr(ctrl.element_info, 'control_type'):
control_type = ctrl.element_info.control_type
if control_type:
class_name = None # 如果有control_type就不需要class_name
else:
control_type = None # 如果control_type为空,仍使用class_name
criteria_texts = []
recorder_texts = []
if title:
criteria_texts.append(u'title="{}"'.format(title))
recorder_texts.append(f"{title}")
if class_name:
criteria_texts.append(u'class_name="{}"'.format(class_name))
if auto_id:
criteria_texts.append(u'auto_id="{}"'.format(auto_id))
if control_type:
criteria_texts.append(u'control_type="{}"'.format(control_type))
recorder_texts.append(f"||{control_type}")
if title or class_name or auto_id:
output += u'\n' + indent + u'child_window(' + u', '.join(criteria_texts) + u')' + " / " + "".join(recorder_texts)
if six.PY3:
log_func(output)
else:
log_func(output.encode(locale.getpreferredencoding(), errors='backslashreplace'))
print_identifiers(ctrl.children(), current_depth + 1, log_func)
if filename is None:
print("Control Identifiers:")
print_identifiers([wrapper_object, ])
else:
log_file = codecs.open(filename, "w", locale.getpreferredencoding())
def log_func(msg):
log_file.write(str(msg) + os.linesep)
log_func("Control Identifiers:")
print_identifiers([wrapper_object, ], log_func=log_func)
log_file.close()

View File

View File

@@ -0,0 +1,394 @@
import asyncio
from asyncio import Event, Future, Lock, Task
from enum import Enum
from dataclasses import dataclass
from typing import Any, Union, Optional, overload
import serial.tools.list_ports
from serial import Serial
from serial.serialutil import SerialException
class RunzeSyringePumpMode(Enum):
Normal = 0
AccuratePos = 1
AccuratePosVel = 2
pulse_freq_grades = {
6000: "0" ,
5600: "1" ,
5000: "2" ,
4400: "3" ,
3800: "4" ,
3200: "5" ,
2600: "6" ,
2200: "7" ,
2000: "8" ,
1800: "9" ,
1600: "10",
1400: "11",
1200: "12",
1000: "13",
800 : "14",
600 : "15",
400 : "16",
200 : "17",
190 : "18",
180 : "19",
170 : "20",
160 : "21",
150 : "22",
140 : "23",
130 : "24",
120 : "25",
110 : "26",
100 : "27",
90 : "28",
80 : "29",
70 : "30",
60 : "31",
50 : "32",
40 : "33",
30 : "34",
20 : "35",
18 : "36",
16 : "37",
14 : "38",
12 : "39",
10 : "40",
}
class RunzeSyringePumpConnectionError(Exception):
pass
@dataclass(frozen=True, kw_only=True)
class RunzeSyringePumpInfo:
port: str
address: str = "1"
volume: float = 25000
mode: RunzeSyringePumpMode = RunzeSyringePumpMode.Normal
def create(self):
return RunzeSyringePumpAsync(self.port, self.address, self.volume, self.mode)
class RunzeSyringePumpAsync:
def __init__(self, port: str, address: str = "1", volume: float = 25000, mode: RunzeSyringePumpMode = None):
self.port = port
self.address = address
self.volume = volume
self.mode = mode
self.total_steps = self.total_steps_vel = 6000
try:
self._serial = Serial(
baudrate=9600,
port=port
)
except (OSError, SerialException) as e:
raise RunzeSyringePumpConnectionError from e
self._busy = False
self._closing = False
self._error_event = Event()
self._query_future = Future[Any]()
self._query_lock = Lock()
self._read_task: Optional[Task[None]] = None
self._run_future: Optional[Future[Any]] = None
self._run_lock = Lock()
def _adjust_total_steps(self):
self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000
self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000
async def _read_loop(self):
try:
while True:
self._receive((await asyncio.to_thread(lambda: self._serial.read_until(b"\n")))[3:-3])
except SerialException as e:
raise RunzeSyringePumpConnectionError from e
finally:
if not self._closing:
self._error_event.set()
if self._query_future and not self._query_future.done():
self._query_future.set_exception(RunzeSyringePumpConnectionError())
if self._run_future and not self._run_future.done():
self._run_future.set_exception(RunzeSyringePumpConnectionError())
@overload
async def _query(self, command: str, dtype: type[bool]) -> bool:
pass
@overload
async def _query(self, command: str, dtype: type[int]) -> int:
pass
@overload
async def _query(self, command: str, dtype = None) -> str:
pass
async def _query(self, command: str, dtype: Optional[type] = None):
async with self._query_lock:
if self._closing or self._error_event.is_set():
raise RunzeSyringePumpConnectionError
self._query_future = Future[Any]()
run = 'R' if not command.startswith("?") else ''
full_command = f"/{self.address}{command}{run}\r\n"
full_command_data = bytearray(full_command, 'ascii')
try:
await asyncio.to_thread(lambda: self._serial.write(full_command_data))
return self._parse(await asyncio.wait_for(asyncio.shield(self._query_future), timeout=2.0), dtype=dtype)
except (SerialException, asyncio.TimeoutError) as e:
self._error_event.set()
raise RunzeSyringePumpConnectionError from e
finally:
self._query_future = None
def _parse(self, data: bytes, dtype: Optional[type] = None):
response = data.decode()
if dtype == bool:
return response == "1"
elif dtype == int:
return int(response)
else:
return response
def _receive(self, data: bytes):
ascii_string = "".join(chr(byte) for byte in data)
was_busy = self._busy
self._busy = ((data[0] & (1 << 5)) < 1) or ascii_string.startswith("@")
if self._run_future and was_busy and not self._busy:
self._run_future.set_result(data)
if self._query_future:
self._query_future.set_result(data)
else:
raise Exception("Dropping data")
async def _run(self, command: str):
async with self._run_lock:
self._run_future = Future[Any]()
try:
await self._query(command)
while True:
await asyncio.sleep(0.5) # Wait for 0.5 seconds before polling again
status = await self.query_device_status()
if status == '`':
break
await asyncio.shield(self._run_future)
finally:
self._run_future = None
async def initialize(self):
response = await self._run("Z")
if self.mode:
self.set_step_mode(self.mode)
else:
# self.mode = RunzeSyringePumpMode.Normal
# # self.set_step_mode(self.mode)
self.mode = await self.query_step_mode()
return response
# Settings
async def set_baudrate(self, baudrate):
if baudrate == 9600:
return await self._run("U41")
elif baudrate == 38400:
return await self._run("U47")
else:
raise ValueError("Unsupported baudrate")
# Mode Settings and Queries
async def set_step_mode(self, mode: RunzeSyringePumpMode):
self.mode = mode
self._adjust_total_steps()
command = f"N{mode.value}"
return await self._run(command)
async def query_step_mode(self):
response = await self._query("?28")
status, mode = response[0], int(response[1])
self.mode = RunzeSyringePumpMode._value2member_map_[mode]
self._adjust_total_steps()
return self.mode
# Speed Settings and Queries
async def set_speed_grade(self, speed: Union[int, str]):
return await self._run(f"S{speed}")
async def set_speed_max(self, speed: float):
pulse_freq = int(speed / self.volume * self.total_steps_vel)
pulse_freq = min(6000, pulse_freq)
return await self._run(f"V{speed}")
async def query_speed_grade(self):
pulse_freq, speed = await self.query_speed_max()
g = "-1"
for freq, grade in pulse_freq_grades.items():
if pulse_freq >= freq:
g = grade
break
return g
async def query_speed_init(self):
response = await self._query("?1")
status, pulse_freq = response[0], int(response[1:])
speed = pulse_freq / self.total_steps_vel * self.volume
return pulse_freq, speed
async def query_speed_max(self):
response = await self._query("?2")
status, pulse_freq = response[0], int(response[1:])
speed = pulse_freq / self.total_steps_vel * self.volume
return pulse_freq, speed
async def query_speed_end(self):
response = await self._query("?3")
status, pulse_freq = response[0], int(response[1:])
speed = pulse_freq / self.total_steps_vel * self.volume
return pulse_freq, speed
# Operations
# Valve Setpoint and Queries
async def set_valve_position(self, position: Union[int, str]):
command = f"I{position}" if type(position) == int or ord(position) <= 57 else position.upper()
return await self._run(command)
async def query_valve_position(self):
response = await self._query("?6")
status, pos_valve = response[0], response[1].upper()
return pos_valve
# Plunger Setpoint and Queries
async def move_plunger_to(self, volume: float):
"""
Move to absolute volume (unit: μL)
Args:
volume (float): absolute position of the plunger, unit: μL
Returns:
None
"""
pos_step = int(volume / self.volume * self.total_steps)
return await self._run(f"A{pos_step}")
async def pull_plunger(self, volume: float):
"""
Pull a fixed volume (unit: μL)
Args:
volume (float): absolute position of the plunger, unit: μL
Returns:
None
"""
pos_step = int(volume / self.volume * self.total_steps)
return await self._run(f"P{pos_step}")
async def push_plunger(self, volume: float):
"""
Push a fixed volume (unit: μL)
Args:
volume (float): absolute position of the plunger, unit: μL
Returns:
None
"""
pos_step = int(volume / self.volume * self.total_steps)
return await self._run(f"D{pos_step}")
async def report_position(self):
response = await self._query("?0")
status, pos_step = response[0], int(response[1:])
return pos_step / self.total_steps * self.volume
async def query_plunger_position(self):
response = await self._query("?4")
status, pos_step = response[0], int(response[1:])
return pos_step / self.total_steps * self.volume
async def stop_operation(self):
return await self._run("T")
# Queries
async def query_device_status(self):
return await self._query("Q")
async def query_command_buffer_status(self):
return await self._query("?10")
async def query_backlash_position(self):
return await self._query("?12")
async def query_aux_input_status_1(self):
return await self._query("?13")
async def query_aux_input_status_2(self):
return await self._query("?14")
async def query_software_version(self):
return await self._query("?23")
async def wait_error(self):
await self._error_event.wait()
async def __aenter__(self):
await self.open()
return self
async def __aexit__(self, exc_type, exc, tb):
await self.close()
async def open(self):
if self._read_task:
raise RunzeSyringePumpConnectionError
self._read_task = asyncio.create_task(self._read_loop())
try:
await self.query_device_status()
except Exception:
await self.close()
raise
async def close(self):
if self._closing or not self._read_task:
raise RunzeSyringePumpConnectionError
self._closing = True
self._read_task.cancel()
try:
await self._read_task
except asyncio.CancelledError:
pass
finally:
del self._read_task
self._serial.close()
@staticmethod
def list():
for item in serial.tools.list_ports.comports():
yield RunzeSyringePumpInfo(port=item.device)

View File

@@ -0,0 +1,388 @@
import asyncio
from threading import Lock, Event
from enum import Enum
from dataclasses import dataclass
import time
from typing import Any, Union, Optional, overload
import serial.tools.list_ports
from serial import Serial
from serial.serialutil import SerialException
class RunzeSyringePumpMode(Enum):
Normal = 0
AccuratePos = 1
AccuratePosVel = 2
pulse_freq_grades = {
6000: "0" ,
5600: "1" ,
5000: "2" ,
4400: "3" ,
3800: "4" ,
3200: "5" ,
2600: "6" ,
2200: "7" ,
2000: "8" ,
1800: "9" ,
1600: "10",
1400: "11",
1200: "12",
1000: "13",
800 : "14",
600 : "15",
400 : "16",
200 : "17",
190 : "18",
180 : "19",
170 : "20",
160 : "21",
150 : "22",
140 : "23",
130 : "24",
120 : "25",
110 : "26",
100 : "27",
90 : "28",
80 : "29",
70 : "30",
60 : "31",
50 : "32",
40 : "33",
30 : "34",
20 : "35",
18 : "36",
16 : "37",
14 : "38",
12 : "39",
10 : "40",
}
class RunzeSyringePumpConnectionError(Exception):
pass
@dataclass(frozen=True, kw_only=True)
class RunzeSyringePumpInfo:
port: str
address: str = "1"
max_volume: float = 25.0
mode: RunzeSyringePumpMode = RunzeSyringePumpMode.Normal
def create(self):
return RunzeSyringePump(self.port, self.address, self.max_volume, self.mode)
class RunzeSyringePump:
def __init__(self, port: str, address: str = "1", max_volume: float = 25.0, mode: RunzeSyringePumpMode = None):
self.port = port
self.address = address
self.max_volume = max_volume
self.total_steps = self.total_steps_vel = 6000
self._status = "Idle"
self._mode = mode
self._max_velocity = 0
self._valve_position = "I"
self._position = 0
try:
# if port in serial_ports and serial_ports[port].is_open:
# self.hardware_interface = serial_ports[port]
# else:
# serial_ports[port] = self.hardware_interface = Serial(
# baudrate=9600,
# port=port
# )
self.hardware_interface = Serial(
baudrate=9600,
port=port
)
except (OSError, SerialException) as e:
# raise RunzeSyringePumpConnectionError from e
self.hardware_interface = port
self._busy = False
self._closing = False
self._error_event = Event()
self._query_lock = Lock()
self._run_lock = Lock()
def _adjust_total_steps(self):
self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000
self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000
def send_command(self, full_command: str):
full_command_data = bytearray(full_command, 'ascii')
response = self.hardware_interface.write(full_command_data)
time.sleep(0.05)
output = self._receive(self.hardware_interface.read_until(b"\n"))
return output
def _query(self, command: str):
with self._query_lock:
if self._closing:
raise RunzeSyringePumpConnectionError
run = 'R' if not "?" in command else ''
full_command = f"/{self.address}{command}{run}\r\n"
output = self.send_command(full_command)[3:-3]
return output
def _parse(self, data: bytes, dtype: Optional[type] = None):
response = data.decode()
if dtype == bool:
return response == "1"
elif dtype == int:
return int(response)
else:
return response
def _receive(self, data: bytes):
ascii_string = "".join(chr(byte) for byte in data)
was_busy = self._busy
self._busy = ((data[0] & (1 << 5)) < 1) or ascii_string.startswith("@")
return ascii_string
def _run(self, command: str):
with self._run_lock:
try:
response = self._query(command)
while True:
time.sleep(0.5) # Wait for 0.5 seconds before polling again
status = self.get_status()
if status == 'Idle':
break
finally:
pass
return response
def initialize(self):
print("Initializing Runze Syringe Pump")
response = self._run("Z")
# if self.mode:
# self.set_mode(self.mode)
# else:
# # self.mode = RunzeSyringePumpMode.Normal
# # self.set_mode(self.mode)
# self.mode = self.get_mode()
return response
# Settings
def set_baudrate(self, baudrate):
if baudrate == 9600:
return self._run("U41")
elif baudrate == 38400:
return self._run("U47")
else:
raise ValueError("Unsupported baudrate")
# Device Status
@property
def status(self) -> str:
return self._status
def _standardize_status(self, status_raw):
return "Idle" if status_raw == "`" else "Busy"
def get_status(self):
status_raw = self._query("Q")
self._status = self._standardize_status(status_raw)
return self._status
# Mode Settings and Queries
@property
def mode(self) -> int:
return self._mode
# def set_mode(self, mode: RunzeSyringePumpMode):
# self.mode = mode
# self._adjust_total_steps()
# command = f"N{mode.value}"
# return self._run(command)
# def get_mode(self):
# response = self._query("?28")
# status_raw, mode = response[0], int(response[1])
# self.mode = RunzeSyringePumpMode._value2member_map_[mode]
# self._adjust_total_steps()
# return self.mode
# Speed Settings and Queries
@property
def max_velocity(self) -> float:
return self._max_velocity
def set_max_velocity(self, velocity: float):
self._max_velocity = velocity
pulse_freq = int(velocity / self.max_volume * self.total_steps_vel)
pulse_freq = min(6000, pulse_freq)
return self._run(f"V{pulse_freq}")
def get_max_velocity(self):
response = self._query("?2")
status_raw, pulse_freq = response[0], int(response[1:])
self._status = self._standardize_status(status_raw)
self._max_velocity = pulse_freq / self.total_steps_vel * self.max_volume
return self._max_velocity
def set_velocity_grade(self, velocity: Union[int, str]):
return self._run(f"S{velocity}")
def get_velocity_grade(self):
response = self._query("?2")
status_raw, pulse_freq = response[0], int(response[1:])
g = "-1"
for freq, grade in pulse_freq_grades.items():
if pulse_freq >= freq:
g = grade
break
return g
def get_velocity_init(self):
response = self._query("?1")
status_raw, pulse_freq = response[0], int(response[1:])
self._status = self._standardize_status(status_raw)
velocity = pulse_freq / self.total_steps_vel * self.max_volume
return pulse_freq, velocity
def get_velocity_end(self):
response = self._query("?3")
status_raw, pulse_freq = response[0], int(response[1:])
self._status = self._standardize_status(status_raw)
velocity = pulse_freq / self.total_steps_vel * self.max_volume
return pulse_freq, velocity
# Operations
# Valve Setpoint and Queries
@property
def valve_position(self) -> str:
return self._valve_position
def set_valve_position(self, position: Union[int, str, float]):
if type(position) == float:
position = round(position / 120)
command = f"I{position}" if type(position) == int or ord(position) <= 57 else position.upper()
response = self._run(command)
self._valve_position = f"{position}" if type(position) == int or ord(position) <= 57 else position.upper()
return response
def get_valve_position(self) -> str:
response = self._query("?6")
status_raw, pos_valve = response[0], response[1].upper()
self._valve_position = pos_valve
self._status = self._standardize_status(status_raw)
return pos_valve
# Plunger Setpoint and Queries
@property
def position(self) -> float:
return self._position
def get_position(self):
response = self._query("?0")
status_raw, pos_step = response[0], int(response[1:])
self._status = self._standardize_status(status_raw)
return pos_step / self.total_steps * self.max_volume
def set_position(self, position: float, max_velocity: float = None):
"""
Move to absolute volume (unit: ml)
Args:
position (float): absolute position of the plunger, unit: ml
max_velocity (float): maximum velocity of the plunger, unit: ml/s
Returns:
None
"""
if max_velocity is not None:
self.set_max_velocity(max_velocity)
pulse_freq = int(max_velocity / self.max_volume * self.total_steps_vel)
pulse_freq = min(6000, pulse_freq)
velocity_cmd = f"V{pulse_freq}"
else:
velocity_cmd = ""
pos_step = int(position / self.max_volume * self.total_steps)
return self._run(f"{velocity_cmd}A{pos_step}")
def pull_plunger(self, volume: float):
"""
Pull a fixed volume (unit: ml)
Args:
volume (float): absolute position of the plunger, unit: mL
Returns:
None
"""
pos_step = int(volume / self.max_volume * self.total_steps)
return self._run(f"P{pos_step}")
def push_plunger(self, volume: float):
"""
Push a fixed volume (unit: ml)
Args:
volume (float): absolute position of the plunger, unit: mL
Returns:
None
"""
pos_step = int(volume / self.max_volume * self.total_steps)
return self._run(f"D{pos_step}")
def get_plunger_position(self):
response = self._query("?4")
status, pos_step = response[0], int(response[1:])
return pos_step / self.total_steps * self.max_volume
def stop_operation(self):
return self._run("T")
# Queries
def query_command_buffer_status(self):
return self._query("?10")
def query_backlash_position(self):
return self._query("?12")
def query_aux_input_status_1(self):
return self._query("?13")
def query_aux_input_status_2(self):
return self._query("?14")
def query_software_version(self):
return self._query("?23")
def wait_error(self):
self._error_event.wait()
def close(self):
if self._closing:
raise RunzeSyringePumpConnectionError
self._closing = True
self.hardware_interface.close()
@staticmethod
def list():
for item in serial.tools.list_ports.comports():
yield RunzeSyringePumpInfo(port=item.device)

View File

@@ -0,0 +1,51 @@
import time
import serial
class SolenoidValve:
def __init__(self, io_device_port: str):
self._status = "Idle"
self._valve_position = "OPEN"
self.io_device_port = io_device_port
try:
self.hardware_interface = serial.Serial(str(io_device_port), 9600, timeout=1)
except serial.SerialException:
# raise Exception(f"Failed to connect to the device at {io_device_port}")
self.hardware_interface = str(io_device_port)
@property
def status(self) -> str:
return self._status
@property
def valve_position(self) -> str:
return self._valve_position
def send_command(self, command):
self.hardware_interface.write(command)
def read_data(self):
return self.hardware_interface.read()
def get_valve_position(self) -> str:
self._valve_position = "OPEN" if self.read_data() else "CLOSED"
return self._valve_position
def set_valve_position(self, position):
self._status = "Busy"
self.send_command(1 if position == "OPEN" else 0)
time.sleep(5)
self._status = "Idle"
def open(self):
self._valve_position = "OPEN"
def close(self):
self._valve_position = "CLOSED"
def is_open(self):
return self._valve_position
def is_closed(self):
return not self._valve_position

View File

@@ -0,0 +1,38 @@
import time
class SolenoidValveMock:
def __init__(self, port: str = "COM6"):
self._status = "Idle"
self._valve_position = "OPEN"
@property
def status(self) -> str:
return self._status
@property
def valve_position(self) -> str:
return self._valve_position
def get_valve_position(self) -> str:
return self._valve_position
def set_valve_position(self, position):
self._status = "Busy"
time.sleep(5)
self._valve_position = position
time.sleep(5)
self._status = "Idle"
def open(self):
self._valve_position = "OPEN"
def close(self):
self._valve_position = "CLOSED"
def is_open(self):
return self._valve_position
def is_closed(self):
return not self._valve_position

View File

@@ -0,0 +1,31 @@
import time
class VacuumPumpMock:
def __init__(self, port: str = "COM6"):
self._status = "OPEN"
@property
def status(self) -> str:
return self._status
def get_status(self) -> str:
return self._status
def set_status(self, position):
time.sleep(5)
self._status = position
time.sleep(5)
def open(self):
self._status = "OPEN"
def close(self):
self._status = "CLOSED"
def is_open(self):
return self._status
def is_closed(self):
return not self._status

View File

View File

@@ -0,0 +1,198 @@
import json
import serial
import struct
import crcmod
import tkinter as tk
from tkinter import messagebox
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import time
class RamanObj:
def __init__(self, port_laser,port_ccd, baudrate_laser=9600, baudrate_ccd=921600):
self.port_laser = port_laser
self.port_ccd = port_ccd
self.baudrate_laser = baudrate_laser
self.baudrate_ccd = baudrate_ccd
self.success = False
self.status = "None"
self.ser_laser = serial.Serial( self.port_laser,
self.baudrate_laser,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1)
self.ser_ccd = serial.Serial( self.port_ccd,
self.baudrate_ccd,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1)
def laser_on_power(self, output_voltage_laser):
def calculate_crc(data):
"""
计算Modbus CRC校验码
"""
crc16 = crcmod.predefined.mkCrcFun('modbus')
return crc16(data)
device_id = 0x01 # 从机地址
register_address = 0x0298 # 寄存器地址
output_voltage = int(output_voltage_laser) # 输出值
# 将输出值转换为需要发送的格式
output_value = int(output_voltage) # 确保是整数
high_byte = (output_value >> 8) & 0xFF
low_byte = output_value & 0xFF
# 构造Modbus RTU数据包
function_code = 0x06 # 写单个寄存器
message = struct.pack('>BBHH', device_id, function_code, register_address, output_value)
crc = calculate_crc(message)
crc_bytes = struct.pack('<H', crc)
full_message = message + crc_bytes
try:
# 打开串口
self.ser_laser.write(full_message)
# 读取响应
response = self.ser_laser.read(8) # Modbus响应通常为8字节
print(f"接收到的响应: {response.hex().upper()}")
# 校验响应CRC
if len(response) >= 5:
response_crc = calculate_crc(response[:-2])
received_crc = struct.unpack('<H', response[-2:])[0]
if response_crc == received_crc:
print("响应CRC校验成功")
else:
print("响应CRC校验失败")
else:
print("响应长度不足,无法校验")
except Exception as e:
print(f"发生错误: {e}")
return
def ccd_time(self, int_time):
# 发送命令
def send_command(ser, command):
try:
ser.write(command)
print(f"\u53d1\u9001: {command.hex()}")
except Exception as e:
print(f"\u53d1\u9001\u5931\u8d25: {e}")
# 接收数据
def receive_data(ser, expected_bytes):
try:
data = ser.read(expected_bytes)
print(f"\u63a5\u6536: {data.hex()}")
return data
except Exception as e:
print(f"\u63a5\u6536\u5931\u8d25: {e}")
return None
# 读取CCD数据并绘图
def read_and_plot(serial_port, int_time):
# 发送数据清空指令
clear_command = bytes([0xAA, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x55])
send_command(serial_port, clear_command)
time.sleep(0.1)
clear_command = bytes([0xAA, 0x10, 0x00, 0x00, 0x00, 0x00, 0x10, 0x55])
send_command(serial_port, clear_command)
receive_data(serial_port, 4136)
time.sleep(0.1)
clear_command = bytes([0xAA, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x55])
send_command(serial_port, clear_command)
time.sleep(0.1)
# 发送数据读取指令
clear_command = bytes([0xAA, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x55])
send_command(serial_port, clear_command)
int_time = int(int_time)
time.sleep(int_time)
read_command = bytes([0xAA, 0x10, 0x00, 0x00, 0x00, 0x00, 0x10, 0x55])
send_command(serial_port, read_command)
# 接收 4136 字节的数据
data = receive_data(serial_port, 4136)
if not data or len(data) != 4136:
messagebox.showerror("\u9519\u8bef", "\u63a5\u6536\u6570\u636e\u5931\u8d25\u6216\u6570\u636e\u957f\u5ea6\u4e0d\u6b63\u786e")
return
# 将接收到的字节数据转换为 16 位无符号整型数组
values = [struct.unpack(">H", data[i:i + 2])[0] for i in range(0, len(data), 2)]
return values
try:
ser = self.ser_ccd
values = read_and_plot(ser, int_time) # 修正传递serial对象
print(f"\u8fd4\u56de\u503c: {values}")
return values
except Exception as e:
messagebox.showerror("\u9519\u8bef", f"\u4e32\u53e3\u521d\u59cb\u5316\u5931\u8d25: {e}")
return None
def raman_without_background(self,int_time, laser_power):
self.laser_on_power(0)
time.sleep(0.1)
ccd_data_background = self.ccd_time(int_time)
time.sleep(0.1)
self.laser_on_power(laser_power)
time.sleep(0.2)
ccd_data_total = self.ccd_time(int_time)
self.laser_on_power(0)
ccd_data = [x - y for x, y in zip(ccd_data_total, ccd_data_background)]
return ccd_data
def raman_without_background_average(self, sample_name , int_time, laser_power, average):
ccd_data = [0] * 4136
for i in range(average):
ccd_data_temp = self.raman_without_background(int_time, laser_power)
ccd_data = [x + y for x, y in zip(ccd_data, ccd_data_temp)]
ccd_data = [x / average for x in ccd_data]
#保存数据 用时间命名
t = time.strftime("%Y-%m-%d-%H-%M-%S-%MS", time.localtime())
folder_path = r"C:\auto\raman_data"
with open(f'{folder_path}/{sample_name}_{t}.txt', 'w') as f:
for i in ccd_data:
f.write(str(i) + '\n')
return ccd_data
def raman_cmd(self, command:str):
self.success = False
try:
cmd_dict = json.loads(command)
ccd_data = self.raman_without_background_average(**cmd_dict)
self.success = True
# return ccd_data
except Exception as e:
# return None
raise f"error: {e}"
if __name__ == "__main__":
raman = RamanObj(port_laser='COM20', port_ccd='COM2')
ccd_data = raman.raman_without_background_average('test44',0.5,3000,1)
plt.plot(ccd_data)
plt.xlabel("Pixel")
plt.ylabel("Intensity")
plt.title("Raman Spectrum")
plt.show()

View File

View File

@@ -0,0 +1,64 @@
import json
from serial import Serial
import time as time_
import threading
class RotavapOne:
def __init__(self, port, rate=9600):
self.ser = Serial(port,rate)
self.pump_state = 'h'
self.pump_time = 0
self.rotate_state = 'h'
self.rotate_time = 0
self.success = True
if not self.ser.is_open:
self.ser.open()
# self.main_loop()
t = threading.Thread(target=self.main_loop ,daemon=True)
t.start()
def cmd_write(self, cmd):
self.ser.write(cmd)
self.ser.read_all()
def set_rotate_time(self, time):
self.success = False
self.rotate_time = time
self.success = True
def set_pump_time(self, time):
self.success = False
self.pump_time = time
self.success = True
def set_timer(self, command):
self.success = False
timer = json.loads(command)
rotate_time = timer['rotate_time']
pump_time = timer['pump_time']
self.rotate_time = rotate_time
self.pump_time = pump_time
self.success = True
def main_loop(self):
param = ['rotate','pump']
while True:
for i in param:
if getattr(self, f'{i}_time') <= 0:
setattr(self, f'{i}_state','l')
else:
setattr(self, f'{i}_state','h')
setattr(self, f'{i}_time',getattr(self, f'{i}_time')-1)
cmd = f'1{self.pump_state}2{self.rotate_state}3l4l\n'
self.cmd_write(cmd.encode())
time_.sleep(1)
if __name__ == '__main__':
ro = RotavapOne(port='COM15')

View File

View File

@@ -0,0 +1,156 @@
import json
import serial
import time as systime
import threading
class SeparatorController:
"""Controls the operation of a separator device using serial communication.
This class manages the interaction with both an executor and a sensor through serial ports, allowing for stirring, settling, and extracting operations based on sensor data.
Args:
port_executor (str): The serial port for the executor device.
port_sensor (str): The serial port for the sensor device.
baudrate_executor (int, optional): The baud rate for the executor device. Defaults to 115200.
baudrate_sensor (int, optional): The baud rate for the sensor device. Defaults to 115200.
Attributes:
serial_executor (Serial): The serial connection to the executor device.
serial_sensor (Serial): The serial connection to the sensor device.
sensordata (float): The latest sensor data read from the sensor device.
success (bool): Indicates whether the last operation was successful.
status (str): The current status of the controller, which can be 'Idle', 'Stirring', 'Settling', or 'Extracting'.
"""
def __init__(
self,
port_executor: str,
port_sensor: str,
baudrate_executor: int = 115200,
baudrate_sensor: int = 115200,
):
self.serial_executor = serial.Serial(port_executor, baudrate_executor)
self.serial_sensor = serial.Serial(port_sensor, baudrate_sensor)
if not self.serial_executor.is_open:
self.serial_executor.open()
if not self.serial_sensor.is_open:
self.serial_sensor.open()
self.sensordata = 0.00
self.success = False
self.status = "Idle" # 'Idle', 'Stirring' ,'Settling' , 'Extracting',
systime.sleep(2)
t = threading.Thread(target=self.read_sensor_loop, daemon=True)
t.start()
def write(self, data):
self.serial_executor.write(data)
a = self.serial_executor.read_all()
def stir(self, stir_time: float = 10, stir_speed: float = 300, settling_time: float = 10):
"""Controls the stirring operation of the separator.
This function initiates a stirring process for a specified duration and speed, followed by a settling phase. It updates the status of the controller and communicates with the executor to perform the stirring action.
Args:
stir_time (float, optional): The duration for which to stir, in seconds. Defaults to 10.
stir_speed (float, optional): The speed of stirring, in RPM. Defaults to 300.
settling_time (float, optional): The duration for which to settle after stirring, in seconds. Defaults to 10.
Returns:
None
"""
self.success = False
start_time = systime.time()
self.status = "Stirring"
stir_speed_second = stir_speed / 60
cmd = f"G91 Z{stir_speed_second}\n"
cmd_data = bytearray(cmd, "ascii")
self.write(bytearray(f"$112={stir_speed_second}", "ascii"))
while self.status != "Idle":
# print(self.sensordata)
if self.status == "Stirring":
if systime.time() - start_time < stir_time:
self.write(cmd_data)
systime.sleep(1)
else:
self.status = "Settling"
start_time = systime.time()
elif self.status == "Settling":
if systime.time() - start_time > settling_time:
break
self.success = True
def valve_open(self, condition, value):
"""Opens the valve, then wait to close the valve based on a specified condition.
This function sends a command to open the valve and continuously monitors the sensor data until the specified condition is met. Once the condition is satisfied, it closes the valve and updates the status of the controller.
Args:
condition (str): The condition to be monitored, either 'delta' or 'time'.
value (float): The threshold value for the condition.
`delta > 0.05`, `time > 60`
Returns:
None
"""
if condition not in ["delta", "time"]:
raise ValueError("Invalid condition")
elif condition == "delta":
valve_position = 0.66
else:
valve_position = 0.8
self.write((f"G91 X{valve_position}\n").encode())
last = self.sensordata
start_time = systime.time()
while True:
data = self.sensordata
delta = abs(data - last)
time = systime.time() - start_time
if eval(f"{condition} > {value}"):
break
last = data
systime.sleep(0.05)
self.status = "Idle"
self.write((f"G91 X-{valve_position}\n").encode())
def valve_open_cmd(self,command:str):
self.success = False
try:
cmd_dict = json.loads(command)
self.valve_open(**cmd_dict)
self.success = True
except Exception as e:
raise f"error: {e}"
def read_sensor_loop(self):
while True:
msg = self.serial_sensor.readline()
ascii_string = "".join(chr(byte) for byte in msg)
# print(msg)
if ascii_string.startswith("A3"):
try:
self.sensordata = float(ascii_string.split(":")[1])
except:
return
self.serial_sensor.read_all()
systime.sleep(0.05)
if __name__ == "__main__":
e = SeparatorController(port_sensor="COM40", port_executor="COM41")
print(e.status)
e.stir(10, 720, 10)
e.valve_open("delta", 0.3)

View File

View File

@@ -0,0 +1,67 @@
import json
import serial
class Chiller():
# def xuanzheng_temp_set(tempset: int):
# 设置目标温度
def __init__(self, port,rate=9600):
self.T_set = 24
self.success = True
self.ser = serial.Serial(
port=port,
baudrate=rate,
bytesize=8,
parity='N',
stopbits=1,
timeout=1
)
def modbus_crc(self, data: bytes) -> bytes:
crc = 0xFFFF
for pos in data:
crc ^= pos
for _ in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc.to_bytes(2, byteorder='little')
def build_modbus_frame(self,device_address: int, function_code: int, register_address: int, value: int) -> bytes:
frame = bytearray([
device_address,
function_code,
(register_address >> 8) & 0xFF,
register_address & 0xFF,
(value >> 8) & 0xFF,
value & 0xFF
])
crc = self.modbus_crc(frame)
return frame + crc
def convert_temperature_to_modbus_value(self, temperature: float, decimal_points: int = 1) -> int:
factor = 10 ** decimal_points
value = int(temperature * factor)
return value & 0xFFFF
def set_temperature(self, command):
T_set = json.loads(command)['temperature']
self.T_set = int(T_set)
self.success = False
temperature_value = self.convert_temperature_to_modbus_value(self.T_set, decimal_points=1)
device_address = 0x01
function_code = 0x06
register_address = 0x000B
frame = self.build_modbus_frame(device_address, function_code, register_address, temperature_value)
self.ser.write(frame)
response = self.ser.read(8)
self.success = True
def stop(self):
self.set_temperature(25)
if __name__ == '__main__':
ch = Chiller(port='COM17')
ch.set_temperature(20)

View File

@@ -0,0 +1,69 @@
import serial
import struct
class TempSensor:
def __init__(self,port,baudrate=9600):
# 配置串口
self.ser = serial.Serial(
port=port,
baudrate=baudrate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1
)
def calculate_crc(self,data):
crc = 0xFFFF
for pos in data:
crc ^= pos
for i in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc
def build_modbus_request(self, device_id, function_code, register_address, register_count):
request = struct.pack('>BBHH', device_id, function_code, register_address, register_count)
crc = self.calculate_crc(request)
request += struct.pack('<H', crc)
return request
def read_modbus_response(self,response):
if len(response) < 5:
return None
data = response[:-2]
crc_received = struct.unpack('<H', response[-2:])[0]
crc_calculated = self.calculate_crc(data)
if crc_received == crc_calculated:
return data[3:]
return None
def send_command(self ,command):
function_code = 0x04 # 读输入寄存器
register_address = 0x0003 # 温度高位寄存器地址
register_count = 0x0002 # 读取两个寄存器
request = self.build_modbus_request(command, function_code, register_address, register_count)
self.ser.write(request)
response = self.ser.read(9) # 读取9字节的响应
data = self.read_modbus_response(response)
if data is None:
return None
high_value = struct.unpack('>H', data[:2])[0]
low_value = struct.unpack('>H', data[2:])[0]
# 组合高位和低位并计算实际温度
raw_temperature = (high_value << 16) | low_value
if raw_temperature & 0x8000: # 如果低位寄存器最高位为1表示负值
raw_temperature -= 0x10000 # 转换为正确的负数表示
actual_temperature = raw_temperature / 10.0
return actual_temperature

View File

@@ -0,0 +1,90 @@
import serial
import time
import struct
import tkinter as tk
from tkinter import ttk
# 配置串口
ser = serial.Serial(
port='COM13',
baudrate=9600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1
)
def calculate_crc(data):
crc = 0xFFFF
for pos in data:
crc ^= pos
for i in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc
def build_modbus_request(device_id, function_code, register_address, register_count):
request = struct.pack('>BBHH', device_id, function_code, register_address, register_count)
crc = calculate_crc(request)
request += struct.pack('<H', crc)
return request
def read_modbus_response(response):
if len(response) < 5:
return None
data = response[:-2]
crc_received = struct.unpack('<H', response[-2:])[0]
crc_calculated = calculate_crc(data)
if crc_received == crc_calculated:
return data[3:]
return None
def read_temperature():
device_id = 0xC0 # 设备地址
function_code = 0x04 # 读输入寄存器
register_address = 0x0003 # 温度高位寄存器地址
register_count = 0x0002 # 读取两个寄存器
request = build_modbus_request(device_id, function_code, register_address, register_count)
ser.write(request)
response = ser.read(9) # 读取9字节的响应
data = read_modbus_response(response)
if data is None:
return None
high_value = struct.unpack('>H', data[:2])[0]
low_value = struct.unpack('>H', data[2:])[0]
# 组合高位和低位并计算实际温度
raw_temperature = (high_value << 16) | low_value
if raw_temperature & 0x8000: # 如果低位寄存器最高位为1表示负值
raw_temperature -= 0x10000 # 转换为正确的负数表示
actual_temperature = raw_temperature / 10.0
return actual_temperature
def update_temperature_label():
temperature = read_temperature()
if temperature is not None:
temperature_label.config(text=f"反应温度: {temperature:.1f} °C")
else:
temperature_label.config(text="Error reading temperature")
root.after(1000, update_temperature_label) # 每秒更新一次
# 创建主窗口
root = tk.Tk()
root.title("反应温度监控")
# 创建标签来显示温度
temperature_label = ttk.Label(root, text="反应温度: -- °C", font=("Helvetica", 20))
temperature_label.pack(padx=20, pady=20)
# 开始更新温度
update_temperature_label()
# 运行主循环
root.mainloop()

View File

@@ -0,0 +1,117 @@
import json
import threading,time
# class TempSensorNode:
# def __init__(self,port,warning,address):
# self._value = 0.0
# self.warning = warning
# self.device_id = address
# self.hardware_interface = port
# # t = threading.Thread(target=self.read_temperature, daemon=True)
# # t.start()
# def send_command(self ,command):
# print('send_command---------------------')
# pass
# @property
# def value(self) -> float:
# self._value = self.send_command(self.device_id)
# return self._value
# # def read_temperature(self):
# # while True:
# # self.value = self.send_command(self.device_id)
# # print(self.value,'-----------')
# # time.sleep(1)
# def set_warning(self, warning_temp):
# self.warning = warning_temp
import serial
import struct
from rclpy.node import Node
import rclpy
import threading
class TempSensorNode():
def __init__(self,port,warning,address,baudrate=9600):
self._value = 0.0
self.warning = warning
self.device_id = address
self.success = False
# 配置串口
self.hardware_interface = serial.Serial(
port=port,
baudrate=baudrate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1
)
self.lock = threading.Lock()
def calculate_crc(self,data):
crc = 0xFFFF
for pos in data:
crc ^= pos
for i in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc
def build_modbus_request(self, device_id, function_code, register_address, register_count):
request = struct.pack('>BBHH', device_id, function_code, register_address, register_count)
crc = self.calculate_crc(request)
request += struct.pack('<H', crc)
return request
def read_modbus_response(self,response):
if len(response) < 5:
return None
data = response[:-2]
crc_received = struct.unpack('<H', response[-2:])[0]
crc_calculated = self.calculate_crc(data)
if crc_received == crc_calculated:
return data[3:]
return None
def send_prototype_command(self ,command):
# with self.lock:
function_code = 0x04 # 读输入寄存器
register_address = 0x0003 # 温度高位寄存器地址
register_count = 0x0002 # 读取两个寄存器
request = self.build_modbus_request(command, function_code, register_address, register_count)
self.hardware_interface.write(request)
response = self.hardware_interface.read(9) # 读取9字节的响应
data = self.read_modbus_response(response)
if data is None:
return None
high_value = struct.unpack('>H', data[:2])[0]
low_value = struct.unpack('>H', data[2:])[0]
# 组合高位和低位并计算实际温度
raw_temperature = (high_value << 16) | low_value
if raw_temperature & 0x8000: # 如果低位寄存器最高位为1表示负值
raw_temperature -= 0x10000 # 转换为正确的负数表示
actual_temperature = raw_temperature / 10.0
return actual_temperature
@property
def value(self) -> float:
self._value = self.send_prototype_command(self.device_id)
return self._value
def set_warning(self, command):
self.success = False
temp = json.loads(command)["warning_temp"]
self.warning = round(float(temp), 1)
self.success = True

View File

View File

@@ -0,0 +1,73 @@
from pydantic import BaseModel, Field
import pint
class Point3D(BaseModel):
x: float = Field(..., title="X coordinate")
y: float = Field(..., title="Y coordinate")
z: float = Field(..., title="Z coordinate")
# Start Protocols
class PumpTransferProtocol(BaseModel):
from_vessel: str
to_vessel: str
volume: float
amount: str = ""
time: float = 0
viscous: bool = False
rinsing_solvent: str = "air"
rinsing_volume: float = 5000
rinsing_repeats: int = 2
solid: bool = False
flowrate: float = 500
transfer_flowrate: float = 2500
class CleanProtocol(BaseModel):
vessel: str
solvent: str
volume: float
temp: float
repeats: int = 1
class SeparateProtocol(BaseModel):
purpose: str # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'.
product_phase: str # 'top' or 'bottom'. Phase that product will be in.
from_vessel: str #Contents of from_vessel are transferred to separation_vessel and separation is performed.
separation_vessel: str # Vessel in which separation of phases will be carried out.
to_vessel: str # Vessel to send product phase to.
waste_phase_to_vessel: str # Optional. Vessel to send waste phase to.
solvent: str # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases.
solvent_volume: float # Optional. Volume of solvent to add.
through: str # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'.
repeats: int # Optional. Number of separations to perform.
stir_time: float # Optional. Time stir for after adding solvent, before separation of phases.
stir_speed: float # Optional. Speed to stir at after adding solvent, before separation of phases.
settling_time: float # Optional. Time
class EvaporateProtocol(BaseModel):
vessel: str
pressure: float
temp: float
time: float
stir_speed: float
class EvacuateAndRefillProtocol(BaseModel):
vessel: str
gas: str
repeats: int
class AGVTransferProtocol(BaseModel):
from_repo: dict
to_repo: dict
from_repo_position: str
to_repo_position: str
__all__ = ["Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", "EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol"]
# End Protocols

View File

View File

@@ -0,0 +1,9 @@
io_snrd:
class:
module: unilabos.device_comms.SRND_16_IO:SRND_16_IO
type: python
hardware_interface:
name: modbus_client
extra_info: address
read: read_io_coil
write: write_io_coil

View File

@@ -0,0 +1,6 @@
serial:
class:
module: unilabos.ros.nodes.presets:ROS2SerialNode
type: ros2
schema:
properties: {}

View File

@@ -0,0 +1,23 @@
# 光学表征设备:红外、紫外可见、拉曼等
raman_home_made:
class:
module: unilabos.devices.raman_uv.home_made_raman:RamanObj
type: python
status_types:
status: String
action_value_mappings:
raman_cmd:
type: SendCmd
goal:
command: command
feedback: {}
result:
success: success
schema:
properties:
status:
type: string
required:
- status
additionalProperties: false
type: object

View File

@@ -0,0 +1,189 @@
liquid_handler:
class:
module: pylabrobot.liquid_handling:LiquidHandler
type: python
status_types:
name: String
action_value_mappings:
aspirate:
type: LiquidHandlerAspirate
goal:
resources: resources
vols: vols
use_channels: use_channels
flow_rates: flow_rates
end_delay: end_delay
offsets: offsets
liquid_height: liquid_height
blow_out_air_volume: blow_out_air_volume
feedback: {}
result:
name: name
discard_tips:
type: LiquidHandlerDiscardTips
goal:
use_channels: use_channels
feedback: {}
result:
name: name
dispense:
type: LiquidHandlerDispense
goal:
resources: resources
vols: vols
use_channels: use_channels
flow_rates: flow_rates
offsets: offsets
blow_out_air_volume: blow_out_air_volume
spread: spread
feedback: {}
result:
name: name
drop_tips:
type: LiquidHandlerDropTips
goal:
tip_spots: tip_spots
use_channels: use_channels
offsets: offsets
allow_nonzero_volume: allow_nonzero_volume
feedback: {}
result:
name: name
drop_tips96:
type: LiquidHandlerDropTips96
goal:
tip_rack: tip_rack
offset: offset
allow_nonzero_volume: allow_nonzero_volume
feedback: {}
result:
name: name
move_lid:
type: LiquidHandlerMoveLid
goal:
lid: lid
to: to
intermediate_locations: intermediate_locations
resource_offset: resource_offset
destination_offset: destination_offset
pickup_direction: pickup_direction
drop_direction: drop_direction
get_direction: get_direction
put_direction: put_direction
pickup_distance_from_top: pickup_distance_from_top
feedback: {}
result:
name: name
move_plate:
type: LiquidHandlerMovePlate
goal:
plate: plate
to: to
intermediate_locations: intermediate_locations
resource_offset: resource_offset
pickup_offset: pickup_offset
destination_offset: destination_offset
pickup_direction: pickup_direction
drop_direction: drop_direction
get_direction: get_direction
put_direction: put_direction
feedback: {}
result:
name: name
move_resource:
type: LiquidHandlerMoveResource
goal:
resource: resource
to: to
intermediate_locations: intermediate_locations
resource_offset: resource_offset
destination_offset: destination_offset
pickup_distance_from_top: pickup_distance_from_top
pickup_direction: pickup_direction
drop_direction: drop_direction
get_direction: get_direction
put_direction: put_direction
feedback: {}
result:
name: name
pick_up_tips:
type: LiquidHandlerPickUpTips
goal:
tip_spots: tip_spots
use_channels: use_channels
offsets: offsets
feedback: {}
result:
name: name
pick_up_tips96:
type: LiquidHandlerPickUpTips96
goal:
tip_rack: tip_rack
offset: offset
feedback: {}
result:
name: name
return_tips:
type: LiquidHandlerReturnTips
goal:
use_channels: use_channels
allow_nonzero_volume: allow_nonzero_volume
feedback: {}
result:
name: name
return_tips96:
type: LiquidHandlerReturnTips96
goal:
allow_nonzero_volume: allow_nonzero_volume
feedback: {}
result:
name: name
stamp:
type: LiquidHandlerStamp
goal:
source: source
target: target
volume: volume
aspiration_flow_rate: aspiration_flow_rate
dispense_flow_rate: dispense_flow_rate
feedback: {}
result:
name: name
transfer:
type: LiquidHandlerTransfer
goal:
source: source
targets: targets
source_vol: source_vol
ratios: ratios
target_vols: target_vols
aspiration_flow_rate: aspiration_flow_rate
dispense_flow_rates: dispense_flow_rates
schema:
type: object
properties:
status:
type: string
description: 液体处理仪器当前状态
required:
- status
additionalProperties: false
liquid_handler.revvity:
class:
module: unilabos.devices.liquid_handling.revvity:Revvity
type: python
status_types:
status: String
action_value_mappings:
run:
type: WorkStationRun
goal:
wf_name: file_path
params: params
resource: resource
feedback:
status: status
result:
success: success

View File

@@ -0,0 +1,71 @@
separator.homemade:
class:
module: unilabos.devices.separator.homemade_grbl_conductivity:Separator_Controller
type: python
status_types:
sensordata: Float64
status: String
action_value_mappings:
stir:
type: Stir
goal:
stir_time: stir_time,
stir_speed: stir_speed
settling_time": settling_time
feedback:
status: status
result:
success: success
valve_open_cmd:
type: SendCmd
goal:
command: command
feedback:
status: status
result":
success: success
schema:
type: object
properties:
status:
type: string
description: The status of the device
sensordata:
type: number
description: 电导传感器数据
required:
- status
- sensordata
additionalProperties: false
rotavap.one:
class:
module: unilabos.devices.rotavap.rotavap_one:RotavapOne
type: python
status_types:
pump_time: Float64
rotate_time: Float64
action_value_mappings:
set_timer:
type: SendCmd
goal:
command: command
feedback: {}
result:
success: success
schema:
type: object
properties:
temperature:
type: number
description: 旋蒸水浴温度
pump_time:
type: number
description: The pump time of the device
rotate_time:
type: number
description: The rotate time of the device
required:
- pump_time
- rotate_time
additionalProperties: false

View File

@@ -0,0 +1,35 @@
syringe_pump_with_valve.runze:
class:
module: unilabos.devices.pump_and_valve.runze_backbone:RunzeSyringePump
type: python
schema:
type: object
properties:
status:
type: string
description: The status of the device
position:
type: number
description: The volume of the syringe
speed_max:
type: number
description: The speed of the syringe
valve_position:
type: string
description: The position of the valve
required:
- status
- position
- valve_position
additionalProperties: false
solenoid_valve.mock:
class:
module: unilabos.devices.pump_and_valve.solenoid_valve_mock:SolenoidValveMock
type: python
solenoid_valve:
class:
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
type: python

View File

@@ -0,0 +1,28 @@
# 仙工智能底盘(知行使用)
agv.SEER:
class:
module: unilabos.devices.agv.agv_navigator:AgvNavigator
type: python
status_types:
pose: Float64MultiArray
status: String
action_value_mappings:
send_nav_task:
type: SendCmd
goal:
command: command
feedback: {}
result:
success: success
schema:
properties:
pose:
type: array
items:
type: number
status:
type: string
required:
- status
additionalProperties: false
type: object

View File

@@ -0,0 +1,36 @@
robotic_arm.UR:
class:
module: unilabos.devices.agv.ur_arm_task:UrArmTask
type: python
status_types:
arm_pose: Float64MultiArray
gripper_pose: Float64
arm_status: String
gripper_status: String
action_value_mappings:
move_pos_task:
type: SendCmd
goal:
command: command
feedback: {}
result:
success: success
schema:
properties:
arm_pose:
type: array
items:
type: number
gripper_pose:
type: number
arm_status:
type: string
description: 机械臂设备状态
gripper_status:
type: string
description: 机械爪设备状态
required:
- arm_status
- gripper_status
additionalProperties: false
type: object

View File

@@ -0,0 +1,36 @@
gripper.mock:
class:
module: unilabos.devices.gripper.mock:MockGripper
type: python
status_types:
position: Float64
torque: Float64
status: String
action_value_mappings:
push_to:
type: GripperCommand
goal:
command.position: position
command.max_effort: torque
feedback:
position: position
effort: torque
result:
position: position
effort: torque
gripper.misumi_rz:
class:
module: unilabos.devices.motor:Grasp.EleGripper
type: python
status_types:
status: String
action_value_mappings:
execute_command_from_outer:
type: SendCmd
goal:
command: command
feedback: {}
result:
success: success

Some files were not shown because too many files have changed in this diff Show More