Files
Uni-Lab-OS/unilabos/devices/laiyu_liquid/backend/rviz_backend.py
ZiWei b263a7e679 Workshop bj (#99)
* Add LaiYu Liquid device integration and tests

Introduce LaiYu Liquid device implementation, including backend, controllers, drivers, configuration, and resource files. Add hardware connection, tip pickup, and simplified test scripts, as well as experiment and registry configuration for LaiYu Liquid. Documentation and .gitignore for the device are also included.

* feat(LaiYu_Liquid): 重构设备模块结构并添加硬件文档

refactor: 重新组织LaiYu_Liquid模块目录结构
docs: 添加SOPA移液器和步进电机控制指令文档
fix: 修正设备配置中的最大体积默认值
test: 新增工作台配置测试用例
chore: 删除过时的测试脚本和配置文件

* add

* 重构: 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py 并更新所有导入引用

- 使用 git mv 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py
- 更新所有相关文件中的导入引用
- 保持代码功能不变,仅改善命名一致性
- 测试确认所有导入正常工作

* 修复: 在 core/__init__.py 中添加 LaiYuLiquidBackend 导出

- 添加 LaiYuLiquidBackend 到导入列表
- 添加 LaiYuLiquidBackend 到 __all__ 导出列表
- 确保所有主要类都可以正确导入

* 修复大小写文件夹名字
2025-10-12 22:54:38 +08:00

209 lines
6.8 KiB
Python

import json
from typing import List, Optional, Union
from pylabrobot.liquid_handling.backends.backend import (
LiquidHandlerBackend,
)
from pylabrobot.liquid_handling.standard import (
Drop,
DropTipRack,
MultiHeadAspirationContainer,
MultiHeadAspirationPlate,
MultiHeadDispenseContainer,
MultiHeadDispensePlate,
Pickup,
PickupTipRack,
ResourceDrop,
ResourceMove,
ResourcePickup,
SingleChannelAspiration,
SingleChannelDispense,
)
from pylabrobot.resources import Resource, Tip
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import JointState
import time
from rclpy.action import ActionClient
from unilabos_msgs.action import SendCmd
import re
from unilabos.devices.ros_dev.liquid_handler_joint_publisher import JointStatePublisher
class LiquidHandlerRvizBackend(LiquidHandlerBackend):
"""Chatter box backend for device-free testing. Prints out all operations."""
_pip_length = 5
_vol_length = 8
_resource_length = 20
_offset_length = 16
_flow_rate_length = 10
_blowout_length = 10
_lld_z_length = 10
_kwargs_length = 15
_tip_type_length = 12
_max_volume_length = 16
_fitting_depth_length = 20
_tip_length_length = 16
# _pickup_method_length = 20
_filter_length = 10
def __init__(self, num_channels: int = 8):
"""Initialize a chatter box backend."""
super().__init__()
self._num_channels = num_channels
# rclpy.init()
if not rclpy.ok():
rclpy.init()
self.joint_state_publisher = None
async def setup(self):
self.joint_state_publisher = JointStatePublisher()
await super().setup()
async def stop(self):
pass
def serialize(self) -> dict:
return {**super().serialize(), "num_channels": self.num_channels}
@property
def num_channels(self) -> int:
return self._num_channels
async def assigned_resource_callback(self, resource: Resource):
pass
async def unassigned_resource_callback(self, name: str):
pass
async def pick_up_tips(self, ops: List[Pickup], use_channels: List[int], **backend_kwargs):
for op, channel in zip(ops, use_channels):
offset = f"{round(op.offset.x, 1)},{round(op.offset.y, 1)},{round(op.offset.z, 1)}"
row = (
f" p{channel}: "
f"{op.resource.name[-30:]:<{LiquidHandlerRvizBackend._resource_length}} "
f"{offset:<{LiquidHandlerRvizBackend._offset_length}} "
f"{op.tip.__class__.__name__:<{LiquidHandlerRvizBackend._tip_type_length}} "
f"{op.tip.maximal_volume:<{LiquidHandlerRvizBackend._max_volume_length}} "
f"{op.tip.fitting_depth:<{LiquidHandlerRvizBackend._fitting_depth_length}} "
f"{op.tip.total_tip_length:<{LiquidHandlerRvizBackend._tip_length_length}} "
# f"{str(op.tip.pickup_method)[-20:]:<{ChatterboxBackend._pickup_method_length}} "
f"{'Yes' if op.tip.has_filter else 'No':<{LiquidHandlerRvizBackend._filter_length}}"
)
coordinate = ops[0].resource.get_absolute_location(x="c",y="c")
x = coordinate.x
y = coordinate.y
z = coordinate.z + 70
self.joint_state_publisher.send_resource_action(ops[0].resource.name, x, y, z, "pick")
# goback()
async def drop_tips(self, ops: List[Drop], use_channels: List[int], **backend_kwargs):
coordinate = ops[0].resource.get_absolute_location(x="c",y="c")
x = coordinate.x
y = coordinate.y
z = coordinate.z + 70
self.joint_state_publisher.send_resource_action(ops[0].resource.name, x, y, z, "drop_trash")
# goback()
async def aspirate(
self,
ops: List[SingleChannelAspiration],
use_channels: List[int],
**backend_kwargs,
):
# 执行吸液操作
pass
for o, p in zip(ops, use_channels):
offset = f"{round(o.offset.x, 1)},{round(o.offset.y, 1)},{round(o.offset.z, 1)}"
row = (
f" p{p}: "
f"{o.volume:<{LiquidHandlerRvizBackend._vol_length}} "
f"{o.resource.name[-20:]:<{LiquidHandlerRvizBackend._resource_length}} "
f"{offset:<{LiquidHandlerRvizBackend._offset_length}} "
f"{str(o.flow_rate):<{LiquidHandlerRvizBackend._flow_rate_length}} "
f"{str(o.blow_out_air_volume):<{LiquidHandlerRvizBackend._blowout_length}} "
f"{str(o.liquid_height):<{LiquidHandlerRvizBackend._lld_z_length}} "
# f"{o.liquids if o.liquids is not None else 'none'}"
)
for key, value in backend_kwargs.items():
if isinstance(value, list) and all(isinstance(v, bool) for v in value):
value = "".join("T" if v else "F" for v in value)
if isinstance(value, list):
value = "".join(map(str, value))
row += f" {value:<15}"
coordinate = ops[0].resource.get_absolute_location(x="c",y="c")
x = coordinate.x
y = coordinate.y
z = coordinate.z + 70
self.joint_state_publisher.send_resource_action(ops[0].resource.name, x, y, z, "")
async def dispense(
self,
ops: List[SingleChannelDispense],
use_channels: List[int],
**backend_kwargs,
):
for o, p in zip(ops, use_channels):
offset = f"{round(o.offset.x, 1)},{round(o.offset.y, 1)},{round(o.offset.z, 1)}"
row = (
f" p{p}: "
f"{o.volume:<{LiquidHandlerRvizBackend._vol_length}} "
f"{o.resource.name[-20:]:<{LiquidHandlerRvizBackend._resource_length}} "
f"{offset:<{LiquidHandlerRvizBackend._offset_length}} "
f"{str(o.flow_rate):<{LiquidHandlerRvizBackend._flow_rate_length}} "
f"{str(o.blow_out_air_volume):<{LiquidHandlerRvizBackend._blowout_length}} "
f"{str(o.liquid_height):<{LiquidHandlerRvizBackend._lld_z_length}} "
# f"{o.liquids if o.liquids is not None else 'none'}"
)
for key, value in backend_kwargs.items():
if isinstance(value, list) and all(isinstance(v, bool) for v in value):
value = "".join("T" if v else "F" for v in value)
if isinstance(value, list):
value = "".join(map(str, value))
row += f" {value:<{LiquidHandlerRvizBackend._kwargs_length}}"
coordinate = ops[0].resource.get_absolute_location(x="c",y="c")
x = coordinate.x
y = coordinate.y
z = coordinate.z + 70
self.joint_state_publisher.send_resource_action(ops[0].resource.name, x, y, z, "")
async def pick_up_tips96(self, pickup: PickupTipRack, **backend_kwargs):
pass
async def drop_tips96(self, drop: DropTipRack, **backend_kwargs):
pass
async def aspirate96(
self, aspiration: Union[MultiHeadAspirationPlate, MultiHeadAspirationContainer]
):
pass
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]):
pass
async def pick_up_resource(self, pickup: ResourcePickup):
# 执行资源拾取操作
pass
async def move_picked_up_resource(self, move: ResourceMove):
# 执行资源移动操作
pass
async def drop_resource(self, drop: ResourceDrop):
# 执行资源放置操作
pass
def can_pick_up_tip(self, channel_idx: int, tip: Tip) -> bool:
return True