第 5 篇:Android 与机器人主控之间,指令、回执、超时和重试怎么设计?
上一篇,我们把 TCP 字节流里的粘包、半包和协议解析问题讲清楚了。
现在 Android 与 Linux 机器人主控之间已经可以完成:
连接
↓
发送字节
↓
拆包
↓
解析 Command
↓
得到一条完整机器人消息
但是到了真正的机器人控制阶段,还有一个更重要的问题:
Android 把一条指令成功发送出去,就代表机器人执行成功了吗?
显然不是。
例如用户在机器人机身屏上点击:
打开 3 号柜门。
Android 调用:
socket.write(...)
没有报错。
这最多只能说明:
Android 已经把数据交给了 TCP。
它不能证明:
Linux 主控已经收到。
更不能证明:
MCU 已经执行。
也不能证明:
柜门真的打开了。
因此,一个真正可用的机器人本地控制协议,不能只有:
Command
还需要:
Command
Ack
Sequence
Timeout
Retry
Result
幂等
这一篇,我们就把一条机器人指令从“用户点击”到“机器人真正执行完成”的整个生命周期串起来。
需要先明确本文的范围:
这里讨论的是机器人机身 Android 控制屏与 Linux 主控通过 TCP 直接通信时的指令模型。
也就是:
机身 Android
↓ TCP
Linux 机器人主控
至于割草机器人那种:
手机 App
↓ HTTPS
云端
↓ MQTT
机器人
虽然同样存在指令、超时、重试和执行结果,但这些能力部署的位置并不相同。
这个区别我们会在下一篇补充篇里单独展开。
一、机器人控制里其实有三种“成功”
假设用户点击:
打开柜门
完整链路可能是:
机身 Android
↓ TCP
Linux 主控
↓ CAN / 串口
MCU
↓
电子锁
↓
柜门
在这个过程中,至少存在三种完全不同的成功。
第一种:发送成功
Android 调用:
write(command)
没有异常。
这只能说明:
Android
↓
TCP Send Buffer
数据已经交给本地 TCP。
它不能证明 Linux 已经处理。
第二种:主控收到成功
Linux 主控解析到了:
OPEN_DOOR
然后返回:
ACK
这说明:
主控已经收到并接受了这条指令。
但这时候柜门仍然可能没有真正打开。
第三种:业务执行成功
Linux 主控继续向下:
OPEN_DOOR
↓
DoorService
↓
MCU
↓
锁控模块
↓
门磁状态
确认柜门真正打开以后,再返回:
OPEN_DOOR_SUCCESS
这时候才叫:
机器人执行成功。
因此机器人控制首先要建立一个重要认知:
发送成功
≠
收到成功
≠
执行成功
二、Command:告诉机器人“我要你做什么”
Command 就是业务指令。
例如:
START_TASK
STOP_TASK
PAUSE_TASK
RESUME_TASK
GO_TO_POINT
OPEN_DOOR
CLOSE_DOOR
RETURN_TO_CHARGE
比如用户点击:
打开 3 号柜门
Android 不应该发送:
GPIO_15 = HIGH
也不应该知道具体 MCU 字节协议。
Android 只表达:
Command = OPEN_DOOR
doorId = 3
因为 Android 关注的是:
机器人需要做什么。
至于底层如何控制电子锁,是 Linux 主控和 MCU 的事情。
三、Ack:告诉 Android“这条指令我收到了”
Ack 是:
Acknowledgement。
可以简单理解成:
确认收到。
例如:
Android
│
│ OPEN_DOOR
▼
Linux 主控
│
│ ACK
▼
Android
这个 ACK 的意义是:
你的 OPEN_DOOR 指令,我已经收到了。
相比发送完以后完全不知道对端是否收到,ACK 至少建立了一层可靠性。
但这里一定要注意:
ACK
≠
SUCCESS
四、为什么 Ack 不能直接当 Result?
假设:
Android
↓
OPEN_DOOR
Linux
↓
ACK
如果 Android 收到 ACK 以后立即显示:
柜门已打开
这其实是不严谨的。
因为接下来仍可能发生:
MCU 无响应
电子锁异常
柜门机械卡住
门磁状态异常
硬件执行超时
所以更加完整的链路应该是:
Android
↓
Command
Linux
↓
Ack
Linux
↓
真正执行
Linux
↓
Result
可以简单记成:
Command
=
请你做这件事
Ack
=
我收到你的请求了
Result
=
这件事最后做成了吗
五、Sequence:这条 Ack 到底属于谁?
如果 Android 只发一条 Command,问题还不明显。
但是实际机器人运行时,可能连续发送:
OPEN_DOOR
GET_STATUS
START_TASK
Linux 也连续返回:
ACK
ACK
ACK
那么 Android 怎么知道:
每一个 ACK 对应哪条指令?
这时候就需要:
Sequence
例如:
seq = 1001
OPEN_DOOR
seq = 1002
GET_STATUS
seq = 1003
START_TASK
Linux 返回:
ACK
seq = 1002
Android 就可以明确知道:
这是:
GET_STATUS
的响应。
所以 Sequence 的核心作用是:
建立请求和响应之间的对应关系。
六、Sequence 很像 requestId
如果做过 HTTP、RPC 或后端接口,可以把 Sequence 理解成:
requestId
例如:
Request
requestId = abc123
对应:
Response
requestId = abc123
机器人 TCP 也是同样的思想。
只不过 TCP 本身不会帮我们管理请求关系。
因此应用协议需要自己增加:
Sequence
Android 端就可以维护:
PendingCommands
1001 → OPEN_DOOR
1002 → GET_STATUS
1003 → START_TASK
收到:
ACK 1002
就找到:
1002 → GET_STATUS
从这里开始,机器人 TCP 已经不仅仅是:
Socket.read()
Socket.write()
而是在逐渐形成一套:
可靠指令系统。
七、Timestamp:这条指令还有效吗?
除了 Sequence,一些协议还会增加:
Timestamp
例如:
sequence = 1001
timestamp = 1723285231000
command = OPEN_DOOR
Timestamp 可以辅助解决:
消息时效
日志排查
延迟分析
旧消息判断
例如由于网络异常:
一条:
STOP_TASK
延迟了很久才到达主控。
这时候 Linux 可以根据:
currentTime - timestamp
判断:
这条指令是不是已经过期。
所以:
Sequence
主要回答:
这是哪一条指令?
而:
Timestamp
可以辅助回答:
这条指令现在还应不应该执行?
八、一条完整 Command 可以长什么样?
结合上一篇协议设计,一条本地机器人控制消息可以包含:
Header
Length
Command
Sequence
Timestamp
Data
Checksum
例如:
Command = OPEN_DOOR
Sequence = 10025
Timestamp = ...
DoorId = 3
Android 发送以后,可以在本地记录:
10025
OPEN_DOOR
WAIT_ACK
然后开始等待主控反馈。
这里其实已经出现了一个很重要的东西:
Pending Command
也就是:
已经发出、但生命周期还没有结束的指令。
九、如果一直没有 Ack 怎么办?
例如:
OPEN_DOOR
sequence = 10025
发送以后开始计时。
如果协议约定:
3 秒
内应该收到 ACK。
但是 3 秒后仍然没有任何响应。
那么:
10025
就可以进入:
TIMEOUT
状态。
不过这里有一个很关键的认知:
TIMEOUT 并不代表机器人一定没有执行。
可能发生:
Android
↓
OPEN_DOOR
Linux
↓
已经收到
Linux
↓
甚至已经执行
Linux
↓
返回 ACK / Result
网络异常
↓
Android 没收到
Android 看到的是:
TIMEOUT
但是机器人可能已经:
执行成功
这时候问题就来了:
能不能直接再发一次?
十、为什么 Timeout 后不能无脑 Retry?
假设:
OPEN_DOOR
第一次实际上已经执行成功。
只是 Result 在回程途中丢了。
Android 超时以后再发一次:
OPEN_DOOR
可能问题并不特别严重。
但如果换成:
发放一个物品
机械臂执行一次动作
释放一次物料
增加一次数量
重复执行就可能出问题。
所以:
Timeout
→
Retry
不能单独设计。
必须和:
幂等
一起考虑。
十一、什么叫幂等?
可以简单理解成:
同一个业务操作即使重复发送,也不能导致业务重复执行。
例如:
SET_LIGHT_ON
发送三次:
SET_LIGHT_ON
SET_LIGHT_ON
SET_LIGHT_ON
最终仍然只是:
灯保持打开
这类操作天然比较容易幂等。
但是:
MOVE_FORWARD_1_METER
执行三次就可能前进:
3 米
所以不能单纯依赖 Command 类型。
更可靠的方法是让主控识别:
这是不是同一条 Command 的重发。
十二、Sequence 也可以用于幂等
假设第一次发送:
seq = 10025
OPEN_DOOR
Linux 收到以后:
执行成功
并保存:
10025 → SUCCESS
结果返回 Android 时网络异常。
Android:
TIMEOUT
准备重试。
这里最重要的一点是:
重试时不要创建新的 Sequence。
仍然发送:
seq = 10025
OPEN_DOOR
Linux 再次收到以后发现:
10025
以前已经执行过。
于是:
不再重复执行
而是直接返回:
10025
SUCCESS
这样就实现了:
消息允许重复
但业务不能重复执行
这就是一种典型的幂等设计。
十三、为什么重试不能生成新的 Sequence?
第一次:
seq = 1001
OPEN_DOOR
超时以后,如果重新生成:
seq = 1002
OPEN_DOOR
对于 Linux 来说:
1001
和:
1002
就是两条新指令。
它无法知道:
1002 其实只是 1001 的网络重试。
所以:
同一次业务操作发生网络重试时,应该保留同一个唯一标识。
这个字段叫什么并不重要:
Sequence
CommandId
RequestId
核心思想都是:
能够识别同一次业务操作。
十四、Ack Timeout 和执行 Timeout 要分开
不同机器人操作执行时间差异很大。
例如:
GET_STATUS
可能几十毫秒。
OPEN_DOOR
可能几秒。
而:
GO_TO_POINT
可能几十秒甚至几分钟。
所以不能规定:
5 秒没完成
=
失败
更加合理的设计是区分:
Ack Timeout
和:
Execution Timeout
例如:
GO_TO_POINT
发送以后:
3 秒内应该收到:
ACK
说明:
主控接受任务
但真正执行过程可能是:
ACK
↓
NAVIGATION_START
↓
NAVIGATING
↓
NAVIGATING
↓
ARRIVED
整个任务可能持续 2 分钟。
因此:
ACK
只是任务生命周期的开始。
十五、机器人 Command 往往会启动一个状态机
普通接口经常是:
Request
↓
Response
然后结束。
但机器人很多指令实际上是:
Command
↓
Ack
↓
Running
↓
Progress
↓
Result
例如:
START_PATROL
可能产生:
ACK
↓
TASK_STARTED
↓
RUNNING
↓
ARRIVED_POINT_1
↓
RUNNING
↓
ARRIVED_POINT_2
↓
FINISHED
所以机器人 Command 很多时候并不是:
“一问一答”。
而是:
触发一个持续运行的机器人状态机。
十六、重复 Ack 和重复 Result 怎么处理?
既然存在:
Retry
就一定要接受一个现实:
响应也可能重复出现。
例如:
seq = 1001
Android 因超时进行了重试。
Linux 两次都返回:
ACK 1001
Android 可能收到:
ACK 1001
ACK 1001
这时候第二个 ACK 不应该再次触发业务操作。
Android 应该根据:
Sequence
找到:
PendingCommand
如果:
1001
已经进入:
ACKED
或者:
SUCCESS
那么后面的重复 ACK 可以:
忽略
或者:
记录日志
而不是当成一个新的业务事件。
十七、Android 端其实已经出现了一个 PendingCommandManager
做到这里,我们会发现 Android 里需要有一个模块:
PendingCommandManager
它维护:
1001
OPEN_DOOR
WAIT_ACK
1002
START_TASK
ACKED
1003
GET_STATUS
WAIT_ACK
收到:
ACK 1001
更新:
1001
OPEN_DOOR
ACKED
收到:
RESULT 1001 SUCCESS
更新:
1001
SUCCESS
然后结束这条 Command 生命周期。
如果超时:
1003
TIMEOUT
再根据 Command 策略:
Retry
或者:
Failed
十八、一条 Command 自己也有生命周期
从这里开始,一条 Command 本身已经可以看作一个小状态机:
CREATED
↓
SENT
↓
WAIT_ACK
↓
ACKED
↓
EXECUTING
↓
SUCCESS
异常情况下:
WAIT_ACK
↓
TIMEOUT
↓
RETRYING
↓
WAIT_ACK
或者:
EXECUTING
↓
EXECUTE_TIMEOUT
↓
FAILED
因此:
Command
并不只是一个:
ByteArray
它实际上是一条:
有完整生命周期的机器人操作。
十九、不是所有 Command 都应该自动重试
例如:
GET_STATUS
GET_BATTERY
GET_POSITION
查询类指令通常比较适合自动重试。
而:
SET_LIGHT_ON
SET_VOLUME
如果本身设计成幂等,也比较容易重试。
但是:
START_TASK
GO_TO_POINT
OPEN_DOOR
机械动作
应该至少有:
Sequence / CommandId
保证幂等以后,再考虑重试。
某些更加危险或者不可逆的动作甚至应该:
TIMEOUT
↓
先查询真实状态
↓
再决定是否重试
而不是:
TIMEOUT
↓
立即重发
所以:
Retry 不应该只是 TCP 层统一无脑处理。
它应该结合具体 Command 的业务语义。
二十、到这里,其实已经出现了“指令中心”
现在再看 Android 端维护的这些东西:
Command 创建
Sequence
PendingCommands
Timeout
Retry
Command State
Result
它们其实已经不只是:
SocketManager
的职责了。
可以进一步把它抽象成一个:
Command Center
例如:
UI / ViewModel
↓
Command Center
↓
TCP Manager
↓
Linux 主控
Command Center 负责:
创建指令
生成 CommandId / Sequence
维护 PendingCommand
管理 Timeout
决定 Retry
跟踪 Result
结束 Command 生命周期
而 TCP Manager 只负责:
连接
发送
接收
这样:
指令可靠性
和:
网络连接
就被进一步拆开了。
二十一、Linux 主控这一侧也需要 Command Handler
Android 有 Command Center。
Linux 主控也不能只是:
收到 ByteArray
↓
直接执行
它通常还需要:
Command Handler
例如:
TCP
↓
Protocol Parser
↓
Command Handler
↓
检查 Sequence
↓
判断是否重复
↓
Ack
↓
Command Dispatcher
↓
业务执行
↓
Result
所以可以抽象成:
Android
【Command Center】
↓ TCP
Linux
【Command Handler】
↓
机器人能力
其中:
Command Center
偏向:
指令生命周期管理。
Command Handler
偏向:
指令接收、校验、去重和执行。
二十二、但“指令中心”并不一定永远在 Android
这里就出现了一个很有意思的问题。
本文中的服务机器人:
机身 Android
↓ TCP
Linux 主控
Android 与机器人直接连接。
因此:
Command Center
放在 Android 很自然。
但是如果换成割草机器人:
手机 App
↓ HTTPS
Cloud
↓ MQTT
Robot
情况就变了。
手机 App 并没有直接连接机器人。
这时候:
PendingCommand
Timeout
Retry
Command Result
如果仍然全部放在手机 App:
架构就会很奇怪。
更合理的位置通常会变成:
Cloud
也就是说:
机器人都需要可靠的指令生命周期,但“指令中心”不一定部署在同一个地方。
这个问题非常值得单独展开。
二十三、总结
这一篇讨论的是:
机身 Android 与 Linux 主控通过 TCP 直连时,一条机器人 Command 应该如何可靠执行。
最重要的认知是:
发送成功
≠
收到成功
≠
执行成功
因此一条完整 Command 通常需要:
Command
↓
Sequence / CommandId
↓
发送
↓
Ack
↓
Executing
↓
Result
同时还需要解决:
Timeout
Retry
重复消息
幂等
过期指令
Command 状态机
随着这些能力逐渐集中,我们会发现 Android 端实际上已经出现了:
Command Center
Linux 主控则存在:
Command Handler
于是本地控制链路就变成:
机身 Android
Command Center
↓ TCP
Linux 主控
Command Handler
↓
机器人能力
但如果换成割草机器人:
App
↓
Cloud
↓
Robot
Command Center 的位置又会发生变化。
这也说明:
Command Center 不是 Android 专属模块,它实际上是机器人可靠控制架构中的一个角色。
下一篇我们先插入一篇补充篇:
补充篇
《机器人为什么都需要“指令中心”?——从服务机器人到割草机器人》
专门把这两个看起来完全不同的机器人架构统一起来。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)