mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-19 14:01:20 +00:00
Workstation templates: Resources and its CRUD, and workstation tasks (#95)
* coin_cell_station draft * refactor: rename "station_resource" to "deck" * add standardized BIOYOND resources: bottle_carrier, bottle * refactor and add BIOYOND resources tests * add BIOYOND deck assignment and pass all tests * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92) * feat: 新威电池测试系统驱动与注册文件 * feat: bring neware driver & battery.json into workstation_dev_YB2 * add bioyond studio draft * bioyond station with communication init and resource sync * fix bioyond station and registry * create/update resources with POST/PUT for big amount/ small amount data * refactor: add itemized_carrier instead of carrier consists of ResourceHolder * create warehouse by factory func * update bioyond launch json * add child_size for itemized_carrier * fix bioyond resource io --------- Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com> Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>
This commit is contained in:
1058
unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py
Normal file
1058
unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py
Normal file
File diff suppressed because it is too large
Load Diff
398
unilabos/devices/workstation/bioyond_studio/experiment.py
Normal file
398
unilabos/devices/workstation/bioyond_studio/experiment.py
Normal file
@@ -0,0 +1,398 @@
|
||||
# experiment_workflow.py
|
||||
"""
|
||||
实验流程主程序
|
||||
"""
|
||||
|
||||
import json
|
||||
from bioyond_rpc import BioyondV1RPC
|
||||
from config import API_CONFIG, WORKFLOW_MAPPINGS
|
||||
|
||||
|
||||
def run_experiment():
|
||||
"""运行实验流程"""
|
||||
|
||||
# 初始化Bioyond客户端
|
||||
config = {
|
||||
**API_CONFIG,
|
||||
"workflow_mappings": WORKFLOW_MAPPINGS
|
||||
}
|
||||
|
||||
Bioyond = BioyondV1RPC(config)
|
||||
|
||||
print("\n============= 多工作流参数测试(简化接口+材料缓存)=============")
|
||||
|
||||
# 显示可用的材料名称(前20个)
|
||||
available_materials = Bioyond.get_available_materials()
|
||||
print(f"可用材料名称(前20个): {available_materials[:20]}")
|
||||
print(f"总共有 {len(available_materials)} 个材料可用\n")
|
||||
|
||||
# 1. 反应器放入
|
||||
print("1. 添加反应器放入工作流,带参数...")
|
||||
Bioyond.reactor_taken_in(
|
||||
assign_material_name="BTDA-DD",
|
||||
cutoff="10000",
|
||||
temperature="-10"
|
||||
)
|
||||
|
||||
# 2. 液体投料-烧杯 (第一个)
|
||||
print("2. 添加液体投料-烧杯,带参数...")
|
||||
Bioyond.liquid_feeding_beaker(
|
||||
volume="34768.7",
|
||||
assign_material_name="ODA",
|
||||
time="0",
|
||||
torque_variation="1",
|
||||
titrationType="1",
|
||||
temperature=-10
|
||||
)
|
||||
|
||||
# 3. 液体投料-烧杯 (第二个)
|
||||
print("3. 添加液体投料-烧杯,带参数...")
|
||||
Bioyond.liquid_feeding_beaker(
|
||||
volume="34080.9",
|
||||
assign_material_name="MPDA",
|
||||
time="5",
|
||||
torque_variation="2",
|
||||
titrationType="1",
|
||||
temperature=0
|
||||
)
|
||||
|
||||
# 4. 液体投料-小瓶非滴定
|
||||
print("4. 添加液体投料-小瓶非滴定,带参数...")
|
||||
Bioyond.liquid_feeding_vials_non_titration(
|
||||
volumeFormula="639.5",
|
||||
assign_material_name="SIDA",
|
||||
titration_type="1",
|
||||
time="0",
|
||||
torque_variation="1",
|
||||
temperature=-10
|
||||
)
|
||||
|
||||
# 5. 液体投料溶剂
|
||||
print("5. 添加液体投料溶剂,带参数...")
|
||||
Bioyond.liquid_feeding_solvents(
|
||||
assign_material_name="NMP",
|
||||
volume="19000",
|
||||
titration_type="1",
|
||||
time="5",
|
||||
torque_variation="2",
|
||||
temperature=-10
|
||||
)
|
||||
|
||||
# 6-8. 固体进料小瓶 (三个)
|
||||
print("6. 添加固体进料小瓶,带参数...")
|
||||
Bioyond.solid_feeding_vials(
|
||||
material_id="3",
|
||||
time="180",
|
||||
torque_variation="2",
|
||||
assign_material_name="BTDA-1",
|
||||
temperature=-10.00
|
||||
)
|
||||
|
||||
print("7. 添加固体进料小瓶,带参数...")
|
||||
Bioyond.solid_feeding_vials(
|
||||
material_id="3",
|
||||
time="180",
|
||||
torque_variation="2",
|
||||
assign_material_name="BTDA-2",
|
||||
temperature=25.00
|
||||
)
|
||||
|
||||
print("8. 添加固体进料小瓶,带参数...")
|
||||
Bioyond.solid_feeding_vials(
|
||||
material_id="3",
|
||||
time="480",
|
||||
torque_variation="2",
|
||||
assign_material_name="BTDA-3",
|
||||
temperature=25.00
|
||||
)
|
||||
|
||||
# 液体投料滴定(第一个)
|
||||
print("9. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="1000",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
# 液体投料滴定(第二个)
|
||||
print("10. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="500",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
# 液体投料滴定(第三个)
|
||||
print("11. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="500",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
print("12. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="500",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
print("13. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="500",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
print("14. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="500",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
|
||||
|
||||
print("15. 添加液体投料溶剂,带参数...")
|
||||
Bioyond.liquid_feeding_solvents(
|
||||
assign_material_name="PGME",
|
||||
volume="16894.6",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature=25.00
|
||||
)
|
||||
|
||||
# 16. 反应器取出
|
||||
print("16. 添加反应器取出工作流...")
|
||||
Bioyond.reactor_taken_out()
|
||||
|
||||
# 显示当前工作流序列
|
||||
sequence = Bioyond.get_workflow_sequence()
|
||||
print("\n当前工作流执行顺序:")
|
||||
print(sequence)
|
||||
|
||||
# 执行process_and_execute_workflow,合并工作流并创建任务
|
||||
print("\n4. 执行process_and_execute_workflow...")
|
||||
|
||||
result = Bioyond.process_and_execute_workflow(
|
||||
workflow_name="test3_86",
|
||||
task_name="实验3_86"
|
||||
)
|
||||
|
||||
# 显示执行结果
|
||||
print("\n5. 执行结果:")
|
||||
if isinstance(result, str):
|
||||
try:
|
||||
result_dict = json.loads(result)
|
||||
if result_dict.get("success"):
|
||||
print("任务创建成功!")
|
||||
print(f"- 工作流: {result_dict.get('workflow', {}).get('name')}")
|
||||
print(f"- 工作流ID: {result_dict.get('workflow', {}).get('id')}")
|
||||
print(f"- 任务结果: {result_dict.get('task')}")
|
||||
else:
|
||||
print(f"任务创建失败: {result_dict.get('error')}")
|
||||
except:
|
||||
print(f"结果解析失败: {result}")
|
||||
else:
|
||||
if result.get("success"):
|
||||
print("任务创建成功!")
|
||||
print(f"- 工作流: {result.get('workflow', {}).get('name')}")
|
||||
print(f"- 工作流ID: {result.get('workflow', {}).get('id')}")
|
||||
print(f"- 任务结果: {result.get('task')}")
|
||||
else:
|
||||
print(f"任务创建失败: {result.get('error')}")
|
||||
|
||||
# 可选:启动调度器
|
||||
# Bioyond.scheduler_start()
|
||||
|
||||
return Bioyond
|
||||
|
||||
|
||||
def prepare_materials(bioyond):
|
||||
"""准备实验材料(可选)"""
|
||||
|
||||
# 样品板材料数据定义
|
||||
material_data_yp_1 = {
|
||||
"typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e",
|
||||
"name": "样品板-1",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
"details": [
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "BPDA-DD-1",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "PEPA",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "BPDA-DD-2",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 3,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "BPDA-1",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "PMDA",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "BPDA-2",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 3,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
}
|
||||
],
|
||||
"Parameters": "{}"
|
||||
}
|
||||
|
||||
material_data_yp_2 = {
|
||||
"typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e",
|
||||
"name": "样品板-2",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
"details": [
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "BPDA-DD",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "SIDA",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "BTDA-1",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "BTDA-2",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
|
||||
"name": "BTDA-3",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 3,
|
||||
"Parameters": "{\"molecular\": 1}"
|
||||
}
|
||||
],
|
||||
"Parameters": "{}"
|
||||
}
|
||||
|
||||
# 烧杯材料数据定义
|
||||
beaker_materials = [
|
||||
{
|
||||
"typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
|
||||
"name": "PDA-1",
|
||||
"unit": "微升",
|
||||
"quantity": 1,
|
||||
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
|
||||
"name": "TFDB",
|
||||
"unit": "微升",
|
||||
"quantity": 1,
|
||||
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
|
||||
"name": "ODA",
|
||||
"unit": "微升",
|
||||
"quantity": 1,
|
||||
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
|
||||
"name": "MPDA",
|
||||
"unit": "微升",
|
||||
"quantity": 1,
|
||||
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
|
||||
"name": "PDA-2",
|
||||
"unit": "微升",
|
||||
"quantity": 1,
|
||||
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||
}
|
||||
]
|
||||
|
||||
# 如果需要,可以在这里调用add_material方法添加材料
|
||||
# 例如:
|
||||
# result = bioyond.add_material(json.dumps(material_data_yp_1))
|
||||
# print(f"添加材料结果: {result}")
|
||||
|
||||
return {
|
||||
"sample_plates": [material_data_yp_1, material_data_yp_2],
|
||||
"beakers": beaker_materials
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 运行主实验流程
|
||||
bioyond_client = run_experiment()
|
||||
|
||||
# 可选:准备材料数据
|
||||
# materials = prepare_materials(bioyond_client)
|
||||
# print(f"\n准备的材料数据: {materials}")
|
||||
400
unilabos/devices/workstation/bioyond_studio/station.py
Normal file
400
unilabos/devices/workstation/bioyond_studio/station.py
Normal file
@@ -0,0 +1,400 @@
|
||||
"""
|
||||
Bioyond工作站实现
|
||||
Bioyond Workstation Implementation
|
||||
|
||||
集成Bioyond物料管理的工作站示例
|
||||
"""
|
||||
import traceback
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
import json
|
||||
|
||||
from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer
|
||||
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC
|
||||
from unilabos.resources.warehouse import WareHouse
|
||||
from unilabos.utils.log import logger
|
||||
from unilabos.resources.graphio import resource_bioyond_to_plr
|
||||
|
||||
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
||||
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||
|
||||
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS
|
||||
|
||||
|
||||
class BioyondResourceSynchronizer(ResourceSynchronizer):
|
||||
"""Bioyond资源同步器
|
||||
|
||||
负责与Bioyond系统进行物料数据的同步
|
||||
"""
|
||||
|
||||
def __init__(self, workstation: 'BioyondWorkstation'):
|
||||
super().__init__(workstation)
|
||||
self.bioyond_api_client = None
|
||||
self.sync_interval = 60 # 默认60秒同步一次
|
||||
self.last_sync_time = 0
|
||||
self.initialize()
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""初始化Bioyond资源同步器"""
|
||||
try:
|
||||
self.bioyond_api_client = self.workstation.hardware_interface
|
||||
if self.bioyond_api_client is None:
|
||||
logger.error("Bioyond API客户端未初始化")
|
||||
return False
|
||||
|
||||
# 设置同步间隔
|
||||
self.sync_interval = self.workstation.bioyond_config.get("sync_interval", 600)
|
||||
|
||||
logger.info("Bioyond资源同步器初始化完成")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Bioyond资源同步器初始化失败: {e}")
|
||||
return False
|
||||
|
||||
def sync_from_external(self) -> bool:
|
||||
"""从Bioyond系统同步物料数据"""
|
||||
try:
|
||||
if self.bioyond_api_client is None:
|
||||
logger.error("Bioyond API客户端未初始化")
|
||||
return False
|
||||
|
||||
bioyond_data = self.bioyond_api_client.stock_material('{"typeMode": 2, "includeDetail": true}')
|
||||
if not bioyond_data:
|
||||
logger.warning("从Bioyond获取的物料数据为空")
|
||||
return False
|
||||
|
||||
# 转换为UniLab格式
|
||||
unilab_resources = resource_bioyond_to_plr(bioyond_data, type_mapping=self.workstation.bioyond_config["material_type_mappings"], deck=self.workstation.deck)
|
||||
|
||||
logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"从Bioyond同步物料数据失败: {e}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def sync_to_external(self, resource: Any) -> bool:
|
||||
"""将本地物料数据变更同步到Bioyond系统"""
|
||||
try:
|
||||
if self.bioyond_api_client is None:
|
||||
logger.error("Bioyond API客户端未初始化")
|
||||
return False
|
||||
|
||||
# 调用入库、出库操作
|
||||
# bioyond_format_data = self._convert_resource_to_bioyond_format(resource)
|
||||
# success = await self.bioyond_api_client.update_material(bioyond_format_data)
|
||||
#
|
||||
# if success
|
||||
except:
|
||||
pass
|
||||
|
||||
def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
||||
"""处理Bioyond系统的变更通知"""
|
||||
try:
|
||||
# 这里可以实现对Bioyond变更的处理逻辑
|
||||
logger.info(f"处理Bioyond变更通知: {change_info}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"处理Bioyond变更通知失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class BioyondWorkstation(WorkstationBase):
|
||||
"""Bioyond工作站
|
||||
|
||||
集成Bioyond物料管理的工作站实现
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bioyond_config: Optional[Dict[str, Any]] = None,
|
||||
deck: Optional[Any] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
# 初始化父类
|
||||
super().__init__(
|
||||
# 桌子
|
||||
deck=deck,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
self.deck.warehouses = {}
|
||||
for resource in self.deck.children:
|
||||
if isinstance(resource, WareHouse):
|
||||
self.deck.warehouses[resource.name] = resource
|
||||
|
||||
self._create_communication_module(bioyond_config)
|
||||
self.resource_synchronizer = BioyondResourceSynchronizer(self)
|
||||
self.resource_synchronizer.sync_from_external()
|
||||
|
||||
# TODO: self._ros_node里面拿属性
|
||||
logger.info(f"Bioyond工作站初始化完成")
|
||||
|
||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||
self._ros_node = ros_node
|
||||
#self.deck = create_a_coin_cell_deck()
|
||||
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||
"resources": [self.deck]
|
||||
})
|
||||
|
||||
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""创建Bioyond通信模块"""
|
||||
self.bioyond_config = config or {
|
||||
**API_CONFIG,
|
||||
"workflow_mappings": WORKFLOW_MAPPINGS,
|
||||
"material_type_mappings": MATERIAL_TYPE_MAPPINGS
|
||||
}
|
||||
self.hardware_interface = BioyondV1RPC(self.bioyond_config)
|
||||
|
||||
return None
|
||||
|
||||
def _register_supported_workflows(self):
|
||||
"""注册Bioyond支持的工作流"""
|
||||
from unilabos.devices.workstation.workstation_base import WorkflowInfo
|
||||
|
||||
# Bioyond物料同步工作流
|
||||
self.supported_workflows["bioyond_sync"] = WorkflowInfo(
|
||||
name="bioyond_sync",
|
||||
description="从Bioyond系统同步物料",
|
||||
parameters={
|
||||
"sync_type": {"type": "string", "default": "full", "options": ["full", "incremental"]},
|
||||
"force_sync": {"type": "boolean", "default": False}
|
||||
}
|
||||
)
|
||||
|
||||
# Bioyond物料更新工作流
|
||||
self.supported_workflows["bioyond_update"] = WorkflowInfo(
|
||||
name="bioyond_update",
|
||||
description="将本地物料变更同步到Bioyond",
|
||||
parameters={
|
||||
"material_ids": {"type": "list", "default": []},
|
||||
"sync_all": {"type": "boolean", "default": True}
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"注册了 {len(self.supported_workflows)} 个Bioyond工作流")
|
||||
|
||||
async def execute_bioyond_sync_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""执行Bioyond同步工作流"""
|
||||
try:
|
||||
sync_type = parameters.get("sync_type", "full")
|
||||
force_sync = parameters.get("force_sync", False)
|
||||
|
||||
logger.info(f"开始执行Bioyond同步工作流: {sync_type}")
|
||||
|
||||
# 获取物料管理模块
|
||||
material_manager = self.material_management
|
||||
|
||||
if sync_type == "full":
|
||||
# 全量同步
|
||||
success = await material_manager.sync_from_bioyond()
|
||||
else:
|
||||
# 增量同步(这里可以实现增量同步逻辑)
|
||||
success = await material_manager.sync_from_bioyond()
|
||||
|
||||
if success:
|
||||
result = {
|
||||
"status": "success",
|
||||
"message": f"Bioyond同步完成: {sync_type}",
|
||||
"synced_resources": len(material_manager.plr_resources)
|
||||
}
|
||||
else:
|
||||
result = {
|
||||
"status": "failed",
|
||||
"message": "Bioyond同步失败"
|
||||
}
|
||||
|
||||
logger.info(f"Bioyond同步工作流执行完成: {result['status']}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Bioyond同步工作流执行失败: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
async def execute_bioyond_update_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""执行Bioyond更新工作流"""
|
||||
try:
|
||||
material_ids = parameters.get("material_ids", [])
|
||||
sync_all = parameters.get("sync_all", True)
|
||||
|
||||
logger.info(f"开始执行Bioyond更新工作流: sync_all={sync_all}")
|
||||
|
||||
# 获取物料管理模块
|
||||
material_manager = self.material_management
|
||||
|
||||
if sync_all:
|
||||
# 同步所有物料
|
||||
success_count = 0
|
||||
for resource in material_manager.plr_resources.values():
|
||||
success = await material_manager.sync_to_bioyond(resource)
|
||||
if success:
|
||||
success_count += 1
|
||||
else:
|
||||
# 同步指定物料
|
||||
success_count = 0
|
||||
for material_id in material_ids:
|
||||
resource = material_manager.find_material_by_id(material_id)
|
||||
if resource:
|
||||
success = await material_manager.sync_to_bioyond(resource)
|
||||
if success:
|
||||
success_count += 1
|
||||
|
||||
result = {
|
||||
"status": "success",
|
||||
"message": f"Bioyond更新完成",
|
||||
"updated_resources": success_count,
|
||||
"total_resources": len(material_ids) if not sync_all else len(material_manager.plr_resources)
|
||||
}
|
||||
|
||||
logger.info(f"Bioyond更新工作流执行完成: {result['status']}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Bioyond更新工作流执行失败: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
def load_bioyond_data_from_file(self, file_path: str) -> bool:
|
||||
"""从文件加载Bioyond数据(用于测试)"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
bioyond_data = json.load(f)
|
||||
|
||||
# 获取物料管理模块
|
||||
material_manager = self.material_management
|
||||
|
||||
# 转换为UniLab格式
|
||||
if isinstance(bioyond_data, dict) and "data" in bioyond_data:
|
||||
unilab_resources = material_manager.resource_bioyond_container_to_ulab(bioyond_data)
|
||||
else:
|
||||
unilab_resources = material_manager.resource_bioyond_to_ulab(bioyond_data)
|
||||
|
||||
# 分配到Deck
|
||||
import asyncio
|
||||
asyncio.create_task(material_manager._assign_resources_to_deck(unilab_resources))
|
||||
|
||||
logger.info(f"从文件 {file_path} 加载了 {len(unilab_resources)} 个Bioyond资源")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从文件加载Bioyond数据失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# 使用示例
|
||||
def create_bioyond_workstation_example():
|
||||
"""创建Bioyond工作站示例"""
|
||||
|
||||
# 配置参数
|
||||
device_id = "bioyond_workstation_001"
|
||||
|
||||
# 子资源配置
|
||||
children = {
|
||||
"plate_1": {
|
||||
"name": "plate_1",
|
||||
"type": "plate",
|
||||
"position": {"x": 100, "y": 100, "z": 0},
|
||||
"config": {
|
||||
"size_x": 127.76,
|
||||
"size_y": 85.48,
|
||||
"size_z": 14.35,
|
||||
"model": "Generic 96 Well Plate"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Bioyond配置
|
||||
bioyond_config = {
|
||||
"base_url": "http://bioyond.example.com/api",
|
||||
"api_key": "your_api_key_here",
|
||||
"sync_interval": 60, # 60秒同步一次
|
||||
"timeout": 30
|
||||
}
|
||||
|
||||
# Deck配置
|
||||
deck_config = {
|
||||
"size_x": 1000.0,
|
||||
"size_y": 1000.0,
|
||||
"size_z": 100.0,
|
||||
"model": "BioyondDeck"
|
||||
}
|
||||
|
||||
# 创建工作站
|
||||
workstation = BioyondWorkstation(
|
||||
station_resource=deck_config,
|
||||
bioyond_config=bioyond_config,
|
||||
deck_config=deck_config,
|
||||
)
|
||||
|
||||
return workstation
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 创建示例工作站
|
||||
#workstation = create_bioyond_workstation_example()
|
||||
|
||||
# 从文件加载测试数据
|
||||
#workstation.load_bioyond_data_from_file("bioyond_test_yibin.json")
|
||||
|
||||
# 获取状态
|
||||
#status = workstation.get_bioyond_status()
|
||||
#print("Bioyond工作站状态:", status)
|
||||
|
||||
# 创建测试数据 - 使用resource_bioyond_container_to_ulab函数期望的格式
|
||||
|
||||
# 读取 bioyond_resources_unilab_output3 copy.json 文件
|
||||
from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type
|
||||
from Bioyond_wuliao import *
|
||||
from typing import List
|
||||
from pylabrobot.resources import Resource as PLRResource
|
||||
import json
|
||||
from pylabrobot.resources.deck import Deck
|
||||
from pylabrobot.resources.coordinate import Coordinate
|
||||
|
||||
with open("./bioyond_test_yibin3_unilab_result_corr.json", "r", encoding="utf-8") as f:
|
||||
bioyond_resources_unilab = json.load(f)
|
||||
print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源")
|
||||
ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource])
|
||||
print(f"转换结果类型: {type(ulab_resources)}")
|
||||
print(f"转换结果长度: {len(ulab_resources) if ulab_resources else 0}")
|
||||
deck = Deck(size_x=2000,
|
||||
size_y=653.5,
|
||||
size_z=900)
|
||||
|
||||
Stack0 = Stack(name="Stack0", location=Coordinate(0, 100, 0))
|
||||
Stack1 = Stack(name="Stack1", location=Coordinate(100, 100, 0))
|
||||
Stack2 = Stack(name="Stack2", location=Coordinate(200, 100, 0))
|
||||
Stack3 = Stack(name="Stack3", location=Coordinate(300, 100, 0))
|
||||
Stack4 = Stack(name="Stack4", location=Coordinate(400, 100, 0))
|
||||
Stack5 = Stack(name="Stack5", location=Coordinate(500, 100, 0))
|
||||
|
||||
deck.assign_child_resource(Stack1, Stack1.location)
|
||||
deck.assign_child_resource(Stack2, Stack2.location)
|
||||
deck.assign_child_resource(Stack3, Stack3.location)
|
||||
deck.assign_child_resource(Stack4, Stack4.location)
|
||||
deck.assign_child_resource(Stack5, Stack5.location)
|
||||
|
||||
Stack0.assign_child_resource(ulab_resources[0], Stack0.location)
|
||||
Stack1.assign_child_resource(ulab_resources[1], Stack1.location)
|
||||
Stack2.assign_child_resource(ulab_resources[2], Stack2.location)
|
||||
Stack3.assign_child_resource(ulab_resources[3], Stack3.location)
|
||||
Stack4.assign_child_resource(ulab_resources[4], Stack4.location)
|
||||
Stack5.assign_child_resource(ulab_resources[5], Stack5.location)
|
||||
|
||||
from unilabos.resources.graphio import convert_resources_from_type
|
||||
from unilabos.app.web.client import http_client
|
||||
|
||||
resources = convert_resources_from_type([deck], [PLRResource])
|
||||
|
||||
|
||||
print(resources)
|
||||
http_client.remote_addr = "https://uni-lab.bohrium.com/api/v1"
|
||||
#http_client.auth = "9F05593C"
|
||||
http_client.auth = "ED634D1C"
|
||||
http_client.resource_add(resources, database_process_later=False)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
14472
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json
Normal file
14472
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -112,17 +112,17 @@ class ResourceSynchronizer(ABC):
|
||||
self.workstation = workstation
|
||||
|
||||
@abstractmethod
|
||||
async def sync_from_external(self) -> bool:
|
||||
def sync_from_external(self) -> bool:
|
||||
"""从外部系统同步物料到本地deck"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def sync_to_external(self, plr_resource: PLRResource) -> bool:
|
||||
def sync_to_external(self, plr_resource: PLRResource) -> bool:
|
||||
"""将本地物料同步到外部系统"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
||||
def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
||||
"""处理外部系统的变更通知"""
|
||||
pass
|
||||
|
||||
@@ -147,17 +147,15 @@ class WorkstationBase(ABC):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
station_resource: PLRResource,
|
||||
deck: Deck,
|
||||
*args,
|
||||
**kwargs, # 必须有kwargs
|
||||
):
|
||||
# 基本配置
|
||||
print(station_resource)
|
||||
self.deck_config = station_resource
|
||||
|
||||
# PLR 物料系统
|
||||
self.deck: Optional[Deck] = None
|
||||
self.deck: Optional[Deck] = deck
|
||||
self.plr_resources: Dict[str, PLRResource] = {}
|
||||
|
||||
self.resource_synchronizer = None # type: Optional[ResourceSynchronizer]
|
||||
# 硬件接口
|
||||
self.hardware_interface: Union[Any, str] = None
|
||||
|
||||
@@ -173,46 +171,7 @@ class WorkstationBase(ABC):
|
||||
def post_init(self, ros_node: ROS2WorkstationNode) -> None:
|
||||
# 初始化物料系统
|
||||
self._ros_node = ros_node
|
||||
self._initialize_material_system()
|
||||
|
||||
def _initialize_material_system(self):
|
||||
"""初始化物料系统 - 使用 graphio 转换"""
|
||||
pass
|
||||
|
||||
def _create_complete_resource_config(self) -> Dict[str, Any]:
|
||||
"""创建完整的资源配置 - 合并 deck_config 和 children"""
|
||||
# 创建主 deck 配置
|
||||
return {}
|
||||
|
||||
def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]:
|
||||
"""标准化子资源配置"""
|
||||
return {
|
||||
"id": resource_id,
|
||||
"name": config.get("name", resource_id),
|
||||
"type": config.get("type", "container"),
|
||||
"position": self._normalize_position(config.get("position", {})),
|
||||
"config": config.get("config", {}),
|
||||
"data": config.get("data", {}),
|
||||
"children": [], # 简化版本:只支持一层子资源
|
||||
"parent": parent_id,
|
||||
}
|
||||
|
||||
def _normalize_position(self, position: Any) -> Dict[str, float]:
|
||||
"""标准化位置信息"""
|
||||
if isinstance(position, dict):
|
||||
return {
|
||||
"x": float(position.get("x", 0)),
|
||||
"y": float(position.get("y", 0)),
|
||||
"z": float(position.get("z", 0)),
|
||||
}
|
||||
elif isinstance(position, (list, tuple)) and len(position) >= 2:
|
||||
return {
|
||||
"x": float(position[0]),
|
||||
"y": float(position[1]),
|
||||
"z": float(position[2]) if len(position) > 2 else 0.0,
|
||||
}
|
||||
else:
|
||||
return {"x": 0.0, "y": 0.0, "z": 0.0}
|
||||
self._ros_node.update_resource([self.deck])
|
||||
|
||||
def _build_resource_mappings(self, deck: Deck):
|
||||
"""递归构建资源映射"""
|
||||
@@ -296,14 +255,14 @@ class WorkstationBase(ABC):
|
||||
"""按类型查找资源"""
|
||||
return [res for res in self.plr_resources.values() if isinstance(res, resource_type)]
|
||||
|
||||
async def sync_with_external_system(self) -> bool:
|
||||
def sync_with_external_system(self) -> bool:
|
||||
"""与外部物料系统同步"""
|
||||
if not self.resource_synchronizer:
|
||||
logger.info(f"工作站 {self._ros_node.device_id} 没有配置资源同步器")
|
||||
return True
|
||||
|
||||
try:
|
||||
success = await self.resource_synchronizer.sync_from_external()
|
||||
success = self.resource_synchronizer.sync_from_external()
|
||||
if success:
|
||||
logger.info(f"工作站 {self._ros_node.device_id} 外部同步成功")
|
||||
else:
|
||||
@@ -391,5 +350,5 @@ class WorkstationBase(ABC):
|
||||
|
||||
|
||||
class ProtocolNode(WorkstationBase):
|
||||
def __init__(self, station_resource: Optional[PLRResource], *args, **kwargs):
|
||||
super().__init__(station_resource, *args, **kwargs)
|
||||
def __init__(self, deck: Optional[PLRResource], *args, **kwargs):
|
||||
super().__init__(deck, *args, **kwargs)
|
||||
|
||||
@@ -149,6 +149,22 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
)
|
||||
self._send_response(error_response)
|
||||
|
||||
def do_OPTIONS(self):
|
||||
"""处理OPTIONS请求 - CORS预检请求"""
|
||||
try:
|
||||
# 发送CORS响应头
|
||||
self.send_response(200)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
||||
self.send_header('Access-Control-Max-Age', '86400')
|
||||
self.end_headers()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"OPTIONS请求处理失败: {e}")
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
|
||||
def _handle_step_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||
"""处理步骤完成报送(统一LIMS协议规范)"""
|
||||
try:
|
||||
@@ -206,7 +222,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
|
||||
# 验证data字段内容
|
||||
data = request_data['data']
|
||||
data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'Status']
|
||||
data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'status']
|
||||
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
@@ -227,7 +243,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
"0": "待生产", "2": "进样", "10": "开始",
|
||||
"20": "完成", "-2": "异常停止", "-3": "人工停止"
|
||||
}
|
||||
status_desc = status_names.get(str(data['Status']), f"状态{data['Status']}")
|
||||
status_desc = status_names.get(str(data['status']), f"状态{data['status']}")
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
@@ -380,6 +396,21 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
"""处理物料变更报送"""
|
||||
try:
|
||||
# 验证必需字段
|
||||
if 'brand' in request_data:
|
||||
if request_data['brand'] == "bioyond": # 奔曜
|
||||
error_msg = request_data["text"]
|
||||
logger.info(f"收到奔曜错误处理报送: {error_msg}")
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"错误处理报送已收到: {error_msg}",
|
||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_msg.get('action_id', 'unknown')}",
|
||||
data=None
|
||||
)
|
||||
else:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"缺少厂家信息(brand字段)"
|
||||
)
|
||||
required_fields = ['workstation_id', 'timestamp', 'resource_id', 'change_type']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
return HttpResponse(
|
||||
@@ -407,23 +438,45 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
def _handle_error_handling_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||
"""处理错误处理报送"""
|
||||
try:
|
||||
# 验证必需字段
|
||||
required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
# 检查是否为奔曜格式的错误报送
|
||||
if 'brand' in request_data and str(request_data['brand']).lower() == "bioyond":
|
||||
# 奔曜格式处理
|
||||
if 'text' not in request_data:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message="奔曜格式缺少text字段"
|
||||
)
|
||||
|
||||
error_data = request_data["text"]
|
||||
logger.info(f"收到奔曜错误处理报送: {error_data}")
|
||||
|
||||
# 调用工作站的处理方法
|
||||
result = self.workstation.handle_external_error(error_data)
|
||||
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
success=True,
|
||||
message=f"错误处理报送已收到: 任务{error_data.get('task', 'unknown')}, 错误代码{error_data.get('code', 'unknown')}",
|
||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_data.get('task', 'unknown')}",
|
||||
data=result
|
||||
)
|
||||
else:
|
||||
# 标准格式处理
|
||||
required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
)
|
||||
|
||||
# 调用工作站的处理方法
|
||||
result = self.workstation.handle_external_error(request_data)
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}",
|
||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}",
|
||||
data=result
|
||||
)
|
||||
|
||||
# 调用工作站的处理方法
|
||||
result = self.workstation.handle_external_error(request_data)
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}",
|
||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}",
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理错误处理报送失败: {e}")
|
||||
@@ -548,13 +601,19 @@ class WorkstationHTTPService:
|
||||
"""停止HTTP服务"""
|
||||
try:
|
||||
if self.running and self.server:
|
||||
logger.info("正在停止工作站HTTP报送服务...")
|
||||
self.running = False
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
|
||||
# 停止serve_forever循环
|
||||
self.server.shutdown()
|
||||
|
||||
# 等待服务器线程结束
|
||||
if self.server_thread and self.server_thread.is_alive():
|
||||
self.server_thread.join(timeout=5.0)
|
||||
|
||||
# 关闭服务器套接字
|
||||
self.server.server_close()
|
||||
|
||||
logger.info("工作站HTTP报送服务已停止")
|
||||
|
||||
except Exception as e:
|
||||
@@ -563,11 +622,13 @@ class WorkstationHTTPService:
|
||||
def _run_server(self):
|
||||
"""运行HTTP服务器"""
|
||||
try:
|
||||
while self.running:
|
||||
self.server.handle_request()
|
||||
# 使用serve_forever()让服务持续运行
|
||||
self.server.serve_forever()
|
||||
except Exception as e:
|
||||
if self.running: # 只在非正常停止时记录错误
|
||||
logger.error(f"HTTP服务运行错误: {e}")
|
||||
finally:
|
||||
logger.info("HTTP服务器线程已退出")
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
@@ -603,3 +664,49 @@ __all__ = [
|
||||
'MaterialChangeReport',
|
||||
'TaskExecutionReport'
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 简单测试HTTP服务
|
||||
class DummyWorkstation:
|
||||
device_id = "WS-001"
|
||||
|
||||
def process_step_finish_report(self, report_request):
|
||||
return {"processed": True}
|
||||
|
||||
def process_sample_finish_report(self, report_request):
|
||||
return {"processed": True}
|
||||
|
||||
def process_order_finish_report(self, report_request, used_materials):
|
||||
return {"processed": True}
|
||||
|
||||
def process_material_change_report(self, report_data):
|
||||
return {"processed": True}
|
||||
|
||||
def handle_external_error(self, error_data):
|
||||
return {"handled": True}
|
||||
|
||||
workstation = DummyWorkstation()
|
||||
http_service = WorkstationHTTPService(workstation)
|
||||
|
||||
try:
|
||||
http_service.start()
|
||||
print(f"测试服务器已启动: {http_service.service_url}")
|
||||
print("按 Ctrl+C 停止服务器")
|
||||
print("服务将持续运行,等待接收HTTP请求...")
|
||||
|
||||
# 保持服务器运行 - 使用更好的等待机制
|
||||
try:
|
||||
while http_service.is_running:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n接收到停止信号...")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n正在停止服务器...")
|
||||
http_service.stop()
|
||||
print("服务器已停止")
|
||||
except Exception as e:
|
||||
print(f"服务器运行错误: {e}")
|
||||
http_service.stop()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user