單一事實來源(Single Source of Truth) 任何接口變更,先改本文件、再改代碼。 文件位置:
master/task_manager/interfaces.md維護人:蒲(Task Manager owner) 對齊文件:May_12/Task_Manager_架構教學.md版本:v1.0(2026-05-12)
目錄
- 0. 約定(讀之前先看)
- 1. 系統參與者與 Channel 總圖
- 2. Redis Channel 規範(Pub/Sub)
- 3. Redis Key 規範(持久化狀態)
- 4. HTTP / WebSocket 接口(HMI)
- 5. 場景 Profile 規範
- 6. 子任務語意(給 Skill Manager)
- 7. 故障碼總表
- 8. 模塊間 Python 接口(同進程)
- 9. Mock 工具(測試 / 開發階段)
- 10. 版本與兼容性
- 附錄 A:欄位字典
- 附錄 B:變更紀錄
0. 約定(讀之前先看)
| 項目 | 約定 |
|---|---|
| 編碼 | 所有字串 UTF-8、所有 JSON 不含尾隨逗號 |
| 時間戳 | ts 一律 Unix 秒(float),精確到毫秒 |
| ID 規則 | task_id / subtask_id 用 UUID4 hex(無連字號),package_id 由視覺端分配(見 §2.1) |
| 座標系 | 機器人 base frame,X 向前、Y 向左、Z 向上,單位 公尺;角度單位 弧度 |
| 列舉值大小寫 | 全部 lowercase snake_case(例:barcode_face: "up") |
| 必填 vs 可選 | 表格中 必填 欄為 Y 表示生產者必須提供;N 表示可缺省 |
| 未知值 | 字串用 "unknown"、數值用 null;不要用空字串 |
| 失敗回傳 | 同步調用走 HTTP,失敗回 4xx/5xx;非同步走 channel,失敗發 task_faults |
1. 系統參與者與 Channel 總圖
┌─────────────┐ vision_events ┌──────────────────┐
│ Algo-Ground │ ──────────────────►│ │
│ (謝) │ │ │
└─────────────┘ │ │
│ Task Manager │ ──── task_progress ────► UI
┌─────────────┐ skill_events │ (蒲) │
│ Skill Mgr │ ──────────────────►│ │ ──── task_faults ────► UI
│ (李/朱) │ ◄── subtask_cmd ──│ │
└─────────────┘ └──────────────────┘
▲
│ HTTP / WS
│
┌────┴────┐
│ UI │
│ (蒲) │
└─────────┘
| 角色 | 進程 | 訂閱 | 發佈 |
|---|---|---|---|
| Algo-Ground(謝) | 獨立 GPU 進程 | — | vision_events |
| Task Manager(蒲) | master/run.py |
vision_events, skill_events, AGENT_REGISTRATION, {robot}_to_RoboOS |
task_progress, task_faults, roboos_to_{robot} |
| Skill Manager(李/朱,住在 slaver) | slaver/run.py |
roboos_to_{robot} |
skill_events, {robot}_to_RoboOS |
| UI | 瀏覽器 / pad | WebSocket | HTTP REST |
| 任務開關(尹) | 嵌入 master/run.py |
— | 提供 Python API |
2. Redis Channel 規範(Pub/Sub)
2.1 vision_events ← Algo-Ground(謝)發 / Task Monitor 收
頻率:10–15 Hz(建議) 負載上限:每幀 ≤ 32 KB(不含圖像,圖像走檔案路徑)
{
"event": "vision_update",
"ts": 1747040000.123,
"frame_id": "img_000123",
"camera": "head",
"image_path": "/tmp/frames/img_000123.jpg",
"objects": [
{
"id": "P_2026051200001",
"class": "package_box",
"bbox": [120, 80, 380, 290],
"score": 0.93,
"pose_3d": {
"position": [0.42, 0.10, 0.85],
"yaw": 0.30
},
"size": {"length": 0.22, "width": 0.15, "height": 0.10},
"barcode_face": "down",
"barcode_score": 0.81,
"zone": "left_conveyor"
}
]
}
| 欄位 | 型別 | 必填 | 列舉 / 範圍 | 說明 |
|---|---|---|---|---|
event |
str | Y | "vision_update" |
固定字串 |
ts |
float | Y | — | Unix 秒 |
frame_id |
str | Y | — | 全局唯一 |
camera |
str | Y | head / left_wrist / right_wrist |
— |
image_path |
str | N | — | 共享檔案路徑(給日後 keyframe 回放) |
objects[].id |
str | Y | P_yyyyMMddNNNNN |
同一包裹跨幀必須穩定(謝負責 tracking) |
objects[].class |
str | Y | package_box / barcode / obstacle |
初版只關心 package_box |
objects[].bbox |
int[4] | Y | [x1,y1,x2,y2] 像素座標 |
— |
objects[].score |
float | Y | [0, 1] |
檢測置信度 |
objects[].pose_3d.position |
float[3] | Y | base frame,m | — |
objects[].pose_3d.yaw |
float | N | rad | 可缺省(未做 6D 估計時) |
objects[].size |
obj | N | m | 用於選擇手與抓取點 |
objects[].barcode_face |
str | Y | up / down / side / unknown |
最關鍵欄位 |
objects[].barcode_score |
float | N | [0, 1] |
朝向判斷置信度 |
objects[].zone |
str | Y | left_conveyor / workspace / right_conveyor / unknown |
用區域而非絕對座標做狀態跳轉 |
平滑策略:Task Monitor 端會做「連續 3 幀同一結果才確認」,所以謝那邊不需要過度濾波。
2.2 skill_events ← Skill Manager(李/朱)發 / Task Monitor 收
頻率:事件驅動(每個技能起 / 訖各一條) 負載上限:≤ 4 KB
{
"event": "skill_update",
"ts": 1747040001.456,
"robot_name": "humanoid_01",
"task_id": "9f1b2c...",
"subtask_id": "t1",
"skill_name": "G1_left_grasp",
"phase": "end",
"status": "ok",
"fault_code": null,
"details": {
"duration_ms": 1340,
"grasped_object": "P_2026051200001"
}
}
| 欄位 | 型別 | 必填 | 列舉 | 說明 |
|---|---|---|---|---|
event |
str | Y | "skill_update" |
— |
ts |
float | Y | — | — |
robot_name |
str | Y | — | 必須與註冊時一致 |
task_id |
str | Y | — | — |
subtask_id |
str | Y | — | 對應 Task Manager 派發時的 ID |
skill_name |
str | Y | 見 §6.2 | — |
phase |
str | Y | begin / progress / end |
— |
status |
str | Y | ok / fail / timeout / cancelled |
phase=end 必填有效值;phase=begin 一律 ok |
fault_code |
str / null | N | SK-xxx |
status≠ok 時必填 |
details |
obj | N | — | 自由欄位 |
2.3 task_progress ← Task Monitor 發 / UI 收
頻率:每次 FSM 狀態跳轉、每秒至多 5 次心跳 負載上限:≤ 2 KB
{
"event": "task_progress",
"ts": 1747040002.000,
"task_id": "9f1b2c...",
"subtask_id": "t1",
"package_id": "P_2026051200001",
"fsm_state": "REORIENT",
"fsm_prev_state": "PICK",
"progress": 0.40,
"barcode_face": "down",
"zone": "workspace",
"queue": {"active": 2, "pending": 5}
}
| 欄位 | 型別 | 必填 | 列舉 | 說明 |
|---|---|---|---|---|
fsm_state |
str | Y | PICK / REORIENT / PUSH / DONE / FAULT |
— |
progress |
float | Y | [0, 1] |
PICK=0.0, REORIENT=0.33, PUSH=0.66, DONE=1.0 |
queue.active |
int | Y | — | 當前活躍任務數 |
queue.pending |
int | Y | — | 等待中任務數 |
2.4 task_faults ← Task Diagnostics 發 / UI 收
頻率:事件驅動 負載上限:≤ 8 KB
{
"event": "task_fault",
"ts": 1747040003.123,
"code": "TM-004",
"name": "REORIENT_FAIL",
"severity": "warn",
"task_id": "9f1b2c...",
"subtask_id": "t2",
"package_id": "P_2026051200001",
"details": {
"retry": 3,
"last_barcode_face": "side",
"trigger_evt": "vision_update"
},
"action_taken": "replan"
}
| 欄位 | 型別 | 必填 | 列舉 | 說明 |
|---|---|---|---|---|
code |
str | Y | TM-xxx(見 §7) |
— |
severity |
str | Y | info / warn / error / critical |
— |
action_taken |
str | Y | retry / replan / pause / abort / none |
診斷模塊的決策 |
2.5 沿用 RoboOS 既有 channel
| Channel | 方向 | Schema | 文件 |
|---|---|---|---|
AGENT_REGISTRATION |
Slaver → Master | {robot_name, robot_tool, robot_state, timestamp} |
slaver/run.py:238 |
roboos_to_{robot_name} |
Master → Slaver | {task_id, task, order} |
master/agents/agent.py:266 |
{robot_name}_to_RoboOS |
Slaver → Master | {robot_name, subtask_handle, subtask_result, tools, task_id} |
slaver/run.py:166 |
本期擴展:
roboos_to_{robot_name}中的task欄位現階段仍是純自然語言(沿用),下期演進為結構化(§6.3)。
3. Redis Key 規範(持久化狀態)
| Key | 型別 | TTL | 寫入者 | 讀取者 | 內容 |
|---|---|---|---|---|---|
task:{task_id}:meta |
string (JSON) | 24h | Task Manager | UI / 日誌 | TaskContext.meta |
task:{task_id}:queue |
string (JSON) | 24h | Task Manager | Task Manager | 子任務隊列快照 |
task:{task_id}:faults |
list (JSON 元素) | 24h | Task Diagnostics | UI / 任務報告 | append-only 故障歷史 |
task:{task_id}:keyframes |
list (JSON 元素) | 24h | Task Monitor | 任務報告 | {ts, image_path, fsm_state} |
task:current_scene |
string (JSON) | 60s | Task Manager(從 vision 聚合) | Task Planner | 規劃時讀此 key 注入 prompt |
task:stats |
hash | ∞ | Task Manager | UI | success_count / fail_count / avg_duration_ms |
task:current_id |
string | 24h | Task Scheduler | 全部模塊 | 當前活躍 task_id(單 robot 假設) |
task:current_scene 範例:
{
"ts": 1747040000.123,
"packages": [
{"id": "P_2026051200001", "zone": "left_conveyor", "barcode_face": "down", "size": {"length": 0.22, "width": 0.15, "height": 0.10}},
{"id": "P_2026051200002", "zone": "left_conveyor", "barcode_face": "up", "size": {"length": 0.18, "width": 0.12, "height": 0.08}}
],
"right_conveyor_free_slots": 2
}
4. HTTP / WebSocket 接口(HMI)
4.1 HTTP(沿用 Flask,基底 http://{master_host}:5000)
| 端點 | 方法 | 請求 | 回應 | 說明 |
|---|---|---|---|---|
/publish_task |
POST | {"task": str \| str[], "refresh": bool, "task_id"?: str} |
{"status": "success", "data": {...subtask_list}} |
沿用既有,作為自然語言任務入口 |
/control/start |
POST | {} |
{"state": str} |
啟動調度器(從 IDLE → RUNNING) |
/control/pause |
POST | {} |
{"state": str} |
— |
/control/resume |
POST | {} |
{"state": str} |
— |
/control/abort |
POST | {"reason"?: str} |
{"state": str} |
— |
/control/reset |
POST | {} |
{"state": str} |
清空隊列、回 IDLE |
/mode |
POST | {"mode": "teleop" \| "auto" \| "semi"} |
{"mode": str} |
— |
/task_status |
GET | — | 見下方 §4.1.1 | — |
/system_status |
GET | — | {cpu_load, memory_usage} |
沿用既有 |
/robot_status |
GET | — | [{robot_name, robot_state}] |
沿用既有 |
/faults |
GET | ?task_id=...&limit=20 |
[FaultRecord] |
讀 task:{task_id}:faults |
/scene/snapshot |
GET | — | task:current_scene |
給 UI 顯示包裹列表 |
4.1.1 /task_status 回應
{
"scheduler_state": "RUNNING",
"mode": "auto",
"current_task_id": "9f1b2c...",
"queue": {
"active": [
{"subtask_id": "t1", "subtask": "...", "robot_name": "humanoid_01", "fsm_state": "REORIENT"}
],
"pending": [
{"subtask_id": "t4", "subtask": "...", "robot_name": "humanoid_01"}
]
},
"stats": {"success_count": 12, "fail_count": 1, "avg_duration_ms": 8200}
}
4.2 WebSocket(沿用 flask-socketio,socket.io 路徑 /)
| Event 名稱 | 方向 | Payload | 對應 |
|---|---|---|---|
text_update |
server→client | {"data": str} |
沿用既有(日誌串流) |
task_progress |
server→client | 見 §2.3 | 直接轉發 task_progress channel |
task_fault |
server→client | 見 §2.4 | 直接轉發 task_faults channel |
scene_update |
server→client | task:current_scene 內容 |
1 Hz 推送 |
5. 場景 Profile 規範
文件位置:master/scene/profile.yaml
讀取者:TaskPlanner 在規劃時注入 prompt;TaskMonitor 校驗 zone 是否合法。
version: "1.0"
domain: package_sorting
scene:
- name: left_conveyor
type: conveyor
role: source
bounds_xyz: [[0.20, -0.50, 0.78], [0.80, -0.10, 0.92]]
contains: []
- name: right_conveyor
type: conveyor
role: sink
bounds_xyz: [[0.20, 0.10, 0.78], [0.80, 0.50, 0.92]]
contains: []
- name: workspace
type: zone
role: manipulation
bounds_xyz: [[0.30, -0.10, 0.80], [0.70, 0.10, 1.20]]
properties:
task_queue_max_length: 3
barcode_face_required: up
reorient_max_retry: 3
grasp_timeout_sec: 8.0
push_timeout_sec: 6.0
vision_event_channel: vision_events
skill_event_channel: skill_events
vision_confirm_frames: 3 # 連續 N 幀相同才確認跳轉
barcode_face_enum: [up, down, side, unknown]
zone_enum: [left_conveyor, workspace, right_conveyor, unknown]
規則:任何
zone/barcode_face取值都必須在這裡的列舉內,否則 Task Monitor 視為unknown。
6. 子任務語意(給 Skill Manager)
6.1 階段 1:自然語言(首版,沿用 RoboOS)
派發到 roboos_to_{robot_name} 的 task 欄位為以下三種固定模板之一:
| 模板 | 範例 |
|---|---|
| Pick | "Pick package {package_id} from left_conveyor" |
| Reorient | "Rotate package {package_id} so barcode face is up" |
| Push | "Push package {package_id} onto right_conveyor" |
Skill Manager(李)在自家的 ToolCallingAgent 內用 LLM 解析 → 選擇 MCP tool。
6.2 技能命名規範(給李、朱)
按 PDF「結構化技能表」對齊:
| 編號 | 名稱 | 標準 skill_name(出現在 skill_events) |
|---|---|---|
| P1 | 視覺定位 | P1_visual_locate |
| P2 | 視覺檢測 | P2_visual_detect |
| M1 | 頭部運動 | M1_head_motion |
| M2 | 腰部運動 | M2_waist_motion |
| G1 | 左手抓取 | G1_left_grasp |
| G2 | 右手抓取 | G2_right_grasp |
| O1 | 拨移包裹 | O1_slide_package |
| O2 | 左手旋轉包裹 | O2_left_rotate_package |
| O3 | 右手旋轉包裹 | O3_right_rotate_package |
| C1 | 左右手傳遞 | C1_hand_to_hand |
| C2 | 雙手搬移 | C2_dual_arm_carry |
6.3 階段 2:結構化(規劃中,下期演進)
{
"task_id": "9f1b2c...",
"subtask_id": "t1",
"action": "pick",
"target": {"package_id": "P_2026051200001"},
"from_zone": "left_conveyor",
"constraints": {"timeout_sec": 8.0, "prefer_hand": "left"}
}
階段 2 在 Sprint 4 之後啟用,現階段雙方都實作 §6.1 即可。
7. 故障碼總表
7.1 任務級(由 Task Manager 維護)
| 碼 | 名稱 | severity | 觸發條件 | 預設 action_taken |
|---|---|---|---|---|
TM-001 |
PLAN_FAIL |
error | LLM 連續 ≥5 次無法輸出合法 JSON | abort |
TM-002 |
SCENE_EMPTY |
info | 視覺連續 ≥20 幀無 package | none(標記任務完成) |
TM-003 |
GRASP_TIMEOUT |
warn | PICK 階段 > grasp_timeout_sec |
retry(一次) |
TM-004 |
REORIENT_FAIL |
warn | REORIENT 重試 ≥ reorient_max_retry |
replan |
TM-005 |
PLACE_OUT_OF_ZONE |
warn | PUSH 完但 zone ≠ right_conveyor |
retry(一次) |
TM-006 |
SKILL_FAULT |
error | 透傳上游 SK-xxx |
replan |
TM-007 |
SOFTWARE_FAULT |
critical | 內部 exception / 超時 | pause |
TM-008 |
MODE_BLOCKED |
info | mode=teleop 時收到自主指令 |
none |
7.2 技能級(由 Skill Manager 維護,李定義具體編號)
預留 SK-001 ~ SK-099 範圍。Task Manager 收到後一律轉成 TM-006,並把原碼放進 details.upstream_code。
7.3 策略級(由 Skill Unit 維護,朱定義)
預留 PL-001 ~ PL-099 範圍。經由 Skill Manager 透傳。
8. 模塊間 Python 接口(同進程)
以下是
task_manager/內 6 個類對外暴露的方法簽名,只列公開 API。
8.1 TaskContext
class TaskContext:
task_id: str
task_text: str
state: str # IDLE / RUNNING / PAUSED / FAULT / RESET
queue: list[dict]
faults: list[FaultRecord]
stats: dict
def to_redis(self, collaborator) -> None: ...
@classmethod
def from_redis(cls, collaborator, task_id: str) -> "TaskContext": ...
8.2 TaskQueueManager
class TaskQueueManager:
def __init__(self, max_active: int = 3): ...
def push(self, subtask: dict) -> None: ...
def pop_next(self) -> dict | None: ...
def remove(self, subtask_id: str) -> bool: ...
def reorder(self, new_order: list[str]) -> None: ...
def insert(self, idx: int, subtask: dict) -> None: ...
def snapshot(self) -> dict: ...
8.3 TaskScheduler
class TaskScheduler:
state: SchedState # IDLE / RUNNING / PAUSED / FAULT / RESET
mode: str # teleop / auto / semi
def start_task(self, task_text: str) -> str: ...
def dispatch_next(self) -> str | None: ...
def pause(self) -> None: ...
def resume(self) -> None: ...
def abort(self, reason: str = "") -> None: ...
def reset(self) -> None: ...
def switch_mode(self, mode: str) -> None: ...
def request_replan(self, task_id: str, reason: str) -> None: ...
def retry_current(self, task_id: str) -> None: ...
8.4 TaskMonitor
class TaskMonitor:
def attach(self, task_id: str, package_id: str) -> None: ...
def detach(self, task_id: str) -> None: ...
def start_listeners(self) -> None: ...
# 內部回調:_on_vision / _on_skill
8.5 TaskDiagnostics
class TaskDiagnostics:
def raise_fault(self, task_id: str, evt, code: str = "TM-006") -> None: ...
def clear_fault(self, task_id: str, code: str) -> None: ...
def list_faults(self, task_id: str) -> list[dict]: ...
8.6 任務開關(由尹實作,本模塊調用)
class TaskSwitch:
def startup(self, config_path: str) -> dict:
"""系統初始化 + 參數加載 + 模型熱加載;回傳 {ready: bool, msg: str}"""
def shutdown(self) -> dict:
"""持久化任務上下文 + 生成任務報告;回傳 {report_path: str}"""
def generate_report(self, task_id: str) -> str:
"""生成單一任務的 Markdown 報告路徑"""
9. Mock 工具(測試 / 開發階段)
9.1 視覺事件 Mock(在 GroundingDINO 未就緒前)
tools/mock_vision.py:
import json, time, random, redis
r = redis.Redis()
PKG_ID = "P_mock_001"
state = {"barcode_face": "down", "zone": "left_conveyor"}
def step():
if state["zone"] == "left_conveyor":
state["zone"] = "workspace"
elif state["barcode_face"] == "down":
state["barcode_face"] = random.choice(["down", "side", "up"])
elif state["barcode_face"] == "up":
state["zone"] = "right_conveyor"
while True:
r.publish("vision_events", json.dumps({
"event": "vision_update", "ts": time.time(),
"frame_id": f"img_{int(time.time()*1000)}", "camera": "head",
"objects": [{
"id": PKG_ID, "class": "package_box",
"bbox": [120,80,380,290], "score": 0.93,
"pose_3d": {"position": [0.42,0.10,0.85], "yaw": 0.30},
"size": {"length": 0.22,"width": 0.15,"height": 0.10},
"barcode_face": state["barcode_face"],
"barcode_score": 0.81, "zone": state["zone"],
}],
}))
time.sleep(1.0)
step()
9.2 技能事件 Mock
tools/mock_skill.py:
import json, sys, time, redis
r = redis.Redis()
skill, status = sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else "ok"
r.publish("skill_events", json.dumps({
"event": "skill_update", "ts": time.time(),
"robot_name": "humanoid_01", "task_id": "mock", "subtask_id": "t1",
"skill_name": skill, "phase": "end", "status": status,
"fault_code": None if status == "ok" else "SK-001",
"details": {"duration_ms": 1000},
}))
用法:python tools/mock_skill.py G1_left_grasp ok
9.3 故障注入
直接用 redis-cli:
redis-cli publish skill_events '{"event":"skill_update","ts":0,"robot_name":"humanoid_01","task_id":"mock","subtask_id":"t1","skill_name":"O2_left_rotate_package","phase":"end","status":"fail","fault_code":"SK-005","details":{}}'
10. 版本與兼容性
| 規則 | 說明 |
|---|---|
| 版本號 | 本文件頂部 版本 欄位,跟隨 semver:MAJOR.MINOR。 |
| Major 變更 | 任何欄位刪除 / 重命名 / 列舉值刪除 → bump major,所有隊友必須升級。 |
| Minor 變更 | 新增欄位(必須附 必填=N)、新增列舉值、新增 channel → bump minor,隊友可漸進升級。 |
| 兼容期 | 任何 channel schema 變更,舊欄位至少保留 2 個 sprint。 |
| 通告 | 任何變更發到團隊群並 @所有 stakeholders。 |
附錄 A:欄位字典
跨多個 schema 出現的欄位統一在這裡定義,避免歧義。
| 欄位 | 統一定義 |
|---|---|
task_id |
UUID4 hex(無 -),由 Task Scheduler 在 start_task 時生成 |
subtask_id |
短字串 t1 / t2 / ...,僅在單一 task_id 內唯一 |
package_id |
P_yyyyMMddNNNNN,由視覺端在首次偵測時分配;同一包裹整個生命週期不變 |
robot_name |
與 slaver/config.yaml 的 robot.name 一致 |
zone |
left_conveyor / workspace / right_conveyor / unknown |
barcode_face |
up / down / side / unknown |
fsm_state |
PICK / REORIENT / PUSH / DONE / FAULT |
scheduler_state |
IDLE / RUNNING / PAUSED / FAULT / RESET |
mode |
teleop / auto / semi |
severity |
info / warn / error / critical |
phase |
begin / progress / end |
status |
ok / fail / timeout / cancelled |
附錄 B:變更紀錄
| 版本 | 日期 | 變更 | 提案人 |
|---|---|---|---|
| v1.0 | 2026-05-12 | 初版,定義 5 個 channel + 7 個 Redis key + 11 個 HTTP 端點 + 7 個任務級故障碼 | 蒲 |