feat: 新增AGV批量物料转运功能

添加AGV工作站设备驱动、注册表定义、批量转运编译器和消息定义。
包含跨工作站批量转运协议、AGV路径规划、容量分批等功能。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Junhan Chang
2026-03-25 13:10:56 +08:00
parent 9de473374f
commit ed80d786c1
11 changed files with 938 additions and 24 deletions

View File

@@ -553,8 +553,13 @@ def main():
os._exit(0)
if not BasicConfig.ak or not BasicConfig.sk:
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
os._exit(1)
if BasicConfig.test_mode:
print_status("测试模式:跳过 ak/sk 检查,使用占位凭据", "warning")
BasicConfig.ak = BasicConfig.ak or "test_ak"
BasicConfig.sk = BasicConfig.sk or "test_sk"
else:
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
os._exit(1)
graph: nx.Graph
resource_tree_set: ResourceTreeSet
resource_links: List[Dict[str, Any]]

View File

@@ -5,6 +5,7 @@ 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 .batch_transfer_protocol import generate_batch_transfer_protocol
from .add_protocol import generate_add_protocol
from .centrifuge_protocol import generate_centrifuge_protocol
from .filter_protocol import generate_filter_protocol
@@ -31,6 +32,7 @@ from .hydrogenate_protocol import generate_hydrogenate_protocol
action_protocol_generators = {
AddProtocol: generate_add_protocol,
AGVTransferProtocol: generate_agv_transfer_protocol,
BatchTransferProtocol: generate_batch_transfer_protocol,
AdjustPHProtocol: generate_adjust_ph_protocol,
CentrifugeProtocol: generate_centrifuge_protocol,
CleanProtocol: generate_clean_protocol,

View File

@@ -0,0 +1,127 @@
"""
AGV 编译器共用工具函数
从 physical_setup_graph 中发现 AGV 节点配置,
供 agv_transfer_protocol 和 batch_transfer_protocol 复用。
"""
from typing import Any, Dict, List, Optional
import networkx as nx
def find_agv_config(G: nx.Graph, agv_id: Optional[str] = None) -> Dict[str, Any]:
"""从设备图中发现 AGV 节点,返回其配置
查找策略:
1. 如果指定 agv_id直接读取该节点
2. 否则查找 class 为 "agv_transport_station" 的节点
3. 兜底查找 config 中包含 device_roles 的 workstation 节点
Returns:
{
"agv_id": str,
"device_roles": {"navigator": "...", "arm": "..."},
"route_table": {"A->B": {"nav_command": ..., "arm_pick": ..., "arm_place": ...}},
"capacity": int,
}
"""
if agv_id and agv_id in G.nodes:
node_data = G.nodes[agv_id]
config = _extract_config(node_data)
if config and "device_roles" in config:
return _build_agv_cfg(agv_id, config, G)
# 查找 agv_transport_station 类型
for nid, ndata in G.nodes(data=True):
node_class = _get_node_class(ndata)
if node_class == "agv_transport_station":
config = _extract_config(ndata)
return _build_agv_cfg(nid, config or {}, G)
# 兜底:查找带有 device_roles 的 workstation
for nid, ndata in G.nodes(data=True):
node_class = _get_node_class(ndata)
if node_class == "workstation":
config = _extract_config(ndata)
if config and "device_roles" in config:
return _build_agv_cfg(nid, config, G)
raise ValueError("设备图中未找到 AGV 节点(需 class=agv_transport_station 或 config.device_roles")
def get_agv_capacity(G: nx.Graph, agv_id: str) -> int:
"""从 AGV 的 Warehouse 子节点计算载具容量"""
for neighbor in G.successors(agv_id) if G.is_directed() else G.neighbors(agv_id):
ndata = G.nodes[neighbor]
node_type = _get_node_type(ndata)
if node_type == "warehouse":
config = _extract_config(ndata)
if config:
x = config.get("num_items_x", 1)
y = config.get("num_items_y", 1)
z = config.get("num_items_z", 1)
return x * y * z
# 如果没有 warehouse 子节点,尝试从配置中读取
return 0
def split_batches(items: list, capacity: int) -> List[list]:
"""按 AGV 容量分批
Args:
items: 待转运的物料列表
capacity: AGV 单批次容量
Returns:
分批后的列表的列表
"""
if capacity <= 0:
raise ValueError(f"AGV 容量必须 > 0当前: {capacity}")
return [items[i:i + capacity] for i in range(0, len(items), capacity)]
def _extract_config(node_data: dict) -> Optional[dict]:
"""从节点数据中提取 config 字段,兼容多种格式"""
# 直接 config 字段
config = node_data.get("config")
if isinstance(config, dict):
return config
# res_content 嵌套格式
res_content = node_data.get("res_content")
if hasattr(res_content, "config"):
return res_content.config if isinstance(res_content.config, dict) else None
if isinstance(res_content, dict):
return res_content.get("config")
return None
def _get_node_class(node_data: dict) -> str:
"""获取节点的 class 字段"""
res_content = node_data.get("res_content")
if hasattr(res_content, "model_dump"):
d = res_content.model_dump()
return d.get("class_", d.get("class", ""))
if isinstance(res_content, dict):
return res_content.get("class_", res_content.get("class", ""))
return node_data.get("class_", node_data.get("class", ""))
def _get_node_type(node_data: dict) -> str:
"""获取节点的 type 字段"""
res_content = node_data.get("res_content")
if hasattr(res_content, "type"):
return res_content.type or ""
if isinstance(res_content, dict):
return res_content.get("type", "")
return node_data.get("type", "")
def _build_agv_cfg(agv_id: str, config: dict, G: nx.Graph) -> Dict[str, Any]:
"""构建标准化的 AGV 配置"""
return {
"agv_id": agv_id,
"device_roles": config.get("device_roles", {}),
"route_table": config.get("route_table", {}),
"capacity": get_agv_capacity(G, agv_id),
}

View File

@@ -1,4 +1,12 @@
"""
AGV 单物料转运编译器
从 physical_setup_graph 中查询 AGV 配置device_roles, route_table
不再硬编码 device_id 和路由表。
"""
import networkx as nx
from unilabos.compile._agv_utils import find_agv_config
def generate_agv_transfer_protocol(
@@ -17,37 +25,32 @@ def generate_agv_transfer_protocol(
from_repo_id = from_repo_["id"]
to_repo_id = to_repo_["id"]
wf_list = {
("AiChemEcoHiWo", "zhixing_agv"): {"nav_command" : '{"target" : "LM14"}',
"arm_command": '{"task_name" : "camera/250111_biaozhi.urp"}'},
("AiChemEcoHiWo", "AGV"): {"nav_command" : '{"target" : "LM14"}',
"arm_command": '{"task_name" : "camera/250111_biaozhi.urp"}'},
# 从 G 中查询 AGV 配置
agv_cfg = find_agv_config(G)
device_roles = agv_cfg["device_roles"]
route_table = agv_cfg["route_table"]
("zhixing_agv", "Revvity"): {"nav_command" : '{"target" : "LM13"}',
"arm_command": '{"task_name" : "camera/250111_put_board.urp"}'},
route_key = f"{from_repo_id}->{to_repo_id}"
if route_key not in route_table:
raise KeyError(f"AGV 路由表中未找到路线: {route_key},可用路线: {list(route_table.keys())}")
("AGV", "Revvity"): {"nav_command" : '{"target" : "LM13"}',
"arm_command": '{"task_name" : "camera/250111_put_board.urp"}'},
route = route_table[route_key]
nav_device = device_roles.get("navigator", device_roles.get("nav"))
arm_device = device_roles.get("arm")
("Revvity", "HPLC"): {"nav_command": '{"target" : "LM13"}',
"arm_command": '{"task_name" : "camera/250111_hplc.urp"}'},
("HPLC", "Revvity"): {"nav_command": '{"target" : "LM13"}',
"arm_command": '{"task_name" : "camera/250111_lfp.urp"}'},
}
return [
{
"device_id": "zhixing_agv",
"device_id": nav_device,
"action_name": "send_nav_task",
"action_kwargs": {
"command": wf_list[(from_repo_id, to_repo_id)]["nav_command"]
"command": route["nav_command"]
}
},
{
"device_id": "zhixing_ur_arm",
"device_id": arm_device,
"action_name": "move_pos_task",
"action_kwargs": {
"command": wf_list[(from_repo_id, to_repo_id)]["arm_command"]
"command": route.get("arm_command", route.get("arm_place", ""))
}
}
]

View File

@@ -0,0 +1,228 @@
"""
批量物料转运编译器
将 BatchTransferProtocol 编译为多批次的 nav → pick × N → nav → place × N 动作序列。
自动按 AGV 容量分批,全程维护三方 children dict 的物料系统一致性。
"""
import copy
from typing import Any, Dict, List
import networkx as nx
from unilabos.compile._agv_utils import find_agv_config, split_batches
def generate_batch_transfer_protocol(
G: nx.Graph,
from_repo: dict,
to_repo: dict,
transfer_resources: list,
from_positions: list,
to_positions: list,
) -> List[Dict[str, Any]]:
"""编译批量转运协议为可执行的 action steps
Args:
G: 设备图 (physical_setup_graph)
from_repo: 来源工站资源 dict{station_id: {..., children: {...}}}
to_repo: 目标工站资源 dict含堆栈和位置信息
transfer_resources: 被转运的物料列表Resource dict
from_positions: 来源 slot 位置列表(与 transfer_resources 平行)
to_positions: 目标 slot 位置列表(与 transfer_resources 平行)
Returns:
action steps 列表ROS2WorkstationNode 按序执行
"""
if not transfer_resources:
return []
n = len(transfer_resources)
if len(from_positions) != n or len(to_positions) != n:
raise ValueError(
f"transfer_resources({n}), from_positions({len(from_positions)}), "
f"to_positions({len(to_positions)}) 长度不一致"
)
# 组合为内部 transfer_items 便于分批处理
transfer_items = []
for i in range(n):
res = transfer_resources[i] if isinstance(transfer_resources[i], dict) else {}
transfer_items.append({
"resource_id": res.get("id", res.get("name", "")),
"resource_uuid": res.get("sample_id", ""),
"from_position": from_positions[i],
"to_position": to_positions[i],
"resource": res,
})
# 查询 AGV 配置
agv_cfg = find_agv_config(G)
agv_id = agv_cfg["agv_id"]
device_roles = agv_cfg["device_roles"]
route_table = agv_cfg["route_table"]
capacity = agv_cfg["capacity"]
if capacity <= 0:
raise ValueError(f"AGV {agv_id} 容量为 0请检查 Warehouse 子节点配置")
nav_device = device_roles.get("navigator", device_roles.get("nav"))
arm_device = device_roles.get("arm")
if not nav_device or not arm_device:
raise ValueError(f"AGV {agv_id} device_roles 缺少 navigator 或 arm: {device_roles}")
from_repo_ = list(from_repo.values())[0]
to_repo_ = list(to_repo.values())[0]
from_station_id = from_repo_["id"]
to_station_id = to_repo_["id"]
# 查找路由
route_to_source = _find_route(route_table, agv_id, from_station_id)
route_to_target = _find_route(route_table, from_station_id, to_station_id)
# 构建 AGV carrier 的 children dict用于 compile 阶段状态追踪)
agv_carrier_children: Dict[str, Any] = {}
# 计算 slot 名称A01, A02, B01, ...
agv_slot_names = _get_agv_slot_names(G, agv_cfg)
# 分批
batches = split_batches(transfer_items, capacity)
steps: List[Dict[str, Any]] = []
for batch_idx, batch in enumerate(batches):
is_last_batch = (batch_idx == len(batches) - 1)
# 阶段 1: AGV 导航到来源工站
steps.append({
"device_id": nav_device,
"action_name": "send_nav_task",
"action_kwargs": {
"command": route_to_source.get("nav_command", "")
},
"_comment": f"批次{batch_idx + 1}/{len(batches)}: AGV 导航至来源 {from_station_id}"
})
# 阶段 2: 逐个 pick
for item_idx, item in enumerate(batch):
from_pos = item["from_position"]
slot = agv_slot_names[item_idx] if item_idx < len(agv_slot_names) else f"S{item_idx + 1}"
# compile 阶段更新 children dict
if from_pos in from_repo_.get("children", {}):
resource_data = from_repo_["children"].pop(from_pos)
resource_data["parent"] = agv_id
agv_carrier_children[slot] = resource_data
steps.append({
"device_id": arm_device,
"action_name": "move_pos_task",
"action_kwargs": {
"command": route_to_source.get("arm_pick", route_to_source.get("arm_command", ""))
},
"_transfer_meta": {
"phase": "pick",
"resource_uuid": item.get("resource_uuid", ""),
"resource_id": item.get("resource_id", ""),
"from_parent": from_station_id,
"from_position": from_pos,
"agv_slot": slot,
},
"_comment": f"Pick {item.get('resource_id', from_pos)} → AGV.{slot}"
})
# 阶段 3: AGV 导航到目标工站
steps.append({
"device_id": nav_device,
"action_name": "send_nav_task",
"action_kwargs": {
"command": route_to_target.get("nav_command", "")
},
"_comment": f"批次{batch_idx + 1}: AGV 导航至目标 {to_station_id}"
})
# 阶段 4: 逐个 place
for item_idx, item in enumerate(batch):
to_pos = item["to_position"]
slot = agv_slot_names[item_idx] if item_idx < len(agv_slot_names) else f"S{item_idx + 1}"
# compile 阶段更新 children dict
if slot in agv_carrier_children:
resource_data = agv_carrier_children.pop(slot)
resource_data["parent"] = to_repo_["id"]
to_repo_["children"][to_pos] = resource_data
steps.append({
"device_id": arm_device,
"action_name": "move_pos_task",
"action_kwargs": {
"command": route_to_target.get("arm_place", route_to_target.get("arm_command", ""))
},
"_transfer_meta": {
"phase": "place",
"resource_uuid": item.get("resource_uuid", ""),
"resource_id": item.get("resource_id", ""),
"to_parent": to_station_id,
"to_position": to_pos,
"agv_slot": slot,
},
"_comment": f"Place AGV.{slot}{to_station_id}.{to_pos}"
})
# 如果还有下一批AGV 需要返回来源取料
if not is_last_batch:
steps.append({
"device_id": nav_device,
"action_name": "send_nav_task",
"action_kwargs": {
"command": route_to_source.get("nav_command", "")
},
"_comment": f"AGV 返回来源 {from_station_id} 取下一批"
})
return steps
def _find_route(route_table: Dict[str, Any], from_id: str, to_id: str) -> Dict[str, str]:
"""在路由表中查找路线,支持 A->B 和 (A, B) 两种 key 格式"""
# 优先 "A->B" 格式
key = f"{from_id}->{to_id}"
if key in route_table:
return route_table[key]
# 兼容 tuple keyJSON 中以逗号分隔字符串表示)
tuple_key = f"({from_id}, {to_id})"
if tuple_key in route_table:
return route_table[tuple_key]
raise KeyError(f"路由表中未找到: {key},可用路线: {list(route_table.keys())}")
def _get_agv_slot_names(G: nx.Graph, agv_cfg: dict) -> List[str]:
"""从设备图中获取 AGV Warehouse 的 slot 名称列表"""
agv_id = agv_cfg["agv_id"]
neighbors = G.successors(agv_id) if G.is_directed() else G.neighbors(agv_id)
for neighbor in neighbors:
ndata = G.nodes[neighbor]
node_type = ndata.get("type", "")
res_content = ndata.get("res_content")
if hasattr(res_content, "type"):
node_type = res_content.type or node_type
elif isinstance(res_content, dict):
node_type = res_content.get("type", node_type)
if node_type == "warehouse":
config = ndata.get("config", {})
if hasattr(res_content, "config") and isinstance(res_content.config, dict):
config = res_content.config
elif isinstance(res_content, dict):
config = res_content.get("config", config)
num_x = config.get("num_items_x", 1)
num_y = config.get("num_items_y", 1)
num_z = config.get("num_items_z", 1)
# 与 warehouse_factory 一致的命名
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
len_x = num_x if num_z == 1 else (num_y if num_x == 1 else num_x)
len_y = num_y if num_z == 1 else (num_z if num_x == 1 else num_z)
return [f"{letters[j]}{i + 1:02d}" for i in range(len_x) for j in range(len_y)]
# 兜底生成通用名称
capacity = agv_cfg.get("capacity", 4)
return [f"S{i + 1}" for i in range(capacity)]

View File

View File

@@ -0,0 +1,127 @@
"""
AGV 通用转运工站 Driver
继承 WorkstationBase通过 WorkstationNodeCreator 自动获得 ROS2WorkstationNode 能力。
Warehouse 作为 children 中的资源节点,由 attach_resource() 自动注册到 resource_tracker。
deck=None不使用 PLR Deck 抽象。
"""
from typing import Any, Dict, List, Optional
from pylabrobot.resources import Deck
from unilabos.devices.workstation.workstation_base import WorkstationBase
from unilabos.resources.warehouse import WareHouse
from unilabos.utils import logger
class AGVTransportStation(WorkstationBase):
"""通用 AGV 转运工站
初始化链路(零框架改动):
ROS2DeviceNode.__init__():
issubclass(AGVTransportStation, WorkstationBase) → True
→ WorkstationNodeCreator.create_instance(data):
data["deck"] = None
→ DeviceClassCreator.create_instance(data) → AGVTransportStation(deck=None, ...)
→ attach_resource(): children 中 type="warehouse" → resource_tracker.add_resource(wh)
→ ROS2WorkstationNode(protocol_type=[...], children=[nav, arm], ...)
→ driver.post_init(ros_node):
self.carrier 从 resource_tracker 中获取 WareHouse
"""
def __init__(
self,
deck: Optional[Deck] = None,
children: Optional[List[Any]] = None,
route_table: Optional[Dict[str, Dict[str, str]]] = None,
device_roles: Optional[Dict[str, str]] = None,
**kwargs,
):
super().__init__(deck=None, **kwargs)
self.route_table: Dict[str, Dict[str, str]] = route_table or {}
self.device_roles: Dict[str, str] = device_roles or {}
# ============ 载具 (Warehouse) ============
@property
def carrier(self) -> Optional[WareHouse]:
"""从 resource_tracker 中找到 AGV 载具 Warehouse"""
if not hasattr(self, "_ros_node"):
return None
for res in self._ros_node.resource_tracker.resources:
if isinstance(res, WareHouse):
return res
return None
@property
def capacity(self) -> int:
"""AGV 载具总容量slot 数)"""
wh = self.carrier
if wh is None:
return 0
return wh.num_items_x * wh.num_items_y * wh.num_items_z
@property
def free_slots(self) -> List[str]:
"""返回当前空闲 slot 名称列表"""
wh = self.carrier
if wh is None:
return []
ordering = getattr(wh, "_ordering", {})
return [name for name, site in ordering.items() if site.resource is None]
@property
def occupied_slots(self) -> Dict[str, Any]:
"""返回已占用的 slot → Resource 映射"""
wh = self.carrier
if wh is None:
return {}
ordering = getattr(wh, "_ordering", {})
return {name: site.resource for name, site in ordering.items() if site.resource is not None}
# ============ 路由查询 ============
def resolve_route(self, from_station: str, to_station: str) -> Dict[str, str]:
"""查询路由表,返回导航和机械臂指令
Args:
from_station: 来源工站 ID
to_station: 目标工站 ID
Returns:
{"nav_command": "...", "arm_pick": "...", "arm_place": "..."}
Raises:
KeyError: 路由表中未找到对应路线
"""
route_key = f"{from_station}->{to_station}"
if route_key not in self.route_table:
raise KeyError(f"路由表中未找到路线: {route_key}")
return self.route_table[route_key]
def get_device_id(self, role: str) -> str:
"""获取子设备 ID
Args:
role: 设备角色,如 "navigator", "arm"
Returns:
设备 ID 字符串
Raises:
KeyError: 未配置该角色的设备
"""
if role not in self.device_roles:
raise KeyError(f"未配置设备角色: {role},当前已配置: {list(self.device_roles.keys())}")
return self.device_roles[role]
# ============ 生命周期 ============
def post_init(self, ros_node) -> None:
super().post_init(ros_node)
wh = self.carrier
if wh is not None:
logger.info(f"AGV {ros_node.device_id} 载具已就绪: {wh.name}, 容量={self.capacity}")
else:
logger.warning(f"AGV {ros_node.device_id} 未发现 Warehouse 载具资源")

View File

@@ -0,0 +1,368 @@
agv_transport_station:
category:
- work_station
- transport_agv
class:
action_value_mappings:
AGVTransferProtocol:
feedback: {}
goal:
from_repo: from_repo
from_repo_position: from_repo_position
to_repo: to_repo
to_repo_position: to_repo_position
goal_default:
from_repo:
category: ''
children: []
config: ''
data: ''
id: ''
name: ''
parent: ''
pose:
orientation:
w: 1.0
x: 0.0
y: 0.0
z: 0.0
position:
x: 0.0
y: 0.0
z: 0.0
sample_id: ''
type: ''
from_repo_position: ''
to_repo:
category: ''
children: []
config: ''
data: ''
id: ''
name: ''
parent: ''
pose:
orientation:
w: 1.0
x: 0.0
y: 0.0
z: 0.0
position:
x: 0.0
y: 0.0
z: 0.0
sample_id: ''
type: ''
to_repo_position: ''
handles: {}
result: {}
schema:
description: ''
properties:
feedback:
properties:
status:
type: string
required:
- status
title: AGVTransfer_Feedback
type: object
goal:
properties:
from_repo:
properties:
category:
type: string
children:
items:
type: string
type: array
config:
type: string
data:
type: string
id:
type: string
name:
type: string
parent:
type: string
pose:
properties:
orientation:
properties:
w:
type: number
x:
type: number
y:
type: number
z:
type: number
required:
- x
- y
- z
- w
title: orientation
type: object
position:
properties:
x:
type: number
y:
type: number
z:
type: number
required:
- x
- y
- z
title: position
type: object
required:
- position
- orientation
title: pose
type: object
sample_id:
type: string
type:
type: string
required:
- id
- name
- sample_id
- children
- parent
- type
- category
- pose
- config
- data
title: from_repo
type: object
from_repo_position:
type: string
to_repo:
properties:
category:
type: string
children:
items:
type: string
type: array
config:
type: string
data:
type: string
id:
type: string
name:
type: string
parent:
type: string
pose:
properties:
orientation:
properties:
w:
type: number
x:
type: number
y:
type: number
z:
type: number
required:
- x
- y
- z
- w
title: orientation
type: object
position:
properties:
x:
type: number
y:
type: number
z:
type: number
required:
- x
- y
- z
title: position
type: object
required:
- position
- orientation
title: pose
type: object
sample_id:
type: string
type:
type: string
required:
- id
- name
- sample_id
- children
- parent
- type
- category
- pose
- config
- data
title: to_repo
type: object
to_repo_position:
type: string
required:
- from_repo
- from_repo_position
- to_repo
- to_repo_position
title: AGVTransfer_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: AGVTransfer_Result
type: object
required:
- goal
title: AGVTransfer
type: object
type: AGVTransfer
BatchTransferProtocol:
feedback: {}
goal:
from_positions: from_positions
from_repo: from_repo
to_positions: to_positions
to_repo: to_repo
transfer_resources: transfer_resources
goal_default:
from_positions: []
from_repo:
category: ''
children: []
config: ''
data: ''
id: ''
name: ''
parent: ''
pose:
orientation:
w: 1.0
x: 0.0
y: 0.0
z: 0.0
position:
x: 0.0
y: 0.0
z: 0.0
sample_id: ''
type: ''
to_positions: []
to_repo:
category: ''
children: []
config: ''
data: ''
id: ''
name: ''
parent: ''
pose:
orientation:
w: 1.0
x: 0.0
y: 0.0
z: 0.0
position:
x: 0.0
y: 0.0
z: 0.0
sample_id: ''
type: ''
transfer_resources: []
handles: {}
result: {}
schema:
description: AGV 批量物料转运协议
properties:
feedback: {}
goal:
properties:
from_positions:
items:
type: string
type: array
from_repo:
type: object
to_positions:
items:
type: string
type: array
to_repo:
type: object
transfer_resources:
items:
type: object
type: array
required:
- from_repo
- to_repo
- transfer_resources
- from_positions
- to_positions
type: object
result: {}
required:
- goal
title: BatchTransferProtocol
type: object
type: BatchTransfer
module: unilabos.devices.transport.agv_workstation:AGVTransportStation
status_types:
agv_status: String
carrier_status: String
type: python
config_info: []
description: 通用 AGV 转运工站。继承 WorkstationBase通过 Warehouse 子资源管理物料中转态。支持单物料和批量物料转运协议,路由表和子设备配置全部由
JSON 驱动。
handles: []
icon: ''
init_param_schema:
config:
properties:
device_roles:
properties:
arm:
type: string
navigator:
type: string
type: object
protocol_type:
items:
type: string
type: array
route_table:
type: object
type: object
data:
properties:
agv_status:
type: string
carrier_status:
type: string
type: object
version: 1.0.0

View File

@@ -577,17 +577,38 @@
{
"id": "AGV",
"name": "AGV",
"children": ["zhixing_agv", "zhixing_ur_arm"],
"children": ["zhixing_agv", "zhixing_ur_arm", "agv_platform"],
"parent": null,
"type": "device",
"class": "workstation",
"class": "agv_transport_station",
"position": {
"x": 698.1111111111111,
"y": 478,
"z": 0
},
"config": {
"protocol_type": ["AGVTransferProtocol"]
"protocol_type": ["AGVTransferProtocol", "BatchTransferProtocol"],
"device_roles": {
"navigator": "zhixing_agv",
"arm": "zhixing_ur_arm"
},
"route_table": {
"AiChemEcoHiWo->Revvity": {
"nav_command": "{\"target\": \"LM14\"}",
"arm_pick": "{\"task_name\": \"camera/250111_biaozhi.urp\"}",
"arm_place": "{\"task_name\": \"camera/250111_put_board.urp\"}"
},
"Revvity->HPLC": {
"nav_command": "{\"target\": \"LM13\"}",
"arm_pick": "{\"task_name\": \"camera/250111_lfp.urp\"}",
"arm_place": "{\"task_name\": \"camera/250111_hplc.urp\"}"
},
"HPLC->Revvity": {
"nav_command": "{\"target\": \"LM13\"}",
"arm_pick": "{\"task_name\": \"camera/250111_hplc.urp\"}",
"arm_place": "{\"task_name\": \"camera/250111_lfp.urp\"}"
}
}
},
"data": {
}
@@ -627,6 +648,27 @@
},
"data": {
}
},
{
"id": "agv_platform",
"name": "agv_platform",
"children": [],
"parent": "AGV",
"type": "warehouse",
"class": "",
"position": {
"x": 698.1111111111111,
"y": 478,
"z": 0
},
"config": {
"name": "agv_platform",
"num_items_x": 2,
"num_items_y": 1,
"num_items_z": 1
},
"data": {
}
}
],
"links": [

View File

@@ -96,6 +96,7 @@ set(action_files
"action/WorkStationRun.action"
"action/AGVTransfer.action"
"action/BatchTransfer.action"
"action/DispenStationSolnPrep.action"
"action/DispenStationVialFeed.action"

View File

@@ -0,0 +1,11 @@
# 批量物料转运
Resource from_repo
Resource to_repo
Resource[] transfer_resources
string[] from_positions
string[] to_positions
---
string return_info
bool success
---
string status