diff --git a/test/experiments/mock_devices/mock_all.json b/test/experiments/mock_devices/mock_all.json new file mode 100644 index 0000000..f263b47 --- /dev/null +++ b/test/experiments/mock_devices/mock_all.json @@ -0,0 +1,296 @@ +{ + "nodes": [ + { + "id": "MockChiller1", + "name": "模拟冷却器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_chiller", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "current_temperature": 25.0, + "target_temperature": 25.0, + "status": "Idle", + "is_cooling": false, + "is_heating": false, + "vessel": "", + "purpose": "" + } + }, + { + "id": "MockFilter1", + "name": "模拟过滤器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_filter", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "is_filtering": false, + "flow_rate": 0.0, + "filter_life": 100.0, + "vessel": "", + "filtrate_vessel": "", + "filtered_volume": 0.0, + "target_volume": 0.0, + "progress": 0.0, + "stir": false, + "stir_speed": 0.0, + "temperature": 25.0, + "continue_heatchill": false + } + }, + { + "id": "MockHeater1", + "name": "模拟加热器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_heater", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "current_temperature": 25.0, + "target_temperature": 25.0, + "status": "Idle", + "is_heating": false, + "heating_power": 0.0, + "max_temperature": 300.0, + "vessel": "Unknown", + "purpose": "Unknown", + "stir": false, + "stir_speed": 0.0 + } + }, + { + "id": "MockPump1", + "name": "模拟泵设备", + "children": [], + "parent": null, + "type": "device", + "class": "mock_pump", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "current_device": "MockPump1", + "pump_state": "Stopped", + "flow_rate": 0.0, + "target_flow_rate": 0.0, + "pressure": 0.0, + "total_volume": 0.0, + "max_flow_rate": 100.0, + "max_pressure": 10.0, + "from_vessel": "", + "to_vessel": "", + "transfer_volume": 0.0, + "amount": "", + "transfer_time": 0.0, + "is_viscous": false, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "is_solid": false, + "time_spent": 0.0, + "time_remaining": 0.0 + } + }, + { + "id": "MockRotavap1", + "name": "模拟旋转蒸发器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_rotavap", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "rotate_state": "Stopped", + "rotate_time": 0.0, + "rotate_speed": 0.0, + "pump_state": "Stopped", + "pump_time": 0.0, + "vacuum_level": 1013.25, + "temperature": 25.0, + "target_temperature": 25.0, + "success": "True" + } + }, + { + "id": "MockSeparator1", + "name": "模拟分离器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_separator", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "settling_time": 0.0, + "valve_state": "Closed", + "shake_time": 0.0, + "shake_status": "Not Shaking", + "current_device": "MockSeparator1", + "purpose": "", + "product_phase": "", + "from_vessel": "", + "separation_vessel": "", + "to_vessel": "", + "waste_phase_to_vessel": "", + "solvent": "", + "solvent_volume": 0.0, + "through": "", + "repeats": 1, + "stir_time": 0.0, + "stir_speed": 0.0, + "time_spent": 0.0, + "time_remaining": 0.0 + } + }, + { + "id": "MockSolenoidValve1", + "name": "模拟电磁阀", + "children": [], + "parent": null, + "type": "device", + "class": "mock_solenoid_valve", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "valve_status": "Closed" + } + }, + { + "id": "MockStirrer1NEW", + "name": "模拟搅拌器(new)", + "children": [], + "parent": null, + "type": "device", + "class": "mock_stirrer_new", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "vessel": "", + "purpose": "", + "stir_speed": 0.0, + "target_stir_speed": 0.0, + "stir_state": "Stopped", + "stir_time": 0.0, + "settling_time": 0.0, + "progress": 0.0, + "max_stir_speed": 2000.0 + } + }, + { + "id": "MockStirrer1", + "name": "模拟搅拌器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_stirrer", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "stir_speed": 0.0, + "target_stir_speed": 0.0, + "stir_state": "Stopped", + "temperature": 25.0, + "target_temperature": 25.0, + "heating_state": "Off", + "heating_power": 0.0, + "max_stir_speed": 2000.0, + "max_temperature": 300.0 + } + }, + { + "id": "MockVacuum1", + "name": "模拟真空泵", + "children": [], + "parent": null, + "type": "device", + "class": "mock_vacuum", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "power_state": "Off", + "pump_state": "Stopped", + "vacuum_level": 1013.25, + "target_vacuum": 50.0, + "pump_speed": 0.0, + "pump_efficiency": 95.0, + "max_pump_speed": 100.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_chiller.json b/test/experiments/mock_devices/mock_chiller.json new file mode 100644 index 0000000..fc0e2f4 --- /dev/null +++ b/test/experiments/mock_devices/mock_chiller.json @@ -0,0 +1,30 @@ +{ + "nodes": [ + { + "id": "MockChiller1", + "name": "模拟冷却器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_chiller", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "current_temperature": 25.0, + "target_temperature": 25.0, + "status": "Idle", + "is_cooling": false, + "is_heating": false, + "vessel": "", + "purpose": "" + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_filter.json b/test/experiments/mock_devices/mock_filter.json new file mode 100644 index 0000000..d8d8029 --- /dev/null +++ b/test/experiments/mock_devices/mock_filter.json @@ -0,0 +1,36 @@ +{ + "nodes": [ + { + "id": "MockFilter1", + "name": "模拟过滤器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_filter", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "is_filtering": false, + "flow_rate": 0.0, + "filter_life": 100.0, + "vessel": "", + "filtrate_vessel": "", + "filtered_volume": 0.0, + "target_volume": 0.0, + "progress": 0.0, + "stir": false, + "stir_speed": 0.0, + "temperature": 25.0, + "continue_heatchill": false + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_heater.json b/test/experiments/mock_devices/mock_heater.json new file mode 100644 index 0000000..1aca968 --- /dev/null +++ b/test/experiments/mock_devices/mock_heater.json @@ -0,0 +1,33 @@ +{ + "nodes": [ + { + "id": "MockHeater1", + "name": "模拟加热器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_heater", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "current_temperature": 25.0, + "target_temperature": 25.0, + "status": "Idle", + "is_heating": false, + "heating_power": 0.0, + "max_temperature": 300.0, + "vessel": "Unknown", + "purpose": "Unknown", + "stir": false, + "stir_speed": 0.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_pump.json b/test/experiments/mock_devices/mock_pump.json new file mode 100644 index 0000000..d1c1394 --- /dev/null +++ b/test/experiments/mock_devices/mock_pump.json @@ -0,0 +1,44 @@ +{ + "nodes": [ + { + "id": "MockPump1", + "name": "模拟泵设备", + "children": [], + "parent": null, + "type": "device", + "class": "mock_pump", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "current_device": "MockPump1", + "pump_state": "Stopped", + "flow_rate": 0.0, + "target_flow_rate": 0.0, + "pressure": 0.0, + "total_volume": 0.0, + "max_flow_rate": 100.0, + "max_pressure": 10.0, + "from_vessel": "", + "to_vessel": "", + "transfer_volume": 0.0, + "amount": "", + "transfer_time": 0.0, + "is_viscous": false, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "is_solid": false, + "time_spent": 0.0, + "time_remaining": 0.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_rotavap.json b/test/experiments/mock_devices/mock_rotavap.json new file mode 100644 index 0000000..b28cfe2 --- /dev/null +++ b/test/experiments/mock_devices/mock_rotavap.json @@ -0,0 +1,33 @@ +{ + "nodes": [ + { + "id": "MockRotavap1", + "name": "模拟旋转蒸发器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_rotavap", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "rotate_state": "Stopped", + "rotate_time": 0.0, + "rotate_speed": 0.0, + "pump_state": "Stopped", + "pump_time": 0.0, + "vacuum_level": 1013.25, + "temperature": 25.0, + "target_temperature": 25.0, + "success": "True" + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_separator.json b/test/experiments/mock_devices/mock_separator.json new file mode 100644 index 0000000..20f2671 --- /dev/null +++ b/test/experiments/mock_devices/mock_separator.json @@ -0,0 +1,43 @@ +{ + "nodes": [ + { + "id": "MockSeparator1", + "name": "模拟分离器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_separator", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "settling_time": 0.0, + "valve_state": "Closed", + "shake_time": 0.0, + "shake_status": "Not Shaking", + "current_device": "MockSeparator1", + "purpose": "", + "product_phase": "", + "from_vessel": "", + "separation_vessel": "", + "to_vessel": "", + "waste_phase_to_vessel": "", + "solvent": "", + "solvent_volume": 0.0, + "through": "", + "repeats": 1, + "stir_time": 0.0, + "stir_speed": 0.0, + "time_spent": 0.0, + "time_remaining": 0.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_solenoid_valve.json b/test/experiments/mock_devices/mock_solenoid_valve.json new file mode 100644 index 0000000..e4e23cc --- /dev/null +++ b/test/experiments/mock_devices/mock_solenoid_valve.json @@ -0,0 +1,25 @@ +{ + "nodes": [ + { + "id": "MockSolenoidValve1", + "name": "模拟电磁阀", + "children": [], + "parent": null, + "type": "device", + "class": "mock_solenoid_valve", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "valve_status": "Closed" + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_stirrer.json b/test/experiments/mock_devices/mock_stirrer.json new file mode 100644 index 0000000..9dfc59c --- /dev/null +++ b/test/experiments/mock_devices/mock_stirrer.json @@ -0,0 +1,33 @@ +{ + "nodes": [ + { + "id": "MockStirrer1", + "name": "模拟搅拌器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_stirrer", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "stir_speed": 0.0, + "target_stir_speed": 0.0, + "stir_state": "Stopped", + "temperature": 25.0, + "target_temperature": 25.0, + "heating_state": "Off", + "heating_power": 0.0, + "max_stir_speed": 2000.0, + "max_temperature": 300.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_stirrer_new.json b/test/experiments/mock_devices/mock_stirrer_new.json new file mode 100644 index 0000000..837b2fe --- /dev/null +++ b/test/experiments/mock_devices/mock_stirrer_new.json @@ -0,0 +1,33 @@ +{ + "nodes": [ + { + "id": "MockStirrer1COPY", + "name": "模拟搅拌器(Copy)", + "children": [], + "parent": null, + "type": "device", + "class": "mock_stirrer_new", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "vessel": "", + "purpose": "", + "stir_speed": 0.0, + "target_stir_speed": 0.0, + "stir_state": "Stopped", + "stir_time": 0.0, + "settling_time": 0.0, + "progress": 0.0, + "max_stir_speed": 2000.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_vacuum.json b/test/experiments/mock_devices/mock_vacuum.json new file mode 100644 index 0000000..31406ae --- /dev/null +++ b/test/experiments/mock_devices/mock_vacuum.json @@ -0,0 +1,31 @@ +{ + "nodes": [ + { + "id": "MockVacuum1", + "name": "模拟真空泵", + "children": [], + "parent": null, + "type": "device", + "class": "mock_vacuum", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "power_state": "Off", + "pump_state": "Stopped", + "vacuum_level": 1013.25, + "target_vacuum": 50.0, + "pump_speed": 0.0, + "pump_efficiency": 95.0, + "max_pump_speed": 100.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/addteststation.json b/test/experiments/mock_protocol/addteststation.json new file mode 100644 index 0000000..dd16a1f --- /dev/null +++ b/test/experiments/mock_protocol/addteststation.json @@ -0,0 +1,250 @@ +{ + "nodes": [ + { + "id": "AddTestStation", + "name": "添加试剂测试工作站", + "children": [ + "pump_add", + "flask_1", + "flask_2", + "flask_3", + "flask_4", + "reactor", + "stirrer", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol"] + }, + "data": {} + }, + { + "id": "pump_add", + "name": "pump_add", + "children": [], + "parent": "AddTestStation", + "type": "device", + "class": "virtual_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 25.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "stirrer", + "name": "stirrer", + "children": [], + "parent": "AddTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 698.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_1", + "name": "通用试剂瓶1", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_2", + "name": "通用试剂瓶2", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_3", + "name": "通用试剂瓶3", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_4", + "name": "通用试剂瓶4", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "reactor", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "flask_air", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "stirrer", + "target": "reactor", + "type": "physical", + "port": { + "stirrer": "top", + "reactor": "bottom" + } + }, + { + "source": "pump_add", + "target": "flask_1", + "type": "physical", + "port": { + "pump_add": "outlet", + "flask_1": "top" + } + }, + { + "source": "pump_add", + "target": "flask_2", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_2": "top" + } + }, + { + "source": "pump_add", + "target": "flask_3", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_3": "top" + } + }, + { + "source": "pump_add", + "target": "flask_4", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_4": "top" + } + }, + { + "source": "pump_add", + "target": "reactor", + "type": "physical", + "port": { + "pump_add": "outlet", + "reactor": "top" + } + }, + { + "source": "pump_add", + "target": "flask_air", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_air": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/centrifugeteststation.json b/test/experiments/mock_protocol/centrifugeteststation.json new file mode 100644 index 0000000..e691834 --- /dev/null +++ b/test/experiments/mock_protocol/centrifugeteststation.json @@ -0,0 +1,271 @@ +{ + "nodes": [ + { + "id": "CentrifugeTestStation", + "name": "离心机测试工作站", + "children": [ + "pump_add", + "flask_1", + "flask_2", + "flask_3", + "reactor", + "stirrer", + "centrifuge_1", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "CentrifugeProtocol"] + }, + "data": {} + }, + { + "id": "pump_add", + "name": "pump_add", + "children": [], + "parent": "CentrifugeTestStation", + "type": "device", + "class": "virtual_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 25.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "stirrer", + "name": "stirrer", + "children": [], + "parent": "CentrifugeTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "centrifuge_1", + "name": "离心机", + "children": [], + "parent": "CentrifugeTestStation", + "type": "device", + "class": "virtual_centrifuge", + "position": { + "x": 800, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_speed": 15000.0, + "max_temp": 40.0, + "min_temp": 4.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_1", + "name": "样品瓶1", + "children": [], + "parent": "CentrifugeTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_2", + "name": "样品瓶2", + "children": [], + "parent": "CentrifugeTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_3", + "name": "缓冲液瓶", + "children": [], + "parent": "CentrifugeTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "CentrifugeTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "CentrifugeTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "stirrer", + "target": "reactor", + "type": "physical", + "port": { + "stirrer": "top", + "reactor": "bottom" + } + }, + { + "source": "pump_add", + "target": "flask_1", + "type": "physical", + "port": { + "pump_add": "outlet", + "flask_1": "top" + } + }, + { + "source": "pump_add", + "target": "flask_2", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_2": "top" + } + }, + { + "source": "pump_add", + "target": "flask_3", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_3": "top" + } + }, + { + "source": "pump_add", + "target": "reactor", + "type": "physical", + "port": { + "pump_add": "outlet", + "reactor": "top" + } + }, + { + "source": "pump_add", + "target": "flask_air", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_air": "top" + } + }, + { + "source": "centrifuge_1", + "target": "reactor", + "type": "logical", + "port": { + "centrifuge_1": "chamber", + "reactor": "vessel" + } + }, + { + "source": "centrifuge_1", + "target": "flask_1", + "type": "logical", + "port": { + "centrifuge_1": "chamber", + "flask_1": "vessel" + } + }, + { + "source": "centrifuge_1", + "target": "flask_2", + "type": "logical", + "port": { + "centrifuge_1": "chamber", + "flask_2": "vessel" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/cleanvesselteststation.json b/test/experiments/mock_protocol/cleanvesselteststation.json new file mode 100644 index 0000000..c5d3086 --- /dev/null +++ b/test/experiments/mock_protocol/cleanvesselteststation.json @@ -0,0 +1,362 @@ +{ + "nodes": [ + { + "id": "CleanVesselTestStation", + "name": "容器清洗测试工作站", + "children": [ + "transfer_pump_cleaner", + "heatchill_1", + "flask_water", + "flask_ethanol", + "flask_acetone", + "flask_waste", + "reactor", + "flask_buffer", + "flask_sample", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["CleanVesselProtocol", "TransferProtocol", "AddProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_cleaner", + "name": "清洗转移泵", + "children": [], + "parent": "CleanVesselTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "current_volume": 0.0, + "max_volume": 50.0, + "transfer_rate": 10.0, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + } + }, + { + "id": "heatchill_1", + "name": "加热冷却器", + "children": [], + "parent": "CleanVesselTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 150.0, + "min_temp": -20.0 + }, + "data": { + "status": "Idle", + "current_temp": 25.0, + "target_temp": 25.0, + "vessel": "", + "purpose": "", + "progress": 0.0, + "current_status": "Ready" + } + }, + { + "id": "flask_water", + "name": "水溶剂瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "water", + "volume": 1500.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇溶剂瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethanol", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮溶剂瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "acetone", + "volume": 1800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "flask_waste", + "name": "废液瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "residue", + "volume": 50.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_buffer", + "name": "缓冲液瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "buffer", + "volume": 1000.0, + "concentration": 10.0 + } + ] + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "transfer_pump_cleaner", + "target": "flask_water", + "type": "physical", + "port": { + "transfer_pump_cleaner": "1", + "flask_water": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_ethanol", + "type": "physical", + "port": { + "transfer_pump_cleaner": "2", + "flask_ethanol": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_acetone", + "type": "physical", + "port": { + "transfer_pump_cleaner": "3", + "flask_acetone": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_waste", + "type": "physical", + "port": { + "transfer_pump_cleaner": "4", + "flask_waste": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump_cleaner": "5", + "reactor": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_buffer", + "type": "physical", + "port": { + "transfer_pump_cleaner": "6", + "flask_buffer": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_sample", + "type": "physical", + "port": { + "transfer_pump_cleaner": "7", + "flask_sample": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_air", + "type": "physical", + "port": { + "transfer_pump_cleaner": "8", + "flask_air": "top" + } + }, + { + "source": "heatchill_1", + "target": "reactor", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "reactor": "bottom" + } + }, + { + "source": "heatchill_1", + "target": "flask_sample", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "flask_sample": "bottom" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/dissolveteststation.json b/test/experiments/mock_protocol/dissolveteststation.json new file mode 100644 index 0000000..8b7ad28 --- /dev/null +++ b/test/experiments/mock_protocol/dissolveteststation.json @@ -0,0 +1,343 @@ +{ + "nodes": [ + { + "id": "DissolveTestStation", + "name": "溶解测试工作站", + "children": [ + "transfer_pump_1", + "heatchill_1", + "stirrer_1", + "flask_water", + "flask_ethanol", + "flask_dmso", + "reactor", + "flask_sample", + "flask_buffer" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["DissolveProtocol", "TransferProtocol", "HeatChillProtocol", "StirProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵", + "children": [], + "parent": "DissolveTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "current_volume": 0.0, + "max_volume": 50.0, + "transfer_rate": 10.0, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + } + }, + { + "id": "heatchill_1", + "name": "加热冷却器", + "children": [], + "parent": "DissolveTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 150.0, + "min_temp": -20.0 + }, + "data": { + "status": "Idle", + "current_temp": 25.0, + "target_temp": 25.0, + "vessel": "", + "purpose": "", + "progress": 0.0, + "current_status": "Ready" + } + }, + { + "id": "stirrer_1", + "name": "搅拌器", + "children": [], + "parent": "DissolveTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 750.1111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_water", + "name": "水溶剂瓶", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "water", + "volume": 1500.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇溶剂瓶", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethanol", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_dmso", + "name": "DMSO溶剂瓶", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "dmso", + "volume": 800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "solid_sample", + "volume": 10.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer", + "name": "缓冲液瓶", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "buffer", + "volume": 1000.0, + "concentration": 10.0 + } + ] + } + } + ], + "links": [ + { + "source": "transfer_pump_1", + "target": "flask_water", + "type": "physical", + "port": { + "transfer_pump_1": "1", + "flask_water": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_ethanol", + "type": "physical", + "port": { + "transfer_pump_1": "2", + "flask_ethanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_dmso", + "type": "physical", + "port": { + "transfer_pump_1": "3", + "flask_dmso": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump_1": "4", + "reactor": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_sample", + "type": "physical", + "port": { + "transfer_pump_1": "5", + "flask_sample": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_buffer", + "type": "physical", + "port": { + "transfer_pump_1": "6", + "flask_buffer": "top" + } + }, + { + "source": "heatchill_1", + "target": "reactor", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "reactor": "bottom" + } + }, + { + "source": "heatchill_1", + "target": "flask_sample", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "flask_sample": "bottom" + } + }, + { + "source": "stirrer_1", + "target": "reactor", + "type": "physical", + "port": { + "stirrer_1": "stir_rod", + "reactor": "center" + } + }, + { + "source": "stirrer_1", + "target": "flask_sample", + "type": "physical", + "port": { + "stirrer_1": "stir_rod", + "flask_sample": "center" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/evaporateteststation.json b/test/experiments/mock_protocol/evaporateteststation.json new file mode 100644 index 0000000..e69de29 diff --git a/test/experiments/mock_protocol/filterteststation.json b/test/experiments/mock_protocol/filterteststation.json new file mode 100644 index 0000000..a816def --- /dev/null +++ b/test/experiments/mock_protocol/filterteststation.json @@ -0,0 +1,270 @@ +{ + "nodes": [ + { + "id": "FilterTestStation", + "name": "过滤器测试工作站", + "children": [ + "pump_add", + "flask_sample", + "flask_filtrate", + "flask_buffer", + "reactor", + "stirrer", + "filter_1", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "FilterProtocol"] + }, + "data": {} + }, + { + "id": "pump_add", + "name": "pump_add", + "children": [], + "parent": "FilterTestStation", + "type": "device", + "class": "virtual_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 25.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "stirrer", + "name": "stirrer", + "children": [], + "parent": "FilterTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "filter_1", + "name": "过滤器", + "children": [], + "parent": "FilterTestStation", + "type": "device", + "class": "virtual_filter", + "position": { + "x": 800, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "FilterTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_filtrate", + "name": "滤液瓶", + "children": [], + "parent": "FilterTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer", + "name": "缓冲液瓶", + "children": [], + "parent": "FilterTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "FilterTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "FilterTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "stirrer", + "target": "reactor", + "type": "physical", + "port": { + "stirrer": "top", + "reactor": "bottom" + } + }, + { + "source": "pump_add", + "target": "flask_sample", + "type": "physical", + "port": { + "pump_add": "outlet", + "flask_sample": "top" + } + }, + { + "source": "pump_add", + "target": "flask_filtrate", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_filtrate": "top" + } + }, + { + "source": "pump_add", + "target": "flask_buffer", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_buffer": "top" + } + }, + { + "source": "pump_add", + "target": "reactor", + "type": "physical", + "port": { + "pump_add": "outlet", + "reactor": "top" + } + }, + { + "source": "pump_add", + "target": "flask_air", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_air": "top" + } + }, + { + "source": "filter_1", + "target": "reactor", + "type": "logical", + "port": { + "filter_1": "input", + "reactor": "vessel" + } + }, + { + "source": "filter_1", + "target": "flask_sample", + "type": "logical", + "port": { + "filter_1": "input", + "flask_sample": "vessel" + } + }, + { + "source": "filter_1", + "target": "flask_filtrate", + "type": "logical", + "port": { + "filter_1": "output", + "flask_filtrate": "vessel" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/filterthroughteststation.json b/test/experiments/mock_protocol/filterthroughteststation.json new file mode 100644 index 0000000..b250df5 --- /dev/null +++ b/test/experiments/mock_protocol/filterthroughteststation.json @@ -0,0 +1,388 @@ +{ + "nodes": [ + { + "id": "FilterThroughTestStation", + "name": "过滤通过测试工作站", + "children": [ + "transfer_pump_1", + "filter_1", + "flask_ethanol", + "flask_water", + "flask_methanol", + "reactor", + "collection_flask", + "waste_flask", + "flask_sample", + "flask_celite", + "flask_silica" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["FilterThroughProtocol", "TransferProtocol", "FilterProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵", + "children": [], + "parent": "FilterThroughTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "current_volume": 0.0, + "max_volume": 50.0, + "transfer_rate": 10.0, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + } + }, + { + "id": "filter_1", + "name": "过滤器", + "children": [], + "parent": "FilterThroughTestStation", + "type": "device", + "class": "virtual_filter", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle", + "filter_state": "Ready", + "current_temp": 25.0, + "target_temp": 25.0, + "max_temp": 100.0, + "stir_speed": 0.0, + "max_stir_speed": 1000.0, + "filtered_volume": 0.0, + "progress": 0.0, + "message": "" + } + }, + { + "id": "flask_ethanol", + "name": "乙醇溶剂瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethanol", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_water", + "name": "水溶剂瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "water", + "volume": 1800.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇溶剂瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "methanol", + "volume": 800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "crude_product", + "volume": 200.0, + "concentration": 80.0 + } + ] + } + }, + { + "id": "collection_flask", + "name": "收集瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_flask", + "name": "废液瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "sample_mixture", + "volume": 100.0, + "concentration": 50.0 + } + ] + } + }, + { + "id": "flask_celite", + "name": "硅藻土容器", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 150, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "celite", + "volume": 50.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_silica", + "name": "硅胶容器", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "silica", + "volume": 30.0, + "concentration": 100.0 + } + ] + } + } + ], + "links": [ + { + "source": "transfer_pump_1", + "target": "flask_ethanol", + "type": "physical", + "port": { + "transfer_pump_1": "1", + "flask_ethanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_water", + "type": "physical", + "port": { + "transfer_pump_1": "2", + "flask_water": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_methanol", + "type": "physical", + "port": { + "transfer_pump_1": "3", + "flask_methanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump_1": "4", + "reactor": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "collection_flask", + "type": "physical", + "port": { + "transfer_pump_1": "5", + "collection_flask": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "waste_flask", + "type": "physical", + "port": { + "transfer_pump_1": "6", + "waste_flask": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_sample", + "type": "physical", + "port": { + "transfer_pump_1": "7", + "flask_sample": "top" + } + }, + { + "source": "filter_1", + "target": "collection_flask", + "type": "physical", + "port": { + "filter_1": "filter_element", + "collection_flask": "top" + } + }, + { + "source": "filter_1", + "target": "reactor", + "type": "physical", + "port": { + "filter_1": "filter_element", + "reactor": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/heatchillteststation.json b/test/experiments/mock_protocol/heatchillteststation.json new file mode 100644 index 0000000..9d243b8 --- /dev/null +++ b/test/experiments/mock_protocol/heatchillteststation.json @@ -0,0 +1,262 @@ +{ + "nodes": [ + { + "id": "HeatChillTestStation", + "name": "加热冷却测试工作站", + "children": [ + "pump_add", + "flask_sample", + "flask_buffer1", + "flask_buffer2", + "reactor", + "stirrer", + "heatchill_1", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol"] + }, + "data": {} + }, + { + "id": "pump_add", + "name": "pump_add", + "children": [], + "parent": "HeatChillTestStation", + "type": "device", + "class": "virtual_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 25.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "stirrer", + "name": "stirrer", + "children": [], + "parent": "HeatChillTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "heatchill_1", + "name": "加热冷却器", + "children": [], + "parent": "HeatChillTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 800, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 200.0, + "min_temp": -80.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "HeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer1", + "name": "缓冲液瓶1", + "children": [], + "parent": "HeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer2", + "name": "缓冲液瓶2", + "children": [], + "parent": "HeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "HeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "HeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "stirrer", + "target": "reactor", + "type": "physical", + "port": { + "stirrer": "top", + "reactor": "bottom" + } + }, + { + "source": "pump_add", + "target": "flask_sample", + "type": "physical", + "port": { + "pump_add": "outlet", + "flask_sample": "top" + } + }, + { + "source": "pump_add", + "target": "flask_buffer1", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_buffer1": "top" + } + }, + { + "source": "pump_add", + "target": "flask_buffer2", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_buffer2": "top" + } + }, + { + "source": "pump_add", + "target": "reactor", + "type": "physical", + "port": { + "pump_add": "outlet", + "reactor": "top" + } + }, + { + "source": "pump_add", + "target": "flask_air", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_air": "top" + } + }, + { + "source": "heatchill_1", + "target": "reactor", + "type": "logical", + "port": { + "heatchill_1": "heating_element", + "reactor": "vessel" + } + }, + { + "source": "heatchill_1", + "target": "flask_sample", + "type": "logical", + "port": { + "heatchill_1": "heating_element", + "flask_sample": "vessel" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/runcolumnteststation.json b/test/experiments/mock_protocol/runcolumnteststation.json new file mode 100644 index 0000000..51741f1 --- /dev/null +++ b/test/experiments/mock_protocol/runcolumnteststation.json @@ -0,0 +1,412 @@ +{ + "nodes": [ + { + "id": "RunColumnTestStation", + "name": "柱层析测试工作站", + "children": [ + "transfer_pump_1", + "column_1", + "flask_sample", + "flask_hexane", + "flask_ethyl_acetate", + "flask_methanol", + "collection_flask_1", + "collection_flask_2", + "collection_flask_3", + "waste_flask", + "reactor" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["RunColumnProtocol", "TransferProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵", + "children": [], + "parent": "RunColumnTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "current_volume": 0.0, + "max_volume": 50.0, + "transfer_rate": 10.0, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + } + }, + { + "id": "column_1", + "name": "柱层析设备", + "children": [], + "parent": "RunColumnTestStation", + "type": "device", + "class": "virtual_column", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_flow_rate": 5.0, + "column_length": 30.0, + "column_diameter": 2.5 + }, + "data": { + "status": "Idle", + "column_state": "Ready", + "current_flow_rate": 0.0, + "max_flow_rate": 5.0, + "column_length": 30.0, + "column_diameter": 2.5, + "processed_volume": 0.0, + "progress": 0.0, + "current_status": "Ready" + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "crude_mixture", + "volume": 200.0, + "concentration": 70.0 + } + ] + } + }, + { + "id": "flask_hexane", + "name": "正己烷洗脱剂", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "hexane", + "volume": 1500.0, + "concentration": 99.8 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯洗脱剂", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethyl_acetate", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇洗脱剂", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "methanol", + "volume": 800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "collection_flask_1", + "name": "收集瓶1", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 750, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_flask_2", + "name": "收集瓶2", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_flask_3", + "name": "收集瓶3", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 1050, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_flask", + "name": "废液瓶", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 1200, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "reaction_mixture", + "volume": 300.0, + "concentration": 85.0 + } + ] + } + } + ], + "links": [ + { + "source": "transfer_pump_1", + "target": "flask_sample", + "type": "physical", + "port": { + "transfer_pump_1": "1", + "flask_sample": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_hexane", + "type": "physical", + "port": { + "transfer_pump_1": "2", + "flask_hexane": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_ethyl_acetate", + "type": "physical", + "port": { + "transfer_pump_1": "3", + "flask_ethyl_acetate": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_methanol", + "type": "physical", + "port": { + "transfer_pump_1": "4", + "flask_methanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "column_1", + "type": "physical", + "port": { + "transfer_pump_1": "5", + "column_1": "inlet" + } + }, + { + "source": "transfer_pump_1", + "target": "collection_flask_1", + "type": "physical", + "port": { + "transfer_pump_1": "6", + "collection_flask_1": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "collection_flask_2", + "type": "physical", + "port": { + "transfer_pump_1": "7", + "collection_flask_2": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "collection_flask_3", + "type": "physical", + "port": { + "transfer_pump_1": "8", + "collection_flask_3": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "waste_flask", + "type": "physical", + "port": { + "transfer_pump_1": "9", + "waste_flask": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump_1": "10", + "reactor": "top" + } + }, + { + "source": "column_1", + "target": "collection_flask_1", + "type": "physical", + "port": { + "column_1": "outlet", + "collection_flask_1": "top" + } + }, + { + "source": "column_1", + "target": "collection_flask_2", + "type": "physical", + "port": { + "column_1": "outlet", + "collection_flask_2": "top" + } + }, + { + "source": "column_1", + "target": "collection_flask_3", + "type": "physical", + "port": { + "column_1": "outlet", + "collection_flask_3": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/stirteststation.json b/test/experiments/mock_protocol/stirteststation.json new file mode 100644 index 0000000..20694d1 --- /dev/null +++ b/test/experiments/mock_protocol/stirteststation.json @@ -0,0 +1,250 @@ +{ + "nodes": [ + { + "id": "StirTestStation", + "name": "搅拌测试工作站", + "children": [ + "pump_add", + "flask_sample", + "flask_buffer1", + "flask_buffer2", + "reactor", + "stirrer", + "flask_waste", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "StirProtocol", "StartStirProtocol", "StopStirProtocol"] + }, + "data": {} + }, + { + "id": "pump_add", + "name": "添加泵", + "children": [], + "parent": "StirTestStation", + "type": "device", + "class": "virtual_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 25.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "stirrer", + "name": "搅拌器", + "children": [], + "parent": "StirTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer1", + "name": "缓冲液瓶1", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer2", + "name": "缓冲液瓶2", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_waste", + "name": "废液瓶", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 3000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "stirrer", + "target": "reactor", + "type": "physical", + "port": { + "stirrer": "top", + "reactor": "bottom" + } + }, + { + "source": "pump_add", + "target": "flask_sample", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_sample": "top" + } + }, + { + "source": "pump_add", + "target": "flask_buffer1", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_buffer1": "top" + } + }, + { + "source": "pump_add", + "target": "flask_buffer2", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_buffer2": "top" + } + }, + { + "source": "pump_add", + "target": "reactor", + "type": "physical", + "port": { + "pump_add": "outlet", + "reactor": "top" + } + }, + { + "source": "pump_add", + "target": "flask_waste", + "type": "physical", + "port": { + "pump_add": "outlet", + "flask_waste": "top" + } + }, + { + "source": "pump_add", + "target": "flask_air", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_air": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/transferteststation.json b/test/experiments/mock_protocol/transferteststation.json new file mode 100644 index 0000000..cbe485b --- /dev/null +++ b/test/experiments/mock_protocol/transferteststation.json @@ -0,0 +1,249 @@ +{ + "nodes": [ + { + "id": "TransferTestStation", + "name": "液体转移测试工作站", + "children": [ + "transfer_pump", + "flask_source1", + "flask_source2", + "flask_target1", + "flask_target2", + "reactor", + "flask_waste", + "flask_rinsing" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["TransferProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump", + "name": "转移泵", + "children": [], + "parent": "TransferTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 5.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_source1", + "name": "源容器1", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_source2", + "name": "源容器2", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_target1", + "name": "目标容器1", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_target2", + "name": "目标容器2", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_waste", + "name": "废液瓶", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_rinsing", + "name": "冲洗液瓶", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "transfer_pump", + "target": "flask_source1", + "type": "physical", + "port": { + "transfer_pump": "inlet", + "flask_source1": "top" + } + }, + { + "source": "transfer_pump", + "target": "flask_source2", + "type": "physical", + "port": { + "transfer_pump": "inlet", + "flask_source2": "top" + } + }, + { + "source": "transfer_pump", + "target": "flask_target1", + "type": "physical", + "port": { + "transfer_pump": "outlet", + "flask_target1": "top" + } + }, + { + "source": "transfer_pump", + "target": "flask_target2", + "type": "physical", + "port": { + "transfer_pump": "outlet", + "flask_target2": "top" + } + }, + { + "source": "transfer_pump", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump": "outlet", + "reactor": "top" + } + }, + { + "source": "transfer_pump", + "target": "flask_waste", + "type": "physical", + "port": { + "transfer_pump": "outlet", + "flask_waste": "top" + } + }, + { + "source": "transfer_pump", + "target": "flask_rinsing", + "type": "physical", + "port": { + "transfer_pump": "inlet", + "flask_rinsing": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/washsolidteststation.json b/test/experiments/mock_protocol/washsolidteststation.json new file mode 100644 index 0000000..170c825 --- /dev/null +++ b/test/experiments/mock_protocol/washsolidteststation.json @@ -0,0 +1,494 @@ +{ + "nodes": [ + { + "id": "WashSolidTestStation", + "name": "固体清洗测试工作站", + "children": [ + "transfer_pump_1", + "heatchill_1", + "stirrer_1", + "filter_1", + "flask_ethanol", + "flask_water", + "flask_acetone", + "flask_methanol", + "reactor", + "collection_flask", + "waste_flask", + "flask_sample", + "filtrate_flask" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["WashSolidProtocol", "TransferProtocol", "FilterProtocol", "HeatChillProtocol", "StirProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵", + "children": [], + "parent": "WashSolidTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "current_volume": 0.0, + "max_volume": 50.0, + "transfer_rate": 10.0, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + } + }, + { + "id": "heatchill_1", + "name": "加热冷却器", + "children": [], + "parent": "WashSolidTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 150.0, + "min_temp": -20.0 + }, + "data": { + "status": "Idle", + "current_temp": 25.0, + "target_temp": 25.0, + "vessel": "", + "purpose": "", + "progress": 0.0, + "current_status": "Ready" + } + }, + { + "id": "stirrer_1", + "name": "搅拌器", + "children": [], + "parent": "WashSolidTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 750.1111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "filter_1", + "name": "过滤器", + "children": [], + "parent": "WashSolidTestStation", + "type": "device", + "class": "virtual_filter", + "position": { + "x": 850.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle", + "filter_state": "Ready", + "current_temp": 25.0, + "target_temp": 25.0, + "max_temp": 100.0, + "stir_speed": 0.0, + "max_stir_speed": 1000.0, + "filtered_volume": 0.0, + "progress": 0.0, + "message": "" + } + }, + { + "id": "flask_ethanol", + "name": "乙醇清洗剂", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethanol", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_water", + "name": "水清洗剂", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "water", + "volume": 1800.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮清洗剂", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "acetone", + "volume": 800.0, + "concentration": 99.8 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇清洗剂", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "methanol", + "volume": 800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "solid_product", + "volume": 50.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "collection_flask", + "name": "收集瓶", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_flask", + "name": "废液瓶", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 1150, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "crude_solid", + "volume": 30.0, + "concentration": 80.0 + } + ] + } + }, + { + "id": "filtrate_flask", + "name": "滤液收集瓶", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1500.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "transfer_pump_1", + "target": "flask_ethanol", + "type": "physical", + "port": { + "transfer_pump_1": "1", + "flask_ethanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_water", + "type": "physical", + "port": { + "transfer_pump_1": "2", + "flask_water": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_acetone", + "type": "physical", + "port": { + "transfer_pump_1": "3", + "flask_acetone": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_methanol", + "type": "physical", + "port": { + "transfer_pump_1": "4", + "flask_methanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump_1": "5", + "reactor": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "collection_flask", + "type": "physical", + "port": { + "transfer_pump_1": "6", + "collection_flask": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "waste_flask", + "type": "physical", + "port": { + "transfer_pump_1": "7", + "waste_flask": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_sample", + "type": "physical", + "port": { + "transfer_pump_1": "8", + "flask_sample": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "filtrate_flask", + "type": "physical", + "port": { + "transfer_pump_1": "9", + "filtrate_flask": "top" + } + }, + { + "source": "heatchill_1", + "target": "reactor", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "reactor": "bottom" + } + }, + { + "source": "heatchill_1", + "target": "flask_sample", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "flask_sample": "bottom" + } + }, + { + "source": "stirrer_1", + "target": "reactor", + "type": "physical", + "port": { + "stirrer_1": "stir_rod", + "reactor": "center" + } + }, + { + "source": "stirrer_1", + "target": "flask_sample", + "type": "physical", + "port": { + "stirrer_1": "stir_rod", + "flask_sample": "center" + } + }, + { + "source": "filter_1", + "target": "reactor", + "type": "physical", + "port": { + "filter_1": "filter_element", + "reactor": "top" + } + }, + { + "source": "filter_1", + "target": "flask_sample", + "type": "physical", + "port": { + "filter_1": "filter_element", + "flask_sample": "top" + } + }, + { + "source": "filter_1", + "target": "filtrate_flask", + "type": "physical", + "port": { + "filter_1": "filter_element", + "filtrate_flask": "top" + } + } + ] +} \ No newline at end of file diff --git a/unilabos/compile/__init__.py b/unilabos/compile/__init__.py index 820f43f..fa61e12 100644 --- a/unilabos/compile/__init__.py +++ b/unilabos/compile/__init__.py @@ -5,6 +5,17 @@ from .separate_protocol import generate_separate_protocol from .evaporate_protocol import generate_evaporate_protocol from .evacuateandrefill_protocol import generate_evacuateandrefill_protocol from .agv_transfer_protocol import generate_agv_transfer_protocol +from .add_protocol import generate_add_protocol +from .centrifuge_protocol import generate_centrifuge_protocol +from .filter_protocol import generate_filter_protocol +from .heatchill_protocol import generate_heat_chill_protocol, generate_heat_chill_start_protocol, generate_heat_chill_stop_protocol +from .stir_protocol import generate_stir_protocol, generate_start_stir_protocol, generate_stop_stir_protocol +from .transfer_protocol import generate_transfer_protocol +from .clean_vessel_protocol import generate_clean_vessel_protocol +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 # Define a dictionary of protocol generators. @@ -15,5 +26,19 @@ action_protocol_generators = { EvaporateProtocol: generate_evaporate_protocol, EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol, AGVTransferProtocol: generate_agv_transfer_protocol, + CentrifugeProtocol: generate_centrifuge_protocol, + AddProtocol: generate_add_protocol, + FilterProtocol: generate_filter_protocol, + HeatChillProtocol: generate_heat_chill_protocol, + HeatChillStartProtocol: generate_heat_chill_start_protocol, + HeatChillStopProtocol: generate_heat_chill_stop_protocol, + StirProtocol: generate_stir_protocol, + StartStirProtocol: generate_start_stir_protocol, + StopStirProtocol: generate_stop_stir_protocol, + TransferProtocol: generate_transfer_protocol, + CleanVesselProtocol: generate_clean_vessel_protocol, + DissolveProtocol: generate_dissolve_protocol, + FilterThroughProtocol: generate_filter_through_protocol, + RunColumnProtocol: generate_run_column_protocol, + WashSolidProtocol: generate_wash_solid_protocol, } -# End Protocols diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py new file mode 100644 index 0000000..e2cdc3c --- /dev/null +++ b/unilabos/compile/add_protocol.py @@ -0,0 +1,74 @@ +import networkx as nx +from typing import List, Dict, Any + +def generate_add_protocol( + G: nx.DiGraph, + vessel: str, + reagent: str, + volume: float, + mass: float, + amount: str, + time: float, + stir: bool, + stir_speed: float, + viscous: bool, + purpose: str +) -> List[Dict[str, Any]]: + """ + 生成添加试剂的协议序列 - 严格按照 Add.action + """ + action_sequence = [] + + # 如果指定了体积,执行液体转移 + if volume > 0: + # 查找可用的试剂瓶 + available_flasks = [node for node in G.nodes() + if node.startswith('flask_') + and G.nodes[node].get('type') == 'container'] + + if not available_flasks: + raise ValueError("没有找到可用的试剂容器") + + reagent_vessel = available_flasks[0] + + # 查找泵设备 + pump_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_pump'] + + if pump_nodes: + pump_id = pump_nodes[0] + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": reagent_vessel, + "to_vessel": vessel, + "volume": volume, + "amount": amount, + "time": time, + "viscous": viscous, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 如果需要搅拌,使用 StartStir 而不是 Stir + if stir: + stirrer_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_stirrer'] + + if stirrer_nodes: + stirrer_id = stirrer_nodes[0] + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", # 使用 start_stir 而不是 stir + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"添加 {reagent} 后搅拌" + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/centrifuge_protocol.py b/unilabos/compile/centrifuge_protocol.py new file mode 100644 index 0000000..e55644d --- /dev/null +++ b/unilabos/compile/centrifuge_protocol.py @@ -0,0 +1,123 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + speed: float, + time: float, + temp: float = 25.0 +) -> List[Dict[str, Any]]: + """ + 生成离心操作的协议序列 + + Args: + G: 有向图,节点为设备和容器 + vessel: 离心容器名称 + speed: 离心速度 (rpm) + time: 离心时间 (秒) + temp: 温度 (摄氏度,可选) + + Returns: + List[Dict[str, Any]]: 离心操作的动作序列 + + Raises: + ValueError: 当找不到离心机设备时抛出异常 + + Examples: + centrifuge_protocol = generate_centrifuge_protocol(G, "reactor", 5000, 300, 4.0) + """ + action_sequence = [] + + # 查找离心机设备 + centrifuge_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_centrifuge'] + + if not centrifuge_nodes: + raise ValueError("没有找到可用的离心机设备") + + # 使用第一个可用的离心机 + centrifuge_id = centrifuge_nodes[0] + + # 验证容器是否存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 {vessel} 不存在于图中") + + # 执行离心操作 + action_sequence.append({ + "device_id": centrifuge_id, + "action_name": "centrifuge", + "action_kwargs": { + "vessel": vessel, + "speed": speed, + "time": time, + "temp": temp + } + }) + + return action_sequence + + +def generate_multi_step_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + steps: List[Dict[str, Any]] +) -> List[Dict[str, Any]]: + """ + 生成多步骤离心操作的协议序列 + + Args: + G: 有向图,节点为设备和容器 + vessel: 离心容器名称 + steps: 离心步骤列表,每个步骤包含 speed, time, temp 参数 + + Returns: + List[Dict[str, Any]]: 多步骤离心操作的动作序列 + + Examples: + steps = [ + {"speed": 1000, "time": 60, "temp": 4.0}, # 低速预离心 + {"speed": 12000, "time": 600, "temp": 4.0} # 高速离心 + ] + protocol = generate_multi_step_centrifuge_protocol(G, "reactor", steps) + """ + action_sequence = [] + + # 查找离心机设备 + centrifuge_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_centrifuge'] + + if not centrifuge_nodes: + raise ValueError("没有找到可用的离心机设备") + + centrifuge_id = centrifuge_nodes[0] + + # 验证容器是否存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 {vessel} 不存在于图中") + + # 执行每个离心步骤 + for i, step in enumerate(steps): + speed = step.get('speed', 5000) + time = step.get('time', 300) + temp = step.get('temp', 25.0) + + action_sequence.append({ + "device_id": centrifuge_id, + "action_name": "centrifuge", + "action_kwargs": { + "vessel": vessel, + "speed": speed, + "time": time, + "temp": temp + } + }) + + # 步骤间等待时间(除了最后一步) + if i < len(steps) - 1: + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 3} + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/clean_vessel_protocol.py b/unilabos/compile/clean_vessel_protocol.py new file mode 100644 index 0000000..d8a746f --- /dev/null +++ b/unilabos/compile/clean_vessel_protocol.py @@ -0,0 +1,126 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_clean_vessel_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + temp: float, + repeats: int = 1 +) -> List[Dict[str, Any]]: + """ + 生成容器清洗操作的协议序列,使用transfer操作实现清洗 + + Args: + G: 有向图,节点为设备和容器 + vessel: 要清洗的容器名称 + solvent: 用于清洗容器的溶剂名称 + volume: 清洗溶剂的体积 + temp: 清洗时的温度 + repeats: 清洗操作的重复次数,默认为 1 + + Returns: + List[Dict[str, Any]]: 容器清洗操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备时抛出异常 + + Examples: + clean_vessel_protocol = generate_clean_vessel_protocol(G, "reactor", "water", 50.0, 25.0, 2) + """ + action_sequence = [] + + # 查找虚拟转移泵设备进行清洗操作 + pump_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_transfer_pump'] + + if not pump_nodes: + raise ValueError("没有找到可用的转移泵设备进行容器清洗") + + pump_id = pump_nodes[0] + + # 验证容器是否存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 {vessel} 不存在于图中") + + # 查找溶剂容器 + solvent_vessel = f"flask_{solvent}" + if solvent_vessel not in G.nodes(): + raise ValueError(f"溶剂容器 {solvent_vessel} 不存在于图中") + + # 查找废液容器 + waste_vessel = "flask_waste" + if waste_vessel not in G.nodes(): + raise ValueError(f"废液容器 {waste_vessel} 不存在于图中") + + # 查找加热设备(如果需要加热) + heatchill_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_heatchill'] + + heatchill_id = heatchill_nodes[0] if heatchill_nodes else None + + # 执行清洗操作序列 + for repeat in range(repeats): + # 1. 如果需要加热,先设置温度 + if temp > 25.0 and heatchill_id: + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "purpose": "cleaning" + } + }) + + # 2. 使用transfer操作:从溶剂容器转移清洗溶剂到目标容器 + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": solvent_vessel, + "to_vessel": vessel, + "volume": volume, + "amount": f"cleaning with {solvent} - cycle {repeat + 1}", + "time": 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 3. 等待清洗作用时间(可选,可以添加wait操作) + # 这里省略wait操作,直接进行下一步 + + # 4. 将清洗后的溶剂转移到废液容器 + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": vessel, + "to_vessel": waste_vessel, + "volume": volume, + "amount": f"waste from cleaning {vessel} - cycle {repeat + 1}", + "time": 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 5. 如果加热了,停止加热 + if temp > 25.0 and heatchill_id: + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": vessel + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/dissolve_protocol.py b/unilabos/compile/dissolve_protocol.py new file mode 100644 index 0000000..eda88cd --- /dev/null +++ b/unilabos/compile/dissolve_protocol.py @@ -0,0 +1,162 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_dissolve_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + amount: str = "", + temp: float = 25.0, + time: float = 0.0, + stir_speed: float = 0.0 +) -> List[Dict[str, Any]]: + """ + 生成溶解操作的协议序列 + + Args: + G: 有向图,节点为设备和容器 + vessel: 装有要溶解物质的容器名称 + solvent: 用于溶解物质的溶剂名称 + volume: 溶剂的体积,可选参数 + amount: 要溶解物质的量,可选参数 + temp: 溶解时的温度,可选参数 + time: 溶解的时间,可选参数 + stir_speed: 搅拌速度,可选参数 + + Returns: + List[Dict[str, Any]]: 溶解操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备时抛出异常 + + Examples: + dissolve_protocol = generate_dissolve_protocol(G, "reactor", "water", 100.0, "NaCl 5g", 60.0, 300.0, 500.0) + """ + action_sequence = [] + + # 验证容器是否存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 {vessel} 不存在于图中") + + # 查找溶剂容器 + solvent_vessel = f"flask_{solvent}" + if solvent_vessel not in G.nodes(): + # 如果没有找到特定溶剂容器,查找可用的源容器 + available_vessels = [node for node in G.nodes() + if node.startswith('flask_') and + G.nodes[node].get('type') == 'container'] + if available_vessels: + solvent_vessel = available_vessels[0] + else: + raise ValueError(f"没有找到溶剂容器 {solvent}") + + # 查找转移泵设备 + pump_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_transfer_pump'] + + if not pump_nodes: + raise ValueError("没有找到可用的转移泵设备") + + pump_id = pump_nodes[0] + + # 查找加热设备(如果需要加热) + heatchill_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_heatchill'] + + heatchill_id = heatchill_nodes[0] if heatchill_nodes else None + + # 查找搅拌设备(如果需要搅拌) + stirrer_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_stirrer'] + + stirrer_id = stirrer_nodes[0] if stirrer_nodes else None + + # 步骤1:如果需要加热,先设置温度 + if temp > 25.0 and heatchill_id: + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "purpose": "dissolution" + } + }) + + # 步骤2:添加溶剂到容器中 + if volume > 0: + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": solvent_vessel, + "to_vessel": vessel, + "volume": volume, + "amount": f"solvent {solvent} for dissolving {amount}", + "time": 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 步骤3:如果需要搅拌,开始搅拌 + if stir_speed > 0 and stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"dissolving {amount} in {solvent}" + } + }) + + # 步骤4:如果指定了溶解时间,等待溶解完成 + if time > 0: + # 这里可以添加等待操作,或者使用搅拌操作来模拟溶解时间 + if stirrer_id and stir_speed > 0: + # 停止之前的搅拌,使用定时搅拌 + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stop_stir", + "action_kwargs": { + "vessel": vessel + } + }) + + # 开始定时搅拌 + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stir", + "action_kwargs": { + "stir_time": time, + "stir_speed": stir_speed, + "settling_time": 10.0 # 搅拌后静置10秒 + } + }) + + # 步骤5:如果加热了,停止加热 + if temp > 25.0 and heatchill_id: + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": vessel + } + }) + + # 步骤6:如果还在搅拌,停止搅拌(除非已经用定时搅拌) + if stir_speed > 0 and stirrer_id and time == 0: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stop_stir", + "action_kwargs": { + "vessel": vessel + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py index 9cde400..66e3ca3 100644 --- a/unilabos/compile/evacuateandrefill_protocol.py +++ b/unilabos/compile/evacuateandrefill_protocol.py @@ -69,14 +69,14 @@ def generate_evacuateandrefill_protocol( "device_id": vacuum_backbone["pump"], "action_name": "set_status", "action_kwargs": { - "command": "ON" + "string": "ON" } }, { "device_id": vacuum_backbone["gas"], "action_name": "set_status", "action_kwargs": { - "command": "OFF" + "string": "OFF" } } ]) @@ -106,14 +106,14 @@ def generate_evacuateandrefill_protocol( "device_id": vacuum_backbone["pump"], "action_name": "set_status", "action_kwargs": { - "command": "OFF" + "string": "OFF" } }, { "device_id": vacuum_backbone["gas"], "action_name": "set_status", "action_kwargs": { - "command": "ON" + "string": "ON" } } ]) @@ -125,7 +125,7 @@ def generate_evacuateandrefill_protocol( "device_id": vacuum_backbone["gas"], "action_name": "set_status", "action_kwargs": { - "command": "OFF" + "string": "OFF" } } ) diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py new file mode 100644 index 0000000..2847c5d --- /dev/null +++ b/unilabos/compile/filter_protocol.py @@ -0,0 +1,70 @@ +from typing import List, Dict, Any +import networkx as nx + +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 +) -> List[Dict[str, Any]]: + """ + 生成过滤操作的协议序列 + + Args: + G: 有向图,节点为设备和容器 + vessel: 过滤容器 + filtrate_vessel: 滤液容器(可选) + stir: 是否搅拌 + stir_speed: 搅拌速度(可选) + temp: 温度(可选,摄氏度) + continue_heatchill: 是否继续加热冷却 + volume: 过滤体积(可选) + + Returns: + List[Dict[str, Any]]: 过滤操作的动作序列 + + Raises: + ValueError: 当找不到过滤设备时抛出异常 + + Examples: + filter_protocol = generate_filter_protocol(G, "reactor", "filtrate_vessel", stir=True, volume=100.0) + """ + action_sequence = [] + + # 查找过滤设备 + filter_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_filter'] + + if not filter_nodes: + raise ValueError("没有找到可用的过滤设备") + + # 使用第一个可用的过滤器 + filter_id = filter_nodes[0] + + # 验证容器是否存在 + if vessel not in G.nodes(): + raise ValueError(f"过滤容器 {vessel} 不存在于图中") + + if filtrate_vessel and filtrate_vessel not in G.nodes(): + raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中") + + # 执行过滤操作 + action_sequence.append({ + "device_id": filter_id, + "action_name": "filter_sample", + "action_kwargs": { + "vessel": vessel, + "filtrate_vessel": filtrate_vessel, + "stir": stir, + "stir_speed": stir_speed, + "temp": temp, + "continue_heatchill": continue_heatchill, + "volume": volume + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/filter_through_protocol.py b/unilabos/compile/filter_through_protocol.py new file mode 100644 index 0000000..009756b --- /dev/null +++ b/unilabos/compile/filter_through_protocol.py @@ -0,0 +1,150 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_filter_through_protocol( + G: nx.DiGraph, + 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 +) -> List[Dict[str, Any]]: + """ + 生成通过过滤介质过滤的协议序列 + + Args: + G: 有向图,节点为设备和容器 + from_vessel: 源容器的名称,即物质起始所在的容器 + to_vessel: 目标容器的名称,物质过滤后要到达的容器 + filter_through: 过滤时所通过的介质,如滤纸、柱子等 + eluting_solvent: 洗脱溶剂的名称,可选参数 + eluting_volume: 洗脱溶剂的体积,可选参数 + eluting_repeats: 洗脱操作的重复次数,默认为 0 + residence_time: 物质在过滤介质中的停留时间,可选参数 + + Returns: + List[Dict[str, Any]]: 过滤操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备时抛出异常 + + Examples: + filter_through_protocol = generate_filter_through_protocol( + G, "reactor", "collection_flask", "celite", "ethanol", 50.0, 2, 60.0 + ) + """ + action_sequence = [] + + # 验证容器是否存在 + if from_vessel not in G.nodes(): + raise ValueError(f"源容器 {from_vessel} 不存在于图中") + + if to_vessel not in G.nodes(): + raise ValueError(f"目标容器 {to_vessel} 不存在于图中") + + # 查找转移泵设备(用于液体转移) + pump_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_transfer_pump'] + + if not pump_nodes: + raise ValueError("没有找到可用的转移泵设备") + + pump_id = pump_nodes[0] + + # 查找过滤设备(可选,如果有专门的过滤设备) + filter_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_filter'] + + filter_id = filter_nodes[0] if filter_nodes else None + + # 查找洗脱溶剂容器(如果需要洗脱) + eluting_vessel = None + if eluting_solvent and eluting_volume > 0: + eluting_vessel = f"flask_{eluting_solvent}" + if eluting_vessel not in G.nodes(): + # 查找可用的溶剂容器 + available_vessels = [node for node in G.nodes() + if node.startswith('flask_') and + G.nodes[node].get('type') == 'container'] + if available_vessels: + eluting_vessel = available_vessels[0] + else: + raise ValueError(f"没有找到洗脱溶剂容器 {eluting_solvent}") + + # 步骤1:将样品从源容器转移到过滤装置(模拟通过过滤介质) + # 这里我们将过滤过程分解为多个转移步骤来模拟通过介质的过程 + + # 首先转移样品(模拟样品通过过滤介质) + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": from_vessel, + "to_vessel": to_vessel, + "volume": 0.0, # 转移所有液体,体积由系统确定 + "amount": f"通过 {filter_through} 过滤", + "time": residence_time if residence_time > 0 else 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": True # 通过过滤介质可能涉及固体分离 + } + }) + + # 步骤2:如果有专门的过滤设备,使用过滤设备处理 + if filter_id: + action_sequence.append({ + "device_id": filter_id, + "action_name": "filter_sample", + "action_kwargs": { + "vessel": to_vessel, + "filtrate_vessel": to_vessel, + "stir": False, + "stir_speed": 0.0, + "temp": 25.0, + "continue_heatchill": False, + "volume": 0.0 + } + }) + + # 步骤3:洗脱操作(如果指定了洗脱溶剂和重复次数) + if eluting_solvent and eluting_volume > 0 and eluting_repeats > 0 and eluting_vessel: + for repeat in range(eluting_repeats): + # 添加洗脱溶剂 + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": eluting_vessel, + "to_vessel": to_vessel, + "volume": eluting_volume, + "amount": f"洗脱溶剂 {eluting_solvent} - 第 {repeat + 1} 次", + "time": 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 如果有过滤设备,再次过滤洗脱液 + if filter_id: + action_sequence.append({ + "device_id": filter_id, + "action_name": "filter_sample", + "action_kwargs": { + "vessel": to_vessel, + "filtrate_vessel": to_vessel, + "stir": False, + "stir_speed": 0.0, + "temp": 25.0, + "continue_heatchill": False, + "volume": eluting_volume + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py new file mode 100644 index 0000000..ac8ca17 --- /dev/null +++ b/unilabos/compile/heatchill_protocol.py @@ -0,0 +1,117 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_heat_chill_protocol( + G: nx.DiGraph, + vessel: str, + temp: float, + time: float, + stir: bool, + stir_speed: float, + purpose: str +) -> List[Dict[str, Any]]: + """ + 生成加热/冷却操作的协议序列 - 严格按照 HeatChill.action + """ + action_sequence = [] + + # 查找加热/冷却设备 + heatchill_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_heatchill'] + + if not heatchill_nodes: + raise ValueError("没有找到可用的加热/冷却设备") + + heatchill_id = heatchill_nodes[0] + + if vessel not in G.nodes(): + raise ValueError(f"容器 {vessel} 不存在于图中") + + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "time": time, + "stir": stir, + "stir_speed": stir_speed, + "purpose": purpose + } + }) + + return action_sequence + + +def generate_heat_chill_start_protocol( + G: nx.DiGraph, + vessel: str, + temp: float, + purpose: str +) -> List[Dict[str, Any]]: + """ + 生成开始加热/冷却操作的协议序列 - 严格按照 HeatChillStart.action + """ + action_sequence = [] + + heatchill_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_heatchill'] + + if not heatchill_nodes: + raise ValueError("没有找到可用的加热/冷却设备") + + heatchill_id = heatchill_nodes[0] + + if vessel not in G.nodes(): + raise ValueError(f"容器 {vessel} 不存在于图中") + + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "purpose": purpose + } + }) + + return action_sequence + + +def generate_heat_chill_stop_protocol( + G: nx.DiGraph, + vessel: str +) -> List[Dict[str, Any]]: + """ + 生成停止加热/冷却操作的协议序列 + + Args: + G: 有向图,节点为设备和容器 + vessel: 容器名称 + + Returns: + List[Dict[str, Any]]: 停止加热/冷却操作的动作序列 + """ + action_sequence = [] + + # 查找加热/冷却设备 + heatchill_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_heatchill'] + + if not heatchill_nodes: + raise ValueError("没有找到可用的加热/冷却设备") + + heatchill_id = heatchill_nodes[0] + + if vessel not in G.nodes(): + raise ValueError(f"容器 {vessel} 不存在于图中") + + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": vessel + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py index 6067028..ffd8efc 100644 --- a/unilabos/compile/pump_protocol.py +++ b/unilabos/compile/pump_protocol.py @@ -24,10 +24,27 @@ def generate_pump_protocol( # 生成泵操作的动作序列 pump_action_sequence = [] - nodes = G.nodes(data=True) - # 从from_vessel到to_vessel的最短路径 - shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) - print(shortest_path) + + # 检查节点是否存在 + if from_vessel not in G.nodes: + print(f"Warning: Source vessel '{from_vessel}' not found in graph. Skipping.") + return [] + + if to_vessel not in G.nodes: + print(f"Warning: Target vessel '{to_vessel}' not found in graph. Skipping.") + return [] + + # 检查是否存在路径 + try: + shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + except nx.NetworkXNoPath: + print(f"Warning: No path from '{from_vessel}' to '{to_vessel}'. Skipping.") + return [] + except nx.NodeNotFound as e: + print(f"Warning: Node not found: {e}. Skipping.") + return [] + + print(f"Shortest path: {shortest_path}") pump_backbone = shortest_path if not from_vessel.startswith("pump"): @@ -35,10 +52,34 @@ def generate_pump_protocol( if not to_vessel.startswith("pump"): pump_backbone = pump_backbone[:-1] + print(f"Pump backbone: {pump_backbone}") + + # 修复:检查pump_backbone是否为空 + if not pump_backbone: + print(f"Warning: No pumps found in path from '{from_vessel}' to '{to_vessel}'. Skipping.") + return [] + if transfer_flowrate == 0: transfer_flowrate = flowrate - min_transfer_volume = min([nodes[pump]["max_volume"] for pump in pump_backbone]) + # 修复:正确访问节点数据 + pump_max_volumes = [] + for pump in pump_backbone: + # 直接使用 G.nodes[pump] 来访问节点数据 + pump_data = G.nodes[pump] if pump in G.nodes else {} + # 尝试多种可能的键名,并提供默认值 + max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume') + if max_vol is None: + # 如果是设备节点,尝试从config中获取 + config = pump_data.get('config', {}) + max_vol = config.get('max_volume', 25.0) + pump_max_volumes.append(float(max_vol)) + + if pump_max_volumes: + min_transfer_volume = min(pump_max_volumes) + else: + min_transfer_volume = 25.0 # 默认值 + repeats = int(np.ceil(volume / min_transfer_volume)) if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")): raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.") @@ -48,84 +89,102 @@ def generate_pump_protocol( # 生成泵操作的动作序列 for i in range(repeats): # 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵 - if not from_vessel.startswith("pump"): - pump_action_sequence.extend([ - { - "device_id": pump_backbone[0], - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]] + if not from_vessel.startswith("pump") and pump_backbone: + # 修复:添加边缘数据检查 + edge_data = G.get_edge_data(pump_backbone[0], from_vessel) + if edge_data and "port" in edge_data: + pump_action_sequence.extend([ + { + "device_id": pump_backbone[0], + "action_name": "set_valve_position", + "action_kwargs": { + "command": edge_data["port"][pump_backbone[0]] + } + }, + { + "device_id": pump_backbone[0], + "action_name": "set_position", + "action_kwargs": { + "position": float(min(volume_left, min_transfer_volume)), + "max_velocity": transfer_flowrate + } } - }, - { - "device_id": pump_backbone[0], - "action_name": "set_position", - "action_kwargs": { - "position": float(min(volume_left, min_transfer_volume)), - "max_velocity": transfer_flowrate - } - } - ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) - for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]): - # 相邻两泵同时切换阀门至连通位置 - pump_action_sequence.append([ - { - "device_id": pumpA, - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(pumpA, pumpB)["port"][pumpA] - } - }, - { - "device_id": pumpB, - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(pumpB, pumpA)["port"][pumpB], - } - } - ]) - # 相邻两泵液体转移:泵A排出液体,泵B吸入液体 - pump_action_sequence.append([ - { - "device_id": pumpA, - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": transfer_flowrate - } - }, - { - "device_id": pumpB, - "action_name": "set_position", - "action_kwargs": { - "position": float(min(volume_left, min_transfer_volume)), - "max_velocity": transfer_flowrate - } - } - ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + else: + print(f"Warning: No edge data found between {pump_backbone[0]} and {from_vessel}") - if not to_vessel.startswith("pump"): + # 修复:检查pump_backbone长度,避免多泵操作时出错 + if len(pump_backbone) > 1: + for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]): + # 相邻两泵同时切换阀门至连通位置 + edge_AB = G.get_edge_data(pumpA, pumpB) + edge_BA = G.get_edge_data(pumpB, pumpA) + + if edge_AB and "port" in edge_AB and edge_BA and "port" in edge_BA: + pump_action_sequence.append([ + { + "device_id": pumpA, + "action_name": "set_valve_position", + "action_kwargs": { + "command": edge_AB["port"][pumpA] + } + }, + { + "device_id": pumpB, + "action_name": "set_valve_position", + "action_kwargs": { + "command": edge_BA["port"][pumpB], + } + } + ]) + # 相邻两泵液体转移:泵A排出液体,泵B吸入液体 + pump_action_sequence.append([ + { + "device_id": pumpA, + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": transfer_flowrate + } + }, + { + "device_id": pumpB, + "action_name": "set_position", + "action_kwargs": { + "position": float(min(volume_left, min_transfer_volume)), + "max_velocity": transfer_flowrate + } + } + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + else: + print(f"Warning: No edge data found between {pumpA} and {pumpB}") + + if not to_vessel.startswith("pump") and pump_backbone: # 单泵依次执行阀指令、活塞指令,将最后一台泵液体缓慢加入容器B - pump_action_sequence.extend([ - { - "device_id": pump_backbone[-1], - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]] + edge_data = G.get_edge_data(pump_backbone[-1], to_vessel) + if edge_data and "port" in edge_data: + pump_action_sequence.extend([ + { + "device_id": pump_backbone[-1], + "action_name": "set_valve_position", + "action_kwargs": { + "command": edge_data["port"][pump_backbone[-1]] + } + }, + { + "device_id": pump_backbone[-1], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": flowrate + } } - }, - { - "device_id": pump_backbone[-1], - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": flowrate - } - } - ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + else: + print(f"Warning: No edge data found between {pump_backbone[-1]} and {to_vessel}") volume_left -= min_transfer_volume return pump_action_sequence @@ -174,18 +233,52 @@ def generate_pump_protocol_with_rinsing( Examples: pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water") """ - air_vessel = "flask_air" - waste_vessel = f"waste_workup" + # 修复:使用实际存在的节点名称 + air_vessel = "flask_air" # 这个在你的配置中存在 - shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) - pump_backbone = shortest_path[1: -1] - nodes = G.nodes(data=True) - min_transfer_volume = float(min([nodes[pump]["max_volume"] for pump in pump_backbone])) + # 寻找合适的废料容器,如果没有找到则使用空的容器作为替代 + waste_vessel = None + available_vessels = [node for node in G.nodes if node.startswith("flask_") and node != air_vessel] + if available_vessels: + # 使用第一个可用的容器作为废料容器 + waste_vessel = available_vessels[0] + print(f"Using {waste_vessel} as waste vessel") + else: + waste_vessel = "flask_1" # 备用选择 + + # 修复:添加路径检查 + try: + shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + pump_backbone = shortest_path[1: -1] + except (nx.NetworkXNoPath, nx.NodeNotFound) as e: + print(f"Warning: Cannot find path from {from_vessel} to {to_vessel}: {e}") + return [] + + # 修复:正确访问节点数据 + pump_max_volumes = [] + for pump in pump_backbone: + # 直接使用 G.nodes[pump] 来访问节点数据 + pump_data = G.nodes[pump] if pump in G.nodes else {} + # 尝试多种可能的键名,并提供默认值 + max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume') + if max_vol is None: + # 如果是设备节点,尝试从config中获取 + config = pump_data.get('config', {}) + max_vol = config.get('max_volume', 25.0) + pump_max_volumes.append(float(max_vol)) + + if pump_max_volumes: + min_transfer_volume = float(min(pump_max_volumes)) + else: + min_transfer_volume = 25.0 # 默认值 + if time != 0: flowrate = transfer_flowrate = volume / time pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate) - if rinsing_solvent != "air": + + # 修复:只在需要清洗且相关节点存在时才执行清洗步骤 + if rinsing_solvent != "air" and pump_backbone: if "," in rinsing_solvent: rinsing_solvents = rinsing_solvent.split(",") assert len(rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats." @@ -194,20 +287,32 @@ def generate_pump_protocol_with_rinsing( for rinsing_solvent in rinsing_solvents: solvent_vessel = f"flask_{rinsing_solvent}" - # 清洗泵 - pump_action_sequence.extend( - generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) + - generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) + - generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate) - ) - # 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。 - if rinsing_solvent == rinsing_solvents[0]: - pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) - pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) + + # 检查溶剂容器是否存在 + if solvent_vessel not in G.nodes: + print(f"Warning: Solvent vessel '{solvent_vessel}' not found in graph. Skipping rinsing step.") + continue + + # 清洗泵 - 只有当所有必需的节点都存在且pump_backbone不为空时才执行 + if pump_backbone and len(pump_backbone) > 0 and waste_vessel in G.nodes: + pump_action_sequence.extend( + generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) + + generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) + + generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate) + ) + + # 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。 + if rinsing_solvent == rinsing_solvents[0]: + pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate)) + pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)) + + pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate)) + pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate)) + + # 最后的空气清洗 - 只有当节点存在时才执行 + if air_vessel in G.nodes: + pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) + pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) return pump_action_sequence # End Protocols diff --git a/unilabos/compile/run_column_protocol.py b/unilabos/compile/run_column_protocol.py new file mode 100644 index 0000000..5aebc2b --- /dev/null +++ b/unilabos/compile/run_column_protocol.py @@ -0,0 +1,102 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_run_column_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + column: str +) -> List[Dict[str, Any]]: + """ + 生成柱层析分离的协议序列 + + Args: + G: 有向图,节点为设备和容器 + from_vessel: 源容器的名称,即样品起始所在的容器 + to_vessel: 目标容器的名称,分离后的样品要到达的容器 + column: 所使用的柱子的名称 + + Returns: + List[Dict[str, Any]]: 柱层析分离操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备时抛出异常 + + Examples: + run_column_protocol = generate_run_column_protocol(G, "reactor", "collection_flask", "silica_column") + """ + action_sequence = [] + + # 验证容器是否存在 + if from_vessel not in G.nodes(): + raise ValueError(f"源容器 {from_vessel} 不存在于图中") + + if to_vessel not in G.nodes(): + raise ValueError(f"目标容器 {to_vessel} 不存在于图中") + + # 查找转移泵设备(用于样品转移) + pump_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_transfer_pump'] + + if not pump_nodes: + raise ValueError("没有找到可用的转移泵设备") + + pump_id = pump_nodes[0] + + # 查找柱层析设备 + column_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_column'] + + if not column_nodes: + raise ValueError("没有找到可用的柱层析设备") + + column_id = column_nodes[0] + + # 步骤1:将样品从源容器转移到柱子上 + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": from_vessel, + "to_vessel": column_id, # 将样品转移到柱子设备 + "volume": 0.0, # 转移所有液体,体积由系统确定 + "amount": f"样品上柱 - 使用 {column}", + "time": 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 步骤2:运行柱层析分离 + action_sequence.append({ + "device_id": column_id, + "action_name": "run_column", + "action_kwargs": { + "from_vessel": from_vessel, + "to_vessel": to_vessel, + "column": column + } + }) + + # 步骤3:将分离后的产物从柱子转移到目标容器 + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": column_id, # 从柱子设备转移 + "to_vessel": to_vessel, + "volume": 0.0, # 转移所有液体,体积由系统确定 + "amount": f"收集分离产物 - 来自 {column}", + "time": 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py new file mode 100644 index 0000000..90a207c --- /dev/null +++ b/unilabos/compile/stir_protocol.py @@ -0,0 +1,137 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_stir_protocol( + G: nx.DiGraph, + stir_time: float, + stir_speed: float, + settling_time: float +) -> List[Dict[str, Any]]: + """ + 生成搅拌操作的协议序列 + + Args: + G: 有向图,节点为设备和容器 + stir_time: 搅拌时间 (秒) + stir_speed: 搅拌速度 (rpm) + settling_time: 沉降时间 (秒) + + Returns: + List[Dict[str, Any]]: 搅拌操作的动作序列 + + Raises: + ValueError: 当找不到搅拌设备时抛出异常 + + Examples: + stir_protocol = generate_stir_protocol(G, 300.0, 500.0, 60.0) + """ + action_sequence = [] + + # 查找搅拌设备 + stirrer_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_stirrer'] + + if not stirrer_nodes: + raise ValueError("没有找到可用的搅拌设备") + + # 使用第一个可用的搅拌器 + stirrer_id = stirrer_nodes[0] + + # 执行搅拌操作 + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stir", + "action_kwargs": { + "stir_time": stir_time, + "stir_speed": stir_speed, + "settling_time": settling_time + } + }) + + return action_sequence + + +def generate_start_stir_protocol( + G: nx.DiGraph, + vessel: str, + stir_speed: float, + purpose: str +) -> List[Dict[str, Any]]: + """ + 生成开始搅拌操作的协议序列 + + Args: + G: 有向图,节点为设备和容器 + vessel: 搅拌容器 + stir_speed: 搅拌速度 (rpm) + purpose: 搅拌目的 + + Returns: + List[Dict[str, Any]]: 开始搅拌操作的动作序列 + """ + action_sequence = [] + + # 查找搅拌设备 + stirrer_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_stirrer'] + + if not stirrer_nodes: + raise ValueError("没有找到可用的搅拌设备") + + stirrer_id = stirrer_nodes[0] + + # 验证容器是否存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 {vessel} 不存在于图中") + + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": purpose + } + }) + + return action_sequence + + +def generate_stop_stir_protocol( + G: nx.DiGraph, + vessel: str +) -> List[Dict[str, Any]]: + """ + 生成停止搅拌操作的协议序列 + + Args: + G: 有向图,节点为设备和容器 + vessel: 搅拌容器 + + Returns: + List[Dict[str, Any]]: 停止搅拌操作的动作序列 + """ + action_sequence = [] + + # 查找搅拌设备 + stirrer_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_stirrer'] + + if not stirrer_nodes: + raise ValueError("没有找到可用的搅拌设备") + + stirrer_id = stirrer_nodes[0] + + # 验证容器是否存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 {vessel} 不存在于图中") + + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stop_stir", + "action_kwargs": { + "vessel": vessel + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/transfer_protocol.py b/unilabos/compile/transfer_protocol.py new file mode 100644 index 0000000..202b009 --- /dev/null +++ b/unilabos/compile/transfer_protocol.py @@ -0,0 +1,79 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_transfer_protocol( + G: nx.DiGraph, + 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 +) -> List[Dict[str, Any]]: + """ + 生成液体转移操作的协议序列 + + Args: + G: 有向图,节点为设备和容器 + from_vessel: 源容器 + to_vessel: 目标容器 + volume: 转移体积 (mL) + amount: 数量描述 (可选) + time: 转移时间 (秒,可选) + viscous: 是否为粘性液体 + rinsing_solvent: 冲洗溶剂 (可选) + rinsing_volume: 冲洗体积 (mL,可选) + rinsing_repeats: 冲洗重复次数 + solid: 是否涉及固体 + + Returns: + List[Dict[str, Any]]: 转移操作的动作序列 + + Raises: + ValueError: 当找不到合适的转移设备时抛出异常 + + Examples: + transfer_protocol = generate_transfer_protocol(G, "flask_1", "reactor", 10.0) + """ + action_sequence = [] + + # 查找虚拟转移泵设备用于液体转移 - 修复:应该查找 virtual_transfer_pump + pump_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_transfer_pump'] + + if not pump_nodes: + raise ValueError("没有找到可用的转移泵设备进行液体转移") + + # 使用第一个可用的泵 + pump_id = pump_nodes[0] + + # 验证容器是否存在 + if from_vessel not in G.nodes(): + raise ValueError(f"源容器 {from_vessel} 不存在于图中") + + if to_vessel not in G.nodes(): + raise ValueError(f"目标容器 {to_vessel} 不存在于图中") + + # 执行液体转移操作 - 参数完全匹配Transfer.action + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": from_vessel, + "to_vessel": to_vessel, + "volume": volume, + "amount": amount, + "time": time, + "viscous": viscous, + "rinsing_solvent": rinsing_solvent, + "rinsing_volume": rinsing_volume, + "rinsing_repeats": rinsing_repeats, + "solid": solid + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py new file mode 100644 index 0000000..a792b8f --- /dev/null +++ b/unilabos/compile/wash_solid_protocol.py @@ -0,0 +1,216 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_wash_solid_protocol( + G: nx.DiGraph, + 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 +) -> List[Dict[str, Any]]: + """ + 生成固体清洗的协议序列 + + Args: + G: 有向图,节点为设备和容器 + vessel: 装有固体物质的容器名称 + solvent: 用于清洗固体的溶剂名称 + volume: 清洗溶剂的体积 + filtrate_vessel: 滤液要收集到的容器名称,可选参数 + temp: 清洗时的温度,可选参数 + stir: 是否在清洗过程中搅拌,默认为 False + stir_speed: 搅拌速度,可选参数 + time: 清洗的时间,可选参数 + repeats: 清洗操作的重复次数,默认为 1 + + Returns: + List[Dict[str, Any]]: 固体清洗操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备时抛出异常 + + Examples: + wash_solid_protocol = generate_wash_solid_protocol( + G, "reactor", "ethanol", 100.0, "waste_flask", 60.0, True, 300.0, 600.0, 3 + ) + """ + action_sequence = [] + + # 验证容器是否存在 + if vessel not in G.nodes(): + raise ValueError(f"固体容器 {vessel} 不存在于图中") + + if filtrate_vessel and filtrate_vessel not in G.nodes(): + raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中") + + # 查找转移泵设备(用于添加溶剂和转移滤液) + pump_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_transfer_pump'] + + if not pump_nodes: + raise ValueError("没有找到可用的转移泵设备") + + pump_id = pump_nodes[0] + + # 查找加热设备(如果需要加热) + heatchill_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_heatchill'] + + heatchill_id = heatchill_nodes[0] if heatchill_nodes else None + + # 查找搅拌设备(如果需要搅拌) + stirrer_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_stirrer'] + + stirrer_id = stirrer_nodes[0] if stirrer_nodes else None + + # 查找过滤设备(用于分离固体和滤液) + filter_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_filter'] + + filter_id = filter_nodes[0] if filter_nodes else None + + # 查找溶剂容器 + solvent_vessel = f"flask_{solvent}" + if solvent_vessel not in G.nodes(): + # 如果没有找到特定溶剂容器,查找可用的源容器 + available_vessels = [node for node in G.nodes() + if node.startswith('flask_') and + G.nodes[node].get('type') == 'container'] + if available_vessels: + solvent_vessel = available_vessels[0] + else: + raise ValueError(f"没有找到溶剂容器 {solvent}") + + # 如果没有指定滤液容器,使用废液容器 + if not filtrate_vessel: + waste_vessels = [node for node in G.nodes() + if 'waste' in node.lower() and + G.nodes[node].get('type') == 'container'] + filtrate_vessel = waste_vessels[0] if waste_vessels else "waste_flask" + + # 重复清洗操作 + for repeat in range(repeats): + repeat_num = repeat + 1 + + # 步骤1:如果需要加热,先设置温度 + if temp > 25.0 and heatchill_id: + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "purpose": f"固体清洗 - 第 {repeat_num} 次" + } + }) + + # 步骤2:添加清洗溶剂到固体容器 + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": solvent_vessel, + "to_vessel": vessel, + "volume": volume, + "amount": f"清洗溶剂 {solvent} - 第 {repeat_num} 次", + "time": 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 步骤3:如果需要搅拌,开始搅拌 + if stir and stir_speed > 0 and stirrer_id: + if time > 0: + # 定时搅拌 + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stir", + "action_kwargs": { + "stir_time": time, + "stir_speed": stir_speed, + "settling_time": 30.0 # 搅拌后静置30秒 + } + }) + else: + # 开始搅拌(需要手动停止) + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"固体清洗搅拌 - 第 {repeat_num} 次" + } + }) + + # 步骤4:如果指定了清洗时间但没有搅拌,等待清洗时间 + if time > 0 and (not stir or stir_speed == 0): + # 这里可以添加等待操作,暂时跳过 + pass + + # 步骤5:如果有搅拌且没有定时,停止搅拌 + if stir and stir_speed > 0 and time == 0 and stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stop_stir", + "action_kwargs": { + "vessel": vessel + } + }) + + # 步骤6:过滤分离固体和滤液 + if filter_id: + action_sequence.append({ + "device_id": filter_id, + "action_name": "filter_sample", + "action_kwargs": { + "vessel": vessel, + "filtrate_vessel": filtrate_vessel, + "stir": False, + "stir_speed": 0.0, + "temp": temp, + "continue_heatchill": temp > 25.0, + "volume": volume + } + }) + else: + # 没有专门的过滤设备,使用转移泵模拟过滤过程 + # 将滤液转移到滤液容器 + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": vessel, + "to_vessel": filtrate_vessel, + "volume": volume, + "amount": f"转移滤液 - 第 {repeat_num} 次清洗", + "time": 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 步骤7:如果加热了,停止加热(在最后一次清洗后) + if temp > 25.0 and heatchill_id and repeat_num == repeats: + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": vessel + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/devices/mock/__init__.py b/unilabos/devices/mock/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unilabos/devices/mock/mock_chiller.py b/unilabos/devices/mock/mock_chiller.py new file mode 100644 index 0000000..fbb823c --- /dev/null +++ b/unilabos/devices/mock/mock_chiller.py @@ -0,0 +1,177 @@ +import time +import threading + + +class MockChiller: + def __init__(self, port: str = "MOCK"): + self.port = port + self._current_temperature: float = 25.0 # 室温开始 + self._target_temperature: float = 25.0 + self._status: str = "Idle" + self._is_cooling: bool = False + self._is_heating: bool = False + self._vessel = "Unknown" + self._purpose = "Unknown" + + # 模拟温度变化的线程 + self._temperature_thread = None + self._running = True + self._temperature_thread = threading.Thread(target=self._temperature_control_loop) + self._temperature_thread.daemon = True + self._temperature_thread.start() + + @property + def current_temperature(self) -> float: + """当前温度 - 会被自动识别的设备属性""" + return self._current_temperature + + @property + def target_temperature(self) -> float: + """目标温度""" + return self._target_temperature + + @property + def status(self) -> str: + """设备状态 - 会被自动识别的设备属性""" + return self._status + + @property + def is_cooling(self) -> bool: + """是否正在冷却""" + return self._is_cooling + + @property + def is_heating(self) -> bool: + """是否正在加热""" + return self._is_heating + + @property + def vessel(self) -> str: + """当前操作的容器名称""" + return self._vessel + + @property + def purpose(self) -> str: + """当前操作目的""" + return self._purpose + + def heat_chill_start(self, vessel: str, temp: float, purpose: str): + """设置目标温度并记录容器和目的""" + self._vessel = str(vessel) + self._purpose = str(purpose) + self._target_temperature = float(temp) + + diff = self._target_temperature - self._current_temperature + if abs(diff) < 0.1: + self._status = "At Target Temperature" + self._is_cooling = False + self._is_heating = False + elif diff < 0: + self._status = "Cooling" + self._is_cooling = True + self._is_heating = False + else: + self._status = "Heating" + self._is_heating = True + self._is_cooling = False + + self._start_temperature_control() + return True + + def heat_chill_stop(self, vessel: str): + """停止加热/制冷""" + if vessel != self._vessel: + return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"} + + # 停止温度控制线程,锁定当前温度 + self._stop_temperature_control() + + # 更新状态 + self._status = "Stopped" + self._is_cooling = False + self._is_heating = False + + # 重新启动线程但保持温度 + self._running = True + self._temperature_thread = threading.Thread(target=self._temperature_control_loop) + self._temperature_thread.daemon = True + self._temperature_thread.start() + + return {"success": True, "status": self._status} + + def _start_temperature_control(self): + """启动温度控制线程""" + self._running = True + if self._temperature_thread is None or not self._temperature_thread.is_alive(): + self._temperature_thread = threading.Thread(target=self._temperature_control_loop) + self._temperature_thread.daemon = True + self._temperature_thread.start() + + def _stop_temperature_control(self): + """停止温度控制""" + self._running = False + if self._temperature_thread: + self._temperature_thread.join(timeout=1.0) + + def _temperature_control_loop(self): + """温度控制循环 - 模拟真实冷却器的温度变化""" + while self._running: + # 如果状态是 Stopped,不改变温度 + if self._status == "Stopped": + time.sleep(1.0) + continue + + temp_diff = self._target_temperature - self._current_temperature + + if abs(temp_diff) < 0.1: + self._status = "At Target Temperature" + self._is_cooling = False + self._is_heating = False + elif temp_diff < 0: + self._status = "Cooling" + self._is_cooling = True + self._is_heating = False + self._current_temperature -= 0.5 + else: + self._status = "Heating" + self._is_heating = True + self._is_cooling = False + self._current_temperature += 0.3 + + time.sleep(1.0) + + def emergency_stop(self): + """紧急停止""" + self._status = "Emergency Stop" + self._stop_temperature_control() + self._is_cooling = False + self._is_heating = False + + def get_status_info(self) -> dict: + """获取完整状态信息""" + return { + "current_temperature": self._current_temperature, + "target_temperature": self._target_temperature, + "status": self._status, + "is_cooling": self._is_cooling, + "is_heating": self._is_heating, + "vessel": self._vessel, + "purpose": self._purpose, + } + + +# 用于测试的主函数 +if __name__ == "__main__": + chiller = MockChiller() + + # 测试基本功能 + print("启动冷却器测试...") + print(f"初始状态: {chiller.get_status_info()}") + + # 模拟运行10秒 + for i in range(10): + time.sleep(1) + print(f"第{i+1}秒: 当前温度={chiller.current_temperature:.1f}°C, 状态={chiller.status}") + + chiller.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/mock/mock_filter.py b/unilabos/devices/mock/mock_filter.py new file mode 100644 index 0000000..f54e41e --- /dev/null +++ b/unilabos/devices/mock/mock_filter.py @@ -0,0 +1,235 @@ +import time +import threading + + +class MockFilter: + def __init__(self, port: str = "MOCK"): + # 基本参数初始化 + self.port = port + self._status: str = "Idle" + self._is_filtering: bool = False + + # 过滤性能参数 + self._flow_rate: float = 1.0 # 流速(L/min) + self._pressure_drop: float = 0.0 # 压降(Pa) + self._filter_life: float = 100.0 # 滤芯寿命(%) + + # 过滤操作参数 + self._vessel: str = "" # 源容器 + self._filtrate_vessel: str = "" # 目标容器 + self._stir: bool = False # 是否搅拌 + self._stir_speed: float = 0.0 # 搅拌速度 + self._temperature: float = 25.0 # 温度(℃) + self._continue_heatchill: bool = False # 是否继续加热/制冷 + self._target_volume: float = 0.0 # 目标过滤体积(L) + self._filtered_volume: float = 0.0 # 已过滤体积(L) + self._progress: float = 0.0 # 过滤进度(%) + + # 线程控制 + self._filter_thread = None + self._running = False + + @property + def status(self) -> str: + return self._status + + @property + def is_filtering(self) -> bool: + return self._is_filtering + + @property + def flow_rate(self) -> float: + return self._flow_rate + + @property + def pressure_drop(self) -> float: + return self._pressure_drop + + @property + def filter_life(self) -> float: + return self._filter_life + # 新增 property + @property + def vessel(self) -> str: + return self._vessel + + @property + def filtrate_vessel(self) -> str: + return self._filtrate_vessel + + @property + def filtered_volume(self) -> float: + return self._filtered_volume + + @property + def progress(self) -> float: + return self._progress + + @property + def stir(self) -> bool: + return self._stir + + @property + def stir_speed(self) -> float: + return self._stir_speed + + @property + def temperature(self) -> float: + return self._temperature + + @property + def continue_heatchill(self) -> bool: + return self._continue_heatchill + + @property + def target_volume(self) -> float: + return self._target_volume + + def filter(self, vessel: str, filtrate_vessel: str, stir: bool = False, stir_speed: float = 0.0, temp: float = 25.0, continue_heatchill: bool = False, volume: float = 0.0) -> dict: + """新的过滤操作""" + # 停止任何正在进行的过滤 + if self._is_filtering: + self.stop_filtering() + # 验证参数 + if volume <= 0: + return {"success": False, "message": "Target volume must be greater than 0"} + # 设置新的过滤参数 + self._vessel = vessel + self._filtrate_vessel = filtrate_vessel + self._stir = stir + self._stir_speed = stir_speed + self._temperature = temp + self._continue_heatchill = continue_heatchill + self._target_volume = volume + # 重置过滤状态 + self._filtered_volume = 0.0 + self._progress = 0.0 + self._status = "Starting Filter" + # 启动过滤过程 + self._flow_rate = 1.0 # 设置默认流速 + self._start_filter_process() + + return {"success": True, "message": "Filter started"} + + def stop_filtering(self): + """停止过滤""" + self._status = "Stopping Filter" + self._stop_filter_process() + self._flow_rate = 0.0 + self._is_filtering = False + self._status = "Stopped" + return True + + def replace_filter(self): + """更换滤芯""" + self._filter_life = 100.0 + self._status = "Filter Replaced" + return True + + def _start_filter_process(self): + """启动过滤过程线程""" + if not self._running: + self._running = True + self._is_filtering = True + self._filter_thread = threading.Thread(target=self._filter_loop) + self._filter_thread.daemon = True + self._filter_thread.start() + + def _stop_filter_process(self): + """停止过滤过程""" + self._running = False + if self._filter_thread: + self._filter_thread.join(timeout=1.0) + + def _filter_loop(self): + """过滤进程主循环""" + update_interval = 1.0 # 更新间隔(秒) + + while self._running and self._is_filtering: + try: + self._status = "Filtering" + + # 计算这一秒过滤的体积 (L/min -> L/s) + volume_increment = (self._flow_rate / 60.0) * update_interval + + # 更新已过滤体积 + self._filtered_volume += volume_increment + + # 更新进度 (避免除零错误) + if self._target_volume > 0: + self._progress = min(100.0, (self._filtered_volume / self._target_volume) * 100.0) + + # 更新滤芯寿命 (每过滤1L减少0.5%寿命) + self._filter_life = max(0.0, self._filter_life - (volume_increment * 0.5)) + + # 更新压降 (根据滤芯寿命和流速动态计算) + life_factor = self._filter_life / 100.0 # 将寿命转换为0-1的因子 + flow_factor = self._flow_rate / 2.0 # 将流速标准化(假设2L/min是标准流速) + base_pressure = 100.0 # 基础压降 + # 压降随滤芯寿命降低而增加,随流速增加而增加 + self._pressure_drop = base_pressure * (2 - life_factor) * flow_factor + + # 检查是否完成目标体积 + if self._target_volume > 0 and self._filtered_volume >= self._target_volume: + self._status = "Completed" + self._progress = 100.0 + self.stop_filtering() + break + + # 检查滤芯寿命 + if self._filter_life <= 10.0: + self._status = "Filter Needs Replacement" + + time.sleep(update_interval) + + except Exception as e: + print(f"Error in filter loop: {e}") + self.emergency_stop() + break + + def emergency_stop(self): + """紧急停止""" + self._status = "Emergency Stop" + self._stop_filter_process() + self._is_filtering = False + self._flow_rate = 0.0 + + def get_status_info(self) -> dict: + """扩展的状态信息""" + return { + "status": self._status, + "is_filtering": self._is_filtering, + "flow_rate": self._flow_rate, + "pressure_drop": self._pressure_drop, + "filter_life": self._filter_life, + "vessel": self._vessel, + "filtrate_vessel": self._filtrate_vessel, + "filtered_volume": self._filtered_volume, + "target_volume": self._target_volume, + "progress": self._progress, + "temperature": self._temperature, + "stir": self._stir, + "stir_speed": self._stir_speed + } + + +# 用于测试的主函数 +if __name__ == "__main__": + filter_device = MockFilter() + + # 测试基本功能 + print("启动过滤器测试...") + print(f"初始状态: {filter_device.get_status_info()}") + + + + # 模拟运行10秒 + for i in range(10): + time.sleep(1) + print( + f"第{i+1}秒: " + f"寿命={filter_device.filter_life:.1f}%, 状态={filter_device.status}" + ) + + filter_device.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/mock/mock_heater.py b/unilabos/devices/mock/mock_heater.py new file mode 100644 index 0000000..47dd8d8 --- /dev/null +++ b/unilabos/devices/mock/mock_heater.py @@ -0,0 +1,247 @@ +import time +import threading + +class MockHeater: + def __init__(self, port: str = "MOCK"): + self.port = port + self._current_temperature: float = 25.0 # 室温开始 + self._target_temperature: float = 25.0 + self._status: str = "Idle" + self._is_heating: bool = False + self._heating_power: float = 0.0 # 加热功率百分比 0-100 + self._max_temperature: float = 300.0 # 最大加热温度 + + # 新增加的属性 + self._vessel: str = "Unknown" + self._purpose: str = "Unknown" + self._stir: bool = False + self._stir_speed: float = 0.0 + + # 模拟加热过程的线程 + self._heating_thread = None + self._running = True + self._heating_thread = threading.Thread(target=self._heating_control_loop) + self._heating_thread.daemon = True + self._heating_thread.start() + + @property + def current_temperature(self) -> float: + """当前温度 - 会被自动识别的设备属性""" + return self._current_temperature + + @property + def target_temperature(self) -> float: + """目标温度""" + return self._target_temperature + + @property + def status(self) -> str: + """设备状态 - 会被自动识别的设备属性""" + return self._status + + @property + def is_heating(self) -> bool: + """是否正在加热""" + return self._is_heating + + @property + def heating_power(self) -> float: + """加热功率百分比""" + return self._heating_power + + @property + def max_temperature(self) -> float: + """最大加热温度""" + return self._max_temperature + + @property + def vessel(self) -> str: + """当前操作的容器名称""" + return self._vessel + + @property + def purpose(self) -> str: + """操作目的""" + return self._purpose + + @property + def stir(self) -> bool: + """是否搅拌""" + return self._stir + + @property + def stir_speed(self) -> float: + """搅拌速度""" + return self._stir_speed + + def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> dict: + """开始加热/制冷过程""" + self._vessel = str(vessel) + self._purpose = str(purpose) + self._target_temperature = float(temp) + + diff = self._target_temperature - self._current_temperature + if abs(diff) < 0.1: + self._status = "At Target Temperature" + self._is_heating = False + elif diff > 0: + self._status = "Heating" + self._is_heating = True + else: + self._status = "Cooling Down" + self._is_heating = False + + return {"success": True, "status": self._status} + + def heat_chill_stop(self, vessel: str) -> dict: + """停止加热/制冷""" + if vessel != self._vessel: + return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"} + + self._status = "Stopped" + self._is_heating = False + self._heating_power = 0.0 + + return {"success": True, "status": self._status} + + def heat_chill(self, vessel: str, temp: float, time: float, + stir: bool = False, stir_speed: float = 0.0, + purpose: str = "Unknown") -> dict: + """完整的加热/制冷控制""" + self._vessel = str(vessel) + self._target_temperature = float(temp) + self._purpose = str(purpose) + self._stir = stir + self._stir_speed = stir_speed + + diff = self._target_temperature - self._current_temperature + if abs(diff) < 0.1: + self._status = "At Target Temperature" + self._is_heating = False + elif diff > 0: + self._status = "Heating" + self._is_heating = True + else: + self._status = "Cooling Down" + self._is_heating = False + + return {"success": True, "status": self._status} + + def set_temperature(self, temperature: float): + """设置目标温度 - 需要在注册表添加的设备动作""" + try: + temperature = float(temperature) + except ValueError: + self._status = "Error: Invalid temperature value" + return False + + if temperature > self._max_temperature: + self._status = f"Error: Temperature exceeds maximum ({self._max_temperature}°C)" + return False + + self._target_temperature = temperature + self._status = "Setting Temperature" + + # 启动加热控制 + self._start_heating_control() + return True + + def set_heating_power(self, power: float): + """设置加热功率""" + try: + power = float(power) + except ValueError: + self._status = "Error: Invalid power value" + return False + + self._heating_power = max(0.0, min(100.0, power)) # 限制在0-100% + return True + + def _start_heating_control(self): + """启动加热控制线程""" + if not self._running: + self._running = True + self._heating_thread = threading.Thread(target=self._heating_control_loop) + self._heating_thread.daemon = True + self._heating_thread.start() + + def _stop_heating_control(self): + """停止加热控制""" + self._running = False + if self._heating_thread: + self._heating_thread.join(timeout=1.0) + + def _heating_control_loop(self): + """加热控制循环""" + while self._running: + # 如果状态是 Stopped,不改变温度 + if self._status == "Stopped": + time.sleep(1.0) + continue + + temp_diff = self._target_temperature - self._current_temperature + + if abs(temp_diff) < 0.1: + self._status = "At Target Temperature" + self._is_heating = False + self._heating_power = 10.0 + elif temp_diff > 0: + self._status = "Heating" + self._is_heating = True + self._heating_power = min(100.0, abs(temp_diff) * 2) + self._current_temperature += 0.5 + else: + self._status = "Cooling Down" + self._is_heating = False + self._heating_power = 0.0 + self._current_temperature -= 0.2 + + time.sleep(1.0) + + def emergency_stop(self): + """紧急停止""" + self._status = "Emergency Stop" + self._stop_heating_control() + self._is_heating = False + self._heating_power = 0.0 + + def get_status_info(self) -> dict: + """获取完整状态信息""" + return { + "current_temperature": self._current_temperature, + "target_temperature": self._target_temperature, + "status": self._status, + "is_heating": self._is_heating, + "heating_power": self._heating_power, + "max_temperature": self._max_temperature, + "vessel": self._vessel, + "purpose": self._purpose, + "stir": self._stir, + "stir_speed": self._stir_speed + } + +# 用于测试的主函数 +if __name__ == "__main__": + heater = MockHeater() + + print("启动加热器测试...") + print(f"初始状态: {heater.get_status_info()}") + + # 设置目标温度为80度 + heater.set_temperature(80.0) + + # 模拟运行15秒 + try: + for i in range(15): + time.sleep(1) + status = heater.get_status_info() + print( + f"\r温度: {status['current_temperature']:.1f}°C / {status['target_temperature']:.1f}°C | " + f"功率: {status['heating_power']:.1f}% | 状态: {status['status']}", + end="" + ) + except KeyboardInterrupt: + heater.emergency_stop() + print("\n测试被手动停止") + + print("\n测试完成") \ No newline at end of file diff --git a/unilabos/devices/mock/mock_pump.py b/unilabos/devices/mock/mock_pump.py new file mode 100644 index 0000000..43cbf00 --- /dev/null +++ b/unilabos/devices/mock/mock_pump.py @@ -0,0 +1,360 @@ +import time +import threading +from datetime import datetime, timedelta + +class MockPump: + def __init__(self, port: str = "MOCK"): + self.port = port + + # 设备基本状态属性 + self._current_device = "MockPump1" # 设备标识符 + self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped + self._pump_state: str = "Stopped" # 泵运行状态:Running, Stopped, Paused + + # 流量相关属性 + self._flow_rate: float = 0.0 # 当前流速 (mL/min) + self._target_flow_rate: float = 0.0 # 目标流速 (mL/min) + self._max_flow_rate: float = 100.0 # 最大流速 (mL/min) + self._total_volume: float = 0.0 # 累计流量 (mL) + + # 压力相关属性 + self._pressure: float = 0.0 # 当前压力 (bar) + self._max_pressure: float = 10.0 # 最大压力 (bar) + + # 运行控制线程 + self._pump_thread = None + self._running = False + self._thread_lock = threading.Lock() + + # 新增 PumpTransfer 相关属性 + self._from_vessel: str = "" + self._to_vessel: str = "" + self._transfer_volume: float = 0.0 + self._amount: str = "" + self._transfer_time: float = 0.0 + self._is_viscous: bool = False + self._rinsing_solvent: str = "" + self._rinsing_volume: float = 0.0 + self._rinsing_repeats: int = 0 + self._is_solid: bool = False + + # 时间追踪 + self._start_time: datetime = None + self._time_spent: timedelta = timedelta() + self._time_remaining: timedelta = timedelta() + + # ==================== 状态属性 ==================== + # 这些属性会被Uni-Lab系统自动识别并定时对外广播 + + @property + def status(self) -> str: + return self._status + + @property + def current_device(self) -> str: + """当前设备标识符""" + return self._current_device + + @property + def pump_state(self) -> str: + return self._pump_state + + @property + def flow_rate(self) -> float: + return self._flow_rate + + @property + def target_flow_rate(self) -> float: + return self._target_flow_rate + + @property + def pressure(self) -> float: + return self._pressure + + @property + def total_volume(self) -> float: + return self._total_volume + + @property + def max_flow_rate(self) -> float: + return self._max_flow_rate + + @property + def max_pressure(self) -> float: + return self._max_pressure + + # 添加新的属性访问器 + @property + def from_vessel(self) -> str: + return self._from_vessel + + @property + def to_vessel(self) -> str: + return self._to_vessel + + @property + def transfer_volume(self) -> float: + return self._transfer_volume + + @property + def amount(self) -> str: + return self._amount + + @property + def transfer_time(self) -> float: + return self._transfer_time + + @property + def is_viscous(self) -> bool: + return self._is_viscous + + @property + def rinsing_solvent(self) -> str: + return self._rinsing_solvent + + @property + def rinsing_volume(self) -> float: + return self._rinsing_volume + + @property + def rinsing_repeats(self) -> int: + return self._rinsing_repeats + + @property + def is_solid(self) -> bool: + return self._is_solid + + # 修改这两个属性装饰器 + @property + def time_spent(self) -> float: + """已用时间(秒)""" + if isinstance(self._time_spent, timedelta): + return self._time_spent.total_seconds() + return float(self._time_spent) + + @property + def time_remaining(self) -> float: + """剩余时间(秒)""" + if isinstance(self._time_remaining, timedelta): + return self._time_remaining.total_seconds() + return float(self._time_remaining) + + # ==================== 设备控制方法 ==================== + # 这些方法需要在注册表中添加,会作为ActionServer接受控制指令 + def pump_transfer(self, from_vessel: str, to_vessel: str, volume: float, + amount: str = "", time: float = 0.0, viscous: bool = False, + rinsing_solvent: str = "", rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, solid: bool = False) -> dict: + """Execute pump transfer operation""" + # Stop any existing operation first + self._stop_pump_operation() + + # Set transfer parameters + self._from_vessel = from_vessel + self._to_vessel = to_vessel + self._transfer_volume = float(volume) + self._amount = amount + self._transfer_time = float(time) + self._is_viscous = viscous + self._rinsing_solvent = rinsing_solvent + self._rinsing_volume = float(rinsing_volume) + self._rinsing_repeats = int(rinsing_repeats) + self._is_solid = solid + + # Calculate flow rate + if self._transfer_time > 0 and self._transfer_volume > 0: + self._target_flow_rate = (self._transfer_volume / self._transfer_time) * 60.0 + else: + self._target_flow_rate = 10.0 if not self._is_viscous else 5.0 + + # Reset timers and counters + self._start_time = datetime.now() + self._time_spent = timedelta() + self._time_remaining = timedelta(seconds=self._transfer_time) + self._total_volume = 0.0 + self._flow_rate = 0.0 + + # Start pump operation + self._pump_state = "Running" + self._status = "Starting Transfer" + self._running = True + + # Start pump operation thread + self._pump_thread = threading.Thread(target=self._pump_operation_loop) + self._pump_thread.daemon = True + self._pump_thread.start() + + # Wait briefly to ensure thread starts + time.sleep(0.1) + + return { + "success": True, + "status": self._status, + "current_device": self._current_device, + "time_spent": 0.0, + "time_remaining": float(self._transfer_time) + } + + def pause_pump(self) -> str: + + if self._pump_state != "Running": + self._status = "Error: Pump not running" + return "Error" + + self._pump_state = "Paused" + self._status = "Pump Paused" + self._stop_pump_operation() + + return "Success" + + def resume_pump(self) -> str: + + if self._pump_state != "Paused": + self._status = "Error: Pump not paused" + return "Error" + + self._pump_state = "Running" + self._status = "Resuming Pump" + self._start_pump_operation() + + return "Success" + + def reset_volume_counter(self) -> str: + self._total_volume = 0.0 + self._status = "Volume counter reset" + return "Success" + + def emergency_stop(self) -> str: + self._status = "Emergency Stop" + self._pump_state = "Stopped" + self._stop_pump_operation() + self._flow_rate = 0.0 + self._pressure = 0.0 + self._target_flow_rate = 0.0 + + return "Success" + + # ==================== 内部控制方法 ==================== + + def _start_pump_operation(self): + with self._thread_lock: + if not self._running: + self._running = True + self._pump_thread = threading.Thread(target=self._pump_operation_loop) + self._pump_thread.daemon = True + self._pump_thread.start() + + def _stop_pump_operation(self): + with self._thread_lock: + self._running = False + if self._pump_thread and self._pump_thread.is_alive(): + self._pump_thread.join(timeout=2.0) + + def _pump_operation_loop(self): + """泵运行主循环""" + print("Pump operation loop started") # Debug print + + while self._running and self._pump_state == "Running": + try: + # Calculate flow rate adjustment + flow_diff = self._target_flow_rate - self._flow_rate + + # Adjust flow rate more aggressively (50% of difference) + adjustment = flow_diff * 0.5 + self._flow_rate += adjustment + + # Ensure flow rate is within bounds + self._flow_rate = max(0.1, min(self._max_flow_rate, self._flow_rate)) + + # Update status based on flow rate + if abs(flow_diff) < 0.1: + self._status = "Running at Target Flow Rate" + else: + self._status = "Adjusting Flow Rate" + + # Calculate volume increment + volume_increment = (self._flow_rate / 60.0) # mL/s + self._total_volume += volume_increment + + # Update time tracking + self._time_spent = datetime.now() - self._start_time + if self._transfer_time > 0: + remaining = self._transfer_time - self._time_spent.total_seconds() + self._time_remaining = timedelta(seconds=max(0, remaining)) + + # Check completion + if self._total_volume >= self._transfer_volume: + self._status = "Transfer Completed" + self._pump_state = "Stopped" + self._running = False + break + + # Update pressure + self._pressure = (self._flow_rate / self._max_flow_rate) * self._max_pressure + + print(f"Debug - Flow: {self._flow_rate:.1f}, Volume: {self._total_volume:.1f}") # Debug print + + time.sleep(1.0) + + except Exception as e: + print(f"Error in pump operation: {str(e)}") + self._status = "Error in pump operation" + self._pump_state = "Stopped" + self._running = False + break + + def get_status_info(self) -> dict: + """ + 获取完整的设备状态信息 + + Returns: + dict: 包含所有设备状态的字典 + """ + return { + "status": self._status, + "pump_state": self._pump_state, + "flow_rate": self._flow_rate, + "target_flow_rate": self._target_flow_rate, + "pressure": self._pressure, + "total_volume": self._total_volume, + "max_flow_rate": self._max_flow_rate, + "max_pressure": self._max_pressure, + "current_device": self._current_device, + "from_vessel": self._from_vessel, + "to_vessel": self._to_vessel, + "transfer_volume": self._transfer_volume, + "amount": self._amount, + "transfer_time": self._transfer_time, + "is_viscous": self._is_viscous, + "rinsing_solvent": self._rinsing_solvent, + "rinsing_volume": self._rinsing_volume, + "rinsing_repeats": self._rinsing_repeats, + "is_solid": self._is_solid, + "time_spent": self._time_spent.total_seconds(), + "time_remaining": self._time_remaining.total_seconds() + } + + +# 用于测试的主函数 +if __name__ == "__main__": + pump = MockPump() + + # 测试基本功能 + print("启动泵设备测试...") + print(f"初始状态: {pump.get_status_info()}") + + # 设置流速并启动 + pump.set_flow_rate(50.0) + pump.start_pump() + + # 模拟运行10秒 + for i in range(10): + time.sleep(1) + print(f"第{i+1}秒: 流速={pump.flow_rate:.1f}mL/min, 压力={pump.pressure:.2f}bar, 状态={pump.status}") + + # 测试方向切换 + print("切换泵方向...") + + + pump.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/mock/mock_rotavap.py b/unilabos/devices/mock/mock_rotavap.py new file mode 100644 index 0000000..9b2ea91 --- /dev/null +++ b/unilabos/devices/mock/mock_rotavap.py @@ -0,0 +1,390 @@ +import time +import threading +import json + + +class MockRotavap: + """ + 模拟旋转蒸发器设备类 + + 这个类模拟了一个实验室旋转蒸发器的行为,包括旋转控制、 + 真空泵控制、温度控制等功能。参考了现有的 RotavapOne 实现。 + """ + + def __init__(self, port: str = "MOCK"): + """ + 初始化MockRotavap实例 + + Args: + port (str): 设备端口,默认为"MOCK"表示模拟设备 + """ + self.port = port + + # 设备基本状态属性 + self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped + + # 旋转相关属性 + self._rotate_state: str = "Stopped" # 旋转状态:Running, Stopped + self._rotate_time: float = 0.0 # 旋转剩余时间 (秒) + self._rotate_speed: float = 0.0 # 旋转速度 (rpm) + self._max_rotate_speed: float = 300.0 # 最大旋转速度 (rpm) + + # 真空泵相关属性 + self._pump_state: str = "Stopped" # 泵状态:Running, Stopped + self._pump_time: float = 0.0 # 泵剩余时间 (秒) + self._vacuum_level: float = 0.0 # 真空度 (mbar) + self._target_vacuum: float = 50.0 # 目标真空度 (mbar) + + # 温度相关属性 + self._temperature: float = 25.0 # 水浴温度 (°C) + self._target_temperature: float = 25.0 # 目标温度 (°C) + self._max_temperature: float = 180.0 # 最大温度 (°C) + + # 运行控制线程 + self._operation_thread = None + self._running = False + self._thread_lock = threading.Lock() + + # 操作成功标志 + self.success: str = "True" # 使用字符串而不是布尔值 + + # ==================== 状态属性 ==================== + # 这些属性会被Uni-Lab系统自动识别并定时对外广播 + + @property + def status(self) -> str: + return self._status + + @property + def rotate_state(self) -> str: + return self._rotate_state + + @property + def rotate_time(self) -> float: + return self._rotate_time + + @property + def rotate_speed(self) -> float: + return self._rotate_speed + + @property + def pump_state(self) -> str: + return self._pump_state + + @property + def pump_time(self) -> float: + return self._pump_time + + @property + def vacuum_level(self) -> float: + return self._vacuum_level + + @property + def temperature(self) -> float: + return self._temperature + + @property + def target_temperature(self) -> float: + return self._target_temperature + + # ==================== 设备控制方法 ==================== + # 这些方法需要在注册表中添加,会作为ActionServer接受控制指令 + + def set_timer(self, command: str) -> str: + """ + 设置定时器 - 兼容现有RotavapOne接口 + + Args: + command (str): JSON格式的命令字符串,包含rotate_time和pump_time + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + try: + timer = json.loads(command) + rotate_time = timer.get("rotate_time", 0) + pump_time = timer.get("pump_time", 0) + + self.success = "False" + self._rotate_time = float(rotate_time) + self._pump_time = float(pump_time) + self.success = "True" + + self._status = "Timer Set" + return "Success" + + except (json.JSONDecodeError, ValueError, KeyError) as e: + self._status = f"Error: Invalid command format - {str(e)}" + self.success = "False" + return "Error" + + def set_rotate_time(self, time_seconds: float) -> str: + """ + 设置旋转时间 + + Args: + time_seconds (float): 旋转时间 (秒) + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + self.success = "False" + self._rotate_time = max(0.0, float(time_seconds)) + self.success = "True" + self._status = "Rotate time set" + return "Success" + + def set_pump_time(self, time_seconds: float) -> str: + """ + 设置泵时间 + + Args: + time_seconds (float): 泵时间 (秒) + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + self.success = "False" + self._pump_time = max(0.0, float(time_seconds)) + self.success = "True" + self._status = "Pump time set" + return "Success" + + def set_rotate_speed(self, speed: float) -> str: + """ + 设置旋转速度 + + Args: + speed (float): 旋转速度 (rpm) + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + if speed < 0 or speed > self._max_rotate_speed: + self._status = f"Error: Speed out of range (0-{self._max_rotate_speed})" + return "Error" + + self._rotate_speed = speed + self._status = "Rotate speed set" + return "Success" + + def set_temperature(self, temperature: float) -> str: + """ + 设置水浴温度 + + Args: + temperature (float): 目标温度 (°C) + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + if temperature < 0 or temperature > self._max_temperature: + self._status = f"Error: Temperature out of range (0-{self._max_temperature})" + return "Error" + + self._target_temperature = temperature + self._status = "Temperature set" + + # 启动操作线程以开始温度控制 + self._start_operation() + + return "Success" + + def start_rotation(self) -> str: + """ + 启动旋转 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + if self._rotate_time <= 0: + self._status = "Error: No rotate time set" + return "Error" + + self._rotate_state = "Running" + self._status = "Rotation started" + return "Success" + + def start_pump(self) -> str: + """ + 启动真空泵 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + if self._pump_time <= 0: + self._status = "Error: No pump time set" + return "Error" + + self._pump_state = "Running" + self._status = "Pump started" + return "Success" + + def stop_all_operations(self) -> str: + """ + 停止所有操作 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._rotate_state = "Stopped" + self._pump_state = "Stopped" + self._stop_operation() + self._rotate_time = 0.0 + self._pump_time = 0.0 + self._vacuum_level = 0.0 + self._status = "All operations stopped" + return "Success" + + def emergency_stop(self) -> str: + """ + 紧急停止 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._status = "Emergency Stop" + self.stop_all_operations() + return "Success" + + # ==================== 内部控制方法 ==================== + + def _start_operation(self): + """ + 启动操作线程 + + 这个方法启动一个后台线程来模拟旋蒸的实际运行过程。 + """ + with self._thread_lock: + if not self._running: + self._running = True + self._operation_thread = threading.Thread(target=self._operation_loop) + self._operation_thread.daemon = True + self._operation_thread.start() + + def _stop_operation(self): + """ + 停止操作线程 + + 安全地停止后台运行线程并等待其完成。 + """ + with self._thread_lock: + self._running = False + if self._operation_thread and self._operation_thread.is_alive(): + self._operation_thread.join(timeout=2.0) + + def _operation_loop(self): + """ + 操作主循环 + + 这个方法在后台线程中运行,模拟真实旋蒸的工作过程: + 1. 时间倒计时 + 2. 温度控制 + 3. 真空度控制 + 4. 状态更新 + """ + while self._running: + try: + # 处理旋转时间倒计时 + if self._rotate_time > 0: + self._rotate_state = "Running" + self._rotate_time = max(0.0, self._rotate_time - 1.0) + else: + self._rotate_state = "Stopped" + + # 处理泵时间倒计时 + if self._pump_time > 0: + self._pump_state = "Running" + self._pump_time = max(0.0, self._pump_time - 1.0) + # 模拟真空度变化 + if self._vacuum_level > self._target_vacuum: + self._vacuum_level = max(self._target_vacuum, self._vacuum_level - 5.0) + else: + self._pump_state = "Stopped" + # 真空度逐渐回升 + self._vacuum_level = min(1013.25, self._vacuum_level + 2.0) + + # 模拟温度控制 + temp_diff = self._target_temperature - self._temperature + if abs(temp_diff) > 0.5: + if temp_diff > 0: + self._temperature += min(1.0, temp_diff * 0.1) + else: + self._temperature += max(-1.0, temp_diff * 0.1) + + # 更新整体状态 + if self._rotate_state == "Running" or self._pump_state == "Running": + self._status = "Operating" + elif self._rotate_time > 0 or self._pump_time > 0: + self._status = "Ready" + else: + self._status = "Idle" + + # 等待1秒后继续下一次循环 + time.sleep(1.0) + + except Exception as e: + self._status = f"Error in operation: {str(e)}" + break + + # 循环结束时的清理工作 + self._status = "Idle" + + def get_status_info(self) -> dict: + """ + 获取完整的设备状态信息 + + Returns: + dict: 包含所有设备状态的字典 + """ + return { + "status": self._status, + "rotate_state": self._rotate_state, + "rotate_time": self._rotate_time, + "rotate_speed": self._rotate_speed, + "pump_state": self._pump_state, + "pump_time": self._pump_time, + "vacuum_level": self._vacuum_level, + "temperature": self._temperature, + "target_temperature": self._target_temperature, + "success": self.success, + } + + +# 用于测试的主函数 +if __name__ == "__main__": + rotavap = MockRotavap() + + # 测试基本功能 + print("启动旋转蒸发器测试...") + print(f"初始状态: {rotavap.get_status_info()}") + + # 设置定时器 + timer_command = '{"rotate_time": 300, "pump_time": 600}' + rotavap.set_timer(timer_command) + + # 设置温度和转速 + rotavap.set_temperature(60.0) + rotavap.set_rotate_speed(120.0) + + # 启动操作 + rotavap.start_rotation() + rotavap.start_pump() + + # 模拟运行10秒 + for i in range(10): + time.sleep(1) + print( + f"第{i+1}秒: 旋转={rotavap.rotate_time:.0f}s, 泵={rotavap.pump_time:.0f}s, " + f"温度={rotavap.temperature:.1f}°C, 真空={rotavap.vacuum_level:.1f}mbar" + ) + + rotavap.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/mock/mock_separator.py b/unilabos/devices/mock/mock_separator.py new file mode 100644 index 0000000..222cb2e --- /dev/null +++ b/unilabos/devices/mock/mock_separator.py @@ -0,0 +1,399 @@ +import time +import threading +from datetime import datetime, timedelta + +class MockSeparator: + def __init__(self, port: str = "MOCK"): + self.port = port + + # 基本状态属性 + self._status: str = "Idle" # 当前总体状态 + self._valve_state: str = "Closed" # 阀门状态:Open 或 Closed + self._settling_time: float = 0.0 # 静置时间(秒) + + # 搅拌相关属性 + self._shake_time: float = 0.0 # 剩余摇摆时间(秒) + self._shake_status: str = "Not Shaking" # 摇摆状态 + + # 用于后台模拟 shake 动作 + self._operation_thread = None + self._thread_lock = threading.Lock() + self._running = False + + # Separate action 相关属性 + self._current_device: str = "MockSeparator1" + self._purpose: str = "" # wash or extract + self._product_phase: str = "" # top or bottom + self._from_vessel: str = "" + self._separation_vessel: str = "" + self._to_vessel: str = "" + self._waste_phase_to_vessel: str = "" + self._solvent: str = "" + self._solvent_volume: float = 0.0 + self._through: str = "" + self._repeats: int = 1 + self._stir_time: float = 0.0 + self._stir_speed: float = 0.0 + self._time_spent = timedelta() + self._time_remaining = timedelta() + self._start_time = datetime.now() # 添加这一行 + + @property + def current_device(self) -> str: + return self._current_device + + @property + def purpose(self) -> str: + return self._purpose + + @property + def valve_state(self) -> str: + return self._valve_state + + @property + def settling_time(self) -> float: + return self._settling_time + + @property + def status(self) -> str: + return self._status + + @property + def shake_time(self) -> float: + with self._thread_lock: + return self._shake_time + + @property + def shake_status(self) -> str: + with self._thread_lock: + return self._shake_status + + @property + def product_phase(self) -> str: + return self._product_phase + + @property + def from_vessel(self) -> str: + return self._from_vessel + + @property + def separation_vessel(self) -> str: + return self._separation_vessel + + @property + def to_vessel(self) -> str: + return self._to_vessel + + @property + def waste_phase_to_vessel(self) -> str: + return self._waste_phase_to_vessel + + @property + def solvent(self) -> str: + return self._solvent + + @property + def solvent_volume(self) -> float: + return self._solvent_volume + + @property + def through(self) -> str: + return self._through + + @property + def repeats(self) -> int: + return self._repeats + + @property + def stir_time(self) -> float: + return self._stir_time + + @property + def stir_speed(self) -> float: + return self._stir_speed + + @property + def time_spent(self) -> float: + if self._running: + self._time_spent = datetime.now() - self._start_time + return self._time_spent.total_seconds() + + @property + def time_remaining(self) -> float: + if self._running: + elapsed = (datetime.now() - self._start_time).total_seconds() + total_time = (self._stir_time + self._settling_time + 10) * self._repeats + remain = max(0, total_time - elapsed) + self._time_remaining = timedelta(seconds=remain) + return self._time_remaining.total_seconds() + + def separate(self, purpose: str, product_phase: str, from_vessel: str, + separation_vessel: str, to_vessel: str, waste_phase_to_vessel: str = "", + solvent: str = "", solvent_volume: float = 0.0, through: str = "", + repeats: int = 1, stir_time: float = 0.0, stir_speed: float = 0.0, + settling_time: float = 60.0) -> dict: + """ + 执行分离操作 + """ + with self._thread_lock: + # 检查是否已经在运行 + if self._running: + return { + "success": False, + "status": "Error: Operation already in progress" + } + # 必填参数验证 + if not all([from_vessel, separation_vessel, to_vessel]): + self._status = "Error: Missing required vessel parameters" + return {"success": False} + # 验证参数 + if purpose not in ["wash", "extract"]: + self._status = "Error: Invalid purpose" + return {"success": False} + + if product_phase not in ["top", "bottom"]: + self._status = "Error: Invalid product phase" + return {"success": False} + # 数值参数验证 + try: + solvent_volume = float(solvent_volume) + repeats = int(repeats) + stir_time = float(stir_time) + stir_speed = float(stir_speed) + settling_time = float(settling_time) + except ValueError: + self._status = "Error: Invalid numeric parameters" + return {"success": False} + + # 设置参数 + self._purpose = purpose + self._product_phase = product_phase + self._from_vessel = from_vessel + self._separation_vessel = separation_vessel + self._to_vessel = to_vessel + self._waste_phase_to_vessel = waste_phase_to_vessel + self._solvent = solvent + self._solvent_volume = float(solvent_volume) + self._through = through + self._repeats = int(repeats) + self._stir_time = float(stir_time) + self._stir_speed = float(stir_speed) + self._settling_time = float(settling_time) + + # 重置计时器 + self._start_time = datetime.now() + self._time_spent = timedelta() + total_time = (self._stir_time + self._settling_time + 10) * self._repeats + self._time_remaining = timedelta(seconds=total_time) + + # 启动分离操作 + self._status = "Starting Separation" + self._running = True + + # 在锁内创建和启动线程 + self._operation_thread = threading.Thread(target=self._operation_loop) + self._operation_thread.daemon = True + self._operation_thread.start() + + # 等待确认操作已经开始 + time.sleep(0.1) # 短暂等待确保操作线程已启动 + + return { + "success": True, + "status": self._status, + "current_device": self._current_device, + "time_spent": self._time_spent.total_seconds(), + "time_remaining": self._time_remaining.total_seconds() + } + + def shake(self, shake_time: float) -> str: + """ + 模拟 shake(搅拌)操作: + - 进入 "Shaking" 状态,倒计时 shake_time 秒 + - shake 结束后,进入 "Settling" 状态,静置时间固定为 5 秒 + - 最后恢复为 Idle + """ + try: + shake_time = float(shake_time) + except ValueError: + self._status = "Error: Invalid shake time" + return "Error" + + with self._thread_lock: + self._status = "Shaking" + self._settling_time = 0.0 + self._shake_time = shake_time + self._shake_status = "Shaking" + + def _run_shake(): + remaining = shake_time + while remaining > 0: + time.sleep(1) + remaining -= 1 + with self._thread_lock: + self._shake_time = remaining + with self._thread_lock: + self._status = "Settling" + self._settling_time = 60.0 # 固定静置时间为60秒 + self._shake_status = "Settling" + while True: + with self._thread_lock: + if self._settling_time <= 0: + self._status = "Idle" + self._shake_status = "Idle" + break + time.sleep(1) + with self._thread_lock: + self._settling_time = max(0.0, self._settling_time - 1) + + self._operation_thread = threading.Thread(target=_run_shake) + self._operation_thread.daemon = True + self._operation_thread.start() + return "Success" + + def set_valve(self, command: str) -> str: + """ + 阀门控制命令:传入 "open" 或 "close" + """ + + command = command.lower() + if command == "open": + self._valve_state = "Open" + self._status = "Valve Opened" + elif command == "close": + self._valve_state = "Closed" + self._status = "Valve Closed" + else: + self._status = "Error: Invalid valve command" + return "Error" + return "Success" + + def _operation_loop(self): + """分离操作主循环""" + try: + current_repeat = 1 + + # 立即更新状态,确保不会停留在Starting Separation + with self._thread_lock: + self._status = f"Separation Cycle {current_repeat}/{self._repeats}" + + while self._running and current_repeat <= self._repeats: + # 第一步:搅拌 + if self._stir_time > 0: + with self._thread_lock: + self._status = f"Stirring (Repeat {current_repeat}/{self._repeats})" + remaining_stir = self._stir_time + while remaining_stir > 0 and self._running: + time.sleep(1) + remaining_stir -= 1 + + # 第二步:静置 + if self._settling_time > 0: + with self._thread_lock: + self._status = f"Settling (Repeat {current_repeat}/{self._repeats})" + remaining_settle = self._settling_time + while remaining_settle > 0 and self._running: + time.sleep(1) + remaining_settle -= 1 + + # 第三步:打开阀门排出 + with self._thread_lock: + self._valve_state = "Open" + self._status = f"Draining (Repeat {current_repeat}/{self._repeats})" + + # 模拟排出时间(5秒) + time.sleep(10) + + # 关闭阀门 + with self._thread_lock: + self._valve_state = "Closed" + + # 检查是否继续下一次重复 + if current_repeat < self._repeats: + current_repeat += 1 + else: + with self._thread_lock: + self._status = "Separation Complete" + break + + except Exception as e: + with self._thread_lock: + self._status = f"Error in separation: {str(e)}" + finally: + with self._thread_lock: + self._running = False + self._valve_state = "Closed" + if self._status == "Starting Separation": + self._status = "Error: Operation failed to start" + elif self._status != "Separation Complete": + self._status = "Stopped" + + def stop_operations(self) -> str: + """停止任何正在执行的操作""" + with self._thread_lock: + self._running = False + if self._operation_thread and self._operation_thread.is_alive(): + self._operation_thread.join(timeout=1.0) + self._operation_thread = None + self._settling_time = 0.0 + self._status = "Idle" + self._shake_status = "Idle" + self._shake_time = 0.0 + self._time_remaining = timedelta() + return "Success" + + def get_status_info(self) -> dict: + """获取当前设备状态信息""" + with self._thread_lock: + current_time = datetime.now() + if self._start_time: + self._time_spent = current_time - self._start_time + + return { + "status": self._status, + "valve_state": self._valve_state, + "settling_time": self._settling_time, + "shake_time": self._shake_time, + "shake_status": self._shake_status, + "current_device": self._current_device, + "purpose": self._purpose, + "product_phase": self._product_phase, + "from_vessel": self._from_vessel, + "separation_vessel": self._separation_vessel, + "to_vessel": self._to_vessel, + "waste_phase_to_vessel": self._waste_phase_to_vessel, + "solvent": self._solvent, + "solvent_volume": self._solvent_volume, + "through": self._through, + "repeats": self._repeats, + "stir_time": self._stir_time, + "stir_speed": self._stir_speed, + "time_spent": self._time_spent.total_seconds(), + "time_remaining": self._time_remaining.total_seconds() + } + + +# 主函数用于测试 +if __name__ == "__main__": + separator = MockSeparator() + + print("启动简单版分离器测试...") + print("初始状态:", separator.get_status_info()) + + # 触发 shake 操作,模拟 10 秒的搅拌 + print("执行 shake 操作...") + print(separator.shake(10.0)) + + # 循环显示状态变化 + for i in range(20): + time.sleep(1) + info = separator.get_status_info() + print( + f"第{i+1}秒: 状态={info['status']}, 静置时间={info['settling_time']:.1f}秒, " + f"阀门状态={info['valve_state']}, shake_time={info['shake_time']:.1f}, " + f"shake_status={info['shake_status']}" + ) + + # 模拟打开阀门 + print("打开阀门...", separator.set_valve("open")) + print("最终状态:", separator.get_status_info()) diff --git a/unilabos/devices/mock/mock_solenoid_valve.py b/unilabos/devices/mock/mock_solenoid_valve.py new file mode 100644 index 0000000..0f0fbe5 --- /dev/null +++ b/unilabos/devices/mock/mock_solenoid_valve.py @@ -0,0 +1,89 @@ +import time + + +class MockSolenoidValve: + """ + 模拟电磁阀设备类 - 简化版本 + + 这个类提供了电磁阀的基本功能:开启、关闭和状态查询 + """ + + def __init__(self, port: str = "MOCK"): + """ + 初始化MockSolenoidValve实例 + + Args: + port (str): 设备端口,默认为"MOCK"表示模拟设备 + """ + self.port = port + self._status: str = "Idle" + self._valve_status: str = "Closed" # 阀门位置:Open, Closed + + @property + def status(self) -> str: + """设备状态 - 会被自动识别的设备属性""" + return self._status + + @property + def valve_status(self) -> str: + """阀门状态""" + return self._valve_status + + def set_valve_status(self, status: str) -> str: + """ + 设置阀门位置 + + Args: + position (str): 阀门位置,可选值:"Open", "Closed" + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + if status not in ["Open", "Closed"]: + self._status = "Error: Invalid position" + return "Error" + + self._status = "Moving" + time.sleep(1) # 模拟阀门动作时间 + + self._valve_status = status + self._status = "Idle" + return "Success" + + def open_valve(self) -> str: + """打开阀门""" + return self.set_valve_status("Open") + + def close_valve(self) -> str: + """关闭阀门""" + return self.set_valve_status("Closed") + + def get_valve_status(self) -> str: + """获取阀门位置""" + return self._valve_status + + def is_open(self) -> bool: + """检查阀门是否打开""" + return self._valve_status == "Open" + + def is_closed(self) -> bool: + """检查阀门是否关闭""" + return self._valve_status == "Closed" + + +# 用于测试的主函数 +if __name__ == "__main__": + valve = MockSolenoidValve() + + print("启动电磁阀测试...") + print(f"初始状态: 位置={valve.valve_status}, 状态={valve.status}") + + # 测试开启阀门 + valve.open_valve() + print(f"开启后: 位置={valve.valve_status}, 状态={valve.status}") + + # 测试关闭阀门 + valve.close_valve() + print(f"关闭后: 位置={valve.valve_status}, 状态={valve.status}") + + print("测试完成") diff --git a/unilabos/devices/mock/mock_stirrer.py b/unilabos/devices/mock/mock_stirrer.py new file mode 100644 index 0000000..a1f2c51 --- /dev/null +++ b/unilabos/devices/mock/mock_stirrer.py @@ -0,0 +1,307 @@ +import time +import threading + + +class MockStirrer: + def __init__(self, port: str = "MOCK"): + self.port = port + + # 设备基本状态属性 + self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped + + # 搅拌相关属性 + self._stir_speed: float = 0.0 # 当前搅拌速度 (rpm) + self._target_stir_speed: float = 0.0 # 目标搅拌速度 (rpm) + self._max_stir_speed: float = 2000.0 # 最大搅拌速度 (rpm) + self._stir_state: str = "Stopped" # 搅拌状态:Running, Stopped + + # 温度相关属性 + self._temperature: float = 25.0 # 当前温度 (°C) + self._target_temperature: float = 25.0 # 目标温度 (°C) + self._max_temperature: float = 300.0 # 最大温度 (°C) + self._heating_state: str = "Off" # 加热状态:On, Off + self._heating_power: float = 0.0 # 加热功率百分比 0-100 + + # 运行控制线程 + self._operation_thread = None + self._running = False + self._thread_lock = threading.Lock() + + # ==================== 状态属性 ==================== + # 这些属性会被Uni-Lab系统自动识别并定时对外广播 + + @property + def status(self) -> str: + return self._status + + @property + def stir_speed(self) -> float: + return self._stir_speed + + @property + def target_stir_speed(self) -> float: + return self._target_stir_speed + + @property + def stir_state(self) -> str: + return self._stir_state + + @property + def temperature(self) -> float: + """ + 当前温度 + + Returns: + float: 当前温度 (°C) + """ + return self._temperature + + @property + def target_temperature(self) -> float: + """ + 目标温度 + + Returns: + float: 目标温度 (°C) + """ + return self._target_temperature + + @property + def heating_state(self) -> str: + return self._heating_state + + @property + def heating_power(self) -> float: + return self._heating_power + + @property + def max_stir_speed(self) -> float: + return self._max_stir_speed + + @property + def max_temperature(self) -> float: + return self._max_temperature + + # ==================== 设备控制方法 ==================== + # 这些方法需要在注册表中添加,会作为ActionServer接受控制指令 + + def set_stir_speed(self, speed: float) -> str: + + speed = float(speed) # 确保传入的速度是浮点数 + + if speed < 0 or speed > self._max_stir_speed: + self._status = f"Error: Speed out of range (0-{self._max_stir_speed})" + return "Error" + + self._target_stir_speed = speed + self._status = "Setting Stir Speed" + + # 如果设置了非零速度,启动搅拌 + if speed > 0: + self._stir_state = "Running" + else: + self._stir_state = "Stopped" + + return "Success" + + def set_temperature(self, temperature: float) -> str: + temperature = float(temperature) # 确保传入的温度是浮点数 + + if temperature < 0 or temperature > self._max_temperature: + self._status = f"Error: Temperature out of range (0-{self._max_temperature})" + return "Error" + + self._target_temperature = temperature + self._status = "Setting Temperature" + + return "Success" + + def start_stirring(self) -> str: + + if self._target_stir_speed <= 0: + self._status = "Error: No target speed set" + return "Error" + + self._stir_state = "Running" + self._status = "Stirring Started" + return "Success" + + def stop_stirring(self) -> str: + self._stir_state = "Stopped" + self._target_stir_speed = 0.0 + self._status = "Stirring Stopped" + return "Success" + + def heating_control(self, heating_state: str = "On") -> str: + + if heating_state not in ["On", "Off"]: + self._status = "Error: Invalid heating state" + return "Error" + + self._heating_state = heating_state + + if heating_state == "On": + self._status = "Heating On" + else: + self._status = "Heating Off" + self._heating_power = 0.0 + + return "Success" + + def stop_all_operations(self) -> str: + self._stir_state = "Stopped" + self._heating_state = "Off" + self._stop_operation() + self._stir_speed = 0.0 + self._target_stir_speed = 0.0 + self._heating_power = 0.0 + self._status = "All operations stopped" + return "Success" + + def emergency_stop(self) -> str: + """ + 紧急停止 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._status = "Emergency Stop" + self.stop_all_operations() + return "Success" + + # ==================== 内部控制方法 ==================== + + def _start_operation(self): + with self._thread_lock: + if not self._running: + self._running = True + self._operation_thread = threading.Thread(target=self._operation_loop) + self._operation_thread.daemon = True + self._operation_thread.start() + + def _stop_operation(self): + """ + 停止操作线程 + + 安全地停止后台运行线程并等待其完成。 + """ + with self._thread_lock: + self._running = False + if self._operation_thread and self._operation_thread.is_alive(): + self._operation_thread.join(timeout=2.0) + + def _operation_loop(self): + while self._running: + try: + # 处理搅拌速度控制 + if self._stir_state == "Running": + speed_diff = self._target_stir_speed - self._stir_speed + + if abs(speed_diff) < 1.0: # 速度接近目标值 + self._stir_speed = self._target_stir_speed + if self._stir_speed > 0: + self._status = "Stirring at Target Speed" + else: + # 模拟速度调节,每秒调整10%的差值 + adjustment = speed_diff * 0.1 + self._stir_speed += adjustment + self._status = "Adjusting Stir Speed" + + # 确保速度在合理范围内 + self._stir_speed = max(0.0, min(self._max_stir_speed, self._stir_speed)) + else: + # 搅拌停止时,速度逐渐降为0 + if self._stir_speed > 0: + self._stir_speed = max(0.0, self._stir_speed - 50.0) # 每秒减少50rpm + + # 处理温度控制 + if self._heating_state == "On": + temp_diff = self._target_temperature - self._temperature + + if abs(temp_diff) < 0.5: # 温度接近目标值 + self._heating_power = 20.0 # 维持温度的最小功率 + elif temp_diff > 0: # 需要加热 + # 根据温差调整加热功率 + if temp_diff > 50: + self._heating_power = 100.0 + elif temp_diff > 20: + self._heating_power = 80.0 + elif temp_diff > 10: + self._heating_power = 60.0 + else: + self._heating_power = 40.0 + + # 模拟加热过程 + heating_rate = self._heating_power / 100.0 * 1.5 # 最大每秒升温1.5度 + self._temperature += heating_rate + else: # 目标温度低于当前温度 + self._heating_power = 0.0 + # 自然冷却 + self._temperature -= 0.1 + else: + self._heating_power = 0.0 + # 自然冷却到室温 + if self._temperature > 25.0: + self._temperature -= 0.2 + + # 限制温度范围 + self._temperature = max(20.0, min(self._max_temperature, self._temperature)) + + # 更新整体状态 + if self._stir_state == "Running" and self._heating_state == "On": + self._status = "Stirring and Heating" + elif self._stir_state == "Running": + self._status = "Stirring Only" + elif self._heating_state == "On": + self._status = "Heating Only" + else: + self._status = "Idle" + + # 等待1秒后继续下一次循环 + time.sleep(1.0) + + except Exception as e: + self._status = f"Error in operation: {str(e)}" + break + + # 循环结束时的清理工作 + self._status = "Idle" + + def get_status_info(self) -> dict: + return { + "status": self._status, + "stir_speed": self._stir_speed, + "target_stir_speed": self._target_stir_speed, + "stir_state": self._stir_state, + "temperature": self._temperature, + "target_temperature": self._target_temperature, + "heating_state": self._heating_state, + "heating_power": self._heating_power, + "max_stir_speed": self._max_stir_speed, + "max_temperature": self._max_temperature, + } + + +# 用于测试的主函数 +if __name__ == "__main__": + stirrer = MockStirrer() + + # 测试基本功能 + print("启动搅拌器测试...") + print(f"初始状态: {stirrer.get_status_info()}") + + # 设置搅拌速度和温度 + stirrer.set_stir_speed(800.0) + stirrer.set_temperature(60.0) + stirrer.heating_control("On") + + # 模拟运行15秒 + for i in range(15): + time.sleep(1) + print( + f"第{i+1}秒: 速度={stirrer.stir_speed:.0f}rpm, 温度={stirrer.temperature:.1f}°C, " + f"功率={stirrer.heating_power:.1f}%, 状态={stirrer.status}" + ) + + stirrer.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/mock/mock_stirrer_new.py b/unilabos/devices/mock/mock_stirrer_new.py new file mode 100644 index 0000000..ac429db --- /dev/null +++ b/unilabos/devices/mock/mock_stirrer_new.py @@ -0,0 +1,229 @@ +import time +import threading +from datetime import datetime, timedelta + +class MockStirrer_new: + def __init__(self, port: str = "MOCK"): + self.port = port + + # 基本状态属性 + self._status: str = "Idle" + self._vessel: str = "" + self._purpose: str = "" + + # 搅拌相关属性 + self._stir_speed: float = 0.0 + self._target_stir_speed: float = 0.0 + self._max_stir_speed: float = 2000.0 + self._stir_state: str = "Stopped" + + # 计时相关 + self._stir_time: float = 0.0 + self._settling_time: float = 0.0 + self._start_time = datetime.now() + self._time_remaining = timedelta() + + # 运行控制 + self._operation_thread = None + self._running = False + self._thread_lock = threading.Lock() + + # 创建操作线程 + self._operation_thread = threading.Thread(target=self._operation_loop) + self._operation_thread.daemon = True + self._operation_thread.start() + + # ==================== 状态属性 ==================== + @property + def status(self) -> str: + return self._status + + @property + def stir_speed(self) -> float: + return self._stir_speed + + @property + def target_stir_speed(self) -> float: + return self._target_stir_speed + + @property + def stir_state(self) -> str: + return self._stir_state + + @property + def vessel(self) -> str: + return self._vessel + + @property + def purpose(self) -> str: + return self._purpose + + @property + def stir_time(self) -> float: + return self._stir_time + + @property + def settling_time(self) -> float: + return self._settling_time + + @property + def max_stir_speed(self) -> float: + return self._max_stir_speed + + @property + def progress(self) -> float: + """返回当前操作的进度(0-100)""" + if not self._running: + return 0.0 + elapsed = (datetime.now() - self._start_time).total_seconds() + total_time = self._stir_time + self._settling_time + if total_time <= 0: + return 100.0 + return min(100.0, (elapsed / total_time) * 100) + + # ==================== Action Server 方法 ==================== + def start_stir(self, vessel: str, stir_speed: float = 0.0, purpose: str = "") -> dict: + """ + StartStir.action 对应的方法 + """ + with self._thread_lock: + if self._running: + return { + "success": False, + "message": "Operation already in progress" + } + + try: + # 重置所有参数 + self._vessel = vessel + self._purpose = purpose + self._stir_time = 0.0 # 连续搅拌模式下不设置搅拌时间 + self._settling_time = 0.0 + self._start_time = datetime.now() # 重置开始时间 + + if stir_speed > 0: + self._target_stir_speed = min(stir_speed, self._max_stir_speed) + + self._stir_state = "Running" + self._status = "Stirring Started" + self._running = True + + return { + "success": True, + "message": "Stirring started successfully" + } + + except Exception as e: + return { + "success": False, + "message": f"Error: {str(e)}" + } + + def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> dict: + """ + Stir.action 对应的方法 + """ + with self._thread_lock: + try: + # 如果已经在运行,先停止当前操作 + if self._running: + self._running = False + self._stir_state = "Stopped" + self._target_stir_speed = 0.0 + time.sleep(0.1) # 给一个短暂的停止时间 + + + # 重置所有参数 + self._stir_time = float(stir_time) + self._settling_time = float(settling_time) + self._target_stir_speed = min(float(stir_speed), self._max_stir_speed) + self._start_time = datetime.now() # 重置开始时间 + self._stir_state = "Running" + self._status = "Stirring" + self._running = True + + return {"success": True} + + except ValueError: + self._status = "Error: Invalid parameters" + return {"success": False} + + def stop_stir(self, vessel: str) -> dict: + """ + StopStir.action 对应的方法 + """ + with self._thread_lock: + if vessel != self._vessel: + return { + "success": False, + "message": "Vessel mismatch" + } + + self._running = False + self._stir_state = "Stopped" + self._target_stir_speed = 0.0 + self._status = "Stirring Stopped" + + return { + "success": True, + "message": "Stirring stopped successfully" + } + + # ==================== 内部控制方法 ==================== + + def _operation_loop(self): + """操作主循环""" + while True: + try: + current_time = datetime.now() + + with self._thread_lock: # 添加锁保护 + if self._stir_state == "Running": + # 实际搅拌逻辑 + speed_diff = self._target_stir_speed - self._stir_speed + if abs(speed_diff) > 0.1: + adjustment = speed_diff * 0.1 + self._stir_speed += adjustment + else: + self._stir_speed = self._target_stir_speed + + # 更新进度 + if self._running: + if self._stir_time > 0: # 定时搅拌模式 + elapsed = (current_time - self._start_time).total_seconds() + if elapsed >= self._stir_time + self._settling_time: + self._running = False + self._stir_state = "Stopped" + self._target_stir_speed = 0.0 + self._stir_speed = 0.0 + self._status = "Stirring Complete" + elif elapsed >= self._stir_time: + self._status = "Settling" + else: # 连续搅拌模式 + self._status = "Stirring" + else: + # 停止状态下慢慢降低速度 + if self._stir_speed > 0: + self._stir_speed = max(0, self._stir_speed - 20.0) + + time.sleep(0.1) + + except Exception as e: + print(f"Error in operation loop: {str(e)}") # 添加错误输出 + self._status = f"Error: {str(e)}" + time.sleep(1.0) # 错误发生时等待较长时间 + + def get_status_info(self) -> dict: + """获取设备状态信息""" + return { + "status": self._status, + "vessel": self._vessel, + "purpose": self._purpose, + "stir_speed": self._stir_speed, + "target_stir_speed": self._target_stir_speed, + "stir_state": self._stir_state, + "stir_time": self._stir_time, # 添加 + "settling_time": self._settling_time, # 添加 + "progress": self.progress, + "max_stir_speed": self._max_stir_speed + } \ No newline at end of file diff --git a/unilabos/devices/mock/mock_vacuum.py b/unilabos/devices/mock/mock_vacuum.py new file mode 100644 index 0000000..9e368a9 --- /dev/null +++ b/unilabos/devices/mock/mock_vacuum.py @@ -0,0 +1,410 @@ +import time +import threading + + +class MockVacuum: + """ + 模拟真空泵设备类 + + 这个类模拟了一个实验室真空泵的行为,包括真空度控制、 + 压力监测、运行状态管理等功能。参考了现有的 VacuumPumpMock 实现。 + """ + + def __init__(self, port: str = "MOCK"): + """ + 初始化MockVacuum实例 + + Args: + port (str): 设备端口,默认为"MOCK"表示模拟设备 + """ + self.port = port + + # 设备基本状态属性 + self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped + self._power_state: str = "Off" # 电源状态:On, Off + self._pump_state: str = "Stopped" # 泵运行状态:Running, Stopped, Paused + + # 真空相关属性 + self._vacuum_level: float = 1013.25 # 当前真空度 (mbar) - 大气压开始 + self._target_vacuum: float = 50.0 # 目标真空度 (mbar) + self._min_vacuum: float = 1.0 # 最小真空度 (mbar) + self._max_vacuum: float = 1013.25 # 最大真空度 (mbar) - 大气压 + + # 泵性能相关属性 + self._pump_speed: float = 0.0 # 泵速 (L/s) + self._max_pump_speed: float = 100.0 # 最大泵速 (L/s) + self._pump_efficiency: float = 95.0 # 泵效率百分比 + + # 运行控制线程 + self._vacuum_thread = None + self._running = False + self._thread_lock = threading.Lock() + + # ==================== 状态属性 ==================== + # 这些属性会被Uni-Lab系统自动识别并定时对外广播 + + @property + def status(self) -> str: + """ + 设备状态 - 会被自动识别的设备属性 + + Returns: + str: 当前设备状态 (Idle, Running, Error, Stopped) + """ + return self._status + + @property + def power_state(self) -> str: + """ + 电源状态 + + Returns: + str: 电源状态 (On, Off) + """ + return self._power_state + + @property + def pump_state(self) -> str: + """ + 泵运行状态 + + Returns: + str: 泵状态 (Running, Stopped, Paused) + """ + return self._pump_state + + @property + def vacuum_level(self) -> float: + """ + 当前真空度 + + Returns: + float: 当前真空度 (mbar) + """ + return self._vacuum_level + + @property + def target_vacuum(self) -> float: + """ + 目标真空度 + + Returns: + float: 目标真空度 (mbar) + """ + return self._target_vacuum + + @property + def pump_speed(self) -> float: + """ + 泵速 + + Returns: + float: 泵速 (L/s) + """ + return self._pump_speed + + @property + def pump_efficiency(self) -> float: + """ + 泵效率 + + Returns: + float: 泵效率百分比 + """ + return self._pump_efficiency + + @property + def max_pump_speed(self) -> float: + """ + 最大泵速 + + Returns: + float: 最大泵速 (L/s) + """ + return self._max_pump_speed + + # ==================== 设备控制方法 ==================== + # 这些方法需要在注册表中添加,会作为ActionServer接受控制指令 + + def power_control(self, power_state: str = "On") -> str: + """ + 电源控制方法 + + Args: + power_state (str): 电源状态,可选值:"On", "Off" + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + if power_state not in ["On", "Off"]: + self._status = "Error: Invalid power state" + return "Error" + + self._power_state = power_state + + if power_state == "On": + self._status = "Power On" + self._start_vacuum_operation() + else: + self._status = "Power Off" + self.stop_vacuum() + + return "Success" + + def set_vacuum_level(self, vacuum_level: float) -> str: + """ + 设置目标真空度 + + Args: + vacuum_level (float): 目标真空度 (mbar) + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + try: + vacuum_level = float(vacuum_level) + except ValueError: + self._status = "Error: Invalid vacuum level" + return "Error" + if self._power_state != "On": + self._status = "Error: Power Off" + return "Error" + + if vacuum_level < self._min_vacuum or vacuum_level > self._max_vacuum: + self._status = f"Error: Vacuum level out of range ({self._min_vacuum}-{self._max_vacuum})" + return "Error" + + self._target_vacuum = vacuum_level + self._status = "Setting Vacuum Level" + + return "Success" + + def start_vacuum(self) -> str: + """ + 启动真空泵 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + if self._power_state != "On": + self._status = "Error: Power Off" + return "Error" + + self._pump_state = "Running" + self._status = "Starting Vacuum Pump" + self._start_vacuum_operation() + + return "Success" + + def stop_vacuum(self) -> str: + """ + 停止真空泵 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._pump_state = "Stopped" + self._status = "Stopping Vacuum Pump" + self._stop_vacuum_operation() + self._pump_speed = 0.0 + + return "Success" + + def pause_vacuum(self) -> str: + """ + 暂停真空泵 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + if self._pump_state != "Running": + self._status = "Error: Pump not running" + return "Error" + + self._pump_state = "Paused" + self._status = "Vacuum Pump Paused" + self._stop_vacuum_operation() + + return "Success" + + def resume_vacuum(self) -> str: + """ + 恢复真空泵运行 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + if self._pump_state != "Paused": + self._status = "Error: Pump not paused" + return "Error" + + if self._power_state != "On": + self._status = "Error: Power Off" + return "Error" + + self._pump_state = "Running" + self._status = "Resuming Vacuum Pump" + self._start_vacuum_operation() + + return "Success" + + def vent_to_atmosphere(self) -> str: + """ + 通大气 - 将真空度恢复到大气压 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._target_vacuum = self._max_vacuum # 设置为大气压 + self._status = "Venting to Atmosphere" + return "Success" + + def emergency_stop(self) -> str: + """ + 紧急停止 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._status = "Emergency Stop" + self._pump_state = "Stopped" + self._stop_vacuum_operation() + self._pump_speed = 0.0 + + return "Success" + + # ==================== 内部控制方法 ==================== + + def _start_vacuum_operation(self): + """ + 启动真空操作线程 + + 这个方法启动一个后台线程来模拟真空泵的实际运行过程。 + """ + with self._thread_lock: + if not self._running and self._power_state == "On": + self._running = True + self._vacuum_thread = threading.Thread(target=self._vacuum_operation_loop) + self._vacuum_thread.daemon = True + self._vacuum_thread.start() + + def _stop_vacuum_operation(self): + """ + 停止真空操作线程 + + 安全地停止后台运行线程并等待其完成。 + """ + with self._thread_lock: + self._running = False + if self._vacuum_thread and self._vacuum_thread.is_alive(): + self._vacuum_thread.join(timeout=2.0) + + def _vacuum_operation_loop(self): + """ + 真空操作主循环 + + 这个方法在后台线程中运行,模拟真空泵的工作过程: + 1. 检查电源状态和运行状态 + 2. 如果泵状态为 "Running",根据目标真空调整泵速和真空度 + 3. 否则等待 + """ + while self._running and self._power_state == "On": + try: + with self._thread_lock: + # 只有泵状态为 Running 时才进行更新 + if self._pump_state == "Running": + vacuum_diff = self._vacuum_level - self._target_vacuum + + if abs(vacuum_diff) < 1.0: # 真空度接近目标值 + self._status = "At Target Vacuum" + self._pump_speed = self._max_pump_speed * 0.2 # 维持真空的最小泵速 + elif vacuum_diff > 0: # 需要抽真空(降低压力) + self._status = "Pumping Down" + if vacuum_diff > 500: + self._pump_speed = self._max_pump_speed + elif vacuum_diff > 100: + self._pump_speed = self._max_pump_speed * 0.8 + elif vacuum_diff > 50: + self._pump_speed = self._max_pump_speed * 0.6 + else: + self._pump_speed = self._max_pump_speed * 0.4 + + # 根据泵速和效率计算真空降幅 + pump_rate = (self._pump_speed / self._max_pump_speed) * self._pump_efficiency / 100.0 + vacuum_reduction = pump_rate * 10.0 # 每秒最大降低10 mbar + self._vacuum_level = max(self._target_vacuum, self._vacuum_level - vacuum_reduction) + else: # 目标真空度高于当前值,需要通气 + self._status = "Venting" + self._pump_speed = 0.0 + self._vacuum_level = min(self._target_vacuum, self._vacuum_level + 5.0) + + # 限制真空度范围 + self._vacuum_level = max(self._min_vacuum, min(self._max_vacuum, self._vacuum_level)) + else: + # 当泵状态不是 Running 时,可保持原状态 + self._status = "Vacuum Pump Not Running" + # 释放锁后等待1秒钟 + time.sleep(1.0) + except Exception as e: + with self._thread_lock: + self._status = f"Error in vacuum operation: {str(e)}" + break + + # 循环结束后的清理工作 + if self._pump_state == "Running": + self._status = "Idle" + # 停止泵后,真空度逐渐回升到大气压 + while self._vacuum_level < self._max_vacuum * 0.9: + with self._thread_lock: + self._vacuum_level += 2.0 + time.sleep(0.1) + + def get_status_info(self) -> dict: + """ + 获取完整的设备状态信息 + + Returns: + dict: 包含所有设备状态的字典 + """ + return { + "status": self._status, + "power_state": self._power_state, + "pump_state": self._pump_state, + "vacuum_level": self._vacuum_level, + "target_vacuum": self._target_vacuum, + "pump_speed": self._pump_speed, + "pump_efficiency": self._pump_efficiency, + "max_pump_speed": self._max_pump_speed, + } + + +# 用于测试的主函数 +if __name__ == "__main__": + vacuum = MockVacuum() + + # 测试基本功能 + print("启动真空泵测试...") + vacuum.power_control("On") + print(f"初始状态: {vacuum.get_status_info()}") + + # 设置目标真空度并启动 + vacuum.set_vacuum_level(10.0) # 设置为10mbar + vacuum.start_vacuum() + + # 模拟运行15秒 + for i in range(15): + time.sleep(1) + print( + f"第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 泵速={vacuum.pump_speed:.1f}L/s, 状态={vacuum.status}" + ) + # 测试通大气 + print("测试通大气...") + vacuum.vent_to_atmosphere() + + # 继续运行5秒观察通大气过程 + for i in range(5): + time.sleep(1) + print(f"通大气第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 状态={vacuum.status}") + + vacuum.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/pump_and_valve/vacuum_pump_mock.py b/unilabos/devices/pump_and_valve/vacuum_pump_mock.py index 96a4842..3565512 100644 --- a/unilabos/devices/pump_and_valve/vacuum_pump_mock.py +++ b/unilabos/devices/pump_and_valve/vacuum_pump_mock.py @@ -12,10 +12,8 @@ class VacuumPumpMock: def get_status(self) -> str: return self._status - def set_status(self, position): - time.sleep(5) - - self._status = position + def set_status(self, string): + self._status = string time.sleep(5) def open(self): diff --git a/unilabos/devices/virtual/__init__.py b/unilabos/devices/virtual/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unilabos/devices/virtual/virtual_centrifuge.py b/unilabos/devices/virtual/virtual_centrifuge.py new file mode 100644 index 0000000..20366ab --- /dev/null +++ b/unilabos/devices/virtual/virtual_centrifuge.py @@ -0,0 +1,158 @@ +import asyncio +import logging +from typing import Dict, Any + +class VirtualCentrifuge: + """Virtual centrifuge device for CentrifugeProtocol testing""" + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + # 处理可能的不同调用方式 + if device_id is None and 'id' in kwargs: + device_id = kwargs.pop('id') + if config is None and 'config' in kwargs: + config = kwargs.pop('config') + + # 设置默认值 + self.device_id = device_id or "unknown_centrifuge" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualCentrifuge.{self.device_id}") + self.data = {} + + # 添加调试信息 + print(f"=== VirtualCentrifuge {self.device_id} is being created! ===") + print(f"=== Config: {self.config} ===") + print(f"=== Kwargs: {kwargs} ===") + + # 从config或kwargs中获取配置参数 + self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') + self._max_speed = self.config.get('max_speed') or kwargs.get('max_speed', 15000.0) + self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 40.0) + self._min_temp = self.config.get('min_temp') or kwargs.get('min_temp', 4.0) + + # 处理其他kwargs参数,但跳过已知的配置参数 + skip_keys = {'port', 'max_speed', 'max_temp', 'min_temp'} + for key, value in kwargs.items(): + if key not in skip_keys and not hasattr(self, key): + setattr(self, key, value) + + async def initialize(self) -> bool: + """Initialize virtual centrifuge""" + print(f"=== VirtualCentrifuge {self.device_id} initialize() called! ===") + self.logger.info(f"Initializing virtual centrifuge {self.device_id}") + self.data.update({ + "status": "Idle", + "current_speed": 0.0, + "target_speed": 0.0, + "current_temp": 25.0, + "target_temp": 25.0, + "max_speed": self._max_speed, + "max_temp": self._max_temp, + "min_temp": self._min_temp, + "centrifuge_state": "Stopped", + "time_remaining": 0.0, + "progress": 0.0, + "message": "" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual centrifuge""" + self.logger.info(f"Cleaning up virtual centrifuge {self.device_id}") + return True + + async def centrifuge(self, vessel: str, speed: float, time: float, temp: float = 25.0) -> bool: + """Execute centrifuge action - matches Centrifuge action""" + self.logger.info(f"Centrifuge: vessel={vessel}, speed={speed} RPM, time={time}s, temp={temp}°C") + + # 验证参数 + if speed > self._max_speed: + self.logger.error(f"Speed {speed} exceeds maximum {self._max_speed}") + self.data["message"] = f"速度 {speed} 超过最大值 {self._max_speed}" + return False + + if temp > self._max_temp or temp < self._min_temp: + self.logger.error(f"Temperature {temp} outside range {self._min_temp}-{self._max_temp}") + self.data["message"] = f"温度 {temp} 超出范围 {self._min_temp}-{self._max_temp}" + return False + + # 开始离心 + self.data.update({ + "status": "Running", + "centrifuge_state": "Centrifuging", + "target_speed": speed, + "current_speed": speed, + "target_temp": temp, + "current_temp": temp, + "time_remaining": time, + "vessel": vessel, + "progress": 0.0, + "message": f"离心中: {vessel} at {speed} RPM" + }) + + # 模拟离心过程 + simulation_time = min(time, 5.0) # 最多等待5秒用于测试 + await asyncio.sleep(simulation_time) + + # 离心完成 + self.data.update({ + "status": "Idle", + "centrifuge_state": "Stopped", + "current_speed": 0.0, + "target_speed": 0.0, + "time_remaining": 0.0, + "progress": 100.0, + "message": f"离心完成: {vessel}" + }) + + self.logger.info(f"Centrifuge completed for vessel {vessel}") + return True + + # 状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def current_speed(self) -> float: + return self.data.get("current_speed", 0.0) + + @property + def target_speed(self) -> float: + return self.data.get("target_speed", 0.0) + + @property + def current_temp(self) -> float: + return self.data.get("current_temp", 25.0) + + @property + def target_temp(self) -> float: + return self.data.get("target_temp", 25.0) + + @property + def max_speed(self) -> float: + return self.data.get("max_speed", self._max_speed) + + @property + def max_temp(self) -> float: + return self.data.get("max_temp", self._max_temp) + + @property + def min_temp(self) -> float: + return self.data.get("min_temp", self._min_temp) + + @property + def centrifuge_state(self) -> str: + return self.data.get("centrifuge_state", "Unknown") + + @property + def time_remaining(self) -> float: + return self.data.get("time_remaining", 0.0) + + @property + def progress(self) -> float: + return self.data.get("progress", 0.0) + + @property + def message(self) -> str: + return self.data.get("message", "") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_column.py b/unilabos/devices/virtual/virtual_column.py new file mode 100644 index 0000000..c83da1c --- /dev/null +++ b/unilabos/devices/virtual/virtual_column.py @@ -0,0 +1,132 @@ +import asyncio +import logging +from typing import Dict, Any, Optional + +class VirtualColumn: + """Virtual column device for RunColumn protocol""" + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + # 处理可能的不同调用方式 + if device_id is None and 'id' in kwargs: + device_id = kwargs.pop('id') + if config is None and 'config' in kwargs: + config = kwargs.pop('config') + + # 设置默认值 + self.device_id = device_id or "unknown_column" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualColumn.{self.device_id}") + self.data = {} + + # 从config或kwargs中获取配置参数 + self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') + self._max_flow_rate = self.config.get('max_flow_rate') or kwargs.get('max_flow_rate', 10.0) + self._column_length = self.config.get('column_length') or kwargs.get('column_length', 25.0) + self._column_diameter = self.config.get('column_diameter') or kwargs.get('column_diameter', 2.0) + + print(f"=== VirtualColumn {self.device_id} created with max_flow_rate={self._max_flow_rate}, length={self._column_length}cm ===") + + async def initialize(self) -> bool: + """Initialize virtual column""" + self.logger.info(f"Initializing virtual column {self.device_id}") + self.data.update({ + "status": "Idle", + "column_state": "Ready", + "current_flow_rate": 0.0, + "max_flow_rate": self._max_flow_rate, + "column_length": self._column_length, + "column_diameter": self._column_diameter, + "processed_volume": 0.0, + "progress": 0.0, + "current_status": "Ready" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual column""" + self.logger.info(f"Cleaning up virtual column {self.device_id}") + return True + + async def run_column(self, from_vessel: str, to_vessel: str, column: str) -> bool: + """Execute column chromatography run - matches RunColumn action""" + self.logger.info(f"Running column separation: {from_vessel} -> {to_vessel} using {column}") + + # 更新设备状态 + self.data.update({ + "status": "Running", + "column_state": "Separating", + "current_status": "Column separation in progress", + "progress": 0.0, + "processed_volume": 0.0 + }) + + # 模拟柱层析分离过程 + # 假设处理时间基于流速和柱子长度 + separation_time = (self._column_length * 2) / self._max_flow_rate # 简化计算 + + steps = 20 # 分20个步骤模拟分离过程 + step_time = separation_time / steps + + for i in range(steps): + await asyncio.sleep(step_time) + + progress = (i + 1) / steps * 100 + volume_processed = (i + 1) * 5.0 # 假设每步处理5mL + + # 更新状态 + self.data.update({ + "progress": progress, + "processed_volume": volume_processed, + "current_status": f"Column separation: {progress:.1f}% - Processing {volume_processed:.1f}mL" + }) + + self.logger.info(f"Column separation progress: {progress:.1f}%") + + # 分离完成 + self.data.update({ + "status": "Idle", + "column_state": "Ready", + "current_status": "Column separation completed", + "progress": 100.0 + }) + + self.logger.info(f"Column separation completed: {from_vessel} -> {to_vessel}") + return True + + # 状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def column_state(self) -> str: + return self.data.get("column_state", "Unknown") + + @property + def current_flow_rate(self) -> float: + return self.data.get("current_flow_rate", 0.0) + + @property + def max_flow_rate(self) -> float: + return self.data.get("max_flow_rate", 0.0) + + @property + def column_length(self) -> float: + return self.data.get("column_length", 0.0) + + @property + def column_diameter(self) -> float: + return self.data.get("column_diameter", 0.0) + + @property + def processed_volume(self) -> float: + return self.data.get("processed_volume", 0.0) + + @property + def progress(self) -> float: + return self.data.get("progress", 0.0) + + @property + def current_status(self) -> str: + return self.data.get("current_status", "Ready") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_filter.py b/unilabos/devices/virtual/virtual_filter.py new file mode 100644 index 0000000..71a984a --- /dev/null +++ b/unilabos/devices/virtual/virtual_filter.py @@ -0,0 +1,151 @@ +import asyncio +import logging +from typing import Dict, Any + +class VirtualFilter: + """Virtual filter device for FilterProtocol testing""" + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + # 处理可能的不同调用方式 + if device_id is None and 'id' in kwargs: + device_id = kwargs.pop('id') + if config is None and 'config' in kwargs: + config = kwargs.pop('config') + + # 设置默认值 + self.device_id = device_id or "unknown_filter" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualFilter.{self.device_id}") + self.data = {} + + # 添加调试信息 + print(f"=== VirtualFilter {self.device_id} is being created! ===") + print(f"=== Config: {self.config} ===") + print(f"=== Kwargs: {kwargs} ===") + + # 从config或kwargs中获取配置参数 + self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') + self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 100.0) + self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0) + + # 处理其他kwargs参数,但跳过已知的配置参数 + skip_keys = {'port', 'max_temp', 'max_stir_speed'} + for key, value in kwargs.items(): + if key not in skip_keys and not hasattr(self, key): + setattr(self, key, value) + + async def initialize(self) -> bool: + """Initialize virtual filter""" + print(f"=== VirtualFilter {self.device_id} initialize() called! ===") + self.logger.info(f"Initializing virtual filter {self.device_id}") + self.data.update({ + "status": "Idle", + "filter_state": "Ready", + "current_temp": 25.0, + "target_temp": 25.0, + "max_temp": self._max_temp, + "stir_speed": 0.0, + "max_stir_speed": self._max_stir_speed, + "filtered_volume": 0.0, + "progress": 0.0, + "message": "" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual filter""" + self.logger.info(f"Cleaning up virtual filter {self.device_id}") + return True + + async def filter_sample(self, 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) -> bool: + """Execute filter action - matches Filter action""" + self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}, stir={stir}, volume={volume}") + + # 验证参数 + if temp > self._max_temp: + self.logger.error(f"Temperature {temp} exceeds maximum {self._max_temp}") + self.data["message"] = f"温度 {temp} 超过最大值 {self._max_temp}" + return False + + if stir and stir_speed > self._max_stir_speed: + self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_stir_speed}") + self.data["message"] = f"搅拌速度 {stir_speed} 超过最大值 {self._max_stir_speed}" + return False + + # 开始过滤 + self.data.update({ + "status": "Running", + "filter_state": "Filtering", + "target_temp": temp, + "current_temp": temp, + "stir_speed": stir_speed if stir else 0.0, + "vessel": vessel, + "filtrate_vessel": filtrate_vessel, + "target_volume": volume, + "progress": 0.0, + "message": f"过滤中: {vessel}" + }) + + # 模拟过滤过程 + simulation_time = min(volume / 10.0 if volume > 0 else 5.0, 10.0) + await asyncio.sleep(simulation_time) + + # 过滤完成 + filtered_vol = volume if volume > 0 else 50.0 # 默认过滤量 + self.data.update({ + "status": "Idle", + "filter_state": "Ready", + "current_temp": 25.0 if not continue_heatchill else temp, + "target_temp": 25.0 if not continue_heatchill else temp, + "stir_speed": 0.0 if not stir else stir_speed, + "filtered_volume": filtered_vol, + "progress": 100.0, + "message": f"过滤完成: {filtered_vol}mL" + }) + + self.logger.info(f"Filter completed: {filtered_vol}mL from {vessel}") + return True + + # 状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def filter_state(self) -> str: + return self.data.get("filter_state", "Unknown") + + @property + def current_temp(self) -> float: + return self.data.get("current_temp", 25.0) + + @property + def target_temp(self) -> float: + return self.data.get("target_temp", 25.0) + + @property + def max_temp(self) -> float: + return self.data.get("max_temp", self._max_temp) + + @property + def stir_speed(self) -> float: + return self.data.get("stir_speed", 0.0) + + @property + def max_stir_speed(self) -> float: + return self.data.get("max_stir_speed", self._max_stir_speed) + + @property + def filtered_volume(self) -> float: + return self.data.get("filtered_volume", 0.0) + + @property + def progress(self) -> float: + return self.data.get("progress", 0.0) + + @property + def message(self) -> str: + return self.data.get("message", "") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_heatchill.py b/unilabos/devices/virtual/virtual_heatchill.py new file mode 100644 index 0000000..98a03ce --- /dev/null +++ b/unilabos/devices/virtual/virtual_heatchill.py @@ -0,0 +1,107 @@ +import asyncio +import logging +from typing import Dict, Any + +class VirtualHeatChill: + """Virtual heat chill device for HeatChillProtocol testing""" + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + # 处理可能的不同调用方式 + if device_id is None and 'id' in kwargs: + device_id = kwargs.pop('id') + if config is None and 'config' in kwargs: + config = kwargs.pop('config') + + # 设置默认值 + self.device_id = device_id or "unknown_heatchill" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualHeatChill.{self.device_id}") + self.data = {} + + # 添加调试信息 + print(f"=== VirtualHeatChill {self.device_id} is being created! ===") + print(f"=== Config: {self.config} ===") + print(f"=== Kwargs: {kwargs} ===") + + # 从config或kwargs中获取配置参数 + self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') + self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 200.0) + self._min_temp = self.config.get('min_temp') or kwargs.get('min_temp', -80.0) + self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0) + + # 处理其他kwargs参数,但跳过已知的配置参数 + skip_keys = {'port', 'max_temp', 'min_temp', 'max_stir_speed'} + for key, value in kwargs.items(): + if key not in skip_keys and not hasattr(self, key): + setattr(self, key, value) + + async def initialize(self) -> bool: + """Initialize virtual heat chill""" + print(f"=== VirtualHeatChill {self.device_id} initialize() called! ===") + self.logger.info(f"Initializing virtual heat chill {self.device_id}") + self.data.update({ + "status": "Idle" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual heat chill""" + self.logger.info(f"Cleaning up virtual heat chill {self.device_id}") + return True + + async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool, + stir_speed: float, purpose: str) -> bool: + """Execute heat chill action - matches HeatChill action exactly""" + self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time}s, stir={stir}, stir_speed={stir_speed}, purpose={purpose}") + + # 验证参数 + if temp > self._max_temp or temp < self._min_temp: + self.logger.error(f"Temperature {temp} outside range {self._min_temp}-{self._max_temp}") + self.data["status"] = f"温度 {temp} 超出范围" + return False + + if stir and stir_speed > self._max_stir_speed: + self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_stir_speed}") + self.data["status"] = f"搅拌速度 {stir_speed} 超出范围" + return False + + # 开始加热/冷却 + self.data.update({ + "status": f"加热/冷却中: {vessel} 至 {temp}°C" + }) + + # 模拟加热/冷却时间 + simulation_time = min(time, 10.0) # 最多等待10秒用于测试 + await asyncio.sleep(simulation_time) + + # 加热/冷却完成 + self.data["status"] = f"完成: {vessel} 已达到 {temp}°C" + + self.logger.info(f"HeatChill completed for vessel {vessel} at {temp}°C") + return True + + async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool: + """Start heat chill - matches HeatChillStart action exactly""" + self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C, purpose={purpose}") + + # 验证参数 + if temp > self._max_temp or temp < self._min_temp: + self.logger.error(f"Temperature {temp} outside range {self._min_temp}-{self._max_temp}") + self.data["status"] = f"温度 {temp} 超出范围" + return False + + self.data["status"] = f"开始加热/冷却: {vessel} 至 {temp}°C" + return True + + async def heat_chill_stop(self, vessel: str) -> bool: + """Stop heat chill - matches HeatChillStop action exactly""" + self.logger.info(f"HeatChillStop: vessel={vessel}") + + self.data["status"] = f"停止加热/冷却: {vessel}" + return True + + # 状态属性 - 只保留 action 中定义的 feedback + @property + def status(self) -> str: + return self.data.get("status", "Idle") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_pump.py b/unilabos/devices/virtual/virtual_pump.py new file mode 100644 index 0000000..d134319 --- /dev/null +++ b/unilabos/devices/virtual/virtual_pump.py @@ -0,0 +1,197 @@ +import asyncio +import logging +from typing import Dict, Any, Optional + +class VirtualPump: + """Virtual pump device for transfer and cleaning operations""" + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + # 处理可能的不同调用方式 + if device_id is None and 'id' in kwargs: + device_id = kwargs.pop('id') + if config is None and 'config' in kwargs: + config = kwargs.pop('config') + + # 设置默认值 + self.device_id = device_id or "unknown_pump" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualPump.{self.device_id}") + self.data = {} + + # 从config或kwargs中获取配置参数 + self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') + self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 50.0) + self._transfer_rate = self.config.get('transfer_rate') or kwargs.get('transfer_rate', 10.0) + + print(f"=== VirtualPump {self.device_id} created with max_volume={self._max_volume}, transfer_rate={self._transfer_rate} ===") + + async def initialize(self) -> bool: + """Initialize virtual pump""" + self.logger.info(f"Initializing virtual pump {self.device_id}") + self.data.update({ + "status": "Idle", + "valve_position": 0, + "current_volume": 0.0, + "max_volume": self._max_volume, + "transfer_rate": self._transfer_rate, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual pump""" + self.logger.info(f"Cleaning up virtual pump {self.device_id}") + return True + + async def transfer(self, from_vessel: str, to_vessel: str, volume: float, + amount: str = "", time: float = 0.0, viscous: bool = False, + rinsing_solvent: str = "", rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, solid: bool = False) -> bool: + """Execute transfer operation""" + self.logger.info(f"Transferring {volume}mL from {from_vessel} to {to_vessel}") + + # 计算转移时间 + transfer_time = volume / self._transfer_rate if time == 0 else time + + self.data.update({ + "status": "Running", + "from_vessel": from_vessel, + "to_vessel": to_vessel, + "current_status": "Transferring", + "progress": 0.0, + "transferred_volume": 0.0 + }) + + # 模拟转移过程 + steps = 10 + step_time = transfer_time / steps + step_volume = volume / steps + + for i in range(steps): + await asyncio.sleep(step_time) + progress = (i + 1) / steps * 100 + current_volume = step_volume * (i + 1) + + self.data.update({ + "progress": progress, + "transferred_volume": current_volume, + "current_status": f"Transferring: {progress:.1f}%" + }) + + self.logger.info(f"Transfer progress: {progress:.1f}%") + + self.data.update({ + "status": "Idle", + "current_status": "Transfer completed", + "progress": 100.0, + "transferred_volume": volume + }) + + return True + + async def clean_vessel(self, vessel: str, solvent: str, volume: float, + temp: float, repeats: int = 1) -> bool: + """Execute vessel cleaning operation - matches CleanVessel action""" + self.logger.info(f"Starting vessel cleaning: {vessel} with {solvent} ({volume}mL at {temp}°C, {repeats} repeats)") + + # 更新设备状态 + self.data.update({ + "status": "Running", + "from_vessel": f"flask_{solvent}", + "to_vessel": vessel, + "current_status": "Cleaning in progress", + "progress": 0.0, + "transferred_volume": 0.0 + }) + + # 计算清洗时间(基于体积和重复次数) + # 假设清洗速度为 transfer_rate 的一半(因为需要加载和排放) + cleaning_rate = self._transfer_rate / 2 + cleaning_time_per_cycle = volume / cleaning_rate + total_cleaning_time = cleaning_time_per_cycle * repeats + + # 模拟清洗过程 + steps_per_repeat = 10 # 每次重复清洗分10个步骤 + total_steps = steps_per_repeat * repeats + step_time = total_cleaning_time / total_steps + + for repeat in range(repeats): + self.logger.info(f"Starting cleaning cycle {repeat + 1}/{repeats}") + + for step in range(steps_per_repeat): + await asyncio.sleep(step_time) + + # 计算当前进度 + current_step = repeat * steps_per_repeat + step + 1 + progress = (current_step / total_steps) * 100 + + # 计算已处理的体积 + volume_processed = (current_step / total_steps) * volume * repeats + + # 更新状态 + self.data.update({ + "progress": progress, + "transferred_volume": volume_processed, + "current_status": f"Cleaning cycle {repeat + 1}/{repeats} - Step {step + 1}/{steps_per_repeat} ({progress:.1f}%)" + }) + + self.logger.info(f"Cleaning progress: {progress:.1f}% (Cycle {repeat + 1}/{repeats})") + + # 清洗完成 + self.data.update({ + "status": "Idle", + "current_status": "Cleaning completed successfully", + "progress": 100.0, + "transferred_volume": volume * repeats, + "from_vessel": "", + "to_vessel": "" + }) + + self.logger.info(f"Vessel cleaning completed: {vessel}") + return True + + # 状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def valve_position(self) -> int: + return self.data.get("valve_position", 0) + + @property + def current_volume(self) -> float: + return self.data.get("current_volume", 0.0) + + @property + def max_volume(self) -> float: + return self.data.get("max_volume", 0.0) + + @property + def transfer_rate(self) -> float: + return self.data.get("transfer_rate", 0.0) + + @property + def from_vessel(self) -> str: + return self.data.get("from_vessel", "") + + @property + def to_vessel(self) -> str: + return self.data.get("to_vessel", "") + + @property + def progress(self) -> float: + return self.data.get("progress", 0.0) + + @property + def transferred_volume(self) -> float: + return self.data.get("transferred_volume", 0.0) + + @property + def current_status(self) -> str: + return self.data.get("current_status", "Ready") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_stirrer.py b/unilabos/devices/virtual/virtual_stirrer.py new file mode 100644 index 0000000..b1a4098 --- /dev/null +++ b/unilabos/devices/virtual/virtual_stirrer.py @@ -0,0 +1,104 @@ +import asyncio +import logging +from typing import Dict, Any + +class VirtualStirrer: + """Virtual stirrer device for StirProtocol testing""" + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + # 处理可能的不同调用方式 + if device_id is None and 'id' in kwargs: + device_id = kwargs.pop('id') + if config is None and 'config' in kwargs: + config = kwargs.pop('config') + + # 设置默认值 + self.device_id = device_id or "unknown_stirrer" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualStirrer.{self.device_id}") + self.data = {} + + # 添加调试信息 + print(f"=== VirtualStirrer {self.device_id} is being created! ===") + print(f"=== Config: {self.config} ===") + print(f"=== Kwargs: {kwargs} ===") + + # 从config或kwargs中获取配置参数 + self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') + self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 100.0) + self._max_speed = self.config.get('max_speed') or kwargs.get('max_speed', 1000.0) + + # 处理其他kwargs参数,但跳过已知的配置参数 + skip_keys = {'port', 'max_temp', 'max_speed'} + for key, value in kwargs.items(): + if key not in skip_keys and not hasattr(self, key): + setattr(self, key, value) + + async def initialize(self) -> bool: + """Initialize virtual stirrer""" + print(f"=== VirtualStirrer {self.device_id} initialize() called! ===") + self.logger.info(f"Initializing virtual stirrer {self.device_id}") + self.data.update({ + "status": "Idle" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual stirrer""" + self.logger.info(f"Cleaning up virtual stirrer {self.device_id}") + return True + + async def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> bool: + """Execute stir action - matches Stir action exactly""" + self.logger.info(f"Stir: speed={stir_speed} RPM, time={stir_time}s, settling={settling_time}s") + + # 验证参数 + if stir_speed > self._max_speed: + self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_speed}") + self.data["status"] = f"搅拌速度 {stir_speed} 超出范围" + return False + + # 开始搅拌 + self.data["status"] = f"搅拌中: {stir_speed} RPM, {stir_time}s" + + # 模拟搅拌时间 + simulation_time = min(stir_time, 10.0) # 最多等待10秒用于测试 + await asyncio.sleep(simulation_time) + + # 搅拌完成,开始沉降 + if settling_time > 0: + self.data["status"] = f"沉降中: {settling_time}s" + settling_simulation = min(settling_time, 5.0) # 最多等待5秒 + await asyncio.sleep(settling_simulation) + + # 操作完成 + self.data["status"] = "搅拌完成" + + self.logger.info(f"Stir completed: {stir_speed} RPM for {stir_time}s") + return True + + async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool: + """Start stir action - matches StartStir action exactly""" + self.logger.info(f"StartStir: vessel={vessel}, speed={stir_speed} RPM, purpose={purpose}") + + # 验证参数 + if stir_speed > self._max_speed: + self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_speed}") + self.data["status"] = f"搅拌速度 {stir_speed} 超出范围" + return False + + self.data["status"] = f"开始搅拌: {vessel} at {stir_speed} RPM" + return True + + async def stop_stir(self, vessel: str) -> bool: + """Stop stir action - matches StopStir action exactly""" + self.logger.info(f"StopStir: vessel={vessel}") + + self.data["status"] = f"停止搅拌: {vessel}" + return True + + # 状态属性 - 只保留 action 中定义的 feedback + @property + def status(self) -> str: + return self.data.get("status", "Idle") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_transferpump.py b/unilabos/devices/virtual/virtual_transferpump.py new file mode 100644 index 0000000..87d4cff --- /dev/null +++ b/unilabos/devices/virtual/virtual_transferpump.py @@ -0,0 +1,149 @@ +import asyncio +import logging +from typing import Dict, Any, Optional + +class VirtualTransferPump: + """Virtual pump device specifically for Transfer protocol""" + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + # 处理可能的不同调用方式 + if device_id is None and 'id' in kwargs: + device_id = kwargs.pop('id') + if config is None and 'config' in kwargs: + config = kwargs.pop('config') + + # 设置默认值 + self.device_id = device_id or "unknown_transfer_pump" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}") + self.data = {} + + # 添加调试信息 + print(f"=== VirtualTransferPump {self.device_id} is being created! ===") + print(f"=== Config: {self.config} ===") + print(f"=== Kwargs: {kwargs} ===") + + # 从config或kwargs中获取配置参数 + self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') + self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 50.0) + self._transfer_rate = self.config.get('transfer_rate') or kwargs.get('transfer_rate', 5.0) + self._current_volume = 0.0 + self.is_running = False + + async def initialize(self) -> bool: + """Initialize virtual transfer pump""" + print(f"=== VirtualTransferPump {self.device_id} initialize() called! ===") + self.logger.info(f"Initializing virtual transfer pump {self.device_id}") + self.data.update({ + "status": "Idle", + "current_volume": 0.0, + "max_volume": self._max_volume, + "transfer_rate": self._transfer_rate, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual transfer pump""" + self.logger.info(f"Cleaning up virtual transfer pump {self.device_id}") + return True + + async def transfer(self, 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) -> bool: + """Execute liquid transfer - matches Transfer action""" + self.logger.info(f"Transfer: {volume}mL from {from_vessel} to {to_vessel}") + + # 计算转移时间 + if time > 0: + transfer_time = time + else: + # 如果是粘性液体,降低转移速率 + rate = self._transfer_rate * 0.5 if viscous else self._transfer_rate + transfer_time = volume / rate + + self.data.update({ + "status": "Running", + "from_vessel": from_vessel, + "to_vessel": to_vessel, + "current_status": "Transferring", + "progress": 0.0, + "transferred_volume": 0.0 + }) + + # 模拟转移过程 + steps = 10 + step_time = transfer_time / steps + step_volume = volume / steps + + for i in range(steps): + await asyncio.sleep(step_time) + progress = (i + 1) / steps * 100 + transferred = (i + 1) * step_volume + + self.data.update({ + "progress": progress, + "transferred_volume": transferred, + "current_status": f"Transferring {progress:.1f}%" + }) + + self.logger.info(f"Transfer progress: {progress:.1f}% ({transferred:.1f}/{volume}mL)") + + # 如果需要冲洗 + if rinsing_solvent and rinsing_volume > 0 and rinsing_repeats > 0: + self.data["current_status"] = "Rinsing" + for repeat in range(rinsing_repeats): + self.logger.info(f"Rinsing cycle {repeat + 1}/{rinsing_repeats} with {rinsing_solvent}") + await asyncio.sleep(1) # 模拟冲洗时间 + + self.data.update({ + "status": "Idle", + "current_status": "Transfer completed", + "progress": 100.0, + "transferred_volume": volume + }) + + return True + + # 添加所有在virtual_device.yaml中定义的状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def current_volume(self) -> float: + return self.data.get("current_volume", 0.0) + + @property + def max_volume(self) -> float: + return self.data.get("max_volume", self._max_volume) + + @property + def transfer_rate(self) -> float: + return self.data.get("transfer_rate", self._transfer_rate) + + @property + def from_vessel(self) -> str: + return self.data.get("from_vessel", "") + + @property + def to_vessel(self) -> str: + return self.data.get("to_vessel", "") + + @property + def progress(self) -> float: + return self.data.get("progress", 0.0) + + @property + def transferred_volume(self) -> float: + return self.data.get("transferred_volume", 0.0) + + @property + def current_status(self) -> str: + return self.data.get("current_status", "Ready") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_valve.py b/unilabos/devices/virtual/virtual_valve.py new file mode 100644 index 0000000..a665e00 --- /dev/null +++ b/unilabos/devices/virtual/virtual_valve.py @@ -0,0 +1,105 @@ +import asyncio +import logging +from typing import Dict, Any + +class VirtualValve: + """Virtual valve device for AddProtocol testing""" + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + # 处理可能的不同调用方式 + if device_id is None and 'id' in kwargs: + device_id = kwargs.pop('id') + if config is None and 'config' in kwargs: + config = kwargs.pop('config') + + # 设置默认值 + self.device_id = device_id or "unknown_valve" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualValve.{self.device_id}") + self.data = {} + + print(f"=== VirtualValve {self.device_id} is being created! ===") + print(f"=== Config: {self.config} ===") + print(f"=== Kwargs: {kwargs} ===") + + # 处理所有配置参数,包括port + self.port = self.config.get('port', 'VIRTUAL') + self.positions = self.config.get('positions', 6) + self.current_position = 0 + + # 忽略其他可能的kwargs参数 + for key, value in kwargs.items(): + if not hasattr(self, key): + setattr(self, key, value) + + async def initialize(self) -> bool: + """Initialize virtual valve""" + print(f"=== VirtualValve {self.device_id} initialize() called! ===") + self.logger.info(f"Initializing virtual valve {self.device_id}") + self.data.update({ + "status": "Idle", + "valve_state": "Closed", + "current_position": 0, + "target_position": 0, + "max_positions": self.positions + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual valve""" + self.logger.info(f"Cleaning up virtual valve {self.device_id}") + return True + + async def set_position(self, position: int) -> bool: + """Set valve position - matches SendCmd action""" + if 0 <= position <= self.positions: + self.logger.info(f"Setting valve position to {position}") + self.data.update({ + "target_position": position, + "current_position": position, + "valve_state": "Open" if position > 0 else "Closed" + }) + return True + else: + self.logger.error(f"Invalid position {position}. Must be 0-{self.positions}") + return False + + async def open(self) -> bool: + """Open valve - matches EmptyIn action""" + self.logger.info("Opening valve") + self.data.update({ + "valve_state": "Open", + "current_position": 1 + }) + return True + + async def close(self) -> bool: + """Close valve - matches EmptyIn action""" + self.logger.info("Closing valve") + self.data.update({ + "valve_state": "Closed", + "current_position": 0 + }) + return True + + # 状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def valve_state(self) -> str: + return self.data.get("valve_state", "Unknown") + + @property + def current_position(self) -> int: + return self.data.get("current_position", 0) + + @property + def target_position(self) -> int: + return self.data.get("target_position", 0) + + @property + def max_positions(self) -> int: + return self.data.get("max_positions", 6) \ No newline at end of file diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py index 7bff6dc..883b9ad 100644 --- a/unilabos/messages/__init__.py +++ b/unilabos/messages/__init__.py @@ -67,7 +67,115 @@ class AGVTransferProtocol(BaseModel): to_repo: dict from_repo_position: str to_repo_position: str +#=============新添加的新的协议================ +class AddProtocol(BaseModel): + vessel: str + reagent: str + volume: float + mass: float + amount: str + time: float + stir: bool + stir_speed: float + viscous: bool + purpose: str +class CentrifugeProtocol(BaseModel): + vessel: str + speed: float + time: float + temp: float # 移除默认值 -__all__ = ["Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", "EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol"] +class FilterProtocol(BaseModel): + vessel: str + filtrate_vessel: str # 移除默认值 + stir: bool # 移除默认值 + stir_speed: float # 移除默认值 + temp: float # 移除默认值 + continue_heatchill: bool # 移除默认值 + volume: float # 移除默认值 + +class HeatChillProtocol(BaseModel): + vessel: str + temp: float + time: float + 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 + 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 + +class CleanVesselProtocol(BaseModel): + vessel: str # 要清洗的容器名称 + solvent: str # 用于清洗容器的溶剂名称 + volume: float # 清洗溶剂的体积,可选参数 + temp: float # 清洗时的温度,可选参数 + repeats: int = 1 # 清洗操作的重复次数,默认为 1 + +class DissolveProtocol(BaseModel): + vessel: str # 装有要溶解物质的容器名称 + solvent: str # 用于溶解物质的溶剂名称 + volume: float # 溶剂的体积,可选参数 + amount: str = "" # 要溶解物质的量,可选参数 + temp: float = 25.0 # 溶解时的温度,可选参数 + time: float = 0.0 # 溶解的时间,可选参数 + 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 # 洗脱操作的重复次数,默认为 0 + residence_time: float = 0.0 # 物质在过滤介质中的停留时间,可选参数 + +class RunColumnProtocol(BaseModel): + from_vessel: str # 源容器的名称,即样品起始所在的容器 + to_vessel: str # 目标容器的名称,分离后的样品要到达的容器 + column: str # 所使用的柱子的名称 + +class WashSolidProtocol(BaseModel): + vessel: str # 装有固体物质的容器名称 + solvent: str # 用于清洗固体的溶剂名称 + volume: float # 清洗溶剂的体积 + filtrate_vessel: str = "" # 滤液要收集到的容器名称,可选参数 + temp: float = 25.0 # 清洗时的温度,可选参数 + stir: bool = False # 是否在清洗过程中搅拌,默认为 False + stir_speed: float = 0.0 # 搅拌速度,可选参数 + time: float = 0.0 # 清洗的时间,可选参数 + repeats: int = 1 # 清洗操作的重复次数,默认为 1 + +__all__ = ["Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", "EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol", "CentrifugeProtocol", "AddProtocol", "FilterProtocol", "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol", "StirProtocol", "StartStirProtocol", "StopStirProtocol", "TransferProtocol", "CleanVesselProtocol", "DissolveProtocol", "FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol"] # End Protocols diff --git a/unilabos/registry/devices/mock_devices.yaml b/unilabos/registry/devices/mock_devices.yaml new file mode 100644 index 0000000..5976046 --- /dev/null +++ b/unilabos/registry/devices/mock_devices.yaml @@ -0,0 +1,892 @@ +mock_chiller: + description: Mock Chiller Device + class: + module: unilabos.devices.mock.mock_chiller:MockChiller + type: python + status_types: + current_temperature: Float64 + target_temperature: Float64 + status: String + is_cooling: Bool + is_heating: Bool + vessel: String # 新增 + purpose: String # 新增 + action_value_mappings: + emergency_stop: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + heat_chill_start: + type: HeatChillStart + goal: + vessel: vessel + temp: temp + purpose: purpose + feedback: {} + result: + success: success + status: status + heat_chill_stop: + type: HeatChillStop + goal: + vessel: vessel + feedback: {} + result: + success: success + status: status + schema: + type: object + properties: + current_temperature: + type: number + description: Current temperature of the chiller in °C + target_temperature: + type: number + description: Target temperature setting in °C + status: + type: string + description: Current status of the device + is_cooling: + type: boolean + description: Whether the device is actively cooling + is_heating: + type: boolean + description: Whether the device is actively heating + vessel: # 新增 + type: string + description: Current vessel being processed + purpose: # 新增 + type: string + description: Purpose of the current operation + required: + - current_temperature + - target_temperature + - status + - vessel + - purpose + additionalProperties: false +mock_filter: + description: Mock Filter Device + class: + module: unilabos.devices.mock.mock_filter:MockFilter + type: python + status_types: + status: String + is_filtering: Bool + flow_rate: Float64 + pressure_drop: Float64 + filter_life: Float64 + vessel: String + filtrate_vessel: String + filtered_volume: Float64 + progress: Float64 + stir: Bool + stir_speed: Float64 + temperature: Float64 + continue_heatchill: Bool + target_volume: Float64 + action_value_mappings: + filter: + type: ProtocolFilter + goal: + vessel: vessel + filtrate_vessel: filtrate_vessel + stir: stir + stir_speed: stir_speed + temp: temp + continue_heatchill: continue_heatchill + volume: volume + feedback: + progress: progress + current_temp: current_temp + filtered_volume: filtered_volume + current_status: current_status + result: + success: success + message: message + stop_filtering: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + replace_filter: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the filter + is_filtering: + type: boolean + description: Whether the filter is actively filtering + flow_rate: + type: number + description: Current flow rate in L/min + pressure_drop: + type: number + description: Pressure drop across the filter in Pa + filter_life: + type: number + description: Remaining filter life percentage + power_on: + type: boolean + description: Power state of the device + required: + - status + - is_filtering + - flow_rate + - filter_life + - vessel + - filtrate_vessel + - filtered_volume + - progress + additionalProperties: false +mock_heater: + description: Mock Heater Device + class: + module: unilabos.devices.mock.mock_heater:MockHeater + type: python + status_types: + current_temperature: Float64 + target_temperature: Float64 + status: String + is_heating: Bool + heating_power: Float64 + max_temperature: Float64 + vessel: String + purpose: String + stir: Bool + stir_speed: Float64 + action_value_mappings: + heat_chill_start: + type: HeatChillStart + goal: + vessel: vessel + temp: temp + purpose: purpose + feedback: + status: status + result: + success: success + heat_chill_stop: + type: HeatChillStop + goal: + vessel: vessel + feedback: + status: status + result: + success: success + heat_chill: + type: HeatChill + goal: + vessel: vessel + temp: temp + time: time + stir: stir + stir_speed: stir_speed + purpose: purpose + feedback: + status: status + result: + success: success + emergency_stop: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + current_temperature: + type: number + description: Current temperature of the heater in °C + target_temperature: + type: number + description: Target temperature setting in °C + status: + type: string + description: Current status of the device + is_heating: + type: boolean + description: Whether the device is actively heating + heating_power: + type: number + description: Current heating power percentage + max_temperature: + type: number + description: Maximum temperature limit + vessel: + type: string + description: Current vessel being heated + purpose: + type: string + description: Purpose of the heating operation + stir: + type: boolean + description: Whether stirring is enabled + stir_speed: + type: number + description: Current stirring speed + required: + - current_temperature + - target_temperature + - status + - vessel + - purpose + additionalProperties: false +mock_pump: + description: Mock Pump Device + class: + module: unilabos.devices.mock.mock_pump:MockPump + type: python + status_types: + status: String + pump_state: String + flow_rate: Float64 + target_flow_rate: Float64 + pressure: Float64 + total_volume: Float64 + max_flow_rate: Float64 + max_pressure: Float64 + from_vessel: String + to_vessel: String + transfer_volume: Float64 + amount: String + transfer_time: Float64 + is_viscous: Bool + rinsing_solvent: String + rinsing_volume: Float64 + rinsing_repeats: Int32 + is_solid: Bool + time_spent: Float64 + time_remaining: Float64 + current_device: String + action_value_mappings: + pump_transfer: + type: PumpTransfer + goal: + from_vessel: from_vessel + to_vessel: to_vessel + volume: volume + amount: amount + time: time + viscous: viscous + rinsing_solvent: rinsing_solvent + rinsing_volume: rinsing_volume + rinsing_repeats: rinsing_repeats + solid: solid + feedback: + status: status + current_device: current_device + time_spent: time_spent + time_remaining: time_remaining + result: + success: success + pause_pump: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + resume_pump: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + reset_volume_counter: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the pump + pump_state: + type: string + description: Pump operation state (Running/Stopped/Paused) + flow_rate: + type: number + description: Current flow rate in mL/min + target_flow_rate: + type: number + description: Target flow rate in mL/min + pressure: + type: number + description: Current pressure in bar + total_volume: + type: number + description: Total accumulated volume in mL + max_flow_rate: + type: number + description: Maximum flow rate in mL/min + max_pressure: + type: number + description: Maximum pressure in bar + from_vessel: + type: string + description: Source vessel for transfer + to_vessel: + type: string + description: Target vessel for transfer + transfer_volume: + type: number + description: Volume to transfer in mL + amount: + type: string + description: Amount description + transfer_time: + type: number + description: Transfer time in seconds + is_viscous: + type: boolean + description: Whether the liquid is viscous + rinsing_solvent: + type: string + description: Solvent used for rinsing + rinsing_volume: + type: number + description: Volume used for rinsing + rinsing_repeats: + type: integer + description: Number of rinsing cycles + is_solid: + type: boolean + description: Whether transferring solid material + current_device: + type: string + description: Current device identifier + required: + - status + - pump_state + - flow_rate + - from_vessel + - to_vessel + additionalProperties: false +mock_rotavap: + description: Mock Rotavap Device + class: + module: unilabos.devices.mock.mock_rotavap:MockRotavap + type: python + status_types: + status: String + rotate_state: String + rotate_time: Float64 + rotate_speed: Float64 + pump_state: String + pump_time: Float64 + vacuum_level: Float64 + temperature: Float64 + target_temperature: Float64 + success: String + action_value_mappings: + set_timer: + type: StrSingleInput + goal: + string: command + feedback: {} + result: + success: success + set_rotate_time: + type: FloatSingleInput + goal: + float_in: time_seconds + feedback: {} + result: + success: success + set_pump_time: + type: FloatSingleInput + goal: + float_in: time_seconds + feedback: {} + result: + success: success + set_rotate_speed: + type: FloatSingleInput + goal: + float_in: speed + feedback: {} + result: + success: success + set_temperature: + type: FloatSingleInput + goal: + float_in: temperature + feedback: {} + result: + success: success + start_rotation: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + start_pump: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the rotavap + rotate_state: + type: string + description: Rotation state (Running/Stopped) + rotate_time: + type: number + description: Remaining rotation time in seconds + rotate_speed: + type: number + description: Rotation speed in rpm + pump_state: + type: string + description: Pump state (Running/Stopped) + pump_time: + type: number + description: Remaining pump time in seconds + vacuum_level: + type: number + description: Current vacuum level in mbar + temperature: + type: number + description: Current water bath temperature + target_temperature: + type: number + description: Target water bath temperature + success: + type: string + description: Operation success status + required: + - status + - rotate_time + - pump_time + - temperature + additionalProperties: false +mock_separator: + description: Simplified Mock Separator Device + class: + module: unilabos.devices.mock.mock_separator:MockSeparator + type: python + status_types: + status: String + settling_time: Float64 + valve_state: String + shake_time: Float64 + shake_status: String + current_device: String + purpose: String + product_phase: String + from_vessel: String + separation_vessel: String + to_vessel: String + waste_phase_to_vessel: String + solvent: String + solvent_volume: Float64 + through: String + repeats: Int32 + stir_time: Float64 + stir_speed: Float64 + time_spent: Float64 + time_remaining: Float64 + action_value_mappings: + separate: + type: Separate + goal: + purpose: purpose + product_phase: product_phase + from_vessel: from_vessel + separation_vessel: separation_vessel + to_vessel: to_vessel + waste_phase_to_vessel: waste_phase_to_vessel + solvent: solvent + solvent_volume: solvent_volume + through: through + repeats: repeats + stir_time: stir_time + stir_speed: stir_speed + settling_time: settling_time + feedback: + status: status + current_device: current_device + time_spent: time_spent + time_remaining: time_remaining + result: + success: success + shake: + type: FloatSingleInput + goal: + float_in: shake_time + feedback: + status: status + result: + success: success + stop_operations: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + set_valve: + type: StrSingleInput + goal: + string: command + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the separator + settling_time: + type: number + description: Settling time in seconds + valve_state: + type: string + description: Valve state (Open/Closed) + shake_time: + type: number + description: Remaining shake time in seconds + shake_status: + type: string + description: Current shake state + purpose: + type: string + description: Separation purpose (wash/extract) + product_phase: + type: string + description: Product phase (top/bottom) + from_vessel: + type: string + description: Source vessel + separation_vessel: + type: string + description: Vessel for separation + to_vessel: + type: string + description: Target vessel + required: + - status + - valve_state + - shake_status + - current_device + additionalProperties: false +mock_solenoid_valve: + description: Mock Solenoid Valve Device + class: + module: unilabos.devices.mock.mock_solenoid_valve:MockSolenoidValve + type: python + status_types: + status: String + valve_status: String + action_value_mappings: + set_valve_status: + type: StrSingleInput + goal: + string: status + feedback: {} + result: + success: success + open_valve: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + close_valve: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the valve + valve_status: + type: string + description: Valve status (Open/Closed) + required: + - status + - valve_status + additionalProperties: false +mock_stirrer: + description: Mock Stirrer Device + class: + module: unilabos.devices.mock.mock_stirrer:MockStirrer + type: python + status_types: + status: String + stir_speed: Float64 + target_stir_speed: Float64 + stir_state: String + temperature: Float64 + target_temperature: Float64 + heating_state: String + heating_power: Float64 + max_stir_speed: Float64 + max_temperature: Float64 + action_value_mappings: + set_stir_speed: + type: FloatSingleInput + goal: + float_in: speed + feedback: {} + result: + success: success + set_temperature: + type: FloatSingleInput + goal: + float_in: temperature + feedback: {} + result: + success: success + start_stirring: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + stop_stirring: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + heating_control: + type: StrSingleInput + goal: + string: heating_state + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the stirrer + stir_speed: + type: number + description: Current stirring speed in rpm + target_stir_speed: + type: number + description: Target stirring speed in rpm + stir_state: + type: string + description: Stirring state (Running/Stopped) + temperature: + type: number + description: Current temperature in °C + target_temperature: + type: number + description: Target temperature in °C + heating_state: + type: string + description: Heating state (On/Off) + heating_power: + type: number + description: Current heating power percentage + max_stir_speed: + type: number + description: Maximum stirring speed in rpm + max_temperature: + type: number + description: Maximum temperature in °C + required: + - status + - stir_speed + - temperature + - power_state + additionalProperties: false +mock_stirrer_new: + description: Mock Stirrer Device (Copy Version) + class: + module: unilabos.devices.mock.mock_stirrer_new:MockStirrer_new + type: python + status_types: + status: String + vessel: String + purpose: String + stir_speed: Float64 + target_stir_speed: Float64 + stir_state: String + stir_time: Float64 + settling_time: Float64 + progress: Float64 + max_stir_speed: Float64 + action_value_mappings: + start_stir: + type: ProtocolStartStir + goal: + vessel: vessel + stir_speed: stir_speed + purpose: purpose + feedback: + progress: progress + current_speed: stir_speed + current_status: status + result: + success: success + message: message + stir: + type: Stir + goal: + stir_time: stir_time + stir_speed: stir_speed + settling_time: settling_time + feedback: + status: status + result: + success: success + stop_stir: + type: ProtocolStopStir + goal: + vessel: vessel + feedback: + progress: progress + current_status: status + result: + success: success + message: message + schema: + type: object + properties: + status: + type: string + vessel: + type: string + purpose: + type: string + stir_speed: + type: number + target_stir_speed: + type: number + stir_state: + type: string + stir_time: + type: number + settling_time: + type: number + progress: + type: number + max_stir_speed: + type: number + required: + - status + - stir_speed + - stir_state + - vessel + additionalProperties: false +mock_vacuum: + description: Mock Vacuum Pump Device + class: + module: unilabos.devices.mock.mock_vacuum:MockVacuum + type: python + status_types: + status: String + power_state: String + pump_state: String + vacuum_level: Float64 + target_vacuum: Float64 + pump_speed: Float64 + pump_efficiency: Float64 + max_pump_speed: Float64 + action_value_mappings: + power_control: + type: StrSingleInput + goal: + string: power_state + feedback: {} + result: + success: success + set_vacuum_level: + type: FloatSingleInput + goal: + float_in: vacuum_level + feedback: {} + result: + success: success + start_vacuum: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + stop_vacuum: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + pause_vacuum: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + resume_vacuum: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + vent_to_atmosphere: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the vacuum pump + power_state: + type: string + description: Power state (On/Off) + pump_state: + type: string + description: Pump operation state (Running/Stopped/Paused) + vacuum_level: + type: number + description: Current vacuum level in mbar + target_vacuum: + type: number + description: Target vacuum level in mbar + pump_speed: + type: number + description: Current pump speed in L/s + pump_efficiency: + type: number + description: Pump efficiency percentage + max_pump_speed: + type: number + description: Maximum pump speed in L/s + required: + - status + - power_state + - pump_state + - vacuum_level + additionalProperties: false diff --git a/unilabos/registry/devices/temperature.yaml b/unilabos/registry/devices/temperature.yaml index 1c01b4e..662ee01 100644 --- a/unilabos/registry/devices/temperature.yaml +++ b/unilabos/registry/devices/temperature.yaml @@ -62,4 +62,4 @@ tempsensor: command: command feedback: {} result: - success: success \ No newline at end of file + success: success diff --git a/unilabos/registry/devices/vacuum_and_purge.yaml b/unilabos/registry/devices/vacuum_and_purge.yaml index b610820..6bdff16 100644 --- a/unilabos/registry/devices/vacuum_and_purge.yaml +++ b/unilabos/registry/devices/vacuum_and_purge.yaml @@ -94,4 +94,4 @@ gas_source.mock: description: "通信端口" default: "COM6" required: - - port + - port \ No newline at end of file diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml new file mode 100644 index 0000000..f92baf2 --- /dev/null +++ b/unilabos/registry/devices/virtual_device.yaml @@ -0,0 +1,388 @@ +virtual_pump: + description: Virtual Pump for PumpTransferProtocol Testing + class: + module: unilabos.devices.virtual.virtual_pump:VirtualPump + type: python + status_types: + status: String + position: Float64 + valve_position: Int32 # 修复:使用 Int32 而不是 String + max_volume: Float64 + current_volume: Float64 + action_value_mappings: + transfer: + type: PumpTransfer + goal: + from_vessel: from_vessel + to_vessel: to_vessel + volume: volume + amount: amount + time: time + viscous: viscous + rinsing_solvent: rinsing_solvent + rinsing_volume: rinsing_volume + rinsing_repeats: rinsing_repeats + solid: solid + feedback: + status: status + result: + success: success + set_valve_position: + type: FloatSingleInput + goal: + Int32: Int32 + feedback: + status: status + result: + success: success + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_volume: + type: number + default: 25.0 + additionalProperties: false + +virtual_stirrer: + description: Virtual Stirrer for StirProtocol Testing + class: + module: unilabos.devices.virtual.virtual_stirrer:VirtualStirrer + type: python + status_types: + 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 + start_stir: + type: ProtocolStartStir + goal: + vessel: vessel + stir_speed: stir_speed + purpose: purpose + feedback: + status: status + result: + success: success + stop_stir: + type: ProtocolStopStir + goal: + vessel: vessel + feedback: + status: status + result: + success: success + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_temp: + type: number + default: 100.0 + max_speed: + type: number + default: 1000.0 + additionalProperties: false + +virtual_valve: + description: Virtual Valve for AddProtocol Testing + class: + module: unilabos.devices.virtual.virtual_valve:VirtualValve + type: python + status_types: + status: String + valve_state: String + current_position: Int32 + target_position: Int32 + max_positions: Int32 + action_value_mappings: + set_position: + type: SendCmd + goal: + command: position + feedback: {} + result: + success: success + open: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + close: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + positions: + type: integer + default: 6 + additionalProperties: false + +virtual_centrifuge: + description: Virtual Centrifuge for CentrifugeProtocol Testing + class: + module: unilabos.devices.virtual.virtual_centrifuge:VirtualCentrifuge + type: python + status_types: + status: String + current_speed: Float64 + target_speed: Float64 + current_temp: Float64 + target_temp: Float64 + max_speed: Float64 + max_temp: Float64 + min_temp: Float64 + centrifuge_state: String + time_remaining: Float64 + action_value_mappings: + centrifuge: + type: ProtocolCentrifuge + goal: + vessel: vessel + speed: speed + time: time + temp: temp + feedback: + progress: progress + current_speed: current_speed + current_temp: current_temp + current_status: status + result: + success: success + message: message + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_speed: + type: number + default: 15000.0 + max_temp: + type: number + default: 40.0 + min_temp: + type: number + default: 4.0 + additionalProperties: false + +virtual_filter: + description: Virtual Filter for FilterProtocol Testing + class: + module: unilabos.devices.virtual.virtual_filter:VirtualFilter + type: python + status_types: + status: String + filter_state: String + current_temp: Float64 + target_temp: Float64 + max_temp: Float64 + stir_speed: Float64 + max_stir_speed: Float64 + filtered_volume: Float64 + progress: Float64 + message: String + action_value_mappings: + filter_sample: + type: ProtocolFilter + goal: + vessel: vessel + filtrate_vessel: filtrate_vessel + stir: stir + stir_speed: stir_speed + temp: temp + continue_heatchill: continue_heatchill + volume: volume + feedback: + progress: progress + current_temp: current_temp + filtered_volume: filtered_volume + current_status: status + result: + success: success + message: message + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_temp: + type: number + default: 100.0 + max_stir_speed: + type: number + default: 1000.0 + additionalProperties: false + +virtual_heatchill: + description: Virtual HeatChill for HeatChillProtocol Testing + class: + module: unilabos.devices.virtual.virtual_heatchill:VirtualHeatChill + type: python + status_types: + status: String + action_value_mappings: + heat_chill: + type: HeatChill + goal: + vessel: vessel + temp: temp + time: time + stir: stir + stir_speed: stir_speed + purpose: purpose + feedback: + status: status + result: + success: success + heat_chill_start: + type: HeatChillStart + goal: + vessel: vessel + temp: temp + purpose: purpose + feedback: + status: status + result: + success: success + heat_chill_stop: + type: HeatChillStop + goal: + vessel: vessel + feedback: + status: status + result: + success: success + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_temp: + type: number + default: 200.0 + min_temp: + type: number + default: -80.0 + max_stir_speed: + type: number + default: 1000.0 + additionalProperties: false + +virtual_transfer_pump: + description: Virtual Transfer Pump for TransferProtocol Testing + class: + module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump + type: python + status_types: + status: String + current_volume: Float64 + max_volume: Float64 + transfer_rate: Float64 + from_vessel: String + to_vessel: String + progress: Float64 + transferred_volume: Float64 + current_status: String + action_value_mappings: + transfer: + type: ProtocolTransfer + goal: + from_vessel: from_vessel + to_vessel: to_vessel + volume: volume + amount: amount + time: time + viscous: viscous + rinsing_solvent: rinsing_solvent + rinsing_volume: rinsing_volume + rinsing_repeats: rinsing_repeats + solid: solid + feedback: + progress: progress + transferred_volume: transferred_volume + current_status: current_status + result: + success: success + message: message + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_volume: + type: number + default: 50.0 + transfer_rate: + type: number + default: 5.0 + additionalProperties: false + +virtual_column: + description: Virtual Column for RunColumn Protocol Testing + class: + module: unilabos.devices.virtual.virtual_column:VirtualColumn + type: python + status_types: + status: String + column_state: String + current_flow_rate: Float64 + max_flow_rate: Float64 + column_length: Float64 + column_diameter: Float64 + processed_volume: Float64 + progress: Float64 + current_status: String + action_value_mappings: + run_column: + type: ProtocolRunColumn + goal: + from_vessel: from_vessel + to_vessel: to_vessel + column: column + feedback: + status: current_status + progress: progress + result: + success: success + message: message + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_flow_rate: + type: number + default: 10.0 + column_length: + type: number + default: 25.0 + column_diameter: + type: number + default: 2.0 + additionalProperties: false \ No newline at end of file diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index 3e98ce6..b818cf0 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -29,8 +29,25 @@ set(action_files "action/HeatChillStart.action" "action/HeatChillStop.action" - "action/LiquidHandlerProtocolCreation.action" + "action/ProtocolCleanVessel.action" + "action/ProtocolDissolve.action" + "action/ProtocolFilterThrough.action" + "action/ProtocolRunColumn.action" + "action/ProtocolWait.action" + "action/ProtocolWashSolid.action" + "action/ProtocolFilter.action" + "action/ProtocolCentrifuge.action" + "action/ProtocolCrystallize.action" + "action/ProtocolDry.action" + "action/ProtocolPurge.action" + "action/ProtocolStartPurge.action" + "action/ProtocolStartStir.action" + "action/ProtocolStopPurge.action" + "action/ProtocolStopStir.action" + "action/ProtocolTransfer.action" + + "action/LiquidHandlerProtocolCreation.action" "action/LiquidHandlerAspirate.action" "action/LiquidHandlerDiscardTips.action" "action/LiquidHandlerDispense.action" diff --git a/unilabos_msgs/action/ProtocolAdd.action b/unilabos_msgs/action/ProtocolAdd.action new file mode 100644 index 0000000..de06c6a --- /dev/null +++ b/unilabos_msgs/action/ProtocolAdd.action @@ -0,0 +1,20 @@ +# Goal - 添加试剂的目标参数 +string vessel # 目标容器 +string reagent # 试剂名称 +float64 volume # 体积 (可选) +float64 mass # 质量 (可选) +string amount # 数量描述 (可选) +float64 time # 添加时间 (可选) +bool stir # 是否搅拌 +float64 stir_speed # 搅拌速度 (可选) +bool viscous # 是否为粘性液体 +string purpose # 添加目的 (可选) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolCentrifuge.action b/unilabos_msgs/action/ProtocolCentrifuge.action new file mode 100644 index 0000000..356ccb9 --- /dev/null +++ b/unilabos_msgs/action/ProtocolCentrifuge.action @@ -0,0 +1,16 @@ +# Goal - 离心操作的目标参数 +string vessel # 离心容器 +float64 speed # 离心速度 (rpm) +float64 time # 离心时间 (秒) +float64 temp # 温度 (可选,摄氏度) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_speed # 当前转速 +float64 current_temp # 当前温度 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolCleanVessel.action b/unilabos_msgs/action/ProtocolCleanVessel.action new file mode 100644 index 0000000..cba232a --- /dev/null +++ b/unilabos_msgs/action/ProtocolCleanVessel.action @@ -0,0 +1,12 @@ +string vessel # 要清洗的容器名称 +string solvent # 用于清洗容器的溶剂名称 +float64 volume # 清洗溶剂的体积,可选参数 +float64 temp # 清洗时的温度,可选参数 +int32 repeats # 清洗操作的重复次数,默认为 1 +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolCrystallize.action b/unilabos_msgs/action/ProtocolCrystallize.action new file mode 100644 index 0000000..50d26cf --- /dev/null +++ b/unilabos_msgs/action/ProtocolCrystallize.action @@ -0,0 +1,14 @@ +# Goal - 结晶操作的目标参数 +string vessel # 结晶容器 +float64 ramp_time # 升温/降温时间 (可选,秒) +float64 ramp_temp # 目标温度 (可选,摄氏度) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_temp # 当前温度 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolDissolve.action b/unilabos_msgs/action/ProtocolDissolve.action new file mode 100644 index 0000000..6b860d0 --- /dev/null +++ b/unilabos_msgs/action/ProtocolDissolve.action @@ -0,0 +1,14 @@ +string vessel # 装有要溶解物质的容器名称 +string solvent # 用于溶解物质的溶剂名称 +float64 volume # 溶剂的体积,可选参数 +string amount # 要溶解物质的量,可选参数 +float64 temp # 溶解时的温度,可选参数 +float64 time # 溶解的时间,可选参数 +float64 stir_speed # 搅拌速度,可选参数 +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolDry.action b/unilabos_msgs/action/ProtocolDry.action new file mode 100644 index 0000000..5692ef2 --- /dev/null +++ b/unilabos_msgs/action/ProtocolDry.action @@ -0,0 +1,17 @@ +# Goal - 干燥操作的目标参数 +string vessel # 干燥容器 +float64 time # 干燥时间 (可选,秒) +float64 pressure # 压力 (可选,Pa) +float64 temp # 温度 (可选,摄氏度) +bool continue_heatchill # 是否继续加热冷却 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_temp # 当前温度 +float64 current_pressure # 当前压力 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolFilter.action b/unilabos_msgs/action/ProtocolFilter.action new file mode 100644 index 0000000..564df1a --- /dev/null +++ b/unilabos_msgs/action/ProtocolFilter.action @@ -0,0 +1,19 @@ +# Goal - 过滤操作的目标参数 +string vessel # 过滤容器 +string filtrate_vessel # 滤液容器 (可选) +bool stir # 是否搅拌 +float64 stir_speed # 搅拌速度 (可选) +float64 temp # 温度 (可选,摄氏度) +bool continue_heatchill # 是否继续加热冷却 +float64 volume # 过滤体积 (可选) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_temp # 当前温度 +float64 filtered_volume # 已过滤体积 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolFilterThrough.action b/unilabos_msgs/action/ProtocolFilterThrough.action new file mode 100644 index 0000000..dbabd12 --- /dev/null +++ b/unilabos_msgs/action/ProtocolFilterThrough.action @@ -0,0 +1,14 @@ +string from_vessel # 源容器的名称,即物质起始所在的容器 +string to_vessel # 目标容器的名称,物质过滤后要到达的容器 +string filter_through # 过滤时所通过的介质,如滤纸、柱子等 +string eluting_solvent # 洗脱溶剂的名称,可选参数 +float64 eluting_volume # 洗脱溶剂的体积,可选参数 +int32 eluting_repeats # 洗脱操作的重复次数,默认为 0 +float64 residence_time # 物质在过滤介质中的停留时间,可选参数 +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolPurge.action b/unilabos_msgs/action/ProtocolPurge.action new file mode 100644 index 0000000..00d76b3 --- /dev/null +++ b/unilabos_msgs/action/ProtocolPurge.action @@ -0,0 +1,17 @@ +# Goal - 清洗/吹扫操作的目标参数 +string vessel # 清洗容器 +string gas # 清洗气体 (可选) +float64 time # 清洗时间 (可选,秒) +float64 pressure # 压力 (可选,Pa) +float64 flow_rate # 流速 (可选,mL/min) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_pressure # 当前压力 +float64 current_flow_rate # 当前流速 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolRunColumn.action b/unilabos_msgs/action/ProtocolRunColumn.action new file mode 100644 index 0000000..3fba948 --- /dev/null +++ b/unilabos_msgs/action/ProtocolRunColumn.action @@ -0,0 +1,10 @@ +string from_vessel # 源容器的名称,即样品起始所在的容器 +string to_vessel # 目标容器的名称,分离后的样品要到达的容器 +string column # 所使用的柱子的名称 +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolStartPurge.action b/unilabos_msgs/action/ProtocolStartPurge.action new file mode 100644 index 0000000..f5500a6 --- /dev/null +++ b/unilabos_msgs/action/ProtocolStartPurge.action @@ -0,0 +1,16 @@ +# Goal - 启动清洗/吹扫操作的目标参数 +string vessel # 清洗容器 +string gas # 清洗气体 (可选) +float64 pressure # 压力 (可选,Pa) +float64 flow_rate # 流速 (可选,mL/min) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_pressure # 当前压力 +float64 current_flow_rate # 当前流速 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolStartStir.action b/unilabos_msgs/action/ProtocolStartStir.action new file mode 100644 index 0000000..534c9f3 --- /dev/null +++ b/unilabos_msgs/action/ProtocolStartStir.action @@ -0,0 +1,14 @@ +# Goal - 启动搅拌操作的目标参数 +string vessel # 搅拌容器 +float64 stir_speed # 搅拌速度 (可选,rpm) +string purpose # 搅拌目的 (可选) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_speed # 当前搅拌速度 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolStopPurge.action b/unilabos_msgs/action/ProtocolStopPurge.action new file mode 100644 index 0000000..b7db891 --- /dev/null +++ b/unilabos_msgs/action/ProtocolStopPurge.action @@ -0,0 +1,11 @@ +# Goal - 停止清洗/吹扫操作的目标参数 +string vessel # 清洗容器 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolStopStir.action b/unilabos_msgs/action/ProtocolStopStir.action new file mode 100644 index 0000000..a320598 --- /dev/null +++ b/unilabos_msgs/action/ProtocolStopStir.action @@ -0,0 +1,11 @@ +# Goal - 停止搅拌操作的目标参数 +string vessel # 搅拌容器 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolTransfer.action b/unilabos_msgs/action/ProtocolTransfer.action new file mode 100644 index 0000000..f31f9dd --- /dev/null +++ b/unilabos_msgs/action/ProtocolTransfer.action @@ -0,0 +1,20 @@ +string from_vessel # 源容器 +string to_vessel # 目标容器 +float64 volume # 转移体积 (可选) +string amount # 数量描述 (可选) +float64 time # 转移时间 (可选,秒) +bool viscous # 是否为粘性液体 +string rinsing_solvent # 冲洗溶剂 (可选) +float64 rinsing_volume # 冲洗体积 (可选) +int32 rinsing_repeats # 冲洗重复次数 +bool solid # 是否涉及固体 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 transferred_volume # 已转移体积 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolWait.action b/unilabos_msgs/action/ProtocolWait.action new file mode 100644 index 0000000..d4c4942 --- /dev/null +++ b/unilabos_msgs/action/ProtocolWait.action @@ -0,0 +1,9 @@ +int32 time # 等待时间(秒) +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) +int32 remaining_time # 剩余等待时间(秒) \ No newline at end of file diff --git a/unilabos_msgs/action/ProtocolWashSolid.action b/unilabos_msgs/action/ProtocolWashSolid.action new file mode 100644 index 0000000..cb57e5c --- /dev/null +++ b/unilabos_msgs/action/ProtocolWashSolid.action @@ -0,0 +1,16 @@ +string vessel # 装有固体物质的容器名称 +string solvent # 用于清洗固体的溶剂名称 +float64 volume # 清洗溶剂的体积 +string filtrate_vessel # 滤液要收集到的容器名称,可选参数 +float64 temp # 清洗时的温度,可选参数 +bool stir # 是否在清洗过程中搅拌,默认为 False +float64 stir_speed # 搅拌速度,可选参数 +float64 time # 清洗的时间,可选参数 +int32 repeats # 清洗操作的重复次数,默认为 1 +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file