控制权与租约#
控制权是允许一个客户端提交普通运动命令的限时独占租约。客户端注册、状态可读和拥有控制租约是
三个不同条件:只读 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()#
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;在该配置下不能使用抢占。
来源顺序由目标产品配置决定。不要根据名称 user、teleoperation 或 model 猜测优先级。
自动续期与租约丢失#
获取成功后,续期任务立即开始,之后按 renew_period_s 运行。gRPC Renew 的单次 deadline 为 0.5 秒;
DDS 使用 default_timeout_ms。可重试错误在租约仍未过期时采用短退避再次尝试。
以下情况会清除 SDK 本地租约:
- Renew 返回不可重试错误,例如 owner 或
lease_id已失效; - 可重试错误持续到服务端签发的过期时间;
- 注册心跳失效;
- 当前客户端被更高优先级来源成功抢占;
- 本地调用
release_control()、成功 handover 或close()。
租约丢失不会改变客户端注册,因此被抢占方仍可能成为显式交还的候选。应用自身还要停止产生新目标 并清理排队任务;SDK 只能停止与旧租约绑定的命令流,无法撤销已经被服务接受的轨迹。
release_control()#
该方法停止续期,并在本地持有租约时尝试发送 Release。没有本地租约时直接返回,可以重复调用。
无论 RPC 成功还是失败,本地 lease_id 都会被清除,相关 Servo 流也会停止。
返回值不表示服务端确认成功。网络中断时,服务端 owner 可能继续存在到租约过期;其他客户端在此期间 仍可能获取失败。Release 成功或租约过期都会清空被抢占候选,不会自动把控制权还给上一个客户端。
handover_to_previous()#
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 的简要定义。