Compare commits

..

2 Commits

Author SHA1 Message Date
Xuwznln
01f8816597 update registry with nested obj 2025-09-19 03:44:18 +08:00
Guangxin Zhang
e5006285df 重新规定了版位推荐的入参 2025-09-18 15:27:22 +01:00
15 changed files with 175 additions and 111 deletions

View File

@@ -208,7 +208,6 @@ def main():
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"), config_path
)
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
os._exit(1)
else:
os._exit(1)
# 加载配置文件

View File

@@ -384,7 +384,7 @@ class MessageProcessor:
"""停止消息处理线程"""
self.is_running = False
if self.thread and self.thread.is_alive():
self.thread.join(timeout=5)
self.thread.join(timeout=2)
logger.info("[MessageProcessor] Stopped")
def _run(self):
@@ -832,7 +832,7 @@ class QueueProcessor:
"""停止队列处理线程"""
self.is_running = False
if self.thread and self.thread.is_alive():
self.thread.join(timeout=5)
self.thread.join(timeout=2)
logger.info("[QueueProcessor] Stopped")
def _run(self):

View File

@@ -4,7 +4,7 @@ import contextlib
import json
import socket
import time
from typing import Any, List, Dict, Optional, TypedDict, Union, Sequence, Iterator, Literal
from typing import Any, List, Dict, Optional, Tuple, TypedDict, Union, Sequence, Iterator, Literal
from pylabrobot.liquid_handling import (
LiquidHandlerBackend,
@@ -1030,24 +1030,26 @@ class DefaultLayout:
def add_lab_resource(self, material_info):
self.labresource = material_info
def recommend_layout(self, needs: Dict[str, int]) -> Dict[str, Any]:
"""根据 needs 推荐布局"""
for k, v in needs.items():
if k not in self.labresource:
raise ValueError(f"Material {k} not found in lab resources.")
# 预留位置12和16不动
def recommend_layout(self, needs: List[Tuple[str, str, int]]) -> Dict[str, Any]:
layout_list = []
for reagent_name, material_name, count in needs:
if material_name not in self.labresource:
raise ValueError(f"Material {reagent_name} not found in lab resources.")
# 预留位置12和16不动
reserved_positions = {12, 16}
available_positions = [i for i in range(1, 17) if i not in reserved_positions]
# 计算总需求
total_needed = sum(needs.values())
# 计算总需求
total_needed = sum(count for _, _, count in needs)
if total_needed > len(available_positions):
raise ValueError(f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置排除位置12和16")
# 依次分配位置
# 依次分配位置
current_pos = 0
for material_name, count in needs.items():
for reagent_name, material_name, count in needs:
material_uuid = self.labresource[material_name]['uuid']
material_enum = self.labresource[material_name]['materialEnum']
@@ -1061,11 +1063,10 @@ class DefaultLayout:
if tablet['Number'] == position:
tablet['Material']['uuid'] = material_uuid
tablet['Material']['materialEnum'] = material_enum
layout_list.append(dict(reagent_name=reagent_name, material_name=material_name, positions=position))
break
current_pos += 1
return self.default_layout
return self.default_layout, layout_list
@@ -1388,7 +1389,7 @@ if __name__ == "__main__":
timeout=10.0, setup=False, debug=True,
matrix_id="c1d0d5dc-40f2-4f24-97ac-9cc49c68496c",
channel_num=1, axis="Left",simulator=True) # Initialize the handler with the deck and host settings
handler.set_tiprack([plate8]) # Set the tip rack for the handler
asyncio.run(handler.setup()) # Initialize the handler and setup the connection
from pylabrobot.resources import set_volume_tracking
@@ -1530,17 +1531,17 @@ if __name__ == "__main__":
layout = DefaultLayout("PRCXI9320")
layout.add_lab_resource(material_info)
MatrixLayout_1 = layout.recommend_layout({
"96 细胞培养皿": 3,
"12道储液槽": 1,
"200μL Tip头": 1,
"10μL加长 Tip头": 1,
})
print(MatrixLayout_1)
MatrixLayout_2 = layout.recommend_layout({
"96深孔板": 4,
"12道储液槽": 1,
"200μL Tip头": 1,
"10μL加长 Tip头": 1,
})
MatrixLayout_1, dict_1 = layout.recommend_layout([
("reagent_1", "96 细胞培养皿", 3),
("reagent_2", "12道储液槽", 1),
("reagent_3", "200μL Tip头", 7),
("reagent_4", "10μL加长 Tip头", 1),
])
print(dict_1)
MatrixLayout_2, dict_2 = layout.recommend_layout([
("reagent_1", "96深孔板", 4),
("reagent_2", "12道储液槽", 1),
("reagent_3", "200μL Tip头", 1),
("reagent_4", "10μL加长 Tip头", 1),
])

View File

@@ -94,7 +94,7 @@ serial:
port:
type: string
resource_tracker:
type: string
type: object
required:
- device_id
- port

View File

@@ -63,7 +63,7 @@ camera.USB:
default: 0.1
type: number
resource_tracker:
type: string
type: object
required: []
type: object
data:

View File

@@ -170,7 +170,7 @@ hplc.agilent:
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
status_types:
could_run: bool
data_file: list
data_file: String
device_status: str
driver_init_ok: bool
finish_status: str
@@ -195,6 +195,8 @@ hplc.agilent:
could_run:
type: boolean
data_file:
items:
type: string
type: array
device_status:
type: string

View File

@@ -4508,14 +4508,22 @@ liquid_handler.biomek:
bind_parent_id:
type: string
liquid_input_slot:
items:
type: integer
type: array
liquid_type:
items:
type: string
type: array
liquid_volume:
items:
type: integer
type: array
resource_tracker:
type: string
type: object
resources:
items:
type: object
type: array
slot_on_deck:
type: integer
@@ -4559,10 +4567,16 @@ liquid_handler.biomek:
id:
type: string
liquid_input_wells:
items:
type: string
type: array
liquid_type:
items:
type: string
type: array
liquid_volume:
items:
type: integer
type: array
parent:
type: string
@@ -6039,6 +6053,8 @@ liquid_handler.prcxi:
properties:
none_keys:
default: []
items:
type: string
type: array
protocol_author:
default: ''
@@ -6139,7 +6155,7 @@ liquid_handler.prcxi:
default: 0
type: number
well:
type: string
type: object
required:
- well
type: object
@@ -8358,7 +8374,7 @@ liquid_handler.prcxi:
default: false
type: string
deck:
type: string
type: object
host:
type: string
matrix_id:

View File

@@ -832,7 +832,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
default: 25.0
type: number
mode:
type: string
type: object
port:
type: string
required:
@@ -1352,7 +1352,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
default: 25.0
type: number
mode:
type: string
type: object
port:
type: string
required:

View File

@@ -133,7 +133,7 @@ robotic_arm.SCARA_with_slider.virtual:
goal:
properties:
ros_node:
type: string
type: object
required:
- ros_node
type: object
@@ -753,7 +753,7 @@ robotic_arm.elite:
module: unilabos.devices.arm.elite_robot:EliteRobot
status_types:
actual_joint_positions: String
arm_pose: list
arm_pose: String
type: python
config_info: []
description: Elite robot arm
@@ -775,6 +775,8 @@ robotic_arm.elite:
actual_joint_positions:
type: string
arm_pose:
items:
type: number
type: array
required:
- arm_pose

View File

@@ -37,7 +37,7 @@ linear_motion.grbl:
goal:
properties:
position:
type: string
type: object
required:
- position
type: object
@@ -450,6 +450,8 @@ linear_motion.grbl:
- 0
- -80
- 0
items:
type: integer
type: array
port:
type: string
@@ -459,7 +461,7 @@ linear_motion.grbl:
data:
properties:
position:
type: string
type: object
spindle_speed:
type: number
status:
@@ -605,7 +607,7 @@ linear_motion.toyo_xyz.sim:
goal:
properties:
ros_node:
type: string
type: object
required:
- ros_node
type: object

View File

@@ -6129,7 +6129,7 @@ workstation:
protocol_type:
type: string
resource_tracker:
type: string
type: object
required:
- device_id
- children
@@ -6171,14 +6171,22 @@ workstation.example:
bind_parent_id:
type: string
liquid_input_slot:
items:
type: integer
type: array
liquid_type:
items:
type: string
type: array
liquid_volume:
items:
type: integer
type: array
resource_tracker:
type: string
type: object
resources:
items:
type: object
type: array
slot_on_deck:
type: integer
@@ -6213,9 +6221,9 @@ workstation.example:
goal:
properties:
base_plate:
type: string
type: object
tip_rack:
type: string
type: object
required:
- tip_rack
- base_plate
@@ -6241,9 +6249,9 @@ workstation.example:
goal:
properties:
from_plate:
type: string
type: object
to_base_plate:
type: string
type: object
required:
- from_plate
- to_base_plate
@@ -6271,7 +6279,7 @@ workstation.example:
protocol_type:
type: string
resource_tracker:
type: string
type: object
required:
- device_id
- children

View File

@@ -5,7 +5,7 @@ import sys
import inspect
import importlib
from pathlib import Path
from typing import Any, Dict, List
from typing import Any, Dict, List, Union, Tuple
import yaml
@@ -294,7 +294,7 @@ class Registry:
logger.warning(f"[UniLab Registry] 设备 {device_id}{field_name} 类型为空,跳过替换")
return type_name
convert_manager = { # 将python基本对象转为ros2基本对象
"str": "String",
"builtins:str": "String",
"bool": "Bool",
"int": "Int64",
"float": "Float64",
@@ -310,37 +310,73 @@ class Registry:
logger.error(f"[UniLab Registry] 无法找到类型 '{type_name}' 用于设备 {device_id}{field_name}")
sys.exit(1)
def _get_json_schema_type(self, type_str: str) -> str:
"""
根据类型字符串返回对应的JSON Schema类型
Args:
type_str: 类型字符串
Returns:
JSON Schema类型字符串
"""
type_lower = type_str.lower()
type_mapping = {
("str", "string"): "string",
("int", "integer"): "integer",
("float", "number"): "number",
("bool", "boolean"): "boolean",
("list", "array"): "array",
("dict", "object"): "object",
}
# 遍历映射找到匹配的类型
for type_variants, json_type in type_mapping.items():
if type_lower in type_variants:
return json_type
# 特殊处理包含冒号的类型如ROS消息类型
if ":" in type_lower:
return "object"
# 默认返回字符串类型
return "string"
def _generate_schema_from_info(
self,
param_name: str,
param_type: str,
param_type: Union[str, Tuple[str]],
param_default: Any,
) -> Dict[str, Any]:
"""
根据参数信息生成JSON Schema
"""
prop_schema = {}
# 根据类型设置schema FIXME 不完整
if param_type:
param_type_lower = param_type.lower()
if param_type_lower in ["str", "string"]:
prop_schema["type"] = "string"
elif param_type_lower in ["int", "integer"]:
prop_schema["type"] = "integer"
elif param_type_lower in ["float", "number"]:
prop_schema["type"] = "number"
elif param_type_lower in ["bool", "boolean"]:
prop_schema["type"] = "boolean"
elif param_type_lower in ["list", "array"]:
prop_schema["type"] = "array"
elif param_type_lower in ["dict", "object"]:
prop_schema["type"] = "object"
# 处理嵌套类型Tuple[str]
if isinstance(param_type, tuple):
if len(param_type) == 2:
outer_type, inner_type = param_type
outer_json_type = self._get_json_schema_type(outer_type)
inner_json_type = self._get_json_schema_type(inner_type)
prop_schema["type"] = outer_json_type
# 根据外层类型设置内层类型信息
if outer_json_type == "array":
prop_schema["items"] = {"type": inner_json_type}
elif outer_json_type == "object":
prop_schema["additionalProperties"] = {"type": inner_json_type}
else:
# 默认为字符串类型
# 不是标准的嵌套类型,默认为字符串
prop_schema["type"] = "string"
else:
# 如果没有类型信息,默认为字符串
prop_schema["type"] = "string"
# 处理非嵌套类型
if param_type:
prop_schema["type"] = self._get_json_schema_type(param_type)
else:
# 如果没有类型信息,默认为字符串
prop_schema["type"] = "string"
# 设置默认值
if param_default is not None:
@@ -456,7 +492,7 @@ class Registry:
{k: v["return_type"] for k, v in enhanced_info["status_methods"].items()}
)
for status_name, status_type in device_config["class"]["status_types"].items():
if status_type in ["Any", "None", "Unknown"]:
if isinstance(status_type, tuple) or status_type in ["Any", "None", "Unknown"]:
status_type = "String" # 替换成ROS的String便于显示
device_config["class"]["status_types"][status_name] = status_type
target_type = self._replace_type_with_class(status_type, device_id, f"状态 {status_name}")

View File

@@ -697,7 +697,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
else:
final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list]
try:
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=True)
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=False)
except Exception as e:
self.lab_logger().error(f"物料实例获取失败: {e}\n{traceback.format_exc()}")
error_skip = True

View File

@@ -199,22 +199,18 @@ class HostNode(BaseROS2DeviceNode):
"children": [],
},
)
resource_with_parent_name = []
resource_with_dirs_name = []
resource_ids_to_instance = {i["id"]: i for i in resources_config}
resource_name_to_with_parent_name = {}
for res in resources_config:
# if res.get("parent") and res.get("type") == "device" and res.get("class"):
# parent_id = res.get("parent")
# parent_res = resource_ids_to_instance[parent_id]
# if parent_res.get("type") == "device" and parent_res.get("class"):
# resource_with_parent_name.append(copy.deepcopy(res))
# resource_name_to_with_parent_name[resource_with_parent_name[-1]["id"]] = f"{parent_res['id']}/{res['id']}"
# resource_with_parent_name[-1]["id"] = f"{parent_res['id']}/{res['id']}"
# continue
resource_with_parent_name.append(copy.deepcopy(res))
# for edge in self.resources_edge_config:
# edge["source"] = resource_name_to_with_parent_name.get(edge.get("source"), edge.get("source"))
# edge["target"] = resource_name_to_with_parent_name.get(edge.get("target"), edge.get("target"))
temp_res = res
res_paths = [res]
while temp_res.get("parent"):
temp_res = resource_ids_to_instance[temp_res.get("parent")]
res_paths.append(temp_res)
dirs = "/" + "/".join([res["id"] for res in res_paths[::-1]])
new_res = copy.deepcopy(res)
new_res["data"]["unilabos_dirs"] = dirs
resource_with_dirs_name.append(new_res)
try:
for bridge in self.bridges:
if hasattr(bridge, "resource_add"):
@@ -222,7 +218,12 @@ class HostNode(BaseROS2DeviceNode):
client: HTTPClient = bridge
resource_start_time = time.time()
resource_add_res = client.resource_add(add_schema(resource_with_parent_name), False)
resource_add_res = client.resource_add(add_schema(resources_config), False)
# DEBUG ONLY
# for i in resource_with_dirs_name:
# http_req = self.bridges[-1].resource_get(i["data"]["unilabos_dirs"], True)
# res = self._resource_get_process(http_req)
# print(res)
resource_end_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
@@ -871,6 +872,12 @@ class HostNode(BaseROS2DeviceNode):
self.lab_logger().info(f"[Host Node-Resource] Add request completed, success: {success}")
return response
def _resource_get_process(self, data: Dict[str, Any]):
r = data["data"]
self.lab_logger().debug(f"[Host Node-Resource] Retrieved from bridge: {len(r)} resources")
resources = [convert_to_ros_msg(Resource, resource) for resource in r]
return resources
def _resource_get_callback(self, request: ResourceGet.Request, response: ResourceGet.Response):
"""
获取资源回调
@@ -884,22 +891,14 @@ class HostNode(BaseROS2DeviceNode):
Returns:
响应对象,包含查询到的资源
"""
self.lab_logger().info(f"[Host Node-Resource] Get request for ID: {request.id}")
if len(self.bridges) > 0:
# 云上物料服务,根据 id 查询物料
try:
r = self.bridges[-1].resource_get(request.id, request.with_children)["data"]
self.lab_logger().debug(f"[Host Node-Resource] Retrieved from bridge: {len(r)} resources")
except Exception as e:
self.lab_logger().error(f"[Host Node-Resource] Error retrieving from bridge: {str(e)}")
r = [resource for resource in self.resources_config if resource.get("id") == request.id]
self.lab_logger().warning(f"[Host Node-Resource] Retrieved from local: {len(r)} resources")
else:
# 本地物料服务,根据 id 查询物料
r = [resource for resource in self.resources_config if resource.get("id") == request.id]
self.lab_logger().debug(f"[Host Node-Resource] Retrieved from local: {len(r)} resources")
try:
http_req = self.bridges[-1].resource_get(request.id, request.with_children)
response.resources = self._resource_get_process(http_req)
return response
except Exception as e:
self.lab_logger().error(f"[Host Node-Resource] Error retrieving from bridge: {str(e)}")
r = [resource for resource in self.resources_config if resource.get("id") == request.id]
self.lab_logger().debug(f"[Host Node-Resource] Retrieved from local: {len(r)} resources")
response.resources = [convert_to_ros_msg(Resource, resource) for resource in r]
return response

View File

@@ -12,8 +12,7 @@ import traceback
import ast
import os
from pathlib import Path
from typing import Dict, List, Any, Optional, Callable, Type
from typing import Dict, List, Any, Optional, Callable, Type, Union, Tuple
__all__ = [
"ImportManager",
@@ -383,7 +382,7 @@ class ImportManager:
signature = inspect.signature(method)
return self._get_type_string(signature.return_annotation)
def _get_type_string(self, annotation) -> str:
def _get_type_string(self, annotation) -> Union[str, Tuple[str, Any]]:
"""将类型注解转换为Class Library中可搜索的类名"""
if annotation == inspect.Parameter.empty:
return "Any" # 如果没有注解返回Any
@@ -400,7 +399,7 @@ class ImportManager:
return "Int64MultiArray"
elif isinstance(arg0, float):
return "Float64MultiArray"
return "list"
return "list", self._get_type_string(arg0)
elif origin is dict:
return "dict"
elif origin is Optional: