契约与安全:OpenAPI/错误策略/幂等/权限-2026.8.24

上层 Agent ↔ 机械臂后端服务 的 HTTP 接口契约(OpenAPI3.1)

python 复制代码
# arm-agent-workflow API 契约 v0.1 ------ 第三周 D1 产物
# OpenAPI 3.1(JSON Schema 2020-12 方言)
openapi: 3.1.0
info:
  title: arm-agent-workflow API
  version: 0.1.0
  description: |
    机械臂上层 Agent 契约 v0.1。
    统一错误响应四字段:code / message / retryable / details;
    每个响应带 X-Request-ID;除 /healthz、/ 与 /static/* 外均需 X-API-Key。
servers:
  - url: http://localhost:5000
security:
  - ApiKeyAuth: []
tags:
  - name: arm
    description: 机械臂动作与状态
  - name: voice
    description: 语音/文本指令
  - name: system
    description: 健康检查
paths:
  /healthz:
    get:
      tags: [system]
      operationId: healthz
      summary: 公开健康检查(无需 API Key)
      security: []
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Healthz"
  /v1/arm/status:
    get:
      tags: [arm]
      operationId: getArmStatus
      summary: 查询机械臂状态
      responses:
        "200":
          description: 当前状态
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArmStatus"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /v1/arm/actions:
    post:
      tags: [arm]
      operationId: armAction
      summary: 下发机械臂动作
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ActionRequest"
            examples:
              gripper_close:
                summary: 关闭夹爪
                value:
                  action: gripper_close
              move_to_pose:
                summary: 移动到 (0.15, 0, 0.20) m
                value:
                  action: move_to_pose
                  x: 0.15
                  y: 0.0
                  z: 0.20
                  speed: 0.3
              estop:
                summary: 急停
                value:
                  action: estop
      responses:
        "200":
          description: 执行结果
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArmActionResult"
              examples:
                ok:
                  summary: 成功
                  value:
                    success: true
                    message: ok
                    action: gripper_close
                busy:
                  summary: 机械臂忙(可重试)
                  value:
                    success: false
                    code: ARM_BUSY
                    message: 机械臂忙,请稍后再试
                    retryable: true
                    details: {}
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/voice:
    post:
      tags: [voice]
      operationId: voiceCommand
      summary: 语音/文本指令 → 场景闭环(快路/抓放/基础动作)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VoiceRequest"
            examples:
              grasp:
                summary: 抓放闭环
                value:
                  text: 把红色方块放到左侧箱
              nod:
                summary: 基础动作
                value:
                  text: 点头
              home:
                summary: 快路回原点
                value:
                  text: 回原点
      responses:
        "200":
          description: 场景执行报告
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VoiceReport"
        "401":
          $ref: "#/components/responses/Unauthorized"
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
  schemas:
    Error:
      type: object
      required: [code, message, retryable, details]
      properties:
        code:
          type: string
          example: INVALID_TASK
        message:
          type: string
          example: 请求体必须是 JSON 对象
        retryable:
          type: boolean
          example: false
        details:
          type: object
    ActionRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [gripper_open, gripper_close, move_to_pose, home, enable, estop]
          example: gripper_close
        x: { type: number, description: 目标 x(米,move_to_pose 用) }
        y: { type: number, description: 目标 y(米) }
        z: { type: number, description: 目标 z(米) }
        speed: { type: number, default: 0.3, minimum: 0.05, maximum: 1.0 }
        on: { type: boolean, description: enable 动作使用 }
    ArmActionResult:
      type: object
      required: [success, message]
      properties:
        success: { type: boolean }
        message: { type: string }
        action: { type: string }
        code: { type: string, description: 失败时的业务码(如 ARM_BUSY / STEP_FAILED) }
        retryable: { type: boolean }
        details: { type: object }
    ArmStatus:
      type: object
      properties:
        state: { type: string, enum: [idle, moving, error, estopped] }
        position:
          type: array
          minItems: 3
          maxItems: 3
          items: { type: number }
        euler:
          type: array
          minItems: 3
          maxItems: 3
          items: { type: number }
        gripper_open: { type: boolean }
        joints:
          type: array
          items: { type: number }
        arm_enabled: { type: boolean }
        motion_status: { type: integer, enum: [0, 1, 2] }
        error_message: { type: string }
        motor_faults:
          type: array
          items: { type: integer }
    VoiceRequest:
      type: object
      required: [text]
      properties:
        text:
          type: string
          minLength: 1
          example: 把红色方块放到左侧箱
    VoiceReport:
      type: object
      properties:
        result: { type: string, example: SUCCESS }
        mode: { type: string, example: joint_sim_demo }
        task: { type: object }
        steps:
          type: array
          items: { type: string }
        message: { type: string }
    Healthz:
      type: object
      properties:
        status: { type: string, example: ok }
        backend: { type: string, example: ros2 }
        arm_state: { type: string, example: idle }
  responses:
    BadRequest:
      description: 请求体非法
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: 缺失或无效 API Key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InternalError:
      description: 系统内部错误(不泄露堆栈)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

整体分层:

  • 外部输入层:语音 / 文本指令(/v1/voice
  • 底层动作层:原始机械臂指令(/v1/arm/*
  • 观测层:状态查询 /v1/arm/status
  • 运维层:健康检查 /healthz

整体数据流:

用户自然语言文本 → POST /v1/voice → Agent业务层解析任务 → 内部调用 /v1/arm/actions下发底层动作 → 驱动机械臂硬件 → 返回执行报告VoiceReport

1、安全机制

复制代码
security:
  - ApiKeyAuth: []
  • 除了 /healthz所有接口请求头必须携带 X‑API‑Key,401 鉴权失败走统一 Error 结构。
  • 每个 HTTP 响应头携带 X‑Request‑ID,用于链路追踪、排查日志。
  • 统一错误体固定 4 字段:code/message/retryable/details,所有 400/401/500 复用这套 schema。

2、接口逐个拆解链路

/healthz GET 健康探针

用途:监控、k8s 存活探针、客户端预检测服务是否活着 不需要 API‑Key。 返回:

复制代码
{"status":"ok","backend":"ros2","arm_state":"idle"}

arm_state 直接透传机械臂当前状态,客户端可以在调用动作前先探活。

/v1/arm/status GET【观测接口】

只读,拿机械臂全量实时状态 返回 ArmStatus 对象:

  • state:状态枚举 idle/moving/error/estopped 空闲 / 运动中 / 报错 / 急停
  • position:笛卡尔坐标 x,y,z
  • euler:姿态欧拉角
  • gripper_open:夹爪开闭布尔
  • joints:各关节角度数组
  • arm_enabled:驱动器是否使能
  • motor_faults:电机故障码

使用场景:

  1. Agent 在下发动作前先拉取状态:如果state=moving就不要下发新动作,返回ARM_BUSY(retryable=true)
  2. UI 前端实时展示机械臂位姿面板

/v1/arm/actions POST【底层原始动作接口】

裸动作接口,不做业务逻辑,只下发硬件原语 ActionRequest支持动作枚举: gripper_open/gripper_close/move_to_pose/home/enable/estop

action 参数说明
move_to_pose 必须附带 x,y,z;speed 速度 0.05‑1.0 m/s
enable 附带on:true/false使能 / 失能电机
estop 急停,立刻截断运动

返回 ArmActionResult

复制代码
{
  "success":true,
  "message":"ok",
  "action":"gripper_close",
  "code":"ARM_BUSY",
  "retryable":true,
  "details":{}
}

重点:retryable 业务标记:

  • retryable=true:比如机械臂忙,客户端可以隔一段时间重试
  • retryable=false:参数非法、硬件故障,不要重试
    调用链路: Agent / 客户端 → POST /v1/arm/actions → service 层转发 ROS2 驱动 → 机械臂执行 → 返回结果。

/v1/voice POST【高层业务闭环接口,核心入口】

复制代码
{"text":"把红色方块放到左侧箱"}

这是面向自然语言的高层接口,不直接控制硬件。 内部处理链路:

  1. 接收自然语言文本 text

  2. 内部 Agent 解析语义,拆解成一连串底层动作序列 plaintext

    复制代码
    1. move_to_pose靠近方块
    2. gripper_close抓取
    3. move_to_pose移动到箱子
    4. gripper_open放下
  3. 内部循环调用本服务的 /v1/arm/actions 接口执行每一步原语

  4. 收集每一步执行结果,组装成 VoiceReport 返回给调用方

VoiceReport返回结构:

复制代码
{
  "result":"SUCCESS",
  "mode":"joint_sim_demo",
  "task":{},
  "steps":["移动到目标","闭合夹爪","移动到投放点","打开夹爪"],
  "message":"任务完成"
}
  • steps:把拆解的每一步暴露出来,方便前端展示进度、日志调试
  • result: SUCCESS / FAILED

⚠️ 边界区分(非常关键)

  1. /v1/arm/actions原语层,你告诉我做什么动作,我直接发给硬件;不理解语义。
  2. /v1/voice任务层,你说人话,服务内部自己拆成原语序列调用 arm 接口。

完整端到端业务时序(举例:指令 "把红色方块放到左侧箱")

复制代码
客户端
   ↓ POST /v1/voice {"text":"把红色方块放到左侧箱"}
arm‑agent‑workflow服务
    ├─语义解析:拆解为一组动作序列
    ├─内部调用GET /v1/arm/status,确认机械臂state=idle
    ├─内部 POST /v1/arm/actions move_to_pose(靠近方块)
    ├─内部 POST /v1/arm/actions gripper_close
    ├─内部 POST /v1/arm/actions move_to_pose(箱子位置)
    ├─内部 POST /v1/arm/actions gripper_open
    └─收集全部步骤,组装 VoiceReport 返回
客户端 ← VoiceReport(steps数组看到每一步发生了什么)

错误流转逻辑

  1. 鉴权失败 → 401 返回统一 Error schema retryable=false
  2. 参数格式错误 →400 BadRequest
  3. 机械臂正在运动,调用/v1/arm/actions →返回success:false code=ARM_BUSY retryable=true,上层可以重试
  4. 硬件故障 → retryable=false,上层不能重试,需要人工介入。

架构分层总结(对应你项目 arm‑agent‑workflow)

层级 API 职责
任务层 /v1/voice 自然语言解析、任务编排、多步骤串联
动作原语层 /v1/arm/actions 单条硬件动作下发
状态观测层 /v1/arm/status 读取位姿、故障、夹爪状态
探针层 /healthz 服务存活检测

demo_contract_validate.py

①openapi-spec-validator 整体校验 ②每个示例载荷用 jsonschema(2020-12) 校验

python 复制代码
#!/usr/bin/env python3
"""demo_contract_validate.py ------ 校验 contracts/openapi.yaml(第三周 D1)。

1) OpenAPI 3.1 规范整体校验(openapi-spec-validator)
2) 每个示例载荷(requestBody/responses examples)用 jsonschema(2020-12) 校验
用法:python3 scripts/demo_contract_validate.py
"""

from __future__ import annotations

import pathlib
import sys

import yaml
from jsonschema import Draft202012Validator
from openapi_spec_validator import validate_spec

ROOT = pathlib.Path(__file__).resolve().parent.parent
SPEC_PATH = ROOT / "contracts" / "openapi.yaml"


def _resolve(schema: dict, schemas: dict) -> dict:
    """扁平 $ref 解析:#/components/schemas/<Name>"""
    if isinstance(schema, dict) and "$ref" in schema:
        ref = schema["$ref"]
        if ref.startswith("#/components/schemas/"):
            name = ref.split("/")[-1]
            if name in schemas:
                return schemas[name]
            raise KeyError(f"未找到 schema: {ref}")
    return schema


def _collect_examples(spec: dict):
    """收集 (位置, schema, 示例载荷)。"""
    schemas = spec.get("components", {}).get("schemas", {})
    examples: list[tuple[str, dict, dict]] = []

    def walk_media(where: str, media: dict):
        schema = _resolve(media.get("schema", {}), schemas)
        for name, ex in (media.get("examples") or {}).items():
            if isinstance(ex, dict) and "value" in ex:
                examples.append((f"{where}.{name}", schema, ex["value"]))
        if "example" in media:
            examples.append((f"{where}.example", schema, media["example"]))

    for path, methods in spec.get("paths", {}).items():
        if not isinstance(methods, dict):
            continue
        for method, op in methods.items():
            if not isinstance(op, dict):
                continue
            rb = op.get("requestBody")
            if isinstance(rb, dict):
                for mime, media in (rb.get("content") or {}).items():
                    walk_media(f"{method.upper()} {path} request", media)
            for status, resp in (op.get("responses") or {}).items():
                if not isinstance(resp, dict):
                    continue
                for mime, media in (resp.get("content") or {}).items():
                    walk_media(f"{method.upper()} {path} {status}", media)
    return examples


def main() -> int:
    spec = yaml.safe_load(SPEC_PATH.read_text(encoding="utf-8"))
    validate_spec(spec)
    print(f"✅ 规范校验通过(openapi={spec.get('openapi')})")

    examples = _collect_examples(spec)
    fails = 0
    for where, schema, value in examples:
        errors = sorted(Draft202012Validator(schema).iter_errors(value), key=lambda e: list(e.path))
        if errors:
            fails += 1
            print(f"❌ {where}: {errors[0].message}(path={list(errors[0].path)})")
        else:
            print(f"✅ {where}: 通过")

    print(f"\n示例载荷 {len(examples)} 个,失败 {fails} 个")
    return 1 if fails else 0


if __name__ == "__main__":
    sys.exit(main())

作用 :专门校验你写的 openapi.yaml API 契约文件,干两件大事

  1. 检查 YAML 本身是不是合法 OpenAPI3.1 文档(语法、关键字不能写错)
  2. 把 YAML 里面写的所有examples示例 JSON,拿对应的 Schema 模型校验:示例数据是否符合接口定义

业务背景:你在 openapi 里面写了很多 request/response 的例子,人眼看容易错;这个脚本自动化跑一遍,防止契约文档和实际样例对不上,后端写代码就不会被文档坑。

文件位置:scripts/demo_contract_validate.py,读取 contracts/openapi.yaml

整体执行流程(从上到下)

plaintext

复制代码
main()入口
    ↓
1.读取yaml文件 → 解析成python字典spec
    ↓
2.openapi_spec_validator.validate_spec(spec) 【第一层校验:OpenAPI文档规范】
    ↓
3._collect_examples(spec) 遍历整个openapi文档,收集全部示例
    ↓
4.循环每一组 (位置, schema模板, 示例数据)
    ↓
5.Draft202012Validator 用Schema校验示例载荷
    ↓
6.打印成功/失败,统计失败数量,返回退出码

逐个函数通俗解释

1. 常量部分

python

运行

复制代码
ROOT = pathlib.Path(__file__).resolve().parent.parent
SPEC_PATH = ROOT / "contracts" / "openapi.yaml"

拿到脚本上级目录,定位契约文件,兼容 WSL/linux,不用写死绝对路径

2. _resolve(schema, schemas) 解析$ref引用

OpenAPI 大量使用 $ref: "#/components/schemas/ArmStatus" 复用模型。

  • 输入:遇到带$ref的字典
  • 逻辑:把引用地址截取,从components/schemas字典取出真实 schema 返回
  • 通俗讲:解引用,把别名替换成真实模型定义

局限:只处理顶层#/components/schemas,不处理嵌套深层 ref,够用,简单实现,不是完整 OpenAPI 解析器。
如果找不到引用的 schema →抛 KeyError,直接脚本报错。

3. _collect_examples(spec) 核心收集函数

输出列表:[(位置字符串, schema模型, 示例数据)]

plaintext

复制代码
examples: list[tuple[str, dict, dict]] = []
  • where:字符串标记这个示例来自哪里,例如POST /v1/arm/actions request.gripper_close,报错的时候方便定位 yaml 哪一块出问题
  • schema:对应的 json schema 模型(ActionRequest / ArmActionResult 等)
  • value:example 里面写的样例 json

内部嵌套函数 walk_media(): 专门处理content -> application/json下面的内容:

  1. 调用_resolve解开$ref拿到真实 schema
  2. 读取examples:{}(多个命名示例,比如 gripper_close、move_to_pose)取里面value
  3. 同时兼容老写法example:(单个示例,不带大 examples)

然后双重循环遍历整个 openapi 的 paths:

  1. 遍历每一个接口路径 /healthz /v1/arm/actions
  2. 遍历方法 get/post
  3. 处理请求体 requestBody,收集请求的 examples
  4. 处理每个响应 200/400/401,收集返回值 examples

跑完之后,我们手上就拿到契约文档全部写出来的样例,不管是入参还是出参。

4. main () 主函数

python

运行

复制代码
spec = yaml.safe_load(SPEC_PATH.read_text(encoding="utf-8"))
validate_spec(spec)
  1. yaml.safe_load:把 yaml 文本转 python 字典;yaml 格式写错这里直接炸。
  2. validate_spec(spec):第三方库校验OpenAPI 规范
    • 检查openapi:3.1.0版本关键字对不对
    • paths、components 语法是否符合 OpenAPI 标准
    • 如果 yaml 写错字段,这里直接抛异常终止脚本。 ✅打印:✅ 规范校验通过

python

运行

复制代码
examples = _collect_examples(spec)

拿到全部样例列表。

循环每一条样例:

python

运行

复制代码
errors = sorted(Draft202012Validator(schema).iter_errors(value), key=lambda e: list(e.path))

Draft202012Validator 就是 JSON‑Schema 2020‑12 校验器,和你 openapi 版本匹配。 iter_errors(value):拿 schema 规则,校验示例数据,产出全部错误,不会直接抛异常。

  • 没有错误:打印✅ xxx:通过
  • 有错误:打印,打印错误信息 + json 路径 path,告诉你样例哪里违反 schema。

最后统计:一共多少样例,失败几个。

返回退出码:

  • 0:全部 ok;
  • 1:有失败; sys.exit(main()) shell 可以拿到返回码,可以接入 CI 流水线,只要返回 1 CI 直接失败。

机械臂后端的契约ROS2

srv/(服务请求/响应):

MoveToPose.srv --- 末端位姿 x/y/z/roll/pitch/yaw + cartesian_path

机械臂笛卡尔空间运动请求,底层驱动层(ROS2 / 硬件 SDK)的入参出参

python 复制代码
# Request: end-effector target pose
float64 x
float64 y
float64 z
float64 roll
float64 pitch
float64 yaw
# Use Cartesian path planning (linear motion)
bool cartesian_path false
# Max velocity scaling factor (0.0~1.0)
float64 velocity_scaling 1.0
# Max acceleration scaling factor (0.0~1.0)
float64 acceleration_scaling 1.0
---
# Response
bool success
string message

字段释义

  1. x, y, z(float64) 末端执行器目标笛卡尔坐标,单位米。就是机械臂工具中心点要到达的空间位置。

  2. roll, pitch, yaw(float64) 欧拉角,单位弧度,代表末端姿态:工具怎么 "拧、俯仰、偏转"。

xyz 决定在哪;rpy 决定工具是什么朝向。
👉 对应你 OpenAPI ArmStatus 里面的 position:[x,y,z]euler:[roll,pitch,yaw]

  1. bool cartesian_path = false
  • true笛卡尔直线规划,末端在空间严格走直线;受奇异点、关节限位约束,有可能规划失败。
  • false:关节空间规划,只关心起点终点,中间路径由关节插值,路径不一定是直线,更容易成功。

关键区别: cartesian_path=true:末端轨迹是直线; cartesian_path=false:关节平滑转动,空间轨迹是曲线。

  1. velocity_scaling =1.0 速度缩放系数,范围 0.0 ~1.0 1.0 = 允许硬件最大速度;0.3 就是最大速度的 30%。

对应上层 API 的 speed 参数,上层 speed 最终映射到这个字段。

  1. acceleration_scaling =1.0 加速度缩放系数,0.0~1.0 控制启动、刹车快慢;1.0 硬件最大加速度,数值越小启动停止越柔和。

MoveToJoint.srv --- 6 个关节角

python 复制代码
# Request: 6 joint angles in radians
float64[6] joint_angles
# Max velocity scaling factor (0.0~1.0)
float64 velocity_scaling 1.0
# Max acceleration scaling factor (0.0~1.0)
float64 acceleration_scaling 1.0
---
# Response
bool success
string message

这是底层驱动:关节角度直接运动接口,和上面笛卡尔位姿接口是两套独立下发入口。

笛卡尔:给末端 xyz+rpy;本接口:直接给 6 个关节角度(弧度)。

复制代码
# Request: 6 joint angles in radians
float64[6] joint_angles
# Max velocity scaling factor (0.0~1.0)
float64 velocity_scaling 1.0
# Max acceleration scaling factor (0.0~1.0)
float64 acceleration_scaling 1.0

字段说明

  1. joint_angles: float646 6 轴机械臂,[j0,j1,j2,j3,j4,j5],单位弧度。 直接指定每一个关节要转到什么角度,不需要运动学逆解。
  • 优点:不会奇异点报错,不做逆解计算;
  • 缺点:你要自己算好每组关节角,不知道末端会走到哪个空间位置。

典型用途:回原点 home、摆固定姿态、演示姿态序列。

  1. velocity_scaling 1.0 速度缩放系数 0.0‑1.0,控制所有关节最大转动速度。 0.3 → 所有关节只跑硬件最大速度的 30%。

  2. acceleration_scaling 1.0 加速度缩放系数,控制关节启动、刹车的剧烈程度。

对比上一个笛卡尔接口:

  • 笛卡尔:输入末端位姿,内部做逆解算出关节角;可能奇异点、工作空间越界规划失败。
  • 关节角度接口:直接给关节目标,不做逆解;但要你保证角度在关节限位内。

GripperControl.srv --- open / close / half_open

Request 请求字段
  • action 字符串枚举,3 种状态
    1. open:完全打开夹爪
    2. close:闭合夹爪(夹紧物体)
    3. half_open:半开,中间位置,用于预定位

不同硬件实现:

  • 简单二值夹爪:half_open 可能不支持,驱动直接返回失败。
  • 自适应 / 位置可控夹爪:可以走到半开位置。
Response 返回
  • success: true:夹爪指令接收下发完成,不等于物理已经到位
  • success: false:执行被拒绝。 典型原因:急停状态、驱动器故障、硬件不支持half_open
  • message:描述文本,例如 gripper hardware fault / half_open not supported

⚠️重点: 下发夹爪指令返回成功,只是命令发给硬件;上层需要读取 /v1/arm/statusgr

GripperSrv.srv --- 夹爪详细控制(角度/力矩/模式/清零)

python 复制代码
# Detailed gripper control
float64 gripper_angle         # Target gripper position (meters, 0.0~0.04)
float64 gripper_effort        # Max effort/torque
uint8 gripper_code            # 0=position mode, 1=effort mode
bool set_zero                 # If true, reset gripper zero position
---
int64 code                    # 0=success, non-zero=error code
bool status
Request 请求字段
  1. gripper_angle float64,单位米,范围 0.0 ~ 0.04m

夹爪开口宽度。

  • 0.0:完全闭合;
  • 0.04:最大张开 4 厘米。

注意单位是米,不是角度!字段命名叫gripper_angle容易迷惑,实际是开口行程距离

  1. gripper_effort float64 最大夹持力 / 力矩上限。
  • 位置模式:限制最大输出力,碰到物体不会硬顶;
  • 力控模式:这就是目标夹持力。
  1. gripper_code uint8 模式选择
  • 0 --- position mode 位置模式 :夹爪走到 gripper_angle 指定开口;到达位置就停止;gripper_effort作为保护最大力。
  • 1 --- effort mode 力控模式 :维持 gripper_effort 恒定夹持力;夹爪会自适应物体厚度,不会卡死。适合抓取易碎物品。
  1. set_zero bool true = 重置夹爪机械零点。 上电夹爪机械偏移、更换夹爪爪片时使用;日常抓取任务填 false

⚠️校准零点动作不要频繁调用。

Response 返回
  • code int64:业务错误码,0成功;非 0 代表硬件错误。
  • status bool:指令下发状态,true= 命令已接收下发。

同样:status=truecode=0 只是下发成功,不等于夹爪已经到达目标位置。上层依旧要读状态接口的夹爪实际状态。

GoZero.srv --- 回零

python 复制代码
# Move arm to home (zero) position
bool use_mit_mode
---
int64 code
bool status
Request 请求
  • use_mit_mode bool 是否使用 MIT 模式回零。
  • false:普通回零,直接执行预设的原点关节角数组,关节运动到机械零点;速度受全局速度缩放限制。
  • true:MIT 模式回零(柔顺 / 零重力模式回原点),电机进入力矩柔顺模式,缓慢归位;遇到碰撞会柔顺退让,适合开机初始化。

硬件差异:部分机械臂固件不支持 use_mit_mode=true,此时传入 true 会返回错误码。

Response 返回
  • code int640 = 请求接收成功;非 0 代表错误码
  • status booltrue = 回零指令已经下发驱动器

⚠️重点:code=0 && status=true 只代表指令下发成功,机械臂还在运动中 。 上层业务必须轮询状态接口 /v1/arm/status,等待 state:idle,才算 home 动作真正完成。

失败常见原因:

  1. 电机未使能;
  2. 处于急停 estopped 状态;
  3. 固件不支持 mit 柔顺回零模式。
Enable.srv --- 使能
python 复制代码
# Enable/disable the arm
bool enable_request
---
bool enable_response
Request 请求
  • enable_request: bool
    • true使能电机,电机上电抱闸释放,机械臂允许接收运动指令;开机之后必须先 enable,才能做 move、home、夹爪动作。
    • false失能电机,电机抱闸锁住,手可以手动掰动机械臂;此时所有运动指令全部拒绝执行。

业务场景:

  1. 系统启动流程:上电 → enable_request=true 使能机械臂
  2. 调试模式:enable_request=false,人手拖动机械臂示教
  3. 发生故障时,可失能保护
Response 返回
  • enable_response: bool
    • true:使能 / 失能执行成功
    • false:执行失败,无法切换使能状态

⚠️注意: enable_response=true 代表状态切换指令执行完毕 ,不是异步运动,不需要轮询等待。 使能完成之后,上层调用 /v1/arm/status,读取 arm_enabled 字段确认硬件真实使能状态。

失败常见原因:

  1. 硬件报错、电机故障,无法使能;
  2. 处于急停 estopped 状态,不允许使能。

msg/(话题消息):

ArmStatus.msg --- arm_enabled / motion_status / motor_faults / gripper_position

python 复制代码
# ArmStatus.msg - Arm status feedback (adapted from Piper's PiperStatusMsg)
std_msgs/Header header

# Overall arm state
bool arm_enabled              # Whether the arm is enabled and accepting commands
uint8 motion_status           # 0=idle, 1=moving, 2=error
string error_message          # Human-readable error description

# Per-motor mode and fault
uint8[6] motor_modes          # Control mode per motor
uint8[6] motor_faults         # Fault code per motor (0 = no fault)

# Joint limit flags: true means the joint has hit its position limit
bool[6] joint_at_limit

# Gripper state
float64 gripper_position      # Gripper position
uint8 gripper_fault           # Gripper motor fault code
1.std_msgs/Header header ROS 标准消息头,携带时间戳、frame_id。

作用:标记这份状态数据是哪个时刻采集的,做时间同步;转 HTTP 输出时可以把时间戳放到响应头X‑Timestamp

2.bool arm_enabled 电机总使能状态。
  • true:电机使能,可以接收运动指令;
  • false:失能,不能执行运动,可手动掰动机械臂。 👉 对应 OpenAPI ArmStatus.arm_enabled
3.uint8 motion_status 整机运动状态
  • 0 → idle 空闲,可以下发新动作
  • 1 → moving 运动中,此时不要下发新动作,返回 ARM_BUSY (retryable=true)
  • 2 → error 整机故障,需要人工处理 👉 映射 OpenAPI ArmStatus.state枚举 idle / moving / error
4.string error_message

整机可读错误文本,例如motor 2 over current。 👉 对应 OpenAPI ArmStatus.error_message

5.uint8[6] motor_modes 6 个电机各自控制模式

数组长度固定 6,每个电机独立模式:力矩模式、位置模式等。上层业务一般只做日志展示,业务逻辑很少使用。

6.uint8[6] motor_faults 每个电机故障码

0 = 无故障;非 0 代表对应电机报错。 👉 直接映射 OpenAPI ArmStatus.motor_faults

7.bool[6] joint_at_limit

6 个关节限位标记。true代表该关节撞到硬件限位。

业务用途:状态上报给前端;如果下发运动指令后出现该标记,可以生成业务错误码JOINT_HIT_LIMIT

8.float64 gripper_position

夹爪实际开口距离(单位米,0.0~0.04),硬件真实反馈值,不是下发的目标值。

上层据此计算布尔值 gripper_open:例如大于 0.02 判定为打开。

9.uint8 gripper_fault 夹爪电机故障码,0 代表无故障。

💡补充:这条 ROS 消息没有携带笛卡尔 xyz/euler、joints 关节角度 。 真实 Piper 机器人:这个 ArmStatus 只反馈故障、使能、运动状态; 末端位姿、关节角度来自另外一个话题 joint_states。 所以你的 OpenAPI ArmStatus 结构是合并两份 ROS 话题的数据:

  • ArmStatus.msg:arm_enabled、motion_status、error_message、motor_faults、joint_at_limit、夹爪状态
  • JointState 话题:joints数组、笛卡尔positioneuler姿态

EndPoseEuler.msg --- 末端位姿(欧拉角)

python 复制代码
# EndPoseEuler.msg - End-effector pose feedback in Euler angles
std_msgs/Header header
float64 x
float64 y
float64 z
float64 roll
float64 pitch
float64 yaw
  1. std_msgs/Header header ROS 标准头,包含时间戳stampframe_id(基座坐标系),用来标记这组位姿的采集时刻。

  2. x / y / z 末端 TCP 空间坐标,单位米,相对于机械臂基座坐标系。

  3. roll / pitch / yaw 末端姿态欧拉角,单位弧度

  • roll:绕 X 轴翻滚
  • pitch:绕 Y 轴俯仰
  • yaw:绕 Z 轴偏航

这是硬件实时反馈出来的真实位姿,不是下发的目标位姿。

ArmPose.msg / PosCmd.msg --- 位姿 + 夹爪复合指令

python 复制代码
# PosCmd.msg - Combined end-effector pose + gripper command
float64 x
float64 y
float64 z
float64 roll
float64 pitch
float64 yaw
float64 gripper              # Gripper value (0.0=close, 1.0=fully open)
uint8 mode1                  # 0=joint-space planning, 1=cartesian path
uint8 mode2                  # Reserved for future use
  1. x, y, z 末端目标笛卡尔坐标,单位米。

  2. roll, pitch, yaw 目标末端欧拉角,单位弧度。

  3. gripper float64 夹爪归一化目标值:

  • 0.0 = 完全闭合
  • 1.0 = 完全张开

⚠️注意:这里是归一化 0‑1;之前精细夹爪接口是物理米0.0~0.04,两者单位不一样,需要做换算。 gripper_val * 0.04 → 转换成物理开口米数。

  1. mode1 uint8 规划模式
  • 0:关节空间规划(不保证末端直线)
  • 1:笛卡尔直线规划,末端走空间直线

等价于之前笛卡尔请求的 cartesian_path 布尔字段。

  1. mode2 uint8 预留字段,现在填 0 即可,留给后续扩展。
相关推荐
QYRdata1 小时前
28.7%年复合增速锚定AI模型安全赛道,2026-2032年行业步入高速扩容新阶段
人工智能·安全
聚铭网络1 小时前
【一周安全资讯】《网络数据安全风险评估办法》正式实施;隐私加密通讯平台Threema遭DDoS攻击,服务大面积瘫痪
网络·安全·ddos
大黄说说2 小时前
类型安全时代:PHP 8+ 的联合类型、交集类型与泛型(模板)最佳实践
开发语言·安全·php
网安蟹佬霸2 小时前
Zero Trust零信任架构实战:从架构设计到落地部署
安全·网络安全·ci/cd·架构·自动化·网安
PHP实战开发录2 小时前
AI接口结构化输出解析异常排查记录
数据库·安全·ai·php·开发
好果不榨汁2 小时前
网络安全攻防实战:基于 Ubuntu 的攻击机与防守机搭建与演练
安全·web安全·ubuntu
F&C嘉准传感器3 小时前
超细聚焦光纤传感模组:微米级极小光斑,精密微型元器件组装定位零偏差
人工智能·安全·目标检测·自动化·产品运营
海云安4 小时前
AI安全体系化治理:管理制度、技术管控与运营评估如何闭环
人工智能·安全
guwentian4 小时前
一文搞懂 AI Agent 安全护栏:用 Python 手写工具网关,把越权挡在执行层
人工智能·python·安全