mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
Merge branch '37-biomek-i5i7' into device_visualization
This commit is contained in:
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
|
||||
|
||||
# Currently, you need to install the `unilabos_msgs` package
|
||||
# You can download the system-specific package from the Release page
|
||||
conda install ros-humble-unilabos-msgs-0.9.1-xxxxx.tar.bz2
|
||||
conda install ros-humble-unilabos-msgs-0.9.2-xxxxx.tar.bz2
|
||||
|
||||
# Install PyLabRobot and other prerequisites
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
|
||||
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
||||
|
||||
# 现阶段,需要安装 `unilabos_msgs` 包
|
||||
# 可以前往 Release 页面下载系统对应的包进行安装
|
||||
conda install ros-humble-unilabos-msgs-0.9.1-xxxxx.tar.bz2
|
||||
conda install ros-humble-unilabos-msgs-0.9.2-xxxxx.tar.bz2
|
||||
|
||||
# 安装PyLabRobot等前置
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.9.1
|
||||
version: 0.9.2
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
folder: ros-humble-unilabos-msgs/src/work
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.9.1"
|
||||
version: "0.9.2"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.9.1',
|
||||
version='0.9.2',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
|
||||
22
test/experiments/biomek.json
Normal file
22
test/experiments/biomek.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "BIOMEK",
|
||||
"name": "BIOMEK",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "liquid_handler.biomek",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
},
|
||||
"data": {},
|
||||
"children": [
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
1710
test/experiments/plr_test_converted_slim.json
Normal file
1710
test/experiments/plr_test_converted_slim.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ from copy import deepcopy
|
||||
|
||||
import yaml
|
||||
|
||||
from unilabos.resources.graphio import tree_to_list
|
||||
|
||||
# 首先添加项目根目录到路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
@@ -144,19 +146,19 @@ def main():
|
||||
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["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
|
||||
args_dict["resources_config"] = list(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())
|
||||
)
|
||||
# args_dict["resources_config"] = initialize_resources(
|
||||
# list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
|
||||
# )
|
||||
args_dict["resources_config"] = 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"]:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
@@ -163,10 +164,12 @@ class MQTTClient:
|
||||
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):
|
||||
def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
jobdata = {"job_id": job_id, "data": feedback_data, "status": status}
|
||||
if return_info is None:
|
||||
return_info = "{}"
|
||||
jobdata = {"job_id": job_id, "data": feedback_data, "status": status, "return_info": return_info}
|
||||
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):
|
||||
|
||||
@@ -30,18 +30,18 @@ class HTTPClient:
|
||||
self.auth = MQConfig.lab_id
|
||||
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
|
||||
|
||||
def resource_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
|
||||
def resource_add(self, resources: List[Dict[str, Any]], database_process_later:bool) -> requests.Response:
|
||||
"""
|
||||
添加资源
|
||||
|
||||
Args:
|
||||
resources: 要添加的资源列表
|
||||
|
||||
database_process_later: 后台处理资源
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/resource/",
|
||||
f"{self.remote_addr}/lab/resource/?database_process_later={1 if database_process_later else 0}",
|
||||
json=resources,
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
timeout=5,
|
||||
|
||||
1106
unilabos/devices/liquid_handling/biomek.py
Normal file
1106
unilabos/devices/liquid_handling/biomek.py
Normal file
File diff suppressed because it is too large
Load Diff
642
unilabos/devices/liquid_handling/biomek.txt
Normal file
642
unilabos/devices/liquid_handling/biomek.txt
Normal file
File diff suppressed because one or more lines are too long
2697
unilabos/devices/liquid_handling/biomek_temporary_protocol.json
Normal file
2697
unilabos/devices/liquid_handling/biomek_temporary_protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
1010
unilabos/devices/liquid_handling/biomek_test.py
Normal file
1010
unilabos/devices/liquid_handling/biomek_test.py
Normal file
File diff suppressed because it is too large
Load Diff
3760
unilabos/devices/liquid_handling/complete_biomek_protocol.json
Normal file
3760
unilabos/devices/liquid_handling/complete_biomek_protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
4201
unilabos/devices/liquid_handling/complete_biomek_protocol_0608.json
Normal file
4201
unilabos/devices/liquid_handling/complete_biomek_protocol_0608.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,13 +6,8 @@ import asyncio
|
||||
import time
|
||||
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.resources import (
|
||||
Resource,
|
||||
TipRack,
|
||||
Container,
|
||||
Coordinate,
|
||||
Well
|
||||
)
|
||||
from pylabrobot.resources import Resource, TipRack, Container, Coordinate, Well
|
||||
|
||||
|
||||
class LiquidHandlerAbstract(LiquidHandler):
|
||||
"""Extended LiquidHandler with additional operations."""
|
||||
@@ -21,6 +16,19 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
# REMOVE LIQUID --------------------------------------------------
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
async def create_protocol(
|
||||
self,
|
||||
protocol_name: str,
|
||||
protocol_description: str,
|
||||
protocol_version: str,
|
||||
protocol_author: str,
|
||||
protocol_date: str,
|
||||
protocol_type: str,
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
"""Create a new protocol with the given metadata."""
|
||||
pass
|
||||
|
||||
async def remove_liquid(
|
||||
self,
|
||||
vols: List[float],
|
||||
@@ -35,8 +43,8 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
|
||||
delays: Optional[List[int]] = None,
|
||||
is_96_well: Optional[bool] = False,
|
||||
top: Optional[List(float)] = None,
|
||||
none_keys: List[str] = []
|
||||
top: Optional[List[float]] = None,
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
"""A complete *remove* (aspirate → waste) operation."""
|
||||
trash = self.deck.get_trash_area()
|
||||
@@ -48,7 +56,7 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
raise ValueError("Length of `vols` must match `sources`.")
|
||||
|
||||
for src, vol in zip(sources, vols):
|
||||
self.move_to(src, dis_to_top=top[0] if top else 0)
|
||||
await self.move_to(src, dis_to_top=top[0] if top else 0)
|
||||
tip = next(self.current_tip)
|
||||
await self.pick_up_tips(tip)
|
||||
await self.aspirate(
|
||||
@@ -100,7 +108,7 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
mix_vol: Optional[int] = None,
|
||||
mix_rate: Optional[int] = None,
|
||||
mix_liquid_height: Optional[float] = None,
|
||||
none_keys: List[str] = []
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
"""A complete *add* (aspirate reagent → dispense into targets) operation."""
|
||||
|
||||
@@ -122,7 +130,7 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
offsets=[offsets[0]] if offsets else None,
|
||||
liquid_height=[liquid_height[0]] if liquid_height else None,
|
||||
blow_out_air_volume=[blow_out_air_volume[0]] if blow_out_air_volume else None,
|
||||
spread=spread
|
||||
spread=spread,
|
||||
)
|
||||
if delays is not None:
|
||||
await self.custom_delay(seconds=delays[0])
|
||||
@@ -144,7 +152,8 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
mix_vol=mix_vol,
|
||||
offsets=offsets if offsets else None,
|
||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||
mix_rate=mix_rate if mix_rate else None)
|
||||
mix_rate=mix_rate if mix_rate else None,
|
||||
)
|
||||
if delays is not None:
|
||||
await self.custom_delay(seconds=delays[1])
|
||||
await self.touch_tip(targets[_])
|
||||
@@ -158,13 +167,13 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
# ---------------------------------------------------------------
|
||||
async def transfer_liquid(
|
||||
self,
|
||||
asp_vols: Union[List[float], float],
|
||||
dis_vols: Union[List[float], float],
|
||||
sources: Sequence[Container],
|
||||
targets: Sequence[Container],
|
||||
tip_racks: Sequence[TipRack],
|
||||
*,
|
||||
use_channels: Optional[List[int]] = None,
|
||||
asp_vols: Union[List[float], float],
|
||||
dis_vols: Union[List[float], float],
|
||||
asp_flow_rates: Optional[List[Optional[float]]] = None,
|
||||
dis_flow_rates: Optional[List[Optional[float]]] = None,
|
||||
offsets: Optional[List[Coordinate]] = None,
|
||||
@@ -179,7 +188,7 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
mix_rate: Optional[int] = None,
|
||||
mix_liquid_height: Optional[float] = None,
|
||||
delays: Optional[List[int]] = None,
|
||||
none_keys: List[str] = []
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
"""Transfer liquid from each *source* well/plate to the corresponding *target*.
|
||||
|
||||
@@ -207,8 +216,9 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
raise ValueError("`sources`, `targets`, and `vols` must have the same length.")
|
||||
|
||||
tip_iter = self.iter_tips(tip_racks)
|
||||
for src, tgt, asp_vol, asp_flow_rate, dis_vol, dis_flow_rate in (
|
||||
zip(sources, targets, asp_vols, asp_flow_rates, dis_vols, dis_flow_rates)):
|
||||
for src, tgt, asp_vol, asp_flow_rate, dis_vol, dis_flow_rate in zip(
|
||||
sources, targets, asp_vols, asp_flow_rates, dis_vols, dis_flow_rates
|
||||
):
|
||||
tip = next(tip_iter)
|
||||
await self.pick_up_tips(tip)
|
||||
# Aspirate from source
|
||||
@@ -247,9 +257,9 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"Liquid transfer failed: {exc}") from exc
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Helper utilities
|
||||
# ---------------------------------------------------------------
|
||||
# ---------------------------------------------------------------
|
||||
# Helper utilities
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
async def custom_delay(self, seconds=0, msg=None):
|
||||
"""
|
||||
@@ -266,28 +276,26 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
print(f"Done: {msg}")
|
||||
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||
|
||||
async def touch_tip(self,
|
||||
targets: Sequence[Container],
|
||||
):
|
||||
async def touch_tip(self, targets: Sequence[Container]):
|
||||
"""Touch the tip to the side of the well."""
|
||||
await self.aspirate(
|
||||
resources=[targets],
|
||||
vols=[0],
|
||||
use_channels=None,
|
||||
flow_rates=None,
|
||||
offsets=[Coordinate(x=-targets.get_size_x()/2,y=0,z=0)],
|
||||
offsets=[Coordinate(x=-targets.get_size_x() / 2, y=0, z=0)],
|
||||
liquid_height=None,
|
||||
blow_out_air_volume=None
|
||||
blow_out_air_volume=None,
|
||||
)
|
||||
#await self.custom_delay(seconds=1) # In the simulation, we do not need to wait
|
||||
# await self.custom_delay(seconds=1) # In the simulation, we do not need to wait
|
||||
await self.aspirate(
|
||||
resources=[targets],
|
||||
vols=[0],
|
||||
use_channels=None,
|
||||
flow_rates=None,
|
||||
offsets=[Coordinate(x=targets.get_size_x()/2,y=0,z=0)],
|
||||
offsets=[Coordinate(x=targets.get_size_x() / 2, y=0, z=0)],
|
||||
liquid_height=None,
|
||||
blow_out_air_volume=None
|
||||
blow_out_air_volume=None,
|
||||
)
|
||||
|
||||
async def mix(
|
||||
@@ -298,7 +306,7 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
height_to_bottom: Optional[float] = None,
|
||||
offsets: Optional[Coordinate] = None,
|
||||
mix_rate: Optional[float] = None,
|
||||
none_keys: List[str] = []
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
if mix_time is None: # No mixing required
|
||||
return
|
||||
@@ -333,7 +341,7 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
tip_iter = self.iter_tips(tip_racks)
|
||||
self.current_tip = tip_iter
|
||||
|
||||
async def move_to(self, well: Well, dis_to_top: float = 0 , channel: int = 0):
|
||||
async def move_to(self, well: Well, dis_to_top: float = 0, channel: int = 0):
|
||||
"""
|
||||
Move a single channel to a specific well with a given z-height.
|
||||
|
||||
@@ -352,4 +360,3 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
await self.move_channel_x(channel, abs_loc.x)
|
||||
await self.move_channel_y(channel, abs_loc.y)
|
||||
await self.move_channel_z(channel, abs_loc.z + well_height + dis_to_top)
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
|
||||
import json
|
||||
from typing import Sequence, Optional, List, Union, Literal
|
||||
json_path = "/Users/guangxinzhang/Documents/Deep Potential/opentrons/convert/protocols/enriched_steps/sci-lucif-assay4.json"
|
||||
|
||||
with open(json_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
transfer_example = data[0]
|
||||
#print(transfer_example)
|
||||
|
||||
|
||||
temp_protocol = []
|
||||
TipLocation = "BC1025F" # Assuming this is a fixed tip location for the transfer
|
||||
sources = transfer_example["sources"] # Assuming sources is a list of Container objects
|
||||
targets = transfer_example["targets"] # Assuming targets is a list of Container objects
|
||||
tip_racks = transfer_example["tip_racks"] # Assuming tip_racks is a list of TipRack objects
|
||||
asp_vols = transfer_example["asp_vols"] # Assuming asp_vols is a list of volumes
|
||||
solvent = "PBS"
|
||||
|
||||
def transfer_liquid(
|
||||
#self,
|
||||
sources,#: Sequence[Container],
|
||||
targets,#: Sequence[Container],
|
||||
tip_racks,#: Sequence[TipRack],
|
||||
TipLocation,
|
||||
# *,
|
||||
# use_channels: Optional[List[int]] = None,
|
||||
asp_vols: Union[List[float], float],
|
||||
solvent: Optional[str] = None,
|
||||
# dis_vols: Union[List[float], float],
|
||||
# asp_flow_rates: Optional[List[Optional[float]]] = None,
|
||||
# dis_flow_rates: Optional[List[Optional[float]]] = None,
|
||||
# offsets,#: Optional[List[]] = None,
|
||||
# touch_tip: bool = False,
|
||||
# liquid_height: Optional[List[Optional[float]]] = None,
|
||||
# blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||||
# spread: Literal["wide", "tight", "custom"] = "wide",
|
||||
# is_96_well: bool = False,
|
||||
# mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
|
||||
# mix_times,#: Optional[list() = None,
|
||||
# mix_vol: Optional[int] = None,
|
||||
# mix_rate: Optional[int] = None,
|
||||
# mix_liquid_height: Optional[float] = None,
|
||||
# delays: Optional[List[int]] = None,
|
||||
# none_keys: List[str] = []
|
||||
):
|
||||
# -------- Build Biomek transfer step --------
|
||||
# 1) Construct default parameter scaffold (values mirror Biomek “Transfer” block).
|
||||
|
||||
transfer_params = {
|
||||
"Span8": False,
|
||||
"Pod": "Pod1",
|
||||
"items": {}, # to be filled below
|
||||
"Wash": False,
|
||||
"Dynamic?": True,
|
||||
"AutoSelectActiveWashTechnique": False,
|
||||
"ActiveWashTechnique": "",
|
||||
"ChangeTipsBetweenDests": False,
|
||||
"ChangeTipsBetweenSources": False,
|
||||
"DefaultCaption": "", # filled after we know first pair/vol
|
||||
"UseExpression": False,
|
||||
"LeaveTipsOn": False,
|
||||
"MandrelExpression": "",
|
||||
"Repeats": "1",
|
||||
"RepeatsByVolume": False,
|
||||
"Replicates": "1",
|
||||
"ShowTipHandlingDetails": False,
|
||||
"ShowTransferDetails": True,
|
||||
"Solvent": "Water",
|
||||
"Span8Wash": False,
|
||||
"Span8WashVolume": "2",
|
||||
"Span8WasteVolume": "1",
|
||||
"SplitVolume": False,
|
||||
"SplitVolumeCleaning": False,
|
||||
"Stop": "Destinations",
|
||||
"TipLocation": "BC1025F",
|
||||
"UseCurrentTips": False,
|
||||
"UseDisposableTips": True,
|
||||
"UseFixedTips": False,
|
||||
"UseJIT": True,
|
||||
"UseMandrelSelection": True,
|
||||
"UseProbes": [True, True, True, True, True, True, True, True],
|
||||
"WashCycles": "1",
|
||||
"WashVolume": "110%",
|
||||
"Wizard": False
|
||||
}
|
||||
|
||||
items: dict = {}
|
||||
for idx, (src, dst) in enumerate(zip(sources, targets)):
|
||||
items[str(idx)] = {
|
||||
"Source": str(src),
|
||||
"Destination": str(dst),
|
||||
"Volume": asp_vols[idx]
|
||||
}
|
||||
transfer_params["items"] = items
|
||||
transfer_params["Solvent"] = solvent if solvent else "Water"
|
||||
transfer_params["TipLocation"] = TipLocation
|
||||
|
||||
if len(tip_racks) == 1:
|
||||
transfer_params['UseCurrentTips'] = True
|
||||
elif len(tip_racks) > 1:
|
||||
transfer_params["ChangeTipsBetweenDests"] = True
|
||||
|
||||
return transfer_params
|
||||
|
||||
action = transfer_liquid(sources=sources,targets=targets,tip_racks=tip_racks, asp_vols=asp_vols,solvent = solvent, TipLocation=TipLocation)
|
||||
print(json.dumps(action,indent=2))
|
||||
# print(action)
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
"transfer": {
|
||||
|
||||
"items": {},
|
||||
"Wash": false,
|
||||
"Dynamic?": true,
|
||||
"AutoSelectActiveWashTechnique": false,
|
||||
"ActiveWashTechnique": "",
|
||||
"ChangeTipsBetweenDests": true,
|
||||
"ChangeTipsBetweenSources": false,
|
||||
"DefaultCaption": "Transfer 100 µL from P13 to P3",
|
||||
"UseExpression": false,
|
||||
"LeaveTipsOn": false,
|
||||
"MandrelExpression": "",
|
||||
"Repeats": "1",
|
||||
"RepeatsByVolume": false,
|
||||
"Replicates": "1",
|
||||
"ShowTipHandlingDetails": true,
|
||||
"ShowTransferDetails": true,
|
||||
|
||||
"Span8Wash": false,
|
||||
"Span8WashVolume": "2",
|
||||
"Span8WasteVolume": "1",
|
||||
"SplitVolume": false,
|
||||
"SplitVolumeCleaning": false,
|
||||
"Stop": "Destinations",
|
||||
"TipLocation": "BC1025F",
|
||||
"UseCurrentTips": false,
|
||||
"UseDisposableTips": false,
|
||||
"UseFixedTips": false,
|
||||
"UseJIT": true,
|
||||
"UseMandrelSelection": true,
|
||||
"UseProbes": [true, true, true, true, true, true, true, true],
|
||||
"WashCycles": "3",
|
||||
"WashVolume": "110%",
|
||||
"Wizard": false
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
liquid_handler:
|
||||
description: Liquid handler device controlled by pylabrobot
|
||||
icon: icon_yiyezhan.webp
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.liquid_handler_abstract:LiquidHandlerAbstract
|
||||
type: python
|
||||
@@ -22,8 +23,8 @@ liquid_handler:
|
||||
is_96_well: is_96_well
|
||||
top: top
|
||||
none_keys: none_keys
|
||||
feedback: { }
|
||||
result: { }
|
||||
feedback: {}
|
||||
result: {}
|
||||
add_liquid:
|
||||
type: LiquidHandlerAdd
|
||||
goal:
|
||||
@@ -43,8 +44,8 @@ liquid_handler:
|
||||
mix_rate: mix_rate
|
||||
mix_liquid_height: mix_liquid_height
|
||||
none_keys: none_keys
|
||||
feedback: { }
|
||||
result: { }
|
||||
feedback: {}
|
||||
result: {}
|
||||
transfer_liquid:
|
||||
type: LiquidHandlerTransfer
|
||||
goal:
|
||||
@@ -69,8 +70,8 @@ liquid_handler:
|
||||
mix_liquid_height: mix_liquid_height
|
||||
delays: delays
|
||||
none_keys: none_keys
|
||||
feedback: { }
|
||||
result: { }
|
||||
feedback: {}
|
||||
result: {}
|
||||
mix:
|
||||
type: LiquidHandlerMix
|
||||
goal:
|
||||
@@ -81,16 +82,16 @@ liquid_handler:
|
||||
offsets: offsets
|
||||
mix_rate: mix_rate
|
||||
none_keys: none_keys
|
||||
feedback: { }
|
||||
result: { }
|
||||
feedback: {}
|
||||
result: {}
|
||||
move_to:
|
||||
type: LiquidHandlerMoveTo
|
||||
goal:
|
||||
well: well
|
||||
dis_to_top: dis_to_top
|
||||
channel: channel
|
||||
feedback: { }
|
||||
result: { }
|
||||
feedback: {}
|
||||
result: {}
|
||||
aspirate:
|
||||
type: LiquidHandlerAspirate
|
||||
goal:
|
||||
@@ -245,6 +246,21 @@ liquid_handler:
|
||||
target_vols: target_vols
|
||||
aspiration_flow_rate: aspiration_flow_rate
|
||||
dispense_flow_rates: dispense_flow_rates
|
||||
handles:
|
||||
input:
|
||||
- handler_key: liquid-input
|
||||
label: Liquid Input
|
||||
data_type: resource
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
output:
|
||||
- handler_key: liquid-output
|
||||
label: Liquid Output
|
||||
data_type: resource
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: liquid
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -272,3 +288,174 @@ liquid_handler.revvity:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
|
||||
liquid_handler.biomek:
|
||||
description: Biomek液体处理器设备,基于pylabrobot控制
|
||||
icon: icon_yiyezhan.webp
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.biomek:LiquidHandlerBiomek
|
||||
type: python
|
||||
status_types: {}
|
||||
action_value_mappings:
|
||||
create_protocol:
|
||||
type: LiquidHandlerProtocolCreation
|
||||
goal:
|
||||
protocol_name: protocol_name
|
||||
protocol_description: protocol_description
|
||||
protocol_version: protocol_version
|
||||
protocol_author: protocol_author
|
||||
protocol_date: protocol_date
|
||||
protocol_type: protocol_type
|
||||
none_keys: none_keys
|
||||
feedback: {}
|
||||
result: {}
|
||||
run_protocol:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
transfer_liquid:
|
||||
type: LiquidHandlerTransfer
|
||||
goal:
|
||||
asp_vols: asp_vols
|
||||
dis_vols: dis_vols
|
||||
sources: sources
|
||||
targets: targets
|
||||
tip_racks: tip_racks
|
||||
use_channels: use_channels
|
||||
asp_flow_rates: asp_flow_rates
|
||||
dis_flow_rates: dis_flow_rates
|
||||
offsets: offsets
|
||||
touch_tip: touch_tip
|
||||
liquid_height: liquid_height
|
||||
blow_out_air_volume: blow_out_air_volume
|
||||
spread: spread
|
||||
is_96_well: is_96_well
|
||||
mix_stage: mix_stage
|
||||
mix_times: mix_times
|
||||
mix_vol: mix_vol
|
||||
mix_rate: mix_rate
|
||||
mix_liquid_height: mix_liquid_height
|
||||
delays: delays
|
||||
none_keys: none_keys
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
input:
|
||||
- handler_key: liquid-input
|
||||
label: Liquid Input
|
||||
data_type: resource
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
output:
|
||||
- handler_key: liquid-output
|
||||
label: Liquid Output
|
||||
data_type: resource
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: liquid
|
||||
transfer_biomek:
|
||||
type: LiquidHandlerTransferBiomek
|
||||
goal:
|
||||
source: source
|
||||
target: target
|
||||
tip_rack: tip_rack
|
||||
volume: volume
|
||||
aspirate_techniques: aspirate_techniques
|
||||
dispense_techniques: dispense_techniques
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
input:
|
||||
- handler_key: sources
|
||||
label: sources
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
- handler_key: targets
|
||||
label: targets
|
||||
data_type: resource
|
||||
data_source: executor
|
||||
data_key: liquid
|
||||
- handler_key: tip_rack
|
||||
label: tip_rack
|
||||
data_type: resource
|
||||
data_source: executor
|
||||
data_key: liquid
|
||||
output:
|
||||
- handler_key: sources_out
|
||||
label: sources
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
- handler_key: targets_out
|
||||
label: targets
|
||||
data_type: resource
|
||||
data_source: executor
|
||||
data_key: liquid
|
||||
oscillation_biomek:
|
||||
type: LiquidHandlerOscillateBiomek
|
||||
goal:
|
||||
rpm: rpm
|
||||
time: time
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
input:
|
||||
- handler_key: plate
|
||||
label: plate
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
output:
|
||||
- handler_key: plate_out
|
||||
label: plate
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
move_biomek:
|
||||
type: LiquidHandlerMoveBiomek
|
||||
goal:
|
||||
source: resource
|
||||
target: target
|
||||
feedback: {}
|
||||
result:
|
||||
name: name
|
||||
handles:
|
||||
input:
|
||||
- handler_key: sources
|
||||
label: sources
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
output:
|
||||
- handler_key: targets
|
||||
label: targets
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
incubation_biomek:
|
||||
type: LiquidHandlerIncubateBiomek
|
||||
goal:
|
||||
time: time
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
input:
|
||||
- handler_key: plate
|
||||
label: plate
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
output:
|
||||
- handler_key: plate_out
|
||||
label: plate
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
schema:
|
||||
type: object
|
||||
properties: {}
|
||||
required: []
|
||||
additionalProperties: false
|
||||
|
||||
@@ -28,12 +28,43 @@ syringe_pump_with_valve.runze:
|
||||
- valve_position
|
||||
additionalProperties: false
|
||||
|
||||
|
||||
solenoid_valve.mock:
|
||||
description: Mock solenoid valve
|
||||
class:
|
||||
module: unilabos.devices.pump_and_valve.solenoid_valve_mock:SolenoidValveMock
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
valve_position: String
|
||||
action_value_mappings:
|
||||
open:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
close:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
input:
|
||||
- handler_key: fluid-input
|
||||
label: Fluid Input
|
||||
data_type: fluid
|
||||
output:
|
||||
- handler_key: fluid-output
|
||||
label: Fluid Output
|
||||
data_type: fluid
|
||||
init_param_schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
description: "通信端口"
|
||||
default: "COM6"
|
||||
required:
|
||||
- port
|
||||
|
||||
solenoid_valve:
|
||||
description: Solenoid valve
|
||||
|
||||
@@ -22,9 +22,76 @@ vacuum_pump.mock:
|
||||
string: string
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
input:
|
||||
- handler_key: fluid-input
|
||||
label: Fluid Input
|
||||
data_type: fluid
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: fluid_in
|
||||
output:
|
||||
- handler_key: fluid-output
|
||||
label: Fluid Output
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_out
|
||||
init_param_schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
description: "通信端口"
|
||||
default: "COM6"
|
||||
required:
|
||||
- port
|
||||
|
||||
gas_source.mock:
|
||||
description: Mock gas source
|
||||
class:
|
||||
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
action_value_mappings:
|
||||
open:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
close:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
set_status:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: string
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
input:
|
||||
- handler_key: fluid-input
|
||||
label: Fluid Input
|
||||
data_type: fluid
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: fluid_in
|
||||
output:
|
||||
- handler_key: fluid-output
|
||||
label: Fluid Output
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_out
|
||||
init_param_schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
description: "通信端口"
|
||||
default: "COM6"
|
||||
required:
|
||||
- port
|
||||
|
||||
@@ -25,9 +25,7 @@ class Registry:
|
||||
self.ResourceCreateFromOuterEasy = self._replace_type_with_class(
|
||||
"ResourceCreateFromOuterEasy", "host_node", f"动作 create_resource"
|
||||
)
|
||||
self.EmptyIn = self._replace_type_with_class(
|
||||
"EmptyIn", "host_node", f""
|
||||
)
|
||||
self.EmptyIn = self._replace_type_with_class("EmptyIn", "host_node", f"")
|
||||
self.device_type_registry = {}
|
||||
self.resource_type_registry = {}
|
||||
self._setup_called = False # 跟踪setup是否已调用
|
||||
@@ -66,6 +64,7 @@ class Registry:
|
||||
"goal_default": yaml.safe_load(
|
||||
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuter.Goal))
|
||||
),
|
||||
"handles": {},
|
||||
},
|
||||
"create_resource": {
|
||||
"type": self.ResourceCreateFromOuterEasy,
|
||||
@@ -86,6 +85,15 @@ class Registry:
|
||||
"goal_default": yaml.safe_load(
|
||||
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal))
|
||||
),
|
||||
"handles": {
|
||||
"output": [{
|
||||
"handler_key": "Labware",
|
||||
"label": "Labware",
|
||||
"data_type": "resource",
|
||||
"data_source": "handle",
|
||||
"data_key": "liquid"
|
||||
}]
|
||||
},
|
||||
},
|
||||
"test_latency": {
|
||||
"type": self.EmptyIn,
|
||||
@@ -94,11 +102,14 @@ class Registry:
|
||||
"result": {"latency_ms": "latency_ms", "time_diff_ms": "time_diff_ms"},
|
||||
"schema": ros_action_to_json_schema(self.EmptyIn),
|
||||
"goal_default": {},
|
||||
"handles": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
"icon": "icon_device.webp",
|
||||
"registry_type": "device",
|
||||
"handles": [],
|
||||
"init_param_schema": {},
|
||||
"schema": {"properties": {}, "additionalProperties": False, "type": "object"},
|
||||
"file_path": "/",
|
||||
}
|
||||
@@ -132,6 +143,10 @@ class Registry:
|
||||
resource_info["description"] = ""
|
||||
if "icon" not in resource_info:
|
||||
resource_info["icon"] = ""
|
||||
if "handles" not in resource_info:
|
||||
resource_info["handles"] = []
|
||||
if "init_param_schema" not in resource_info:
|
||||
resource_info["init_param_schema"] = {}
|
||||
resource_info["registry_type"] = "resource"
|
||||
self.resource_type_registry.update(data)
|
||||
logger.debug(
|
||||
@@ -194,6 +209,10 @@ class Registry:
|
||||
device_config["description"] = ""
|
||||
if "icon" not in device_config:
|
||||
device_config["icon"] = ""
|
||||
if "handles" not in device_config:
|
||||
device_config["handles"] = []
|
||||
if "init_param_schema" not in device_config:
|
||||
device_config["init_param_schema"] = {}
|
||||
device_config["registry_type"] = "device"
|
||||
if "class" in device_config:
|
||||
# 处理状态类型
|
||||
@@ -206,6 +225,8 @@ class Registry:
|
||||
# 处理动作值映射
|
||||
if "action_value_mappings" in device_config["class"]:
|
||||
for action_name, action_config in device_config["class"]["action_value_mappings"].items():
|
||||
if "handles" not in action_config:
|
||||
action_config["handles"] = []
|
||||
if "type" in action_config:
|
||||
action_config["type"] = self._replace_type_with_class(
|
||||
action_config["type"], device_id, f"动作 {action_name}"
|
||||
|
||||
@@ -131,7 +131,7 @@ _msg_converter: Dict[Type, Any] = {
|
||||
Bool: lambda x: Bool(data=bool(x)),
|
||||
str: str,
|
||||
String: lambda x: String(data=str(x)),
|
||||
Point: lambda x: Point(x=x.x, y=x.y, z=x.z),
|
||||
Point: lambda x: Point(x=x.x, y=x.y, z=x.z) if not isinstance(x, dict) else Point(x=x.get("x", 0), y=x.get("y", 0), z=x.get("z", 0)),
|
||||
Resource: lambda x: Resource(
|
||||
id=x.get("id", ""),
|
||||
name=x.get("name", ""),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import copy
|
||||
import functools
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
@@ -20,16 +19,29 @@ from rclpy.service import Service
|
||||
from unilabos_msgs.action import SendCmd
|
||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||
|
||||
from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type, resource_ulab_to_plr, \
|
||||
initialize_resources, list_to_nested_dict, dict_to_tree, resource_plr_to_ulab, tree_to_list
|
||||
from unilabos.resources.graphio import (
|
||||
convert_resources_to_type,
|
||||
convert_resources_from_type,
|
||||
resource_ulab_to_plr,
|
||||
initialize_resources,
|
||||
dict_to_tree,
|
||||
resource_plr_to_ulab,
|
||||
tree_to_list,
|
||||
)
|
||||
from unilabos.ros.msgs.message_converter import (
|
||||
convert_to_ros_msg,
|
||||
convert_from_ros_msg,
|
||||
convert_from_ros_msg_with_mapping,
|
||||
convert_to_ros_msg_with_mapping, ros_action_to_json_schema,
|
||||
convert_to_ros_msg_with_mapping,
|
||||
)
|
||||
from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, \
|
||||
SerialCommand # type: ignore
|
||||
from unilabos_msgs.srv import (
|
||||
ResourceAdd,
|
||||
ResourceGet,
|
||||
ResourceDelete,
|
||||
ResourceUpdate,
|
||||
ResourceList,
|
||||
SerialCommand,
|
||||
) # type: ignore
|
||||
from unilabos_msgs.msg import Resource # type: ignore
|
||||
|
||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
@@ -37,7 +49,7 @@ from unilabos.ros.x.rclpyx import get_event_loop
|
||||
from unilabos.ros.utils.driver_creator import ProtocolNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
||||
from unilabos.utils.async_util import run_async_func
|
||||
from unilabos.utils.log import info, debug, warning, error, critical, logger
|
||||
from unilabos.utils.type_check import get_type_class, TypeEncoder
|
||||
from unilabos.utils.type_check import get_type_class, TypeEncoder, serialize_result_info
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -292,7 +304,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
self.create_ros_action_server(action_name, action_value_mapping)
|
||||
|
||||
# 创建线程池执行器
|
||||
self._executor = ThreadPoolExecutor(max_workers=max(len(action_value_mappings), 1), thread_name_prefix=f"ROSDevice{self.device_id}")
|
||||
self._executor = ThreadPoolExecutor(
|
||||
max_workers=max(len(action_value_mappings), 1), thread_name_prefix=f"ROSDevice{self.device_id}"
|
||||
)
|
||||
|
||||
# 创建资源管理客户端
|
||||
self._resource_clients: Dict[str, Client] = {
|
||||
@@ -334,7 +348,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
other_calling_param["slot"] = slot
|
||||
# 本地拿到这个物料,可能需要先做初始化?
|
||||
if isinstance(resources, list):
|
||||
if len(resources) == 1 and isinstance(resources[0], list) and not initialize_full: # 取消,不存在的情况
|
||||
if (
|
||||
len(resources) == 1 and isinstance(resources[0], list) and not initialize_full
|
||||
): # 取消,不存在的情况
|
||||
# 预先initialize过,以整组的形式传入
|
||||
request.resources = [convert_to_ros_msg(Resource, resource_) for resource_ in resources[0]]
|
||||
elif initialize_full:
|
||||
@@ -349,6 +365,20 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
response = rclient.call(request)
|
||||
# 应该先add_resource了
|
||||
res.response = "OK"
|
||||
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
||||
if hasattr(self.driver_instance, "create_resource"):
|
||||
create_resource_func = getattr(self.driver_instance, "create_resource")
|
||||
create_resource_func(
|
||||
resource_tracker=self.resource_tracker,
|
||||
resources=request.resources,
|
||||
bind_parent_id=bind_parent_id,
|
||||
bind_location=location,
|
||||
liquid_input_slot=LIQUID_INPUT_SLOT,
|
||||
liquid_type=ADD_LIQUID_TYPE,
|
||||
liquid_volume=LIQUID_VOLUME,
|
||||
slot_on_deck=slot,
|
||||
)
|
||||
return res
|
||||
# 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中
|
||||
resource = self.resource_tracker.figure_resource({"name": bind_parent_id})
|
||||
# request.resources = [convert_to_ros_msg(Resource, resources)]
|
||||
@@ -359,6 +389,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
from pylabrobot.resources import Coordinate
|
||||
from pylabrobot.resources import OTDeck
|
||||
from pylabrobot.resources import Plate
|
||||
|
||||
contain_model = not isinstance(resource, Deck)
|
||||
if isinstance(resource, ResourcePLR):
|
||||
# resources.list()
|
||||
@@ -366,25 +397,38 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
plr_instance = resource_ulab_to_plr(resources_tree[0], contain_model)
|
||||
if isinstance(plr_instance, Plate):
|
||||
empty_liquid_info_in = [(None, 0)] * plr_instance.num_items
|
||||
for liquid_type, liquid_volume, liquid_input_slot in zip(ADD_LIQUID_TYPE, LIQUID_VOLUME, LIQUID_INPUT_SLOT):
|
||||
for liquid_type, liquid_volume, liquid_input_slot in zip(
|
||||
ADD_LIQUID_TYPE, LIQUID_VOLUME, LIQUID_INPUT_SLOT
|
||||
):
|
||||
empty_liquid_info_in[liquid_input_slot] = (liquid_type, liquid_volume)
|
||||
plr_instance.set_well_liquids(empty_liquid_info_in)
|
||||
if isinstance(resource, OTDeck) and "slot" in other_calling_param:
|
||||
resource.assign_child_at_slot(plr_instance, **other_calling_param)
|
||||
else:
|
||||
_discard_slot = other_calling_param.pop("slot", -1)
|
||||
resource.assign_child_resource(plr_instance, Coordinate(location["x"], location["y"], location["z"]), **other_calling_param)
|
||||
request2.resources = [convert_to_ros_msg(Resource, r) for r in tree_to_list([resource_plr_to_ulab(resource)])]
|
||||
resource.assign_child_resource(
|
||||
plr_instance,
|
||||
Coordinate(location["x"], location["y"], location["z"]),
|
||||
**other_calling_param,
|
||||
)
|
||||
request2.resources = [
|
||||
convert_to_ros_msg(Resource, r) for r in tree_to_list([resource_plr_to_ulab(resource)])
|
||||
]
|
||||
rclient2.call(request2)
|
||||
# 发送给ResourceMeshManager
|
||||
action_client = ActionClient(
|
||||
self, SendCmd, "/devices/resource_mesh_manager/add_resource_mesh", callback_group=self.callback_group
|
||||
self,
|
||||
SendCmd,
|
||||
"/devices/resource_mesh_manager/add_resource_mesh",
|
||||
callback_group=self.callback_group,
|
||||
)
|
||||
goal = SendCmd.Goal()
|
||||
goal.command = json.dumps({
|
||||
goal.command = json.dumps(
|
||||
{
|
||||
"resources": resources,
|
||||
"bind_parent_id": bind_parent_id,
|
||||
})
|
||||
}
|
||||
)
|
||||
future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4())
|
||||
|
||||
def done_cb(*args):
|
||||
@@ -401,10 +445,16 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
# noinspection PyTypeChecker
|
||||
self._service_server: Dict[str, Service] = {
|
||||
"query_host_name": self.create_service(
|
||||
SerialCommand, f"/srv{self.namespace}/query_host_name", query_host_name_cb, callback_group=self.callback_group
|
||||
SerialCommand,
|
||||
f"/srv{self.namespace}/query_host_name",
|
||||
query_host_name_cb,
|
||||
callback_group=self.callback_group,
|
||||
),
|
||||
"append_resource": self.create_service(
|
||||
SerialCommand, f"/srv{self.namespace}/append_resource", append_resource, callback_group=self.callback_group
|
||||
SerialCommand,
|
||||
f"/srv{self.namespace}/append_resource",
|
||||
append_resource,
|
||||
callback_group=self.callback_group,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -433,6 +483,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
registered_devices[self.device_id] = device_info
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||
|
||||
if not BasicConfig.is_host_mode:
|
||||
sclient = self.create_client(SerialCommand, "/node_info_update")
|
||||
# 启动线程执行发送任务
|
||||
@@ -440,7 +491,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
target=self.send_slave_node_info,
|
||||
args=(sclient,),
|
||||
daemon=True,
|
||||
name=f"ROSDevice{self.device_id}_send_slave_node_info"
|
||||
name=f"ROSDevice{self.device_id}_send_slave_node_info",
|
||||
).start()
|
||||
else:
|
||||
host_node = HostNode.get_instance(0)
|
||||
@@ -451,12 +502,18 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
sclient.wait_for_service()
|
||||
request = SerialCommand.Request()
|
||||
from unilabos.config.config import BasicConfig
|
||||
request.command = json.dumps({
|
||||
|
||||
request.command = json.dumps(
|
||||
{
|
||||
"SYNC_SLAVE_NODE_INFO": {
|
||||
"machine_name": BasicConfig.machine_name,
|
||||
"type": "slave",
|
||||
"edge_device_id": self.device_id
|
||||
}}, ensure_ascii=False, cls=TypeEncoder)
|
||||
"edge_device_id": self.device_id,
|
||||
}
|
||||
},
|
||||
ensure_ascii=False,
|
||||
cls=TypeEncoder,
|
||||
)
|
||||
|
||||
# 发送异步请求并等待结果
|
||||
future = sclient.call_async(request)
|
||||
@@ -529,6 +586,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
"""创建动作执行回调函数"""
|
||||
|
||||
async def execute_callback(goal_handle: ServerGoalHandle):
|
||||
# 初始化结果信息变量
|
||||
execution_error = ""
|
||||
execution_success = False
|
||||
action_return_value = None
|
||||
|
||||
self.lab_logger().info(f"执行动作: {action_name}")
|
||||
goal = goal_handle.request
|
||||
|
||||
@@ -568,7 +630,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
current_resources.extend(response.resources)
|
||||
else:
|
||||
r = ResourceGet.Request()
|
||||
r.id = action_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else action_kwargs[k][0]["id"]
|
||||
r.id = (
|
||||
action_kwargs[k]["id"]
|
||||
if v == "unilabos_msgs/Resource"
|
||||
else action_kwargs[k][0]["id"]
|
||||
)
|
||||
r.with_children = True
|
||||
response = await self._resource_clients["resource_get"].call_async(r)
|
||||
current_resources.extend(response.resources)
|
||||
@@ -591,7 +657,19 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
if asyncio.iscoroutinefunction(ACTION):
|
||||
try:
|
||||
self.lab_logger().info(f"异步执行动作 {ACTION}")
|
||||
future = ROS2DeviceNode.run_async_func(ACTION, **action_kwargs)
|
||||
future = ROS2DeviceNode.run_async_func(ACTION, trace_error=False, **action_kwargs)
|
||||
|
||||
def _handle_future_exception(fut):
|
||||
nonlocal execution_error, execution_success, action_return_value
|
||||
try:
|
||||
action_return_value = fut.result()
|
||||
execution_success = True
|
||||
except Exception as e:
|
||||
execution_error = traceback.format_exc()
|
||||
error(f"异步任务 {ACTION.__name__} 报错了")
|
||||
error(traceback.format_exc())
|
||||
|
||||
future.add_done_callback(_handle_future_exception)
|
||||
except Exception as e:
|
||||
self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}")
|
||||
raise e
|
||||
@@ -600,9 +678,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
future = self._executor.submit(ACTION, **action_kwargs)
|
||||
|
||||
def _handle_future_exception(fut):
|
||||
nonlocal execution_error, execution_success, action_return_value
|
||||
try:
|
||||
fut.result()
|
||||
action_return_value = fut.result()
|
||||
execution_success = True
|
||||
except Exception as e:
|
||||
execution_error = traceback.format_exc()
|
||||
error(f"同步任务 {ACTION.__name__} 报错了")
|
||||
error(traceback.format_exc())
|
||||
|
||||
@@ -693,6 +774,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
for attr_name in result_msg_types.keys():
|
||||
if attr_name in ["success", "reached_goal"]:
|
||||
setattr(result_msg, attr_name, True)
|
||||
elif attr_name == "return_info":
|
||||
setattr(result_msg, attr_name, serialize_result_info(execution_error, execution_success, action_return_value))
|
||||
|
||||
self.lab_logger().info(f"动作 {action_name} 完成并返回结果")
|
||||
return result_msg
|
||||
@@ -738,8 +821,8 @@ class ROS2DeviceNode:
|
||||
return cls._loop
|
||||
|
||||
@classmethod
|
||||
def run_async_func(cls, func, **kwargs):
|
||||
return run_async_func(func, loop=cls._loop, **kwargs)
|
||||
def run_async_func(cls, func, trace_error=True, **kwargs):
|
||||
return run_async_func(func, loop=cls._loop, trace_error=trace_error, **kwargs)
|
||||
|
||||
@property
|
||||
def driver_instance(self):
|
||||
@@ -791,7 +874,11 @@ class ROS2DeviceNode:
|
||||
self.resource_tracker = DeviceNodeResourceTracker()
|
||||
|
||||
# use_pylabrobot_creator 使用 cls的包路径检测
|
||||
use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") or driver_class.__name__ == "LiquidHandlerAbstract"
|
||||
use_pylabrobot_creator = (
|
||||
driver_class.__module__.startswith("pylabrobot")
|
||||
or driver_class.__name__ == "LiquidHandlerAbstract"
|
||||
or driver_class.__name__ == "LiquidHandlerBiomek"
|
||||
)
|
||||
|
||||
# TODO: 要在创建之前预先请求服务器是否有当前id的物料,放到resource_tracker中,让pylabrobot进行创建
|
||||
# 创建设备类实例
|
||||
|
||||
@@ -151,7 +151,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
mqtt_client.publish_registry(device_info["id"], device_info)
|
||||
for resource_info in lab_registry.obtain_registry_resource_info():
|
||||
mqtt_client.publish_registry(resource_info["id"], resource_info)
|
||||
|
||||
time.sleep(1) # 等待MQTT连接稳定
|
||||
# 首次发现网络中的设备
|
||||
self._discover_devices()
|
||||
|
||||
@@ -203,8 +203,12 @@ class HostNode(BaseROS2DeviceNode):
|
||||
try:
|
||||
for bridge in self.bridges:
|
||||
if hasattr(bridge, "resource_add"):
|
||||
self.lab_logger().info("[Host Node-Resource] Adding resources to bridge.")
|
||||
resource_add_res = bridge.resource_add(add_schema(resource_with_parent_name))
|
||||
resource_start_time = time.time()
|
||||
resource_add_res = bridge.resource_add(add_schema(resource_with_parent_name), True)
|
||||
resource_end_time = time.time()
|
||||
self.lab_logger().info(
|
||||
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
|
||||
)
|
||||
except Exception as ex:
|
||||
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
@@ -610,13 +614,21 @@ class HostNode(BaseROS2DeviceNode):
|
||||
"""获取结果回调"""
|
||||
result_msg = future.result().result
|
||||
result_data = convert_from_ros_msg(result_msg)
|
||||
status = "success"
|
||||
try:
|
||||
ret = json.loads(result_data.get("return_info", "{}")) # 确保返回信息是有效的JSON
|
||||
suc = ret.get("suc", False)
|
||||
if not suc:
|
||||
status = "failed"
|
||||
except json.JSONDecodeError:
|
||||
status = "failed"
|
||||
self.lab_logger().info(f"[Host Node] Result for {action_id} ({uuid_str}): success")
|
||||
self.lab_logger().debug(f"[Host Node] Result data: {result_data}")
|
||||
|
||||
if uuid_str:
|
||||
for bridge in self.bridges:
|
||||
if hasattr(bridge, "publish_job_status"):
|
||||
bridge.publish_job_status(result_data, uuid_str, "success")
|
||||
bridge.publish_job_status(result_data, uuid_str, status, result_data.get("return_info", "{}"))
|
||||
|
||||
def cancel_goal(self, goal_uuid: str) -> None:
|
||||
"""取消目标"""
|
||||
@@ -856,7 +868,6 @@ class HostNode(BaseROS2DeviceNode):
|
||||
测试网络延迟的action实现
|
||||
通过5次ping-pong机制校对时间误差并计算实际延迟
|
||||
"""
|
||||
import time
|
||||
import uuid as uuid_module
|
||||
|
||||
self.lab_logger().info("=" * 60)
|
||||
|
||||
@@ -5,7 +5,7 @@ from asyncio import get_event_loop
|
||||
from unilabos.utils.log import error
|
||||
|
||||
|
||||
def run_async_func(func, *, loop=None, **kwargs):
|
||||
def run_async_func(func, *, loop=None, trace_error=True, **kwargs):
|
||||
if loop is None:
|
||||
loop = get_event_loop()
|
||||
|
||||
@@ -17,5 +17,6 @@ def run_async_func(func, *, loop=None, **kwargs):
|
||||
error(traceback.format_exc())
|
||||
|
||||
future = asyncio.run_coroutine_threadsafe(func(**kwargs), loop)
|
||||
if trace_error:
|
||||
future.add_done_callback(_handle_future_exception)
|
||||
return future
|
||||
@@ -1,4 +1,4 @@
|
||||
import collections
|
||||
import collections.abc
|
||||
import json
|
||||
from typing import get_origin, get_args
|
||||
|
||||
@@ -21,3 +21,46 @@ class TypeEncoder(json.JSONEncoder):
|
||||
return str(obj)[8:-2]
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
class ResultInfoEncoder(json.JSONEncoder):
|
||||
"""专门用于处理任务执行结果信息的JSON编码器"""
|
||||
|
||||
def default(self, obj):
|
||||
# 优先处理类型对象
|
||||
if isinstance(obj, type):
|
||||
return str(obj)[8:-2]
|
||||
|
||||
# 对于无法序列化的对象,统一转换为字符串
|
||||
try:
|
||||
# 尝试调用 __dict__ 或者其他序列化方法
|
||||
if hasattr(obj, "__dict__"):
|
||||
return obj.__dict__
|
||||
elif hasattr(obj, "_asdict"): # namedtuple
|
||||
return obj._asdict()
|
||||
elif hasattr(obj, "to_dict"):
|
||||
return obj.to_dict()
|
||||
elif hasattr(obj, "dict"):
|
||||
return obj.dict()
|
||||
else:
|
||||
# 如果都不行,转换为字符串
|
||||
return str(obj)
|
||||
except Exception:
|
||||
# 如果转换失败,直接返回字符串表示
|
||||
return str(obj)
|
||||
|
||||
|
||||
def serialize_result_info(error: str, suc: bool, return_value=None) -> str:
|
||||
"""
|
||||
序列化任务执行结果信息
|
||||
|
||||
Args:
|
||||
error: 错误信息字符串
|
||||
suc: 是否成功的布尔值
|
||||
return_value: 返回值,可以是任何类型
|
||||
|
||||
Returns:
|
||||
JSON字符串格式的结果信息
|
||||
"""
|
||||
result_info = {"error": error, "suc": suc, "return_value": return_value}
|
||||
|
||||
return json.dumps(result_info, ensure_ascii=False, cls=ResultInfoEncoder)
|
||||
|
||||
@@ -29,6 +29,8 @@ set(action_files
|
||||
"action/HeatChillStart.action"
|
||||
"action/HeatChillStop.action"
|
||||
|
||||
"action/LiquidHandlerProtocolCreation.action"
|
||||
|
||||
"action/LiquidHandlerAspirate.action"
|
||||
"action/LiquidHandlerDiscardTips.action"
|
||||
"action/LiquidHandlerDispense.action"
|
||||
@@ -44,6 +46,11 @@ set(action_files
|
||||
"action/LiquidHandlerStamp.action"
|
||||
"action/LiquidHandlerTransfer.action"
|
||||
|
||||
"action/LiquidHandlerTransferBiomek.action"
|
||||
"action/LiquidHandlerIncubateBiomek.action"
|
||||
"action/LiquidHandlerMoveBiomek.action"
|
||||
"action/LiquidHandlerOscillateBiomek.action"
|
||||
|
||||
"action/LiquidHandlerAdd.action"
|
||||
"action/LiquidHandlerMix.action"
|
||||
"action/LiquidHandlerMoveTo.action"
|
||||
|
||||
@@ -4,6 +4,7 @@ string from_repo_position
|
||||
Resource to_repo
|
||||
string to_repo_position
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
|
||||
@@ -5,6 +5,7 @@ float64 volume # Optional. Volume of solvent to clean vessel with.
|
||||
float64 temp # Optional. Temperature to heat vessel to while cleaning.
|
||||
int32 repeats # Optional. Number of cleaning cycles to perform.
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
---
|
||||
|
||||
string return_info
|
||||
---
|
||||
@@ -3,6 +3,7 @@ string vessel
|
||||
string gas
|
||||
int32 repeats
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
|
||||
@@ -5,6 +5,7 @@ float64 temp
|
||||
float64 time
|
||||
float64 stir_speed
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
float64 float_in
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
@@ -6,6 +6,7 @@ bool stir
|
||||
float64 stir_speed
|
||||
string purpose
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
@@ -3,6 +3,7 @@ string vessel
|
||||
float64 temp
|
||||
string purpose
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
@@ -1,6 +1,7 @@
|
||||
# Organic
|
||||
string vessel
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
@@ -1,4 +1,5 @@
|
||||
int32 int_input
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
@@ -15,6 +15,7 @@ int32 mix_rate
|
||||
float64 mix_liquid_height
|
||||
string[] none_keys
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈
|
||||
@@ -7,5 +7,6 @@ float64[] liquid_height
|
||||
float64[] blow_out_air_volume
|
||||
string spread
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
@@ -3,6 +3,7 @@
|
||||
int32[] use_channels
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
@@ -8,6 +8,7 @@ int32[] blow_out_air_volume
|
||||
string spread
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
@@ -6,6 +6,7 @@ geometry_msgs/Point[] offsets
|
||||
bool allow_nonzero_volume
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
@@ -5,6 +5,7 @@ geometry_msgs/Point offset
|
||||
bool allow_nonzero_volume
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
6
unilabos_msgs/action/LiquidHandlerIncubateBiomek.action
Normal file
6
unilabos_msgs/action/LiquidHandlerIncubateBiomek.action
Normal file
@@ -0,0 +1,6 @@
|
||||
int32 time
|
||||
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
@@ -6,6 +6,7 @@ geometry_msgs/Point[] offsets
|
||||
float64 mix_rate
|
||||
string[] none_keys
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈
|
||||
7
unilabos_msgs/action/LiquidHandlerMoveBiomek.action
Normal file
7
unilabos_msgs/action/LiquidHandlerMoveBiomek.action
Normal file
@@ -0,0 +1,7 @@
|
||||
string source
|
||||
string target
|
||||
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
@@ -12,6 +12,7 @@ string put_direction
|
||||
float64 pickup_distance_from_top
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
@@ -13,6 +13,7 @@ string put_direction
|
||||
float64 pickup_distance_from_top
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
@@ -12,6 +12,7 @@ string get_direction
|
||||
string put_direction
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
@@ -2,6 +2,7 @@ Resource well
|
||||
float64 dis_to_top
|
||||
int32 channel
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈
|
||||
7
unilabos_msgs/action/LiquidHandlerOscillateBiomek.action
Normal file
7
unilabos_msgs/action/LiquidHandlerOscillateBiomek.action
Normal file
@@ -0,0 +1,7 @@
|
||||
int32 rpm
|
||||
int32 time
|
||||
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
@@ -5,6 +5,7 @@ int32[] use_channels
|
||||
geometry_msgs/Point[] offsets
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
@@ -4,6 +4,7 @@ Resource tip_rack
|
||||
geometry_msgs/Point offset
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
10
unilabos_msgs/action/LiquidHandlerProtocolCreation.action
Normal file
10
unilabos_msgs/action/LiquidHandlerProtocolCreation.action
Normal file
@@ -0,0 +1,10 @@
|
||||
string protocol_name
|
||||
string protocol_description
|
||||
string protocol_version
|
||||
string protocol_author
|
||||
string protocol_date
|
||||
string protocol_type
|
||||
string[] none_keys
|
||||
---
|
||||
string return_info
|
||||
---
|
||||
@@ -12,6 +12,7 @@ bool is_96_well
|
||||
float64[] top
|
||||
string[] none_keys
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈
|
||||
@@ -4,6 +4,7 @@ int32[] use_channels
|
||||
bool allow_nonzero_volume
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
@@ -3,6 +3,7 @@
|
||||
bool allow_nonzero_volume
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
@@ -7,6 +7,7 @@ float64 aspiration_flow_rate
|
||||
float64 dispense_flow_rate
|
||||
---
|
||||
# 结果字段
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈字段
|
||||
@@ -20,6 +20,7 @@ float64 mix_liquid_height
|
||||
int32[] delays
|
||||
string[] none_keys
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
# 反馈
|
||||
11
unilabos_msgs/action/LiquidHandlerTransferBiomek.action
Normal file
11
unilabos_msgs/action/LiquidHandlerTransferBiomek.action
Normal file
@@ -0,0 +1,11 @@
|
||||
string source
|
||||
string target
|
||||
string tip_rack
|
||||
float64 volume
|
||||
string aspirate_technique
|
||||
string dispense_technique
|
||||
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
@@ -2,5 +2,6 @@ float64 x
|
||||
float64 y
|
||||
float64 z
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
@@ -10,6 +10,7 @@ float64 rinsing_volume
|
||||
int32 rinsing_repeats
|
||||
bool solid
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
|
||||
@@ -4,5 +4,6 @@ string[] bind_parent_ids
|
||||
geometry_msgs/Point[] bind_locations
|
||||
string[] other_calling_params
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
@@ -8,5 +8,6 @@ string[] liquid_type
|
||||
float32[] liquid_volume
|
||||
int32 slot_on_deck
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Simple
|
||||
string command
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
|
||||
@@ -13,6 +13,7 @@ float64 stir_time # Optional. Time stir for after adding solvent, before separat
|
||||
float64 stir_speed # Optional. Speed to stir at after adding solvent, before separation of phases.
|
||||
float64 settling_time # Optional. Time
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
|
||||
@@ -2,6 +2,7 @@ int32 powder_tube_number
|
||||
string target_tube_position
|
||||
float64 compound_mass
|
||||
---
|
||||
string return_info
|
||||
float64 actual_mass_mg
|
||||
bool success
|
||||
---
|
||||
@@ -3,6 +3,7 @@ float64 stir_time
|
||||
float64 stir_speed
|
||||
float64 settling_time
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
@@ -1,4 +1,5 @@
|
||||
string string
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
@@ -3,6 +3,7 @@ string wf_name
|
||||
string params
|
||||
Resource resource
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
|
||||
Reference in New Issue
Block a user