Skip to content
控制权与租约

控制权与租约#

控制权是允许一个客户端提交普通运动命令的限时独占租约。客户端注册、状态可读和拥有控制租约是 三个不同条件:只读 getter 不要求租约,acquire_control() 成功也不会自动切换控制模式或移动机械臂。 直接核对 gRPC/DDS 服务时,参见控制权与状态 RPC。

协调后再申请控制权

获取、抢占或交还控制权会影响同一机械臂上的其他控制客户端。先确认现场安全、急停可用,并与 当前任务协调。抢占不会自动停止已经被服务接受的轨迹,也不会替新客户端对齐第一个运动目标。

客户端身份属性#

下面四项是只读属性,不是方法;访问时不要加括号。

client.get_client_id          # str
client.get_source_name        # str
client.get_client_instance_id # str
client.get_client_session_id  # str | None
属性 含义 生命周期
get_client_id 规范化后的 client_name,在当前服务注册域内标识具体客户端 客户端对象创建时确定
get_source_name 服务端确认的控制来源类别;用于查找配置优先级 客户端注册时确定
get_client_instance_id 标识当前进程实例的 UUID 客户端对象创建时确定
get_client_session_id 服务端签发的不透明注册 session 注册协议可用且注册有效时为字符串;legacy 模式或注册心跳失效后为 None

四个属性在 gRPC 和 DDS 后端均可用。构造时 SDK 自动注册并每 3 秒发送一次注册心跳;当前服务端基线 的注册有效期为 10 秒。租约丢失后,注册心跳可以继续存在。反过来,注册 session 失效会同时清除 SDK 本地租约。

client_session_id 用于同一服务内的多客户端协作,不是用户登录凭据、TLS 身份或权限令牌。

注册与租约的区别#

状态 get_client_session_id 可读取状态 可发送普通运动命令
已连接、状态首帧未到且尚未申请租约 字符串或 legacy 下的 None getter 可能返回 None
已注册、未持有租约 字符串
已注册、持有有效租约 字符串 满足对应控制模式等其他前置后才可以
租约丢失、注册仍有效 字符串 否;旧 lease_id 会被拒绝
注册心跳失效 None 只读状态仍可能可读 否;需要关闭并重新创建客户端

SDK 没有公开“当前是否仍持有租约”的查询属性。acquire_control()True 只表示当次获取成功; 后续续期失败或被其他客户端抢占后,旧结果不再有效。运动调用失败时应把租约丢失作为排查项。

获取、续期、丢失和释放#

客户端获取、续期、丢失或释放控制租约的时序 客户端获取、续期、丢失或释放控制租约的时序

SDK 在获取成功后自动续期。可重试的短暂传输错误会在当前过期时间之前退避重试;不可重试错误、 注册心跳失效或租约已经过期时,SDK 清除本地租约并停止与旧租约绑定的 Servo 数据流。显式 Release 和租约自然过期都让资源进入未占用状态,不会自动恢复曾被抢占的客户端。

acquire_control()#

client.acquire_control(
    lease_ms=15000,
    renew_period_s=5.0,
    *,
    preempt=False,
) -> bool

gRPC 和 DDS 后端均支持该方法。

参数 类型 默认值 规则
lease_ms int 15000 请求租约时长,单位毫秒。当前服务端把请求限制在 500..30000 ms;SDK 只提前拒绝大于 3600000 ms 或短于续期间隔的值
renew_period_s float 5.0 自动续期间隔,单位秒。应大于 0,且不大于 lease_ms / 1000;当前 SDK 未单独拒绝非正值,不要传入此类值
preempt bool False 是否显式请求抢占当前 owner。只有配置为更高优先级且不在 non-preemptive 列表中的来源才可能成功

建议把 lease_ms 保持在服务端公开范围内,并让续期间隔明显短于租约时长。不要利用服务端夹紧来 推断实际过期时间。

返回 True 表示服务端签发租约,SDK 已保存 lease_id 并启动自动续期。返回 False 可能表示参数 组合无效、资源已被占用、抢占不允许或 RPC 最终失败;详细原因写入 SDK 日志。重复获取当前客户端 已经持有的资源时,服务端返回当前租约,不创建第二个 owner。

旧服务进入 legacy 模式后仍可使用 preempt=False 获取租约;传入 preempt=True 会抛出 UnsupportedOperationError,不会改为普通的非抢占请求。

默认 gRPC 控制 RPC 的单次 deadline 为 1 秒;DDS 使用 default_timeout_ms(默认 3000 毫秒)。 瞬时错误还会按 rpc_retry_count 重试,默认一次,因此调用总等待可能超过单次 deadline。

并发和抢占#

  • preempt=False 时,已有其他 owner 会导致获取失败。
  • preempt=True 仍要求请求来源严格高于当前 owner;同级、低优先级和 non-preemptive 来源会失败。
  • gRPC 和 DDS 客户端共享同一机械臂的控制权,不会因传输不同而各自拥有一个 owner。
  • 单臂以及双臂的 left/right 资源分别管理;DDS side 和目标服务必须对应。
  • 当前核对的产品配置只有 default,并将它标为 non-preemptive;在该配置下不能使用抢占。

来源顺序由目标产品配置决定。不要根据名称 userteleoperationmodel 猜测优先级。

自动续期与租约丢失#

获取成功后,续期任务立即开始,之后按 renew_period_s 运行。gRPC Renew 的单次 deadline 为 0.5 秒; DDS 使用 default_timeout_ms。可重试错误在租约仍未过期时采用短退避再次尝试。

以下情况会清除 SDK 本地租约:

  • Renew 返回不可重试错误,例如 owner 或 lease_id 已失效;
  • 可重试错误持续到服务端签发的过期时间;
  • 注册心跳失效;
  • 当前客户端被更高优先级来源成功抢占;
  • 本地调用 release_control()、成功 handover 或 close()

租约丢失不会改变客户端注册,因此被抢占方仍可能成为显式交还的候选。应用自身还要停止产生新目标 并清理排队任务;SDK 只能停止与旧租约绑定的命令流,无法撤销已经被服务接受的轨迹。

release_control()#

client.release_control() -> None

该方法停止续期,并在本地持有租约时尝试发送 Release。没有本地租约时直接返回,可以重复调用。 无论 RPC 成功还是失败,本地 lease_id 都会被清除,相关 Servo 流也会停止。

返回值不表示服务端确认成功。网络中断时,服务端 owner 可能继续存在到租约过期;其他客户端在此期间 仍可能获取失败。Release 成功或租约过期都会清空被抢占候选,不会自动把控制权还给上一个客户端。

handover_to_previous()#

client.handover_to_previous() -> bool

handover 只适用于注册控制权协议,gRPC 和具备 V2 消息/服务的 DDS 后端均支持。legacy 模式调用会 抛出 UnsupportedOperationError

当前 owner 调用后,服务端从后进先出候选中选择最近一个注册仍有效、未超过 suspended TTL 的被抢占 客户端,并为它签发全新的租约。成功返回 True,当前调用方清除本地租约;没有本地租约时返回 False。RPC 失败或没有有效候选时返回 False,当前调用方保留原租约并重启续期。

handover 失败不会退化为 Release。成功后,被恢复客户端应再次调用 acquire_control(),取得服务端 已经为其签发的新租约并恢复自动续期;旧租约不会恢复。

只申请控制权、不发送运动#

下面的结构用于验证租约流程,不切换模式,也不发送运动命令。执行前仍需和同一机械臂的其他客户端 协调,因为获取控制权会阻止它们取得租约。

from arm_p7_sdk import AirbotClient

with AirbotClient(
    host="P7_IP_ADDRESS",
    port=50071,
    client_name="diagnostic-console-01",
) as client:
    if not client.acquire_control(lease_ms=15000, renew_period_s=5.0):
        raise RuntimeError("control lease was not acquired; inspect SDK logs")

    try:
        print("registered session:", client.get_client_session_id)
        print("service:", client.get_service_state())
    finally:
        client.release_control()

退出 context 仍会调用 close(),所以即使 finally 中的释放遇到异常,本地资源也会关闭。

常见失败#

现象 可能原因 处理
构造时 ClientNameConflictError 另一实例占用了同一 client_name 改用唯一名称;确认旧进程已退出或等待其注册过期
acquire_control() 返回 False owner 已存在、来源优先级不足、参数组合无效或传输失败 查看 SDK/服务日志;确认来源配置和当前 owner,不要盲目循环抢占
获取成功后运动调用失败 租约随后丢失,或控制模式/状态不满足 停止下发目标,检查日志和 ServiceState;重新协调后再获取
handover_to_previous() 返回 False 没有有效候选、当前客户端不是 owner 或 RPC 失败 当前 owner 通常仍保留租约;决定重试 handover 还是显式 Release
进程崩溃后其他客户端暂时无法获取 没有机会 Release 等当前租约过期;不要用无界重试掩盖持续的服务故障

相关页面#

  • SDK 最小可执行用例:单独运行获取、释放和 handover。
  • 常见故障排查:获取失败、抢占和 Release 后反馈仍变化时的处理;
  • 客户端生命周期:身份构造参数和 close() 的清理顺序。
  • 状态读取参考:不申请控制权的诊断方式。
  • 命令生命周期:最终 fencing 校验、接受、执行和完成。
  • 生命周期与状态转移:owner 变化与已接受任务的关系。
  • 术语表:注册 session、租约和 owner 的简要定义。