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