最低材料成本
约 £82
不含被改造的 Raise3D 打印机本体
运动系统
XYZU 四轴
保留打印机 XYZ,并新增注射器 U 轴
重复移液误差
约 2.2%
重复液体转移实验中的累计相对误差
Deck 结构
6-slot SBS
兼容标准 labware 和 96 孔板工作流
控制链路
Marlin + G-code
SKR Mini E3 V3.0 + 修改后的固件配置
上位机
Python + JSON
GUI、协议抽象和自然语言接口
项目概览
本毕设项目面向实验室重复液体处理场景。许多移液任务本身并不复杂,但一旦进入批量分配、多孔板操作或重复流程,手工操作会占用大量时间,一致性也难以保证。商用自动化平台已经比较成熟,但价格通常较高,对教学、小型研究和原型验证场景来说门槛并不低。
因此项目路线不是从零设计一台全新的实验自动化设备,而是复用一台已经退役的消费级 3D 打印机,保留其现成的运动平台和控制基础,再围绕“自动液体处理”任务补齐机械、固件和软件部分。
最终系统将一台 Raise3D Pro2 Plus 改造成低成本自动化液体处理平台。原有 XYZ 平台被保留下来,同时新增一个用于推动注射器活塞的 U 轴,让整个平台变成 XYZU 四轴系统。上位机部分使用 Python 实现控制与协议执行逻辑,并进一步接入自然语言转协议接口,让高层实验描述可以转换成结构化 JSON 协议,再下发到底层执行。
如果只看结果,这个项目做成的不是一台现成商品,而是一个完整的研究原型:它已经能够在标准 labware 上完成自动液体分配任务,也把机械改造、运动控制、上位机软件和高层协议真正串成了一个闭环。
问题定义
项目关注的核心问题不是“自动移液能不能做”,而是一个更现实的问题:实验室自动化的成本门槛是否一定要这么高?
在很多实验流程里,液体处理是最常见也最重复的动作之一。人工移液具备灵活性,但一旦面对多孔板、重复操作或者批量分配场景,就很容易遇到几个实际问题:
- 重复操作非常耗时
- 小体积液体对一致性更敏感
- 多孔板流程下人工步骤容易累积误差
- 商业自动化平台价格高,难以作为低成本入口方案
项目最终选择从“再利用现有硬件”这个方向切入。和从零做一套运动平台相比,消费级 3D 打印机本身已经具备几项天然优势:笛卡尔坐标运动结构、成熟的步进驱动体系、可编程的 G-code 控制链路,以及较低的获取成本。
这个项目的重点,不只是做一个能吸液的机械装置,而是验证一条完整思路:一台原本用于打印的机器,能不能被重新组织成一个真正可用的自动化液体处理系统。
系统整体架构
整个系统可以理解成四层结构,从下往上分别是运动平台层、机械执行层、控制执行层,以及上位机和高层协议层。
底层是一台被改造后的 Raise3D Pro2 Plus。原有 X、Y、Z 三轴负责空间定位,新加入的 U 轴负责推动注射器活塞,实现体积控制。机械层围绕这个运动平台补齐了注射器式移液机构、枪头适配结构以及兼容标准 labware 的 deck。控制层使用 SKR Mini E3 V3.0 控制板,并基于修改后的 Marlin 固件接收 G-code 指令、驱动 XYZU 四轴执行。上位机层使用 Python 实现 GUI、串口控制、JSON 协议解析和动作抽象,并接入基于云端 API 的自然语言协议生成能力。
这种层次划分使各层职责保持清晰:机械决定“怎么动”,固件负责“如何执行”,上位机负责“如何组织动作”,协议层负责“用户到底想让它做什么”。
关键工程实现
1. 从打印平台到液体处理平台
项目的第一步不是写代码,而是先确认这台打印机值不值得改。虽然这台 Raise3D Pro2 Plus 原本的挤出部分已经不能继续承担正常打印任务,但它的运动机构、导轨、皮带、丝杆和整体刚性依然可用。对这个项目来说,这比“还能不能打印”更重要。
保留这些现成结构的意义很直接:项目不需要从零搭建新的 XYZ 平台,而是可以把主要工作集中在“如何让它完成液体处理任务”上。这让整个项目从一开始就带着低成本约束,而不是先做一个理想系统,再回头解释为什么它很贵。
2. 新增注射器 U 轴,实现真正的移液动作
为了让系统不仅能定位,还能实际处理液体,项目新增了一个基于注射器的 U 轴机构。它本质上是一个把步进电机旋转运动转换成活塞线性位移的丝杆推进结构,用于控制吸液和分液。
这个部分真正的难点不在原理,而在约束:它既要能稳定推动注射器活塞,又要和原有平台机械兼容,还得考虑枪头连接、打印件装配和后续调试的便利性。最终版本采用的是比较直接但可验证的方案:独立步进电机 + 丝杆驱动 + 注射器固定与导向结构。
3. 设计兼容标准 labware 的 deck
有了运动系统和移液机构之后,还需要把实验耗材放到一个稳定、可重复访问的坐标体系里。这一步最终落到了 deck 设计上。
deck 没有采用临时容器摆放方式,而是做成兼容 SBS 标准 labware 的结构。这样 96 孔板、枪头盒、试剂槽和废液位都可以用统一规则定义。后续软件层抽象 slot、well 和协议时,不需要再面向“某一个临时位置”,而是面向一个更接近真实实验流程的坐标体系。
这一层在代码里也被单独抽象成了 Deck 和 LabwareInstance。deck 负责把实验平台的 slot 坐标转换成 machine 坐标,而 labware 则把 A1、B3 这种孔位映射到具体的 XY 位置。这样,控制逻辑就不需要把坐标常数散落在各个函数里,而是统一通过对象层去取位置。
DECK_LAYOUT = {
"origin_machine": {"x": -8, "y": -5},
"slots": {
"1": {"x": 90.9, "y": 70.0},
"2": {"x": 242.1, "y": 70.0},
"3": {"x": 90.9, "y": 170.0},
"4": {"x": 242.1, "y": 170.0},
"5": {"x": 90.9, "y": 270.0},
"6": {"x": 242.1, "y": 270.0},
},
}
x_src, y_src = src_plate.well_position_machine(src_well)
这一层抽象看起来很基础,但它直接决定了后面 GUI、协议和高层自动化是不是能站得住。如果没有统一坐标体系,协议层最后就会退化成“硬编码几个位置点”的脚本,而不是一个可以继续扩展的平台。
4. 修改固件和控制板配置,把平台真正变成 XYZU 系统
机械部分改完之后,下一步是控制层。系统使用 SKR Mini E3 V3.0 控制板,并基于 Marlin 做了适配这套系统的修改。这里最关键的不是“换板子”本身,而是把一套原本面向 3D 打印的控制逻辑,重新组织成适合非打印场景的四轴执行系统。
更细的固件修改过程,包括 E0 复用成 U 轴、TMC2209 配置、配套界面设置和编译刷写流程,单独整理在下面这篇记录中。
为实验移液自动化修改 Marlin 固件
配置上保留了打印机的运动框架,但去掉挤出相关功能,并把第四轴作为 U 轴使用。项目代码里的 Marlin 配置能直接看出这件事:控制板是 BOARD_BTT_SKR_MINI_E3_V3_0,串口走 115200,EXTRUDERS 被设成 0,四轴步进参数则改成了 X/Y/Z/U = { 80, 80, 800, 4000 }。配合 DEFAULT_MAX_FEEDRATE 和 DEFAULT_MAX_ACCELERATION,平台就不再是默认的打印机运动模型,而是一套更适合液体处理的执行系统。
#define MOTHERBOARD BOARD_BTT_SKR_MINI_E3_V3_0
#define BAUDRATE 115200
#define EXTRUDERS 0
#define DEFAULT_AXIS_STEPS_PER_UNIT { 80, 80, 800, 4000 }
#define DEFAULT_MAX_FEEDRATE { 500, 500, 500, 500 }
#define DEFAULT_MAX_ACCELERATION { 500, 500, 100, 100 }
#define X_BED_SIZE 305
#define Y_BED_SIZE 305
#define Z_MAX_POS 605
这部分改完之后,底层控制接口仍然保留 G-code。原因很现实:G-code 本身已经是一个足够成熟的底层运动语言,它非常适合作为机械执行层和上位机之间的边界。这样,上层不用关心具体电机如何脉冲输出,只需要决定什么时候发 G28、什么时候发 G1、什么时候停顿和等待即可。
5. 用 Python 建立上位机控制和协议层
固件层解决的是“怎么执行动作”,上位机解决的是“怎么把实验流程组织成动作”。上位机使用 Python 实现串口通信、动作封装、GUI 交互以及 JSON 协议解析逻辑。
GUI 这一层做的不是简单按钮联动,而是把常用动作拆成了几类比较明确的协议步骤:home_xyz、pick_tip、drop_tip、transfer、aspirate、dispense 和 dwell。这意味着平台不只是“点一下按钮动一下”,而是能把一个实验流程保存成结构化步骤,再统一执行。
values=[
"home_xyz",
"pick_tip",
"drop_tip",
"transfer",
"aspirate",
"dispense",
"dwell",
]
从协议角度看,项目选择 JSON,而不是直接在 GUI 里把每一步都写死。原因很直接:JSON 在这个场景里足够简单,既适合存档,也适合后续接自然语言生成。
[
{"type": "home_xyz", "params": {}},
{"type": "pick_tip", "params": {"slot": "1", "well": "A1"}},
{
"type": "transfer",
"params": {
"src_slot": "4",
"src_well": "A1",
"dst_slot": "4",
"dst_well": "B3",
"volume_ul": 50
}
},
{"type": "drop_tip", "params": {"slot": "2", "edge": "left"}}
]
这条 transfer 协议里,src_slot 和 dst_slot 表示 deck 上的槽位编号,src_well 和 dst_well 表示对应 labware 内部的孔位编号,volume_ul 表示本次转移体积。也就是说,协议层并不直接写 X/Y/Z/U 坐标,而是先用实验人员更容易理解的槽位、孔位和体积来描述任务。
执行层会遍历协议步骤,根据不同的 type 调用对应的高层函数。这个结构把“实验意图”和“底层运动细节”分开了。
for idx, step in enumerate(self.protocol_steps, start=1):
self.log(f"Step {idx}: {step}")
self._run_single_step(step)
这段执行逻辑可以概括为:
load protocol
for each step in protocol:
if step.type == home_xyz:
home XYZ
elif step.type == pick_tip:
move to tip rack and mount tip
elif step.type == transfer:
aspirate from source -> move -> dispense to destination
elif step.type == drop_tip:
move to waste and scrape tip off
elif step.type == dwell:
wait for a fixed duration
6. 从协议到实际液体动作
真正把软件层和机械层接起来的,是 robot.py 这一层高层动作封装。比如 transfer_volume() 并不是单条命令,而是一整段连续动作:先把 U 轴回到基准,再移动到源孔位、下探、吸液,之后抬起、移动到目标孔位、下探、排液,最后回到安全高度。
这一点很重要,因为它意味着上层协议不需要知道每个 G-code 细节,只需要表达“从哪里取多少液体,送到哪里去”。底层动作链则由控制代码统一展开。
以 transfer 为例,协议解析后会先完成位置解析:slot 决定 labware 放在 deck 的哪个基准位置,well 决定目标孔位相对这个 labware 原点的偏移。两者叠加后,得到机器坐标系下的 x/y 位置。
params = step["params"]
src_plate = deck.get_labware(params["src_slot"])
dst_plate = deck.get_labware(params["dst_slot"])
x_src, y_src = src_plate.well_position_machine(params["src_well"])
x_dst, y_dst = dst_plate.well_position_machine(params["dst_well"])
robot.transfer_volume(
x_src=x_src,
y_src=y_src,
x_dst=x_dst,
y_dst=y_dst,
volume_ul=params["volume_ul"],
)
到这一步,src_slot: 4, src_well: A1 已经不再只是字符串,而是被解析成了源孔位的机器坐标;dst_slot: 4, dst_well: B3 同样会被解析成目标孔位的机器坐标。volume_ul 则会换算成 U 轴注射器需要移动的距离。
# aspirate
self.move_to(u=u_base, feedrate=feed_u)
self.move_to(z=z_safe_src, feedrate=feed_z_up)
self.move_to(x=x_src, y=y_src, feedrate=feed_xy)
self.move_to(z=z_asp, feedrate=feed_z_down)
self.move_to(u=u_asp, feedrate=feed_u)
self.move_to(z=z_safe_src, feedrate=feed_z_up)
# dispense
self.move_to(z=z_safe_dst, feedrate=feed_z_up)
self.move_to(x=x_dst, y=y_dst, feedrate=feed_xy)
self.move_to(z=z_disp, feedrate=feed_z_down)
self.move_to(u=u_base, feedrate=feed_u)
self.move_to(z=z_safe_dst, feedrate=feed_z_up)
再往下一层,move_to() 会把函数参数格式化成 Marlin 能识别的 G-code。上层动作代码只需要传入目标坐标和速度,底层串口层负责生成并发送具体指令。
def move_to(self, x=None, y=None, z=None, u=None, feedrate=None):
parts = ["G1"]
if x is not None:
parts.append(f"X{x:.2f}")
if y is not None:
parts.append(f"Y{y:.2f}")
if z is not None:
parts.append(f"Z{z:.2f}")
if u is not None:
parts.append(f"U{u:.3f}")
if feedrate is not None:
parts.append(f"F{feedrate}")
self.send_gcode(" ".join(parts))
同一条 transfer protocol 最终会展开成一组连续的 G-code 指令。下面用一组示例指令表示从 slot 4 / A1 吸取 50 µL,再移动到 slot 4 / B3 分液的过程。其中 U 轴数值表示注射器轴位置,实际值由体积标定关系换算得到。
; move to source well
G1 X168.12 Y94.76 F12000
G1 Z52.0 F3000
G1 Z18.0 F1200
; aspirate liquid
G1 U8.000 F600
; retract
G1 Z52.0 F3000
; move to destination well
G1 X181.59 Y107.23 F12000
G1 Z18.0 F1200
; dispense liquid
G1 U5.000 F600
; finish
G1 Z52.0 F3000
这段逻辑看起来不像复杂算法,但它是整个平台最核心的“动作语义层”。机械、固件、deck 坐标和协议抽象,最后都要落到这里才能变成真正可执行的任务。
7. 加入自然语言到协议的转换接口
自然语言到协议的转换是项目中进一步扩展的一层。在基础自动化流程跑通之后,系统尝试把自然语言实验描述接入控制链路。用户不再只是在 GUI 里手动定义每一步动作,而是可以先给出一个更接近实验意图的描述,再由云端 API 和大语言模型生成结构化协议。
在 GUI 代码里,这一层通过一个单独的 GPT Helper 标签页接入。前端把文本描述发给云端接口,接口返回协议结果,再交由上位机展示和执行。模型不直接控制电机,而是保留一层中间协议:用户输入自然语言,模型生成 JSON 协议,上位机解释协议,固件执行底层动作。这样虽然多了一层转换,但系统更可验证,也更容易调试。
resp = requests.post(
GPT_API_URL,
json={"description": text},
timeout=300,
)
答辩展示中保留了一个典型的两轮对话例子。第一轮只给出任务目标:将 A 和 B 两种溶液按 1:1 / 1:2 / 1:4 / 1:8 的比例混合,每孔总体积为 180 µL。这时信息仍然不足,因为 A 液和 B 液分别放在哪个槽位、目标孔板放在哪个槽位、要用哪些孔位,以及是否每次都换 tip,都还没有说明。系统没有直接编造一套动作,而是先指出缺失信息,同时给出一个带 MISSING 占位符的协议草稿。
第二轮补齐缺失条件:A 在 slot 4、B 在 slot 6、目标孔板在 slot 2,并且只用 A1-A4 这四个孔。到这一步,系统可以把高层描述收敛成一组明确的 JSON 协议步骤,包括 pick_tip、从不同原液位吸液、在对应孔位分配以及最终 drop_tip。这个例子说明,对话接口不是“生成一段看起来像答案的文字”,而是能够收敛到可执行协议。
实验结果
最终这个项目并不只是“能动起来”,而是完成了一个比较完整的研究原型验证。关键结果包括:
- 最低功能配置材料成本约为 £82,不包含被改造打印机本体
- 系统已经可以完成基于标准 labware 和 96 孔板的自动液体处理流程
- 重复液体转移实验中的累计相对误差约为 2.2%
- 已经实现从自然语言描述到结构化协议再到底层执行的完整链路
其中,U 轴定位误差测试和重复移液实验最能说明系统当前的边界。U 轴本身仍然受到丝杆间隙、机构刚性和 backlash 的影响,但测试结果说明它的误差是有界、可重复的。对于一个原型系统来说,这一点比“单次绝对精度漂亮”更重要,因为它意味着后续还有继续补偿和优化的空间。
项目价值
如果只看单个模块,这个项目里的很多部分都不是最复杂的设计:机械机构本身不算特别复杂,固件也不是从零写起,上位机逻辑也不是重量级软件工程。真正的难点一直是系统整合。
项目价值在于,它不是停留在概念图或者单点功能验证,而是把机械改造、控制执行、上位机抽象和高层协议真正连成了一个完整系统。它回答的问题也不是“能不能做出一个移液动作”,而是“消费级硬件能不能被重新组织成一个真正可用的实验自动化入口平台”。
从工程实践角度看,这个项目的重点不是单点功能,而是系统如何从原理、结构、控制到验证逐步落地。
演示视频
最后这段视频对应的是一个完整的色素混合任务演示:先输入协议,再由平台按步骤执行移液和分配动作。对这个项目来说,它的意义不是展示“某一个动作能动”,而是说明从协议到底层执行这条链路确实已经跑通了。
色素混合任务演示视频,来源于 Zenodo 公开记录。可以直接在页面内播放,也可以打开原始视频文件查看完整版本。
局限与下一步
这个项目当前更适合定义为“已经完成核心功能验证的研究原型”,而不是成熟仪器。它当前最明确的局限包括:
- U 轴精度仍受机械刚性和丝杆间隙限制
- 当前结果主要是功能级验证,不是严格的实验级定量验证
- 系统还没有做到多通道、高通量液体处理
- 视觉校准、液位检测和闭环体积控制都还没有加入
后续可以继续推进的方向包括:
- 优化 U 轴机构刚性和传动精度
- 加入更稳定的闭环或半闭环体积控制
- 扩展成多通道移液头
- 加入视觉检测和自动校准
- 继续完善协议层和 GUI 的实验表达能力
从结果上看,这个项目已经证明了方向是可行的。下一步要做的,不是重新定义问题,而是把这个原型继续推向一个更可靠的平台。
总结
这个毕设项目的重点不只是做出一台能够执行液体处理动作的机器,而是完成了一次从机械改造、固件控制、上位机软件到自动化流程整合的系统工程实践。
项目结果表明:实验室自动化的高成本并不完全来自技术本身,很多时候也来自既有系统形态和实现路径。换一个工程视角,利用消费级硬件构建更低成本、更可访问的自动化平台,是一条可行路线。