mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-06 23:15:10 +00:00
Compare commits
115 Commits
workstatio
...
23eb1139a9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23eb1139a9 | ||
|
|
7b93332bf5 | ||
|
|
d82ccd5cf1 | ||
|
|
acc9e5ce0d | ||
|
|
ab2ab7fcc7 | ||
|
|
5767266563 | ||
|
|
4c6e437eb1 | ||
|
|
bef44b2293 | ||
|
|
b78c6c6ba9 | ||
|
|
0d512c9e38 | ||
|
|
c8c755057c | ||
|
|
a6ec20e279 | ||
|
|
b69aceaff3 | ||
|
|
21afdb62bc | ||
|
|
d7d43af40a | ||
|
|
132955617d | ||
|
|
e7521972e4 | ||
|
|
f2753fc69a | ||
|
|
09ad905280 | ||
|
|
7714c71cd2 | ||
|
|
64832718be | ||
|
|
68871358c2 | ||
|
|
498b3cad6a | ||
|
|
157da1759d | ||
|
|
be0a73eb19 | ||
|
|
9be6e1069a | ||
|
|
817e88cfc4 | ||
|
|
15f3f8518b | ||
|
|
bbc49e9aab | ||
|
|
f9a9e91d56 | ||
|
|
96e9c76709 | ||
|
|
06b7962ef9 | ||
|
|
6faa19a250 | ||
|
|
46cec82a51 | ||
|
|
f7db8d17c5 | ||
|
|
a354965f8e | ||
|
|
934276d2f7 | ||
|
|
803809480b | ||
|
|
5478ba3237 | ||
|
|
49f1aa9c28 | ||
|
|
d5d516f0ef | ||
|
|
4471fed4b8 | ||
|
|
30d143e1a5 | ||
|
|
75ea45f21e | ||
|
|
66af337d6c | ||
|
|
ae3c65c1d3 | ||
|
|
11e4f053f1 | ||
|
|
96f37b3b0d | ||
|
|
d7d0a27976 | ||
|
|
34151f5cb2 | ||
|
|
369a21b904 | ||
|
|
90169981c1 | ||
|
|
d297abfd19 | ||
|
|
9c515a252a | ||
|
|
ea5e7a5ce2 | ||
|
|
2e9a0a4677 | ||
|
|
4c7aa8a89a | ||
|
|
d8a0c5e715 | ||
|
|
133ffaac17 | ||
|
|
729a0fcf0c | ||
|
|
6ae77e0408 | ||
|
|
bab4b1d67a | ||
|
|
12c17ec26e | ||
|
|
6577fe12eb | ||
|
|
f1fee5fad9 | ||
|
|
9b3377aedb | ||
|
|
526327727d | ||
|
|
aaa86314e3 | ||
|
|
6a14104e6b | ||
|
|
ab0c4b708b | ||
|
|
c0b7f2decd | ||
|
|
b6c9530c61 | ||
|
|
8698821c52 | ||
|
|
3f53f88390 | ||
|
|
e840516ba4 | ||
|
|
146d8c5296 | ||
|
|
6573c9e02e | ||
|
|
c7b9c6a825 | ||
|
|
48c43d3303 | ||
|
|
55be5e8188 | ||
|
|
1b9f3c666d | ||
|
|
097114d38c | ||
|
|
5bec899479 | ||
|
|
5e86112ebf | ||
|
|
24ecb13b79 | ||
|
|
2573d34713 | ||
|
|
106d71e1db | ||
|
|
3c2a4a64ac | ||
|
|
1e00a66a65 | ||
|
|
46da42deef | ||
|
|
101c1bc3cc | ||
|
|
a62112ae26 | ||
|
|
dd5a7cab75 | ||
|
|
39de3ac58e | ||
|
|
b99969278c | ||
|
|
b957ad2f71 | ||
|
|
e1a7c3a103 | ||
|
|
e63c15997c | ||
|
|
c5a495f409 | ||
|
|
5b240cb0ea | ||
|
|
147b8f47c0 | ||
|
|
6d2489af5f | ||
|
|
807dcdd226 | ||
|
|
8a29bc5597 | ||
|
|
6f6c70ee57 | ||
|
|
478a85951c | ||
|
|
0f2555c90c | ||
|
|
d2dda6ee03 | ||
|
|
208540b307 | ||
|
|
cb7c56a1d9 | ||
|
|
ea2e9c3e3a | ||
|
|
0452a68180 | ||
|
|
90a0f3db9b | ||
|
|
055d120ba8 | ||
|
|
a948f09f60 |
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
|
||||
|
||||
# Currently, you need to install the `unilabos_msgs` package
|
||||
# You can download the system-specific package from the Release page
|
||||
conda install ros-humble-unilabos-msgs-0.9.7-xxxxx.tar.bz2
|
||||
conda install ros-humble-unilabos-msgs-0.9.10-xxxxx.tar.bz2
|
||||
|
||||
# Install PyLabRobot and other prerequisites
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
|
||||
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
||||
|
||||
# 现阶段,需要安装 `unilabos_msgs` 包
|
||||
# 可以前往 Release 页面下载系统对应的包进行安装
|
||||
conda install ros-humble-unilabos-msgs-0.9.7-xxxxx.tar.bz2
|
||||
conda install ros-humble-unilabos-msgs-0.9.11-xxxxx.tar.bz2
|
||||
|
||||
# 安装PyLabRobot等前置
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
channel_sources:
|
||||
- robostack,robostack-staging,conda-forge,defaults
|
||||
|
||||
gazebo:
|
||||
- '11'
|
||||
libpqxx:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.9.7
|
||||
version: 0.9.11
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
folder: ros-humble-unilabos-msgs/src/work
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.9.7"
|
||||
version: "0.9.11"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.9.7',
|
||||
version='0.9.11',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
|
||||
45
test/experiments/camera.json
Normal file
45
test/experiments/camera.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "Camera",
|
||||
"name": "摄像头",
|
||||
"children": [
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "camera",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"camera_index": 0,
|
||||
"period": 0.05
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Gripper1",
|
||||
"name": "假夹爪",
|
||||
"children": [
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "gripper.mock",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
|
||||
]
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
HeatChillProtocol: generate_heat_chill_protocol, (√)
|
||||
HeatChillStartProtocol: generate_heat_chill_start_protocol, (√)
|
||||
HeatChillStopProtocol: generate_heat_chill_stop_protocol, (√)
|
||||
HeatChillToTempProtocol:
|
||||
StirProtocol: generate_stir_protocol, (√)
|
||||
StartStirProtocol: generate_start_stir_protocol, (√)
|
||||
StopStirProtocol: generate_stop_stir_protocol, (√)
|
||||
@@ -30,7 +31,228 @@
|
||||
CleanVesselProtocol: generate_clean_vessel_protocol, (√)
|
||||
DissolveProtocol: generate_dissolve_protocol, (√)
|
||||
FilterThroughProtocol: generate_filter_through_protocol, (√)
|
||||
RunColumnProtocol: generate_run_column_protocol, (×)
|
||||
WashSolidProtocol: generate_wash_solid_protocol, (×)
|
||||
RunColumnProtocol: generate_run_column_protocol, (√)<RunColumn Rf="?" column="column" from_vessel="rotavap" ratio="5:95" solvent1="methanol" solvent2="chloroform" to_vessel="rotavap"/>
|
||||
|
||||
上下文体积搜索
|
||||
上下文体积搜索
|
||||
3. 还没创建的protocol
|
||||
ResetHandling 写完了 <ResetHandling solvent="methanol"/>
|
||||
Dry 写完了 <Dry compound="product" vessel="filter"/>
|
||||
AdjustPH 写完了 <AdjustPH pH="8.0" reagent="hydrochloric acid" vessel="main_reactor"/>
|
||||
Recrystallize 写完了 <Recrystallize ratio="?" solvent1="dichloromethane" solvent2="methanol" vessel="filter" volume="?"/>
|
||||
TakeSample <TakeSample id="a" vessel="rotavap"/>
|
||||
Hydrogenate <Hydrogenate temp="45 °C" time="?" vessel="main_reactor"/>
|
||||
4. 参数对齐
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class PumpTransferProtocol(BaseModel):
|
||||
from_vessel: str
|
||||
to_vessel: str
|
||||
volume: float
|
||||
amount: str = ""
|
||||
time: float = 0
|
||||
viscous: bool = False
|
||||
rinsing_solvent: str = "air" <Transfer from_vessel="main_reactor" to_vessel="rotavap"/>
|
||||
rinsing_volume: float = 5000 <Transfer event="A" from_vessel="reactor" rate_spec="dropwise" to_vessel="main_reactor"/>
|
||||
rinsing_repeats: int = 2 <Transfer from_vessel="separator" through="cartridge" to_vessel="rotavap"/>
|
||||
solid: bool = False 测完了三个都能跑✅
|
||||
flowrate: float = 500
|
||||
transfer_flowrate: float = 2500
|
||||
|
||||
class SeparateProtocol(BaseModel):
|
||||
purpose: str
|
||||
product_phase: str
|
||||
from_vessel: str
|
||||
separation_vessel: str
|
||||
to_vessel: str
|
||||
waste_phase_to_vessel: str
|
||||
solvent: str
|
||||
solvent_volume: float <Separate product_phase="bottom" purpose="wash" solvent="water" vessel="separator" volume="?"/>
|
||||
through: str <Separate product_phase="top" purpose="separate" vessel="separator"/>
|
||||
repeats: int <Separate product_phase="bottom" purpose="extract" repeats="3" solvent="CH2Cl2" vessel="separator" volume="?"/>
|
||||
stir_time: float <Separate product_phase="top" product_vessel="flask" purpose="separate" vessel="separator" waste_vessel="separator"/>
|
||||
stir_speed: float
|
||||
settling_time: float 测完了能跑✅
|
||||
|
||||
|
||||
class EvaporateProtocol(BaseModel):
|
||||
vessel: str
|
||||
pressure: float
|
||||
temp: float <Evaporate solvent="ethanol" vessel="rotavap"/>
|
||||
time: float 测完了能跑✅
|
||||
stir_speed: float
|
||||
|
||||
|
||||
class EvacuateAndRefillProtocol(BaseModel):
|
||||
vessel: str
|
||||
gas: str <EvacuateAndRefill gas="nitrogen" vessel="main_reactor"/>
|
||||
repeats: int 测完了能跑✅
|
||||
|
||||
class AddProtocol(BaseModel):
|
||||
vessel: str
|
||||
reagent: str
|
||||
volume: float
|
||||
mass: float
|
||||
amount: str
|
||||
time: float
|
||||
stir: bool
|
||||
stir_speed: float <Add reagent="ethanol" vessel="main_reactor" volume="2.7 mL"/>
|
||||
<Add event="A" mass="19.3 g" mol="0.28 mol" rate_spec="portionwise" reagent="sodium nitrite" time="1 h" vessel="main_reactor"/>
|
||||
<Add mass="4.5 g" mol="16.2 mmol" reagent="(S)-2-phthalimido-6-hydroxyhexanoic acid" vessel="main_reactor"/>
|
||||
<Add purpose="dilute" reagent="hydrochloric acid" vessel="main_reactor" volume="?"/>
|
||||
<Add equiv="1.1" event="B" mol="25.2 mmol" rate_spec="dropwise" reagent="1-fluoro-2-nitrobenzene" time="20 min"
|
||||
vessel="main_reactor" volume="2.67 mL"/>
|
||||
<Add ratio="?" reagent="tetrahydrofuran|tert-butanol" vessel="main_reactor" volume="?"/>
|
||||
viscous: bool
|
||||
purpose: str 测完了能跑✅
|
||||
|
||||
class CentrifugeProtocol(BaseModel):
|
||||
vessel: str
|
||||
speed: float
|
||||
time: float 没毛病
|
||||
temp: float
|
||||
|
||||
class FilterProtocol(BaseModel):
|
||||
vessel: str
|
||||
filtrate_vessel: str
|
||||
stir: bool <Filter vessel="filter"/>
|
||||
stir_speed: float <Filter filtrate_vessel="rotavap" vessel="filter"/>
|
||||
temp: float 测完了能跑✅
|
||||
continue_heatchill: bool
|
||||
volume: float
|
||||
|
||||
class HeatChillProtocol(BaseModel):
|
||||
vessel: str
|
||||
temp: float
|
||||
time: float <HeatChill pressure="1 mbar" temp_spec="room temperature" time="?" vessel="main_reactor"/>
|
||||
<HeatChill temp_spec="room temperature" time_spec="overnight" vessel="main_reactor"/>
|
||||
<HeatChill temp="256 °C" time="?" vessel="main_reactor"/>
|
||||
<HeatChill reflux_solvent="methanol" temp_spec="reflux" time="2 h" vessel="main_reactor"/>
|
||||
<HeatChillToTemp temp_spec="room temperature" vessel="main_reactor"/>
|
||||
stir: bool 测完了能跑✅
|
||||
stir_speed: float
|
||||
purpose: str
|
||||
|
||||
class HeatChillStartProtocol(BaseModel):
|
||||
vessel: str
|
||||
temp: float 疑似没有
|
||||
purpose: str
|
||||
|
||||
class HeatChillStopProtocol(BaseModel):
|
||||
vessel: str 疑似没有
|
||||
|
||||
class StirProtocol(BaseModel):
|
||||
stir_time: float
|
||||
stir_speed: float <Stir time="0.5 h" vessel="main_reactor"/>
|
||||
<Stir event="A" time="30 min" vessel="main_reactor"/>
|
||||
<Stir time_spec="several minutes" vessel="filter"/>
|
||||
settling_time: float 测完了能跑✅
|
||||
|
||||
class StartStirProtocol(BaseModel):
|
||||
vessel: str
|
||||
stir_speed: float 疑似没有
|
||||
purpose: str
|
||||
|
||||
class StopStirProtocol(BaseModel):
|
||||
vessel: str 疑似没有
|
||||
|
||||
class TransferProtocol(BaseModel):
|
||||
from_vessel: str
|
||||
to_vessel: str
|
||||
volume: float
|
||||
amount: str = ""
|
||||
time: float = 0
|
||||
viscous: bool = False
|
||||
rinsing_solvent: str = ""
|
||||
rinsing_volume: float = 0.0
|
||||
rinsing_repeats: int = 0
|
||||
solid: bool = False 这个protocol早该删掉了
|
||||
|
||||
class CleanVesselProtocol(BaseModel):
|
||||
vessel: str
|
||||
solvent: str
|
||||
volume: float
|
||||
temp: float
|
||||
repeats: int = 1 <CleanVessel vessel="centrifuge"/>
|
||||
|
||||
class DissolveProtocol(BaseModel):
|
||||
vessel: str
|
||||
solvent: str
|
||||
volume: float <Dissolve mass="2.9 g" mol="0.12 mol" reagent="magnesium" vessel="main_reactor"/>
|
||||
amount: str = "" <Dissolve mass="12.9 g" reagent="4-tert-butylbenzyl bromide" vessel="main_reactor"/>
|
||||
temp: float = 25.0 <Dissolve solvent="diisopropyl ether" vessel="rotavap" volume="?"/>
|
||||
time: float = 0.0 <Dissolve event="A" mass="?" reagent="pyridinone" vessel="main_reactor"/>
|
||||
stir_speed: float = 0.0 测完了能跑✅
|
||||
|
||||
class FilterThroughProtocol(BaseModel):
|
||||
from_vessel: str
|
||||
to_vessel: str
|
||||
filter_through: str
|
||||
eluting_solvent: str = ""
|
||||
eluting_volume: float = 0.0 疑似没有
|
||||
eluting_repeats: int = 0
|
||||
residence_time: float = 0.0
|
||||
|
||||
class RunColumnProtocol(BaseModel):
|
||||
from_vessel: str
|
||||
to_vessel: str <RunColumn Rf="?" column="column" from_vessel="rotavap" pct1="40 %" pct2="50 %" solvent1="ethyl acetate" solvent2="hexane" to_vessel="rotavap"/>
|
||||
column: str 测完了能跑✅
|
||||
|
||||
class WashSolidProtocol(BaseModel):
|
||||
vessel: str
|
||||
solvent: str
|
||||
volume: float
|
||||
filtrate_vessel: str = "" <WashSolid repeats="4" solvent="water" vessel="main_reactor" volume="400 mL"/>
|
||||
temp: float = 25.0 <WashSolid filtrate_vessel="rotavap" solvent="formic acid" vessel="main_reactor" volume="?"/>
|
||||
stir: bool = False <WashSolid solvent="acetone" vessel="rotavap" volume="5 mL"/>
|
||||
<WashSolid solvent="ethyl alcohol" vessel="main_reactor" volume_spec="small volume"/>
|
||||
<WashSolid filtrate_vessel="rotavap" mass="10 g" solvent="toluene" vessel="separator"/>
|
||||
<WashSolid repeats_spec="several" solvent="water" vessel="main_reactor" volume="?"/>
|
||||
stir_speed: float = 0.0 测完了能跑✅
|
||||
time: float = 0.0
|
||||
repeats: int = 1
|
||||
|
||||
class AdjustPHProtocol(BaseModel):
|
||||
vessel: str = Field(..., description="目标容器")
|
||||
ph_value: float = Field(..., description="目标pH值") # 改为 ph_value
|
||||
reagent: str = Field(..., description="酸碱试剂名称")
|
||||
# 移除其他可选参数,使用默认值 <新写的,没问题>
|
||||
|
||||
class ResetHandlingProtocol(BaseModel):
|
||||
solvent: str = Field(..., description="溶剂名称") <新写的,没问题>
|
||||
|
||||
class DryProtocol(BaseModel):
|
||||
compound: str = Field(..., description="化合物名称") <新写的,没问题>
|
||||
vessel: str = Field(..., description="目标容器")
|
||||
|
||||
class RecrystallizeProtocol(BaseModel):
|
||||
ratio: str = Field(..., description="溶剂比例(如 '1:1', '3:7')")
|
||||
solvent1: str = Field(..., description="第一种溶剂名称") <新写的,没问题>
|
||||
solvent2: str = Field(..., description="第二种溶剂名称")
|
||||
vessel: str = Field(..., description="目标容器")
|
||||
volume: float = Field(..., description="总体积 (mL)")
|
||||
|
||||
class HydrogenateProtocol(BaseModel):
|
||||
temp: str = Field(..., description="反应温度(如 '45 °C')")
|
||||
time: str = Field(..., description="反应时间(如 '2 h')") <新写的,没问题>
|
||||
vessel: str = Field(..., description="反应容器")
|
||||
|
||||
还差
|
||||
<dissolve>
|
||||
<separate>
|
||||
<CleanVessel vessel="centrifuge"/>
|
||||
|
||||
|
||||
单位修复:
|
||||
evaporate
|
||||
heatchill
|
||||
recrysitallize
|
||||
stir
|
||||
wash solid
|
||||
@@ -23,14 +23,20 @@
|
||||
"waste_bottle_2",
|
||||
"solenoid_valve_1",
|
||||
"solenoid_valve_2",
|
||||
"solenoid_valve_3",
|
||||
"vacuum_pump_1",
|
||||
"gas_source_1",
|
||||
"h2_gas_source",
|
||||
"filter_1",
|
||||
"column_1",
|
||||
"separator_1",
|
||||
"collection_bottle_1",
|
||||
"collection_bottle_2",
|
||||
"collection_bottle_3"
|
||||
"collection_bottle_3",
|
||||
"solid_dispenser_1",
|
||||
"solid_reagent_bottle_1",
|
||||
"solid_reagent_bottle_2",
|
||||
"solid_reagent_bottle_3"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
@@ -60,7 +66,12 @@
|
||||
"HeatChillStartProtocol",
|
||||
"HeatChillStopProtocol",
|
||||
"EvacuateAndRefillProtocol",
|
||||
"PumpTransferProtocol"
|
||||
"PumpTransferProtocol",
|
||||
"AdjustPHProtocol",
|
||||
"ResetHandlingProtocol",
|
||||
"DryProtocol",
|
||||
"HydrogenateProtocol",
|
||||
"RecrystallizeProtocol"
|
||||
]
|
||||
},
|
||||
"data": {}
|
||||
@@ -461,6 +472,28 @@
|
||||
"is_open": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solenoid_valve_3",
|
||||
"name": "氢气电磁阀",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "device",
|
||||
"class": "virtual_solenoid_valve",
|
||||
"position": {
|
||||
"x": 450,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"voltage": 12.0,
|
||||
"response_time": 0.1,
|
||||
"gas_compatible": true
|
||||
},
|
||||
"data": {
|
||||
"valve_state": "Closed",
|
||||
"is_open": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vacuum_pump_1",
|
||||
"name": "真空泵",
|
||||
@@ -500,6 +533,29 @@
|
||||
"max_pressure": 5.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "h2_gas_source",
|
||||
"name": "氢气气源",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "device",
|
||||
"class": "virtual_gas_source",
|
||||
"position": {
|
||||
"x": 500,
|
||||
"y": 350,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_pressure": 10.0,
|
||||
"gas_type": "hydrogen"
|
||||
},
|
||||
"data": {
|
||||
"gas_type": "hydrogen",
|
||||
"max_pressure": 10.0,
|
||||
"current_pressure": 0.0,
|
||||
"status": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "filter_1",
|
||||
"name": "过滤器",
|
||||
@@ -620,6 +676,98 @@
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solid_dispenser_1",
|
||||
"name": "固体粉末加样器",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "device",
|
||||
"class": "virtual_solid_dispenser",
|
||||
"position": {
|
||||
"x": 600,
|
||||
"y": 300,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_capacity": 100.0,
|
||||
"precision": 0.001
|
||||
},
|
||||
"data": {
|
||||
"status": "Ready",
|
||||
"current_reagent": "",
|
||||
"dispensed_amount": 0.0,
|
||||
"total_operations": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solid_reagent_bottle_1",
|
||||
"name": "固体试剂瓶1-氯化钠",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "container",
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 550,
|
||||
"y": 250,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 500.0,
|
||||
"reagent": "sodium_chloride",
|
||||
"physical_state": "solid"
|
||||
},
|
||||
"data": {
|
||||
"current_mass": 500.0,
|
||||
"reagent_name": "sodium_chloride",
|
||||
"physical_state": "solid"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solid_reagent_bottle_2",
|
||||
"name": "固体试剂瓶2-碳酸钠",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "container",
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 600,
|
||||
"y": 250,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 500.0,
|
||||
"reagent": "sodium_carbonate",
|
||||
"physical_state": "solid"
|
||||
},
|
||||
"data": {
|
||||
"current_mass": 500.0,
|
||||
"reagent_name": "sodium_carbonate",
|
||||
"physical_state": "solid"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solid_reagent_bottle_3",
|
||||
"name": "固体试剂瓶3-氯化镁",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "container",
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 650,
|
||||
"y": 250,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 500.0,
|
||||
"reagent": "magnesium_chloride",
|
||||
"physical_state": "solid"
|
||||
},
|
||||
"data": {
|
||||
"current_mass": 500.0,
|
||||
"reagent_name": "magnesium_chloride",
|
||||
"physical_state": "solid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
@@ -680,7 +828,7 @@
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "5",
|
||||
"rotavap_1": "sample_in"
|
||||
"rotavap_1": "samplein"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -750,7 +898,7 @@
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "3",
|
||||
"solenoid_valve_2": "in"
|
||||
"solenoid_valve_2": "out"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -760,7 +908,7 @@
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"gas_source_1": "gassource",
|
||||
"solenoid_valve_2": "out"
|
||||
"solenoid_valve_2": "in"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -770,7 +918,7 @@
|
||||
"type": "transport",
|
||||
"port": {
|
||||
"multiway_valve_2": "4",
|
||||
"filter_1": "filter_in"
|
||||
"filter_1": "filterin"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -800,7 +948,7 @@
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "6",
|
||||
"separator_1": "separator_in"
|
||||
"separator_1": "separatorin"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -809,7 +957,7 @@
|
||||
"target": "collection_bottle_3",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"separator_1": "bottom_phase_out",
|
||||
"separator_1": "bottomphaseout",
|
||||
"collection_bottle_3": "top"
|
||||
}
|
||||
},
|
||||
@@ -859,7 +1007,7 @@
|
||||
"target": "waste_bottle_2",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"separator_1": "top_phase_out",
|
||||
"separator_1": "topphaseout",
|
||||
"waste_bottle_2": "top"
|
||||
}
|
||||
},
|
||||
@@ -874,14 +1022,14 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_filter_filtrate_to_collection1",
|
||||
"source": "filter_1",
|
||||
"target": "collection_bottle_1",
|
||||
"type": "transport",
|
||||
"port": {
|
||||
"filter_1": "filtrate_out",
|
||||
"collection_bottle_1": "top"
|
||||
}
|
||||
"id": "link_filter_filtrate_to_collection1",
|
||||
"source": "filter_1",
|
||||
"target": "collection_bottle_1",
|
||||
"type": "transport",
|
||||
"port": {
|
||||
"filter_1": "filtrateout",
|
||||
"collection_bottle_1": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_filter_retentate_to_waste1",
|
||||
@@ -889,9 +1037,69 @@
|
||||
"target": "waste_bottle_1",
|
||||
"type": "transport",
|
||||
"port": {
|
||||
"filter_1": "retentate_out",
|
||||
"filter_1": "retentateout",
|
||||
"waste_bottle_1": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_h2_gas_to_valve3",
|
||||
"source": "h2_gas_source",
|
||||
"target": "solenoid_valve_3",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"h2_gas_source": "gassource",
|
||||
"solenoid_valve_3": "in"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve3_to_reactor",
|
||||
"source": "solenoid_valve_3",
|
||||
"target": "main_reactor",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"solenoid_valve_3": "out",
|
||||
"main_reactor": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_solid_dispenser_to_reactor",
|
||||
"source": "solid_dispenser_1",
|
||||
"target": "main_reactor",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_dispenser_1": "SolidOut",
|
||||
"main_reactor": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_solid_bottle1_to_dispenser",
|
||||
"source": "solid_reagent_bottle_1",
|
||||
"target": "solid_dispenser_1",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_reagent_bottle_1": "top",
|
||||
"solid_dispenser_1": "SolidIn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_solid_bottle2_to_dispenser",
|
||||
"source": "solid_reagent_bottle_2",
|
||||
"target": "solid_dispenser_1",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_reagent_bottle_2": "top",
|
||||
"solid_dispenser_1": "SolidIn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_solid_bottle3_to_dispenser",
|
||||
"source": "solid_reagent_bottle_3",
|
||||
"target": "solid_dispenser_1",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_reagent_bottle_3": "top",
|
||||
"solid_dispenser_1": "SolidIn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -48,8 +48,9 @@ dependencies:
|
||||
- ros-humble-ros2-control
|
||||
- ros-humble-robot-state-publisher
|
||||
- ros-humble-joint-state-publisher
|
||||
# web
|
||||
# web and visualization
|
||||
- ros-humble-rosbridge-server
|
||||
- ros-humble-cv-bridge
|
||||
# geometry & motion planning
|
||||
- ros-humble-tf2
|
||||
- ros-humble-moveit
|
||||
|
||||
@@ -50,8 +50,9 @@ dependencies:
|
||||
- ros-humble-ros2-control
|
||||
- ros-humble-robot-state-publisher
|
||||
- ros-humble-joint-state-publisher
|
||||
# web
|
||||
# web and visualization
|
||||
- ros-humble-rosbridge-server
|
||||
- ros-humble-cv-bridge
|
||||
# geometry & motion planning
|
||||
- ros-humble-tf2
|
||||
- ros-humble-moveit
|
||||
|
||||
@@ -6,12 +6,12 @@ channels:
|
||||
dependencies:
|
||||
# Basics
|
||||
- python=3.11.11
|
||||
- compilers
|
||||
- cmake
|
||||
- make
|
||||
- ninja
|
||||
- sphinx
|
||||
- sphinx_rtd_theme
|
||||
# - compilers
|
||||
# - cmake
|
||||
# - make
|
||||
# - ninja
|
||||
# - sphinx
|
||||
# - sphinx_rtd_theme
|
||||
# Data Visualization
|
||||
- numpy
|
||||
- scipy
|
||||
@@ -23,7 +23,7 @@ dependencies:
|
||||
- pyserial
|
||||
- pyusb
|
||||
- pylibftdi
|
||||
- pymodbus
|
||||
- pymodbus==3.6.9
|
||||
- python-can
|
||||
- pyvisa
|
||||
- opencv
|
||||
@@ -48,8 +48,9 @@ dependencies:
|
||||
- ros-humble-ros2-control
|
||||
- ros-humble-robot-state-publisher
|
||||
- ros-humble-joint-state-publisher
|
||||
# web
|
||||
# web and visualization
|
||||
- ros-humble-rosbridge-server
|
||||
- ros-humble-cv-bridge
|
||||
# geometry & motion planning
|
||||
- ros-humble-tf2
|
||||
- ros-humble-moveit
|
||||
@@ -61,5 +62,12 @@ dependencies:
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
# ros-humble-unilabos-msgs
|
||||
# driver
|
||||
#- crcmod
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
- paho-mqtt
|
||||
# driver
|
||||
#- ur-rtde # set PYTHONUTF8=1
|
||||
#- pyautogui
|
||||
#- pywinauto
|
||||
#- pywinauto_recorder
|
||||
@@ -25,12 +25,13 @@ def job_add(req: JobAddReq) -> JobData:
|
||||
if req.job_id is None:
|
||||
req.job_id = str(uuid.uuid4())
|
||||
action_name = req.data["action"]
|
||||
action_kwargs = req.data["action_kwargs"]
|
||||
req.data['action'] = action_name
|
||||
if action_name == "execute_command_from_outer":
|
||||
action_kwargs = {"command": json.dumps(action_kwargs)}
|
||||
elif "command" in action_kwargs:
|
||||
action_kwargs = action_kwargs["command"]
|
||||
action_type = req.data.get("action_type", "LocalUnknown")
|
||||
action_args = req.data.get("action_kwargs", None) # 兼容老版本,后续删除
|
||||
if action_args is None:
|
||||
action_args = req.data.get("action_args")
|
||||
else:
|
||||
if "command" in action_args:
|
||||
action_args = action_args["command"]
|
||||
# print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
|
||||
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id, server_info=req.server_info)
|
||||
HostNode.get_instance().send_goal(req.device_id, action_type=action_type, action_name=action_name, action_kwargs=action_args, goal_uuid=req.job_id, server_info=req.server_info)
|
||||
return JobData(jobId=req.job_id)
|
||||
|
||||
@@ -56,6 +56,10 @@ class MQTTClient:
|
||||
payload_json["data"] = {}
|
||||
if "action" in payload_json:
|
||||
payload_json["data"]["action"] = payload_json.pop("action")
|
||||
if "action_type" in payload_json:
|
||||
payload_json["data"]["action_type"] = payload_json.pop("action_type")
|
||||
if "action_args" in payload_json:
|
||||
payload_json["data"]["action_args"] = payload_json.pop("action_args")
|
||||
if "action_kwargs" in payload_json:
|
||||
payload_json["data"]["action_kwargs"] = payload_json.pop("action_kwargs")
|
||||
job_req = JobAddReq.model_validate(payload_json)
|
||||
|
||||
@@ -34,7 +34,7 @@ def main():
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="注册设备和资源到 MQTT")
|
||||
parser.add_argument(
|
||||
"--registry_path",
|
||||
"--registry",
|
||||
type=str,
|
||||
default=None,
|
||||
action="append",
|
||||
@@ -46,10 +46,16 @@ def main():
|
||||
default=None,
|
||||
help="配置文件路径,支持.py格式的Python配置文件",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--complete_registry",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="是否补全注册表",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# 构建注册表
|
||||
build_registry(args.registry_path)
|
||||
build_registry(args.registry, args.complete_registry)
|
||||
load_config_from_file(args.config)
|
||||
|
||||
from unilabos.app.mq import mqtt_client
|
||||
|
||||
@@ -7,6 +7,7 @@ Web页面模块
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
@@ -17,7 +18,7 @@ from jinja2 import Environment, FileSystemLoader
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.registry.registry import lab_registry
|
||||
from unilabos.ros.msgs.message_converter import msg_converter_manager
|
||||
from unilabos.utils.log import error
|
||||
from unilabos.utils.log import error, debug
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
from unilabos.app.web.utils.device_utils import get_registry_info
|
||||
from unilabos.app.web.utils.host_utils import get_host_node_info
|
||||
@@ -123,6 +124,7 @@ def setup_web_pages(router: APIRouter) -> None:
|
||||
|
||||
return html
|
||||
except Exception as e:
|
||||
debug(traceback.format_exc())
|
||||
error(f"生成状态页面时出错: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Error generating status page: {str(e)}")
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ def get_yaml_from_goal_type(goal_type) -> str:
|
||||
Returns:
|
||||
str: 默认Goal参数的YAML格式字符串
|
||||
"""
|
||||
if isinstance(goal_type, str):
|
||||
return "{}"
|
||||
if not goal_type:
|
||||
return "{}"
|
||||
|
||||
|
||||
@@ -21,16 +21,23 @@ from .dissolve_protocol import generate_dissolve_protocol
|
||||
from .filter_through_protocol import generate_filter_through_protocol
|
||||
from .run_column_protocol import generate_run_column_protocol
|
||||
from .wash_solid_protocol import generate_wash_solid_protocol
|
||||
from .adjustph_protocol import generate_adjust_ph_protocol
|
||||
from .reset_handling_protocol import generate_reset_handling_protocol
|
||||
from .dry_protocol import generate_dry_protocol
|
||||
from .recrystallize_protocol import generate_recrystallize_protocol
|
||||
from .hydrogenate_protocol import generate_hydrogenate_protocol
|
||||
|
||||
|
||||
# Define a dictionary of protocol generators.
|
||||
action_protocol_generators = {
|
||||
AddProtocol: generate_add_protocol,
|
||||
AGVTransferProtocol: generate_agv_transfer_protocol,
|
||||
AdjustPHProtocol: generate_adjust_ph_protocol,
|
||||
CentrifugeProtocol: generate_centrifuge_protocol,
|
||||
CleanProtocol: generate_clean_protocol,
|
||||
CleanVesselProtocol: generate_clean_vessel_protocol,
|
||||
DissolveProtocol: generate_dissolve_protocol,
|
||||
DryProtocol: generate_dry_protocol,
|
||||
EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol,
|
||||
EvaporateProtocol: generate_evaporate_protocol,
|
||||
FilterProtocol: generate_filter_protocol,
|
||||
@@ -38,7 +45,10 @@ action_protocol_generators = {
|
||||
HeatChillProtocol: generate_heat_chill_protocol,
|
||||
HeatChillStartProtocol: generate_heat_chill_start_protocol,
|
||||
HeatChillStopProtocol: generate_heat_chill_stop_protocol,
|
||||
HydrogenateProtocol: generate_hydrogenate_protocol,
|
||||
PumpTransferProtocol: generate_pump_protocol_with_rinsing,
|
||||
RecrystallizeProtocol: generate_recrystallize_protocol,
|
||||
ResetHandlingProtocol: generate_reset_handling_protocol,
|
||||
RunColumnProtocol: generate_run_column_protocol,
|
||||
SeparateProtocol: generate_separate_protocol,
|
||||
StartStirProtocol: generate_start_stir_protocol,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
411
unilabos/compile/adjustph_protocol.py
Normal file
411
unilabos/compile/adjustph_protocol.py
Normal file
@@ -0,0 +1,411 @@
|
||||
import networkx as nx
|
||||
from typing import List, Dict, Any
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
|
||||
def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
|
||||
"""
|
||||
查找酸碱试剂容器,支持多种匹配模式
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
reagent: 试剂名称(如 "hydrochloric acid", "sodium hydroxide")
|
||||
|
||||
Returns:
|
||||
str: 试剂容器ID
|
||||
"""
|
||||
print(f"ADJUST_PH: 正在查找试剂 '{reagent}' 的容器...")
|
||||
|
||||
# 常见酸碱试剂的别名映射
|
||||
reagent_aliases = {
|
||||
"hydrochloric acid": ["HCl", "hydrochloric_acid", "hcl", "muriatic_acid"],
|
||||
"sodium hydroxide": ["NaOH", "sodium_hydroxide", "naoh", "caustic_soda"],
|
||||
"sulfuric acid": ["H2SO4", "sulfuric_acid", "h2so4"],
|
||||
"nitric acid": ["HNO3", "nitric_acid", "hno3"],
|
||||
"acetic acid": ["CH3COOH", "acetic_acid", "glacial_acetic_acid"],
|
||||
"ammonia": ["NH3", "ammonium_hydroxide", "nh3"],
|
||||
"potassium hydroxide": ["KOH", "potassium_hydroxide", "koh"]
|
||||
}
|
||||
|
||||
# 构建搜索名称列表
|
||||
search_names = [reagent.lower()]
|
||||
|
||||
# 添加别名
|
||||
for base_name, aliases in reagent_aliases.items():
|
||||
if reagent.lower() in base_name.lower() or base_name.lower() in reagent.lower():
|
||||
search_names.extend([alias.lower() for alias in aliases])
|
||||
|
||||
# 构建可能的容器名称
|
||||
possible_names = []
|
||||
for name in search_names:
|
||||
name_clean = name.replace(" ", "_").replace("-", "_")
|
||||
possible_names.extend([
|
||||
f"flask_{name_clean}",
|
||||
f"bottle_{name_clean}",
|
||||
f"reagent_{name_clean}",
|
||||
f"acid_{name_clean}" if "acid" in name else f"base_{name_clean}",
|
||||
f"{name_clean}_bottle",
|
||||
f"{name_clean}_flask",
|
||||
name_clean
|
||||
])
|
||||
|
||||
# 第一步:通过容器名称匹配
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
print(f"ADJUST_PH: 通过名称匹配找到容器: {vessel_name}")
|
||||
return vessel_name
|
||||
|
||||
# 第二步:通过模糊匹配
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
node_name = G.nodes[node_id].get('name', '').lower()
|
||||
|
||||
# 检查是否包含任何搜索名称
|
||||
for search_name in search_names:
|
||||
if search_name in node_id.lower() or search_name in node_name:
|
||||
print(f"ADJUST_PH: 通过模糊匹配找到容器: {node_id}")
|
||||
return node_id
|
||||
|
||||
# 第三步:通过液体类型匹配
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
vessel_data = G.nodes[node_id].get('data', {})
|
||||
liquids = vessel_data.get('liquid', [])
|
||||
|
||||
for liquid in liquids:
|
||||
if isinstance(liquid, dict):
|
||||
liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower()
|
||||
reagent_name = vessel_data.get('reagent_name', '').lower()
|
||||
|
||||
for search_name in search_names:
|
||||
if search_name in liquid_type or search_name in reagent_name:
|
||||
print(f"ADJUST_PH: 通过液体类型匹配找到容器: {node_id}")
|
||||
return node_id
|
||||
|
||||
# 列出可用容器帮助调试
|
||||
available_containers = []
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
vessel_data = G.nodes[node_id].get('data', {})
|
||||
liquids = vessel_data.get('liquid', [])
|
||||
liquid_types = [liquid.get('liquid_type', '') or liquid.get('name', '')
|
||||
for liquid in liquids if isinstance(liquid, dict)]
|
||||
|
||||
available_containers.append({
|
||||
'id': node_id,
|
||||
'name': G.nodes[node_id].get('name', ''),
|
||||
'liquids': liquid_types,
|
||||
'reagent_name': vessel_data.get('reagent_name', '')
|
||||
})
|
||||
|
||||
print(f"ADJUST_PH: 可用容器列表:")
|
||||
for container in available_containers:
|
||||
print(f" - {container['id']}: {container['name']}")
|
||||
print(f" 液体: {container['liquids']}")
|
||||
print(f" 试剂: {container['reagent_name']}")
|
||||
|
||||
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names}")
|
||||
|
||||
|
||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
|
||||
"""查找与容器相连的搅拌器"""
|
||||
stirrer_nodes = [node for node in G.nodes()
|
||||
if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
|
||||
|
||||
for stirrer in stirrer_nodes:
|
||||
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
|
||||
return stirrer
|
||||
|
||||
return stirrer_nodes[0] if stirrer_nodes else None
|
||||
|
||||
|
||||
def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float: # 改为 target_ph_value
|
||||
"""
|
||||
估算需要的试剂体积来调节pH
|
||||
|
||||
Args:
|
||||
target_ph_value: 目标pH值 # 改为 target_ph_value
|
||||
reagent: 试剂名称
|
||||
vessel_volume: 容器体积 (mL)
|
||||
|
||||
Returns:
|
||||
float: 估算的试剂体积 (mL)
|
||||
"""
|
||||
# 简化的pH调节体积估算(实际应用中需要更精确的计算)
|
||||
if "acid" in reagent.lower() or "hcl" in reagent.lower():
|
||||
# 酸性试剂:pH越低需要的体积越大
|
||||
if target_ph_value < 3: # 改为 target_ph_value
|
||||
return vessel_volume * 0.05 # 5%
|
||||
elif target_ph_value < 5: # 改为 target_ph_value
|
||||
return vessel_volume * 0.02 # 2%
|
||||
else:
|
||||
return vessel_volume * 0.01 # 1%
|
||||
|
||||
elif "hydroxide" in reagent.lower() or "naoh" in reagent.lower():
|
||||
# 碱性试剂:pH越高需要的体积越大
|
||||
if target_ph_value > 11: # 改为 target_ph_value
|
||||
return vessel_volume * 0.05 # 5%
|
||||
elif target_ph_value > 9: # 改为 target_ph_value
|
||||
return vessel_volume * 0.02 # 2%
|
||||
else:
|
||||
return vessel_volume * 0.01 # 1%
|
||||
|
||||
else:
|
||||
# 未知试剂,使用默认值
|
||||
return vessel_volume * 0.01
|
||||
|
||||
|
||||
def generate_adjust_ph_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
ph_value: float, # 改为 ph_value
|
||||
reagent: str,
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成调节pH的协议序列
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为容器和设备
|
||||
vessel: 目标容器(需要调节pH的容器)
|
||||
ph_value: 目标pH值(从XDL传入) # 改为 ph_value
|
||||
reagent: 酸碱试剂名称(从XDL传入)
|
||||
**kwargs: 其他可选参数,使用默认值
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
action_sequence = []
|
||||
|
||||
# 从kwargs中获取可选参数,如果没有则使用默认值
|
||||
volume = kwargs.get('volume', 0.0) # 自动估算体积
|
||||
stir = kwargs.get('stir', True) # 默认搅拌
|
||||
stir_speed = kwargs.get('stir_speed', 300.0) # 默认搅拌速度
|
||||
stir_time = kwargs.get('stir_time', 60.0) # 默认搅拌时间
|
||||
settling_time = kwargs.get('settling_time', 30.0) # 默认平衡时间
|
||||
|
||||
print(f"ADJUST_PH: 开始生成pH调节协议")
|
||||
print(f" - 目标容器: {vessel}")
|
||||
print(f" - 目标pH: {ph_value}") # 改为 ph_value
|
||||
print(f" - 试剂: {reagent}")
|
||||
print(f" - 使用默认参数: 体积=自动估算, 搅拌=True, 搅拌速度=300RPM")
|
||||
|
||||
# 1. 验证目标容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 2. 查找酸碱试剂容器
|
||||
try:
|
||||
reagent_vessel = find_acid_base_vessel(G, reagent)
|
||||
print(f"ADJUST_PH: 找到试剂容器: {reagent_vessel}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}")
|
||||
|
||||
# 3. 如果未指定体积,自动估算
|
||||
if volume <= 0:
|
||||
# 获取目标容器的体积信息
|
||||
vessel_data = G.nodes[vessel].get('data', {})
|
||||
vessel_volume = vessel_data.get('max_volume', 100.0) # 默认100mL
|
||||
|
||||
estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume) # 改为 ph_value
|
||||
volume = estimated_volume
|
||||
print(f"ADJUST_PH: 自动估算试剂体积: {volume:.2f} mL")
|
||||
|
||||
# 4. 验证路径存在
|
||||
try:
|
||||
path = nx.shortest_path(G, source=reagent_vessel, target=vessel)
|
||||
print(f"ADJUST_PH: 找到路径: {' → '.join(path)}")
|
||||
except nx.NetworkXNoPath:
|
||||
raise ValueError(f"从试剂容器 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径")
|
||||
|
||||
# 5. 先启动搅拌(如果需要)
|
||||
stirrer_id = None
|
||||
if stir:
|
||||
try:
|
||||
stirrer_id = find_connected_stirrer(G, vessel)
|
||||
|
||||
if stirrer_id:
|
||||
print(f"ADJUST_PH: 找到搅拌器 {stirrer_id},启动搅拌")
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": f"pH调节: 启动搅拌,准备添加 {reagent}"
|
||||
}
|
||||
})
|
||||
|
||||
# 等待搅拌稳定
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5}
|
||||
})
|
||||
else:
|
||||
print(f"ADJUST_PH: 警告 - 未找到搅拌器,继续执行")
|
||||
|
||||
except Exception as e:
|
||||
print(f"ADJUST_PH: 搅拌器配置出错: {str(e)}")
|
||||
|
||||
# 6. 缓慢添加试剂 - 使用pump_protocol
|
||||
print(f"ADJUST_PH: 开始添加试剂 {volume:.2f} mL")
|
||||
|
||||
# 计算添加时间(pH调节需要缓慢添加)
|
||||
addition_time = max(30.0, volume * 2.0) # 至少30秒,每mL需要2秒
|
||||
|
||||
try:
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=reagent_vessel,
|
||||
to_vessel=vessel,
|
||||
volume=volume,
|
||||
amount="",
|
||||
time=addition_time,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # pH调节不需要清洗
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=0.5 # 缓慢注入
|
||||
)
|
||||
|
||||
action_sequence.extend(pump_actions)
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"生成泵协议时出错: {str(e)}")
|
||||
|
||||
# 7. 持续搅拌以混合和平衡
|
||||
if stir and stirrer_id:
|
||||
print(f"ADJUST_PH: 持续搅拌 {stir_time} 秒以混合试剂")
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stir",
|
||||
"action_kwargs": {
|
||||
"stir_time": stir_time,
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": settling_time,
|
||||
"purpose": f"pH调节: 混合试剂,目标pH={ph_value}" # 改为 ph_value
|
||||
}
|
||||
})
|
||||
|
||||
# 8. 等待反应平衡
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": settling_time,
|
||||
"description": f"等待pH平衡到目标值 {ph_value}" # 改为 ph_value
|
||||
}
|
||||
})
|
||||
|
||||
print(f"ADJUST_PH: 协议生成完成,共 {len(action_sequence)} 个动作")
|
||||
print(f"ADJUST_PH: 预计总时间: {addition_time + stir_time + settling_time:.0f} 秒")
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
def generate_adjust_ph_protocol_stepwise(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
ph_value: float,
|
||||
reagent: str,
|
||||
max_volume: float = 10.0,
|
||||
steps: int = 3
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
分步调节pH的协议(更安全,避免过度调节)
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
vessel: 目标容器
|
||||
pH: 目标pH值
|
||||
reagent: 酸碱试剂
|
||||
max_volume: 最大试剂体积
|
||||
steps: 分步数量
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
action_sequence = []
|
||||
|
||||
print(f"ADJUST_PH: 开始分步pH调节({steps}步)")
|
||||
|
||||
# 每步添加的体积
|
||||
step_volume = max_volume / steps
|
||||
|
||||
for i in range(steps):
|
||||
print(f"ADJUST_PH: 第 {i+1}/{steps} 步,添加 {step_volume} mL")
|
||||
|
||||
# 生成单步协议
|
||||
step_actions = generate_adjust_ph_protocol(
|
||||
G=G,
|
||||
vessel=vessel,
|
||||
ph_value=ph_value,
|
||||
reagent=reagent,
|
||||
volume=step_volume,
|
||||
stir=True,
|
||||
stir_speed=300.0,
|
||||
stir_time=30.0,
|
||||
settling_time=20.0
|
||||
)
|
||||
|
||||
action_sequence.extend(step_actions)
|
||||
|
||||
# 步骤间等待
|
||||
if i < steps - 1:
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 30,
|
||||
"description": f"pH调节第{i+1}步完成,等待下一步"
|
||||
}
|
||||
})
|
||||
|
||||
print(f"ADJUST_PH: 分步pH调节完成")
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 便捷函数:常用pH调节
|
||||
def generate_acidify_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
target_ph: float = 2.0,
|
||||
acid: str = "hydrochloric acid"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""酸化协议"""
|
||||
return generate_adjust_ph_protocol(
|
||||
G, vessel, target_ph, acid, 0.0, True, 300.0, 120.0, 60.0
|
||||
)
|
||||
|
||||
|
||||
def generate_basify_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
target_ph: float = 12.0,
|
||||
base: str = "sodium hydroxide"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""碱化协议"""
|
||||
return generate_adjust_ph_protocol(
|
||||
G, vessel, target_ph, base, 0.0, True, 300.0, 120.0, 60.0
|
||||
)
|
||||
|
||||
|
||||
def generate_neutralize_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
reagent: str = "sodium hydroxide"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""中和协议(pH=7)"""
|
||||
return generate_adjust_ph_protocol(
|
||||
G, vessel, 7.0, reagent, 0.0, True, 350.0, 180.0, 90.0
|
||||
)
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_adjust_ph_protocol():
|
||||
"""测试pH调节协议"""
|
||||
print("=== ADJUST PH PROTOCOL 测试 ===")
|
||||
print("测试完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_adjust_ph_protocol()
|
||||
File diff suppressed because it is too large
Load Diff
165
unilabos/compile/dry_protocol.py
Normal file
165
unilabos/compile/dry_protocol.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import networkx as nx
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
def find_connected_heater(G: nx.DiGraph, vessel: str) -> str:
|
||||
"""
|
||||
查找与容器相连的加热器
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
vessel: 容器名称
|
||||
|
||||
Returns:
|
||||
str: 加热器ID,如果没有则返回None
|
||||
"""
|
||||
print(f"DRY: 正在查找与容器 '{vessel}' 相连的加热器...")
|
||||
|
||||
# 查找所有加热器节点
|
||||
heater_nodes = [node for node in G.nodes()
|
||||
if ('heater' in node.lower() or
|
||||
'heat' in node.lower() or
|
||||
G.nodes[node].get('class') == 'virtual_heatchill' or
|
||||
G.nodes[node].get('type') == 'heater')]
|
||||
|
||||
print(f"DRY: 找到的加热器节点: {heater_nodes}")
|
||||
|
||||
# 检查是否有加热器与目标容器相连
|
||||
for heater in heater_nodes:
|
||||
if G.has_edge(heater, vessel) or G.has_edge(vessel, heater):
|
||||
print(f"DRY: 找到与容器 '{vessel}' 相连的加热器: {heater}")
|
||||
return heater
|
||||
|
||||
# 如果没有直接连接,查找距离最近的加热器
|
||||
for heater in heater_nodes:
|
||||
try:
|
||||
path = nx.shortest_path(G, source=heater, target=vessel)
|
||||
if len(path) <= 3: # 最多2个中间节点
|
||||
print(f"DRY: 找到距离较近的加热器: {heater}, 路径: {' → '.join(path)}")
|
||||
return heater
|
||||
except nx.NetworkXNoPath:
|
||||
continue
|
||||
|
||||
print(f"DRY: 未找到与容器 '{vessel}' 相连的加热器")
|
||||
return None
|
||||
|
||||
|
||||
def generate_dry_protocol(
|
||||
G: nx.DiGraph,
|
||||
compound: str,
|
||||
vessel: str,
|
||||
**kwargs # 接收其他可能的参数但不使用
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成干燥协议序列
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为容器和设备
|
||||
compound: 化合物名称(从XDL传入)
|
||||
vessel: 目标容器(从XDL传入)
|
||||
**kwargs: 其他可选参数,但不使用
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
action_sequence = []
|
||||
|
||||
# 默认参数
|
||||
dry_temp = 60.0 # 默认干燥温度 60°C
|
||||
dry_time = 3600.0 # 默认干燥时间 1小时(3600秒)
|
||||
|
||||
print(f"DRY: 开始生成干燥协议")
|
||||
print(f" - 化合物: {compound}")
|
||||
print(f" - 容器: {vessel}")
|
||||
print(f" - 干燥温度: {dry_temp}°C")
|
||||
print(f" - 干燥时间: {dry_time/60:.0f} 分钟")
|
||||
|
||||
# 1. 验证目标容器存在
|
||||
if vessel not in G.nodes():
|
||||
print(f"DRY: 警告 - 容器 '{vessel}' 不存在于系统中,跳过干燥")
|
||||
return action_sequence
|
||||
|
||||
# 2. 查找相连的加热器
|
||||
heater_id = find_connected_heater(G, vessel)
|
||||
|
||||
if heater_id is None:
|
||||
print(f"DRY: 警告 - 未找到与容器 '{vessel}' 相连的加热器,跳过干燥")
|
||||
# 添加一个等待动作,表示干燥过程(模拟)
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 60.0, # 等待1分钟
|
||||
"description": f"模拟干燥 {compound} (无加热器可用)"
|
||||
}
|
||||
})
|
||||
return action_sequence
|
||||
|
||||
# 3. 启动加热器进行干燥
|
||||
print(f"DRY: 启动加热器 {heater_id} 进行干燥")
|
||||
|
||||
# 3.1 启动加热
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"temp": dry_temp,
|
||||
"purpose": f"干燥 {compound}"
|
||||
}
|
||||
})
|
||||
|
||||
# 3.2 等待温度稳定
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 60.0,
|
||||
"description": f"等待温度稳定到 {dry_temp}°C"
|
||||
}
|
||||
})
|
||||
|
||||
# 3.3 保持干燥温度
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"temp": dry_temp,
|
||||
"time": dry_time,
|
||||
"purpose": f"干燥 {compound},保持温度 {dry_temp}°C"
|
||||
}
|
||||
})
|
||||
|
||||
# 3.4 停止加热
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"purpose": f"干燥完成,停止加热"
|
||||
}
|
||||
})
|
||||
|
||||
# 3.5 等待冷却
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 300.0, # 等待5分钟冷却
|
||||
"description": f"等待 {compound} 冷却"
|
||||
}
|
||||
})
|
||||
|
||||
print(f"DRY: 协议生成完成,共 {len(action_sequence)} 个动作")
|
||||
print(f"DRY: 预计总时间: {(dry_time + 360)/60:.0f} 分钟")
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_dry_protocol():
|
||||
"""测试干燥协议"""
|
||||
print("=== DRY PROTOCOL 测试 ===")
|
||||
print("测试完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_dry_protocol()
|
||||
@@ -1,173 +1,357 @@
|
||||
import numpy as np
|
||||
import networkx as nx
|
||||
import logging
|
||||
import uuid # 🔧 移到顶部
|
||||
from typing import List, Dict, Any, Optional
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol
|
||||
|
||||
# 设置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出函数"""
|
||||
print(f"[EVACUATE_REFILL] {message}", flush=True)
|
||||
logger.info(f"[EVACUATE_REFILL] {message}")
|
||||
|
||||
def find_gas_source(G: nx.DiGraph, gas: str) -> str:
|
||||
"""根据气体名称查找对应的气源"""
|
||||
# 按照命名规则查找气源
|
||||
"""
|
||||
根据气体名称查找对应的气源,支持多种匹配模式:
|
||||
1. 容器名称匹配
|
||||
2. 气体类型匹配(data.gas_type)
|
||||
3. 默认气源
|
||||
"""
|
||||
debug_print(f"正在查找气体 '{gas}' 的气源...")
|
||||
|
||||
# 第一步:通过容器名称匹配
|
||||
gas_source_patterns = [
|
||||
f"gas_source_{gas}",
|
||||
f"gas_{gas}",
|
||||
f"flask_{gas}",
|
||||
f"{gas}_source"
|
||||
f"{gas}_source",
|
||||
f"source_{gas}",
|
||||
f"reagent_bottle_{gas}",
|
||||
f"bottle_{gas}"
|
||||
]
|
||||
|
||||
for pattern in gas_source_patterns:
|
||||
if pattern in G.nodes():
|
||||
debug_print(f"通过名称匹配找到气源: {pattern}")
|
||||
return pattern
|
||||
|
||||
# 模糊匹配
|
||||
for node in G.nodes():
|
||||
node_class = G.nodes[node].get('class', '') or ''
|
||||
if 'gas_source' in node_class and gas.lower() in node.lower():
|
||||
return node
|
||||
if node.startswith('flask_') and gas.lower() in node.lower():
|
||||
return node
|
||||
# 第二步:通过气体类型匹配 (data.gas_type)
|
||||
for node_id in G.nodes():
|
||||
node_data = G.nodes[node_id]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
# 检查是否是气源设备
|
||||
if ('gas_source' in node_class or
|
||||
'gas' in node_id.lower() or
|
||||
node_id.startswith('flask_')):
|
||||
|
||||
# 检查 data.gas_type
|
||||
data = node_data.get('data', {})
|
||||
gas_type = data.get('gas_type', '')
|
||||
|
||||
if gas_type.lower() == gas.lower():
|
||||
debug_print(f"通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})")
|
||||
return node_id
|
||||
|
||||
# 检查 config.gas_type
|
||||
config = node_data.get('config', {})
|
||||
config_gas_type = config.get('gas_type', '')
|
||||
|
||||
if config_gas_type.lower() == gas.lower():
|
||||
debug_print(f"通过配置气体类型匹配找到气源: {node_id} (config.gas_type: {config_gas_type})")
|
||||
return node_id
|
||||
|
||||
# 查找所有可用的气源
|
||||
available_gas_sources = [
|
||||
# 第三步:查找所有可用的气源设备
|
||||
available_gas_sources = []
|
||||
for node_id in G.nodes():
|
||||
node_data = G.nodes[node_id]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if ('gas_source' in node_class or
|
||||
'gas' in node_id.lower() or
|
||||
(node_id.startswith('flask_') and any(g in node_id.lower() for g in ['air', 'nitrogen', 'argon']))):
|
||||
|
||||
data = node_data.get('data', {})
|
||||
gas_type = data.get('gas_type', 'unknown')
|
||||
available_gas_sources.append(f"{node_id} (gas_type: {gas_type})")
|
||||
|
||||
debug_print(f"可用气源列表: {available_gas_sources}")
|
||||
|
||||
# 第四步:如果找不到特定气体,使用默认的第一个气源
|
||||
default_gas_sources = [
|
||||
node for node in G.nodes()
|
||||
if ((G.nodes[node].get('class') or '').startswith('virtual_gas_source')
|
||||
or ('gas' in node and 'source' in node)
|
||||
or (node.startswith('flask_') and any(g in node.lower() for g in ['air', 'nitrogen', 'argon', 'vacuum'])))
|
||||
if ((G.nodes[node].get('class') or '').find('virtual_gas_source') != -1
|
||||
or 'gas_source' in node)
|
||||
]
|
||||
|
||||
if default_gas_sources:
|
||||
default_source = default_gas_sources[0]
|
||||
debug_print(f"⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}")
|
||||
return default_source
|
||||
|
||||
raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}")
|
||||
|
||||
|
||||
def find_vacuum_pump(G: nx.DiGraph) -> str:
|
||||
"""查找真空泵设备"""
|
||||
vacuum_pumps = [
|
||||
node for node in G.nodes()
|
||||
if ((G.nodes[node].get('class') or '').startswith('virtual_vacuum_pump')
|
||||
or 'vacuum_pump' in node
|
||||
or 'vacuum' in (G.nodes[node].get('class') or ''))
|
||||
]
|
||||
debug_print("查找真空泵设备...")
|
||||
|
||||
vacuum_pumps = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if ('virtual_vacuum_pump' in node_class or
|
||||
'vacuum_pump' in node.lower() or
|
||||
'vacuum' in node_class.lower()):
|
||||
vacuum_pumps.append(node)
|
||||
|
||||
if not vacuum_pumps:
|
||||
raise ValueError("系统中未找到真空泵设备")
|
||||
|
||||
debug_print(f"找到真空泵: {vacuum_pumps[0]}")
|
||||
return vacuum_pumps[0]
|
||||
|
||||
|
||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
|
||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]:
|
||||
"""查找与指定容器相连的搅拌器"""
|
||||
stirrer_nodes = [node for node in G.nodes()
|
||||
if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
|
||||
debug_print(f"查找与容器 {vessel} 相连的搅拌器...")
|
||||
|
||||
stirrer_nodes = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if 'virtual_stirrer' in node_class or 'stirrer' in node.lower():
|
||||
stirrer_nodes.append(node)
|
||||
|
||||
# 检查哪个搅拌器与目标容器相连
|
||||
for stirrer in stirrer_nodes:
|
||||
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
|
||||
debug_print(f"找到连接的搅拌器: {stirrer}")
|
||||
return stirrer
|
||||
|
||||
return stirrer_nodes[0] if stirrer_nodes else None
|
||||
|
||||
|
||||
def find_associated_solenoid_valve(G: nx.DiGraph, device_id: str) -> Optional[str]:
|
||||
"""查找与指定设备相关联的电磁阀"""
|
||||
solenoid_valves = [
|
||||
node for node in G.nodes()
|
||||
if ('solenoid' in (G.nodes[node].get('class') or '').lower()
|
||||
or 'solenoid_valve' in node)
|
||||
]
|
||||
|
||||
# 通过网络连接查找直接相连的电磁阀
|
||||
for solenoid in solenoid_valves:
|
||||
if G.has_edge(device_id, solenoid) or G.has_edge(solenoid, device_id):
|
||||
return solenoid
|
||||
|
||||
# 通过命名规则查找关联的电磁阀
|
||||
device_type = ""
|
||||
if 'vacuum' in device_id.lower():
|
||||
device_type = "vacuum"
|
||||
elif 'gas' in device_id.lower():
|
||||
device_type = "gas"
|
||||
|
||||
if device_type:
|
||||
for solenoid in solenoid_valves:
|
||||
if device_type in solenoid.lower():
|
||||
return solenoid
|
||||
# 如果没有连接的搅拌器,返回第一个可用的
|
||||
if stirrer_nodes:
|
||||
debug_print(f"未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}")
|
||||
return stirrer_nodes[0]
|
||||
|
||||
debug_print("未找到搅拌器")
|
||||
return None
|
||||
|
||||
def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]:
|
||||
"""查找真空泵相关的电磁阀 - 根据实际连接逻辑"""
|
||||
debug_print(f"查找真空泵 {vacuum_pump} 相关的电磁阀...")
|
||||
|
||||
# 查找所有电磁阀
|
||||
solenoid_valves = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
|
||||
solenoid_valves.append(node)
|
||||
|
||||
debug_print(f"找到的电磁阀: {solenoid_valves}")
|
||||
|
||||
# 🔧 修复:根据实际组态图连接逻辑查找
|
||||
# vacuum_pump_1 <- solenoid_valve_1 <- multiway_valve_2
|
||||
for solenoid in solenoid_valves:
|
||||
# 检查电磁阀是否连接到真空泵
|
||||
if G.has_edge(solenoid, vacuum_pump) or G.has_edge(vacuum_pump, solenoid):
|
||||
debug_print(f"✓ 找到连接真空泵的电磁阀: {solenoid}")
|
||||
return solenoid
|
||||
|
||||
# 通过命名规则查找(备选方案)
|
||||
for solenoid in solenoid_valves:
|
||||
if 'vacuum' in solenoid.lower() or solenoid == 'solenoid_valve_1':
|
||||
debug_print(f"✓ 通过命名规则找到真空电磁阀: {solenoid}")
|
||||
return solenoid
|
||||
|
||||
debug_print("⚠️ 未找到真空电磁阀")
|
||||
return None
|
||||
|
||||
def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]:
|
||||
"""查找气源相关的电磁阀 - 根据实际连接逻辑"""
|
||||
debug_print(f"查找气源 {gas_source} 相关的电磁阀...")
|
||||
|
||||
# 查找所有电磁阀
|
||||
solenoid_valves = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
|
||||
solenoid_valves.append(node)
|
||||
|
||||
# 🔧 修复:根据实际组态图连接逻辑查找
|
||||
# gas_source_1 -> solenoid_valve_2 -> multiway_valve_2
|
||||
for solenoid in solenoid_valves:
|
||||
# 检查气源是否连接到电磁阀
|
||||
if G.has_edge(gas_source, solenoid) or G.has_edge(solenoid, gas_source):
|
||||
debug_print(f"✓ 找到连接气源的电磁阀: {solenoid}")
|
||||
return solenoid
|
||||
|
||||
# 通过命名规则查找(备选方案)
|
||||
for solenoid in solenoid_valves:
|
||||
if 'gas' in solenoid.lower() or solenoid == 'solenoid_valve_2':
|
||||
debug_print(f"✓ 通过命名规则找到气源电磁阀: {solenoid}")
|
||||
return solenoid
|
||||
|
||||
debug_print("⚠️ 未找到气源电磁阀")
|
||||
return None
|
||||
|
||||
def generate_evacuateandrefill_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
gas: str,
|
||||
repeats: int = 1
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成抽真空和充气操作的动作序列
|
||||
生成抽真空和充气操作的动作序列 - 最终修复版本
|
||||
|
||||
**修复版本**: 正确调用 pump_protocol 并处理异常
|
||||
Args:
|
||||
G: 设备图
|
||||
vessel: 目标容器名称(必需)
|
||||
gas: 气体名称(必需)
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
|
||||
# 硬编码重复次数为 3
|
||||
repeats = 3
|
||||
|
||||
# 🔧 修复:在函数开始就生成协议ID
|
||||
protocol_id = str(uuid.uuid4())
|
||||
debug_print(f"生成协议ID: {protocol_id}")
|
||||
|
||||
debug_print("=" * 60)
|
||||
debug_print("开始生成抽真空充气协议")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - vessel: {vessel}")
|
||||
debug_print(f" - gas: {gas}")
|
||||
debug_print(f" - repeats: {repeats} (硬编码)")
|
||||
debug_print(f" - 其他参数: {kwargs}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
# 参数设置 - 关键修复:减小体积避免超出泵容量
|
||||
VACUUM_VOLUME = 20.0 # 减小抽真空体积
|
||||
REFILL_VOLUME = 20.0 # 减小充气体积
|
||||
PUMP_FLOW_RATE = 2.5 # 降低流速
|
||||
STIR_SPEED = 300.0
|
||||
# === 参数验证和修正 ===
|
||||
debug_print("步骤1: 参数验证和修正...")
|
||||
|
||||
print(f"EVACUATE_REFILL: 开始生成协议,目标容器: {vessel}, 气体: {gas}, 重复次数: {repeats}")
|
||||
# 验证必需参数
|
||||
if not vessel:
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
if not gas:
|
||||
raise ValueError("gas 参数不能为空")
|
||||
|
||||
# 1. 验证设备存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 标准化气体名称
|
||||
gas_aliases = {
|
||||
'n2': 'nitrogen',
|
||||
'ar': 'argon',
|
||||
'air': 'air',
|
||||
'o2': 'oxygen',
|
||||
'co2': 'carbon_dioxide',
|
||||
'h2': 'hydrogen'
|
||||
}
|
||||
|
||||
original_gas = gas
|
||||
gas_lower = gas.lower().strip()
|
||||
if gas_lower in gas_aliases:
|
||||
gas = gas_aliases[gas_lower]
|
||||
debug_print(f"标准化气体名称: {original_gas} -> {gas}")
|
||||
|
||||
debug_print(f"最终参数: vessel={vessel}, gas={gas}, repeats={repeats}")
|
||||
|
||||
# === 查找设备 ===
|
||||
debug_print("步骤2: 查找设备...")
|
||||
|
||||
# 2. 查找设备
|
||||
try:
|
||||
vacuum_pump = find_vacuum_pump(G)
|
||||
vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump)
|
||||
gas_source = find_gas_source(G, gas)
|
||||
gas_solenoid = find_associated_solenoid_valve(G, gas_source)
|
||||
vacuum_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump)
|
||||
gas_solenoid = find_gas_solenoid_valve(G, gas_source)
|
||||
stirrer_id = find_connected_stirrer(G, vessel)
|
||||
|
||||
print(f"EVACUATE_REFILL: 找到设备")
|
||||
print(f" - 真空泵: {vacuum_pump}")
|
||||
print(f" - 气源: {gas_source}")
|
||||
print(f" - 真空电磁阀: {vacuum_solenoid}")
|
||||
print(f" - 气源电磁阀: {gas_solenoid}")
|
||||
print(f" - 搅拌器: {stirrer_id}")
|
||||
debug_print(f"设备配置:")
|
||||
debug_print(f" - 真空泵: {vacuum_pump}")
|
||||
debug_print(f" - 气源: {gas_source}")
|
||||
debug_print(f" - 真空电磁阀: {vacuum_solenoid}")
|
||||
debug_print(f" - 气源电磁阀: {gas_solenoid}")
|
||||
debug_print(f" - 搅拌器: {stirrer_id}")
|
||||
|
||||
except ValueError as e:
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)}")
|
||||
raise ValueError(f"设备查找失败: {str(e)}")
|
||||
|
||||
# 3. **关键修复**: 验证路径存在性
|
||||
try:
|
||||
# 验证抽真空路径
|
||||
vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump)
|
||||
print(f"EVACUATE_REFILL: 抽真空路径: {' → '.join(vacuum_path)}")
|
||||
|
||||
# 验证充气路径
|
||||
gas_path = nx.shortest_path(G, source=gas_source, target=vessel)
|
||||
print(f"EVACUATE_REFILL: 充气路径: {' → '.join(gas_path)}")
|
||||
|
||||
# **新增**: 检查路径中的边数据
|
||||
for i in range(len(vacuum_path) - 1):
|
||||
nodeA, nodeB = vacuum_path[i], vacuum_path[i + 1]
|
||||
edge_data = G.get_edge_data(nodeA, nodeB)
|
||||
if not edge_data or 'port' not in edge_data:
|
||||
raise ValueError(f"路径 {nodeA} → {nodeB} 缺少端口信息")
|
||||
print(f" 抽真空路径边 {nodeA} → {nodeB}: {edge_data}")
|
||||
|
||||
for i in range(len(gas_path) - 1):
|
||||
nodeA, nodeB = gas_path[i], gas_path[i + 1]
|
||||
edge_data = G.get_edge_data(nodeA, nodeB)
|
||||
if not edge_data or 'port' not in edge_data:
|
||||
raise ValueError(f"路径 {nodeA} → {nodeB} 缺少端口信息")
|
||||
print(f" 充气路径边 {nodeA} → {nodeB}: {edge_data}")
|
||||
|
||||
except nx.NetworkXNoPath as e:
|
||||
raise ValueError(f"路径不存在: {str(e)}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"路径验证失败: {str(e)}")
|
||||
# === 参数设置 ===
|
||||
debug_print("步骤3: 参数设置...")
|
||||
|
||||
# 根据气体类型调整参数
|
||||
if gas.lower() in ['nitrogen', 'argon']:
|
||||
VACUUM_VOLUME = 25.0
|
||||
REFILL_VOLUME = 25.0
|
||||
PUMP_FLOW_RATE = 2.0
|
||||
VACUUM_TIME = 30.0
|
||||
REFILL_TIME = 20.0
|
||||
debug_print("惰性气体:使用标准参数")
|
||||
elif gas.lower() in ['air', 'oxygen']:
|
||||
VACUUM_VOLUME = 20.0
|
||||
REFILL_VOLUME = 20.0
|
||||
PUMP_FLOW_RATE = 1.5
|
||||
VACUUM_TIME = 45.0
|
||||
REFILL_TIME = 25.0
|
||||
debug_print("活性气体:使用保守参数")
|
||||
else:
|
||||
VACUUM_VOLUME = 15.0
|
||||
REFILL_VOLUME = 15.0
|
||||
PUMP_FLOW_RATE = 1.0
|
||||
VACUUM_TIME = 60.0
|
||||
REFILL_TIME = 30.0
|
||||
debug_print("未知气体:使用安全参数")
|
||||
|
||||
STIR_SPEED = 200.0
|
||||
|
||||
debug_print(f"操作参数:")
|
||||
debug_print(f" - 抽真空体积: {VACUUM_VOLUME}mL")
|
||||
debug_print(f" - 充气体积: {REFILL_VOLUME}mL")
|
||||
debug_print(f" - 泵流速: {PUMP_FLOW_RATE}mL/s")
|
||||
debug_print(f" - 抽真空时间: {VACUUM_TIME}s")
|
||||
debug_print(f" - 充气时间: {REFILL_TIME}s")
|
||||
debug_print(f" - 搅拌速度: {STIR_SPEED}RPM")
|
||||
|
||||
# === 路径验证 ===
|
||||
debug_print("步骤4: 路径验证...")
|
||||
|
||||
try:
|
||||
# 验证抽真空路径: vessel -> vacuum_pump (通过八通阀和电磁阀)
|
||||
if nx.has_path(G, vessel, vacuum_pump):
|
||||
vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump)
|
||||
debug_print(f"抽真空路径: {' → '.join(vacuum_path)}")
|
||||
else:
|
||||
debug_print(f"⚠️ 抽真空路径不存在,继续执行但可能有问题")
|
||||
|
||||
# 验证充气路径: gas_source -> vessel (通过电磁阀和八通阀)
|
||||
if nx.has_path(G, gas_source, vessel):
|
||||
gas_path = nx.shortest_path(G, source=gas_source, target=vessel)
|
||||
debug_print(f"充气路径: {' → '.join(gas_path)}")
|
||||
else:
|
||||
debug_print(f"⚠️ 充气路径不存在,继续执行但可能有问题")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"⚠️ 路径验证失败: {str(e)},继续执行")
|
||||
|
||||
# === 启动搅拌器 ===
|
||||
debug_print("步骤5: 启动搅拌器...")
|
||||
|
||||
# 4. 启动搅拌器
|
||||
if stirrer_id:
|
||||
debug_print(f"启动搅拌器: {stirrer_id}")
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
@@ -177,15 +361,26 @@ def generate_evacuateandrefill_protocol(
|
||||
"purpose": "抽真空充气操作前启动搅拌"
|
||||
}
|
||||
})
|
||||
|
||||
# 等待搅拌稳定
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5.0}
|
||||
})
|
||||
else:
|
||||
debug_print("未找到搅拌器,跳过搅拌启动")
|
||||
|
||||
# === 执行 3 次抽真空-充气循环 ===
|
||||
debug_print("步骤6: 执行抽真空-充气循环...")
|
||||
|
||||
# 5. 执行多次抽真空-充气循环
|
||||
for cycle in range(repeats):
|
||||
print(f"EVACUATE_REFILL: === 第 {cycle+1}/{repeats} 次循环 ===")
|
||||
debug_print(f"=== 第 {cycle+1}/{repeats} 次循环 ===")
|
||||
|
||||
# ============ 抽真空阶段 ============
|
||||
print(f"EVACUATE_REFILL: 抽真空阶段开始")
|
||||
debug_print(f"抽真空阶段开始")
|
||||
|
||||
# 启动真空泵
|
||||
debug_print(f"启动真空泵: {vacuum_pump}")
|
||||
action_sequence.append({
|
||||
"device_id": vacuum_pump,
|
||||
"action_name": "set_status",
|
||||
@@ -194,18 +389,17 @@ def generate_evacuateandrefill_protocol(
|
||||
|
||||
# 开启真空电磁阀
|
||||
if vacuum_solenoid:
|
||||
debug_print(f"开启真空电磁阀: {vacuum_solenoid}")
|
||||
action_sequence.append({
|
||||
"device_id": vacuum_solenoid,
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {"command": "OPEN"}
|
||||
})
|
||||
|
||||
# **关键修复**: 改进 pump_protocol 调用和错误处理
|
||||
print(f"EVACUATE_REFILL: 调用抽真空 pump_protocol: {vessel} → {vacuum_pump}")
|
||||
print(f" - 体积: {VACUUM_VOLUME} mL")
|
||||
print(f" - 流速: {PUMP_FLOW_RATE} mL/s")
|
||||
|
||||
# 抽真空操作 - 使用液体转移协议
|
||||
debug_print(f"抽真空操作: {vessel} → {vacuum_pump}")
|
||||
try:
|
||||
|
||||
vacuum_transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=vessel,
|
||||
@@ -214,7 +408,7 @@ def generate_evacuateandrefill_protocol(
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # **修复**: 明确不使用清洗
|
||||
rinsing_solvent="",
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
@@ -224,52 +418,31 @@ def generate_evacuateandrefill_protocol(
|
||||
|
||||
if vacuum_transfer_actions:
|
||||
action_sequence.extend(vacuum_transfer_actions)
|
||||
print(f"EVACUATE_REFILL: ✅ 成功添加 {len(vacuum_transfer_actions)} 个抽真空动作")
|
||||
debug_print(f"✅ 添加了 {len(vacuum_transfer_actions)} 个抽真空动作")
|
||||
else:
|
||||
print(f"EVACUATE_REFILL: ⚠️ 抽真空 pump_protocol 返回空序列")
|
||||
# **修复**: 添加手动泵动作作为备选
|
||||
action_sequence.extend([
|
||||
{
|
||||
"device_id": "multiway_valve_1",
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {"command": "5"} # 连接到反应器
|
||||
},
|
||||
{
|
||||
"device_id": "transfer_pump_1",
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": VACUUM_VOLUME,
|
||||
"max_velocity": PUMP_FLOW_RATE
|
||||
}
|
||||
}
|
||||
])
|
||||
print(f"EVACUATE_REFILL: 使用备选手动泵动作")
|
||||
debug_print("⚠️ 抽真空协议返回空序列,添加手动动作")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": VACUUM_TIME}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"EVACUATE_REFILL: ❌ 抽真空 pump_protocol 失败: {str(e)}")
|
||||
import traceback
|
||||
print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}")
|
||||
|
||||
# **修复**: 添加手动动作而不是忽略错误
|
||||
print(f"EVACUATE_REFILL: 使用手动备选方案")
|
||||
action_sequence.extend([
|
||||
{
|
||||
"device_id": "multiway_valve_1",
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {"command": "5"} # 反应器端口
|
||||
},
|
||||
{
|
||||
"device_id": "transfer_pump_1",
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": VACUUM_VOLUME,
|
||||
"max_velocity": PUMP_FLOW_RATE
|
||||
}
|
||||
}
|
||||
])
|
||||
debug_print(f"❌ 抽真空失败: {str(e)}")
|
||||
# 添加等待时间作为备选
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": VACUUM_TIME}
|
||||
})
|
||||
|
||||
# 抽真空后等待
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": VACUUM_TIME}
|
||||
})
|
||||
|
||||
# 关闭真空电磁阀
|
||||
if vacuum_solenoid:
|
||||
debug_print(f"关闭真空电磁阀: {vacuum_solenoid}")
|
||||
action_sequence.append({
|
||||
"device_id": vacuum_solenoid,
|
||||
"action_name": "set_valve_position",
|
||||
@@ -277,34 +450,43 @@ def generate_evacuateandrefill_protocol(
|
||||
})
|
||||
|
||||
# 关闭真空泵
|
||||
debug_print(f"关闭真空泵: {vacuum_pump}")
|
||||
action_sequence.append({
|
||||
"device_id": vacuum_pump,
|
||||
"action_name": "set_status",
|
||||
"action_kwargs": {"string": "OFF"}
|
||||
})
|
||||
|
||||
# 抽真空后等待
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5.0}
|
||||
})
|
||||
|
||||
# ============ 充气阶段 ============
|
||||
print(f"EVACUATE_REFILL: 充气阶段开始")
|
||||
debug_print(f"充气阶段开始")
|
||||
|
||||
# 启动气源
|
||||
debug_print(f"启动气源: {gas_source}")
|
||||
action_sequence.append({
|
||||
"device_id": gas_source,
|
||||
"action_name": "set_status",
|
||||
"action_name": "set_status",
|
||||
"action_kwargs": {"string": "ON"}
|
||||
})
|
||||
|
||||
# 开启气源电磁阀
|
||||
if gas_solenoid:
|
||||
debug_print(f"开启气源电磁阀: {gas_solenoid}")
|
||||
action_sequence.append({
|
||||
"device_id": gas_solenoid,
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {"command": "OPEN"}
|
||||
})
|
||||
|
||||
# **关键修复**: 改进充气 pump_protocol 调用
|
||||
print(f"EVACUATE_REFILL: 调用充气 pump_protocol: {gas_source} → {vessel}")
|
||||
|
||||
# 充气操作 - 使用液体转移协议
|
||||
debug_print(f"充气操作: {gas_source} → {vessel}")
|
||||
try:
|
||||
|
||||
gas_transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=gas_source,
|
||||
@@ -313,7 +495,7 @@ def generate_evacuateandrefill_protocol(
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # **修复**: 明确不使用清洗
|
||||
rinsing_solvent="",
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
@@ -323,77 +505,31 @@ def generate_evacuateandrefill_protocol(
|
||||
|
||||
if gas_transfer_actions:
|
||||
action_sequence.extend(gas_transfer_actions)
|
||||
print(f"EVACUATE_REFILL: ✅ 成功添加 {len(gas_transfer_actions)} 个充气动作")
|
||||
debug_print(f"✅ 添加了 {len(gas_transfer_actions)} 个充气动作")
|
||||
else:
|
||||
print(f"EVACUATE_REFILL: ⚠️ 充气 pump_protocol 返回空序列")
|
||||
# **修复**: 添加手动充气动作
|
||||
action_sequence.extend([
|
||||
{
|
||||
"device_id": "multiway_valve_2",
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {"command": "8"} # 氮气端口
|
||||
},
|
||||
{
|
||||
"device_id": "transfer_pump_2",
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": REFILL_VOLUME,
|
||||
"max_velocity": PUMP_FLOW_RATE
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": "multiway_valve_2",
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {"command": "5"} # 反应器端口
|
||||
},
|
||||
{
|
||||
"device_id": "transfer_pump_2",
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": PUMP_FLOW_RATE
|
||||
}
|
||||
}
|
||||
])
|
||||
debug_print("⚠️ 充气协议返回空序列,添加手动动作")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": REFILL_TIME}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"EVACUATE_REFILL: ❌ 充气 pump_protocol 失败: {str(e)}")
|
||||
import traceback
|
||||
print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}")
|
||||
|
||||
# **修复**: 使用手动充气动作
|
||||
print(f"EVACUATE_REFILL: 使用手动充气方案")
|
||||
action_sequence.extend([
|
||||
{
|
||||
"device_id": "multiway_valve_2",
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {"command": "8"} # 连接气源
|
||||
},
|
||||
{
|
||||
"device_id": "transfer_pump_2",
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": REFILL_VOLUME,
|
||||
"max_velocity": PUMP_FLOW_RATE
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": "multiway_valve_2",
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {"command": "5"} # 连接反应器
|
||||
},
|
||||
{
|
||||
"device_id": "transfer_pump_2",
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": PUMP_FLOW_RATE
|
||||
}
|
||||
}
|
||||
])
|
||||
debug_print(f"❌ 充气失败: {str(e)}")
|
||||
# 添加等待时间作为备选
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": REFILL_TIME}
|
||||
})
|
||||
|
||||
# 充气后等待
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": REFILL_TIME}
|
||||
})
|
||||
|
||||
# 关闭气源电磁阀
|
||||
if gas_solenoid:
|
||||
debug_print(f"关闭气源电磁阀: {gas_solenoid}")
|
||||
action_sequence.append({
|
||||
"device_id": gas_solenoid,
|
||||
"action_name": "set_valve_position",
|
||||
@@ -401,6 +537,7 @@ def generate_evacuateandrefill_protocol(
|
||||
})
|
||||
|
||||
# 关闭气源
|
||||
debug_print(f"关闭气源: {gas_source}")
|
||||
action_sequence.append({
|
||||
"device_id": gas_source,
|
||||
"action_name": "set_status",
|
||||
@@ -409,29 +546,59 @@ def generate_evacuateandrefill_protocol(
|
||||
|
||||
# 等待下一次循环
|
||||
if cycle < repeats - 1:
|
||||
debug_print(f"等待下一次循环...")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 2.0}
|
||||
"action_kwargs": {"time": 10.0}
|
||||
})
|
||||
|
||||
# 停止搅拌器
|
||||
# === 停止搅拌器 ===
|
||||
debug_print("步骤7: 停止搅拌器...")
|
||||
|
||||
if stirrer_id:
|
||||
debug_print(f"停止搅拌器: {stirrer_id}")
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stop_stir",
|
||||
"action_kwargs": {"vessel": vessel}
|
||||
})
|
||||
|
||||
print(f"EVACUATE_REFILL: 协议生成完成,共 {len(action_sequence)} 个动作")
|
||||
# === 最终等待 ===
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 10.0}
|
||||
})
|
||||
|
||||
# === 总结 ===
|
||||
debug_print("=" * 60)
|
||||
debug_print(f"抽真空充气协议生成完成")
|
||||
debug_print(f"总动作数: {len(action_sequence)}")
|
||||
debug_print(f"处理容器: {vessel}")
|
||||
debug_print(f"使用气体: {gas}")
|
||||
debug_print(f"重复次数: {repeats} (硬编码)")
|
||||
debug_print("=" * 60)
|
||||
|
||||
return action_sequence
|
||||
|
||||
# === 便捷函数 ===
|
||||
|
||||
def generate_nitrogen_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
|
||||
"""生成氮气置换协议"""
|
||||
return generate_evacuateandrefill_protocol(G, vessel, "nitrogen", **kwargs)
|
||||
|
||||
def generate_argon_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
|
||||
"""生成氩气置换协议"""
|
||||
return generate_evacuateandrefill_protocol(G, vessel, "argon", **kwargs)
|
||||
|
||||
def generate_air_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
|
||||
"""生成空气置换协议"""
|
||||
return generate_evacuateandrefill_protocol(G, vessel, "air", **kwargs)
|
||||
|
||||
# 测试函数
|
||||
def test_evacuateandrefill_protocol():
|
||||
"""测试抽真空充气协议"""
|
||||
print("=== EVACUATE AND REFILL PROTOCOL 测试 ===")
|
||||
print("测试完成")
|
||||
|
||||
debug_print("=== EVACUATE AND REFILL PROTOCOL 测试 ===")
|
||||
debug_print("测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_evacuateandrefill_protocol()
|
||||
@@ -1,326 +1,404 @@
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
import networkx as nx
|
||||
from .pump_protocol import generate_pump_protocol
|
||||
import logging
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"[EVAPORATE] {message}", flush=True)
|
||||
logger.info(f"[EVAPORATE] {message}")
|
||||
|
||||
def parse_time_input(time_input: Union[str, float]) -> float:
|
||||
"""
|
||||
获取容器中的液体体积
|
||||
解析时间输入,支持带单位的字符串
|
||||
|
||||
Args:
|
||||
time_input: 时间输入(如 "3 min", "180", "0.5 h" 等)
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if vessel not in G.nodes():
|
||||
return 0.0
|
||||
if isinstance(time_input, (int, float)):
|
||||
return float(time_input)
|
||||
|
||||
vessel_data = G.nodes[vessel].get('data', {})
|
||||
liquids = vessel_data.get('liquid', [])
|
||||
if not time_input or not str(time_input).strip():
|
||||
return 180.0 # 默认3分钟
|
||||
|
||||
total_volume = 0.0
|
||||
for liquid in liquids:
|
||||
if isinstance(liquid, dict) and 'liquid_volume' in liquid:
|
||||
total_volume += liquid['liquid_volume']
|
||||
time_str = str(time_input).lower().strip()
|
||||
debug_print(f"解析时间输入: '{time_str}'")
|
||||
|
||||
return total_volume
|
||||
# 处理未知时间
|
||||
if time_str in ['?', 'unknown', 'tbd']:
|
||||
default_time = 180.0 # 默认3分钟
|
||||
debug_print(f"检测到未知时间,使用默认值: {default_time}s")
|
||||
return default_time
|
||||
|
||||
# 移除空格并提取数字和单位
|
||||
time_clean = re.sub(r'\s+', '', time_str)
|
||||
|
||||
# 匹配数字和单位的正则表达式
|
||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
|
||||
|
||||
if not match:
|
||||
# 如果无法解析,尝试直接转换为数字(默认秒)
|
||||
try:
|
||||
value = float(time_str)
|
||||
debug_print(f"时间解析: {time_str} → {value}s(无单位,默认秒)")
|
||||
return value
|
||||
except ValueError:
|
||||
debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值180s")
|
||||
return 180.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or 's' # 默认单位为秒
|
||||
|
||||
# 转换为秒
|
||||
if unit in ['min', 'minute']:
|
||||
time_sec = value * 60.0 # min -> s
|
||||
elif unit in ['h', 'hr', 'hour']:
|
||||
time_sec = value * 3600.0 # h -> s
|
||||
elif unit in ['d', 'day']:
|
||||
time_sec = value * 86400.0 # d -> s
|
||||
else: # s, sec, second 或默认
|
||||
time_sec = value # 已经是s
|
||||
|
||||
debug_print(f"时间转换: {value}{unit} → {time_sec}s")
|
||||
return time_sec
|
||||
|
||||
def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
|
||||
"""
|
||||
在组态图中查找旋转蒸发仪设备
|
||||
|
||||
Args:
|
||||
G: 设备图
|
||||
vessel: 指定的设备名称(可选)
|
||||
|
||||
Returns:
|
||||
str: 找到的旋转蒸发仪设备ID,如果没找到返回None
|
||||
"""
|
||||
debug_print("查找旋转蒸发仪设备...")
|
||||
|
||||
# 如果指定了vessel,先检查是否存在且是旋转蒸发仪
|
||||
if vessel:
|
||||
if vessel in G.nodes():
|
||||
node_data = G.nodes[vessel]
|
||||
node_class = node_data.get('class', '')
|
||||
node_type = node_data.get('type', '')
|
||||
|
||||
debug_print(f"检查指定设备 {vessel}: class={node_class}, type={node_type}")
|
||||
|
||||
# 检查是否为旋转蒸发仪
|
||||
if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
|
||||
debug_print(f"✓ 找到指定的旋转蒸发仪: {vessel}")
|
||||
return vessel
|
||||
elif node_type == 'device':
|
||||
debug_print(f"✓ 指定设备存在,尝试直接使用: {vessel}")
|
||||
return vessel
|
||||
else:
|
||||
debug_print(f"✗ 指定的设备 {vessel} 不存在")
|
||||
|
||||
# 在所有设备中查找旋转蒸发仪
|
||||
rotavap_candidates = []
|
||||
|
||||
for node_id, node_data in G.nodes(data=True):
|
||||
node_class = node_data.get('class', '')
|
||||
node_type = node_data.get('type', '')
|
||||
|
||||
# 跳过非设备节点
|
||||
if node_type != 'device':
|
||||
continue
|
||||
|
||||
# 检查设备类型
|
||||
if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
|
||||
rotavap_candidates.append(node_id)
|
||||
debug_print(f"✓ 找到旋转蒸发仪候选: {node_id} (class: {node_class})")
|
||||
elif any(keyword in str(node_id).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
|
||||
rotavap_candidates.append(node_id)
|
||||
debug_print(f"✓ 找到旋转蒸发仪候选 (按名称): {node_id}")
|
||||
|
||||
if rotavap_candidates:
|
||||
selected = rotavap_candidates[0] # 选择第一个找到的
|
||||
debug_print(f"✓ 选择旋转蒸发仪: {selected}")
|
||||
return selected
|
||||
|
||||
debug_print("✗ 未找到旋转蒸发仪设备")
|
||||
return None
|
||||
|
||||
def find_rotavap_device(G: nx.DiGraph) -> str:
|
||||
"""查找旋转蒸发仪设备"""
|
||||
rotavap_nodes = [node for node in G.nodes()
|
||||
if (G.nodes[node].get('class') or '') == 'virtual_rotavap']
|
||||
def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]:
|
||||
"""
|
||||
查找与旋转蒸发仪连接的容器
|
||||
|
||||
if rotavap_nodes:
|
||||
return rotavap_nodes[0]
|
||||
Args:
|
||||
G: 设备图
|
||||
rotavap_device: 旋转蒸发仪设备ID
|
||||
|
||||
raise ValueError("系统中未找到旋转蒸发仪设备")
|
||||
|
||||
|
||||
def find_solvent_recovery_vessel(G: nx.DiGraph) -> str:
|
||||
"""查找溶剂回收容器"""
|
||||
possible_names = [
|
||||
"flask_distillate",
|
||||
"bottle_distillate",
|
||||
"vessel_distillate",
|
||||
"distillate",
|
||||
"solvent_recovery",
|
||||
"flask_solvent_recovery",
|
||||
"collection_flask"
|
||||
]
|
||||
Returns:
|
||||
str: 连接的容器ID,如果没找到返回None
|
||||
"""
|
||||
debug_print(f"查找与 {rotavap_device} 连接的容器...")
|
||||
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
return vessel_name
|
||||
# 查看旋转蒸发仪的子设备
|
||||
rotavap_data = G.nodes[rotavap_device]
|
||||
children = rotavap_data.get('children', [])
|
||||
|
||||
# 如果找不到专门的回收容器,使用废液容器
|
||||
waste_names = ["waste_workup", "flask_waste", "bottle_waste", "waste"]
|
||||
for vessel_name in waste_names:
|
||||
if vessel_name in G.nodes():
|
||||
return vessel_name
|
||||
for child_id in children:
|
||||
if child_id in G.nodes():
|
||||
child_data = G.nodes[child_id]
|
||||
child_type = child_data.get('type', '')
|
||||
|
||||
if child_type == 'container':
|
||||
debug_print(f"✓ 找到连接的容器: {child_id}")
|
||||
return child_id
|
||||
|
||||
raise ValueError(f"未找到溶剂回收容器。尝试了以下名称: {possible_names + waste_names}")
|
||||
|
||||
# 查看邻接的容器
|
||||
for neighbor in G.neighbors(rotavap_device):
|
||||
neighbor_data = G.nodes[neighbor]
|
||||
neighbor_type = neighbor_data.get('type', '')
|
||||
|
||||
if neighbor_type == 'container':
|
||||
debug_print(f"✓ 找到邻接的容器: {neighbor}")
|
||||
return neighbor
|
||||
|
||||
debug_print("✗ 未找到连接的容器")
|
||||
return None
|
||||
|
||||
def generate_evaporate_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
pressure: float = 0.1,
|
||||
temp: float = 60.0,
|
||||
time: float = 1800.0,
|
||||
stir_speed: float = 100.0
|
||||
time: Union[str, float] = "180", # 🔧 修改:支持字符串时间
|
||||
stir_speed: float = 100.0,
|
||||
solvent: str = "",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成蒸发操作的协议序列
|
||||
|
||||
蒸发流程:
|
||||
1. 液体转移:将待蒸发溶液从源容器转移到旋转蒸发仪
|
||||
2. 蒸发操作:执行旋转蒸发
|
||||
3. (可选) 溶剂回收:将冷凝的溶剂转移到回收容器
|
||||
4. 残留物转移:将浓缩物从旋转蒸发仪转移回原容器或新容器
|
||||
生成蒸发操作的协议序列 - 支持单位
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为设备和容器,边为流体管道
|
||||
vessel: 包含待蒸发溶液的容器名称
|
||||
pressure: 蒸发时的真空度 (bar),默认0.1 bar
|
||||
temp: 蒸发时的加热温度 (°C),默认60°C
|
||||
time: 蒸发时间 (秒),默认1800秒(30分钟)
|
||||
stir_speed: 旋转速度 (RPM),默认100 RPM
|
||||
G: 设备图
|
||||
vessel: 容器名称或旋转蒸发仪名称
|
||||
pressure: 真空度 (bar),默认0.1
|
||||
temp: 加热温度 (°C),默认60
|
||||
time: 蒸发时间(支持 "3 min", "180", "0.5 h" 等)
|
||||
stir_speed: 旋转速度 (RPM),默认100
|
||||
solvent: 溶剂名称(用于参数优化)
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 蒸发操作的动作序列
|
||||
|
||||
Raises:
|
||||
ValueError: 当找不到必要的设备时抛出异常
|
||||
|
||||
Examples:
|
||||
evaporate_actions = generate_evaporate_protocol(G, "reaction_mixture", 0.05, 80.0, 3600.0)
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
|
||||
debug_print("=" * 50)
|
||||
debug_print("开始生成蒸发协议(支持单位)")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - vessel: {vessel}")
|
||||
debug_print(f" - pressure: {pressure} bar")
|
||||
debug_print(f" - temp: {temp}°C")
|
||||
debug_print(f" - time: {time} (类型: {type(time)})")
|
||||
debug_print(f" - stir_speed: {stir_speed} RPM")
|
||||
debug_print(f" - solvent: '{solvent}'")
|
||||
debug_print("=" * 50)
|
||||
|
||||
# === 步骤1: 查找旋转蒸发仪设备 ===
|
||||
debug_print("步骤1: 查找旋转蒸发仪设备...")
|
||||
|
||||
# 验证vessel参数
|
||||
if not vessel:
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
# 查找旋转蒸发仪设备
|
||||
rotavap_device = find_rotavap_device(G, vessel)
|
||||
if not rotavap_device:
|
||||
raise ValueError(f"未找到旋转蒸发仪设备。请检查组态图中是否包含 class 包含 'rotavap'、'rotary' 或 'evaporat' 的设备")
|
||||
|
||||
# === 步骤2: 确定目标容器 ===
|
||||
debug_print("步骤2: 确定目标容器...")
|
||||
|
||||
target_vessel = vessel
|
||||
|
||||
# 如果vessel就是旋转蒸发仪设备,查找连接的容器
|
||||
if vessel == rotavap_device:
|
||||
connected_vessel = find_connected_vessel(G, rotavap_device)
|
||||
if connected_vessel:
|
||||
target_vessel = connected_vessel
|
||||
debug_print(f"使用连接的容器: {target_vessel}")
|
||||
else:
|
||||
debug_print(f"未找到连接的容器,使用设备本身: {rotavap_device}")
|
||||
target_vessel = rotavap_device
|
||||
elif vessel in G.nodes() and G.nodes[vessel].get('type') == 'container':
|
||||
debug_print(f"使用指定的容器: {vessel}")
|
||||
target_vessel = vessel
|
||||
else:
|
||||
debug_print(f"容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device}")
|
||||
target_vessel = rotavap_device
|
||||
|
||||
# === 🔧 新增:步骤3:单位解析处理 ===
|
||||
debug_print("步骤3: 单位解析处理...")
|
||||
|
||||
# 解析时间
|
||||
final_time = parse_time_input(time)
|
||||
debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)")
|
||||
|
||||
# === 步骤4: 参数验证和修正 ===
|
||||
debug_print("步骤4: 参数验证和修正...")
|
||||
|
||||
# 修正参数范围
|
||||
if pressure <= 0 or pressure > 1.0:
|
||||
debug_print(f"真空度 {pressure} bar 超出范围,修正为 0.1 bar")
|
||||
pressure = 0.1
|
||||
|
||||
if temp < 10.0 or temp > 200.0:
|
||||
debug_print(f"温度 {temp}°C 超出范围,修正为 60°C")
|
||||
temp = 60.0
|
||||
|
||||
if final_time <= 0:
|
||||
debug_print(f"时间 {final_time}s 无效,修正为 180s")
|
||||
final_time = 180.0
|
||||
|
||||
if stir_speed < 10.0 or stir_speed > 300.0:
|
||||
debug_print(f"旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM")
|
||||
stir_speed = 100.0
|
||||
|
||||
# 根据溶剂优化参数
|
||||
if solvent:
|
||||
debug_print(f"根据溶剂 '{solvent}' 优化参数...")
|
||||
solvent_lower = solvent.lower()
|
||||
|
||||
if any(s in solvent_lower for s in ['water', 'aqueous', 'h2o']):
|
||||
temp = max(temp, 80.0)
|
||||
pressure = max(pressure, 0.2)
|
||||
debug_print("水系溶剂:提高温度和真空度")
|
||||
elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
|
||||
temp = min(temp, 50.0)
|
||||
pressure = min(pressure, 0.05)
|
||||
debug_print("易挥发溶剂:降低温度和真空度")
|
||||
elif any(s in solvent_lower for s in ['dmso', 'dmi', 'toluene']):
|
||||
temp = max(temp, 100.0)
|
||||
pressure = min(pressure, 0.01)
|
||||
debug_print("高沸点溶剂:提高温度,降低真空度")
|
||||
|
||||
debug_print(f"最终参数: pressure={pressure}, temp={temp}, time={final_time}, stir_speed={stir_speed}")
|
||||
|
||||
# === 步骤5: 生成动作序列 ===
|
||||
debug_print("步骤5: 生成动作序列...")
|
||||
|
||||
action_sequence = []
|
||||
|
||||
print(f"EVAPORATE: 开始生成蒸发协议")
|
||||
print(f" - 源容器: {vessel}")
|
||||
print(f" - 真空度: {pressure} bar")
|
||||
print(f" - 温度: {temp}°C")
|
||||
print(f" - 时间: {time}s ({time/60:.1f}分钟)")
|
||||
print(f" - 旋转速度: {stir_speed} RPM")
|
||||
|
||||
# 验证源容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"源容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 获取源容器中的液体体积
|
||||
source_volume = get_vessel_liquid_volume(G, vessel)
|
||||
print(f"EVAPORATE: 源容器 {vessel} 中有 {source_volume} mL 液体")
|
||||
|
||||
# 查找旋转蒸发仪
|
||||
try:
|
||||
rotavap_id = find_rotavap_device(G)
|
||||
print(f"EVAPORATE: 找到旋转蒸发仪: {rotavap_id}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到旋转蒸发仪: {str(e)}")
|
||||
|
||||
# 查找旋转蒸发仪样品容器
|
||||
rotavap_vessel = None
|
||||
possible_rotavap_vessels = ["rotavap_flask", "rotavap", "flask_rotavap", "evaporation_flask"]
|
||||
for rv in possible_rotavap_vessels:
|
||||
if rv in G.nodes():
|
||||
rotavap_vessel = rv
|
||||
break
|
||||
|
||||
if not rotavap_vessel:
|
||||
raise ValueError(f"未找到旋转蒸发仪样品容器。尝试了: {possible_rotavap_vessels}")
|
||||
|
||||
print(f"EVAPORATE: 找到旋转蒸发仪样品容器: {rotavap_vessel}")
|
||||
|
||||
# 查找溶剂回收容器
|
||||
try:
|
||||
distillate_vessel = find_solvent_recovery_vessel(G)
|
||||
print(f"EVAPORATE: 找到溶剂回收容器: {distillate_vessel}")
|
||||
except ValueError as e:
|
||||
print(f"EVAPORATE: 警告 - {str(e)}")
|
||||
distillate_vessel = None
|
||||
|
||||
# === 简化的体积计算策略 ===
|
||||
if source_volume > 0:
|
||||
# 如果能检测到液体体积,使用实际体积的大部分
|
||||
transfer_volume = min(source_volume * 0.9, 250.0) # 90%或最多250mL
|
||||
print(f"EVAPORATE: 检测到液体体积,将转移 {transfer_volume} mL")
|
||||
else:
|
||||
# 如果检测不到液体体积,默认转移一整瓶 250mL
|
||||
transfer_volume = 250.0
|
||||
print(f"EVAPORATE: 未检测到液体体积,默认转移整瓶 {transfer_volume} mL")
|
||||
|
||||
# === 第一步:将待蒸发溶液转移到旋转蒸发仪 ===
|
||||
print(f"EVAPORATE: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {rotavap_vessel}")
|
||||
try:
|
||||
transfer_to_rotavap_actions = generate_pump_protocol(
|
||||
G=G,
|
||||
from_vessel=vessel,
|
||||
to_vessel=rotavap_vessel,
|
||||
volume=transfer_volume,
|
||||
flowrate=2.0,
|
||||
transfer_flowrate=2.0
|
||||
)
|
||||
action_sequence.extend(transfer_to_rotavap_actions)
|
||||
except Exception as e:
|
||||
raise ValueError(f"无法将溶液转移到旋转蒸发仪: {str(e)}")
|
||||
|
||||
# 转移后等待
|
||||
wait_action = {
|
||||
# 等待稳定
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 10}
|
||||
}
|
||||
action_sequence.append(wait_action)
|
||||
})
|
||||
|
||||
# === 第二步:执行旋转蒸发 ===
|
||||
print(f"EVAPORATE: 执行旋转蒸发操作")
|
||||
# 执行蒸发
|
||||
debug_print(f"执行蒸发: 设备={rotavap_device}, 容器={target_vessel}")
|
||||
evaporate_action = {
|
||||
"device_id": rotavap_id,
|
||||
"device_id": rotavap_device,
|
||||
"action_name": "evaporate",
|
||||
"action_kwargs": {
|
||||
"vessel": rotavap_vessel,
|
||||
"vessel": target_vessel,
|
||||
"pressure": pressure,
|
||||
"temp": temp,
|
||||
"time": time,
|
||||
"stir_speed": stir_speed
|
||||
"time": final_time,
|
||||
"stir_speed": stir_speed,
|
||||
"solvent": solvent
|
||||
}
|
||||
}
|
||||
action_sequence.append(evaporate_action)
|
||||
|
||||
# 蒸发后等待系统稳定
|
||||
wait_action = {
|
||||
# 蒸发后等待
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 30}
|
||||
}
|
||||
action_sequence.append(wait_action)
|
||||
})
|
||||
|
||||
# === 第三步:溶剂回收(如果有回收容器)===
|
||||
if distillate_vessel:
|
||||
print(f"EVAPORATE: 回收冷凝溶剂到 {distillate_vessel}")
|
||||
try:
|
||||
condenser_vessel = "rotavap_condenser"
|
||||
if condenser_vessel in G.nodes():
|
||||
# 估算回收体积(约为转移体积的70% - 大部分溶剂被蒸发回收)
|
||||
recovery_volume = transfer_volume * 0.7
|
||||
print(f"EVAPORATE: 预计回收 {recovery_volume} mL 溶剂")
|
||||
|
||||
recovery_actions = generate_pump_protocol(
|
||||
G=G,
|
||||
from_vessel=condenser_vessel,
|
||||
to_vessel=distillate_vessel,
|
||||
volume=recovery_volume,
|
||||
flowrate=3.0,
|
||||
transfer_flowrate=3.0
|
||||
)
|
||||
action_sequence.extend(recovery_actions)
|
||||
else:
|
||||
print("EVAPORATE: 未找到冷凝器容器,跳过溶剂回收")
|
||||
except Exception as e:
|
||||
print(f"EVAPORATE: 溶剂回收失败: {str(e)}")
|
||||
|
||||
# === 第四步:将浓缩物转移回原容器 ===
|
||||
print(f"EVAPORATE: 将浓缩物从旋转蒸发仪转移回 {vessel}")
|
||||
try:
|
||||
# 估算浓缩物体积(约为转移体积的20% - 大部分溶剂已蒸发)
|
||||
concentrate_volume = transfer_volume * 0.2
|
||||
print(f"EVAPORATE: 预计浓缩物体积 {concentrate_volume} mL")
|
||||
|
||||
transfer_back_actions = generate_pump_protocol(
|
||||
G=G,
|
||||
from_vessel=rotavap_vessel,
|
||||
to_vessel=vessel,
|
||||
volume=concentrate_volume,
|
||||
flowrate=1.0, # 浓缩物可能粘稠,用较慢流速
|
||||
transfer_flowrate=1.0
|
||||
)
|
||||
action_sequence.extend(transfer_back_actions)
|
||||
except Exception as e:
|
||||
print(f"EVAPORATE: 将浓缩物转移回容器失败: {str(e)}")
|
||||
|
||||
# === 第五步:清洗旋转蒸发仪 ===
|
||||
print(f"EVAPORATE: 清洗旋转蒸发仪")
|
||||
try:
|
||||
# 查找清洗溶剂
|
||||
cleaning_solvent = None
|
||||
for solvent in ["flask_ethanol", "flask_acetone", "flask_water"]:
|
||||
if solvent in G.nodes():
|
||||
cleaning_solvent = solvent
|
||||
break
|
||||
|
||||
if cleaning_solvent and distillate_vessel:
|
||||
# 用固定量溶剂清洗(不依赖检测体积)
|
||||
cleaning_volume = 50.0 # 固定50mL清洗
|
||||
print(f"EVAPORATE: 用 {cleaning_volume} mL {cleaning_solvent} 清洗")
|
||||
|
||||
# 清洗溶剂加入
|
||||
cleaning_actions = generate_pump_protocol(
|
||||
G=G,
|
||||
from_vessel=cleaning_solvent,
|
||||
to_vessel=rotavap_vessel,
|
||||
volume=cleaning_volume,
|
||||
flowrate=2.0,
|
||||
transfer_flowrate=2.0
|
||||
)
|
||||
action_sequence.extend(cleaning_actions)
|
||||
|
||||
# 将清洗液转移到废液/回收容器
|
||||
waste_actions = generate_pump_protocol(
|
||||
G=G,
|
||||
from_vessel=rotavap_vessel,
|
||||
to_vessel=distillate_vessel, # 使用回收容器作为废液
|
||||
volume=cleaning_volume,
|
||||
flowrate=2.0,
|
||||
transfer_flowrate=2.0
|
||||
)
|
||||
action_sequence.extend(waste_actions)
|
||||
|
||||
except Exception as e:
|
||||
print(f"EVAPORATE: 清洗步骤失败: {str(e)}")
|
||||
|
||||
print(f"EVAPORATE: 生成了 {len(action_sequence)} 个动作")
|
||||
print(f"EVAPORATE: 蒸发协议生成完成")
|
||||
print(f"EVAPORATE: 总处理体积: {transfer_volume} mL")
|
||||
# === 总结 ===
|
||||
debug_print("=" * 50)
|
||||
debug_print(f"蒸发协议生成完成")
|
||||
debug_print(f"总动作数: {len(action_sequence)}")
|
||||
debug_print(f"旋转蒸发仪: {rotavap_device}")
|
||||
debug_print(f"目标容器: {target_vessel}")
|
||||
debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {final_time}s, {stir_speed} RPM")
|
||||
debug_print("=" * 50)
|
||||
|
||||
return action_sequence
|
||||
|
||||
# === 便捷函数 ===
|
||||
|
||||
# 便捷函数:常用蒸发方案 - 都使用250mL标准瓶体积
|
||||
def generate_quick_evaporate_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float = 40.0,
|
||||
time: float = 900.0 # 15分钟
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""快速蒸发:低温、短时间、整瓶处理"""
|
||||
return generate_evaporate_protocol(G, vessel, 0.2, temp, time, 80.0)
|
||||
|
||||
"""快速蒸发:低温短时间"""
|
||||
return generate_evaporate_protocol(
|
||||
G, vessel,
|
||||
pressure=0.2,
|
||||
temp=40.0,
|
||||
time="15 min", # 🔧 使用带单位的时间
|
||||
stir_speed=80.0,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def generate_gentle_evaporate_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float = 50.0,
|
||||
time: float = 2700.0 # 45分钟
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""温和蒸发:中等条件、较长时间、整瓶处理"""
|
||||
return generate_evaporate_protocol(G, vessel, 0.1, temp, time, 60.0)
|
||||
|
||||
"""温和蒸发:中等条件"""
|
||||
return generate_evaporate_protocol(
|
||||
G, vessel,
|
||||
pressure=0.1,
|
||||
temp=50.0,
|
||||
time="45 min", # 🔧 使用带单位的时间
|
||||
stir_speed=60.0,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def generate_high_vacuum_evaporate_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float = 35.0,
|
||||
time: float = 3600.0 # 1小时
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""高真空蒸发:低温、高真空、长时间、整瓶处理"""
|
||||
return generate_evaporate_protocol(G, vessel, 0.01, temp, time, 120.0)
|
||||
|
||||
"""高真空蒸发:低温高真空"""
|
||||
return generate_evaporate_protocol(
|
||||
G, vessel,
|
||||
pressure=0.01,
|
||||
temp=35.0,
|
||||
time="1 h", # 🔧 使用带单位的时间
|
||||
stir_speed=120.0,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def generate_standard_evaporate_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str
|
||||
vessel: str,
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""标准蒸发:常用参数、整瓶250mL处理"""
|
||||
"""标准蒸发:常用参数"""
|
||||
return generate_evaporate_protocol(
|
||||
G=G,
|
||||
vessel=vessel,
|
||||
pressure=0.1, # 标准真空度
|
||||
temp=60.0, # 适中温度
|
||||
time=1800.0, # 30分钟
|
||||
stir_speed=100.0 # 适中旋转速度
|
||||
G, vessel,
|
||||
pressure=0.1,
|
||||
temp=60.0,
|
||||
time="3 min", # 🔧 使用带单位的时间
|
||||
stir_speed=100.0,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# 测试函数
|
||||
def test_time_parsing():
|
||||
"""测试时间解析功能"""
|
||||
print("=== EVAPORATE 时间解析测试 ===")
|
||||
|
||||
test_times = ["3 min", "180", "0.5 h", "2 hours", "?", "unknown", "1.5", "30 s"]
|
||||
for time_str in test_times:
|
||||
result = parse_time_input(time_str)
|
||||
print(f"时间解析: '{time_str}' → {result}s ({result/60:.1f}分钟)")
|
||||
|
||||
print("✅ 测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_time_parsing()
|
||||
|
||||
@@ -1,304 +1,215 @@
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Optional
|
||||
import networkx as nx
|
||||
from .pump_protocol import generate_pump_protocol
|
||||
import logging
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
|
||||
"""获取容器中的液体体积"""
|
||||
if vessel not in G.nodes():
|
||||
return 0.0
|
||||
|
||||
vessel_data = G.nodes[vessel].get('data', {})
|
||||
liquids = vessel_data.get('liquid', [])
|
||||
|
||||
total_volume = 0.0
|
||||
for liquid in liquids:
|
||||
if isinstance(liquid, dict) and 'liquid_volume' in liquid:
|
||||
total_volume += liquid['liquid_volume']
|
||||
|
||||
return total_volume
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"[FILTER] {message}", flush=True)
|
||||
logger.info(f"[FILTER] {message}")
|
||||
|
||||
def find_filter_device(G: nx.DiGraph) -> str:
|
||||
"""查找过滤器设备"""
|
||||
filter_nodes = [node for node in G.nodes()
|
||||
if (G.nodes[node].get('class') or '') == 'virtual_filter']
|
||||
debug_print("查找过滤器设备...")
|
||||
|
||||
if filter_nodes:
|
||||
return filter_nodes[0]
|
||||
# 查找过滤器设备
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if 'filter' in node_class.lower() or 'filter' in node.lower():
|
||||
debug_print(f"找到过滤器设备: {node}")
|
||||
return node
|
||||
|
||||
raise ValueError("系统中未找到过滤器设备")
|
||||
# 如果没找到,寻找可能的过滤器名称
|
||||
possible_names = ["filter", "filter_1", "virtual_filter", "filtration_unit"]
|
||||
for name in possible_names:
|
||||
if name in G.nodes():
|
||||
debug_print(f"找到过滤器设备: {name}")
|
||||
return name
|
||||
|
||||
raise ValueError("未找到过滤器设备")
|
||||
|
||||
|
||||
def find_filter_vessel(G: nx.DiGraph) -> str:
|
||||
"""查找过滤器专用容器"""
|
||||
possible_names = [
|
||||
"filter_vessel", # 标准过滤器容器
|
||||
"filtration_vessel", # 备选名称
|
||||
"vessel_filter", # 备选名称
|
||||
"filter_unit", # 备选名称
|
||||
"filter" # 简单名称
|
||||
]
|
||||
def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") -> None:
|
||||
"""验证容器是否存在"""
|
||||
if not vessel:
|
||||
raise ValueError(f"{vessel_type}不能为空")
|
||||
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
return vessel_name
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"{vessel_type} '{vessel}' 不存在于系统中")
|
||||
|
||||
raise ValueError(f"未找到过滤器容器。尝试了以下名称: {possible_names}")
|
||||
|
||||
|
||||
def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str:
|
||||
"""查找滤液收集容器"""
|
||||
if filtrate_vessel and filtrate_vessel in G.nodes():
|
||||
return filtrate_vessel
|
||||
|
||||
# 自动查找滤液容器
|
||||
possible_names = [
|
||||
"filtrate_vessel",
|
||||
"collection_bottle_1",
|
||||
"collection_bottle_2",
|
||||
"waste_workup"
|
||||
]
|
||||
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
return vessel_name
|
||||
|
||||
raise ValueError(f"未找到滤液收集容器。尝试了以下名称: {possible_names}")
|
||||
|
||||
|
||||
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
|
||||
"""查找与指定容器相连的加热搅拌器"""
|
||||
# 查找所有加热搅拌器节点
|
||||
heatchill_nodes = [node for node in G.nodes()
|
||||
if G.nodes[node].get('class') == 'virtual_heatchill']
|
||||
|
||||
# 检查哪个加热器与目标容器相连
|
||||
for heatchill in heatchill_nodes:
|
||||
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
|
||||
return heatchill
|
||||
|
||||
# 如果没有直接连接,返回第一个可用的加热器
|
||||
if heatchill_nodes:
|
||||
return heatchill_nodes[0]
|
||||
|
||||
raise ValueError(f"未找到与容器 {vessel} 相连的加热搅拌器")
|
||||
|
||||
debug_print(f"✅ {vessel_type} '{vessel}' 验证通过")
|
||||
|
||||
def generate_filter_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
filtrate_vessel: str = "",
|
||||
stir: bool = False,
|
||||
stir_speed: float = 300.0,
|
||||
temp: float = 25.0,
|
||||
continue_heatchill: bool = False,
|
||||
volume: float = 0.0
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成过滤操作的协议序列,复用 pump_protocol 的成熟算法
|
||||
|
||||
过滤流程:
|
||||
1. 液体转移:将待过滤溶液从源容器转移到过滤器
|
||||
2. 启动加热搅拌:设置温度和搅拌
|
||||
3. 执行过滤:通过过滤器分离固液
|
||||
4. (可选) 继续或停止加热搅拌
|
||||
生成过滤操作的协议序列
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为设备和容器,边为流体管道
|
||||
vessel: 包含待过滤溶液的容器名称
|
||||
filtrate_vessel: 滤液收集容器(可选,自动查找)
|
||||
stir: 是否在过滤过程中搅拌
|
||||
stir_speed: 搅拌速度 (RPM)
|
||||
temp: 过滤温度 (°C)
|
||||
continue_heatchill: 过滤后是否继续加热搅拌
|
||||
volume: 预期过滤体积 (mL),0表示全部过滤
|
||||
G: 设备图
|
||||
vessel: 过滤容器名称(必需)- 包含需要过滤的混合物
|
||||
filtrate_vessel: 滤液容器名称(可选)- 如果提供则收集滤液
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 过滤操作的动作序列
|
||||
"""
|
||||
|
||||
debug_print("=" * 60)
|
||||
debug_print("开始生成过滤协议")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - vessel: {vessel}")
|
||||
debug_print(f" - filtrate_vessel: {filtrate_vessel}")
|
||||
debug_print(f" - 其他参数: {kwargs}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
print(f"FILTER: 开始生成过滤协议")
|
||||
print(f" - 源容器: {vessel}")
|
||||
print(f" - 滤液容器: {filtrate_vessel}")
|
||||
print(f" - 搅拌: {stir} ({stir_speed} RPM)" if stir else " - 搅拌: 否")
|
||||
print(f" - 过滤温度: {temp}°C")
|
||||
print(f" - 预期过滤体积: {volume} mL" if volume > 0 else " - 预期过滤体积: 全部")
|
||||
print(f" - 继续加热搅拌: {continue_heatchill}")
|
||||
# === 参数验证 ===
|
||||
debug_print("步骤1: 参数验证...")
|
||||
|
||||
# 验证源容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"源容器 '{vessel}' 不存在于系统中")
|
||||
# 验证必需参数
|
||||
validate_vessel(G, vessel, "过滤容器")
|
||||
|
||||
# 获取源容器中的液体体积
|
||||
source_volume = get_vessel_liquid_volume(G, vessel)
|
||||
print(f"FILTER: 源容器 {vessel} 中有 {source_volume} mL 液体")
|
||||
|
||||
# 查找过滤器设备
|
||||
try:
|
||||
filter_id = find_filter_device(G)
|
||||
print(f"FILTER: 找到过滤器: {filter_id}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到过滤器: {str(e)}")
|
||||
|
||||
# 查找过滤器容器
|
||||
try:
|
||||
filter_vessel_id = find_filter_vessel(G)
|
||||
print(f"FILTER: 找到过滤器容器: {filter_vessel_id}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到过滤器容器: {str(e)}")
|
||||
|
||||
# 查找滤液收集容器
|
||||
try:
|
||||
actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel)
|
||||
print(f"FILTER: 找到滤液收集容器: {actual_filtrate_vessel}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到滤液收集容器: {str(e)}")
|
||||
|
||||
# 查找加热搅拌器(如果需要温度控制或搅拌)
|
||||
heatchill_id = None
|
||||
if temp != 25.0 or stir or continue_heatchill:
|
||||
try:
|
||||
heatchill_id = find_connected_heatchill(G, filter_vessel_id)
|
||||
print(f"FILTER: 找到加热搅拌器: {heatchill_id}")
|
||||
except ValueError as e:
|
||||
print(f"FILTER: 警告 - {str(e)}")
|
||||
|
||||
# === 简化的体积计算策略 ===
|
||||
if volume > 0:
|
||||
transfer_volume = min(volume, source_volume if source_volume > 0 else volume)
|
||||
print(f"FILTER: 指定过滤体积 {transfer_volume} mL")
|
||||
elif source_volume > 0:
|
||||
transfer_volume = source_volume * 0.9 # 90%
|
||||
print(f"FILTER: 检测到液体体积,将过滤 {transfer_volume} mL")
|
||||
# 验证可选参数
|
||||
if filtrate_vessel:
|
||||
validate_vessel(G, filtrate_vessel, "滤液容器")
|
||||
debug_print("模式: 过滤并收集滤液")
|
||||
else:
|
||||
transfer_volume = 50.0 # 默认过滤量
|
||||
print(f"FILTER: 未检测到液体体积,默认过滤 {transfer_volume} mL")
|
||||
debug_print("模式: 过滤并收集固体")
|
||||
|
||||
# === 第一步:启动加热搅拌器(在转移前预热) ===
|
||||
if heatchill_id and (temp != 25.0 or stir):
|
||||
print(f"FILTER: 启动加热搅拌器,温度: {temp}°C,搅拌: {stir}")
|
||||
|
||||
heatchill_action = {
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": filter_vessel_id,
|
||||
"temp": temp,
|
||||
"purpose": f"过滤过程温度控制和搅拌"
|
||||
}
|
||||
}
|
||||
action_sequence.append(heatchill_action)
|
||||
|
||||
# 等待温度稳定
|
||||
if temp != 25.0:
|
||||
wait_time = min(30, abs(temp - 25.0) * 1.0) # 根据温差估算预热时间
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": wait_time}
|
||||
})
|
||||
# === 查找设备 ===
|
||||
debug_print("步骤2: 查找设备...")
|
||||
|
||||
# === 第二步:将待过滤溶液转移到过滤器 ===
|
||||
print(f"FILTER: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {filter_vessel_id}")
|
||||
try:
|
||||
# 使用成熟的 pump_protocol 算法进行液体转移
|
||||
transfer_to_filter_actions = generate_pump_protocol(
|
||||
G=G,
|
||||
from_vessel=vessel,
|
||||
to_vessel=filter_vessel_id,
|
||||
volume=transfer_volume,
|
||||
flowrate=1.0, # 过滤转移用较慢速度,避免扰动
|
||||
transfer_flowrate=1.5
|
||||
)
|
||||
action_sequence.extend(transfer_to_filter_actions)
|
||||
filter_device = find_filter_device(G)
|
||||
debug_print(f"使用过滤器设备: {filter_device}")
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"无法将溶液转移到过滤器: {str(e)}")
|
||||
debug_print(f"❌ 设备查找失败: {str(e)}")
|
||||
raise ValueError(f"设备查找失败: {str(e)}")
|
||||
|
||||
# 转移后等待
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5}
|
||||
})
|
||||
# === 转移到过滤器(如果需要)===
|
||||
debug_print("步骤3: 转移到过滤器...")
|
||||
|
||||
# === 第三步:执行过滤操作(完全按照 Filter.action 参数) ===
|
||||
print(f"FILTER: 执行过滤操作")
|
||||
if vessel != filter_device:
|
||||
debug_print(f"需要转移: {vessel} → {filter_device}")
|
||||
|
||||
try:
|
||||
# 使用pump protocol转移液体到过滤器
|
||||
transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=vessel,
|
||||
to_vessel=filter_device,
|
||||
volume=0.0, # 转移所有液体
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="",
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.0,
|
||||
transfer_flowrate=2.0
|
||||
)
|
||||
|
||||
if transfer_actions:
|
||||
action_sequence.extend(transfer_actions)
|
||||
debug_print(f"✅ 添加了 {len(transfer_actions)} 个转移动作")
|
||||
else:
|
||||
debug_print("⚠️ 转移协议返回空序列")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 转移失败: {str(e)}")
|
||||
# 继续执行,可能是直接连接的过滤器
|
||||
else:
|
||||
debug_print("过滤容器就是过滤器,无需转移")
|
||||
|
||||
# === 执行过滤操作 ===
|
||||
debug_print("步骤4: 执行过滤操作...")
|
||||
|
||||
# 构建过滤动作参数
|
||||
filter_kwargs = {
|
||||
"vessel": filter_device, # 过滤器设备
|
||||
"filtrate_vessel": filtrate_vessel, # 滤液容器(可能为空)
|
||||
"stir": kwargs.get("stir", False),
|
||||
"stir_speed": kwargs.get("stir_speed", 0.0),
|
||||
"temp": kwargs.get("temp", 25.0),
|
||||
"continue_heatchill": kwargs.get("continue_heatchill", False),
|
||||
"volume": kwargs.get("volume", 0.0) # 0表示过滤所有
|
||||
}
|
||||
|
||||
debug_print(f"过滤参数: {filter_kwargs}")
|
||||
|
||||
# 过滤动作
|
||||
filter_action = {
|
||||
"device_id": filter_id,
|
||||
"device_id": filter_device,
|
||||
"action_name": "filter",
|
||||
"action_kwargs": {
|
||||
"vessel": filter_vessel_id,
|
||||
"filtrate_vessel": actual_filtrate_vessel,
|
||||
"stir": stir,
|
||||
"stir_speed": stir_speed,
|
||||
"temp": temp,
|
||||
"continue_heatchill": continue_heatchill,
|
||||
"volume": transfer_volume
|
||||
}
|
||||
"action_kwargs": filter_kwargs
|
||||
}
|
||||
action_sequence.append(filter_action)
|
||||
|
||||
# 过滤后等待
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 10}
|
||||
"action_kwargs": {"time": 10.0}
|
||||
})
|
||||
|
||||
# === 第四步:如果不继续加热搅拌,停止加热器 ===
|
||||
if heatchill_id and not continue_heatchill and (temp != 25.0 or stir):
|
||||
print(f"FILTER: 停止加热搅拌器")
|
||||
|
||||
stop_action = {
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": filter_vessel_id
|
||||
}
|
||||
}
|
||||
action_sequence.append(stop_action)
|
||||
# === 收集滤液(如果需要)===
|
||||
debug_print("步骤5: 收集滤液...")
|
||||
|
||||
print(f"FILTER: 生成了 {len(action_sequence)} 个动作")
|
||||
print(f"FILTER: 过滤协议生成完成")
|
||||
if filtrate_vessel:
|
||||
debug_print(f"收集滤液: {filter_device} → {filtrate_vessel}")
|
||||
|
||||
try:
|
||||
# 使用pump protocol收集滤液
|
||||
collect_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=filter_device,
|
||||
to_vessel=filtrate_vessel,
|
||||
volume=0.0, # 收集所有滤液
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="",
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.0,
|
||||
transfer_flowrate=2.0
|
||||
)
|
||||
|
||||
if collect_actions:
|
||||
action_sequence.extend(collect_actions)
|
||||
debug_print(f"✅ 添加了 {len(collect_actions)} 个收集动作")
|
||||
else:
|
||||
debug_print("⚠️ 收集协议返回空序列")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 收集滤液失败: {str(e)}")
|
||||
# 继续执行,可能滤液直接流入指定容器
|
||||
else:
|
||||
debug_print("未指定滤液容器,固体保留在过滤器中")
|
||||
|
||||
# === 最终等待 ===
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5.0}
|
||||
})
|
||||
|
||||
# === 总结 ===
|
||||
debug_print("=" * 60)
|
||||
debug_print(f"过滤协议生成完成")
|
||||
debug_print(f"总动作数: {len(action_sequence)}")
|
||||
debug_print(f"过滤容器: {vessel}")
|
||||
debug_print(f"过滤器设备: {filter_device}")
|
||||
debug_print(f"滤液容器: {filtrate_vessel or '无(保留固体)'}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 便捷函数:常用过滤方案
|
||||
def generate_gravity_filter_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
filtrate_vessel: str = ""
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""重力过滤:室温,无搅拌"""
|
||||
return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, 25.0, False, 0.0)
|
||||
|
||||
|
||||
def generate_hot_filter_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
filtrate_vessel: str = "",
|
||||
temp: float = 60.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""热过滤:高温过滤,防止结晶析出"""
|
||||
return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, temp, False, 0.0)
|
||||
|
||||
|
||||
def generate_stirred_filter_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
filtrate_vessel: str = "",
|
||||
stir_speed: float = 200.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""搅拌过滤:低速搅拌,防止滤饼堵塞"""
|
||||
return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, 25.0, False, 0.0)
|
||||
|
||||
|
||||
def generate_hot_stirred_filter_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
filtrate_vessel: str = "",
|
||||
temp: float = 60.0,
|
||||
stir_speed: float = 300.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""热搅拌过滤:高温搅拌过滤"""
|
||||
return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, temp, False, 0.0)
|
||||
@@ -1,148 +1,550 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from typing import List, Dict, Any, Union
|
||||
import networkx as nx
|
||||
import logging
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"[HEATCHILL] {message}", flush=True)
|
||||
logger.info(f"[HEATCHILL] {message}")
|
||||
|
||||
def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
|
||||
"""
|
||||
解析带单位的时间输入
|
||||
|
||||
Args:
|
||||
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
|
||||
default_unit: 默认单位(默认为秒)
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if not time_input:
|
||||
return 0.0
|
||||
|
||||
# 处理数值输入
|
||||
if isinstance(time_input, (int, float)):
|
||||
result = float(time_input)
|
||||
debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
|
||||
return result
|
||||
|
||||
# 处理字符串输入
|
||||
time_str = str(time_input).lower().strip()
|
||||
debug_print(f"解析时间字符串: '{time_str}'")
|
||||
|
||||
# 处理特殊值
|
||||
if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
||||
default_time = 300.0 # 5分钟默认值
|
||||
debug_print(f"检测到未知时间,使用默认值: {default_time}s")
|
||||
return default_time
|
||||
|
||||
# 如果是纯数字,使用默认单位
|
||||
try:
|
||||
value = float(time_str)
|
||||
if default_unit == "s":
|
||||
result = value
|
||||
elif default_unit in ["min", "minute"]:
|
||||
result = value * 60.0
|
||||
elif default_unit in ["h", "hour"]:
|
||||
result = value * 3600.0
|
||||
else:
|
||||
result = value # 默认秒
|
||||
debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
|
||||
return result
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 使用正则表达式匹配数字和单位
|
||||
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
|
||||
match = re.match(pattern, time_str)
|
||||
|
||||
if not match:
|
||||
debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
|
||||
return 60.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or default_unit
|
||||
|
||||
# 单位转换映射
|
||||
unit_multipliers = {
|
||||
# 秒
|
||||
's': 1.0,
|
||||
'sec': 1.0,
|
||||
'second': 1.0,
|
||||
'seconds': 1.0,
|
||||
|
||||
# 分钟
|
||||
'm': 60.0,
|
||||
'min': 60.0,
|
||||
'mins': 60.0,
|
||||
'minute': 60.0,
|
||||
'minutes': 60.0,
|
||||
|
||||
# 小时
|
||||
'h': 3600.0,
|
||||
'hr': 3600.0,
|
||||
'hrs': 3600.0,
|
||||
'hour': 3600.0,
|
||||
'hours': 3600.0,
|
||||
|
||||
# 天
|
||||
'd': 86400.0,
|
||||
'day': 86400.0,
|
||||
'days': 86400.0,
|
||||
}
|
||||
|
||||
multiplier = unit_multipliers.get(unit, 1.0)
|
||||
result = value * multiplier
|
||||
|
||||
debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
|
||||
return result
|
||||
|
||||
def parse_temp_spec(temp_spec: str) -> float:
|
||||
"""解析温度规格为具体温度"""
|
||||
if not temp_spec:
|
||||
return 25.0
|
||||
|
||||
temp_spec = temp_spec.strip().lower()
|
||||
|
||||
# 特殊温度规格
|
||||
special_temps = {
|
||||
"room temperature": 25.0, # 室温
|
||||
"reflux": 78.0, # 默认回流温度
|
||||
"ice bath": 0.0, # 冰浴
|
||||
"boiling": 100.0, # 沸腾
|
||||
"hot": 60.0, # 热
|
||||
"warm": 40.0, # 温热
|
||||
"cold": 10.0, # 冷
|
||||
}
|
||||
|
||||
if temp_spec in special_temps:
|
||||
return special_temps[temp_spec]
|
||||
|
||||
# 解析带单位的温度(如 "256 °C")
|
||||
temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?'
|
||||
match = re.search(temp_pattern, temp_spec)
|
||||
|
||||
if match:
|
||||
return float(match.group(1))
|
||||
|
||||
return 25.0
|
||||
|
||||
def parse_time_spec(time_spec: str) -> float:
|
||||
"""解析时间规格为秒数"""
|
||||
if not time_spec:
|
||||
return 300.0
|
||||
|
||||
time_spec = time_spec.strip().lower()
|
||||
|
||||
# 特殊时间规格
|
||||
special_times = {
|
||||
"overnight": 43200.0, # 12小时
|
||||
"several hours": 10800.0, # 3小时
|
||||
"few hours": 7200.0, # 2小时
|
||||
"long time": 3600.0, # 1小时
|
||||
"short time": 300.0, # 5分钟
|
||||
}
|
||||
|
||||
if time_spec in special_times:
|
||||
return special_times[time_spec]
|
||||
|
||||
# 解析带单位的时间(如 "2 h")
|
||||
time_pattern = r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+)'
|
||||
match = re.search(time_pattern, time_spec)
|
||||
|
||||
if match:
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2).lower()
|
||||
|
||||
unit_multipliers = {
|
||||
's': 1.0,
|
||||
'sec': 1.0,
|
||||
'min': 60.0,
|
||||
'minute': 60.0,
|
||||
'minutes': 60.0,
|
||||
'h': 3600.0,
|
||||
'hr': 3600.0,
|
||||
'hour': 3600.0,
|
||||
'hours': 3600.0,
|
||||
}
|
||||
|
||||
multiplier = unit_multipliers.get(unit, 3600.0)
|
||||
return value * multiplier
|
||||
|
||||
return 300.0
|
||||
|
||||
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
|
||||
"""
|
||||
查找与指定容器相连的加热/冷却设备
|
||||
"""
|
||||
"""查找与指定容器相连的加热/冷却设备"""
|
||||
debug_print(f"查找加热设备,目标容器: {vessel}")
|
||||
|
||||
# 查找所有加热/冷却设备节点
|
||||
heatchill_nodes = [node for node in G.nodes()
|
||||
if (G.nodes[node].get('class') or '') == 'virtual_heatchill']
|
||||
heatchill_nodes = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if 'heatchill' in node_class.lower() or 'virtual_heatchill' in node_class:
|
||||
heatchill_nodes.append(node)
|
||||
debug_print(f"找到加热设备: {node}")
|
||||
|
||||
# 检查哪个加热/冷却设备与目标容器相连(机械连接)
|
||||
for heatchill in heatchill_nodes:
|
||||
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
|
||||
return heatchill
|
||||
if vessel:
|
||||
# 检查哪个加热设备与目标容器相连
|
||||
for heatchill in heatchill_nodes:
|
||||
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
|
||||
debug_print(f"加热设备 '{heatchill}' 与容器 '{vessel}' 相连")
|
||||
return heatchill
|
||||
|
||||
# 如果没有直接连接,返回第一个可用的加热/冷却设备
|
||||
# 如果没有指定容器或没有直接连接,返回第一个可用的加热设备
|
||||
if heatchill_nodes:
|
||||
debug_print(f"使用第一个加热设备: {heatchill_nodes[0]}")
|
||||
return heatchill_nodes[0]
|
||||
|
||||
raise ValueError("系统中未找到可用的加热/冷却设备")
|
||||
|
||||
debug_print("未找到加热设备,使用默认设备")
|
||||
return "heatchill_1"
|
||||
|
||||
def generate_heat_chill_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float,
|
||||
time: float,
|
||||
temp: float = 25.0,
|
||||
time: Union[str, float] = "300", # 🔧 修改:支持字符串时间
|
||||
temp_spec: str = "",
|
||||
time_spec: str = "",
|
||||
pressure: str = "",
|
||||
reflux_solvent: str = "",
|
||||
stir: bool = False,
|
||||
stir_speed: float = 300.0,
|
||||
purpose: str = "加热/冷却操作"
|
||||
purpose: str = "",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成加热/冷却操作的协议序列 - 带时间限制的完整操作
|
||||
生成加热/冷却操作的协议序列 - 支持单位
|
||||
"""
|
||||
|
||||
debug_print("=" * 50)
|
||||
debug_print("开始生成加热冷却协议(支持单位)")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - vessel: {vessel}")
|
||||
debug_print(f" - temp: {temp}°C")
|
||||
debug_print(f" - time: {time} (类型: {type(time)})")
|
||||
debug_print(f" - temp_spec: {temp_spec}")
|
||||
debug_print(f" - time_spec: {time_spec}")
|
||||
debug_print(f" - pressure: {pressure}")
|
||||
debug_print(f" - reflux_solvent: {reflux_solvent}")
|
||||
debug_print(f" - stir: {stir}")
|
||||
debug_print(f" - stir_speed: {stir_speed} RPM")
|
||||
debug_print(f" - purpose: {purpose}")
|
||||
debug_print(f" - 其他参数: {kwargs}")
|
||||
debug_print("=" * 50)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
print(f"HEATCHILL: 开始生成加热/冷却协议")
|
||||
print(f" - 容器: {vessel}")
|
||||
print(f" - 目标温度: {temp}°C")
|
||||
print(f" - 持续时间: {time}秒")
|
||||
print(f" - 使用内置搅拌: {stir}, 速度: {stir_speed} RPM")
|
||||
print(f" - 目的: {purpose}")
|
||||
# === 参数验证 ===
|
||||
debug_print("步骤1: 参数验证...")
|
||||
|
||||
# 验证必需参数
|
||||
if not vessel:
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
# 1. 验证容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 2. 查找加热/冷却设备
|
||||
# === 🔧 新增:单位解析处理 ===
|
||||
debug_print("步骤2: 单位解析处理...")
|
||||
|
||||
# 温度解析:优先使用 temp_spec,然后是 temp
|
||||
final_temp = temp
|
||||
if temp_spec:
|
||||
final_temp = parse_temp_spec(temp_spec)
|
||||
debug_print(f"温度解析: '{temp_spec}' → {final_temp}°C")
|
||||
|
||||
# 时间解析:优先使用 time_spec,然后是 time
|
||||
if time_spec:
|
||||
final_time = parse_time_spec(time_spec) # 使用现有的time_spec解析
|
||||
debug_print(f"时间解析: '{time_spec}' → {final_time}s")
|
||||
else:
|
||||
final_time = parse_time_with_units(time, "s")
|
||||
debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)")
|
||||
|
||||
# 参数范围验证
|
||||
if final_temp < -50.0 or final_temp > 300.0:
|
||||
debug_print(f"温度 {final_temp}°C 超出范围,修正为 25°C")
|
||||
final_temp = 25.0
|
||||
|
||||
if final_time < 0:
|
||||
debug_print(f"时间 {final_time}s 无效,修正为 300s")
|
||||
final_time = 300.0
|
||||
|
||||
if stir_speed < 0 or stir_speed > 1500.0:
|
||||
debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM")
|
||||
stir_speed = 300.0
|
||||
|
||||
debug_print(f"✅ 单位解析和参数验证通过")
|
||||
|
||||
# === 查找加热设备 ===
|
||||
debug_print("步骤3: 查找加热设备...")
|
||||
|
||||
try:
|
||||
heatchill_id = find_connected_heatchill(G, vessel)
|
||||
print(f"HEATCHILL: 找到加热/冷却设备: {heatchill_id}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
|
||||
debug_print(f"设备配置: 加热设备 = {heatchill_id}")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)}")
|
||||
raise ValueError(f"无法找到加热设备: {str(e)}")
|
||||
|
||||
# === 执行加热操作 ===
|
||||
debug_print("步骤4: 执行加热操作...")
|
||||
|
||||
# 3. 执行加热/冷却操作
|
||||
heatchill_action = {
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"temp": temp,
|
||||
"time": time,
|
||||
"stir": stir,
|
||||
"stir_speed": stir_speed,
|
||||
"status": "start"
|
||||
"temp": float(final_temp), # 🔧 确保是浮点数
|
||||
"time": float(final_time), # 🔧 确保是浮点数
|
||||
"stir": bool(stir), # 🔧 确保是布尔值
|
||||
"stir_speed": float(stir_speed), # 🔧 确保是浮点数
|
||||
"purpose": str(purpose or f"加热到 {final_temp}°C") # 🔧 确保是字符串
|
||||
}
|
||||
}
|
||||
|
||||
action_sequence.append(heatchill_action)
|
||||
|
||||
print(f"HEATCHILL: 生成了 {len(action_sequence)} 个动作")
|
||||
# === 总结 ===
|
||||
debug_print("=" * 50)
|
||||
debug_print(f"加热冷却协议生成完成(支持单位)")
|
||||
debug_print(f"总动作数: {len(action_sequence)}")
|
||||
debug_print(f"加热容器: {vessel}")
|
||||
debug_print(f"目标温度: {final_temp}°C")
|
||||
debug_print(f"加热时间: {final_time}s ({final_time/60:.1f}分钟)")
|
||||
if pressure:
|
||||
debug_print(f"压力参数: {pressure} (已接收,不做特殊处理)")
|
||||
if reflux_solvent:
|
||||
debug_print(f"回流溶剂: {reflux_solvent} (已接收,不做特殊处理)")
|
||||
debug_print("=" * 50)
|
||||
|
||||
return action_sequence
|
||||
|
||||
def generate_heat_chill_to_temp_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float = 25.0,
|
||||
time: Union[str, float] = 300.0, # 🔧 也支持字符串
|
||||
temp_spec: str = "",
|
||||
time_spec: str = "",
|
||||
pressure: str = "",
|
||||
reflux_solvent: str = "",
|
||||
stir: bool = False,
|
||||
stir_speed: float = 300.0,
|
||||
purpose: str = "",
|
||||
**kwargs # 🔧 接受额外参数,增强兼容性
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成加热/冷却操作的协议序列
|
||||
|
||||
Args:
|
||||
G: 设备图
|
||||
vessel: 加热容器名称(必需)
|
||||
temp: 目标温度 (°C)
|
||||
time: 加热时间(支持字符串和数字)
|
||||
temp_spec: 温度规格(如 'room temperature', 'reflux')
|
||||
time_spec: 时间规格(如 'overnight', '2 h')
|
||||
pressure: 压力规格(如 '1 mbar'),不做特殊处理
|
||||
reflux_solvent: 回流溶剂名称,不做特殊处理
|
||||
stir: 是否搅拌
|
||||
stir_speed: 搅拌速度 (RPM)
|
||||
purpose: 操作目的
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 加热操作的动作序列
|
||||
"""
|
||||
|
||||
debug_print("=" * 50)
|
||||
debug_print("开始生成加热冷却协议")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - vessel: {vessel}")
|
||||
debug_print(f" - temp: {temp}°C")
|
||||
debug_print(f" - time: {time} (类型: {type(time)})")
|
||||
debug_print(f" - temp_spec: {temp_spec}")
|
||||
debug_print(f" - time_spec: {time_spec}")
|
||||
debug_print(f" - pressure: {pressure}")
|
||||
debug_print(f" - reflux_solvent: {reflux_solvent}")
|
||||
debug_print(f" - stir: {stir}")
|
||||
debug_print(f" - stir_speed: {stir_speed} RPM")
|
||||
debug_print(f" - purpose: {purpose}")
|
||||
debug_print(f" - 其他参数: {kwargs}")
|
||||
debug_print("=" * 50)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
# === 参数验证 ===
|
||||
debug_print("步骤1: 参数验证...")
|
||||
|
||||
# 验证必需参数
|
||||
if not vessel:
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 温度解析:优先使用 temp_spec,然后是 temp
|
||||
final_temp = temp
|
||||
if temp_spec:
|
||||
final_temp = parse_temp_spec(temp_spec)
|
||||
debug_print(f"温度解析: '{temp_spec}' → {final_temp}°C")
|
||||
|
||||
# 🔧 修复:时间解析,支持字符串输入
|
||||
if time_spec:
|
||||
final_time = parse_time_spec(time_spec)
|
||||
debug_print(f"时间解析: '{time_spec}' → {final_time}s ({final_time / 60:.1f}分钟)")
|
||||
else:
|
||||
final_time = parse_time_with_units(time, "s")
|
||||
debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)")
|
||||
|
||||
# 参数范围验证
|
||||
if final_temp < -50.0 or final_temp > 300.0:
|
||||
debug_print(f"温度 {final_temp}°C 超出范围,修正为 25°C")
|
||||
final_temp = 25.0
|
||||
|
||||
if final_time < 0:
|
||||
debug_print(f"时间 {final_time}s 无效,修正为 300s")
|
||||
final_time = 300.0
|
||||
|
||||
if stir_speed < 0 or stir_speed > 1500.0:
|
||||
debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM")
|
||||
stir_speed = 300.0
|
||||
|
||||
debug_print(f"✅ 参数验证通过")
|
||||
|
||||
# === 查找加热设备 ===
|
||||
debug_print("步骤2: 查找加热设备...")
|
||||
|
||||
try:
|
||||
heatchill_id = find_connected_heatchill(G, vessel)
|
||||
debug_print(f"设备配置: 加热设备 = {heatchill_id}")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)}")
|
||||
raise ValueError(f"无法找到加热设备: {str(e)}")
|
||||
|
||||
# === 执行加热操作 ===
|
||||
debug_print("步骤3: 执行加热操作...")
|
||||
|
||||
heatchill_action = {
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"temp": float(final_temp), # 🔧 确保是浮点数
|
||||
"time": float(final_time), # 🔧 确保是浮点数
|
||||
"stir": bool(stir), # 🔧 确保是布尔值
|
||||
"stir_speed": float(stir_speed), # 🔧 确保是浮点数
|
||||
"purpose": str(purpose or f"加热到 {final_temp}°C") # 🔧 确保是字符串
|
||||
}
|
||||
}
|
||||
|
||||
action_sequence.append(heatchill_action)
|
||||
|
||||
# === 总结 ===
|
||||
debug_print("=" * 50)
|
||||
debug_print(f"加热冷却协议生成完成")
|
||||
debug_print(f"总动作数: {len(action_sequence)}")
|
||||
debug_print(f"加热容器: {vessel}")
|
||||
debug_print(f"目标温度: {final_temp}°C")
|
||||
debug_print(f"加热时间: {final_time}s ({final_time / 60:.1f}分钟)")
|
||||
if pressure:
|
||||
debug_print(f"压力参数: {pressure} (已接收,不做特殊处理)")
|
||||
if reflux_solvent:
|
||||
debug_print(f"回流溶剂: {reflux_solvent} (已接收,不做特殊处理)")
|
||||
debug_print("=" * 50)
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
def generate_heat_chill_start_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float,
|
||||
purpose: str = "开始加热/冷却"
|
||||
temp: float = 25.0,
|
||||
purpose: str = "",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成开始加热/冷却操作的协议序列
|
||||
"""
|
||||
"""生成开始加热操作的协议序列"""
|
||||
|
||||
debug_print("=" * 50)
|
||||
debug_print("开始生成启动加热协议")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - vessel: {vessel}")
|
||||
debug_print(f" - temp: {temp}°C")
|
||||
debug_print(f" - purpose: {purpose}")
|
||||
debug_print("=" * 50)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
print(f"HEATCHILL_START: 开始生成加热/冷却启动协议")
|
||||
print(f" - 容器: {vessel}")
|
||||
print(f" - 目标温度: {temp}°C")
|
||||
print(f" - 目的: {purpose}")
|
||||
# 验证参数
|
||||
if not vessel:
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
# 1. 验证容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 2. 查找加热/冷却设备
|
||||
# 查找加热设备
|
||||
try:
|
||||
heatchill_id = find_connected_heatchill(G, vessel)
|
||||
print(f"HEATCHILL_START: 找到加热/冷却设备: {heatchill_id}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
|
||||
debug_print(f"设备配置: 加热设备 = {heatchill_id}")
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)}")
|
||||
raise ValueError(f"无法找到加热设备: {str(e)}")
|
||||
|
||||
# 3. 执行开始加热/冷却操作
|
||||
heatchill_start_action = {
|
||||
# 执行开始加热操作
|
||||
start_action = {
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"temp": temp,
|
||||
"purpose": purpose
|
||||
"purpose": purpose or f"开始加热到 {temp}°C"
|
||||
}
|
||||
}
|
||||
|
||||
action_sequence.append(heatchill_start_action)
|
||||
action_sequence.append(start_action)
|
||||
|
||||
print(f"HEATCHILL_START: 生成了 {len(action_sequence)} 个动作")
|
||||
debug_print(f"启动加热协议生成完成,动作数: {len(action_sequence)}")
|
||||
return action_sequence
|
||||
|
||||
|
||||
def generate_heat_chill_stop_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str
|
||||
vessel: str,
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成停止加热/冷却操作的协议序列
|
||||
"""
|
||||
"""生成停止加热操作的协议序列"""
|
||||
|
||||
debug_print("=" * 50)
|
||||
debug_print("开始生成停止加热协议")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - vessel: {vessel}")
|
||||
debug_print("=" * 50)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
print(f"HEATCHILL_STOP: 开始生成加热/冷却停止协议")
|
||||
print(f" - 容器: {vessel}")
|
||||
# 验证参数
|
||||
if not vessel:
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
# 1. 验证容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 2. 查找加热/冷却设备
|
||||
# 查找加热设备
|
||||
try:
|
||||
heatchill_id = find_connected_heatchill(G, vessel)
|
||||
print(f"HEATCHILL_STOP: 找到加热/冷却设备: {heatchill_id}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
|
||||
debug_print(f"设备配置: 加热设备 = {heatchill_id}")
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)}")
|
||||
raise ValueError(f"无法找到加热设备: {str(e)}")
|
||||
|
||||
# 3. 执行停止加热/冷却操作
|
||||
heatchill_stop_action = {
|
||||
# 执行停止加热操作
|
||||
stop_action = {
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
@@ -150,224 +552,16 @@ def generate_heat_chill_stop_protocol(
|
||||
}
|
||||
}
|
||||
|
||||
action_sequence.append(heatchill_stop_action)
|
||||
action_sequence.append(stop_action)
|
||||
|
||||
print(f"HEATCHILL_STOP: 生成了 {len(action_sequence)} 个动作")
|
||||
debug_print(f"停止加热协议生成完成,动作数: {len(action_sequence)}")
|
||||
return action_sequence
|
||||
|
||||
|
||||
def generate_heat_chill_to_temp_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float,
|
||||
active: bool = True,
|
||||
continue_heatchill: bool = False,
|
||||
stir: bool = False,
|
||||
stir_speed: Optional[float] = None,
|
||||
purpose: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成加热/冷却到指定温度的协议序列 - 智能温控协议
|
||||
|
||||
**关键修复**: 学习 pump_protocol 的模式,直接使用设备基础动作,不依赖特定的 Action 文件
|
||||
"""
|
||||
action_sequence = []
|
||||
|
||||
# 设置默认值
|
||||
if stir_speed is None:
|
||||
stir_speed = 300.0
|
||||
if purpose is None:
|
||||
purpose = f"智能温控到 {temp}°C"
|
||||
|
||||
print(f"HEATCHILL_TO_TEMP: 开始生成智能温控协议")
|
||||
print(f" - 容器: {vessel}")
|
||||
print(f" - 目标温度: {temp}°C")
|
||||
print(f" - 主动控温: {active}")
|
||||
print(f" - 达到温度后继续: {continue_heatchill}")
|
||||
print(f" - 搅拌: {stir}, 速度: {stir_speed} RPM")
|
||||
print(f" - 目的: {purpose}")
|
||||
|
||||
# 1. 验证容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 2. 查找加热/冷却设备
|
||||
try:
|
||||
heatchill_id = find_connected_heatchill(G, vessel)
|
||||
print(f"HEATCHILL_TO_TEMP: 找到加热/冷却设备: {heatchill_id}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
|
||||
|
||||
# 3. 根据参数选择合适的基础动作组合 (学习 pump_protocol 的模式)
|
||||
if not active:
|
||||
print(f"HEATCHILL_TO_TEMP: 非主动模式,仅等待")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 10.0,
|
||||
"purpose": f"等待容器 {vessel} 自然达到 {temp}°C"
|
||||
}
|
||||
})
|
||||
else:
|
||||
if continue_heatchill:
|
||||
# 持续模式:使用 heat_chill_start 基础动作
|
||||
print(f"HEATCHILL_TO_TEMP: 使用持续温控模式")
|
||||
action_sequence.append({
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_start", # ← 直接使用设备基础动作
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"temp": temp,
|
||||
"purpose": f"{purpose} (持续保温)"
|
||||
}
|
||||
})
|
||||
else:
|
||||
# 一次性模式:使用 heat_chill 基础动作
|
||||
print(f"HEATCHILL_TO_TEMP: 使用一次性温控模式")
|
||||
estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0))
|
||||
print(f"HEATCHILL_TO_TEMP: 估算所需时间: {estimated_time}秒")
|
||||
|
||||
action_sequence.append({
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill", # ← 直接使用设备基础动作
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"temp": temp,
|
||||
"time": estimated_time,
|
||||
"stir": stir,
|
||||
"stir_speed": stir_speed,
|
||||
"status": "start"
|
||||
}
|
||||
})
|
||||
|
||||
print(f"HEATCHILL_TO_TEMP: 生成了 {len(action_sequence)} 个动作")
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 扩展版本的加热/冷却协议,集成智能温控功能
|
||||
def generate_smart_heat_chill_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float,
|
||||
time: float = 0.0, # 0表示自动估算
|
||||
active: bool = True,
|
||||
continue_heatchill: bool = False,
|
||||
stir: bool = False,
|
||||
stir_speed: float = 300.0,
|
||||
purpose: str = "智能加热/冷却"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
这个函数集成了 generate_heat_chill_to_temp_protocol 的智能逻辑,
|
||||
但使用现有的 Action 类型
|
||||
"""
|
||||
# 如果时间为0,自动估算
|
||||
if time == 0.0:
|
||||
estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0))
|
||||
time = estimated_time
|
||||
|
||||
if continue_heatchill:
|
||||
# 使用持续模式
|
||||
return generate_heat_chill_start_protocol(G, vessel, temp, purpose)
|
||||
else:
|
||||
# 使用定时模式
|
||||
return generate_heat_chill_protocol(G, vessel, temp, time, stir, stir_speed, purpose)
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def generate_heating_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float,
|
||||
time: float = 300.0,
|
||||
stir: bool = True,
|
||||
stir_speed: float = 300.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""生成加热协议的便捷函数"""
|
||||
return generate_heat_chill_protocol(
|
||||
G=G, vessel=vessel, temp=temp, time=time,
|
||||
stir=stir, stir_speed=stir_speed, purpose=f"加热到 {temp}°C"
|
||||
)
|
||||
|
||||
|
||||
def generate_cooling_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float,
|
||||
time: float = 600.0,
|
||||
stir: bool = True,
|
||||
stir_speed: float = 200.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""生成冷却协议的便捷函数"""
|
||||
return generate_heat_chill_protocol(
|
||||
G=G, vessel=vessel, temp=temp, time=time,
|
||||
stir=stir, stir_speed=stir_speed, purpose=f"冷却到 {temp}°C"
|
||||
)
|
||||
|
||||
|
||||
# # 温度预设快捷函数
|
||||
# def generate_room_temp_protocol(
|
||||
# G: nx.DiGraph,
|
||||
# vessel: str,
|
||||
# stir: bool = False
|
||||
# ) -> List[Dict[str, Any]]:
|
||||
# """返回室温的快捷函数"""
|
||||
# return generate_heat_chill_to_temp_protocol(
|
||||
# G=G,
|
||||
# vessel=vessel,
|
||||
# temp=25.0,
|
||||
# active=True,
|
||||
# continue_heatchill=False,
|
||||
# stir=stir,
|
||||
# purpose="冷却到室温"
|
||||
# )
|
||||
|
||||
|
||||
# def generate_reflux_heating_protocol(
|
||||
# G: nx.DiGraph,
|
||||
# vessel: str,
|
||||
# temp: float,
|
||||
# time: float = 3600.0 # 1小时回流
|
||||
# ) -> List[Dict[str, Any]]:
|
||||
# """回流加热的快捷函数"""
|
||||
# return generate_heat_chill_protocol(
|
||||
# G=G,
|
||||
# vessel=vessel,
|
||||
# temp=temp,
|
||||
# time=time,
|
||||
# stir=True,
|
||||
# stir_speed=400.0, # 回流时较快搅拌
|
||||
# purpose=f"回流加热到 {temp}°C"
|
||||
# )
|
||||
|
||||
|
||||
# def generate_ice_bath_protocol(
|
||||
# G: nx.DiGraph,
|
||||
# vessel: str,
|
||||
# time: float = 600.0 # 10分钟冰浴
|
||||
# ) -> List[Dict[str, Any]]:
|
||||
# """冰浴冷却的快捷函数"""
|
||||
# return generate_heat_chill_protocol(
|
||||
# G=G,
|
||||
# vessel=vessel,
|
||||
# temp=0.0,
|
||||
# time=time,
|
||||
# stir=True,
|
||||
# stir_speed=150.0, # 冰浴时缓慢搅拌
|
||||
# purpose="冰浴冷却到 0°C"
|
||||
# )
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_heatchill_protocol():
|
||||
"""测试加热/冷却协议的示例"""
|
||||
print("=== HEAT CHILL PROTOCOL 测试 ===")
|
||||
print("完整的四个协议函数:")
|
||||
print("1. generate_heat_chill_protocol - 带时间限制的完整操作")
|
||||
print("2. generate_heat_chill_start_protocol - 持续加热/冷却")
|
||||
print("3. generate_heat_chill_stop_protocol - 停止加热/冷却")
|
||||
print("4. generate_heat_chill_to_temp_protocol - 智能温控 (您的 HeatChillToTemp)")
|
||||
print("测试完成")
|
||||
|
||||
"""测试加热协议"""
|
||||
debug_print("=== HEATCHILL PROTOCOL 测试 ===")
|
||||
debug_print("✅ 测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_heatchill_protocol()
|
||||
366
unilabos/compile/hydrogenate_protocol.py
Normal file
366
unilabos/compile/hydrogenate_protocol.py
Normal file
@@ -0,0 +1,366 @@
|
||||
import networkx as nx
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
def parse_temperature(temp_str: str) -> float:
|
||||
"""
|
||||
解析温度字符串,支持多种格式
|
||||
|
||||
Args:
|
||||
temp_str: 温度字符串(如 "45 °C", "45°C", "45")
|
||||
|
||||
Returns:
|
||||
float: 温度值(摄氏度)
|
||||
"""
|
||||
try:
|
||||
# 移除常见的温度单位和符号
|
||||
temp_clean = temp_str.replace("°C", "").replace("°", "").replace("C", "").strip()
|
||||
return float(temp_clean)
|
||||
except ValueError:
|
||||
print(f"HYDROGENATE: 无法解析温度 '{temp_str}',使用默认温度 25°C")
|
||||
return 25.0
|
||||
|
||||
|
||||
def parse_time(time_str: str) -> float:
|
||||
"""
|
||||
解析时间字符串,支持多种格式
|
||||
|
||||
Args:
|
||||
time_str: 时间字符串(如 "2 h", "120 min", "7200 s")
|
||||
|
||||
Returns:
|
||||
float: 时间值(秒)
|
||||
"""
|
||||
try:
|
||||
time_clean = time_str.lower().strip()
|
||||
|
||||
# 处理小时
|
||||
if "h" in time_clean:
|
||||
hours = float(time_clean.replace("h", "").strip())
|
||||
return hours * 3600.0
|
||||
|
||||
# 处理分钟
|
||||
if "min" in time_clean:
|
||||
minutes = float(time_clean.replace("min", "").strip())
|
||||
return minutes * 60.0
|
||||
|
||||
# 处理秒
|
||||
if "s" in time_clean:
|
||||
seconds = float(time_clean.replace("s", "").strip())
|
||||
return seconds
|
||||
|
||||
# 默认按小时处理
|
||||
return float(time_clean) * 3600.0
|
||||
|
||||
except ValueError:
|
||||
print(f"HYDROGENATE: 无法解析时间 '{time_str}',使用默认时间 2小时")
|
||||
return 7200.0 # 2小时
|
||||
|
||||
|
||||
def find_associated_solenoid_valve(G: nx.DiGraph, device_id: str) -> Optional[str]:
|
||||
"""查找与指定设备相关联的电磁阀"""
|
||||
solenoid_valves = [
|
||||
node for node in G.nodes()
|
||||
if ('solenoid' in (G.nodes[node].get('class') or '').lower()
|
||||
or 'solenoid_valve' in node)
|
||||
]
|
||||
|
||||
# 通过网络连接查找直接相连的电磁阀
|
||||
for solenoid in solenoid_valves:
|
||||
if G.has_edge(device_id, solenoid) or G.has_edge(solenoid, device_id):
|
||||
return solenoid
|
||||
|
||||
# 通过命名规则查找关联的电磁阀
|
||||
device_type = ""
|
||||
if 'gas' in device_id.lower():
|
||||
device_type = "gas"
|
||||
elif 'h2' in device_id.lower() or 'hydrogen' in device_id.lower():
|
||||
device_type = "gas"
|
||||
|
||||
if device_type:
|
||||
for solenoid in solenoid_valves:
|
||||
if device_type in solenoid.lower():
|
||||
return solenoid
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_connected_device(G: nx.DiGraph, vessel: str, device_type: str) -> str:
|
||||
"""
|
||||
查找与容器相连的指定类型设备
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
vessel: 容器名称
|
||||
device_type: 设备类型 ('heater', 'stirrer', 'gas_source')
|
||||
|
||||
Returns:
|
||||
str: 设备ID,如果没有则返回None
|
||||
"""
|
||||
print(f"HYDROGENATE: 正在查找与容器 '{vessel}' 相连的 {device_type}...")
|
||||
|
||||
# 根据设备类型定义搜索关键词
|
||||
if device_type == 'heater':
|
||||
keywords = ['heater', 'heat', 'heatchill']
|
||||
device_class = 'virtual_heatchill'
|
||||
elif device_type == 'stirrer':
|
||||
keywords = ['stirrer', 'stir']
|
||||
device_class = 'virtual_stirrer'
|
||||
elif device_type == 'gas_source':
|
||||
keywords = ['gas', 'h2', 'hydrogen']
|
||||
device_class = 'virtual_gas_source'
|
||||
else:
|
||||
return None
|
||||
|
||||
# 查找设备节点
|
||||
device_nodes = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_name = node.lower()
|
||||
node_class = node_data.get('class', '').lower()
|
||||
|
||||
# 通过名称匹配
|
||||
if any(keyword in node_name for keyword in keywords):
|
||||
device_nodes.append(node)
|
||||
# 通过类型匹配
|
||||
elif device_class in node_class:
|
||||
device_nodes.append(node)
|
||||
|
||||
print(f"HYDROGENATE: 找到的{device_type}节点: {device_nodes}")
|
||||
|
||||
# 检查是否有设备与目标容器相连
|
||||
for device in device_nodes:
|
||||
if G.has_edge(device, vessel) or G.has_edge(vessel, device):
|
||||
print(f"HYDROGENATE: 找到与容器 '{vessel}' 相连的{device_type}: {device}")
|
||||
return device
|
||||
|
||||
# 如果没有直接连接,查找距离最近的设备
|
||||
for device in device_nodes:
|
||||
try:
|
||||
path = nx.shortest_path(G, source=device, target=vessel)
|
||||
if len(path) <= 3: # 最多2个中间节点
|
||||
print(f"HYDROGENATE: 找到距离较近的{device_type}: {device}")
|
||||
return device
|
||||
except nx.NetworkXNoPath:
|
||||
continue
|
||||
|
||||
print(f"HYDROGENATE: 未找到与容器 '{vessel}' 相连的{device_type}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_hydrogenate_protocol(
|
||||
G: nx.DiGraph,
|
||||
temp: str,
|
||||
time: str,
|
||||
vessel: str,
|
||||
**kwargs # 接收其他可能的参数但不使用
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成氢化反应协议序列
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为容器和设备
|
||||
temp: 反应温度(如 "45 °C")
|
||||
time: 反应时间(如 "2 h")
|
||||
vessel: 反应容器
|
||||
**kwargs: 其他可选参数,但不使用
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
action_sequence = []
|
||||
|
||||
# 解析参数
|
||||
temperature = parse_temperature(temp)
|
||||
reaction_time = parse_time(time)
|
||||
|
||||
print(f"HYDROGENATE: 开始生成氢化反应协议")
|
||||
print(f" - 反应温度: {temperature}°C")
|
||||
print(f" - 反应时间: {reaction_time/3600:.1f} 小时")
|
||||
print(f" - 反应容器: {vessel}")
|
||||
|
||||
# 1. 验证目标容器存在
|
||||
if vessel not in G.nodes():
|
||||
print(f"HYDROGENATE: 警告 - 容器 '{vessel}' 不存在于系统中,跳过氢化反应")
|
||||
return action_sequence
|
||||
|
||||
# 2. 查找相连的设备
|
||||
heater_id = find_connected_device(G, vessel, 'heater')
|
||||
stirrer_id = find_connected_device(G, vessel, 'stirrer')
|
||||
gas_source_id = find_connected_device(G, vessel, 'gas_source')
|
||||
|
||||
# 3. 启动搅拌器
|
||||
if stirrer_id:
|
||||
print(f"HYDROGENATE: 启动搅拌器 {stirrer_id}")
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"stir_speed": 300.0,
|
||||
"purpose": "氢化反应: 开始搅拌"
|
||||
}
|
||||
})
|
||||
else:
|
||||
print(f"HYDROGENATE: 警告 - 未找到搅拌器,继续执行")
|
||||
|
||||
# 4. 启动气源(氢气)- 修复版本
|
||||
if gas_source_id:
|
||||
print(f"HYDROGENATE: 启动气源 {gas_source_id} (氢气)")
|
||||
action_sequence.append({
|
||||
"device_id": gas_source_id,
|
||||
"action_name": "set_status", # 修改为 set_status
|
||||
"action_kwargs": {
|
||||
"string": "ON" # 修改参数格式
|
||||
}
|
||||
})
|
||||
|
||||
# 查找相关的电磁阀
|
||||
gas_solenoid = find_associated_solenoid_valve(G, gas_source_id)
|
||||
if gas_solenoid:
|
||||
print(f"HYDROGENATE: 开启气源电磁阀 {gas_solenoid}")
|
||||
action_sequence.append({
|
||||
"device_id": gas_solenoid,
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": "OPEN"
|
||||
}
|
||||
})
|
||||
else:
|
||||
print(f"HYDROGENATE: 警告 - 未找到气源,继续执行")
|
||||
|
||||
# 5. 等待气体稳定
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 30.0,
|
||||
"description": "等待氢气环境稳定"
|
||||
}
|
||||
})
|
||||
|
||||
# 6. 启动加热器
|
||||
if heater_id:
|
||||
print(f"HYDROGENATE: 启动加热器 {heater_id} 到 {temperature}°C")
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"temp": temperature,
|
||||
"purpose": f"氢化反应: 加热到 {temperature}°C"
|
||||
}
|
||||
})
|
||||
|
||||
# 等待温度稳定
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 120.0,
|
||||
"description": f"等待温度稳定到 {temperature}°C"
|
||||
}
|
||||
})
|
||||
|
||||
# 保持反应温度
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"temp": temperature,
|
||||
"time": reaction_time,
|
||||
"purpose": f"氢化反应: 保持 {temperature}°C,反应 {reaction_time/3600:.1f} 小时"
|
||||
}
|
||||
})
|
||||
else:
|
||||
print(f"HYDROGENATE: 警告 - 未找到加热器,使用室温反应")
|
||||
# 室温反应,只等待时间
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": reaction_time,
|
||||
"description": f"室温氢化反应 {reaction_time/3600:.1f} 小时"
|
||||
}
|
||||
})
|
||||
|
||||
# 7. 停止加热
|
||||
if heater_id:
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"purpose": "氢化反应完成,停止加热"
|
||||
}
|
||||
})
|
||||
|
||||
# 8. 等待冷却
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 300.0,
|
||||
"description": "等待反应混合物冷却"
|
||||
}
|
||||
})
|
||||
|
||||
# 9. 停止气源 - 修复版本
|
||||
if gas_source_id:
|
||||
# 先关闭电磁阀
|
||||
gas_solenoid = find_associated_solenoid_valve(G, gas_source_id)
|
||||
if gas_solenoid:
|
||||
print(f"HYDROGENATE: 关闭气源电磁阀 {gas_solenoid}")
|
||||
action_sequence.append({
|
||||
"device_id": gas_solenoid,
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": "CLOSED"
|
||||
}
|
||||
})
|
||||
|
||||
# 再关闭气源
|
||||
action_sequence.append({
|
||||
"device_id": gas_source_id,
|
||||
"action_name": "set_status", # 修改为 set_status
|
||||
"action_kwargs": {
|
||||
"string": "OFF" # 修改参数格式
|
||||
}
|
||||
})
|
||||
|
||||
# 10. 停止搅拌
|
||||
if stirrer_id:
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stop_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"purpose": "氢化反应完成,停止搅拌"
|
||||
}
|
||||
})
|
||||
|
||||
print(f"HYDROGENATE: 协议生成完成,共 {len(action_sequence)} 个动作")
|
||||
print(f"HYDROGENATE: 预计总时间: {(reaction_time + 450)/3600:.1f} 小时")
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_hydrogenate_protocol():
|
||||
"""测试氢化反应协议"""
|
||||
print("=== HYDROGENATE PROTOCOL 测试 ===")
|
||||
|
||||
# 测试温度解析
|
||||
test_temps = ["45 °C", "45°C", "45", "25 C", "invalid"]
|
||||
for temp in test_temps:
|
||||
parsed = parse_temperature(temp)
|
||||
print(f"温度 '{temp}' -> {parsed}°C")
|
||||
|
||||
# 测试时间解析
|
||||
test_times = ["2 h", "120 min", "7200 s", "2", "invalid"]
|
||||
for time in test_times:
|
||||
parsed = parse_time(time)
|
||||
print(f"时间 '{time}' -> {parsed/3600:.1f} 小时")
|
||||
|
||||
print("测试完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_hydrogenate_protocol()
|
||||
File diff suppressed because it is too large
Load Diff
358
unilabos/compile/recrystallize_protocol.py
Normal file
358
unilabos/compile/recrystallize_protocol.py
Normal file
@@ -0,0 +1,358 @@
|
||||
import networkx as nx
|
||||
import re
|
||||
from typing import List, Dict, Any, Tuple, Union
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
|
||||
def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
|
||||
"""
|
||||
解析带单位的体积输入
|
||||
|
||||
Args:
|
||||
volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0)
|
||||
default_unit: 默认单位(默认为毫升)
|
||||
|
||||
Returns:
|
||||
float: 体积(毫升)
|
||||
"""
|
||||
if not volume_input:
|
||||
return 0.0
|
||||
|
||||
# 处理数值输入
|
||||
if isinstance(volume_input, (int, float)):
|
||||
result = float(volume_input)
|
||||
print(f"RECRYSTALLIZE: 数值体积输入: {volume_input} → {result}mL(默认单位)")
|
||||
return result
|
||||
|
||||
# 处理字符串输入
|
||||
volume_str = str(volume_input).lower().strip()
|
||||
print(f"RECRYSTALLIZE: 解析体积字符串: '{volume_str}'")
|
||||
|
||||
# 处理特殊值
|
||||
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
||||
default_volume = 50.0 # 50mL默认值
|
||||
print(f"RECRYSTALLIZE: 检测到未知体积,使用默认值: {default_volume}mL")
|
||||
return default_volume
|
||||
|
||||
# 如果是纯数字,使用默认单位
|
||||
try:
|
||||
value = float(volume_str)
|
||||
if default_unit.lower() in ["ml", "milliliter"]:
|
||||
result = value
|
||||
elif default_unit.lower() in ["l", "liter"]:
|
||||
result = value * 1000.0
|
||||
elif default_unit.lower() in ["μl", "ul", "microliter"]:
|
||||
result = value / 1000.0
|
||||
else:
|
||||
result = value # 默认mL
|
||||
print(f"RECRYSTALLIZE: 纯数字输入: {volume_str} → {result}mL(单位: {default_unit})")
|
||||
return result
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 移除空格并提取数字和单位
|
||||
volume_clean = re.sub(r'\s+', '', volume_str)
|
||||
|
||||
# 匹配数字和单位的正则表达式
|
||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
|
||||
|
||||
if not match:
|
||||
print(f"RECRYSTALLIZE: ⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL")
|
||||
return 50.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or default_unit.lower()
|
||||
|
||||
# 转换为毫升
|
||||
if unit in ['l', 'liter']:
|
||||
volume = value * 1000.0 # L -> mL
|
||||
elif unit in ['μl', 'ul', 'microliter']:
|
||||
volume = value / 1000.0 # μL -> mL
|
||||
else: # ml, milliliter 或默认
|
||||
volume = value # 已经是mL
|
||||
|
||||
print(f"RECRYSTALLIZE: 体积解析: '{volume_str}' → {value} {unit} → {volume}mL")
|
||||
return volume
|
||||
|
||||
|
||||
def parse_ratio(ratio_str: str) -> Tuple[float, float]:
|
||||
"""
|
||||
解析比例字符串,支持多种格式
|
||||
|
||||
Args:
|
||||
ratio_str: 比例字符串(如 "1:1", "3:7", "50:50")
|
||||
|
||||
Returns:
|
||||
Tuple[float, float]: 比例元组 (ratio1, ratio2)
|
||||
"""
|
||||
try:
|
||||
# 处理 "1:1", "3:7", "50:50" 等格式
|
||||
if ":" in ratio_str:
|
||||
parts = ratio_str.split(":")
|
||||
if len(parts) == 2:
|
||||
ratio1 = float(parts[0])
|
||||
ratio2 = float(parts[1])
|
||||
return ratio1, ratio2
|
||||
|
||||
# 处理 "1-1", "3-7" 等格式
|
||||
if "-" in ratio_str:
|
||||
parts = ratio_str.split("-")
|
||||
if len(parts) == 2:
|
||||
ratio1 = float(parts[0])
|
||||
ratio2 = float(parts[1])
|
||||
return ratio1, ratio2
|
||||
|
||||
# 处理 "1,1", "3,7" 等格式
|
||||
if "," in ratio_str:
|
||||
parts = ratio_str.split(",")
|
||||
if len(parts) == 2:
|
||||
ratio1 = float(parts[0])
|
||||
ratio2 = float(parts[1])
|
||||
return ratio1, ratio2
|
||||
|
||||
# 默认 1:1
|
||||
print(f"RECRYSTALLIZE: 无法解析比例 '{ratio_str}',使用默认比例 1:1")
|
||||
return 1.0, 1.0
|
||||
|
||||
except ValueError:
|
||||
print(f"RECRYSTALLIZE: 比例解析错误 '{ratio_str}',使用默认比例 1:1")
|
||||
return 1.0, 1.0
|
||||
|
||||
|
||||
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
|
||||
"""
|
||||
查找溶剂容器
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
solvent: 溶剂名称
|
||||
|
||||
Returns:
|
||||
str: 溶剂容器ID
|
||||
"""
|
||||
print(f"RECRYSTALLIZE: 正在查找溶剂 '{solvent}' 的容器...")
|
||||
|
||||
# 构建可能的容器名称
|
||||
possible_names = [
|
||||
f"flask_{solvent}",
|
||||
f"bottle_{solvent}",
|
||||
f"reagent_{solvent}",
|
||||
f"reagent_bottle_{solvent}",
|
||||
f"{solvent}_flask",
|
||||
f"{solvent}_bottle",
|
||||
f"{solvent}",
|
||||
f"vessel_{solvent}",
|
||||
]
|
||||
|
||||
# 第一步:通过容器名称匹配
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
print(f"RECRYSTALLIZE: 通过名称匹配找到容器: {vessel_name}")
|
||||
return vessel_name
|
||||
|
||||
# 第二步:通过模糊匹配
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
node_name = G.nodes[node_id].get('name', '').lower()
|
||||
|
||||
if solvent.lower() in node_id.lower() or solvent.lower() in node_name:
|
||||
print(f"RECRYSTALLIZE: 通过模糊匹配找到容器: {node_id}")
|
||||
return node_id
|
||||
|
||||
# 第三步:通过液体类型匹配
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
vessel_data = G.nodes[node_id].get('data', {})
|
||||
liquids = vessel_data.get('liquid', [])
|
||||
|
||||
for liquid in liquids:
|
||||
if isinstance(liquid, dict):
|
||||
liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower()
|
||||
reagent_name = vessel_data.get('reagent_name', '').lower()
|
||||
|
||||
if solvent.lower() in liquid_type or solvent.lower() in reagent_name:
|
||||
print(f"RECRYSTALLIZE: 通过液体类型匹配找到容器: {node_id}")
|
||||
return node_id
|
||||
|
||||
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
|
||||
|
||||
|
||||
def generate_recrystallize_protocol(
|
||||
G: nx.DiGraph,
|
||||
ratio: str,
|
||||
solvent1: str,
|
||||
solvent2: str,
|
||||
vessel: str,
|
||||
volume: Union[str, float], # 🔧 修改:支持字符串和数值
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成重结晶协议序列 - 支持单位
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为容器和设备
|
||||
ratio: 溶剂比例(如 "1:1", "3:7")
|
||||
solvent1: 第一种溶剂名称
|
||||
solvent2: 第二种溶剂名称
|
||||
vessel: 目标容器
|
||||
volume: 总体积(支持 "100 mL", "50", "2.5 L" 等)
|
||||
**kwargs: 其他可选参数
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
action_sequence = []
|
||||
|
||||
print(f"RECRYSTALLIZE: 开始生成重结晶协议(支持单位)")
|
||||
print(f" - 比例: {ratio}")
|
||||
print(f" - 溶剂1: {solvent1}")
|
||||
print(f" - 溶剂2: {solvent2}")
|
||||
print(f" - 容器: {vessel}")
|
||||
print(f" - 总体积: {volume} (类型: {type(volume)})")
|
||||
|
||||
# 1. 验证目标容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 2. 🔧 新增:解析体积(支持单位)
|
||||
final_volume = parse_volume_with_units(volume, "mL")
|
||||
print(f"RECRYSTALLIZE: 解析体积: {volume} → {final_volume}mL")
|
||||
|
||||
# 3. 解析比例
|
||||
ratio1, ratio2 = parse_ratio(ratio)
|
||||
total_ratio = ratio1 + ratio2
|
||||
|
||||
# 4. 计算各溶剂体积
|
||||
volume1 = final_volume * (ratio1 / total_ratio)
|
||||
volume2 = final_volume * (ratio2 / total_ratio)
|
||||
|
||||
print(f"RECRYSTALLIZE: 解析比例: {ratio1}:{ratio2}")
|
||||
print(f"RECRYSTALLIZE: {solvent1} 体积: {volume1:.2f} mL")
|
||||
print(f"RECRYSTALLIZE: {solvent2} 体积: {volume2:.2f} mL")
|
||||
|
||||
# 5. 查找溶剂容器
|
||||
try:
|
||||
solvent1_vessel = find_solvent_vessel(G, solvent1)
|
||||
print(f"RECRYSTALLIZE: 找到溶剂1容器: {solvent1_vessel}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到溶剂1 '{solvent1}': {str(e)}")
|
||||
|
||||
try:
|
||||
solvent2_vessel = find_solvent_vessel(G, solvent2)
|
||||
print(f"RECRYSTALLIZE: 找到溶剂2容器: {solvent2_vessel}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到溶剂2 '{solvent2}': {str(e)}")
|
||||
|
||||
# 6. 验证路径存在
|
||||
try:
|
||||
path1 = nx.shortest_path(G, source=solvent1_vessel, target=vessel)
|
||||
print(f"RECRYSTALLIZE: 溶剂1路径: {' → '.join(path1)}")
|
||||
except nx.NetworkXNoPath:
|
||||
raise ValueError(f"从溶剂1容器 '{solvent1_vessel}' 到目标容器 '{vessel}' 没有可用路径")
|
||||
|
||||
try:
|
||||
path2 = nx.shortest_path(G, source=solvent2_vessel, target=vessel)
|
||||
print(f"RECRYSTALLIZE: 溶剂2路径: {' → '.join(path2)}")
|
||||
except nx.NetworkXNoPath:
|
||||
raise ValueError(f"从溶剂2容器 '{solvent2_vessel}' 到目标容器 '{vessel}' 没有可用路径")
|
||||
|
||||
# 7. 添加第一种溶剂
|
||||
print(f"RECRYSTALLIZE: 开始添加溶剂1 {volume1:.2f} mL")
|
||||
|
||||
try:
|
||||
pump_actions1 = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent1_vessel,
|
||||
to_vessel=vessel,
|
||||
volume=volume1, # 使用解析后的体积
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # 重结晶不需要清洗
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.0, # 正常流速
|
||||
transfer_flowrate=0.5
|
||||
)
|
||||
|
||||
action_sequence.extend(pump_actions1)
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"生成溶剂1泵协议时出错: {str(e)}")
|
||||
|
||||
# 8. 等待溶剂1稳定
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 10.0,
|
||||
"description": f"等待溶剂1 {solvent1} 稳定"
|
||||
}
|
||||
})
|
||||
|
||||
# 9. 添加第二种溶剂
|
||||
print(f"RECRYSTALLIZE: 开始添加溶剂2 {volume2:.2f} mL")
|
||||
|
||||
try:
|
||||
pump_actions2 = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent2_vessel,
|
||||
to_vessel=vessel,
|
||||
volume=volume2, # 使用解析后的体积
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # 重结晶不需要清洗
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.0, # 正常流速
|
||||
transfer_flowrate=0.5
|
||||
)
|
||||
|
||||
action_sequence.extend(pump_actions2)
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"生成溶剂2泵协议时出错: {str(e)}")
|
||||
|
||||
# 10. 等待溶剂2稳定
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 10.0,
|
||||
"description": f"等待溶剂2 {solvent2} 稳定"
|
||||
}
|
||||
})
|
||||
|
||||
# 11. 等待重结晶完成
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 600.0, # 等待10分钟进行重结晶
|
||||
"description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio},总体积 {final_volume}mL)"
|
||||
}
|
||||
})
|
||||
|
||||
print(f"RECRYSTALLIZE: 协议生成完成,共 {len(action_sequence)} 个动作")
|
||||
print(f"RECRYSTALLIZE: 预计总时间: {620/60:.1f} 分钟")
|
||||
print(f"RECRYSTALLIZE: 总体积: {final_volume}mL")
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_recrystallize_protocol():
|
||||
"""测试重结晶协议"""
|
||||
print("=== RECRYSTALLIZE PROTOCOL 测试 ===")
|
||||
|
||||
# 测试比例解析
|
||||
test_ratios = ["1:1", "3:7", "50:50", "1-1", "2,8", "invalid"]
|
||||
for ratio in test_ratios:
|
||||
r1, r2 = parse_ratio(ratio)
|
||||
print(f"比例 '{ratio}' -> {r1}:{r2}")
|
||||
|
||||
print("测试完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_recrystallize_protocol()
|
||||
180
unilabos/compile/reset_handling_protocol.py
Normal file
180
unilabos/compile/reset_handling_protocol.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import networkx as nx
|
||||
from typing import List, Dict, Any
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
|
||||
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
|
||||
"""
|
||||
查找溶剂容器,支持多种匹配模式
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
solvent: 溶剂名称(如 "methanol", "ethanol", "water")
|
||||
|
||||
Returns:
|
||||
str: 溶剂容器ID
|
||||
"""
|
||||
print(f"RESET_HANDLING: 正在查找溶剂 '{solvent}' 的容器...")
|
||||
|
||||
# 构建可能的容器名称
|
||||
possible_names = [
|
||||
f"flask_{solvent}", # flask_methanol
|
||||
f"bottle_{solvent}", # bottle_methanol
|
||||
f"reagent_{solvent}", # reagent_methanol
|
||||
f"reagent_bottle_{solvent}", # reagent_bottle_methanol
|
||||
f"{solvent}_flask", # methanol_flask
|
||||
f"{solvent}_bottle", # methanol_bottle
|
||||
f"{solvent}", # methanol
|
||||
f"vessel_{solvent}", # vessel_methanol
|
||||
]
|
||||
|
||||
# 第一步:通过容器名称匹配
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
print(f"RESET_HANDLING: 通过名称匹配找到容器: {vessel_name}")
|
||||
return vessel_name
|
||||
|
||||
# 第二步:通过模糊匹配
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
node_name = G.nodes[node_id].get('name', '').lower()
|
||||
|
||||
# 检查是否包含溶剂名称
|
||||
if solvent.lower() in node_id.lower() or solvent.lower() in node_name:
|
||||
print(f"RESET_HANDLING: 通过模糊匹配找到容器: {node_id}")
|
||||
return node_id
|
||||
|
||||
# 第三步:通过液体类型匹配
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
vessel_data = G.nodes[node_id].get('data', {})
|
||||
liquids = vessel_data.get('liquid', [])
|
||||
|
||||
for liquid in liquids:
|
||||
if isinstance(liquid, dict):
|
||||
liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower()
|
||||
reagent_name = vessel_data.get('reagent_name', '').lower()
|
||||
|
||||
if solvent.lower() in liquid_type or solvent.lower() in reagent_name:
|
||||
print(f"RESET_HANDLING: 通过液体类型匹配找到容器: {node_id}")
|
||||
return node_id
|
||||
|
||||
# 列出可用容器帮助调试
|
||||
available_containers = []
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
vessel_data = G.nodes[node_id].get('data', {})
|
||||
liquids = vessel_data.get('liquid', [])
|
||||
liquid_types = [liquid.get('liquid_type', '') or liquid.get('name', '')
|
||||
for liquid in liquids if isinstance(liquid, dict)]
|
||||
|
||||
available_containers.append({
|
||||
'id': node_id,
|
||||
'name': G.nodes[node_id].get('name', ''),
|
||||
'liquids': liquid_types,
|
||||
'reagent_name': vessel_data.get('reagent_name', '')
|
||||
})
|
||||
|
||||
print(f"RESET_HANDLING: 可用容器列表:")
|
||||
for container in available_containers:
|
||||
print(f" - {container['id']}: {container['name']}")
|
||||
print(f" 液体: {container['liquids']}")
|
||||
print(f" 试剂: {container['reagent_name']}")
|
||||
|
||||
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器。尝试了: {possible_names}")
|
||||
|
||||
|
||||
def generate_reset_handling_protocol(
|
||||
G: nx.DiGraph,
|
||||
solvent: str,
|
||||
**kwargs # 接收其他可能的参数但不使用
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成重置处理协议序列
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为容器和设备
|
||||
solvent: 溶剂名称(从XDL传入)
|
||||
**kwargs: 其他可选参数,但不使用
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
action_sequence = []
|
||||
|
||||
# 固定参数
|
||||
target_vessel = "main_reactor" # 默认目标容器
|
||||
volume = 100.0 # 默认体积 100 mL
|
||||
|
||||
print(f"RESET_HANDLING: 开始生成重置处理协议")
|
||||
print(f" - 溶剂: {solvent}")
|
||||
print(f" - 目标容器: {target_vessel}")
|
||||
print(f" - 体积: {volume} mL")
|
||||
|
||||
# 1. 验证目标容器存在
|
||||
if target_vessel not in G.nodes():
|
||||
raise ValueError(f"目标容器 '{target_vessel}' 不存在于系统中")
|
||||
|
||||
# 2. 查找溶剂容器
|
||||
try:
|
||||
solvent_vessel = find_solvent_vessel(G, solvent)
|
||||
print(f"RESET_HANDLING: 找到溶剂容器: {solvent_vessel}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}")
|
||||
|
||||
# 3. 验证路径存在
|
||||
try:
|
||||
path = nx.shortest_path(G, source=solvent_vessel, target=target_vessel)
|
||||
print(f"RESET_HANDLING: 找到路径: {' → '.join(path)}")
|
||||
except nx.NetworkXNoPath:
|
||||
raise ValueError(f"从溶剂容器 '{solvent_vessel}' 到目标容器 '{target_vessel}' 没有可用路径")
|
||||
|
||||
# 4. 使用pump_protocol转移溶剂
|
||||
print(f"RESET_HANDLING: 开始转移溶剂 {volume} mL")
|
||||
|
||||
try:
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent_vessel,
|
||||
to_vessel=target_vessel,
|
||||
volume=volume,
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # 重置处理不需要清洗
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.5, # 正常流速
|
||||
transfer_flowrate=0.5 # 正常转移流速
|
||||
)
|
||||
|
||||
action_sequence.extend(pump_actions)
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"生成泵协议时出错: {str(e)}")
|
||||
|
||||
# 5. 等待溶剂稳定
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 10.0,
|
||||
"description": f"等待溶剂 {solvent} 稳定"
|
||||
}
|
||||
})
|
||||
|
||||
print(f"RESET_HANDLING: 协议生成完成,共 {len(action_sequence)} 个动作")
|
||||
print(f"RESET_HANDLING: 已添加 {volume} mL {solvent} 到 {target_vessel}")
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_reset_handling_protocol():
|
||||
"""测试重置处理协议"""
|
||||
print("=== RESET HANDLING PROTOCOL 测试 ===")
|
||||
print("测试完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_reset_handling_protocol()
|
||||
@@ -1,312 +1,668 @@
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Union
|
||||
import networkx as nx
|
||||
from .pump_protocol import generate_pump_protocol
|
||||
import logging
|
||||
import re
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
|
||||
"""获取容器中的液体体积"""
|
||||
if vessel not in G.nodes():
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"[RUN_COLUMN] {message}", flush=True)
|
||||
logger.info(f"[RUN_COLUMN] {message}")
|
||||
|
||||
def parse_percentage(pct_str: str) -> float:
|
||||
"""
|
||||
解析百分比字符串为数值
|
||||
|
||||
Args:
|
||||
pct_str: 百分比字符串(如 "40 %", "40%", "40")
|
||||
|
||||
Returns:
|
||||
float: 百分比数值(0-100)
|
||||
"""
|
||||
if not pct_str or not pct_str.strip():
|
||||
return 0.0
|
||||
|
||||
vessel_data = G.nodes[vessel].get('data', {})
|
||||
liquids = vessel_data.get('liquid', [])
|
||||
pct_str = pct_str.strip().lower()
|
||||
debug_print(f"解析百分比: '{pct_str}'")
|
||||
|
||||
total_volume = 0.0
|
||||
for liquid in liquids:
|
||||
if isinstance(liquid, dict):
|
||||
# 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume)
|
||||
volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0)
|
||||
total_volume += volume
|
||||
# 移除百分号和空格
|
||||
pct_clean = re.sub(r'[%\s]', '', pct_str)
|
||||
|
||||
return total_volume
|
||||
# 提取数字
|
||||
match = re.search(r'([0-9]*\.?[0-9]+)', pct_clean)
|
||||
if match:
|
||||
value = float(match.group(1))
|
||||
debug_print(f"百分比解析结果: {value}%")
|
||||
return value
|
||||
|
||||
debug_print(f"⚠️ 无法解析百分比: '{pct_str}',返回0.0")
|
||||
return 0.0
|
||||
|
||||
def parse_ratio(ratio_str: str) -> tuple:
|
||||
"""
|
||||
解析比例字符串为两个数值
|
||||
|
||||
Args:
|
||||
ratio_str: 比例字符串(如 "5:95", "1:1", "40:60")
|
||||
|
||||
Returns:
|
||||
tuple: (ratio1, ratio2) 两个比例值
|
||||
"""
|
||||
if not ratio_str or not ratio_str.strip():
|
||||
return (50.0, 50.0) # 默认1:1
|
||||
|
||||
ratio_str = ratio_str.strip()
|
||||
debug_print(f"解析比例: '{ratio_str}'")
|
||||
|
||||
# 支持多种分隔符:: / -
|
||||
if ':' in ratio_str:
|
||||
parts = ratio_str.split(':')
|
||||
elif '/' in ratio_str:
|
||||
parts = ratio_str.split('/')
|
||||
elif '-' in ratio_str:
|
||||
parts = ratio_str.split('-')
|
||||
elif 'to' in ratio_str.lower():
|
||||
parts = ratio_str.lower().split('to')
|
||||
else:
|
||||
debug_print(f"⚠️ 无法解析比例格式: '{ratio_str}',使用默认1:1")
|
||||
return (50.0, 50.0)
|
||||
|
||||
if len(parts) >= 2:
|
||||
try:
|
||||
ratio1 = float(parts[0].strip())
|
||||
ratio2 = float(parts[1].strip())
|
||||
total = ratio1 + ratio2
|
||||
|
||||
# 转换为百分比
|
||||
pct1 = (ratio1 / total) * 100
|
||||
pct2 = (ratio2 / total) * 100
|
||||
|
||||
debug_print(f"比例解析结果: {ratio1}:{ratio2} -> {pct1:.1f}%:{pct2:.1f}%")
|
||||
return (pct1, pct2)
|
||||
except ValueError as e:
|
||||
debug_print(f"⚠️ 比例数值转换失败: {str(e)}")
|
||||
|
||||
debug_print(f"⚠️ 比例解析失败,使用默认1:1")
|
||||
return (50.0, 50.0)
|
||||
|
||||
def find_column_device(G: nx.DiGraph, column: str) -> str:
|
||||
def parse_rf_value(rf_str: str) -> float:
|
||||
"""
|
||||
解析Rf值字符串
|
||||
|
||||
Args:
|
||||
rf_str: Rf值字符串(如 "0.3", "0.45", "?")
|
||||
|
||||
Returns:
|
||||
float: Rf值(0-1)
|
||||
"""
|
||||
if not rf_str or not rf_str.strip():
|
||||
return 0.3 # 默认Rf值
|
||||
|
||||
rf_str = rf_str.strip().lower()
|
||||
debug_print(f"解析Rf值: '{rf_str}'")
|
||||
|
||||
# 处理未知Rf值
|
||||
if rf_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
||||
default_rf = 0.3
|
||||
debug_print(f"检测到未知Rf值,使用默认值: {default_rf}")
|
||||
return default_rf
|
||||
|
||||
# 提取数字
|
||||
match = re.search(r'([0-9]*\.?[0-9]+)', rf_str)
|
||||
if match:
|
||||
value = float(match.group(1))
|
||||
# 确保Rf值在0-1范围内
|
||||
if value > 1.0:
|
||||
value = value / 100.0 # 可能是百分比形式
|
||||
value = max(0.0, min(1.0, value)) # 限制在0-1范围
|
||||
debug_print(f"Rf值解析结果: {value}")
|
||||
return value
|
||||
|
||||
debug_print(f"⚠️ 无法解析Rf值: '{rf_str}',使用默认值0.3")
|
||||
return 0.3
|
||||
|
||||
def find_column_device(G: nx.DiGraph) -> str:
|
||||
"""查找柱层析设备"""
|
||||
# 首先检查是否有虚拟柱设备
|
||||
column_nodes = [node for node in G.nodes()
|
||||
if (G.nodes[node].get('class') or '') == 'virtual_column']
|
||||
debug_print("查找柱层析设备...")
|
||||
|
||||
if column_nodes:
|
||||
return column_nodes[0]
|
||||
# 查找虚拟柱设备
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if 'virtual_column' in node_class.lower() or 'column' in node_class.lower():
|
||||
debug_print(f"✅ 找到柱层析设备: {node}")
|
||||
return node
|
||||
|
||||
# 如果没有虚拟柱设备,抛出异常
|
||||
raise ValueError(f"系统中未找到柱层析设备。请确保配置了 virtual_column 设备")
|
||||
|
||||
# 如果没有找到,尝试创建虚拟设备名称
|
||||
possible_names = ['column_1', 'virtual_column_1', 'chromatography_column_1']
|
||||
for name in possible_names:
|
||||
if name in G.nodes():
|
||||
debug_print(f"✅ 找到柱设备: {name}")
|
||||
return name
|
||||
|
||||
debug_print("⚠️ 未找到柱层析设备,将使用pump protocol直接转移")
|
||||
return ""
|
||||
|
||||
def find_column_vessel(G: nx.DiGraph, column: str) -> str:
|
||||
"""查找柱容器"""
|
||||
# 直接使用 column 参数作为容器名称
|
||||
if column in G.nodes():
|
||||
return column
|
||||
debug_print(f"查找柱容器: '{column}'")
|
||||
|
||||
# 尝试常见的柱容器命名规则
|
||||
# 直接检查column参数是否是容器
|
||||
if column in G.nodes():
|
||||
node_type = G.nodes[column].get('type', '')
|
||||
if node_type == 'container':
|
||||
debug_print(f"✅ 找到柱容器: {column}")
|
||||
return column
|
||||
|
||||
# 尝试常见的命名规则
|
||||
possible_names = [
|
||||
f"column_{column}",
|
||||
f"{column}_column",
|
||||
f"{column}_column",
|
||||
f"vessel_{column}",
|
||||
f"{column}_vessel",
|
||||
"column_vessel",
|
||||
"chromatography_column",
|
||||
"silica_column",
|
||||
"preparative_column"
|
||||
"preparative_column",
|
||||
"column"
|
||||
]
|
||||
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
return vessel_name
|
||||
node_type = G.nodes[vessel_name].get('type', '')
|
||||
if node_type == 'container':
|
||||
debug_print(f"✅ 找到柱容器: {vessel_name}")
|
||||
return vessel_name
|
||||
|
||||
raise ValueError(f"未找到柱容器 '{column}'。尝试了以下名称: {[column] + possible_names}")
|
||||
debug_print(f"⚠️ 未找到柱容器,将直接在源容器中进行分离")
|
||||
return ""
|
||||
|
||||
|
||||
def find_eluting_solvent_vessel(G: nx.DiGraph, eluting_solvent: str) -> str:
|
||||
"""查找洗脱溶剂容器"""
|
||||
if not eluting_solvent:
|
||||
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
|
||||
"""查找溶剂容器 - 增强版"""
|
||||
if not solvent or not solvent.strip():
|
||||
return ""
|
||||
|
||||
# 按照命名规则查找溶剂瓶
|
||||
solvent_vessel_id = f"flask_{eluting_solvent}"
|
||||
solvent = solvent.strip().replace(' ', '_').lower()
|
||||
debug_print(f"查找溶剂容器: '{solvent}'")
|
||||
|
||||
if solvent_vessel_id in G.nodes():
|
||||
return solvent_vessel_id
|
||||
|
||||
# 如果直接匹配失败,尝试模糊匹配
|
||||
# 🔧 方法1:直接搜索 data.reagent_name
|
||||
for node in G.nodes():
|
||||
if node.startswith('flask_') and eluting_solvent.lower() in node.lower():
|
||||
return node
|
||||
node_data = G.nodes[node].get('data', {})
|
||||
node_type = G.nodes[node].get('type', '')
|
||||
|
||||
# 只搜索容器类型的节点
|
||||
if node_type == 'container':
|
||||
reagent_name = node_data.get('reagent_name', '').lower()
|
||||
reagent_config = G.nodes[node].get('config', {}).get('reagent', '').lower()
|
||||
|
||||
# 检查 data.reagent_name 和 config.reagent
|
||||
if reagent_name == solvent or reagent_config == solvent:
|
||||
debug_print(f"✅ 通过reagent_name找到溶剂容器: {node} (reagent: {reagent_name or reagent_config})")
|
||||
return node
|
||||
|
||||
# 模糊匹配 reagent_name
|
||||
if solvent in reagent_name or reagent_name in solvent:
|
||||
debug_print(f"✅ 通过reagent_name模糊匹配到溶剂容器: {node} (reagent: {reagent_name})")
|
||||
return node
|
||||
|
||||
if solvent in reagent_config or reagent_config in solvent:
|
||||
debug_print(f"✅ 通过config.reagent模糊匹配到溶剂容器: {node} (reagent: {reagent_config})")
|
||||
return node
|
||||
|
||||
# 如果还是找不到,列出所有可用的溶剂瓶
|
||||
available_flasks = [node for node in G.nodes()
|
||||
if node.startswith('flask_')
|
||||
and G.nodes[node].get('type') == 'container']
|
||||
# 🔧 方法2:常见的溶剂容器命名规则
|
||||
possible_names = [
|
||||
f"flask_{solvent}",
|
||||
f"bottle_{solvent}",
|
||||
f"reagent_{solvent}",
|
||||
f"{solvent}_bottle",
|
||||
f"{solvent}_flask",
|
||||
f"solvent_{solvent}",
|
||||
f"reagent_bottle_{solvent}"
|
||||
]
|
||||
|
||||
raise ValueError(f"找不到洗脱溶剂 '{eluting_solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}")
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
node_type = G.nodes[vessel_name].get('type', '')
|
||||
if node_type == 'container':
|
||||
debug_print(f"✅ 通过命名规则找到溶剂容器: {vessel_name}")
|
||||
return vessel_name
|
||||
|
||||
# 🔧 方法3:节点名称模糊匹配
|
||||
for node in G.nodes():
|
||||
node_type = G.nodes[node].get('type', '')
|
||||
if node_type == 'container':
|
||||
if ('flask_' in node or 'bottle_' in node or 'reagent_' in node) and solvent in node.lower():
|
||||
debug_print(f"✅ 通过节点名称模糊匹配到溶剂容器: {node}")
|
||||
return node
|
||||
|
||||
# 🔧 方法4:特殊溶剂名称映射
|
||||
solvent_mapping = {
|
||||
'dmf': ['dmf', 'dimethylformamide', 'n,n-dimethylformamide'],
|
||||
'ethyl_acetate': ['ethyl_acetate', 'ethylacetate', 'etoac', 'ea'],
|
||||
'hexane': ['hexane', 'hexanes', 'n-hexane'],
|
||||
'methanol': ['methanol', 'meoh', 'ch3oh'],
|
||||
'water': ['water', 'h2o', 'distilled_water'],
|
||||
'acetone': ['acetone', 'ch3coch3', '2-propanone'],
|
||||
'dichloromethane': ['dichloromethane', 'dcm', 'ch2cl2', 'methylene_chloride'],
|
||||
'chloroform': ['chloroform', 'chcl3', 'trichloromethane']
|
||||
}
|
||||
|
||||
# 查找映射的同义词
|
||||
for canonical_name, synonyms in solvent_mapping.items():
|
||||
if solvent in synonyms:
|
||||
debug_print(f"检测到溶剂同义词: '{solvent}' -> '{canonical_name}'")
|
||||
return find_solvent_vessel(G, canonical_name) # 递归搜索
|
||||
|
||||
debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器")
|
||||
return ""
|
||||
|
||||
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
|
||||
"""获取容器中的液体体积 - 增强版"""
|
||||
if vessel not in G.nodes():
|
||||
debug_print(f"⚠️ 节点 '{vessel}' 不存在")
|
||||
return 0.0
|
||||
|
||||
node_type = G.nodes[vessel].get('type', '')
|
||||
vessel_data = G.nodes[vessel].get('data', {})
|
||||
|
||||
debug_print(f"读取节点 '{vessel}' (类型: {node_type}) 体积数据: {vessel_data}")
|
||||
|
||||
# 🔧 如果是设备类型,尝试查找关联的容器
|
||||
if node_type == 'device':
|
||||
debug_print(f"'{vessel}' 是设备,尝试查找关联容器...")
|
||||
|
||||
# 查找是否有内置容器数据
|
||||
config_data = G.nodes[vessel].get('config', {})
|
||||
if 'volume' in config_data:
|
||||
default_volume = config_data.get('volume', 100.0)
|
||||
debug_print(f"使用设备默认容量: {default_volume}mL")
|
||||
return default_volume
|
||||
|
||||
# 对于旋蒸等设备,使用默认值
|
||||
if 'rotavap' in vessel.lower():
|
||||
default_volume = 100.0
|
||||
debug_print(f"旋蒸设备使用默认容量: {default_volume}mL")
|
||||
return default_volume
|
||||
|
||||
debug_print(f"⚠️ 设备 '{vessel}' 无法确定容量,返回0")
|
||||
return 0.0
|
||||
|
||||
# 🔧 如果是容器类型,正常读取体积
|
||||
total_volume = 0.0
|
||||
|
||||
# 方法1:检查液体列表
|
||||
liquids = vessel_data.get('liquid', [])
|
||||
if isinstance(liquids, list):
|
||||
for liquid in liquids:
|
||||
if isinstance(liquid, dict):
|
||||
volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0)
|
||||
total_volume += volume
|
||||
|
||||
# 方法2:检查直接体积字段
|
||||
if total_volume == 0.0:
|
||||
volume_keys = ['current_volume', 'total_volume', 'volume', 'liquid_volume']
|
||||
for key in volume_keys:
|
||||
if key in vessel_data:
|
||||
try:
|
||||
total_volume = float(vessel_data[key])
|
||||
if total_volume > 0:
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
# 方法3:检查配置中的初始体积
|
||||
if total_volume == 0.0:
|
||||
config_data = G.nodes[vessel].get('config', {})
|
||||
if 'current_volume' in config_data:
|
||||
try:
|
||||
total_volume = float(config_data['current_volume'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
debug_print(f"容器 '{vessel}' 总体积: {total_volume}mL")
|
||||
return total_volume
|
||||
|
||||
def calculate_solvent_volumes(total_volume: float, pct1: float, pct2: float) -> tuple:
|
||||
"""根据百分比计算溶剂体积"""
|
||||
volume1 = (total_volume * pct1) / 100.0
|
||||
volume2 = (total_volume * pct2) / 100.0
|
||||
|
||||
debug_print(f"溶剂体积计算: 总体积{total_volume}mL")
|
||||
debug_print(f" - 溶剂1: {pct1}% = {volume1}mL")
|
||||
debug_print(f" - 溶剂2: {pct2}% = {volume2}mL")
|
||||
|
||||
return (volume1, volume2)
|
||||
|
||||
def generate_run_column_protocol(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
column: str
|
||||
column: str,
|
||||
rf: str = "",
|
||||
pct1: str = "",
|
||||
pct2: str = "",
|
||||
solvent1: str = "",
|
||||
solvent2: str = "",
|
||||
ratio: str = "",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成柱层析分离的协议序列
|
||||
生成柱层析分离的协议序列 - 增强版
|
||||
|
||||
支持新版XDL的所有参数,具有高兼容性和容错性
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为设备和容器,边为流体管道
|
||||
from_vessel: 源容器的名称,即样品起始所在的容器
|
||||
to_vessel: 目标容器的名称,分离后的样品要到达的容器
|
||||
column: 所使用的柱子的名称
|
||||
from_vessel: 源容器的名称,即样品起始所在的容器(必需)
|
||||
to_vessel: 目标容器的名称,分离后的样品要到达的容器(必需)
|
||||
column: 所使用的柱子的名称(必需)
|
||||
rf: Rf值(可选,支持 "?" 表示未知)
|
||||
pct1: 第一种溶剂百分比(如 "40 %",可选)
|
||||
pct2: 第二种溶剂百分比(如 "50 %",可选)
|
||||
solvent1: 第一种溶剂名称(可选)
|
||||
solvent2: 第二种溶剂名称(可选)
|
||||
ratio: 溶剂比例(如 "5:95",可选,优先级高于pct1/pct2)
|
||||
**kwargs: 其他可选参数
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 柱层析分离操作的动作序列
|
||||
"""
|
||||
|
||||
debug_print("=" * 60)
|
||||
debug_print("开始生成柱层析协议")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - from_vessel: '{from_vessel}'")
|
||||
debug_print(f" - to_vessel: '{to_vessel}'")
|
||||
debug_print(f" - column: '{column}'")
|
||||
debug_print(f" - rf: '{rf}'")
|
||||
debug_print(f" - pct1: '{pct1}'")
|
||||
debug_print(f" - pct2: '{pct2}'")
|
||||
debug_print(f" - solvent1: '{solvent1}'")
|
||||
debug_print(f" - solvent2: '{solvent2}'")
|
||||
debug_print(f" - ratio: '{ratio}'")
|
||||
debug_print(f" - 其他参数: {kwargs}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
print(f"RUN_COLUMN: 开始生成柱层析协议")
|
||||
print(f" - 源容器: {from_vessel}")
|
||||
print(f" - 目标容器: {to_vessel}")
|
||||
print(f" - 柱子: {column}")
|
||||
# === 参数验证 ===
|
||||
debug_print("步骤1: 参数验证...")
|
||||
|
||||
if not from_vessel:
|
||||
raise ValueError("from_vessel 参数不能为空")
|
||||
if not to_vessel:
|
||||
raise ValueError("to_vessel 参数不能为空")
|
||||
if not column:
|
||||
raise ValueError("column 参数不能为空")
|
||||
|
||||
# 验证源容器和目标容器存在
|
||||
if from_vessel not in G.nodes():
|
||||
raise ValueError(f"源容器 '{from_vessel}' 不存在于系统中")
|
||||
|
||||
if to_vessel not in G.nodes():
|
||||
raise ValueError(f"目标容器 '{to_vessel}' 不存在于系统中")
|
||||
|
||||
# 查找柱层析设备
|
||||
column_device_id = None
|
||||
column_nodes = [node for node in G.nodes()
|
||||
if (G.nodes[node].get('class') or '') == 'virtual_column']
|
||||
debug_print("✅ 基本参数验证通过")
|
||||
|
||||
if column_nodes:
|
||||
column_device_id = column_nodes[0]
|
||||
print(f"RUN_COLUMN: 找到柱层析设备: {column_device_id}")
|
||||
# === 参数解析 ===
|
||||
debug_print("步骤2: 参数解析...")
|
||||
|
||||
# 解析Rf值
|
||||
final_rf = parse_rf_value(rf)
|
||||
debug_print(f"最终Rf值: {final_rf}")
|
||||
|
||||
# 解析溶剂比例(ratio优先级高于pct1/pct2)
|
||||
if ratio and ratio.strip():
|
||||
final_pct1, final_pct2 = parse_ratio(ratio)
|
||||
debug_print(f"使用ratio参数: {final_pct1:.1f}% : {final_pct2:.1f}%")
|
||||
else:
|
||||
print(f"RUN_COLUMN: 警告 - 未找到柱层析设备")
|
||||
final_pct1 = parse_percentage(pct1) if pct1 else 50.0
|
||||
final_pct2 = parse_percentage(pct2) if pct2 else 50.0
|
||||
|
||||
# 如果百分比和不是100%,进行归一化
|
||||
total_pct = final_pct1 + final_pct2
|
||||
if total_pct == 0:
|
||||
final_pct1, final_pct2 = 50.0, 50.0
|
||||
elif total_pct != 100.0:
|
||||
final_pct1 = (final_pct1 / total_pct) * 100
|
||||
final_pct2 = (final_pct2 / total_pct) * 100
|
||||
|
||||
debug_print(f"使用百分比参数: {final_pct1:.1f}% : {final_pct2:.1f}%")
|
||||
|
||||
# 设置默认溶剂(如果未指定)
|
||||
final_solvent1 = solvent1.strip() if solvent1 else "ethyl_acetate"
|
||||
final_solvent2 = solvent2.strip() if solvent2 else "hexane"
|
||||
|
||||
debug_print(f"最终溶剂: {final_solvent1} : {final_solvent2}")
|
||||
|
||||
# === 查找设备和容器 ===
|
||||
debug_print("步骤3: 查找设备和容器...")
|
||||
|
||||
# 查找柱层析设备
|
||||
column_device_id = find_column_device(G)
|
||||
|
||||
# 查找柱容器
|
||||
column_vessel = find_column_vessel(G, column)
|
||||
|
||||
# 查找溶剂容器
|
||||
solvent1_vessel = find_solvent_vessel(G, final_solvent1)
|
||||
solvent2_vessel = find_solvent_vessel(G, final_solvent2)
|
||||
|
||||
debug_print(f"设备映射:")
|
||||
debug_print(f" - 柱设备: '{column_device_id}'")
|
||||
debug_print(f" - 柱容器: '{column_vessel}'")
|
||||
debug_print(f" - 溶剂1容器: '{solvent1_vessel}'")
|
||||
debug_print(f" - 溶剂2容器: '{solvent2_vessel}'")
|
||||
|
||||
# === 获取源容器体积 ===
|
||||
debug_print("步骤4: 获取源容器体积...")
|
||||
|
||||
# 获取源容器中的液体体积
|
||||
source_volume = get_vessel_liquid_volume(G, from_vessel)
|
||||
print(f"RUN_COLUMN: 源容器 {from_vessel} 中有 {source_volume} mL 液体")
|
||||
if source_volume <= 0:
|
||||
source_volume = 100.0 # 默认体积
|
||||
debug_print(f"⚠️ 无法获取源容器体积,使用默认值: {source_volume}mL")
|
||||
else:
|
||||
debug_print(f"✅ 源容器体积: {source_volume}mL")
|
||||
|
||||
# === 第一步:样品转移到柱子(如果柱子是容器) ===
|
||||
if column in G.nodes() and G.nodes[column].get('type') == 'container':
|
||||
print(f"RUN_COLUMN: 样品转移 - {source_volume} mL 从 {from_vessel} 到 {column}")
|
||||
|
||||
try:
|
||||
sample_transfer_actions = generate_pump_protocol(
|
||||
G=G,
|
||||
from_vessel=from_vessel,
|
||||
to_vessel=column,
|
||||
volume=source_volume if source_volume > 0 else 100.0,
|
||||
flowrate=2.0
|
||||
)
|
||||
action_sequence.extend(sample_transfer_actions)
|
||||
except Exception as e:
|
||||
print(f"RUN_COLUMN: 样品转移失败: {str(e)}")
|
||||
# === 计算溶剂体积 ===
|
||||
debug_print("步骤5: 计算溶剂体积...")
|
||||
|
||||
# === 第二步:使用柱层析设备执行分离 ===
|
||||
if column_device_id:
|
||||
print(f"RUN_COLUMN: 使用柱层析设备执行分离")
|
||||
# 洗脱溶剂通常是样品体积的2-5倍
|
||||
total_elution_volume = source_volume * 3.0
|
||||
solvent1_volume, solvent2_volume = calculate_solvent_volumes(
|
||||
total_elution_volume, final_pct1, final_pct2
|
||||
)
|
||||
|
||||
# === 执行柱层析流程 ===
|
||||
debug_print("步骤6: 执行柱层析流程...")
|
||||
|
||||
try:
|
||||
# 步骤6.1: 样品上柱(如果有独立的柱容器)
|
||||
if column_vessel and column_vessel != from_vessel:
|
||||
debug_print(f"6.1: 样品上柱 - {source_volume}mL 从 {from_vessel} 到 {column_vessel}")
|
||||
|
||||
try:
|
||||
sample_transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=from_vessel,
|
||||
to_vessel=column_vessel,
|
||||
volume=source_volume,
|
||||
flowrate=1.0, # 慢速上柱
|
||||
transfer_flowrate=0.5,
|
||||
rinsing_solvent="", # 暂不冲洗
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0
|
||||
)
|
||||
action_sequence.extend(sample_transfer_actions)
|
||||
debug_print(f"✅ 样品上柱完成,添加了 {len(sample_transfer_actions)} 个动作")
|
||||
except Exception as e:
|
||||
debug_print(f"⚠️ 样品上柱失败: {str(e)}")
|
||||
|
||||
column_separation_action = {
|
||||
"device_id": column_device_id,
|
||||
"action_name": "run_column",
|
||||
"action_kwargs": {
|
||||
"from_vessel": from_vessel,
|
||||
"to_vessel": to_vessel,
|
||||
"column": column
|
||||
# 步骤6.2: 添加洗脱溶剂1(如果有溶剂容器)
|
||||
if solvent1_vessel and solvent1_volume > 0:
|
||||
debug_print(f"6.2: 添加洗脱溶剂1 - {solvent1_volume:.1f}mL {final_solvent1}")
|
||||
|
||||
try:
|
||||
target_vessel = column_vessel if column_vessel else from_vessel
|
||||
solvent1_transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent1_vessel,
|
||||
to_vessel=target_vessel,
|
||||
volume=solvent1_volume,
|
||||
flowrate=2.0,
|
||||
transfer_flowrate=1.0
|
||||
)
|
||||
action_sequence.extend(solvent1_transfer_actions)
|
||||
debug_print(f"✅ 溶剂1添加完成,添加了 {len(solvent1_transfer_actions)} 个动作")
|
||||
except Exception as e:
|
||||
debug_print(f"⚠️ 溶剂1添加失败: {str(e)}")
|
||||
|
||||
# 步骤6.3: 添加洗脱溶剂2(如果有溶剂容器)
|
||||
if solvent2_vessel and solvent2_volume > 0:
|
||||
debug_print(f"6.3: 添加洗脱溶剂2 - {solvent2_volume:.1f}mL {final_solvent2}")
|
||||
|
||||
try:
|
||||
target_vessel = column_vessel if column_vessel else from_vessel
|
||||
solvent2_transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent2_vessel,
|
||||
to_vessel=target_vessel,
|
||||
volume=solvent2_volume,
|
||||
flowrate=2.0,
|
||||
transfer_flowrate=1.0
|
||||
)
|
||||
action_sequence.extend(solvent2_transfer_actions)
|
||||
debug_print(f"✅ 溶剂2添加完成,添加了 {len(solvent2_transfer_actions)} 个动作")
|
||||
except Exception as e:
|
||||
debug_print(f"⚠️ 溶剂2添加失败: {str(e)}")
|
||||
|
||||
# 步骤6.4: 使用柱层析设备执行分离(如果有设备)
|
||||
if column_device_id:
|
||||
debug_print(f"6.4: 使用柱层析设备执行分离")
|
||||
|
||||
column_separation_action = {
|
||||
"device_id": column_device_id,
|
||||
"action_name": "run_column",
|
||||
"action_kwargs": {
|
||||
"from_vessel": from_vessel,
|
||||
"to_vessel": to_vessel,
|
||||
"column": column,
|
||||
"rf": rf,
|
||||
"pct1": pct1,
|
||||
"pct2": pct2,
|
||||
"solvent1": solvent1,
|
||||
"solvent2": solvent2,
|
||||
"ratio": ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
action_sequence.append(column_separation_action)
|
||||
|
||||
# 等待柱层析设备完成分离
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 60}
|
||||
})
|
||||
|
||||
# === 第三步:从柱子转移到目标容器(如果需要) ===
|
||||
if column in G.nodes() and column != to_vessel:
|
||||
print(f"RUN_COLUMN: 产物转移 - 从 {column} 到 {to_vessel}")
|
||||
|
||||
try:
|
||||
product_transfer_actions = generate_pump_protocol(
|
||||
G=G,
|
||||
from_vessel=column,
|
||||
to_vessel=to_vessel,
|
||||
volume=source_volume * 0.8 if source_volume > 0 else 80.0, # 假设有一些损失
|
||||
flowrate=1.5
|
||||
)
|
||||
action_sequence.extend(product_transfer_actions)
|
||||
except Exception as e:
|
||||
print(f"RUN_COLUMN: 产物转移失败: {str(e)}")
|
||||
|
||||
print(f"RUN_COLUMN: 生成了 {len(action_sequence)} 个动作")
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 便捷函数:常用柱层析方案
|
||||
def generate_flash_column_protocol(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
column_material: str = "silica_gel",
|
||||
mobile_phase: str = "ethyl_acetate",
|
||||
mobile_phase_volume: float = 100.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""快速柱层析:高流速分离"""
|
||||
return generate_run_column_protocol(
|
||||
G, from_vessel, to_vessel, column_material,
|
||||
mobile_phase, mobile_phase_volume, 1, "", 0.0, 3.0
|
||||
)
|
||||
|
||||
|
||||
def generate_preparative_column_protocol(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
column_material: str = "silica_gel",
|
||||
equilibration_solvent: str = "hexane",
|
||||
eluting_solvent: str = "ethyl_acetate",
|
||||
eluting_volume: float = 50.0,
|
||||
eluting_repeats: int = 3
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""制备柱层析:带平衡和多次洗脱"""
|
||||
return generate_run_column_protocol(
|
||||
G, from_vessel, to_vessel, column_material,
|
||||
eluting_solvent, eluting_volume, eluting_repeats,
|
||||
equilibration_solvent, 30.0, 1.5
|
||||
)
|
||||
|
||||
|
||||
def generate_gradient_column_protocol(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
column_material: str = "silica_gel",
|
||||
gradient_solvents: List[str] = None,
|
||||
gradient_volumes: List[float] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""梯度洗脱柱层析:多种溶剂系统"""
|
||||
if gradient_solvents is None:
|
||||
gradient_solvents = ["hexane", "ethyl_acetate", "methanol"]
|
||||
if gradient_volumes is None:
|
||||
gradient_volumes = [50.0, 50.0, 30.0]
|
||||
|
||||
action_sequence = []
|
||||
|
||||
# 每种溶剂单独执行一次柱层析
|
||||
for i, (solvent, volume) in enumerate(zip(gradient_solvents, gradient_volumes)):
|
||||
print(f"RUN_COLUMN: 梯度洗脱第 {i+1}/{len(gradient_solvents)} 步: {volume} mL {solvent}")
|
||||
|
||||
# 第一步使用源容器,后续步骤使用柱子作为源
|
||||
step_from_vessel = from_vessel if i == 0 else column_material
|
||||
# 最后一步使用目标容器,其他步骤使用柱子作为目标
|
||||
step_to_vessel = to_vessel if i == len(gradient_solvents) - 1 else column_material
|
||||
|
||||
step_actions = generate_run_column_protocol(
|
||||
G, step_from_vessel, step_to_vessel, column_material,
|
||||
solvent, volume, 1, "", 0.0, 1.0
|
||||
)
|
||||
action_sequence.extend(step_actions)
|
||||
|
||||
# 在梯度步骤之间加入等待时间
|
||||
if i < len(gradient_solvents) - 1:
|
||||
action_sequence.append(column_separation_action)
|
||||
debug_print(f"✅ 柱层析设备动作已添加")
|
||||
|
||||
# 等待分离完成
|
||||
separation_time = max(30, int(total_elution_volume / 2)) # 基于体积估算时间
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 20}
|
||||
"action_kwargs": {"time": separation_time}
|
||||
})
|
||||
debug_print(f"✅ 等待分离完成: {separation_time}秒")
|
||||
|
||||
# 步骤6.5: 产物收集(从柱容器到目标容器)
|
||||
if column_vessel and column_vessel != to_vessel:
|
||||
debug_print(f"6.5: 产物收集 - 从 {column_vessel} 到 {to_vessel}")
|
||||
|
||||
try:
|
||||
# 估算产物体积(原始样品体积的70-90%)
|
||||
product_volume = source_volume * 0.8
|
||||
|
||||
product_transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=column_vessel,
|
||||
to_vessel=to_vessel,
|
||||
volume=product_volume,
|
||||
flowrate=1.5,
|
||||
transfer_flowrate=0.8
|
||||
)
|
||||
action_sequence.extend(product_transfer_actions)
|
||||
debug_print(f"✅ 产物收集完成,添加了 {len(product_transfer_actions)} 个动作")
|
||||
except Exception as e:
|
||||
debug_print(f"⚠️ 产物收集失败: {str(e)}")
|
||||
|
||||
# 步骤6.6: 如果没有独立的柱设备和容器,执行简化的直接转移
|
||||
if not column_device_id and not column_vessel:
|
||||
debug_print(f"6.6: 简化模式 - 直接转移 {source_volume}mL 从 {from_vessel} 到 {to_vessel}")
|
||||
|
||||
try:
|
||||
direct_transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=from_vessel,
|
||||
to_vessel=to_vessel,
|
||||
volume=source_volume,
|
||||
flowrate=2.0,
|
||||
transfer_flowrate=1.0
|
||||
)
|
||||
action_sequence.extend(direct_transfer_actions)
|
||||
debug_print(f"✅ 直接转移完成,添加了 {len(direct_transfer_actions)} 个动作")
|
||||
except Exception as e:
|
||||
debug_print(f"⚠️ 直接转移失败: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 柱层析流程执行失败: {str(e)}")
|
||||
# 添加错误日志动作
|
||||
action_sequence.append({
|
||||
"device_id": "system",
|
||||
"action_name": "log_message",
|
||||
"action_kwargs": {
|
||||
"message": f"柱层析失败: {str(e)}"
|
||||
}
|
||||
})
|
||||
|
||||
# === 最终结果 ===
|
||||
debug_print("=" * 60)
|
||||
debug_print(f"✅ 柱层析协议生成完成")
|
||||
debug_print(f"📊 总动作数: {len(action_sequence)}")
|
||||
debug_print(f"📋 参数总结:")
|
||||
debug_print(f" - 源容器: {from_vessel} ({source_volume}mL)")
|
||||
debug_print(f" - 目标容器: {to_vessel}")
|
||||
debug_print(f" - 柱子: {column}")
|
||||
debug_print(f" - Rf值: {final_rf}")
|
||||
debug_print(f" - 溶剂比例: {final_solvent1} {final_pct1:.1f}% : {final_solvent2} {final_pct2:.1f}%")
|
||||
debug_print(f" - 洗脱体积: {solvent1_volume:.1f}mL + {solvent2_volume:.1f}mL")
|
||||
debug_print("=" * 60)
|
||||
|
||||
return action_sequence
|
||||
|
||||
# === 便捷函数 ===
|
||||
|
||||
def generate_silica_gel_column_protocol(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""硅胶柱层析协议便捷函数"""
|
||||
return generate_run_column_protocol(
|
||||
G, from_vessel, to_vessel,
|
||||
column="silica_column",
|
||||
solvent1="ethyl_acetate",
|
||||
solvent2="hexane",
|
||||
ratio="1:9", # 常见的EA:Hex比例
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def generate_reverse_phase_column_protocol(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
column_material: str = "C18",
|
||||
aqueous_phase: str = "water",
|
||||
organic_phase: str = "methanol",
|
||||
gradient_ratio: float = 0.5
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""反相柱层析:C18柱,水-有机相梯度"""
|
||||
# 先用水相平衡
|
||||
equilibration_volume = 20.0
|
||||
# 然后用有机相洗脱
|
||||
eluting_volume = 30.0 * gradient_ratio
|
||||
|
||||
"""反相柱层析协议便捷函数"""
|
||||
return generate_run_column_protocol(
|
||||
G, from_vessel, to_vessel, column_material,
|
||||
organic_phase, eluting_volume, 2,
|
||||
aqueous_phase, equilibration_volume, 0.8
|
||||
G, from_vessel, to_vessel,
|
||||
column="c18_column",
|
||||
solvent1="methanol",
|
||||
solvent2="water",
|
||||
ratio="7:3", # 常见的MeOH:H2O比例
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def generate_ion_exchange_column_protocol(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
column_material: str = "ion_exchange",
|
||||
buffer_solution: str = "buffer",
|
||||
salt_solution: str = "NaCl_solution",
|
||||
salt_volume: float = 40.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""离子交换柱层析:缓冲液平衡,盐溶液洗脱"""
|
||||
return generate_run_column_protocol(
|
||||
G, from_vessel, to_vessel, column_material,
|
||||
salt_solution, salt_volume, 1,
|
||||
buffer_solution, 25.0, 0.5
|
||||
)
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_run_column_protocol():
|
||||
"""测试柱层析协议的示例"""
|
||||
print("=== RUN COLUMN PROTOCOL 测试 ===")
|
||||
print("测试完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_run_column_protocol()
|
||||
@@ -1,230 +1,448 @@
|
||||
import numpy as np
|
||||
import networkx as nx
|
||||
import re
|
||||
import logging
|
||||
from typing import List, Dict, Any, Union
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"[SEPARATE] {message}", flush=True)
|
||||
logger.info(f"[SEPARATE] {message}")
|
||||
|
||||
def parse_volume_input(volume_input: Union[str, float]) -> float:
|
||||
"""
|
||||
解析体积输入,支持带单位的字符串
|
||||
|
||||
Args:
|
||||
volume_input: 体积输入(如 "200 mL", "?", 50.0)
|
||||
|
||||
Returns:
|
||||
float: 体积(毫升)
|
||||
"""
|
||||
if isinstance(volume_input, (int, float)):
|
||||
return float(volume_input)
|
||||
|
||||
if not volume_input or not str(volume_input).strip():
|
||||
return 0.0
|
||||
|
||||
volume_str = str(volume_input).lower().strip()
|
||||
debug_print(f"解析体积输入: '{volume_str}'")
|
||||
|
||||
# 处理未知体积
|
||||
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
||||
default_volume = 100.0 # 默认100mL
|
||||
debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
|
||||
return default_volume
|
||||
|
||||
# 移除空格并提取数字和单位
|
||||
volume_clean = re.sub(r'\s+', '', volume_str)
|
||||
|
||||
# 匹配数字和单位的正则表达式
|
||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
|
||||
|
||||
if not match:
|
||||
debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值100mL")
|
||||
return 100.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or 'ml' # 默认单位为毫升
|
||||
|
||||
# 转换为毫升
|
||||
if unit in ['l', 'liter']:
|
||||
volume = value * 1000.0 # L -> mL
|
||||
elif unit in ['μl', 'ul', 'microliter']:
|
||||
volume = value / 1000.0 # μL -> mL
|
||||
else: # ml, milliliter 或默认
|
||||
volume = value # 已经是mL
|
||||
|
||||
debug_print(f"体积转换: {value}{unit} → {volume}mL")
|
||||
return volume
|
||||
|
||||
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
|
||||
"""查找溶剂容器"""
|
||||
if not solvent or not solvent.strip():
|
||||
return ""
|
||||
|
||||
debug_print(f"查找溶剂 '{solvent}' 的容器...")
|
||||
|
||||
# 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node].get('data', {})
|
||||
node_type = G.nodes[node].get('type', '')
|
||||
config_data = G.nodes[node].get('config', {})
|
||||
|
||||
# 只搜索容器类型的节点
|
||||
if node_type == 'container':
|
||||
reagent_name = node_data.get('reagent_name', '').lower()
|
||||
config_reagent = config_data.get('reagent', '').lower()
|
||||
|
||||
# 精确匹配
|
||||
if reagent_name == solvent.lower() or config_reagent == solvent.lower():
|
||||
debug_print(f"✅ 通过reagent字段找到容器: {node}")
|
||||
return node
|
||||
|
||||
# 模糊匹配
|
||||
if (solvent.lower() in reagent_name and reagent_name) or \
|
||||
(solvent.lower() in config_reagent and config_reagent):
|
||||
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
|
||||
return node
|
||||
|
||||
# 🔧 方法2:常见的容器命名规则
|
||||
solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_')
|
||||
possible_names = [
|
||||
f"flask_{solvent_clean}",
|
||||
f"bottle_{solvent_clean}",
|
||||
f"vessel_{solvent_clean}",
|
||||
f"{solvent_clean}_flask",
|
||||
f"{solvent_clean}_bottle",
|
||||
f"solvent_{solvent_clean}",
|
||||
f"reagent_{solvent_clean}",
|
||||
f"reagent_bottle_{solvent_clean}"
|
||||
]
|
||||
|
||||
for name in possible_names:
|
||||
if name in G.nodes():
|
||||
node_type = G.nodes[name].get('type', '')
|
||||
if node_type == 'container':
|
||||
debug_print(f"✅ 通过命名规则找到容器: {name}")
|
||||
return name
|
||||
|
||||
# 🔧 方法3:使用第一个试剂瓶作为备选
|
||||
for node_id in G.nodes():
|
||||
node_data = G.nodes[node_id]
|
||||
if (node_data.get('type') == 'container' and
|
||||
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
|
||||
debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}")
|
||||
return node_id
|
||||
|
||||
debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器")
|
||||
return ""
|
||||
|
||||
def find_separator_device(G: nx.DiGraph, vessel: str) -> str:
|
||||
"""查找分离器设备"""
|
||||
debug_print(f"查找容器 '{vessel}' 对应的分离器设备...")
|
||||
|
||||
# 方法1:查找连接到容器的分离器设备
|
||||
for node in G.nodes():
|
||||
node_class = G.nodes[node].get('class', '').lower()
|
||||
if 'separator' in node_class:
|
||||
# 检查是否连接到目标容器
|
||||
if G.has_edge(node, vessel) or G.has_edge(vessel, node):
|
||||
debug_print(f"✅ 找到连接的分离器: {node}")
|
||||
return node
|
||||
|
||||
# 方法2:根据命名规则查找
|
||||
possible_names = [
|
||||
f"{vessel}_controller",
|
||||
f"{vessel}_separator",
|
||||
vessel, # 容器本身可能就是分离器
|
||||
"separator_1",
|
||||
"virtual_separator"
|
||||
]
|
||||
|
||||
for name in possible_names:
|
||||
if name in G.nodes():
|
||||
node_class = G.nodes[name].get('class', '').lower()
|
||||
if 'separator' in node_class:
|
||||
debug_print(f"✅ 通过命名规则找到分离器: {name}")
|
||||
return name
|
||||
|
||||
# 方法3:查找第一个分离器设备
|
||||
for node in G.nodes():
|
||||
node_class = G.nodes[node].get('class', '').lower()
|
||||
if 'separator' in node_class:
|
||||
debug_print(f"⚠️ 使用第一个分离器设备: {node}")
|
||||
return node
|
||||
|
||||
debug_print(f"⚠️ 未找到分离器设备")
|
||||
return ""
|
||||
|
||||
def generate_separate_protocol(
|
||||
G: nx.DiGraph,
|
||||
purpose: str, # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'.
|
||||
product_phase: str, # 'top' or 'bottom'. Phase that product will be in.
|
||||
from_vessel: str, #Contents of from_vessel are transferred to separation_vessel and separation is performed.
|
||||
separation_vessel: str, # Vessel in which separation of phases will be carried out.
|
||||
to_vessel: str, # Vessel to send product phase to.
|
||||
waste_phase_to_vessel: str, # Optional. Vessel to send waste phase to.
|
||||
solvent: str, # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases.
|
||||
solvent_volume: float = 50, # Optional. Volume of solvent to add (mL).
|
||||
through: str = "", # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'.
|
||||
repeats: int = 1, # Optional. Number of separations to perform.
|
||||
stir_time: float = 30, # Optional. Time stir for after adding solvent, before separation of phases.
|
||||
stir_speed: float = 300, # Optional. Speed to stir at after adding solvent, before separation of phases.
|
||||
settling_time: float = 300 # Optional. Time
|
||||
) -> list[dict]:
|
||||
G: nx.DiGraph,
|
||||
# 🔧 基础参数,支持XDL的vessel参数
|
||||
vessel: str = "", # XDL: 分离容器
|
||||
purpose: str = "separate", # 分离目的
|
||||
product_phase: str = "top", # 产物相
|
||||
# 🔧 可选的详细参数
|
||||
from_vessel: str = "", # 源容器(通常在separate前已经transfer了)
|
||||
separation_vessel: str = "", # 分离容器(与vessel同义)
|
||||
to_vessel: str = "", # 目标容器(可选)
|
||||
waste_phase_to_vessel: str = "", # 废相目标容器
|
||||
product_vessel: str = "", # XDL: 产物容器(与to_vessel同义)
|
||||
waste_vessel: str = "", # XDL: 废液容器(与waste_phase_to_vessel同义)
|
||||
# 🔧 溶剂相关参数
|
||||
solvent: str = "", # 溶剂名称
|
||||
solvent_volume: Union[str, float] = 0.0, # 溶剂体积
|
||||
volume: Union[str, float] = 0.0, # XDL: 体积(与solvent_volume同义)
|
||||
# 🔧 操作参数
|
||||
through: str = "", # 通过材料
|
||||
repeats: int = 1, # 重复次数
|
||||
stir_time: float = 30.0, # 搅拌时间(秒)
|
||||
stir_speed: float = 300.0, # 搅拌速度
|
||||
settling_time: float = 300.0, # 沉降时间(秒)
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate a protocol to clean a vessel with a solvent.
|
||||
生成分离操作的协议序列 - 修复版
|
||||
|
||||
:param G: Directed graph. Nodes are containers and pumps, edges are fluidic connections.
|
||||
:param vessel: Vessel to clean.
|
||||
:param solvent: Solvent to clean vessel with.
|
||||
:param volume: Volume of solvent to clean vessel with.
|
||||
:param temp: Temperature to heat vessel to while cleaning.
|
||||
:param repeats: Number of cleaning cycles to perform.
|
||||
:return: List of actions to clean vessel.
|
||||
支持XDL参数格式:
|
||||
- vessel: 分离容器(必需)
|
||||
- purpose: "wash", "extract", "separate"
|
||||
- product_phase: "top", "bottom"
|
||||
- product_vessel: 产物收集容器
|
||||
- waste_vessel: 废液收集容器
|
||||
- solvent: 溶剂名称
|
||||
- volume: "200 mL", "?" 或数值
|
||||
- repeats: 重复次数
|
||||
|
||||
分离流程:
|
||||
1. (可选)添加溶剂到分离容器
|
||||
2. 搅拌混合
|
||||
3. 静置分层
|
||||
4. 收集指定相到目标容器
|
||||
5. 重复指定次数
|
||||
"""
|
||||
|
||||
# 生成泵操作的动作序列
|
||||
pump_action_sequence = []
|
||||
reactor_volume = 500.0
|
||||
waste_vessel = waste_phase_to_vessel
|
||||
debug_print("=" * 60)
|
||||
debug_print("开始生成分离协议 - 修复版")
|
||||
debug_print(f"原始参数:")
|
||||
debug_print(f" - vessel: '{vessel}'")
|
||||
debug_print(f" - purpose: '{purpose}'")
|
||||
debug_print(f" - product_phase: '{product_phase}'")
|
||||
debug_print(f" - solvent: '{solvent}'")
|
||||
debug_print(f" - volume: {volume} (类型: {type(volume)})")
|
||||
debug_print(f" - repeats: {repeats}")
|
||||
debug_print(f" - product_vessel: '{product_vessel}'")
|
||||
debug_print(f" - waste_vessel: '{waste_vessel}'")
|
||||
debug_print("=" * 60)
|
||||
|
||||
# TODO:通过物料管理系统找到溶剂的容器
|
||||
if "," in solvent:
|
||||
solvents = solvent.split(",")
|
||||
assert len(solvents) == repeats, "Number of solvents must match number of repeats."
|
||||
else:
|
||||
solvents = [solvent] * repeats
|
||||
action_sequence = []
|
||||
|
||||
# TODO: 通过设备连接图找到分离容器的控制器、底部出口
|
||||
separator_controller = f"{separation_vessel}_controller"
|
||||
separation_vessel_bottom = f"flask_{separation_vessel}"
|
||||
# === 参数验证和标准化 ===
|
||||
debug_print("步骤1: 参数验证和标准化...")
|
||||
|
||||
transfer_flowrate = flowrate = 2.5
|
||||
# 统一容器参数
|
||||
final_vessel = vessel or separation_vessel
|
||||
if not final_vessel:
|
||||
raise ValueError("必须指定分离容器 (vessel 或 separation_vessel)")
|
||||
|
||||
if from_vessel != separation_vessel:
|
||||
pump_action_sequence.append(
|
||||
{
|
||||
"device_id": "",
|
||||
"action_name": "PumpTransferProtocol",
|
||||
"action_kwargs": {
|
||||
"from_vessel": from_vessel,
|
||||
"to_vessel": separation_vessel,
|
||||
"volume": reactor_volume,
|
||||
"time": reactor_volume / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
final_to_vessel = to_vessel or product_vessel
|
||||
final_waste_vessel = waste_phase_to_vessel or waste_vessel
|
||||
|
||||
# 统一体积参数
|
||||
final_volume = parse_volume_input(volume or solvent_volume)
|
||||
|
||||
# 🔧 修复:确保repeats至少为1
|
||||
if repeats <= 0:
|
||||
repeats = 1
|
||||
debug_print(f"⚠️ repeats参数 <= 0,自动设置为1")
|
||||
|
||||
debug_print(f"标准化参数:")
|
||||
debug_print(f" - 分离容器: '{final_vessel}'")
|
||||
debug_print(f" - 产物容器: '{final_to_vessel}'")
|
||||
debug_print(f" - 废液容器: '{final_waste_vessel}'")
|
||||
debug_print(f" - 溶剂体积: {final_volume}mL")
|
||||
debug_print(f" - 重复次数: {repeats}")
|
||||
|
||||
# 验证必需参数
|
||||
if not purpose:
|
||||
purpose = "separate"
|
||||
if not product_phase:
|
||||
product_phase = "top"
|
||||
if purpose not in ["wash", "extract", "separate"]:
|
||||
debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'")
|
||||
purpose = "separate"
|
||||
if product_phase not in ["top", "bottom"]:
|
||||
debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
|
||||
product_phase = "top"
|
||||
|
||||
debug_print("✅ 参数验证通过")
|
||||
|
||||
# === 查找设备 ===
|
||||
debug_print("步骤2: 查找设备...")
|
||||
|
||||
# 查找分离器设备
|
||||
separator_device = find_separator_device(G, final_vessel)
|
||||
if not separator_device:
|
||||
debug_print("⚠️ 未找到分离器设备,可能无法执行分离操作")
|
||||
|
||||
# 查找溶剂容器(如果需要)
|
||||
solvent_vessel = ""
|
||||
if solvent and solvent.strip():
|
||||
solvent_vessel = find_solvent_vessel(G, solvent)
|
||||
|
||||
debug_print(f"设备映射:")
|
||||
debug_print(f" - 分离器设备: '{separator_device}'")
|
||||
debug_print(f" - 溶剂容器: '{solvent_vessel}'")
|
||||
|
||||
# === 执行分离流程 ===
|
||||
debug_print("步骤3: 执行分离流程...")
|
||||
|
||||
try:
|
||||
for repeat_idx in range(repeats):
|
||||
debug_print(f"3.{repeat_idx+1}: 第 {repeat_idx+1}/{repeats} 次分离")
|
||||
|
||||
# 步骤3.1: 添加溶剂(如果需要)
|
||||
if solvent_vessel and final_volume > 0:
|
||||
debug_print(f"3.{repeat_idx+1}.1: 添加溶剂 {solvent} ({final_volume}mL)")
|
||||
|
||||
# 使用pump protocol添加溶剂
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent_vessel,
|
||||
to_vessel=final_vessel,
|
||||
volume=final_volume,
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="",
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.5,
|
||||
transfer_flowrate=0.5,
|
||||
rate_spec="",
|
||||
event="",
|
||||
through="",
|
||||
**kwargs
|
||||
)
|
||||
action_sequence.extend(pump_actions)
|
||||
debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作")
|
||||
|
||||
# 步骤3.2: 执行分离操作
|
||||
if separator_device:
|
||||
debug_print(f"3.{repeat_idx+1}.2: 执行分离操作")
|
||||
|
||||
# 调用分离器设备的separate方法
|
||||
separate_action = {
|
||||
"device_id": separator_device,
|
||||
"action_name": "separate",
|
||||
"action_kwargs": {
|
||||
"purpose": purpose,
|
||||
"product_phase": product_phase,
|
||||
"from_vessel": from_vessel or final_vessel,
|
||||
"separation_vessel": final_vessel,
|
||||
"to_vessel": final_to_vessel or final_vessel,
|
||||
"waste_phase_to_vessel": final_waste_vessel or final_vessel,
|
||||
"solvent": solvent,
|
||||
"solvent_volume": final_volume,
|
||||
"through": through,
|
||||
"repeats": 1, # 每次调用只做一次分离
|
||||
"stir_time": stir_time,
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": settling_time
|
||||
}
|
||||
}
|
||||
action_sequence.append(separate_action)
|
||||
debug_print(f"✅ 分离操作添加完成")
|
||||
|
||||
else:
|
||||
debug_print(f"3.{repeat_idx+1}.2: 无分离器设备,跳过分离操作")
|
||||
# 添加等待时间模拟分离
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": stir_time + settling_time}
|
||||
})
|
||||
|
||||
# 等待间隔(除了最后一次)
|
||||
if repeat_idx < repeats - 1:
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"⚠️ 分离流程执行失败: {str(e)}")
|
||||
# 添加错误日志
|
||||
action_sequence.append({
|
||||
"device_id": "system",
|
||||
"action_name": "log_message",
|
||||
"action_kwargs": {
|
||||
"message": f"分离操作失败: {str(e)}"
|
||||
}
|
||||
)
|
||||
|
||||
# for i in range(2):
|
||||
# pump_action_sequence.append(
|
||||
# {
|
||||
# "device_id": "",
|
||||
# "action_name": "CleanProtocol",
|
||||
# "action_kwargs": {
|
||||
# "vessel": from_vessel,
|
||||
# "solvent": "H2O", # Solvent to clean vessel with.
|
||||
# "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with.
|
||||
# "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning.
|
||||
# "repeats": 1
|
||||
# }
|
||||
# }
|
||||
# )
|
||||
# pump_action_sequence.append(
|
||||
# {
|
||||
# "device_id": "",
|
||||
# "action_name": "CleanProtocol",
|
||||
# "action_kwargs": {
|
||||
# "vessel": from_vessel,
|
||||
# "solvent": "CH2Cl2", # Solvent to clean vessel with.
|
||||
# "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with.
|
||||
# "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning.
|
||||
# "repeats": 1
|
||||
# }
|
||||
# }
|
||||
# )
|
||||
})
|
||||
|
||||
# 生成泵操作的动作序列
|
||||
for i in range(repeats):
|
||||
# 找到当次萃取所用溶剂
|
||||
solvent_thistime = solvents[i]
|
||||
solvent_vessel = f"flask_{solvent_thistime}"
|
||||
|
||||
pump_action_sequence.append(
|
||||
{
|
||||
"device_id": "",
|
||||
"action_name": "PumpTransferProtocol",
|
||||
"action_kwargs": {
|
||||
"from_vessel": solvent_vessel,
|
||||
"to_vessel": separation_vessel,
|
||||
"volume": solvent_volume,
|
||||
"time": solvent_volume / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
)
|
||||
pump_action_sequence.extend([
|
||||
# 搅拌、静置
|
||||
{
|
||||
"device_id": separator_controller,
|
||||
"action_name": "stir",
|
||||
"action_kwargs": {
|
||||
"stir_time": stir_time,
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": settling_time
|
||||
}
|
||||
},
|
||||
# 分液(判断电导突跃)
|
||||
{
|
||||
"device_id": separator_controller,
|
||||
"action_name": "valve_open",
|
||||
"action_kwargs": {
|
||||
"command": "delta > 0.05"
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
if product_phase == "bottom":
|
||||
# 产物转移到目标瓶
|
||||
pump_action_sequence.append(
|
||||
{
|
||||
"device_id": "",
|
||||
"action_name": "PumpTransferProtocol",
|
||||
"action_kwargs": {
|
||||
"from_vessel": separation_vessel_bottom,
|
||||
"to_vessel": to_vessel,
|
||||
"volume": 250.0,
|
||||
"time": 250.0 / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
)
|
||||
# 放出上面那一相,60秒后关阀门
|
||||
pump_action_sequence.append(
|
||||
{
|
||||
"device_id": separator_controller,
|
||||
"action_name": "valve_open",
|
||||
"action_kwargs": {
|
||||
"command": "time > 60"
|
||||
}
|
||||
}
|
||||
)
|
||||
# 弃去上面那一相进废液
|
||||
pump_action_sequence.append(
|
||||
{
|
||||
"device_id": "",
|
||||
"action_name": "PumpTransferProtocol",
|
||||
"action_kwargs": {
|
||||
"from_vessel": separation_vessel_bottom,
|
||||
"to_vessel": waste_vessel,
|
||||
"volume": 250.0,
|
||||
"time": 250.0 / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
)
|
||||
elif product_phase == "top":
|
||||
# 弃去下面那一相进废液
|
||||
pump_action_sequence.append(
|
||||
{
|
||||
"device_id": "",
|
||||
"action_name": "PumpTransferProtocol",
|
||||
"action_kwargs": {
|
||||
"from_vessel": separation_vessel_bottom,
|
||||
"to_vessel": waste_vessel,
|
||||
"volume": 250.0,
|
||||
"time": 250.0 / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
)
|
||||
# 放出上面那一相
|
||||
pump_action_sequence.append(
|
||||
{
|
||||
"device_id": separator_controller,
|
||||
"action_name": "valve_open",
|
||||
"action_kwargs": {
|
||||
"command": "time > 60"
|
||||
}
|
||||
}
|
||||
)
|
||||
# 产物转移到目标瓶
|
||||
pump_action_sequence.append(
|
||||
{
|
||||
"device_id": "",
|
||||
"action_name": "PumpTransferProtocol",
|
||||
"action_kwargs": {
|
||||
"from_vessel": separation_vessel_bottom,
|
||||
"to_vessel": to_vessel,
|
||||
"volume": 250.0,
|
||||
"time": 250.0 / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
)
|
||||
elif product_phase == "organic":
|
||||
pass
|
||||
|
||||
# 如果不是最后一次,从中转瓶转移回分液漏斗
|
||||
if i < repeats - 1:
|
||||
pump_action_sequence.append(
|
||||
{
|
||||
"device_id": "",
|
||||
"action_name": "PumpTransferProtocol",
|
||||
"action_kwargs": {
|
||||
"from_vessel": to_vessel,
|
||||
"to_vessel": separation_vessel,
|
||||
"volume": 250.0,
|
||||
"time": 250.0 / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
)
|
||||
return pump_action_sequence
|
||||
# === 最终结果 ===
|
||||
debug_print("=" * 60)
|
||||
debug_print(f"✅ 分离协议生成完成")
|
||||
debug_print(f"📊 总动作数: {len(action_sequence)}")
|
||||
debug_print(f"📋 处理总结:")
|
||||
debug_print(f" - 分离容器: {final_vessel}")
|
||||
debug_print(f" - 分离目的: {purpose}")
|
||||
debug_print(f" - 产物相: {product_phase}")
|
||||
debug_print(f" - 重复次数: {repeats}")
|
||||
if solvent:
|
||||
debug_print(f" - 溶剂: {solvent} ({final_volume}mL)")
|
||||
if final_to_vessel:
|
||||
debug_print(f" - 产物容器: {final_to_vessel}")
|
||||
if final_waste_vessel:
|
||||
debug_print(f" - 废液容器: {final_waste_vessel}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
return action_sequence
|
||||
|
||||
# === 便捷函数 ===
|
||||
|
||||
def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top",
|
||||
product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]:
|
||||
"""仅进行相分离(不添加溶剂)"""
|
||||
return generate_separate_protocol(
|
||||
G, vessel=vessel,
|
||||
purpose="separate",
|
||||
product_phase=product_phase,
|
||||
product_vessel=product_vessel,
|
||||
waste_vessel=waste_vessel
|
||||
)
|
||||
|
||||
def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
|
||||
product_phase: str = "top", repeats: int = 1) -> List[Dict[str, Any]]:
|
||||
"""用溶剂洗涤"""
|
||||
return generate_separate_protocol(
|
||||
G, vessel=vessel,
|
||||
purpose="wash",
|
||||
product_phase=product_phase,
|
||||
solvent=solvent,
|
||||
volume=volume,
|
||||
repeats=repeats
|
||||
)
|
||||
|
||||
def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
|
||||
product_phase: str = "bottom", repeats: int = 3) -> List[Dict[str, Any]]:
|
||||
"""用溶剂萃取"""
|
||||
return generate_separate_protocol(
|
||||
G, vessel=vessel,
|
||||
purpose="extract",
|
||||
product_phase=product_phase,
|
||||
solvent=solvent,
|
||||
volume=volume,
|
||||
repeats=repeats
|
||||
)
|
||||
|
||||
def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "top",
|
||||
product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]:
|
||||
"""水-有机相分离"""
|
||||
return generate_separate_protocol(
|
||||
G, vessel=vessel,
|
||||
purpose="separate",
|
||||
product_phase=organic_phase,
|
||||
product_vessel=product_vessel,
|
||||
waste_vessel=waste_vessel
|
||||
)
|
||||
|
||||
# 测试函数
|
||||
def test_separate_protocol():
|
||||
"""测试分离协议的各种参数解析"""
|
||||
print("=== SEPARATE PROTOCOL 增强版测试 ===")
|
||||
|
||||
# 测试体积解析
|
||||
volumes = ["200 mL", "?", 100.0, "1 L", "500 μL"]
|
||||
for vol in volumes:
|
||||
result = parse_volume_input(vol)
|
||||
print(f"体积解析: {vol} → {result}mL")
|
||||
|
||||
print("✅ 测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_separate_protocol()
|
||||
|
||||
@@ -1,96 +1,517 @@
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Union
|
||||
import networkx as nx
|
||||
import logging
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"[STIR] {message}", flush=True)
|
||||
logger.info(f"[STIR] {message}")
|
||||
|
||||
def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
|
||||
"""
|
||||
解析带单位的时间输入
|
||||
|
||||
Args:
|
||||
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
|
||||
default_unit: 默认单位(默认为秒)
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if not time_input:
|
||||
return 0.0
|
||||
|
||||
# 处理数值输入
|
||||
if isinstance(time_input, (int, float)):
|
||||
result = float(time_input)
|
||||
debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
|
||||
return result
|
||||
|
||||
# 处理字符串输入
|
||||
time_str = str(time_input).lower().strip()
|
||||
debug_print(f"解析时间字符串: '{time_str}'")
|
||||
|
||||
# 处理特殊值
|
||||
if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
||||
default_time = 300.0 # 5分钟默认值
|
||||
debug_print(f"检测到未知时间,使用默认值: {default_time}s")
|
||||
return default_time
|
||||
|
||||
# 如果是纯数字,使用默认单位
|
||||
try:
|
||||
value = float(time_str)
|
||||
if default_unit == "s":
|
||||
result = value
|
||||
elif default_unit in ["min", "minute"]:
|
||||
result = value * 60.0
|
||||
elif default_unit in ["h", "hour"]:
|
||||
result = value * 3600.0
|
||||
else:
|
||||
result = value # 默认秒
|
||||
debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
|
||||
return result
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 使用正则表达式匹配数字和单位
|
||||
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
|
||||
match = re.match(pattern, time_str)
|
||||
|
||||
if not match:
|
||||
debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
|
||||
return 60.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or default_unit
|
||||
|
||||
# 单位转换映射
|
||||
unit_multipliers = {
|
||||
# 秒
|
||||
's': 1.0,
|
||||
'sec': 1.0,
|
||||
'second': 1.0,
|
||||
'seconds': 1.0,
|
||||
|
||||
# 分钟
|
||||
'm': 60.0,
|
||||
'min': 60.0,
|
||||
'mins': 60.0,
|
||||
'minute': 60.0,
|
||||
'minutes': 60.0,
|
||||
|
||||
# 小时
|
||||
'h': 3600.0,
|
||||
'hr': 3600.0,
|
||||
'hrs': 3600.0,
|
||||
'hour': 3600.0,
|
||||
'hours': 3600.0,
|
||||
|
||||
# 天
|
||||
'd': 86400.0,
|
||||
'day': 86400.0,
|
||||
'days': 86400.0,
|
||||
}
|
||||
|
||||
multiplier = unit_multipliers.get(unit, 1.0)
|
||||
result = value * multiplier
|
||||
|
||||
debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
|
||||
return result
|
||||
|
||||
def parse_time_spec(time_spec: str) -> float:
|
||||
"""
|
||||
解析时间规格字符串为秒数
|
||||
|
||||
Args:
|
||||
time_spec: 时间规格字符串(如 "several minutes", "overnight", "few hours")
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if not time_spec:
|
||||
return 0.0
|
||||
|
||||
time_spec = time_spec.lower().strip()
|
||||
|
||||
# 预定义的时间规格映射
|
||||
time_spec_map = {
|
||||
# 几分钟
|
||||
"several minutes": 5.0 * 60, # 5分钟
|
||||
"few minutes": 3.0 * 60, # 3分钟
|
||||
"couple of minutes": 2.0 * 60, # 2分钟
|
||||
"a few minutes": 3.0 * 60, # 3分钟
|
||||
"some minutes": 5.0 * 60, # 5分钟
|
||||
|
||||
# 几小时
|
||||
"several hours": 3.0 * 3600, # 3小时
|
||||
"few hours": 2.0 * 3600, # 2小时
|
||||
"couple of hours": 2.0 * 3600, # 2小时
|
||||
"a few hours": 3.0 * 3600, # 3小时
|
||||
"some hours": 4.0 * 3600, # 4小时
|
||||
|
||||
# 特殊时间
|
||||
"overnight": 12.0 * 3600, # 12小时
|
||||
"over night": 12.0 * 3600, # 12小时
|
||||
"morning": 4.0 * 3600, # 4小时
|
||||
"afternoon": 6.0 * 3600, # 6小时
|
||||
"evening": 4.0 * 3600, # 4小时
|
||||
|
||||
# 短时间
|
||||
"briefly": 30.0, # 30秒
|
||||
"momentarily": 10.0, # 10秒
|
||||
"quickly": 60.0, # 1分钟
|
||||
"slowly": 10.0 * 60, # 10分钟
|
||||
|
||||
# 长时间
|
||||
"extended": 6.0 * 3600, # 6小时
|
||||
"prolonged": 8.0 * 3600, # 8小时
|
||||
"extensively": 12.0 * 3600, # 12小时
|
||||
}
|
||||
|
||||
# 直接匹配
|
||||
if time_spec in time_spec_map:
|
||||
result = time_spec_map[time_spec]
|
||||
debug_print(f"时间规格解析: '{time_spec}' → {result/60:.1f}分钟")
|
||||
return result
|
||||
|
||||
# 模糊匹配
|
||||
for spec, value in time_spec_map.items():
|
||||
if spec in time_spec or time_spec in spec:
|
||||
result = value
|
||||
debug_print(f"时间规格模糊匹配: '{time_spec}' → '{spec}' → {result/60:.1f}分钟")
|
||||
return result
|
||||
|
||||
# 如果无法识别,返回默认值
|
||||
default_time = 5.0 * 60 # 5分钟
|
||||
debug_print(f"⚠️ 无法识别时间规格: '{time_spec}',使用默认值: {default_time/60:.1f}分钟")
|
||||
return default_time
|
||||
|
||||
def parse_time_string(time_str: str) -> float:
|
||||
"""
|
||||
解析时间字符串为秒数,支持多种单位
|
||||
|
||||
Args:
|
||||
time_str: 时间字符串(如 "0.5 h", "30 min", "120 s", "2.5")
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if not time_str:
|
||||
return 0.0
|
||||
|
||||
# 如果是纯数字,默认单位为秒
|
||||
try:
|
||||
return float(time_str)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 清理字符串
|
||||
time_str = time_str.lower().strip()
|
||||
|
||||
# 使用正则表达式匹配数字和单位
|
||||
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
|
||||
match = re.match(pattern, time_str)
|
||||
|
||||
if not match:
|
||||
debug_print(f"⚠️ 无法解析时间字符串: '{time_str}',使用默认值: 60秒")
|
||||
return 60.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2)
|
||||
|
||||
# 单位转换映射
|
||||
unit_map = {
|
||||
# 秒
|
||||
's': 1.0,
|
||||
'sec': 1.0,
|
||||
'second': 1.0,
|
||||
'seconds': 1.0,
|
||||
|
||||
# 分钟
|
||||
'm': 60.0,
|
||||
'min': 60.0,
|
||||
'mins': 60.0,
|
||||
'minute': 60.0,
|
||||
'minutes': 60.0,
|
||||
|
||||
# 小时
|
||||
'h': 3600.0,
|
||||
'hr': 3600.0,
|
||||
'hrs': 3600.0,
|
||||
'hour': 3600.0,
|
||||
'hours': 3600.0,
|
||||
|
||||
# 天
|
||||
'd': 86400.0,
|
||||
'day': 86400.0,
|
||||
'days': 86400.0,
|
||||
|
||||
# 如果没有单位,默认为秒
|
||||
'': 1.0,
|
||||
}
|
||||
|
||||
multiplier = unit_map.get(unit, 1.0)
|
||||
result = value * multiplier
|
||||
|
||||
debug_print(f"时间字符串解析: '{time_str}' → {value} {unit or 'seconds'} → {result}秒")
|
||||
return result
|
||||
|
||||
def parse_time_input(time_input: Union[str, float, int], time_spec: str = "") -> float:
|
||||
"""
|
||||
统一的时间输入解析函数
|
||||
|
||||
Args:
|
||||
time_input: 时间输入(可以是字符串、浮点数或整数)
|
||||
time_spec: 时间规格字符串(优先级高于time_input)
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
# 优先处理 time_spec
|
||||
if time_spec:
|
||||
return parse_time_spec(time_spec)
|
||||
|
||||
# 处理 time_input
|
||||
if isinstance(time_input, (int, float)):
|
||||
# 数字默认单位为秒
|
||||
result = float(time_input)
|
||||
debug_print(f"数字时间输入: {time_input} → {result}秒")
|
||||
return result
|
||||
|
||||
if isinstance(time_input, str):
|
||||
return parse_time_string(time_input)
|
||||
|
||||
# 默认值
|
||||
debug_print(f"⚠️ 无法处理时间输入: {time_input},使用默认值: 60秒")
|
||||
return 60.0
|
||||
|
||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
|
||||
"""
|
||||
查找与指定容器相连的搅拌设备,或查找可用的搅拌设备
|
||||
"""
|
||||
debug_print(f"查找搅拌设备,目标容器: {vessel}")
|
||||
|
||||
# 查找所有搅拌设备节点
|
||||
stirrer_nodes = [node for node in G.nodes()
|
||||
if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
|
||||
stirrer_nodes = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if 'stirrer' in node_class.lower() or 'virtual_stirrer' in node_class:
|
||||
stirrer_nodes.append(node)
|
||||
debug_print(f"找到搅拌设备: {node}")
|
||||
|
||||
if vessel:
|
||||
# 检查哪个搅拌设备与目标容器相连(机械连接)
|
||||
for stirrer in stirrer_nodes:
|
||||
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
|
||||
debug_print(f"搅拌设备 '{stirrer}' 与容器 '{vessel}' 相连")
|
||||
return stirrer
|
||||
|
||||
# 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备
|
||||
if stirrer_nodes:
|
||||
debug_print(f"使用第一个搅拌设备: {stirrer_nodes[0]}")
|
||||
return stirrer_nodes[0]
|
||||
|
||||
raise ValueError("系统中未找到可用的搅拌设备")
|
||||
|
||||
debug_print("未找到搅拌设备,使用默认设备")
|
||||
return "stirrer_1" # 默认设备
|
||||
|
||||
def generate_stir_protocol(
|
||||
G: nx.DiGraph,
|
||||
stir_time: float,
|
||||
stir_speed: float,
|
||||
settling_time: float
|
||||
vessel: str,
|
||||
time: Union[str, float, int] = "300", # 🔧 修改:默认为字符串
|
||||
stir_time: Union[str, float, int] = "0", # 🔧 修改:支持字符串
|
||||
time_spec: str = "",
|
||||
event: str = "",
|
||||
stir_speed: float = 200.0,
|
||||
settling_time: Union[str, float] = "60", # 🔧 修改:支持字符串
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成搅拌操作的协议序列 - 定时搅拌 + 沉降
|
||||
生成搅拌操作的协议序列 - 支持单位
|
||||
|
||||
Args:
|
||||
G: 设备图
|
||||
vessel: 搅拌容器名称(必需)
|
||||
time: 搅拌时间(支持 "5 min", "300", "0.5 h" 等)
|
||||
stir_time: 搅拌时间(与time等效,支持单位)
|
||||
time_spec: 时间规格(优先级最高)
|
||||
event: 事件标识
|
||||
stir_speed: 搅拌速度 (RPM),默认200 RPM
|
||||
settling_time: 沉降时间(支持单位,默认60秒)
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 搅拌操作的动作序列
|
||||
"""
|
||||
|
||||
debug_print("=" * 50)
|
||||
debug_print("开始生成搅拌协议(支持单位)")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - vessel: {vessel}")
|
||||
debug_print(f" - time: {time}")
|
||||
debug_print(f" - stir_time: {stir_time}")
|
||||
debug_print(f" - time_spec: {time_spec}")
|
||||
debug_print(f" - event: {event}")
|
||||
debug_print(f" - stir_speed: {stir_speed}")
|
||||
debug_print(f" - settling_time: {settling_time}")
|
||||
debug_print(f" - 其他参数: {kwargs}")
|
||||
debug_print("=" * 50)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
print(f"STIR: 开始生成搅拌协议")
|
||||
print(f" - 搅拌时间: {stir_time}秒")
|
||||
print(f" - 搅拌速度: {stir_speed} RPM")
|
||||
print(f" - 沉降时间: {settling_time}秒")
|
||||
# === 参数验证 ===
|
||||
debug_print("步骤1: 参数验证...")
|
||||
|
||||
# 验证必需参数
|
||||
if not vessel:
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
debug_print(f"✅ 参数验证通过")
|
||||
|
||||
# === 🔧 新增:单位解析处理 ===
|
||||
debug_print("步骤2: 单位解析处理...")
|
||||
|
||||
# 确定实际使用的时间值(stir_time优先)
|
||||
actual_time_input = stir_time if stir_time not in ["0", 0, 0.0] else time
|
||||
|
||||
# 解析时间(time_spec > actual_time_input)
|
||||
if time_spec:
|
||||
parsed_time = parse_time_spec(time_spec) # 使用现有的time_spec解析
|
||||
debug_print(f"使用time_spec: '{time_spec}' → {parsed_time}s")
|
||||
else:
|
||||
parsed_time = parse_time_with_units(actual_time_input, "s")
|
||||
debug_print(f"解析时间: {actual_time_input} → {parsed_time}s")
|
||||
|
||||
# 解析沉降时间
|
||||
parsed_settling_time = parse_time_with_units(settling_time, "s")
|
||||
debug_print(f"解析沉降时间: {settling_time} → {parsed_settling_time}s")
|
||||
|
||||
debug_print(f"时间解析结果:")
|
||||
debug_print(f" - 原始输入: time={time}, stir_time={stir_time}")
|
||||
debug_print(f" - 时间规格: {time_spec}")
|
||||
debug_print(f" - 最终搅拌时间: {parsed_time}s ({parsed_time/60:.1f}分钟)")
|
||||
debug_print(f" - 最终沉降时间: {parsed_settling_time}s ({parsed_settling_time/60:.1f}分钟)")
|
||||
|
||||
# 修正参数范围
|
||||
if parsed_time < 0:
|
||||
debug_print(f"搅拌时间 {parsed_time}s 无效,修正为 300s")
|
||||
parsed_time = 300.0
|
||||
elif parsed_time > 7200:
|
||||
debug_print(f"搅拌时间 {parsed_time}s 过长,修正为 3600s")
|
||||
parsed_time = 3600.0
|
||||
|
||||
if stir_speed < 10.0:
|
||||
debug_print(f"搅拌速度 {stir_speed} RPM 过低,修正为 100 RPM")
|
||||
stir_speed = 100.0
|
||||
elif stir_speed > 1500.0:
|
||||
debug_print(f"搅拌速度 {stir_speed} RPM 过高,修正为 1000 RPM")
|
||||
stir_speed = 1000.0
|
||||
|
||||
if parsed_settling_time < 0:
|
||||
debug_print(f"沉降时间 {parsed_settling_time}s 无效,修正为 60s")
|
||||
parsed_settling_time = 60.0
|
||||
elif parsed_settling_time > 1800:
|
||||
debug_print(f"沉降时间 {parsed_settling_time}s 过长,修正为 600s")
|
||||
parsed_settling_time = 600.0
|
||||
|
||||
# === 查找搅拌设备 ===
|
||||
debug_print("步骤3: 查找搅拌设备...")
|
||||
|
||||
# 查找搅拌设备
|
||||
try:
|
||||
stirrer_id = find_connected_stirrer(G)
|
||||
print(f"STIR: 找到搅拌设备: {stirrer_id}")
|
||||
except ValueError as e:
|
||||
stirrer_id = find_connected_stirrer(G, vessel)
|
||||
debug_print(f"设备配置: 搅拌设备 = {stirrer_id}")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)}")
|
||||
raise ValueError(f"无法找到搅拌设备: {str(e)}")
|
||||
|
||||
# 执行搅拌操作
|
||||
# === 执行搅拌操作 ===
|
||||
debug_print("步骤4: 执行搅拌操作...")
|
||||
|
||||
# 构建搅拌动作参数
|
||||
stir_kwargs = {
|
||||
"vessel": vessel,
|
||||
"time": str(time), # 保持原始字符串格式
|
||||
"event": event,
|
||||
"time_spec": time_spec,
|
||||
"stir_time": parsed_time, # 解析后的时间(秒)
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": parsed_settling_time # 解析后的沉降时间(秒)
|
||||
}
|
||||
|
||||
debug_print(f"搅拌参数: {stir_kwargs}")
|
||||
|
||||
stir_action = {
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stir",
|
||||
"action_kwargs": {
|
||||
"stir_time": stir_time,
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": settling_time
|
||||
}
|
||||
"action_kwargs": stir_kwargs
|
||||
}
|
||||
|
||||
action_sequence.append(stir_action)
|
||||
|
||||
print(f"STIR: 生成了 {len(action_sequence)} 个动作")
|
||||
# === 总结 ===
|
||||
debug_print("=" * 50)
|
||||
debug_print(f"搅拌协议生成完成(支持单位)")
|
||||
debug_print(f"总动作数: {len(action_sequence)}")
|
||||
debug_print(f"搅拌容器: {vessel}")
|
||||
debug_print(f"搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {parsed_settling_time}s")
|
||||
debug_print("=" * 50)
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
def generate_start_stir_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
stir_speed: float,
|
||||
purpose: str
|
||||
stir_speed: float = 200.0,
|
||||
purpose: str = "",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成开始搅拌操作的协议序列 - 持续搅拌
|
||||
|
||||
Args:
|
||||
G: 设备图
|
||||
vessel: 搅拌容器名称(必需)
|
||||
stir_speed: 搅拌速度 (RPM),默认200 RPM
|
||||
purpose: 搅拌目的(可选)
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 开始搅拌操作的动作序列
|
||||
"""
|
||||
|
||||
debug_print("=" * 50)
|
||||
debug_print("开始生成启动搅拌协议")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - vessel: {vessel}")
|
||||
debug_print(f" - stir_speed: {stir_speed} RPM")
|
||||
debug_print(f" - purpose: {purpose}")
|
||||
debug_print(f" - 其他参数: {kwargs}")
|
||||
debug_print("=" * 50)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
print(f"START_STIR: 开始生成启动搅拌协议")
|
||||
print(f" - 容器: {vessel}")
|
||||
print(f" - 搅拌速度: {stir_speed} RPM")
|
||||
print(f" - 目的: {purpose}")
|
||||
# === 参数验证 ===
|
||||
debug_print("步骤1: 参数验证...")
|
||||
|
||||
# 验证必需参数
|
||||
if not vessel:
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
# 验证容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 查找搅拌设备
|
||||
# 修正参数范围
|
||||
if stir_speed < 10.0:
|
||||
debug_print(f"搅拌速度 {stir_speed} RPM 过低,修正为 100 RPM")
|
||||
stir_speed = 100.0
|
||||
elif stir_speed > 1500.0:
|
||||
debug_print(f"搅拌速度 {stir_speed} RPM 过高,修正为 1000 RPM")
|
||||
stir_speed = 1000.0
|
||||
|
||||
debug_print(f"✅ 参数验证通过")
|
||||
|
||||
# === 查找搅拌设备 ===
|
||||
debug_print("步骤2: 查找搅拌设备...")
|
||||
|
||||
try:
|
||||
stirrer_id = find_connected_stirrer(G, vessel)
|
||||
print(f"START_STIR: 找到搅拌设备: {stirrer_id}")
|
||||
except ValueError as e:
|
||||
debug_print(f"设备配置: 搅拌设备 = {stirrer_id}")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)}")
|
||||
raise ValueError(f"无法找到搅拌设备: {str(e)}")
|
||||
|
||||
# 执行开始搅拌操作
|
||||
# === 执行开始搅拌操作 ===
|
||||
debug_print("步骤3: 执行开始搅拌操作...")
|
||||
|
||||
start_stir_action = {
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
@@ -103,34 +524,69 @@ def generate_start_stir_protocol(
|
||||
|
||||
action_sequence.append(start_stir_action)
|
||||
|
||||
print(f"START_STIR: 生成了 {len(action_sequence)} 个动作")
|
||||
# === 总结 ===
|
||||
debug_print("=" * 50)
|
||||
debug_print(f"启动搅拌协议生成完成")
|
||||
debug_print(f"总动作数: {len(action_sequence)}")
|
||||
debug_print(f"搅拌容器: {vessel}")
|
||||
debug_print(f"搅拌速度: {stir_speed} RPM")
|
||||
debug_print(f"搅拌目的: {purpose}")
|
||||
debug_print("=" * 50)
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
def generate_stop_stir_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str
|
||||
vessel: str,
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成停止搅拌操作的协议序列
|
||||
|
||||
Args:
|
||||
G: 设备图
|
||||
vessel: 搅拌容器名称(必需)
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 停止搅拌操作的动作序列
|
||||
"""
|
||||
|
||||
debug_print("=" * 50)
|
||||
debug_print("开始生成停止搅拌协议")
|
||||
debug_print(f"输入参数:")
|
||||
debug_print(f" - vessel: {vessel}")
|
||||
debug_print(f" - 其他参数: {kwargs}")
|
||||
debug_print("=" * 50)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
print(f"STOP_STIR: 开始生成停止搅拌协议")
|
||||
print(f" - 容器: {vessel}")
|
||||
# === 参数验证 ===
|
||||
debug_print("步骤1: 参数验证...")
|
||||
|
||||
# 验证必需参数
|
||||
if not vessel:
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
# 验证容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
|
||||
# 查找搅拌设备
|
||||
debug_print(f"✅ 参数验证通过")
|
||||
|
||||
# === 查找搅拌设备 ===
|
||||
debug_print("步骤2: 查找搅拌设备...")
|
||||
|
||||
try:
|
||||
stirrer_id = find_connected_stirrer(G, vessel)
|
||||
print(f"STOP_STIR: 找到搅拌设备: {stirrer_id}")
|
||||
except ValueError as e:
|
||||
debug_print(f"设备配置: 搅拌设备 = {stirrer_id}")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)}")
|
||||
raise ValueError(f"无法找到搅拌设备: {str(e)}")
|
||||
|
||||
# 执行停止搅拌操作
|
||||
# === 执行停止搅拌操作 ===
|
||||
debug_print("步骤3: 执行停止搅拌操作...")
|
||||
|
||||
stop_stir_action = {
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stop_stir",
|
||||
@@ -141,26 +597,11 @@ def generate_stop_stir_protocol(
|
||||
|
||||
action_sequence.append(stop_stir_action)
|
||||
|
||||
print(f"STOP_STIR: 生成了 {len(action_sequence)} 个动作")
|
||||
# === 总结 ===
|
||||
debug_print("=" * 50)
|
||||
debug_print(f"停止搅拌协议生成完成")
|
||||
debug_print(f"总动作数: {len(action_sequence)}")
|
||||
debug_print(f"搅拌容器: {vessel}")
|
||||
debug_print("=" * 50)
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def generate_fast_stir_protocol(
|
||||
G: nx.DiGraph,
|
||||
time: float = 300.0,
|
||||
speed: float = 800.0,
|
||||
settling: float = 60.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""快速搅拌的便捷函数"""
|
||||
return generate_stir_protocol(G, time, speed, settling)
|
||||
|
||||
|
||||
def generate_gentle_stir_protocol(
|
||||
G: nx.DiGraph,
|
||||
time: float = 600.0,
|
||||
speed: float = 200.0,
|
||||
settling: float = 120.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""温和搅拌的便捷函数"""
|
||||
return generate_stir_protocol(G, time, speed, settling)
|
||||
206
unilabos/compile/utils/unit_parser.py
Normal file
206
unilabos/compile/utils/unit_parser.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
统一的单位解析工具模块
|
||||
支持时间、体积、质量等各种单位的解析
|
||||
"""
|
||||
|
||||
import re
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message, prefix="[UNIT_PARSER]"):
|
||||
"""调试输出"""
|
||||
print(f"{prefix} {message}", flush=True)
|
||||
logger.info(f"{prefix} {message}")
|
||||
|
||||
def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
|
||||
"""
|
||||
解析带单位的时间输入
|
||||
|
||||
Args:
|
||||
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
|
||||
default_unit: 默认单位(默认为秒)
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if not time_input:
|
||||
return 0.0
|
||||
|
||||
# 处理数值输入
|
||||
if isinstance(time_input, (int, float)):
|
||||
result = float(time_input)
|
||||
debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
|
||||
return result
|
||||
|
||||
# 处理字符串输入
|
||||
time_str = str(time_input).lower().strip()
|
||||
debug_print(f"解析时间字符串: '{time_str}'")
|
||||
|
||||
# 处理特殊值
|
||||
if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
||||
default_time = 300.0 # 5分钟默认值
|
||||
debug_print(f"检测到未知时间,使用默认值: {default_time}s")
|
||||
return default_time
|
||||
|
||||
# 如果是纯数字,使用默认单位
|
||||
try:
|
||||
value = float(time_str)
|
||||
if default_unit == "s":
|
||||
result = value
|
||||
elif default_unit in ["min", "minute"]:
|
||||
result = value * 60.0
|
||||
elif default_unit in ["h", "hour"]:
|
||||
result = value * 3600.0
|
||||
else:
|
||||
result = value # 默认秒
|
||||
debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
|
||||
return result
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 使用正则表达式匹配数字和单位
|
||||
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
|
||||
match = re.match(pattern, time_str)
|
||||
|
||||
if not match:
|
||||
debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
|
||||
return 60.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or default_unit
|
||||
|
||||
# 单位转换映射
|
||||
unit_multipliers = {
|
||||
# 秒
|
||||
's': 1.0,
|
||||
'sec': 1.0,
|
||||
'second': 1.0,
|
||||
'seconds': 1.0,
|
||||
|
||||
# 分钟
|
||||
'm': 60.0,
|
||||
'min': 60.0,
|
||||
'mins': 60.0,
|
||||
'minute': 60.0,
|
||||
'minutes': 60.0,
|
||||
|
||||
# 小时
|
||||
'h': 3600.0,
|
||||
'hr': 3600.0,
|
||||
'hrs': 3600.0,
|
||||
'hour': 3600.0,
|
||||
'hours': 3600.0,
|
||||
|
||||
# 天
|
||||
'd': 86400.0,
|
||||
'day': 86400.0,
|
||||
'days': 86400.0,
|
||||
}
|
||||
|
||||
multiplier = unit_multipliers.get(unit, 1.0)
|
||||
result = value * multiplier
|
||||
|
||||
debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
|
||||
return result
|
||||
|
||||
def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
|
||||
"""
|
||||
解析带单位的体积输入
|
||||
|
||||
Args:
|
||||
volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0)
|
||||
default_unit: 默认单位(默认为毫升)
|
||||
|
||||
Returns:
|
||||
float: 体积(毫升)
|
||||
"""
|
||||
if not volume_input:
|
||||
return 0.0
|
||||
|
||||
# 处理数值输入
|
||||
if isinstance(volume_input, (int, float)):
|
||||
result = float(volume_input)
|
||||
debug_print(f"数值体积输入: {volume_input} → {result}mL(默认单位)")
|
||||
return result
|
||||
|
||||
# 处理字符串输入
|
||||
volume_str = str(volume_input).lower().strip()
|
||||
debug_print(f"解析体积字符串: '{volume_str}'")
|
||||
|
||||
# 处理特殊值
|
||||
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
||||
default_volume = 50.0 # 50mL默认值
|
||||
debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
|
||||
return default_volume
|
||||
|
||||
# 如果是纯数字,使用默认单位
|
||||
try:
|
||||
value = float(volume_str)
|
||||
if default_unit.lower() in ["ml", "milliliter"]:
|
||||
result = value
|
||||
elif default_unit.lower() in ["l", "liter"]:
|
||||
result = value * 1000.0
|
||||
elif default_unit.lower() in ["μl", "ul", "microliter"]:
|
||||
result = value / 1000.0
|
||||
else:
|
||||
result = value # 默认mL
|
||||
debug_print(f"纯数字输入: {volume_str} → {result}mL(单位: {default_unit})")
|
||||
return result
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 移除空格并提取数字和单位
|
||||
volume_clean = re.sub(r'\s+', '', volume_str)
|
||||
|
||||
# 匹配数字和单位的正则表达式
|
||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
|
||||
|
||||
if not match:
|
||||
debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL")
|
||||
return 50.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or default_unit.lower()
|
||||
|
||||
# 转换为毫升
|
||||
if unit in ['l', 'liter']:
|
||||
volume = value * 1000.0 # L -> mL
|
||||
elif unit in ['μl', 'ul', 'microliter']:
|
||||
volume = value / 1000.0 # μL -> mL
|
||||
else: # ml, milliliter 或默认
|
||||
volume = value # 已经是mL
|
||||
|
||||
debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL")
|
||||
return volume
|
||||
|
||||
# 测试函数
|
||||
def test_unit_parser():
|
||||
"""测试单位解析功能"""
|
||||
print("=== 单位解析器测试 ===")
|
||||
|
||||
# 测试时间解析
|
||||
time_tests = [
|
||||
"30 min", "1 h", "300", "5.5 h", "?", 60.0, "2 hours", "30 s"
|
||||
]
|
||||
|
||||
print("\n时间解析测试:")
|
||||
for time_input in time_tests:
|
||||
result = parse_time_with_units(time_input)
|
||||
print(f" {time_input} → {result}s ({result/60:.1f}min)")
|
||||
|
||||
# 测试体积解析
|
||||
volume_tests = [
|
||||
"100 mL", "2.5 L", "500", "?", 100.0, "500 μL", "1 liter"
|
||||
]
|
||||
|
||||
print("\n体积解析测试:")
|
||||
for volume_input in volume_tests:
|
||||
result = parse_volume_with_units(volume_input)
|
||||
print(f" {volume_input} → {result}mL")
|
||||
|
||||
print("\n✅ 测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_unit_parser()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,12 @@
|
||||
import rtde_control
|
||||
import dashboard_client
|
||||
try:
|
||||
import rtde_control
|
||||
import dashboard_client
|
||||
import rtde_receive
|
||||
except ImportError as ex:
|
||||
print("Import Error, Please Install Packages in ur_arm_task.py First!", ex)
|
||||
import time
|
||||
import json
|
||||
from unilabos.devices.agv.robotiq_gripper import RobotiqGripper
|
||||
import rtde_receive
|
||||
from std_msgs.msg import Float64MultiArray
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -234,71 +234,71 @@ class Laiyu:
|
||||
resp_reset = self.reset()
|
||||
return actual_mass_mg
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
'''
|
||||
样例:对单个粉筒进行称量
|
||||
'''
|
||||
|
||||
modbus = Laiyu(port="COM25")
|
||||
|
||||
mass_test = modbus.add_powder_tube(1, 'h12', 6.0)
|
||||
print(f"实际出料质量:{mass_test}mg")
|
||||
|
||||
|
||||
'''
|
||||
样例:对单个粉筒进行称量
|
||||
'''
|
||||
'''
|
||||
样例: 对一份excel文件记录的化合物进行称量
|
||||
'''
|
||||
|
||||
modbus = Laiyu(port="COM25")
|
||||
excel_file = r"C:\auto\laiyu\test1.xlsx"
|
||||
# 定义输出文件路径,用于记录实际加样多少
|
||||
output_file = r"C:\auto\laiyu\test_output.xlsx"
|
||||
|
||||
mass_test = modbus.add_powder_tube(1, 'h12', 6.0)
|
||||
print(f"实际出料质量:{mass_test}mg")
|
||||
# 定义物料名称和料筒位置关系
|
||||
compound_positions = {
|
||||
'XPhos': '1',
|
||||
'Cu(OTf)2': '2',
|
||||
'CuSO4': '3',
|
||||
'PPh3': '4',
|
||||
}
|
||||
|
||||
# read excel file
|
||||
# excel_file = r"C:\auto\laiyu\test.xlsx"
|
||||
df = pd.read_excel(excel_file, sheet_name='Sheet1')
|
||||
# 读取Excel文件中的数据
|
||||
# 遍历每一行数据
|
||||
for index, row in df.iterrows():
|
||||
# 获取物料名称和质量
|
||||
copper_name = row['copper']
|
||||
copper_mass = row['copper_mass']
|
||||
ligand_name = row['ligand']
|
||||
ligand_mass = row['ligand_mass']
|
||||
target_tube_position = row['position']
|
||||
# 获取物料位置 from compound_positions
|
||||
copper_position = compound_positions.get(copper_name)
|
||||
ligand_position = compound_positions.get(ligand_name)
|
||||
# 判断物料位置是否存在
|
||||
if copper_position is None:
|
||||
print(f"物料位置不存在:{copper_name}")
|
||||
continue
|
||||
if ligand_position is None:
|
||||
print(f"物料位置不存在:{ligand_name}")
|
||||
continue
|
||||
# 加铜
|
||||
copper_actual_mass = modbus.add_powder_tube(int(copper_position), target_tube_position, copper_mass)
|
||||
time.sleep(1)
|
||||
# 加配体
|
||||
ligand_actual_mass = modbus.add_powder_tube(int(ligand_position), target_tube_position, ligand_mass)
|
||||
time.sleep(1)
|
||||
# 保存至df
|
||||
df.at[index, 'copper_actual_mass'] = copper_actual_mass
|
||||
df.at[index, 'ligand_actual_mass'] = ligand_actual_mass
|
||||
|
||||
'''
|
||||
样例: 对一份excel文件记录的化合物进行称量
|
||||
'''
|
||||
# 保存修改后的数据到新的Excel文件
|
||||
df.to_excel(output_file, index=False)
|
||||
print(f"已保存到文件:{output_file}")
|
||||
|
||||
excel_file = r"C:\auto\laiyu\test1.xlsx"
|
||||
# 定义输出文件路径,用于记录实际加样多少
|
||||
output_file = r"C:\auto\laiyu\test_output.xlsx"
|
||||
|
||||
# 定义物料名称和料筒位置关系
|
||||
compound_positions = {
|
||||
'XPhos': '1',
|
||||
'Cu(OTf)2': '2',
|
||||
'CuSO4': '3',
|
||||
'PPh3': '4',
|
||||
}
|
||||
|
||||
# read excel file
|
||||
# excel_file = r"C:\auto\laiyu\test.xlsx"
|
||||
df = pd.read_excel(excel_file, sheet_name='Sheet1')
|
||||
# 读取Excel文件中的数据
|
||||
# 遍历每一行数据
|
||||
for index, row in df.iterrows():
|
||||
# 获取物料名称和质量
|
||||
copper_name = row['copper']
|
||||
copper_mass = row['copper_mass']
|
||||
ligand_name = row['ligand']
|
||||
ligand_mass = row['ligand_mass']
|
||||
target_tube_position = row['position']
|
||||
# 获取物料位置 from compound_positions
|
||||
copper_position = compound_positions.get(copper_name)
|
||||
ligand_position = compound_positions.get(ligand_name)
|
||||
# 判断物料位置是否存在
|
||||
if copper_position is None:
|
||||
print(f"物料位置不存在:{copper_name}")
|
||||
continue
|
||||
if ligand_position is None:
|
||||
print(f"物料位置不存在:{ligand_name}")
|
||||
continue
|
||||
# 加铜
|
||||
copper_actual_mass = modbus.add_powder_tube(int(copper_position), target_tube_position, copper_mass)
|
||||
time.sleep(1)
|
||||
# 加配体
|
||||
ligand_actual_mass = modbus.add_powder_tube(int(ligand_position), target_tube_position, ligand_mass)
|
||||
time.sleep(1)
|
||||
# 保存至df
|
||||
df.at[index, 'copper_actual_mass'] = copper_actual_mass
|
||||
df.at[index, 'ligand_actual_mass'] = ligand_actual_mass
|
||||
|
||||
# 保存修改后的数据到新的Excel文件
|
||||
df.to_excel(output_file, index=False)
|
||||
print(f"已保存到文件:{output_file}")
|
||||
|
||||
# 关闭串口
|
||||
modbus.ser.close()
|
||||
print("串口已关闭")
|
||||
# 关闭串口
|
||||
modbus.ser.close()
|
||||
print("串口已关闭")
|
||||
|
||||
|
||||
328
unilabos/devices/liquid_handling/prcxi/prcxi.py
Normal file
328
unilabos/devices/liquid_handling/prcxi/prcxi.py
Normal file
@@ -0,0 +1,328 @@
|
||||
import socket, json, contextlib
|
||||
from typing import Any, List, Dict, Optional
|
||||
|
||||
|
||||
class PRCXIError(RuntimeError):
|
||||
"""Lilith 返回 Success=false 时抛出的业务异常"""
|
||||
|
||||
|
||||
class PRCXI9300:
|
||||
|
||||
def __init__(self, host: str = "127.0.0.1", port: int = 9999,
|
||||
timeout: float = 10.0) -> None:
|
||||
self.host, self.port, self.timeout = host, port, timeout
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _len_prefix(n: int) -> bytes:
|
||||
return bytes.fromhex(format(n, "016x"))
|
||||
|
||||
def _raw_request(self, payload: str) -> str:
|
||||
with contextlib.closing(socket.socket()) as sock:
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect((self.host, self.port))
|
||||
data = payload.encode()
|
||||
sock.sendall(self._len_prefix(len(data)) + data)
|
||||
|
||||
chunks, first = [], True
|
||||
while True:
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
if first:
|
||||
chunk, first = chunk[8:], False
|
||||
chunks.append(chunk)
|
||||
return b"".join(chunks).decode()
|
||||
|
||||
def _call(self, service: str, method: str,
|
||||
params: Optional[list] = None) -> Any:
|
||||
payload = json.dumps(
|
||||
{"ServiceName": service,
|
||||
"MethodName": method,
|
||||
"Paramters": params or []},
|
||||
separators=(",", ":")
|
||||
)
|
||||
resp = json.loads(self._raw_request(payload))
|
||||
if not resp.get("Success", False):
|
||||
raise PRCXIError(resp.get("Msg", "Unknown error"))
|
||||
data = resp.get("Data")
|
||||
try:
|
||||
return json.loads(data)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return data
|
||||
|
||||
# ---------------------------------------------------- 方案相关(ISolution)
|
||||
def list_solutions(self) -> List[Dict[str, Any]]:
|
||||
"""GetSolutionList"""
|
||||
return self._call("ISolution", "GetSolutionList")
|
||||
|
||||
def load_solution(self, solution_id: str) -> bool:
|
||||
"""LoadSolution"""
|
||||
return self._call("ISolution", "LoadSolution", [solution_id])
|
||||
|
||||
def add_solution(self, name: str, matrix_id: str,
|
||||
steps: List[Dict[str, Any]]) -> str:
|
||||
"""AddSolution → 返回新方案 GUID"""
|
||||
return self._call("ISolution", "AddSolution",
|
||||
[name, matrix_id, steps])
|
||||
|
||||
# ---------------------------------------------------- 自动化控制(IAutomation)
|
||||
def start(self) -> bool:
|
||||
return self._call("IAutomation", "Start")
|
||||
|
||||
def stop(self) -> bool:
|
||||
"""Stop"""
|
||||
return self._call("IAutomation", "Stop")
|
||||
|
||||
def reset(self) -> bool:
|
||||
"""Reset"""
|
||||
return self._call("IAutomation", "Reset")
|
||||
|
||||
def pause(self) -> bool:
|
||||
"""Pause"""
|
||||
return self._call("IAutomation", "Pause")
|
||||
|
||||
def resume(self) -> bool:
|
||||
"""Resume"""
|
||||
return self._call("IAutomation", "Resume")
|
||||
|
||||
def get_error_code(self) -> Optional[str]:
|
||||
"""GetErrorCode"""
|
||||
return self._call("IAutomation", "GetErrorCode")
|
||||
|
||||
def clear_error_code(self) -> bool:
|
||||
"""RemoveErrorCodet"""
|
||||
return self._call("IAutomation", "RemoveErrorCodet")
|
||||
|
||||
# ---------------------------------------------------- 运行状态(IMachineState)
|
||||
def step_state_list(self) -> List[Dict[str, Any]]:
|
||||
"""GetStepStateList"""
|
||||
return self._call("IMachineState", "GetStepStateList")
|
||||
|
||||
def step_status(self, seq_num: int) -> Dict[str, Any]:
|
||||
"""GetStepStatus"""
|
||||
return self._call("IMachineState", "GetStepStatus", [seq_num])
|
||||
|
||||
def step_state(self, seq_num: int) -> Dict[str, Any]:
|
||||
"""GetStepState"""
|
||||
return self._call("IMachineState", "GetStepState", [seq_num])
|
||||
|
||||
def axis_location(self, axis_num: int = 1) -> Dict[str, Any]:
|
||||
"""GetLocation"""
|
||||
return self._call("IMachineState", "GetLocation", [axis_num])
|
||||
|
||||
# ---------------------------------------------------- 版位矩阵(IMatrix)
|
||||
def list_matrices(self) -> List[Dict[str, Any]]:
|
||||
"""GetWorkTabletMatrices"""
|
||||
return self._call("IMatrix", "GetWorkTabletMatrices")
|
||||
|
||||
def matrix_by_id(self, matrix_id: str) -> Dict[str, Any]:
|
||||
"""GetWorkTabletMatrixById"""
|
||||
return self._call("IMatrix", "GetWorkTabletMatrixById", [matrix_id])
|
||||
|
||||
def add_WorkTablet_Matrix(self,matrix):
|
||||
return self._call("IMatrix", "AddWorkTabletMatrix", [matrix])
|
||||
|
||||
# ---------------------------------------------------- 一键运行
|
||||
def run_solution(self, solution_id: str, channel_idx: int = 1) -> None:
|
||||
self.load_solution(solution_id)
|
||||
self.start(channel_idx)
|
||||
|
||||
# ---------------------------------------------------- 单点动作
|
||||
|
||||
def Load(
|
||||
self,
|
||||
axis: str,
|
||||
dosage: int,
|
||||
plate_no: int,
|
||||
is_whole_plate: bool,
|
||||
hole_row: int,
|
||||
hole_col: int,
|
||||
blending_times: int,
|
||||
balance_height: int,
|
||||
plate_or_hole: str,
|
||||
hole_numbers: str,
|
||||
assist_fun1: str = "",
|
||||
assist_fun2: str = "",
|
||||
assist_fun3: str = "",
|
||||
assist_fun4: str = "",
|
||||
assist_fun5: str = "",
|
||||
liquid_method: str = "NormalDispense"
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"StepAxis": axis,
|
||||
"Function": "Load",
|
||||
"DosageNum": dosage,
|
||||
"PlateNo": plate_no,
|
||||
"IsWholePlate": is_whole_plate,
|
||||
"HoleRow": hole_row,
|
||||
"HoleCol": hole_col,
|
||||
"BlendingTimes": blending_times,
|
||||
"BalanceHeight": balance_height,
|
||||
"PlateOrHoleNum": plate_or_hole,
|
||||
"AssistFun1": assist_fun1,
|
||||
"AssistFun2": assist_fun2,
|
||||
"AssistFun3": assist_fun3,
|
||||
"AssistFun4": assist_fun4,
|
||||
"AssistFun5": assist_fun5,
|
||||
"HoleNumbers": hole_numbers,
|
||||
"LiquidDispensingMethod": liquid_method
|
||||
}
|
||||
|
||||
def Imbibing(
|
||||
self,
|
||||
axis: str,
|
||||
dosage: int,
|
||||
plate_no: int,
|
||||
is_whole_plate: bool,
|
||||
hole_row: int,
|
||||
hole_col: int,
|
||||
blending_times: int,
|
||||
balance_height: int,
|
||||
plate_or_hole: str,
|
||||
hole_numbers: str,
|
||||
assist_fun1: str = "",
|
||||
assist_fun2: str = "",
|
||||
assist_fun3: str = "",
|
||||
assist_fun4: str = "",
|
||||
assist_fun5: str = "",
|
||||
liquid_method: str = "NormalDispense"
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"StepAxis": axis,
|
||||
"Function": "Imbibing",
|
||||
"DosageNum": dosage,
|
||||
"PlateNo": plate_no,
|
||||
"IsWholePlate": is_whole_plate,
|
||||
"HoleRow": hole_row,
|
||||
"HoleCol": hole_col,
|
||||
"BlendingTimes": blending_times,
|
||||
"BalanceHeight": balance_height,
|
||||
"PlateOrHoleNum": plate_or_hole,
|
||||
"AssistFun1": assist_fun1,
|
||||
"AssistFun2": assist_fun2,
|
||||
"AssistFun3": assist_fun3,
|
||||
"AssistFun4": assist_fun4,
|
||||
"AssistFun5": assist_fun5,
|
||||
"HoleNumbers": hole_numbers,
|
||||
"LiquidDispensingMethod": liquid_method
|
||||
}
|
||||
|
||||
|
||||
def Tapping(
|
||||
self,
|
||||
axis: str,
|
||||
dosage: int,
|
||||
plate_no: int,
|
||||
is_whole_plate: bool,
|
||||
hole_row: int,
|
||||
hole_col: int,
|
||||
blending_times: int,
|
||||
balance_height: int,
|
||||
plate_or_hole: str,
|
||||
hole_numbers: str,
|
||||
assist_fun1: str = "",
|
||||
assist_fun2: str = "",
|
||||
assist_fun3: str = "",
|
||||
assist_fun4: str = "",
|
||||
assist_fun5: str = "",
|
||||
liquid_method: str = "NormalDispense"
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"StepAxis": axis,
|
||||
"Function": "Tapping",
|
||||
"DosageNum": dosage,
|
||||
"PlateNo": plate_no,
|
||||
"IsWholePlate": is_whole_plate,
|
||||
"HoleRow": hole_row,
|
||||
"HoleCol": hole_col,
|
||||
"BlendingTimes": blending_times,
|
||||
"BalanceHeight": balance_height,
|
||||
"PlateOrHoleNum": plate_or_hole,
|
||||
"AssistFun1": assist_fun1,
|
||||
"AssistFun2": assist_fun2,
|
||||
"AssistFun3": assist_fun3,
|
||||
"AssistFun4": assist_fun4,
|
||||
"AssistFun5": assist_fun5,
|
||||
"HoleNumbers": hole_numbers,
|
||||
"LiquidDispensingMethod": liquid_method
|
||||
}
|
||||
|
||||
|
||||
def Blending(
|
||||
self,
|
||||
axis: str,
|
||||
dosage: int,
|
||||
plate_no: int,
|
||||
is_whole_plate: bool,
|
||||
hole_row: int,
|
||||
hole_col: int,
|
||||
blending_times: int,
|
||||
balance_height: int,
|
||||
plate_or_hole: str,
|
||||
hole_numbers: str,
|
||||
assist_fun1: str = "",
|
||||
assist_fun2: str = "",
|
||||
assist_fun3: str = "",
|
||||
assist_fun4: str = "",
|
||||
assist_fun5: str = "",
|
||||
liquid_method: str = "NormalDispense"
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"StepAxis": axis,
|
||||
"Function": "Blending",
|
||||
"DosageNum": dosage,
|
||||
"PlateNo": plate_no,
|
||||
"IsWholePlate": is_whole_plate,
|
||||
"HoleRow": hole_row,
|
||||
"HoleCol": hole_col,
|
||||
"BlendingTimes": blending_times,
|
||||
"BalanceHeight": balance_height,
|
||||
"PlateOrHoleNum": plate_or_hole,
|
||||
"AssistFun1": assist_fun1,
|
||||
"AssistFun2": assist_fun2,
|
||||
"AssistFun3": assist_fun3,
|
||||
"AssistFun4": assist_fun4,
|
||||
"AssistFun5": assist_fun5,
|
||||
"HoleNumbers": hole_numbers,
|
||||
"LiquidDispensingMethod": liquid_method
|
||||
}
|
||||
|
||||
def UnLoad(
|
||||
self,
|
||||
axis: str,
|
||||
dosage: int,
|
||||
plate_no: int,
|
||||
is_whole_plate: bool,
|
||||
hole_row: int,
|
||||
hole_col: int,
|
||||
blending_times: int,
|
||||
balance_height: int,
|
||||
plate_or_hole: str,
|
||||
hole_numbers: str,
|
||||
assist_fun1: str = "",
|
||||
assist_fun2: str = "",
|
||||
assist_fun3: str = "",
|
||||
assist_fun4: str = "",
|
||||
assist_fun5: str = "",
|
||||
liquid_method: str = "NormalDispense"
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"StepAxis": axis,
|
||||
"Function": "UnLoad",
|
||||
"DosageNum": dosage,
|
||||
"PlateNo": plate_no,
|
||||
"IsWholePlate": is_whole_plate,
|
||||
"HoleRow": hole_row,
|
||||
"HoleCol": hole_col,
|
||||
"BlendingTimes": blending_times,
|
||||
"BalanceHeight": balance_height,
|
||||
"PlateOrHoleNum": plate_or_hole,
|
||||
"AssistFun1": assist_fun1,
|
||||
"AssistFun2": assist_fun2,
|
||||
"AssistFun3": assist_fun3,
|
||||
"AssistFun4": assist_fun4,
|
||||
"AssistFun5": assist_fun5,
|
||||
"HoleNumbers": hole_numbers,
|
||||
"LiquidDispensingMethod": liquid_method
|
||||
}
|
||||
@@ -3,7 +3,11 @@ import sys
|
||||
import io
|
||||
# sys.path.insert(0, r'C:\kui\winprep_cli\winprep_c_Uni-lab\x64\Debug')
|
||||
|
||||
import winprep_c
|
||||
try:
|
||||
import winprep_c
|
||||
except ImportError as e:
|
||||
print("Error importing winprep_c:", e)
|
||||
print("Please ensure that the winprep_c module is correctly installed and accessible.")
|
||||
from queue import Queue
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ except Exception as e:
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))
|
||||
from unilabos.utils.pywinauto_util import connect_application, get_process_pid_by_name, get_ui_path_with_window_specification, print_wrapper_identifiers
|
||||
from unilabos.device_comms.universal_driver import UniversalDriver, SingleRunningExecutor
|
||||
from unilabos.devices.template_driver import universal_driver as ud
|
||||
from unilabos.device_comms import universal_driver as ud
|
||||
print(f"使用文件DEBUG运行: {e}")
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
import time
|
||||
|
||||
class RamanObj:
|
||||
def __init__(self, port_laser,port_ccd, baudrate_laser=9600, baudrate_ccd=921600):
|
||||
def __init__(self, port_laser, port_ccd, baudrate_laser=9600, baudrate_ccd=921600):
|
||||
self.port_laser = port_laser
|
||||
self.port_ccd = port_ccd
|
||||
|
||||
|
||||
@@ -67,6 +67,16 @@ class VirtualFilter:
|
||||
volume: float = 0.0
|
||||
) -> bool:
|
||||
"""Execute filter action - 完全按照 Filter.action 参数"""
|
||||
|
||||
# 🔧 新增:温度自动调整
|
||||
original_temp = temp
|
||||
if temp == 0.0:
|
||||
temp = 25.0 # 0度自动设置为室温
|
||||
self.logger.info(f"温度自动调整: {original_temp}°C → {temp}°C (室温)")
|
||||
elif temp < 4.0:
|
||||
temp = 4.0 # 小于4度自动设置为4度
|
||||
self.logger.info(f"温度自动调整: {original_temp}°C → {temp}°C (最低温度)")
|
||||
|
||||
self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}")
|
||||
self.logger.info(f" stir={stir}, stir_speed={stir_speed}, temp={temp}")
|
||||
self.logger.info(f" continue_heatchill={continue_heatchill}, volume={volume}")
|
||||
|
||||
@@ -58,12 +58,30 @@ class VirtualHeatChill:
|
||||
})
|
||||
return True
|
||||
|
||||
async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool,
|
||||
async def heat_chill(self, vessel: str, temp: float, time, stir: bool,
|
||||
stir_speed: float, purpose: str) -> bool:
|
||||
"""Execute heat chill action - 按实际时间运行,实时更新剩余时间"""
|
||||
self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time}s, stir={stir}, stir_speed={stir_speed}")
|
||||
"""Execute heat chill action - 🔧 修复:确保参数类型正确"""
|
||||
|
||||
# 验证参数
|
||||
# 🔧 关键修复:确保所有参数类型正确
|
||||
try:
|
||||
temp = float(temp)
|
||||
time_value = float(time) # 强制转换为浮点数
|
||||
stir_speed = float(stir_speed)
|
||||
stir = bool(stir)
|
||||
vessel = str(vessel)
|
||||
purpose = str(purpose)
|
||||
except (ValueError, TypeError) as e:
|
||||
error_msg = f"参数类型转换错误: temp={temp}({type(temp)}), time={time}({type(time)}), error={str(e)}"
|
||||
self.logger.error(error_msg)
|
||||
self.data.update({
|
||||
"status": f"Error: {error_msg}",
|
||||
"operation_mode": "Error"
|
||||
})
|
||||
return False
|
||||
|
||||
self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time_value}s, stir={stir}, stir_speed={stir_speed}")
|
||||
|
||||
# 验证参数范围
|
||||
if temp > self._max_temp or temp < self._min_temp:
|
||||
error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)"
|
||||
self.logger.error(error_msg)
|
||||
@@ -82,6 +100,15 @@ class VirtualHeatChill:
|
||||
})
|
||||
return False
|
||||
|
||||
if time_value <= 0:
|
||||
error_msg = f"时间 {time_value}s 必须大于0"
|
||||
self.logger.error(error_msg)
|
||||
self.data.update({
|
||||
"status": f"Error: {error_msg}",
|
||||
"operation_mode": "Error"
|
||||
})
|
||||
return False
|
||||
|
||||
# 确定操作模式
|
||||
if temp > 25.0:
|
||||
operation_mode = "Heating"
|
||||
@@ -93,9 +120,9 @@ class VirtualHeatChill:
|
||||
operation_mode = "Maintaining"
|
||||
status_action = "保温"
|
||||
|
||||
# **修复**: 使用重命名的time模块
|
||||
# 🔧 修复:使用转换后的时间值
|
||||
start_time = time_module.time()
|
||||
total_time = time
|
||||
total_time = time_value # 使用转换后的浮点数
|
||||
|
||||
# 开始操作
|
||||
stir_info = f" | 搅拌: {stir_speed} RPM" if stir else ""
|
||||
@@ -107,9 +134,9 @@ class VirtualHeatChill:
|
||||
"remaining_time": total_time,
|
||||
})
|
||||
|
||||
# **修复**: 在等待过程中每秒更新剩余时间
|
||||
# 在等待过程中每秒更新剩余时间
|
||||
while True:
|
||||
current_time = time_module.time() # 使用重命名的time模块
|
||||
current_time = time_module.time()
|
||||
elapsed = current_time - start_time
|
||||
remaining = max(0, total_time - elapsed)
|
||||
|
||||
@@ -141,6 +168,21 @@ class VirtualHeatChill:
|
||||
|
||||
async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool:
|
||||
"""Start continuous heat chill"""
|
||||
|
||||
# 🔧 添加类型转换
|
||||
try:
|
||||
temp = float(temp)
|
||||
vessel = str(vessel)
|
||||
purpose = str(purpose)
|
||||
except (ValueError, TypeError) as e:
|
||||
error_msg = f"参数类型转换错误: {str(e)}"
|
||||
self.logger.error(error_msg)
|
||||
self.data.update({
|
||||
"status": f"Error: {error_msg}",
|
||||
"operation_mode": "Error"
|
||||
})
|
||||
return False
|
||||
|
||||
self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C")
|
||||
|
||||
# 验证参数
|
||||
@@ -176,6 +218,15 @@ class VirtualHeatChill:
|
||||
|
||||
async def heat_chill_stop(self, vessel: str) -> bool:
|
||||
"""Stop heat chill"""
|
||||
|
||||
# 🔧 添加类型转换
|
||||
try:
|
||||
vessel = str(vessel)
|
||||
except (ValueError, TypeError) as e:
|
||||
error_msg = f"参数类型转换错误: {str(e)}"
|
||||
self.logger.error(error_msg)
|
||||
return False
|
||||
|
||||
self.logger.info(f"HeatChillStop: vessel={vessel}")
|
||||
|
||||
self.data.update({
|
||||
|
||||
@@ -3,6 +3,9 @@ import logging
|
||||
import time as time_module
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"[ROTAVAP] {message}", flush=True)
|
||||
|
||||
class VirtualRotavap:
|
||||
"""Virtual rotary evaporator device - 简化版,只保留核心功能"""
|
||||
@@ -70,12 +73,36 @@ class VirtualRotavap:
|
||||
vessel: str,
|
||||
pressure: float = 0.1,
|
||||
temp: float = 60.0,
|
||||
time: float = 1800.0, # 30分钟默认
|
||||
stir_speed: float = 100.0
|
||||
time: float = 180.0,
|
||||
stir_speed: float = 100.0,
|
||||
solvent: str = "",
|
||||
**kwargs
|
||||
) -> bool:
|
||||
"""Execute evaporate action - 简化的蒸发流程"""
|
||||
self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM")
|
||||
|
||||
"""Execute evaporate action - 简化版"""
|
||||
|
||||
# 🔧 简化处理:如果vessel就是设备自己,直接操作
|
||||
if vessel == self.device_id:
|
||||
debug_print(f"在设备 {self.device_id} 上直接执行蒸发操作")
|
||||
actual_vessel = self.device_id
|
||||
else:
|
||||
actual_vessel = vessel
|
||||
|
||||
# 参数预处理
|
||||
if solvent:
|
||||
self.logger.info(f"识别到溶剂: {solvent}")
|
||||
# 根据溶剂调整参数
|
||||
solvent_lower = solvent.lower()
|
||||
if any(s in solvent_lower for s in ['water', 'aqueous']):
|
||||
temp = max(temp, 80.0)
|
||||
pressure = max(pressure, 0.2)
|
||||
self.logger.info("水系溶剂:调整参数")
|
||||
elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
|
||||
temp = min(temp, 50.0)
|
||||
pressure = min(pressure, 0.05)
|
||||
self.logger.info("易挥发溶剂:调整参数")
|
||||
|
||||
self.logger.info(f"Evaporate: vessel={actual_vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM, solvent={solvent}")
|
||||
|
||||
# 验证参数
|
||||
if temp > self._max_temp or temp < 10.0:
|
||||
error_msg = f"温度 {temp}°C 超出范围 (10-{self._max_temp}°C)"
|
||||
@@ -83,6 +110,9 @@ class VirtualRotavap:
|
||||
self.data.update({
|
||||
"status": f"Error: {error_msg}",
|
||||
"rotavap_state": "Error",
|
||||
"current_temp": 25.0,
|
||||
"progress": 0.0,
|
||||
"evaporated_volume": 0.0,
|
||||
"message": error_msg
|
||||
})
|
||||
return False
|
||||
@@ -93,6 +123,9 @@ class VirtualRotavap:
|
||||
self.data.update({
|
||||
"status": f"Error: {error_msg}",
|
||||
"rotavap_state": "Error",
|
||||
"current_temp": 25.0,
|
||||
"progress": 0.0,
|
||||
"evaporated_volume": 0.0,
|
||||
"message": error_msg
|
||||
})
|
||||
return False
|
||||
@@ -103,13 +136,16 @@ class VirtualRotavap:
|
||||
self.data.update({
|
||||
"status": f"Error: {error_msg}",
|
||||
"rotavap_state": "Error",
|
||||
"current_temp": 25.0,
|
||||
"progress": 0.0,
|
||||
"evaporated_volume": 0.0,
|
||||
"message": error_msg
|
||||
})
|
||||
return False
|
||||
|
||||
# 开始蒸发
|
||||
self.data.update({
|
||||
"status": f"蒸发中: {vessel}",
|
||||
"status": f"蒸发中: {actual_vessel}",
|
||||
"rotavap_state": "Evaporating",
|
||||
"current_temp": temp,
|
||||
"target_temp": temp,
|
||||
@@ -118,7 +154,7 @@ class VirtualRotavap:
|
||||
"remaining_time": time,
|
||||
"progress": 0.0,
|
||||
"evaporated_volume": 0.0,
|
||||
"message": f"Evaporating {vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
|
||||
"message": f"Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
|
||||
})
|
||||
|
||||
try:
|
||||
@@ -135,12 +171,13 @@ class VirtualRotavap:
|
||||
# 模拟蒸发体积
|
||||
evaporated_vol = progress * 0.8 # 假设最多蒸发80mL
|
||||
|
||||
# 更新状态
|
||||
# 🔧 更新状态 - 确保包含所有必需字段
|
||||
self.data.update({
|
||||
"remaining_time": remaining,
|
||||
"progress": progress,
|
||||
"evaporated_volume": evaporated_vol,
|
||||
"status": f"蒸发中: {vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s",
|
||||
"progress": progress, # 确保这个字段存在
|
||||
"evaporated_volume": evaporated_vol, # 确保这个字段存在
|
||||
"current_temp": temp, # 确保这个字段存在
|
||||
"status": f"蒸发中: {actual_vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s",
|
||||
"message": f"Evaporating: {progress:.1f}% complete, {remaining:.0f}s remaining"
|
||||
})
|
||||
|
||||
@@ -154,18 +191,18 @@ class VirtualRotavap:
|
||||
# 蒸发完成
|
||||
final_evaporated = 80.0
|
||||
self.data.update({
|
||||
"status": f"蒸发完成: {vessel} | 蒸发量: {final_evaporated:.1f}mL",
|
||||
"status": f"蒸发完成: {actual_vessel} | 蒸发量: {final_evaporated:.1f}mL",
|
||||
"rotavap_state": "Completed",
|
||||
"evaporated_volume": final_evaporated,
|
||||
"progress": 100.0,
|
||||
"current_temp": temp, # 保持温度信息
|
||||
"remaining_time": 0.0,
|
||||
"current_temp": 25.0, # 冷却下来
|
||||
"rotation_speed": 0.0, # 停止旋转
|
||||
"vacuum_pressure": 1.0, # 恢复大气压
|
||||
"message": f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}"
|
||||
"message": f"Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}"
|
||||
})
|
||||
|
||||
self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}")
|
||||
self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -176,6 +213,8 @@ class VirtualRotavap:
|
||||
"status": f"蒸发错误: {str(e)}",
|
||||
"rotavap_state": "Error",
|
||||
"current_temp": 25.0,
|
||||
"progress": 0.0,
|
||||
"evaporated_volume": 0.0,
|
||||
"rotation_speed": 0.0,
|
||||
"vacuum_pressure": 1.0,
|
||||
"message": f"Evaporation failed: {str(e)}"
|
||||
|
||||
@@ -43,10 +43,25 @@ class VirtualSolenoidValve:
|
||||
def is_open(self) -> bool:
|
||||
return self._is_open
|
||||
|
||||
def get_valve_position(self) -> str:
|
||||
@property
|
||||
def valve_position(self) -> str:
|
||||
"""获取阀门位置状态"""
|
||||
return "OPEN" if self._is_open else "CLOSED"
|
||||
|
||||
@property
|
||||
def state(self) -> dict:
|
||||
"""获取阀门完整状态"""
|
||||
return {
|
||||
"device_id": self.device_id,
|
||||
"port": self.port,
|
||||
"voltage": self.voltage,
|
||||
"response_time": self.response_time,
|
||||
"is_open": self._is_open,
|
||||
"valve_state": self._valve_state,
|
||||
"status": self._status,
|
||||
"position": self.valve_position
|
||||
}
|
||||
|
||||
async def set_valve_position(self, command: str = None, **kwargs):
|
||||
"""
|
||||
设置阀门位置 - ROS动作接口
|
||||
@@ -91,7 +106,7 @@ class VirtualSolenoidValve:
|
||||
return {
|
||||
"success": True,
|
||||
"message": result_msg,
|
||||
"valve_position": self.get_valve_position()
|
||||
"valve_position": self.valve_position
|
||||
}
|
||||
|
||||
async def open(self, **kwargs):
|
||||
@@ -102,21 +117,25 @@ class VirtualSolenoidValve:
|
||||
"""关闭电磁阀 - ROS动作接口"""
|
||||
return await self.set_valve_position(command="CLOSED")
|
||||
|
||||
async def set_state(self, command: Union[bool, str], **kwargs):
|
||||
async def set_status(self, string: str = None, **kwargs):
|
||||
"""
|
||||
设置阀门状态 - 兼容 SendCmd 类型
|
||||
设置阀门状态 - 兼容 StrSingleInput 类型
|
||||
|
||||
Args:
|
||||
command: True/False 或 "open"/"close"
|
||||
string: "ON"/"OFF" 或 "OPEN"/"CLOSED"
|
||||
"""
|
||||
if isinstance(command, bool):
|
||||
cmd_str = "OPEN" if command else "CLOSED"
|
||||
elif isinstance(command, str):
|
||||
cmd_str = command
|
||||
else:
|
||||
return {"success": False, "message": "Invalid command type"}
|
||||
if string is None:
|
||||
return {"success": False, "message": "Missing string parameter"}
|
||||
|
||||
return await self.set_valve_position(command=cmd_str)
|
||||
# 将 string 参数转换为 command 参数
|
||||
if string.upper() in ["ON", "OPEN"]:
|
||||
command = "OPEN"
|
||||
elif string.upper() in ["OFF", "CLOSED"]:
|
||||
command = "CLOSED"
|
||||
else:
|
||||
command = string
|
||||
|
||||
return await self.set_valve_position(command=command)
|
||||
|
||||
def toggle(self):
|
||||
"""切换阀门状态"""
|
||||
@@ -129,19 +148,6 @@ class VirtualSolenoidValve:
|
||||
"""检查阀门是否关闭"""
|
||||
return not self._is_open
|
||||
|
||||
def get_state(self) -> dict:
|
||||
"""获取阀门完整状态"""
|
||||
return {
|
||||
"device_id": self.device_id,
|
||||
"port": self.port,
|
||||
"voltage": self.voltage,
|
||||
"response_time": self.response_time,
|
||||
"is_open": self._is_open,
|
||||
"valve_state": self._valve_state,
|
||||
"status": self._status,
|
||||
"position": self.get_valve_position()
|
||||
}
|
||||
|
||||
async def reset(self):
|
||||
"""重置阀门到关闭状态"""
|
||||
return await self.close()
|
||||
335
unilabos/devices/virtual/virtual_solid_dispenser.py
Normal file
335
unilabos/devices/virtual/virtual_solid_dispenser.py
Normal file
@@ -0,0 +1,335 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class VirtualSolidDispenser:
|
||||
"""
|
||||
虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加
|
||||
|
||||
特点:
|
||||
- 高兼容性:缺少参数不报错
|
||||
- 智能识别:自动查找固体试剂瓶
|
||||
- 简单反馈:成功/失败 + 消息
|
||||
"""
|
||||
|
||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||
self.device_id = device_id or "virtual_solid_dispenser"
|
||||
self.config = config or {}
|
||||
|
||||
# 设备参数
|
||||
self.max_capacity = float(self.config.get('max_capacity', 100.0)) # 最大加样量 (g)
|
||||
self.precision = float(self.config.get('precision', 0.001)) # 精度 (g)
|
||||
|
||||
# 状态变量
|
||||
self._status = "Idle"
|
||||
self._current_reagent = ""
|
||||
self._dispensed_amount = 0.0
|
||||
self._total_operations = 0
|
||||
|
||||
self.logger = logging.getLogger(f"VirtualSolidDispenser.{self.device_id}")
|
||||
|
||||
print(f"=== VirtualSolidDispenser {self.device_id} 创建成功! ===")
|
||||
print(f"=== 最大容量: {self.max_capacity}g, 精度: {self.precision}g ===")
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""初始化固体加样器"""
|
||||
self.logger.info(f"初始化固体加样器 {self.device_id}")
|
||||
self._status = "Ready"
|
||||
self._current_reagent = ""
|
||||
self._dispensed_amount = 0.0
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""清理固体加样器"""
|
||||
self.logger.info(f"清理固体加样器 {self.device_id}")
|
||||
self._status = "Idle"
|
||||
return True
|
||||
|
||||
def parse_mass_string(self, mass_str: str) -> float:
|
||||
"""
|
||||
解析质量字符串为数值 (g)
|
||||
|
||||
支持格式: "2.9 g", "19.3g", "4.5 mg", "1.2 kg" 等
|
||||
"""
|
||||
if not mass_str or not isinstance(mass_str, str):
|
||||
return 0.0
|
||||
|
||||
# 移除空格并转小写
|
||||
mass_clean = mass_str.strip().lower()
|
||||
|
||||
# 正则匹配数字和单位
|
||||
pattern = r'(\d+(?:\.\d+)?)\s*([a-z]*)'
|
||||
match = re.search(pattern, mass_clean)
|
||||
|
||||
if not match:
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or 'g' # 默认单位 g
|
||||
|
||||
# 单位转换为 g
|
||||
unit_multipliers = {
|
||||
'g': 1.0,
|
||||
'gram': 1.0,
|
||||
'grams': 1.0,
|
||||
'mg': 0.001,
|
||||
'milligram': 0.001,
|
||||
'milligrams': 0.001,
|
||||
'kg': 1000.0,
|
||||
'kilogram': 1000.0,
|
||||
'kilograms': 1000.0,
|
||||
'μg': 0.000001,
|
||||
'ug': 0.000001,
|
||||
'microgram': 0.000001,
|
||||
'micrograms': 0.000001,
|
||||
}
|
||||
|
||||
multiplier = unit_multipliers.get(unit, 1.0)
|
||||
return value * multiplier
|
||||
|
||||
except (ValueError, TypeError):
|
||||
self.logger.warning(f"无法解析质量字符串: {mass_str}")
|
||||
return 0.0
|
||||
|
||||
def parse_mol_string(self, mol_str: str) -> float:
|
||||
"""
|
||||
解析摩尔数字符串为数值 (mol)
|
||||
|
||||
支持格式: "0.12 mol", "16.2 mmol", "25.2mmol" 等
|
||||
"""
|
||||
if not mol_str or not isinstance(mol_str, str):
|
||||
return 0.0
|
||||
|
||||
# 移除空格并转小写
|
||||
mol_clean = mol_str.strip().lower()
|
||||
|
||||
# 正则匹配数字和单位
|
||||
pattern = r'(\d+(?:\.\d+)?)\s*(m?mol)'
|
||||
match = re.search(pattern, mol_clean)
|
||||
|
||||
if not match:
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2)
|
||||
|
||||
# 单位转换为 mol
|
||||
if unit == 'mmol':
|
||||
return value * 0.001
|
||||
else: # mol
|
||||
return value
|
||||
|
||||
except (ValueError, TypeError):
|
||||
self.logger.warning(f"无法解析摩尔数字符串: {mol_str}")
|
||||
return 0.0
|
||||
|
||||
def find_solid_reagent_bottle(self, reagent_name: str) -> str:
|
||||
"""
|
||||
查找固体试剂瓶
|
||||
|
||||
这是一个简化版本,实际使用时应该连接到系统的设备图
|
||||
"""
|
||||
if not reagent_name:
|
||||
return "unknown_solid_bottle"
|
||||
|
||||
# 可能的固体试剂瓶命名模式
|
||||
possible_names = [
|
||||
f"solid_bottle_{reagent_name}",
|
||||
f"reagent_solid_{reagent_name}",
|
||||
f"powder_{reagent_name}",
|
||||
f"{reagent_name}_solid",
|
||||
f"{reagent_name}_powder",
|
||||
f"solid_{reagent_name}",
|
||||
]
|
||||
|
||||
# 这里简化处理,实际应该查询设备图
|
||||
return possible_names[0]
|
||||
|
||||
async def add_solid(
|
||||
self,
|
||||
vessel: str,
|
||||
reagent: str,
|
||||
mass: str = "",
|
||||
mol: str = "",
|
||||
purpose: str = "",
|
||||
**kwargs # 兼容额外参数
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
添加固体试剂的主要方法
|
||||
|
||||
Args:
|
||||
vessel: 目标容器
|
||||
reagent: 试剂名称
|
||||
mass: 质量字符串 (如 "2.9 g")
|
||||
mol: 摩尔数字符串 (如 "0.12 mol")
|
||||
purpose: 添加目的
|
||||
**kwargs: 其他兼容参数
|
||||
|
||||
Returns:
|
||||
Dict: 操作结果
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"=== 开始固体加样操作 ===")
|
||||
self.logger.info(f"目标容器: {vessel}")
|
||||
self.logger.info(f"试剂: {reagent}")
|
||||
self.logger.info(f"质量: {mass}")
|
||||
self.logger.info(f"摩尔数: {mol}")
|
||||
self.logger.info(f"目的: {purpose}")
|
||||
|
||||
# 参数验证 - 宽松处理
|
||||
if not vessel:
|
||||
vessel = "main_reactor" # 默认容器
|
||||
self.logger.warning(f"未指定容器,使用默认容器: {vessel}")
|
||||
|
||||
if not reagent:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "错误: 必须指定试剂名称",
|
||||
"return_info": "missing_reagent"
|
||||
}
|
||||
|
||||
# 解析质量和摩尔数
|
||||
mass_value = self.parse_mass_string(mass)
|
||||
mol_value = self.parse_mol_string(mol)
|
||||
|
||||
self.logger.info(f"解析后 - 质量: {mass_value}g, 摩尔数: {mol_value}mol")
|
||||
|
||||
# 确定实际加样量
|
||||
if mass_value > 0:
|
||||
actual_amount = mass_value
|
||||
amount_unit = "g"
|
||||
self.logger.info(f"按质量加样: {actual_amount} {amount_unit}")
|
||||
elif mol_value > 0:
|
||||
# 简化处理:假设分子量为100 g/mol
|
||||
assumed_mw = 100.0
|
||||
actual_amount = mol_value * assumed_mw
|
||||
amount_unit = "g (from mol)"
|
||||
self.logger.info(f"按摩尔数加样: {mol_value} mol → {actual_amount} g (假设分子量 {assumed_mw})")
|
||||
else:
|
||||
# 没有指定量,使用默认值
|
||||
actual_amount = 1.0
|
||||
amount_unit = "g (default)"
|
||||
self.logger.warning(f"未指定质量或摩尔数,使用默认值: {actual_amount} {amount_unit}")
|
||||
|
||||
# 检查容量限制
|
||||
if actual_amount > self.max_capacity:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"错误: 请求量 {actual_amount}g 超过最大容量 {self.max_capacity}g",
|
||||
"return_info": "exceeds_capacity"
|
||||
}
|
||||
|
||||
# 查找试剂瓶
|
||||
reagent_bottle = self.find_solid_reagent_bottle(reagent)
|
||||
self.logger.info(f"使用试剂瓶: {reagent_bottle}")
|
||||
|
||||
# 模拟加样过程
|
||||
self._status = "Dispensing"
|
||||
self._current_reagent = reagent
|
||||
|
||||
# 计算操作时间 (基于质量)
|
||||
operation_time = max(0.5, actual_amount * 0.1) # 每克0.1秒,最少0.5秒
|
||||
|
||||
self.logger.info(f"开始加样,预计时间: {operation_time:.1f}秒")
|
||||
await asyncio.sleep(operation_time)
|
||||
|
||||
# 更新状态
|
||||
self._dispensed_amount = actual_amount
|
||||
self._total_operations += 1
|
||||
self._status = "Ready"
|
||||
|
||||
# 成功结果
|
||||
success_message = f"成功添加 {reagent} {actual_amount:.3f} {amount_unit} 到 {vessel}"
|
||||
|
||||
self.logger.info(f"=== 固体加样完成 ===")
|
||||
self.logger.info(success_message)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": success_message,
|
||||
"return_info": f"dispensed_{actual_amount:.3f}g"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"固体加样失败: {str(e)}"
|
||||
self.logger.error(error_message)
|
||||
self._status = "Error"
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"message": error_message,
|
||||
"return_info": "operation_failed"
|
||||
}
|
||||
|
||||
# 状态属性
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def current_reagent(self) -> str:
|
||||
return self._current_reagent
|
||||
|
||||
@property
|
||||
def dispensed_amount(self) -> float:
|
||||
return self._dispensed_amount
|
||||
|
||||
@property
|
||||
def total_operations(self) -> int:
|
||||
return self._total_operations
|
||||
|
||||
def get_device_info(self) -> Dict[str, Any]:
|
||||
"""获取设备状态信息"""
|
||||
return {
|
||||
"device_id": self.device_id,
|
||||
"status": self._status,
|
||||
"current_reagent": self._current_reagent,
|
||||
"last_dispensed_amount": self._dispensed_amount,
|
||||
"total_operations": self._total_operations,
|
||||
"max_capacity": self.max_capacity,
|
||||
"precision": self.precision
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return f"VirtualSolidDispenser({self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)"
|
||||
|
||||
|
||||
# 测试函数
|
||||
async def test_solid_dispenser():
|
||||
"""测试固体加样器"""
|
||||
print("=== 固体加样器测试 ===")
|
||||
|
||||
dispenser = VirtualSolidDispenser("test_dispenser")
|
||||
await dispenser.initialize()
|
||||
|
||||
# 测试1: 按质量加样
|
||||
result1 = await dispenser.add_solid(
|
||||
vessel="main_reactor",
|
||||
reagent="magnesium",
|
||||
mass="2.9 g"
|
||||
)
|
||||
print(f"测试1结果: {result1}")
|
||||
|
||||
# 测试2: 按摩尔数加样
|
||||
result2 = await dispenser.add_solid(
|
||||
vessel="main_reactor",
|
||||
reagent="sodium_nitrite",
|
||||
mol="0.28 mol"
|
||||
)
|
||||
print(f"测试2结果: {result2}")
|
||||
|
||||
# 测试3: 缺少参数
|
||||
result3 = await dispenser.add_solid(
|
||||
reagent="test_compound"
|
||||
)
|
||||
print(f"测试3结果: {result3}")
|
||||
|
||||
print(f"设备信息: {dispenser.get_device_info()}")
|
||||
print("=== 测试完成 ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_solid_dispenser())
|
||||
@@ -1,11 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import socket
|
||||
import json
|
||||
import base64
|
||||
import argparse
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
|
||||
|
||||
@@ -96,17 +94,20 @@ class ZhidaClient:
|
||||
def abort(self) -> dict:
|
||||
return self._send_command({"command": "abort"})
|
||||
|
||||
"""
|
||||
a,b,c
|
||||
1,2,4
|
||||
2,4,5
|
||||
"""
|
||||
|
||||
client = ZhidaClient()
|
||||
# 连接
|
||||
client.connect()
|
||||
# 获取状态
|
||||
print(client.status)
|
||||
if __name__ == "__main__":
|
||||
|
||||
"""
|
||||
a,b,c
|
||||
1,2,4
|
||||
2,4,5
|
||||
"""
|
||||
|
||||
client = ZhidaClient()
|
||||
# 连接
|
||||
client.connect()
|
||||
# 获取状态
|
||||
print(client.status)
|
||||
|
||||
|
||||
# 命令格式:python zhida.py <subcommand> [options]
|
||||
# 命令格式:python zhida.py <subcommand> [options]
|
||||
|
||||
@@ -10,18 +10,88 @@ class Point3D(BaseModel):
|
||||
# Start Protocols
|
||||
|
||||
class PumpTransferProtocol(BaseModel):
|
||||
# === 核心参数(保持必需) ===
|
||||
from_vessel: str
|
||||
to_vessel: str
|
||||
volume: float
|
||||
|
||||
# === 所有其他参数都改为可选,添加默认值 ===
|
||||
volume: float = 0.0 # 🔧 改为-1,表示转移全部体积
|
||||
amount: str = ""
|
||||
time: float = 0
|
||||
time: float = 0.0
|
||||
viscous: bool = False
|
||||
rinsing_solvent: str = "air"
|
||||
rinsing_volume: float = 5000
|
||||
rinsing_repeats: int = 2
|
||||
rinsing_solvent: str = ""
|
||||
rinsing_volume: float = 0.0
|
||||
rinsing_repeats: int = 0
|
||||
solid: bool = False
|
||||
flowrate: float = 500
|
||||
transfer_flowrate: float = 2500
|
||||
flowrate: float = 2.5
|
||||
transfer_flowrate: float = 0.5
|
||||
|
||||
# === 新版XDL兼容参数(可选) ===
|
||||
rate_spec: str = ""
|
||||
event: str = ""
|
||||
through: str = ""
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""后处理:智能参数处理和兼容性调整"""
|
||||
|
||||
# 如果指定了 amount 但volume是默认值,尝试解析 amount
|
||||
if self.amount and self.volume == 0.0:
|
||||
parsed_volume = self._parse_amount_to_volume(self.amount)
|
||||
if parsed_volume > 0:
|
||||
self.volume = parsed_volume
|
||||
|
||||
# 如果指定了 time 但没有明确设置流速,根据时间计算流速
|
||||
if self.time > 0 and self.volume > 0:
|
||||
if self.flowrate == 2.5 and self.transfer_flowrate == 0.5:
|
||||
calculated_flowrate = self.volume / self.time
|
||||
self.flowrate = min(calculated_flowrate, 10.0)
|
||||
self.transfer_flowrate = min(calculated_flowrate, 5.0)
|
||||
|
||||
# 🔧 核心修复:如果flowrate为0(ROS2传入),使用默认值
|
||||
if self.flowrate <= 0:
|
||||
self.flowrate = 2.5
|
||||
if self.transfer_flowrate <= 0:
|
||||
self.transfer_flowrate = 0.5
|
||||
|
||||
# 根据 rate_spec 调整流速
|
||||
if self.rate_spec == "dropwise":
|
||||
self.flowrate = min(self.flowrate, 0.1)
|
||||
self.transfer_flowrate = min(self.transfer_flowrate, 0.1)
|
||||
elif self.rate_spec == "slowly":
|
||||
self.flowrate = min(self.flowrate, 0.5)
|
||||
self.transfer_flowrate = min(self.transfer_flowrate, 0.3)
|
||||
elif self.rate_spec == "quickly":
|
||||
self.flowrate = max(self.flowrate, 5.0)
|
||||
self.transfer_flowrate = max(self.transfer_flowrate, 2.0)
|
||||
|
||||
def _parse_amount_to_volume(self, amount: str) -> float:
|
||||
"""解析 amount 字符串为体积"""
|
||||
if not amount:
|
||||
return 0.0
|
||||
|
||||
amount = amount.lower().strip()
|
||||
|
||||
# 处理特殊关键词
|
||||
if amount == "all":
|
||||
return 0.0 # 🔧 "all"也表示转移全部
|
||||
|
||||
# 提取数字
|
||||
import re
|
||||
numbers = re.findall(r'[\d.]+', amount)
|
||||
if numbers:
|
||||
volume = float(numbers[0])
|
||||
|
||||
# 单位转换
|
||||
if 'ml' in amount or 'milliliter' in amount:
|
||||
return volume
|
||||
elif 'l' in amount and 'ml' not in amount:
|
||||
return volume * 1000
|
||||
elif 'μl' in amount or 'microliter' in amount:
|
||||
return volume / 1000
|
||||
else:
|
||||
return volume
|
||||
|
||||
return 0.0
|
||||
|
||||
|
||||
class CleanProtocol(BaseModel):
|
||||
@@ -49,17 +119,96 @@ class SeparateProtocol(BaseModel):
|
||||
|
||||
|
||||
class EvaporateProtocol(BaseModel):
|
||||
vessel: str
|
||||
pressure: float
|
||||
temp: float
|
||||
time: float
|
||||
stir_speed: float
|
||||
# === 核心参数(必需) ===
|
||||
vessel: str = Field(..., description="蒸发容器名称")
|
||||
|
||||
# === 所有其他参数都改为可选,添加默认值 ===
|
||||
pressure: float = Field(0.1, description="真空度 (bar),默认0.1 bar")
|
||||
temp: float = Field(60.0, description="加热温度 (°C),默认60°C")
|
||||
time: float = Field(180.0, description="蒸发时间 (秒),默认1800s (30分钟)")
|
||||
stir_speed: float = Field(100.0, description="旋转速度 (RPM),默认100 RPM")
|
||||
|
||||
# === 新版XDL兼容参数(可选) ===
|
||||
solvent: str = Field("", description="溶剂名称(用于识别蒸发的溶剂类型)")
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""后处理:智能参数处理和兼容性调整"""
|
||||
|
||||
# 参数范围验证和修正
|
||||
if self.pressure <= 0 or self.pressure > 1.0:
|
||||
logger.warning(f"真空度 {self.pressure} bar 超出范围,修正为 0.1 bar")
|
||||
self.pressure = 0.1
|
||||
|
||||
if self.temp < 10.0 or self.temp > 200.0:
|
||||
logger.warning(f"温度 {self.temp}°C 超出范围,修正为 60°C")
|
||||
self.temp = 60.0
|
||||
|
||||
if self.time <= 0:
|
||||
logger.warning(f"时间 {self.time}s 无效,修正为 1800s")
|
||||
self.time = 1800.0
|
||||
|
||||
if self.stir_speed < 10.0 or self.stir_speed > 300.0:
|
||||
logger.warning(f"旋转速度 {self.stir_speed} RPM 超出范围,修正为 100 RPM")
|
||||
self.stir_speed = 100.0
|
||||
|
||||
# 根据溶剂类型调整参数
|
||||
if self.solvent:
|
||||
self._adjust_parameters_by_solvent()
|
||||
|
||||
def _adjust_parameters_by_solvent(self):
|
||||
"""根据溶剂类型调整蒸发参数"""
|
||||
solvent_lower = self.solvent.lower()
|
||||
|
||||
# 水系溶剂:较高温度,较低真空度
|
||||
if any(s in solvent_lower for s in ['water', 'aqueous', 'h2o']):
|
||||
if self.temp == 60.0: # 如果是默认值,则调整
|
||||
self.temp = 80.0
|
||||
if self.pressure == 0.1:
|
||||
self.pressure = 0.2
|
||||
|
||||
# 有机溶剂:根据沸点调整
|
||||
elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
|
||||
if self.temp == 60.0:
|
||||
self.temp = 50.0
|
||||
if self.pressure == 0.1:
|
||||
self.pressure = 0.05
|
||||
|
||||
# 高沸点溶剂:更高温度
|
||||
elif any(s in solvent_lower for s in ['dmso', 'dmi', 'toluene']):
|
||||
if self.temp == 60.0:
|
||||
self.temp = 100.0
|
||||
if self.pressure == 0.1:
|
||||
self.pressure = 0.01
|
||||
|
||||
|
||||
class EvacuateAndRefillProtocol(BaseModel):
|
||||
vessel: str
|
||||
gas: str
|
||||
repeats: int
|
||||
# === 必需参数 ===
|
||||
vessel: str = Field(..., description="目标容器名称")
|
||||
gas: str = Field(..., description="气体名称")
|
||||
|
||||
# 🔧 删除 repeats 参数,直接在代码中硬编码为 3 次
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""后处理:参数验证和兼容性调整"""
|
||||
|
||||
# 验证气体名称
|
||||
if not self.gas.strip():
|
||||
logger.warning("气体名称为空,使用默认值 'nitrogen'")
|
||||
self.gas = "nitrogen"
|
||||
|
||||
# 标准化气体名称
|
||||
gas_aliases = {
|
||||
'n2': 'nitrogen',
|
||||
'ar': 'argon',
|
||||
'air': 'air',
|
||||
'o2': 'oxygen',
|
||||
'co2': 'carbon_dioxide',
|
||||
'h2': 'hydrogen'
|
||||
}
|
||||
|
||||
gas_lower = self.gas.lower().strip()
|
||||
if gas_lower in gas_aliases:
|
||||
self.gas = gas_aliases[gas_lower]
|
||||
|
||||
|
||||
class AGVTransferProtocol(BaseModel):
|
||||
@@ -88,42 +237,282 @@ class CentrifugeProtocol(BaseModel):
|
||||
temp: float
|
||||
|
||||
class FilterProtocol(BaseModel):
|
||||
vessel: str
|
||||
filtrate_vessel: str
|
||||
stir: bool
|
||||
stir_speed: float
|
||||
temp: float
|
||||
continue_heatchill: bool
|
||||
volume: float
|
||||
# === 必需参数 ===
|
||||
vessel: str = Field(..., description="过滤容器名称")
|
||||
|
||||
# === 可选参数 ===
|
||||
filtrate_vessel: str = Field("", description="滤液容器名称(可选,自动查找)")
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""后处理:参数验证"""
|
||||
# 验证容器名称
|
||||
if not self.vessel.strip():
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
class HeatChillProtocol(BaseModel):
|
||||
vessel: str
|
||||
temp: float
|
||||
time: float
|
||||
stir: bool
|
||||
stir_speed: float
|
||||
purpose: str
|
||||
# === 必需参数 ===
|
||||
vessel: str = Field(..., description="加热容器名称")
|
||||
|
||||
# === 可选参数 - 温度相关 ===
|
||||
temp: float = Field(25.0, description="目标温度 (°C)")
|
||||
temp_spec: str = Field("", description="温度规格(如 'room temperature', 'reflux')")
|
||||
|
||||
# === 可选参数 - 时间相关 ===
|
||||
time: float = Field(300.0, description="加热时间 (秒)")
|
||||
time_spec: str = Field("", description="时间规格(如 'overnight', '2 h')")
|
||||
|
||||
# === 可选参数 - 其他XDL参数 ===
|
||||
pressure: str = Field("", description="压力规格(如 '1 mbar'),不做特殊处理")
|
||||
reflux_solvent: str = Field("", description="回流溶剂名称,不做特殊处理")
|
||||
|
||||
# === 可选参数 - 搅拌相关 ===
|
||||
stir: bool = Field(False, description="是否搅拌")
|
||||
stir_speed: float = Field(300.0, description="搅拌速度 (RPM)")
|
||||
purpose: str = Field("", description="操作目的")
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""后处理:参数验证和解析"""
|
||||
|
||||
# 验证必需参数
|
||||
if not self.vessel.strip():
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
# 温度解析:优先使用 temp_spec,然后是 temp
|
||||
if self.temp_spec:
|
||||
self.temp = self._parse_temp_spec(self.temp_spec)
|
||||
|
||||
# 时间解析:优先使用 time_spec,然后是 time
|
||||
if self.time_spec:
|
||||
self.time = self._parse_time_spec(self.time_spec)
|
||||
|
||||
# 参数范围验证
|
||||
if self.temp < -50.0 or self.temp > 300.0:
|
||||
logger.warning(f"温度 {self.temp}°C 超出范围,修正为 25°C")
|
||||
self.temp = 25.0
|
||||
|
||||
if self.time < 0:
|
||||
logger.warning(f"时间 {self.time}s 无效,修正为 300s")
|
||||
self.time = 300.0
|
||||
|
||||
if self.stir_speed < 0 or self.stir_speed > 1500.0:
|
||||
logger.warning(f"搅拌速度 {self.stir_speed} RPM 超出范围,修正为 300 RPM")
|
||||
self.stir_speed = 300.0
|
||||
|
||||
def _parse_temp_spec(self, temp_spec: str) -> float:
|
||||
"""解析温度规格为具体温度"""
|
||||
|
||||
temp_spec = temp_spec.strip().lower()
|
||||
|
||||
# 特殊温度规格
|
||||
special_temps = {
|
||||
"room temperature": 25.0, # 室温
|
||||
"reflux": 78.0, # 默认回流温度(乙醇沸点)
|
||||
"ice bath": 0.0, # 冰浴
|
||||
"boiling": 100.0, # 沸腾
|
||||
"hot": 60.0, # 热
|
||||
"warm": 40.0, # 温热
|
||||
"cold": 10.0, # 冷
|
||||
}
|
||||
|
||||
if temp_spec in special_temps:
|
||||
return special_temps[temp_spec]
|
||||
|
||||
# 解析带单位的温度(如 "256 °C")
|
||||
import re
|
||||
temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?'
|
||||
match = re.search(temp_pattern, temp_spec)
|
||||
|
||||
if match:
|
||||
return float(match.group(1))
|
||||
|
||||
return 25.0 # 默认室温
|
||||
|
||||
def _parse_time_spec(self, time_spec: str) -> float:
|
||||
"""解析时间规格为秒数"""
|
||||
|
||||
time_spec = time_spec.strip().lower()
|
||||
|
||||
# 特殊时间规格
|
||||
special_times = {
|
||||
"overnight": 43200.0, # 12小时
|
||||
"several hours": 10800.0, # 3小时
|
||||
"few hours": 7200.0, # 2小时
|
||||
"long time": 3600.0, # 1小时
|
||||
"short time": 300.0, # 5分钟
|
||||
}
|
||||
|
||||
if time_spec in special_times:
|
||||
return special_times[time_spec]
|
||||
|
||||
# 解析带单位的时间(如 "2 h")
|
||||
import re
|
||||
time_pattern = r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+)'
|
||||
match = re.search(time_pattern, time_spec)
|
||||
|
||||
if match:
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2).lower()
|
||||
|
||||
unit_multipliers = {
|
||||
's': 1.0,
|
||||
'sec': 1.0,
|
||||
'second': 1.0,
|
||||
'seconds': 1.0,
|
||||
'min': 60.0,
|
||||
'minute': 60.0,
|
||||
'minutes': 60.0,
|
||||
'h': 3600.0,
|
||||
'hr': 3600.0,
|
||||
'hour': 3600.0,
|
||||
'hours': 3600.0,
|
||||
}
|
||||
|
||||
multiplier = unit_multipliers.get(unit, 3600.0) # 默认按小时计算
|
||||
return value * multiplier
|
||||
|
||||
return 300.0 # 默认5分钟
|
||||
|
||||
|
||||
class HeatChillStartProtocol(BaseModel):
|
||||
vessel: str
|
||||
temp: float
|
||||
purpose: str
|
||||
# === 必需参数 ===
|
||||
vessel: str = Field(..., description="加热容器名称")
|
||||
|
||||
# === 可选参数 - 温度相关 ===
|
||||
temp: float = Field(25.0, description="目标温度 (°C)")
|
||||
temp_spec: str = Field("", description="温度规格(如 'room temperature', 'reflux')")
|
||||
|
||||
# === 可选参数 - 其他XDL参数 ===
|
||||
pressure: str = Field("", description="压力规格(如 '1 mbar'),不做特殊处理")
|
||||
reflux_solvent: str = Field("", description="回流溶剂名称,不做特殊处理")
|
||||
|
||||
# === 可选参数 - 搅拌相关 ===
|
||||
stir: bool = Field(False, description="是否搅拌")
|
||||
stir_speed: float = Field(300.0, description="搅拌速度 (RPM)")
|
||||
purpose: str = Field("", description="操作目的")
|
||||
|
||||
|
||||
class HeatChillStopProtocol(BaseModel):
|
||||
vessel: str
|
||||
# === 必需参数 ===
|
||||
vessel: str = Field(..., description="加热容器名称")
|
||||
|
||||
|
||||
class StirProtocol(BaseModel):
|
||||
stir_time: float
|
||||
stir_speed: float
|
||||
settling_time: float
|
||||
# === 必需参数 ===
|
||||
vessel: str = Field(..., description="搅拌容器名称")
|
||||
|
||||
# === 可选参数 ===
|
||||
time: str = Field("5 min", description="搅拌时间(如 '0.5 h', '30 min')")
|
||||
event: str = Field("", description="事件标识(如 'A', 'B')")
|
||||
time_spec: str = Field("", description="时间规格(如 'several minutes', 'overnight')")
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""后处理:参数验证和时间解析"""
|
||||
|
||||
# 验证必需参数
|
||||
if not self.vessel.strip():
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
# 优先使用 time_spec,然后是 time
|
||||
if self.time_spec:
|
||||
self.time = self.time_spec
|
||||
|
||||
# 时间解析和验证
|
||||
if self.time:
|
||||
try:
|
||||
# 解析时间字符串为秒数
|
||||
parsed_time = self._parse_time_string(self.time)
|
||||
if parsed_time <= 0:
|
||||
logger.warning(f"时间 '{self.time}' 解析结果无效,使用默认值 300s")
|
||||
self.time = "5 min"
|
||||
except Exception as e:
|
||||
logger.warning(f"时间 '{self.time}' 解析失败: {e},使用默认值 300s")
|
||||
self.time = "5 min"
|
||||
|
||||
def _parse_time_string(self, time_str: str) -> float:
|
||||
"""解析时间字符串为秒数"""
|
||||
import re
|
||||
|
||||
time_str = time_str.strip().lower()
|
||||
|
||||
# 特殊时间规格
|
||||
special_times = {
|
||||
"several minutes": 300.0, # 5分钟
|
||||
"few minutes": 180.0, # 3分钟
|
||||
"overnight": 43200.0, # 12小时
|
||||
"room temperature": 300.0, # 默认5分钟
|
||||
}
|
||||
|
||||
if time_str in special_times:
|
||||
return special_times[time_str]
|
||||
|
||||
# 正则表达式匹配数字和单位
|
||||
pattern = r'(\d+\.?\d*)\s*([a-zA-Z]+)'
|
||||
match = re.match(pattern, time_str)
|
||||
|
||||
if not match:
|
||||
return 300.0 # 默认5分钟
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2).lower()
|
||||
|
||||
# 时间单位转换
|
||||
unit_multipliers = {
|
||||
's': 1.0,
|
||||
'sec': 1.0,
|
||||
'second': 1.0,
|
||||
'seconds': 1.0,
|
||||
'min': 60.0,
|
||||
'minute': 60.0,
|
||||
'minutes': 60.0,
|
||||
'h': 3600.0,
|
||||
'hr': 3600.0,
|
||||
'hour': 3600.0,
|
||||
'hours': 3600.0,
|
||||
'd': 86400.0,
|
||||
'day': 86400.0,
|
||||
'days': 86400.0,
|
||||
}
|
||||
|
||||
multiplier = unit_multipliers.get(unit, 60.0) # 默认按分钟计算
|
||||
return value * multiplier
|
||||
|
||||
def get_time_in_seconds(self) -> float:
|
||||
"""获取时间(秒)"""
|
||||
return self._parse_time_string(self.time)
|
||||
|
||||
class StartStirProtocol(BaseModel):
|
||||
vessel: str
|
||||
stir_speed: float
|
||||
purpose: str
|
||||
# === 必需参数 ===
|
||||
vessel: str = Field(..., description="搅拌容器名称")
|
||||
|
||||
# === 可选参数,添加默认值 ===
|
||||
stir_speed: float = Field(200.0, description="搅拌速度 (RPM),默认200 RPM")
|
||||
purpose: str = Field("", description="搅拌目的(可选)")
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""后处理:参数验证和修正"""
|
||||
|
||||
# 验证必需参数
|
||||
if not self.vessel.strip():
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
# 修正参数范围
|
||||
if self.stir_speed < 10.0:
|
||||
logger.warning(f"搅拌速度 {self.stir_speed} RPM 过低,修正为 100 RPM")
|
||||
self.stir_speed = 100.0
|
||||
elif self.stir_speed > 1500.0:
|
||||
logger.warning(f"搅拌速度 {self.stir_speed} RPM 过高,修正为 1000 RPM")
|
||||
self.stir_speed = 1000.0
|
||||
|
||||
class StopStirProtocol(BaseModel):
|
||||
vessel: str
|
||||
# === 必需参数 ===
|
||||
vessel: str = Field(..., description="搅拌容器名称")
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""后处理:参数验证"""
|
||||
|
||||
# 验证必需参数
|
||||
if not self.vessel.strip():
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
class TransferProtocol(BaseModel):
|
||||
from_vessel: str
|
||||
@@ -168,23 +557,87 @@ class RunColumnProtocol(BaseModel):
|
||||
column: str
|
||||
|
||||
class WashSolidProtocol(BaseModel):
|
||||
vessel: str
|
||||
solvent: str
|
||||
volume: float
|
||||
filtrate_vessel: str = ""
|
||||
temp: float = 25.0
|
||||
stir: bool = False
|
||||
stir_speed: float = 0.0
|
||||
time: float = 0.0
|
||||
repeats: int = 1
|
||||
# === 必需参数 ===
|
||||
vessel: str = Field(..., description="装有固体的容器名称")
|
||||
solvent: str = Field(..., description="清洗溶剂名称")
|
||||
volume: float = Field(..., description="清洗溶剂体积 (mL)")
|
||||
|
||||
# === 可选参数,添加默认值 ===
|
||||
filtrate_vessel: str = Field("", description="滤液收集容器(可选,自动查找)")
|
||||
temp: float = Field(25.0, description="清洗温度 (°C),默认25°C")
|
||||
stir: bool = Field(False, description="是否搅拌,默认False")
|
||||
stir_speed: float = Field(0.0, description="搅拌速度 (RPM),默认0")
|
||||
time: float = Field(0.0, description="清洗时间 (秒),默认0")
|
||||
repeats: int = Field(1, description="重复次数,默认1")
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""后处理:参数验证和修正"""
|
||||
|
||||
# 验证必需参数
|
||||
if not self.vessel.strip():
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
if not self.solvent.strip():
|
||||
raise ValueError("solvent 参数不能为空")
|
||||
|
||||
if self.volume <= 0:
|
||||
raise ValueError("volume 必须大于0")
|
||||
|
||||
# 修正参数范围
|
||||
if self.temp < 0 or self.temp > 200:
|
||||
logger.warning(f"温度 {self.temp}°C 超出范围,修正为 25°C")
|
||||
self.temp = 25.0
|
||||
|
||||
if self.stir_speed < 0 or self.stir_speed > 500:
|
||||
logger.warning(f"搅拌速度 {self.stir_speed} RPM 超出范围,修正为 0")
|
||||
self.stir_speed = 0.0
|
||||
|
||||
if self.time < 0:
|
||||
logger.warning(f"时间 {self.time}s 无效,修正为 0")
|
||||
self.time = 0.0
|
||||
|
||||
if self.repeats < 1:
|
||||
logger.warning(f"重复次数 {self.repeats} 无效,修正为 1")
|
||||
self.repeats = 1
|
||||
elif self.repeats > 10:
|
||||
logger.warning(f"重复次数 {self.repeats} 过多,修正为 10")
|
||||
self.repeats = 10
|
||||
|
||||
class AdjustPHProtocol(BaseModel):
|
||||
vessel: str = Field(..., description="目标容器")
|
||||
ph_value: float = Field(..., description="目标pH值") # 改为 ph_value
|
||||
reagent: str = Field(..., description="酸碱试剂名称")
|
||||
# 移除其他可选参数,使用默认值
|
||||
|
||||
class ResetHandlingProtocol(BaseModel):
|
||||
solvent: str = Field(..., description="溶剂名称")
|
||||
|
||||
class DryProtocol(BaseModel):
|
||||
compound: str = Field(..., description="化合物名称")
|
||||
vessel: str = Field(..., description="目标容器")
|
||||
|
||||
class RecrystallizeProtocol(BaseModel):
|
||||
ratio: str = Field(..., description="溶剂比例(如 '1:1', '3:7')")
|
||||
solvent1: str = Field(..., description="第一种溶剂名称")
|
||||
solvent2: str = Field(..., description="第二种溶剂名称")
|
||||
vessel: str = Field(..., description="目标容器")
|
||||
volume: float = Field(..., description="总体积 (mL)")
|
||||
|
||||
class HydrogenateProtocol(BaseModel):
|
||||
temp: str = Field(..., description="反应温度(如 '45 °C')")
|
||||
time: str = Field(..., description="反应时间(如 '2 h')")
|
||||
vessel: str = Field(..., description="反应容器")
|
||||
|
||||
__all__ = [
|
||||
"Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol",
|
||||
"EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol",
|
||||
"CentrifugeProtocol", "AddProtocol", "FilterProtocol",
|
||||
"HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol",
|
||||
"HeatChillProtocol",
|
||||
"HeatChillStartProtocol", "HeatChillStopProtocol",
|
||||
"StirProtocol", "StartStirProtocol", "StopStirProtocol",
|
||||
"TransferProtocol", "CleanVesselProtocol", "DissolveProtocol",
|
||||
"FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol"
|
||||
"FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol",
|
||||
"AdjustPHProtocol", "ResetHandlingProtocol", "DryProtocol",
|
||||
"RecrystallizeProtocol", "HydrogenateProtocol"
|
||||
]
|
||||
# End Protocols
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
io_snrd:
|
||||
description: IO Board with 16 IOs
|
||||
class:
|
||||
module: ilabos.device_comms.SRND_16_IO:SRND_16_IO
|
||||
type: python
|
||||
hardware_interface:
|
||||
name: modbus_client
|
||||
extra_info: []
|
||||
read: read_io_coil
|
||||
write: write_io_coil
|
||||
#io_snrd:
|
||||
# description: IO Board with 16 IOs
|
||||
# class:
|
||||
# module: unilabos.device_comms.SRND_16_IO:SRND_16_IO
|
||||
# type: python
|
||||
# hardware_interface:
|
||||
# name: modbus_client
|
||||
# extra_info: []
|
||||
# read: read_io_coil
|
||||
# write: write_io_coil
|
||||
@@ -1,7 +1,102 @@
|
||||
serial:
|
||||
description: Serial communication interface, used when sharing same serial port for multiple devices
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-handle_serial_request:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
request: null
|
||||
response: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: handle_serial_request的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
request:
|
||||
type: string
|
||||
response:
|
||||
type: string
|
||||
required:
|
||||
- request
|
||||
- response
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: handle_serial_request参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-read_data:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: read_data的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: read_data参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-send_command:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send_command的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send_command参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.ros.nodes.presets.serial_node:ROS2SerialNode
|
||||
status_types: {}
|
||||
type: ros2
|
||||
schema:
|
||||
properties: {}
|
||||
description: Serial communication interface, used when sharing same serial port
|
||||
for multiple devices
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
baudrate:
|
||||
default: 9600
|
||||
type: integer
|
||||
device_id:
|
||||
type: string
|
||||
port:
|
||||
type: string
|
||||
resource_tracker:
|
||||
type: string
|
||||
required:
|
||||
- device_id
|
||||
- port
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
|
||||
69
unilabos/registry/devices/camera.yaml
Normal file
69
unilabos/registry/devices/camera.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
camera:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-destroy_node:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: destroy_node的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: destroy_node参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-timer_callback:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: timer_callback的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: timer_callback参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.ros.nodes.presets.camera:VideoPublisher
|
||||
status_types: {}
|
||||
type: ros2
|
||||
description: ''
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
camera_index:
|
||||
default: 0
|
||||
type: string
|
||||
device_id:
|
||||
default: video_publisher
|
||||
type: string
|
||||
period:
|
||||
default: 0.1
|
||||
type: number
|
||||
resource_tracker:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
@@ -1,67 +1,433 @@
|
||||
# 光学表征设备:红外、紫外可见、拉曼等
|
||||
raman_home_made:
|
||||
description: Raman spectroscopy device
|
||||
class:
|
||||
module: unilabos.devices.raman_uv.home_made_raman:RamanObj
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
action_value_mappings:
|
||||
raman_cmd:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
additionalProperties: false
|
||||
type: object
|
||||
hplc.agilent:
|
||||
description: HPLC device
|
||||
class:
|
||||
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
||||
type: python
|
||||
status_types:
|
||||
device_status: String
|
||||
could_run: Bool
|
||||
driver_init_ok: Bool
|
||||
is_running: Bool
|
||||
finish_status: String
|
||||
status_text: String
|
||||
action_value_mappings:
|
||||
auto-check_status:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: check_status的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_status参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-extract_data_from_txt:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
file_path: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: extract_data_from_txt的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
file_path:
|
||||
type: string
|
||||
required:
|
||||
- file_path
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: extract_data_from_txt参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-start_sequence:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
params: null
|
||||
resource: null
|
||||
wf_name: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: start_sequence的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
params:
|
||||
type: string
|
||||
resource:
|
||||
type: object
|
||||
wf_name:
|
||||
type: string
|
||||
required:
|
||||
- wf_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: start_sequence参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-try_close_sub_device:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
device_name: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: try_close_sub_device的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
device_name:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: try_close_sub_device参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-try_open_sub_device:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
device_name: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: try_open_sub_device的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
device_name:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: try_open_sub_device参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
execute_command_from_outer:
|
||||
type: SendCmd
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
properties:
|
||||
device_status:
|
||||
type: string
|
||||
could_run:
|
||||
type: boolean
|
||||
driver_init_ok:
|
||||
type: boolean
|
||||
is_running:
|
||||
type: boolean
|
||||
finish_status:
|
||||
type: string
|
||||
status_text:
|
||||
type: string
|
||||
required:
|
||||
- device_status
|
||||
- could_run
|
||||
- driver_init_ok
|
||||
- is_running
|
||||
- finish_status
|
||||
- status_text
|
||||
additionalProperties: false
|
||||
type: object
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
||||
status_types:
|
||||
could_run: bool
|
||||
data_file: list
|
||||
device_status: str
|
||||
driver_init_ok: bool
|
||||
finish_status: str
|
||||
is_running: bool
|
||||
status_text: str
|
||||
success: bool
|
||||
type: python
|
||||
description: HPLC device
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
driver_debug:
|
||||
default: false
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
could_run:
|
||||
type: boolean
|
||||
data_file:
|
||||
type: array
|
||||
device_status:
|
||||
type: string
|
||||
driver_init_ok:
|
||||
type: boolean
|
||||
finish_status:
|
||||
type: string
|
||||
is_running:
|
||||
type: boolean
|
||||
status_text:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- status_text
|
||||
- device_status
|
||||
- could_run
|
||||
- driver_init_ok
|
||||
- is_running
|
||||
- success
|
||||
- finish_status
|
||||
- data_file
|
||||
type: object
|
||||
raman_home_made:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-ccd_time:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
int_time: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ccd_time的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
int_time:
|
||||
type: string
|
||||
required:
|
||||
- int_time
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: ccd_time参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-laser_on_power:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
output_voltage_laser: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: laser_on_power的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
output_voltage_laser:
|
||||
type: string
|
||||
required:
|
||||
- output_voltage_laser
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: laser_on_power参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-raman_cmd:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: raman_cmd的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: raman_cmd参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-raman_without_background:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
int_time: null
|
||||
laser_power: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: raman_without_background的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
int_time:
|
||||
type: string
|
||||
laser_power:
|
||||
type: string
|
||||
required:
|
||||
- int_time
|
||||
- laser_power
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: raman_without_background参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-raman_without_background_average:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
average: null
|
||||
int_time: null
|
||||
laser_power: null
|
||||
sample_name: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: raman_without_background_average的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
average:
|
||||
type: string
|
||||
int_time:
|
||||
type: string
|
||||
laser_power:
|
||||
type: string
|
||||
sample_name:
|
||||
type: string
|
||||
required:
|
||||
- sample_name
|
||||
- int_time
|
||||
- laser_power
|
||||
- average
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: raman_without_background_average参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
raman_cmd:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.raman_uv.home_made_raman:RamanObj
|
||||
status_types: {}
|
||||
type: python
|
||||
description: Raman spectroscopy device
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
baudrate_ccd:
|
||||
default: 921600
|
||||
type: string
|
||||
baudrate_laser:
|
||||
default: 9600
|
||||
type: string
|
||||
port_ccd:
|
||||
type: string
|
||||
port_laser:
|
||||
type: string
|
||||
required:
|
||||
- port_laser
|
||||
- port_ccd
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
hotel.thermo_orbitor_rs2_hotel:
|
||||
description: Thermo Orbitor RS2 Hotel
|
||||
class:
|
||||
class:
|
||||
action_value_mappings: {}
|
||||
module: unilabos.devices.resource_container.container:HotelContainer
|
||||
status_types:
|
||||
rotation: String
|
||||
type: python
|
||||
description: Thermo Orbitor RS2 Hotel
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
device_config:
|
||||
type: object
|
||||
rotation:
|
||||
type: object
|
||||
required:
|
||||
- rotation
|
||||
- device_config
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
rotation:
|
||||
type: string
|
||||
required:
|
||||
- rotation
|
||||
type: object
|
||||
model:
|
||||
type: device
|
||||
mesh: thermo_orbitor_rs2_hotel
|
||||
|
||||
type: device
|
||||
|
||||
@@ -1,56 +1,582 @@
|
||||
laiyu_add_solid:
|
||||
description: Laiyu Add Solid
|
||||
class:
|
||||
module: unilabos.devices.laiyu_add_solid.laiyu:Laiyu
|
||||
type: python
|
||||
status_types: {}
|
||||
action_value_mappings:
|
||||
add_powder_tube:
|
||||
feedback: {}
|
||||
goal:
|
||||
compound_mass: compound_mass
|
||||
powder_tube_number: powder_tube_number
|
||||
target_tube_position: target_tube_position
|
||||
goal_default:
|
||||
compound_mass: 0.0
|
||||
powder_tube_number: 0
|
||||
target_tube_position: ''
|
||||
handles: []
|
||||
result:
|
||||
actual_mass_mg: actual_mass_mg
|
||||
schema:
|
||||
description: ROS Action SolidDispenseAddPowderTube 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: SolidDispenseAddPowderTube_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
compound_mass:
|
||||
type: number
|
||||
powder_tube_number:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
target_tube_position:
|
||||
type: string
|
||||
required:
|
||||
- powder_tube_number
|
||||
- target_tube_position
|
||||
- compound_mass
|
||||
title: SolidDispenseAddPowderTube_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
actual_mass_mg:
|
||||
type: number
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- actual_mass_mg
|
||||
- success
|
||||
title: SolidDispenseAddPowderTube_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SolidDispenseAddPowderTube
|
||||
type: object
|
||||
type: SolidDispenseAddPowderTube
|
||||
auto-add_powder_tube:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
compound_mass: null
|
||||
powder_tube_number: null
|
||||
target_tube_position: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: add_powder_tube的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
compound_mass:
|
||||
type: string
|
||||
powder_tube_number:
|
||||
type: string
|
||||
target_tube_position:
|
||||
type: string
|
||||
required:
|
||||
- powder_tube_number
|
||||
- target_tube_position
|
||||
- compound_mass
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: add_powder_tube参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-calculate_crc:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
data: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: calculate_crc的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
data:
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: calculate_crc参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-discharge:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
float_in: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: discharge的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
float_in:
|
||||
type: number
|
||||
required:
|
||||
- float_in
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: discharge参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-move_to_plate:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
string: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: move_to_plate的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
string:
|
||||
type: string
|
||||
required:
|
||||
- string
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: move_to_plate参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-move_to_xyz:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
x: null
|
||||
y: null
|
||||
z: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: move_to_xyz的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
x:
|
||||
type: number
|
||||
y:
|
||||
type: number
|
||||
z:
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: move_to_xyz参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-pick_powder_tube:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
int_input: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: pick_powder_tube的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
int_input:
|
||||
type: integer
|
||||
required:
|
||||
- int_input
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: pick_powder_tube参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-put_powder_tube:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
int_input: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: put_powder_tube的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
int_input:
|
||||
type: integer
|
||||
required:
|
||||
- int_input
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: put_powder_tube参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-reset:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: reset的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: reset参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-send_command:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send_command的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send_command参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
discharge:
|
||||
feedback: {}
|
||||
goal:
|
||||
float_input: float_input
|
||||
goal_default:
|
||||
float_in: 0.0
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action FloatSingleInput 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: FloatSingleInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
float_in:
|
||||
type: number
|
||||
required:
|
||||
- float_in
|
||||
title: FloatSingleInput_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: FloatSingleInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: FloatSingleInput
|
||||
type: object
|
||||
type: FloatSingleInput
|
||||
move_to_plate:
|
||||
feedback: {}
|
||||
goal:
|
||||
string: string
|
||||
goal_default:
|
||||
string: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action StrSingleInput 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: StrSingleInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
string:
|
||||
type: string
|
||||
required:
|
||||
- string
|
||||
title: StrSingleInput_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: StrSingleInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: StrSingleInput
|
||||
type: object
|
||||
type: StrSingleInput
|
||||
move_to_xyz:
|
||||
type: Point3DSeparateInput
|
||||
feedback: {}
|
||||
goal:
|
||||
x: x
|
||||
y: y
|
||||
z: z
|
||||
feedback: {}
|
||||
goal_default:
|
||||
x: 0.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action Point3DSeparateInput 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: Point3DSeparateInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
x:
|
||||
type: number
|
||||
y:
|
||||
type: number
|
||||
z:
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
title: Point3DSeparateInput_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: Point3DSeparateInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: Point3DSeparateInput
|
||||
type: object
|
||||
type: Point3DSeparateInput
|
||||
pick_powder_tube:
|
||||
type: IntSingleInput
|
||||
feedback: {}
|
||||
goal:
|
||||
int_input: int_input
|
||||
feedback: {}
|
||||
goal_default:
|
||||
int_input: 0
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action IntSingleInput 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: IntSingleInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
int_input:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
required:
|
||||
- int_input
|
||||
title: IntSingleInput_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: IntSingleInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: IntSingleInput
|
||||
type: object
|
||||
type: IntSingleInput
|
||||
put_powder_tube:
|
||||
type: IntSingleInput
|
||||
feedback: {}
|
||||
goal:
|
||||
int_input: int_input
|
||||
feedback: {}
|
||||
goal_default:
|
||||
int_input: 0
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action IntSingleInput 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: IntSingleInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
int_input:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
required:
|
||||
- int_input
|
||||
title: IntSingleInput_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: IntSingleInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: IntSingleInput
|
||||
type: object
|
||||
type: IntSingleInput
|
||||
reset:
|
||||
type: EmptyIn
|
||||
feedback: {}
|
||||
goal: {}
|
||||
feedback: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
add_powder_tube:
|
||||
type: SolidDispenseAddPowderTube
|
||||
goal:
|
||||
powder_tube_number: powder_tube_number
|
||||
target_tube_position: target_tube_position
|
||||
compound_mass: compound_mass
|
||||
feedback: {}
|
||||
result:
|
||||
actual_mass_mg: actual_mass_mg
|
||||
move_to_plate:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: string
|
||||
feedback: {}
|
||||
result: {}
|
||||
discharge:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
float_input: float_input
|
||||
feedback: {}
|
||||
result: {}
|
||||
|
||||
schema:
|
||||
properties: {}
|
||||
schema:
|
||||
description: ROS Action EmptyIn 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
module: unilabos.devices.laiyu_add_solid.laiyu:Laiyu
|
||||
status_types:
|
||||
status: str
|
||||
type: python
|
||||
description: Laiyu Add Solid
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
baudrate:
|
||||
default: 115200
|
||||
type: string
|
||||
port:
|
||||
type: string
|
||||
timeout:
|
||||
default: 0.5
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,56 +1,858 @@
|
||||
moveit.toyo_xyz:
|
||||
description: Toyo XYZ
|
||||
class:
|
||||
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
|
||||
type: python
|
||||
action_value_mappings:
|
||||
set_position:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: command
|
||||
feedback: { }
|
||||
result: { }
|
||||
pick_and_place:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: command
|
||||
feedback: { }
|
||||
result: { }
|
||||
set_status:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: command
|
||||
feedback: { }
|
||||
result: { }
|
||||
|
||||
model:
|
||||
type: device
|
||||
mesh: toyo_xyz
|
||||
|
||||
moveit.arm_slider:
|
||||
description: Arm with Slider
|
||||
model:
|
||||
type: device
|
||||
mesh: arm_slider
|
||||
class:
|
||||
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
|
||||
type: python
|
||||
action_value_mappings:
|
||||
set_position:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: command
|
||||
auto-check_tf_update_actions:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: check_tf_update_actions的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_tf_update_actions参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_joint_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
joint_names: null
|
||||
joint_positions: null
|
||||
move_group: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_joint_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
joint_names:
|
||||
type: string
|
||||
joint_positions:
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- joint_positions
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_joint_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
cartesian: false
|
||||
move_group: null
|
||||
offsets:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
position: null
|
||||
quaternion: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
target_link: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
cartesian:
|
||||
default: false
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
offsets:
|
||||
default:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
type: string
|
||||
position:
|
||||
type: string
|
||||
quaternion:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
target_link:
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- position
|
||||
- quaternion
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-pick_and_place:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: pick_and_place的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: pick_and_place参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-post_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
ros_node: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: post_init的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
ros_node:
|
||||
type: string
|
||||
required:
|
||||
- ros_node
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: post_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-resource_manager:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parent_link: null
|
||||
resource: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: resource_manager的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parent_link:
|
||||
type: string
|
||||
resource:
|
||||
type: string
|
||||
required:
|
||||
- resource
|
||||
- parent_link
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: resource_manager参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_status:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_status的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_status参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_for_resource_action:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_for_resource_action的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_for_resource_action参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
pick_and_place:
|
||||
type: SendCmd
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_position:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_status:
|
||||
type: SendCmd
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
|
||||
status_types: {}
|
||||
type: python
|
||||
description: Arm with Slider
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
device_config:
|
||||
type: string
|
||||
joint_poses:
|
||||
type: string
|
||||
moveit_type:
|
||||
type: string
|
||||
rotation:
|
||||
type: string
|
||||
required:
|
||||
- moveit_type
|
||||
- joint_poses
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
model:
|
||||
mesh: arm_slider
|
||||
type: device
|
||||
moveit.toyo_xyz:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-check_tf_update_actions:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: check_tf_update_actions的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_tf_update_actions参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_joint_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
joint_names: null
|
||||
joint_positions: null
|
||||
move_group: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_joint_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
joint_names:
|
||||
type: string
|
||||
joint_positions:
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- joint_positions
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_joint_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
cartesian: false
|
||||
move_group: null
|
||||
offsets:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
position: null
|
||||
quaternion: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
target_link: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
cartesian:
|
||||
default: false
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
offsets:
|
||||
default:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
type: string
|
||||
position:
|
||||
type: string
|
||||
quaternion:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
target_link:
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- position
|
||||
- quaternion
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-pick_and_place:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: pick_and_place的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: pick_and_place参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-post_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
ros_node: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: post_init的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
ros_node:
|
||||
type: string
|
||||
required:
|
||||
- ros_node
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: post_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-resource_manager:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parent_link: null
|
||||
resource: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: resource_manager的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parent_link:
|
||||
type: string
|
||||
resource:
|
||||
type: string
|
||||
required:
|
||||
- resource
|
||||
- parent_link
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: resource_manager参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_status:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_status的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_status参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_for_resource_action:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_for_resource_action的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_for_resource_action参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
pick_and_place:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_position:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_status:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
|
||||
status_types: {}
|
||||
type: python
|
||||
description: Toyo XYZ
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
device_config:
|
||||
type: string
|
||||
joint_poses:
|
||||
type: string
|
||||
moveit_type:
|
||||
type: string
|
||||
rotation:
|
||||
type: string
|
||||
required:
|
||||
- moveit_type
|
||||
- joint_poses
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
model:
|
||||
mesh: toyo_xyz
|
||||
type: device
|
||||
|
||||
@@ -1,73 +1,470 @@
|
||||
separator.homemade:
|
||||
description: Separator device with homemade grbl controller
|
||||
class:
|
||||
module: unilabos.devices.separator.homemade_grbl_conductivity:SeparatorController
|
||||
type: python
|
||||
status_types:
|
||||
sensordata: Float64
|
||||
status: String
|
||||
action_value_mappings:
|
||||
stir:
|
||||
type: Stir
|
||||
goal:
|
||||
stir_time: stir_time,
|
||||
stir_speed: stir_speed
|
||||
settling_time: settling_time
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
valve_open_cmd:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: command
|
||||
feedback:
|
||||
status: status
|
||||
result":
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: The status of the device
|
||||
sensordata:
|
||||
type: number
|
||||
description: 电导传感器数据
|
||||
required:
|
||||
- status
|
||||
- sensordata
|
||||
additionalProperties: false
|
||||
|
||||
rotavap.one:
|
||||
description: Rotavap device
|
||||
class:
|
||||
module: unilabos.devices.rotavap.rotavap_one:RotavapOne
|
||||
type: python
|
||||
status_types:
|
||||
pump_time: Float64
|
||||
rotate_time: Float64
|
||||
action_value_mappings:
|
||||
auto-cmd_write:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
cmd: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: cmd_write的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
cmd:
|
||||
type: string
|
||||
required:
|
||||
- cmd
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: cmd_write参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-main_loop:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: main_loop的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: main_loop参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_pump_time:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
time: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_pump_time的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
time:
|
||||
type: string
|
||||
required:
|
||||
- time
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_pump_time参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_rotate_time:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
time: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_rotate_time的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
time:
|
||||
type: string
|
||||
required:
|
||||
- time
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_rotate_time参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_timer:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_timer的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_timer参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
set_timer:
|
||||
type: SendCmd
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
temperature:
|
||||
type: number
|
||||
description: 旋蒸水浴温度
|
||||
pump_time:
|
||||
type: number
|
||||
description: The pump time of the device
|
||||
rotate_time:
|
||||
type: number
|
||||
description: The rotate time of the device
|
||||
required:
|
||||
- pump_time
|
||||
- rotate_time
|
||||
additionalProperties: false
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.rotavap.rotavap_one:RotavapOne
|
||||
status_types: {}
|
||||
type: python
|
||||
description: Rotavap device
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
rate:
|
||||
default: 9600
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
separator.homemade:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-read_sensor_loop:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: read_sensor_loop的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: read_sensor_loop参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-stir:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
settling_time: 10
|
||||
stir_speed: 300
|
||||
stir_time: 10
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: stir的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
settling_time:
|
||||
default: 10
|
||||
type: number
|
||||
stir_speed:
|
||||
default: 300
|
||||
type: number
|
||||
stir_time:
|
||||
default: 10
|
||||
type: number
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: stir参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-valve_open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
condition: null
|
||||
value: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: valve_open的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
condition:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- condition
|
||||
- value
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: valve_open参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-valve_open_cmd:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: valve_open_cmd的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: valve_open_cmd参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-write:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
data: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: write的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
data:
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: write参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
stir:
|
||||
feedback:
|
||||
status: status
|
||||
goal:
|
||||
settling_time: settling_time
|
||||
stir_speed: stir_speed
|
||||
stir_time: stir_time,
|
||||
goal_default:
|
||||
event: ''
|
||||
settling_time: 0.0
|
||||
stir_speed: 0.0
|
||||
stir_time: 0.0
|
||||
time: ''
|
||||
time_spec: ''
|
||||
vessel: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ROS Action Stir 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: Stir_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
event:
|
||||
type: string
|
||||
settling_time:
|
||||
type: number
|
||||
stir_speed:
|
||||
type: number
|
||||
stir_time:
|
||||
type: number
|
||||
time:
|
||||
type: string
|
||||
time_spec:
|
||||
type: string
|
||||
vessel:
|
||||
type: string
|
||||
required:
|
||||
- vessel
|
||||
- time
|
||||
- event
|
||||
- time_spec
|
||||
- stir_time
|
||||
- stir_speed
|
||||
- settling_time
|
||||
title: Stir_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- success
|
||||
- message
|
||||
- return_info
|
||||
title: Stir_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: Stir
|
||||
type: object
|
||||
type: Stir
|
||||
valve_open_cmd:
|
||||
feedback:
|
||||
status: status
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result":
|
||||
success: success
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.separator.homemade_grbl_conductivity:SeparatorController
|
||||
status_types: {}
|
||||
type: python
|
||||
description: Separator device with homemade grbl controller
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
baudrate_executor:
|
||||
default: 115200
|
||||
type: integer
|
||||
baudrate_sensor:
|
||||
default: 115200
|
||||
type: integer
|
||||
port_executor:
|
||||
type: string
|
||||
port_sensor:
|
||||
type: string
|
||||
required:
|
||||
- port_executor
|
||||
- port_sensor
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
|
||||
@@ -1,85 +1,877 @@
|
||||
syringe_pump_with_valve.runze:
|
||||
description: Runze Syringe pump with valve
|
||||
solenoid_valve:
|
||||
class:
|
||||
module: unilabos.devices.pump_and_valve.runze_backbone:RunzeSyringePump
|
||||
action_value_mappings:
|
||||
auto-close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: close的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: close参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-is_closed:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: is_closed的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: is_closed参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-is_open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: is_open的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: is_open参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: open的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: open参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-read_data:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: read_data的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: read_data参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-send_command:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send_command的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send_command参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_valve_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
position: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_valve_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
position:
|
||||
type: string
|
||||
required:
|
||||
- position
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_valve_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
set_valve_position:
|
||||
feedback: {}
|
||||
goal:
|
||||
string: position
|
||||
goal_default:
|
||||
string: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action StrSingleInput 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: StrSingleInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
string:
|
||||
type: string
|
||||
required:
|
||||
- string
|
||||
title: StrSingleInput_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: StrSingleInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: StrSingleInput
|
||||
type: object
|
||||
type: StrSingleInput
|
||||
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
|
||||
status_types:
|
||||
status: str
|
||||
valve_position: str
|
||||
type: python
|
||||
description: Solenoid valve
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
io_device_port:
|
||||
type: string
|
||||
required:
|
||||
- io_device_port
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
valve_position:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- valve_position
|
||||
type: object
|
||||
solenoid_valve.mock:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: close的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: close参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-is_closed:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: is_closed的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: is_closed参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-is_open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: is_open的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: is_open参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: open的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: open参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_valve_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
position: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_valve_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
position:
|
||||
type: string
|
||||
required:
|
||||
- position
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_valve_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action EmptyIn 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action EmptyIn 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
module: unilabos.devices.pump_and_valve.solenoid_valve_mock:SolenoidValveMock
|
||||
status_types:
|
||||
status: str
|
||||
valve_position: str
|
||||
type: python
|
||||
description: Mock solenoid valve
|
||||
handles:
|
||||
- data_type: fluid
|
||||
handler_key: in
|
||||
io_type: target
|
||||
label: in
|
||||
side: NORTH
|
||||
- data_type: fluid
|
||||
handler_key: out
|
||||
io_type: source
|
||||
label: out
|
||||
side: SOUTH
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
port:
|
||||
default: COM6
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
valve_position:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- valve_position
|
||||
type: object
|
||||
syringe_pump_with_valve.runze:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: close的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: close参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-initialize:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: initialize的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: initialize参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-pull_plunger:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
volume: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: pull_plunger的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
volume:
|
||||
type: number
|
||||
required:
|
||||
- volume
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: pull_plunger参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-push_plunger:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
volume: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: push_plunger的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
volume:
|
||||
type: number
|
||||
required:
|
||||
- volume
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: push_plunger参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-query_aux_input_status_1:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: query_aux_input_status_1的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: query_aux_input_status_1参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-query_aux_input_status_2:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: query_aux_input_status_2的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: query_aux_input_status_2参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-query_backlash_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: query_backlash_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: query_backlash_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-query_command_buffer_status:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: query_command_buffer_status的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: query_command_buffer_status参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-query_software_version:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: query_software_version的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: query_software_version参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-send_command:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
full_command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send_command的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
full_command:
|
||||
type: string
|
||||
required:
|
||||
- full_command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send_command参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_baudrate:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
baudrate: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_baudrate的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
baudrate:
|
||||
type: string
|
||||
required:
|
||||
- baudrate
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_baudrate参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_max_velocity:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
velocity: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_max_velocity的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
velocity:
|
||||
type: number
|
||||
required:
|
||||
- velocity
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_max_velocity参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
max_velocity: null
|
||||
position: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
max_velocity:
|
||||
type: number
|
||||
position:
|
||||
type: number
|
||||
required:
|
||||
- position
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_valve_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
position: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_valve_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
position:
|
||||
type: string
|
||||
required:
|
||||
- position
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_valve_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_velocity_grade:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
velocity: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_velocity_grade的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
velocity:
|
||||
type: string
|
||||
required:
|
||||
- velocity
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_velocity_grade参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-stop_operation:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: stop_operation的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: stop_operation参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_error:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_error的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_error参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
hardware_interface:
|
||||
name: hardware_interface
|
||||
read: send_command
|
||||
write: send_command
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: The status of the device
|
||||
position:
|
||||
type: number
|
||||
description: The volume of the syringe
|
||||
speed_max:
|
||||
type: number
|
||||
description: The speed of the syringe
|
||||
valve_position:
|
||||
type: string
|
||||
description: The position of the valve
|
||||
required:
|
||||
- status
|
||||
- position
|
||||
- valve_position
|
||||
additionalProperties: false
|
||||
|
||||
solenoid_valve.mock:
|
||||
description: Mock solenoid valve
|
||||
class:
|
||||
module: unilabos.devices.pump_and_valve.solenoid_valve_mock:SolenoidValveMock
|
||||
type: python
|
||||
module: unilabos.devices.pump_and_valve.runze_backbone:RunzeSyringePump
|
||||
status_types:
|
||||
status: String
|
||||
valve_position: String
|
||||
action_value_mappings:
|
||||
open:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
close:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
- handler_key: in
|
||||
label: in
|
||||
io_type: target
|
||||
data_type: fluid
|
||||
side: NORTH
|
||||
- handler_key: out
|
||||
label: out
|
||||
io_type: source
|
||||
data_type: fluid
|
||||
side: SOUTH
|
||||
max_velocity: float
|
||||
mode: int
|
||||
plunger_position: String
|
||||
position: float
|
||||
status: str
|
||||
valve_position: str
|
||||
velocity_end: String
|
||||
velocity_grade: String
|
||||
velocity_init: String
|
||||
type: python
|
||||
description: Runze Syringe pump with valve
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
description: "通信端口"
|
||||
default: "COM6"
|
||||
required:
|
||||
- port
|
||||
|
||||
solenoid_valve:
|
||||
description: Solenoid valve
|
||||
class:
|
||||
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
valve_position: String
|
||||
action_value_mappings:
|
||||
set_valve_position:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: position
|
||||
feedback: {}
|
||||
result: {}
|
||||
config:
|
||||
properties:
|
||||
address:
|
||||
default: '1'
|
||||
type: string
|
||||
max_volume:
|
||||
default: 25.0
|
||||
type: number
|
||||
mode:
|
||||
type: string
|
||||
port:
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
max_velocity:
|
||||
type: number
|
||||
mode:
|
||||
type: integer
|
||||
plunger_position:
|
||||
type: string
|
||||
position:
|
||||
type: number
|
||||
status:
|
||||
type: string
|
||||
valve_position:
|
||||
type: string
|
||||
velocity_end:
|
||||
type: string
|
||||
velocity_grade:
|
||||
type: string
|
||||
velocity_init:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- mode
|
||||
- max_velocity
|
||||
- velocity_grade
|
||||
- velocity_init
|
||||
- velocity_end
|
||||
- valve_position
|
||||
- position
|
||||
- plunger_position
|
||||
type: object
|
||||
|
||||
@@ -1,29 +1,132 @@
|
||||
# 仙工智能底盘(知行使用)
|
||||
agv.SEER:
|
||||
description: SEER AGV
|
||||
class:
|
||||
module: unilabos.devices.agv.agv_navigator:AgvNavigator
|
||||
type: python
|
||||
status_types:
|
||||
pose: Float64MultiArray
|
||||
status: String
|
||||
action_value_mappings:
|
||||
auto-send:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
cmd: null
|
||||
ex_data: ''
|
||||
obj: receive_socket
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
cmd:
|
||||
type: string
|
||||
ex_data:
|
||||
default: ''
|
||||
type: string
|
||||
obj:
|
||||
default: receive_socket
|
||||
type: string
|
||||
required:
|
||||
- cmd
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-send_nav_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send_nav_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send_nav_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
send_nav_task:
|
||||
type: SendCmd
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
properties:
|
||||
pose:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.agv.agv_navigator:AgvNavigator
|
||||
status_types:
|
||||
pose: list
|
||||
status: str
|
||||
type: python
|
||||
description: SEER AGV
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
host:
|
||||
type: string
|
||||
required:
|
||||
- host
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
pose:
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- pose
|
||||
- status
|
||||
additionalProperties: false
|
||||
type: object
|
||||
type: object
|
||||
|
||||
@@ -1,37 +1,199 @@
|
||||
robotic_arm.UR:
|
||||
description: UR robotic arm
|
||||
class:
|
||||
module: unilabos.devices.agv.ur_arm_task:UrArmTask
|
||||
type: python
|
||||
status_types:
|
||||
arm_pose: Float64MultiArray
|
||||
gripper_pose: Float64
|
||||
arm_status: String
|
||||
gripper_status: String
|
||||
action_value_mappings:
|
||||
auto-arm_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: arm_init的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: arm_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-load_pose_data:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
data: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: load_pose_data的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
data:
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: load_pose_data参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-load_pose_file:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
file: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: load_pose_file的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
required:
|
||||
- file
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: load_pose_file参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-move_pos_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: move_pos_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: move_pos_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-reload_pose:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: reload_pose的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: reload_pose参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
move_pos_task:
|
||||
type: SendCmd
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
properties:
|
||||
arm_pose:
|
||||
type: array
|
||||
items:
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.agv.ur_arm_task:UrArmTask
|
||||
status_types:
|
||||
arm_pose: list
|
||||
arm_status: str
|
||||
gripper_pose: float
|
||||
gripper_status: str
|
||||
type: python
|
||||
description: UR robotic arm
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
host:
|
||||
type: string
|
||||
retry:
|
||||
default: 30
|
||||
type: string
|
||||
required:
|
||||
- host
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
arm_pose:
|
||||
type: array
|
||||
arm_status:
|
||||
type: string
|
||||
gripper_pose:
|
||||
type: number
|
||||
gripper_pose:
|
||||
type: number
|
||||
arm_status:
|
||||
type: string
|
||||
description: 机械臂设备状态
|
||||
gripper_status:
|
||||
type: string
|
||||
description: 机械爪设备状态
|
||||
required:
|
||||
gripper_status:
|
||||
type: string
|
||||
required:
|
||||
- arm_pose
|
||||
- gripper_pose
|
||||
- arm_status
|
||||
- gripper_status
|
||||
additionalProperties: false
|
||||
type: object
|
||||
type: object
|
||||
|
||||
@@ -1,37 +1,626 @@
|
||||
gripper.mock:
|
||||
description: Mock gripper
|
||||
class:
|
||||
module: unilabos.devices.gripper.mock:MockGripper
|
||||
type: python
|
||||
status_types:
|
||||
position: Float64
|
||||
torque: Float64
|
||||
status: String
|
||||
action_value_mappings:
|
||||
push_to:
|
||||
type: GripperCommand
|
||||
goal:
|
||||
command.position: position
|
||||
command.max_effort: torque
|
||||
feedback:
|
||||
position: position
|
||||
effort: torque
|
||||
result:
|
||||
position: position
|
||||
effort: torque
|
||||
|
||||
gripper.misumi_rz:
|
||||
description: Misumi RZ gripper
|
||||
class:
|
||||
module: unilabos.devices.motor:Grasp.EleGripper
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
action_value_mappings:
|
||||
auto-data_loop:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: data_loop的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: data_loop参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-data_reader:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: data_reader的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: data_reader参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-gripper_move:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
force: null
|
||||
pos: null
|
||||
speed: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: gripper_move的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
force:
|
||||
type: string
|
||||
pos:
|
||||
type: string
|
||||
speed:
|
||||
type: string
|
||||
required:
|
||||
- pos
|
||||
- speed
|
||||
- force
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: gripper_move参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-init_gripper:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: init_gripper的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: init_gripper参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-modbus_crc:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
data: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: modbus_crc的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
data:
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: modbus_crc参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-move_and_rotate:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
grasp_F: null
|
||||
grasp_pos: null
|
||||
grasp_v: null
|
||||
spin_F: null
|
||||
spin_pos: null
|
||||
spin_v: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: move_and_rotate的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
grasp_F:
|
||||
type: string
|
||||
grasp_pos:
|
||||
type: string
|
||||
grasp_v:
|
||||
type: string
|
||||
spin_F:
|
||||
type: string
|
||||
spin_pos:
|
||||
type: string
|
||||
spin_v:
|
||||
type: string
|
||||
required:
|
||||
- spin_pos
|
||||
- grasp_pos
|
||||
- spin_v
|
||||
- grasp_v
|
||||
- spin_F
|
||||
- grasp_F
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: move_and_rotate参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-node_gripper_move:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
cmd: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: node_gripper_move的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
cmd:
|
||||
type: string
|
||||
required:
|
||||
- cmd
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: node_gripper_move参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-node_rotate_move:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
cmd: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: node_rotate_move的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
cmd:
|
||||
type: string
|
||||
required:
|
||||
- cmd
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: node_rotate_move参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-read_address:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
address: null
|
||||
data_len: null
|
||||
id: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: read_address的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
address:
|
||||
type: string
|
||||
data_len:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- address
|
||||
- data_len
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: read_address参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-rotate_move_abs:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
force: null
|
||||
pos: null
|
||||
speed: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: rotate_move_abs的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
force:
|
||||
type: string
|
||||
pos:
|
||||
type: string
|
||||
speed:
|
||||
type: string
|
||||
required:
|
||||
- pos
|
||||
- speed
|
||||
- force
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: rotate_move_abs参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-send_cmd:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
address: null
|
||||
data: null
|
||||
fun: null
|
||||
id: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send_cmd的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
address:
|
||||
type: string
|
||||
data:
|
||||
type: string
|
||||
fun:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- fun
|
||||
- address
|
||||
- data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send_cmd参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_for_gripper:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_for_gripper的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_for_gripper参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_for_gripper_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_for_gripper_init的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_for_gripper_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_for_rotate:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_for_rotate的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_for_rotate参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
execute_command_from_outer:
|
||||
type: SendCmd
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.motor.Grasp:EleGripper
|
||||
status_types:
|
||||
status: str
|
||||
type: python
|
||||
description: Misumi RZ gripper
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
baudrate:
|
||||
default: 115200
|
||||
type: string
|
||||
id:
|
||||
default: 9
|
||||
type: string
|
||||
port:
|
||||
type: string
|
||||
pos_error:
|
||||
default: -11
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
gripper.mock:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-edit_id:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
params: '{}'
|
||||
resource:
|
||||
Gripper1: {}
|
||||
wf_name: gripper_run
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: edit_id的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
params:
|
||||
default: '{}'
|
||||
type: string
|
||||
resource:
|
||||
default:
|
||||
Gripper1: {}
|
||||
type: object
|
||||
wf_name:
|
||||
default: gripper_run
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: edit_id参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-push_to:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
position: null
|
||||
torque: null
|
||||
velocity: 0.0
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: push_to的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
position:
|
||||
type: number
|
||||
torque:
|
||||
type: number
|
||||
velocity:
|
||||
default: 0.0
|
||||
type: number
|
||||
required:
|
||||
- position
|
||||
- torque
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: push_to参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
push_to:
|
||||
feedback:
|
||||
effort: torque
|
||||
position: position
|
||||
goal:
|
||||
command.max_effort: torque
|
||||
command.position: position
|
||||
goal_default:
|
||||
command:
|
||||
max_effort: 0.0
|
||||
position: 0.0
|
||||
handles: []
|
||||
result:
|
||||
effort: torque
|
||||
position: position
|
||||
schema:
|
||||
description: ROS Action GripperCommand 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
effort:
|
||||
type: number
|
||||
position:
|
||||
type: number
|
||||
reached_goal:
|
||||
type: boolean
|
||||
stalled:
|
||||
type: boolean
|
||||
required:
|
||||
- position
|
||||
- effort
|
||||
- stalled
|
||||
- reached_goal
|
||||
title: GripperCommand_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
properties:
|
||||
max_effort:
|
||||
type: number
|
||||
position:
|
||||
type: number
|
||||
required:
|
||||
- position
|
||||
- max_effort
|
||||
title: GripperCommand
|
||||
type: object
|
||||
required:
|
||||
- command
|
||||
title: GripperCommand_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
effort:
|
||||
type: number
|
||||
position:
|
||||
type: number
|
||||
reached_goal:
|
||||
type: boolean
|
||||
stalled:
|
||||
type: boolean
|
||||
required:
|
||||
- position
|
||||
- effort
|
||||
- stalled
|
||||
- reached_goal
|
||||
title: GripperCommand_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: GripperCommand
|
||||
type: object
|
||||
type: GripperCommand
|
||||
module: unilabos.devices.gripper.mock:MockGripper
|
||||
status_types:
|
||||
position: float
|
||||
status: str
|
||||
torque: float
|
||||
velocity: float
|
||||
type: python
|
||||
description: Mock gripper
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
position:
|
||||
type: number
|
||||
status:
|
||||
type: string
|
||||
torque:
|
||||
type: number
|
||||
velocity:
|
||||
type: number
|
||||
required:
|
||||
- position
|
||||
- velocity
|
||||
- torque
|
||||
- status
|
||||
type: object
|
||||
|
||||
@@ -1,57 +1,693 @@
|
||||
linear_motion.grbl:
|
||||
description: Grbl CNC
|
||||
class:
|
||||
module: unilabos.devices.cnc.grbl_sync:GrblCNC
|
||||
type: python
|
||||
action_value_mappings:
|
||||
move_through_points: &move_through_points
|
||||
type: NavigateThroughPoses
|
||||
goal:
|
||||
poses[].pose.position: positions[]
|
||||
feedback:
|
||||
current_pose.pose.position: position
|
||||
navigation_time.sec: time_spent
|
||||
estimated_time_remaining.sec: time_remaining
|
||||
number_of_poses_remaining: pose_number_remaining
|
||||
auto-initialize:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
set_spindle_speed:
|
||||
type: SingleJointPosition
|
||||
schema:
|
||||
description: initialize的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: initialize参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-move_through_points:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
positions: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: move_through_points的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
positions:
|
||||
type: array
|
||||
required:
|
||||
- positions
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: move_through_points参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
position: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
position:
|
||||
type: string
|
||||
required:
|
||||
- position
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_spindle_speed:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
max_velocity: 500
|
||||
spindle_speed: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_spindle_speed的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
max_velocity:
|
||||
default: 500
|
||||
type: number
|
||||
spindle_speed:
|
||||
type: number
|
||||
required:
|
||||
- spindle_speed
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_spindle_speed参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-stop_operation:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: stop_operation的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: stop_operation参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_error:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_error的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_error参数
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
move_through_points:
|
||||
feedback:
|
||||
current_pose.pose.position: position
|
||||
estimated_time_remaining.sec: time_remaining
|
||||
navigation_time.sec: time_spent
|
||||
number_of_poses_remaining: pose_number_remaining
|
||||
goal:
|
||||
position: spindle_speed
|
||||
poses[].pose.position: positions[]
|
||||
goal_default:
|
||||
behavior_tree: ''
|
||||
poses:
|
||||
- header:
|
||||
frame_id: ''
|
||||
stamp:
|
||||
nanosec: 0
|
||||
sec: 0
|
||||
pose:
|
||||
orientation:
|
||||
w: 1.0
|
||||
x: 0.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
position:
|
||||
x: 0.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action NavigateThroughPoses 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
current_pose:
|
||||
properties:
|
||||
header:
|
||||
properties:
|
||||
frame_id:
|
||||
type: string
|
||||
stamp:
|
||||
properties:
|
||||
nanosec:
|
||||
maximum: 4294967295
|
||||
minimum: 0
|
||||
type: integer
|
||||
sec:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Time
|
||||
type: object
|
||||
required:
|
||||
- stamp
|
||||
- frame_id
|
||||
title: Header
|
||||
type: object
|
||||
pose:
|
||||
properties:
|
||||
orientation:
|
||||
properties:
|
||||
w:
|
||||
type: number
|
||||
x:
|
||||
type: number
|
||||
y:
|
||||
type: number
|
||||
z:
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
- w
|
||||
title: Quaternion
|
||||
type: object
|
||||
position:
|
||||
properties:
|
||||
x:
|
||||
type: number
|
||||
y:
|
||||
type: number
|
||||
z:
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
title: Point
|
||||
type: object
|
||||
required:
|
||||
- position
|
||||
- orientation
|
||||
title: Pose
|
||||
type: object
|
||||
required:
|
||||
- header
|
||||
- pose
|
||||
title: PoseStamped
|
||||
type: object
|
||||
distance_remaining:
|
||||
type: number
|
||||
estimated_time_remaining:
|
||||
properties:
|
||||
nanosec:
|
||||
maximum: 4294967295
|
||||
minimum: 0
|
||||
type: integer
|
||||
sec:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Duration
|
||||
type: object
|
||||
navigation_time:
|
||||
properties:
|
||||
nanosec:
|
||||
maximum: 4294967295
|
||||
minimum: 0
|
||||
type: integer
|
||||
sec:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Duration
|
||||
type: object
|
||||
number_of_poses_remaining:
|
||||
maximum: 32767
|
||||
minimum: -32768
|
||||
type: integer
|
||||
number_of_recoveries:
|
||||
maximum: 32767
|
||||
minimum: -32768
|
||||
type: integer
|
||||
required:
|
||||
- current_pose
|
||||
- navigation_time
|
||||
- estimated_time_remaining
|
||||
- number_of_recoveries
|
||||
- distance_remaining
|
||||
- number_of_poses_remaining
|
||||
title: NavigateThroughPoses_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
behavior_tree:
|
||||
type: string
|
||||
poses:
|
||||
items:
|
||||
properties:
|
||||
header:
|
||||
properties:
|
||||
frame_id:
|
||||
type: string
|
||||
stamp:
|
||||
properties:
|
||||
nanosec:
|
||||
maximum: 4294967295
|
||||
minimum: 0
|
||||
type: integer
|
||||
sec:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Time
|
||||
type: object
|
||||
required:
|
||||
- stamp
|
||||
- frame_id
|
||||
title: Header
|
||||
type: object
|
||||
pose:
|
||||
properties:
|
||||
orientation:
|
||||
properties:
|
||||
w:
|
||||
type: number
|
||||
x:
|
||||
type: number
|
||||
y:
|
||||
type: number
|
||||
z:
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
- w
|
||||
title: Quaternion
|
||||
type: object
|
||||
position:
|
||||
properties:
|
||||
x:
|
||||
type: number
|
||||
y:
|
||||
type: number
|
||||
z:
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
title: Point
|
||||
type: object
|
||||
required:
|
||||
- position
|
||||
- orientation
|
||||
title: Pose
|
||||
type: object
|
||||
required:
|
||||
- header
|
||||
- pose
|
||||
title: PoseStamped
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- poses
|
||||
- behavior_tree
|
||||
title: NavigateThroughPoses_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
result:
|
||||
properties: {}
|
||||
required: []
|
||||
title: Empty
|
||||
type: object
|
||||
required:
|
||||
- result
|
||||
title: NavigateThroughPoses_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: NavigateThroughPoses
|
||||
type: object
|
||||
type: NavigateThroughPoses
|
||||
set_spindle_speed:
|
||||
feedback:
|
||||
position: spindle_speed
|
||||
goal:
|
||||
position: spindle_speed
|
||||
goal_default:
|
||||
max_velocity: 0.0
|
||||
min_duration:
|
||||
nanosec: 0
|
||||
sec: 0
|
||||
position: 0.0
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
position:
|
||||
type: array
|
||||
items:
|
||||
schema:
|
||||
description: ROS Action SingleJointPosition 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
error:
|
||||
type: number
|
||||
header:
|
||||
properties:
|
||||
frame_id:
|
||||
type: string
|
||||
stamp:
|
||||
properties:
|
||||
nanosec:
|
||||
maximum: 4294967295
|
||||
minimum: 0
|
||||
type: integer
|
||||
sec:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Time
|
||||
type: object
|
||||
required:
|
||||
- stamp
|
||||
- frame_id
|
||||
title: Header
|
||||
type: object
|
||||
position:
|
||||
type: number
|
||||
velocity:
|
||||
type: number
|
||||
required:
|
||||
- header
|
||||
- position
|
||||
- velocity
|
||||
- error
|
||||
title: SingleJointPosition_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
max_velocity:
|
||||
type: number
|
||||
min_duration:
|
||||
properties:
|
||||
nanosec:
|
||||
maximum: 4294967295
|
||||
minimum: 0
|
||||
type: integer
|
||||
sec:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Duration
|
||||
type: object
|
||||
position:
|
||||
type: number
|
||||
required:
|
||||
- position
|
||||
- min_duration
|
||||
- max_velocity
|
||||
title: SingleJointPosition_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: SingleJointPosition_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SingleJointPosition
|
||||
type: object
|
||||
type: SingleJointPosition
|
||||
module: unilabos.devices.cnc.grbl_sync:GrblCNC
|
||||
status_types:
|
||||
position: unilabos.messages:Point3D
|
||||
spindle_speed: float
|
||||
status: str
|
||||
type: python
|
||||
description: Grbl CNC
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
address:
|
||||
default: '1'
|
||||
type: string
|
||||
limits:
|
||||
default:
|
||||
- -150
|
||||
- 150
|
||||
- -200
|
||||
- 0
|
||||
- -80
|
||||
- 0
|
||||
type: array
|
||||
port:
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
position:
|
||||
type: string
|
||||
spindle_speed:
|
||||
type: number
|
||||
description: The position of the device
|
||||
spindle_speed:
|
||||
type: number
|
||||
description: The spindle speed of the device
|
||||
required:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- position
|
||||
- spindle_speed
|
||||
additionalProperties: false
|
||||
|
||||
|
||||
type: object
|
||||
motor.iCL42:
|
||||
description: iCL42 motor
|
||||
class:
|
||||
module: unilabos.devices.motor.iCL42:iCL42Driver
|
||||
type: python
|
||||
status_types:
|
||||
motor_position: Int64
|
||||
is_executing_run: Bool
|
||||
success: Bool
|
||||
action_value_mappings:
|
||||
auto-execute_run_motor:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
mode: null
|
||||
position: null
|
||||
velocity: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: execute_run_motor的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
position:
|
||||
type: number
|
||||
velocity:
|
||||
type: integer
|
||||
required:
|
||||
- mode
|
||||
- position
|
||||
- velocity
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: execute_run_motor参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-init_device:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: init_device的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: init_device参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-run_motor:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
mode: null
|
||||
position: null
|
||||
velocity: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: run_motor的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
position:
|
||||
type: number
|
||||
velocity:
|
||||
type: integer
|
||||
required:
|
||||
- mode
|
||||
- position
|
||||
- velocity
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: run_motor参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
execute_command_from_outer:
|
||||
type: SendCmd
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
success: success
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.motor.iCL42:iCL42Driver
|
||||
status_types:
|
||||
is_executing_run: bool
|
||||
motor_position: int
|
||||
success: bool
|
||||
type: python
|
||||
description: iCL42 motor
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
device_address:
|
||||
default: 1
|
||||
type: integer
|
||||
device_com:
|
||||
default: COM9
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
is_executing_run:
|
||||
type: boolean
|
||||
motor_position:
|
||||
type: integer
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- motor_position
|
||||
- is_executing_run
|
||||
- success
|
||||
type: object
|
||||
|
||||
@@ -1,5 +1,311 @@
|
||||
lh_joint_publisher:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-check_tf_update_actions:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: check_tf_update_actions的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_tf_update_actions参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-find_resource_parent:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
resource_id: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: find_resource_parent的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
resource_id:
|
||||
type: string
|
||||
required:
|
||||
- resource_id
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: find_resource_parent参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-inverse_kinematics:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parent_id: null
|
||||
x: null
|
||||
x_joint: null
|
||||
y: null
|
||||
y_joint: null
|
||||
z: null
|
||||
z_joint: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: inverse_kinematics的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parent_id:
|
||||
type: string
|
||||
x:
|
||||
type: string
|
||||
x_joint:
|
||||
type: object
|
||||
y:
|
||||
type: string
|
||||
y_joint:
|
||||
type: object
|
||||
z:
|
||||
type: string
|
||||
z_joint:
|
||||
type: object
|
||||
required:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
- parent_id
|
||||
- x_joint
|
||||
- y_joint
|
||||
- z_joint
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: inverse_kinematics参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-lh_joint_action_callback:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
goal_handle: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: lh_joint_action_callback的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
goal_handle:
|
||||
type: string
|
||||
required:
|
||||
- goal_handle
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: lh_joint_action_callback参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-lh_joint_pub_callback:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: lh_joint_pub_callback的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: lh_joint_pub_callback参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-move_joints:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
option: null
|
||||
resource_names: null
|
||||
speed: 0.1
|
||||
x: null
|
||||
x_joint: null
|
||||
y: null
|
||||
y_joint: null
|
||||
z: null
|
||||
z_joint: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: move_joints的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
option:
|
||||
type: string
|
||||
resource_names:
|
||||
type: string
|
||||
speed:
|
||||
default: 0.1
|
||||
type: string
|
||||
x:
|
||||
type: string
|
||||
x_joint:
|
||||
type: string
|
||||
y:
|
||||
type: string
|
||||
y_joint:
|
||||
type: string
|
||||
z:
|
||||
type: string
|
||||
z_joint:
|
||||
type: string
|
||||
required:
|
||||
- resource_names
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
- option
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: move_joints参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-move_to:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
joint_positions: null
|
||||
parent_id: null
|
||||
speed: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: move_to的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
joint_positions:
|
||||
type: string
|
||||
parent_id:
|
||||
type: string
|
||||
speed:
|
||||
type: string
|
||||
required:
|
||||
- joint_positions
|
||||
- speed
|
||||
- parent_id
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: move_to参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-resource_move:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
channels: null
|
||||
link_name: null
|
||||
resource_id: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: resource_move的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
link_name:
|
||||
type: string
|
||||
resource_id:
|
||||
type: string
|
||||
required:
|
||||
- resource_id
|
||||
- link_name
|
||||
- channels
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: resource_move参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-send_resource_action:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
link_name: null
|
||||
resource_id_list: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send_resource_action的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
link_name:
|
||||
type: string
|
||||
resource_id_list:
|
||||
type: array
|
||||
required:
|
||||
- resource_id_list
|
||||
- link_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send_resource_action参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.ros_dev.liquid_handler_joint_publisher:LiquidHandlerJointPublisher
|
||||
status_types: {}
|
||||
type: ros2
|
||||
|
||||
description: ''
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
device_id:
|
||||
default: lh_joint_publisher
|
||||
type: string
|
||||
rate:
|
||||
default: 50
|
||||
type: string
|
||||
resource_tracker:
|
||||
type: string
|
||||
resources_config:
|
||||
type: array
|
||||
required:
|
||||
- resources_config
|
||||
- resource_tracker
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
|
||||
@@ -1,65 +1,811 @@
|
||||
heaterstirrer.dalong:
|
||||
description: DaLong heater stirrer
|
||||
chiller:
|
||||
class:
|
||||
module: unilabos.devices.heaterstirrer.dalong:HeaterStirrer_DaLong
|
||||
type: python
|
||||
status_types:
|
||||
temp: Float64
|
||||
temp_warning: Float64
|
||||
stir_speed: Float64
|
||||
action_value_mappings:
|
||||
set_temp_warning:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: temp
|
||||
auto-build_modbus_frame:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
device_address: null
|
||||
function_code: null
|
||||
register_address: null
|
||||
value: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: build_modbus_frame的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
device_address:
|
||||
type: integer
|
||||
function_code:
|
||||
type: integer
|
||||
register_address:
|
||||
type: integer
|
||||
value:
|
||||
type: integer
|
||||
required:
|
||||
- device_address
|
||||
- function_code
|
||||
- register_address
|
||||
- value
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: build_modbus_frame参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-convert_temperature_to_modbus_value:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
decimal_points: 1
|
||||
temperature: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: convert_temperature_to_modbus_value的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
decimal_points:
|
||||
default: 1
|
||||
type: integer
|
||||
temperature:
|
||||
type: number
|
||||
required:
|
||||
- temperature
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: convert_temperature_to_modbus_value参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-modbus_crc:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
data: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: modbus_crc的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
data:
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: modbus_crc参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_temperature:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_temperature的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_temperature参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-stop:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: stop的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: stop参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
set_temperature:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
set_temp_target:
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: temp
|
||||
module: unilabos.devices.temperature.chiller:Chiller
|
||||
status_types: {}
|
||||
type: python
|
||||
description: Chiller
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
rate:
|
||||
default: 9600
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
heaterstirrer.dalong:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-close:
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: close的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: close参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-heatchill:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
purpose: reaction
|
||||
stir: true
|
||||
stir_speed: 300
|
||||
temp: null
|
||||
time: 3600
|
||||
vessel: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: heatchill的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
purpose:
|
||||
default: reaction
|
||||
type: string
|
||||
stir:
|
||||
default: true
|
||||
type: boolean
|
||||
stir_speed:
|
||||
default: 300
|
||||
type: number
|
||||
temp:
|
||||
type: number
|
||||
time:
|
||||
default: 3600
|
||||
type: number
|
||||
vessel:
|
||||
type: string
|
||||
required:
|
||||
- vessel
|
||||
- temp
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: heatchill参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_stir_speed:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
speed: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_stir_speed的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
speed:
|
||||
type: number
|
||||
required:
|
||||
- speed
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_stir_speed参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_temp_inner:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
temp: null
|
||||
type: warning
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_temp_inner的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
temp:
|
||||
type: number
|
||||
type:
|
||||
default: warning
|
||||
type: string
|
||||
required:
|
||||
- temp
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_temp_inner参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_temp_target:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
temp: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_temp_target的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
temp:
|
||||
type: string
|
||||
required:
|
||||
- temp
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_temp_target参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_temp_warning:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
temp: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_temp_warning的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
temp:
|
||||
type: string
|
||||
required:
|
||||
- temp
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_temp_warning参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
heatchill:
|
||||
type: HeatChill
|
||||
goal:
|
||||
vessel: vessel
|
||||
temp: temp
|
||||
time: time
|
||||
purpose: purpose
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
|
||||
chiller:
|
||||
description: Chiller
|
||||
class:
|
||||
module: unilabos.devices.temperature.chiller:Chiller
|
||||
type: python
|
||||
action_value_mappings:
|
||||
set_temperature:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
purpose: purpose
|
||||
temp: temp
|
||||
time: time
|
||||
vessel: vessel
|
||||
goal_default:
|
||||
pressure: ''
|
||||
purpose: ''
|
||||
reflux_solvent: ''
|
||||
stir: false
|
||||
stir_speed: 0.0
|
||||
temp: 0.0
|
||||
temp_spec: ''
|
||||
time: 0.0
|
||||
time_spec: ''
|
||||
vessel: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
tempsensor:
|
||||
description: Temperature sensor
|
||||
class:
|
||||
module: unilabos.devices.temperature.sensor_node:TempSensorNode
|
||||
type: python
|
||||
schema:
|
||||
description: ROS Action HeatChill 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: HeatChill_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
pressure:
|
||||
type: string
|
||||
purpose:
|
||||
type: string
|
||||
reflux_solvent:
|
||||
type: string
|
||||
stir:
|
||||
type: boolean
|
||||
stir_speed:
|
||||
type: number
|
||||
temp:
|
||||
type: number
|
||||
temp_spec:
|
||||
type: string
|
||||
time:
|
||||
type: number
|
||||
time_spec:
|
||||
type: string
|
||||
vessel:
|
||||
type: string
|
||||
required:
|
||||
- vessel
|
||||
- temp
|
||||
- time
|
||||
- temp_spec
|
||||
- time_spec
|
||||
- pressure
|
||||
- reflux_solvent
|
||||
- stir
|
||||
- stir_speed
|
||||
- purpose
|
||||
title: HeatChill_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- success
|
||||
- message
|
||||
- return_info
|
||||
title: HeatChill_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: HeatChill
|
||||
type: object
|
||||
type: HeatChill
|
||||
set_temp_target:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: temp
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_temp_warning:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: temp
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.heaterstirrer.dalong:HeaterStirrer_DaLong
|
||||
status_types:
|
||||
value: Float64
|
||||
warning: Float64
|
||||
status: str
|
||||
stir_speed: float
|
||||
temp: float
|
||||
temp_target: float
|
||||
temp_warning: float
|
||||
type: python
|
||||
description: DaLong heater stirrer
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
baudrate:
|
||||
default: 9600
|
||||
type: integer
|
||||
port:
|
||||
default: COM6
|
||||
type: string
|
||||
temp_warning:
|
||||
default: 50.0
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
stir_speed:
|
||||
type: number
|
||||
temp:
|
||||
type: number
|
||||
temp_target:
|
||||
type: number
|
||||
temp_warning:
|
||||
type: number
|
||||
required:
|
||||
- status
|
||||
- stir_speed
|
||||
- temp
|
||||
- temp_warning
|
||||
- temp_target
|
||||
type: object
|
||||
tempsensor:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-build_modbus_request:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
device_id: null
|
||||
function_code: null
|
||||
register_address: null
|
||||
register_count: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: build_modbus_request的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
device_id:
|
||||
type: string
|
||||
function_code:
|
||||
type: string
|
||||
register_address:
|
||||
type: string
|
||||
register_count:
|
||||
type: string
|
||||
required:
|
||||
- device_id
|
||||
- function_code
|
||||
- register_address
|
||||
- register_count
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: build_modbus_request参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-calculate_crc:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
data: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: calculate_crc的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
data:
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: calculate_crc参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-read_modbus_response:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
response: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: read_modbus_response的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
response:
|
||||
type: string
|
||||
required:
|
||||
- response
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: read_modbus_response参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-send_prototype_command:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send_prototype_command的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send_prototype_command参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_warning:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_warning的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_warning参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
set_warning:
|
||||
type: SendCmd
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ROS Action SendCmd 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.temperature.sensor_node:TempSensorNode
|
||||
status_types:
|
||||
value: float
|
||||
type: python
|
||||
description: Temperature sensor
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
address:
|
||||
type: string
|
||||
baudrate:
|
||||
default: 9600
|
||||
type: string
|
||||
port:
|
||||
type: string
|
||||
warning:
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
- warning
|
||||
- address
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
value:
|
||||
type: number
|
||||
required:
|
||||
- value
|
||||
type: object
|
||||
|
||||
@@ -1,81 +1,496 @@
|
||||
vacuum_pump.mock:
|
||||
description: Mock vacuum pump
|
||||
class:
|
||||
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
action_value_mappings:
|
||||
open:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
close:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
set_status:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: string
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
- handler_key: out
|
||||
label: out
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: handle
|
||||
data_key: fluid_in
|
||||
init_param_schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
description: "通信端口"
|
||||
default: "COM6"
|
||||
required:
|
||||
- port
|
||||
|
||||
gas_source.mock:
|
||||
description: Mock gas source
|
||||
class:
|
||||
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
action_value_mappings:
|
||||
open:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
auto-close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: close的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: close参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-is_closed:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: is_closed的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: is_closed参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-is_open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: is_open的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: is_open参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: open的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: open参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_status:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
string: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_status的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
string:
|
||||
type: string
|
||||
required:
|
||||
- string
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_status参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
close:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action EmptyIn 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action EmptyIn 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
set_status:
|
||||
type: StrSingleInput
|
||||
feedback: {}
|
||||
goal:
|
||||
string: string
|
||||
feedback: {}
|
||||
goal_default:
|
||||
string: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action StrSingleInput 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: StrSingleInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
string:
|
||||
type: string
|
||||
required:
|
||||
- string
|
||||
title: StrSingleInput_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: StrSingleInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: StrSingleInput
|
||||
type: object
|
||||
type: StrSingleInput
|
||||
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
|
||||
status_types:
|
||||
status: str
|
||||
type: python
|
||||
description: Mock gas source
|
||||
handles:
|
||||
- handler_key: out
|
||||
label: out
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_out
|
||||
- data_key: fluid_out
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
handler_key: out
|
||||
io_type: source
|
||||
label: out
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
description: "通信端口"
|
||||
default: "COM6"
|
||||
required:
|
||||
- port
|
||||
config:
|
||||
properties:
|
||||
port:
|
||||
default: COM6
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
vacuum_pump.mock:
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: close的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: close参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-is_closed:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: is_closed的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: is_closed参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-is_open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: is_open的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: is_open参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: open的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: open参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_status:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
string: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_status的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
string:
|
||||
type: string
|
||||
required:
|
||||
- string
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_status参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action EmptyIn 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
open:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action EmptyIn 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
set_status:
|
||||
feedback: {}
|
||||
goal:
|
||||
string: string
|
||||
goal_default:
|
||||
string: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action StrSingleInput 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: StrSingleInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
string:
|
||||
type: string
|
||||
required:
|
||||
- string
|
||||
title: StrSingleInput_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: StrSingleInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: StrSingleInput
|
||||
type: object
|
||||
type: StrSingleInput
|
||||
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
|
||||
status_types:
|
||||
status: str
|
||||
type: python
|
||||
description: Mock vacuum pump
|
||||
handles:
|
||||
- data_key: fluid_in
|
||||
data_source: handle
|
||||
data_type: fluid
|
||||
handler_key: out
|
||||
io_type: source
|
||||
label: out
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
port:
|
||||
default: COM6
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,232 @@
|
||||
zhida_hplc:
|
||||
description: Zhida HPLC
|
||||
class:
|
||||
module: unilabos.devices.zhida_hplc.zhida:ZhidaClient
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
action_value_mappings:
|
||||
abort:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action EmptyIn 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
auto-abort:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: abort的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: abort参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: close的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: close参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-connect:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: connect的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: connect参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-start:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
text: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: start的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
required:
|
||||
- text
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: start参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
get_methods:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ROS Action EmptyIn 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
start:
|
||||
type: StrSingleInput
|
||||
feedback: {}
|
||||
goal:
|
||||
string: string
|
||||
feedback: {}
|
||||
goal_default:
|
||||
string: ''
|
||||
handles: []
|
||||
result: {}
|
||||
abort:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
get_methods:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
|
||||
schema:
|
||||
properties: {}
|
||||
schema:
|
||||
description: ROS Action StrSingleInput 的 JSON Schema
|
||||
properties:
|
||||
feedback:
|
||||
description: Action 反馈 - 执行过程中从服务器发送到客户端
|
||||
properties: {}
|
||||
required: []
|
||||
title: StrSingleInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
description: Action 目标 - 从客户端发送到服务器
|
||||
properties:
|
||||
string:
|
||||
type: string
|
||||
required:
|
||||
- string
|
||||
title: StrSingleInput_Goal
|
||||
type: object
|
||||
result:
|
||||
description: Action 结果 - 完成后从服务器发送到客户端
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: StrSingleInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: StrSingleInput
|
||||
type: object
|
||||
type: StrSingleInput
|
||||
module: unilabos.devices.zhida_hplc.zhida:ZhidaClient
|
||||
status_types:
|
||||
methods: dict
|
||||
status: dict
|
||||
type: python
|
||||
description: Zhida HPLC
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
host:
|
||||
default: 192.168.1.47
|
||||
type: string
|
||||
port:
|
||||
default: 5792
|
||||
type: string
|
||||
timeout:
|
||||
default: 10.0
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
methods:
|
||||
type: object
|
||||
status:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- methods
|
||||
type: object
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import copy
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import yaml
|
||||
|
||||
from unilabos.ros.msgs.message_converter import msg_converter_manager, ros_action_to_json_schema
|
||||
from unilabos.ros.msgs.message_converter import msg_converter_manager, ros_action_to_json_schema, String
|
||||
from unilabos.utils import logger
|
||||
from unilabos.utils.decorator import singleton
|
||||
from unilabos.utils.import_manager import get_enhanced_class_info
|
||||
from unilabos.utils.type_check import NoAliasDumper
|
||||
|
||||
DEFAULT_PATHS = [Path(__file__).absolute().parent]
|
||||
|
||||
@@ -32,7 +35,7 @@ class Registry:
|
||||
# 其他状态变量
|
||||
# self.is_host_mode = False # 移至BasicConfig中
|
||||
|
||||
def setup(self):
|
||||
def setup(self, complete_registry=False):
|
||||
# 检查是否已调用过setup
|
||||
if self._setup_called:
|
||||
logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用")
|
||||
@@ -86,13 +89,15 @@ class Registry:
|
||||
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal))
|
||||
),
|
||||
"handles": {
|
||||
"output": [{
|
||||
"handler_key": "labware",
|
||||
"label": "Labware",
|
||||
"data_type": "resource",
|
||||
"data_source": "handle",
|
||||
"data_key": "liquid"
|
||||
}]
|
||||
"output": [
|
||||
{
|
||||
"handler_key": "labware",
|
||||
"label": "Labware",
|
||||
"data_type": "resource",
|
||||
"data_source": "handle",
|
||||
"data_key": "liquid",
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
"test_latency": {
|
||||
@@ -110,7 +115,6 @@ class Registry:
|
||||
"registry_type": "device",
|
||||
"handles": [],
|
||||
"init_param_schema": {},
|
||||
"schema": {"properties": {}, "additionalProperties": False, "type": "object"},
|
||||
"file_path": "/",
|
||||
}
|
||||
}
|
||||
@@ -121,13 +125,13 @@ class Registry:
|
||||
sys_path = path.parent
|
||||
logger.debug(f"[UniLab Registry] Path {i+1}/{len(self.registry_paths)}: {sys_path}")
|
||||
sys.path.append(str(sys_path))
|
||||
self.load_device_types(path)
|
||||
self.load_resource_types(path)
|
||||
self.load_device_types(path, complete_registry)
|
||||
self.load_resource_types(path, complete_registry)
|
||||
logger.info("[UniLab Registry] 注册表设置完成")
|
||||
# 标记setup已被调用
|
||||
self._setup_called = True
|
||||
|
||||
def load_resource_types(self, path: os.PathLike):
|
||||
def load_resource_types(self, path: os.PathLike, complete_registry: bool):
|
||||
abs_path = Path(path).absolute()
|
||||
resource_path = abs_path / "resources"
|
||||
files = list(resource_path.glob("*/*.yaml"))
|
||||
@@ -176,7 +180,14 @@ class Registry:
|
||||
if not type_name or type_name == "":
|
||||
logger.warning(f"[UniLab Registry] 设备 {device_id} 的 {field_name} 类型为空,跳过替换")
|
||||
return type_name
|
||||
if "." in type_name:
|
||||
convert_manager = { # 将python基本对象转为ros2基本对象
|
||||
"str": "String",
|
||||
"bool": "Bool",
|
||||
"int": "Int64",
|
||||
"float": "Float64",
|
||||
}
|
||||
type_name = convert_manager.get(type_name, type_name) # 替换为ROS2类型
|
||||
if ":" in type_name:
|
||||
type_class = msg_converter_manager.get_class(type_name)
|
||||
else:
|
||||
type_class = msg_converter_manager.search_class(type_name)
|
||||
@@ -186,7 +197,98 @@ class Registry:
|
||||
logger.error(f"[UniLab Registry] 无法找到类型 '{type_name}' 用于设备 {device_id} 的 {field_name}")
|
||||
sys.exit(1)
|
||||
|
||||
def load_device_types(self, path: os.PathLike):
|
||||
def _generate_schema_from_info(
|
||||
self,
|
||||
param_name: str,
|
||||
param_type: str,
|
||||
param_default: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
根据参数信息生成JSON Schema
|
||||
"""
|
||||
prop_schema = {}
|
||||
# 根据类型设置schema FIXME 不完整
|
||||
if param_type:
|
||||
param_type_lower = param_type.lower()
|
||||
if param_type_lower in ["str", "string"]:
|
||||
prop_schema["type"] = "string"
|
||||
elif param_type_lower in ["int", "integer"]:
|
||||
prop_schema["type"] = "integer"
|
||||
elif param_type_lower in ["float", "number"]:
|
||||
prop_schema["type"] = "number"
|
||||
elif param_type_lower in ["bool", "boolean"]:
|
||||
prop_schema["type"] = "boolean"
|
||||
elif param_type_lower in ["list", "array"]:
|
||||
prop_schema["type"] = "array"
|
||||
elif param_type_lower in ["dict", "object"]:
|
||||
prop_schema["type"] = "object"
|
||||
else:
|
||||
# 默认为字符串类型
|
||||
prop_schema["type"] = "string"
|
||||
else:
|
||||
# 如果没有类型信息,默认为字符串
|
||||
prop_schema["type"] = "string"
|
||||
|
||||
# 设置默认值
|
||||
if param_default is not None:
|
||||
prop_schema["default"] = param_default
|
||||
|
||||
return prop_schema
|
||||
|
||||
def _generate_status_types_schema(self, status_types: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
根据状态类型生成JSON Schema
|
||||
"""
|
||||
status_schema = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
}
|
||||
for status_name, status_type in status_types.items():
|
||||
status_schema["properties"][status_name] = self._generate_schema_from_info(
|
||||
status_name, status_type["return_type"], None
|
||||
)
|
||||
status_schema["required"].append(status_name)
|
||||
return status_schema
|
||||
|
||||
def _generate_unilab_json_command_schema(
|
||||
self, method_args: List[Dict[str, Any]], method_name: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
根据UniLabJsonCommand方法信息生成JSON Schema,暂不支持嵌套类型
|
||||
|
||||
Args:
|
||||
method_args: 方法信息字典,包含args等
|
||||
method_name: 方法名称
|
||||
|
||||
Returns:
|
||||
JSON Schema格式的参数schema
|
||||
"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
}
|
||||
for arg_info in method_args:
|
||||
param_name = arg_info.get("name", "")
|
||||
param_type = arg_info.get("type", "")
|
||||
param_default = arg_info.get("default")
|
||||
param_required = arg_info.get("required", True)
|
||||
schema["properties"][param_name] = self._generate_schema_from_info(
|
||||
param_name, param_type, param_default
|
||||
)
|
||||
if param_required:
|
||||
schema["required"].append(param_name)
|
||||
|
||||
return {
|
||||
"title": f"{method_name}参数",
|
||||
"description": f"{method_name}的参数schema",
|
||||
"type": "object",
|
||||
"properties": {"goal": schema, "feedback": {}, "result": {}},
|
||||
"required": ["goal"],
|
||||
}
|
||||
|
||||
def load_device_types(self, path: os.PathLike, complete_registry: bool):
|
||||
abs_path = Path(path).absolute()
|
||||
devices_path = abs_path / "devices"
|
||||
device_comms_path = abs_path / "device_comms"
|
||||
@@ -199,12 +301,18 @@ class Registry:
|
||||
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
||||
|
||||
for i, file in enumerate(files):
|
||||
data = yaml.safe_load(open(file, encoding="utf-8"))
|
||||
with open(file, encoding="utf-8", mode="r") as f:
|
||||
data = yaml.safe_load(io.StringIO(f.read()))
|
||||
complete_data = {}
|
||||
action_str_type_mapping = {
|
||||
"UniLabJsonCommand": "UniLabJsonCommand",
|
||||
"UniLabJsonCommandAsync": "UniLabJsonCommandAsync",
|
||||
}
|
||||
status_str_type_mapping = {}
|
||||
if data:
|
||||
# 在添加到注册表前处理类型替换
|
||||
for device_id, device_config in data.items():
|
||||
# 添加文件路径信息 - 使用规范化的完整文件路径
|
||||
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||
if "description" not in device_config:
|
||||
device_config["description"] = ""
|
||||
if "icon" not in device_config:
|
||||
@@ -213,42 +321,128 @@ class Registry:
|
||||
device_config["handles"] = []
|
||||
if "init_param_schema" not in device_config:
|
||||
device_config["init_param_schema"] = {}
|
||||
device_config["registry_type"] = "device"
|
||||
if "class" in device_config:
|
||||
# 处理状态类型
|
||||
if "status_types" in device_config["class"]:
|
||||
for status_name, status_type in device_config["class"]["status_types"].items():
|
||||
device_config["class"]["status_types"][status_name] = self._replace_type_with_class(
|
||||
status_type, device_id, f"状态 {status_name}"
|
||||
)
|
||||
if "status_types" not in device_config["class"]:
|
||||
device_config["class"]["status_types"] = {}
|
||||
if "action_value_mappings" not in device_config["class"]:
|
||||
device_config["class"]["action_value_mappings"] = {}
|
||||
enhanced_info = {}
|
||||
if complete_registry:
|
||||
device_config["class"]["status_types"].clear()
|
||||
enhanced_info = get_enhanced_class_info(device_config["class"]["module"], use_dynamic=True)
|
||||
device_config["class"]["status_types"].update(
|
||||
{k: v["return_type"] for k, v in enhanced_info["status_methods"].items()}
|
||||
)
|
||||
for status_name, status_type in device_config["class"]["status_types"].items():
|
||||
if status_type in ["Any", "None", "Unknown"]:
|
||||
status_type = "String" # 替换成ROS的String,便于显示
|
||||
device_config["class"]["status_types"][status_name] = status_type
|
||||
target_type = self._replace_type_with_class(status_type, device_id, f"状态 {status_name}")
|
||||
if target_type in [
|
||||
dict,
|
||||
list,
|
||||
]: # 对于嵌套类型返回的对象,暂时处理成字符串,无法直接进行转换
|
||||
target_type = String
|
||||
status_str_type_mapping[status_type] = target_type
|
||||
device_config["class"]["status_types"] = dict(
|
||||
sorted(device_config["class"]["status_types"].items())
|
||||
)
|
||||
if complete_registry:
|
||||
device_config["class"]["action_value_mappings"] = {
|
||||
k: v
|
||||
for k, v in device_config["class"]["action_value_mappings"].items()
|
||||
if not k.startswith("auto-")
|
||||
}
|
||||
# 处理动作值映射
|
||||
device_config["class"]["action_value_mappings"].update(
|
||||
{
|
||||
f"auto-{k}": {
|
||||
"type": "UniLabJsonCommandAsync" if v["is_async"] else "UniLabJsonCommand",
|
||||
"goal": {},
|
||||
"feedback": {},
|
||||
"result": {},
|
||||
"schema": self._generate_unilab_json_command_schema(v["args"], k),
|
||||
"goal_default": {i["name"]: i["default"] for i in v["args"]},
|
||||
"handles": [],
|
||||
}
|
||||
for k, v in enhanced_info["action_methods"].items()
|
||||
}
|
||||
)
|
||||
device_config["init_param_schema"] = {}
|
||||
device_config["init_param_schema"]["config"] = self._generate_unilab_json_command_schema(
|
||||
enhanced_info["init_params"], "__init__"
|
||||
)["properties"]["goal"]
|
||||
device_config["init_param_schema"]["data"] = self._generate_status_types_schema(
|
||||
enhanced_info["status_methods"]
|
||||
)
|
||||
|
||||
# 处理动作值映射
|
||||
if "action_value_mappings" in device_config["class"]:
|
||||
for action_name, action_config in device_config["class"]["action_value_mappings"].items():
|
||||
if "handles" not in action_config:
|
||||
action_config["handles"] = []
|
||||
if "type" in action_config:
|
||||
action_config["type"] = self._replace_type_with_class(
|
||||
action_config["type"], device_id, f"动作 {action_name}"
|
||||
device_config.pop("schema", None)
|
||||
device_config["class"]["action_value_mappings"] = dict(
|
||||
sorted(device_config["class"]["action_value_mappings"].items())
|
||||
)
|
||||
for action_name, action_config in device_config["class"]["action_value_mappings"].items():
|
||||
if "handles" not in action_config:
|
||||
action_config["handles"] = []
|
||||
if "type" in action_config:
|
||||
action_type_str: str = action_config["type"]
|
||||
# 通过Json发放指令,而不是通过特殊的ros action进行处理
|
||||
if not action_type_str.startswith("UniLabJsonCommand"):
|
||||
target_type = self._replace_type_with_class(
|
||||
action_type_str, device_id, f"动作 {action_name}"
|
||||
)
|
||||
if action_config["type"] is not None:
|
||||
action_str_type_mapping[action_type_str] = target_type
|
||||
if target_type is not None:
|
||||
action_config["goal_default"] = yaml.safe_load(
|
||||
io.StringIO(get_yaml_from_goal_type(action_config["type"].Goal))
|
||||
io.StringIO(get_yaml_from_goal_type(target_type.Goal))
|
||||
)
|
||||
action_config["schema"] = ros_action_to_json_schema(action_config["type"])
|
||||
action_config["schema"] = ros_action_to_json_schema(target_type)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[UniLab Registry] 设备 {device_id} 的动作 {action_name} 类型为空,跳过替换"
|
||||
)
|
||||
|
||||
self.device_type_registry.update(data)
|
||||
|
||||
for device_id in data.keys():
|
||||
complete_data[device_id] = copy.deepcopy(dict(sorted(device_config.items()))) # 稍后dump到文件
|
||||
for status_name, status_type in device_config["class"]["status_types"].items():
|
||||
device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type]
|
||||
for action_name, action_config in device_config["class"]["action_value_mappings"].items():
|
||||
action_config["type"] = action_str_type_mapping[action_config["type"]]
|
||||
for additional_action in ["_execute_driver_command", "_execute_driver_command_async"]:
|
||||
device_config["class"]["action_value_mappings"][additional_action] = {
|
||||
"type": self._replace_type_with_class(
|
||||
"StrSingleInput", device_id, f"动作 {additional_action}"
|
||||
),
|
||||
"goal": {"string": "string"},
|
||||
"feedback": {},
|
||||
"result": {},
|
||||
"schema": ros_action_to_json_schema(
|
||||
self._replace_type_with_class(
|
||||
"StrSingleInput", device_id, f"动作 {additional_action}"
|
||||
)
|
||||
),
|
||||
"goal_default": yaml.safe_load(
|
||||
io.StringIO(
|
||||
get_yaml_from_goal_type(
|
||||
self._replace_type_with_class(
|
||||
"StrSingleInput", device_id, f"动作 {additional_action}"
|
||||
).Goal
|
||||
)
|
||||
)
|
||||
),
|
||||
"handles": [],
|
||||
}
|
||||
if "registry_type" not in device_config:
|
||||
device_config["registry_type"] = "device"
|
||||
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||
device_config["registry_type"] = "device"
|
||||
logger.debug(
|
||||
f"[UniLab Registry] Device-{current_device_number} File-{i+1}/{len(files)} Add {device_id} "
|
||||
+ f"[{data[device_id].get('name', '未命名设备')}]"
|
||||
)
|
||||
current_device_number += 1
|
||||
complete_data = dict(sorted(complete_data.items()))
|
||||
complete_data = copy.deepcopy(complete_data)
|
||||
with open(file, "w", encoding="utf-8") as f:
|
||||
yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper)
|
||||
self.device_type_registry.update(data)
|
||||
else:
|
||||
logger.debug(
|
||||
f"[UniLab Registry] Device File-{i+1}/{len(files)} Not Valid YAML File: {file.absolute()}"
|
||||
@@ -257,7 +451,28 @@ class Registry:
|
||||
def obtain_registry_device_info(self):
|
||||
devices = []
|
||||
for device_id, device_info in self.device_type_registry.items():
|
||||
msg = {"id": device_id, **device_info}
|
||||
device_info_copy = copy.deepcopy(device_info)
|
||||
if "class" in device_info_copy and "action_value_mappings" in device_info_copy["class"]:
|
||||
action_mappings = device_info_copy["class"]["action_value_mappings"]
|
||||
for action_name, action_config in action_mappings.items():
|
||||
if "schema" in action_config and action_config["schema"]:
|
||||
schema = action_config["schema"]
|
||||
# 确保schema结构存在
|
||||
if (
|
||||
"properties" in schema
|
||||
and "goal" in schema["properties"]
|
||||
and "properties" in schema["properties"]["goal"]
|
||||
):
|
||||
schema["properties"]["goal"]["properties"] = {
|
||||
"unilabos_device_id": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "UniLabOS设备ID,用于指定执行动作的具体设备实例",
|
||||
},
|
||||
**schema["properties"]["goal"]["properties"],
|
||||
}
|
||||
|
||||
msg = {"id": device_id, **device_info_copy}
|
||||
devices.append(msg)
|
||||
return devices
|
||||
|
||||
@@ -273,7 +488,7 @@ class Registry:
|
||||
lab_registry = Registry()
|
||||
|
||||
|
||||
def build_registry(registry_paths=None):
|
||||
def build_registry(registry_paths=None, complete_registry=False):
|
||||
"""
|
||||
构建或获取Registry单例实例
|
||||
|
||||
@@ -297,6 +512,6 @@ def build_registry(registry_paths=None):
|
||||
lab_registry.registry_paths.append(path)
|
||||
|
||||
# 初始化注册表
|
||||
lab_registry.setup()
|
||||
lab_registry.setup(complete_registry)
|
||||
|
||||
return lab_registry
|
||||
|
||||
@@ -55,7 +55,7 @@ def ros2_device_node(
|
||||
"read": "read_data",
|
||||
"extra_info": [],
|
||||
}
|
||||
|
||||
# FIXME 后面要删除
|
||||
for k, v in cls.__dict__.items():
|
||||
if not k.startswith("_") and isinstance(v, property):
|
||||
# noinspection PyUnresolvedReferences
|
||||
|
||||
@@ -343,7 +343,7 @@ def convert_to_ros_msg(ros_msg_type: Union[Type, Any], obj: Any) -> Any:
|
||||
if hasattr(ros_msg, key):
|
||||
attr = getattr(ros_msg, key)
|
||||
if isinstance(attr, (float, int, str, bool)):
|
||||
setattr(ros_msg, key, value)
|
||||
setattr(ros_msg, key, type(attr)(value))
|
||||
elif isinstance(attr, (list, tuple)) and isinstance(value, Iterable):
|
||||
td = ros_msg.SLOT_TYPES[ind].value_type
|
||||
if isinstance(td, NamespacedType):
|
||||
@@ -727,7 +727,6 @@ def ros_action_to_json_schema(action_class: Any) -> Dict[str, Any]:
|
||||
|
||||
# 创建基础 schema
|
||||
schema = {
|
||||
'$schema': 'http://json-schema.org/draft-07/schema#',
|
||||
'title': action_class.__name__,
|
||||
'description': f"ROS Action {action_class.__name__} 的 JSON Schema",
|
||||
'type': 'object',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import copy
|
||||
import io
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
@@ -10,6 +11,7 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
import asyncio
|
||||
|
||||
import rclpy
|
||||
import yaml
|
||||
from rclpy.node import Node
|
||||
from rclpy.action import ActionServer, ActionClient
|
||||
from rclpy.action.server import ServerGoalHandle
|
||||
@@ -166,7 +168,10 @@ class PropertyPublisher:
|
||||
self.print_publish = print_publish
|
||||
|
||||
self._value = None
|
||||
self.publisher_ = node.create_publisher(msg_type, f"{name}", 10)
|
||||
try:
|
||||
self.publisher_ = node.create_publisher(msg_type, f"{name}", 10)
|
||||
except AttributeError as ex:
|
||||
logger.error(f"创建发布者失败,可能由于注册表有误,类型: {msg_type},错误: {ex}\n{traceback.format_exc()}")
|
||||
self.timer = node.create_timer(self.timer_period, self.publish_property)
|
||||
self.__loop = get_event_loop()
|
||||
str_msg_type = str(msg_type)[8:-2]
|
||||
@@ -302,6 +307,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
# 创建动作服务
|
||||
if self.create_action_server:
|
||||
for action_name, action_value_mapping in self._action_value_mappings.items():
|
||||
if action_name.startswith("auto-"):
|
||||
continue
|
||||
self.create_ros_action_server(action_name, action_value_mapping)
|
||||
|
||||
# 创建线程池执行器
|
||||
@@ -838,6 +845,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
class DeviceInitError(Exception):
|
||||
pass
|
||||
|
||||
class JsonCommandInitError(Exception):
|
||||
pass
|
||||
|
||||
class ROS2DeviceNode:
|
||||
"""
|
||||
@@ -954,12 +963,51 @@ class ROS2DeviceNode:
|
||||
self._ros_node: BaseROS2DeviceNode
|
||||
self._ros_node.lab_logger().info(f"初始化完成 {self._ros_node.uuid} {self.driver_is_ros}")
|
||||
self.driver_instance._ros_node = self._ros_node # type: ignore
|
||||
self.driver_instance._execute_driver_command = self._execute_driver_command # type: ignore
|
||||
self.driver_instance._execute_driver_command_async = self._execute_driver_command_async # type: ignore
|
||||
if hasattr(self.driver_instance, "post_init"):
|
||||
try:
|
||||
self.driver_instance.post_init(self._ros_node) # type: ignore
|
||||
except Exception as e:
|
||||
self._ros_node.lab_logger().error(f"设备后初始化失败: {e}")
|
||||
|
||||
def _execute_driver_command(self, string: str):
|
||||
try:
|
||||
target = json.loads(string)
|
||||
except Exception as ex:
|
||||
try:
|
||||
target = yaml.safe_load(io.StringIO(string))
|
||||
except Exception as ex2:
|
||||
raise JsonCommandInitError(f"执行动作时JSON/YAML解析失败: \n{ex}\n{ex2}\n原内容: {string}\n{traceback.format_exc()}")
|
||||
try:
|
||||
function_name = target["function_name"]
|
||||
function_args = target["function_args"]
|
||||
assert isinstance(function_args, dict), "执行动作时JSON必须为dict类型\n原JSON: {string}"
|
||||
function = getattr(self.driver_instance, function_name)
|
||||
assert callable(function), f"执行动作时JSON中的function_name对应的函数不可调用: {function_name}\n原JSON: {string}"
|
||||
return function(**function_args)
|
||||
except KeyError as ex:
|
||||
raise JsonCommandInitError(f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}")
|
||||
|
||||
async def _execute_driver_command_async(self, string: str):
|
||||
try:
|
||||
target = json.loads(string)
|
||||
except Exception as ex:
|
||||
try:
|
||||
target = yaml.safe_load(io.StringIO(string))
|
||||
except Exception as ex2:
|
||||
raise JsonCommandInitError(f"执行动作时JSON/YAML解析失败: \n{ex}\n{ex2}\n原内容: {string}\n{traceback.format_exc()}")
|
||||
try:
|
||||
function_name = target["function_name"]
|
||||
function_args = target["function_args"]
|
||||
assert isinstance(function_args, dict), "执行动作时JSON必须为dict类型\n原JSON: {string}"
|
||||
function = getattr(self.driver_instance, function_name)
|
||||
assert callable(function), f"执行动作时JSON中的function_name对应的函数不可调用: {function_name}\n原JSON: {string}"
|
||||
assert asyncio.iscoroutinefunction(function), f"执行动作时JSON中的function并非异步: {function_name}\n原JSON: {string}"
|
||||
return await function(**function_args)
|
||||
except KeyError as ex:
|
||||
raise JsonCommandInitError(f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}")
|
||||
|
||||
def _start_loop(self):
|
||||
def run_event_loop():
|
||||
loop = asyncio.new_event_loop()
|
||||
|
||||
61
unilabos/ros/nodes/presets/camera.py
Normal file
61
unilabos/ros/nodes/presets/camera.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
import cv2
|
||||
from sensor_msgs.msg import Image
|
||||
from cv_bridge import CvBridge
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker
|
||||
|
||||
class VideoPublisher(BaseROS2DeviceNode):
|
||||
def __init__(self, device_id='video_publisher', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None):
|
||||
# 初始化BaseROS2DeviceNode,使用自身作为driver_instance
|
||||
BaseROS2DeviceNode.__init__(
|
||||
self,
|
||||
driver_instance=self,
|
||||
device_id=device_id,
|
||||
status_types={},
|
||||
action_value_mappings={},
|
||||
hardware_interface="camera",
|
||||
print_publish=False,
|
||||
resource_tracker=resource_tracker,
|
||||
)
|
||||
# 创建一个发布者,发布到 /video 话题,消息类型为 sensor_msgs/Image,队列长度设为 10
|
||||
self.publisher_ = self.create_publisher(Image, f'/{device_id}/video', 10)
|
||||
# 初始化摄像头(默认设备索引为 0)
|
||||
self.cap = cv2.VideoCapture(camera_index)
|
||||
if not self.cap.isOpened():
|
||||
self.get_logger().error("无法打开摄像头")
|
||||
# 用于将 OpenCV 的图像转换为 ROS 图像消息
|
||||
self.bridge = CvBridge()
|
||||
# 设置定时器,10 Hz 发布一次
|
||||
timer_period = period # 单位:秒
|
||||
self.timer = self.create_timer(timer_period, self.timer_callback)
|
||||
|
||||
def timer_callback(self):
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
self.get_logger().error("读取视频帧失败")
|
||||
return
|
||||
# 将 OpenCV 图像转换为 ROS Image 消息,注意图像编码需与摄像头数据匹配,这里使用 bgr8
|
||||
img_msg = self.bridge.cv2_to_imgmsg(frame, encoding="bgr8")
|
||||
self.publisher_.publish(img_msg)
|
||||
# self.get_logger().info("已发布视频帧")
|
||||
|
||||
def destroy_node(self):
|
||||
# 释放摄像头资源
|
||||
if self.cap.isOpened():
|
||||
self.cap.release()
|
||||
super().destroy_node()
|
||||
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
node = VideoPublisher()
|
||||
try:
|
||||
rclpy.spin(node)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
node.destroy_node()
|
||||
rclpy.shutdown()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -459,6 +459,8 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self.devices_instances[device_id] = d
|
||||
# noinspection PyProtectedMember
|
||||
for action_name, action_value_mapping in d._ros_node._action_value_mappings.items():
|
||||
if action_name.startswith("auto-"):
|
||||
continue
|
||||
action_id = f"/devices/{device_id}/{action_name}"
|
||||
if action_id not in self._action_clients:
|
||||
action_type = action_value_mapping["type"]
|
||||
@@ -567,6 +569,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
def send_goal(
|
||||
self,
|
||||
device_id: str,
|
||||
action_type: str,
|
||||
action_name: str,
|
||||
action_kwargs: Dict[str, Any],
|
||||
goal_uuid: Optional[str] = None,
|
||||
@@ -577,11 +580,26 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
Args:
|
||||
device_id: 设备ID
|
||||
action_type: 动作类型
|
||||
action_name: 动作名称
|
||||
action_kwargs: 动作参数
|
||||
goal_uuid: 目标UUID,如果为None则自动生成
|
||||
server_info: 服务器发送信息,包含发送时间戳等
|
||||
"""
|
||||
action_id = f"/devices/{device_id}/{action_name}"
|
||||
if action_type.startswith("UniLabJsonCommand"):
|
||||
if action_name.startswith("auto-"):
|
||||
action_name = action_name[5:]
|
||||
action_id = f"/devices/{device_id}/_execute_driver_command"
|
||||
action_kwargs = {
|
||||
"string": json.dumps({
|
||||
"function_name": action_name,
|
||||
"function_args": action_kwargs,
|
||||
})
|
||||
}
|
||||
if action_type.startswith("UniLabJsonCommandAsync"):
|
||||
action_id = f"/devices/{device_id}/_execute_driver_command_async"
|
||||
else:
|
||||
action_id = f"/devices/{device_id}/{action_name}"
|
||||
if action_name == "test_latency" and server_info is not None:
|
||||
self.server_latest_timestamp = server_info.get("send_timestamp", 0.0)
|
||||
if action_id not in self._action_clients:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import time
|
||||
import asyncio
|
||||
import traceback
|
||||
from types import MethodType
|
||||
from typing import Union
|
||||
|
||||
import rclpy
|
||||
@@ -19,9 +17,11 @@ from unilabos.ros.msgs.message_converter import (
|
||||
get_action_type,
|
||||
convert_to_ros_msg,
|
||||
convert_from_ros_msg,
|
||||
convert_from_ros_msg_with_mapping,
|
||||
convert_from_ros_msg_with_mapping, String,
|
||||
)
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode
|
||||
from unilabos.utils.log import error
|
||||
from unilabos.utils.type_check import serialize_result_info
|
||||
|
||||
|
||||
class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
@@ -33,7 +33,15 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
|
||||
# create_action_server = False # Action Server要自己创建
|
||||
|
||||
def __init__(self, device_id: str, children: dict, protocol_type: Union[str, list[str]], resource_tracker: DeviceNodeResourceTracker, *args, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
children: dict,
|
||||
protocol_type: Union[str, list[str]],
|
||||
resource_tracker: DeviceNodeResourceTracker,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self._setup_protocol_names(protocol_type)
|
||||
|
||||
# 初始化其它属性
|
||||
@@ -60,12 +68,14 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
|
||||
for device_id, device_config in self.children.items():
|
||||
if device_config.get("type", "device") != "device":
|
||||
self.lab_logger().debug(f"[Protocol Node] Skipping type {device_config['type']} {device_id} already existed, skipping.")
|
||||
self.lab_logger().debug(
|
||||
f"[Protocol Node] Skipping type {device_config['type']} {device_id} already existed, skipping."
|
||||
)
|
||||
continue
|
||||
try:
|
||||
d = self.initialize_device(device_id, device_config)
|
||||
except Exception as ex:
|
||||
self.lab_logger().error(f"[Protocol Node] Failed to initialize device {device_id}: {ex}")
|
||||
self.lab_logger().error(f"[Protocol Node] Failed to initialize device {device_id}: {ex}\n{traceback.format_exc()}")
|
||||
d = None
|
||||
if d is None:
|
||||
continue
|
||||
@@ -76,22 +86,27 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
|
||||
# 设置硬件接口代理
|
||||
if d:
|
||||
hardware_interface = d.ros_node_instance._hardware_interface
|
||||
if (
|
||||
hasattr(d.driver_instance, d.ros_node_instance._hardware_interface["name"])
|
||||
and hasattr(d.driver_instance, d.ros_node_instance._hardware_interface["write"])
|
||||
and (d.ros_node_instance._hardware_interface["read"] is None or hasattr(d.driver_instance, d.ros_node_instance._hardware_interface["read"]))
|
||||
hasattr(d.driver_instance, hardware_interface["name"])
|
||||
and hasattr(d.driver_instance, hardware_interface["write"])
|
||||
and (hardware_interface["read"] is None or hasattr(d.driver_instance, hardware_interface["read"]))
|
||||
):
|
||||
|
||||
name = getattr(d.driver_instance, d.ros_node_instance._hardware_interface["name"])
|
||||
read = d.ros_node_instance._hardware_interface.get("read", None)
|
||||
write = d.ros_node_instance._hardware_interface.get("write", None)
|
||||
name = getattr(d.driver_instance, hardware_interface["name"])
|
||||
read = hardware_interface.get("read", None)
|
||||
write = hardware_interface.get("write", None)
|
||||
|
||||
# 如果硬件接口是字符串,通过通信设备提供
|
||||
if isinstance(name, str) and name in self.sub_devices:
|
||||
communicate_device = self.sub_devices[name]
|
||||
communicate_hardware_info = communicate_device.ros_node_instance._hardware_interface
|
||||
self._setup_hardware_proxy(d, self.sub_devices[name], read, write)
|
||||
self.lab_logger().info(f"\n通信代理:为子设备{device_id}\n 添加了{read}方法(来源:{name} {communicate_hardware_info['write']}) \n 添加了{write}方法(来源:{name} {communicate_hardware_info['read']})")
|
||||
self.lab_logger().info(
|
||||
f"\n通信代理:为子设备{device_id}\n "
|
||||
f"添加了{read}方法(来源:{name} {communicate_hardware_info['write']}) \n "
|
||||
f"添加了{write}方法(来源:{name} {communicate_hardware_info['read']})"
|
||||
)
|
||||
|
||||
def _setup_protocol_names(self, protocol_type):
|
||||
# 处理协议类型
|
||||
@@ -119,11 +134,17 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
if d is not None and hasattr(d, "ros_node_instance"):
|
||||
node = d.ros_node_instance
|
||||
for action_name, action_mapping in node._action_value_mappings.items():
|
||||
if action_name.startswith("auto-"):
|
||||
continue
|
||||
action_id = f"/devices/{device_id_abs}/{action_name}"
|
||||
if action_id not in self._action_clients:
|
||||
self._action_clients[action_id] = ActionClient(
|
||||
self, action_mapping["type"], action_id, callback_group=self.callback_group
|
||||
)
|
||||
try:
|
||||
self._action_clients[action_id] = ActionClient(
|
||||
self, action_mapping["type"], action_id, callback_group=self.callback_group
|
||||
)
|
||||
except Exception as ex:
|
||||
self.lab_logger().error(f"创建动作客户端失败: {action_id}, 错误: {ex}")
|
||||
continue
|
||||
self.lab_logger().debug(f"为子设备 {device_id} 创建动作客户端: {action_name}")
|
||||
return d
|
||||
|
||||
@@ -149,63 +170,126 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
def _create_protocol_execute_callback(self, protocol_name, protocol_steps_generator):
|
||||
async def execute_protocol(goal_handle: ServerGoalHandle):
|
||||
"""执行完整的工作流"""
|
||||
self.get_logger().info(f'Executing {protocol_name} action...')
|
||||
# 初始化结果信息变量
|
||||
execution_error = ""
|
||||
execution_success = False
|
||||
protocol_return_value = None
|
||||
self.get_logger().info(f"Executing {protocol_name} action...")
|
||||
action_value_mapping = self._action_value_mappings[protocol_name]
|
||||
print('+'*30)
|
||||
print(protocol_steps_generator)
|
||||
# 从目标消息中提取参数, 并调用Protocol生成器(根据设备连接图)生成action步骤
|
||||
goal = goal_handle.request
|
||||
protocol_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"])
|
||||
try:
|
||||
print("+" * 30)
|
||||
print(protocol_steps_generator)
|
||||
# 从目标消息中提取参数, 并调用Protocol生成器(根据设备连接图)生成action步骤
|
||||
goal = goal_handle.request
|
||||
protocol_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"])
|
||||
|
||||
# 向Host查询物料当前状态
|
||||
for k, v in goal.get_fields_and_field_types().items():
|
||||
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||
r = ResourceGet.Request()
|
||||
r.id = protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"]
|
||||
r.with_children = True
|
||||
response = await self._resource_clients["resource_get"].call_async(r)
|
||||
protocol_kwargs[k] = list_to_nested_dict([convert_from_ros_msg(rs) for rs in response.resources])
|
||||
# 向Host查询物料当前状态
|
||||
for k, v in goal.get_fields_and_field_types().items():
|
||||
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||
r = ResourceGet.Request()
|
||||
resource_id = (
|
||||
protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"]
|
||||
)
|
||||
r.id = resource_id
|
||||
r.with_children = True
|
||||
response = await self._resource_clients["resource_get"].call_async(r)
|
||||
protocol_kwargs[k] = list_to_nested_dict(
|
||||
[convert_from_ros_msg(rs) for rs in response.resources]
|
||||
)
|
||||
|
||||
from unilabos.resources.graphio import physical_setup_graph
|
||||
self.get_logger().info(f'Working on physical setup: {physical_setup_graph}')
|
||||
protocol_steps = protocol_steps_generator(G=physical_setup_graph, **protocol_kwargs)
|
||||
from unilabos.resources.graphio import physical_setup_graph
|
||||
|
||||
self.get_logger().info(f'Goal received: {protocol_kwargs}, running steps: \n{protocol_steps}')
|
||||
self.lab_logger().info(f"Working on physical setup: {physical_setup_graph}")
|
||||
protocol_steps = protocol_steps_generator(G=physical_setup_graph, **protocol_kwargs)
|
||||
|
||||
time_start = time.time()
|
||||
time_overall = 100
|
||||
self._busy = True
|
||||
self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: \n{protocol_steps}")
|
||||
|
||||
# 逐步执行工作流
|
||||
for i, action in enumerate(protocol_steps):
|
||||
self.get_logger().info(f'Running step {i+1}: {action}')
|
||||
if type(action) == dict:
|
||||
# 如果是单个动作,直接执行
|
||||
if action["action_name"] == "wait":
|
||||
time.sleep(action["action_kwargs"]["time"])
|
||||
else:
|
||||
result = await self.execute_single_action(**action)
|
||||
elif type(action) == list:
|
||||
# 如果是并行动作,同时执行
|
||||
actions = action
|
||||
futures = [rclpy.get_global_executor().create_task(self.execute_single_action(**a)) for a in actions]
|
||||
results = [await f for f in futures]
|
||||
time_start = time.time()
|
||||
time_overall = 100
|
||||
self._busy = True
|
||||
|
||||
# 向Host更新物料当前状态
|
||||
for k, v in goal.get_fields_and_field_types().items():
|
||||
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||
r = ResourceUpdate.Request()
|
||||
r.resources = [
|
||||
convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k])
|
||||
]
|
||||
response = await self._resource_clients["resource_update"].call_async(r)
|
||||
# 逐步执行工作流
|
||||
step_results = []
|
||||
for i, action in enumerate(protocol_steps):
|
||||
self.get_logger().info(f"Running step {i + 1}: {action}")
|
||||
if isinstance(action, dict):
|
||||
# 如果是单个动作,直接执行
|
||||
if action["action_name"] == "wait":
|
||||
time.sleep(action["action_kwargs"]["time"])
|
||||
step_results.append({"step": i + 1, "action": "wait", "result": "completed"})
|
||||
else:
|
||||
result = await self.execute_single_action(**action)
|
||||
step_results.append({"step": i + 1, "action": action["action_name"], "result": result})
|
||||
elif isinstance(action, list):
|
||||
# 如果是并行动作,同时执行
|
||||
actions = action
|
||||
futures = [
|
||||
rclpy.get_global_executor().create_task(self.execute_single_action(**a)) for a in actions
|
||||
]
|
||||
results = [await f for f in futures]
|
||||
step_results.append(
|
||||
{
|
||||
"step": i + 1,
|
||||
"parallel_actions": [a["action_name"] for a in actions],
|
||||
"results": results,
|
||||
}
|
||||
)
|
||||
|
||||
goal_handle.succeed()
|
||||
# 向Host更新物料当前状态
|
||||
for k, v in goal.get_fields_and_field_types().items():
|
||||
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||
r = ResourceUpdate.Request()
|
||||
r.resources = [
|
||||
convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k])
|
||||
]
|
||||
response = await self._resource_clients["resource_update"].call_async(r)
|
||||
|
||||
# 设置成功状态和返回值
|
||||
execution_success = True
|
||||
protocol_return_value = {
|
||||
"protocol_name": protocol_name,
|
||||
"steps_executed": len(protocol_steps),
|
||||
"step_results": step_results,
|
||||
"total_time": time.time() - time_start,
|
||||
}
|
||||
|
||||
goal_handle.succeed()
|
||||
|
||||
except Exception as e:
|
||||
# 捕获并记录错误信息
|
||||
execution_error = traceback.format_exc()
|
||||
execution_success = False
|
||||
error(f"协议 {protocol_name} 执行失败")
|
||||
error(traceback.format_exc())
|
||||
self.lab_logger().error(f"协议执行出错: {str(e)}")
|
||||
|
||||
# 设置动作失败
|
||||
goal_handle.abort()
|
||||
|
||||
finally:
|
||||
self._busy = False
|
||||
|
||||
# 创建结果消息
|
||||
result = action_value_mapping["type"].Result()
|
||||
result.success = True
|
||||
result.success = execution_success
|
||||
|
||||
self._busy = False
|
||||
# 获取结果消息类型信息,检查是否有return_info字段
|
||||
result_msg_types = action_value_mapping["type"].Result.get_fields_and_field_types()
|
||||
|
||||
# 设置return_info字段(如果存在)
|
||||
for attr_name in result_msg_types.keys():
|
||||
if attr_name in ["success", "reached_goal"]:
|
||||
setattr(result, attr_name, execution_success)
|
||||
elif attr_name == "return_info":
|
||||
setattr(
|
||||
result,
|
||||
attr_name,
|
||||
serialize_result_info(execution_error, execution_success, protocol_return_value),
|
||||
)
|
||||
|
||||
self.lab_logger().info(f"协议 {protocol_name} 完成并返回结果")
|
||||
return result
|
||||
|
||||
return execute_protocol
|
||||
|
||||
async def execute_single_action(self, device_id, action_name, action_kwargs):
|
||||
@@ -241,14 +325,19 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
|
||||
return result_future.result
|
||||
|
||||
|
||||
"""还没有改过的部分"""
|
||||
|
||||
def _setup_hardware_proxy(self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method):
|
||||
def _setup_hardware_proxy(
|
||||
self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method
|
||||
):
|
||||
"""为设备设置硬件接口代理"""
|
||||
# extra_info = [getattr(device.driver_instance, info) for info in communication_device.ros_node_instance._hardware_interface.get("extra_info", [])]
|
||||
write_func = getattr(communication_device.driver_instance, communication_device.ros_node_instance._hardware_interface["write"])
|
||||
read_func = getattr(communication_device.driver_instance, communication_device.ros_node_instance._hardware_interface["read"])
|
||||
write_func = getattr(
|
||||
communication_device.driver_instance, communication_device.ros_node_instance._hardware_interface["write"]
|
||||
)
|
||||
read_func = getattr(
|
||||
communication_device.driver_instance, communication_device.ros_node_instance._hardware_interface["read"]
|
||||
)
|
||||
|
||||
def _read(*args, **kwargs):
|
||||
return read_func(*args, **kwargs)
|
||||
@@ -264,7 +353,6 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
# bound_write = MethodType(_write, device.driver_instance)
|
||||
setattr(device.driver_instance, write_method, _write)
|
||||
|
||||
|
||||
async def _update_resources(self, goal, protocol_kwargs):
|
||||
"""更新资源状态"""
|
||||
for k, v in goal.get_fields_and_field_types().items():
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
import builtins
|
||||
import importlib
|
||||
import inspect
|
||||
import sys
|
||||
import traceback
|
||||
import ast
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional, Callable, Type
|
||||
|
||||
|
||||
@@ -18,8 +22,12 @@ __all__ = [
|
||||
"get_class",
|
||||
"get_module",
|
||||
"init_from_list",
|
||||
"get_class_info_static",
|
||||
"get_registry_class_info",
|
||||
]
|
||||
|
||||
from ast import Constant
|
||||
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
@@ -114,15 +122,16 @@ class ImportManager:
|
||||
# 尝试动态导入
|
||||
if ":" in class_name:
|
||||
module_path, cls_name = class_name.rsplit(":", 1)
|
||||
# 如果cls_name是builtins中的关键字,则返回对应类
|
||||
if cls_name in builtins.__dict__:
|
||||
return builtins.__dict__[cls_name]
|
||||
module = self.load_module(module_path)
|
||||
if hasattr(module, cls_name):
|
||||
cls = getattr(module, cls_name)
|
||||
self._classes[class_name] = cls
|
||||
self._classes[cls_name] = cls
|
||||
return cls
|
||||
else:
|
||||
# 如果cls_name是builtins中的关键字,则返回对应类
|
||||
if class_name in builtins.__dict__:
|
||||
return builtins.__dict__[class_name]
|
||||
|
||||
raise KeyError(f"找不到类: {class_name}")
|
||||
|
||||
@@ -149,6 +158,9 @@ class ImportManager:
|
||||
Returns:
|
||||
找到的类对象,如果未找到则返回None
|
||||
"""
|
||||
# 如果cls_name是builtins中的关键字,则返回对应类
|
||||
if class_name in builtins.__dict__:
|
||||
return builtins.__dict__[class_name]
|
||||
# 首先在已索引的类中查找
|
||||
if class_name in self._classes:
|
||||
return self._classes[class_name]
|
||||
@@ -161,7 +173,9 @@ class ImportManager:
|
||||
# 遍历所有已加载的模块进行搜索
|
||||
for module_path, module in self._modules.items():
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if inspect.isclass(obj) and ((name.lower() == class_name.lower()) if search_lower else (name == class_name)):
|
||||
if inspect.isclass(obj) and (
|
||||
(name.lower() == class_name.lower()) if search_lower else (name == class_name)
|
||||
):
|
||||
# 将找到的类添加到索引中
|
||||
self._classes[name] = obj
|
||||
self._classes[f"{module_path}:{name}"] = obj
|
||||
@@ -169,6 +183,562 @@ class ImportManager:
|
||||
|
||||
return None
|
||||
|
||||
def get_enhanced_class_info(self, module_path: str, use_dynamic: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
获取增强的类信息,支持动态导入和静态分析
|
||||
|
||||
Args:
|
||||
module_path: 模块路径,格式为 "module.path" 或 "module.path:ClassName"
|
||||
use_dynamic: 是否优先使用动态导入
|
||||
|
||||
Returns:
|
||||
包含详细类信息的字典
|
||||
"""
|
||||
result = {
|
||||
"module_path": module_path,
|
||||
"dynamic_import_success": False,
|
||||
"static_analysis_success": False,
|
||||
"init_params": {},
|
||||
"status_methods": {}, # get_ 开头和 @property 方法
|
||||
"action_methods": {}, # set_ 开头和其他非_开头方法
|
||||
}
|
||||
|
||||
# 尝试动态导入
|
||||
dynamic_info = None
|
||||
static_info = None
|
||||
if use_dynamic:
|
||||
try:
|
||||
dynamic_info = self._get_dynamic_class_info(module_path)
|
||||
result["dynamic_import_success"] = True
|
||||
logger.debug(f"[ImportManager] 动态导入类 {module_path} 成功")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[UniLab Registry] 在补充注册表时,动态导入类 "
|
||||
f"{module_path} 失败(将使用静态分析,"
|
||||
f"建议修复导入错误,以实现更好的注册表识别效果!): {e}"
|
||||
)
|
||||
use_dynamic = False
|
||||
if not use_dynamic:
|
||||
# 尝试静态分析
|
||||
try:
|
||||
static_info = self._get_static_class_info(module_path)
|
||||
result["static_analysis_success"] = True
|
||||
logger.debug(f"[ImportManager] 静态分析类 {module_path} 成功")
|
||||
except Exception as e:
|
||||
logger.warning(f"[ImportManager] 静态分析类 {module_path} 失败: {e}")
|
||||
|
||||
# 合并信息(优先使用动态导入的信息)
|
||||
if dynamic_info:
|
||||
result.update(dynamic_info)
|
||||
elif static_info:
|
||||
result.update(static_info)
|
||||
|
||||
return result
|
||||
|
||||
def _get_dynamic_class_info(self, class_path: str) -> Dict[str, Any]:
|
||||
"""使用inspect模块动态获取类信息"""
|
||||
cls = get_class(class_path)
|
||||
class_name = cls.__name__
|
||||
|
||||
result = {"class_name": class_name, "init_params": self._analyze_method_signature(cls.__init__)["args"],
|
||||
"status_methods": {}, "action_methods": {}}
|
||||
# 分析类的所有成员
|
||||
for name, method in cls.__dict__.items():
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
|
||||
# 检查是否是property
|
||||
if isinstance(method, property):
|
||||
# @property 装饰的方法
|
||||
# noinspection PyTypeChecker
|
||||
return_type = self._get_return_type_from_method(method.fget) if method.fget else "Any"
|
||||
prop_info = {
|
||||
"name": name,
|
||||
"return_type": return_type,
|
||||
}
|
||||
result["status_methods"][name] = prop_info
|
||||
|
||||
# 检查是否有对应的setter
|
||||
if method.fset:
|
||||
setter_info = self._analyze_method_signature(method.fset)
|
||||
result["action_methods"][name] = setter_info
|
||||
|
||||
elif inspect.ismethod(method) or inspect.isfunction(method):
|
||||
if name.startswith("get_"):
|
||||
actual_name = name[4:] # 去掉get_前缀
|
||||
if actual_name in result["status_methods"]:
|
||||
continue
|
||||
# get_ 开头的方法归类为status
|
||||
method_info = self._analyze_method_signature(method)
|
||||
result["status_methods"][actual_name] = method_info
|
||||
elif not name.startswith("_"):
|
||||
# 其他非_开头的方法归类为action
|
||||
method_info = self._analyze_method_signature(method)
|
||||
result["action_methods"][name] = method_info
|
||||
|
||||
return result
|
||||
|
||||
def _get_static_class_info(self, module_path: str) -> Dict[str, Any]:
|
||||
"""使用AST静态分析获取类信息"""
|
||||
module_name, class_name = module_path.rsplit(":", 1)
|
||||
# 将模块路径转换为文件路径
|
||||
file_path = self._module_path_to_file_path(module_name)
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"找不到模块文件: {module_name} -> {file_path}")
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
source_code = f.read()
|
||||
|
||||
tree = ast.parse(source_code)
|
||||
|
||||
# 查找目标类
|
||||
target_class = None
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef):
|
||||
if node.name == class_name:
|
||||
target_class = node
|
||||
break
|
||||
|
||||
if target_class is None:
|
||||
raise AttributeError(f"在文件 {file_path} 中找不到类 {class_name}")
|
||||
|
||||
result = {
|
||||
"class_name": class_name,
|
||||
"init_params": {},
|
||||
"status_methods": {},
|
||||
"action_methods": {},
|
||||
}
|
||||
|
||||
# 分析类的方法
|
||||
for node in target_class.body:
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
method_info = self._analyze_method_node(node)
|
||||
method_name = node.name
|
||||
if method_name == "__init__":
|
||||
result["init_params"] = method_info["args"]
|
||||
elif method_name.startswith("_"):
|
||||
continue
|
||||
elif self._is_property_method(node):
|
||||
# @property 装饰的方法
|
||||
result["status_methods"][method_name] = method_info
|
||||
elif method_name.startswith("get_"):
|
||||
# get_ 开头的方法归类为status
|
||||
actual_name = method_name[4:] # 去掉get_前缀
|
||||
if actual_name not in result["status_methods"]:
|
||||
result["status_methods"][actual_name] = method_info
|
||||
else:
|
||||
# 其他非_开头的方法归类为action
|
||||
result["action_methods"][method_name] = method_info
|
||||
return result
|
||||
|
||||
def _analyze_method_signature(self, method) -> Dict[str, Any]:
|
||||
"""
|
||||
分析方法签名,提取具体的命名参数信息
|
||||
|
||||
注意:此方法会跳过*args和**kwargs,只提取具体的命名参数
|
||||
这样可以确保通过**dict方式传参时的准确性
|
||||
|
||||
示例用法:
|
||||
method_info = self._analyze_method_signature(some_method)
|
||||
params = {"param1": "value1", "param2": "value2"}
|
||||
result = some_method(**params) # 安全的参数传递
|
||||
"""
|
||||
signature = inspect.signature(method)
|
||||
args = []
|
||||
num_required = 0
|
||||
|
||||
for param_name, param in signature.parameters.items():
|
||||
# 跳过self参数
|
||||
if param_name == "self":
|
||||
continue
|
||||
|
||||
# 跳过*args和**kwargs参数
|
||||
if param.kind == param.VAR_POSITIONAL: # *args
|
||||
continue
|
||||
if param.kind == param.VAR_KEYWORD: # **kwargs
|
||||
continue
|
||||
|
||||
is_required = param.default == inspect.Parameter.empty
|
||||
if is_required:
|
||||
num_required += 1
|
||||
|
||||
args.append(
|
||||
{
|
||||
"name": param_name,
|
||||
"type": self._get_type_string(param.annotation),
|
||||
"required": is_required,
|
||||
"default": None if param.default == inspect.Parameter.empty else param.default,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"name": method.__name__,
|
||||
"args": args,
|
||||
"return_type": self._get_type_string(signature.return_annotation),
|
||||
"is_async": inspect.iscoroutinefunction(method),
|
||||
}
|
||||
|
||||
def _get_return_type_from_method(self, method) -> str:
|
||||
"""从方法中获取返回类型"""
|
||||
signature = inspect.signature(method)
|
||||
return self._get_type_string(signature.return_annotation)
|
||||
|
||||
def _get_type_string(self, annotation) -> str:
|
||||
"""将类型注解转换为Class Library中可搜索的类名"""
|
||||
if annotation == inspect.Parameter.empty:
|
||||
return "Any" # 如果没有注解,返回Any
|
||||
if annotation is None:
|
||||
return "None" # 明确的None类型
|
||||
if hasattr(annotation, "__origin__"):
|
||||
# 处理typing模块的类型
|
||||
origin = annotation.__origin__
|
||||
if origin in (list, set, tuple):
|
||||
if hasattr(annotation, "__args__") and annotation.__args__:
|
||||
if len(annotation.__args__):
|
||||
arg0 = annotation.__args__[0]
|
||||
if isinstance(arg0, int):
|
||||
return "Int64MultiArray"
|
||||
elif isinstance(arg0, float):
|
||||
return "Float64MultiArray"
|
||||
return "list"
|
||||
elif origin is dict:
|
||||
return "dict"
|
||||
elif origin is Optional:
|
||||
return "Unknown"
|
||||
return f"Unknown"
|
||||
annotation_str = str(annotation)
|
||||
# 处理typing模块的复杂类型
|
||||
if "typing." in annotation_str:
|
||||
# 简化typing类型显示
|
||||
return (
|
||||
annotation_str.replace("typing.", "")
|
||||
if getattr(annotation, "_name", None) is None
|
||||
else annotation._name.lower()
|
||||
)
|
||||
# 如果是类型对象
|
||||
if hasattr(annotation, "__name__"):
|
||||
# 如果是内置类型
|
||||
if annotation.__module__ == "builtins":
|
||||
return annotation.__name__
|
||||
else:
|
||||
# 如果是自定义类,返回完整路径
|
||||
return f"{annotation.__module__}:{annotation.__name__}"
|
||||
# 如果是typing模块的类型
|
||||
elif hasattr(annotation, "_name"):
|
||||
return annotation._name
|
||||
# 如果是字符串形式的类型注解
|
||||
elif isinstance(annotation, str):
|
||||
return annotation
|
||||
else:
|
||||
return annotation_str
|
||||
|
||||
def _is_property_method(self, node: ast.FunctionDef) -> bool:
|
||||
"""检查是否是@property装饰的方法"""
|
||||
for decorator in node.decorator_list:
|
||||
if isinstance(decorator, ast.Name) and decorator.id == "property":
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_setter_method(self, node: ast.FunctionDef) -> bool:
|
||||
"""检查是否是@xxx.setter装饰的方法"""
|
||||
for decorator in node.decorator_list:
|
||||
if isinstance(decorator, ast.Attribute) and decorator.attr == "setter":
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_property_name_from_setter(self, node: ast.FunctionDef) -> str:
|
||||
"""从setter装饰器中获取属性名"""
|
||||
for decorator in node.decorator_list:
|
||||
if isinstance(decorator, ast.Attribute) and decorator.attr == "setter":
|
||||
if isinstance(decorator.value, ast.Name):
|
||||
return decorator.value.id
|
||||
return node.name
|
||||
|
||||
def get_class_info_static(self, module_class_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
静态分析获取类的方法信息,不需要实际导入模块
|
||||
|
||||
Args:
|
||||
module_class_path: 格式为 "module.path:ClassName" 的字符串
|
||||
|
||||
Returns:
|
||||
包含类方法信息的字典
|
||||
"""
|
||||
try:
|
||||
if ":" not in module_class_path:
|
||||
raise ValueError("module_class_path必须是 'module.path:ClassName' 格式")
|
||||
|
||||
module_path, class_name = module_class_path.rsplit(":", 1)
|
||||
|
||||
# 将模块路径转换为文件路径
|
||||
file_path = self._module_path_to_file_path(module_path)
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
logger.warning(f"找不到模块文件: {module_path} -> {file_path}")
|
||||
return {}
|
||||
|
||||
# 解析源码
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
source_code = f.read()
|
||||
|
||||
tree = ast.parse(source_code)
|
||||
|
||||
# 查找目标类
|
||||
class_node = None
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
||||
class_node = node
|
||||
break
|
||||
|
||||
if not class_node:
|
||||
logger.warning(f"在模块 {module_path} 中找不到类 {class_name}")
|
||||
return {}
|
||||
|
||||
# 分析类的方法
|
||||
methods_info = {}
|
||||
for node in class_node.body:
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
method_info = self._analyze_method_node(node)
|
||||
methods_info[node.name] = method_info
|
||||
|
||||
return {
|
||||
"class_name": class_name,
|
||||
"module_path": module_path,
|
||||
"file_path": file_path,
|
||||
"methods": methods_info,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"静态分析类 {module_class_path} 时出错: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _module_path_to_file_path(self, module_path: str) -> Optional[str]:
|
||||
for path in sys.path:
|
||||
potential_path = Path(path) / module_path.replace(".", "/")
|
||||
|
||||
# 检查是否为包
|
||||
if (potential_path / "__init__.py").exists():
|
||||
return str(potential_path / "__init__.py")
|
||||
|
||||
# 检查是否为模块文件
|
||||
if (potential_path.parent / f"{potential_path.name}.py").exists():
|
||||
return str(potential_path.parent / f"{potential_path.name}.py")
|
||||
|
||||
return None
|
||||
|
||||
def _analyze_method_node(self, node: ast.FunctionDef) -> Dict[str, Any]:
|
||||
"""分析方法节点,提取参数和返回类型信息"""
|
||||
method_info = {
|
||||
"name": node.name,
|
||||
"args": [],
|
||||
"return_type": None,
|
||||
"is_async": isinstance(node, ast.AsyncFunctionDef),
|
||||
}
|
||||
# 获取默认值列表
|
||||
defaults = node.args.defaults
|
||||
num_defaults = len(defaults)
|
||||
|
||||
# 计算必需参数数量
|
||||
total_args = len(node.args.args)
|
||||
num_required = total_args - num_defaults
|
||||
|
||||
# 提取参数信息
|
||||
for i, arg in enumerate(node.args.args):
|
||||
if arg.arg == "self":
|
||||
continue
|
||||
arg_info = {
|
||||
"name": arg.arg,
|
||||
"type": None,
|
||||
"default": None,
|
||||
"required": i < num_required,
|
||||
}
|
||||
|
||||
# 提取类型注解
|
||||
if arg.annotation:
|
||||
arg_info["type"] = ast.unparse(arg.annotation) if hasattr(ast, "unparse") else str(arg.annotation)
|
||||
|
||||
# 提取默认值并推断类型
|
||||
if i >= num_required:
|
||||
default_index = i - num_required
|
||||
if default_index < len(defaults):
|
||||
default_value: Constant = defaults[default_index] # type: ignore
|
||||
assert isinstance(default_value, Constant), "暂不支持对非常量类型进行推断,可反馈开源仓库"
|
||||
arg_info["default"] = default_value.value
|
||||
# 如果没有类型注解,尝试从默认值推断类型
|
||||
if not arg_info["type"]:
|
||||
arg_info["type"] = self._get_type_string(type(arg_info["default"]))
|
||||
method_info["args"].append(arg_info)
|
||||
|
||||
# 提取返回类型
|
||||
if node.returns:
|
||||
method_info["return_type"] = ast.unparse(node.returns) if hasattr(ast, "unparse") else str(node.returns)
|
||||
|
||||
return method_info
|
||||
|
||||
def _infer_type_from_default(self, node: ast.AST) -> Optional[str]:
|
||||
"""从默认值推断参数类型"""
|
||||
if isinstance(node, ast.Constant):
|
||||
value = node.value
|
||||
if isinstance(value, bool):
|
||||
return "bool"
|
||||
elif isinstance(value, int):
|
||||
return "int"
|
||||
elif isinstance(value, float):
|
||||
return "float"
|
||||
elif isinstance(value, str):
|
||||
return "str"
|
||||
elif value is None:
|
||||
return "Optional[Any]"
|
||||
elif isinstance(node, ast.List):
|
||||
return "List"
|
||||
elif isinstance(node, ast.Dict):
|
||||
return "Dict"
|
||||
elif isinstance(node, ast.Tuple):
|
||||
return "Tuple"
|
||||
elif isinstance(node, ast.Set):
|
||||
return "Set"
|
||||
elif isinstance(node, ast.Name):
|
||||
# 常见的默认值模式
|
||||
if node.id in ["None"]:
|
||||
return "Optional[Any]"
|
||||
elif node.id in ["True", "False"]:
|
||||
return "bool"
|
||||
|
||||
return None
|
||||
|
||||
def _infer_types_from_docstring(self, method_info: Dict[str, Any]) -> None:
|
||||
"""从docstring中推断参数类型"""
|
||||
docstring = method_info.get("docstring", "")
|
||||
if not docstring:
|
||||
return
|
||||
|
||||
lines = docstring.split("\n")
|
||||
in_args_section = False
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
|
||||
# 检测Args或Arguments段落
|
||||
if line.lower().startswith(("args:", "arguments:")):
|
||||
in_args_section = True
|
||||
continue
|
||||
elif line.startswith(("returns:", "return:", "yields:", "raises:")):
|
||||
in_args_section = False
|
||||
continue
|
||||
elif not line or not in_args_section:
|
||||
continue
|
||||
|
||||
# 解析参数行,格式通常是: param_name (type): description 或 param_name: description
|
||||
if ":" in line:
|
||||
parts = line.split(":", 1)
|
||||
param_part = parts[0].strip()
|
||||
|
||||
# 提取参数名和类型
|
||||
param_name = None
|
||||
param_type = None
|
||||
|
||||
if "(" in param_part and ")" in param_part:
|
||||
# 格式: param_name (type)
|
||||
param_name = param_part.split("(")[0].strip()
|
||||
type_part = param_part.split("(")[1].split(")")[0].strip()
|
||||
param_type = type_part
|
||||
else:
|
||||
# 格式: param_name
|
||||
param_name = param_part
|
||||
|
||||
# 更新对应参数的类型信息
|
||||
if param_name:
|
||||
for arg_info in method_info["args"]:
|
||||
if arg_info["name"] == param_name and not arg_info["type"]:
|
||||
if param_type:
|
||||
arg_info["inferred_type"] = param_type
|
||||
elif not arg_info["inferred_type"]:
|
||||
# 从描述中推断类型
|
||||
description = parts[1].strip().lower()
|
||||
if any(word in description for word in ["path", "file", "directory", "filename"]):
|
||||
arg_info["inferred_type"] = "str"
|
||||
elif any(
|
||||
word in description for word in ["port", "number", "count", "size", "length"]
|
||||
):
|
||||
arg_info["inferred_type"] = "int"
|
||||
elif any(
|
||||
word in description for word in ["rate", "ratio", "percentage", "temperature"]
|
||||
):
|
||||
arg_info["inferred_type"] = "float"
|
||||
elif any(word in description for word in ["flag", "enable", "disable", "option"]):
|
||||
arg_info["inferred_type"] = "bool"
|
||||
|
||||
def get_registry_class_info(self, module_class_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取适用于注册表的类信息,包含完整的类型推断
|
||||
|
||||
Args:
|
||||
module_class_path: 格式为 "module.path:ClassName" 的字符串
|
||||
|
||||
Returns:
|
||||
适用于注册表的类信息字典
|
||||
"""
|
||||
class_info = self.get_class_info_static(module_class_path)
|
||||
if not class_info:
|
||||
return {}
|
||||
|
||||
registry_info = {
|
||||
"class_name": class_info["class_name"],
|
||||
"module_path": class_info["module_path"],
|
||||
"file_path": class_info["file_path"],
|
||||
"methods": {},
|
||||
"properties": [],
|
||||
"init_params": {},
|
||||
"action_methods": {},
|
||||
}
|
||||
|
||||
for method_name, method_info in class_info["methods"].items():
|
||||
# 分类处理不同类型的方法
|
||||
if method_info["is_property"]:
|
||||
registry_info["properties"].append(
|
||||
{
|
||||
"name": method_name,
|
||||
"return_type": method_info.get("return_type"),
|
||||
"docstring": method_info.get("docstring"),
|
||||
}
|
||||
)
|
||||
elif method_name == "__init__":
|
||||
# 处理初始化参数
|
||||
init_params = {}
|
||||
for arg in method_info["args"]:
|
||||
if arg["name"] != "self":
|
||||
param_info = {
|
||||
"name": arg["name"],
|
||||
"type": arg.get("type") or arg.get("inferred_type"),
|
||||
"required": arg.get("is_required", True),
|
||||
"default": arg.get("default"),
|
||||
}
|
||||
init_params[arg["name"]] = param_info
|
||||
registry_info["init_params"] = init_params
|
||||
elif not method_name.startswith("_"):
|
||||
# 处理公共方法(可能的action方法)
|
||||
action_info = {
|
||||
"name": method_name,
|
||||
"params": {},
|
||||
"return_type": method_info.get("return_type"),
|
||||
"docstring": method_info.get("docstring"),
|
||||
"num_required": method_info.get("num_required", 0) - 1, # 减去self
|
||||
"num_defaults": method_info.get("num_defaults", 0),
|
||||
}
|
||||
|
||||
for arg in method_info["args"]:
|
||||
if arg["name"] != "self":
|
||||
param_info = {
|
||||
"name": arg["name"],
|
||||
"type": arg.get("type") or arg.get("inferred_type"),
|
||||
"required": arg.get("is_required", True),
|
||||
"default": arg.get("default"),
|
||||
}
|
||||
action_info["params"][arg["name"]] = param_info
|
||||
|
||||
registry_info["action_methods"][method_name] = action_info
|
||||
|
||||
return registry_info
|
||||
|
||||
|
||||
# 全局实例,便于直接使用
|
||||
default_manager = ImportManager()
|
||||
@@ -193,3 +763,18 @@ def init_from_list(module_list: List[str]) -> None:
|
||||
"""从模块列表初始化默认管理器"""
|
||||
global default_manager
|
||||
default_manager = ImportManager(module_list)
|
||||
|
||||
|
||||
def get_class_info_static(module_class_path: str) -> Dict[str, Any]:
|
||||
"""静态分析获取类信息的便捷函数"""
|
||||
return default_manager.get_class_info_static(module_class_path)
|
||||
|
||||
|
||||
def get_registry_class_info(module_class_path: str) -> Dict[str, Any]:
|
||||
"""获取适用于注册表的类信息的便捷函数"""
|
||||
return default_manager.get_registry_class_info(module_class_path)
|
||||
|
||||
|
||||
def get_enhanced_class_info(module_path: str, use_dynamic: bool = True) -> Dict[str, Any]:
|
||||
"""获取增强的类信息的便捷函数"""
|
||||
return default_manager.get_enhanced_class_info(module_path, use_dynamic)
|
||||
|
||||
@@ -2,6 +2,8 @@ import collections.abc
|
||||
import json
|
||||
from typing import get_origin, get_args
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def get_type_class(type_hint):
|
||||
origin = get_origin(type_hint)
|
||||
@@ -22,6 +24,12 @@ class TypeEncoder(json.JSONEncoder):
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
class NoAliasDumper(yaml.SafeDumper):
|
||||
def ignore_aliases(self, data):
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class ResultInfoEncoder(json.JSONEncoder):
|
||||
"""专门用于处理任务执行结果信息的JSON编码器"""
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(unilabos_msgs)
|
||||
|
||||
# Default to C99
|
||||
@@ -28,7 +28,11 @@ set(action_files
|
||||
"action/HeatChill.action"
|
||||
"action/HeatChillStart.action"
|
||||
"action/HeatChillStop.action"
|
||||
|
||||
"action/AdjustPH.action"
|
||||
"action/ResetHandling.action"
|
||||
"action/Dry.action"
|
||||
"action/Hydrogenate.action"
|
||||
"action/Recrystallize.action"
|
||||
"action/CleanVessel.action"
|
||||
"action/Dissolve.action"
|
||||
"action/FilterThrough.action"
|
||||
@@ -37,9 +41,9 @@ set(action_files
|
||||
"action/WashSolid.action"
|
||||
"action/Filter.action"
|
||||
"action/Add.action"
|
||||
"action/AddSolid.action"
|
||||
"action/Centrifuge.action"
|
||||
"action/Crystallize.action"
|
||||
"action/Dry.action"
|
||||
"action/Purge.action"
|
||||
"action/StartPurge.action"
|
||||
"action/StartStir.action"
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
# Goal - 添加试剂的目标参数
|
||||
string vessel # 目标容器
|
||||
string reagent # 试剂名称
|
||||
float64 volume # 体积 (可选)
|
||||
float64 mass # 质量 (可选)
|
||||
string amount # 数量描述 (可选)
|
||||
float64 time # 添加时间 (可选)
|
||||
bool stir # 是否搅拌
|
||||
float64 stir_speed # 搅拌速度 (可选)
|
||||
bool viscous # 是否为粘性液体
|
||||
string purpose # 添加目的 (可选)
|
||||
string vessel # 目标容器(必需)
|
||||
string reagent # 试剂名称(必需)
|
||||
string volume # 体积(如 "2.7 mL",可选)
|
||||
string mass # 质量(如 "19.3 g",可选)
|
||||
string amount # 数量描述(可选)
|
||||
string time # 添加时间(如 "1 h", "20 min",可选)
|
||||
bool stir # 是否搅拌(可选)
|
||||
float64 stir_speed # 搅拌速度 (RPM,可选)
|
||||
bool viscous # 是否为粘性液体(可选)
|
||||
string purpose # 添加目的(可选)
|
||||
string event # 事件标识(如 'A', 'B',可选)
|
||||
string mol # 摩尔数(如 '0.28 mol', '16.2 mmol',可选)
|
||||
string rate_spec # 速率规格(如 'portionwise', 'dropwise',可选)
|
||||
string equiv # 当量(如 '1.1',可选)
|
||||
string ratio # 比例(如 '1:1',可选)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
|
||||
15
unilabos_msgs/action/AddSolid.action
Normal file
15
unilabos_msgs/action/AddSolid.action
Normal file
@@ -0,0 +1,15 @@
|
||||
# Goal - 固体加样操作的目标参数
|
||||
string vessel # 目标容器(必需)
|
||||
string reagent # 试剂名称(必需)
|
||||
string mass # 质量字符串(如 "2.9 g",可选)
|
||||
string mol # 摩尔数字符串(如 "0.12 mol",可选)
|
||||
string purpose # 添加目的(可选)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
string return_info # 返回信息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
string current_status # 当前状态描述
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
13
unilabos_msgs/action/AdjustPH.action
Normal file
13
unilabos_msgs/action/AdjustPH.action
Normal file
@@ -0,0 +1,13 @@
|
||||
# Request - 与您的 AdjustPHProtocol 类匹配
|
||||
string vessel
|
||||
float64 ph_value
|
||||
string reagent
|
||||
---
|
||||
# Result - 标准结果格式
|
||||
bool success
|
||||
string message
|
||||
string return_info
|
||||
---
|
||||
# Feedback - 标准反馈格式
|
||||
string status
|
||||
float64 progress
|
||||
@@ -1,14 +1,21 @@
|
||||
string vessel # 装有要溶解物质的容器名称
|
||||
string solvent # 用于溶解物质的溶剂名称
|
||||
float64 volume # 溶剂的体积,可选参数
|
||||
string amount # 要溶解物质的量,可选参数
|
||||
float64 temp # 溶解时的温度,可选参数
|
||||
float64 time # 溶解的时间,可选参数
|
||||
float64 stir_speed # 搅拌速度,可选参数
|
||||
# Goal - 溶解操作的目标参数
|
||||
string vessel # 装有要溶解物质的容器名称(必需)
|
||||
string solvent # 用于溶解物质的溶剂名称(可选)
|
||||
string volume # 溶剂的体积(如 "10 mL",可选)
|
||||
string amount # 要溶解物质的量描述(可选)
|
||||
string temp # 溶解时的温度(如 "60 °C", "room temperature",可选)
|
||||
string time # 溶解的时间(如 "30 min", "1 h",可选)
|
||||
float64 stir_speed # 搅拌速度(可选,默认300 RPM)
|
||||
string mass # 物质质量(如 "2.9 g",可选)
|
||||
string mol # 物质摩尔数(如 "0.12 mol",可选)
|
||||
string reagent # 试剂名称(可选)
|
||||
string event # 事件标识(如 'A', 'B',可选)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
string return_info
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
string status # 当前状态描述
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
@@ -1,17 +1,12 @@
|
||||
# Goal - 干燥操作的目标参数
|
||||
string vessel # 干燥容器
|
||||
float64 time # 干燥时间 (可选,秒)
|
||||
float64 pressure # 压力 (可选,Pa)
|
||||
float64 temp # 温度 (可选,摄氏度)
|
||||
bool continue_heatchill # 是否继续加热冷却
|
||||
# Request
|
||||
string compound # 化合物
|
||||
string vessel # 干燥容器
|
||||
---
|
||||
# Result - 操作结果
|
||||
# Result
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
string return_info
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
float64 current_temp # 当前温度
|
||||
float64 current_pressure # 当前压力
|
||||
string current_status # 当前状态描述
|
||||
# Feedback
|
||||
string status # 当前状态描述
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
@@ -1,7 +1,6 @@
|
||||
# Organic
|
||||
# Organic Synthesis Station EvacuateAndRefill Action
|
||||
string vessel
|
||||
string gas
|
||||
int32 repeats
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Organic
|
||||
string vessel
|
||||
float64 pressure
|
||||
float64 temp
|
||||
float64 time
|
||||
float64 stir_speed
|
||||
# Organic Synthesis Station Evaporate Action
|
||||
string vessel # 目标容器
|
||||
float64 pressure # 真空度
|
||||
float64 temp # 温度
|
||||
string time # 🔧 蒸发时间(支持带单位,如"3 min","180",默认秒)
|
||||
float64 stir_speed # 旋转速度
|
||||
string solvent # 溶剂名称
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Goal - 过滤操作的目标参数
|
||||
string vessel # 过滤容器
|
||||
string filtrate_vessel # 滤液容器 (可选)
|
||||
bool stir # 是否搅拌
|
||||
float64 stir_speed # 搅拌速度 (可选)
|
||||
float64 temp # 温度 (可选,摄氏度)
|
||||
bool continue_heatchill # 是否继续加热冷却
|
||||
float64 volume # 过滤体积 (可选)
|
||||
string vessel # 过滤容器(必需)
|
||||
string filtrate_vessel # 滤液容器(可选)
|
||||
bool stir # 是否搅拌(默认false)
|
||||
float64 stir_speed # 搅拌速度(默认0.0)
|
||||
float64 temp # 温度(默认25.0)
|
||||
bool continue_heatchill # 是否继续加热冷却(默认false)
|
||||
float64 volume # 过滤体积(默认0.0)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
# Organic
|
||||
string vessel
|
||||
float64 temp
|
||||
float64 time
|
||||
bool stir
|
||||
float64 stir_speed
|
||||
string purpose
|
||||
# Goal - 加热冷却操作的目标参数
|
||||
string vessel # 加热容器名称(必需)
|
||||
float64 temp # 目标温度(可选,默认25.0)
|
||||
string time # 🔧 加热时间(支持带单位,如"5 min","300",默认秒)
|
||||
string temp_spec # 温度规格(可选)
|
||||
string time_spec # 时间规格(可选)
|
||||
string pressure # 压力规格(可选,不做特殊处理)
|
||||
string reflux_solvent # 回流溶剂名称(可选,不做特殊处理)
|
||||
bool stir # 是否搅拌(可选,默认false)
|
||||
float64 stir_speed # 搅拌速度(可选,默认300.0)
|
||||
string purpose # 操作目的(可选)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
# Feedback - 实时反馈
|
||||
string status # 当前状态描述
|
||||
13
unilabos_msgs/action/Hydrogenate.action
Normal file
13
unilabos_msgs/action/Hydrogenate.action
Normal file
@@ -0,0 +1,13 @@
|
||||
# Request
|
||||
string temp
|
||||
string time
|
||||
string vessel
|
||||
---
|
||||
# Result
|
||||
bool success
|
||||
string message
|
||||
string return_info
|
||||
---
|
||||
# Feedback
|
||||
string status
|
||||
float64 progress
|
||||
@@ -9,6 +9,11 @@ string rinsing_solvent
|
||||
float64 rinsing_volume
|
||||
int32 rinsing_repeats
|
||||
bool solid
|
||||
float64 flowrate
|
||||
float64 transfer_flowrate
|
||||
string rate_spec
|
||||
string event
|
||||
string through
|
||||
---
|
||||
string return_info
|
||||
bool success
|
||||
|
||||
15
unilabos_msgs/action/Recrystallize.action
Normal file
15
unilabos_msgs/action/Recrystallize.action
Normal file
@@ -0,0 +1,15 @@
|
||||
# Request
|
||||
string ratio # 溶剂比例(如"1:1","3:7")
|
||||
string solvent1 # 第一种溶剂
|
||||
string solvent2 # 第二种溶剂
|
||||
string vessel # 目标容器
|
||||
string volume # 🔧 总体积(支持带单位,如"100 mL","50",默认mL)
|
||||
---
|
||||
# Result
|
||||
bool success
|
||||
string message
|
||||
string return_info
|
||||
---
|
||||
# Feedback
|
||||
string status
|
||||
float64 progress
|
||||
11
unilabos_msgs/action/ResetHandling.action
Normal file
11
unilabos_msgs/action/ResetHandling.action
Normal file
@@ -0,0 +1,11 @@
|
||||
# Request
|
||||
string solvent
|
||||
---
|
||||
# Result
|
||||
bool success
|
||||
string message
|
||||
string return_info
|
||||
---
|
||||
# Feedback
|
||||
string status
|
||||
float64 progress
|
||||
@@ -1,10 +1,19 @@
|
||||
string from_vessel # 源容器的名称,即样品起始所在的容器
|
||||
string to_vessel # 目标容器的名称,分离后的样品要到达的容器
|
||||
string column # 所使用的柱子的名称
|
||||
# Goal - 柱层析操作的目标参数
|
||||
string from_vessel # 源容器的名称,即样品起始所在的容器(必需)
|
||||
string to_vessel # 目标容器的名称,分离后的样品要到达的容器(必需)
|
||||
string column # 所使用的柱子的名称(必需)
|
||||
string rf # Rf值(可选)
|
||||
string pct1 # 第一种溶剂百分比(如 "40 %",可选)
|
||||
string pct2 # 第二种溶剂百分比(如 "50 %",可选)
|
||||
string solvent1 # 第一种溶剂名称(可选)
|
||||
string solvent2 # 第二种溶剂名称(可选)
|
||||
string ratio # 溶剂比例(如 "5:95",可选)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
string return_info
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
string status # 当前状态描述
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
@@ -1,22 +1,27 @@
|
||||
# Organic
|
||||
string purpose # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'.
|
||||
string product_phase # 'top' or 'bottom'. Phase that product will be in.
|
||||
string from_vessel #Contents of from_vessel are transferred to separation_vessel and separation is performed.
|
||||
string separation_vessel # Vessel in which separation of phases will be carried out.
|
||||
string to_vessel # Vessel to send product phase to.
|
||||
string waste_phase_to_vessel # Optional. Vessel to send waste phase to.
|
||||
string solvent # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases.
|
||||
float64 solvent_volume # Optional. Volume of solvent to add.
|
||||
string through # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'.
|
||||
int32 repeats # Optional. Number of separations to perform.
|
||||
float64 stir_time # Optional. Time stir for after adding solvent, before separation of phases.
|
||||
float64 stir_speed # Optional. Speed to stir at after adding solvent, before separation of phases.
|
||||
float64 settling_time # Optional. Time
|
||||
# Goal - 分离操作的目标参数
|
||||
string vessel # 分离容器名称(XDL参数,必需)
|
||||
string purpose # 分离目的 ('wash', 'extract', 'separate',可选)
|
||||
string product_phase # 产物相 ('top', 'bottom',可选)
|
||||
string from_vessel # 源容器(可选)
|
||||
string separation_vessel # 分离容器(与vessel同义,可选)
|
||||
string to_vessel # 目标容器(可选)
|
||||
string waste_phase_to_vessel # 废相目标容器(可选)
|
||||
string product_vessel # 产物收集容器(XDL参数,可选)
|
||||
string waste_vessel # 废液收集容器(XDL参数,可选)
|
||||
string solvent # 溶剂名称(可选)
|
||||
string solvent_volume # 溶剂体积(如 "200 mL",可选)
|
||||
string volume # 体积规格(XDL参数,如 "?",可选)
|
||||
string through # 通过材料(如 'celite',可选)
|
||||
int32 repeats # 重复次数(可选,默认1)
|
||||
float64 stir_time # 搅拌时间(可选,默认30秒)
|
||||
float64 stir_speed # 搅拌速度(可选,默认300 RPM)
|
||||
float64 settling_time # 沉降时间(可选,默认300秒)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
string current_device
|
||||
builtin_interfaces/Duration time_spent
|
||||
builtin_interfaces/Duration time_remaining
|
||||
# Feedback - 实时反馈
|
||||
string status # 当前状态描述
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
# Organic
|
||||
float64 stir_time
|
||||
float64 stir_speed
|
||||
float64 settling_time
|
||||
# Goal - 搅拌操作的目标参数
|
||||
string vessel # 搅拌容器名称(必需)
|
||||
string time # 🔧 搅拌时间(如 "0.5 h", "30 min", "300",默认秒)
|
||||
string event # 事件标识(如 "A", "B")
|
||||
string time_spec # 时间规格(如 "several minutes")
|
||||
float64 stir_time # 解析后的搅拌时间(秒)
|
||||
float64 stir_speed # 搅拌速度(默认200.0)
|
||||
string settling_time # 🔧 沉降时间(支持带单位,默认秒)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
string return_info
|
||||
bool success
|
||||
---
|
||||
string status
|
||||
# Feedback - 实时反馈
|
||||
string status # 当前状态描述
|
||||
@@ -1,16 +1,23 @@
|
||||
string vessel # 装有固体物质的容器名称
|
||||
string solvent # 用于清洗固体的溶剂名称
|
||||
float64 volume # 清洗溶剂的体积
|
||||
string filtrate_vessel # 滤液要收集到的容器名称,可选参数
|
||||
float64 temp # 清洗时的温度,可选参数
|
||||
bool stir # 是否在清洗过程中搅拌,默认为 False
|
||||
float64 stir_speed # 搅拌速度,可选参数
|
||||
float64 time # 清洗的时间,可选参数
|
||||
int32 repeats # 清洗操作的重复次数,默认为 1
|
||||
# Goal - 固体清洗操作的目标参数
|
||||
string vessel # 装有固体的容器名称(必需)
|
||||
string solvent # 清洗溶剂名称(必需)
|
||||
string volume # 🔧 体积(支持数字和带单位的字符串,如"100 mL","?")
|
||||
string filtrate_vessel # 滤液收集容器(可选,默认"")
|
||||
float64 temp # 清洗温度(可选,默认25.0)
|
||||
bool stir # 是否搅拌(可选,默认false)
|
||||
float64 stir_speed # 搅拌速度(可选,默认0.0)
|
||||
string time # 🔧 清洗时间(支持带单位,如"5 min","300 s",默认秒)
|
||||
int32 repeats # 重复次数(与repeats_spec二选一)
|
||||
string volume_spec # 体积规格(优先级高于volume)
|
||||
string repeats_spec # 重复次数规格(优先级高于repeats)
|
||||
string mass # 固体质量描述(可选)
|
||||
string event # 事件标识符(可选)
|
||||
---
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
string return_info
|
||||
---
|
||||
string status # 当前状态描述
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
# Feedback - 实时反馈
|
||||
string status # 当前状态描述
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
Reference in New Issue
Block a user