Compare commits

...

11 Commits

Author SHA1 Message Date
Xuwznln
478a85951c 修复biomek缺少的字段 2025-05-31 00:00:55 +08:00
Xuwznln
0f2555c90c 注册表上报handle和schema (param input) 2025-05-31 00:00:39 +08:00
Guangxin Zhang
d2dda6ee03 Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-05-30 17:11:23 +08:00
Guangxin Zhang
208540b307 Update biomek.py 2025-05-30 17:08:19 +08:00
Guangxin Zhang
cb7c56a1d9 Convert LH action to biomek. 2025-05-30 17:00:06 +08:00
Xuwznln
ea2e9c3e3a fix biomek success type 2025-05-30 16:50:13 +08:00
Guangxin Zhang
0452a68180 Test 2025-05-30 16:03:49 +08:00
Xuwznln
90a0f3db9b merge 2025-05-30 15:40:14 +08:00
Junhan Chang
055d120ba8 更新LiquidHandlerBiomek类,添加资源创建功能,优化协议创建方法,修复部分代码格式问题,更新YAML配置以支持新功能。 2025-05-30 15:38:23 +08:00
Junhan Chang
a948f09f60 add biomek.py demo implementation 2025-05-30 13:33:10 +08:00
Xuwznln
6f69df440c 修复linux 64构建问题 2025-05-30 01:27:34 +08:00
14 changed files with 4686 additions and 72 deletions

View File

@@ -0,0 +1,214 @@
import requests
from typing import List, Sequence, Optional, Union, Literal
from geometry_msgs.msg import Point
from unilabos_msgs.msg import Resource
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker # type: ignore
from .liquid_handler_abstract import LiquidHandlerAbstract
class LiquidHandlerBiomek(LiquidHandlerAbstract):
"""
Biomek液体处理器的实现类继承自LiquidHandlerAbstract。
该类用于处理Biomek液体处理器的特定操作。
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._status = "Idle" # 初始状态为 Idle
self._success = False # 初始成功状态为 False
self._status_queue = kwargs.get("status_queue", None) # 状态队列
self.temp_protocol = {}
self.py32_path = "/opt/py32" # Biomek的Python 3.2路径
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] = [],
):
"""
创建一个新的协议。
Args:
protocol_name (str): 协议名称
protocol_description (str): 协议描述
protocol_version (str): 协议版本
protocol_author (str): 协议作者
protocol_date (str): 协议日期
protocol_type (str): 协议类型
none_keys (List[str]): 需要设置为None的键列表
Returns:
dict: 创建的协议字典
"""
self.temp_protocol = {
"meta": {
"name": protocol_name,
"description": protocol_description,
"version": protocol_version,
"author": protocol_author,
"date": protocol_date,
"type": protocol_type,
},
"labwares": [],
"steps": [],
}
return self.temp_protocol
def run_protocol(self):
"""
执行创建的实验流程。
工作站的完整执行流程是,
从 create_protocol 开始,创建新的 method
随后执行 transfer_liquid 等操作向实验流程添加步骤,
最后 run_protocol 执行整个方法。
Returns:
dict: 执行结果
"""
#use popen or subprocess to create py32 process and communicate send the temp protocol to it
if not self.temp_protocol:
raise ValueError("No protocol created. Please create a protocol first.")
# 模拟执行协议
self._status = "Running"
self._success = True
# 在这里可以添加实际执行协议的逻辑
response = requests.post("localhost:5000/api/protocols", json=self.temp_protocol)
def create_resource(
self,
resource_tracker: DeviceNodeResourceTracker,
resources: list[Resource],
bind_parent_id: str,
bind_location: dict[str, float],
liquid_input_slot: list[int],
liquid_type: list[str],
liquid_volume: list[int],
slot_on_deck: int,
):
"""
创建一个新的资源。
Args:
device_id (str): 设备ID
res_id (str): 资源ID
class_name (str): 资源类名
parent (str): 父级ID
bind_locations (Point): 绑定位置
liquid_input_slot (list[int]): 液体输入槽列表
liquid_type (list[str]): 液体类型列表
liquid_volume (list[int]): 液体体积列表
slot_on_deck (int): 甲板上的槽位
Returns:
dict: 创建的资源字典
"""
# TODO需要对好接口下面这个是临时的
resource = {
"id": res_id,
"class": class_name,
"parent": parent,
"bind_locations": bind_locations.to_dict(),
"liquid_input_slot": liquid_input_slot,
"liquid_type": liquid_type,
"liquid_volume": liquid_volume,
"slot_on_deck": slot_on_deck,
}
self.temp_protocol["labwares"].append(resource)
return resource
def transfer_liquid(
self,
sources: Sequence[Container],
targets: Sequence[Container],
tip_racks: Sequence[TipRack],
solvent: Optional[str] = None,
TipLocation: Optional[str] = None,
*,
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,
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(int)] = 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] = []
):
transfer_params = {
"Span8": False,
"Pod": "Pod1",
"items": {},
"Wash": False,
"Dynamic?": True,
"AutoSelectActiveWashTechnique": False,
"ActiveWashTechnique": "",
"ChangeTipsBetweenDests": False,
"ChangeTipsBetweenSources": False,
"DefaultCaption": "",
"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
self.temp_protocol["steps"].append(transfer_params)
return
s

View File

@@ -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,26 +43,26 @@ 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()
try:
if is_96_well:
pass # This mode is not verified
pass # This mode is not verified
else:
if len(vols) != len(sources):
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(
resources=[src],
vols=[vol],
use_channels=use_channels, # only aspirate96 used, default to None
use_channels=use_channels, # only aspirate96 used, default to None
flow_rates=[flow_rates[0]] if flow_rates else None,
offsets=[offsets[0]] if offsets else None,
liquid_height=[liquid_height[0]] if liquid_height else None,
@@ -64,15 +72,15 @@ class LiquidHandlerAbstract(LiquidHandler):
await self.custom_delay(seconds=delays[0] if delays else 0)
await self.dispense(
resources=waste_liquid,
vols=[vol],
use_channels=use_channels,
flow_rates=[flow_rates[1]] if flow_rates else None,
offsets=[offsets[1]] if offsets else None,
liquid_height=[liquid_height[1]] if liquid_height else None,
blow_out_air_volume=blow_out_air_volume[1] if blow_out_air_volume else None,
spread=spread,
)
await self.discard_tips() # For now, each of tips is discarded after use
vols=[vol],
use_channels=use_channels,
flow_rates=[flow_rates[1]] if flow_rates else None,
offsets=[offsets[1]] if offsets else None,
liquid_height=[liquid_height[1]] if liquid_height else None,
blow_out_air_volume=blow_out_air_volume[1] if blow_out_air_volume else None,
spread=spread,
)
await self.discard_tips() # For now, each of tips is discarded after use
except Exception as e:
raise RuntimeError(f"Liquid removal failed: {e}") from e
@@ -100,13 +108,13 @@ 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."""
try:
if is_96_well:
pass # This mode is not verified.
pass # This mode is not verified.
else:
if len(asp_vols) != len(targets):
raise ValueError("Length of `vols` must match `targets`.")
@@ -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*.
@@ -201,14 +210,15 @@ class LiquidHandlerAbstract(LiquidHandler):
# 96channel head mode
# ------------------------------------------------------------------
if is_96_well:
pass # This mode is not verified
pass # This mode is not verified
else:
if not (len(asp_vols) == len(sources) and len(dis_vols) == len(targets)):
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,9 +306,9 @@ 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
if mix_time is None: # No mixing required
return
"""Mix the liquid in the target wells."""
for _ in range(mix_time):
@@ -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)

View File

@@ -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

View File

@@ -5,22 +5,22 @@ 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"

View File

@@ -4,17 +4,17 @@ 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)

View File

@@ -22,8 +22,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 +43,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 +69,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 +81,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 +245,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 +287,60 @@ 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: {}
schema:
type: object
properties: {}
required: []
additionalProperties: false

View File

@@ -23,20 +23,51 @@ syringe_pump_with_valve.runze:
type: string
description: The position of the valve
required:
- status
- position
- valve_position
- status
- position
- 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
class:
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
type: python
type: python

View File

@@ -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

View File

@@ -4,4 +4,4 @@ workstation:
module: unilabos.ros.nodes.presets.protocol_node:ROS2ProtocolNode
type: ros2
schema:
properties: {}
properties: {}

View File

@@ -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是否已调用
@@ -99,6 +97,8 @@ class Registry:
},
"icon": "icon_device.webp",
"registry_type": "device",
"handles": [],
"init_param_schema": {},
"schema": {"properties": {}, "additionalProperties": False, "type": "object"},
"file_path": "/",
}
@@ -132,6 +132,12 @@ class Registry:
resource_info["description"] = ""
if "icon" not in resource_info:
resource_info["icon"] = ""
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 +200,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:
# 处理状态类型

View File

@@ -349,6 +349,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)]

View File

@@ -13,6 +13,7 @@ endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
add_compile_options(-include cstdint)
endif()
# find dependencies
@@ -28,6 +29,8 @@ set(action_files
"action/HeatChillStart.action"
"action/HeatChillStop.action"
"action/LiquidHandlerProtocolCreation.action"
"action/LiquidHandlerAspirate.action"
"action/LiquidHandlerDiscardTips.action"
"action/LiquidHandlerDispense.action"

View File

@@ -0,0 +1,9 @@
string protocol_name
string protocol_description
string protocol_version
string protocol_author
string protocol_date
string protocol_type
string[] none_keys
---
---