第 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 专属模块,它实际上是机器人可靠控制架构中的一个角色。

下一篇我们先插入一篇补充篇:

补充篇

《机器人为什么都需要"指令中心"?------从服务机器人到割草机器人》

专门把这两个看起来完全不同的机器人架构统一起来。

相关推荐
别动我齐刘海4 小时前
机器学习基础2——C++、OpenCV、点云、Open3D
c++·人工智能·opencv·机器学习·计算机视觉·机器人·ros2
消失的旧时光-19436 小时前
第 4 篇:TCP 字节流中的粘包、半包与机器人协议解析
机器人·tcp·通信协议·半包·粘包
某林2126 小时前
从“仿真能跑”到“真机能走”:一套轮腿机械狗强化学习系统的工程化实践
人工智能·stm32·嵌入式硬件·架构·机器人·机械狗
消失的旧时光-19438 小时前
补充篇:机器人为什么都需要“指令中心”?——从服务机器人到割草机器人
机器人·割草机·commandcenter
热心市民R先生8 小时前
如何在CSDN博客编辑器中导入自己的Markdown文件
机器人
M-Robots echo8 小时前
王成录:M-Robots 积木式架构,解决机器人软硬件重复开发难题
机器人·开源鸿蒙·开源社区·m-robots·王成录
热心市民R先生9 小时前
【无标题】
机器人
具身智能进化论1 天前
协作机器人产业进入规模化部署期,未来几年的增长从何而来
大数据·运维·人工智能·机器人·自动化·工厂方法模式
极新1 天前
中国机器人、AI、创新药出海:这次出海的重头戏
人工智能·机器人