4. 配方上传流程设计与实现

配方上传(Recipe Upload)在 SEMI 标准语境下,指的是设备把自身存储的配方内容通过通信链路回传给主机的过程。它与配方下载方向相反,但工程复杂度往往更高:下载时主机掌握全部数据,设备只需按序接收;上传时数据源在设备侧,主机必须先把“要什么、要多少、怎么切”讲清楚,设备才能按约定把配方内容分片送出。本节围绕这条链路,从请求、响应、分片、重组、确认到异常重试逐层展开。

一、先明确上传链路的角色与消息分工

在典型的 GEM 实现中,上传流程涉及三类消息:主机发出的上传请求(通常承载在 S2F49 或厂商自定义的 S2F 上,携带配方名、配方类型、目标版本等参数)、设备返回的上传响应(S2F50 或对应回复,告知是否接受请求、数据总长度、分片大小等),以及后续的数据分片传输(S6F11/S6F12 或 S2F51/S2F52 等,按片携带序号与内容)。设备端在收到请求后,需要先做一次“能不能给”的判断:配方是否存在、是否处于可读状态、是否被 PPID 锁定、当前是否正在被工艺使用。只有判断通过,才进入分片发送阶段。

这里有一个容易被忽略的细节:上传请求里的“配方标识”必须与设备本地存储的键一致。如果主机用 PPID 请求,而设备内部用 RecipeID 索引,就需要在设备侧做一次映射,否则会出现“主机认为存在、设备查不到”的假失败。建议在实现时把映射关系固化在配置表中,并在日志中同时打印两个标识,便于排障。

二、分片与重组:把大配方切成可管理的块

配方文件可能只有几 KB,也可能达到几百 KB 甚至更大。SECS 消息本身有长度限制,因此必须分片。分片策略的核心参数有三个:分片大小(Chunk Size)、总片数(Total Chunks)、片序号(Chunk Index)。设备端在发送前先计算总长度,按固定大小切分,最后一片可能不足整片,需要用实际长度标记。主机侧则按序号缓存,全部收齐后按序拼接,再校验整体长度与校验和。

下面给出一个设备端分片发送的伪代码示例,参数为示例假设值,实际项目中需根据设备能力与主机约定调整:

// 示例:分片大小 4096 字节,配方总长 10000 字节
chunkSize = 4096
totalLen  = 10000
totalChunks = ceil(totalLen / chunkSize)  // = 3

for i in 0 .. totalChunks-1:
    offset = i * chunkSize
    length = min(chunkSize, totalLen - offset)
    payload = recipeData[offset : offset+length]
    send S2F51 {
        RecipeName : "RCP_A",
        ChunkIndex : i,
        TotalChunks: totalChunks,
        ChunkLength: length,
        Data       : payload
    }
    wait for S2F52 ACK

主机侧重组时,建议维护一个接收位图,每收到一片就标记一片,全部标记完成后再拼接。如果某片超时未到,只重传该片,而不是整包重来。这样可以把重传代价从 O(总长) 降到 O(片长)。

三、上传确认:不只是“收到了”

上传确认分两层:片级确认和整体确认。片级确认由主机在收到每一片后立即回复,告诉设备“这一片我收下了,可以发下一片”。整体确认则在所有片收齐、拼接、校验通过后,由主机发出一个“上传完成”的确认,设备收到后才认为本次上传事务结束。如果主机校验失败,应返回明确的失败原因(长度不符、校验和不符、片序号缺失等),设备据此决定是否重传。

建议在整体确认中携带一个内容摘要(如 CRC32 或 MD5),设备端可以本地计算同一摘要进行比对。这样即使传输过程中没有报错,也能发现静默的数据损坏。

四、异常处理与重试策略

上传过程中常见的异常有四类:超时(某片发出后未收到 ACK)、片丢失(主机报告序号缺失)、校验失败(拼接后摘要不符)、事务中断(设备侧配方被删除或状态变化)。针对不同异常,重试策略也应不同:

  • 片级超时:重传当前片,最多重试 N 次(示例 N=3),每次重试间隔可递增(如 1s、2s、4s),避免链路拥塞时雪崩。
  • 片丢失:主机明确告知缺失序号,设备只补发缺失片,不重发已确认片。
  • 校验失败:整包重传,因为无法定位具体损坏位置;重传前建议先重新读取配方内容,防止源数据已变化。
  • 事务中断:立即终止上传,向主机发送失败响应,并记录中断原因;不要继续发送剩余片,否则主机侧会收到无意义的碎片。

重试次数和间隔应作为可配置参数,而不是硬编码。示例配置如下:

upload.retry.maxPerChunk = 3
upload.retry.backoffMs  = [1000, 2000, 4000]
upload.retry.maxTotal    = 10
upload.timeout.chunkAckMs = 5000
upload.timeout.overallMs  = 120000

五、把链路串起来看

下图把设备端请求、主机响应、分片发送、主机重组、确认与异常重试放在同一条链路上,便于对照理解各环节的输入输出与判断分支。

设备端(Equipment) 主机(Host) SECS 消息链路 ① 接收上传请求 S2F49 / 自定义 ② 校验配方可读性 存在 / 锁定 / 使用中 ③ 计算分片参数 大小 / 总片数 / 序号 ④ 逐片发送 S2F51 + 片数据 ⑤ 等待片级 ACK 超时则重传 ⑥ 接收整体确认 成功 / 失败原因 ① 发起上传请求 配方名 / 类型 / 版本 ② 返回上传响应 接受 / 拒绝 + 参数 ③ 接收分片 按序号缓存 ④ 回复片级 ACK S2F52 ⑤ 重组与校验 拼接 / 长度 / 摘要 ⑥ 发送整体确认 成功 / 失败原因 上传请求 → ← 上传响应 分片数据 → ← 片级 ACK 整体确认 → 异常分支 超时 / 丢片 / 校验失败 → 按策略重传或终止 注:图中消息编号为常见实现示例,实际项目以设备与主机约定为准

从图中可以看到,上传不是一条单向的“设备发、主机收”,而是请求—响应—分片—确认—重试的闭环。设备端在④⑤之间循环,直到所有片都被确认;主机端在③⑤之间循环,直到重组校验通过。异常分支从⑤回到④,形成重试回环。理解这个闭环,就能在实现时把状态机设计清楚:设备侧至少需要“空闲、发送中、等待ACK、重试中、完成、失败”六个状态,主机侧需要“空闲、接收中、重组中、校验中、完成、失败”六个状态。状态迁移的触发条件就是图中各条箭头的实际消息到达或超时。

实现时务必注意:重试不能改变片序号。重传的片必须携带与首次发送相同的 ChunkIndex,否则主机侧会把它当成新片,导致重组错位。同时,重试期间如果设备侧配方内容发生变化,应终止本次上传并重新发起,而不是继续发送旧数据。

最后给出一个可直接参考的上传事务检查清单,用于实现自测或联调前核对:

检查项 判断依据 示例值
配方标识映射 主机请求标识与设备本地键一致 PPID → RecipeID 映射表
分片大小 不超过 SECS 消息长度限制 4096 字节
片级超时 发出后未收到 ACK 的等待上限 5000 ms
单片段重试次数 超过则终止事务 3 次
整体校验 长度 + 摘要双重校验 CRC32 比对
事务总超时 从请求到整体确认的上限 120000 ms

把这张清单落到代码里,就是一组可配置参数加一个状态机。上传流程的难点不在单条消息的收发,而在于分片边界、确认时机、重试范围这三件事的配合。只要把片序号、总片数、片长度、摘要这四个字段在设备与主机两侧对齐,上传链路就能稳定运行。