mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 04:51:10 +00:00
* 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__ 导出列表 - 确保所有主要类都可以正确导入 * 修复大小写文件夹名字
1074 lines
40 KiB
Python
1074 lines
40 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
移液控制器模块
|
||
封装SOPA移液器的高级控制功能
|
||
"""
|
||
|
||
# 添加项目根目录到Python路径以解决模块导入问题
|
||
import sys
|
||
import os
|
||
|
||
# 无论如何都添加项目根目录到路径
|
||
current_file = os.path.abspath(__file__)
|
||
# 从 .../Uni-Lab-OS/unilabos/devices/LaiYu_Liquid/controllers/pipette_controller.py
|
||
# 向上5级到 .../Uni-Lab-OS
|
||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_file)))))
|
||
# 强制添加项目根目录到sys.path的开头
|
||
sys.path.insert(0, project_root)
|
||
|
||
import time
|
||
import logging
|
||
from typing import Optional, List, Dict, Tuple
|
||
from dataclasses import dataclass
|
||
from enum import Enum
|
||
|
||
from unilabos.devices.laiyu_liquid.drivers.sopa_pipette_driver import (
|
||
SOPAPipette,
|
||
SOPAConfig,
|
||
SOPAStatusCode,
|
||
DetectionMode,
|
||
create_sopa_pipette,
|
||
)
|
||
from unilabos.devices.laiyu_liquid.drivers.xyz_stepper_driver import (
|
||
XYZStepperController,
|
||
MotorAxis,
|
||
MotorStatus,
|
||
ModbusException
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class TipStatus(Enum):
|
||
"""枪头状态"""
|
||
NO_TIP = "no_tip"
|
||
TIP_ATTACHED = "tip_attached"
|
||
TIP_USED = "tip_used"
|
||
|
||
|
||
class LiquidClass(Enum):
|
||
"""液体类型"""
|
||
WATER = "water"
|
||
SERUM = "serum"
|
||
VISCOUS = "viscous"
|
||
VOLATILE = "volatile"
|
||
CUSTOM = "custom"
|
||
|
||
|
||
@dataclass
|
||
class LiquidParameters:
|
||
"""液体处理参数"""
|
||
aspirate_speed: int = 500 # 吸液速度
|
||
dispense_speed: int = 800 # 排液速度
|
||
air_gap: float = 10.0 # 空气间隙
|
||
blow_out: float = 5.0 # 吹出量
|
||
pre_wet: bool = False # 预润湿
|
||
mix_cycles: int = 0 # 混合次数
|
||
mix_volume: float = 50.0 # 混合体积
|
||
touch_tip: bool = False # 接触壁
|
||
delay_after_aspirate: float = 0.5 # 吸液后延时
|
||
delay_after_dispense: float = 0.5 # 排液后延时
|
||
|
||
|
||
class PipetteController:
|
||
"""移液控制器"""
|
||
|
||
# 预定义液体参数
|
||
LIQUID_PARAMS = {
|
||
LiquidClass.WATER: LiquidParameters(
|
||
aspirate_speed=500,
|
||
dispense_speed=800,
|
||
air_gap=10.0
|
||
),
|
||
LiquidClass.SERUM: LiquidParameters(
|
||
aspirate_speed=200,
|
||
dispense_speed=400,
|
||
air_gap=15.0,
|
||
pre_wet=True,
|
||
delay_after_aspirate=1.0
|
||
),
|
||
LiquidClass.VISCOUS: LiquidParameters(
|
||
aspirate_speed=100,
|
||
dispense_speed=200,
|
||
air_gap=20.0,
|
||
delay_after_aspirate=2.0,
|
||
delay_after_dispense=2.0
|
||
),
|
||
LiquidClass.VOLATILE: LiquidParameters(
|
||
aspirate_speed=800,
|
||
dispense_speed=1000,
|
||
air_gap=5.0,
|
||
delay_after_aspirate=0.2,
|
||
delay_after_dispense=0.2
|
||
)
|
||
}
|
||
|
||
def __init__(self, port: str, address: int = 4, xyz_port: Optional[str] = None):
|
||
"""
|
||
初始化移液控制器
|
||
|
||
Args:
|
||
port: 移液器串口端口
|
||
address: 移液器RS485地址
|
||
xyz_port: XYZ步进电机串口端口(可选,用于枪头装载等运动控制)
|
||
"""
|
||
self.config = SOPAConfig(
|
||
port=port,
|
||
address=address,
|
||
baudrate=115200
|
||
)
|
||
self.pipette = SOPAPipette(self.config)
|
||
self.tip_status = TipStatus.NO_TIP
|
||
self.current_volume = 0.0
|
||
self.max_volume = 1000.0 # 默认1000ul
|
||
self.liquid_class = LiquidClass.WATER
|
||
self.liquid_params = self.LIQUID_PARAMS[LiquidClass.WATER]
|
||
|
||
# XYZ步进电机控制器(用于运动控制)
|
||
self.xyz_controller: Optional[XYZStepperController] = None
|
||
self.xyz_port = xyz_port
|
||
self.xyz_connected = False
|
||
|
||
# 统计信息
|
||
self.tip_count = 0
|
||
self.aspirate_count = 0
|
||
self.dispense_count = 0
|
||
|
||
def connect(self) -> bool:
|
||
"""连接移液器和XYZ步进电机控制器"""
|
||
try:
|
||
# 连接移液器
|
||
if not self.pipette.connect():
|
||
logger.error("移液器连接失败")
|
||
return False
|
||
logger.info("移液器连接成功")
|
||
|
||
# 连接XYZ步进电机控制器(如果提供了端口)
|
||
if self.xyz_port:
|
||
try:
|
||
self.xyz_controller = XYZStepperController(self.xyz_port)
|
||
if self.xyz_controller.connect():
|
||
self.xyz_connected = True
|
||
logger.info(f"XYZ步进电机控制器连接成功: {self.xyz_port}")
|
||
else:
|
||
logger.warning(f"XYZ步进电机控制器连接失败: {self.xyz_port}")
|
||
self.xyz_controller = None
|
||
except Exception as e:
|
||
logger.warning(f"XYZ步进电机控制器连接异常: {e}")
|
||
self.xyz_controller = None
|
||
self.xyz_connected = False
|
||
else:
|
||
logger.info("未配置XYZ步进电机端口,跳过运动控制器连接")
|
||
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"设备连接失败: {e}")
|
||
return False
|
||
|
||
def initialize(self) -> bool:
|
||
"""初始化移液器"""
|
||
try:
|
||
if self.pipette.initialize():
|
||
logger.info("移液器初始化成功")
|
||
# 检查枪头状态
|
||
self._update_tip_status()
|
||
return True
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"移液器初始化失败: {e}")
|
||
return False
|
||
|
||
def disconnect(self):
|
||
"""断开连接"""
|
||
# 断开移液器连接
|
||
self.pipette.disconnect()
|
||
logger.info("移液器已断开")
|
||
|
||
# 断开 XYZ 步进电机连接
|
||
if self.xyz_controller and self.xyz_connected:
|
||
try:
|
||
self.xyz_controller.disconnect()
|
||
self.xyz_connected = False
|
||
logger.info("XYZ 步进电机已断开")
|
||
except Exception as e:
|
||
logger.error(f"断开 XYZ 步进电机失败: {e}")
|
||
|
||
def _check_xyz_safety(self, axis: MotorAxis, target_position: int) -> bool:
|
||
"""
|
||
检查 XYZ 轴移动的安全性
|
||
|
||
Args:
|
||
axis: 电机轴
|
||
target_position: 目标位置(步数)
|
||
|
||
Returns:
|
||
是否安全
|
||
"""
|
||
try:
|
||
# 获取当前电机状态
|
||
motor_position = self.xyz_controller.get_motor_status(axis)
|
||
|
||
# 检查电机状态是否正常 (不是碰撞停止或限位停止)
|
||
if motor_position.status in [MotorStatus.COLLISION_STOP,
|
||
MotorStatus.FORWARD_LIMIT_STOP,
|
||
MotorStatus.REVERSE_LIMIT_STOP]:
|
||
logger.error(f"{axis.name} 轴电机处于错误状态: {motor_position.status.name}")
|
||
return False
|
||
|
||
# 检查位置限制 (扩大安全范围以适应实际硬件)
|
||
# 步进电机的位置范围通常很大,这里设置更合理的范围
|
||
if target_position < -500000 or target_position > 500000:
|
||
logger.error(f"{axis.name} 轴目标位置超出安全范围: {target_position}")
|
||
return False
|
||
|
||
# 检查移动距离是否过大 (单次移动不超过 20000 步,约12mm)
|
||
current_position = motor_position.steps
|
||
move_distance = abs(target_position - current_position)
|
||
if move_distance > 20000:
|
||
logger.error(f"{axis.name} 轴单次移动距离过大: {move_distance}步")
|
||
return False
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"安全检查失败: {e}")
|
||
return False
|
||
|
||
def move_z_relative(self, distance_mm: float, speed: int = 2000, acceleration: int = 500) -> bool:
|
||
"""
|
||
Z轴相对移动
|
||
|
||
Args:
|
||
distance_mm: 移动距离(mm),正值向下,负值向上
|
||
speed: 移动速度(rpm)
|
||
acceleration: 加速度(rpm/s)
|
||
|
||
Returns:
|
||
移动是否成功
|
||
"""
|
||
if not self.xyz_controller or not self.xyz_connected:
|
||
logger.error("XYZ 步进电机未连接,无法执行移动")
|
||
return False
|
||
|
||
try:
|
||
# 参数验证
|
||
if abs(distance_mm) > 15.0:
|
||
logger.error(f"移动距离过大: {distance_mm}mm,最大允许15mm")
|
||
return False
|
||
|
||
if speed < 100 or speed > 5000:
|
||
logger.error(f"速度参数无效: {speed}rpm,范围应为100-5000")
|
||
return False
|
||
|
||
# 获取当前 Z 轴位置
|
||
current_status = self.xyz_controller.get_motor_status(MotorAxis.Z)
|
||
current_z_position = current_status.steps
|
||
|
||
# 计算移动距离对应的步数 (1mm = 1638.4步)
|
||
mm_to_steps = 1638.4
|
||
move_distance_steps = int(distance_mm * mm_to_steps)
|
||
|
||
# 计算目标位置
|
||
target_z_position = current_z_position + move_distance_steps
|
||
|
||
# 安全检查
|
||
if not self._check_xyz_safety(MotorAxis.Z, target_z_position):
|
||
logger.error("Z轴移动安全检查失败")
|
||
return False
|
||
|
||
logger.info(f"Z轴相对移动: {distance_mm}mm ({move_distance_steps}步)")
|
||
logger.info(f"当前位置: {current_z_position}步 -> 目标位置: {target_z_position}步")
|
||
|
||
# 执行移动
|
||
success = self.xyz_controller.move_to_position(
|
||
axis=MotorAxis.Z,
|
||
position=target_z_position,
|
||
speed=speed,
|
||
acceleration=acceleration,
|
||
precision=50
|
||
)
|
||
|
||
if not success:
|
||
logger.error("Z轴移动命令发送失败")
|
||
return False
|
||
|
||
# 等待移动完成
|
||
if not self.xyz_controller.wait_for_completion(MotorAxis.Z, timeout=10.0):
|
||
logger.error("Z轴移动超时")
|
||
return False
|
||
|
||
# 验证移动结果
|
||
final_status = self.xyz_controller.get_motor_status(MotorAxis.Z)
|
||
final_position = final_status.steps
|
||
position_error = abs(final_position - target_z_position)
|
||
|
||
logger.info(f"Z轴移动完成,最终位置: {final_position}步,误差: {position_error}步")
|
||
|
||
if position_error > 100:
|
||
logger.warning(f"Z轴位置误差较大: {position_error}步")
|
||
|
||
return True
|
||
|
||
except ModbusException as e:
|
||
logger.error(f"Modbus通信错误: {e}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"Z轴移动失败: {e}")
|
||
return False
|
||
|
||
def emergency_stop(self) -> bool:
|
||
"""
|
||
紧急停止所有运动
|
||
|
||
Returns:
|
||
停止是否成功
|
||
"""
|
||
success = True
|
||
|
||
# 停止移液器操作
|
||
try:
|
||
if self.pipette and self.connected:
|
||
# 这里可以添加移液器的紧急停止逻辑
|
||
logger.info("移液器紧急停止")
|
||
except Exception as e:
|
||
logger.error(f"移液器紧急停止失败: {e}")
|
||
success = False
|
||
|
||
# 停止 XYZ 轴运动
|
||
try:
|
||
if self.xyz_controller and self.xyz_connected:
|
||
self.xyz_controller.emergency_stop()
|
||
logger.info("XYZ 轴紧急停止")
|
||
except Exception as e:
|
||
logger.error(f"XYZ 轴紧急停止失败: {e}")
|
||
success = False
|
||
|
||
return success
|
||
|
||
def pickup_tip(self) -> bool:
|
||
"""
|
||
装载枪头 - Z轴向下移动10mm进行枪头装载
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
if self.tip_status == TipStatus.TIP_ATTACHED:
|
||
logger.warning("已有枪头,无需重复装载")
|
||
return True
|
||
|
||
logger.info("开始装载枪头 - Z轴向下移动10mm")
|
||
|
||
# 使用相对移动方法,向下移动10mm
|
||
if self.move_z_relative(distance_mm=10.0, speed=2000, acceleration=500):
|
||
# 更新枪头状态
|
||
self.tip_status = TipStatus.TIP_ATTACHED
|
||
self.tip_count += 1
|
||
self.current_volume = 0.0
|
||
logger.info("枪头装载成功")
|
||
return True
|
||
else:
|
||
logger.error("枪头装载失败 - Z轴移动失败")
|
||
return False
|
||
|
||
def eject_tip(self) -> bool:
|
||
"""
|
||
弹出枪头
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
if self.tip_status == TipStatus.NO_TIP:
|
||
logger.warning("无枪头可弹出")
|
||
return True
|
||
|
||
try:
|
||
if self.pipette.eject_tip():
|
||
self.tip_status = TipStatus.NO_TIP
|
||
self.current_volume = 0.0
|
||
logger.info("枪头已弹出")
|
||
return True
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"弹出枪头失败: {e}")
|
||
return False
|
||
|
||
def aspirate(self, volume: float, liquid_class: Optional[LiquidClass] = None,
|
||
detection: bool = True) -> bool:
|
||
"""
|
||
吸液
|
||
|
||
Args:
|
||
volume: 吸液体积(ul)
|
||
liquid_class: 液体类型
|
||
detection: 是否开启液位检测
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
if self.tip_status != TipStatus.TIP_ATTACHED:
|
||
logger.error("无枪头,无法吸液")
|
||
return False
|
||
|
||
if self.current_volume + volume > self.max_volume:
|
||
logger.error(f"吸液量超过枪头容量: {self.current_volume + volume} > {self.max_volume}")
|
||
return False
|
||
|
||
# 设置液体参数
|
||
if liquid_class:
|
||
self.set_liquid_class(liquid_class)
|
||
|
||
try:
|
||
# 设置吸液速度
|
||
self.pipette.set_max_speed(self.liquid_params.aspirate_speed)
|
||
|
||
# 执行液位检测
|
||
if detection:
|
||
if not self.pipette.liquid_level_detection():
|
||
logger.warning("液位检测失败,继续吸液")
|
||
|
||
# 预润湿
|
||
if self.liquid_params.pre_wet and self.current_volume == 0:
|
||
logger.info("执行预润湿")
|
||
self._pre_wet(volume * 0.2)
|
||
|
||
# 吸液
|
||
if self.pipette.aspirate(volume, detection=False):
|
||
self.current_volume += volume
|
||
self.aspirate_count += 1
|
||
|
||
# 吸液后延时
|
||
time.sleep(self.liquid_params.delay_after_aspirate)
|
||
|
||
# 吸取空气间隙
|
||
if self.liquid_params.air_gap > 0:
|
||
self.pipette.aspirate(self.liquid_params.air_gap, detection=False)
|
||
self.current_volume += self.liquid_params.air_gap
|
||
|
||
logger.info(f"吸液完成: {volume}ul, 当前体积: {self.current_volume}ul")
|
||
return True
|
||
else:
|
||
logger.error("吸液失败")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"吸液异常: {e}")
|
||
return False
|
||
|
||
def dispense(self, volume: float, blow_out: bool = False) -> bool:
|
||
"""
|
||
排液
|
||
|
||
Args:
|
||
volume: 排液体积(ul)
|
||
blow_out: 是否吹出
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
if self.tip_status != TipStatus.TIP_ATTACHED:
|
||
logger.error("无枪头,无法排液")
|
||
return False
|
||
|
||
if volume > self.current_volume:
|
||
logger.error(f"排液量超过当前体积: {volume} > {self.current_volume}")
|
||
return False
|
||
|
||
try:
|
||
# 设置排液速度
|
||
self.pipette.set_max_speed(self.liquid_params.dispense_speed)
|
||
|
||
# 排液
|
||
if self.pipette.dispense(volume):
|
||
self.current_volume -= volume
|
||
self.dispense_count += 1
|
||
|
||
# 排液后延时
|
||
time.sleep(self.liquid_params.delay_after_dispense)
|
||
|
||
# 吹出
|
||
if blow_out and self.liquid_params.blow_out > 0:
|
||
self.pipette.dispense(self.liquid_params.blow_out)
|
||
logger.debug(f"执行吹出: {self.liquid_params.blow_out}ul")
|
||
|
||
# 接触壁
|
||
if self.liquid_params.touch_tip:
|
||
self._touch_tip()
|
||
|
||
logger.info(f"排液完成: {volume}ul, 剩余体积: {self.current_volume}ul")
|
||
return True
|
||
else:
|
||
logger.error("排液失败")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"排液异常: {e}")
|
||
return False
|
||
|
||
def transfer(self, volume: float,
|
||
source_well: Optional[str] = None,
|
||
dest_well: Optional[str] = None,
|
||
liquid_class: Optional[LiquidClass] = None,
|
||
new_tip: bool = True,
|
||
mix_before: Optional[Tuple[int, float]] = None,
|
||
mix_after: Optional[Tuple[int, float]] = None) -> bool:
|
||
"""
|
||
液体转移
|
||
|
||
Args:
|
||
volume: 转移体积
|
||
source_well: 源孔位
|
||
dest_well: 目标孔位
|
||
liquid_class: 液体类型
|
||
new_tip: 是否使用新枪头
|
||
mix_before: 吸液前混合(次数, 体积)
|
||
mix_after: 排液后混合(次数, 体积)
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
try:
|
||
# 装载新枪头
|
||
if new_tip:
|
||
self.eject_tip()
|
||
if not self.pickup_tip():
|
||
return False
|
||
|
||
# 设置液体类型
|
||
if liquid_class:
|
||
self.set_liquid_class(liquid_class)
|
||
|
||
# 吸液前混合
|
||
if mix_before:
|
||
cycles, mix_vol = mix_before
|
||
self.mix(cycles, mix_vol)
|
||
|
||
# 吸液
|
||
if not self.aspirate(volume):
|
||
return False
|
||
|
||
# 排液
|
||
if not self.dispense(volume, blow_out=True):
|
||
return False
|
||
|
||
# 排液后混合
|
||
if mix_after:
|
||
cycles, mix_vol = mix_after
|
||
self.mix(cycles, mix_vol)
|
||
|
||
logger.info(f"液体转移完成: {volume}ul")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"液体转移失败: {e}")
|
||
return False
|
||
|
||
def mix(self, cycles: int = 3, volume: Optional[float] = None) -> bool:
|
||
"""
|
||
混合
|
||
|
||
Args:
|
||
cycles: 混合次数
|
||
volume: 混合体积
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
volume = volume or self.liquid_params.mix_volume
|
||
|
||
logger.info(f"开始混合: {cycles}次, {volume}ul")
|
||
|
||
for i in range(cycles):
|
||
if not self.aspirate(volume, detection=False):
|
||
return False
|
||
if not self.dispense(volume):
|
||
return False
|
||
|
||
logger.info("混合完成")
|
||
return True
|
||
|
||
def _pre_wet(self, volume: float):
|
||
"""预润湿"""
|
||
self.pipette.aspirate(volume, detection=False)
|
||
time.sleep(0.2)
|
||
self.pipette.dispense(volume)
|
||
time.sleep(0.2)
|
||
|
||
def _touch_tip(self):
|
||
"""接触壁(需要与运动控制配合)"""
|
||
# TODO: 实现接触壁动作
|
||
logger.debug("执行接触壁")
|
||
time.sleep(0.5)
|
||
|
||
def _update_tip_status(self):
|
||
"""更新枪头状态"""
|
||
if self.pipette.get_tip_status():
|
||
self.tip_status = TipStatus.TIP_ATTACHED
|
||
else:
|
||
self.tip_status = TipStatus.NO_TIP
|
||
|
||
def set_liquid_class(self, liquid_class: LiquidClass):
|
||
"""设置液体类型"""
|
||
self.liquid_class = liquid_class
|
||
if liquid_class in self.LIQUID_PARAMS:
|
||
self.liquid_params = self.LIQUID_PARAMS[liquid_class]
|
||
logger.info(f"液体类型设置为: {liquid_class.value}")
|
||
|
||
def set_custom_parameters(self, params: LiquidParameters):
|
||
"""设置自定义液体参数"""
|
||
self.liquid_params = params
|
||
self.liquid_class = LiquidClass.CUSTOM
|
||
|
||
def calibrate_volume(self, expected: float, actual: float):
|
||
"""
|
||
体积校准
|
||
|
||
Args:
|
||
expected: 期望体积
|
||
actual: 实际体积
|
||
"""
|
||
factor = actual / expected
|
||
self.pipette.set_calibration_factor(factor)
|
||
logger.info(f"体积校准系数: {factor}")
|
||
|
||
def get_status(self) -> Dict:
|
||
"""获取状态信息"""
|
||
return {
|
||
'tip_status': self.tip_status.value,
|
||
'current_volume': self.current_volume,
|
||
'max_volume': self.max_volume,
|
||
'liquid_class': self.liquid_class.value,
|
||
'statistics': {
|
||
'tip_count': self.tip_count,
|
||
'aspirate_count': self.aspirate_count,
|
||
'dispense_count': self.dispense_count
|
||
}
|
||
}
|
||
|
||
def reset_statistics(self):
|
||
"""重置统计信息"""
|
||
self.tip_count = 0
|
||
self.aspirate_count = 0
|
||
self.dispense_count = 0
|
||
|
||
# ============================================================================
|
||
# 实例化代码块 - 移液控制器使用示例
|
||
# ============================================================================
|
||
|
||
if __name__ == "__main__":
|
||
# 配置日志
|
||
import logging
|
||
|
||
# 设置日志级别
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
|
||
def interactive_test():
|
||
"""交互式测试模式 - 适用于已连接的设备"""
|
||
print("\n" + "=" * 60)
|
||
print("🧪 移液器交互式测试模式")
|
||
print("=" * 60)
|
||
|
||
# 获取用户输入的连接参数
|
||
print("\n📡 设备连接配置:")
|
||
port = input("请输入移液器串口端口 (默认: /dev/ttyUSB0): ").strip() or "/dev/ttyUSB0"
|
||
address_input = input("请输入移液器设备地址 (默认: 4): ").strip()
|
||
address = int(address_input) if address_input else 4
|
||
|
||
# 询问是否连接 XYZ 步进电机控制器
|
||
xyz_enable = input("是否连接 XYZ 步进电机控制器? (y/N): ").strip().lower()
|
||
xyz_port = None
|
||
if xyz_enable in ['y', 'yes']:
|
||
xyz_port = input("请输入 XYZ 控制器串口端口 (默认: /dev/ttyUSB1): ").strip() or "/dev/ttyUSB1"
|
||
|
||
try:
|
||
# 创建移液控制器实例
|
||
if xyz_port:
|
||
print(f"\n🔧 创建移液控制器实例 (移液器端口: {port}, 地址: {address}, XYZ端口: {xyz_port})...")
|
||
pipette = PipetteController(port=port, address=address, xyz_port=xyz_port)
|
||
else:
|
||
print(f"\n🔧 创建移液控制器实例 (端口: {port}, 地址: {address})...")
|
||
pipette = PipetteController(port=port, address=address)
|
||
|
||
# 连接设备
|
||
print("\n📞 连接移液器设备...")
|
||
if not pipette.connect():
|
||
print("❌ 设备连接失败,请检查连接")
|
||
return
|
||
print("✅ 设备连接成功")
|
||
|
||
# 初始化设备
|
||
print("\n🚀 初始化设备...")
|
||
if not pipette.initialize():
|
||
print("❌ 设备初始化失败")
|
||
return
|
||
print("✅ 设备初始化成功")
|
||
|
||
# 交互式菜单
|
||
while True:
|
||
print("\n" + "=" * 50)
|
||
print("🎮 交互式操作菜单:")
|
||
print("1. 📋 查看设备状态")
|
||
print("2. 🔧 装载枪头")
|
||
print("3. 🗑️ 弹出枪头")
|
||
print("4. 💧 吸液操作")
|
||
print("5. 💦 排液操作")
|
||
print("6. 🌀 混合操作")
|
||
print("7. 🔄 液体转移")
|
||
print("8. ⚙️ 设置液体类型")
|
||
print("9. 🎯 自定义参数")
|
||
print("10. 📊 校准体积")
|
||
print("11. 🧹 重置统计")
|
||
print("12. 🔍 液体类型测试")
|
||
print("99. 🚨 紧急停止")
|
||
print("0. 🚪 退出程序")
|
||
print("=" * 50)
|
||
|
||
choice = input("\n请选择操作 (0-12, 99): ").strip()
|
||
|
||
if choice == "0":
|
||
print("\n👋 退出程序...")
|
||
break
|
||
elif choice == "1":
|
||
# 查看设备状态
|
||
status = pipette.get_status()
|
||
print("\n📊 设备状态信息:")
|
||
print(f" 🎯 枪头状态: {status['tip_status']}")
|
||
print(f" 💧 当前体积: {status['current_volume']}ul")
|
||
print(f" 📏 最大体积: {status['max_volume']}ul")
|
||
print(f" 🧪 液体类型: {status['liquid_class']}")
|
||
print(f" 📈 统计信息:")
|
||
print(f" 🔧 枪头使用次数: {status['statistics']['tip_count']}")
|
||
print(f" ⬆️ 吸液次数: {status['statistics']['aspirate_count']}")
|
||
print(f" ⬇️ 排液次数: {status['statistics']['dispense_count']}")
|
||
|
||
elif choice == "2":
|
||
# 装载枪头
|
||
print("\n🔧 装载枪头...")
|
||
if pipette.xyz_connected:
|
||
print("📍 使用 XYZ 控制器进行 Z 轴定位 (下移 10mm)")
|
||
else:
|
||
print("⚠️ 未连接 XYZ 控制器,仅执行移液器枪头装载")
|
||
|
||
if pipette.pickup_tip():
|
||
print("✅ 枪头装载成功")
|
||
if pipette.xyz_connected:
|
||
print("📍 Z 轴已移动到装载位置")
|
||
else:
|
||
print("❌ 枪头装载失败")
|
||
|
||
elif choice == "3":
|
||
# 弹出枪头
|
||
print("\n🗑️ 弹出枪头...")
|
||
if pipette.eject_tip():
|
||
print("✅ 枪头弹出成功")
|
||
else:
|
||
print("❌ 枪头弹出失败")
|
||
|
||
elif choice == "4":
|
||
# 吸液操作
|
||
try:
|
||
volume = float(input("请输入吸液体积 (ul): "))
|
||
detection = input("是否启用液面检测? (y/n, 默认y): ").strip().lower() != 'n'
|
||
print(f"\n💧 执行吸液操作 ({volume}ul)...")
|
||
if pipette.aspirate(volume, detection=detection):
|
||
print(f"✅ 吸液成功: {volume}ul")
|
||
print(f"📊 当前体积: {pipette.current_volume}ul")
|
||
else:
|
||
print("❌ 吸液失败")
|
||
except ValueError:
|
||
print("❌ 请输入有效的数字")
|
||
|
||
elif choice == "5":
|
||
# 排液操作
|
||
try:
|
||
volume = float(input("请输入排液体积 (ul): "))
|
||
blow_out = input("是否执行吹出操作? (y/n, 默认n): ").strip().lower() == 'y'
|
||
print(f"\n💦 执行排液操作 ({volume}ul)...")
|
||
if pipette.dispense(volume, blow_out=blow_out):
|
||
print(f"✅ 排液成功: {volume}ul")
|
||
print(f"📊 剩余体积: {pipette.current_volume}ul")
|
||
else:
|
||
print("❌ 排液失败")
|
||
except ValueError:
|
||
print("❌ 请输入有效的数字")
|
||
|
||
elif choice == "6":
|
||
# 混合操作
|
||
try:
|
||
cycles = int(input("请输入混合次数 (默认3): ") or "3")
|
||
volume_input = input("请输入混合体积 (ul, 默认使用当前体积的50%): ").strip()
|
||
volume = float(volume_input) if volume_input else None
|
||
print(f"\n🌀 执行混合操作 ({cycles}次)...")
|
||
if pipette.mix(cycles=cycles, volume=volume):
|
||
print("✅ 混合完成")
|
||
else:
|
||
print("❌ 混合失败")
|
||
except ValueError:
|
||
print("❌ 请输入有效的数字")
|
||
|
||
elif choice == "7":
|
||
# 液体转移
|
||
try:
|
||
volume = float(input("请输入转移体积 (ul): "))
|
||
source = input("源孔位 (可选, 如A1): ").strip() or None
|
||
dest = input("目标孔位 (可选, 如B1): ").strip() or None
|
||
new_tip = input("是否使用新枪头? (y/n, 默认y): ").strip().lower() != 'n'
|
||
|
||
print(f"\n🔄 执行液体转移 ({volume}ul)...")
|
||
if pipette.transfer(volume=volume, source_well=source, dest_well=dest, new_tip=new_tip):
|
||
print("✅ 液体转移完成")
|
||
else:
|
||
print("❌ 液体转移失败")
|
||
except ValueError:
|
||
print("❌ 请输入有效的数字")
|
||
|
||
elif choice == "8":
|
||
# 设置液体类型
|
||
print("\n🧪 可用液体类型:")
|
||
liquid_options = {
|
||
"1": (LiquidClass.WATER, "水溶液"),
|
||
"2": (LiquidClass.SERUM, "血清"),
|
||
"3": (LiquidClass.VISCOUS, "粘稠液体"),
|
||
"4": (LiquidClass.VOLATILE, "挥发性液体")
|
||
}
|
||
|
||
for key, (liquid_class, description) in liquid_options.items():
|
||
print(f" {key}. {description}")
|
||
|
||
liquid_choice = input("请选择液体类型 (1-4): ").strip()
|
||
if liquid_choice in liquid_options:
|
||
liquid_class, description = liquid_options[liquid_choice]
|
||
pipette.set_liquid_class(liquid_class)
|
||
print(f"✅ 液体类型设置为: {description}")
|
||
|
||
# 显示参数
|
||
params = pipette.liquid_params
|
||
print(f"📋 参数设置:")
|
||
print(f" ⬆️ 吸液速度: {params.aspirate_speed}")
|
||
print(f" ⬇️ 排液速度: {params.dispense_speed}")
|
||
print(f" 💨 空气间隙: {params.air_gap}ul")
|
||
print(f" 💧 预润湿: {'是' if params.pre_wet else '否'}")
|
||
else:
|
||
print("❌ 无效选择")
|
||
|
||
elif choice == "9":
|
||
# 自定义参数
|
||
try:
|
||
print("\n⚙️ 设置自定义参数 (直接回车使用默认值):")
|
||
aspirate_speed = input("吸液速度 (默认500): ").strip()
|
||
dispense_speed = input("排液速度 (默认800): ").strip()
|
||
air_gap = input("空气间隙 (ul, 默认10.0): ").strip()
|
||
pre_wet = input("预润湿 (y/n, 默认n): ").strip().lower() == 'y'
|
||
|
||
custom_params = LiquidParameters(
|
||
aspirate_speed=int(aspirate_speed) if aspirate_speed else 500,
|
||
dispense_speed=int(dispense_speed) if dispense_speed else 800,
|
||
air_gap=float(air_gap) if air_gap else 10.0,
|
||
pre_wet=pre_wet
|
||
)
|
||
|
||
pipette.set_custom_parameters(custom_params)
|
||
print("✅ 自定义参数设置完成")
|
||
except ValueError:
|
||
print("❌ 请输入有效的数字")
|
||
|
||
elif choice == "10":
|
||
# 校准体积
|
||
try:
|
||
expected = float(input("期望体积 (ul): "))
|
||
actual = float(input("实际测量体积 (ul): "))
|
||
pipette.calibrate_volume(expected, actual)
|
||
print(f"✅ 校准完成,校准系数: {actual/expected:.3f}")
|
||
except ValueError:
|
||
print("❌ 请输入有效的数字")
|
||
|
||
elif choice == "11":
|
||
# 重置统计
|
||
pipette.reset_statistics()
|
||
print("✅ 统计信息已重置")
|
||
|
||
elif choice == "12":
|
||
# 液体类型测试
|
||
print("\n🧪 液体类型参数对比:")
|
||
liquid_tests = [
|
||
(LiquidClass.WATER, "水溶液"),
|
||
(LiquidClass.SERUM, "血清"),
|
||
(LiquidClass.VISCOUS, "粘稠液体"),
|
||
(LiquidClass.VOLATILE, "挥发性液体")
|
||
]
|
||
|
||
for liquid_class, description in liquid_tests:
|
||
params = pipette.LIQUID_PARAMS[liquid_class]
|
||
print(f"\n📋 {description} ({liquid_class.value}):")
|
||
print(f" ⬆️ 吸液速度: {params.aspirate_speed}")
|
||
print(f" ⬇️ 排液速度: {params.dispense_speed}")
|
||
print(f" 💨 空气间隙: {params.air_gap}ul")
|
||
print(f" 💧 预润湿: {'是' if params.pre_wet else '否'}")
|
||
print(f" ⏱️ 吸液后延时: {params.delay_after_aspirate}s")
|
||
|
||
elif choice == "99":
|
||
# 紧急停止
|
||
print("\n🚨 执行紧急停止...")
|
||
success = pipette.emergency_stop()
|
||
if success:
|
||
print("✅ 紧急停止执行成功")
|
||
print("⚠️ 所有运动已停止,请检查设备状态")
|
||
else:
|
||
print("❌ 紧急停止执行失败")
|
||
print("⚠️ 请手动检查设备状态并采取必要措施")
|
||
|
||
# 紧急停止后询问是否继续
|
||
continue_choice = input("\n是否继续操作?(y/n): ").strip().lower()
|
||
if continue_choice != 'y':
|
||
print("🚪 退出程序")
|
||
break
|
||
|
||
else:
|
||
print("❌ 无效选择,请重新输入")
|
||
|
||
# 等待用户确认继续
|
||
input("\n按回车键继续...")
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n\n⚠️ 用户中断操作")
|
||
except Exception as e:
|
||
print(f"\n❌ 发生异常: {e}")
|
||
finally:
|
||
# 断开连接
|
||
print("\n📞 断开设备连接...")
|
||
try:
|
||
pipette.disconnect()
|
||
print("✅ 连接已断开")
|
||
except:
|
||
print("⚠️ 断开连接时出现问题")
|
||
|
||
def demo_test():
|
||
"""演示测试模式 - 完整功能演示"""
|
||
print("\n" + "=" * 60)
|
||
print("🎬 移液控制器演示测试")
|
||
print("=" * 60)
|
||
|
||
try:
|
||
# 创建移液控制器实例
|
||
print("1. 🔧 创建移液控制器实例...")
|
||
pipette = PipetteController(port="/dev/ttyUSB0", address=4)
|
||
print("✅ 移液控制器实例创建成功")
|
||
|
||
# 连接设备
|
||
print("\n2. 📞 连接移液器设备...")
|
||
if pipette.connect():
|
||
print("✅ 设备连接成功")
|
||
else:
|
||
print("❌ 设备连接失败")
|
||
return False
|
||
|
||
# 初始化设备
|
||
print("\n3. 🚀 初始化设备...")
|
||
if pipette.initialize():
|
||
print("✅ 设备初始化成功")
|
||
else:
|
||
print("❌ 设备初始化失败")
|
||
return False
|
||
|
||
# 装载枪头
|
||
print("\n4. 🔧 装载枪头...")
|
||
if pipette.pickup_tip():
|
||
print("✅ 枪头装载成功")
|
||
else:
|
||
print("❌ 枪头装载失败")
|
||
|
||
# 设置液体类型
|
||
print("\n5. 🧪 设置液体类型为血清...")
|
||
pipette.set_liquid_class(LiquidClass.SERUM)
|
||
print("✅ 液体类型设置完成")
|
||
|
||
# 吸液操作
|
||
print("\n6. 💧 执行吸液操作...")
|
||
volume_to_aspirate = 100.0
|
||
if pipette.aspirate(volume_to_aspirate, detection=True):
|
||
print(f"✅ 吸液成功: {volume_to_aspirate}ul")
|
||
print(f"📊 当前体积: {pipette.current_volume}ul")
|
||
else:
|
||
print("❌ 吸液失败")
|
||
|
||
# 排液操作
|
||
print("\n7. 💦 执行排液操作...")
|
||
volume_to_dispense = 50.0
|
||
if pipette.dispense(volume_to_dispense, blow_out=True):
|
||
print(f"✅ 排液成功: {volume_to_dispense}ul")
|
||
print(f"📊 剩余体积: {pipette.current_volume}ul")
|
||
else:
|
||
print("❌ 排液失败")
|
||
|
||
# 混合操作
|
||
print("\n8. 🌀 执行混合操作...")
|
||
if pipette.mix(cycles=3, volume=30.0):
|
||
print("✅ 混合完成")
|
||
else:
|
||
print("❌ 混合失败")
|
||
|
||
# 获取状态信息
|
||
print("\n9. 📊 获取设备状态...")
|
||
status = pipette.get_status()
|
||
print("设备状态信息:")
|
||
print(f" 🎯 枪头状态: {status['tip_status']}")
|
||
print(f" 💧 当前体积: {status['current_volume']}ul")
|
||
print(f" 📏 最大体积: {status['max_volume']}ul")
|
||
print(f" 🧪 液体类型: {status['liquid_class']}")
|
||
print(f" 📈 统计信息:")
|
||
print(f" 🔧 枪头使用次数: {status['statistics']['tip_count']}")
|
||
print(f" ⬆️ 吸液次数: {status['statistics']['aspirate_count']}")
|
||
print(f" ⬇️ 排液次数: {status['statistics']['dispense_count']}")
|
||
|
||
# 弹出枪头
|
||
print("\n10. 🗑️ 弹出枪头...")
|
||
if pipette.eject_tip():
|
||
print("✅ 枪头弹出成功")
|
||
else:
|
||
print("❌ 枪头弹出失败")
|
||
|
||
print("\n" + "=" * 60)
|
||
print("✅ 移液控制器演示测试完成")
|
||
print("=" * 60)
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"\n❌ 测试过程中发生异常: {e}")
|
||
return False
|
||
|
||
finally:
|
||
# 断开连接
|
||
print("\n📞 断开连接...")
|
||
pipette.disconnect()
|
||
print("✅ 连接已断开")
|
||
|
||
# 主程序入口
|
||
print("🧪 移液器控制器测试程序")
|
||
print("=" * 40)
|
||
print("1. 🎮 交互式测试 (推荐)")
|
||
print("2. 🎬 演示测试")
|
||
print("0. 🚪 退出")
|
||
print("=" * 40)
|
||
|
||
mode = input("请选择测试模式 (0-2): ").strip()
|
||
|
||
if mode == "1":
|
||
interactive_test()
|
||
elif mode == "2":
|
||
demo_test()
|
||
elif mode == "0":
|
||
print("👋 再见!")
|
||
else:
|
||
print("❌ 无效选择")
|
||
|
||
print("\n🎉 程序结束!")
|
||
print("\n💡 使用说明:")
|
||
print("1. 确保移液器硬件已正确连接")
|
||
print("2. 根据实际情况修改串口端口号")
|
||
print("3. 交互模式支持实时操作和参数调整")
|
||
print("4. 在实际使用中需要配合运动控制器进行位置移动")
|