Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
9aeffeb
0.10.7 Update (#101)
Xuwznln Oct 12, 2025
fb93b1c
fix startup env check.
Xuwznln Oct 12, 2025
eb1f3fb
Try fix one-key build on linux
Xuwznln Oct 12, 2025
6b5765b
Complete all one key installation
Xuwznln Oct 12, 2025
51d3e61
fix: rename schema field to resource_schema with serialization and va…
Mile-Away Oct 12, 2025
e0da1c7
Fix one-key installation build
Xuwznln Oct 12, 2025
2a8e8d0
Fix conda pack on windows
Xuwznln Oct 13, 2025
ef3f24e
add plr_to_bioyond, and refactor bioyond stations
TablewareBox Oct 13, 2025
b64466d
modify default config
TablewareBox Oct 13, 2025
c70eafa
Fix one-key installation build for windows
Xuwznln Oct 13, 2025
c85c498
Fix workstation startup
Xuwznln Oct 13, 2025
7c440d1
Fix/resource UUID and doc fix (#109)
Xuwznln Oct 16, 2025
0260cbb
Close #107
Xuwznln Oct 16, 2025
d4415f5
Fix/update resource (#112)
Xuwznln Oct 16, 2025
1b43c53
fix resource_get in action
TablewareBox Oct 17, 2025
166d84a
fix(reaction_station): 清空工作流序列和参数避免重复执行 (#113)
ZiWei09 Oct 17, 2025
bc30f23
Update create_resource device_id
Xuwznln Oct 20, 2025
37ee43d
Update ResourceTracker
TablewareBox Oct 18, 2025
bb3ca64
Update graphio together with workstation design.
ZiWei09 Oct 18, 2025
a2a827d
Update workstation & bioyond example
ZiWei09 Oct 21, 2025
9645609
PRCXI Update
qxw138 Oct 21, 2025
42b78ab
Update resource extra & uuid.
Xuwznln Oct 22, 2025
9bd72b4
Update workstation.
ZiWei09 Oct 27, 2025
5fc7eb7
封膜仪、撕膜仪、耗材站接口
ElijahChang929 Jun 7, 2025
8807865
添加Raman和xrd相关代码
WenzheG Nov 5, 2025
b6dfe2b
Resource update & asyncio fix
Xuwznln Oct 31, 2025
813400f
bump version to 0.10.9
Xuwznln Nov 14, 2025
872b3d7
PRCXI Reset Error Correction (#166)
ALITTLELZ Nov 14, 2025
304827f
1114物料手册定义教程byxinyu (#165)
lixinyu1011 Nov 14, 2025
448e007
3d sim (#97)
q434343 Nov 14, 2025
a242253
标准化opcua设备接入unilab (#78)
tt11142023 Nov 14, 2025
37e0f10
add new laiyu liquid driver, yaml and json files (#164)
xiaoyu10031 Nov 14, 2025
a625a86
HR物料同步,前端展示位置修复 (#135)
ZiWei09 Nov 14, 2025
b475db6
nmr
WenzheG Sep 29, 2025
4d3475a
Update devices
Xuwznln Nov 14, 2025
891f126
bump version to 0.10.10
Xuwznln Nov 14, 2025
48895a9
Update repo files.
Xuwznln Nov 14, 2025
4189a2c
Add get_resource_with_dir & get_resource method
Xuwznln Nov 15, 2025
549a502
fix camera & workstation & warehouse & reaction station driver
ZiWei09 Nov 16, 2025
75f0903
update docs, test examples
Xuwznln Nov 18, 2025
7f7b1c1
bump version to 0.10.11
Xuwznln Nov 18, 2025
acf5fde
Add startup_json_path, disable_browser, port config
Xuwznln Nov 18, 2025
d39662f
Update oss config
Xuwznln Nov 18, 2025
931614f
feat(bioyond_studio): 添加项目API接口支持及优化物料管理功能
ZiWei09 Nov 18, 2025
a662c75
feat(bioyond): 添加测量小瓶仓库和更新仓库工厂函数参数
ZiWei09 Nov 19, 2025
554bcad
Support unilabos_samples key
Xuwznln Nov 19, 2025
d328282
add session_id and normal_exit
Xuwznln Nov 20, 2025
8fa3407
Add result schema and add TypedDict conversion.
Xuwznln Nov 25, 2025
f1ad0c9
Fix port error
Xuwznln Nov 25, 2025
ffc583e
Add backend api and update doc
Xuwznln Nov 26, 2025
ed8ee29
Add get_regular_container func
Xuwznln Nov 27, 2025
d390236
Add get_regular_container func
Xuwznln Nov 27, 2025
6fdd482
Transfer_liquid (#176)
ALITTLELZ Nov 26, 2025
c7c14d2
Auto dump logs, fix workstation input schema
Xuwznln Nov 27, 2025
5ce433e
Fix startup with remote resource error
ZiWei09 Nov 28, 2025
52544a2
signal when host node is ready
Xuwznln Dec 2, 2025
9854ed8
fix ros2 future
Xuwznln Dec 4, 2025
b1cdef9
update version to 0.10.12
Xuwznln Dec 4, 2025
91aadba
修改sample_uuid的返回值
Dec 5, 2025
1ef698d
修改pose标签设定机制
Dec 6, 2025
c678008
修改host_node
Dec 8, 2025
b46a51c
添加 aspiate函数返回值
Dec 8, 2025
d7302c3
返回dispense后的sample_uuid
Dec 8, 2025
cc56a68
添加self.pending_liquids_dict的重置方法
Dec 9, 2025
c27f7e4
修改prcxi的json文件,解决trach错误问题
Dec 9, 2025
0d150f7
修改prcxijson,防止PlateT4的硬件错误
Dec 9, 2025
896f287
对laiyu移液站进行部分修改,取消多次初始化的问题
Dec 10, 2025
8ba911b
修改根据新的物料格式,修改可视化
Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions unilabos/device_mesh/resource_visalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,21 @@ def __init__(self, device: dict, resource: dict, enable_rviz: bool = True):
new_dev.set("device_name", node["id"]+"_")
# if node["parent"] is not None:
# new_dev.set("station_name", node["parent"]+'_')

new_dev.set("x",str(float(node["position"]["position"]["x"])/1000))
new_dev.set("y",str(float(node["position"]["position"]["y"])/1000))
new_dev.set("z",str(float(node["position"]["position"]["z"])/1000))
if "position" in node:
new_dev.set("x",str(float(node["position"]["position"]["x"])/1000))
new_dev.set("y",str(float(node["position"]["position"]["y"])/1000))
new_dev.set("z",str(float(node["position"]["position"]["z"])/1000))
if "rotation" in node["config"]:
new_dev.set("rx",str(float(node["config"]["rotation"]["x"])))
new_dev.set("ry",str(float(node["config"]["rotation"]["y"])))
new_dev.set("r",str(float(node["config"]["rotation"]["z"])))
if "pose" in node:
new_dev.set("x",str(float(node["pose"]["position"]["x"])/1000))
new_dev.set("y",str(float(node["pose"]["position"]["y"])/1000))
new_dev.set("z",str(float(node["pose"]["position"]["z"])/1000))
new_dev.set("rx",str(float(node["pose"]["rotation"]["x"])))
new_dev.set("ry",str(float(node["pose"]["rotation"]["y"])))
new_dev.set("r",str(float(node["pose"]["rotation"]["z"])))
if "device_config" in node["config"]:
for key, value in node["config"]["device_config"].items():
new_dev.set(key, str(value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ async def pick_up_tips(self, ops: List[Pickup], use_channels: List[int], **backe
if self.hardware_interface.tip_status == TipStatus.TIP_ATTACHED:
print("已有枪头,无需重复拾取")
return
self.hardware_interface.xyz_controller.move_to_work_coord_safe(x=x, y=-y, z=z,speed=100)
self.hardware_interface.xyz_controller.move_to_work_coord_safe(x=x, y=-y, z=z,speed=200)
self.hardware_interface.xyz_controller.move_to_work_coord_safe(z=self.hardware_interface.xyz_controller.machine_config.safe_z_height,speed=100)
# self.joint_state_publisher.send_resource_action(ops[0].resource.name, x, y, z, "pick",channels=use_channels)
# goback()
Expand Down Expand Up @@ -202,7 +202,7 @@ async def drop_tips(self, ops: List[Drop], use_channels: List[int], **backend_kw
if self.hardware_interface.tip_status == TipStatus.NO_TIP:
print("无枪头,无需丢弃")
return
self.hardware_interface.xyz_controller.move_to_work_coord_safe(x=x, y=-y, z=z)
self.hardware_interface.xyz_controller.move_to_work_coord_safe(x=x, y=-y, z=z,speed=200)
self.hardware_interface.eject_tip
self.hardware_interface.xyz_controller.move_to_work_coord_safe(z=self.hardware_interface.xyz_controller.machine_config.safe_z_height)

Expand Down Expand Up @@ -267,7 +267,7 @@ async def aspirate(
return

# 移动到吸液位置
self.hardware_interface.xyz_controller.move_to_work_coord_safe(x=x, y=-y, z=z)
self.hardware_interface.xyz_controller.move_to_work_coord_safe(x=x, y=-y, z=z,speed=200)
self.pipette_aspirate(volume=ops[0].volume, flow_rate=flow_rate)


Expand Down Expand Up @@ -340,7 +340,7 @@ async def dispense(


# 移动到排液位置
self.hardware_interface.xyz_controller.move_to_work_coord_safe(x=x, y=-y, z=z)
self.hardware_interface.xyz_controller.move_to_work_coord_safe(x=x, y=-y, z=z,speed=200)
self.pipette_dispense(volume=ops[0].volume, flow_rate=flow_rate)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def __init__(self, port: str, address: int = 4, xyz_port: Optional[str] = None):
baudrate=115200
)
self.pipette = SOPAPipette(self.config)
self.pipette_port = port
self.tip_status = TipStatus.NO_TIP
self.current_volume = 0.0
self.max_volume = 1000.0 # 默认1000ul
Expand All @@ -154,7 +155,7 @@ def connect(self) -> bool:
logger.info("移液器连接成功")

# 连接XYZ步进电机控制器(如果提供了端口)
if self.xyz_port:
if self.xyz_port != self.pipette_port:
try:
self.xyz_controller = XYZController(self.xyz_port)
if self.xyz_controller.connect():
Expand All @@ -168,7 +169,12 @@ def connect(self) -> bool:
self.xyz_controller = None
self.xyz_connected = False
else:
logger.info("未配置XYZ步进电机端口,跳过运动控制器连接")
try:
self.xyz_controller = XYZController(self.xyz_port, auto_connect=False)
self.xyz_controller.serial_conn = self.pipette.serial_port
self.xyz_controller.is_connected = True
except Exception as e:
logger.info("未配置XYZ步进电机端口,跳过运动控制器连接")

return True
except Exception as e:
Expand Down
95 changes: 84 additions & 11 deletions unilabos/devices/liquid_handling/liquid_handler_abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from collections import Counter
from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast

from typing_extensions import TypedDict
from pylabrobot.liquid_handling import LiquidHandler, LiquidHandlerBackend, LiquidHandlerChatterboxBackend, Strictness
from unilabos.devices.liquid_handling.rviz_backend import UniLiquidHandlerRvizBackend
from unilabos.devices.liquid_handling.laiyu.backend.laiyu_v_backend import UniLiquidHandlerLaiyuBackend
Expand All @@ -28,12 +29,15 @@
)

from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode

class SimpleReturn(TypedDict):
samples: list
volumes: list

class LiquidHandlerMiddleware(LiquidHandler):
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool = False, channel_num: int = 8, **kwargs):
self._simulator = simulator
self.channel_num = channel_num
self.pending_liquids_dict = {}
joint_config = kwargs.get("joint_config", None)
if simulator:
if joint_config:
Expand Down Expand Up @@ -131,7 +135,9 @@ async def drop_tips(
return await self._simulate_handler.drop_tips(
tip_spots, use_channels, offsets, allow_nonzero_volume, **backend_kwargs
)
return await super().drop_tips(tip_spots, use_channels, offsets, allow_nonzero_volume, **backend_kwargs)
await super().drop_tips(tip_spots, use_channels, offsets, allow_nonzero_volume, **backend_kwargs)
self.pending_liquids_dict = {}
return

async def return_tips(
self, use_channels: Optional[list[int]] = None, allow_nonzero_volume: bool = False, **backend_kwargs
Expand All @@ -154,8 +160,10 @@ async def discard_tips(
offsets = [Coordinate.zero()] * len(use_channels)
if self._simulator:
return await self._simulate_handler.discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
return await super().discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)

await super().discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
self.pending_liquids_dict = {}
return

def _check_containers(self, resources: Sequence[Resource]):
super()._check_containers(resources)

Expand All @@ -171,6 +179,8 @@ async def aspirate(
spread: Literal["wide", "tight", "custom"] = "wide",
**backend_kwargs,
):


if self._simulator:
return await self._simulate_handler.aspirate(
resources,
Expand All @@ -183,7 +193,7 @@ async def aspirate(
spread,
**backend_kwargs,
)
return await super().aspirate(
await super().aspirate(
resources,
vols,
use_channels,
Expand All @@ -195,6 +205,18 @@ async def aspirate(
**backend_kwargs,
)

res_samples = []
res_volumes = []
for resource, volume, channel in zip(resources, vols, use_channels):
res_samples.append({"name": resource.name, "sample_uuid": resource.unilabos_extra.get("sample_uuid", None)})
res_volumes.append(volume)
self.pending_liquids_dict[channel] = {
"sample_uuid": resource.unilabos_extra.get("sample_uuid", None),
"volume": volume
}
return SimpleReturn(samples=res_samples, volumes=res_volumes)


async def dispense(
self,
resources: Sequence[Container],
Expand All @@ -206,7 +228,7 @@ async def dispense(
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Literal["wide", "tight", "custom"] = "wide",
**backend_kwargs,
):
) -> SimpleReturn:
if self._simulator:
return await self._simulate_handler.dispense(
resources,
Expand All @@ -219,7 +241,7 @@ async def dispense(
spread,
**backend_kwargs,
)
return await super().dispense(
await super().dispense(
resources,
vols,
use_channels,
Expand All @@ -229,7 +251,17 @@ async def dispense(
blow_out_air_volume,
**backend_kwargs,
)

res_samples = []
res_volumes = []
for resource, volume, channel in zip(resources, vols, use_channels):
res_uuid = self.pending_liquids_dict[channel]["sample_uuid"]
self.pending_liquids_dict[channel]["volume"] -= volume
resource.unilabos_extra["sample_uuid"] = res_uuid
res_samples.append({"name": resource.name, "sample_uuid": res_uuid})
res_volumes.append(volume)

return SimpleReturn(samples=res_samples, volumes=res_volumes)

async def transfer(
self,
source: Well,
Expand Down Expand Up @@ -549,25 +581,66 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
support_touch_tip = True
_ros_node: BaseROS2DeviceNode

def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8):
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8, total_height:float = 310):
"""Initialize a LiquidHandler.

Args:
backend: Backend to use.
deck: Deck to use.
"""
backend_type = None
if isinstance(backend, dict) and "type" in backend:
backend_dict = backend.copy()
type_str = backend_dict.pop("type")
try:
# Try to get class from string using globals (current module), or fallback to pylabrobot or unilabos namespaces
backend_cls = None
if type_str in globals():
backend_cls = globals()[type_str]
else:
# Try resolving dotted notation, e.g. "xxx.yyy.ClassName"
components = type_str.split(".")
mod = None
if len(components) > 1:
module_name = ".".join(components[:-1])
try:
import importlib
mod = importlib.import_module(module_name)
except ImportError:
mod = None
if mod is not None:
backend_cls = getattr(mod, components[-1], None)
if backend_cls is None:
# Try pylabrobot style import (if available)
try:
import pylabrobot
backend_cls = getattr(pylabrobot, type_str, None)
except Exception:
backend_cls = None
if backend_cls is not None and isinstance(backend_cls, type):
backend_type = backend_cls(**backend_dict) # pass the rest of dict as kwargs
except Exception as exc:
raise RuntimeError(f"Failed to convert backend type '{type_str}' to class: {exc}")
else:
backend_type = backend
self._simulator = simulator
self.group_info = dict()
super().__init__(backend, deck, simulator, channel_num)
super().__init__(backend_type, deck, simulator, channel_num)

def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node

@classmethod
def set_liquid(cls, wells: list[Well], liquid_names: list[str], volumes: list[float]):
def set_liquid(cls, wells: list[Well], liquid_names: list[str], volumes: list[float]) -> SimpleReturn:
"""Set the liquid in a well."""
res_samples = []
res_volumes = []
for well, liquid_name, volume in zip(wells, liquid_names, volumes):
well.set_liquids([(liquid_name, volume)]) # type: ignore
res_samples.append({"name": well.name, "sample_uuid": well.unilabos_extra.get("sample_uuid", None)})
res_volumes.append(volume)

return SimpleReturn(samples=res_samples, volumes=res_volumes)
# ---------------------------------------------------------------
# REMOVE LIQUID --------------------------------------------------
# ---------------------------------------------------------------
Expand Down
30 changes: 27 additions & 3 deletions unilabos/devices/liquid_handling/prcxi/prcxi.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
)
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash

from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract, SimpleReturn
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode


Expand Down Expand Up @@ -176,7 +176,7 @@ def post_init(self, ros_node: BaseROS2DeviceNode):
super().post_init(ros_node)
self._unilabos_backend.post_init(ros_node)

def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]):
def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]) -> SimpleReturn:
return super().set_liquid(wells, liquid_names, volumes)

def set_group(self, group_name: str, wells: List[Well], volumes: List[float]):
Expand Down Expand Up @@ -505,7 +505,11 @@ def run_protocol(self):
print(f"PRCXI9300Backend created solution with ID: {solution_id}")
self.api_client.load_solution(solution_id)
print(json.dumps(self.steps_todo_list, indent=2))
return self.api_client.start()
if not self.api_client.start():
return False
if not self.api_client.wait_for_finish():
return False
return True

@classmethod
def check_channels(cls, use_channels: List[int]) -> List[int]:
Expand Down Expand Up @@ -890,6 +894,26 @@ def add_solution(self, name: str, matrix_id: str, steps: List[Dict[str, Any]]) -
def start(self) -> bool:
return self.call("IAutomation", "Start")

def wait_for_finish(self) -> bool:
success = False
start = False
while not success:
status = self.step_state_list()
if status is None:
break
if len(status) == 0:
break
if status[-1]["State"] == 2 and start:
success = True
elif status[-1]["State"] > 2:
break
elif status[-1]["State"] == 0:
start = True
else:
time.sleep(1)
return success


def call(self, service: str, method: str, params: Optional[list] = None) -> Any:
payload = json.dumps(
{"ServiceName": service, "MethodName": method, "Paramters": params or []}, separators=(",", ":")
Expand Down
Loading
Loading