mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 04:51:10 +00:00
* 修改lh的json启动 * 修改lh的json启动 * 修改backend,做成sim的通用backend * 修改yaml的地址,3D模型适配网页生产环境 * 添加laiyu硬件连接 * 修改移液枪的状态判断方法, 修改移液枪的状态判断方法, 添加三轴的表定点与零点之间的转换 添加三轴真实移动的backend * 修改laiyu移液站 简化移动方法, 取消软件限制位置, 修改当值使用Z轴时也需要重新复位Z轴的问题 * 更新lh以及laiyu workshop 1,现在可以直接通过修改backend,适配其他的移液站,主类依旧使用LiquidHandler,不用重新编写 2,修改枪头判断标准,使用枪头自身判断而不是类的判断, 3,将归零参数用毫米计算,方便手动调整, 4,修改归零方式,上电使用机械归零,确定机械零点,手动归零设置工作区域零点方便计算,二者互不干涉 * 修改枪头动作 * 修改虚拟仿真方法 --------- Co-authored-by: zhangshixiang <@zhangshixiang> Co-authored-by: Junhan Chang <changjh@dp.tech>
1098 lines
41 KiB
Python
1098 lines
41 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
移液控制器模块
|
||
封装SOPA移液器的高级控制功能
|
||
"""
|
||
|
||
# 添加项目根目录到Python路径以解决模块导入问题
|
||
import sys
|
||
import os
|
||
from tkinter import N
|
||
|
||
from unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver import ModbusException
|
||
|
||
# 无论如何都添加项目根目录到路径
|
||
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.liquid_handling.laiyu.drivers.sopa_pipette_driver import (
|
||
SOPAPipette,
|
||
SOPAConfig,
|
||
SOPAStatusCode,
|
||
DetectionMode,
|
||
create_sopa_pipette,
|
||
)
|
||
# from unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver import (
|
||
# XYZStepperController,
|
||
# MotorAxis,
|
||
# MotorStatus,
|
||
# ModbusException
|
||
# )
|
||
|
||
from unilabos.devices.liquid_handling.laiyu.controllers.xyz_controller import (
|
||
XYZController,
|
||
MotorAxis,
|
||
MotorStatus
|
||
)
|
||
|
||
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[XYZController] = None
|
||
self.xyz_port = xyz_port if xyz_port else port
|
||
self.xyz_connected = True
|
||
|
||
# 统计信息
|
||
# 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 = XYZController(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()
|
||
self.xyz_controller.home_all_axes()
|
||
self.xyz_controller.move_to_work_coord_safe(x=0, y=-150, z=0)
|
||
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:
|
||
是否成功
|
||
"""
|
||
self._update_tip_status()
|
||
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._update_tip_status()
|
||
# self.tip_status = TipStatus.TIP_ATTACHED
|
||
# self.tip_count += 1
|
||
self.current_volume = 0.0
|
||
if self.tip_status == TipStatus.TIP_ATTACHED:
|
||
logger.info("枪头装载成功")
|
||
return True
|
||
else :
|
||
logger.info("枪头装载失败")
|
||
return False
|
||
else:
|
||
logger.error("枪头装载失败 - Z轴移动失败")
|
||
return False
|
||
|
||
def eject_tip(self) -> bool:
|
||
"""
|
||
弹出枪头
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
|
||
self._update_tip_status()
|
||
|
||
if self.tip_status == TipStatus.NO_TIP:
|
||
logger.warning("无枪头可弹出")
|
||
return True
|
||
|
||
try:
|
||
if self.pipette.eject_tip():
|
||
self._update_tip_status()
|
||
if 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:
|
||
是否成功
|
||
"""
|
||
self._update_tip_status()
|
||
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:
|
||
是否成功
|
||
"""
|
||
self._update_tip_status()
|
||
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:
|
||
"""获取状态信息"""
|
||
self._update_tip_status()
|
||
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/ttyUSB_CH340): ").strip() or "/dev/ttyUSB_CH340"
|
||
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 not in ['n', 'no']:
|
||
xyz_port = input("请输入 XYZ 控制器串口端口 (默认: /dev/ttyUSB_CH340): ").strip() or "/dev/ttyUSB_CH340"
|
||
|
||
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. 在实际使用中需要配合运动控制器进行位置移动")
|